_bsplines.py 89 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614
  1. import operator
  2. from math import prod
  3. from types import GenericAlias
  4. import numpy as np
  5. from scipy._lib._util import normalize_axis_index
  6. from scipy.linalg import (get_lapack_funcs, LinAlgError,
  7. cholesky_banded, cho_solve_banded,
  8. solve, solve_banded)
  9. from scipy.optimize import minimize_scalar
  10. from . import _dierckx
  11. from . import _fitpack_impl
  12. from scipy.sparse import csr_array
  13. from scipy.special import poch
  14. from itertools import combinations
  15. from scipy._lib._array_api import array_namespace, concat_1d, xp_capabilities
  16. __all__ = ["BSpline", "make_interp_spline", "make_lsq_spline",
  17. "make_smoothing_spline"]
  18. def _get_dtype(dtype):
  19. """Return np.complex128 for complex dtypes, np.float64 otherwise."""
  20. if np.issubdtype(dtype, np.complexfloating):
  21. return np.complex128
  22. else:
  23. return np.float64
  24. def _as_float_array(x, check_finite=False):
  25. """Convert the input into a C contiguous float array.
  26. NB: Upcasts half- and single-precision floats to double precision.
  27. """
  28. x = np.ascontiguousarray(x)
  29. dtyp = _get_dtype(x.dtype)
  30. x = x.astype(dtyp, copy=False)
  31. if check_finite and not np.isfinite(x).all():
  32. raise ValueError("Array must not contain infs or nans.")
  33. return x
  34. def _dual_poly(j, k, t, y):
  35. """
  36. Dual polynomial of the B-spline B_{j,k,t} -
  37. polynomial which is associated with B_{j,k,t}:
  38. $p_{j,k}(y) = (y - t_{j+1})(y - t_{j+2})...(y - t_{j+k})$
  39. """
  40. if k == 0:
  41. return 1
  42. return np.prod([(y - t[j + i]) for i in range(1, k + 1)])
  43. def _diff_dual_poly(j, k, y, d, t):
  44. """
  45. d-th derivative of the dual polynomial $p_{j,k}(y)$
  46. """
  47. if d == 0:
  48. return _dual_poly(j, k, t, y)
  49. if d == k:
  50. return poch(1, k)
  51. comb = list(combinations(range(j + 1, j + k + 1), d))
  52. res = 0
  53. for i in range(len(comb) * len(comb[0])):
  54. res += np.prod([(y - t[j + p]) for p in range(1, k + 1)
  55. if (j + p) not in comb[i//d]])
  56. return res
  57. @xp_capabilities(
  58. cpu_only=True, jax_jit=False,
  59. skip_backends=[
  60. ("dask.array",
  61. "https://github.com/data-apis/array-api-extra/issues/488")
  62. ]
  63. )
  64. class BSpline:
  65. r"""Univariate spline in the B-spline basis.
  66. .. math::
  67. S(x) = \sum_{j=0}^{n-1} c_j B_{j, k; t}(x)
  68. where :math:`B_{j, k; t}` are B-spline basis functions of degree `k`
  69. and knots `t`.
  70. Parameters
  71. ----------
  72. t : ndarray, shape (n+k+1,)
  73. knots
  74. c : ndarray, shape (>=n, ...)
  75. spline coefficients
  76. k : int
  77. B-spline degree
  78. extrapolate : bool or 'periodic', optional
  79. whether to extrapolate beyond the base interval, ``t[k] .. t[n]``,
  80. or to return nans.
  81. If True, extrapolates the first and last polynomial pieces of b-spline
  82. functions active on the base interval.
  83. If 'periodic', periodic extrapolation is used.
  84. Default is True.
  85. axis : int, optional
  86. Interpolation axis. Default is zero.
  87. Attributes
  88. ----------
  89. t : ndarray
  90. knot vector
  91. c : ndarray
  92. spline coefficients
  93. k : int
  94. spline degree
  95. extrapolate : bool
  96. If True, extrapolates the first and last polynomial pieces of b-spline
  97. functions active on the base interval.
  98. axis : int
  99. Interpolation axis.
  100. tck : tuple
  101. A read-only equivalent of ``(self.t, self.c, self.k)``
  102. Methods
  103. -------
  104. __call__
  105. basis_element
  106. derivative
  107. antiderivative
  108. integrate
  109. insert_knot
  110. construct_fast
  111. design_matrix
  112. from_power_basis
  113. Notes
  114. -----
  115. B-spline basis elements are defined via
  116. .. math::
  117. B_{i, 0}(x) = 1, \textrm{if $t_i \le x < t_{i+1}$, otherwise $0$,}
  118. B_{i, k}(x) = \frac{x - t_i}{t_{i+k} - t_i} B_{i, k-1}(x)
  119. + \frac{t_{i+k+1} - x}{t_{i+k+1} - t_{i+1}} B_{i+1, k-1}(x)
  120. **Implementation details**
  121. - At least ``k+1`` coefficients are required for a spline of degree `k`,
  122. so that ``n >= k+1``. Additional coefficients, ``c[j]`` with
  123. ``j > n``, are ignored.
  124. - B-spline basis elements of degree `k` form a partition of unity on the
  125. *base interval*, ``t[k] <= x <= t[n]``.
  126. Examples
  127. --------
  128. Translating the recursive definition of B-splines into Python code, we have:
  129. >>> def B(x, k, i, t):
  130. ... if k == 0:
  131. ... return 1.0 if t[i] <= x < t[i+1] else 0.0
  132. ... if t[i+k] == t[i]:
  133. ... c1 = 0.0
  134. ... else:
  135. ... c1 = (x - t[i])/(t[i+k] - t[i]) * B(x, k-1, i, t)
  136. ... if t[i+k+1] == t[i+1]:
  137. ... c2 = 0.0
  138. ... else:
  139. ... c2 = (t[i+k+1] - x)/(t[i+k+1] - t[i+1]) * B(x, k-1, i+1, t)
  140. ... return c1 + c2
  141. >>> def bspline(x, t, c, k):
  142. ... n = len(t) - k - 1
  143. ... assert (n >= k+1) and (len(c) >= n)
  144. ... return sum(c[i] * B(x, k, i, t) for i in range(n))
  145. Note that this is an inefficient (if straightforward) way to
  146. evaluate B-splines --- this spline class does it in an equivalent,
  147. but much more efficient way.
  148. Here we construct a quadratic spline function on the base interval
  149. ``2 <= x <= 4`` and compare with the naive way of evaluating the spline:
  150. >>> from scipy.interpolate import BSpline
  151. >>> k = 2
  152. >>> t = [0, 1, 2, 3, 4, 5, 6]
  153. >>> c = [-1, 2, 0, -1]
  154. >>> spl = BSpline(t, c, k)
  155. >>> spl(2.5)
  156. array(1.375)
  157. >>> bspline(2.5, t, c, k)
  158. 1.375
  159. Note that outside of the base interval results differ. This is because
  160. `BSpline` extrapolates the first and last polynomial pieces of B-spline
  161. functions active on the base interval.
  162. >>> import matplotlib.pyplot as plt
  163. >>> import numpy as np
  164. >>> fig, ax = plt.subplots()
  165. >>> xx = np.linspace(1.5, 4.5, 50)
  166. >>> ax.plot(xx, [bspline(x, t, c ,k) for x in xx], 'r-', lw=3, label='naive')
  167. >>> ax.plot(xx, spl(xx), 'b-', lw=4, alpha=0.7, label='BSpline')
  168. >>> ax.grid(True)
  169. >>> ax.legend(loc='best')
  170. >>> plt.show()
  171. References
  172. ----------
  173. .. [1] Tom Lyche and Knut Morken, Spline methods,
  174. http://www.uio.no/studier/emner/matnat/ifi/INF-MAT5340/v05/undervisningsmateriale/
  175. .. [2] Carl de Boor, A practical guide to splines, Springer, 2001.
  176. """
  177. # generic type compatibility with scipy-stubs
  178. __class_getitem__ = classmethod(GenericAlias)
  179. def __init__(self, t, c, k, extrapolate=True, axis=0):
  180. super().__init__()
  181. self._asarray = array_namespace(c, t).asarray
  182. self.k = operator.index(k)
  183. self._c = np.asarray(c)
  184. self._t = np.ascontiguousarray(t, dtype=np.float64)
  185. if extrapolate == 'periodic':
  186. self.extrapolate = extrapolate
  187. else:
  188. self.extrapolate = bool(extrapolate)
  189. n = self._t.shape[0] - self.k - 1
  190. axis = normalize_axis_index(axis, self._c.ndim)
  191. # Note that the normalized axis is stored in the object.
  192. self.axis = axis
  193. if axis != 0:
  194. # roll the interpolation axis to be the first one in self.c
  195. # More specifically, the target shape for self.c is (n, ...),
  196. # and axis !=0 means that we have c.shape (..., n, ...)
  197. # ^
  198. # axis
  199. self._c = np.moveaxis(self._c, axis, 0)
  200. if k < 0:
  201. raise ValueError("Spline order cannot be negative.")
  202. if self._t.ndim != 1:
  203. raise ValueError("Knot vector must be one-dimensional.")
  204. if n < self.k + 1:
  205. raise ValueError(f"Need at least {2*k + 2} knots for degree {k}")
  206. if (np.diff(self._t) < 0).any():
  207. raise ValueError("Knots must be in a non-decreasing order.")
  208. if len(np.unique(self._t[k:n+1])) < 2:
  209. raise ValueError("Need at least two internal knots.")
  210. if not np.isfinite(self._t).all():
  211. raise ValueError("Knots should not have nans or infs.")
  212. if self._c.ndim < 1:
  213. raise ValueError("Coefficients must be at least 1-dimensional.")
  214. if self._c.shape[0] < n:
  215. raise ValueError("Knots, coefficients and degree are inconsistent.")
  216. dt = _get_dtype(self._c.dtype)
  217. self._c = np.ascontiguousarray(self._c, dtype=dt)
  218. @classmethod
  219. def construct_fast(cls, t, c, k, extrapolate=True, axis=0):
  220. """Construct a spline without making checks.
  221. Accepts same parameters as the regular constructor. Input arrays
  222. `t` and `c` must of correct shape and dtype.
  223. """
  224. self = object.__new__(cls)
  225. self._t, self._c, self.k = np.asarray(t), np.asarray(c), k
  226. self.extrapolate = extrapolate
  227. self.axis = axis
  228. self._asarray = array_namespace(t, c).asarray
  229. return self
  230. @property
  231. def tck(self):
  232. """Equivalent to ``(self.t, self.c, self.k)`` (read-only).
  233. """
  234. return self.t, self.c, self.k
  235. # Under the hood, self._c and self._t are always saved as numpy array
  236. # because they are used in a C extension expecting numpy arrays.
  237. @property
  238. def t(self):
  239. return self._asarray(self._t)
  240. @t.setter
  241. def t(self, t):
  242. self._t = np.asarray(t)
  243. @property
  244. def c(self):
  245. return self._asarray(self._c)
  246. @c.setter
  247. def c(self, c):
  248. self._c = np.asarray(c)
  249. @classmethod
  250. def basis_element(cls, t, extrapolate=True):
  251. """Return a B-spline basis element ``B(x | t[0], ..., t[k+1])``.
  252. Parameters
  253. ----------
  254. t : ndarray, shape (k+2,)
  255. internal knots
  256. extrapolate : bool or 'periodic', optional
  257. whether to extrapolate beyond the base interval, ``t[0] .. t[k+1]``,
  258. or to return nans.
  259. If 'periodic', periodic extrapolation is used.
  260. Default is True.
  261. Returns
  262. -------
  263. basis_element : callable
  264. A callable representing a B-spline basis element for the knot
  265. vector `t`.
  266. Notes
  267. -----
  268. The degree of the B-spline, `k`, is inferred from the length of `t` as
  269. ``len(t)-2``. The knot vector is constructed by appending and prepending
  270. ``k+1`` elements to internal knots `t`.
  271. Examples
  272. --------
  273. Construct a cubic B-spline:
  274. >>> import numpy as np
  275. >>> from scipy.interpolate import BSpline
  276. >>> b = BSpline.basis_element([0, 1, 2, 3, 4])
  277. >>> k = b.k
  278. >>> b.t[k:-k]
  279. array([ 0., 1., 2., 3., 4.])
  280. >>> k
  281. 3
  282. Construct a quadratic B-spline on ``[0, 1, 1, 2]``, and compare
  283. to its explicit form:
  284. >>> t = [0, 1, 1, 2]
  285. >>> b = BSpline.basis_element(t)
  286. >>> def f(x):
  287. ... return np.where(x < 1, x*x, (2. - x)**2)
  288. >>> import matplotlib.pyplot as plt
  289. >>> fig, ax = plt.subplots()
  290. >>> x = np.linspace(0, 2, 51)
  291. >>> ax.plot(x, b(x), 'g', lw=3)
  292. >>> ax.plot(x, f(x), 'r', lw=8, alpha=0.4)
  293. >>> ax.grid(True)
  294. >>> plt.show()
  295. """
  296. xp = array_namespace(t)
  297. t = np.asarray(t)
  298. k = t.shape[0] - 2
  299. t = _as_float_array(t) # TODO: use concat_1d instead of np.r_
  300. t = np.r_[(t[0]-1,) * k, t, (t[-1]+1,) * k]
  301. c = np.zeros_like(t)
  302. c[k] = 1.
  303. t, c = xp.asarray(t), xp.asarray(c)
  304. return cls.construct_fast(t, c, k, extrapolate)
  305. @classmethod
  306. def design_matrix(cls, x, t, k, extrapolate=False):
  307. """
  308. Returns a design matrix as a CSR format sparse array.
  309. Parameters
  310. ----------
  311. x : array_like, shape (n,)
  312. Points to evaluate the spline at.
  313. t : array_like, shape (nt,)
  314. Sorted 1D array of knots.
  315. k : int
  316. B-spline degree.
  317. extrapolate : bool or 'periodic', optional
  318. Whether to extrapolate based on the first and last intervals
  319. or raise an error. If 'periodic', periodic extrapolation is used.
  320. Default is False.
  321. .. versionadded:: 1.10.0
  322. Returns
  323. -------
  324. design_matrix : `csr_array` object
  325. Sparse matrix in CSR format where each row contains all the basis
  326. elements of the input row (first row = basis elements of x[0],
  327. ..., last row = basis elements x[-1]).
  328. Examples
  329. --------
  330. Construct a design matrix for a B-spline
  331. >>> from scipy.interpolate import make_interp_spline, BSpline
  332. >>> import numpy as np
  333. >>> x = np.linspace(0, np.pi * 2, 4)
  334. >>> y = np.sin(x)
  335. >>> k = 3
  336. >>> bspl = make_interp_spline(x, y, k=k)
  337. >>> design_matrix = bspl.design_matrix(x, bspl.t, k)
  338. >>> design_matrix.toarray()
  339. [[1. , 0. , 0. , 0. ],
  340. [0.2962963 , 0.44444444, 0.22222222, 0.03703704],
  341. [0.03703704, 0.22222222, 0.44444444, 0.2962963 ],
  342. [0. , 0. , 0. , 1. ]]
  343. Construct a design matrix for some vector of knots
  344. >>> k = 2
  345. >>> t = [-1, 0, 1, 2, 3, 4, 5, 6]
  346. >>> x = [1, 2, 3, 4]
  347. >>> design_matrix = BSpline.design_matrix(x, t, k).toarray()
  348. >>> design_matrix
  349. [[0.5, 0.5, 0. , 0. , 0. ],
  350. [0. , 0.5, 0.5, 0. , 0. ],
  351. [0. , 0. , 0.5, 0.5, 0. ],
  352. [0. , 0. , 0. , 0.5, 0.5]]
  353. This result is equivalent to the one created in the sparse format
  354. >>> c = np.eye(len(t) - k - 1)
  355. >>> design_matrix_gh = BSpline(t, c, k)(x)
  356. >>> np.allclose(design_matrix, design_matrix_gh, atol=1e-14)
  357. True
  358. Notes
  359. -----
  360. .. versionadded:: 1.8.0
  361. In each row of the design matrix all the basis elements are evaluated
  362. at the certain point (first row - x[0], ..., last row - x[-1]).
  363. `nt` is a length of the vector of knots: as far as there are
  364. `nt - k - 1` basis elements, `nt` should be not less than `2 * k + 2`
  365. to have at least `k + 1` basis element.
  366. Out of bounds `x` raises a ValueError.
  367. """
  368. x = _as_float_array(x, True)
  369. t = _as_float_array(t, True)
  370. if extrapolate != 'periodic':
  371. extrapolate = bool(extrapolate)
  372. if k < 0:
  373. raise ValueError("Spline order cannot be negative.")
  374. if t.ndim != 1 or np.any(t[1:] < t[:-1]):
  375. raise ValueError(f"Expect t to be a 1-D sorted array_like, but "
  376. f"got t={t}.")
  377. # There are `nt - k - 1` basis elements in a BSpline built on the
  378. # vector of knots with length `nt`, so to have at least `k + 1` basis
  379. # elements we need to have at least `2 * k + 2` elements in the vector
  380. # of knots.
  381. if len(t) < 2 * k + 2:
  382. raise ValueError(f"Length t is not enough for k={k}.")
  383. if extrapolate == 'periodic':
  384. # With periodic extrapolation we map x to the segment
  385. # [t[k], t[n]].
  386. n = t.size - k - 1
  387. x = t[k] + (x - t[k]) % (t[n] - t[k])
  388. extrapolate = False
  389. elif not extrapolate and (
  390. (min(x) < t[k]) or (max(x) > t[t.shape[0] - k - 1])
  391. ):
  392. # Checks from `find_interval` function
  393. raise ValueError(f'Out of bounds w/ x = {x}.')
  394. # Compute number of non-zeros of final CSR array in order to determine
  395. # the dtype of indices and indptr of the CSR array.
  396. n = x.shape[0]
  397. nnz = n * (k + 1)
  398. if nnz < np.iinfo(np.int32).max:
  399. int_dtype = np.int32
  400. else:
  401. int_dtype = np.int64
  402. # Get the non-zero elements of the design matrix and per-row `offsets`:
  403. # In row `i`, k+1 nonzero elements are consecutive, and start from `offset[i]`
  404. data, offsets, _ = _dierckx.data_matrix(x, t, k, np.ones_like(x), extrapolate)
  405. data = data.ravel()
  406. if offsets.dtype != int_dtype:
  407. offsets = offsets.astype(int_dtype)
  408. # Convert from per-row offsets to the CSR indices/indptr format
  409. indices = np.repeat(offsets, k+1).reshape(-1, k+1)
  410. indices = indices + np.arange(k+1, dtype=int_dtype)
  411. indices = indices.ravel()
  412. indptr = np.arange(0, (n + 1) * (k + 1), k + 1, dtype=int_dtype)
  413. return csr_array(
  414. (data, indices, indptr),
  415. shape=(x.shape[0], t.shape[0] - k - 1)
  416. )
  417. def __call__(self, x, nu=0, extrapolate=None):
  418. """
  419. Evaluate a spline function.
  420. Parameters
  421. ----------
  422. x : array_like
  423. points to evaluate the spline at.
  424. nu : int, optional
  425. derivative to evaluate (default is 0).
  426. extrapolate : bool or 'periodic', optional
  427. whether to extrapolate based on the first and last intervals
  428. or return nans. If 'periodic', periodic extrapolation is used.
  429. Default is `self.extrapolate`.
  430. Returns
  431. -------
  432. y : array_like
  433. Shape is determined by replacing the interpolation axis
  434. in the coefficient array with the shape of `x`.
  435. """
  436. if extrapolate is None:
  437. extrapolate = self.extrapolate
  438. x = np.asarray(x)
  439. x_shape, x_ndim = x.shape, x.ndim
  440. x = np.ascontiguousarray(x.ravel(), dtype=np.float64)
  441. # With periodic extrapolation we map x to the segment
  442. # [self.t[k], self.t[n]].
  443. if extrapolate == 'periodic':
  444. n = self._t.size - self.k - 1
  445. x = self._t[self.k] + (x - self._t[self.k]) % (self._t[n] - self._t[self.k])
  446. extrapolate = False
  447. self._ensure_c_contiguous()
  448. # if self.c is complex: the C code in _dierckxmodule.cc expects
  449. # floats, so make a view---this expands the last axis, and
  450. # the view is C contiguous if the original is.
  451. # if c.dtype is complex of shape (n,), c.view(float).shape == (2*n,)
  452. # if c.dtype is complex of shape (n, m), c.view(float).shape == (n, 2*m)
  453. is_complex = self._c.dtype.kind == 'c'
  454. if is_complex:
  455. cc = self._c.view(float)
  456. if self._c.ndim == 1:
  457. cc = cc.reshape(self._c.shape[0], 2)
  458. else:
  459. cc = self._c
  460. # flatten the trailing dims
  461. cc = cc.reshape(cc.shape[0], -1)
  462. # heavy lifting: actually perform the evaluations
  463. out = _dierckx.evaluate_spline(self._t, cc, self.k, x, nu, extrapolate)
  464. if is_complex:
  465. out = out.view(complex)
  466. out = out.reshape(x_shape + self._c.shape[1:])
  467. if self.axis != 0:
  468. # transpose to move the calculated values to the interpolation axis
  469. l = list(range(out.ndim))
  470. l = l[x_ndim:x_ndim+self.axis] + l[:x_ndim] + l[x_ndim+self.axis:]
  471. out = out.transpose(l)
  472. return self._asarray(out)
  473. def _ensure_c_contiguous(self):
  474. """
  475. c and t may be modified by the user. The Cython code expects
  476. that they are C contiguous.
  477. """
  478. if not self._t.flags.c_contiguous:
  479. self._t = self._t.copy()
  480. if not self._c.flags.c_contiguous:
  481. self._c = self._c.copy()
  482. def derivative(self, nu=1):
  483. """Return a B-spline representing the derivative.
  484. Parameters
  485. ----------
  486. nu : int, optional
  487. Derivative order.
  488. Default is 1.
  489. Returns
  490. -------
  491. b : `BSpline` object
  492. A new instance representing the derivative.
  493. See Also
  494. --------
  495. splder, splantider
  496. """
  497. c = self._asarray(self.c, copy=True)
  498. t = self.t
  499. xp = array_namespace(t, c)
  500. # pad the c array if needed
  501. ct = t.shape[0] - c.shape[0]
  502. if ct > 0:
  503. c = concat_1d(xp, c, xp.zeros((ct,) + c.shape[1:]))
  504. tck = _fitpack_impl.splder((t, c, self.k), nu)
  505. return self.construct_fast(*tck, extrapolate=self.extrapolate,
  506. axis=self.axis)
  507. def antiderivative(self, nu=1):
  508. """Return a B-spline representing the antiderivative.
  509. Parameters
  510. ----------
  511. nu : int, optional
  512. Antiderivative order. Default is 1.
  513. Returns
  514. -------
  515. b : `BSpline` object
  516. A new instance representing the antiderivative.
  517. Notes
  518. -----
  519. If antiderivative is computed and ``self.extrapolate='periodic'``,
  520. it will be set to False for the returned instance. This is done because
  521. the antiderivative is no longer periodic and its correct evaluation
  522. outside of the initially given x interval is difficult.
  523. See Also
  524. --------
  525. splder, splantider
  526. """
  527. c = self._asarray(self.c, copy=True)
  528. t = self.t
  529. xp = array_namespace(t, c)
  530. # pad the c array if needed
  531. ct = t.shape[0] - c.shape[0]
  532. if ct > 0:
  533. c = concat_1d(xp, c, xp.zeros((ct,) + c.shape[1:]))
  534. tck = _fitpack_impl.splantider((t, c, self.k), nu)
  535. if self.extrapolate == 'periodic':
  536. extrapolate = False
  537. else:
  538. extrapolate = self.extrapolate
  539. return self.construct_fast(*tck, extrapolate=extrapolate,
  540. axis=self.axis)
  541. def integrate(self, a, b, extrapolate=None):
  542. """Compute a definite integral of the spline.
  543. Parameters
  544. ----------
  545. a : float
  546. Lower limit of integration.
  547. b : float
  548. Upper limit of integration.
  549. extrapolate : bool or 'periodic', optional
  550. whether to extrapolate beyond the base interval,
  551. ``t[k] .. t[-k-1]``, or take the spline to be zero outside of the
  552. base interval. If 'periodic', periodic extrapolation is used.
  553. If None (default), use `self.extrapolate`.
  554. Returns
  555. -------
  556. I : array_like
  557. Definite integral of the spline over the interval ``[a, b]``.
  558. Examples
  559. --------
  560. Construct the linear spline ``x if x < 1 else 2 - x`` on the base
  561. interval :math:`[0, 2]`, and integrate it
  562. >>> from scipy.interpolate import BSpline
  563. >>> b = BSpline.basis_element([0, 1, 2])
  564. >>> b.integrate(0, 1)
  565. array(0.5)
  566. If the integration limits are outside of the base interval, the result
  567. is controlled by the `extrapolate` parameter
  568. >>> b.integrate(-1, 1)
  569. array(0.0)
  570. >>> b.integrate(-1, 1, extrapolate=False)
  571. array(0.5)
  572. >>> import matplotlib.pyplot as plt
  573. >>> fig, ax = plt.subplots()
  574. >>> ax.grid(True)
  575. >>> ax.axvline(0, c='r', lw=5, alpha=0.5) # base interval
  576. >>> ax.axvline(2, c='r', lw=5, alpha=0.5)
  577. >>> xx = [-1, 1, 2]
  578. >>> ax.plot(xx, b(xx))
  579. >>> plt.show()
  580. """
  581. if extrapolate is None:
  582. extrapolate = self.extrapolate
  583. # Prepare self.t and self.c.
  584. self._ensure_c_contiguous()
  585. # Swap integration bounds if needed.
  586. sign = 1
  587. if b < a:
  588. a, b = b, a
  589. sign = -1
  590. n = self._t.size - self.k - 1
  591. if extrapolate != "periodic" and not extrapolate:
  592. # Shrink the integration interval, if needed.
  593. a = max(a, self._t[self.k])
  594. b = min(b, self._t[n])
  595. if self._c.ndim == 1:
  596. # Fast path: use FITPACK's routine
  597. # (cf _fitpack_impl.splint).
  598. integral = _fitpack_impl.splint(a, b, (self._t, self._c, self.k))
  599. return self._asarray(integral * sign)
  600. # Compute the antiderivative.
  601. c = self._c
  602. ct = len(self._t) - len(c)
  603. if ct > 0:
  604. c = np.r_[c, np.zeros((ct,) + c.shape[1:])]
  605. ta, ca, ka = _fitpack_impl.splantider((self._t, c, self.k), 1)
  606. if extrapolate == 'periodic':
  607. # Split the integral into the part over period (can be several
  608. # of them) and the remaining part.
  609. ts, te = self._t[self.k], self._t[n]
  610. period = te - ts
  611. interval = b - a
  612. n_periods, left = divmod(interval, period)
  613. if n_periods > 0:
  614. # Evaluate the difference of antiderivatives.
  615. x = np.asarray([ts, te], dtype=np.float64)
  616. out = _dierckx.evaluate_spline(ta, ca.reshape(ca.shape[0], -1),
  617. ka, x, 0, False)
  618. integral = out[1] - out[0]
  619. integral *= n_periods
  620. else:
  621. integral = np.zeros((1, prod(self._c.shape[1:])),
  622. dtype=self._c.dtype)
  623. # Map a to [ts, te], b is always a + left.
  624. a = ts + (a - ts) % period
  625. b = a + left
  626. # If b <= te then we need to integrate over [a, b], otherwise
  627. # over [a, te] and from xs to what is remained.
  628. if b <= te:
  629. x = np.asarray([a, b], dtype=np.float64)
  630. out = _dierckx.evaluate_spline(ta, ca.reshape(ca.shape[0], -1),
  631. ka, x, 0, False)
  632. integral += out[1] - out[0]
  633. else:
  634. x = np.asarray([a, te], dtype=np.float64)
  635. out = _dierckx.evaluate_spline(ta, ca.reshape(ca.shape[0], -1),
  636. ka, x, 0, False)
  637. integral += out[1] - out[0]
  638. x = np.asarray([ts, ts + b - te], dtype=np.float64)
  639. out = _dierckx.evaluate_spline(ta, ca.reshape(ca.shape[0], -1),
  640. ka, x, 0, False)
  641. integral += out[1] - out[0]
  642. else:
  643. # Evaluate the difference of antiderivatives.
  644. x = np.asarray([a, b], dtype=np.float64)
  645. out = _dierckx.evaluate_spline(ta, ca.reshape(ca.shape[0], -1),
  646. ka, x, 0, extrapolate)
  647. integral = out[1] - out[0]
  648. integral *= sign
  649. return self._asarray(integral.reshape(ca.shape[1:]))
  650. @classmethod
  651. def from_power_basis(cls, pp, bc_type='not-a-knot'):
  652. r"""
  653. Construct a polynomial in the B-spline basis
  654. from a piecewise polynomial in the power basis.
  655. For now, accepts ``CubicSpline`` instances only.
  656. Parameters
  657. ----------
  658. pp : CubicSpline
  659. A piecewise polynomial in the power basis, as created
  660. by ``CubicSpline``
  661. bc_type : string, optional
  662. Boundary condition type as in ``CubicSpline``: one of the
  663. ``not-a-knot``, ``natural``, ``clamped``, or ``periodic``.
  664. Necessary for construction an instance of ``BSpline`` class.
  665. Default is ``not-a-knot``.
  666. Returns
  667. -------
  668. b : `BSpline` object
  669. A new instance representing the initial polynomial
  670. in the B-spline basis.
  671. Notes
  672. -----
  673. .. versionadded:: 1.8.0
  674. Accepts only ``CubicSpline`` instances for now.
  675. The algorithm follows from differentiation
  676. the Marsden's identity [1]: each of coefficients of spline
  677. interpolation function in the B-spline basis is computed as follows:
  678. .. math::
  679. c_j = \sum_{m=0}^{k} \frac{(k-m)!}{k!}
  680. c_{m,i} (-1)^{k-m} D^m p_{j,k}(x_i)
  681. :math:`c_{m, i}` - a coefficient of CubicSpline,
  682. :math:`D^m p_{j, k}(x_i)` - an m-th defivative of a dual polynomial
  683. in :math:`x_i`.
  684. ``k`` always equals 3 for now.
  685. First ``n - 2`` coefficients are computed in :math:`x_i = x_j`, e.g.
  686. .. math::
  687. c_1 = \sum_{m=0}^{k} \frac{(k-1)!}{k!} c_{m,1} D^m p_{j,3}(x_1)
  688. Last ``nod + 2`` coefficients are computed in ``x[-2]``,
  689. ``nod`` - number of derivatives at the ends.
  690. For example, consider :math:`x = [0, 1, 2, 3, 4]`,
  691. :math:`y = [1, 1, 1, 1, 1]` and bc_type = ``natural``
  692. The coefficients of CubicSpline in the power basis:
  693. :math:`[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0],
  694. [0, 0, 0, 0, 0], [1, 1, 1, 1, 1]]`
  695. The knot vector: :math:`t = [0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4]`
  696. In this case
  697. .. math::
  698. c_j = \frac{0!}{k!} c_{3, i} k! = c_{3, i} = 1,~j = 0, ..., 6
  699. References
  700. ----------
  701. .. [1] Tom Lyche and Knut Morken, Spline Methods, 2005, Section 3.1.2
  702. """
  703. from ._cubic import CubicSpline
  704. if not isinstance(pp, CubicSpline):
  705. raise NotImplementedError(f"Only CubicSpline objects are accepted "
  706. f"for now. Got {type(pp)} instead.")
  707. x = pp.x
  708. coef = pp.c
  709. k = pp.c.shape[0] - 1
  710. n = x.shape[0]
  711. if bc_type == 'not-a-knot':
  712. t = _not_a_knot(x, k)
  713. elif bc_type == 'natural' or bc_type == 'clamped':
  714. t = _augknt(x, k)
  715. elif bc_type == 'periodic':
  716. t = _periodic_knots(x, k)
  717. else:
  718. raise TypeError(f'Unknown boundary condition: {bc_type}')
  719. nod = t.shape[0] - (n + k + 1) # number of derivatives at the ends
  720. c = np.zeros(n + nod, dtype=pp.c.dtype)
  721. for m in range(k + 1):
  722. for i in range(n - 2):
  723. c[i] += poch(k + 1, -m) * coef[m, i]\
  724. * np.power(-1, k - m)\
  725. * _diff_dual_poly(i, k, x[i], m, t)
  726. for j in range(n - 2, n + nod):
  727. c[j] += poch(k + 1, -m) * coef[m, n - 2]\
  728. * np.power(-1, k - m)\
  729. * _diff_dual_poly(j, k, x[n - 2], m, t)
  730. return cls.construct_fast(t, c, k, pp.extrapolate, pp.axis)
  731. def insert_knot(self, x, m=1):
  732. """Insert a new knot at `x` of multiplicity `m`.
  733. Given the knots and coefficients of a B-spline representation, create a
  734. new B-spline with a knot inserted `m` times at point `x`.
  735. Parameters
  736. ----------
  737. x : float
  738. The position of the new knot
  739. m : int, optional
  740. The number of times to insert the given knot (its multiplicity).
  741. Default is 1.
  742. Returns
  743. -------
  744. spl : `BSpline` object
  745. A new `BSpline` object with the new knot inserted.
  746. Notes
  747. -----
  748. Based on algorithms from [1]_ and [2]_.
  749. In case of a periodic spline (``self.extrapolate == "periodic"``)
  750. there must be either at least k interior knots t(j) satisfying
  751. ``t(k+1)<t(j)<=x`` or at least k interior knots t(j) satisfying
  752. ``x<=t(j)<t(n-k)``.
  753. This routine is functionally equivalent to `scipy.interpolate.insert`.
  754. .. versionadded:: 1.13
  755. References
  756. ----------
  757. .. [1] W. Boehm, "Inserting new knots into b-spline curves.",
  758. Computer Aided Design, 12, p.199-201, 1980.
  759. :doi:`10.1016/0010-4485(80)90154-2`.
  760. .. [2] P. Dierckx, "Curve and surface fitting with splines, Monographs on
  761. Numerical Analysis", Oxford University Press, 1993.
  762. See Also
  763. --------
  764. scipy.interpolate.insert
  765. Examples
  766. --------
  767. You can insert knots into a B-spline:
  768. >>> import numpy as np
  769. >>> from scipy.interpolate import BSpline, make_interp_spline
  770. >>> x = np.linspace(0, 10, 5)
  771. >>> y = np.sin(x)
  772. >>> spl = make_interp_spline(x, y, k=3)
  773. >>> spl.t
  774. array([ 0., 0., 0., 0., 5., 10., 10., 10., 10.])
  775. Insert a single knot
  776. >>> spl_1 = spl.insert_knot(3)
  777. >>> spl_1.t
  778. array([ 0., 0., 0., 0., 3., 5., 10., 10., 10., 10.])
  779. Insert a multiple knot
  780. >>> spl_2 = spl.insert_knot(8, m=3)
  781. >>> spl_2.t
  782. array([ 0., 0., 0., 0., 5., 8., 8., 8., 10., 10., 10., 10.])
  783. """
  784. x = float(x)
  785. if x < self._t[self.k] or x > self._t[-self.k-1]:
  786. raise ValueError(f"Cannot insert a knot at {x}.")
  787. if m <= 0:
  788. raise ValueError(f"`m` must be positive, got {m = }.")
  789. tt = self._t.copy()
  790. cc = self._c.copy()
  791. for _ in range(m):
  792. tt, cc = _insert(x, tt, cc, self.k, self.extrapolate == "periodic")
  793. tt, cc = self._asarray(tt), self._asarray(cc)
  794. return self.construct_fast(tt, cc, self.k, self.extrapolate, self.axis)
  795. def _insert(xval, t, c, k, periodic=False):
  796. """Insert a single knot at `xval`."""
  797. #
  798. # This is a port of the FORTRAN `insert` routine by P. Dierckx,
  799. # https://github.com/scipy/scipy/blob/maintenance/1.11.x/scipy/interpolate/fitpack/insert.f
  800. # which carries the following comment:
  801. #
  802. # subroutine insert inserts a new knot x into a spline function s(x)
  803. # of degree k and calculates the b-spline representation of s(x) with
  804. # respect to the new set of knots. in addition, if iopt.ne.0, s(x)
  805. # will be considered as a periodic spline with period per=t(n-k)-t(k+1)
  806. # satisfying the boundary constraints
  807. # t(i+n-2*k-1) = t(i)+per ,i=1,2,...,2*k+1
  808. # c(i+n-2*k-1) = c(i) ,i=1,2,...,k
  809. # in that case, the knots and b-spline coefficients returned will also
  810. # satisfy these boundary constraints, i.e.
  811. # tt(i+nn-2*k-1) = tt(i)+per ,i=1,2,...,2*k+1
  812. # cc(i+nn-2*k-1) = cc(i) ,i=1,2,...,k
  813. interval = _dierckx.find_interval(t, k, float(xval), k, False)
  814. if interval < 0:
  815. # extrapolated values are guarded for in BSpline.insert_knot
  816. raise ValueError(f"Cannot insert the knot at {xval}.")
  817. # super edge case: a knot with multiplicity > k+1
  818. # see https://github.com/scipy/scipy/commit/037204c3e91
  819. if t[interval] == t[interval + k + 1]:
  820. interval -= 1
  821. if periodic:
  822. if (interval + 1 <= 2*k) and (interval + 1 >= t.shape[0] - 2*k):
  823. # in case of a periodic spline (iopt.ne.0) there must be
  824. # either at least k interior knots t(j) satisfying t(k+1)<t(j)<=x
  825. # or at least k interior knots t(j) satisfying x<=t(j)<t(n-k)
  826. raise ValueError("Not enough internal knots.")
  827. # knots
  828. tt = np.r_[t[:interval+1], xval, t[interval+1:]]
  829. newshape = (c.shape[0] + 1,) + c.shape[1:]
  830. cc = np.zeros(newshape, dtype=c.dtype)
  831. # coefficients
  832. cc[interval+1:, ...] = c[interval:, ...]
  833. for i in range(interval, interval-k, -1):
  834. fac = (xval - tt[i]) / (tt[i+k+1] - tt[i])
  835. cc[i, ...] = fac*c[i, ...] + (1. - fac)*c[i-1, ...]
  836. cc[:interval - k+1, ...] = c[:interval - k+1, ...]
  837. if periodic:
  838. # c incorporate the boundary conditions for a periodic spline.
  839. n = tt.shape[0]
  840. nk = n - k - 1
  841. n2k = n - 2*k - 1
  842. T = tt[nk] - tt[k] # period
  843. if interval >= nk - k:
  844. # adjust the left-hand boundary knots & coefs
  845. tt[:k] = tt[nk - k:nk] - T
  846. cc[:k, ...] = cc[n2k:n2k + k, ...]
  847. if interval <= 2*k-1:
  848. # adjust the right-hand boundary knots & coefs
  849. tt[n-k:] = tt[k+1:k+1+k] + T
  850. cc[n2k:n2k + k, ...] = cc[:k, ...]
  851. return tt, cc
  852. #################################
  853. # Interpolating spline helpers #
  854. #################################
  855. def _not_a_knot(x, k):
  856. """Given data x, construct the knot vector w/ not-a-knot BC.
  857. cf de Boor, XIII(12).
  858. For even k, it's a bit ad hoc: Greville sites + omit 2nd and 2nd-to-last
  859. data points, a la not-a-knot.
  860. This seems to match what Dierckx does, too:
  861. https://github.com/scipy/scipy/blob/maintenance/1.11.x/scipy/interpolate/fitpack/fpcurf.f#L63-L80
  862. """
  863. x = np.asarray(x)
  864. if k % 2 == 1:
  865. k2 = (k + 1) // 2
  866. t = x.copy()
  867. else:
  868. k2 = k // 2
  869. t = (x[1:] + x[:-1]) / 2
  870. t = t[k2:-k2]
  871. t = np.r_[(x[0],)*(k+1), t, (x[-1],)*(k+1)]
  872. return t
  873. def _augknt(x, k):
  874. """Construct a knot vector appropriate for the order-k interpolation."""
  875. return np.r_[(x[0],)*k, x, (x[-1],)*k]
  876. def _convert_string_aliases(deriv, target_shape):
  877. if isinstance(deriv, str):
  878. if deriv == "clamped":
  879. deriv = [(1, np.zeros(target_shape))]
  880. elif deriv == "natural":
  881. deriv = [(2, np.zeros(target_shape))]
  882. else:
  883. raise ValueError(f"Unknown boundary condition : {deriv}")
  884. return deriv
  885. def _process_deriv_spec(deriv):
  886. if deriv is not None:
  887. try:
  888. ords, vals = zip(*deriv)
  889. except TypeError as e:
  890. msg = ("Derivatives, `bc_type`, should be specified as a pair of "
  891. "iterables of pairs of (order, value).")
  892. raise ValueError(msg) from e
  893. else:
  894. ords, vals = [], []
  895. return np.atleast_1d(ords, vals)
  896. def _woodbury_algorithm(A, ur, ll, b, k):
  897. '''
  898. Solve a cyclic banded linear system with upper right
  899. and lower blocks of size ``(k-1) / 2`` using
  900. the Woodbury formula
  901. Parameters
  902. ----------
  903. A : 2-D array, shape(k, n)
  904. Matrix of diagonals of original matrix (see
  905. ``solve_banded`` documentation).
  906. ur : 2-D array, shape(bs, bs)
  907. Upper right block matrix.
  908. ll : 2-D array, shape(bs, bs)
  909. Lower left block matrix.
  910. b : 1-D array, shape(n,)
  911. Vector of constant terms of the system of linear equations.
  912. k : int
  913. B-spline degree.
  914. Returns
  915. -------
  916. c : 1-D array, shape(n,)
  917. Solution of the original system of linear equations.
  918. Notes
  919. -----
  920. This algorithm works only for systems with banded matrix A plus
  921. a correction term U @ V.T, where the matrix U @ V.T gives upper right
  922. and lower left block of A
  923. The system is solved with the following steps:
  924. 1. New systems of linear equations are constructed:
  925. A @ z_i = u_i,
  926. u_i - column vector of U,
  927. i = 1, ..., k - 1
  928. 2. Matrix Z is formed from vectors z_i:
  929. Z = [ z_1 | z_2 | ... | z_{k - 1} ]
  930. 3. Matrix H = (1 + V.T @ Z)^{-1}
  931. 4. The system A' @ y = b is solved
  932. 5. x = y - Z @ (H @ V.T @ y)
  933. Also, ``n`` should be greater than ``k``, otherwise corner block
  934. elements will intersect with diagonals.
  935. Examples
  936. --------
  937. Consider the case of n = 8, k = 5 (size of blocks - 2 x 2).
  938. The matrix of a system: U: V:
  939. x x x * * a b a b 0 0 0 0 1 0
  940. x x x x * * c 0 c 0 0 0 0 0 1
  941. x x x x x * * 0 0 0 0 0 0 0 0
  942. * x x x x x * 0 0 0 0 0 0 0 0
  943. * * x x x x x 0 0 0 0 0 0 0 0
  944. d * * x x x x 0 0 d 0 1 0 0 0
  945. e f * * x x x 0 0 e f 0 1 0 0
  946. References
  947. ----------
  948. .. [1] William H. Press, Saul A. Teukolsky, William T. Vetterling
  949. and Brian P. Flannery, Numerical Recipes, 2007, Section 2.7.3
  950. '''
  951. k_mod = k - k % 2
  952. bs = int((k - 1) / 2) + (k + 1) % 2
  953. n = A.shape[1] + 1
  954. U = np.zeros((n - 1, k_mod))
  955. VT = np.zeros((k_mod, n - 1)) # V transpose
  956. # upper right block
  957. U[:bs, :bs] = ur
  958. VT[np.arange(bs), np.arange(bs) - bs] = 1
  959. # lower left block
  960. U[-bs:, -bs:] = ll
  961. VT[np.arange(bs) - bs, np.arange(bs)] = 1
  962. Z = solve_banded((bs, bs), A, U)
  963. H = solve(np.identity(k_mod) + VT @ Z, np.identity(k_mod))
  964. y = solve_banded((bs, bs), A, b)
  965. c = y - Z @ (H @ (VT @ y))
  966. return c
  967. def _periodic_knots(x, k):
  968. '''
  969. returns vector of nodes on circle
  970. '''
  971. xc = np.copy(x)
  972. n = len(xc)
  973. if k % 2 == 0:
  974. dx = np.diff(xc)
  975. xc[1: -1] -= dx[:-1] / 2
  976. dx = np.diff(xc)
  977. t = np.zeros(n + 2 * k)
  978. t[k: -k] = xc
  979. for i in range(0, k):
  980. # filling first `k` elements in descending order
  981. t[k - i - 1] = t[k - i] - dx[-(i % (n - 1)) - 1]
  982. # filling last `k` elements in ascending order
  983. t[-k + i] = t[-k + i - 1] + dx[i % (n - 1)]
  984. return t
  985. def _make_interp_per_full_matr(x, y, t, k):
  986. '''
  987. Returns a solution of a system for B-spline interpolation with periodic
  988. boundary conditions. First ``k - 1`` rows of matrix are conditions of
  989. periodicity (continuity of ``k - 1`` derivatives at the boundary points).
  990. Last ``n`` rows are interpolation conditions.
  991. RHS is ``k - 1`` zeros and ``n`` ordinates in this case.
  992. Parameters
  993. ----------
  994. x : 1-D array, shape (n,)
  995. Values of x - coordinate of a given set of points.
  996. y : 1-D array, shape (n,)
  997. Values of y - coordinate of a given set of points.
  998. t : 1-D array, shape(n+2*k,)
  999. Vector of knots.
  1000. k : int
  1001. The maximum degree of spline
  1002. Returns
  1003. -------
  1004. c : 1-D array, shape (n+k-1,)
  1005. B-spline coefficients
  1006. Notes
  1007. -----
  1008. ``t`` is supposed to be taken on circle.
  1009. '''
  1010. x, y, t = map(np.asarray, (x, y, t))
  1011. n = x.size
  1012. # LHS: the colocation matrix + derivatives at edges
  1013. matr = np.zeros((n + k - 1, n + k - 1))
  1014. # derivatives at x[0] and x[-1]:
  1015. for i in range(k - 1):
  1016. bb = _dierckx.evaluate_all_bspl(t, k, x[0], k, i + 1)
  1017. matr[i, : k + 1] += bb
  1018. bb = _dierckx.evaluate_all_bspl(t, k, x[-1], n + k - 1, i + 1)[:-1]
  1019. matr[i, -k:] -= bb
  1020. # colocation matrix
  1021. for i in range(n):
  1022. xval = x[i]
  1023. # find interval
  1024. if xval == t[k]:
  1025. left = k
  1026. else:
  1027. left = np.searchsorted(t, xval) - 1
  1028. # fill a row
  1029. bb = _dierckx.evaluate_all_bspl(t, k, xval, left)
  1030. matr[i + k - 1, left-k:left+1] = bb
  1031. # RHS
  1032. b = np.r_[[0] * (k - 1), y]
  1033. c = solve(matr, b)
  1034. return c
  1035. def _handle_lhs_derivatives(t, k, xval, ab, kl, ku, deriv_ords, offset=0):
  1036. """ Fill in the entries of the colocation matrix corresponding to known
  1037. derivatives at `xval`.
  1038. The colocation matrix is in the banded storage, as prepared by _coloc.
  1039. No error checking.
  1040. Parameters
  1041. ----------
  1042. t : ndarray, shape (nt + k + 1,)
  1043. knots
  1044. k : integer
  1045. B-spline order
  1046. xval : float
  1047. The value at which to evaluate the derivatives at.
  1048. ab : ndarray, shape(2*kl + ku + 1, nt), Fortran order
  1049. B-spline colocation matrix.
  1050. This argument is modified *in-place*.
  1051. kl : integer
  1052. Number of lower diagonals of ab.
  1053. ku : integer
  1054. Number of upper diagonals of ab.
  1055. deriv_ords : 1D ndarray
  1056. Orders of derivatives known at xval
  1057. offset : integer, optional
  1058. Skip this many rows of the matrix ab.
  1059. """
  1060. # find where `xval` is in the knot vector, `t`
  1061. left = _dierckx.find_interval(t, k, float(xval), k, False)
  1062. # compute and fill in the derivatives @ xval
  1063. for row in range(deriv_ords.shape[0]):
  1064. nu = deriv_ords[row]
  1065. wrk = _dierckx.evaluate_all_bspl(t, k, xval, left, nu)
  1066. # if A were a full matrix, it would be just
  1067. # ``A[row + offset, left-k:left+1] = bb``.
  1068. for a in range(k+1):
  1069. clmn = left - k + a
  1070. ab[kl + ku + offset + row - clmn, clmn] = wrk[a]
  1071. def _make_periodic_spline(x, y, t, k, axis, *, xp):
  1072. '''
  1073. Compute the (coefficients of) interpolating B-spline with periodic
  1074. boundary conditions.
  1075. Parameters
  1076. ----------
  1077. x : array_like, shape (n,)
  1078. Abscissas.
  1079. y : array_like, shape (n,)
  1080. Ordinates.
  1081. k : int
  1082. B-spline degree.
  1083. t : array_like, shape (n + 2 * k,).
  1084. Knots taken on a circle, ``k`` on the left and ``k`` on the right
  1085. of the vector ``x``.
  1086. Returns
  1087. -------
  1088. b : `BSpline` object
  1089. A `BSpline` object of the degree ``k`` and with knots ``t``.
  1090. Notes
  1091. -----
  1092. The original system is formed by ``n + k - 1`` equations where the first
  1093. ``k - 1`` of them stand for the ``k - 1`` derivatives continuity on the
  1094. edges while the other equations correspond to an interpolating case
  1095. (matching all the input points). Due to a special form of knot vector, it
  1096. can be proved that in the original system the first and last ``k``
  1097. coefficients of a spline function are the same, respectively. It follows
  1098. from the fact that all ``k - 1`` derivatives are equal term by term at ends
  1099. and that the matrix of the original system of linear equations is
  1100. non-degenerate. So, we can reduce the number of equations to ``n - 1``
  1101. (first ``k - 1`` equations could be reduced). Another trick of this
  1102. implementation is cyclic shift of values of B-splines due to equality of
  1103. ``k`` unknown coefficients. With this we can receive matrix of the system
  1104. with upper right and lower left blocks, and ``k`` diagonals. It allows
  1105. to use Woodbury formula to optimize the computations.
  1106. '''
  1107. n = y.shape[0]
  1108. extradim = prod(y.shape[1:])
  1109. y_new = y.reshape(n, extradim)
  1110. c = np.zeros((n + k - 1, extradim))
  1111. # n <= k case is solved with full matrix
  1112. if n <= k:
  1113. for i in range(extradim):
  1114. c[:, i] = _make_interp_per_full_matr(x, y_new[:, i], t, k)
  1115. c = np.ascontiguousarray(c.reshape((n + k - 1,) + y.shape[1:]))
  1116. t, c = xp.asarray(t), xp.asarray(c)
  1117. return BSpline.construct_fast(t, c, k, extrapolate='periodic', axis=axis)
  1118. nt = len(t) - k - 1
  1119. # size of block elements
  1120. kul = int(k / 2)
  1121. # kl = ku = k
  1122. ab = np.zeros((3 * k + 1, nt), dtype=np.float64, order='F')
  1123. # upper right and lower left blocks
  1124. ur = np.zeros((kul, kul))
  1125. ll = np.zeros_like(ur)
  1126. # `offset` is made to shift all the non-zero elements to the end of the
  1127. # matrix
  1128. # NB: 1. drop the last element of `x` because `x[0] = x[-1] + T` & `y[0] == y[-1]`
  1129. # 2. pass ab.T to _coloc to make it C-ordered; below it'll be fed to banded
  1130. # LAPACK, which needs F-ordered arrays
  1131. _dierckx._coloc(x[:-1], t, k, ab.T, k)
  1132. # remove zeros before the matrix
  1133. ab = ab[-k - (k + 1) % 2:, :]
  1134. # The least elements in rows (except repetitions) are diagonals
  1135. # of block matrices. Upper right matrix is an upper triangular
  1136. # matrix while lower left is a lower triangular one.
  1137. for i in range(kul):
  1138. ur += np.diag(ab[-i - 1, i: kul], k=i)
  1139. ll += np.diag(ab[i, -kul - (k % 2): n - 1 + 2 * kul - i], k=-i)
  1140. # remove elements that occur in the last point
  1141. # (first and last points are equivalent)
  1142. A = ab[:, kul: -k + kul]
  1143. for i in range(extradim):
  1144. cc = _woodbury_algorithm(A, ur, ll, y_new[:, i][:-1], k)
  1145. c[:, i] = np.concatenate((cc[-kul:], cc, cc[:kul + k % 2]))
  1146. c = np.ascontiguousarray(c.reshape((n + k - 1,) + y.shape[1:]))
  1147. t, c = xp.asarray(t), xp.asarray(c)
  1148. return BSpline.construct_fast(t, c, k, extrapolate='periodic', axis=axis)
  1149. @xp_capabilities(cpu_only=True, jax_jit=False, allow_dask_compute=True)
  1150. def make_interp_spline(x, y, k=3, t=None, bc_type=None, axis=0,
  1151. check_finite=True):
  1152. """Create an interpolating B-spline with specified degree and boundary conditions.
  1153. Parameters
  1154. ----------
  1155. x : array_like, shape (n,)
  1156. Abscissas.
  1157. y : array_like, shape (n, ...)
  1158. Ordinates.
  1159. k : int, optional
  1160. B-spline degree. Default is cubic, ``k = 3``.
  1161. t : array_like, shape (nt + k + 1,), optional.
  1162. Knots.
  1163. The number of knots needs to agree with the number of data points and
  1164. the number of derivatives at the edges. Specifically, ``nt - n`` must
  1165. equal ``len(deriv_l) + len(deriv_r)``.
  1166. bc_type : 2-tuple or None
  1167. Boundary conditions.
  1168. Default is None, which means choosing the boundary conditions
  1169. automatically. Otherwise, it must be a length-two tuple where the first
  1170. element (``deriv_l``) sets the boundary conditions at ``x[0]`` and
  1171. the second element (``deriv_r``) sets the boundary conditions at
  1172. ``x[-1]``. Each of these must be an iterable of pairs
  1173. ``(order, value)`` which gives the values of derivatives of specified
  1174. orders at the given edge of the interpolation interval.
  1175. Alternatively, the following string aliases are recognized:
  1176. * ``"clamped"``: The first derivatives at the ends are zero. This is
  1177. equivalent to ``bc_type=([(1, 0.0)], [(1, 0.0)])``.
  1178. * ``"natural"``: The second derivatives at ends are zero. This is
  1179. equivalent to ``bc_type=([(2, 0.0)], [(2, 0.0)])``.
  1180. * ``"not-a-knot"`` (default): The first and second segments are the
  1181. same polynomial. This is equivalent to having ``bc_type=None``.
  1182. * ``"periodic"``: The values and the first ``k-1`` derivatives at the
  1183. ends are equivalent.
  1184. axis : int, optional
  1185. Interpolation axis. Default is 0.
  1186. check_finite : bool, optional
  1187. Whether to check that the input arrays contain only finite numbers.
  1188. Disabling may give a performance gain, but may result in problems
  1189. (crashes, non-termination) if the inputs do contain infinities or NaNs.
  1190. Default is True.
  1191. Returns
  1192. -------
  1193. b : `BSpline` object
  1194. A `BSpline` object of the degree ``k`` and with knots ``t``.
  1195. See Also
  1196. --------
  1197. BSpline : base class representing the B-spline objects
  1198. CubicSpline : a cubic spline in the polynomial basis
  1199. make_lsq_spline : a similar factory function for spline fitting
  1200. UnivariateSpline : a wrapper over FITPACK spline fitting routines
  1201. splrep : a wrapper over FITPACK spline fitting routines
  1202. Examples
  1203. --------
  1204. Use cubic interpolation on Chebyshev nodes:
  1205. >>> import numpy as np
  1206. >>> import matplotlib.pyplot as plt
  1207. >>> def cheb_nodes(N):
  1208. ... jj = 2.*np.arange(N) + 1
  1209. ... x = np.cos(np.pi * jj / 2 / N)[::-1]
  1210. ... return x
  1211. >>> x = cheb_nodes(20)
  1212. >>> y = np.sqrt(1 - x**2)
  1213. >>> from scipy.interpolate import BSpline, make_interp_spline
  1214. >>> b = make_interp_spline(x, y)
  1215. >>> np.allclose(b(x), y)
  1216. True
  1217. Note that the default is a cubic spline with a not-a-knot boundary condition
  1218. >>> b.k
  1219. 3
  1220. Here we use a 'natural' spline, with zero 2nd derivatives at edges:
  1221. >>> l, r = [(2, 0.0)], [(2, 0.0)]
  1222. >>> b_n = make_interp_spline(x, y, bc_type=(l, r)) # or, bc_type="natural"
  1223. >>> np.allclose(b_n(x), y)
  1224. True
  1225. >>> x0, x1 = x[0], x[-1]
  1226. >>> np.allclose([b_n(x0, 2), b_n(x1, 2)], [0, 0])
  1227. True
  1228. Interpolation of parametric curves is also supported. As an example, we
  1229. compute a discretization of a snail curve in polar coordinates
  1230. >>> phi = np.linspace(0, 2.*np.pi, 40)
  1231. >>> r = 0.3 + np.cos(phi)
  1232. >>> x, y = r*np.cos(phi), r*np.sin(phi) # convert to Cartesian coordinates
  1233. Build an interpolating curve, parameterizing it by the angle
  1234. >>> spl = make_interp_spline(phi, np.c_[x, y])
  1235. Evaluate the interpolant on a finer grid (note that we transpose the result
  1236. to unpack it into a pair of x- and y-arrays)
  1237. >>> phi_new = np.linspace(0, 2.*np.pi, 100)
  1238. >>> x_new, y_new = spl(phi_new).T
  1239. Plot the result
  1240. >>> plt.plot(x, y, 'o')
  1241. >>> plt.plot(x_new, y_new, '-')
  1242. >>> plt.show()
  1243. Build a B-spline curve with 2 dimensional y
  1244. >>> x = np.linspace(0, 2*np.pi, 10)
  1245. >>> y = np.array([np.sin(x), np.cos(x)])
  1246. Periodic condition is satisfied because y coordinates of points on the ends
  1247. are equivalent
  1248. >>> ax = plt.axes(projection='3d')
  1249. >>> xx = np.linspace(0, 2*np.pi, 100)
  1250. >>> bspl = make_interp_spline(x, y, k=5, bc_type='periodic', axis=1)
  1251. >>> ax.plot3D(xx, *bspl(xx))
  1252. >>> ax.scatter3D(x, *y, color='red')
  1253. >>> plt.show()
  1254. """
  1255. # convert string aliases for the boundary conditions
  1256. if bc_type is None or bc_type == 'not-a-knot' or bc_type == 'periodic':
  1257. deriv_l, deriv_r = None, None
  1258. elif isinstance(bc_type, str):
  1259. deriv_l, deriv_r = bc_type, bc_type
  1260. else:
  1261. try:
  1262. deriv_l, deriv_r = bc_type
  1263. except TypeError as e:
  1264. raise ValueError(f"Unknown boundary condition: {bc_type}") from e
  1265. xp = array_namespace(x, y, t)
  1266. x = _as_float_array(x, check_finite)
  1267. y = _as_float_array(y, check_finite)
  1268. axis = normalize_axis_index(axis, y.ndim)
  1269. y = np.moveaxis(y, axis, 0) # now internally interp axis is zero
  1270. # sanity check the input
  1271. if bc_type == 'periodic' and not np.allclose(y[0], y[-1], atol=1e-15):
  1272. raise ValueError("First and last points does not match while "
  1273. "periodic case expected")
  1274. if x.size != y.shape[0]:
  1275. raise ValueError(f'Shapes of x {x.shape} and y {y.shape} are incompatible')
  1276. if np.any(x[1:] == x[:-1]):
  1277. raise ValueError("Expect x to not have duplicates")
  1278. if x.ndim != 1 or np.any(x[1:] < x[:-1]):
  1279. raise ValueError("Expect x to be a 1D strictly increasing sequence.")
  1280. # special-case k=0 right away
  1281. if k == 0:
  1282. if any(_ is not None for _ in (t, deriv_l, deriv_r)):
  1283. raise ValueError("Too much info for k=0: t and bc_type can only "
  1284. "be None.")
  1285. t = np.r_[x, x[-1]]
  1286. c = np.asarray(y)
  1287. c = np.ascontiguousarray(c, dtype=_get_dtype(c.dtype))
  1288. t, c = xp.asarray(t), xp.asarray(c)
  1289. return BSpline.construct_fast(t, c, k, axis=axis)
  1290. # special-case k=1 (e.g., Lyche and Morken, Eq.(2.16))
  1291. if k == 1 and t is None:
  1292. if not (deriv_l is None and deriv_r is None):
  1293. raise ValueError("Too much info for k=1: bc_type can only be None.")
  1294. t = np.r_[x[0], x, x[-1]]
  1295. c = np.asarray(y)
  1296. c = np.ascontiguousarray(c, dtype=_get_dtype(c.dtype))
  1297. t, c = xp.asarray(t), xp.asarray(c)
  1298. return BSpline.construct_fast(t, c, k, axis=axis)
  1299. k = operator.index(k)
  1300. if bc_type == 'periodic' and t is not None:
  1301. raise NotImplementedError("For periodic case t is constructed "
  1302. "automatically and can not be passed "
  1303. "manually")
  1304. # come up with a sensible knot vector, if needed
  1305. if t is None:
  1306. if deriv_l is None and deriv_r is None:
  1307. if bc_type == 'periodic':
  1308. t = _periodic_knots(x, k)
  1309. else:
  1310. t = _not_a_knot(x, k)
  1311. else:
  1312. t = _augknt(x, k)
  1313. t = _as_float_array(t, check_finite)
  1314. if k < 0:
  1315. raise ValueError("Expect non-negative k.")
  1316. if t.ndim != 1 or np.any(t[1:] < t[:-1]):
  1317. raise ValueError("Expect t to be a 1-D sorted array_like.")
  1318. if t.size < x.size + k + 1:
  1319. raise ValueError(f"Got {t.size} knots, need at least {x.size + k + 1}.")
  1320. if (x[0] < t[k]) or (x[-1] > t[-k]):
  1321. raise ValueError(f'Out of bounds w/ x = {x}.')
  1322. if bc_type == 'periodic':
  1323. return _make_periodic_spline(x, y, t, k, axis, xp=xp)
  1324. # Here : deriv_l, r = [(nu, value), ...]
  1325. deriv_l = _convert_string_aliases(deriv_l, y.shape[1:])
  1326. deriv_l_ords, deriv_l_vals = _process_deriv_spec(deriv_l)
  1327. nleft = deriv_l_ords.shape[0]
  1328. deriv_r = _convert_string_aliases(deriv_r, y.shape[1:])
  1329. deriv_r_ords, deriv_r_vals = _process_deriv_spec(deriv_r)
  1330. nright = deriv_r_ords.shape[0]
  1331. if not all(0 <= i <= k for i in deriv_l_ords):
  1332. raise ValueError(f"Bad boundary conditions at {x[0]}.")
  1333. if not all(0 <= i <= k for i in deriv_r_ords):
  1334. raise ValueError(f"Bad boundary conditions at {x[-1]}.")
  1335. # have `n` conditions for `nt` coefficients; need nt-n derivatives
  1336. n = x.size
  1337. nt = t.size - k - 1
  1338. if nt - n != nleft + nright:
  1339. raise ValueError("The number of derivatives at boundaries does not "
  1340. f"match: expected {nt-n}, got {nleft}+{nright}")
  1341. # bail out if the `y` array is zero-sized
  1342. if y.size == 0:
  1343. c = np.zeros((nt,) + y.shape[1:], dtype=float)
  1344. return BSpline.construct_fast(t, c, k, axis=axis)
  1345. # set up the LHS: the colocation matrix + derivatives at boundaries
  1346. # NB: ab is in F order for banded LAPACK; _coloc needs C-ordered arrays,
  1347. # this pass ab.T into _coloc
  1348. kl = ku = k
  1349. ab = np.zeros((2*kl + ku + 1, nt), dtype=np.float64, order='F')
  1350. _dierckx._coloc(x, t, k, ab.T, nleft)
  1351. if nleft > 0:
  1352. _handle_lhs_derivatives(t, k, x[0], ab, kl, ku, deriv_l_ords)
  1353. if nright > 0:
  1354. _handle_lhs_derivatives(t, k, x[-1], ab, kl, ku, deriv_r_ords,
  1355. offset=nt-nright)
  1356. # set up the RHS: values to interpolate (+ derivative values, if any)
  1357. extradim = prod(y.shape[1:])
  1358. rhs = np.empty((nt, extradim), dtype=y.dtype)
  1359. if nleft > 0:
  1360. rhs[:nleft] = deriv_l_vals.reshape(-1, extradim)
  1361. rhs[nleft:nt - nright] = y.reshape(-1, extradim)
  1362. if nright > 0:
  1363. rhs[nt - nright:] = deriv_r_vals.reshape(-1, extradim)
  1364. # solve Ab @ x = rhs; this is the relevant part of linalg.solve_banded
  1365. if check_finite:
  1366. ab, rhs = map(np.asarray_chkfinite, (ab, rhs))
  1367. gbsv, = get_lapack_funcs(('gbsv',), (ab, rhs))
  1368. lu, piv, c, info = gbsv(kl, ku, ab, rhs,
  1369. overwrite_ab=True, overwrite_b=True)
  1370. if info > 0:
  1371. raise LinAlgError("Colocation matrix is singular.")
  1372. elif info < 0:
  1373. raise ValueError(f'illegal value in {-info}-th argument of internal gbsv')
  1374. c = np.ascontiguousarray(c.reshape((nt,) + y.shape[1:]))
  1375. t, c = xp.asarray(t), xp.asarray(c)
  1376. return BSpline.construct_fast(t, c, k, axis=axis)
  1377. @xp_capabilities(cpu_only=True, jax_jit=False, allow_dask_compute=True)
  1378. def make_lsq_spline(x, y, t, k=3, w=None, axis=0, check_finite=True, *, method="qr"):
  1379. r"""Create a smoothing B-spline satisfying the Least SQuares (LSQ) criterion.
  1380. The result is a linear combination
  1381. .. math::
  1382. S(x) = \sum_j c_j B_j(x; t)
  1383. of the B-spline basis elements, :math:`B_j(x; t)`, which minimizes
  1384. .. math::
  1385. \sum_{j} \left( w_j \times (S(x_j) - y_j) \right)^2
  1386. Parameters
  1387. ----------
  1388. x : array_like, shape (m,)
  1389. Abscissas.
  1390. y : array_like, shape (m, ...)
  1391. Ordinates.
  1392. t : array_like, shape (n + k + 1,).
  1393. Knots.
  1394. Knots and data points must satisfy Schoenberg-Whitney conditions.
  1395. k : int, optional
  1396. B-spline degree. Default is cubic, ``k = 3``.
  1397. w : array_like, shape (m,), optional
  1398. Weights for spline fitting. Must be positive. If ``None``,
  1399. then weights are all equal.
  1400. Default is ``None``.
  1401. axis : int, optional
  1402. Interpolation axis. Default is zero.
  1403. check_finite : bool, optional
  1404. Whether to check that the input arrays contain only finite numbers.
  1405. Disabling may give a performance gain, but may result in problems
  1406. (crashes, non-termination) if the inputs do contain infinities or NaNs.
  1407. Default is True.
  1408. method : str, optional
  1409. Method for solving the linear LSQ problem. Allowed values are "norm-eq"
  1410. (Explicitly construct and solve the normal system of equations), and
  1411. "qr" (Use the QR factorization of the design matrix).
  1412. Default is "qr".
  1413. Returns
  1414. -------
  1415. b : `BSpline` object
  1416. A `BSpline` object of the degree ``k`` with knots ``t``.
  1417. See Also
  1418. --------
  1419. BSpline : base class representing the B-spline objects
  1420. make_interp_spline : a similar factory function for interpolating splines
  1421. LSQUnivariateSpline : a FITPACK-based spline fitting routine
  1422. splrep : a FITPACK-based fitting routine
  1423. Notes
  1424. -----
  1425. The number of data points must be larger than the spline degree ``k``.
  1426. Knots ``t`` must satisfy the Schoenberg-Whitney conditions,
  1427. i.e., there must be a subset of data points ``x[j]`` such that
  1428. ``t[j] < x[j] < t[j+k+1]``, for ``j=0, 1,...,n-k-2``.
  1429. Examples
  1430. --------
  1431. Generate some noisy data:
  1432. >>> import numpy as np
  1433. >>> import matplotlib.pyplot as plt
  1434. >>> rng = np.random.default_rng()
  1435. >>> x = np.linspace(-3, 3, 50)
  1436. >>> y = np.exp(-x**2) + 0.1 * rng.standard_normal(50)
  1437. Now fit a smoothing cubic spline with a pre-defined internal knots.
  1438. Here we make the knot vector (k+1)-regular by adding boundary knots:
  1439. >>> from scipy.interpolate import make_lsq_spline, BSpline
  1440. >>> t = [-1, 0, 1]
  1441. >>> k = 3
  1442. >>> t = np.r_[(x[0],)*(k+1),
  1443. ... t,
  1444. ... (x[-1],)*(k+1)]
  1445. >>> spl = make_lsq_spline(x, y, t, k)
  1446. For comparison, we also construct an interpolating spline for the same
  1447. set of data:
  1448. >>> from scipy.interpolate import make_interp_spline
  1449. >>> spl_i = make_interp_spline(x, y)
  1450. Plot both:
  1451. >>> xs = np.linspace(-3, 3, 100)
  1452. >>> plt.plot(x, y, 'ro', ms=5)
  1453. >>> plt.plot(xs, spl(xs), 'g-', lw=3, label='LSQ spline')
  1454. >>> plt.plot(xs, spl_i(xs), 'b-', lw=3, alpha=0.7, label='interp spline')
  1455. >>> plt.legend(loc='best')
  1456. >>> plt.show()
  1457. **NaN handling**: If the input arrays contain ``nan`` values, the result is
  1458. not useful since the underlying spline fitting routines cannot deal with
  1459. ``nan``. A workaround is to use zero weights for not-a-number data points:
  1460. >>> y[8] = np.nan
  1461. >>> w = np.isnan(y)
  1462. >>> y[w] = 0.
  1463. >>> tck = make_lsq_spline(x, y, t, w=~w)
  1464. Notice the need to replace a ``nan`` by a numerical value (precise value
  1465. does not matter as long as the corresponding weight is zero.)
  1466. """
  1467. xp = array_namespace(x, y, t, w)
  1468. x = _as_float_array(x, check_finite)
  1469. y = _as_float_array(y, check_finite)
  1470. t = _as_float_array(t, check_finite)
  1471. if w is not None:
  1472. w = _as_float_array(w, check_finite)
  1473. else:
  1474. w = np.ones_like(x)
  1475. k = operator.index(k)
  1476. axis = normalize_axis_index(axis, y.ndim)
  1477. y = np.moveaxis(y, axis, 0) # now internally interp axis is zero
  1478. if not y.flags.c_contiguous:
  1479. # C routines in _dierckx currently require C contiguity
  1480. y = y.copy(order='C')
  1481. if x.ndim != 1:
  1482. raise ValueError("Expect x to be a 1-D sequence.")
  1483. if x.shape[0] < k+1:
  1484. raise ValueError("Need more x points.")
  1485. if k < 0:
  1486. raise ValueError("Expect non-negative k.")
  1487. if t.ndim != 1 or np.any(t[1:] - t[:-1] < 0):
  1488. raise ValueError("Expect t to be a 1D strictly increasing sequence.")
  1489. if x.size != y.shape[0]:
  1490. raise ValueError(f'Shapes of x {x.shape} and y {y.shape} are incompatible')
  1491. if k > 0 and np.any((x < t[k]) | (x > t[-k])):
  1492. raise ValueError(f'Out of bounds w/ x = {x}.')
  1493. if x.size != w.size:
  1494. raise ValueError(f'Shapes of x {x.shape} and w {w.shape} are incompatible')
  1495. if method == "norm-eq" and np.any(x[1:] - x[:-1] <= 0):
  1496. raise ValueError("Expect x to be a 1D strictly increasing sequence.")
  1497. if method == "qr" and any(x[1:] - x[:-1] < 0):
  1498. raise ValueError("Expect x to be a 1D non-decreasing sequence.")
  1499. # number of coefficients
  1500. n = t.size - k - 1
  1501. # complex y: view as float, preserve the length
  1502. was_complex = y.dtype.kind == 'c'
  1503. yy = y.view(float)
  1504. if was_complex and y.ndim == 1:
  1505. yy = yy.reshape(y.shape[0], 2)
  1506. # multiple r.h.s
  1507. extradim = prod(yy.shape[1:])
  1508. yy = yy.reshape(-1, extradim)
  1509. # complex y: view as float, preserve the length
  1510. was_complex = y.dtype.kind == 'c'
  1511. yy = y.view(float)
  1512. if was_complex and y.ndim == 1:
  1513. yy = yy.reshape(y.shape[0], 2)
  1514. # multiple r.h.s
  1515. extradim = prod(yy.shape[1:])
  1516. yy = yy.reshape(-1, extradim)
  1517. if method == "norm-eq":
  1518. # construct A.T @ A and rhs with A the colocation matrix, and
  1519. # rhs = A.T @ y for solving the LSQ problem ``A.T @ A @ c = A.T @ y``
  1520. lower = True
  1521. ab = np.zeros((k+1, n), dtype=np.float64, order='F')
  1522. rhs = np.zeros((n, extradim), dtype=np.float64)
  1523. _dierckx._norm_eq_lsq(x, t, k,
  1524. yy,
  1525. w,
  1526. ab.T, rhs)
  1527. # undo complex -> float and flattening the trailing dims
  1528. if was_complex:
  1529. rhs = rhs.view(complex)
  1530. rhs = rhs.reshape((n,) + y.shape[1:])
  1531. # have observation matrix & rhs, can solve the LSQ problem
  1532. cho_decomp = cholesky_banded(ab, overwrite_ab=True, lower=lower,
  1533. check_finite=check_finite)
  1534. m = rhs.shape[0]
  1535. c = cho_solve_banded((cho_decomp, lower), rhs.reshape(m, -1), overwrite_b=True,
  1536. check_finite=check_finite).reshape(rhs.shape)
  1537. elif method == "qr":
  1538. _, _, c, _, _ = _lsq_solve_qr(x, yy, t, k, w)
  1539. if was_complex:
  1540. c = c.view(complex)
  1541. else:
  1542. raise ValueError(f"Unknown {method =}.")
  1543. # restore the shape of `c` for both single and multiple r.h.s.
  1544. c = c.reshape((n,) + y.shape[1:])
  1545. c = np.ascontiguousarray(c)
  1546. t, c = xp.asarray(t), xp.asarray(c)
  1547. return BSpline.construct_fast(t, c, k, axis=axis)
  1548. ######################
  1549. # LSQ spline helpers #
  1550. ######################
  1551. def _lsq_solve_qr_for_root_rati_periodic(x, y, t, k, w):
  1552. """Solve for the LSQ spline coeffs given x, y and knots.
  1553. `y` is always 2D: for 1D data, the shape is ``(m, 1)``.
  1554. `w` is always 1D: one weight value per `x` value.
  1555. """
  1556. y_w = y * w[:, None]
  1557. # Ref: https://github.com/scipy/scipy/blob/maintenance/1.16.x/scipy/interpolate/fitpack/fpperi.f#L221-L238
  1558. R, H1, H2, offset, nc = _dierckx.data_matrix_periodic(x, t, k, w, False)
  1559. # Ref: https://github.com/scipy/scipy/blob/maintenance/1.16.x/scipy/interpolate/fitpack/fpperi.f#L239-L314
  1560. A1, A2, Z, p, _ = _dierckx.qr_reduce_periodic(
  1561. R, H1, H2, offset, nc, y_w, k,
  1562. len(t), True
  1563. ) # modifies arguments in-place
  1564. # Ref: https://github.com/scipy/scipy/blob/main/scipy/interpolate/fitpack/fpbacp.f
  1565. c, residuals, _ = _dierckx.fpbacp(A1, A2, Z, k, k, x, y, t, w)
  1566. return R, A1, A2, Z, y_w, c, p, residuals
  1567. def _lsq_solve_qr(x, y, t, k, w, periodic=False):
  1568. """Solve for the LSQ spline coeffs given x, y and knots.
  1569. `y` is always 2D: for 1D data, the shape is ``(m, 1)``.
  1570. `w` is always 1D: one weight value per `x` value.
  1571. """
  1572. y_w = y * w[:, None]
  1573. if not periodic:
  1574. A, offset, nc = _dierckx.data_matrix(x, t, k, w)
  1575. _dierckx.qr_reduce(A, offset, nc, y_w) # modifies arguments in-place
  1576. c, residuals, fp = _dierckx.fpback(A, nc, x, y, t, k, w, y_w)
  1577. return A, y_w, c, fp, residuals
  1578. else:
  1579. # Ref: https://github.com/scipy/scipy/blob/maintenance/1.16.x/scipy/interpolate/fitpack/fpperi.f#L221-L238
  1580. R, H1, H2, offset, nc = _dierckx.data_matrix_periodic(x, t, k, w, False)
  1581. # Ref: https://github.com/scipy/scipy/blob/maintenance/1.16.x/scipy/interpolate/fitpack/fpperi.f#L239-L314
  1582. A1, A2, Z, fp = _dierckx.qr_reduce_periodic(
  1583. R, H1, H2, offset, nc, y_w, k,
  1584. len(t), False) # modifies arguments in-place
  1585. # Ref: https://github.com/scipy/scipy/blob/main/scipy/interpolate/fitpack/fpbacp.f
  1586. c, residuals, _ = _dierckx.fpbacp(A1, A2, Z, k, k, x, y, t, w)
  1587. return R, y_w, c, fp, residuals
  1588. #############################
  1589. # Smoothing spline helpers #
  1590. #############################
  1591. def _compute_optimal_gcv_parameter(X, wE, y, w):
  1592. """
  1593. Returns an optimal regularization parameter from the GCV criteria [1].
  1594. Parameters
  1595. ----------
  1596. X : array, shape (5, n)
  1597. 5 bands of the design matrix ``X`` stored in LAPACK banded storage.
  1598. wE : array, shape (5, n)
  1599. 5 bands of the penalty matrix :math:`W^{-1} E` stored in LAPACK banded
  1600. storage.
  1601. y : array, shape (n,)
  1602. Ordinates.
  1603. w : array, shape (n,)
  1604. Vector of weights.
  1605. Returns
  1606. -------
  1607. lam : float
  1608. An optimal from the GCV criteria point of view regularization
  1609. parameter.
  1610. Notes
  1611. -----
  1612. No checks are performed.
  1613. References
  1614. ----------
  1615. .. [1] G. Wahba, "Estimating the smoothing parameter" in Spline models
  1616. for observational data, Philadelphia, Pennsylvania: Society for
  1617. Industrial and Applied Mathematics, 1990, pp. 45-65.
  1618. :doi:`10.1137/1.9781611970128`
  1619. """
  1620. def compute_banded_symmetric_XT_W_Y(X, w, Y):
  1621. """
  1622. Assuming that the product :math:`X^T W Y` is symmetric and both ``X``
  1623. and ``Y`` are 5-banded, compute the unique bands of the product.
  1624. Parameters
  1625. ----------
  1626. X : array, shape (5, n)
  1627. 5 bands of the matrix ``X`` stored in LAPACK banded storage.
  1628. w : array, shape (n,)
  1629. Array of weights
  1630. Y : array, shape (5, n)
  1631. 5 bands of the matrix ``Y`` stored in LAPACK banded storage.
  1632. Returns
  1633. -------
  1634. res : array, shape (4, n)
  1635. The result of the product :math:`X^T Y` stored in the banded way.
  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. # compute W Y
  1643. W_Y = np.copy(Y)
  1644. W_Y[2] *= w
  1645. for i in range(2):
  1646. W_Y[i, 2 - i:] *= w[:-2 + i]
  1647. W_Y[3 + i, :-1 - i] *= w[1 + i:]
  1648. n = X.shape[1]
  1649. res = np.zeros((4, n))
  1650. for i in range(n):
  1651. for j in range(min(n-i, 4)):
  1652. res[-j-1, i + j] = sum(X[j:, i] * W_Y[:5-j, i + j])
  1653. return res
  1654. def compute_b_inv(A):
  1655. """
  1656. Inverse 3 central bands of matrix :math:`A=U^T D^{-1} U` assuming that
  1657. ``U`` is a unit upper triangular banded matrix using an algorithm
  1658. proposed in [1].
  1659. Parameters
  1660. ----------
  1661. A : array, shape (4, n)
  1662. Matrix to inverse, stored in LAPACK banded storage.
  1663. Returns
  1664. -------
  1665. B : array, shape (4, n)
  1666. 3 unique bands of the symmetric matrix that is an inverse to ``A``.
  1667. The first row is filled with zeros.
  1668. Notes
  1669. -----
  1670. The algorithm is based on the cholesky decomposition and, therefore,
  1671. in case matrix ``A`` is close to not positive defined, the function
  1672. raises LinalgError.
  1673. Both matrices ``A`` and ``B`` are stored in LAPACK banded storage.
  1674. References
  1675. ----------
  1676. .. [1] M. F. Hutchinson and F. R. de Hoog, "Smoothing noisy data with
  1677. spline functions," Numerische Mathematik, vol. 47, no. 1,
  1678. pp. 99-106, 1985.
  1679. :doi:`10.1007/BF01389878`
  1680. """
  1681. def find_b_inv_elem(i, j, U, D, B):
  1682. rng = min(3, n - i - 1)
  1683. rng_sum = 0.
  1684. if j == 0:
  1685. # use 2-nd formula from [1]
  1686. for k in range(1, rng + 1):
  1687. rng_sum -= U[-k - 1, i + k] * B[-k - 1, i + k]
  1688. rng_sum += D[i]
  1689. B[-1, i] = rng_sum
  1690. else:
  1691. # use 1-st formula from [1]
  1692. for k in range(1, rng + 1):
  1693. diag = abs(k - j)
  1694. ind = i + min(k, j)
  1695. rng_sum -= U[-k - 1, i + k] * B[-diag - 1, ind + diag]
  1696. B[-j - 1, i + j] = rng_sum
  1697. U = cholesky_banded(A)
  1698. for i in range(2, 5):
  1699. U[-i, i-1:] /= U[-1, :-i+1]
  1700. D = 1. / (U[-1])**2
  1701. U[-1] /= U[-1]
  1702. n = U.shape[1]
  1703. B = np.zeros(shape=(4, n))
  1704. for i in range(n - 1, -1, -1):
  1705. for j in range(min(3, n - i - 1), -1, -1):
  1706. find_b_inv_elem(i, j, U, D, B)
  1707. # the first row contains garbage and should be removed
  1708. B[0] = [0.] * n
  1709. return B
  1710. def _gcv(lam, X, XtWX, wE, XtE, y):
  1711. r"""
  1712. Computes the generalized cross-validation criteria [1].
  1713. Parameters
  1714. ----------
  1715. lam : float, (:math:`\lambda \geq 0`)
  1716. Regularization parameter.
  1717. X : array, shape (5, n)
  1718. Matrix is stored in LAPACK banded storage.
  1719. XtWX : array, shape (4, n)
  1720. Product :math:`X^T W X` stored in LAPACK banded storage.
  1721. wE : array, shape (5, n)
  1722. Matrix :math:`W^{-1} E` stored in LAPACK banded storage.
  1723. XtE : array, shape (4, n)
  1724. Product :math:`X^T E` stored in LAPACK banded storage.
  1725. Returns
  1726. -------
  1727. res : float
  1728. Value of the GCV criteria with the regularization parameter
  1729. :math:`\lambda`.
  1730. Notes
  1731. -----
  1732. Criteria is computed from the formula (1.3.2) [3]:
  1733. .. math:
  1734. GCV(\lambda) = \dfrac{1}{n} \sum\limits_{k = 1}^{n} \dfrac{ \left(
  1735. y_k - f_{\lambda}(x_k) \right)^2}{\left( 1 - \Tr{A}/n\right)^2}$.
  1736. The criteria is discussed in section 1.3 [3].
  1737. The numerator is computed using (2.2.4) [3] and the denominator is
  1738. computed using an algorithm from [2] (see in the ``compute_b_inv``
  1739. function).
  1740. References
  1741. ----------
  1742. .. [1] G. Wahba, "Estimating the smoothing parameter" in Spline models
  1743. for observational data, Philadelphia, Pennsylvania: Society for
  1744. Industrial and Applied Mathematics, 1990, pp. 45-65.
  1745. :doi:`10.1137/1.9781611970128`
  1746. .. [2] M. F. Hutchinson and F. R. de Hoog, "Smoothing noisy data with
  1747. spline functions," Numerische Mathematik, vol. 47, no. 1,
  1748. pp. 99-106, 1985.
  1749. :doi:`10.1007/BF01389878`
  1750. .. [3] E. Zemlyanoy, "Generalized cross-validation smoothing splines",
  1751. BSc thesis, 2022. Might be available (in Russian)
  1752. `here <https://www.hse.ru/ba/am/students/diplomas/620910604>`_
  1753. """
  1754. # Compute the numerator from (2.2.4) [3]
  1755. n = X.shape[1]
  1756. c = solve_banded((2, 2), X + lam * wE, y)
  1757. res = np.zeros(n)
  1758. # compute ``W^{-1} E c`` with respect to banded-storage of ``E``
  1759. tmp = wE * c
  1760. for i in range(n):
  1761. for j in range(max(0, i - n + 3), min(5, i + 3)):
  1762. res[i] += tmp[j, i + 2 - j]
  1763. numer = np.linalg.norm(lam * res)**2 / n
  1764. # compute the denominator
  1765. lhs = XtWX + lam * XtE
  1766. try:
  1767. b_banded = compute_b_inv(lhs)
  1768. # compute the trace of the product b_banded @ XtX
  1769. tr = b_banded * XtWX
  1770. tr[:-1] *= 2
  1771. # find the denominator
  1772. denom = (1 - sum(sum(tr)) / n)**2
  1773. except LinAlgError:
  1774. # cholesky decomposition cannot be performed
  1775. raise ValueError('Seems like the problem is ill-posed')
  1776. res = numer / denom
  1777. return res
  1778. n = X.shape[1]
  1779. XtWX = compute_banded_symmetric_XT_W_Y(X, w, X)
  1780. XtE = compute_banded_symmetric_XT_W_Y(X, w, wE)
  1781. if y.ndim == 1:
  1782. gcv_est = minimize_scalar(
  1783. _gcv, bounds=(0, n), method='Bounded', args=(X, XtWX, wE, XtE, y)
  1784. )
  1785. if gcv_est.success:
  1786. return gcv_est.x
  1787. raise ValueError(f"Unable to find minimum of the GCV "
  1788. f"function: {gcv_est.message}")
  1789. elif y.ndim == 2:
  1790. gcv_est = np.empty(y.shape[1])
  1791. for i in range(y.shape[1]):
  1792. est = minimize_scalar(
  1793. _gcv, bounds=(0, n), method='Bounded', args=(X, XtWX, wE, XtE, y[:, i])
  1794. )
  1795. if est.success:
  1796. gcv_est[i] = est.x
  1797. else:
  1798. raise ValueError(f"Unable to find minimum of the GCV "
  1799. f"function: {gcv_est.message}")
  1800. return gcv_est
  1801. else:
  1802. # trailing dims must have been flattened already.
  1803. raise RuntimeError("Internal error. Please report it to scipy developers.")
  1804. def _coeff_of_divided_diff(x):
  1805. """
  1806. Returns the coefficients of the divided difference.
  1807. Parameters
  1808. ----------
  1809. x : array, shape (n,)
  1810. Array which is used for the computation of divided difference.
  1811. Returns
  1812. -------
  1813. res : array_like, shape (n,)
  1814. Coefficients of the divided difference.
  1815. Notes
  1816. -----
  1817. Vector ``x`` should have unique elements, otherwise an error division by
  1818. zero might be raised.
  1819. No checks are performed.
  1820. """
  1821. n = x.shape[0]
  1822. res = np.zeros(n)
  1823. for i in range(n):
  1824. pp = 1.
  1825. for k in range(n):
  1826. if k != i:
  1827. pp *= (x[i] - x[k])
  1828. res[i] = 1. / pp
  1829. return res
  1830. @xp_capabilities(cpu_only=True, jax_jit=False, allow_dask_compute=True)
  1831. def make_smoothing_spline(x, y, w=None, lam=None, *, axis=0):
  1832. r"""
  1833. Create a smoothing B-spline satisfying the Generalized Cross Validation (GCV) criterion.
  1834. Compute the (coefficients of) smoothing cubic spline function using
  1835. ``lam`` to control the tradeoff between the amount of smoothness of the
  1836. curve and its proximity to the data. In case ``lam`` is None, using the
  1837. GCV criteria [1] to find it.
  1838. A smoothing spline is found as a solution to the regularized weighted
  1839. linear regression problem:
  1840. .. math::
  1841. \sum\limits_{i=1}^n w_i\lvert y_i - f(x_i) \rvert^2 +
  1842. \lambda\int\limits_{x_1}^{x_n} (f^{(2)}(u))^2 d u
  1843. where :math:`f` is a spline function, :math:`w` is a vector of weights and
  1844. :math:`\lambda` is a regularization parameter.
  1845. If ``lam`` is None, we use the GCV criteria to find an optimal
  1846. regularization parameter, otherwise we solve the regularized weighted
  1847. linear regression problem with given parameter. The parameter controls
  1848. the tradeoff in the following way: the larger the parameter becomes, the
  1849. smoother the function gets.
  1850. Parameters
  1851. ----------
  1852. x : array_like, shape (n,)
  1853. Abscissas. `n` must be at least 5.
  1854. y : array_like, shape (n, ...)
  1855. Ordinates. `n` must be at least 5.
  1856. w : array_like, shape (n,), optional
  1857. Vector of weights. Default is ``np.ones_like(x)``.
  1858. lam : float, (:math:`\lambda \geq 0`), optional
  1859. Regularization parameter. If ``lam`` is None, then it is found from
  1860. the GCV criteria. Default is None.
  1861. axis : int, optional
  1862. The data axis. Default is zero.
  1863. The assumption is that ``y.shape[axis] == n``, and all other axes of ``y``
  1864. are batching axes.
  1865. Returns
  1866. -------
  1867. func : `BSpline` object
  1868. An object representing a spline in the B-spline basis
  1869. as a solution of the problem of smoothing splines using
  1870. the GCV criteria [1] in case ``lam`` is None, otherwise using the
  1871. given parameter ``lam``.
  1872. Notes
  1873. -----
  1874. This algorithm is a clean room reimplementation of the algorithm
  1875. introduced by Woltring in FORTRAN [2]. The original version cannot be used
  1876. in SciPy source code because of the license issues. The details of the
  1877. reimplementation are discussed here (available only in Russian) [4].
  1878. If the vector of weights ``w`` is None, we assume that all the points are
  1879. equal in terms of weights, and vector of weights is vector of ones.
  1880. Note that in weighted residual sum of squares, weights are not squared:
  1881. :math:`\sum\limits_{i=1}^n w_i\lvert y_i - f(x_i) \rvert^2` while in
  1882. ``splrep`` the sum is built from the squared weights.
  1883. In cases when the initial problem is ill-posed (for example, the product
  1884. :math:`X^T W X` where :math:`X` is a design matrix is not a positive
  1885. defined matrix) a ValueError is raised.
  1886. References
  1887. ----------
  1888. .. [1] G. Wahba, "Estimating the smoothing parameter" in Spline models for
  1889. observational data, Philadelphia, Pennsylvania: Society for Industrial
  1890. and Applied Mathematics, 1990, pp. 45-65.
  1891. :doi:`10.1137/1.9781611970128`
  1892. .. [2] H. J. Woltring, A Fortran package for generalized, cross-validatory
  1893. spline smoothing and differentiation, Advances in Engineering
  1894. Software, vol. 8, no. 2, pp. 104-113, 1986.
  1895. :doi:`10.1016/0141-1195(86)90098-7`
  1896. .. [3] T. Hastie, J. Friedman, and R. Tisbshirani, "Smoothing Splines" in
  1897. The elements of Statistical Learning: Data Mining, Inference, and
  1898. prediction, New York: Springer, 2017, pp. 241-249.
  1899. :doi:`10.1007/978-0-387-84858-7`
  1900. .. [4] E. Zemlyanoy, "Generalized cross-validation smoothing splines",
  1901. BSc thesis, 2022.
  1902. `<https://www.hse.ru/ba/am/students/diplomas/620910604>`_ (in
  1903. Russian)
  1904. Examples
  1905. --------
  1906. Generate some noisy data
  1907. >>> import numpy as np
  1908. >>> np.random.seed(1234)
  1909. >>> n = 200
  1910. >>> def func(x):
  1911. ... return x**3 + x**2 * np.sin(4 * x)
  1912. >>> x = np.sort(np.random.random_sample(n) * 4 - 2)
  1913. >>> y = func(x) + np.random.normal(scale=1.5, size=n)
  1914. Make a smoothing spline function
  1915. >>> from scipy.interpolate import make_smoothing_spline
  1916. >>> spl = make_smoothing_spline(x, y)
  1917. Plot both
  1918. >>> import matplotlib.pyplot as plt
  1919. >>> grid = np.linspace(x[0], x[-1], 400)
  1920. >>> plt.plot(x, y, '.')
  1921. >>> plt.plot(grid, spl(grid), label='Spline')
  1922. >>> plt.plot(grid, func(grid), label='Original function')
  1923. >>> plt.legend(loc='best')
  1924. >>> plt.show()
  1925. """ # noqa:E501
  1926. xp = array_namespace(x, y)
  1927. x = np.ascontiguousarray(x, dtype=float)
  1928. y = np.ascontiguousarray(y, dtype=float)
  1929. if any(x[1:] - x[:-1] <= 0):
  1930. raise ValueError('``x`` should be an ascending array')
  1931. if x.ndim != 1 or x.shape[0] != y.shape[axis]:
  1932. raise ValueError(f'``x`` should be 1D and {x.shape = } == {y.shape = }')
  1933. if w is None:
  1934. w = np.ones(len(x))
  1935. else:
  1936. w = np.ascontiguousarray(w)
  1937. if any(w <= 0):
  1938. raise ValueError('Invalid vector of weights')
  1939. t = np.r_[[x[0]] * 3, x, [x[-1]] * 3]
  1940. n = x.shape[0]
  1941. if n <= 4:
  1942. raise ValueError('``x`` and ``y`` length must be at least 5')
  1943. # Internals assume that the data axis is the zero-th axis
  1944. axis = normalize_axis_index(axis, y.ndim)
  1945. y = np.moveaxis(y, axis, 0)
  1946. # flatten the trailing axes of y to simplify further manipulations
  1947. y_shape1 = y.shape[1:]
  1948. if y_shape1 != ():
  1949. y = y.reshape((n, -1))
  1950. # It is known that the solution to the stated minimization problem exists
  1951. # and is a natural cubic spline with vector of knots equal to the unique
  1952. # elements of ``x`` [3], so we will solve the problem in the basis of
  1953. # natural splines.
  1954. # create design matrix in the B-spline basis
  1955. X_bspl = BSpline.design_matrix(x, t, 3)
  1956. # move from B-spline basis to the basis of natural splines using equations
  1957. # (2.1.7) [4]
  1958. # central elements
  1959. X = np.zeros((5, n))
  1960. for i in range(1, 4):
  1961. X[i, 2: -2] = X_bspl[i: i - 4, 3: -3][np.diag_indices(n - 4)]
  1962. # first elements
  1963. X[1, 1] = X_bspl[0, 0]
  1964. X[2, :2] = ((x[2] + x[1] - 2 * x[0]) * X_bspl[0, 0],
  1965. X_bspl[1, 1] + X_bspl[1, 2])
  1966. X[3, :2] = ((x[2] - x[0]) * X_bspl[1, 1], X_bspl[2, 2])
  1967. # last elements
  1968. X[1, -2:] = (X_bspl[-3, -3], (x[-1] - x[-3]) * X_bspl[-2, -2])
  1969. X[2, -2:] = (X_bspl[-2, -3] + X_bspl[-2, -2],
  1970. (2 * x[-1] - x[-2] - x[-3]) * X_bspl[-1, -1])
  1971. X[3, -2] = X_bspl[-1, -1]
  1972. # create penalty matrix and divide it by vector of weights: W^{-1} E
  1973. wE = np.zeros((5, n))
  1974. wE[2:, 0] = _coeff_of_divided_diff(x[:3]) / w[:3]
  1975. wE[1:, 1] = _coeff_of_divided_diff(x[:4]) / w[:4]
  1976. for j in range(2, n - 2):
  1977. wE[:, j] = (x[j+2] - x[j-2]) * _coeff_of_divided_diff(x[j-2:j+3])\
  1978. / w[j-2: j+3]
  1979. wE[:-1, -2] = -_coeff_of_divided_diff(x[-4:]) / w[-4:]
  1980. wE[:-2, -1] = _coeff_of_divided_diff(x[-3:]) / w[-3:]
  1981. wE *= 6
  1982. if lam is None:
  1983. lam = _compute_optimal_gcv_parameter(X, wE, y, w)
  1984. elif lam < 0.:
  1985. raise ValueError('Regularization parameter should be non-negative')
  1986. # solve the initial problem in the basis of natural splines
  1987. if np.ndim(lam) == 0:
  1988. c = solve_banded((2, 2), X + lam * wE, y)
  1989. elif np.ndim(lam) == 1:
  1990. # XXX: solve_banded does not suppport batched `ab` matrices; loop manually
  1991. c = np.empty((n, lam.shape[0]))
  1992. for i in range(lam.shape[0]):
  1993. c[:, i] = solve_banded((2, 2), X + lam[i] * wE, y[:, i])
  1994. else:
  1995. # this should not happen, ever
  1996. raise RuntimeError("Internal error, please report it to SciPy developers.")
  1997. c = c.reshape((c.shape[0], *y_shape1))
  1998. # hack: these are c[0], c[1] etc, shape-compatible with np.r_ below
  1999. c0, c1 = c[0:1, ...], c[1:2, ...] # c[0], c[1]
  2000. cm0, cm1 = c[-1:-2:-1, ...], c[-2:-3:-1, ...] # c[-1], c[-2]
  2001. # move back to B-spline basis using equations (2.2.10) [4]
  2002. c_ = np.r_[c0 * (t[5] + t[4] - 2 * t[3]) + c1,
  2003. c0 * (t[5] - t[3]) + c1,
  2004. c[1: -1, ...],
  2005. cm0 * (t[-4] - t[-6]) + cm1,
  2006. cm0 * (2 * t[-4] - t[-5] - t[-6]) + cm1]
  2007. t, c_ = xp.asarray(t), xp.asarray(c_)
  2008. return BSpline.construct_fast(t, c_, 3, axis=axis)
  2009. ########################
  2010. # FITPACK look-alikes #
  2011. ########################
  2012. def fpcheck(x, t, k, periodic=False):
  2013. """Check consistency of data vector `x` and knot vector `t`.
  2014. Parameters
  2015. ----------
  2016. x : array_like, shape (m,)
  2017. 1D sorted array of data points.
  2018. t : array_like, shape (n,)
  2019. 1D non-decreasing knot vector.
  2020. k : int
  2021. Degree of the spline.
  2022. periodic : bool, optional
  2023. Whether the spline is periodic. Default is False.
  2024. Raises
  2025. ------
  2026. ValueError
  2027. If the configuration of `x`, `t`, and `k` violates any required condition.
  2028. """
  2029. # This routine is a unified clone of the FITPACK Fortran routines `fpchec.f`
  2030. # and `fpchep.f`:
  2031. # - https://github.com/scipy/scipy/blob/main/scipy/interpolate/fitpack/fpchec.f
  2032. # - https://github.com/scipy/scipy/blob/main/scipy/interpolate/fitpack/fpchep.f
  2033. #
  2034. # These routines verify the number and position of the knots t(j), j=1,...,n,
  2035. # of a spline of degree k, relative to the number and distribution of data points
  2036. # x(i), i=1,...,m. If all of the following conditions are fulfilled,
  2037. # validation passes.
  2038. #
  2039. # For non-periodic splines:
  2040. # 1) k+1 <= n-k-1 <= m
  2041. # 2) t(1) <= t(2) <= ... <= t(k+1)
  2042. # t(n-k) <= t(n-k+1) <= ... <= t(n)
  2043. # 3) t(k+1) < t(k+2) < ... < t(n-k)
  2044. # 4) t(k+1) <= x(i) <= t(n-k)
  2045. # 5) Schoenberg-Whitney conditions hold: there exists a subset y(j) such that
  2046. # t(j) < y(j) < t(j+k+1), for j = 1, ..., n-k-1
  2047. #
  2048. # For periodic splines:
  2049. # 1) k+1 <= n-k-1 <= m + k - 1
  2050. # 2) Same boundary knot monotonicity as above
  2051. # 3) Same strict interior knot increase as above
  2052. # 4) t(k+1) <= x(i) <= t(n-k)
  2053. # 5) Schoenberg-Whitney conditions must hold for *some periodic shift*
  2054. # of the data sequence; i.e. wrapped data points x(i) must satisfy
  2055. # t(j) < y(j) < t(j+k+1), j = k+1, ..., n-k-1
  2056. x = np.asarray(x)
  2057. t = np.asarray(t)
  2058. if x.ndim != 1 or t.ndim != 1:
  2059. raise ValueError(f"Expect `x` and `t` be 1D sequences. Got {x = } and {t = }")
  2060. m = x.shape[0]
  2061. n = t.shape[0]
  2062. nk1 = n - k - 1
  2063. # check condition no 1
  2064. if periodic:
  2065. # c 1) k+1 <= nk1 <= m+k-1
  2066. if not (k + 1 <= nk1 <= m + k - 1):
  2067. raise ValueError(f"Need k+1 <= n-k-1 <= m+k-1. Got {m = }, {n = }, {k = }")
  2068. else:
  2069. # c 1) k+1 <= n-k-1 <= m
  2070. if not (k + 1 <= nk1 <= m):
  2071. raise ValueError(f"Need k+1 <= n-k-1 <= m. Got {m = }, {n = } and {k = }.")
  2072. # check condition no 2
  2073. # c 2) t(1) <= t(2) <= ... <= t(k+1)
  2074. # c t(n-k) <= t(n-k+1) <= ... <= t(n)
  2075. if (t[:k+1] > t[1:k+2]).any():
  2076. raise ValueError(f"First k knots must be ordered; got {t = }.")
  2077. if (t[nk1:] < t[nk1-1:-1]).any():
  2078. raise ValueError(f"Last k knots must be ordered; got {t = }.")
  2079. # c check condition no 3
  2080. # c 3) t(k+1) < t(k+2) < ... < t(n-k)
  2081. if (t[k+1:n-k] <= t[k:n-k-1]).any():
  2082. raise ValueError(f"Internal knots must be distinct. Got {t = }.")
  2083. # c check condition no 4
  2084. # c 4) t(k+1) <= x(i) <= t(n-k)
  2085. # NB: FITPACK's fpchec only checks x[0] & x[-1], so we follow.
  2086. if (x[0] < t[k]) or (x[-1] > t[n-k-1]):
  2087. raise ValueError(f"Out of bounds: {x = } and {t = }.")
  2088. # c check condition no 5
  2089. # c 5) the conditions specified by Schoenberg and Whitney must hold
  2090. # c for at least one subset of data points y(j) such that
  2091. # c t(j) < y(j) < t(j+k+1)
  2092. # c
  2093. # c For non-periodic splines:
  2094. # c j = 1, 2, ..., n-k-1 (i.e., j in [1, n-k-1])
  2095. # c The data points must lie strictly inside some B-spline supports.
  2096. # c
  2097. # c For periodic splines:
  2098. # c j = k+1, ..., n-k-1
  2099. # c The condition must hold for a wrapped subset of the data points,
  2100. # c i.e., there exists a cyclic shift of the data such that
  2101. # c t(j) < x(i) < t(j+k+1)
  2102. # c holds for all j in that range. The test must account for the
  2103. # c periodic domain length: per = t(n-k) - t(k+1), and wrap around x(i)
  2104. # c as x(i) + per if needed.
  2105. mesg = f"Schoenberg-Whitney condition is violated with {t = } and {x =}."
  2106. if periodic:
  2107. per = t[n - k - 1] - t[k]
  2108. m1 = m - 1
  2109. for shift in range(1, m):
  2110. for j in range(k, nk1):
  2111. tj = t[j]
  2112. tl = t[j + k + 1]
  2113. found = False
  2114. for i in range(shift, shift + m1 + 1):
  2115. idx = i if i < m else i - m
  2116. xi = x[idx] + (0 if i < m else per)
  2117. if tj < xi < tl:
  2118. found = True
  2119. break
  2120. if not found:
  2121. break
  2122. else:
  2123. return
  2124. raise ValueError(mesg)
  2125. else:
  2126. if (x[0] >= t[k+1]) or (x[-1] <= t[n-k-2]):
  2127. raise ValueError(mesg)
  2128. m = x.shape[0]
  2129. l = k+1
  2130. nk3 = n - k - 3
  2131. if nk3 < 2:
  2132. return
  2133. for j in range(1, nk3+1):
  2134. tj = t[j]
  2135. l += 1
  2136. tl = t[l]
  2137. i = np.argmax(x > tj)
  2138. if i >= m-1:
  2139. raise ValueError(mesg)
  2140. if x[i] >= tl:
  2141. raise ValueError(mesg)
  2142. return