Coverage for /usr/lib/python3/dist-packages/sympy/functions/special/bsplines.py: 11%

107 statements  

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

1from sympy.core import S, sympify 

2from sympy.core.symbol import (Dummy, symbols) 

3from sympy.functions import Piecewise, piecewise_fold 

4from sympy.logic.boolalg import And 

5from sympy.sets.sets import Interval 

6 

7from functools import lru_cache 

8 

9 

10def _ivl(cond, x): 

11 """return the interval corresponding to the condition 

12 

13 Conditions in spline's Piecewise give the range over 

14 which an expression is valid like (lo <= x) & (x <= hi). 

15 This function returns (lo, hi). 

16 """ 

17 if isinstance(cond, And) and len(cond.args) == 2: 

18 a, b = cond.args 

19 if a.lts == x: 

20 a, b = b, a 

21 return a.lts, b.gts 

22 raise TypeError('unexpected cond type: %s' % cond) 

23 

24 

25def _add_splines(c, b1, d, b2, x): 

26 """Construct c*b1 + d*b2.""" 

27 

28 if S.Zero in (b1, c): 

29 rv = piecewise_fold(d * b2) 

30 elif S.Zero in (b2, d): 

31 rv = piecewise_fold(c * b1) 

32 else: 

33 new_args = [] 

34 # Just combining the Piecewise without any fancy optimization 

35 p1 = piecewise_fold(c * b1) 

36 p2 = piecewise_fold(d * b2) 

37 

38 # Search all Piecewise arguments except (0, True) 

39 p2args = list(p2.args[:-1]) 

40 

41 # This merging algorithm assumes the conditions in 

42 # p1 and p2 are sorted 

43 for arg in p1.args[:-1]: 

44 expr = arg.expr 

45 cond = arg.cond 

46 

47 lower = _ivl(cond, x)[0] 

48 

49 # Check p2 for matching conditions that can be merged 

50 for i, arg2 in enumerate(p2args): 

51 expr2 = arg2.expr 

52 cond2 = arg2.cond 

53 

54 lower_2, upper_2 = _ivl(cond2, x) 

55 if cond2 == cond: 

56 # Conditions match, join expressions 

57 expr += expr2 

58 # Remove matching element 

59 del p2args[i] 

60 # No need to check the rest 

61 break 

62 elif lower_2 < lower and upper_2 <= lower: 

63 # Check if arg2 condition smaller than arg1, 

64 # add to new_args by itself (no match expected 

65 # in p1) 

66 new_args.append(arg2) 

67 del p2args[i] 

68 break 

69 

70 # Checked all, add expr and cond 

71 new_args.append((expr, cond)) 

72 

73 # Add remaining items from p2args 

74 new_args.extend(p2args) 

75 

76 # Add final (0, True) 

77 new_args.append((0, True)) 

78 

79 rv = Piecewise(*new_args, evaluate=False) 

80 

81 return rv.expand() 

82 

83 

84@lru_cache(maxsize=128) 

85def bspline_basis(d, knots, n, x): 

86 """ 

87 The $n$-th B-spline at $x$ of degree $d$ with knots. 

88 

89 Explanation 

90 =========== 

91 

92 B-Splines are piecewise polynomials of degree $d$. They are defined on a 

93 set of knots, which is a sequence of integers or floats. 

94 

95 Examples 

96 ======== 

97 

98 The 0th degree splines have a value of 1 on a single interval: 

99 

100 >>> from sympy import bspline_basis 

101 >>> from sympy.abc import x 

102 >>> d = 0 

103 >>> knots = tuple(range(5)) 

104 >>> bspline_basis(d, knots, 0, x) 

105 Piecewise((1, (x >= 0) & (x <= 1)), (0, True)) 

106 

107 For a given ``(d, knots)`` there are ``len(knots)-d-1`` B-splines 

108 defined, that are indexed by ``n`` (starting at 0). 

109 

110 Here is an example of a cubic B-spline: 

111 

112 >>> bspline_basis(3, tuple(range(5)), 0, x) 

113 Piecewise((x**3/6, (x >= 0) & (x <= 1)), 

114 (-x**3/2 + 2*x**2 - 2*x + 2/3, 

115 (x >= 1) & (x <= 2)), 

116 (x**3/2 - 4*x**2 + 10*x - 22/3, 

117 (x >= 2) & (x <= 3)), 

118 (-x**3/6 + 2*x**2 - 8*x + 32/3, 

119 (x >= 3) & (x <= 4)), 

120 (0, True)) 

121 

122 By repeating knot points, you can introduce discontinuities in the 

123 B-splines and their derivatives: 

124 

125 >>> d = 1 

126 >>> knots = (0, 0, 2, 3, 4) 

127 >>> bspline_basis(d, knots, 0, x) 

128 Piecewise((1 - x/2, (x >= 0) & (x <= 2)), (0, True)) 

129 

130 It is quite time consuming to construct and evaluate B-splines. If 

131 you need to evaluate a B-spline many times, it is best to lambdify them 

132 first: 

133 

134 >>> from sympy import lambdify 

135 >>> d = 3 

136 >>> knots = tuple(range(10)) 

137 >>> b0 = bspline_basis(d, knots, 0, x) 

138 >>> f = lambdify(x, b0) 

139 >>> y = f(0.5) 

140 

141 Parameters 

142 ========== 

143 

144 d : integer 

145 degree of bspline 

146 

147 knots : list of integer values 

148 list of knots points of bspline 

149 

150 n : integer 

151 $n$-th B-spline 

152 

153 x : symbol 

154 

155 See Also 

156 ======== 

157 

158 bspline_basis_set 

159 

160 References 

161 ========== 

162 

163 .. [1] https://en.wikipedia.org/wiki/B-spline 

164 

165 """ 

166 # make sure x has no assumptions so conditions don't evaluate 

167 xvar = x 

168 x = Dummy() 

169 

170 knots = tuple(sympify(k) for k in knots) 

171 d = int(d) 

172 n = int(n) 

173 n_knots = len(knots) 

174 n_intervals = n_knots - 1 

175 if n + d + 1 > n_intervals: 

176 raise ValueError("n + d + 1 must not exceed len(knots) - 1") 

177 if d == 0: 

178 result = Piecewise( 

179 (S.One, Interval(knots[n], knots[n + 1]).contains(x)), (0, True) 

180 ) 

181 elif d > 0: 

182 denom = knots[n + d + 1] - knots[n + 1] 

183 if denom != S.Zero: 

184 B = (knots[n + d + 1] - x) / denom 

185 b2 = bspline_basis(d - 1, knots, n + 1, x) 

186 else: 

187 b2 = B = S.Zero 

188 

189 denom = knots[n + d] - knots[n] 

190 if denom != S.Zero: 

191 A = (x - knots[n]) / denom 

192 b1 = bspline_basis(d - 1, knots, n, x) 

193 else: 

194 b1 = A = S.Zero 

195 

196 result = _add_splines(A, b1, B, b2, x) 

197 else: 

198 raise ValueError("degree must be non-negative: %r" % n) 

199 

200 # return result with user-given x 

201 return result.xreplace({x: xvar}) 

202 

203 

204def bspline_basis_set(d, knots, x): 

205 """ 

206 Return the ``len(knots)-d-1`` B-splines at *x* of degree *d* 

207 with *knots*. 

208 

209 Explanation 

210 =========== 

211 

212 This function returns a list of piecewise polynomials that are the 

213 ``len(knots)-d-1`` B-splines of degree *d* for the given knots. 

214 This function calls ``bspline_basis(d, knots, n, x)`` for different 

215 values of *n*. 

216 

217 Examples 

218 ======== 

219 

220 >>> from sympy import bspline_basis_set 

221 >>> from sympy.abc import x 

222 >>> d = 2 

223 >>> knots = range(5) 

224 >>> splines = bspline_basis_set(d, knots, x) 

225 >>> splines 

226 [Piecewise((x**2/2, (x >= 0) & (x <= 1)), 

227 (-x**2 + 3*x - 3/2, (x >= 1) & (x <= 2)), 

228 (x**2/2 - 3*x + 9/2, (x >= 2) & (x <= 3)), 

229 (0, True)), 

230 Piecewise((x**2/2 - x + 1/2, (x >= 1) & (x <= 2)), 

231 (-x**2 + 5*x - 11/2, (x >= 2) & (x <= 3)), 

232 (x**2/2 - 4*x + 8, (x >= 3) & (x <= 4)), 

233 (0, True))] 

234 

235 Parameters 

236 ========== 

237 

238 d : integer 

239 degree of bspline 

240 

241 knots : list of integers 

242 list of knots points of bspline 

243 

244 x : symbol 

245 

246 See Also 

247 ======== 

248 

249 bspline_basis 

250 

251 """ 

252 n_splines = len(knots) - d - 1 

253 return [bspline_basis(d, tuple(knots), i, x) for i in range(n_splines)] 

254 

255 

256def interpolating_spline(d, x, X, Y): 

257 """ 

258 Return spline of degree *d*, passing through the given *X* 

259 and *Y* values. 

260 

261 Explanation 

262 =========== 

263 

264 This function returns a piecewise function such that each part is 

265 a polynomial of degree not greater than *d*. The value of *d* 

266 must be 1 or greater and the values of *X* must be strictly 

267 increasing. 

268 

269 Examples 

270 ======== 

271 

272 >>> from sympy import interpolating_spline 

273 >>> from sympy.abc import x 

274 >>> interpolating_spline(1, x, [1, 2, 4, 7], [3, 6, 5, 7]) 

275 Piecewise((3*x, (x >= 1) & (x <= 2)), 

276 (7 - x/2, (x >= 2) & (x <= 4)), 

277 (2*x/3 + 7/3, (x >= 4) & (x <= 7))) 

278 >>> interpolating_spline(3, x, [-2, 0, 1, 3, 4], [4, 2, 1, 1, 3]) 

279 Piecewise((7*x**3/117 + 7*x**2/117 - 131*x/117 + 2, (x >= -2) & (x <= 1)), 

280 (10*x**3/117 - 2*x**2/117 - 122*x/117 + 77/39, (x >= 1) & (x <= 4))) 

281 

282 Parameters 

283 ========== 

284 

285 d : integer 

286 Degree of Bspline strictly greater than equal to one 

287 

288 x : symbol 

289 

290 X : list of strictly increasing real values 

291 list of X coordinates through which the spline passes 

292 

293 Y : list of real values 

294 list of corresponding Y coordinates through which the spline passes 

295 

296 See Also 

297 ======== 

298 

299 bspline_basis_set, interpolating_poly 

300 

301 """ 

302 from sympy.solvers.solveset import linsolve 

303 from sympy.matrices.dense import Matrix 

304 

305 # Input sanitization 

306 d = sympify(d) 

307 if not (d.is_Integer and d.is_positive): 

308 raise ValueError("Spline degree must be a positive integer, not %s." % d) 

309 if len(X) != len(Y): 

310 raise ValueError("Number of X and Y coordinates must be the same.") 

311 if len(X) < d + 1: 

312 raise ValueError("Degree must be less than the number of control points.") 

313 if not all(a < b for a, b in zip(X, X[1:])): 

314 raise ValueError("The x-coordinates must be strictly increasing.") 

315 X = [sympify(i) for i in X] 

316 

317 # Evaluating knots value 

318 if d.is_odd: 

319 j = (d + 1) // 2 

320 interior_knots = X[j:-j] 

321 else: 

322 j = d // 2 

323 interior_knots = [ 

324 (a + b)/2 for a, b in zip(X[j : -j - 1], X[j + 1 : -j]) 

325 ] 

326 

327 knots = [X[0]] * (d + 1) + list(interior_knots) + [X[-1]] * (d + 1) 

328 

329 basis = bspline_basis_set(d, knots, x) 

330 

331 A = [[b.subs(x, v) for b in basis] for v in X] 

332 

333 coeff = linsolve((Matrix(A), Matrix(Y)), symbols("c0:{}".format(len(X)), cls=Dummy)) 

334 coeff = list(coeff)[0] 

335 intervals = {c for b in basis for (e, c) in b.args if c != True} 

336 

337 # Sorting the intervals 

338 # ival contains the end-points of each interval 

339 ival = [_ivl(c, x) for c in intervals] 

340 com = zip(ival, intervals) 

341 com = sorted(com, key=lambda x: x[0]) 

342 intervals = [y for x, y in com] 

343 

344 basis_dicts = [{c: e for (e, c) in b.args} for b in basis] 

345 spline = [] 

346 for i in intervals: 

347 piece = sum( 

348 [c * d.get(i, S.Zero) for (c, d) in zip(coeff, basis_dicts)], S.Zero 

349 ) 

350 spline.append((piece, i)) 

351 return Piecewise(*spline)