Coverage for /usr/lib/python3/dist-packages/scipy/interpolate/_bsplines.py: 8%

579 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1import operator 

2from math import prod 

3 

4import numpy as np 

5from numpy.core.multiarray import normalize_axis_index 

6from scipy.linalg import (get_lapack_funcs, LinAlgError, 

7 cholesky_banded, cho_solve_banded, 

8 solve, solve_banded) 

9from scipy.optimize import minimize_scalar 

10from . import _bspl 

11from . import _fitpack_impl 

12from scipy.sparse import csr_array 

13from scipy.special import poch 

14from itertools import combinations 

15 

16__all__ = ["BSpline", "make_interp_spline", "make_lsq_spline", 

17 "make_smoothing_spline"] 

18 

19 

20def _get_dtype(dtype): 

21 """Return np.complex128 for complex dtypes, np.float64 otherwise.""" 

22 if np.issubdtype(dtype, np.complexfloating): 

23 return np.complex_ 

24 else: 

25 return np.float_ 

26 

27 

28def _as_float_array(x, check_finite=False): 

29 """Convert the input into a C contiguous float array. 

30 

31 NB: Upcasts half- and single-precision floats to double precision. 

32 """ 

33 x = np.ascontiguousarray(x) 

34 dtyp = _get_dtype(x.dtype) 

35 x = x.astype(dtyp, copy=False) 

36 if check_finite and not np.isfinite(x).all(): 

37 raise ValueError("Array must not contain infs or nans.") 

38 return x 

39 

40 

41def _dual_poly(j, k, t, y): 

42 """ 

43 Dual polynomial of the B-spline B_{j,k,t} - 

44 polynomial which is associated with B_{j,k,t}: 

45 $p_{j,k}(y) = (y - t_{j+1})(y - t_{j+2})...(y - t_{j+k})$ 

46 """ 

47 if k == 0: 

48 return 1 

49 return np.prod([(y - t[j + i]) for i in range(1, k + 1)]) 

50 

51 

52def _diff_dual_poly(j, k, y, d, t): 

53 """ 

54 d-th derivative of the dual polynomial $p_{j,k}(y)$ 

55 """ 

56 if d == 0: 

57 return _dual_poly(j, k, t, y) 

58 if d == k: 

59 return poch(1, k) 

60 comb = list(combinations(range(j + 1, j + k + 1), d)) 

61 res = 0 

62 for i in range(len(comb) * len(comb[0])): 

63 res += np.prod([(y - t[j + p]) for p in range(1, k + 1) 

64 if (j + p) not in comb[i//d]]) 

65 return res 

66 

67 

68class BSpline: 

69 r"""Univariate spline in the B-spline basis. 

70 

71 .. math:: 

72 

73 S(x) = \sum_{j=0}^{n-1} c_j B_{j, k; t}(x) 

74 

75 where :math:`B_{j, k; t}` are B-spline basis functions of degree `k` 

76 and knots `t`. 

77 

78 Parameters 

79 ---------- 

80 t : ndarray, shape (n+k+1,) 

81 knots 

82 c : ndarray, shape (>=n, ...) 

83 spline coefficients 

84 k : int 

85 B-spline degree 

86 extrapolate : bool or 'periodic', optional 

87 whether to extrapolate beyond the base interval, ``t[k] .. t[n]``, 

88 or to return nans. 

89 If True, extrapolates the first and last polynomial pieces of b-spline 

90 functions active on the base interval. 

91 If 'periodic', periodic extrapolation is used. 

92 Default is True. 

93 axis : int, optional 

94 Interpolation axis. Default is zero. 

95 

96 Attributes 

97 ---------- 

98 t : ndarray 

99 knot vector 

100 c : ndarray 

101 spline coefficients 

102 k : int 

103 spline degree 

104 extrapolate : bool 

105 If True, extrapolates the first and last polynomial pieces of b-spline 

106 functions active on the base interval. 

107 axis : int 

108 Interpolation axis. 

109 tck : tuple 

110 A read-only equivalent of ``(self.t, self.c, self.k)`` 

111 

112 Methods 

113 ------- 

114 __call__ 

115 basis_element 

116 derivative 

117 antiderivative 

118 integrate 

119 construct_fast 

120 design_matrix 

121 from_power_basis 

122 

123 Notes 

124 ----- 

125 B-spline basis elements are defined via 

126 

127 .. math:: 

128 

129 B_{i, 0}(x) = 1, \textrm{if $t_i \le x < t_{i+1}$, otherwise $0$,} 

130 

131 B_{i, k}(x) = \frac{x - t_i}{t_{i+k} - t_i} B_{i, k-1}(x) 

132 + \frac{t_{i+k+1} - x}{t_{i+k+1} - t_{i+1}} B_{i+1, k-1}(x) 

133 

134 **Implementation details** 

135 

136 - At least ``k+1`` coefficients are required for a spline of degree `k`, 

137 so that ``n >= k+1``. Additional coefficients, ``c[j]`` with 

138 ``j > n``, are ignored. 

139 

140 - B-spline basis elements of degree `k` form a partition of unity on the 

141 *base interval*, ``t[k] <= x <= t[n]``. 

142 

143 

144 Examples 

145 -------- 

146 

147 Translating the recursive definition of B-splines into Python code, we have: 

148 

149 >>> def B(x, k, i, t): 

150 ... if k == 0: 

151 ... return 1.0 if t[i] <= x < t[i+1] else 0.0 

152 ... if t[i+k] == t[i]: 

153 ... c1 = 0.0 

154 ... else: 

155 ... c1 = (x - t[i])/(t[i+k] - t[i]) * B(x, k-1, i, t) 

156 ... if t[i+k+1] == t[i+1]: 

157 ... c2 = 0.0 

158 ... else: 

159 ... c2 = (t[i+k+1] - x)/(t[i+k+1] - t[i+1]) * B(x, k-1, i+1, t) 

160 ... return c1 + c2 

161 

162 >>> def bspline(x, t, c, k): 

163 ... n = len(t) - k - 1 

164 ... assert (n >= k+1) and (len(c) >= n) 

165 ... return sum(c[i] * B(x, k, i, t) for i in range(n)) 

166 

167 Note that this is an inefficient (if straightforward) way to 

168 evaluate B-splines --- this spline class does it in an equivalent, 

169 but much more efficient way. 

170 

171 Here we construct a quadratic spline function on the base interval 

172 ``2 <= x <= 4`` and compare with the naive way of evaluating the spline: 

173 

174 >>> from scipy.interpolate import BSpline 

175 >>> k = 2 

176 >>> t = [0, 1, 2, 3, 4, 5, 6] 

177 >>> c = [-1, 2, 0, -1] 

178 >>> spl = BSpline(t, c, k) 

179 >>> spl(2.5) 

180 array(1.375) 

181 >>> bspline(2.5, t, c, k) 

182 1.375 

183 

184 Note that outside of the base interval results differ. This is because 

185 `BSpline` extrapolates the first and last polynomial pieces of B-spline 

186 functions active on the base interval. 

187 

188 >>> import matplotlib.pyplot as plt 

189 >>> import numpy as np 

190 >>> fig, ax = plt.subplots() 

191 >>> xx = np.linspace(1.5, 4.5, 50) 

192 >>> ax.plot(xx, [bspline(x, t, c ,k) for x in xx], 'r-', lw=3, label='naive') 

193 >>> ax.plot(xx, spl(xx), 'b-', lw=4, alpha=0.7, label='BSpline') 

194 >>> ax.grid(True) 

195 >>> ax.legend(loc='best') 

196 >>> plt.show() 

197 

198 

199 References 

200 ---------- 

201 .. [1] Tom Lyche and Knut Morken, Spline methods, 

202 http://www.uio.no/studier/emner/matnat/ifi/INF-MAT5340/v05/undervisningsmateriale/ 

203 .. [2] Carl de Boor, A practical guide to splines, Springer, 2001. 

204 

205 """ 

206 

207 def __init__(self, t, c, k, extrapolate=True, axis=0): 

208 super().__init__() 

209 

210 self.k = operator.index(k) 

211 self.c = np.asarray(c) 

212 self.t = np.ascontiguousarray(t, dtype=np.float64) 

213 

214 if extrapolate == 'periodic': 

215 self.extrapolate = extrapolate 

216 else: 

217 self.extrapolate = bool(extrapolate) 

218 

219 n = self.t.shape[0] - self.k - 1 

220 

221 axis = normalize_axis_index(axis, self.c.ndim) 

222 

223 # Note that the normalized axis is stored in the object. 

224 self.axis = axis 

225 if axis != 0: 

226 # roll the interpolation axis to be the first one in self.c 

227 # More specifically, the target shape for self.c is (n, ...), 

228 # and axis !=0 means that we have c.shape (..., n, ...) 

229 # ^ 

230 # axis 

231 self.c = np.moveaxis(self.c, axis, 0) 

232 

233 if k < 0: 

234 raise ValueError("Spline order cannot be negative.") 

235 if self.t.ndim != 1: 

236 raise ValueError("Knot vector must be one-dimensional.") 

237 if n < self.k + 1: 

238 raise ValueError("Need at least %d knots for degree %d" % 

239 (2*k + 2, k)) 

240 if (np.diff(self.t) < 0).any(): 

241 raise ValueError("Knots must be in a non-decreasing order.") 

242 if len(np.unique(self.t[k:n+1])) < 2: 

243 raise ValueError("Need at least two internal knots.") 

244 if not np.isfinite(self.t).all(): 

245 raise ValueError("Knots should not have nans or infs.") 

246 if self.c.ndim < 1: 

247 raise ValueError("Coefficients must be at least 1-dimensional.") 

248 if self.c.shape[0] < n: 

249 raise ValueError("Knots, coefficients and degree are inconsistent.") 

250 

251 dt = _get_dtype(self.c.dtype) 

252 self.c = np.ascontiguousarray(self.c, dtype=dt) 

253 

254 @classmethod 

255 def construct_fast(cls, t, c, k, extrapolate=True, axis=0): 

256 """Construct a spline without making checks. 

257 

258 Accepts same parameters as the regular constructor. Input arrays 

259 `t` and `c` must of correct shape and dtype. 

260 """ 

261 self = object.__new__(cls) 

262 self.t, self.c, self.k = t, c, k 

263 self.extrapolate = extrapolate 

264 self.axis = axis 

265 return self 

266 

267 @property 

268 def tck(self): 

269 """Equivalent to ``(self.t, self.c, self.k)`` (read-only). 

270 """ 

271 return self.t, self.c, self.k 

272 

273 @classmethod 

274 def basis_element(cls, t, extrapolate=True): 

275 """Return a B-spline basis element ``B(x | t[0], ..., t[k+1])``. 

276 

277 Parameters 

278 ---------- 

279 t : ndarray, shape (k+2,) 

280 internal knots 

281 extrapolate : bool or 'periodic', optional 

282 whether to extrapolate beyond the base interval, ``t[0] .. t[k+1]``, 

283 or to return nans. 

284 If 'periodic', periodic extrapolation is used. 

285 Default is True. 

286 

287 Returns 

288 ------- 

289 basis_element : callable 

290 A callable representing a B-spline basis element for the knot 

291 vector `t`. 

292 

293 Notes 

294 ----- 

295 The degree of the B-spline, `k`, is inferred from the length of `t` as 

296 ``len(t)-2``. The knot vector is constructed by appending and prepending 

297 ``k+1`` elements to internal knots `t`. 

298 

299 Examples 

300 -------- 

301 

302 Construct a cubic B-spline: 

303 

304 >>> import numpy as np 

305 >>> from scipy.interpolate import BSpline 

306 >>> b = BSpline.basis_element([0, 1, 2, 3, 4]) 

307 >>> k = b.k 

308 >>> b.t[k:-k] 

309 array([ 0., 1., 2., 3., 4.]) 

310 >>> k 

311 3 

312 

313 Construct a quadratic B-spline on ``[0, 1, 1, 2]``, and compare 

314 to its explicit form: 

315 

316 >>> t = [0, 1, 1, 2] 

317 >>> b = BSpline.basis_element(t) 

318 >>> def f(x): 

319 ... return np.where(x < 1, x*x, (2. - x)**2) 

320 

321 >>> import matplotlib.pyplot as plt 

322 >>> fig, ax = plt.subplots() 

323 >>> x = np.linspace(0, 2, 51) 

324 >>> ax.plot(x, b(x), 'g', lw=3) 

325 >>> ax.plot(x, f(x), 'r', lw=8, alpha=0.4) 

326 >>> ax.grid(True) 

327 >>> plt.show() 

328 

329 """ 

330 k = len(t) - 2 

331 t = _as_float_array(t) 

332 t = np.r_[(t[0]-1,) * k, t, (t[-1]+1,) * k] 

333 c = np.zeros_like(t) 

334 c[k] = 1. 

335 return cls.construct_fast(t, c, k, extrapolate) 

336 

337 @classmethod 

338 def design_matrix(cls, x, t, k, extrapolate=False): 

339 """ 

340 Returns a design matrix as a CSR format sparse array. 

341 

342 Parameters 

343 ---------- 

344 x : array_like, shape (n,) 

345 Points to evaluate the spline at. 

346 t : array_like, shape (nt,) 

347 Sorted 1D array of knots. 

348 k : int 

349 B-spline degree. 

350 extrapolate : bool or 'periodic', optional 

351 Whether to extrapolate based on the first and last intervals 

352 or raise an error. If 'periodic', periodic extrapolation is used. 

353 Default is False. 

354 

355 .. versionadded:: 1.10.0 

356 

357 Returns 

358 ------- 

359 design_matrix : `csr_array` object 

360 Sparse matrix in CSR format where each row contains all the basis 

361 elements of the input row (first row = basis elements of x[0], 

362 ..., last row = basis elements x[-1]). 

363 

364 Examples 

365 -------- 

366 Construct a design matrix for a B-spline 

367 

368 >>> from scipy.interpolate import make_interp_spline, BSpline 

369 >>> import numpy as np 

370 >>> x = np.linspace(0, np.pi * 2, 4) 

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

372 >>> k = 3 

373 >>> bspl = make_interp_spline(x, y, k=k) 

374 >>> design_matrix = bspl.design_matrix(x, bspl.t, k) 

375 >>> design_matrix.toarray() 

376 [[1. , 0. , 0. , 0. ], 

377 [0.2962963 , 0.44444444, 0.22222222, 0.03703704], 

378 [0.03703704, 0.22222222, 0.44444444, 0.2962963 ], 

379 [0. , 0. , 0. , 1. ]] 

380 

381 Construct a design matrix for some vector of knots 

382 

383 >>> k = 2 

384 >>> t = [-1, 0, 1, 2, 3, 4, 5, 6] 

385 >>> x = [1, 2, 3, 4] 

386 >>> design_matrix = BSpline.design_matrix(x, t, k).toarray() 

387 >>> design_matrix 

388 [[0.5, 0.5, 0. , 0. , 0. ], 

389 [0. , 0.5, 0.5, 0. , 0. ], 

390 [0. , 0. , 0.5, 0.5, 0. ], 

391 [0. , 0. , 0. , 0.5, 0.5]] 

392 

393 This result is equivalent to the one created in the sparse format 

394 

395 >>> c = np.eye(len(t) - k - 1) 

396 >>> design_matrix_gh = BSpline(t, c, k)(x) 

397 >>> np.allclose(design_matrix, design_matrix_gh, atol=1e-14) 

398 True 

399 

400 Notes 

401 ----- 

402 .. versionadded:: 1.8.0 

403 

404 In each row of the design matrix all the basis elements are evaluated 

405 at the certain point (first row - x[0], ..., last row - x[-1]). 

406 

407 `nt` is a length of the vector of knots: as far as there are 

408 `nt - k - 1` basis elements, `nt` should be not less than `2 * k + 2` 

409 to have at least `k + 1` basis element. 

410 

411 Out of bounds `x` raises a ValueError. 

412 """ 

413 x = _as_float_array(x, True) 

414 t = _as_float_array(t, True) 

415 

416 if extrapolate != 'periodic': 

417 extrapolate = bool(extrapolate) 

418 

419 if k < 0: 

420 raise ValueError("Spline order cannot be negative.") 

421 if t.ndim != 1 or np.any(t[1:] < t[:-1]): 

422 raise ValueError(f"Expect t to be a 1-D sorted array_like, but " 

423 f"got t={t}.") 

424 # There are `nt - k - 1` basis elements in a BSpline built on the 

425 # vector of knots with length `nt`, so to have at least `k + 1` basis 

426 # elements we need to have at least `2 * k + 2` elements in the vector 

427 # of knots. 

428 if len(t) < 2 * k + 2: 

429 raise ValueError(f"Length t is not enough for k={k}.") 

430 

431 if extrapolate == 'periodic': 

432 # With periodic extrapolation we map x to the segment 

433 # [t[k], t[n]]. 

434 n = t.size - k - 1 

435 x = t[k] + (x - t[k]) % (t[n] - t[k]) 

436 extrapolate = False 

437 elif not extrapolate and ( 

438 (min(x) < t[k]) or (max(x) > t[t.shape[0] - k - 1]) 

439 ): 

440 # Checks from `find_interval` function 

441 raise ValueError(f'Out of bounds w/ x = {x}.') 

442 

443 # Compute number of non-zeros of final CSR array in order to determine 

444 # the dtype of indices and indptr of the CSR array. 

445 n = x.shape[0] 

446 nnz = n * (k + 1) 

447 if nnz < np.iinfo(np.int32).max: 

448 int_dtype = np.int32 

449 else: 

450 int_dtype = np.int64 

451 # Preallocate indptr and indices 

452 indices = np.empty(n * (k + 1), dtype=int_dtype) 

453 indptr = np.arange(0, (n + 1) * (k + 1), k + 1, dtype=int_dtype) 

454 

455 # indptr is not passed to Cython as it is already fully computed 

456 data, indices = _bspl._make_design_matrix( 

457 x, t, k, extrapolate, indices 

458 ) 

459 return csr_array( 

460 (data, indices, indptr), 

461 shape=(x.shape[0], t.shape[0] - k - 1) 

462 ) 

463 

464 def __call__(self, x, nu=0, extrapolate=None): 

465 """ 

466 Evaluate a spline function. 

467 

468 Parameters 

469 ---------- 

470 x : array_like 

471 points to evaluate the spline at. 

472 nu : int, optional 

473 derivative to evaluate (default is 0). 

474 extrapolate : bool or 'periodic', optional 

475 whether to extrapolate based on the first and last intervals 

476 or return nans. If 'periodic', periodic extrapolation is used. 

477 Default is `self.extrapolate`. 

478 

479 Returns 

480 ------- 

481 y : array_like 

482 Shape is determined by replacing the interpolation axis 

483 in the coefficient array with the shape of `x`. 

484 

485 """ 

486 if extrapolate is None: 

487 extrapolate = self.extrapolate 

488 x = np.asarray(x) 

489 x_shape, x_ndim = x.shape, x.ndim 

490 x = np.ascontiguousarray(x.ravel(), dtype=np.float_) 

491 

492 # With periodic extrapolation we map x to the segment 

493 # [self.t[k], self.t[n]]. 

494 if extrapolate == 'periodic': 

495 n = self.t.size - self.k - 1 

496 x = self.t[self.k] + (x - self.t[self.k]) % (self.t[n] - 

497 self.t[self.k]) 

498 extrapolate = False 

499 

500 out = np.empty((len(x), prod(self.c.shape[1:])), dtype=self.c.dtype) 

501 self._ensure_c_contiguous() 

502 self._evaluate(x, nu, extrapolate, out) 

503 out = out.reshape(x_shape + self.c.shape[1:]) 

504 if self.axis != 0: 

505 # transpose to move the calculated values to the interpolation axis 

506 l = list(range(out.ndim)) 

507 l = l[x_ndim:x_ndim+self.axis] + l[:x_ndim] + l[x_ndim+self.axis:] 

508 out = out.transpose(l) 

509 return out 

510 

511 def _evaluate(self, xp, nu, extrapolate, out): 

512 _bspl.evaluate_spline(self.t, self.c.reshape(self.c.shape[0], -1), 

513 self.k, xp, nu, extrapolate, out) 

514 

515 def _ensure_c_contiguous(self): 

516 """ 

517 c and t may be modified by the user. The Cython code expects 

518 that they are C contiguous. 

519 

520 """ 

521 if not self.t.flags.c_contiguous: 

522 self.t = self.t.copy() 

523 if not self.c.flags.c_contiguous: 

524 self.c = self.c.copy() 

525 

526 def derivative(self, nu=1): 

527 """Return a B-spline representing the derivative. 

528 

529 Parameters 

530 ---------- 

531 nu : int, optional 

532 Derivative order. 

533 Default is 1. 

534 

535 Returns 

536 ------- 

537 b : BSpline object 

538 A new instance representing the derivative. 

539 

540 See Also 

541 -------- 

542 splder, splantider 

543 

544 """ 

545 c = self.c 

546 # pad the c array if needed 

547 ct = len(self.t) - len(c) 

548 if ct > 0: 

549 c = np.r_[c, np.zeros((ct,) + c.shape[1:])] 

550 tck = _fitpack_impl.splder((self.t, c, self.k), nu) 

551 return self.construct_fast(*tck, extrapolate=self.extrapolate, 

552 axis=self.axis) 

553 

554 def antiderivative(self, nu=1): 

555 """Return a B-spline representing the antiderivative. 

556 

557 Parameters 

558 ---------- 

559 nu : int, optional 

560 Antiderivative order. Default is 1. 

561 

562 Returns 

563 ------- 

564 b : BSpline object 

565 A new instance representing the antiderivative. 

566 

567 Notes 

568 ----- 

569 If antiderivative is computed and ``self.extrapolate='periodic'``, 

570 it will be set to False for the returned instance. This is done because 

571 the antiderivative is no longer periodic and its correct evaluation 

572 outside of the initially given x interval is difficult. 

573 

574 See Also 

575 -------- 

576 splder, splantider 

577 

578 """ 

579 c = self.c 

580 # pad the c array if needed 

581 ct = len(self.t) - len(c) 

582 if ct > 0: 

583 c = np.r_[c, np.zeros((ct,) + c.shape[1:])] 

584 tck = _fitpack_impl.splantider((self.t, c, self.k), nu) 

585 

586 if self.extrapolate == 'periodic': 

587 extrapolate = False 

588 else: 

589 extrapolate = self.extrapolate 

590 

591 return self.construct_fast(*tck, extrapolate=extrapolate, 

592 axis=self.axis) 

593 

594 def integrate(self, a, b, extrapolate=None): 

595 """Compute a definite integral of the spline. 

596 

597 Parameters 

598 ---------- 

599 a : float 

600 Lower limit of integration. 

601 b : float 

602 Upper limit of integration. 

603 extrapolate : bool or 'periodic', optional 

604 whether to extrapolate beyond the base interval, 

605 ``t[k] .. t[-k-1]``, or take the spline to be zero outside of the 

606 base interval. If 'periodic', periodic extrapolation is used. 

607 If None (default), use `self.extrapolate`. 

608 

609 Returns 

610 ------- 

611 I : array_like 

612 Definite integral of the spline over the interval ``[a, b]``. 

613 

614 Examples 

615 -------- 

616 Construct the linear spline ``x if x < 1 else 2 - x`` on the base 

617 interval :math:`[0, 2]`, and integrate it 

618 

619 >>> from scipy.interpolate import BSpline 

620 >>> b = BSpline.basis_element([0, 1, 2]) 

621 >>> b.integrate(0, 1) 

622 array(0.5) 

623 

624 If the integration limits are outside of the base interval, the result 

625 is controlled by the `extrapolate` parameter 

626 

627 >>> b.integrate(-1, 1) 

628 array(0.0) 

629 >>> b.integrate(-1, 1, extrapolate=False) 

630 array(0.5) 

631 

632 >>> import matplotlib.pyplot as plt 

633 >>> fig, ax = plt.subplots() 

634 >>> ax.grid(True) 

635 >>> ax.axvline(0, c='r', lw=5, alpha=0.5) # base interval 

636 >>> ax.axvline(2, c='r', lw=5, alpha=0.5) 

637 >>> xx = [-1, 1, 2] 

638 >>> ax.plot(xx, b(xx)) 

639 >>> plt.show() 

640 

641 """ 

642 if extrapolate is None: 

643 extrapolate = self.extrapolate 

644 

645 # Prepare self.t and self.c. 

646 self._ensure_c_contiguous() 

647 

648 # Swap integration bounds if needed. 

649 sign = 1 

650 if b < a: 

651 a, b = b, a 

652 sign = -1 

653 n = self.t.size - self.k - 1 

654 

655 if extrapolate != "periodic" and not extrapolate: 

656 # Shrink the integration interval, if needed. 

657 a = max(a, self.t[self.k]) 

658 b = min(b, self.t[n]) 

659 

660 if self.c.ndim == 1: 

661 # Fast path: use FITPACK's routine 

662 # (cf _fitpack_impl.splint). 

663 integral = _fitpack_impl.splint(a, b, self.tck) 

664 return integral * sign 

665 

666 out = np.empty((2, prod(self.c.shape[1:])), dtype=self.c.dtype) 

667 

668 # Compute the antiderivative. 

669 c = self.c 

670 ct = len(self.t) - len(c) 

671 if ct > 0: 

672 c = np.r_[c, np.zeros((ct,) + c.shape[1:])] 

673 ta, ca, ka = _fitpack_impl.splantider((self.t, c, self.k), 1) 

674 

675 if extrapolate == 'periodic': 

676 # Split the integral into the part over period (can be several 

677 # of them) and the remaining part. 

678 

679 ts, te = self.t[self.k], self.t[n] 

680 period = te - ts 

681 interval = b - a 

682 n_periods, left = divmod(interval, period) 

683 

684 if n_periods > 0: 

685 # Evaluate the difference of antiderivatives. 

686 x = np.asarray([ts, te], dtype=np.float_) 

687 _bspl.evaluate_spline(ta, ca.reshape(ca.shape[0], -1), 

688 ka, x, 0, False, out) 

689 integral = out[1] - out[0] 

690 integral *= n_periods 

691 else: 

692 integral = np.zeros((1, prod(self.c.shape[1:])), 

693 dtype=self.c.dtype) 

694 

695 # Map a to [ts, te], b is always a + left. 

696 a = ts + (a - ts) % period 

697 b = a + left 

698 

699 # If b <= te then we need to integrate over [a, b], otherwise 

700 # over [a, te] and from xs to what is remained. 

701 if b <= te: 

702 x = np.asarray([a, b], dtype=np.float_) 

703 _bspl.evaluate_spline(ta, ca.reshape(ca.shape[0], -1), 

704 ka, x, 0, False, out) 

705 integral += out[1] - out[0] 

706 else: 

707 x = np.asarray([a, te], dtype=np.float_) 

708 _bspl.evaluate_spline(ta, ca.reshape(ca.shape[0], -1), 

709 ka, x, 0, False, out) 

710 integral += out[1] - out[0] 

711 

712 x = np.asarray([ts, ts + b - te], dtype=np.float_) 

713 _bspl.evaluate_spline(ta, ca.reshape(ca.shape[0], -1), 

714 ka, x, 0, False, out) 

715 integral += out[1] - out[0] 

716 else: 

717 # Evaluate the difference of antiderivatives. 

718 x = np.asarray([a, b], dtype=np.float_) 

719 _bspl.evaluate_spline(ta, ca.reshape(ca.shape[0], -1), 

720 ka, x, 0, extrapolate, out) 

721 integral = out[1] - out[0] 

722 

723 integral *= sign 

724 return integral.reshape(ca.shape[1:]) 

725 

726 @classmethod 

727 def from_power_basis(cls, pp, bc_type='not-a-knot'): 

728 r""" 

729 Construct a polynomial in the B-spline basis 

730 from a piecewise polynomial in the power basis. 

731 

732 For now, accepts ``CubicSpline`` instances only. 

733 

734 Parameters 

735 ---------- 

736 pp : CubicSpline 

737 A piecewise polynomial in the power basis, as created 

738 by ``CubicSpline`` 

739 bc_type : string, optional 

740 Boundary condition type as in ``CubicSpline``: one of the 

741 ``not-a-knot``, ``natural``, ``clamped``, or ``periodic``. 

742 Necessary for construction an instance of ``BSpline`` class. 

743 Default is ``not-a-knot``. 

744 

745 Returns 

746 ------- 

747 b : BSpline object 

748 A new instance representing the initial polynomial 

749 in the B-spline basis. 

750 

751 Notes 

752 ----- 

753 .. versionadded:: 1.8.0 

754 

755 Accepts only ``CubicSpline`` instances for now. 

756 

757 The algorithm follows from differentiation 

758 the Marsden's identity [1]: each of coefficients of spline 

759 interpolation function in the B-spline basis is computed as follows: 

760 

761 .. math:: 

762 

763 c_j = \sum_{m=0}^{k} \frac{(k-m)!}{k!} 

764 c_{m,i} (-1)^{k-m} D^m p_{j,k}(x_i) 

765 

766 :math:`c_{m, i}` - a coefficient of CubicSpline, 

767 :math:`D^m p_{j, k}(x_i)` - an m-th defivative of a dual polynomial 

768 in :math:`x_i`. 

769 

770 ``k`` always equals 3 for now. 

771 

772 First ``n - 2`` coefficients are computed in :math:`x_i = x_j`, e.g. 

773 

774 .. math:: 

775 

776 c_1 = \sum_{m=0}^{k} \frac{(k-1)!}{k!} c_{m,1} D^m p_{j,3}(x_1) 

777 

778 Last ``nod + 2`` coefficients are computed in ``x[-2]``, 

779 ``nod`` - number of derivatives at the ends. 

780 

781 For example, consider :math:`x = [0, 1, 2, 3, 4]`, 

782 :math:`y = [1, 1, 1, 1, 1]` and bc_type = ``natural`` 

783 

784 The coefficients of CubicSpline in the power basis: 

785 

786 :math:`[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], 

787 [0, 0, 0, 0, 0], [1, 1, 1, 1, 1]]` 

788 

789 The knot vector: :math:`t = [0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4]` 

790 

791 In this case 

792 

793 .. math:: 

794 

795 c_j = \frac{0!}{k!} c_{3, i} k! = c_{3, i} = 1,~j = 0, ..., 6 

796 

797 References 

798 ---------- 

799 .. [1] Tom Lyche and Knut Morken, Spline Methods, 2005, Section 3.1.2 

800 

801 """ 

802 from ._cubic import CubicSpline 

803 if not isinstance(pp, CubicSpline): 

804 raise NotImplementedError("Only CubicSpline objects are accepted" 

805 "for now. Got %s instead." % type(pp)) 

806 x = pp.x 

807 coef = pp.c 

808 k = pp.c.shape[0] - 1 

809 n = x.shape[0] 

810 

811 if bc_type == 'not-a-knot': 

812 t = _not_a_knot(x, k) 

813 elif bc_type == 'natural' or bc_type == 'clamped': 

814 t = _augknt(x, k) 

815 elif bc_type == 'periodic': 

816 t = _periodic_knots(x, k) 

817 else: 

818 raise TypeError('Unknown boundary condition: %s' % bc_type) 

819 

820 nod = t.shape[0] - (n + k + 1) # number of derivatives at the ends 

821 c = np.zeros(n + nod, dtype=pp.c.dtype) 

822 for m in range(k + 1): 

823 for i in range(n - 2): 

824 c[i] += poch(k + 1, -m) * coef[m, i]\ 

825 * np.power(-1, k - m)\ 

826 * _diff_dual_poly(i, k, x[i], m, t) 

827 for j in range(n - 2, n + nod): 

828 c[j] += poch(k + 1, -m) * coef[m, n - 2]\ 

829 * np.power(-1, k - m)\ 

830 * _diff_dual_poly(j, k, x[n - 2], m, t) 

831 return cls.construct_fast(t, c, k, pp.extrapolate, pp.axis) 

832 

833 

834################################# 

835# Interpolating spline helpers # 

836################################# 

837 

838def _not_a_knot(x, k): 

839 """Given data x, construct the knot vector w/ not-a-knot BC. 

840 cf de Boor, XIII(12).""" 

841 x = np.asarray(x) 

842 if k % 2 != 1: 

843 raise ValueError("Odd degree for now only. Got %s." % k) 

844 

845 m = (k - 1) // 2 

846 t = x[m+1:-m-1] 

847 t = np.r_[(x[0],)*(k+1), t, (x[-1],)*(k+1)] 

848 return t 

849 

850 

851def _augknt(x, k): 

852 """Construct a knot vector appropriate for the order-k interpolation.""" 

853 return np.r_[(x[0],)*k, x, (x[-1],)*k] 

854 

855 

856def _convert_string_aliases(deriv, target_shape): 

857 if isinstance(deriv, str): 

858 if deriv == "clamped": 

859 deriv = [(1, np.zeros(target_shape))] 

860 elif deriv == "natural": 

861 deriv = [(2, np.zeros(target_shape))] 

862 else: 

863 raise ValueError("Unknown boundary condition : %s" % deriv) 

864 return deriv 

865 

866 

867def _process_deriv_spec(deriv): 

868 if deriv is not None: 

869 try: 

870 ords, vals = zip(*deriv) 

871 except TypeError as e: 

872 msg = ("Derivatives, `bc_type`, should be specified as a pair of " 

873 "iterables of pairs of (order, value).") 

874 raise ValueError(msg) from e 

875 else: 

876 ords, vals = [], [] 

877 return np.atleast_1d(ords, vals) 

878 

879 

880def _woodbury_algorithm(A, ur, ll, b, k): 

881 ''' 

882 Solve a cyclic banded linear system with upper right 

883 and lower blocks of size ``(k-1) / 2`` using 

884 the Woodbury formula 

885 

886 Parameters 

887 ---------- 

888 A : 2-D array, shape(k, n) 

889 Matrix of diagonals of original matrix (see 

890 ``solve_banded`` documentation). 

891 ur : 2-D array, shape(bs, bs) 

892 Upper right block matrix. 

893 ll : 2-D array, shape(bs, bs) 

894 Lower left block matrix. 

895 b : 1-D array, shape(n,) 

896 Vector of constant terms of the system of linear equations. 

897 k : int 

898 B-spline degree. 

899 

900 Returns 

901 ------- 

902 c : 1-D array, shape(n,) 

903 Solution of the original system of linear equations. 

904 

905 Notes 

906 ----- 

907 This algorithm works only for systems with banded matrix A plus 

908 a correction term U @ V.T, where the matrix U @ V.T gives upper right 

909 and lower left block of A 

910 The system is solved with the following steps: 

911 1. New systems of linear equations are constructed: 

912 A @ z_i = u_i, 

913 u_i - columnn vector of U, 

914 i = 1, ..., k - 1 

915 2. Matrix Z is formed from vectors z_i: 

916 Z = [ z_1 | z_2 | ... | z_{k - 1} ] 

917 3. Matrix H = (1 + V.T @ Z)^{-1} 

918 4. The system A' @ y = b is solved 

919 5. x = y - Z @ (H @ V.T @ y) 

920 Also, ``n`` should be greater than ``k``, otherwise corner block 

921 elements will intersect with diagonals. 

922 

923 Examples 

924 -------- 

925 Consider the case of n = 8, k = 5 (size of blocks - 2 x 2). 

926 The matrix of a system: U: V: 

927 x x x * * a b a b 0 0 0 0 1 0 

928 x x x x * * c 0 c 0 0 0 0 0 1 

929 x x x x x * * 0 0 0 0 0 0 0 0 

930 * x x x x x * 0 0 0 0 0 0 0 0 

931 * * x x x x x 0 0 0 0 0 0 0 0 

932 d * * x x x x 0 0 d 0 1 0 0 0 

933 e f * * x x x 0 0 e f 0 1 0 0 

934 

935 References 

936 ---------- 

937 .. [1] William H. Press, Saul A. Teukolsky, William T. Vetterling 

938 and Brian P. Flannery, Numerical Recipes, 2007, Section 2.7.3 

939 

940 ''' 

941 k_mod = k - k % 2 

942 bs = int((k - 1) / 2) + (k + 1) % 2 

943 

944 n = A.shape[1] + 1 

945 U = np.zeros((n - 1, k_mod)) 

946 VT = np.zeros((k_mod, n - 1)) # V transpose 

947 

948 # upper right block 

949 U[:bs, :bs] = ur 

950 VT[np.arange(bs), np.arange(bs) - bs] = 1 

951 

952 # lower left block 

953 U[-bs:, -bs:] = ll 

954 VT[np.arange(bs) - bs, np.arange(bs)] = 1 

955 

956 Z = solve_banded((bs, bs), A, U) 

957 

958 H = solve(np.identity(k_mod) + VT @ Z, np.identity(k_mod)) 

959 

960 y = solve_banded((bs, bs), A, b) 

961 c = y - Z @ (H @ (VT @ y)) 

962 

963 return c 

964 

965 

966def _periodic_knots(x, k): 

967 ''' 

968 returns vector of nodes on circle 

969 ''' 

970 xc = np.copy(x) 

971 n = len(xc) 

972 if k % 2 == 0: 

973 dx = np.diff(xc) 

974 xc[1: -1] -= dx[:-1] / 2 

975 dx = np.diff(xc) 

976 t = np.zeros(n + 2 * k) 

977 t[k: -k] = xc 

978 for i in range(0, k): 

979 # filling first `k` elements in descending order 

980 t[k - i - 1] = t[k - i] - dx[-(i % (n - 1)) - 1] 

981 # filling last `k` elements in ascending order 

982 t[-k + i] = t[-k + i - 1] + dx[i % (n - 1)] 

983 return t 

984 

985 

986def _make_interp_per_full_matr(x, y, t, k): 

987 ''' 

988 Returns a solution of a system for B-spline interpolation with periodic 

989 boundary conditions. First ``k - 1`` rows of matrix are condtions of 

990 periodicity (continuity of ``k - 1`` derivatives at the boundary points). 

991 Last ``n`` rows are interpolation conditions. 

992 RHS is ``k - 1`` zeros and ``n`` ordinates in this case. 

993 

994 Parameters 

995 ---------- 

996 x : 1-D array, shape (n,) 

997 Values of x - coordinate of a given set of points. 

998 y : 1-D array, shape (n,) 

999 Values of y - coordinate of a given set of points. 

1000 t : 1-D array, shape(n+2*k,) 

1001 Vector of knots. 

1002 k : int 

1003 The maximum degree of spline 

1004 

1005 Returns 

1006 ------- 

1007 c : 1-D array, shape (n+k-1,) 

1008 B-spline coefficients 

1009 

1010 Notes 

1011 ----- 

1012 ``t`` is supposed to be taken on circle. 

1013 

1014 ''' 

1015 

1016 x, y, t = map(np.asarray, (x, y, t)) 

1017 

1018 n = x.size 

1019 # LHS: the collocation matrix + derivatives at edges 

1020 matr = np.zeros((n + k - 1, n + k - 1)) 

1021 

1022 # derivatives at x[0] and x[-1]: 

1023 for i in range(k - 1): 

1024 bb = _bspl.evaluate_all_bspl(t, k, x[0], k, nu=i + 1) 

1025 matr[i, : k + 1] += bb 

1026 bb = _bspl.evaluate_all_bspl(t, k, x[-1], n + k - 1, nu=i + 1)[:-1] 

1027 matr[i, -k:] -= bb 

1028 

1029 # collocation matrix 

1030 for i in range(n): 

1031 xval = x[i] 

1032 # find interval 

1033 if xval == t[k]: 

1034 left = k 

1035 else: 

1036 left = np.searchsorted(t, xval) - 1 

1037 

1038 # fill a row 

1039 bb = _bspl.evaluate_all_bspl(t, k, xval, left) 

1040 matr[i + k - 1, left-k:left+1] = bb 

1041 

1042 # RHS 

1043 b = np.r_[[0] * (k - 1), y] 

1044 

1045 c = solve(matr, b) 

1046 return c 

1047 

1048 

1049def _make_periodic_spline(x, y, t, k, axis): 

1050 ''' 

1051 Compute the (coefficients of) interpolating B-spline with periodic 

1052 boundary conditions. 

1053 

1054 Parameters 

1055 ---------- 

1056 x : array_like, shape (n,) 

1057 Abscissas. 

1058 y : array_like, shape (n,) 

1059 Ordinates. 

1060 k : int 

1061 B-spline degree. 

1062 t : array_like, shape (n + 2 * k,). 

1063 Knots taken on a circle, ``k`` on the left and ``k`` on the right 

1064 of the vector ``x``. 

1065 

1066 Returns 

1067 ------- 

1068 b : a BSpline object of the degree ``k`` and with knots ``t``. 

1069 

1070 Notes 

1071 ----- 

1072 The original system is formed by ``n + k - 1`` equations where the first 

1073 ``k - 1`` of them stand for the ``k - 1`` derivatives continuity on the 

1074 edges while the other equations correspond to an interpolating case 

1075 (matching all the input points). Due to a special form of knot vector, it 

1076 can be proved that in the original system the first and last ``k`` 

1077 coefficients of a spline function are the same, respectively. It follows 

1078 from the fact that all ``k - 1`` derivatives are equal term by term at ends 

1079 and that the matrix of the original system of linear equations is 

1080 non-degenerate. So, we can reduce the number of equations to ``n - 1`` 

1081 (first ``k - 1`` equations could be reduced). Another trick of this 

1082 implementation is cyclic shift of values of B-splines due to equality of 

1083 ``k`` unknown coefficients. With this we can receive matrix of the system 

1084 with upper right and lower left blocks, and ``k`` diagonals. It allows 

1085 to use Woodbury formula to optimize the computations. 

1086 

1087 ''' 

1088 n = y.shape[0] 

1089 

1090 extradim = prod(y.shape[1:]) 

1091 y_new = y.reshape(n, extradim) 

1092 c = np.zeros((n + k - 1, extradim)) 

1093 

1094 # n <= k case is solved with full matrix 

1095 if n <= k: 

1096 for i in range(extradim): 

1097 c[:, i] = _make_interp_per_full_matr(x, y_new[:, i], t, k) 

1098 c = np.ascontiguousarray(c.reshape((n + k - 1,) + y.shape[1:])) 

1099 return BSpline.construct_fast(t, c, k, extrapolate='periodic', axis=axis) 

1100 

1101 nt = len(t) - k - 1 

1102 

1103 # size of block elements 

1104 kul = int(k / 2) 

1105 

1106 # kl = ku = k 

1107 ab = np.zeros((3 * k + 1, nt), dtype=np.float_, order='F') 

1108 

1109 # upper right and lower left blocks 

1110 ur = np.zeros((kul, kul)) 

1111 ll = np.zeros_like(ur) 

1112 

1113 # `offset` is made to shift all the non-zero elements to the end of the 

1114 # matrix 

1115 _bspl._colloc(x, t, k, ab, offset=k) 

1116 

1117 # remove zeros before the matrix 

1118 ab = ab[-k - (k + 1) % 2:, :] 

1119 

1120 # The least elements in rows (except repetitions) are diagonals 

1121 # of block matrices. Upper right matrix is an upper triangular 

1122 # matrix while lower left is a lower triangular one. 

1123 for i in range(kul): 

1124 ur += np.diag(ab[-i - 1, i: kul], k=i) 

1125 ll += np.diag(ab[i, -kul - (k % 2): n - 1 + 2 * kul - i], k=-i) 

1126 

1127 # remove elements that occur in the last point 

1128 # (first and last points are equivalent) 

1129 A = ab[:, kul: -k + kul] 

1130 

1131 for i in range(extradim): 

1132 cc = _woodbury_algorithm(A, ur, ll, y_new[:, i][:-1], k) 

1133 c[:, i] = np.concatenate((cc[-kul:], cc, cc[:kul + k % 2])) 

1134 c = np.ascontiguousarray(c.reshape((n + k - 1,) + y.shape[1:])) 

1135 return BSpline.construct_fast(t, c, k, extrapolate='periodic', axis=axis) 

1136 

1137 

1138def make_interp_spline(x, y, k=3, t=None, bc_type=None, axis=0, 

1139 check_finite=True): 

1140 """Compute the (coefficients of) interpolating B-spline. 

1141 

1142 Parameters 

1143 ---------- 

1144 x : array_like, shape (n,) 

1145 Abscissas. 

1146 y : array_like, shape (n, ...) 

1147 Ordinates. 

1148 k : int, optional 

1149 B-spline degree. Default is cubic, ``k = 3``. 

1150 t : array_like, shape (nt + k + 1,), optional. 

1151 Knots. 

1152 The number of knots needs to agree with the number of data points and 

1153 the number of derivatives at the edges. Specifically, ``nt - n`` must 

1154 equal ``len(deriv_l) + len(deriv_r)``. 

1155 bc_type : 2-tuple or None 

1156 Boundary conditions. 

1157 Default is None, which means choosing the boundary conditions 

1158 automatically. Otherwise, it must be a length-two tuple where the first 

1159 element (``deriv_l``) sets the boundary conditions at ``x[0]`` and 

1160 the second element (``deriv_r``) sets the boundary conditions at 

1161 ``x[-1]``. Each of these must be an iterable of pairs 

1162 ``(order, value)`` which gives the values of derivatives of specified 

1163 orders at the given edge of the interpolation interval. 

1164 Alternatively, the following string aliases are recognized: 

1165 

1166 * ``"clamped"``: The first derivatives at the ends are zero. This is 

1167 equivalent to ``bc_type=([(1, 0.0)], [(1, 0.0)])``. 

1168 * ``"natural"``: The second derivatives at ends are zero. This is 

1169 equivalent to ``bc_type=([(2, 0.0)], [(2, 0.0)])``. 

1170 * ``"not-a-knot"`` (default): The first and second segments are the 

1171 same polynomial. This is equivalent to having ``bc_type=None``. 

1172 * ``"periodic"``: The values and the first ``k-1`` derivatives at the 

1173 ends are equivalent. 

1174 

1175 axis : int, optional 

1176 Interpolation axis. Default is 0. 

1177 check_finite : bool, optional 

1178 Whether to check that the input arrays contain only finite numbers. 

1179 Disabling may give a performance gain, but may result in problems 

1180 (crashes, non-termination) if the inputs do contain infinities or NaNs. 

1181 Default is True. 

1182 

1183 Returns 

1184 ------- 

1185 b : a BSpline object of the degree ``k`` and with knots ``t``. 

1186 

1187 See Also 

1188 -------- 

1189 BSpline : base class representing the B-spline objects 

1190 CubicSpline : a cubic spline in the polynomial basis 

1191 make_lsq_spline : a similar factory function for spline fitting 

1192 UnivariateSpline : a wrapper over FITPACK spline fitting routines 

1193 splrep : a wrapper over FITPACK spline fitting routines 

1194 

1195 Examples 

1196 -------- 

1197 

1198 Use cubic interpolation on Chebyshev nodes: 

1199 

1200 >>> import numpy as np 

1201 >>> import matplotlib.pyplot as plt 

1202 >>> def cheb_nodes(N): 

1203 ... jj = 2.*np.arange(N) + 1 

1204 ... x = np.cos(np.pi * jj / 2 / N)[::-1] 

1205 ... return x 

1206 

1207 >>> x = cheb_nodes(20) 

1208 >>> y = np.sqrt(1 - x**2) 

1209 

1210 >>> from scipy.interpolate import BSpline, make_interp_spline 

1211 >>> b = make_interp_spline(x, y) 

1212 >>> np.allclose(b(x), y) 

1213 True 

1214 

1215 Note that the default is a cubic spline with a not-a-knot boundary condition 

1216 

1217 >>> b.k 

1218 3 

1219 

1220 Here we use a 'natural' spline, with zero 2nd derivatives at edges: 

1221 

1222 >>> l, r = [(2, 0.0)], [(2, 0.0)] 

1223 >>> b_n = make_interp_spline(x, y, bc_type=(l, r)) # or, bc_type="natural" 

1224 >>> np.allclose(b_n(x), y) 

1225 True 

1226 >>> x0, x1 = x[0], x[-1] 

1227 >>> np.allclose([b_n(x0, 2), b_n(x1, 2)], [0, 0]) 

1228 True 

1229 

1230 Interpolation of parametric curves is also supported. As an example, we 

1231 compute a discretization of a snail curve in polar coordinates 

1232 

1233 >>> phi = np.linspace(0, 2.*np.pi, 40) 

1234 >>> r = 0.3 + np.cos(phi) 

1235 >>> x, y = r*np.cos(phi), r*np.sin(phi) # convert to Cartesian coordinates 

1236 

1237 Build an interpolating curve, parameterizing it by the angle 

1238 

1239 >>> spl = make_interp_spline(phi, np.c_[x, y]) 

1240 

1241 Evaluate the interpolant on a finer grid (note that we transpose the result 

1242 to unpack it into a pair of x- and y-arrays) 

1243 

1244 >>> phi_new = np.linspace(0, 2.*np.pi, 100) 

1245 >>> x_new, y_new = spl(phi_new).T 

1246 

1247 Plot the result 

1248 

1249 >>> plt.plot(x, y, 'o') 

1250 >>> plt.plot(x_new, y_new, '-') 

1251 >>> plt.show() 

1252 

1253 Build a B-spline curve with 2 dimensional y 

1254 

1255 >>> x = np.linspace(0, 2*np.pi, 10) 

1256 >>> y = np.array([np.sin(x), np.cos(x)]) 

1257 

1258 Periodic condition is satisfied because y coordinates of points on the ends 

1259 are equivalent 

1260 

1261 >>> ax = plt.axes(projection='3d') 

1262 >>> xx = np.linspace(0, 2*np.pi, 100) 

1263 >>> bspl = make_interp_spline(x, y, k=5, bc_type='periodic', axis=1) 

1264 >>> ax.plot3D(xx, *bspl(xx)) 

1265 >>> ax.scatter3D(x, *y, color='red') 

1266 >>> plt.show() 

1267 

1268 """ 

1269 # convert string aliases for the boundary conditions 

1270 if bc_type is None or bc_type == 'not-a-knot' or bc_type == 'periodic': 

1271 deriv_l, deriv_r = None, None 

1272 elif isinstance(bc_type, str): 

1273 deriv_l, deriv_r = bc_type, bc_type 

1274 else: 

1275 try: 

1276 deriv_l, deriv_r = bc_type 

1277 except TypeError as e: 

1278 raise ValueError("Unknown boundary condition: %s" % bc_type) from e 

1279 

1280 y = np.asarray(y) 

1281 

1282 axis = normalize_axis_index(axis, y.ndim) 

1283 

1284 x = _as_float_array(x, check_finite) 

1285 y = _as_float_array(y, check_finite) 

1286 

1287 y = np.moveaxis(y, axis, 0) # now internally interp axis is zero 

1288 

1289 # sanity check the input 

1290 if bc_type == 'periodic' and not np.allclose(y[0], y[-1], atol=1e-15): 

1291 raise ValueError("First and last points does not match while " 

1292 "periodic case expected") 

1293 if x.size != y.shape[0]: 

1294 raise ValueError('Shapes of x {} and y {} are incompatible' 

1295 .format(x.shape, y.shape)) 

1296 if np.any(x[1:] == x[:-1]): 

1297 raise ValueError("Expect x to not have duplicates") 

1298 if x.ndim != 1 or np.any(x[1:] < x[:-1]): 

1299 raise ValueError("Expect x to be a 1D strictly increasing sequence.") 

1300 

1301 # special-case k=0 right away 

1302 if k == 0: 

1303 if any(_ is not None for _ in (t, deriv_l, deriv_r)): 

1304 raise ValueError("Too much info for k=0: t and bc_type can only " 

1305 "be None.") 

1306 t = np.r_[x, x[-1]] 

1307 c = np.asarray(y) 

1308 c = np.ascontiguousarray(c, dtype=_get_dtype(c.dtype)) 

1309 return BSpline.construct_fast(t, c, k, axis=axis) 

1310 

1311 # special-case k=1 (e.g., Lyche and Morken, Eq.(2.16)) 

1312 if k == 1 and t is None: 

1313 if not (deriv_l is None and deriv_r is None): 

1314 raise ValueError("Too much info for k=1: bc_type can only be None.") 

1315 t = np.r_[x[0], x, x[-1]] 

1316 c = np.asarray(y) 

1317 c = np.ascontiguousarray(c, dtype=_get_dtype(c.dtype)) 

1318 return BSpline.construct_fast(t, c, k, axis=axis) 

1319 

1320 k = operator.index(k) 

1321 

1322 if bc_type == 'periodic' and t is not None: 

1323 raise NotImplementedError("For periodic case t is constructed " 

1324 "automatically and can not be passed " 

1325 "manually") 

1326 

1327 # come up with a sensible knot vector, if needed 

1328 if t is None: 

1329 if deriv_l is None and deriv_r is None: 

1330 if bc_type == 'periodic': 

1331 t = _periodic_knots(x, k) 

1332 elif k == 2: 

1333 # OK, it's a bit ad hoc: Greville sites + omit 

1334 # 2nd and 2nd-to-last points, a la not-a-knot 

1335 t = (x[1:] + x[:-1]) / 2. 

1336 t = np.r_[(x[0],)*(k+1), 

1337 t[1:-1], 

1338 (x[-1],)*(k+1)] 

1339 else: 

1340 t = _not_a_knot(x, k) 

1341 else: 

1342 t = _augknt(x, k) 

1343 

1344 t = _as_float_array(t, check_finite) 

1345 

1346 if k < 0: 

1347 raise ValueError("Expect non-negative k.") 

1348 if t.ndim != 1 or np.any(t[1:] < t[:-1]): 

1349 raise ValueError("Expect t to be a 1-D sorted array_like.") 

1350 if t.size < x.size + k + 1: 

1351 raise ValueError('Got %d knots, need at least %d.' % 

1352 (t.size, x.size + k + 1)) 

1353 if (x[0] < t[k]) or (x[-1] > t[-k]): 

1354 raise ValueError('Out of bounds w/ x = %s.' % x) 

1355 

1356 if bc_type == 'periodic': 

1357 return _make_periodic_spline(x, y, t, k, axis) 

1358 

1359 # Here : deriv_l, r = [(nu, value), ...] 

1360 deriv_l = _convert_string_aliases(deriv_l, y.shape[1:]) 

1361 deriv_l_ords, deriv_l_vals = _process_deriv_spec(deriv_l) 

1362 nleft = deriv_l_ords.shape[0] 

1363 

1364 deriv_r = _convert_string_aliases(deriv_r, y.shape[1:]) 

1365 deriv_r_ords, deriv_r_vals = _process_deriv_spec(deriv_r) 

1366 nright = deriv_r_ords.shape[0] 

1367 

1368 # have `n` conditions for `nt` coefficients; need nt-n derivatives 

1369 n = x.size 

1370 nt = t.size - k - 1 

1371 

1372 if nt - n != nleft + nright: 

1373 raise ValueError("The number of derivatives at boundaries does not " 

1374 "match: expected {}, got {}+{}".format(nt-n, nleft, nright)) 

1375 

1376 # bail out if the `y` array is zero-sized 

1377 if y.size == 0: 

1378 c = np.zeros((nt,) + y.shape[1:], dtype=float) 

1379 return BSpline.construct_fast(t, c, k, axis=axis) 

1380 

1381 # set up the LHS: the collocation matrix + derivatives at boundaries 

1382 kl = ku = k 

1383 ab = np.zeros((2*kl + ku + 1, nt), dtype=np.float_, order='F') 

1384 _bspl._colloc(x, t, k, ab, offset=nleft) 

1385 if nleft > 0: 

1386 _bspl._handle_lhs_derivatives(t, k, x[0], ab, kl, ku, deriv_l_ords) 

1387 if nright > 0: 

1388 _bspl._handle_lhs_derivatives(t, k, x[-1], ab, kl, ku, deriv_r_ords, 

1389 offset=nt-nright) 

1390 

1391 # set up the RHS: values to interpolate (+ derivative values, if any) 

1392 extradim = prod(y.shape[1:]) 

1393 rhs = np.empty((nt, extradim), dtype=y.dtype) 

1394 if nleft > 0: 

1395 rhs[:nleft] = deriv_l_vals.reshape(-1, extradim) 

1396 rhs[nleft:nt - nright] = y.reshape(-1, extradim) 

1397 if nright > 0: 

1398 rhs[nt - nright:] = deriv_r_vals.reshape(-1, extradim) 

1399 

1400 # solve Ab @ x = rhs; this is the relevant part of linalg.solve_banded 

1401 if check_finite: 

1402 ab, rhs = map(np.asarray_chkfinite, (ab, rhs)) 

1403 gbsv, = get_lapack_funcs(('gbsv',), (ab, rhs)) 

1404 lu, piv, c, info = gbsv(kl, ku, ab, rhs, 

1405 overwrite_ab=True, overwrite_b=True) 

1406 

1407 if info > 0: 

1408 raise LinAlgError("Collocation matrix is singular.") 

1409 elif info < 0: 

1410 raise ValueError('illegal value in %d-th argument of internal gbsv' % -info) 

1411 

1412 c = np.ascontiguousarray(c.reshape((nt,) + y.shape[1:])) 

1413 return BSpline.construct_fast(t, c, k, axis=axis) 

1414 

1415 

1416def make_lsq_spline(x, y, t, k=3, w=None, axis=0, check_finite=True): 

1417 r"""Compute the (coefficients of) an LSQ (Least SQuared) based 

1418 fitting B-spline. 

1419 

1420 The result is a linear combination 

1421 

1422 .. math:: 

1423 

1424 S(x) = \sum_j c_j B_j(x; t) 

1425 

1426 of the B-spline basis elements, :math:`B_j(x; t)`, which minimizes 

1427 

1428 .. math:: 

1429 

1430 \sum_{j} \left( w_j \times (S(x_j) - y_j) \right)^2 

1431 

1432 Parameters 

1433 ---------- 

1434 x : array_like, shape (m,) 

1435 Abscissas. 

1436 y : array_like, shape (m, ...) 

1437 Ordinates. 

1438 t : array_like, shape (n + k + 1,). 

1439 Knots. 

1440 Knots and data points must satisfy Schoenberg-Whitney conditions. 

1441 k : int, optional 

1442 B-spline degree. Default is cubic, ``k = 3``. 

1443 w : array_like, shape (m,), optional 

1444 Weights for spline fitting. Must be positive. If ``None``, 

1445 then weights are all equal. 

1446 Default is ``None``. 

1447 axis : int, optional 

1448 Interpolation axis. Default is zero. 

1449 check_finite : bool, optional 

1450 Whether to check that the input arrays contain only finite numbers. 

1451 Disabling may give a performance gain, but may result in problems 

1452 (crashes, non-termination) if the inputs do contain infinities or NaNs. 

1453 Default is True. 

1454 

1455 Returns 

1456 ------- 

1457 b : a BSpline object of the degree ``k`` with knots ``t``. 

1458 

1459 See Also 

1460 -------- 

1461 BSpline : base class representing the B-spline objects 

1462 make_interp_spline : a similar factory function for interpolating splines 

1463 LSQUnivariateSpline : a FITPACK-based spline fitting routine 

1464 splrep : a FITPACK-based fitting routine 

1465 

1466 Notes 

1467 ----- 

1468 The number of data points must be larger than the spline degree ``k``. 

1469 

1470 Knots ``t`` must satisfy the Schoenberg-Whitney conditions, 

1471 i.e., there must be a subset of data points ``x[j]`` such that 

1472 ``t[j] < x[j] < t[j+k+1]``, for ``j=0, 1,...,n-k-2``. 

1473 

1474 Examples 

1475 -------- 

1476 Generate some noisy data: 

1477 

1478 >>> import numpy as np 

1479 >>> import matplotlib.pyplot as plt 

1480 >>> rng = np.random.default_rng() 

1481 >>> x = np.linspace(-3, 3, 50) 

1482 >>> y = np.exp(-x**2) + 0.1 * rng.standard_normal(50) 

1483 

1484 Now fit a smoothing cubic spline with a pre-defined internal knots. 

1485 Here we make the knot vector (k+1)-regular by adding boundary knots: 

1486 

1487 >>> from scipy.interpolate import make_lsq_spline, BSpline 

1488 >>> t = [-1, 0, 1] 

1489 >>> k = 3 

1490 >>> t = np.r_[(x[0],)*(k+1), 

1491 ... t, 

1492 ... (x[-1],)*(k+1)] 

1493 >>> spl = make_lsq_spline(x, y, t, k) 

1494 

1495 For comparison, we also construct an interpolating spline for the same 

1496 set of data: 

1497 

1498 >>> from scipy.interpolate import make_interp_spline 

1499 >>> spl_i = make_interp_spline(x, y) 

1500 

1501 Plot both: 

1502 

1503 >>> xs = np.linspace(-3, 3, 100) 

1504 >>> plt.plot(x, y, 'ro', ms=5) 

1505 >>> plt.plot(xs, spl(xs), 'g-', lw=3, label='LSQ spline') 

1506 >>> plt.plot(xs, spl_i(xs), 'b-', lw=3, alpha=0.7, label='interp spline') 

1507 >>> plt.legend(loc='best') 

1508 >>> plt.show() 

1509 

1510 **NaN handling**: If the input arrays contain ``nan`` values, the result is 

1511 not useful since the underlying spline fitting routines cannot deal with 

1512 ``nan``. A workaround is to use zero weights for not-a-number data points: 

1513 

1514 >>> y[8] = np.nan 

1515 >>> w = np.isnan(y) 

1516 >>> y[w] = 0. 

1517 >>> tck = make_lsq_spline(x, y, t, w=~w) 

1518 

1519 Notice the need to replace a ``nan`` by a numerical value (precise value 

1520 does not matter as long as the corresponding weight is zero.) 

1521 

1522 """ 

1523 x = _as_float_array(x, check_finite) 

1524 y = _as_float_array(y, check_finite) 

1525 t = _as_float_array(t, check_finite) 

1526 if w is not None: 

1527 w = _as_float_array(w, check_finite) 

1528 else: 

1529 w = np.ones_like(x) 

1530 k = operator.index(k) 

1531 

1532 axis = normalize_axis_index(axis, y.ndim) 

1533 

1534 y = np.moveaxis(y, axis, 0) # now internally interp axis is zero 

1535 

1536 if x.ndim != 1 or np.any(x[1:] - x[:-1] <= 0): 

1537 raise ValueError("Expect x to be a 1-D sorted array_like.") 

1538 if x.shape[0] < k+1: 

1539 raise ValueError("Need more x points.") 

1540 if k < 0: 

1541 raise ValueError("Expect non-negative k.") 

1542 if t.ndim != 1 or np.any(t[1:] - t[:-1] < 0): 

1543 raise ValueError("Expect t to be a 1-D sorted array_like.") 

1544 if x.size != y.shape[0]: 

1545 raise ValueError('Shapes of x {} and y {} are incompatible' 

1546 .format(x.shape, y.shape)) 

1547 if k > 0 and np.any((x < t[k]) | (x > t[-k])): 

1548 raise ValueError('Out of bounds w/ x = %s.' % x) 

1549 if x.size != w.size: 

1550 raise ValueError('Shapes of x {} and w {} are incompatible' 

1551 .format(x.shape, w.shape)) 

1552 

1553 # number of coefficients 

1554 n = t.size - k - 1 

1555 

1556 # construct A.T @ A and rhs with A the collocation matrix, and 

1557 # rhs = A.T @ y for solving the LSQ problem ``A.T @ A @ c = A.T @ y`` 

1558 lower = True 

1559 extradim = prod(y.shape[1:]) 

1560 ab = np.zeros((k+1, n), dtype=np.float_, order='F') 

1561 rhs = np.zeros((n, extradim), dtype=y.dtype, order='F') 

1562 _bspl._norm_eq_lsq(x, t, k, 

1563 y.reshape(-1, extradim), 

1564 w, 

1565 ab, rhs) 

1566 rhs = rhs.reshape((n,) + y.shape[1:]) 

1567 

1568 # have observation matrix & rhs, can solve the LSQ problem 

1569 cho_decomp = cholesky_banded(ab, overwrite_ab=True, lower=lower, 

1570 check_finite=check_finite) 

1571 c = cho_solve_banded((cho_decomp, lower), rhs, overwrite_b=True, 

1572 check_finite=check_finite) 

1573 

1574 c = np.ascontiguousarray(c) 

1575 return BSpline.construct_fast(t, c, k, axis=axis) 

1576 

1577 

1578############################# 

1579# Smoothing spline helpers # 

1580############################# 

1581 

1582def _compute_optimal_gcv_parameter(X, wE, y, w): 

1583 """ 

1584 Returns an optimal regularization parameter from the GCV criteria [1]. 

1585 

1586 Parameters 

1587 ---------- 

1588 X : array, shape (5, n) 

1589 5 bands of the design matrix ``X`` stored in LAPACK banded storage. 

1590 wE : array, shape (5, n) 

1591 5 bands of the penalty matrix :math:`W^{-1} E` stored in LAPACK banded 

1592 storage. 

1593 y : array, shape (n,) 

1594 Ordinates. 

1595 w : array, shape (n,) 

1596 Vector of weights. 

1597 

1598 Returns 

1599 ------- 

1600 lam : float 

1601 An optimal from the GCV criteria point of view regularization 

1602 parameter. 

1603 

1604 Notes 

1605 ----- 

1606 No checks are performed. 

1607 

1608 References 

1609 ---------- 

1610 .. [1] G. Wahba, "Estimating the smoothing parameter" in Spline models 

1611 for observational data, Philadelphia, Pennsylvania: Society for 

1612 Industrial and Applied Mathematics, 1990, pp. 45-65. 

1613 :doi:`10.1137/1.9781611970128` 

1614 

1615 """ 

1616 

1617 def compute_banded_symmetric_XT_W_Y(X, w, Y): 

1618 """ 

1619 Assuming that the product :math:`X^T W Y` is symmetric and both ``X`` 

1620 and ``Y`` are 5-banded, compute the unique bands of the product. 

1621 

1622 Parameters 

1623 ---------- 

1624 X : array, shape (5, n) 

1625 5 bands of the matrix ``X`` stored in LAPACK banded storage. 

1626 w : array, shape (n,) 

1627 Array of weights 

1628 Y : array, shape (5, n) 

1629 5 bands of the matrix ``Y`` stored in LAPACK banded storage. 

1630 

1631 Returns 

1632 ------- 

1633 res : array, shape (4, n) 

1634 The result of the product :math:`X^T Y` stored in the banded way. 

1635 

1636 Notes 

1637 ----- 

1638 As far as the matrices ``X`` and ``Y`` are 5-banded, their product 

1639 :math:`X^T W Y` is 7-banded. It is also symmetric, so we can store only 

1640 unique diagonals. 

1641 

1642 """ 

1643 # compute W Y 

1644 W_Y = np.copy(Y) 

1645 

1646 W_Y[2] *= w 

1647 for i in range(2): 

1648 W_Y[i, 2 - i:] *= w[:-2 + i] 

1649 W_Y[3 + i, :-1 - i] *= w[1 + i:] 

1650 

1651 n = X.shape[1] 

1652 res = np.zeros((4, n)) 

1653 for i in range(n): 

1654 for j in range(min(n-i, 4)): 

1655 res[-j-1, i + j] = sum(X[j:, i] * W_Y[:5-j, i + j]) 

1656 return res 

1657 

1658 def compute_b_inv(A): 

1659 """ 

1660 Inverse 3 central bands of matrix :math:`A=U^T D^{-1} U` assuming that 

1661 ``U`` is a unit upper triangular banded matrix using an algorithm 

1662 proposed in [1]. 

1663 

1664 Parameters 

1665 ---------- 

1666 A : array, shape (4, n) 

1667 Matrix to inverse, stored in LAPACK banded storage. 

1668 

1669 Returns 

1670 ------- 

1671 B : array, shape (4, n) 

1672 3 unique bands of the symmetric matrix that is an inverse to ``A``. 

1673 The first row is filled with zeros. 

1674 

1675 Notes 

1676 ----- 

1677 The algorithm is based on the cholesky decomposition and, therefore, 

1678 in case matrix ``A`` is close to not positive defined, the function 

1679 raises LinalgError. 

1680 

1681 Both matrices ``A`` and ``B`` are stored in LAPACK banded storage. 

1682 

1683 References 

1684 ---------- 

1685 .. [1] M. F. Hutchinson and F. R. de Hoog, "Smoothing noisy data with 

1686 spline functions," Numerische Mathematik, vol. 47, no. 1, 

1687 pp. 99-106, 1985. 

1688 :doi:`10.1007/BF01389878` 

1689 

1690 """ 

1691 

1692 def find_b_inv_elem(i, j, U, D, B): 

1693 rng = min(3, n - i - 1) 

1694 rng_sum = 0. 

1695 if j == 0: 

1696 # use 2-nd formula from [1] 

1697 for k in range(1, rng + 1): 

1698 rng_sum -= U[-k - 1, i + k] * B[-k - 1, i + k] 

1699 rng_sum += D[i] 

1700 B[-1, i] = rng_sum 

1701 else: 

1702 # use 1-st formula from [1] 

1703 for k in range(1, rng + 1): 

1704 diag = abs(k - j) 

1705 ind = i + min(k, j) 

1706 rng_sum -= U[-k - 1, i + k] * B[-diag - 1, ind + diag] 

1707 B[-j - 1, i + j] = rng_sum 

1708 

1709 U = cholesky_banded(A) 

1710 for i in range(2, 5): 

1711 U[-i, i-1:] /= U[-1, :-i+1] 

1712 D = 1. / (U[-1])**2 

1713 U[-1] /= U[-1] 

1714 

1715 n = U.shape[1] 

1716 

1717 B = np.zeros(shape=(4, n)) 

1718 for i in range(n - 1, -1, -1): 

1719 for j in range(min(3, n - i - 1), -1, -1): 

1720 find_b_inv_elem(i, j, U, D, B) 

1721 # the first row contains garbage and should be removed 

1722 B[0] = [0.] * n 

1723 return B 

1724 

1725 def _gcv(lam, X, XtWX, wE, XtE): 

1726 r""" 

1727 Computes the generalized cross-validation criteria [1]. 

1728 

1729 Parameters 

1730 ---------- 

1731 lam : float, (:math:`\lambda \geq 0`) 

1732 Regularization parameter. 

1733 X : array, shape (5, n) 

1734 Matrix is stored in LAPACK banded storage. 

1735 XtWX : array, shape (4, n) 

1736 Product :math:`X^T W X` stored in LAPACK banded storage. 

1737 wE : array, shape (5, n) 

1738 Matrix :math:`W^{-1} E` stored in LAPACK banded storage. 

1739 XtE : array, shape (4, n) 

1740 Product :math:`X^T E` stored in LAPACK banded storage. 

1741 

1742 Returns 

1743 ------- 

1744 res : float 

1745 Value of the GCV criteria with the regularization parameter 

1746 :math:`\lambda`. 

1747 

1748 Notes 

1749 ----- 

1750 Criteria is computed from the formula (1.3.2) [3]: 

1751 

1752 .. math: 

1753 

1754 GCV(\lambda) = \dfrac{1}{n} \sum\limits_{k = 1}^{n} \dfrac{ \left( 

1755 y_k - f_{\lambda}(x_k) \right)^2}{\left( 1 - \Tr{A}/n\right)^2}$. 

1756 The criteria is discussed in section 1.3 [3]. 

1757 

1758 The numerator is computed using (2.2.4) [3] and the denominator is 

1759 computed using an algorithm from [2] (see in the ``compute_b_inv`` 

1760 function). 

1761 

1762 References 

1763 ---------- 

1764 .. [1] G. Wahba, "Estimating the smoothing parameter" in Spline models 

1765 for observational data, Philadelphia, Pennsylvania: Society for 

1766 Industrial and Applied Mathematics, 1990, pp. 45-65. 

1767 :doi:`10.1137/1.9781611970128` 

1768 .. [2] M. F. Hutchinson and F. R. de Hoog, "Smoothing noisy data with 

1769 spline functions," Numerische Mathematik, vol. 47, no. 1, 

1770 pp. 99-106, 1985. 

1771 :doi:`10.1007/BF01389878` 

1772 .. [3] E. Zemlyanoy, "Generalized cross-validation smoothing splines", 

1773 BSc thesis, 2022. Might be available (in Russian) 

1774 `here <https://www.hse.ru/ba/am/students/diplomas/620910604>`_ 

1775 

1776 """ 

1777 # Compute the numerator from (2.2.4) [3] 

1778 n = X.shape[1] 

1779 c = solve_banded((2, 2), X + lam * wE, y) 

1780 res = np.zeros(n) 

1781 # compute ``W^{-1} E c`` with respect to banded-storage of ``E`` 

1782 tmp = wE * c 

1783 for i in range(n): 

1784 for j in range(max(0, i - n + 3), min(5, i + 3)): 

1785 res[i] += tmp[j, i + 2 - j] 

1786 numer = np.linalg.norm(lam * res)**2 / n 

1787 

1788 # compute the denominator 

1789 lhs = XtWX + lam * XtE 

1790 try: 

1791 b_banded = compute_b_inv(lhs) 

1792 # compute the trace of the product b_banded @ XtX 

1793 tr = b_banded * XtWX 

1794 tr[:-1] *= 2 

1795 # find the denominator 

1796 denom = (1 - sum(sum(tr)) / n)**2 

1797 except LinAlgError: 

1798 # cholesky decomposition cannot be performed 

1799 raise ValueError('Seems like the problem is ill-posed') 

1800 

1801 res = numer / denom 

1802 

1803 return res 

1804 

1805 n = X.shape[1] 

1806 

1807 XtWX = compute_banded_symmetric_XT_W_Y(X, w, X) 

1808 XtE = compute_banded_symmetric_XT_W_Y(X, w, wE) 

1809 

1810 def fun(lam): 

1811 return _gcv(lam, X, XtWX, wE, XtE) 

1812 

1813 gcv_est = minimize_scalar(fun, bounds=(0, n), method='Bounded') 

1814 if gcv_est.success: 

1815 return gcv_est.x 

1816 raise ValueError(f"Unable to find minimum of the GCV " 

1817 f"function: {gcv_est.message}") 

1818 

1819 

1820def _coeff_of_divided_diff(x): 

1821 """ 

1822 Returns the coefficients of the divided difference. 

1823 

1824 Parameters 

1825 ---------- 

1826 x : array, shape (n,) 

1827 Array which is used for the computation of divided difference. 

1828 

1829 Returns 

1830 ------- 

1831 res : array_like, shape (n,) 

1832 Coefficients of the divided difference. 

1833 

1834 Notes 

1835 ----- 

1836 Vector ``x`` should have unique elements, otherwise an error division by 

1837 zero might be raised. 

1838 

1839 No checks are performed. 

1840 

1841 """ 

1842 n = x.shape[0] 

1843 res = np.zeros(n) 

1844 for i in range(n): 

1845 pp = 1. 

1846 for k in range(n): 

1847 if k != i: 

1848 pp *= (x[i] - x[k]) 

1849 res[i] = 1. / pp 

1850 return res 

1851 

1852 

1853def make_smoothing_spline(x, y, w=None, lam=None): 

1854 r""" 

1855 Compute the (coefficients of) smoothing cubic spline function using 

1856 ``lam`` to control the tradeoff between the amount of smoothness of the 

1857 curve and its proximity to the data. In case ``lam`` is None, using the 

1858 GCV criteria [1] to find it. 

1859 

1860 A smoothing spline is found as a solution to the regularized weighted 

1861 linear regression problem: 

1862 

1863 .. math:: 

1864 

1865 \sum\limits_{i=1}^n w_i\lvert y_i - f(x_i) \rvert^2 + 

1866 \lambda\int\limits_{x_1}^{x_n} (f^{(2)}(u))^2 d u 

1867 

1868 where :math:`f` is a spline function, :math:`w` is a vector of weights and 

1869 :math:`\lambda` is a regularization parameter. 

1870 

1871 If ``lam`` is None, we use the GCV criteria to find an optimal 

1872 regularization parameter, otherwise we solve the regularized weighted 

1873 linear regression problem with given parameter. The parameter controls 

1874 the tradeoff in the following way: the larger the parameter becomes, the 

1875 smoother the function gets. 

1876 

1877 Parameters 

1878 ---------- 

1879 x : array_like, shape (n,) 

1880 Abscissas. `n` must be larger than 5. 

1881 y : array_like, shape (n,) 

1882 Ordinates. `n` must be larger than 5. 

1883 w : array_like, shape (n,), optional 

1884 Vector of weights. Default is ``np.ones_like(x)``. 

1885 lam : float, (:math:`\lambda \geq 0`), optional 

1886 Regularization parameter. If ``lam`` is None, then it is found from 

1887 the GCV criteria. Default is None. 

1888 

1889 Returns 

1890 ------- 

1891 func : a BSpline object. 

1892 A callable representing a spline in the B-spline basis 

1893 as a solution of the problem of smoothing splines using 

1894 the GCV criteria [1] in case ``lam`` is None, otherwise using the 

1895 given parameter ``lam``. 

1896 

1897 Notes 

1898 ----- 

1899 This algorithm is a clean room reimplementation of the algorithm 

1900 introduced by Woltring in FORTRAN [2]. The original version cannot be used 

1901 in SciPy source code because of the license issues. The details of the 

1902 reimplementation are discussed here (available only in Russian) [4]. 

1903 

1904 If the vector of weights ``w`` is None, we assume that all the points are 

1905 equal in terms of weights, and vector of weights is vector of ones. 

1906 

1907 Note that in weighted residual sum of squares, weights are not squared: 

1908 :math:`\sum\limits_{i=1}^n w_i\lvert y_i - f(x_i) \rvert^2` while in 

1909 ``splrep`` the sum is built from the squared weights. 

1910 

1911 In cases when the initial problem is ill-posed (for example, the product 

1912 :math:`X^T W X` where :math:`X` is a design matrix is not a positive 

1913 defined matrix) a ValueError is raised. 

1914 

1915 References 

1916 ---------- 

1917 .. [1] G. Wahba, "Estimating the smoothing parameter" in Spline models for 

1918 observational data, Philadelphia, Pennsylvania: Society for Industrial 

1919 and Applied Mathematics, 1990, pp. 45-65. 

1920 :doi:`10.1137/1.9781611970128` 

1921 .. [2] H. J. Woltring, A Fortran package for generalized, cross-validatory 

1922 spline smoothing and differentiation, Advances in Engineering 

1923 Software, vol. 8, no. 2, pp. 104-113, 1986. 

1924 :doi:`10.1016/0141-1195(86)90098-7` 

1925 .. [3] T. Hastie, J. Friedman, and R. Tisbshirani, "Smoothing Splines" in 

1926 The elements of Statistical Learning: Data Mining, Inference, and 

1927 prediction, New York: Springer, 2017, pp. 241-249. 

1928 :doi:`10.1007/978-0-387-84858-7` 

1929 .. [4] E. Zemlyanoy, "Generalized cross-validation smoothing splines", 

1930 BSc thesis, 2022. 

1931 `<https://www.hse.ru/ba/am/students/diplomas/620910604>`_ (in 

1932 Russian) 

1933 

1934 Examples 

1935 -------- 

1936 Generate some noisy data 

1937 

1938 >>> import numpy as np 

1939 >>> np.random.seed(1234) 

1940 >>> n = 200 

1941 >>> def func(x): 

1942 ... return x**3 + x**2 * np.sin(4 * x) 

1943 >>> x = np.sort(np.random.random_sample(n) * 4 - 2) 

1944 >>> y = func(x) + np.random.normal(scale=1.5, size=n) 

1945 

1946 Make a smoothing spline function 

1947 

1948 >>> from scipy.interpolate import make_smoothing_spline 

1949 >>> spl = make_smoothing_spline(x, y) 

1950 

1951 Plot both 

1952 

1953 >>> import matplotlib.pyplot as plt 

1954 >>> grid = np.linspace(x[0], x[-1], 400) 

1955 >>> plt.plot(grid, spl(grid), label='Spline') 

1956 >>> plt.plot(grid, func(grid), label='Original function') 

1957 >>> plt.scatter(x, y, marker='.') 

1958 >>> plt.legend(loc='best') 

1959 >>> plt.show() 

1960 

1961 """ 

1962 

1963 x = np.ascontiguousarray(x, dtype=float) 

1964 y = np.ascontiguousarray(y, dtype=float) 

1965 

1966 if any(x[1:] - x[:-1] <= 0): 

1967 raise ValueError('``x`` should be an ascending array') 

1968 

1969 if x.ndim != 1 or y.ndim != 1 or x.shape[0] != y.shape[0]: 

1970 raise ValueError('``x`` and ``y`` should be one dimensional and the' 

1971 ' same size') 

1972 

1973 if w is None: 

1974 w = np.ones(len(x)) 

1975 else: 

1976 w = np.ascontiguousarray(w) 

1977 if any(w <= 0): 

1978 raise ValueError('Invalid vector of weights') 

1979 

1980 t = np.r_[[x[0]] * 3, x, [x[-1]] * 3] 

1981 n = x.shape[0] 

1982 

1983 if n <= 4: 

1984 raise ValueError('``x`` and ``y`` length must be larger than 5') 

1985 

1986 # It is known that the solution to the stated minimization problem exists 

1987 # and is a natural cubic spline with vector of knots equal to the unique 

1988 # elements of ``x`` [3], so we will solve the problem in the basis of 

1989 # natural splines. 

1990 

1991 # create design matrix in the B-spline basis 

1992 X_bspl = BSpline.design_matrix(x, t, 3) 

1993 # move from B-spline basis to the basis of natural splines using equations 

1994 # (2.1.7) [4] 

1995 # central elements 

1996 X = np.zeros((5, n)) 

1997 for i in range(1, 4): 

1998 X[i, 2: -2] = X_bspl[i: i - 4, 3: -3][np.diag_indices(n - 4)] 

1999 

2000 # first elements 

2001 X[1, 1] = X_bspl[0, 0] 

2002 X[2, :2] = ((x[2] + x[1] - 2 * x[0]) * X_bspl[0, 0], 

2003 X_bspl[1, 1] + X_bspl[1, 2]) 

2004 X[3, :2] = ((x[2] - x[0]) * X_bspl[1, 1], X_bspl[2, 2]) 

2005 

2006 # last elements 

2007 X[1, -2:] = (X_bspl[-3, -3], (x[-1] - x[-3]) * X_bspl[-2, -2]) 

2008 X[2, -2:] = (X_bspl[-2, -3] + X_bspl[-2, -2], 

2009 (2 * x[-1] - x[-2] - x[-3]) * X_bspl[-1, -1]) 

2010 X[3, -2] = X_bspl[-1, -1] 

2011 

2012 # create penalty matrix and divide it by vector of weights: W^{-1} E 

2013 wE = np.zeros((5, n)) 

2014 wE[2:, 0] = _coeff_of_divided_diff(x[:3]) / w[:3] 

2015 wE[1:, 1] = _coeff_of_divided_diff(x[:4]) / w[:4] 

2016 for j in range(2, n - 2): 

2017 wE[:, j] = (x[j+2] - x[j-2]) * _coeff_of_divided_diff(x[j-2:j+3])\ 

2018 / w[j-2: j+3] 

2019 

2020 wE[:-1, -2] = -_coeff_of_divided_diff(x[-4:]) / w[-4:] 

2021 wE[:-2, -1] = _coeff_of_divided_diff(x[-3:]) / w[-3:] 

2022 wE *= 6 

2023 

2024 if lam is None: 

2025 lam = _compute_optimal_gcv_parameter(X, wE, y, w) 

2026 elif lam < 0.: 

2027 raise ValueError('Regularization parameter should be non-negative') 

2028 

2029 # solve the initial problem in the basis of natural splines 

2030 c = solve_banded((2, 2), X + lam * wE, y) 

2031 # move back to B-spline basis using equations (2.2.10) [4] 

2032 c_ = np.r_[c[0] * (t[5] + t[4] - 2 * t[3]) + c[1], 

2033 c[0] * (t[5] - t[3]) + c[1], 

2034 c[1: -1], 

2035 c[-1] * (t[-4] - t[-6]) + c[-2], 

2036 c[-1] * (2 * t[-4] - t[-5] - t[-6]) + c[-2]] 

2037 

2038 return BSpline.construct_fast(t, c_, 3)