feectools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. feectools/__init__.py +0 -0
  2. feectools/accelerate/__init__.py +0 -0
  3. feectools/accelerate/accelerate.py +220 -0
  4. feectools/accelerate/compile_psydac.mk +52 -0
  5. feectools/api/__init__.py +0 -0
  6. feectools/api/essential_bc.py +122 -0
  7. feectools/api/fem_bilinear_form.py +2226 -0
  8. feectools/api/fem_common.py +286 -0
  9. feectools/api/fem_sum_form.py +123 -0
  10. feectools/api/settings.py +82 -0
  11. feectools/core/__init__.py +11 -0
  12. feectools/core/bsplines.py +1107 -0
  13. feectools/core/bsplines_kernels.py +1349 -0
  14. feectools/core/field_evaluation_kernels.py +5015 -0
  15. feectools/core/tests/__init__.py +0 -0
  16. feectools/core/tests/test_bsplines.py +263 -0
  17. feectools/core/tests/test_bsplines_kernel.py +40 -0
  18. feectools/core/tests/test_bsplines_pyccel.py +752 -0
  19. feectools/ddm/__init__.py +3 -0
  20. feectools/ddm/basic.py +78 -0
  21. feectools/ddm/blocking_data_exchanger.py +348 -0
  22. feectools/ddm/cart.py +1835 -0
  23. feectools/ddm/interface_data_exchanger.py +122 -0
  24. feectools/ddm/mpi.py +109 -0
  25. feectools/ddm/nonblocking_data_exchanger.py +331 -0
  26. feectools/ddm/partition.py +207 -0
  27. feectools/ddm/petsc.py +112 -0
  28. feectools/ddm/tests/__init__.py +0 -0
  29. feectools/ddm/tests/test_cart_1d.py +138 -0
  30. feectools/ddm/tests/test_cart_2d.py +164 -0
  31. feectools/ddm/tests/test_cart_3d.py +158 -0
  32. feectools/ddm/tests/test_multicart_2d.py +173 -0
  33. feectools/ddm/tests/test_partition.py +124 -0
  34. feectools/ddm/utilities.py +24 -0
  35. feectools/feec/__init__.py +0 -0
  36. feectools/feec/derivatives.py +780 -0
  37. feectools/feec/dof_kernels.py +210 -0
  38. feectools/feec/global_geometric_projectors.py +1073 -0
  39. feectools/feec/hodge.py +148 -0
  40. feectools/fem/__init__.py +0 -0
  41. feectools/fem/basic.py +465 -0
  42. feectools/fem/grid.py +181 -0
  43. feectools/fem/partitioning.py +344 -0
  44. feectools/fem/projectors.py +160 -0
  45. feectools/fem/splines.py +559 -0
  46. feectools/fem/tensor.py +1393 -0
  47. feectools/fem/tests/__init__.py +0 -0
  48. feectools/fem/tests/analytical_profiles_1d.py +100 -0
  49. feectools/fem/tests/analytical_profiles_base.py +34 -0
  50. feectools/fem/tests/splines_error_bounds.py +155 -0
  51. feectools/fem/tests/test_spline_histopolation.py +120 -0
  52. feectools/fem/tests/test_spline_interpolation.py +182 -0
  53. feectools/fem/tests/test_splines.py +184 -0
  54. feectools/fem/tests/test_splines_par.py +46 -0
  55. feectools/fem/tests/test_vector_spaces.py +150 -0
  56. feectools/fem/tests/utilities.py +47 -0
  57. feectools/fem/vector.py +729 -0
  58. feectools/linalg/__init__.py +0 -0
  59. feectools/linalg/basic.py +1386 -0
  60. feectools/linalg/block.py +1451 -0
  61. feectools/linalg/direct_solvers.py +201 -0
  62. feectools/linalg/fft.py +258 -0
  63. feectools/linalg/kernels/__init__.py +0 -0
  64. feectools/linalg/kernels/axpy_kernels.py +57 -0
  65. feectools/linalg/kernels/inner_kernels.py +100 -0
  66. feectools/linalg/kernels/matvec_kernels.py +206 -0
  67. feectools/linalg/kernels/stencil2IJV_kernels.py +227 -0
  68. feectools/linalg/kernels/stencil2coo_kernels.py +179 -0
  69. feectools/linalg/kernels/transpose_kernels.py +263 -0
  70. feectools/linalg/kron.py +911 -0
  71. feectools/linalg/solvers.py +1914 -0
  72. feectools/linalg/sparse.py +114 -0
  73. feectools/linalg/stencil.py +2923 -0
  74. feectools/linalg/stencil_dot_kernels.py +317 -0
  75. feectools/linalg/stencil_transpose_kernels.py +372 -0
  76. feectools/linalg/tests/__init__.py +0 -0
  77. feectools/linalg/tests/test_block.py +1588 -0
  78. feectools/linalg/tests/test_fft.py +106 -0
  79. feectools/linalg/tests/test_kron_stencil_matrix.py +114 -0
  80. feectools/linalg/tests/test_linalg.py +1065 -0
  81. feectools/linalg/tests/test_matrix_free.py +128 -0
  82. feectools/linalg/tests/test_solvers.py +213 -0
  83. feectools/linalg/tests/test_stencil_interface_matrix.py +379 -0
  84. feectools/linalg/tests/test_stencil_vector.py +1036 -0
  85. feectools/linalg/tests/test_stencil_vector_space.py +440 -0
  86. feectools/linalg/topetsc.py +522 -0
  87. feectools/linalg/utilities.py +200 -0
  88. feectools/utilities/__init__.py +0 -0
  89. feectools/utilities/quadratures.py +113 -0
  90. feectools/utilities/utils.py +166 -0
  91. feectools/version.py +1 -0
  92. feectools-0.1.0.dist-info/METADATA +66 -0
  93. feectools-0.1.0.dist-info/RECORD +98 -0
  94. feectools-0.1.0.dist-info/WHEEL +5 -0
  95. feectools-0.1.0.dist-info/entry_points.txt +3 -0
  96. feectools-0.1.0.dist-info/licenses/AUTHORS +22 -0
  97. feectools-0.1.0.dist-info/licenses/LICENSE +21 -0
  98. feectools-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,752 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from feectools.utilities.quadratures import gauss_legendre
5
+ from feectools.core.bsplines import (find_span,
6
+ basis_funs,
7
+ basis_funs_1st_der,
8
+ basis_funs_all_ders,
9
+ collocation_matrix,
10
+ histopolation_matrix,
11
+ breakpoints,
12
+ greville,
13
+ elements_spans,
14
+ make_knots,
15
+ elevate_knots,
16
+ quadrature_grid,
17
+ basis_integrals,
18
+ basis_ders_on_quad_grid)
19
+
20
+ # The pytest-xdist plugin requires that every worker sees the same parameters
21
+ # in the unit tests. As in this module random parameters are used, here we set
22
+ # the same random seed for all workers.
23
+ np.random.seed(0)
24
+
25
+ ###############################################################################
26
+ # "True" Functions
27
+ ###############################################################################
28
+
29
+ def find_span_true( knots, degree, x ):
30
+ # Knot index at left/right boundary
31
+ low = degree
32
+ high = len(knots)-1-degree
33
+
34
+ # Check if point is exactly on left/right boundary, or outside domain
35
+ if x <= knots[low ]: return low
36
+ if x >= knots[high]: return high-1
37
+
38
+ # Perform binary search
39
+ span = (low+high)//2
40
+ while x < knots[span] or x >= knots[span+1]:
41
+ if x < knots[span]:
42
+ high = span
43
+ else:
44
+ low = span
45
+ span = (low+high)//2
46
+
47
+ return span
48
+
49
+ #==============================================================================
50
+ def basis_funs_true( knots, degree, x, span ):
51
+ left = np.empty( degree , dtype=float )
52
+ right = np.empty( degree , dtype=float )
53
+ values = np.empty( degree+1, dtype=float )
54
+
55
+ values[0] = 1.0
56
+ for j in range(0,degree):
57
+ left [j] = x - knots[span-j]
58
+ right[j] = knots[span+1+j] - x
59
+ saved = 0.0
60
+ for r in range(0,j+1):
61
+ temp = values[r] / (right[r] + left[j-r])
62
+ values[r] = saved + right[r] * temp
63
+ saved = left[j-r] * temp
64
+ values[j+1] = saved
65
+
66
+ return values
67
+
68
+ #==============================================================================
69
+ def basis_funs_1st_der_true( knots, degree, x, span ):
70
+ # Compute nonzero basis functions and knot differences for splines
71
+ # up to degree deg-1
72
+ values = basis_funs_true( knots, degree-1, x, span )
73
+
74
+ # Compute derivatives at x using formula based on difference of splines of
75
+ # degree deg-1
76
+ # -------
77
+ # j = 0
78
+ ders = np.empty( degree+1, dtype=float )
79
+ saved = degree * values[0] / (knots[span+1]-knots[span+1-degree])
80
+ ders[0] = -saved
81
+ # j = 1,...,degree-1
82
+ for j in range(1,degree):
83
+ temp = saved
84
+ saved = degree * values[j] / (knots[span+j+1]-knots[span+j+1-degree])
85
+ ders[j] = temp - saved
86
+ # j = degree
87
+ ders[degree] = saved
88
+
89
+ return ders
90
+
91
+ #==============================================================================
92
+ def basis_funs_all_ders_true(knots, degree, x, span, n, normalization='B'):
93
+ """
94
+ Evaluate value and n derivatives at x of all basis functions with
95
+ support in interval [x_{span-1}, x_{span}].
96
+ If called with normalization='M', this uses M-splines instead of B-splines.
97
+ ders[i,j] = (d/dx)^i B_k(x) with k=(span-degree+j),
98
+ for 0 <= i <= n and 0 <= j <= degree+1.
99
+ Parameters
100
+ ----------
101
+ knots : array_like
102
+ Knots sequence.
103
+ degree : int
104
+ Polynomial degree of B-splines.
105
+ x : float
106
+ Evaluation point.
107
+ span : int
108
+ Knot span index.
109
+ n : int
110
+ Max derivative of interest.
111
+ normalization: str
112
+ Set to 'B' to get B-Splines and 'M' to get M-Splines
113
+ Returns
114
+ -------
115
+ ders : numpy.ndarray (n+1,degree+1)
116
+ 2D array of n+1 (from 0-th to n-th) derivatives at x of all (degree+1)
117
+ non-vanishing basis functions in given span.
118
+ Notes
119
+ -----
120
+ The original Algorithm A2.3 in The NURBS Book [1] is here improved:
121
+ - 'left' and 'right' arrays are 1 element shorter;
122
+ - inverse of knot differences are saved to avoid unnecessary divisions;
123
+ - innermost loops are replaced with vector operations on slices.
124
+ """
125
+ left = np.empty( degree )
126
+ right = np.empty( degree )
127
+ ndu = np.empty( (degree+1, degree+1) )
128
+ a = np.empty( ( 2, degree+1) )
129
+ ders = np.zeros( ( n+1, degree+1) ) # output array
130
+
131
+ # Number of derivatives that need to be effectively computed
132
+ # Derivatives higher than degree are = 0.
133
+ ne = min( n, degree )
134
+
135
+ # Compute nonzero basis functions and knot differences for splines
136
+ # up to degree, which are needed to compute derivatives.
137
+ # Store values in 2D temporary array 'ndu' (square matrix).
138
+ ndu[0,0] = 1.0
139
+ for j in range(0,degree):
140
+ left [j] = x - knots[span-j]
141
+ right[j] = knots[span+1+j] - x
142
+ saved = 0.0
143
+ for r in range(0,j+1):
144
+ # compute inverse of knot differences and save them into lower triangular part of ndu
145
+ ndu[j+1,r] = 1.0 / (right[r] + left[j-r])
146
+ # compute basis functions and save them into upper triangular part of ndu
147
+ temp = ndu[r,j] * ndu[j+1,r]
148
+ ndu[r,j+1] = saved + right[r] * temp
149
+ saved = left[j-r] * temp
150
+ ndu[j+1,j+1] = saved
151
+
152
+ # Compute derivatives in 2D output array 'ders'
153
+ ders[0,:] = ndu[:,degree]
154
+ for r in range(0,degree+1):
155
+ s1 = 0
156
+ s2 = 1
157
+ a[0,0] = 1.0
158
+ for k in range(1,ne+1):
159
+ d = 0.0
160
+ rk = r-k
161
+ pk = degree-k
162
+ if r >= k:
163
+ a[s2,0] = a[s1,0] * ndu[pk+1,rk]
164
+ d = a[s2,0] * ndu[rk,pk]
165
+ j1 = 1 if (rk > -1 ) else -rk
166
+ j2 = k-1 if (r-1 <= pk) else degree-r
167
+ a[s2,j1:j2+1] = (a[s1,j1:j2+1] - a[s1,j1-1:j2]) * ndu[pk+1,rk+j1:rk+j2+1]
168
+ d += np.dot( a[s2,j1:j2+1], ndu[rk+j1:rk+j2+1,pk] )
169
+ if r <= pk:
170
+ a[s2,k] = - a[s1,k-1] * ndu[pk+1,r]
171
+ d += a[s2,k] * ndu[r,pk]
172
+ ders[k,r] = d
173
+ j = s1
174
+ s1 = s2
175
+ s2 = j
176
+
177
+ # Multiply derivatives by correct factors
178
+ r = degree
179
+ for k in range(1,ne+1):
180
+ ders[k,:] = ders[k,:] * r
181
+ r = r * (degree-k)
182
+
183
+ # Normalization to get M-Splines
184
+ if normalization == 'M':
185
+ ders *= [(degree + 1) / (knots[i + degree + 1] - knots[i]) \
186
+ for i in range(span - degree, span + 1)]
187
+ return ders
188
+
189
+ #==============================================================================
190
+ def collocation_matrix_true(knots, degree, periodic, normalization, xgrid):
191
+ # Number of basis functions (in periodic case remove degree repeated elements)
192
+ nb = len(knots)-degree-1
193
+ if periodic:
194
+ nb -= degree
195
+
196
+ # Number of evaluation points
197
+ nx = len(xgrid)
198
+
199
+ # Collocation matrix as 2D Numpy array (dense storage)
200
+ mat = np.zeros( (nx,nb) )
201
+
202
+ # Indexing of basis functions (periodic or not) for a given span
203
+ if periodic:
204
+ js = lambda span: [(span-degree+s) % nb for s in range( degree+1 )]
205
+ else:
206
+ js = lambda span: slice( span-degree, span+1 )
207
+
208
+ # Rescaling of B-splines, to get M-splines if needed
209
+ if normalization == 'B':
210
+ normalize = lambda basis, span: basis
211
+ elif normalization == 'M':
212
+ scaling = 1 / basis_integrals_true(knots, degree)
213
+ normalize = lambda basis, span: basis * scaling[span-degree: span+1]
214
+
215
+ # Fill in non-zero matrix values
216
+ for i,x in enumerate( xgrid ):
217
+ span = find_span_true( knots, degree, x )
218
+ basis = basis_funs_true( knots, degree, x, span )
219
+ mat[i,js(span)] = normalize(basis, span)
220
+
221
+ # Mitigate round-off errors
222
+ mat[abs(mat) < 1e-14] = 0.0
223
+
224
+ return mat
225
+
226
+ #==============================================================================
227
+ def histopolation_matrix_true(knots, degree, periodic, normalization, xgrid):
228
+ # Check that knots are ordered (but allow repeated knots)
229
+ if not np.all(np.diff(knots) >= 0):
230
+ raise ValueError("Cannot accept knot sequence: {}".format(knots))
231
+
232
+ # Check that spline degree is non-negative integer
233
+ if not isinstance(degree, (int, np.integer)):
234
+ raise TypeError("Degree {} must be integer, got type {} instead".format(degree, type(degree)))
235
+ if degree < 0:
236
+ raise ValueError("Cannot accept negative degree: {}".format(degree))
237
+
238
+ # Check 'periodic' flag
239
+ if not isinstance(periodic, bool):
240
+ raise TypeError("Cannot accept non-boolean 'periodic' parameter: {}".format(periodic))
241
+
242
+ # Check 'normalization' option
243
+ if normalization not in ['B', 'M']:
244
+ raise ValueError("Cannot accept 'normalization' parameter: {}".format(normalization))
245
+
246
+ # Check that grid points are ordered, and do not allow repetitions
247
+ if not np.all(np.diff(xgrid) > 0):
248
+ raise ValueError("Grid points must be ordered, with no repetitions: {}".format(xgrid))
249
+
250
+ # Number of basis functions (in periodic case remove degree repeated elements)
251
+ nb = len(knots)-degree-1
252
+ if periodic:
253
+ nb -= degree
254
+
255
+ # Number of evaluation points
256
+ nx = len(xgrid)
257
+
258
+ # In periodic case, make sure that evaluation points include domain boundaries
259
+ # TODO: only do this if the user asks for it!
260
+ if periodic:
261
+ xmin = knots[degree]
262
+ xmax = knots[-1-degree]
263
+ if xgrid[0] > xmin:
264
+ xgrid = [xmin, *xgrid]
265
+ if xgrid[-1] < xmax:
266
+ xgrid = [*xgrid, xmax]
267
+
268
+ # B-splines of degree p+1: basis[i,j] := Bj(xi)
269
+ #
270
+ # NOTES:
271
+ # . cannot use M-splines in analytical formula for histopolation matrix
272
+ # . always use non-periodic splines to avoid circulant matrix structure
273
+ C = collocation_matrix_true(
274
+ knots = elevate_knots_true(knots, degree, periodic),
275
+ degree = degree + 1,
276
+ periodic = False,
277
+ normalization = 'B',
278
+ xgrid = xgrid
279
+ )
280
+
281
+ # Rescaling of M-splines, to get B-splines if needed
282
+ if normalization == 'M':
283
+ normalize = lambda bi, j: bi
284
+ elif normalization == 'B':
285
+ scaling = basis_integrals_true(knots, degree)
286
+ normalize = lambda bi, j: bi * scaling[j]
287
+
288
+ # Compute span for each row (index of last non-zero basis function)
289
+ # TODO: would be better to have this ready beforehand
290
+ # TODO: use tolerance instead of comparing against zero
291
+ spans = [(row != 0).argmax() + (degree+1) for row in C]
292
+
293
+ # Compute histopolation matrix from collocation matrix of higher degree
294
+ m = C.shape[0] - 1
295
+ n = C.shape[1] - 1
296
+ H = np.zeros((m, n))
297
+ for i in range(m):
298
+ # Indices of first/last non-zero elements in row of collocation matrix
299
+ jstart = spans[i] - (degree+1)
300
+ jend = min(spans[i+1], n)
301
+ # Compute non-zero values of histopolation matrix
302
+ for j in range(1+jstart, jend+1):
303
+ s = C[i, 0:j].sum() - C[i+1, 0:j].sum()
304
+ H[i, j-1] = normalize(s, j-1)
305
+
306
+ # Mitigate round-off errors
307
+ H[abs(H) < 1e-14] = 0.0
308
+ # Non periodic case: stop here
309
+ if not periodic:
310
+ return H
311
+
312
+ # Periodic case: wrap around histopolation matrix
313
+ # 1. identify repeated basis functions (sum columns)
314
+ # 2. identify split interval (sum rows)
315
+ Hp = np.zeros((nx, nb))
316
+ for i in range(m):
317
+ for j in range(n):
318
+ Hp[i % nx, j % nb] += H[i, j]
319
+
320
+ return Hp
321
+
322
+ #==============================================================================
323
+ def breakpoints_true( knots, degree ,tol=1e-15):
324
+ knots = np.array(knots)
325
+ diff = np.append(True, abs(np.diff(knots[degree:-degree]))>tol)
326
+ return knots[degree:-degree][diff]
327
+
328
+ #==============================================================================
329
+ def greville_true( knots, degree, periodic ):
330
+ T = knots
331
+ p = degree
332
+ n = len(T)-2*p-1 if periodic else len(T)-p-1
333
+
334
+ # Compute greville abscissas as average of p consecutive knot values
335
+ xg = np.array([sum(T[i:i+p])/p for i in range(1,1+n)])
336
+
337
+ # Domain boundaries
338
+ a = T[p]
339
+ b = T[-1-p]
340
+
341
+ # If needed apply periodic boundary conditions, then sort array
342
+ if periodic:
343
+ xg = (xg-a) % (b-a) + a
344
+ xg = xg[np.argsort(xg)]
345
+
346
+ # Make sure roundoff errors don't push Greville points outside domain
347
+ xg[ 0] = max(xg[ 0], a)
348
+ xg[-1] = min(xg[-1], b)
349
+
350
+ return xg
351
+
352
+ #===============================================================================
353
+ def elements_spans_true( knots, degree ):
354
+ breaks = breakpoints_true( knots, degree )
355
+ nk = len(knots)
356
+ ne = len(breaks)-1
357
+ spans = np.zeros( ne, dtype=int )
358
+
359
+ ie = 0
360
+ for ik in range( degree, nk-degree ):
361
+ if knots[ik+1]-knots[ik]>=1e-15:
362
+ spans[ie] = ik
363
+ ie += 1
364
+ if ie == ne:
365
+ break
366
+
367
+ return spans
368
+
369
+ #===============================================================================
370
+ def make_knots_true( breaks, degree, periodic, multiplicity=1 ):
371
+ # Type checking
372
+ assert isinstance( degree , int )
373
+ assert isinstance( periodic, bool )
374
+
375
+ # Consistency checks
376
+ assert len(breaks) > 1
377
+ assert all( np.diff(breaks) > 0 )
378
+ assert degree > 0
379
+ assert 1 <= multiplicity and multiplicity <= degree + 1
380
+
381
+ if periodic:
382
+ assert len(breaks) > degree
383
+
384
+ T = np.zeros(multiplicity * len(breaks[1:-1]) + 2 + 2 * degree)
385
+ ncells = len(breaks) - 1
386
+
387
+ for i in range(0, ncells+1):
388
+ T[degree + 1 + (i-1) * multiplicity :degree + 1 + i * multiplicity ] = breaks[i]
389
+
390
+ len_out = len(T)
391
+
392
+ if periodic:
393
+ period = breaks[-1]-breaks[0]
394
+
395
+ T[: degree + 1 - multiplicity] = T[len_out - 2 * (degree + 1 )+ multiplicity: len_out - (degree + 1)] - period
396
+ T[len_out - (degree + 1 - multiplicity) :] = T[degree + 1:2*(degree + 1)- multiplicity] + period
397
+
398
+ else:
399
+ T[0:degree + 1 - multiplicity] = breaks[0]
400
+ T[len_out - degree - 1 + multiplicity:] = breaks[-1]
401
+
402
+ return T
403
+
404
+ #==============================================================================
405
+ def elevate_knots_true(knots, degree, periodic, multiplicity=1, tol=1e-15):
406
+ knots = np.array(knots)
407
+
408
+ if periodic:
409
+ T, p = knots, degree
410
+ period = T[len(knots) -1 - p] - T[p]
411
+ left = [T[len(knots) -2 - 2 * p + multiplicity-1] - period]
412
+ right = [T[2 * p + 2 - multiplicity] + period]
413
+ else:
414
+ left = [knots[0],*knots[:degree+1]]
415
+ right = [knots[-1],*knots[-degree-1:]]
416
+
417
+ diff = np.append(True, np.diff(knots[degree+1:-degree-1])>tol)
418
+ if len(knots[degree+1:-degree-1])>0:
419
+ unique = knots[degree+1:-degree-1][diff]
420
+ knots = np.repeat(unique, multiplicity)
421
+ else:
422
+ knots = knots[degree+1:-degree-1]
423
+
424
+ return np.array([*left, *knots, *right])
425
+
426
+ #==============================================================================
427
+ def quadrature_grid_true(breaks, quad_rule_x, quad_rule_w):
428
+ # Check that input arrays have correct size
429
+ assert len(breaks) >= 2
430
+ assert len(quad_rule_x) == len(quad_rule_w)
431
+
432
+ # Check that provided quadrature rule is defined on interval [-1, 1]
433
+ assert min(quad_rule_x) >= -1
434
+ assert max(quad_rule_x) <= +1
435
+
436
+ quad_rule_x = np.asarray(quad_rule_x)
437
+ quad_rule_w = np.asarray(quad_rule_w)
438
+
439
+ ne = len(breaks) - 1
440
+ nq = len(quad_rule_x)
441
+ quad_x = np.zeros((ne, nq))
442
+ quad_w = np.zeros((ne, nq))
443
+
444
+ # Compute location and weight of quadrature points from basic rule
445
+ for ie, (a, b) in enumerate(zip(breaks[:-1], breaks[1:])):
446
+ c0 = 0.5 * (a + b)
447
+ c1 = 0.5 * (b - a)
448
+ quad_x[ie, :] = c1 * quad_rule_x[:] + c0
449
+ quad_w[ie, :] = c1 * quad_rule_w[:]
450
+
451
+ return quad_x, quad_w
452
+
453
+ #==============================================================================
454
+ def basis_ders_on_quad_grid_true(knots, degree, quad_grid, nders, normalization):
455
+ ne,nq = quad_grid.shape
456
+ basis = np.zeros((ne, degree+1, nders+1, nq))
457
+
458
+ if normalization == 'M':
459
+ scaling = 1. / basis_integrals_true(knots, degree)
460
+
461
+ for ie in range(ne):
462
+ xx = quad_grid[ie, :]
463
+ for iq, xq in enumerate(xx):
464
+ span = find_span_true(knots, degree, xq)
465
+ ders = basis_funs_all_ders_true(knots, degree, xq, span, nders)
466
+ if normalization == 'M':
467
+ ders *= scaling[None, span-degree:span+1]
468
+ basis[ie, :, :, iq] = ders.transpose()
469
+
470
+ return basis
471
+
472
+ #==============================================================================
473
+ def basis_integrals_true(knots, degree):
474
+ T = knots
475
+ p = degree
476
+ n = len(T)-p-1
477
+ K = np.array([(T[i+p+1] - T[i]) / (p + 1) for i in range(n)])
478
+
479
+ return K
480
+
481
+
482
+ ###############################################################################
483
+ # Tests
484
+ ###############################################################################
485
+ # Tolerance for testing float equality
486
+ RTOL = 1e-11
487
+ ATOL = 1e-11
488
+
489
+
490
+ @pytest.mark.parametrize(('knots', 'degree'),
491
+ [(np.sort(np.random.random(15)), 2),
492
+ (np.sort(np.random.random(15)), 3),
493
+ (np.sort(np.random.random(15)), 4),
494
+ (np.sort(np.random.random(15)), 5),
495
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
496
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
497
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
498
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
499
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
500
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
501
+ @pytest.mark.parametrize('x', (np.random.random(), np.random.random(), np.random.random()))
502
+
503
+ def test_find_span(knots, degree, x):
504
+ expected = find_span_true(knots, degree, x)
505
+ out = find_span(knots, degree, x)
506
+
507
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
508
+
509
+
510
+ @pytest.mark.parametrize(('knots', 'degree'),
511
+ [(np.sort(np.random.random(15)), 2),
512
+ (np.sort(np.random.random(15)), 3),
513
+ (np.sort(np.random.random(15)), 4),
514
+ (np.sort(np.random.random(15)), 5),
515
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
516
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
517
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
518
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
519
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
520
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
521
+ @pytest.mark.parametrize('x', (np.random.random(), np.random.random(), np.random.random()))
522
+ def test_basis_funs(knots, degree, x):
523
+ span = find_span(knots, degree, x)
524
+ expected = basis_funs_true(knots, degree, x, span)
525
+ out = basis_funs(knots, degree, x, span)
526
+
527
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
528
+
529
+
530
+ @pytest.mark.parametrize(('knots', 'degree'),
531
+ [(np.sort(np.random.random(15)), 2),
532
+ (np.sort(np.random.random(15)), 3),
533
+ (np.sort(np.random.random(15)), 4),
534
+ (np.sort(np.random.random(15)), 5),
535
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
536
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
537
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
538
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
539
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
540
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
541
+ @pytest.mark.parametrize('x', (np.random.random(), np.random.random(), np.random.random()))
542
+ def test_basis_funs_1st_der(knots, degree, x):
543
+ span = find_span(knots, degree, x)
544
+ expected = basis_funs_1st_der_true(knots, degree, x, span)
545
+ out = basis_funs_1st_der(knots, degree, x, span)
546
+
547
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
548
+
549
+
550
+ @pytest.mark.parametrize(('knots', 'degree'),
551
+ [(np.sort(np.random.random(15)), 2),
552
+ (np.sort(np.random.random(15)), 3),
553
+ (np.sort(np.random.random(15)), 4),
554
+ (np.sort(np.random.random(15)), 5),
555
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
556
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
557
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
558
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
559
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
560
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
561
+ @pytest.mark.parametrize('x', (np.random.random(), np.random.random(), np.random.random()))
562
+ @pytest.mark.parametrize('n', (2, 3, 4, 5))
563
+ @pytest.mark.parametrize('normalization', ('B', 'M'))
564
+ def test_basis_funs_all_ders(knots, degree, x, n, normalization):
565
+ span = find_span(knots, degree, x)
566
+ expected = basis_funs_all_ders_true(knots, degree, x, span, n, normalization)
567
+ out = basis_funs_all_ders(knots, degree, x, span, n, normalization)
568
+
569
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
570
+
571
+
572
+ @pytest.mark.parametrize(('knots', 'degree'),
573
+ [(np.sort(np.random.random(15)), 2),
574
+ (np.sort(np.random.random(15)), 3),
575
+ (np.sort(np.random.random(15)), 4),
576
+ (np.sort(np.random.random(15)), 5),
577
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
578
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
579
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
580
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
581
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
582
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
583
+ @pytest.mark.parametrize('periodic', (True, False))
584
+ @pytest.mark.parametrize('normalization', ('B', 'M'))
585
+ @pytest.mark.parametrize('xgrid', (np.random.random(10), np.random.random(15)))
586
+ def test_collocation_matrix(knots, degree, periodic, normalization, xgrid):
587
+ expected = collocation_matrix_true(knots, degree, periodic, normalization, xgrid)
588
+ out = collocation_matrix(knots, degree, periodic, normalization, xgrid)
589
+
590
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
591
+
592
+
593
+ @pytest.mark.parametrize(('knots', 'degree'),
594
+ [(np.sort(np.random.random(15)), 2),
595
+ (np.sort(np.random.random(15)), 3),
596
+ (np.sort(np.random.random(15)), 4),
597
+ (np.sort(np.random.random(15)), 5),
598
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
599
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
600
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
601
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
602
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
603
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
604
+ @pytest.mark.parametrize('periodic', [True, False])
605
+ @pytest.mark.parametrize('normalization', ('B', 'M'))
606
+ @pytest.mark.parametrize('xgrid', (np.random.random(10), np.random.random(15)))
607
+ def test_histopolation_matrix(knots, degree, periodic, normalization, xgrid):
608
+ xgrid = np.sort(np.unique(xgrid))
609
+ expected = histopolation_matrix_true(knots, degree, periodic, normalization, xgrid)
610
+ out = histopolation_matrix(knots, degree, periodic, normalization, xgrid)
611
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
612
+
613
+
614
+ @pytest.mark.parametrize(('knots', 'degree'),
615
+ [(np.sort(np.random.random(15)), 2),
616
+ (np.sort(np.random.random(15)), 3),
617
+ (np.sort(np.random.random(15)), 4),
618
+ (np.sort(np.random.random(15)), 5),
619
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
620
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
621
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
622
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
623
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
624
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
625
+ def test_breakpoints(knots, degree):
626
+ expected = breakpoints_true(knots, degree)
627
+ out = breakpoints(knots, degree)
628
+
629
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
630
+
631
+ @pytest.mark.parametrize(('knots', 'degree'),
632
+ [(np.sort(np.random.random(15)), 2),
633
+ (np.sort(np.random.random(15)), 3),
634
+ (np.sort(np.random.random(15)), 4),
635
+ (np.sort(np.random.random(15)), 5),
636
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
637
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
638
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
639
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
640
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
641
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
642
+ @pytest.mark.parametrize('periodic', [True, False])
643
+ def test_greville(knots, degree, periodic):
644
+ expected = greville_true(knots, degree, periodic)
645
+ out = greville(knots, degree, periodic)
646
+
647
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
648
+
649
+
650
+ @pytest.mark.parametrize(('knots', 'degree'),
651
+ [(np.sort(np.random.random(15)), 2),
652
+ (np.sort(np.random.random(15)), 3),
653
+ (np.sort(np.random.random(15)), 4),
654
+ (np.sort(np.random.random(15)), 5),
655
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
656
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
657
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
658
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
659
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
660
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
661
+ def test_elements_spans(knots, degree):
662
+ expected = elements_spans_true(knots, degree)
663
+ out = elements_spans(knots, degree)
664
+
665
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
666
+
667
+ @pytest.mark.parametrize('breaks', (np.linspace(0, 1, 10, endpoint=False),
668
+ np.sort(np.random.random(15))))
669
+ @pytest.mark.parametrize(('degree', 'multiplicity'), [(2, 1),
670
+ (3, 1), (3, 2),
671
+ (4, 1), (4, 2), (4, 3),
672
+ (5, 1), (5, 2), (5, 3), (5, 4)])
673
+ @pytest.mark.parametrize('periodic', (True, False))
674
+ def test_make_knots(breaks, degree, periodic, multiplicity):
675
+ expected = make_knots_true(breaks, degree, periodic, multiplicity)
676
+ out = make_knots(breaks, degree, periodic, multiplicity)
677
+ print(out, expected)
678
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
679
+
680
+
681
+ @pytest.mark.parametrize(('knots', 'degree'),
682
+ [(np.sort(np.random.random(15)), 2),
683
+ (np.sort(np.random.random(15)), 3),
684
+ (np.sort(np.random.random(15)), 4),
685
+ (np.sort(np.random.random(15)), 5),
686
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
687
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
688
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
689
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
690
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
691
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
692
+ @pytest.mark.parametrize('periodic', (True, False))
693
+ @pytest.mark.parametrize('multiplicity', (1, 2, 3))
694
+ def test_elevate_knots(knots, degree, periodic, multiplicity):
695
+ expected = elevate_knots_true(knots, degree, periodic, multiplicity)
696
+ out = elevate_knots(knots, degree, periodic, multiplicity)
697
+
698
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
699
+
700
+
701
+ @pytest.mark.parametrize('breaks', (np.linspace(0, 1, 10, endpoint=False),
702
+ np.sort(np.random.random(15))))
703
+ @pytest.mark.parametrize('nquads', (2, 3, 4, 5))
704
+ def test_quadrature_grid(breaks, nquads):
705
+ quad_x, quad_w = gauss_legendre(nquads)
706
+ expected = quadrature_grid_true(breaks, quad_x, quad_w)
707
+ out = quadrature_grid(breaks, quad_x, quad_w)
708
+
709
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
710
+
711
+
712
+ @pytest.mark.parametrize(('knots', 'degree'),
713
+ [(np.sort(np.random.random(15)), 2),
714
+ (np.sort(np.random.random(15)), 3),
715
+ (np.sort(np.random.random(15)), 4),
716
+ (np.sort(np.random.random(15)), 5),
717
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
718
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
719
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
720
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
721
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
722
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
723
+ @pytest.mark.parametrize('n', (2, 3, 4, 5))
724
+ @pytest.mark.parametrize('normalization', ('B', 'M'))
725
+ @pytest.mark.parametrize('nquads', (2, 3, 4, 5))
726
+ def test_basis_ders_on_quad_grid(knots, degree, n, normalization, nquads):
727
+ quad_rule_x, quad_rule_w = gauss_legendre(nquads)
728
+ breaks = breakpoints_true(knots, degree)
729
+ quad_grid, quad_weights = quadrature_grid_true(breaks, quad_rule_x, quad_rule_w)
730
+
731
+ expected = basis_ders_on_quad_grid_true(knots, degree, quad_grid, n, normalization)
732
+ out = basis_ders_on_quad_grid(knots, degree, quad_grid, n, normalization)
733
+
734
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)
735
+
736
+
737
+ @pytest.mark.parametrize(('knots', 'degree'),
738
+ [(np.sort(np.random.random(15)), 2),
739
+ (np.sort(np.random.random(15)), 3),
740
+ (np.sort(np.random.random(15)), 4),
741
+ (np.sort(np.random.random(15)), 5),
742
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 2),
743
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 3),
744
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 4),
745
+ (np.array([0.0, 0.0, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0]), 5),
746
+ (np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]), 2),
747
+ (np.array([0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 3)])
748
+ def test_basis_integrals(knots, degree):
749
+ expected = basis_integrals_true(knots, degree)
750
+ out = basis_integrals(knots, degree)
751
+
752
+ assert np.allclose(expected, out, atol=ATOL, rtol=RTOL)