Coverage for HARK / interpolation.py: 97%

1553 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-25 05:22 +0000

1""" 

2Custom interpolation methods for representing approximations to functions. 

3It also includes wrapper classes to enforce standard methods across classes. 

4Each interpolation class must have a distance() method that compares itself to 

5another instance; this is used in HARK.core's solve() method to check for solution 

6convergence. The interpolator classes currently in this module inherit their 

7distance method from MetricObject. 

8""" 

9 

10import warnings 

11from copy import deepcopy 

12 

13import numpy as np 

14from scipy.interpolate import CubicHermiteSpline 

15from HARK.metric import MetricObject 

16from HARK.rewards import CRRAutility, CRRAutilityP, CRRAutilityPP 

17from numba import njit 

18 

19 

20def _isscalar(x): 

21 """ 

22 Check whether x is if a scalar type, or 0-dim. 

23 

24 Parameters 

25 ---------- 

26 x : anything 

27 An input to be checked for scalar-ness. 

28 

29 Returns 

30 ------- 

31 is_scalar : boolean 

32 True if the input is a scalar, False otherwise. 

33 """ 

34 return np.isscalar(x) or hasattr(x, "shape") and x.shape == () 

35 

36 

37def _check_grid_dimensions(dimension, *args): 

38 if dimension == 1: 

39 if len(args[0]) != len(args[1]): 

40 raise ValueError("Grid dimensions of x and f(x) do not match") 

41 elif dimension == 2: 

42 if args[0].shape != (args[1].size, args[2].size): 

43 raise ValueError("Grid dimensions of x, y and f(x, y) do not match") 

44 elif dimension == 3: 

45 if args[0].shape != (args[1].size, args[2].size, args[3].size): 

46 raise ValueError("Grid dimensions of x, y, z and f(x, y, z) do not match") 

47 elif dimension == 4: 

48 if args[0].shape != (args[1].size, args[2].size, args[3].size, args[4].size): 

49 raise ValueError("Grid dimensions of x, y, z and f(x, y, z) do not match") 

50 else: 

51 raise ValueError("Dimension should be between 1 and 4 inclusive.") 

52 

53 

54def _check_flatten(dimension, *args): 

55 if dimension == 1: 

56 if isinstance(args[0], np.ndarray) and args[0].shape != args[0].flatten().shape: 

57 warnings.warn("input not of the size (n, ), attempting to flatten") 

58 return False 

59 else: 

60 return True 

61 

62 

63class HARKinterpolator1D(MetricObject): 

64 """ 

65 A wrapper class for 1D interpolation methods in HARK. 

66 """ 

67 

68 distance_criteria = [] 

69 

70 def __call__(self, x): 

71 """ 

72 Evaluates the interpolated function at the given input. 

73 

74 Parameters 

75 ---------- 

76 x : np.array or float 

77 Real values to be evaluated in the interpolated function. 

78 

79 Returns 

80 ------- 

81 y : np.array or float 

82 The interpolated function evaluated at x: y = f(x), with the same 

83 shape as x. 

84 """ 

85 z = np.asarray(x) 

86 return (self._evaluate(z.flatten())).reshape(z.shape) 

87 

88 def derivative(self, x): 

89 """ 

90 Evaluates the derivative of the interpolated function at the given input. 

91 

92 Parameters 

93 ---------- 

94 x : np.array or float 

95 Real values to be evaluated in the interpolated function. 

96 

97 Returns 

98 ------- 

99 dydx : np.array or float 

100 The interpolated function's first derivative evaluated at x: 

101 dydx = f'(x), with the same shape as x. 

102 """ 

103 z = np.asarray(x) 

104 return (self._der(z.flatten())).reshape(z.shape) 

105 

106 derivativeX = derivative # alias 

107 

108 def eval_with_derivative(self, x): 

109 """ 

110 Evaluates the interpolated function and its derivative at the given input. 

111 

112 Parameters 

113 ---------- 

114 x : np.array or float 

115 Real values to be evaluated in the interpolated function. 

116 

117 Returns 

118 ------- 

119 y : np.array or float 

120 The interpolated function evaluated at x: y = f(x), with the same 

121 shape as x. 

122 dydx : np.array or float 

123 The interpolated function's first derivative evaluated at x: 

124 dydx = f'(x), with the same shape as x. 

125 """ 

126 z = np.asarray(x) 

127 y, dydx = self._evalAndDer(z.flatten()) 

128 return y.reshape(z.shape), dydx.reshape(z.shape) 

129 

130 def _evaluate(self, x): 

131 """ 

132 Interpolated function evaluator, to be defined in subclasses. 

133 """ 

134 raise NotImplementedError() 

135 

136 def _der(self, x): 

137 """ 

138 Interpolated function derivative evaluator, to be defined in subclasses. 

139 """ 

140 raise NotImplementedError() 

141 

142 def _evalAndDer(self, x): 

143 """ 

144 Interpolated function and derivative evaluator, to be defined in subclasses. 

145 """ 

146 raise NotImplementedError() 

147 

148 

149class HARKinterpolator2D(MetricObject): 

150 """ 

151 A wrapper class for 2D interpolation methods in HARK. 

152 """ 

153 

154 distance_criteria = [] 

155 

156 def __call__(self, x, y): 

157 """ 

158 Evaluates the interpolated function at the given input. 

159 

160 Parameters 

161 ---------- 

162 x : np.array or float 

163 Real values to be evaluated in the interpolated function. 

164 y : np.array or float 

165 Real values to be evaluated in the interpolated function. If both 

166 are arrays, they must be broadcastable to the same shape. 

167 Scalar inputs will be broadcast to match array inputs. 

168 

169 Returns 

170 ------- 

171 fxy : np.array or float 

172 The interpolated function evaluated at x,y: fxy = f(x,y), with the 

173 same shape as x and y. 

174 """ 

175 xa = np.asarray(x) 

176 ya = np.asarray(y) 

177 # Broadcast to common shape to handle mixed scalar/array inputs 

178 xa, ya = np.broadcast_arrays(xa, ya) 

179 return (self._evaluate(xa.flatten(), ya.flatten())).reshape(xa.shape) 

180 

181 def derivativeX(self, x, y): 

182 """ 

183 Evaluates the partial derivative of interpolated function with respect 

184 to x (the first argument) at the given input. 

185 

186 Parameters 

187 ---------- 

188 x : np.array or float 

189 Real values to be evaluated in the interpolated function. 

190 y : np.array or float 

191 Real values to be evaluated in the interpolated function. If both 

192 are arrays, they must be broadcastable to the same shape. 

193 Scalar inputs will be broadcast to match array inputs. 

194 

195 Returns 

196 ------- 

197 dfdx : np.array or float 

198 The derivative of the interpolated function with respect to x, eval- 

199 uated at x,y: dfdx = f_x(x,y), with the same shape as x and y. 

200 """ 

201 xa = np.asarray(x) 

202 ya = np.asarray(y) 

203 # Broadcast to common shape to handle mixed scalar/array inputs 

204 xa, ya = np.broadcast_arrays(xa, ya) 

205 return (self._derX(xa.flatten(), ya.flatten())).reshape(xa.shape) 

206 

207 def derivativeY(self, x, y): 

208 """ 

209 Evaluates the partial derivative of interpolated function with respect 

210 to y (the second argument) at the given input. 

211 

212 Parameters 

213 ---------- 

214 x : np.array or float 

215 Real values to be evaluated in the interpolated function. 

216 y : np.array or float 

217 Real values to be evaluated in the interpolated function. If both 

218 are arrays, they must be broadcastable to the same shape. 

219 Scalar inputs will be broadcast to match array inputs. 

220 

221 Returns 

222 ------- 

223 dfdy : np.array or float 

224 The derivative of the interpolated function with respect to y, eval- 

225 uated at x,y: dfdx = f_y(x,y), with the same shape as x and y. 

226 """ 

227 xa = np.asarray(x) 

228 ya = np.asarray(y) 

229 # Broadcast to common shape to handle mixed scalar/array inputs 

230 xa, ya = np.broadcast_arrays(xa, ya) 

231 return (self._derY(xa.flatten(), ya.flatten())).reshape(xa.shape) 

232 

233 def _evaluate(self, x, y): 

234 """ 

235 Interpolated function evaluator, to be defined in subclasses. 

236 """ 

237 raise NotImplementedError() 

238 

239 def _derX(self, x, y): 

240 """ 

241 Interpolated function x-derivative evaluator, to be defined in subclasses. 

242 """ 

243 raise NotImplementedError() 

244 

245 def _derY(self, x, y): 

246 """ 

247 Interpolated function y-derivative evaluator, to be defined in subclasses. 

248 """ 

249 raise NotImplementedError() 

250 

251 

252class HARKinterpolator3D(MetricObject): 

253 """ 

254 A wrapper class for 3D interpolation methods in HARK. 

255 """ 

256 

257 distance_criteria = [] 

258 

259 def __call__(self, x, y, z): 

260 """ 

261 Evaluates the interpolated function at the given input. 

262 

263 Parameters 

264 ---------- 

265 x : np.array or float 

266 Real values to be evaluated in the interpolated function. 

267 y : np.array or float 

268 Real values to be evaluated in the interpolated function. If multiple 

269 inputs are arrays, they must be broadcastable to the same shape. 

270 Scalar inputs will be broadcast to match array inputs. 

271 z : np.array or float 

272 Real values to be evaluated in the interpolated function. If multiple 

273 inputs are arrays, they must be broadcastable to the same shape. 

274 Scalar inputs will be broadcast to match array inputs. 

275 

276 Returns 

277 ------- 

278 fxyz : np.array or float 

279 The interpolated function evaluated at x,y,z: fxyz = f(x,y,z), with 

280 the same shape as x, y, and z. 

281 """ 

282 xa = np.asarray(x) 

283 ya = np.asarray(y) 

284 za = np.asarray(z) 

285 # Broadcast to common shape to handle mixed scalar/array inputs 

286 xa, ya, za = np.broadcast_arrays(xa, ya, za) 

287 return (self._evaluate(xa.flatten(), ya.flatten(), za.flatten())).reshape( 

288 xa.shape 

289 ) 

290 

291 def derivativeX(self, x, y, z): 

292 """ 

293 Evaluates the partial derivative of the interpolated function with respect 

294 to x (the first argument) at the given input. 

295 

296 Parameters 

297 ---------- 

298 x : np.array or float 

299 Real values to be evaluated in the interpolated function. 

300 y : np.array or float 

301 Real values to be evaluated in the interpolated function. If multiple 

302 inputs are arrays, they must be broadcastable to the same shape. 

303 Scalar inputs will be broadcast to match array inputs. 

304 z : np.array or float 

305 Real values to be evaluated in the interpolated function. If multiple 

306 inputs are arrays, they must be broadcastable to the same shape. 

307 Scalar inputs will be broadcast to match array inputs. 

308 

309 Returns 

310 ------- 

311 dfdx : np.array or float 

312 The derivative with respect to x of the interpolated function evaluated 

313 at x,y,z: dfdx = f_x(x,y,z), with the same shape as x, y, and z. 

314 """ 

315 xa = np.asarray(x) 

316 ya = np.asarray(y) 

317 za = np.asarray(z) 

318 # Broadcast to common shape to handle mixed scalar/array inputs 

319 xa, ya, za = np.broadcast_arrays(xa, ya, za) 

320 return (self._derX(xa.flatten(), ya.flatten(), za.flatten())).reshape(xa.shape) 

321 

322 def derivativeY(self, x, y, z): 

323 """ 

324 Evaluates the partial derivative of the interpolated function with respect 

325 to y (the second argument) at the given input. 

326 

327 Parameters 

328 ---------- 

329 x : np.array or float 

330 Real values to be evaluated in the interpolated function. 

331 y : np.array or float 

332 Real values to be evaluated in the interpolated function. If multiple 

333 inputs are arrays, they must be broadcastable to the same shape. 

334 Scalar inputs will be broadcast to match array inputs. 

335 z : np.array or float 

336 Real values to be evaluated in the interpolated function. If multiple 

337 inputs are arrays, they must be broadcastable to the same shape. 

338 Scalar inputs will be broadcast to match array inputs. 

339 

340 Returns 

341 ------- 

342 dfdy : np.array or float 

343 The derivative with respect to y of the interpolated function evaluated 

344 at x,y,z: dfdy = f_y(x,y,z), with the same shape as x, y, and z. 

345 """ 

346 xa = np.asarray(x) 

347 ya = np.asarray(y) 

348 za = np.asarray(z) 

349 # Broadcast to common shape to handle mixed scalar/array inputs 

350 xa, ya, za = np.broadcast_arrays(xa, ya, za) 

351 return (self._derY(xa.flatten(), ya.flatten(), za.flatten())).reshape(xa.shape) 

352 

353 def derivativeZ(self, x, y, z): 

354 """ 

355 Evaluates the partial derivative of the interpolated function with respect 

356 to z (the third argument) at the given input. 

357 

358 Parameters 

359 ---------- 

360 x : np.array or float 

361 Real values to be evaluated in the interpolated function. 

362 y : np.array or float 

363 Real values to be evaluated in the interpolated function. If multiple 

364 inputs are arrays, they must be broadcastable to the same shape. 

365 Scalar inputs will be broadcast to match array inputs. 

366 z : np.array or float 

367 Real values to be evaluated in the interpolated function. If multiple 

368 inputs are arrays, they must be broadcastable to the same shape. 

369 Scalar inputs will be broadcast to match array inputs. 

370 

371 Returns 

372 ------- 

373 dfdz : np.array or float 

374 The derivative with respect to z of the interpolated function evaluated 

375 at x,y,z: dfdz = f_z(x,y,z), with the same shape as x, y, and z. 

376 """ 

377 xa = np.asarray(x) 

378 ya = np.asarray(y) 

379 za = np.asarray(z) 

380 # Broadcast to common shape to handle mixed scalar/array inputs 

381 xa, ya, za = np.broadcast_arrays(xa, ya, za) 

382 return (self._derZ(xa.flatten(), ya.flatten(), za.flatten())).reshape(xa.shape) 

383 

384 def _evaluate(self, x, y, z): 

385 """ 

386 Interpolated function evaluator, to be defined in subclasses. 

387 """ 

388 raise NotImplementedError() 

389 

390 def _derX(self, x, y, z): 

391 """ 

392 Interpolated function x-derivative evaluator, to be defined in subclasses. 

393 """ 

394 raise NotImplementedError() 

395 

396 def _derY(self, x, y, z): 

397 """ 

398 Interpolated function y-derivative evaluator, to be defined in subclasses. 

399 """ 

400 raise NotImplementedError() 

401 

402 def _derZ(self, x, y, z): 

403 """ 

404 Interpolated function y-derivative evaluator, to be defined in subclasses. 

405 """ 

406 raise NotImplementedError() 

407 

408 

409class HARKinterpolator4D(MetricObject): 

410 """ 

411 A wrapper class for 4D interpolation methods in HARK. 

412 """ 

413 

414 distance_criteria = [] 

415 

416 def __call__(self, w, x, y, z): 

417 """ 

418 Evaluates the interpolated function at the given input. 

419 

420 Parameters 

421 ---------- 

422 w : np.array or float 

423 Real values to be evaluated in the interpolated function. 

424 x : np.array or float 

425 Real values to be evaluated in the interpolated function. If multiple 

426 inputs are arrays, they must be broadcastable to the same shape. 

427 Scalar inputs will be broadcast to match array inputs. 

428 y : np.array or float 

429 Real values to be evaluated in the interpolated function. If multiple 

430 inputs are arrays, they must be broadcastable to the same shape. 

431 Scalar inputs will be broadcast to match array inputs. 

432 z : np.array or float 

433 Real values to be evaluated in the interpolated function. If multiple 

434 inputs are arrays, they must be broadcastable to the same shape. 

435 Scalar inputs will be broadcast to match array inputs. 

436 

437 Returns 

438 ------- 

439 fwxyz : np.array or float 

440 The interpolated function evaluated at w,x,y,z: fwxyz = f(w,x,y,z), 

441 with the same shape as w, x, y, and z. 

442 """ 

443 wa = np.asarray(w) 

444 xa = np.asarray(x) 

445 ya = np.asarray(y) 

446 za = np.asarray(z) 

447 # Broadcast to common shape to handle mixed scalar/array inputs 

448 wa, xa, ya, za = np.broadcast_arrays(wa, xa, ya, za) 

449 return ( 

450 self._evaluate(wa.flatten(), xa.flatten(), ya.flatten(), za.flatten()) 

451 ).reshape(wa.shape) 

452 

453 def derivativeW(self, w, x, y, z): 

454 """ 

455 Evaluates the partial derivative with respect to w (the first argument) 

456 of the interpolated function at the given input. 

457 

458 Parameters 

459 ---------- 

460 w : np.array or float 

461 Real values to be evaluated in the interpolated function. 

462 x : np.array or float 

463 Real values to be evaluated in the interpolated function. If multiple 

464 inputs are arrays, they must be broadcastable to the same shape. 

465 Scalar inputs will be broadcast to match array inputs. 

466 y : np.array or float 

467 Real values to be evaluated in the interpolated function. If multiple 

468 inputs are arrays, they must be broadcastable to the same shape. 

469 Scalar inputs will be broadcast to match array inputs. 

470 z : np.array or float 

471 Real values to be evaluated in the interpolated function. If multiple 

472 inputs are arrays, they must be broadcastable to the same shape. 

473 Scalar inputs will be broadcast to match array inputs. 

474 

475 Returns 

476 ------- 

477 dfdw : np.array or float 

478 The derivative with respect to w of the interpolated function eval- 

479 uated at w,x,y,z: dfdw = f_w(w,x,y,z), with the same shape as inputs. 

480 """ 

481 wa = np.asarray(w) 

482 xa = np.asarray(x) 

483 ya = np.asarray(y) 

484 za = np.asarray(z) 

485 # Broadcast to common shape to handle mixed scalar/array inputs 

486 wa, xa, ya, za = np.broadcast_arrays(wa, xa, ya, za) 

487 return ( 

488 self._derW(wa.flatten(), xa.flatten(), ya.flatten(), za.flatten()) 

489 ).reshape(wa.shape) 

490 

491 def derivativeX(self, w, x, y, z): 

492 """ 

493 Evaluates the partial derivative with respect to x (the second argument) 

494 of the interpolated function at the given input. 

495 

496 Parameters 

497 ---------- 

498 w : np.array or float 

499 Real values to be evaluated in the interpolated function. 

500 x : np.array or float 

501 Real values to be evaluated in the interpolated function. If multiple 

502 inputs are arrays, they must be broadcastable to the same shape. 

503 Scalar inputs will be broadcast to match array inputs. 

504 y : np.array or float 

505 Real values to be evaluated in the interpolated function. If multiple 

506 inputs are arrays, they must be broadcastable to the same shape. 

507 Scalar inputs will be broadcast to match array inputs. 

508 z : np.array or float 

509 Real values to be evaluated in the interpolated function. If multiple 

510 inputs are arrays, they must be broadcastable to the same shape. 

511 Scalar inputs will be broadcast to match array inputs. 

512 

513 Returns 

514 ------- 

515 dfdx : np.array or float 

516 The derivative with respect to x of the interpolated function eval- 

517 uated at w,x,y,z: dfdx = f_x(w,x,y,z), with the same shape as inputs. 

518 """ 

519 wa = np.asarray(w) 

520 xa = np.asarray(x) 

521 ya = np.asarray(y) 

522 za = np.asarray(z) 

523 # Broadcast to common shape to handle mixed scalar/array inputs 

524 wa, xa, ya, za = np.broadcast_arrays(wa, xa, ya, za) 

525 return ( 

526 self._derX(wa.flatten(), xa.flatten(), ya.flatten(), za.flatten()) 

527 ).reshape(wa.shape) 

528 

529 def derivativeY(self, w, x, y, z): 

530 """ 

531 Evaluates the partial derivative with respect to y (the third argument) 

532 of the interpolated function at the given input. 

533 

534 Parameters 

535 ---------- 

536 w : np.array or float 

537 Real values to be evaluated in the interpolated function. 

538 x : np.array or float 

539 Real values to be evaluated in the interpolated function. If multiple 

540 inputs are arrays, they must be broadcastable to the same shape. 

541 Scalar inputs will be broadcast to match array inputs. 

542 y : np.array or float 

543 Real values to be evaluated in the interpolated function. If multiple 

544 inputs are arrays, they must be broadcastable to the same shape. 

545 Scalar inputs will be broadcast to match array inputs. 

546 z : np.array or float 

547 Real values to be evaluated in the interpolated function. If multiple 

548 inputs are arrays, they must be broadcastable to the same shape. 

549 Scalar inputs will be broadcast to match array inputs. 

550 

551 Returns 

552 ------- 

553 dfdy : np.array or float 

554 The derivative with respect to y of the interpolated function eval- 

555 uated at w,x,y,z: dfdy = f_y(w,x,y,z), with the same shape as inputs. 

556 """ 

557 wa = np.asarray(w) 

558 xa = np.asarray(x) 

559 ya = np.asarray(y) 

560 za = np.asarray(z) 

561 # Broadcast to common shape to handle mixed scalar/array inputs 

562 wa, xa, ya, za = np.broadcast_arrays(wa, xa, ya, za) 

563 return ( 

564 self._derY(wa.flatten(), xa.flatten(), ya.flatten(), za.flatten()) 

565 ).reshape(wa.shape) 

566 

567 def derivativeZ(self, w, x, y, z): 

568 """ 

569 Evaluates the partial derivative with respect to z (the fourth argument) 

570 of the interpolated function at the given input. 

571 

572 Parameters 

573 ---------- 

574 w : np.array or float 

575 Real values to be evaluated in the interpolated function. 

576 x : np.array or float 

577 Real values to be evaluated in the interpolated function. If multiple 

578 inputs are arrays, they must be broadcastable to the same shape. 

579 Scalar inputs will be broadcast to match array inputs. 

580 y : np.array or float 

581 Real values to be evaluated in the interpolated function. If multiple 

582 inputs are arrays, they must be broadcastable to the same shape. 

583 Scalar inputs will be broadcast to match array inputs. 

584 z : np.array or float 

585 Real values to be evaluated in the interpolated function. If multiple 

586 inputs are arrays, they must be broadcastable to the same shape. 

587 Scalar inputs will be broadcast to match array inputs. 

588 

589 Returns 

590 ------- 

591 dfdz : np.array or float 

592 The derivative with respect to z of the interpolated function eval- 

593 uated at w,x,y,z: dfdz = f_z(w,x,y,z), with the same shape as inputs. 

594 """ 

595 wa = np.asarray(w) 

596 xa = np.asarray(x) 

597 ya = np.asarray(y) 

598 za = np.asarray(z) 

599 # Broadcast to common shape to handle mixed scalar/array inputs 

600 wa, xa, ya, za = np.broadcast_arrays(wa, xa, ya, za) 

601 return ( 

602 self._derZ(wa.flatten(), xa.flatten(), ya.flatten(), za.flatten()) 

603 ).reshape(wa.shape) 

604 

605 def _evaluate(self, w, x, y, z): 

606 """ 

607 Interpolated function evaluator, to be defined in subclasses. 

608 """ 

609 raise NotImplementedError() 

610 

611 def _derW(self, w, x, y, z): 

612 """ 

613 Interpolated function w-derivative evaluator, to be defined in subclasses. 

614 """ 

615 raise NotImplementedError() 

616 

617 def _derX(self, w, x, y, z): 

618 """ 

619 Interpolated function w-derivative evaluator, to be defined in subclasses. 

620 """ 

621 raise NotImplementedError() 

622 

623 def _derY(self, w, x, y, z): 

624 """ 

625 Interpolated function w-derivative evaluator, to be defined in subclasses. 

626 """ 

627 raise NotImplementedError() 

628 

629 def _derZ(self, w, x, y, z): 

630 """ 

631 Interpolated function w-derivative evaluator, to be defined in subclasses. 

632 """ 

633 raise NotImplementedError() 

634 

635 

636class IdentityFunction(MetricObject): 

637 """ 

638 A fairly trivial interpolator that simply returns one of its arguments. Useful for avoiding 

639 numeric error in extreme cases. 

640 

641 Parameters 

642 ---------- 

643 i_dim : int 

644 Index of the dimension on which the identity is defined. ``f(*x) = x[i]`` 

645 n_dims : int 

646 Total number of input dimensions for this function. 

647 """ 

648 

649 distance_criteria = ["i_dim"] 

650 

651 def __init__(self, i_dim=0, n_dims=1): 

652 self.i_dim = i_dim 

653 self.n_dims = n_dims 

654 

655 def __call__(self, *args): 

656 """ 

657 Evaluate the identity function. 

658 """ 

659 return args[self.i_dim] 

660 

661 def derivative(self, *args): 

662 """ 

663 Returns the derivative of the function with respect to the first dimension. 

664 """ 

665 if self.i_dim == 0: 

666 return np.ones_like(args[0]) 

667 else: 

668 return np.zeros_like(args[0]) 

669 

670 def derivativeX(self, *args): 

671 """ 

672 Returns the derivative of the function with respect to the X dimension. 

673 This is the first input whenever n_dims < 4 and the second input otherwise. 

674 """ 

675 if self.n_dims >= 4: 

676 j = 1 

677 else: 

678 j = 0 

679 if self.i_dim == j: 

680 return np.ones_like(args[0]) 

681 else: 

682 return np.zeros_like(args[0]) 

683 

684 def derivativeY(self, *args): 

685 """ 

686 Returns the derivative of the function with respect to the Y dimension. 

687 This is the second input whenever n_dims < 4 and the third input otherwise. 

688 """ 

689 if self.n_dims >= 4: 

690 j = 2 

691 else: 

692 j = 1 

693 if self.i_dim == j: 

694 return np.ones_like(args[0]) 

695 else: 

696 return np.zeros_like(args[0]) 

697 

698 def derivativeZ(self, *args): 

699 """ 

700 Returns the derivative of the function with respect to the Z dimension. 

701 This is the third input whenever n_dims < 4 and the fourth input otherwise. 

702 """ 

703 if self.n_dims >= 4: 

704 j = 3 

705 else: 

706 j = 2 

707 if self.i_dim == j: 

708 return np.ones_like(args[0]) 

709 else: 

710 return np.zeros_like(args[0]) 

711 

712 def derivativeW(self, *args): 

713 """ 

714 Returns the derivative of the function with respect to the W dimension. 

715 This should only exist when n_dims >= 4. 

716 """ 

717 if self.n_dims >= 4: 

718 j = 0 

719 else: 

720 assert False, ( 

721 "Derivative with respect to W can't be called when n_dims < 4!" 

722 ) 

723 if self.i_dim == j: 

724 return np.ones_like(args[0]) 

725 else: 

726 return np.zeros_like(args[0]) 

727 

728 

729class ConstantFunction(MetricObject): 

730 """ 

731 A class for representing trivial functions that return the same real output for any input. This 

732 is convenient for models where an object might be a (non-trivial) function, but in some variations 

733 that object is just a constant number. Rather than needing to make a (Bi/Tri/Quad)- 

734 LinearInterpolation with trivial state grids and the same f_value in every entry, ConstantFunction 

735 allows the user to quickly make a constant/trivial function. This comes up, e.g., in models 

736 with endogenous pricing of insurance contracts; a contract's premium might depend on some state 

737 variables of the individual, but in some variations the premium of a contract is just a number. 

738 

739 Parameters 

740 ---------- 

741 value : float 

742 The constant value that the function returns. 

743 """ 

744 

745 convergence_criteria = ["value"] 

746 

747 def __init__(self, value): 

748 self.value = float(value) 

749 

750 def __call__(self, *args): 

751 """ 

752 Evaluate the constant function. The first input must exist and should be an array. 

753 Returns an array of identical shape to args[0] (if it exists). 

754 """ 

755 if ( 

756 len(args) > 0 

757 ): # If there is at least one argument, return appropriately sized array 

758 if _isscalar(args[0]): 

759 return self.value 

760 else: 

761 shape = args[0].shape 

762 return self.value * np.ones(shape) 

763 else: # Otherwise, return a single instance of the constant value 

764 return self.value 

765 

766 def _der(self, *args): 

767 """ 

768 Evaluate the derivative of the function. The first input must exist and should be an array. 

769 Returns an array of identical shape to args[0] (if it exists). This is an array of zeros. 

770 """ 

771 if len(args) > 0: 

772 if _isscalar(args[0]): 

773 return 0.0 

774 else: 

775 shape = args[0].shape 

776 return np.zeros(shape) 

777 else: 

778 return 0.0 

779 

780 def eval_with_derivative(self, x): 

781 val = self(x) 

782 der = self._der(x) 

783 return val, der 

784 

785 # All other derivatives are also zero everywhere, so these methods just point to derivative 

786 derivative = _der 

787 derivativeX = derivative 

788 derivativeY = derivative 

789 derivativeZ = derivative 

790 derivativeW = derivative 

791 derivativeXX = derivative 

792 

793 

794class LinearInterp(HARKinterpolator1D): 

795 """ 

796 A "from scratch" 1D linear interpolation class. Allows for linear or decay 

797 extrapolation (approaching a limiting linear function from below). 

798 

799 NOTE: When no input is given for the limiting linear function, linear 

800 extrapolation is used above the highest gridpoint. 

801 

802 Parameters 

803 ---------- 

804 x_list : np.array 

805 List of x values composing the grid. 

806 y_list : np.array 

807 List of y values, representing f(x) at the points in x_list. 

808 intercept_limit : float 

809 Intercept of limiting linear function. 

810 slope_limit : float 

811 Slope of limiting linear function. 

812 lower_extrap : bool 

813 Indicator for whether lower extrapolation is allowed. False means 

814 f(x) = NaN for x < min(x_list); True means linear extrapolation. 

815 pre_compute : bool 

816 Indicator for whether interpolation coefficients should be pre-computed 

817 and stored as attributes of self (default False). More memory will be used, 

818 and instantiation will take slightly longer, but later evaluation will 

819 be faster due to less arithmetic. 

820 indexer : function or None (default) 

821 If provided, a custom function that identifies the index of the interpolant 

822 segment for each query point. Should return results identically to the 

823 default behavior of np.maximum(np.searchsorted(self.x_list[:-1], x), 1). 

824 WARNING: User is responsible for verifying that their custom indexer is 

825 actually correct versus default behavior. 

826 """ 

827 

828 distance_criteria = ["x_list", "y_list"] 

829 

830 def __init__( 

831 self, 

832 x_list, 

833 y_list, 

834 intercept_limit=None, 

835 slope_limit=None, 

836 lower_extrap=False, 

837 pre_compute=False, 

838 indexer=None, 

839 ): 

840 # Make the basic linear spline interpolation 

841 self.x_list = ( 

842 np.array(x_list) 

843 if _check_flatten(1, x_list) 

844 else np.array(x_list).flatten() 

845 ) 

846 self.y_list = ( 

847 np.array(y_list) 

848 if _check_flatten(1, y_list) 

849 else np.array(y_list).flatten() 

850 ) 

851 _check_grid_dimensions(1, self.y_list, self.x_list) 

852 self.lower_extrap = lower_extrap 

853 self.x_n = self.x_list.size 

854 self.indexer = indexer 

855 

856 # Make a decay extrapolation 

857 if intercept_limit is not None and slope_limit is not None: 

858 slope_at_top = (y_list[-1] - y_list[-2]) / (x_list[-1] - x_list[-2]) 

859 level_diff = intercept_limit + slope_limit * x_list[-1] - y_list[-1] 

860 slope_diff = slope_limit - slope_at_top 

861 # If the model that can handle uncertainty has been calibrated with 

862 # with uncertainty set to zero, the 'extrapolation' will blow up 

863 # Guard against that and nearby problems by testing slope equality 

864 if not np.isclose(slope_limit, slope_at_top, atol=1e-15): 

865 self.decay_extrap_A = level_diff 

866 self.decay_extrap_B = -slope_diff / level_diff 

867 self.intercept_limit = intercept_limit 

868 self.slope_limit = slope_limit 

869 self.decay_extrap = True 

870 else: 

871 self.decay_extrap = False 

872 else: 

873 self.decay_extrap = False 

874 

875 # Calculate interpolation coefficients now rather than at evaluation time 

876 if pre_compute: 

877 self.slopes = (self.y_list[1:] - self.y_list[:-1]) / ( 

878 self.x_list[1:] - self.x_list[:-1] 

879 ) 

880 self.intercepts = self.y_list[:-1] - self.slopes * self.x_list[:-1] 

881 

882 def _evalOrDer(self, x, _eval, _Der): 

883 """ 

884 Returns the level and/or first derivative of the function at each value in 

885 x. Only called internally by HARKinterpolator1D.eval_and_der (etc). 

886 

887 Parameters 

888 ---------- 

889 x_list : scalar or np.array 

890 Set of points where we want to evlauate the interpolated function and/or its derivative.. 

891 _eval : boolean 

892 Indicator for whether to evalute the level of the interpolated function. 

893 _Der : boolean 

894 Indicator for whether to evaluate the derivative of the interpolated function. 

895 

896 Returns 

897 ------- 

898 A list including the level and/or derivative of the interpolated function where requested. 

899 """ 

900 if self.indexer is None: 

901 i = np.maximum(np.searchsorted(self.x_list[:-1], x), 1) 

902 else: 

903 i = self.indexer(x) 

904 

905 if hasattr(self, "slopes"): 

906 # Coefficients were pre-computed, use those 

907 j = i - 1 

908 dydx = self.slopes[j] 

909 if _eval: 

910 y = self.intercepts[j] + dydx * x 

911 

912 else: 

913 # Find relative weights between endpoints and evaluate interpolation 

914 alpha = (x - self.x_list[i - 1]) / (self.x_list[i] - self.x_list[i - 1]) 

915 

916 if _eval: 

917 y = (1.0 - alpha) * self.y_list[i - 1] + alpha * self.y_list[i] 

918 if _Der: 

919 dydx = (self.y_list[i] - self.y_list[i - 1]) / ( 

920 self.x_list[i] - self.x_list[i - 1] 

921 ) 

922 

923 if not self.lower_extrap: 

924 below_lower_bound = x < self.x_list[0] 

925 

926 if _eval: 

927 y[below_lower_bound] = np.nan 

928 if _Der: 

929 dydx[below_lower_bound] = np.nan 

930 

931 if self.decay_extrap: 

932 above_upper_bound = x > self.x_list[-1] 

933 x_temp = x[above_upper_bound] - self.x_list[-1] 

934 

935 if _eval: 

936 y[above_upper_bound] = ( 

937 self.intercept_limit 

938 + self.slope_limit * x[above_upper_bound] 

939 - self.decay_extrap_A * np.exp(-self.decay_extrap_B * x_temp) 

940 ) 

941 

942 if _Der: 

943 dydx[above_upper_bound] = ( 

944 self.slope_limit 

945 + self.decay_extrap_B 

946 * self.decay_extrap_A 

947 * np.exp(-self.decay_extrap_B * x_temp) 

948 ) 

949 

950 output = [] 

951 if _eval: 

952 output += [ 

953 y, 

954 ] 

955 if _Der: 

956 output += [ 

957 dydx, 

958 ] 

959 

960 return output 

961 

962 def _evaluate(self, x, return_indices=False): 

963 """ 

964 Returns the level of the interpolated function at each value in x. Only 

965 called internally by HARKinterpolator1D.__call__ (etc). 

966 """ 

967 return self._evalOrDer(x, True, False)[0] 

968 

969 def _der(self, x): 

970 """ 

971 Returns the first derivative of the interpolated function at each value 

972 in x. Only called internally by HARKinterpolator1D.derivative (etc). 

973 """ 

974 return self._evalOrDer(x, False, True)[0] 

975 

976 def _evalAndDer(self, x): 

977 """ 

978 Returns the level and first derivative of the function at each value in 

979 x. Only called internally by HARKinterpolator1D.eval_and_der (etc). 

980 """ 

981 y, dydx = self._evalOrDer(x, True, True) 

982 

983 return y, dydx 

984 

985 

986class CubicInterp(HARKinterpolator1D): 

987 """ 

988 An interpolating function using piecewise cubic splines. Matches level and 

989 slope of 1D function at gridpoints, smoothly interpolating in between. 

990 Extrapolation above highest gridpoint approaches a limiting linear function 

991 if desired (linear extrapolation also enabled.) 

992 

993 NOTE: When no input is given for the limiting linear function, linear 

994 extrapolation is used above the highest gridpoint. 

995 

996 Parameters 

997 ---------- 

998 x_list : np.array 

999 List of x values composing the grid. 

1000 y_list : np.array 

1001 List of y values, representing f(x) at the points in x_list. 

1002 dydx_list : np.array 

1003 List of dydx values, representing f'(x) at the points in x_list 

1004 intercept_limit : float 

1005 Intercept of limiting linear function. 

1006 slope_limit : float 

1007 Slope of limiting linear function. 

1008 lower_extrap : boolean 

1009 Indicator for whether lower extrapolation is allowed. False means 

1010 f(x) = NaN for x < min(x_list); True means linear extrapolation. 

1011 """ 

1012 

1013 distance_criteria = ["x_list", "y_list", "dydx_list"] 

1014 

1015 def __init__( 

1016 self, 

1017 x_list, 

1018 y_list, 

1019 dydx_list, 

1020 intercept_limit=None, 

1021 slope_limit=None, 

1022 lower_extrap=False, 

1023 ): 

1024 self.x_list = ( 

1025 np.asarray(x_list) 

1026 if _check_flatten(1, x_list) 

1027 else np.array(x_list).flatten() 

1028 ) 

1029 self.y_list = ( 

1030 np.asarray(y_list) 

1031 if _check_flatten(1, y_list) 

1032 else np.array(y_list).flatten() 

1033 ) 

1034 self.dydx_list = ( 

1035 np.asarray(dydx_list) 

1036 if _check_flatten(1, dydx_list) 

1037 else np.array(dydx_list).flatten() 

1038 ) 

1039 _check_grid_dimensions(1, self.y_list, self.x_list) 

1040 _check_grid_dimensions(1, self.dydx_list, self.x_list) 

1041 

1042 self.n = len(x_list) 

1043 

1044 # Define lower extrapolation as linear function (or just NaN) 

1045 if lower_extrap: 

1046 self.coeffs = [[y_list[0], dydx_list[0], 0, 0]] 

1047 else: 

1048 self.coeffs = [[np.nan, np.nan, np.nan, np.nan]] 

1049 

1050 # Calculate interpolation coefficients on segments mapped to [0,1] 

1051 for i in range(self.n - 1): 

1052 x0 = x_list[i] 

1053 y0 = y_list[i] 

1054 x1 = x_list[i + 1] 

1055 y1 = y_list[i + 1] 

1056 Span = x1 - x0 

1057 dydx0 = dydx_list[i] * Span 

1058 dydx1 = dydx_list[i + 1] * Span 

1059 

1060 temp = [ 

1061 y0, 

1062 dydx0, 

1063 3 * (y1 - y0) - 2 * dydx0 - dydx1, 

1064 2 * (y0 - y1) + dydx0 + dydx1, 

1065 ] 

1066 self.coeffs.append(temp) 

1067 

1068 # Calculate extrapolation coefficients as a decay toward limiting function y = mx+b 

1069 if slope_limit is None and intercept_limit is None: 

1070 slope_limit = dydx_list[-1] 

1071 intercept_limit = y_list[-1] - slope_limit * x_list[-1] 

1072 gap = slope_limit * x1 + intercept_limit - y1 

1073 slope = slope_limit - dydx_list[self.n - 1] 

1074 if (gap != 0) and (slope <= 0): 

1075 temp = [intercept_limit, slope_limit, gap, slope / gap] 

1076 elif slope > 0: 

1077 temp = [ 

1078 intercept_limit, 

1079 slope_limit, 

1080 0, 

1081 0, 

1082 ] # fixing a problem when slope is positive 

1083 else: 

1084 temp = [intercept_limit, slope_limit, gap, 0] 

1085 self.coeffs.append(temp) 

1086 self.coeffs = np.array(self.coeffs) 

1087 

1088 def _evaluate(self, x): 

1089 """ 

1090 Returns the level of the interpolated function at each value in x. Only 

1091 called internally by HARKinterpolator1D.__call__ (etc). 

1092 """ 

1093 

1094 m = len(x) 

1095 pos = np.searchsorted(self.x_list, x, side="right") 

1096 y = np.zeros(m) 

1097 if y.size > 0: 

1098 out_bot = pos == 0 

1099 out_top = pos == self.n 

1100 in_bnds = np.logical_not(np.logical_or(out_bot, out_top)) 

1101 

1102 # Do the "in bounds" evaluation points 

1103 i = pos[in_bnds] 

1104 coeffs_in = self.coeffs[i, :] 

1105 alpha = (x[in_bnds] - self.x_list[i - 1]) / ( 

1106 self.x_list[i] - self.x_list[i - 1] 

1107 ) 

1108 y[in_bnds] = coeffs_in[:, 0] + alpha * ( 

1109 coeffs_in[:, 1] + alpha * (coeffs_in[:, 2] + alpha * coeffs_in[:, 3]) 

1110 ) 

1111 

1112 # Do the "out of bounds" evaluation points 

1113 y[out_bot] = self.coeffs[0, 0] + self.coeffs[0, 1] * ( 

1114 x[out_bot] - self.x_list[0] 

1115 ) 

1116 alpha = x[out_top] - self.x_list[self.n - 1] 

1117 y[out_top] = ( 

1118 self.coeffs[self.n, 0] 

1119 + x[out_top] * self.coeffs[self.n, 1] 

1120 - self.coeffs[self.n, 2] * np.exp(alpha * self.coeffs[self.n, 3]) 

1121 ) 

1122 

1123 return y 

1124 

1125 def _der(self, x): 

1126 """ 

1127 Returns the first derivative of the interpolated function at each value 

1128 in x. Only called internally by HARKinterpolator1D.derivative (etc). 

1129 """ 

1130 

1131 m = len(x) 

1132 pos = np.searchsorted(self.x_list, x, side="right") 

1133 dydx = np.zeros(m) 

1134 if dydx.size > 0: 

1135 out_bot = pos == 0 

1136 out_top = pos == self.n 

1137 in_bnds = np.logical_not(np.logical_or(out_bot, out_top)) 

1138 

1139 # Do the "in bounds" evaluation points 

1140 i = pos[in_bnds] 

1141 coeffs_in = self.coeffs[i, :] 

1142 alpha = (x[in_bnds] - self.x_list[i - 1]) / ( 

1143 self.x_list[i] - self.x_list[i - 1] 

1144 ) 

1145 dydx[in_bnds] = ( 

1146 coeffs_in[:, 1] 

1147 + alpha * (2 * coeffs_in[:, 2] + alpha * 3 * coeffs_in[:, 3]) 

1148 ) / (self.x_list[i] - self.x_list[i - 1]) 

1149 

1150 # Do the "out of bounds" evaluation points 

1151 dydx[out_bot] = self.coeffs[0, 1] 

1152 alpha = x[out_top] - self.x_list[self.n - 1] 

1153 dydx[out_top] = self.coeffs[self.n, 1] - self.coeffs[ 

1154 self.n, 2 

1155 ] * self.coeffs[self.n, 3] * np.exp(alpha * self.coeffs[self.n, 3]) 

1156 return dydx 

1157 

1158 def _evalAndDer(self, x): 

1159 """ 

1160 Returns the level and first derivative of the function at each value in 

1161 x. Only called internally by HARKinterpolator1D.eval_and_der (etc). 

1162 """ 

1163 m = len(x) 

1164 pos = np.searchsorted(self.x_list, x, side="right") 

1165 y = np.zeros(m) 

1166 dydx = np.zeros(m) 

1167 if y.size > 0: 

1168 out_bot = pos == 0 

1169 out_top = pos == self.n 

1170 in_bnds = np.logical_not(np.logical_or(out_bot, out_top)) 

1171 

1172 # Do the "in bounds" evaluation points 

1173 i = pos[in_bnds] 

1174 coeffs_in = self.coeffs[i, :] 

1175 alpha = (x[in_bnds] - self.x_list[i - 1]) / ( 

1176 self.x_list[i] - self.x_list[i - 1] 

1177 ) 

1178 y[in_bnds] = coeffs_in[:, 0] + alpha * ( 

1179 coeffs_in[:, 1] + alpha * (coeffs_in[:, 2] + alpha * coeffs_in[:, 3]) 

1180 ) 

1181 dydx[in_bnds] = ( 

1182 coeffs_in[:, 1] 

1183 + alpha * (2 * coeffs_in[:, 2] + alpha * 3 * coeffs_in[:, 3]) 

1184 ) / (self.x_list[i] - self.x_list[i - 1]) 

1185 

1186 # Do the "out of bounds" evaluation points 

1187 y[out_bot] = self.coeffs[0, 0] + self.coeffs[0, 1] * ( 

1188 x[out_bot] - self.x_list[0] 

1189 ) 

1190 dydx[out_bot] = self.coeffs[0, 1] 

1191 alpha = x[out_top] - self.x_list[self.n - 1] 

1192 y[out_top] = ( 

1193 self.coeffs[self.n, 0] 

1194 + x[out_top] * self.coeffs[self.n, 1] 

1195 - self.coeffs[self.n, 2] * np.exp(alpha * self.coeffs[self.n, 3]) 

1196 ) 

1197 dydx[out_top] = self.coeffs[self.n, 1] - self.coeffs[ 

1198 self.n, 2 

1199 ] * self.coeffs[self.n, 3] * np.exp(alpha * self.coeffs[self.n, 3]) 

1200 return y, dydx 

1201 

1202 

1203class CubicHermiteInterp(HARKinterpolator1D): 

1204 """ 

1205 An interpolating function using piecewise cubic splines. Matches level and 

1206 slope of 1D function at gridpoints, smoothly interpolating in between. 

1207 Extrapolation above highest gridpoint approaches a limiting linear function 

1208 if desired (linear extrapolation also enabled.) 

1209 

1210 NOTE: When no input is given for the limiting linear function, linear 

1211 extrapolation is used above the highest gridpoint. 

1212 

1213 Parameters 

1214 ---------- 

1215 x_list : np.array 

1216 List of x values composing the grid. 

1217 y_list : np.array 

1218 List of y values, representing f(x) at the points in x_list. 

1219 dydx_list : np.array 

1220 List of dydx values, representing f'(x) at the points in x_list 

1221 intercept_limit : float 

1222 Intercept of limiting linear function. 

1223 slope_limit : float 

1224 Slope of limiting linear function. 

1225 lower_extrap : boolean 

1226 Indicator for whether lower extrapolation is allowed. False means 

1227 f(x) = NaN for x < min(x_list); True means linear extrapolation. 

1228 """ 

1229 

1230 distance_criteria = ["x_list", "y_list", "dydx_list"] 

1231 

1232 def __init__( 

1233 self, 

1234 x_list, 

1235 y_list, 

1236 dydx_list, 

1237 intercept_limit=None, 

1238 slope_limit=None, 

1239 lower_extrap=False, 

1240 ): 

1241 self.x_list = ( 

1242 np.asarray(x_list) 

1243 if _check_flatten(1, x_list) 

1244 else np.array(x_list).flatten() 

1245 ) 

1246 self.y_list = ( 

1247 np.asarray(y_list) 

1248 if _check_flatten(1, y_list) 

1249 else np.array(y_list).flatten() 

1250 ) 

1251 self.dydx_list = ( 

1252 np.asarray(dydx_list) 

1253 if _check_flatten(1, dydx_list) 

1254 else np.array(dydx_list).flatten() 

1255 ) 

1256 _check_grid_dimensions(1, self.y_list, self.x_list) 

1257 _check_grid_dimensions(1, self.dydx_list, self.x_list) 

1258 

1259 self.n = len(x_list) 

1260 

1261 self._chs = CubicHermiteSpline( 

1262 self.x_list, self.y_list, self.dydx_list, extrapolate=None 

1263 ) 

1264 self.coeffs = np.flip(self._chs.c.T, 1) 

1265 

1266 # Define lower extrapolation as linear function (or just NaN) 

1267 if lower_extrap: 

1268 temp = np.array([self.y_list[0], self.dydx_list[0], 0, 0]) 

1269 else: 

1270 temp = np.array([np.nan, np.nan, np.nan, np.nan]) 

1271 

1272 self.coeffs = np.vstack((temp, self.coeffs)) 

1273 

1274 x1 = self.x_list[self.n - 1] 

1275 y1 = self.y_list[self.n - 1] 

1276 

1277 # Calculate extrapolation coefficients as a decay toward limiting function y = mx+b 

1278 if slope_limit is None and intercept_limit is None: 

1279 slope_limit = self.dydx_list[-1] 

1280 intercept_limit = self.y_list[-1] - slope_limit * self.x_list[-1] 

1281 gap = slope_limit * x1 + intercept_limit - y1 

1282 slope = slope_limit - self.dydx_list[self.n - 1] 

1283 if (gap != 0) and (slope <= 0): 

1284 temp = np.array([intercept_limit, slope_limit, gap, slope / gap]) 

1285 elif slope > 0: 

1286 # fixing a problem when slope is positive 

1287 temp = np.array([intercept_limit, slope_limit, 0, 0]) 

1288 else: 

1289 temp = np.array([intercept_limit, slope_limit, gap, 0]) 

1290 self.coeffs = np.vstack((self.coeffs, temp)) 

1291 

1292 def out_of_bounds(self, x): 

1293 out_bot = x < self.x_list[0] 

1294 out_top = x > self.x_list[-1] 

1295 return out_bot, out_top 

1296 

1297 def _evaluate(self, x): 

1298 """ 

1299 Returns the level of the interpolated function at each value in x. Only 

1300 called internally by HARKinterpolator1D.__call__ (etc). 

1301 """ 

1302 out_bot, out_top = self.out_of_bounds(x) 

1303 

1304 return self._eval_helper(x, out_bot, out_top) 

1305 

1306 def _eval_helper(self, x, out_bot, out_top): 

1307 y = self._chs(x) 

1308 

1309 # Do the "out of bounds" evaluation points 

1310 if any(out_bot): 

1311 y[out_bot] = self.coeffs[0, 0] + self.coeffs[0, 1] * ( 

1312 x[out_bot] - self.x_list[0] 

1313 ) 

1314 

1315 if any(out_top): 

1316 alpha = x[out_top] - self.x_list[self.n - 1] 

1317 y[out_top] = ( 

1318 self.coeffs[self.n, 0] 

1319 + x[out_top] * self.coeffs[self.n, 1] 

1320 - self.coeffs[self.n, 2] * np.exp(alpha * self.coeffs[self.n, 3]) 

1321 ) 

1322 

1323 return y 

1324 

1325 def _der(self, x): 

1326 """ 

1327 Returns the first derivative of the interpolated function at each value 

1328 in x. Only called internally by HARKinterpolator1D.derivative (etc). 

1329 """ 

1330 out_bot, out_top = self.out_of_bounds(x) 

1331 

1332 return self._der_helper(x, out_bot, out_top) 

1333 

1334 def _der_helper(self, x, out_bot, out_top): 

1335 dydx = self._chs(x, nu=1) 

1336 

1337 # Do the "out of bounds" evaluation points 

1338 if any(out_bot): 

1339 dydx[out_bot] = self.coeffs[0, 1] 

1340 if any(out_top): 

1341 alpha = x[out_top] - self.x_list[self.n - 1] 

1342 dydx[out_top] = self.coeffs[self.n, 1] - self.coeffs[ 

1343 self.n, 2 

1344 ] * self.coeffs[self.n, 3] * np.exp(alpha * self.coeffs[self.n, 3]) 

1345 

1346 return dydx 

1347 

1348 def _evalAndDer(self, x): 

1349 """ 

1350 Returns the level and first derivative of the function at each value in 

1351 x. Only called internally by HARKinterpolator1D.eval_and_der (etc). 

1352 """ 

1353 out_bot, out_top = self.out_of_bounds(x) 

1354 y = self._eval_helper(x, out_bot, out_top) 

1355 dydx = self._der_helper(x, out_bot, out_top) 

1356 return y, dydx 

1357 

1358 def der_interp(self, nu=1): 

1359 """ 

1360 Construct a new piecewise polynomial representing the derivative. 

1361 See `scipy` for additional documentation: 

1362 https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html 

1363 """ 

1364 return self._chs.derivative(nu) 

1365 

1366 def antider_interp(self, nu=1): 

1367 """ 

1368 Construct a new piecewise polynomial representing the antiderivative. 

1369 

1370 Antiderivative is also the indefinite integral of the function, 

1371 and derivative is its inverse operation. 

1372 

1373 See `scipy` for additional documentation: 

1374 https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html 

1375 """ 

1376 return self._chs.antiderivative(nu) 

1377 

1378 def integrate(self, a, b, extrapolate=False): 

1379 """ 

1380 Compute a definite integral over a piecewise polynomial. 

1381 

1382 See `scipy` for additional documentation: 

1383 https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html 

1384 """ 

1385 return self._chs.integrate(a, b, extrapolate) 

1386 

1387 def roots(self, discontinuity=True, extrapolate=False): 

1388 """ 

1389 Find real roots of the the piecewise polynomial. 

1390 

1391 See `scipy` for additional documentation: 

1392 https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html 

1393 """ 

1394 return self._chs.roots(discontinuity, extrapolate) 

1395 

1396 def solve(self, y=0.0, discontinuity=True, extrapolate=False): 

1397 """ 

1398 Find real solutions of the the equation ``pp(x) == y``. 

1399 

1400 See `scipy` for additional documentation: 

1401 https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html 

1402 """ 

1403 return self._chs.solve(y, discontinuity, extrapolate) 

1404 

1405 

1406class BilinearInterp(HARKinterpolator2D): 

1407 """ 

1408 Bilinear full (or tensor) grid interpolation of a function f(x,y). 

1409 

1410 Parameters 

1411 ---------- 

1412 f_values : numpy.array 

1413 An array of size (x_n,y_n) such that f_values[i,j] = f(x_list[i],y_list[j]) 

1414 x_list : numpy.array 

1415 An array of x values, with length designated x_n. 

1416 y_list : numpy.array 

1417 An array of y values, with length designated y_n. 

1418 xSearchFunc : function 

1419 An optional function that returns the reference location for x values: 

1420 indices = xSearchFunc(x_list,x). Default is np.searchsorted 

1421 ySearchFunc : function 

1422 An optional function that returns the reference location for y values: 

1423 indices = ySearchFunc(y_list,y). Default is np.searchsorted 

1424 """ 

1425 

1426 distance_criteria = ["x_list", "y_list", "f_values"] 

1427 

1428 def __init__(self, f_values, x_list, y_list, xSearchFunc=None, ySearchFunc=None): 

1429 self.f_values = f_values 

1430 self.x_list = ( 

1431 np.array(x_list) 

1432 if _check_flatten(1, x_list) 

1433 else np.array(x_list).flatten() 

1434 ) 

1435 self.y_list = ( 

1436 np.array(y_list) 

1437 if _check_flatten(1, y_list) 

1438 else np.array(y_list).flatten() 

1439 ) 

1440 _check_grid_dimensions(2, self.f_values, self.x_list, self.y_list) 

1441 self.x_n = x_list.size 

1442 self.y_n = y_list.size 

1443 if xSearchFunc is None: 

1444 xSearchFunc = np.searchsorted 

1445 if ySearchFunc is None: 

1446 ySearchFunc = np.searchsorted 

1447 self.xSearchFunc = xSearchFunc 

1448 self.ySearchFunc = ySearchFunc 

1449 

1450 def _evaluate(self, x, y): 

1451 """ 

1452 Returns the level of the interpolated function at each value in x,y. 

1453 Only called internally by HARKinterpolator2D.__call__ (etc). 

1454 """ 

1455 x_pos = self.xSearchFunc(self.x_list, x) 

1456 x_pos[x_pos < 1] = 1 

1457 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1458 y_pos = self.ySearchFunc(self.y_list, y) 

1459 y_pos[y_pos < 1] = 1 

1460 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1461 alpha = (x - self.x_list[x_pos - 1]) / ( 

1462 self.x_list[x_pos] - self.x_list[x_pos - 1] 

1463 ) 

1464 beta = (y - self.y_list[y_pos - 1]) / ( 

1465 self.y_list[y_pos] - self.y_list[y_pos - 1] 

1466 ) 

1467 f = ( 

1468 (1 - alpha) * (1 - beta) * self.f_values[x_pos - 1, y_pos - 1] 

1469 + (1 - alpha) * beta * self.f_values[x_pos - 1, y_pos] 

1470 + alpha * (1 - beta) * self.f_values[x_pos, y_pos - 1] 

1471 + alpha * beta * self.f_values[x_pos, y_pos] 

1472 ) 

1473 return f 

1474 

1475 def _derX(self, x, y): 

1476 """ 

1477 Returns the derivative with respect to x of the interpolated function 

1478 at each value in x,y. Only called internally by HARKinterpolator2D.derivativeX. 

1479 """ 

1480 x_pos = self.xSearchFunc(self.x_list, x) 

1481 x_pos[x_pos < 1] = 1 

1482 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1483 y_pos = self.ySearchFunc(self.y_list, y) 

1484 y_pos[y_pos < 1] = 1 

1485 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1486 beta = (y - self.y_list[y_pos - 1]) / ( 

1487 self.y_list[y_pos] - self.y_list[y_pos - 1] 

1488 ) 

1489 dfdx = ( 

1490 ( 

1491 (1 - beta) * self.f_values[x_pos, y_pos - 1] 

1492 + beta * self.f_values[x_pos, y_pos] 

1493 ) 

1494 - ( 

1495 (1 - beta) * self.f_values[x_pos - 1, y_pos - 1] 

1496 + beta * self.f_values[x_pos - 1, y_pos] 

1497 ) 

1498 ) / (self.x_list[x_pos] - self.x_list[x_pos - 1]) 

1499 return dfdx 

1500 

1501 def _derY(self, x, y): 

1502 """ 

1503 Returns the derivative with respect to y of the interpolated function 

1504 at each value in x,y. Only called internally by HARKinterpolator2D.derivativeY. 

1505 """ 

1506 x_pos = self.xSearchFunc(self.x_list, x) 

1507 x_pos[x_pos < 1] = 1 

1508 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1509 y_pos = self.ySearchFunc(self.y_list, y) 

1510 y_pos[y_pos < 1] = 1 

1511 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1512 alpha = (x - self.x_list[x_pos - 1]) / ( 

1513 self.x_list[x_pos] - self.x_list[x_pos - 1] 

1514 ) 

1515 dfdy = ( 

1516 ( 

1517 (1 - alpha) * self.f_values[x_pos - 1, y_pos] 

1518 + alpha * self.f_values[x_pos, y_pos] 

1519 ) 

1520 - ( 

1521 (1 - alpha) * self.f_values[x_pos - 1, y_pos - 1] 

1522 + alpha * self.f_values[x_pos, y_pos - 1] 

1523 ) 

1524 ) / (self.y_list[y_pos] - self.y_list[y_pos - 1]) 

1525 return dfdy 

1526 

1527 

1528class TrilinearInterp(HARKinterpolator3D): 

1529 """ 

1530 Trilinear full (or tensor) grid interpolation of a function f(x,y,z). 

1531 

1532 Parameters 

1533 ---------- 

1534 f_values : numpy.array 

1535 An array of size (x_n,y_n,z_n) such that f_values[i,j,k] = 

1536 f(x_list[i],y_list[j],z_list[k]) 

1537 x_list : numpy.array 

1538 An array of x values, with length designated x_n. 

1539 y_list : numpy.array 

1540 An array of y values, with length designated y_n. 

1541 z_list : numpy.array 

1542 An array of z values, with length designated z_n. 

1543 xSearchFunc : function 

1544 An optional function that returns the reference location for x values: 

1545 indices = xSearchFunc(x_list,x). Default is np.searchsorted 

1546 ySearchFunc : function 

1547 An optional function that returns the reference location for y values: 

1548 indices = ySearchFunc(y_list,y). Default is np.searchsorted 

1549 zSearchFunc : function 

1550 An optional function that returns the reference location for z values: 

1551 indices = zSearchFunc(z_list,z). Default is np.searchsorted 

1552 """ 

1553 

1554 distance_criteria = ["f_values", "x_list", "y_list", "z_list"] 

1555 

1556 def __init__( 

1557 self, 

1558 f_values, 

1559 x_list, 

1560 y_list, 

1561 z_list, 

1562 xSearchFunc=None, 

1563 ySearchFunc=None, 

1564 zSearchFunc=None, 

1565 ): 

1566 self.f_values = f_values 

1567 self.x_list = ( 

1568 np.array(x_list) 

1569 if _check_flatten(1, x_list) 

1570 else np.array(x_list).flatten() 

1571 ) 

1572 self.y_list = ( 

1573 np.array(y_list) 

1574 if _check_flatten(1, y_list) 

1575 else np.array(y_list).flatten() 

1576 ) 

1577 self.z_list = ( 

1578 np.array(z_list) 

1579 if _check_flatten(1, z_list) 

1580 else np.array(z_list).flatten() 

1581 ) 

1582 _check_grid_dimensions(3, self.f_values, self.x_list, self.y_list, self.z_list) 

1583 self.x_n = x_list.size 

1584 self.y_n = y_list.size 

1585 self.z_n = z_list.size 

1586 if xSearchFunc is None: 

1587 xSearchFunc = np.searchsorted 

1588 if ySearchFunc is None: 

1589 ySearchFunc = np.searchsorted 

1590 if zSearchFunc is None: 

1591 zSearchFunc = np.searchsorted 

1592 self.xSearchFunc = xSearchFunc 

1593 self.ySearchFunc = ySearchFunc 

1594 self.zSearchFunc = zSearchFunc 

1595 

1596 def _evaluate(self, x, y, z): 

1597 """ 

1598 Returns the level of the interpolated function at each value in x,y,z. 

1599 Only called internally by HARKinterpolator3D.__call__ (etc). 

1600 """ 

1601 x_pos = self.xSearchFunc(self.x_list, x) 

1602 x_pos[x_pos < 1] = 1 

1603 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1604 y_pos = self.ySearchFunc(self.y_list, y) 

1605 y_pos[y_pos < 1] = 1 

1606 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1607 z_pos = self.zSearchFunc(self.z_list, z) 

1608 z_pos[z_pos < 1] = 1 

1609 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1610 alpha = (x - self.x_list[x_pos - 1]) / ( 

1611 self.x_list[x_pos] - self.x_list[x_pos - 1] 

1612 ) 

1613 beta = (y - self.y_list[y_pos - 1]) / ( 

1614 self.y_list[y_pos] - self.y_list[y_pos - 1] 

1615 ) 

1616 gamma = (z - self.z_list[z_pos - 1]) / ( 

1617 self.z_list[z_pos] - self.z_list[z_pos - 1] 

1618 ) 

1619 f = ( 

1620 (1 - alpha) 

1621 * (1 - beta) 

1622 * (1 - gamma) 

1623 * self.f_values[x_pos - 1, y_pos - 1, z_pos - 1] 

1624 + (1 - alpha) 

1625 * (1 - beta) 

1626 * gamma 

1627 * self.f_values[x_pos - 1, y_pos - 1, z_pos] 

1628 + (1 - alpha) 

1629 * beta 

1630 * (1 - gamma) 

1631 * self.f_values[x_pos - 1, y_pos, z_pos - 1] 

1632 + (1 - alpha) * beta * gamma * self.f_values[x_pos - 1, y_pos, z_pos] 

1633 + alpha 

1634 * (1 - beta) 

1635 * (1 - gamma) 

1636 * self.f_values[x_pos, y_pos - 1, z_pos - 1] 

1637 + alpha * (1 - beta) * gamma * self.f_values[x_pos, y_pos - 1, z_pos] 

1638 + alpha * beta * (1 - gamma) * self.f_values[x_pos, y_pos, z_pos - 1] 

1639 + alpha * beta * gamma * self.f_values[x_pos, y_pos, z_pos] 

1640 ) 

1641 return f 

1642 

1643 def _derX(self, x, y, z): 

1644 """ 

1645 Returns the derivative with respect to x of the interpolated function 

1646 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeX. 

1647 """ 

1648 x_pos = self.xSearchFunc(self.x_list, x) 

1649 x_pos[x_pos < 1] = 1 

1650 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1651 y_pos = self.ySearchFunc(self.y_list, y) 

1652 y_pos[y_pos < 1] = 1 

1653 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1654 z_pos = self.zSearchFunc(self.z_list, z) 

1655 z_pos[z_pos < 1] = 1 

1656 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1657 beta = (y - self.y_list[y_pos - 1]) / ( 

1658 self.y_list[y_pos] - self.y_list[y_pos - 1] 

1659 ) 

1660 gamma = (z - self.z_list[z_pos - 1]) / ( 

1661 self.z_list[z_pos] - self.z_list[z_pos - 1] 

1662 ) 

1663 dfdx = ( 

1664 ( 

1665 (1 - beta) * (1 - gamma) * self.f_values[x_pos, y_pos - 1, z_pos - 1] 

1666 + (1 - beta) * gamma * self.f_values[x_pos, y_pos - 1, z_pos] 

1667 + beta * (1 - gamma) * self.f_values[x_pos, y_pos, z_pos - 1] 

1668 + beta * gamma * self.f_values[x_pos, y_pos, z_pos] 

1669 ) 

1670 - ( 

1671 (1 - beta) 

1672 * (1 - gamma) 

1673 * self.f_values[x_pos - 1, y_pos - 1, z_pos - 1] 

1674 + (1 - beta) * gamma * self.f_values[x_pos - 1, y_pos - 1, z_pos] 

1675 + beta * (1 - gamma) * self.f_values[x_pos - 1, y_pos, z_pos - 1] 

1676 + beta * gamma * self.f_values[x_pos - 1, y_pos, z_pos] 

1677 ) 

1678 ) / (self.x_list[x_pos] - self.x_list[x_pos - 1]) 

1679 return dfdx 

1680 

1681 def _derY(self, x, y, z): 

1682 """ 

1683 Returns the derivative with respect to y of the interpolated function 

1684 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeY. 

1685 """ 

1686 x_pos = self.xSearchFunc(self.x_list, x) 

1687 x_pos[x_pos < 1] = 1 

1688 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1689 y_pos = self.ySearchFunc(self.y_list, y) 

1690 y_pos[y_pos < 1] = 1 

1691 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1692 z_pos = self.zSearchFunc(self.z_list, z) 

1693 z_pos[z_pos < 1] = 1 

1694 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1695 alpha = (x - self.x_list[x_pos - 1]) / ( 

1696 self.x_list[x_pos] - self.x_list[x_pos - 1] 

1697 ) 

1698 gamma = (z - self.z_list[z_pos - 1]) / ( 

1699 self.z_list[z_pos] - self.z_list[z_pos - 1] 

1700 ) 

1701 dfdy = ( 

1702 ( 

1703 (1 - alpha) * (1 - gamma) * self.f_values[x_pos - 1, y_pos, z_pos - 1] 

1704 + (1 - alpha) * gamma * self.f_values[x_pos - 1, y_pos, z_pos] 

1705 + alpha * (1 - gamma) * self.f_values[x_pos, y_pos, z_pos - 1] 

1706 + alpha * gamma * self.f_values[x_pos, y_pos, z_pos] 

1707 ) 

1708 - ( 

1709 (1 - alpha) 

1710 * (1 - gamma) 

1711 * self.f_values[x_pos - 1, y_pos - 1, z_pos - 1] 

1712 + (1 - alpha) * gamma * self.f_values[x_pos - 1, y_pos - 1, z_pos] 

1713 + alpha * (1 - gamma) * self.f_values[x_pos, y_pos - 1, z_pos - 1] 

1714 + alpha * gamma * self.f_values[x_pos, y_pos - 1, z_pos] 

1715 ) 

1716 ) / (self.y_list[y_pos] - self.y_list[y_pos - 1]) 

1717 return dfdy 

1718 

1719 def _derZ(self, x, y, z): 

1720 """ 

1721 Returns the derivative with respect to z of the interpolated function 

1722 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeZ. 

1723 """ 

1724 x_pos = self.xSearchFunc(self.x_list, x) 

1725 x_pos[x_pos < 1] = 1 

1726 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1727 y_pos = self.ySearchFunc(self.y_list, y) 

1728 y_pos[y_pos < 1] = 1 

1729 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1730 z_pos = self.zSearchFunc(self.z_list, z) 

1731 z_pos[z_pos < 1] = 1 

1732 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1733 alpha = (x - self.x_list[x_pos - 1]) / ( 

1734 self.x_list[x_pos] - self.x_list[x_pos - 1] 

1735 ) 

1736 beta = (y - self.y_list[y_pos - 1]) / ( 

1737 self.y_list[y_pos] - self.y_list[y_pos - 1] 

1738 ) 

1739 dfdz = ( 

1740 ( 

1741 (1 - alpha) * (1 - beta) * self.f_values[x_pos - 1, y_pos - 1, z_pos] 

1742 + (1 - alpha) * beta * self.f_values[x_pos - 1, y_pos, z_pos] 

1743 + alpha * (1 - beta) * self.f_values[x_pos, y_pos - 1, z_pos] 

1744 + alpha * beta * self.f_values[x_pos, y_pos, z_pos] 

1745 ) 

1746 - ( 

1747 (1 - alpha) 

1748 * (1 - beta) 

1749 * self.f_values[x_pos - 1, y_pos - 1, z_pos - 1] 

1750 + (1 - alpha) * beta * self.f_values[x_pos - 1, y_pos, z_pos - 1] 

1751 + alpha * (1 - beta) * self.f_values[x_pos, y_pos - 1, z_pos - 1] 

1752 + alpha * beta * self.f_values[x_pos, y_pos, z_pos - 1] 

1753 ) 

1754 ) / (self.z_list[z_pos] - self.z_list[z_pos - 1]) 

1755 return dfdz 

1756 

1757 

1758class QuadlinearInterp(HARKinterpolator4D): 

1759 """ 

1760 Quadlinear full (or tensor) grid interpolation of a function f(w,x,y,z). 

1761 

1762 Parameters 

1763 ---------- 

1764 f_values : numpy.array 

1765 An array of size (w_n,x_n,y_n,z_n) such that f_values[i,j,k,l] = 

1766 f(w_list[i],x_list[j],y_list[k],z_list[l]) 

1767 w_list : numpy.array 

1768 An array of x values, with length designated w_n. 

1769 x_list : numpy.array 

1770 An array of x values, with length designated x_n. 

1771 y_list : numpy.array 

1772 An array of y values, with length designated y_n. 

1773 z_list : numpy.array 

1774 An array of z values, with length designated z_n. 

1775 wSearchFunc : function 

1776 An optional function that returns the reference location for w values: 

1777 indices = wSearchFunc(w_list,w). Default is np.searchsorted 

1778 xSearchFunc : function 

1779 An optional function that returns the reference location for x values: 

1780 indices = xSearchFunc(x_list,x). Default is np.searchsorted 

1781 ySearchFunc : function 

1782 An optional function that returns the reference location for y values: 

1783 indices = ySearchFunc(y_list,y). Default is np.searchsorted 

1784 zSearchFunc : function 

1785 An optional function that returns the reference location for z values: 

1786 indices = zSearchFunc(z_list,z). Default is np.searchsorted 

1787 """ 

1788 

1789 distance_criteria = ["f_values", "w_list", "x_list", "y_list", "z_list"] 

1790 

1791 def __init__( 

1792 self, 

1793 f_values, 

1794 w_list, 

1795 x_list, 

1796 y_list, 

1797 z_list, 

1798 wSearchFunc=None, 

1799 xSearchFunc=None, 

1800 ySearchFunc=None, 

1801 zSearchFunc=None, 

1802 ): 

1803 self.f_values = f_values 

1804 self.w_list = ( 

1805 np.array(w_list) 

1806 if _check_flatten(1, w_list) 

1807 else np.array(w_list).flatten() 

1808 ) 

1809 self.x_list = ( 

1810 np.array(x_list) 

1811 if _check_flatten(1, x_list) 

1812 else np.array(x_list).flatten() 

1813 ) 

1814 self.y_list = ( 

1815 np.array(y_list) 

1816 if _check_flatten(1, y_list) 

1817 else np.array(y_list).flatten() 

1818 ) 

1819 self.z_list = ( 

1820 np.array(z_list) 

1821 if _check_flatten(1, z_list) 

1822 else np.array(z_list).flatten() 

1823 ) 

1824 _check_grid_dimensions( 

1825 4, self.f_values, self.w_list, self.x_list, self.y_list, self.z_list 

1826 ) 

1827 self.w_n = w_list.size 

1828 self.x_n = x_list.size 

1829 self.y_n = y_list.size 

1830 self.z_n = z_list.size 

1831 if wSearchFunc is None: 

1832 wSearchFunc = np.searchsorted 

1833 if xSearchFunc is None: 

1834 xSearchFunc = np.searchsorted 

1835 if ySearchFunc is None: 

1836 ySearchFunc = np.searchsorted 

1837 if zSearchFunc is None: 

1838 zSearchFunc = np.searchsorted 

1839 self.wSearchFunc = wSearchFunc 

1840 self.xSearchFunc = xSearchFunc 

1841 self.ySearchFunc = ySearchFunc 

1842 self.zSearchFunc = zSearchFunc 

1843 

1844 def _evaluate(self, w, x, y, z): 

1845 """ 

1846 Returns the level of the interpolated function at each value in x,y,z. 

1847 Only called internally by HARKinterpolator4D.__call__ (etc). 

1848 """ 

1849 w_pos = self.wSearchFunc(self.w_list, w) 

1850 w_pos[w_pos < 1] = 1 

1851 w_pos[w_pos > self.w_n - 1] = self.w_n - 1 

1852 x_pos = self.xSearchFunc(self.x_list, x) 

1853 x_pos[x_pos < 1] = 1 

1854 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1855 y_pos = self.ySearchFunc(self.y_list, y) 

1856 y_pos[y_pos < 1] = 1 

1857 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1858 z_pos = self.zSearchFunc(self.z_list, z) 

1859 z_pos[z_pos < 1] = 1 

1860 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1861 i = w_pos # for convenience 

1862 j = x_pos 

1863 k = y_pos 

1864 l = z_pos 

1865 alpha = (w - self.w_list[i - 1]) / (self.w_list[i] - self.w_list[i - 1]) 

1866 beta = (x - self.x_list[j - 1]) / (self.x_list[j] - self.x_list[j - 1]) 

1867 gamma = (y - self.y_list[k - 1]) / (self.y_list[k] - self.y_list[k - 1]) 

1868 delta = (z - self.z_list[l - 1]) / (self.z_list[l] - self.z_list[l - 1]) 

1869 f = (1 - alpha) * ( 

1870 (1 - beta) 

1871 * ( 

1872 (1 - gamma) * (1 - delta) * self.f_values[i - 1, j - 1, k - 1, l - 1] 

1873 + (1 - gamma) * delta * self.f_values[i - 1, j - 1, k - 1, l] 

1874 + gamma * (1 - delta) * self.f_values[i - 1, j - 1, k, l - 1] 

1875 + gamma * delta * self.f_values[i - 1, j - 1, k, l] 

1876 ) 

1877 + beta 

1878 * ( 

1879 (1 - gamma) * (1 - delta) * self.f_values[i - 1, j, k - 1, l - 1] 

1880 + (1 - gamma) * delta * self.f_values[i - 1, j, k - 1, l] 

1881 + gamma * (1 - delta) * self.f_values[i - 1, j, k, l - 1] 

1882 + gamma * delta * self.f_values[i - 1, j, k, l] 

1883 ) 

1884 ) + alpha * ( 

1885 (1 - beta) 

1886 * ( 

1887 (1 - gamma) * (1 - delta) * self.f_values[i, j - 1, k - 1, l - 1] 

1888 + (1 - gamma) * delta * self.f_values[i, j - 1, k - 1, l] 

1889 + gamma * (1 - delta) * self.f_values[i, j - 1, k, l - 1] 

1890 + gamma * delta * self.f_values[i, j - 1, k, l] 

1891 ) 

1892 + beta 

1893 * ( 

1894 (1 - gamma) * (1 - delta) * self.f_values[i, j, k - 1, l - 1] 

1895 + (1 - gamma) * delta * self.f_values[i, j, k - 1, l] 

1896 + gamma * (1 - delta) * self.f_values[i, j, k, l - 1] 

1897 + gamma * delta * self.f_values[i, j, k, l] 

1898 ) 

1899 ) 

1900 return f 

1901 

1902 def _derW(self, w, x, y, z): 

1903 """ 

1904 Returns the derivative with respect to w of the interpolated function 

1905 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeW. 

1906 """ 

1907 w_pos = self.wSearchFunc(self.w_list, w) 

1908 w_pos[w_pos < 1] = 1 

1909 w_pos[w_pos > self.w_n - 1] = self.w_n - 1 

1910 x_pos = self.xSearchFunc(self.x_list, x) 

1911 x_pos[x_pos < 1] = 1 

1912 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1913 y_pos = self.ySearchFunc(self.y_list, y) 

1914 y_pos[y_pos < 1] = 1 

1915 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1916 z_pos = self.zSearchFunc(self.z_list, z) 

1917 z_pos[z_pos < 1] = 1 

1918 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1919 i = w_pos # for convenience 

1920 j = x_pos 

1921 k = y_pos 

1922 l = z_pos 

1923 beta = (x - self.x_list[j - 1]) / (self.x_list[j] - self.x_list[j - 1]) 

1924 gamma = (y - self.y_list[k - 1]) / (self.y_list[k] - self.y_list[k - 1]) 

1925 delta = (z - self.z_list[l - 1]) / (self.z_list[l] - self.z_list[l - 1]) 

1926 dfdw = ( 

1927 ( 

1928 (1 - beta) 

1929 * (1 - gamma) 

1930 * (1 - delta) 

1931 * self.f_values[i, j - 1, k - 1, l - 1] 

1932 + (1 - beta) * (1 - gamma) * delta * self.f_values[i, j - 1, k - 1, l] 

1933 + (1 - beta) * gamma * (1 - delta) * self.f_values[i, j - 1, k, l - 1] 

1934 + (1 - beta) * gamma * delta * self.f_values[i, j - 1, k, l] 

1935 + beta * (1 - gamma) * (1 - delta) * self.f_values[i, j, k - 1, l - 1] 

1936 + beta * (1 - gamma) * delta * self.f_values[i, j, k - 1, l] 

1937 + beta * gamma * (1 - delta) * self.f_values[i, j, k, l - 1] 

1938 + beta * gamma * delta * self.f_values[i, j, k, l] 

1939 ) 

1940 - ( 

1941 (1 - beta) 

1942 * (1 - gamma) 

1943 * (1 - delta) 

1944 * self.f_values[i - 1, j - 1, k - 1, l - 1] 

1945 + (1 - beta) 

1946 * (1 - gamma) 

1947 * delta 

1948 * self.f_values[i - 1, j - 1, k - 1, l] 

1949 + (1 - beta) 

1950 * gamma 

1951 * (1 - delta) 

1952 * self.f_values[i - 1, j - 1, k, l - 1] 

1953 + (1 - beta) * gamma * delta * self.f_values[i - 1, j - 1, k, l] 

1954 + beta 

1955 * (1 - gamma) 

1956 * (1 - delta) 

1957 * self.f_values[i - 1, j, k - 1, l - 1] 

1958 + beta * (1 - gamma) * delta * self.f_values[i - 1, j, k - 1, l] 

1959 + beta * gamma * (1 - delta) * self.f_values[i - 1, j, k, l - 1] 

1960 + beta * gamma * delta * self.f_values[i - 1, j, k, l] 

1961 ) 

1962 ) / (self.w_list[i] - self.w_list[i - 1]) 

1963 return dfdw 

1964 

1965 def _derX(self, w, x, y, z): 

1966 """ 

1967 Returns the derivative with respect to x of the interpolated function 

1968 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeX. 

1969 """ 

1970 w_pos = self.wSearchFunc(self.w_list, w) 

1971 w_pos[w_pos < 1] = 1 

1972 w_pos[w_pos > self.w_n - 1] = self.w_n - 1 

1973 x_pos = self.xSearchFunc(self.x_list, x) 

1974 x_pos[x_pos < 1] = 1 

1975 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

1976 y_pos = self.ySearchFunc(self.y_list, y) 

1977 y_pos[y_pos < 1] = 1 

1978 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

1979 z_pos = self.zSearchFunc(self.z_list, z) 

1980 z_pos[z_pos < 1] = 1 

1981 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

1982 i = w_pos # for convenience 

1983 j = x_pos 

1984 k = y_pos 

1985 l = z_pos 

1986 alpha = (w - self.w_list[i - 1]) / (self.w_list[i] - self.w_list[i - 1]) 

1987 gamma = (y - self.y_list[k - 1]) / (self.y_list[k] - self.y_list[k - 1]) 

1988 delta = (z - self.z_list[l - 1]) / (self.z_list[l] - self.z_list[l - 1]) 

1989 dfdx = ( 

1990 ( 

1991 (1 - alpha) 

1992 * (1 - gamma) 

1993 * (1 - delta) 

1994 * self.f_values[i - 1, j, k - 1, l - 1] 

1995 + (1 - alpha) * (1 - gamma) * delta * self.f_values[i - 1, j, k - 1, l] 

1996 + (1 - alpha) * gamma * (1 - delta) * self.f_values[i - 1, j, k, l - 1] 

1997 + (1 - alpha) * gamma * delta * self.f_values[i - 1, j, k, l] 

1998 + alpha * (1 - gamma) * (1 - delta) * self.f_values[i, j, k - 1, l - 1] 

1999 + alpha * (1 - gamma) * delta * self.f_values[i, j, k - 1, l] 

2000 + alpha * gamma * (1 - delta) * self.f_values[i, j, k, l - 1] 

2001 + alpha * gamma * delta * self.f_values[i, j, k, l] 

2002 ) 

2003 - ( 

2004 (1 - alpha) 

2005 * (1 - gamma) 

2006 * (1 - delta) 

2007 * self.f_values[i - 1, j - 1, k - 1, l - 1] 

2008 + (1 - alpha) 

2009 * (1 - gamma) 

2010 * delta 

2011 * self.f_values[i - 1, j - 1, k - 1, l] 

2012 + (1 - alpha) 

2013 * gamma 

2014 * (1 - delta) 

2015 * self.f_values[i - 1, j - 1, k, l - 1] 

2016 + (1 - alpha) * gamma * delta * self.f_values[i - 1, j - 1, k, l] 

2017 + alpha 

2018 * (1 - gamma) 

2019 * (1 - delta) 

2020 * self.f_values[i, j - 1, k - 1, l - 1] 

2021 + alpha * (1 - gamma) * delta * self.f_values[i, j - 1, k - 1, l] 

2022 + alpha * gamma * (1 - delta) * self.f_values[i, j - 1, k, l - 1] 

2023 + alpha * gamma * delta * self.f_values[i, j - 1, k, l] 

2024 ) 

2025 ) / (self.x_list[j] - self.x_list[j - 1]) 

2026 return dfdx 

2027 

2028 def _derY(self, w, x, y, z): 

2029 """ 

2030 Returns the derivative with respect to y of the interpolated function 

2031 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeY. 

2032 """ 

2033 w_pos = self.wSearchFunc(self.w_list, w) 

2034 w_pos[w_pos < 1] = 1 

2035 w_pos[w_pos > self.w_n - 1] = self.w_n - 1 

2036 x_pos = self.xSearchFunc(self.x_list, x) 

2037 x_pos[x_pos < 1] = 1 

2038 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

2039 y_pos = self.ySearchFunc(self.y_list, y) 

2040 y_pos[y_pos < 1] = 1 

2041 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

2042 z_pos = self.zSearchFunc(self.z_list, z) 

2043 z_pos[z_pos < 1] = 1 

2044 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

2045 i = w_pos # for convenience 

2046 j = x_pos 

2047 k = y_pos 

2048 l = z_pos 

2049 alpha = (w - self.w_list[i - 1]) / (self.w_list[i] - self.w_list[i - 1]) 

2050 beta = (x - self.x_list[j - 1]) / (self.x_list[j] - self.x_list[j - 1]) 

2051 delta = (z - self.z_list[l - 1]) / (self.z_list[l] - self.z_list[l - 1]) 

2052 dfdy = ( 

2053 ( 

2054 (1 - alpha) 

2055 * (1 - beta) 

2056 * (1 - delta) 

2057 * self.f_values[i - 1, j - 1, k, l - 1] 

2058 + (1 - alpha) * (1 - beta) * delta * self.f_values[i - 1, j - 1, k, l] 

2059 + (1 - alpha) * beta * (1 - delta) * self.f_values[i - 1, j, k, l - 1] 

2060 + (1 - alpha) * beta * delta * self.f_values[i - 1, j, k, l] 

2061 + alpha * (1 - beta) * (1 - delta) * self.f_values[i, j - 1, k, l - 1] 

2062 + alpha * (1 - beta) * delta * self.f_values[i, j - 1, k, l] 

2063 + alpha * beta * (1 - delta) * self.f_values[i, j, k, l - 1] 

2064 + alpha * beta * delta * self.f_values[i, j, k, l] 

2065 ) 

2066 - ( 

2067 (1 - alpha) 

2068 * (1 - beta) 

2069 * (1 - delta) 

2070 * self.f_values[i - 1, j - 1, k - 1, l - 1] 

2071 + (1 - alpha) 

2072 * (1 - beta) 

2073 * delta 

2074 * self.f_values[i - 1, j - 1, k - 1, l] 

2075 + (1 - alpha) 

2076 * beta 

2077 * (1 - delta) 

2078 * self.f_values[i - 1, j, k - 1, l - 1] 

2079 + (1 - alpha) * beta * delta * self.f_values[i - 1, j, k - 1, l] 

2080 + alpha 

2081 * (1 - beta) 

2082 * (1 - delta) 

2083 * self.f_values[i, j - 1, k - 1, l - 1] 

2084 + alpha * (1 - beta) * delta * self.f_values[i, j - 1, k - 1, l] 

2085 + alpha * beta * (1 - delta) * self.f_values[i, j, k - 1, l - 1] 

2086 + alpha * beta * delta * self.f_values[i, j, k - 1, l] 

2087 ) 

2088 ) / (self.y_list[k] - self.y_list[k - 1]) 

2089 return dfdy 

2090 

2091 def _derZ(self, w, x, y, z): 

2092 """ 

2093 Returns the derivative with respect to z of the interpolated function 

2094 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeZ. 

2095 """ 

2096 w_pos = self.wSearchFunc(self.w_list, w) 

2097 w_pos[w_pos < 1] = 1 

2098 w_pos[w_pos > self.w_n - 1] = self.w_n - 1 

2099 x_pos = self.xSearchFunc(self.x_list, x) 

2100 x_pos[x_pos < 1] = 1 

2101 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

2102 y_pos = self.ySearchFunc(self.y_list, y) 

2103 y_pos[y_pos < 1] = 1 

2104 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

2105 z_pos = self.zSearchFunc(self.z_list, z) 

2106 z_pos[z_pos < 1] = 1 

2107 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

2108 i = w_pos # for convenience 

2109 j = x_pos 

2110 k = y_pos 

2111 l = z_pos 

2112 alpha = (w - self.w_list[i - 1]) / (self.w_list[i] - self.w_list[i - 1]) 

2113 beta = (x - self.x_list[j - 1]) / (self.x_list[j] - self.x_list[j - 1]) 

2114 gamma = (y - self.y_list[k - 1]) / (self.y_list[k] - self.y_list[k - 1]) 

2115 dfdz = ( 

2116 ( 

2117 (1 - alpha) 

2118 * (1 - beta) 

2119 * (1 - gamma) 

2120 * self.f_values[i - 1, j - 1, k - 1, l] 

2121 + (1 - alpha) * (1 - beta) * gamma * self.f_values[i - 1, j - 1, k, l] 

2122 + (1 - alpha) * beta * (1 - gamma) * self.f_values[i - 1, j, k - 1, l] 

2123 + (1 - alpha) * beta * gamma * self.f_values[i - 1, j, k, l] 

2124 + alpha * (1 - beta) * (1 - gamma) * self.f_values[i, j - 1, k - 1, l] 

2125 + alpha * (1 - beta) * gamma * self.f_values[i, j - 1, k, l] 

2126 + alpha * beta * (1 - gamma) * self.f_values[i, j, k - 1, l] 

2127 + alpha * beta * gamma * self.f_values[i, j, k, l] 

2128 ) 

2129 - ( 

2130 (1 - alpha) 

2131 * (1 - beta) 

2132 * (1 - gamma) 

2133 * self.f_values[i - 1, j - 1, k - 1, l - 1] 

2134 + (1 - alpha) 

2135 * (1 - beta) 

2136 * gamma 

2137 * self.f_values[i - 1, j - 1, k, l - 1] 

2138 + (1 - alpha) 

2139 * beta 

2140 * (1 - gamma) 

2141 * self.f_values[i - 1, j, k - 1, l - 1] 

2142 + (1 - alpha) * beta * gamma * self.f_values[i - 1, j, k, l - 1] 

2143 + alpha 

2144 * (1 - beta) 

2145 * (1 - gamma) 

2146 * self.f_values[i, j - 1, k - 1, l - 1] 

2147 + alpha * (1 - beta) * gamma * self.f_values[i, j - 1, k, l - 1] 

2148 + alpha * beta * (1 - gamma) * self.f_values[i, j, k - 1, l - 1] 

2149 + alpha * beta * gamma * self.f_values[i, j, k, l - 1] 

2150 ) 

2151 ) / (self.z_list[l] - self.z_list[l - 1]) 

2152 return dfdz 

2153 

2154 

2155class LowerEnvelope(HARKinterpolator1D): 

2156 """ 

2157 The lower envelope of a finite set of 1D functions, each of which can be of 

2158 any class that has the methods __call__, derivative, and eval_with_derivative. 

2159 Generally: it combines HARKinterpolator1Ds. 

2160 

2161 Parameters 

2162 ---------- 

2163 *functions : function 

2164 Any number of real functions; often instances of HARKinterpolator1D 

2165 nan_bool : boolean 

2166 An indicator for whether the solver should exclude NA's when 

2167 forming the lower envelope 

2168 """ 

2169 

2170 distance_criteria = ["functions"] 

2171 

2172 def __init__(self, *functions, nan_bool=True): 

2173 if nan_bool: 

2174 self.compare = np.nanmin 

2175 self.argcompare = np.nanargmin 

2176 else: 

2177 self.compare = np.min 

2178 self.argcompare = np.argmin 

2179 

2180 self.functions = [] 

2181 for function in functions: 

2182 self.functions.append(function) 

2183 self.funcCount = len(self.functions) 

2184 

2185 def _evaluate(self, x): 

2186 """ 

2187 Returns the level of the function at each value in x as the minimum among 

2188 all of the functions. Only called internally by HARKinterpolator1D.__call__. 

2189 """ 

2190 m = len(x) 

2191 fx = np.zeros((m, self.funcCount)) 

2192 for j in range(self.funcCount): 

2193 fx[:, j] = self.functions[j](x) 

2194 y = self.compare(fx, axis=1) 

2195 return y 

2196 

2197 def _der(self, x): 

2198 """ 

2199 Returns the first derivative of the function at each value in x. Only 

2200 called internally by HARKinterpolator1D.derivative. 

2201 """ 

2202 y, dydx = self._evalAndDer(x) 

2203 return dydx # Sadly, this is the fastest / most convenient way... 

2204 

2205 def _evalAndDer(self, x): 

2206 """ 

2207 Returns the level and first derivative of the function at each value in 

2208 x. Only called internally by HARKinterpolator1D.eval_and_der. 

2209 """ 

2210 m = len(x) 

2211 fx = np.zeros((m, self.funcCount)) 

2212 for j in range(self.funcCount): 

2213 fx[:, j] = self.functions[j](x) 

2214 i = self.argcompare(fx, axis=1) 

2215 y = fx[np.arange(m), i] 

2216 dydx = np.zeros_like(y) 

2217 for j in np.unique(i): 

2218 c = i == j 

2219 dydx[c] = self.functions[j].derivative(x[c]) 

2220 return y, dydx 

2221 

2222 

2223class UpperEnvelope(HARKinterpolator1D): 

2224 """ 

2225 The upper envelope of a finite set of 1D functions, each of which can be of 

2226 any class that has the methods __call__, derivative, and eval_with_derivative. 

2227 Generally: it combines HARKinterpolator1Ds. 

2228 

2229 Parameters 

2230 ---------- 

2231 *functions : function 

2232 Any number of real functions; often instances of HARKinterpolator1D 

2233 nan_bool : boolean 

2234 An indicator for whether the solver should exclude NA's when forming 

2235 the lower envelope. 

2236 """ 

2237 

2238 distance_criteria = ["functions"] 

2239 

2240 def __init__(self, *functions, nan_bool=True): 

2241 if nan_bool: 

2242 self.compare = np.nanmax 

2243 self.argcompare = np.nanargmax 

2244 else: 

2245 self.compare = np.max 

2246 self.argcompare = np.argmax 

2247 self.functions = [] 

2248 for function in functions: 

2249 self.functions.append(function) 

2250 self.funcCount = len(self.functions) 

2251 

2252 def _evaluate(self, x): 

2253 """ 

2254 Returns the level of the function at each value in x as the maximum among 

2255 all of the functions. Only called internally by HARKinterpolator1D.__call__. 

2256 """ 

2257 m = len(x) 

2258 fx = np.zeros((m, self.funcCount)) 

2259 for j in range(self.funcCount): 

2260 fx[:, j] = self.functions[j](x) 

2261 y = self.compare(fx, axis=1) 

2262 return y 

2263 

2264 def _der(self, x): 

2265 """ 

2266 Returns the first derivative of the function at each value in x. Only 

2267 called internally by HARKinterpolator1D.derivative. 

2268 """ 

2269 y, dydx = self._evalAndDer(x) 

2270 return dydx # Sadly, this is the fastest / most convenient way... 

2271 

2272 def _evalAndDer(self, x): 

2273 """ 

2274 Returns the level and first derivative of the function at each value in 

2275 x. Only called internally by HARKinterpolator1D.eval_and_der. 

2276 """ 

2277 m = len(x) 

2278 fx = np.zeros((m, self.funcCount)) 

2279 for j in range(self.funcCount): 

2280 fx[:, j] = self.functions[j](x) 

2281 i = self.argcompare(fx, axis=1) 

2282 y = fx[np.arange(m), i] 

2283 dydx = np.zeros_like(y) 

2284 for j in np.unique(i): 

2285 c = i == j 

2286 dydx[c] = self.functions[j].derivative(x[c]) 

2287 return y, dydx 

2288 

2289 

2290class LowerEnvelope2D(HARKinterpolator2D): 

2291 """ 

2292 The lower envelope of a finite set of 2D functions, each of which can be of 

2293 any class that has the methods __call__, derivativeX, and derivativeY. 

2294 Generally: it combines HARKinterpolator2Ds. 

2295 

2296 Parameters 

2297 ---------- 

2298 *functions : function 

2299 Any number of real functions; often instances of HARKinterpolator2D 

2300 nan_bool : boolean 

2301 An indicator for whether the solver should exclude NA's when forming 

2302 the lower envelope. 

2303 """ 

2304 

2305 distance_criteria = ["functions"] 

2306 

2307 def __init__(self, *functions, nan_bool=True): 

2308 if nan_bool: 

2309 self.compare = np.nanmin 

2310 self.argcompare = np.nanargmin 

2311 else: 

2312 self.compare = np.min 

2313 self.argcompare = np.argmin 

2314 self.functions = [] 

2315 for function in functions: 

2316 self.functions.append(function) 

2317 self.funcCount = len(self.functions) 

2318 

2319 def _evaluate(self, x, y): 

2320 """ 

2321 Returns the level of the function at each value in (x,y) as the minimum 

2322 among all of the functions. Only called internally by 

2323 HARKinterpolator2D.__call__. 

2324 """ 

2325 m = len(x) 

2326 temp = np.zeros((m, self.funcCount)) 

2327 for j in range(self.funcCount): 

2328 temp[:, j] = self.functions[j](x, y) 

2329 f = self.compare(temp, axis=1) 

2330 return f 

2331 

2332 def _derX(self, x, y): 

2333 """ 

2334 Returns the first derivative of the function with respect to X at each 

2335 value in (x,y). Only called internally by HARKinterpolator2D._derX. 

2336 """ 

2337 m = len(x) 

2338 temp = np.zeros((m, self.funcCount)) 

2339 for j in range(self.funcCount): 

2340 temp[:, j] = self.functions[j](x, y) 

2341 i = self.argcompare(temp, axis=1) 

2342 dfdx = np.zeros_like(x) 

2343 for j in np.unique(i): 

2344 c = i == j 

2345 dfdx[c] = self.functions[j].derivativeX(x[c], y[c]) 

2346 return dfdx 

2347 

2348 def _derY(self, x, y): 

2349 """ 

2350 Returns the first derivative of the function with respect to Y at each 

2351 value in (x,y). Only called internally by HARKinterpolator2D._derY. 

2352 """ 

2353 m = len(x) 

2354 temp = np.zeros((m, self.funcCount)) 

2355 for j in range(self.funcCount): 

2356 temp[:, j] = self.functions[j](x, y) 

2357 i = self.argcompare(temp, axis=1) 

2358 y = temp[np.arange(m), i] 

2359 dfdy = np.zeros_like(x) 

2360 for j in np.unique(i): 

2361 c = i == j 

2362 dfdy[c] = self.functions[j].derivativeY(x[c], y[c]) 

2363 return dfdy 

2364 

2365 

2366class LowerEnvelope3D(HARKinterpolator3D): 

2367 """ 

2368 The lower envelope of a finite set of 3D functions, each of which can be of 

2369 any class that has the methods __call__, derivativeX, derivativeY, and 

2370 derivativeZ. Generally: it combines HARKinterpolator2Ds. 

2371 

2372 Parameters 

2373 ---------- 

2374 *functions : function 

2375 Any number of real functions; often instances of HARKinterpolator3D 

2376 nan_bool : boolean 

2377 An indicator for whether the solver should exclude NA's when forming 

2378 the lower envelope. 

2379 """ 

2380 

2381 distance_criteria = ["functions"] 

2382 

2383 def __init__(self, *functions, nan_bool=True): 

2384 if nan_bool: 

2385 self.compare = np.nanmin 

2386 self.argcompare = np.nanargmin 

2387 else: 

2388 self.compare = np.min 

2389 self.argcompare = np.argmin 

2390 self.functions = [] 

2391 for function in functions: 

2392 self.functions.append(function) 

2393 self.funcCount = len(self.functions) 

2394 

2395 def _evaluate(self, x, y, z): 

2396 """ 

2397 Returns the level of the function at each value in (x,y,z) as the minimum 

2398 among all of the functions. Only called internally by 

2399 HARKinterpolator3D.__call__. 

2400 """ 

2401 m = len(x) 

2402 temp = np.zeros((m, self.funcCount)) 

2403 for j in range(self.funcCount): 

2404 temp[:, j] = self.functions[j](x, y, z) 

2405 f = self.compare(temp, axis=1) 

2406 return f 

2407 

2408 def _derX(self, x, y, z): 

2409 """ 

2410 Returns the first derivative of the function with respect to X at each 

2411 value in (x,y,z). Only called internally by HARKinterpolator3D._derX. 

2412 """ 

2413 m = len(x) 

2414 temp = np.zeros((m, self.funcCount)) 

2415 for j in range(self.funcCount): 

2416 temp[:, j] = self.functions[j](x, y, z) 

2417 i = self.argcompare(temp, axis=1) 

2418 dfdx = np.zeros_like(x) 

2419 for j in np.unique(i): 

2420 c = i == j 

2421 dfdx[c] = self.functions[j].derivativeX(x[c], y[c], z[c]) 

2422 return dfdx 

2423 

2424 def _derY(self, x, y, z): 

2425 """ 

2426 Returns the first derivative of the function with respect to Y at each 

2427 value in (x,y,z). Only called internally by HARKinterpolator3D._derY. 

2428 """ 

2429 m = len(x) 

2430 temp = np.zeros((m, self.funcCount)) 

2431 for j in range(self.funcCount): 

2432 temp[:, j] = self.functions[j](x, y, z) 

2433 i = self.argcompare(temp, axis=1) 

2434 y = temp[np.arange(m), i] 

2435 dfdy = np.zeros_like(x) 

2436 for j in np.unique(i): 

2437 c = i == j 

2438 dfdy[c] = self.functions[j].derivativeY(x[c], y[c], z[c]) 

2439 return dfdy 

2440 

2441 def _derZ(self, x, y, z): 

2442 """ 

2443 Returns the first derivative of the function with respect to Z at each 

2444 value in (x,y,z). Only called internally by HARKinterpolator3D._derZ. 

2445 """ 

2446 m = len(x) 

2447 temp = np.zeros((m, self.funcCount)) 

2448 for j in range(self.funcCount): 

2449 temp[:, j] = self.functions[j](x, y, z) 

2450 i = self.argcompare(temp, axis=1) 

2451 y = temp[np.arange(m), i] 

2452 dfdz = np.zeros_like(x) 

2453 for j in np.unique(i): 

2454 c = i == j 

2455 dfdz[c] = self.functions[j].derivativeZ(x[c], y[c], z[c]) 

2456 return dfdz 

2457 

2458 

2459class VariableLowerBoundFunc2D(MetricObject): 

2460 """ 

2461 A class for representing a function with two real inputs whose lower bound 

2462 in the first input depends on the second input. Useful for managing curved 

2463 natural borrowing constraints, as occurs in the persistent shocks model. 

2464 

2465 Parameters 

2466 ---------- 

2467 func : function 

2468 A function f: (R_+ x R) --> R representing the function of interest 

2469 shifted by its lower bound in the first input. 

2470 lowerBound : function 

2471 The lower bound in the first input of the function of interest, as 

2472 a function of the second input. 

2473 """ 

2474 

2475 distance_criteria = ["func", "lowerBound"] 

2476 

2477 def __init__(self, func, lowerBound): 

2478 self.func = func 

2479 self.lowerBound = lowerBound 

2480 

2481 def __call__(self, x, y): 

2482 """ 

2483 Evaluate the function at given state space points. 

2484 

2485 Parameters 

2486 ---------- 

2487 x : np.array 

2488 First input values. 

2489 y : np.array 

2490 Second input values; should be of same shape as x. 

2491 

2492 Returns 

2493 ------- 

2494 f_out : np.array 

2495 Function evaluated at (x,y), of same shape as inputs. 

2496 """ 

2497 xShift = self.lowerBound(y) 

2498 f_out = self.func(x - xShift, y) 

2499 return f_out 

2500 

2501 def derivativeX(self, x, y): 

2502 """ 

2503 Evaluate the first derivative with respect to x of the function at given 

2504 state space points. 

2505 

2506 Parameters 

2507 ---------- 

2508 x : np.array 

2509 First input values. 

2510 y : np.array 

2511 Second input values; should be of same shape as x. 

2512 

2513 Returns 

2514 ------- 

2515 dfdx_out : np.array 

2516 First derivative of function with respect to the first input, 

2517 evaluated at (x,y), of same shape as inputs. 

2518 """ 

2519 xShift = self.lowerBound(y) 

2520 dfdx_out = self.func.derivativeX(x - xShift, y) 

2521 return dfdx_out 

2522 

2523 def derivativeY(self, x, y): 

2524 """ 

2525 Evaluate the first derivative with respect to y of the function at given 

2526 state space points. 

2527 

2528 Parameters 

2529 ---------- 

2530 x : np.array 

2531 First input values. 

2532 y : np.array 

2533 Second input values; should be of same shape as x. 

2534 

2535 Returns 

2536 ------- 

2537 dfdy_out : np.array 

2538 First derivative of function with respect to the second input, 

2539 evaluated at (x,y), of same shape as inputs. 

2540 """ 

2541 xShift, xShiftDer = self.lowerBound.eval_with_derivative(y) 

2542 dfdy_out = self.func.derivativeY( 

2543 x - xShift, y 

2544 ) - xShiftDer * self.func.derivativeX(x - xShift, y) 

2545 return dfdy_out 

2546 

2547 

2548class VariableLowerBoundFunc3D(MetricObject): 

2549 """ 

2550 A class for representing a function with three real inputs whose lower bound 

2551 in the first input depends on the second input. Useful for managing curved 

2552 natural borrowing constraints. 

2553 

2554 Parameters 

2555 ---------- 

2556 func : function 

2557 A function f: (R_+ x R^2) --> R representing the function of interest 

2558 shifted by its lower bound in the first input. 

2559 lowerBound : function 

2560 The lower bound in the first input of the function of interest, as 

2561 a function of the second input. 

2562 """ 

2563 

2564 distance_criteria = ["func", "lowerBound"] 

2565 

2566 def __init__(self, func, lowerBound): 

2567 self.func = func 

2568 self.lowerBound = lowerBound 

2569 

2570 def __call__(self, x, y, z): 

2571 """ 

2572 Evaluate the function at given state space points. 

2573 

2574 Parameters 

2575 ---------- 

2576 x : np.array 

2577 First input values. 

2578 y : np.array 

2579 Second input values; should be of same shape as x. 

2580 z : np.array 

2581 Third input values; should be of same shape as x. 

2582 

2583 Returns 

2584 ------- 

2585 f_out : np.array 

2586 Function evaluated at (x,y,z), of same shape as inputs. 

2587 """ 

2588 xShift = self.lowerBound(y) 

2589 f_out = self.func(x - xShift, y, z) 

2590 return f_out 

2591 

2592 def derivativeX(self, x, y, z): 

2593 """ 

2594 Evaluate the first derivative with respect to x of the function at given 

2595 state space points. 

2596 

2597 Parameters 

2598 ---------- 

2599 x : np.array 

2600 First input values. 

2601 y : np.array 

2602 Second input values; should be of same shape as x. 

2603 z : np.array 

2604 Third input values; should be of same shape as x. 

2605 

2606 Returns 

2607 ------- 

2608 dfdx_out : np.array 

2609 First derivative of function with respect to the first input, 

2610 evaluated at (x,y,z), of same shape as inputs. 

2611 """ 

2612 xShift = self.lowerBound(y) 

2613 dfdx_out = self.func.derivativeX(x - xShift, y, z) 

2614 return dfdx_out 

2615 

2616 def derivativeY(self, x, y, z): 

2617 """ 

2618 Evaluate the first derivative with respect to y of the function at given 

2619 state space points. 

2620 

2621 Parameters 

2622 ---------- 

2623 x : np.array 

2624 First input values. 

2625 y : np.array 

2626 Second input values; should be of same shape as x. 

2627 z : np.array 

2628 Third input values; should be of same shape as x. 

2629 

2630 Returns 

2631 ------- 

2632 dfdy_out : np.array 

2633 First derivative of function with respect to the second input, 

2634 evaluated at (x,y,z), of same shape as inputs. 

2635 """ 

2636 xShift, xShiftDer = self.lowerBound.eval_with_derivative(y) 

2637 dfdy_out = self.func.derivativeY( 

2638 x - xShift, y, z 

2639 ) - xShiftDer * self.func.derivativeX(x - xShift, y, z) 

2640 return dfdy_out 

2641 

2642 def derivativeZ(self, x, y, z): 

2643 """ 

2644 Evaluate the first derivative with respect to z of the function at given 

2645 state space points. 

2646 

2647 Parameters 

2648 ---------- 

2649 x : np.array 

2650 First input values. 

2651 y : np.array 

2652 Second input values; should be of same shape as x. 

2653 z : np.array 

2654 Third input values; should be of same shape as x. 

2655 

2656 Returns 

2657 ------- 

2658 dfdz_out : np.array 

2659 First derivative of function with respect to the third input, 

2660 evaluated at (x,y,z), of same shape as inputs. 

2661 """ 

2662 xShift = self.lowerBound(y) 

2663 dfdz_out = self.func.derivativeZ(x - xShift, y, z) 

2664 return dfdz_out 

2665 

2666 

2667class LinearInterpOnInterp1D(HARKinterpolator2D): 

2668 """ 

2669 A 2D interpolator that linearly interpolates among a list of 1D interpolators. 

2670 

2671 Parameters 

2672 ---------- 

2673 xInterpolators : [HARKinterpolator1D] 

2674 A list of 1D interpolations over the x variable. The nth element of 

2675 xInterpolators represents f(x,y_values[n]). 

2676 y_values: numpy.array 

2677 An array of y values equal in length to xInterpolators. 

2678 """ 

2679 

2680 distance_criteria = ["xInterpolators", "y_list"] 

2681 

2682 def __init__(self, xInterpolators, y_values): 

2683 self.xInterpolators = xInterpolators 

2684 self.y_list = y_values 

2685 self.y_n = y_values.size 

2686 

2687 def _evaluate(self, x, y): 

2688 """ 

2689 Returns the level of the interpolated function at each value in x,y. 

2690 Only called internally by HARKinterpolator2D.__call__ (etc). 

2691 """ 

2692 m = len(x) 

2693 y_pos = np.searchsorted(self.y_list, y) 

2694 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

2695 y_pos[y_pos < 1] = 1 

2696 f = np.zeros(m) + np.nan 

2697 if y.size > 0: 

2698 for i in range(1, self.y_n): 

2699 c = y_pos == i 

2700 if np.any(c): 

2701 alpha = (y[c] - self.y_list[i - 1]) / ( 

2702 self.y_list[i] - self.y_list[i - 1] 

2703 ) 

2704 f[c] = (1 - alpha) * self.xInterpolators[i - 1]( 

2705 x[c] 

2706 ) + alpha * self.xInterpolators[i](x[c]) 

2707 return f 

2708 

2709 def _derX(self, x, y): 

2710 """ 

2711 Returns the derivative with respect to x of the interpolated function 

2712 at each value in x,y. Only called internally by HARKinterpolator2D.derivativeX. 

2713 """ 

2714 m = len(x) 

2715 y_pos = np.searchsorted(self.y_list, y) 

2716 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

2717 y_pos[y_pos < 1] = 1 

2718 dfdx = np.zeros(m) + np.nan 

2719 if y.size > 0: 

2720 for i in range(1, self.y_n): 

2721 c = y_pos == i 

2722 if np.any(c): 

2723 alpha = (y[c] - self.y_list[i - 1]) / ( 

2724 self.y_list[i] - self.y_list[i - 1] 

2725 ) 

2726 dfdx[c] = (1 - alpha) * self.xInterpolators[i - 1]._der( 

2727 x[c] 

2728 ) + alpha * self.xInterpolators[i]._der(x[c]) 

2729 return dfdx 

2730 

2731 def _derY(self, x, y): 

2732 """ 

2733 Returns the derivative with respect to y of the interpolated function 

2734 at each value in x,y. Only called internally by HARKinterpolator2D.derivativeY. 

2735 """ 

2736 m = len(x) 

2737 y_pos = np.searchsorted(self.y_list, y) 

2738 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

2739 y_pos[y_pos < 1] = 1 

2740 dfdy = np.zeros(m) + np.nan 

2741 if y.size > 0: 

2742 for i in range(1, self.y_n): 

2743 c = y_pos == i 

2744 if np.any(c): 

2745 dfdy[c] = ( 

2746 self.xInterpolators[i](x[c]) - self.xInterpolators[i - 1](x[c]) 

2747 ) / (self.y_list[i] - self.y_list[i - 1]) 

2748 return dfdy 

2749 

2750 

2751class BilinearInterpOnInterp1D(HARKinterpolator3D): 

2752 """ 

2753 A 3D interpolator that bilinearly interpolates among a list of lists of 1D 

2754 interpolators. 

2755 

2756 Constructor for the class, generating an approximation to a function of 

2757 the form f(x,y,z) using interpolations over f(x,y_0,z_0) for a fixed grid 

2758 of y_0 and z_0 values. 

2759 

2760 Parameters 

2761 ---------- 

2762 xInterpolators : [[HARKinterpolator1D]] 

2763 A list of lists of 1D interpolations over the x variable. The i,j-th 

2764 element of xInterpolators represents f(x,y_values[i],z_values[j]). 

2765 y_values: numpy.array 

2766 An array of y values equal in length to xInterpolators. 

2767 z_values: numpy.array 

2768 An array of z values equal in length to xInterpolators[0]. 

2769 """ 

2770 

2771 distance_criteria = ["xInterpolators", "y_list", "z_list"] 

2772 

2773 def __init__(self, xInterpolators, y_values, z_values): 

2774 self.xInterpolators = xInterpolators 

2775 self.y_list = y_values 

2776 self.y_n = y_values.size 

2777 self.z_list = z_values 

2778 self.z_n = z_values.size 

2779 

2780 def _evaluate(self, x, y, z): 

2781 """ 

2782 Returns the level of the interpolated function at each value in x,y,z. 

2783 Only called internally by HARKinterpolator3D.__call__ (etc). 

2784 

2785 Optimized to avoid nested loops by processing all unique (i,j) combinations 

2786 with vectorized operations. 

2787 """ 

2788 m = len(x) 

2789 y_pos = np.searchsorted(self.y_list, y) 

2790 y_pos = np.clip(y_pos, 1, self.y_n - 1) 

2791 z_pos = np.searchsorted(self.z_list, z) 

2792 z_pos = np.clip(z_pos, 1, self.z_n - 1) 

2793 

2794 f = np.full(m, np.nan) 

2795 

2796 # Find unique combinations of (y_pos, z_pos) to avoid redundant computations 

2797 unique_pairs = np.unique(np.column_stack((y_pos, z_pos)), axis=0) 

2798 

2799 for i, j in unique_pairs: 

2800 c = (i == y_pos) & (j == z_pos) 

2801 alpha = (y[c] - self.y_list[i - 1]) / (self.y_list[i] - self.y_list[i - 1]) 

2802 beta = (z[c] - self.z_list[j - 1]) / (self.z_list[j] - self.z_list[j - 1]) 

2803 f[c] = ( 

2804 (1 - alpha) * (1 - beta) * self.xInterpolators[i - 1][j - 1](x[c]) 

2805 + (1 - alpha) * beta * self.xInterpolators[i - 1][j](x[c]) 

2806 + alpha * (1 - beta) * self.xInterpolators[i][j - 1](x[c]) 

2807 + alpha * beta * self.xInterpolators[i][j](x[c]) 

2808 ) 

2809 return f 

2810 

2811 def _derX(self, x, y, z): 

2812 """ 

2813 Returns the derivative with respect to x of the interpolated function 

2814 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeX. 

2815 

2816 Optimized to avoid nested loops by processing unique (i,j) combinations. 

2817 """ 

2818 m = len(x) 

2819 y_pos = np.searchsorted(self.y_list, y) 

2820 y_pos = np.clip(y_pos, 1, self.y_n - 1) 

2821 z_pos = np.searchsorted(self.z_list, z) 

2822 z_pos = np.clip(z_pos, 1, self.z_n - 1) 

2823 

2824 dfdx = np.full(m, np.nan) 

2825 

2826 # Find unique combinations to avoid redundant computations 

2827 unique_pairs = np.unique(np.column_stack((y_pos, z_pos)), axis=0) 

2828 

2829 for i, j in unique_pairs: 

2830 c = (i == y_pos) & (j == z_pos) 

2831 alpha = (y[c] - self.y_list[i - 1]) / (self.y_list[i] - self.y_list[i - 1]) 

2832 beta = (z[c] - self.z_list[j - 1]) / (self.z_list[j] - self.z_list[j - 1]) 

2833 dfdx[c] = ( 

2834 (1 - alpha) * (1 - beta) * self.xInterpolators[i - 1][j - 1]._der(x[c]) 

2835 + (1 - alpha) * beta * self.xInterpolators[i - 1][j]._der(x[c]) 

2836 + alpha * (1 - beta) * self.xInterpolators[i][j - 1]._der(x[c]) 

2837 + alpha * beta * self.xInterpolators[i][j]._der(x[c]) 

2838 ) 

2839 return dfdx 

2840 

2841 def _derY(self, x, y, z): 

2842 """ 

2843 Returns the derivative with respect to y of the interpolated function 

2844 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeY. 

2845 

2846 Optimized to avoid nested loops by processing unique (i,j) combinations. 

2847 """ 

2848 m = len(x) 

2849 y_pos = np.searchsorted(self.y_list, y) 

2850 y_pos = np.clip(y_pos, 1, self.y_n - 1) 

2851 z_pos = np.searchsorted(self.z_list, z) 

2852 z_pos = np.clip(z_pos, 1, self.z_n - 1) 

2853 

2854 dfdy = np.full(m, np.nan) 

2855 

2856 # Find unique combinations to avoid redundant computations 

2857 unique_pairs = np.unique(np.column_stack((y_pos, z_pos)), axis=0) 

2858 

2859 for i, j in unique_pairs: 

2860 c = (i == y_pos) & (j == z_pos) 

2861 beta = (z[c] - self.z_list[j - 1]) / (self.z_list[j] - self.z_list[j - 1]) 

2862 dfdy[c] = ( 

2863 ( 

2864 (1 - beta) * self.xInterpolators[i][j - 1](x[c]) 

2865 + beta * self.xInterpolators[i][j](x[c]) 

2866 ) 

2867 - ( 

2868 (1 - beta) * self.xInterpolators[i - 1][j - 1](x[c]) 

2869 + beta * self.xInterpolators[i - 1][j](x[c]) 

2870 ) 

2871 ) / (self.y_list[i] - self.y_list[i - 1]) 

2872 return dfdy 

2873 

2874 def _derZ(self, x, y, z): 

2875 """ 

2876 Returns the derivative with respect to z of the interpolated function 

2877 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeZ. 

2878 

2879 Optimized to avoid nested loops by processing unique (i,j) combinations. 

2880 """ 

2881 m = len(x) 

2882 y_pos = np.searchsorted(self.y_list, y) 

2883 y_pos = np.clip(y_pos, 1, self.y_n - 1) 

2884 z_pos = np.searchsorted(self.z_list, z) 

2885 z_pos = np.clip(z_pos, 1, self.z_n - 1) 

2886 

2887 dfdz = np.full(m, np.nan) 

2888 

2889 # Find unique combinations to avoid redundant computations 

2890 unique_pairs = np.unique(np.column_stack((y_pos, z_pos)), axis=0) 

2891 

2892 for i, j in unique_pairs: 

2893 c = (i == y_pos) & (j == z_pos) 

2894 alpha = (y[c] - self.y_list[i - 1]) / (self.y_list[i] - self.y_list[i - 1]) 

2895 dfdz[c] = ( 

2896 ( 

2897 (1 - alpha) * self.xInterpolators[i - 1][j](x[c]) 

2898 + alpha * self.xInterpolators[i][j](x[c]) 

2899 ) 

2900 - ( 

2901 (1 - alpha) * self.xInterpolators[i - 1][j - 1](x[c]) 

2902 + alpha * self.xInterpolators[i][j - 1](x[c]) 

2903 ) 

2904 ) / (self.z_list[j] - self.z_list[j - 1]) 

2905 return dfdz 

2906 

2907 

2908class TrilinearInterpOnInterp1D(HARKinterpolator4D): 

2909 """ 

2910 A 4D interpolator that trilinearly interpolates among a list of lists of 1D interpolators. 

2911 

2912 Constructor for the class, generating an approximation to a function of 

2913 the form f(w,x,y,z) using interpolations over f(w,x_0,y_0,z_0) for a fixed 

2914 grid of y_0 and z_0 values. 

2915 

2916 Parameters 

2917 ---------- 

2918 wInterpolators : [[[HARKinterpolator1D]]] 

2919 A list of lists of lists of 1D interpolations over the x variable. 

2920 The i,j,k-th element of wInterpolators represents f(w,x_values[i],y_values[j],z_values[k]). 

2921 x_values: numpy.array 

2922 An array of x values equal in length to wInterpolators. 

2923 y_values: numpy.array 

2924 An array of y values equal in length to wInterpolators[0]. 

2925 z_values: numpy.array 

2926 An array of z values equal in length to wInterpolators[0][0] 

2927 """ 

2928 

2929 distance_criteria = ["wInterpolators", "x_list", "y_list", "z_list"] 

2930 

2931 def __init__(self, wInterpolators, x_values, y_values, z_values): 

2932 self.wInterpolators = wInterpolators 

2933 self.x_list = x_values 

2934 self.x_n = x_values.size 

2935 self.y_list = y_values 

2936 self.y_n = y_values.size 

2937 self.z_list = z_values 

2938 self.z_n = z_values.size 

2939 

2940 def _evaluate(self, w, x, y, z): 

2941 """ 

2942 Returns the level of the interpolated function at each value in w,x,y,z. 

2943 Only called internally by HARKinterpolator4D.__call__ (etc). 

2944 """ 

2945 m = len(x) 

2946 x_pos = np.searchsorted(self.x_list, x) 

2947 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

2948 y_pos = np.searchsorted(self.y_list, y) 

2949 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

2950 y_pos[y_pos < 1] = 1 

2951 z_pos = np.searchsorted(self.z_list, z) 

2952 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

2953 z_pos[z_pos < 1] = 1 

2954 f = np.zeros(m) + np.nan 

2955 for i in range(1, self.x_n): 

2956 for j in range(1, self.y_n): 

2957 for k in range(1, self.z_n): 

2958 c = np.logical_and( 

2959 np.logical_and(i == x_pos, j == y_pos), k == z_pos 

2960 ) 

2961 if np.any(c): 

2962 alpha = (x[c] - self.x_list[i - 1]) / ( 

2963 self.x_list[i] - self.x_list[i - 1] 

2964 ) 

2965 beta = (y[c] - self.y_list[j - 1]) / ( 

2966 self.y_list[j] - self.y_list[j - 1] 

2967 ) 

2968 gamma = (z[c] - self.z_list[k - 1]) / ( 

2969 self.z_list[k] - self.z_list[k - 1] 

2970 ) 

2971 f[c] = ( 

2972 (1 - alpha) 

2973 * (1 - beta) 

2974 * (1 - gamma) 

2975 * self.wInterpolators[i - 1][j - 1][k - 1](w[c]) 

2976 + (1 - alpha) 

2977 * (1 - beta) 

2978 * gamma 

2979 * self.wInterpolators[i - 1][j - 1][k](w[c]) 

2980 + (1 - alpha) 

2981 * beta 

2982 * (1 - gamma) 

2983 * self.wInterpolators[i - 1][j][k - 1](w[c]) 

2984 + (1 - alpha) 

2985 * beta 

2986 * gamma 

2987 * self.wInterpolators[i - 1][j][k](w[c]) 

2988 + alpha 

2989 * (1 - beta) 

2990 * (1 - gamma) 

2991 * self.wInterpolators[i][j - 1][k - 1](w[c]) 

2992 + alpha 

2993 * (1 - beta) 

2994 * gamma 

2995 * self.wInterpolators[i][j - 1][k](w[c]) 

2996 + alpha 

2997 * beta 

2998 * (1 - gamma) 

2999 * self.wInterpolators[i][j][k - 1](w[c]) 

3000 + alpha * beta * gamma * self.wInterpolators[i][j][k](w[c]) 

3001 ) 

3002 return f 

3003 

3004 def _derW(self, w, x, y, z): 

3005 """ 

3006 Returns the derivative with respect to w of the interpolated function 

3007 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeW. 

3008 """ 

3009 m = len(x) 

3010 x_pos = np.searchsorted(self.x_list, x) 

3011 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

3012 y_pos = np.searchsorted(self.y_list, y) 

3013 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3014 y_pos[y_pos < 1] = 1 

3015 z_pos = np.searchsorted(self.z_list, z) 

3016 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3017 z_pos[z_pos < 1] = 1 

3018 dfdw = np.zeros(m) + np.nan 

3019 for i in range(1, self.x_n): 

3020 for j in range(1, self.y_n): 

3021 for k in range(1, self.z_n): 

3022 c = np.logical_and( 

3023 np.logical_and(i == x_pos, j == y_pos), k == z_pos 

3024 ) 

3025 if np.any(c): 

3026 alpha = (x[c] - self.x_list[i - 1]) / ( 

3027 self.x_list[i] - self.x_list[i - 1] 

3028 ) 

3029 beta = (y[c] - self.y_list[j - 1]) / ( 

3030 self.y_list[j] - self.y_list[j - 1] 

3031 ) 

3032 gamma = (z[c] - self.z_list[k - 1]) / ( 

3033 self.z_list[k] - self.z_list[k - 1] 

3034 ) 

3035 dfdw[c] = ( 

3036 (1 - alpha) 

3037 * (1 - beta) 

3038 * (1 - gamma) 

3039 * self.wInterpolators[i - 1][j - 1][k - 1]._der(w[c]) 

3040 + (1 - alpha) 

3041 * (1 - beta) 

3042 * gamma 

3043 * self.wInterpolators[i - 1][j - 1][k]._der(w[c]) 

3044 + (1 - alpha) 

3045 * beta 

3046 * (1 - gamma) 

3047 * self.wInterpolators[i - 1][j][k - 1]._der(w[c]) 

3048 + (1 - alpha) 

3049 * beta 

3050 * gamma 

3051 * self.wInterpolators[i - 1][j][k]._der(w[c]) 

3052 + alpha 

3053 * (1 - beta) 

3054 * (1 - gamma) 

3055 * self.wInterpolators[i][j - 1][k - 1]._der(w[c]) 

3056 + alpha 

3057 * (1 - beta) 

3058 * gamma 

3059 * self.wInterpolators[i][j - 1][k]._der(w[c]) 

3060 + alpha 

3061 * beta 

3062 * (1 - gamma) 

3063 * self.wInterpolators[i][j][k - 1]._der(w[c]) 

3064 + alpha 

3065 * beta 

3066 * gamma 

3067 * self.wInterpolators[i][j][k]._der(w[c]) 

3068 ) 

3069 return dfdw 

3070 

3071 def _derX(self, w, x, y, z): 

3072 """ 

3073 Returns the derivative with respect to x of the interpolated function 

3074 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeX. 

3075 """ 

3076 m = len(x) 

3077 x_pos = np.searchsorted(self.x_list, x) 

3078 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

3079 y_pos = np.searchsorted(self.y_list, y) 

3080 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3081 y_pos[y_pos < 1] = 1 

3082 z_pos = np.searchsorted(self.z_list, z) 

3083 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3084 z_pos[z_pos < 1] = 1 

3085 dfdx = np.zeros(m) + np.nan 

3086 for i in range(1, self.x_n): 

3087 for j in range(1, self.y_n): 

3088 for k in range(1, self.z_n): 

3089 c = np.logical_and( 

3090 np.logical_and(i == x_pos, j == y_pos), k == z_pos 

3091 ) 

3092 if np.any(c): 

3093 beta = (y[c] - self.y_list[j - 1]) / ( 

3094 self.y_list[j] - self.y_list[j - 1] 

3095 ) 

3096 gamma = (z[c] - self.z_list[k - 1]) / ( 

3097 self.z_list[k] - self.z_list[k - 1] 

3098 ) 

3099 dfdx[c] = ( 

3100 ( 

3101 (1 - beta) 

3102 * (1 - gamma) 

3103 * self.wInterpolators[i][j - 1][k - 1](w[c]) 

3104 + (1 - beta) 

3105 * gamma 

3106 * self.wInterpolators[i][j - 1][k](w[c]) 

3107 + beta 

3108 * (1 - gamma) 

3109 * self.wInterpolators[i][j][k - 1](w[c]) 

3110 + beta * gamma * self.wInterpolators[i][j][k](w[c]) 

3111 ) 

3112 - ( 

3113 (1 - beta) 

3114 * (1 - gamma) 

3115 * self.wInterpolators[i - 1][j - 1][k - 1](w[c]) 

3116 + (1 - beta) 

3117 * gamma 

3118 * self.wInterpolators[i - 1][j - 1][k](w[c]) 

3119 + beta 

3120 * (1 - gamma) 

3121 * self.wInterpolators[i - 1][j][k - 1](w[c]) 

3122 + beta * gamma * self.wInterpolators[i - 1][j][k](w[c]) 

3123 ) 

3124 ) / (self.x_list[i] - self.x_list[i - 1]) 

3125 return dfdx 

3126 

3127 def _derY(self, w, x, y, z): 

3128 """ 

3129 Returns the derivative with respect to y of the interpolated function 

3130 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeY. 

3131 """ 

3132 m = len(x) 

3133 x_pos = np.searchsorted(self.x_list, x) 

3134 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

3135 y_pos = np.searchsorted(self.y_list, y) 

3136 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3137 y_pos[y_pos < 1] = 1 

3138 z_pos = np.searchsorted(self.z_list, z) 

3139 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3140 z_pos[z_pos < 1] = 1 

3141 dfdy = np.zeros(m) + np.nan 

3142 for i in range(1, self.x_n): 

3143 for j in range(1, self.y_n): 

3144 for k in range(1, self.z_n): 

3145 c = np.logical_and( 

3146 np.logical_and(i == x_pos, j == y_pos), k == z_pos 

3147 ) 

3148 if np.any(c): 

3149 alpha = (x[c] - self.x_list[i - 1]) / ( 

3150 self.x_list[i] - self.x_list[i - 1] 

3151 ) 

3152 gamma = (z[c] - self.z_list[k - 1]) / ( 

3153 self.z_list[k] - self.z_list[k - 1] 

3154 ) 

3155 dfdy[c] = ( 

3156 ( 

3157 (1 - alpha) 

3158 * (1 - gamma) 

3159 * self.wInterpolators[i - 1][j][k - 1](w[c]) 

3160 + (1 - alpha) 

3161 * gamma 

3162 * self.wInterpolators[i - 1][j][k](w[c]) 

3163 + alpha 

3164 * (1 - gamma) 

3165 * self.wInterpolators[i][j][k - 1](w[c]) 

3166 + alpha * gamma * self.wInterpolators[i][j][k](w[c]) 

3167 ) 

3168 - ( 

3169 (1 - alpha) 

3170 * (1 - gamma) 

3171 * self.wInterpolators[i - 1][j - 1][k - 1](w[c]) 

3172 + (1 - alpha) 

3173 * gamma 

3174 * self.wInterpolators[i - 1][j - 1][k](w[c]) 

3175 + alpha 

3176 * (1 - gamma) 

3177 * self.wInterpolators[i][j - 1][k - 1](w[c]) 

3178 + alpha * gamma * self.wInterpolators[i][j - 1][k](w[c]) 

3179 ) 

3180 ) / (self.y_list[j] - self.y_list[j - 1]) 

3181 return dfdy 

3182 

3183 def _derZ(self, w, x, y, z): 

3184 """ 

3185 Returns the derivative with respect to z of the interpolated function 

3186 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeZ. 

3187 """ 

3188 m = len(x) 

3189 x_pos = np.searchsorted(self.x_list, x) 

3190 x_pos[x_pos > self.x_n - 1] = self.x_n - 1 

3191 y_pos = np.searchsorted(self.y_list, y) 

3192 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3193 y_pos[y_pos < 1] = 1 

3194 z_pos = np.searchsorted(self.z_list, z) 

3195 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3196 z_pos[z_pos < 1] = 1 

3197 dfdz = np.zeros(m) + np.nan 

3198 for i in range(1, self.x_n): 

3199 for j in range(1, self.y_n): 

3200 for k in range(1, self.z_n): 

3201 c = np.logical_and( 

3202 np.logical_and(i == x_pos, j == y_pos), k == z_pos 

3203 ) 

3204 if np.any(c): 

3205 alpha = (x[c] - self.x_list[i - 1]) / ( 

3206 self.x_list[i] - self.x_list[i - 1] 

3207 ) 

3208 beta = (y[c] - self.y_list[j - 1]) / ( 

3209 self.y_list[j] - self.y_list[j - 1] 

3210 ) 

3211 dfdz[c] = ( 

3212 ( 

3213 (1 - alpha) 

3214 * (1 - beta) 

3215 * self.wInterpolators[i - 1][j - 1][k](w[c]) 

3216 + (1 - alpha) 

3217 * beta 

3218 * self.wInterpolators[i - 1][j][k](w[c]) 

3219 + alpha 

3220 * (1 - beta) 

3221 * self.wInterpolators[i][j - 1][k](w[c]) 

3222 + alpha * beta * self.wInterpolators[i][j][k](w[c]) 

3223 ) 

3224 - ( 

3225 (1 - alpha) 

3226 * (1 - beta) 

3227 * self.wInterpolators[i - 1][j - 1][k - 1](w[c]) 

3228 + (1 - alpha) 

3229 * beta 

3230 * self.wInterpolators[i - 1][j][k - 1](w[c]) 

3231 + alpha 

3232 * (1 - beta) 

3233 * self.wInterpolators[i][j - 1][k - 1](w[c]) 

3234 + alpha * beta * self.wInterpolators[i][j][k - 1](w[c]) 

3235 ) 

3236 ) / (self.z_list[k] - self.z_list[k - 1]) 

3237 return dfdz 

3238 

3239 

3240class LinearInterpOnInterp2D(HARKinterpolator3D): 

3241 """ 

3242 A 3D interpolation method that linearly interpolates between "layers" of 

3243 arbitrary 2D interpolations. Useful for models with two endogenous state 

3244 variables and one exogenous state variable when solving with the endogenous 

3245 grid method. NOTE: should not be used if an exogenous 3D grid is used, will 

3246 be significantly slower than TrilinearInterp. 

3247 

3248 Constructor for the class, generating an approximation to a function of 

3249 the form f(x,y,z) using interpolations over f(x,y,z_0) for a fixed grid 

3250 of z_0 values. 

3251 

3252 Parameters 

3253 ---------- 

3254 xyInterpolators : [HARKinterpolator2D] 

3255 A list of 2D interpolations over the x and y variables. The nth 

3256 element of xyInterpolators represents f(x,y,z_values[n]). 

3257 z_values: numpy.array 

3258 An array of z values equal in length to xyInterpolators. 

3259 """ 

3260 

3261 distance_criteria = ["xyInterpolators", "z_list"] 

3262 

3263 def __init__(self, xyInterpolators, z_values): 

3264 self.xyInterpolators = xyInterpolators 

3265 self.z_list = z_values 

3266 self.z_n = z_values.size 

3267 

3268 def _evaluate(self, x, y, z): 

3269 """ 

3270 Returns the level of the interpolated function at each value in x,y,z. 

3271 Only called internally by HARKinterpolator3D.__call__ (etc). 

3272 """ 

3273 m = len(x) 

3274 z_pos = np.searchsorted(self.z_list, z) 

3275 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3276 z_pos[z_pos < 1] = 1 

3277 f = np.zeros(m) + np.nan 

3278 if x.size > 0: 

3279 for i in range(1, self.z_n): 

3280 c = z_pos == i 

3281 if np.any(c): 

3282 alpha = (z[c] - self.z_list[i - 1]) / ( 

3283 self.z_list[i] - self.z_list[i - 1] 

3284 ) 

3285 f[c] = (1 - alpha) * self.xyInterpolators[i - 1]( 

3286 x[c], y[c] 

3287 ) + alpha * self.xyInterpolators[i](x[c], y[c]) 

3288 return f 

3289 

3290 def _derX(self, x, y, z): 

3291 """ 

3292 Returns the derivative with respect to x of the interpolated function 

3293 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeX. 

3294 """ 

3295 m = len(x) 

3296 z_pos = np.searchsorted(self.z_list, z) 

3297 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3298 z_pos[z_pos < 1] = 1 

3299 dfdx = np.zeros(m) + np.nan 

3300 if x.size > 0: 

3301 for i in range(1, self.z_n): 

3302 c = z_pos == i 

3303 if np.any(c): 

3304 alpha = (z[c] - self.z_list[i - 1]) / ( 

3305 self.z_list[i] - self.z_list[i - 1] 

3306 ) 

3307 dfdx[c] = (1 - alpha) * self.xyInterpolators[i - 1].derivativeX( 

3308 x[c], y[c] 

3309 ) + alpha * self.xyInterpolators[i].derivativeX(x[c], y[c]) 

3310 return dfdx 

3311 

3312 def _derY(self, x, y, z): 

3313 """ 

3314 Returns the derivative with respect to y of the interpolated function 

3315 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeY. 

3316 """ 

3317 m = len(x) 

3318 z_pos = np.searchsorted(self.z_list, z) 

3319 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3320 z_pos[z_pos < 1] = 1 

3321 dfdy = np.zeros(m) + np.nan 

3322 if x.size > 0: 

3323 for i in range(1, self.z_n): 

3324 c = z_pos == i 

3325 if np.any(c): 

3326 alpha = (z[c] - self.z_list[i - 1]) / ( 

3327 self.z_list[i] - self.z_list[i - 1] 

3328 ) 

3329 dfdy[c] = (1 - alpha) * self.xyInterpolators[i - 1].derivativeY( 

3330 x[c], y[c] 

3331 ) + alpha * self.xyInterpolators[i].derivativeY(x[c], y[c]) 

3332 return dfdy 

3333 

3334 def _derZ(self, x, y, z): 

3335 """ 

3336 Returns the derivative with respect to z of the interpolated function 

3337 at each value in x,y,z. Only called internally by HARKinterpolator3D.derivativeZ. 

3338 """ 

3339 m = len(x) 

3340 z_pos = np.searchsorted(self.z_list, z) 

3341 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3342 z_pos[z_pos < 1] = 1 

3343 dfdz = np.zeros(m) + np.nan 

3344 if x.size > 0: 

3345 for i in range(1, self.z_n): 

3346 c = z_pos == i 

3347 if np.any(c): 

3348 dfdz[c] = ( 

3349 self.xyInterpolators[i](x[c], y[c]) 

3350 - self.xyInterpolators[i - 1](x[c], y[c]) 

3351 ) / (self.z_list[i] - self.z_list[i - 1]) 

3352 return dfdz 

3353 

3354 

3355class BilinearInterpOnInterp2D(HARKinterpolator4D): 

3356 """ 

3357 A 4D interpolation method that bilinearly interpolates among "layers" of 

3358 arbitrary 2D interpolations. Useful for models with two endogenous state 

3359 variables and two exogenous state variables when solving with the endogenous 

3360 grid method. NOTE: should not be used if an exogenous 4D grid is used, will 

3361 be significantly slower than QuadlinearInterp. 

3362 

3363 Constructor for the class, generating an approximation to a function of 

3364 the form f(w,x,y,z) using interpolations over f(w,x,y_0,z_0) for a fixed 

3365 grid of y_0 and z_0 values. 

3366 

3367 Parameters 

3368 ---------- 

3369 wxInterpolators : [[HARKinterpolator2D]] 

3370 A list of lists of 2D interpolations over the w and x variables. 

3371 The i,j-th element of wxInterpolators represents 

3372 f(w,x,y_values[i],z_values[j]). 

3373 y_values: numpy.array 

3374 An array of y values equal in length to wxInterpolators. 

3375 z_values: numpy.array 

3376 An array of z values equal in length to wxInterpolators[0]. 

3377 """ 

3378 

3379 distance_criteria = ["wxInterpolators", "y_list", "z_list"] 

3380 

3381 def __init__(self, wxInterpolators, y_values, z_values): 

3382 self.wxInterpolators = wxInterpolators 

3383 self.y_list = y_values 

3384 self.y_n = y_values.size 

3385 self.z_list = z_values 

3386 self.z_n = z_values.size 

3387 

3388 def _evaluate(self, w, x, y, z): 

3389 """ 

3390 Returns the level of the interpolated function at each value in x,y,z. 

3391 Only called internally by HARKinterpolator4D.__call__ (etc). 

3392 """ 

3393 m = len(x) 

3394 y_pos = np.searchsorted(self.y_list, y) 

3395 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3396 y_pos[y_pos < 1] = 1 

3397 z_pos = np.searchsorted(self.z_list, z) 

3398 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3399 z_pos[z_pos < 1] = 1 

3400 f = np.zeros(m) + np.nan 

3401 for i in range(1, self.y_n): 

3402 for j in range(1, self.z_n): 

3403 c = np.logical_and(i == y_pos, j == z_pos) 

3404 if np.any(c): 

3405 alpha = (y[c] - self.y_list[i - 1]) / ( 

3406 self.y_list[i] - self.y_list[i - 1] 

3407 ) 

3408 beta = (z[c] - self.z_list[j - 1]) / ( 

3409 self.z_list[j] - self.z_list[j - 1] 

3410 ) 

3411 f[c] = ( 

3412 (1 - alpha) 

3413 * (1 - beta) 

3414 * self.wxInterpolators[i - 1][j - 1](w[c], x[c]) 

3415 + (1 - alpha) 

3416 * beta 

3417 * self.wxInterpolators[i - 1][j](w[c], x[c]) 

3418 + alpha 

3419 * (1 - beta) 

3420 * self.wxInterpolators[i][j - 1](w[c], x[c]) 

3421 + alpha * beta * self.wxInterpolators[i][j](w[c], x[c]) 

3422 ) 

3423 return f 

3424 

3425 def _derW(self, w, x, y, z): 

3426 """ 

3427 Returns the derivative with respect to w of the interpolated function 

3428 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeW. 

3429 """ 

3430 # This may look strange, as we call the derivativeX() method to get the 

3431 # derivative with respect to w, but that's just a quirk of 4D interpolations 

3432 # beginning with w rather than x. The derivative wrt the first dimension 

3433 # of an element of wxInterpolators is the w-derivative of the main function. 

3434 m = len(x) 

3435 y_pos = np.searchsorted(self.y_list, y) 

3436 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3437 y_pos[y_pos < 1] = 1 

3438 z_pos = np.searchsorted(self.z_list, z) 

3439 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3440 z_pos[z_pos < 1] = 1 

3441 dfdw = np.zeros(m) + np.nan 

3442 for i in range(1, self.y_n): 

3443 for j in range(1, self.z_n): 

3444 c = np.logical_and(i == y_pos, j == z_pos) 

3445 if np.any(c): 

3446 alpha = (y[c] - self.y_list[i - 1]) / ( 

3447 self.y_list[i] - self.y_list[i - 1] 

3448 ) 

3449 beta = (z[c] - self.z_list[j - 1]) / ( 

3450 self.z_list[j] - self.z_list[j - 1] 

3451 ) 

3452 dfdw[c] = ( 

3453 (1 - alpha) 

3454 * (1 - beta) 

3455 * self.wxInterpolators[i - 1][j - 1].derivativeX(w[c], x[c]) 

3456 + (1 - alpha) 

3457 * beta 

3458 * self.wxInterpolators[i - 1][j].derivativeX(w[c], x[c]) 

3459 + alpha 

3460 * (1 - beta) 

3461 * self.wxInterpolators[i][j - 1].derivativeX(w[c], x[c]) 

3462 + alpha 

3463 * beta 

3464 * self.wxInterpolators[i][j].derivativeX(w[c], x[c]) 

3465 ) 

3466 return dfdw 

3467 

3468 def _derX(self, w, x, y, z): 

3469 """ 

3470 Returns the derivative with respect to x of the interpolated function 

3471 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeX. 

3472 """ 

3473 # This may look strange, as we call the derivativeY() method to get the 

3474 # derivative with respect to x, but that's just a quirk of 4D interpolations 

3475 # beginning with w rather than x. The derivative wrt the second dimension 

3476 # of an element of wxInterpolators is the x-derivative of the main function. 

3477 m = len(x) 

3478 y_pos = np.searchsorted(self.y_list, y) 

3479 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3480 y_pos[y_pos < 1] = 1 

3481 z_pos = np.searchsorted(self.z_list, z) 

3482 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3483 z_pos[z_pos < 1] = 1 

3484 dfdx = np.zeros(m) + np.nan 

3485 for i in range(1, self.y_n): 

3486 for j in range(1, self.z_n): 

3487 c = np.logical_and(i == y_pos, j == z_pos) 

3488 if np.any(c): 

3489 alpha = (y[c] - self.y_list[i - 1]) / ( 

3490 self.y_list[i] - self.y_list[i - 1] 

3491 ) 

3492 beta = (z[c] - self.z_list[j - 1]) / ( 

3493 self.z_list[j] - self.z_list[j - 1] 

3494 ) 

3495 dfdx[c] = ( 

3496 (1 - alpha) 

3497 * (1 - beta) 

3498 * self.wxInterpolators[i - 1][j - 1].derivativeY(w[c], x[c]) 

3499 + (1 - alpha) 

3500 * beta 

3501 * self.wxInterpolators[i - 1][j].derivativeY(w[c], x[c]) 

3502 + alpha 

3503 * (1 - beta) 

3504 * self.wxInterpolators[i][j - 1].derivativeY(w[c], x[c]) 

3505 + alpha 

3506 * beta 

3507 * self.wxInterpolators[i][j].derivativeY(w[c], x[c]) 

3508 ) 

3509 return dfdx 

3510 

3511 def _derY(self, w, x, y, z): 

3512 """ 

3513 Returns the derivative with respect to y of the interpolated function 

3514 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeY. 

3515 """ 

3516 m = len(x) 

3517 y_pos = np.searchsorted(self.y_list, y) 

3518 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3519 y_pos[y_pos < 1] = 1 

3520 z_pos = np.searchsorted(self.z_list, z) 

3521 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3522 z_pos[z_pos < 1] = 1 

3523 dfdy = np.zeros(m) + np.nan 

3524 for i in range(1, self.y_n): 

3525 for j in range(1, self.z_n): 

3526 c = np.logical_and(i == y_pos, j == z_pos) 

3527 if np.any(c): 

3528 beta = (z[c] - self.z_list[j - 1]) / ( 

3529 self.z_list[j] - self.z_list[j - 1] 

3530 ) 

3531 dfdy[c] = ( 

3532 ( 

3533 (1 - beta) * self.wxInterpolators[i][j - 1](w[c], x[c]) 

3534 + beta * self.wxInterpolators[i][j](w[c], x[c]) 

3535 ) 

3536 - ( 

3537 (1 - beta) * self.wxInterpolators[i - 1][j - 1](w[c], x[c]) 

3538 + beta * self.wxInterpolators[i - 1][j](w[c], x[c]) 

3539 ) 

3540 ) / (self.y_list[i] - self.y_list[i - 1]) 

3541 return dfdy 

3542 

3543 def _derZ(self, w, x, y, z): 

3544 """ 

3545 Returns the derivative with respect to z of the interpolated function 

3546 at each value in w,x,y,z. Only called internally by HARKinterpolator4D.derivativeZ. 

3547 """ 

3548 m = len(x) 

3549 y_pos = np.searchsorted(self.y_list, y) 

3550 y_pos[y_pos > self.y_n - 1] = self.y_n - 1 

3551 y_pos[y_pos < 1] = 1 

3552 z_pos = np.searchsorted(self.z_list, z) 

3553 z_pos[z_pos > self.z_n - 1] = self.z_n - 1 

3554 z_pos[z_pos < 1] = 1 

3555 dfdz = np.zeros(m) + np.nan 

3556 for i in range(1, self.y_n): 

3557 for j in range(1, self.z_n): 

3558 c = np.logical_and(i == y_pos, j == z_pos) 

3559 if np.any(c): 

3560 alpha = (y[c] - self.y_list[i - 1]) / ( 

3561 self.y_list[i] - self.y_list[i - 1] 

3562 ) 

3563 dfdz[c] = ( 

3564 ( 

3565 (1 - alpha) * self.wxInterpolators[i - 1][j](w[c], x[c]) 

3566 + alpha * self.wxInterpolators[i][j](w[c], x[c]) 

3567 ) 

3568 - ( 

3569 (1 - alpha) * self.wxInterpolators[i - 1][j - 1](w[c], x[c]) 

3570 + alpha * self.wxInterpolators[i][j - 1](w[c], x[c]) 

3571 ) 

3572 ) / (self.z_list[j] - self.z_list[j - 1]) 

3573 return dfdz 

3574 

3575 

3576class Curvilinear2DInterp(HARKinterpolator2D): 

3577 """ 

3578 A 2D interpolation method for curvilinear or "warped grid" interpolation, as 

3579 in White (2015). Used for models with two endogenous states that are solved 

3580 with the endogenous grid method. Allows multiple function outputs, but all of 

3581 the interpolated functions must share a common curvilinear grid. 

3582 

3583 Parameters 

3584 ---------- 

3585 f_values: numpy.array or [numpy.array] 

3586 One or more 2D arrays of function values such that f_values[i,j] = 

3587 f(x_values[i,j],y_values[i,j]). 

3588 x_values: numpy.array 

3589 A 2D array of x values of the same shape as f_values. 

3590 y_values: numpy.array 

3591 A 2D array of y values of the same shape as f_values. 

3592 """ 

3593 

3594 distance_criteria = ["f_values", "x_values", "y_values"] 

3595 

3596 def __init__(self, f_values, x_values, y_values): 

3597 if isinstance(f_values, list): 

3598 N_funcs = len(f_values) 

3599 multi = True 

3600 else: 

3601 N_funcs = 1 

3602 multi = False 

3603 my_shape = x_values.shape 

3604 if not (my_shape == y_values.shape): 

3605 raise ValueError("y_values must have the same shape as x_values!") 

3606 if multi: 

3607 for n in range(N_funcs): 

3608 if not (my_shape == f_values[n].shape): 

3609 raise ValueError( 

3610 "Each element of f_values must have the same shape as x_values!" 

3611 ) 

3612 else: 

3613 if not (my_shape == f_values.shape): 

3614 raise ValueError("f_values must have the same shape as x_values!") 

3615 

3616 if multi: 

3617 self.f_values = f_values 

3618 else: 

3619 self.f_values = [f_values] 

3620 self.x_values = x_values 

3621 self.y_values = y_values 

3622 self.x_n = my_shape[0] 

3623 self.y_n = my_shape[1] 

3624 self.N_funcs = N_funcs 

3625 self.multi = multi 

3626 self.update_polarity() 

3627 

3628 def __call__(self, x, y): 

3629 """ 

3630 Modification of HARKinterpolator2D.__call__ to account for multiple outputs. 

3631 """ 

3632 xa = np.asarray(x) 

3633 ya = np.asarray(y) 

3634 S = xa.shape 

3635 fa = self._evaluate(xa.flatten(), ya.flatten()) 

3636 output = [fa[n].reshape(S) for n in range(self.N_funcs)] 

3637 if self.multi: 

3638 return output 

3639 else: 

3640 return output[0] 

3641 

3642 def derivativeX(self, x, y): 

3643 """ 

3644 Modification of HARKinterpolator2D.derivativeX to account for multiple outputs. 

3645 """ 

3646 xa = np.asarray(x) 

3647 ya = np.asarray(y) 

3648 S = xa.shape 

3649 dfdxa = self._derX(xa.flatten(), ya.flatten()) 

3650 output = [dfdxa[n].reshape(S) for n in range(self.N_funcs)] 

3651 if self.multi: 

3652 return output 

3653 else: 

3654 return output[0] 

3655 

3656 def derivativeY(self, x, y): 

3657 """ 

3658 Modification of HARKinterpolator2D.derivativeY to account for multiple outputs. 

3659 """ 

3660 xa = np.asarray(x) 

3661 ya = np.asarray(y) 

3662 S = xa.shape 

3663 dfdya = self._derY(xa.flatten(), ya.flatten()) 

3664 output = [dfdya[n].reshape(S) for n in range(self.N_funcs)] 

3665 if self.multi: 

3666 return output 

3667 else: 

3668 return output[0] 

3669 

3670 def update_polarity(self): 

3671 """ 

3672 Fills in the polarity attribute of the interpolation, determining whether 

3673 the "plus" (True) or "minus" (False) solution of the system of equations 

3674 should be used for each sector. Needs to be called in __init__. 

3675 

3676 Parameters 

3677 ---------- 

3678 none 

3679 

3680 Returns 

3681 ------- 

3682 none 

3683 """ 

3684 # Grab a point known to be inside each sector: the midway point between 

3685 # the lower left and upper right vertex of each sector 

3686 x_temp = 0.5 * ( 

3687 self.x_values[0 : (self.x_n - 1), 0 : (self.y_n - 1)] 

3688 + self.x_values[1 : self.x_n, 1 : self.y_n] 

3689 ) 

3690 y_temp = 0.5 * ( 

3691 self.y_values[0 : (self.x_n - 1), 0 : (self.y_n - 1)] 

3692 + self.y_values[1 : self.x_n, 1 : self.y_n] 

3693 ) 

3694 size = (self.x_n - 1) * (self.y_n - 1) 

3695 x_temp = np.reshape(x_temp, size) 

3696 y_temp = np.reshape(y_temp, size) 

3697 y_pos = np.tile(np.arange(0, self.y_n - 1), self.x_n - 1) 

3698 x_pos = np.reshape( 

3699 np.tile(np.arange(0, self.x_n - 1), (self.y_n - 1, 1)).transpose(), size 

3700 ) 

3701 

3702 # Set the polarity of all sectors to "plus", then test each sector 

3703 self.polarity = np.ones((self.x_n - 1, self.y_n - 1), dtype=bool) 

3704 alpha, beta = self.find_coords(x_temp, y_temp, x_pos, y_pos) 

3705 polarity = np.logical_and( 

3706 np.logical_and(alpha > 0, alpha < 1), np.logical_and(beta > 0, beta < 1) 

3707 ) 

3708 

3709 # Update polarity: if (alpha,beta) not in the unit square, then that 

3710 # sector must use the "minus" solution instead 

3711 self.polarity = np.reshape(polarity, (self.x_n - 1, self.y_n - 1)) 

3712 

3713 def find_sector(self, x, y): 

3714 """ 

3715 Finds the quadrilateral "sector" for each (x,y) point in the input. 

3716 Only called as a subroutine of _evaluate(), etc. Uses a numba helper 

3717 function below to accelerate computation. 

3718 

3719 Parameters 

3720 ---------- 

3721 x : np.array 

3722 Values whose sector should be found. 

3723 y : np.array 

3724 Values whose sector should be found. Should be same size as x. 

3725 

3726 Returns 

3727 ------- 

3728 x_pos : np.array 

3729 Sector x-coordinates for each point of the input, of the same size. 

3730 y_pos : np.array 

3731 Sector y-coordinates for each point of the input, of the same size. 

3732 """ 

3733 x_pos, y_pos = find_sector_numba(x, y, self.x_values, self.y_values) 

3734 return x_pos, y_pos 

3735 

3736 def find_coords(self, x, y, x_pos, y_pos): 

3737 """ 

3738 Calculates the relative coordinates (alpha,beta) for each point (x,y), 

3739 given the sectors (x_pos,y_pos) in which they reside. Only called as 

3740 a subroutine of _evaluate(), etc. Uses a numba helper function to acc- 

3741 elerate computation, and has a "backup method" for when the math fails. 

3742 

3743 Parameters 

3744 ---------- 

3745 x : np.array 

3746 Values whose sector should be found. 

3747 y : np.array 

3748 Values whose sector should be found. Should be same size as x. 

3749 x_pos : np.array 

3750 Sector x-coordinates for each point in (x,y), of the same size. 

3751 y_pos : np.array 

3752 Sector y-coordinates for each point in (x,y), of the same size. 

3753 

3754 Returns 

3755 ------- 

3756 alpha : np.array 

3757 Relative "horizontal" position of the input in their respective sectors. 

3758 beta : np.array 

3759 Relative "vertical" position of the input in their respective sectors. 

3760 """ 

3761 alpha, beta = find_coords_numba( 

3762 x, y, x_pos, y_pos, self.x_values, self.y_values, self.polarity 

3763 ) 

3764 

3765 # Alternate method if there are sectors that are "too regular" 

3766 # These points weren't able to identify coordinates 

3767 z = np.logical_or(np.isnan(alpha), np.isnan(beta)) 

3768 if np.any(z): 

3769 ii = x_pos[z] 

3770 jj = y_pos[z] 

3771 xA = self.x_values[ii, jj] 

3772 xB = self.x_values[ii + 1, jj] 

3773 xC = self.x_values[ii, jj + 1] 

3774 xD = self.x_values[ii + 1, jj + 1] 

3775 yA = self.y_values[ii, jj] 

3776 yB = self.y_values[ii + 1, jj] 

3777 yC = self.y_values[ii, jj + 1] 

3778 # yD = self.y_values[ii + 1, jj + 1] 

3779 b = xB - xA 

3780 f = yB - yA 

3781 kappa = f / b 

3782 int_bot = yA - kappa * xA 

3783 int_top = yC - kappa * xC 

3784 int_these = y[z] - kappa * x[z] 

3785 beta_temp = (int_these - int_bot) / (int_top - int_bot) 

3786 x_left = beta_temp * xC + (1.0 - beta_temp) * xA 

3787 x_right = beta_temp * xD + (1.0 - beta_temp) * xB 

3788 alpha_temp = (x[z] - x_left) / (x_right - x_left) 

3789 beta[z] = beta_temp 

3790 alpha[z] = alpha_temp 

3791 

3792 return alpha, beta 

3793 

3794 def _evaluate(self, x, y): 

3795 """ 

3796 Returns the level of the interpolated function at each value in x,y. 

3797 Only called internally by __call__ (etc). 

3798 """ 

3799 x_pos, y_pos = self.find_sector(x, y) 

3800 alpha, beta = self.find_coords(x, y, x_pos, y_pos) 

3801 

3802 # Get weights on each vertex 

3803 alpha_C = 1.0 - alpha 

3804 beta_C = 1.0 - beta 

3805 wA = alpha_C * beta_C 

3806 wB = alpha * beta_C 

3807 wC = alpha_C * beta 

3808 wD = alpha * beta 

3809 

3810 # Evaluate each function by bilinear interpolation 

3811 f = [] 

3812 for n in range(self.N_funcs): 

3813 f_n = ( 

3814 0.0 

3815 + wA * self.f_values[n][x_pos, y_pos] 

3816 + wB * self.f_values[n][x_pos + 1, y_pos] 

3817 + wC * self.f_values[n][x_pos, y_pos + 1] 

3818 + wD * self.f_values[n][x_pos + 1, y_pos + 1] 

3819 ) 

3820 f.append(f_n) 

3821 return f 

3822 

3823 def _derX(self, x, y): 

3824 """ 

3825 Returns the derivative with respect to x of the interpolated function 

3826 at each value in x,y. Only called internally by derivativeX. 

3827 """ 

3828 x_pos, y_pos = self.find_sector(x, y) 

3829 alpha, beta = self.find_coords(x, y, x_pos, y_pos) 

3830 

3831 # Get four corners data for each point 

3832 xA = self.x_values[x_pos, y_pos] 

3833 xB = self.x_values[x_pos + 1, y_pos] 

3834 xC = self.x_values[x_pos, y_pos + 1] 

3835 xD = self.x_values[x_pos + 1, y_pos + 1] 

3836 yA = self.y_values[x_pos, y_pos] 

3837 yB = self.y_values[x_pos + 1, y_pos] 

3838 yC = self.y_values[x_pos, y_pos + 1] 

3839 yD = self.y_values[x_pos + 1, y_pos + 1] 

3840 

3841 # Calculate components of the alpha,beta --> x,y delta translation matrix 

3842 alpha_C = 1 - alpha 

3843 beta_C = 1 - beta 

3844 alpha_x = beta_C * (xB - xA) + beta * (xD - xC) 

3845 alpha_y = beta_C * (yB - yA) + beta * (yD - yC) 

3846 beta_x = alpha_C * (xC - xA) + alpha * (xD - xB) 

3847 beta_y = alpha_C * (yC - yA) + alpha * (yD - yB) 

3848 

3849 # Invert the delta translation matrix into x,y --> alpha,beta 

3850 det = alpha_x * beta_y - beta_x * alpha_y 

3851 x_alpha = beta_y / det 

3852 x_beta = -alpha_y / det 

3853 

3854 # Calculate the derivative of f w.r.t. alpha and beta for each function 

3855 dfdx = [] 

3856 for n in range(self.N_funcs): 

3857 fA = self.f_values[n][x_pos, y_pos] 

3858 fB = self.f_values[n][x_pos + 1, y_pos] 

3859 fC = self.f_values[n][x_pos, y_pos + 1] 

3860 fD = self.f_values[n][x_pos + 1, y_pos + 1] 

3861 dfda = beta_C * (fB - fA) + beta * (fD - fC) 

3862 dfdb = alpha_C * (fC - fA) + alpha * (fD - fB) 

3863 

3864 # Calculate the derivative with respect to x 

3865 dfdx_n = x_alpha * dfda + x_beta * dfdb 

3866 dfdx.append(dfdx_n) 

3867 return dfdx 

3868 

3869 def _derY(self, x, y): 

3870 """ 

3871 Returns the derivative with respect to y of the interpolated function 

3872 at each value in x,y. Only called internally by derivativeY. 

3873 """ 

3874 x_pos, y_pos = self.find_sector(x, y) 

3875 alpha, beta = self.find_coords(x, y, x_pos, y_pos) 

3876 

3877 # Get four corners data for each point 

3878 xA = self.x_values[x_pos, y_pos] 

3879 xB = self.x_values[x_pos + 1, y_pos] 

3880 xC = self.x_values[x_pos, y_pos + 1] 

3881 xD = self.x_values[x_pos + 1, y_pos + 1] 

3882 yA = self.y_values[x_pos, y_pos] 

3883 yB = self.y_values[x_pos + 1, y_pos] 

3884 yC = self.y_values[x_pos, y_pos + 1] 

3885 yD = self.y_values[x_pos + 1, y_pos + 1] 

3886 

3887 # Calculate components of the alpha,beta --> x,y delta translation matrix 

3888 alpha_C = 1 - alpha 

3889 beta_C = 1 - beta 

3890 alpha_x = beta_C * (xB - xA) + beta * (xD - xC) 

3891 alpha_y = beta_C * (yB - yA) + beta * (yD - yC) 

3892 beta_x = alpha_C * (xC - xA) + alpha * (xD - xB) 

3893 beta_y = alpha_C * (yC - yA) + alpha * (yD - yB) 

3894 

3895 # Invert the delta translation matrix into x,y --> alpha,beta 

3896 det = alpha_x * beta_y - beta_x * alpha_y 

3897 y_alpha = -beta_x / det 

3898 y_beta = alpha_x / det 

3899 

3900 # Calculate the derivative of f w.r.t. alpha and beta for each function 

3901 dfdy = [] 

3902 for n in range(self.N_funcs): 

3903 fA = self.f_values[n][x_pos, y_pos] 

3904 fB = self.f_values[n][x_pos + 1, y_pos] 

3905 fC = self.f_values[n][x_pos, y_pos + 1] 

3906 fD = self.f_values[n][x_pos + 1, y_pos + 1] 

3907 dfda = beta_C * (fB - fA) + beta * (fD - fC) 

3908 dfdb = alpha_C * (fC - fA) + alpha * (fD - fB) 

3909 

3910 # Calculate the derivative with respect to y 

3911 dfdy_n = y_alpha * dfda + y_beta * dfdb 

3912 dfdy.append(dfdy_n) 

3913 return dfdy 

3914 

3915 

3916# Define a function that checks whether a set of points violates a linear boundary 

3917# defined by (x1,y1) and (x2,y2), where the latter is *COUNTER CLOCKWISE* from the 

3918# former. Returns 1 if the point is outside the boundary and 0 otherwise. 

3919@njit 

3920def boundary_check(xq, yq, x1, y1, x2, y2): # pragma: no cover 

3921 return int((y2 - y1) * xq - (x2 - x1) * yq > x1 * y2 - y1 * x2) 

3922 

3923 

3924# Define a numba helper function for finding the sector in the irregular grid 

3925@njit 

3926def find_sector_numba(X_query, Y_query, X_values, Y_values): # pragma: no cover 

3927 # Initialize the sector guess 

3928 M = X_query.size 

3929 x_n = X_values.shape[0] 

3930 y_n = X_values.shape[1] 

3931 ii = int(x_n / 2) 

3932 jj = int(y_n / 2) 

3933 top_ii = x_n - 2 

3934 top_jj = y_n - 2 

3935 

3936 # Initialize the output arrays 

3937 X_pos = np.empty(M, dtype=np.int32) 

3938 Y_pos = np.empty(M, dtype=np.int32) 

3939 

3940 # Identify the correct sector for each point to be evaluated 

3941 max_loops = x_n + y_n 

3942 for m in range(M): 

3943 found = False 

3944 loops = 0 

3945 while not found and loops < max_loops: 

3946 # Get coordinates for the four vertices: (xA,yA),...,(xD,yD) 

3947 x0 = X_query[m] 

3948 y0 = Y_query[m] 

3949 xA = X_values[ii, jj] 

3950 xB = X_values[ii + 1, jj] 

3951 xC = X_values[ii, jj + 1] 

3952 xD = X_values[ii + 1, jj + 1] 

3953 yA = Y_values[ii, jj] 

3954 yB = Y_values[ii + 1, jj] 

3955 yC = Y_values[ii, jj + 1] 

3956 yD = Y_values[ii + 1, jj + 1] 

3957 

3958 # Check the "bounding box" for the sector: is this guess plausible? 

3959 D = int(y0 < np.minimum(yA, yB)) 

3960 R = int(x0 > np.maximum(xB, xD)) 

3961 U = int(y0 > np.maximum(yC, yD)) 

3962 L = int(x0 < np.minimum(xA, xC)) 

3963 

3964 # Check which boundaries are violated (and thus where to look next) 

3965 in_box = np.all(np.logical_not(np.array([D, R, U, L]))) 

3966 if in_box: 

3967 D = boundary_check(x0, y0, xA, yA, xB, yB) 

3968 R = boundary_check(x0, y0, xB, yB, xD, yD) 

3969 U = boundary_check(x0, y0, xD, yD, xC, yC) 

3970 L = boundary_check(x0, y0, xC, yC, xA, yA) 

3971 

3972 # Update the sector guess based on the violations 

3973 ii_next = np.maximum(np.minimum(ii - L + R, top_ii), 0) 

3974 jj_next = np.maximum(np.minimum(jj - D + U, top_jj), 0) 

3975 

3976 # Check whether sector guess changed and go to next iteration 

3977 found = (ii == ii_next) and (jj == jj_next) 

3978 ii = ii_next 

3979 jj = jj_next 

3980 loops += 1 

3981 

3982 # Put the final sector guess into the output array 

3983 X_pos[m] = ii 

3984 Y_pos[m] = jj 

3985 

3986 # Return the output 

3987 return X_pos, Y_pos 

3988 

3989 

3990# Define a numba helper function for finding relative coordinates within sector 

3991@njit 

3992def find_coords_numba( 

3993 X_query, Y_query, X_pos, Y_pos, X_values, Y_values, polarity 

3994): # pragma: no cover 

3995 M = X_query.size 

3996 alpha = np.empty(M) 

3997 beta = np.empty(M) 

3998 

3999 # Calculate relative coordinates in the sector for each point 

4000 for m in range(M): 

4001 try: 

4002 x0 = X_query[m] 

4003 y0 = Y_query[m] 

4004 ii = X_pos[m] 

4005 jj = Y_pos[m] 

4006 xA = X_values[ii, jj] 

4007 xB = X_values[ii + 1, jj] 

4008 xC = X_values[ii, jj + 1] 

4009 xD = X_values[ii + 1, jj + 1] 

4010 yA = Y_values[ii, jj] 

4011 yB = Y_values[ii + 1, jj] 

4012 yC = Y_values[ii, jj + 1] 

4013 yD = Y_values[ii + 1, jj + 1] 

4014 p = 2.0 * polarity[ii, jj] - 1.0 

4015 a = xA 

4016 b = xB - xA 

4017 c = xC - xA 

4018 d = xA - xB - xC + xD 

4019 e = yA 

4020 f = yB - yA 

4021 g = yC - yA 

4022 h = yA - yB - yC + yD 

4023 denom = d * g - h * c 

4024 mu = (h * b - d * f) / denom 

4025 tau = (h * (a - x0) - d * (e - y0)) / denom 

4026 zeta = a - x0 + c * tau 

4027 eta = b + c * mu + d * tau 

4028 theta = d * mu 

4029 alph = (-eta + p * np.sqrt(eta**2 - 4 * zeta * theta)) / (2 * theta) 

4030 bet = mu * alph + tau 

4031 except: 

4032 alph = np.nan 

4033 bet = np.nan 

4034 alpha[m] = alph 

4035 beta[m] = bet 

4036 

4037 return alpha, beta 

4038 

4039 

4040class DiscreteInterp(MetricObject): 

4041 """ 

4042 An interpolator for variables that can only take a discrete set of values. 

4043 

4044 If the function we wish to interpolate, f(args) can take on the list of 

4045 values discrete_vals, this class expects an interpolator for the index of 

4046 f's value in discrete_vals. 

4047 E.g., if f(a,b,c) = discrete_vals[5], then index_interp(a,b,c) = 5. 

4048 

4049 Parameters 

4050 ---------- 

4051 index_interp: HARKInterpolator 

4052 An interpolator giving an approximation to the index of the value in 

4053 discrete_vals that corresponds to a given set of arguments. 

4054 discrete_vals: numpy.array 

4055 A 1D array containing the values in the range of the discrete function 

4056 to be interpolated. 

4057 """ 

4058 

4059 distance_criteria = ["index_interp"] 

4060 

4061 def __init__(self, index_interp, discrete_vals): 

4062 self.index_interp = index_interp 

4063 self.discrete_vals = discrete_vals 

4064 self.n_vals = len(self.discrete_vals) 

4065 

4066 def __call__(self, *args): 

4067 # Interpolate indices and round to integers 

4068 inds = np.rint(self.index_interp(*args)).astype(int) 

4069 if type(inds) is not np.ndarray: 

4070 inds = np.array(inds) 

4071 # Deal with out-of range indices 

4072 inds[inds < 0] = 0 

4073 inds[inds >= self.n_vals] = self.n_vals - 1 

4074 

4075 # Get values from grid 

4076 return self.discrete_vals[inds] 

4077 

4078 

4079class IndexedInterp(MetricObject): 

4080 """ 

4081 An interpolator for functions whose first argument is an integer-valued index. 

4082 Constructor takes in a list of functions as its only argument. When evaluated 

4083 at f(i,X), interpolator returns f[i](X), where X can be any number of inputs. 

4084 This simply provides a different interface for accessing the same functions. 

4085 

4086 Parameters 

4087 ---------- 

4088 functions : [Callable] 

4089 List of one or more functions to be indexed. 

4090 """ 

4091 

4092 distance_criteria = ["functions"] 

4093 

4094 def __init__(self, functions): 

4095 self.functions = functions 

4096 self.N = len(functions) 

4097 

4098 def __call__(self, idx, *args): 

4099 out = np.empty(idx.shape) 

4100 out.fill(np.nan) 

4101 

4102 for n in range(self.N): 

4103 these = idx == n 

4104 if not np.any(these): 

4105 continue 

4106 temp = [arg[these] for arg in args] 

4107 out[these] = self.functions[n](*temp) 

4108 return out 

4109 

4110 

4111############################################################################### 

4112## Functions used in discrete choice models with T1EV taste shocks ############ 

4113############################################################################### 

4114 

4115 

4116def calc_log_sum_choice_probs(Vals, sigma): 

4117 """ 

4118 Returns the final optimal value and choice probabilities given the choice 

4119 specific value functions `Vals`. Probabilities are degenerate if sigma == 0.0. 

4120 Parameters 

4121 ---------- 

4122 Vals : [numpy.array] 

4123 A numpy.array that holds choice specific values at common grid points. 

4124 sigma : float 

4125 A number that controls the variance of the taste shocks 

4126 Returns 

4127 ------- 

4128 V : [numpy.array] 

4129 A numpy.array that holds the integrated value function. 

4130 P : [numpy.array] 

4131 A numpy.array that holds the discrete choice probabilities 

4132 """ 

4133 # Assumes that NaNs have been replaced by -numpy.inf or similar 

4134 if sigma == 0.0: 

4135 # We could construct a linear index here and use unravel_index. 

4136 Pflat = np.argmax(Vals, axis=0) 

4137 

4138 V = np.zeros(Vals[0].shape) 

4139 Probs = np.zeros(Vals.shape) 

4140 for i in range(Vals.shape[0]): 

4141 optimalIndices = Pflat == i 

4142 V[optimalIndices] = Vals[i][optimalIndices] 

4143 Probs[i][optimalIndices] = 1 

4144 return V, Probs 

4145 

4146 # else we have a taste shock 

4147 maxV = np.max(Vals, axis=0) 

4148 

4149 # calculate maxV+sigma*log(sum_i=1^J exp((V[i]-maxV))/sigma) 

4150 sumexp = np.sum(np.exp((Vals - maxV) / sigma), axis=0) 

4151 LogSumV = np.log(sumexp) 

4152 LogSumV = maxV + sigma * LogSumV 

4153 

4154 Probs = np.exp((Vals - LogSumV) / sigma) 

4155 return LogSumV, Probs 

4156 

4157 

4158def calc_choice_probs(Vals, sigma): 

4159 """ 

4160 Returns the choice probabilities given the choice specific value functions 

4161 `Vals`. Probabilities are degenerate if sigma == 0.0. 

4162 Parameters 

4163 ---------- 

4164 Vals : [numpy.array] 

4165 A numpy.array that holds choice specific values at common grid points. 

4166 sigma : float 

4167 A number that controls the variance of the taste shocks 

4168 Returns 

4169 ------- 

4170 Probs : [numpy.array] 

4171 A numpy.array that holds the discrete choice probabilities 

4172 """ 

4173 

4174 # Assumes that NaNs have been replaced by -numpy.inf or similar 

4175 if sigma == 0.0: 

4176 # We could construct a linear index here and use unravel_index. 

4177 Pflat = np.argmax(Vals, axis=0) 

4178 Probs = np.zeros(Vals.shape) 

4179 for i in range(Vals.shape[0]): 

4180 Probs[i][Pflat == i] = 1 

4181 return Probs 

4182 

4183 maxV = np.max(Vals, axis=0) 

4184 Probs = np.divide( 

4185 np.exp((Vals - maxV) / sigma), np.sum(np.exp((Vals - maxV) / sigma), axis=0) 

4186 ) 

4187 return Probs 

4188 

4189 

4190def calc_log_sum(Vals, sigma): 

4191 """ 

4192 Returns the optimal value given the choice specific value functions Vals. 

4193 Parameters 

4194 ---------- 

4195 Vals : [numpy.array] 

4196 A numpy.array that holds choice specific values at common grid points. 

4197 sigma : float 

4198 A number that controls the variance of the taste shocks 

4199 Returns 

4200 ------- 

4201 V : [numpy.array] 

4202 A numpy.array that holds the integrated value function. 

4203 """ 

4204 

4205 # Assumes that NaNs have been replaced by -numpy.inf or similar 

4206 if sigma == 0.0: 

4207 # We could construct a linear index here and use unravel_index. 

4208 V = np.amax(Vals, axis=0) 

4209 return V 

4210 

4211 # else we have a taste shock 

4212 maxV = np.max(Vals, axis=0) 

4213 

4214 # calculate maxV+sigma*log(sum_i=1^J exp((V[i]-maxV))/sigma) 

4215 sumexp = np.sum(np.exp((Vals - maxV) / sigma), axis=0) 

4216 LogSumV = np.log(sumexp) 

4217 LogSumV = maxV + sigma * LogSumV 

4218 return LogSumV 

4219 

4220 

4221############################################################################### 

4222# Tools for value and marginal-value functions in models where # 

4223# - dvdm = u'(c). # 

4224# - u is of the CRRA family. # 

4225############################################################################### 

4226 

4227 

4228class ValueFuncCRRA(MetricObject): 

4229 """ 

4230 A class for representing a value function. The underlying interpolation is 

4231 in the space of (state,u_inv(v)); this class "re-curves" to the value function. 

4232 

4233 Parameters 

4234 ---------- 

4235 vFuncNvrs : function 

4236 A real function representing the value function composed with the 

4237 inverse utility function, defined on the state: u_inv(vFunc(state)) 

4238 CRRA : float 

4239 Coefficient of relative risk aversion. 

4240 illegal_value : float, optional 

4241 If provided, value to return for "out-of-bounds" inputs that return NaN 

4242 from the pseudo-inverse value function. Most common choice is -np.inf, 

4243 which makes the outcome infinitely bad. 

4244 """ 

4245 

4246 distance_criteria = ["func", "CRRA"] 

4247 

4248 def __init__(self, vFuncNvrs, CRRA, illegal_value=None): 

4249 self.vFuncNvrs = deepcopy(vFuncNvrs) 

4250 self.CRRA = CRRA 

4251 self.illegal_value = illegal_value 

4252 

4253 if hasattr(vFuncNvrs, "grid_list"): 

4254 self.grid_list = vFuncNvrs.grid_list 

4255 else: 

4256 self.grid_list = None 

4257 

4258 def __call__(self, *vFuncArgs): 

4259 """ 

4260 Evaluate the value function at given levels of market resources m. 

4261 

4262 Parameters 

4263 ---------- 

4264 vFuncArgs : floats or np.arrays, all of the same dimensions. 

4265 Values for the state variables. These usually start with 'm', 

4266 market resources normalized by the level of permanent income. 

4267 

4268 Returns 

4269 ------- 

4270 v : float or np.array 

4271 Lifetime value of beginning this period with the given states; has 

4272 same size as the state inputs. 

4273 """ 

4274 temp = self.vFuncNvrs(*vFuncArgs) 

4275 v = CRRAutility(temp, self.CRRA) 

4276 if self.illegal_value is not None: 

4277 illegal = np.isnan(temp) 

4278 v[illegal] = self.illegal_value 

4279 return v 

4280 

4281 def gradient(self, *args): 

4282 NvrsGrad = self.vFuncNvrs.gradient(*args) 

4283 marg_u = CRRAutilityP(*args, self.CRRA) 

4284 grad = [g * marg_u for g in NvrsGrad] 

4285 return grad 

4286 

4287 def _eval_and_grad(self, *args): 

4288 return (self.__call__(*args), self.gradient(*args)) 

4289 

4290 

4291class MargValueFuncCRRA(MetricObject): 

4292 """ 

4293 A class for representing a marginal value function in models where the 

4294 standard envelope condition of dvdm(state) = u'(c(state)) holds (with CRRA utility). 

4295 

4296 Parameters 

4297 ---------- 

4298 cFunc : function. 

4299 Its first argument must be normalized market resources m. 

4300 A real function representing the marginal value function composed 

4301 with the inverse marginal utility function, defined on the state 

4302 variables: uP_inv(dvdmFunc(state)). Called cFunc because when standard 

4303 envelope condition applies, uP_inv(dvdm(state)) = cFunc(state). 

4304 CRRA : float 

4305 Coefficient of relative risk aversion. 

4306 """ 

4307 

4308 distance_criteria = ["cFunc", "CRRA"] 

4309 

4310 def __init__(self, cFunc, CRRA): 

4311 self.cFunc = deepcopy(cFunc) 

4312 self.CRRA = CRRA 

4313 

4314 if hasattr(cFunc, "grid_list"): 

4315 self.grid_list = cFunc.grid_list 

4316 else: 

4317 self.grid_list = None 

4318 

4319 def __call__(self, *cFuncArgs): 

4320 """ 

4321 Evaluate the marginal value function at given levels of market resources m. 

4322 

4323 Parameters 

4324 ---------- 

4325 cFuncArgs : floats or np.arrays 

4326 Values of the state variables at which to evaluate the marginal 

4327 value function. 

4328 

4329 Returns 

4330 ------- 

4331 vP : float or np.array 

4332 Marginal lifetime value of beginning this period with state 

4333 cFuncArgs 

4334 """ 

4335 return CRRAutilityP(self.cFunc(*cFuncArgs), rho=self.CRRA) 

4336 

4337 def derivativeX(self, *cFuncArgs): 

4338 """ 

4339 Evaluate the derivative of the marginal value function with respect to 

4340 market resources at given state; this is the marginal marginal value 

4341 function. 

4342 

4343 Parameters 

4344 ---------- 

4345 cFuncArgs : floats or np.arrays 

4346 State variables. 

4347 

4348 Returns 

4349 ------- 

4350 vPP : float or np.array 

4351 Marginal marginal lifetime value of beginning this period with 

4352 state cFuncArgs; has same size as inputs. 

4353 

4354 """ 

4355 

4356 # The derivative method depends on the dimension of the function 

4357 if isinstance(self.cFunc, HARKinterpolator1D): 

4358 c, MPC = self.cFunc.eval_with_derivative(*cFuncArgs) 

4359 

4360 elif hasattr(self.cFunc, "derivativeX"): 

4361 c = self.cFunc(*cFuncArgs) 

4362 MPC = self.cFunc.derivativeX(*cFuncArgs) 

4363 

4364 else: 

4365 raise Exception( 

4366 "cFunc does not have a 'derivativeX' attribute. Can't compute" 

4367 + "marginal marginal value." 

4368 ) 

4369 

4370 return MPC * CRRAutilityPP(c, rho=self.CRRA) 

4371 

4372 

4373class MargMargValueFuncCRRA(MetricObject): 

4374 """ 

4375 A class for representing a marginal marginal value function in models where 

4376 the standard envelope condition of dvdm = u'(c(state)) holds (with CRRA utility). 

4377 

4378 Parameters 

4379 ---------- 

4380 cFunc : function. 

4381 Its first argument must be normalized market resources m. 

4382 A real function representing the marginal value function composed 

4383 with the inverse marginal utility function, defined on the state 

4384 variables: uP_inv(dvdmFunc(state)). Called cFunc because when standard 

4385 envelope condition applies, uP_inv(dvdm(state)) = cFunc(state). 

4386 CRRA : float 

4387 Coefficient of relative risk aversion. 

4388 """ 

4389 

4390 distance_criteria = ["cFunc", "CRRA"] 

4391 

4392 def __init__(self, cFunc, CRRA): 

4393 self.cFunc = deepcopy(cFunc) 

4394 self.CRRA = CRRA 

4395 

4396 def __call__(self, *cFuncArgs): 

4397 """ 

4398 Evaluate the marginal marginal value function at given levels of market 

4399 resources m. 

4400 

4401 Parameters 

4402 ---------- 

4403 m : float or np.array 

4404 Market resources (normalized by permanent income) whose marginal 

4405 marginal value is to be found. 

4406 

4407 Returns 

4408 ------- 

4409 vPP : float or np.array 

4410 Marginal marginal lifetime value of beginning this period with market 

4411 resources m; has same size as input m. 

4412 """ 

4413 

4414 # The derivative method depends on the dimension of the function 

4415 if isinstance(self.cFunc, HARKinterpolator1D): 

4416 c, MPC = self.cFunc.eval_with_derivative(*cFuncArgs) 

4417 

4418 elif hasattr(self.cFunc, "derivativeX"): 

4419 c = self.cFunc(*cFuncArgs) 

4420 MPC = self.cFunc.derivativeX(*cFuncArgs) 

4421 

4422 else: 

4423 raise Exception( 

4424 "cFunc does not have a 'derivativeX' attribute. Can't compute" 

4425 + "marginal marginal value." 

4426 ) 

4427 return MPC * CRRAutilityPP(c, rho=self.CRRA)