Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/interpolate/_cubic.py: 10%

262 statements  

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

1"""Interpolation algorithms using piecewise cubic polynomials.""" 

2 

3import numpy as np 

4 

5from . import PPoly 

6from ._polyint import _isscalar 

7from scipy.linalg import solve_banded, solve 

8 

9 

10__all__ = ["CubicHermiteSpline", "PchipInterpolator", "pchip_interpolate", 

11 "Akima1DInterpolator", "CubicSpline"] 

12 

13 

14def prepare_input(x, y, axis, dydx=None): 

15 """Prepare input for cubic spline interpolators. 

16 

17 All data are converted to numpy arrays and checked for correctness. 

18 Axes equal to `axis` of arrays `y` and `dydx` are moved to be the 0th 

19 axis. The value of `axis` is converted to lie in 

20 [0, number of dimensions of `y`). 

21 """ 

22 

23 x, y = map(np.asarray, (x, y)) 

24 if np.issubdtype(x.dtype, np.complexfloating): 

25 raise ValueError("`x` must contain real values.") 

26 x = x.astype(float) 

27 

28 if np.issubdtype(y.dtype, np.complexfloating): 

29 dtype = complex 

30 else: 

31 dtype = float 

32 

33 if dydx is not None: 

34 dydx = np.asarray(dydx) 

35 if y.shape != dydx.shape: 

36 raise ValueError("The shapes of `y` and `dydx` must be identical.") 

37 if np.issubdtype(dydx.dtype, np.complexfloating): 

38 dtype = complex 

39 dydx = dydx.astype(dtype, copy=False) 

40 

41 y = y.astype(dtype, copy=False) 

42 axis = axis % y.ndim 

43 if x.ndim != 1: 

44 raise ValueError("`x` must be 1-dimensional.") 

45 if x.shape[0] < 2: 

46 raise ValueError("`x` must contain at least 2 elements.") 

47 if x.shape[0] != y.shape[axis]: 

48 raise ValueError("The length of `y` along `axis`={0} doesn't " 

49 "match the length of `x`".format(axis)) 

50 

51 if not np.all(np.isfinite(x)): 

52 raise ValueError("`x` must contain only finite values.") 

53 if not np.all(np.isfinite(y)): 

54 raise ValueError("`y` must contain only finite values.") 

55 

56 if dydx is not None and not np.all(np.isfinite(dydx)): 

57 raise ValueError("`dydx` must contain only finite values.") 

58 

59 dx = np.diff(x) 

60 if np.any(dx <= 0): 

61 raise ValueError("`x` must be strictly increasing sequence.") 

62 

63 y = np.moveaxis(y, axis, 0) 

64 if dydx is not None: 

65 dydx = np.moveaxis(dydx, axis, 0) 

66 

67 return x, dx, y, axis, dydx 

68 

69 

70class CubicHermiteSpline(PPoly): 

71 """Piecewise-cubic interpolator matching values and first derivatives. 

72 

73 The result is represented as a `PPoly` instance. 

74 

75 Parameters 

76 ---------- 

77 x : array_like, shape (n,) 

78 1-D array containing values of the independent variable. 

79 Values must be real, finite and in strictly increasing order. 

80 y : array_like 

81 Array containing values of the dependent variable. It can have 

82 arbitrary number of dimensions, but the length along ``axis`` 

83 (see below) must match the length of ``x``. Values must be finite. 

84 dydx : array_like 

85 Array containing derivatives of the dependent variable. It can have 

86 arbitrary number of dimensions, but the length along ``axis`` 

87 (see below) must match the length of ``x``. Values must be finite. 

88 axis : int, optional 

89 Axis along which `y` is assumed to be varying. Meaning that for 

90 ``x[i]`` the corresponding values are ``np.take(y, i, axis=axis)``. 

91 Default is 0. 

92 extrapolate : {bool, 'periodic', None}, optional 

93 If bool, determines whether to extrapolate to out-of-bounds points 

94 based on first and last intervals, or to return NaNs. If 'periodic', 

95 periodic extrapolation is used. If None (default), it is set to True. 

96 

97 Attributes 

98 ---------- 

99 x : ndarray, shape (n,) 

100 Breakpoints. The same ``x`` which was passed to the constructor. 

101 c : ndarray, shape (4, n-1, ...) 

102 Coefficients of the polynomials on each segment. The trailing 

103 dimensions match the dimensions of `y`, excluding ``axis``. 

104 For example, if `y` is 1-D, then ``c[k, i]`` is a coefficient for 

105 ``(x-x[i])**(3-k)`` on the segment between ``x[i]`` and ``x[i+1]``. 

106 axis : int 

107 Interpolation axis. The same axis which was passed to the 

108 constructor. 

109 

110 Methods 

111 ------- 

112 __call__ 

113 derivative 

114 antiderivative 

115 integrate 

116 roots 

117 

118 See Also 

119 -------- 

120 Akima1DInterpolator : Akima 1D interpolator. 

121 PchipInterpolator : PCHIP 1-D monotonic cubic interpolator. 

122 CubicSpline : Cubic spline data interpolator. 

123 PPoly : Piecewise polynomial in terms of coefficients and breakpoints 

124 

125 Notes 

126 ----- 

127 If you want to create a higher-order spline matching higher-order 

128 derivatives, use `BPoly.from_derivatives`. 

129 

130 References 

131 ---------- 

132 .. [1] `Cubic Hermite spline 

133 <https://en.wikipedia.org/wiki/Cubic_Hermite_spline>`_ 

134 on Wikipedia. 

135 """ 

136 

137 def __init__(self, x, y, dydx, axis=0, extrapolate=None): 

138 if extrapolate is None: 

139 extrapolate = True 

140 

141 x, dx, y, axis, dydx = prepare_input(x, y, axis, dydx) 

142 

143 dxr = dx.reshape([dx.shape[0]] + [1] * (y.ndim - 1)) 

144 slope = np.diff(y, axis=0) / dxr 

145 t = (dydx[:-1] + dydx[1:] - 2 * slope) / dxr 

146 

147 c = np.empty((4, len(x) - 1) + y.shape[1:], dtype=t.dtype) 

148 c[0] = t / dxr 

149 c[1] = (slope - dydx[:-1]) / dxr - t 

150 c[2] = dydx[:-1] 

151 c[3] = y[:-1] 

152 

153 super().__init__(c, x, extrapolate=extrapolate) 

154 self.axis = axis 

155 

156 

157class PchipInterpolator(CubicHermiteSpline): 

158 r"""PCHIP 1-D monotonic cubic interpolation. 

159 

160 ``x`` and ``y`` are arrays of values used to approximate some function f, 

161 with ``y = f(x)``. The interpolant uses monotonic cubic splines 

162 to find the value of new points. (PCHIP stands for Piecewise Cubic 

163 Hermite Interpolating Polynomial). 

164 

165 Parameters 

166 ---------- 

167 x : ndarray 

168 A 1-D array of monotonically increasing real values. ``x`` cannot 

169 include duplicate values (otherwise f is overspecified) 

170 y : ndarray 

171 A 1-D array of real values. ``y``'s length along the interpolation 

172 axis must be equal to the length of ``x``. If N-D array, use ``axis`` 

173 parameter to select correct axis. 

174 axis : int, optional 

175 Axis in the y array corresponding to the x-coordinate values. 

176 extrapolate : bool, optional 

177 Whether to extrapolate to out-of-bounds points based on first 

178 and last intervals, or to return NaNs. 

179 

180 Methods 

181 ------- 

182 __call__ 

183 derivative 

184 antiderivative 

185 roots 

186 

187 See Also 

188 -------- 

189 CubicHermiteSpline : Piecewise-cubic interpolator. 

190 Akima1DInterpolator : Akima 1D interpolator. 

191 CubicSpline : Cubic spline data interpolator. 

192 PPoly : Piecewise polynomial in terms of coefficients and breakpoints. 

193 

194 Notes 

195 ----- 

196 The interpolator preserves monotonicity in the interpolation data and does 

197 not overshoot if the data is not smooth. 

198 

199 The first derivatives are guaranteed to be continuous, but the second 

200 derivatives may jump at :math:`x_k`. 

201 

202 Determines the derivatives at the points :math:`x_k`, :math:`f'_k`, 

203 by using PCHIP algorithm [1]_. 

204 

205 Let :math:`h_k = x_{k+1} - x_k`, and :math:`d_k = (y_{k+1} - y_k) / h_k` 

206 are the slopes at internal points :math:`x_k`. 

207 If the signs of :math:`d_k` and :math:`d_{k-1}` are different or either of 

208 them equals zero, then :math:`f'_k = 0`. Otherwise, it is given by the 

209 weighted harmonic mean 

210 

211 .. math:: 

212 

213 \frac{w_1 + w_2}{f'_k} = \frac{w_1}{d_{k-1}} + \frac{w_2}{d_k} 

214 

215 where :math:`w_1 = 2 h_k + h_{k-1}` and :math:`w_2 = h_k + 2 h_{k-1}`. 

216 

217 The end slopes are set using a one-sided scheme [2]_. 

218 

219 

220 References 

221 ---------- 

222 .. [1] F. N. Fritsch and J. Butland, 

223 A method for constructing local 

224 monotone piecewise cubic interpolants, 

225 SIAM J. Sci. Comput., 5(2), 300-304 (1984). 

226 :doi:`10.1137/0905021`. 

227 .. [2] see, e.g., C. Moler, Numerical Computing with Matlab, 2004. 

228 :doi:`10.1137/1.9780898717952` 

229 

230 

231 """ 

232 

233 def __init__(self, x, y, axis=0, extrapolate=None): 

234 x, _, y, axis, _ = prepare_input(x, y, axis) 

235 xp = x.reshape((x.shape[0],) + (1,)*(y.ndim-1)) 

236 dk = self._find_derivatives(xp, y) 

237 super().__init__(x, y, dk, axis=0, extrapolate=extrapolate) 

238 self.axis = axis 

239 

240 @staticmethod 

241 def _edge_case(h0, h1, m0, m1): 

242 # one-sided three-point estimate for the derivative 

243 d = ((2*h0 + h1)*m0 - h0*m1) / (h0 + h1) 

244 

245 # try to preserve shape 

246 mask = np.sign(d) != np.sign(m0) 

247 mask2 = (np.sign(m0) != np.sign(m1)) & (np.abs(d) > 3.*np.abs(m0)) 

248 mmm = (~mask) & mask2 

249 

250 d[mask] = 0. 

251 d[mmm] = 3.*m0[mmm] 

252 

253 return d 

254 

255 @staticmethod 

256 def _find_derivatives(x, y): 

257 # Determine the derivatives at the points y_k, d_k, by using 

258 # PCHIP algorithm is: 

259 # We choose the derivatives at the point x_k by 

260 # Let m_k be the slope of the kth segment (between k and k+1) 

261 # If m_k=0 or m_{k-1}=0 or sgn(m_k) != sgn(m_{k-1}) then d_k == 0 

262 # else use weighted harmonic mean: 

263 # w_1 = 2h_k + h_{k-1}, w_2 = h_k + 2h_{k-1} 

264 # 1/d_k = 1/(w_1 + w_2)*(w_1 / m_k + w_2 / m_{k-1}) 

265 # where h_k is the spacing between x_k and x_{k+1} 

266 y_shape = y.shape 

267 if y.ndim == 1: 

268 # So that _edge_case doesn't end up assigning to scalars 

269 x = x[:, None] 

270 y = y[:, None] 

271 

272 hk = x[1:] - x[:-1] 

273 mk = (y[1:] - y[:-1]) / hk 

274 

275 if y.shape[0] == 2: 

276 # edge case: only have two points, use linear interpolation 

277 dk = np.zeros_like(y) 

278 dk[0] = mk 

279 dk[1] = mk 

280 return dk.reshape(y_shape) 

281 

282 smk = np.sign(mk) 

283 condition = (smk[1:] != smk[:-1]) | (mk[1:] == 0) | (mk[:-1] == 0) 

284 

285 w1 = 2*hk[1:] + hk[:-1] 

286 w2 = hk[1:] + 2*hk[:-1] 

287 

288 # values where division by zero occurs will be excluded 

289 # by 'condition' afterwards 

290 with np.errstate(divide='ignore', invalid='ignore'): 

291 whmean = (w1/mk[:-1] + w2/mk[1:]) / (w1 + w2) 

292 

293 dk = np.zeros_like(y) 

294 dk[1:-1][condition] = 0.0 

295 dk[1:-1][~condition] = 1.0 / whmean[~condition] 

296 

297 # special case endpoints, as suggested in 

298 # Cleve Moler, Numerical Computing with MATLAB, Chap 3.6 (pchiptx.m) 

299 dk[0] = PchipInterpolator._edge_case(hk[0], hk[1], mk[0], mk[1]) 

300 dk[-1] = PchipInterpolator._edge_case(hk[-1], hk[-2], mk[-1], mk[-2]) 

301 

302 return dk.reshape(y_shape) 

303 

304 

305def pchip_interpolate(xi, yi, x, der=0, axis=0): 

306 """ 

307 Convenience function for pchip interpolation. 

308 

309 xi and yi are arrays of values used to approximate some function f, 

310 with ``yi = f(xi)``. The interpolant uses monotonic cubic splines 

311 to find the value of new points x and the derivatives there. 

312 

313 See `scipy.interpolate.PchipInterpolator` for details. 

314 

315 Parameters 

316 ---------- 

317 xi : array_like 

318 A sorted list of x-coordinates, of length N. 

319 yi : array_like 

320 A 1-D array of real values. `yi`'s length along the interpolation 

321 axis must be equal to the length of `xi`. If N-D array, use axis 

322 parameter to select correct axis. 

323 x : scalar or array_like 

324 Of length M. 

325 der : int or list, optional 

326 Derivatives to extract. The 0th derivative can be included to 

327 return the function value. 

328 axis : int, optional 

329 Axis in the yi array corresponding to the x-coordinate values. 

330 

331 See Also 

332 -------- 

333 PchipInterpolator : PCHIP 1-D monotonic cubic interpolator. 

334 

335 Returns 

336 ------- 

337 y : scalar or array_like 

338 The result, of length R or length M or M by R, 

339 

340 Examples 

341 -------- 

342 We can interpolate 2D observed data using pchip interpolation: 

343 

344 >>> import numpy as np 

345 >>> import matplotlib.pyplot as plt 

346 >>> from scipy.interpolate import pchip_interpolate 

347 >>> x_observed = np.linspace(0.0, 10.0, 11) 

348 >>> y_observed = np.sin(x_observed) 

349 >>> x = np.linspace(min(x_observed), max(x_observed), num=100) 

350 >>> y = pchip_interpolate(x_observed, y_observed, x) 

351 >>> plt.plot(x_observed, y_observed, "o", label="observation") 

352 >>> plt.plot(x, y, label="pchip interpolation") 

353 >>> plt.legend() 

354 >>> plt.show() 

355 

356 """ 

357 P = PchipInterpolator(xi, yi, axis=axis) 

358 

359 if der == 0: 

360 return P(x) 

361 elif _isscalar(der): 

362 return P.derivative(der)(x) 

363 else: 

364 return [P.derivative(nu)(x) for nu in der] 

365 

366 

367class Akima1DInterpolator(CubicHermiteSpline): 

368 """ 

369 Akima interpolator 

370 

371 Fit piecewise cubic polynomials, given vectors x and y. The interpolation 

372 method by Akima uses a continuously differentiable sub-spline built from 

373 piecewise cubic polynomials. The resultant curve passes through the given 

374 data points and will appear smooth and natural. 

375 

376 Parameters 

377 ---------- 

378 x : ndarray, shape (m, ) 

379 1-D array of monotonically increasing real values. 

380 y : ndarray, shape (m, ...) 

381 N-D array of real values. The length of ``y`` along the first axis 

382 must be equal to the length of ``x``. 

383 axis : int, optional 

384 Specifies the axis of ``y`` along which to interpolate. Interpolation 

385 defaults to the first axis of ``y``. 

386 

387 Methods 

388 ------- 

389 __call__ 

390 derivative 

391 antiderivative 

392 roots 

393 

394 See Also 

395 -------- 

396 PchipInterpolator : PCHIP 1-D monotonic cubic interpolator. 

397 CubicSpline : Cubic spline data interpolator. 

398 PPoly : Piecewise polynomial in terms of coefficients and breakpoints 

399 

400 Notes 

401 ----- 

402 .. versionadded:: 0.14 

403 

404 Use only for precise data, as the fitted curve passes through the given 

405 points exactly. This routine is useful for plotting a pleasingly smooth 

406 curve through a few given points for purposes of plotting. 

407 

408 References 

409 ---------- 

410 [1] A new method of interpolation and smooth curve fitting based 

411 on local procedures. Hiroshi Akima, J. ACM, October 1970, 17(4), 

412 589-602. 

413 

414 """ 

415 

416 def __init__(self, x, y, axis=0): 

417 # Original implementation in MATLAB by N. Shamsundar (BSD licensed), see 

418 # https://www.mathworks.com/matlabcentral/fileexchange/1814-akima-interpolation 

419 x, dx, y, axis, _ = prepare_input(x, y, axis) 

420 

421 # determine slopes between breakpoints 

422 m = np.empty((x.size + 3, ) + y.shape[1:]) 

423 dx = dx[(slice(None), ) + (None, ) * (y.ndim - 1)] 

424 m[2:-2] = np.diff(y, axis=0) / dx 

425 

426 # add two additional points on the left ... 

427 m[1] = 2. * m[2] - m[3] 

428 m[0] = 2. * m[1] - m[2] 

429 # ... and on the right 

430 m[-2] = 2. * m[-3] - m[-4] 

431 m[-1] = 2. * m[-2] - m[-3] 

432 

433 # if m1 == m2 != m3 == m4, the slope at the breakpoint is not 

434 # defined. This is the fill value: 

435 t = .5 * (m[3:] + m[:-3]) 

436 # get the denominator of the slope t 

437 dm = np.abs(np.diff(m, axis=0)) 

438 f1 = dm[2:] 

439 f2 = dm[:-2] 

440 f12 = f1 + f2 

441 # These are the mask of where the slope at breakpoint is defined: 

442 ind = np.nonzero(f12 > 1e-9 * np.max(f12, initial=-np.inf)) 

443 x_ind, y_ind = ind[0], ind[1:] 

444 # Set the slope at breakpoint 

445 t[ind] = (f1[ind] * m[(x_ind + 1,) + y_ind] + 

446 f2[ind] * m[(x_ind + 2,) + y_ind]) / f12[ind] 

447 

448 super().__init__(x, y, t, axis=0, extrapolate=False) 

449 self.axis = axis 

450 

451 def extend(self, c, x, right=True): 

452 raise NotImplementedError("Extending a 1-D Akima interpolator is not " 

453 "yet implemented") 

454 

455 # These are inherited from PPoly, but they do not produce an Akima 

456 # interpolator. Hence stub them out. 

457 @classmethod 

458 def from_spline(cls, tck, extrapolate=None): 

459 raise NotImplementedError("This method does not make sense for " 

460 "an Akima interpolator.") 

461 

462 @classmethod 

463 def from_bernstein_basis(cls, bp, extrapolate=None): 

464 raise NotImplementedError("This method does not make sense for " 

465 "an Akima interpolator.") 

466 

467 

468class CubicSpline(CubicHermiteSpline): 

469 """Cubic spline data interpolator. 

470 

471 Interpolate data with a piecewise cubic polynomial which is twice 

472 continuously differentiable [1]_. The result is represented as a `PPoly` 

473 instance with breakpoints matching the given data. 

474 

475 Parameters 

476 ---------- 

477 x : array_like, shape (n,) 

478 1-D array containing values of the independent variable. 

479 Values must be real, finite and in strictly increasing order. 

480 y : array_like 

481 Array containing values of the dependent variable. It can have 

482 arbitrary number of dimensions, but the length along ``axis`` 

483 (see below) must match the length of ``x``. Values must be finite. 

484 axis : int, optional 

485 Axis along which `y` is assumed to be varying. Meaning that for 

486 ``x[i]`` the corresponding values are ``np.take(y, i, axis=axis)``. 

487 Default is 0. 

488 bc_type : string or 2-tuple, optional 

489 Boundary condition type. Two additional equations, given by the 

490 boundary conditions, are required to determine all coefficients of 

491 polynomials on each segment [2]_. 

492 

493 If `bc_type` is a string, then the specified condition will be applied 

494 at both ends of a spline. Available conditions are: 

495 

496 * 'not-a-knot' (default): The first and second segment at a curve end 

497 are the same polynomial. It is a good default when there is no 

498 information on boundary conditions. 

499 * 'periodic': The interpolated functions is assumed to be periodic 

500 of period ``x[-1] - x[0]``. The first and last value of `y` must be 

501 identical: ``y[0] == y[-1]``. This boundary condition will result in 

502 ``y'[0] == y'[-1]`` and ``y''[0] == y''[-1]``. 

503 * 'clamped': The first derivative at curves ends are zero. Assuming 

504 a 1D `y`, ``bc_type=((1, 0.0), (1, 0.0))`` is the same condition. 

505 * 'natural': The second derivative at curve ends are zero. Assuming 

506 a 1D `y`, ``bc_type=((2, 0.0), (2, 0.0))`` is the same condition. 

507 

508 If `bc_type` is a 2-tuple, the first and the second value will be 

509 applied at the curve start and end respectively. The tuple values can 

510 be one of the previously mentioned strings (except 'periodic') or a 

511 tuple `(order, deriv_values)` allowing to specify arbitrary 

512 derivatives at curve ends: 

513 

514 * `order`: the derivative order, 1 or 2. 

515 * `deriv_value`: array_like containing derivative values, shape must 

516 be the same as `y`, excluding ``axis`` dimension. For example, if 

517 `y` is 1-D, then `deriv_value` must be a scalar. If `y` is 3-D with 

518 the shape (n0, n1, n2) and axis=2, then `deriv_value` must be 2-D 

519 and have the shape (n0, n1). 

520 extrapolate : {bool, 'periodic', None}, optional 

521 If bool, determines whether to extrapolate to out-of-bounds points 

522 based on first and last intervals, or to return NaNs. If 'periodic', 

523 periodic extrapolation is used. If None (default), ``extrapolate`` is 

524 set to 'periodic' for ``bc_type='periodic'`` and to True otherwise. 

525 

526 Attributes 

527 ---------- 

528 x : ndarray, shape (n,) 

529 Breakpoints. The same ``x`` which was passed to the constructor. 

530 c : ndarray, shape (4, n-1, ...) 

531 Coefficients of the polynomials on each segment. The trailing 

532 dimensions match the dimensions of `y`, excluding ``axis``. 

533 For example, if `y` is 1-d, then ``c[k, i]`` is a coefficient for 

534 ``(x-x[i])**(3-k)`` on the segment between ``x[i]`` and ``x[i+1]``. 

535 axis : int 

536 Interpolation axis. The same axis which was passed to the 

537 constructor. 

538 

539 Methods 

540 ------- 

541 __call__ 

542 derivative 

543 antiderivative 

544 integrate 

545 roots 

546 

547 See Also 

548 -------- 

549 Akima1DInterpolator : Akima 1D interpolator. 

550 PchipInterpolator : PCHIP 1-D monotonic cubic interpolator. 

551 PPoly : Piecewise polynomial in terms of coefficients and breakpoints. 

552 

553 Notes 

554 ----- 

555 Parameters `bc_type` and ``extrapolate`` work independently, i.e. the 

556 former controls only construction of a spline, and the latter only 

557 evaluation. 

558 

559 When a boundary condition is 'not-a-knot' and n = 2, it is replaced by 

560 a condition that the first derivative is equal to the linear interpolant 

561 slope. When both boundary conditions are 'not-a-knot' and n = 3, the 

562 solution is sought as a parabola passing through given points. 

563 

564 When 'not-a-knot' boundary conditions is applied to both ends, the 

565 resulting spline will be the same as returned by `splrep` (with ``s=0``) 

566 and `InterpolatedUnivariateSpline`, but these two methods use a 

567 representation in B-spline basis. 

568 

569 .. versionadded:: 0.18.0 

570 

571 Examples 

572 -------- 

573 In this example the cubic spline is used to interpolate a sampled sinusoid. 

574 You can see that the spline continuity property holds for the first and 

575 second derivatives and violates only for the third derivative. 

576 

577 >>> import numpy as np 

578 >>> from scipy.interpolate import CubicSpline 

579 >>> import matplotlib.pyplot as plt 

580 >>> x = np.arange(10) 

581 >>> y = np.sin(x) 

582 >>> cs = CubicSpline(x, y) 

583 >>> xs = np.arange(-0.5, 9.6, 0.1) 

584 >>> fig, ax = plt.subplots(figsize=(6.5, 4)) 

585 >>> ax.plot(x, y, 'o', label='data') 

586 >>> ax.plot(xs, np.sin(xs), label='true') 

587 >>> ax.plot(xs, cs(xs), label="S") 

588 >>> ax.plot(xs, cs(xs, 1), label="S'") 

589 >>> ax.plot(xs, cs(xs, 2), label="S''") 

590 >>> ax.plot(xs, cs(xs, 3), label="S'''") 

591 >>> ax.set_xlim(-0.5, 9.5) 

592 >>> ax.legend(loc='lower left', ncol=2) 

593 >>> plt.show() 

594 

595 In the second example, the unit circle is interpolated with a spline. A 

596 periodic boundary condition is used. You can see that the first derivative 

597 values, ds/dx=0, ds/dy=1 at the periodic point (1, 0) are correctly 

598 computed. Note that a circle cannot be exactly represented by a cubic 

599 spline. To increase precision, more breakpoints would be required. 

600 

601 >>> theta = 2 * np.pi * np.linspace(0, 1, 5) 

602 >>> y = np.c_[np.cos(theta), np.sin(theta)] 

603 >>> cs = CubicSpline(theta, y, bc_type='periodic') 

604 >>> print("ds/dx={:.1f} ds/dy={:.1f}".format(cs(0, 1)[0], cs(0, 1)[1])) 

605 ds/dx=0.0 ds/dy=1.0 

606 >>> xs = 2 * np.pi * np.linspace(0, 1, 100) 

607 >>> fig, ax = plt.subplots(figsize=(6.5, 4)) 

608 >>> ax.plot(y[:, 0], y[:, 1], 'o', label='data') 

609 >>> ax.plot(np.cos(xs), np.sin(xs), label='true') 

610 >>> ax.plot(cs(xs)[:, 0], cs(xs)[:, 1], label='spline') 

611 >>> ax.axes.set_aspect('equal') 

612 >>> ax.legend(loc='center') 

613 >>> plt.show() 

614 

615 The third example is the interpolation of a polynomial y = x**3 on the 

616 interval 0 <= x<= 1. A cubic spline can represent this function exactly. 

617 To achieve that we need to specify values and first derivatives at 

618 endpoints of the interval. Note that y' = 3 * x**2 and thus y'(0) = 0 and 

619 y'(1) = 3. 

620 

621 >>> cs = CubicSpline([0, 1], [0, 1], bc_type=((1, 0), (1, 3))) 

622 >>> x = np.linspace(0, 1) 

623 >>> np.allclose(x**3, cs(x)) 

624 True 

625 

626 References 

627 ---------- 

628 .. [1] `Cubic Spline Interpolation 

629 <https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation>`_ 

630 on Wikiversity. 

631 .. [2] Carl de Boor, "A Practical Guide to Splines", Springer-Verlag, 1978. 

632 """ 

633 

634 def __init__(self, x, y, axis=0, bc_type='not-a-knot', extrapolate=None): 

635 x, dx, y, axis, _ = prepare_input(x, y, axis) 

636 n = len(x) 

637 

638 bc, y = self._validate_bc(bc_type, y, y.shape[1:], axis) 

639 

640 if extrapolate is None: 

641 if bc[0] == 'periodic': 

642 extrapolate = 'periodic' 

643 else: 

644 extrapolate = True 

645 

646 if y.size == 0: 

647 # bail out early for zero-sized arrays 

648 s = np.zeros_like(y) 

649 else: 

650 dxr = dx.reshape([dx.shape[0]] + [1] * (y.ndim - 1)) 

651 slope = np.diff(y, axis=0) / dxr 

652 

653 # If bc is 'not-a-knot' this change is just a convention. 

654 # If bc is 'periodic' then we already checked that y[0] == y[-1], 

655 # and the spline is just a constant, we handle this case in the 

656 # same way by setting the first derivatives to slope, which is 0. 

657 if n == 2: 

658 if bc[0] in ['not-a-knot', 'periodic']: 

659 bc[0] = (1, slope[0]) 

660 if bc[1] in ['not-a-knot', 'periodic']: 

661 bc[1] = (1, slope[0]) 

662 

663 # This is a special case, when both conditions are 'not-a-knot' 

664 # and n == 3. In this case 'not-a-knot' can't be handled regularly 

665 # as the both conditions are identical. We handle this case by 

666 # constructing a parabola passing through given points. 

667 if n == 3 and bc[0] == 'not-a-knot' and bc[1] == 'not-a-knot': 

668 A = np.zeros((3, 3)) # This is a standard matrix. 

669 b = np.empty((3,) + y.shape[1:], dtype=y.dtype) 

670 

671 A[0, 0] = 1 

672 A[0, 1] = 1 

673 A[1, 0] = dx[1] 

674 A[1, 1] = 2 * (dx[0] + dx[1]) 

675 A[1, 2] = dx[0] 

676 A[2, 1] = 1 

677 A[2, 2] = 1 

678 

679 b[0] = 2 * slope[0] 

680 b[1] = 3 * (dxr[0] * slope[1] + dxr[1] * slope[0]) 

681 b[2] = 2 * slope[1] 

682 

683 s = solve(A, b, overwrite_a=True, overwrite_b=True, 

684 check_finite=False) 

685 elif n == 3 and bc[0] == 'periodic': 

686 # In case when number of points is 3 we compute the derivatives 

687 # manually 

688 s = np.empty((n,) + y.shape[1:], dtype=y.dtype) 

689 t = (slope / dxr).sum() / (1. / dxr).sum() 

690 s.fill(t) 

691 else: 

692 # Find derivative values at each x[i] by solving a tridiagonal 

693 # system. 

694 A = np.zeros((3, n)) # This is a banded matrix representation. 

695 b = np.empty((n,) + y.shape[1:], dtype=y.dtype) 

696 

697 # Filling the system for i=1..n-2 

698 # (x[i-1] - x[i]) * s[i-1] +\ 

699 # 2 * ((x[i] - x[i-1]) + (x[i+1] - x[i])) * s[i] +\ 

700 # (x[i] - x[i-1]) * s[i+1] =\ 

701 # 3 * ((x[i+1] - x[i])*(y[i] - y[i-1])/(x[i] - x[i-1]) +\ 

702 # (x[i] - x[i-1])*(y[i+1] - y[i])/(x[i+1] - x[i])) 

703 

704 A[1, 1:-1] = 2 * (dx[:-1] + dx[1:]) # The diagonal 

705 A[0, 2:] = dx[:-1] # The upper diagonal 

706 A[-1, :-2] = dx[1:] # The lower diagonal 

707 

708 b[1:-1] = 3 * (dxr[1:] * slope[:-1] + dxr[:-1] * slope[1:]) 

709 

710 bc_start, bc_end = bc 

711 

712 if bc_start == 'periodic': 

713 # Due to the periodicity, and because y[-1] = y[0], the 

714 # linear system has (n-1) unknowns/equations instead of n: 

715 A = A[:, 0:-1] 

716 A[1, 0] = 2 * (dx[-1] + dx[0]) 

717 A[0, 1] = dx[-1] 

718 

719 b = b[:-1] 

720 

721 # Also, due to the periodicity, the system is not tri-diagonal. 

722 # We need to compute a "condensed" matrix of shape (n-2, n-2). 

723 # See https://web.archive.org/web/20151220180652/http://www.cfm.brown.edu/people/gk/chap6/node14.html 

724 # for more explanations. 

725 # The condensed matrix is obtained by removing the last column 

726 # and last row of the (n-1, n-1) system matrix. The removed 

727 # values are saved in scalar variables with the (n-1, n-1) 

728 # system matrix indices forming their names: 

729 a_m1_0 = dx[-2] # lower left corner value: A[-1, 0] 

730 a_m1_m2 = dx[-1] 

731 a_m1_m1 = 2 * (dx[-1] + dx[-2]) 

732 a_m2_m1 = dx[-3] 

733 a_0_m1 = dx[0] 

734 

735 b[0] = 3 * (dxr[0] * slope[-1] + dxr[-1] * slope[0]) 

736 b[-1] = 3 * (dxr[-1] * slope[-2] + dxr[-2] * slope[-1]) 

737 

738 Ac = A[:, :-1] 

739 b1 = b[:-1] 

740 b2 = np.zeros_like(b1) 

741 b2[0] = -a_0_m1 

742 b2[-1] = -a_m2_m1 

743 

744 # s1 and s2 are the solutions of (n-2, n-2) system 

745 s1 = solve_banded((1, 1), Ac, b1, overwrite_ab=False, 

746 overwrite_b=False, check_finite=False) 

747 

748 s2 = solve_banded((1, 1), Ac, b2, overwrite_ab=False, 

749 overwrite_b=False, check_finite=False) 

750 

751 # computing the s[n-2] solution: 

752 s_m1 = ((b[-1] - a_m1_0 * s1[0] - a_m1_m2 * s1[-1]) / 

753 (a_m1_m1 + a_m1_0 * s2[0] + a_m1_m2 * s2[-1])) 

754 

755 # s is the solution of the (n, n) system: 

756 s = np.empty((n,) + y.shape[1:], dtype=y.dtype) 

757 s[:-2] = s1 + s_m1 * s2 

758 s[-2] = s_m1 

759 s[-1] = s[0] 

760 else: 

761 if bc_start == 'not-a-knot': 

762 A[1, 0] = dx[1] 

763 A[0, 1] = x[2] - x[0] 

764 d = x[2] - x[0] 

765 b[0] = ((dxr[0] + 2*d) * dxr[1] * slope[0] + 

766 dxr[0]**2 * slope[1]) / d 

767 elif bc_start[0] == 1: 

768 A[1, 0] = 1 

769 A[0, 1] = 0 

770 b[0] = bc_start[1] 

771 elif bc_start[0] == 2: 

772 A[1, 0] = 2 * dx[0] 

773 A[0, 1] = dx[0] 

774 b[0] = -0.5 * bc_start[1] * dx[0]**2 + 3 * (y[1] - y[0]) 

775 

776 if bc_end == 'not-a-knot': 

777 A[1, -1] = dx[-2] 

778 A[-1, -2] = x[-1] - x[-3] 

779 d = x[-1] - x[-3] 

780 b[-1] = ((dxr[-1]**2*slope[-2] + 

781 (2*d + dxr[-1])*dxr[-2]*slope[-1]) / d) 

782 elif bc_end[0] == 1: 

783 A[1, -1] = 1 

784 A[-1, -2] = 0 

785 b[-1] = bc_end[1] 

786 elif bc_end[0] == 2: 

787 A[1, -1] = 2 * dx[-1] 

788 A[-1, -2] = dx[-1] 

789 b[-1] = 0.5 * bc_end[1] * dx[-1]**2 + 3 * (y[-1] - y[-2]) 

790 

791 s = solve_banded((1, 1), A, b, overwrite_ab=True, 

792 overwrite_b=True, check_finite=False) 

793 

794 super().__init__(x, y, s, axis=0, extrapolate=extrapolate) 

795 self.axis = axis 

796 

797 @staticmethod 

798 def _validate_bc(bc_type, y, expected_deriv_shape, axis): 

799 """Validate and prepare boundary conditions. 

800 

801 Returns 

802 ------- 

803 validated_bc : 2-tuple 

804 Boundary conditions for a curve start and end. 

805 y : ndarray 

806 y casted to complex dtype if one of the boundary conditions has 

807 complex dtype. 

808 """ 

809 if isinstance(bc_type, str): 

810 if bc_type == 'periodic': 

811 if not np.allclose(y[0], y[-1], rtol=1e-15, atol=1e-15): 

812 raise ValueError( 

813 "The first and last `y` point along axis {} must " 

814 "be identical (within machine precision) when " 

815 "bc_type='periodic'.".format(axis)) 

816 

817 bc_type = (bc_type, bc_type) 

818 

819 else: 

820 if len(bc_type) != 2: 

821 raise ValueError("`bc_type` must contain 2 elements to " 

822 "specify start and end conditions.") 

823 

824 if 'periodic' in bc_type: 

825 raise ValueError("'periodic' `bc_type` is defined for both " 

826 "curve ends and cannot be used with other " 

827 "boundary conditions.") 

828 

829 validated_bc = [] 

830 for bc in bc_type: 

831 if isinstance(bc, str): 

832 if bc == 'clamped': 

833 validated_bc.append((1, np.zeros(expected_deriv_shape))) 

834 elif bc == 'natural': 

835 validated_bc.append((2, np.zeros(expected_deriv_shape))) 

836 elif bc in ['not-a-knot', 'periodic']: 

837 validated_bc.append(bc) 

838 else: 

839 raise ValueError("bc_type={} is not allowed.".format(bc)) 

840 else: 

841 try: 

842 deriv_order, deriv_value = bc 

843 except Exception as e: 

844 raise ValueError( 

845 "A specified derivative value must be " 

846 "given in the form (order, value)." 

847 ) from e 

848 

849 if deriv_order not in [1, 2]: 

850 raise ValueError("The specified derivative order must " 

851 "be 1 or 2.") 

852 

853 deriv_value = np.asarray(deriv_value) 

854 if deriv_value.shape != expected_deriv_shape: 

855 raise ValueError( 

856 "`deriv_value` shape {} is not the expected one {}." 

857 .format(deriv_value.shape, expected_deriv_shape)) 

858 

859 if np.issubdtype(deriv_value.dtype, np.complexfloating): 

860 y = y.astype(complex, copy=False) 

861 

862 validated_bc.append((deriv_order, deriv_value)) 

863 

864 return validated_bc, y