bspy 4.1__py3-none-any.whl → 4.3__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.
- bspy/__init__.py +3 -0
- bspy/_spline_domain.py +94 -30
- bspy/_spline_evaluation.py +80 -21
- bspy/_spline_fitting.py +205 -49
- bspy/_spline_intersection.py +442 -283
- bspy/_spline_operations.py +93 -74
- bspy/hyperplane.py +13 -9
- bspy/manifold.py +10 -5
- bspy/solid.py +22 -15
- bspy/spline.py +195 -53
- bspy/splineOpenGLFrame.py +346 -303
- bspy/spline_block.py +460 -0
- bspy/viewer.py +26 -16
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/METADATA +14 -6
- bspy-4.3.dist-info/RECORD +18 -0
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/WHEEL +1 -1
- bspy-4.1.dist-info/RECORD +0 -17
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/LICENSE +0 -0
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/top_level.txt +0 -0
bspy/__init__.py
CHANGED
|
@@ -12,6 +12,8 @@ Available subpackages
|
|
|
12
12
|
`bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
|
|
13
13
|
piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
|
|
14
14
|
|
|
15
|
+
`bspy.spline_block` : Provides the `SplineBlock` class that processes an array-like collection of splines which represent a system of equations.
|
|
16
|
+
|
|
15
17
|
`bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
|
|
16
18
|
|
|
17
19
|
`bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
|
|
@@ -22,5 +24,6 @@ from bspy.solid import Solid, Boundary
|
|
|
22
24
|
from bspy.manifold import Manifold
|
|
23
25
|
from bspy.hyperplane import Hyperplane
|
|
24
26
|
from bspy.spline import Spline
|
|
27
|
+
from bspy.spline_block import SplineBlock
|
|
25
28
|
from bspy.splineOpenGLFrame import SplineOpenGLFrame
|
|
26
29
|
from bspy.viewer import Viewer, Graphics
|
bspy/_spline_domain.py
CHANGED
|
@@ -310,26 +310,73 @@ def fold(self, foldedInd):
|
|
|
310
310
|
coefficientlessSpline = type(self)(len(coefficientlessOrder), 0, coefficientlessOrder, coefficientlessNCoef, coefficientlessKnots, coefficientlessCoefs, self.metadata)
|
|
311
311
|
return foldedSpline, coefficientlessSpline
|
|
312
312
|
|
|
313
|
-
def insert_knots(self,
|
|
314
|
-
if not(len(
|
|
315
|
-
knotsList = list(self.knots)
|
|
316
|
-
coefs = self.coefs
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
313
|
+
def insert_knots(self, newKnotList):
|
|
314
|
+
if not(len(newKnotList) == self.nInd): raise ValueError("Invalid newKnots")
|
|
315
|
+
knotsList = list(self.knots) # Create a new knot list
|
|
316
|
+
coefs = self.coefs # Set initial value for coefs to check later if it's changed
|
|
317
|
+
|
|
318
|
+
# Insert new knots into each independent variable.
|
|
319
|
+
for ind, (order, knots, newKnots) in enumerate(zip(self.order, self.knots, newKnotList)):
|
|
320
|
+
coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
|
|
321
|
+
degree = order - 1
|
|
322
|
+
for knot in newKnots:
|
|
323
|
+
# Determine new knot multiplicity.
|
|
324
|
+
if np.isscalar(knot):
|
|
325
|
+
multiplicity = 1
|
|
324
326
|
else:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
327
|
+
multiplicity = knot[1]
|
|
328
|
+
knot = knot[0]
|
|
329
|
+
if multiplicity < 1:
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# Check if knot and its total multiplicity is valid.
|
|
333
|
+
if knot < knots[degree] or knot > knots[-order]:
|
|
334
|
+
raise ValueError(f"Knot insertion outside domain: {knot}")
|
|
335
|
+
position = np.searchsorted(knots, knot, 'right')
|
|
336
|
+
oldMultiplicity = 0
|
|
337
|
+
for k in knots[position - 1::-1]:
|
|
338
|
+
if knot == k:
|
|
339
|
+
oldMultiplicity += 1
|
|
340
|
+
else:
|
|
341
|
+
break
|
|
342
|
+
if oldMultiplicity + multiplicity > order:
|
|
343
|
+
raise ValueError("Knot multiplicity > order")
|
|
344
|
+
|
|
345
|
+
# Initialize oldCoefs and expanded coefs array with multiplicity new coefficients, as well as some indices.
|
|
346
|
+
oldCoefs = coefs[position - order:position].copy()
|
|
347
|
+
lastKnotIndex = position - oldMultiplicity
|
|
348
|
+
firstCoefIndex = position - degree
|
|
349
|
+
coefs = np.insert(coefs, firstCoefIndex, oldCoefs[:multiplicity], axis=0)
|
|
350
|
+
# Compute inserted coefficients (multiplicity of them) and the degree - oldMultiplicity - 1 number of changed coefficients.
|
|
351
|
+
for j in range(multiplicity):
|
|
352
|
+
# Allocate new coefficients for the current multiplicity.
|
|
353
|
+
size = degree - oldMultiplicity - j
|
|
354
|
+
if size < 1:
|
|
355
|
+
# Full multiplicity knot, so use oldCoefs.
|
|
356
|
+
coefs[firstCoefIndex + j] = oldCoefs[0]
|
|
357
|
+
else:
|
|
358
|
+
# Otherwise, allocate space for newCoefs.
|
|
359
|
+
newCoefs = np.empty((size, *coefs.shape[1:]), coefs.dtype)
|
|
360
|
+
|
|
361
|
+
# Compute the new coefficients.
|
|
362
|
+
for i, k in zip(range(size), range(lastKnotIndex - size, lastKnotIndex)):
|
|
363
|
+
alpha = (knot - knots[k]) / (knots[k + degree - j] - knots[k])
|
|
364
|
+
newCoefs[i] = (1.0 - alpha) * oldCoefs[i] + alpha * oldCoefs[i + 1]
|
|
365
|
+
|
|
366
|
+
# Assign the ends of the new coefficients into their respective positions.
|
|
367
|
+
coefs[firstCoefIndex + j] = newCoefs[0]
|
|
368
|
+
if size > 1:
|
|
369
|
+
coefs[lastKnotIndex + multiplicity - j - 2] = newCoefs[-1]
|
|
370
|
+
oldCoefs = newCoefs
|
|
371
|
+
|
|
372
|
+
# Assign remaining computed coefficients (the ones in the middle).
|
|
373
|
+
if size > 2:
|
|
374
|
+
coefs[firstCoefIndex + multiplicity:firstCoefIndex + multiplicity + size - 2] = newCoefs[1:-1]
|
|
375
|
+
|
|
376
|
+
# Insert the inserted coefficients and inserted knots.
|
|
377
|
+
knotsList[ind] = knots = np.insert(knots, position, (knot,) * multiplicity)
|
|
378
|
+
|
|
379
|
+
coefs = coefs.swapaxes(0, ind + 1) # Swap back
|
|
333
380
|
|
|
334
381
|
if self.coefs is coefs:
|
|
335
382
|
return self
|
|
@@ -535,10 +582,12 @@ def transpose(self, axes=None):
|
|
|
535
582
|
def trim(self, newDomain):
|
|
536
583
|
if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
|
|
537
584
|
if self.nInd < 1: return self
|
|
538
|
-
newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
|
|
585
|
+
newDomain = np.array(newDomain, self.knots[0].dtype, copy=True) # Force dtype and convert None to nan
|
|
586
|
+
epsilon = np.finfo(newDomain.dtype).eps
|
|
539
587
|
|
|
540
588
|
# Step 1: Determine the knots to insert at the new domain bounds.
|
|
541
589
|
newKnotsList = []
|
|
590
|
+
noChange = True
|
|
542
591
|
for (order, knots, bounds) in zip(self.order, self.knots, newDomain):
|
|
543
592
|
if not(len(bounds) == 2): raise ValueError("Invalid newDomain")
|
|
544
593
|
unique, counts = np.unique(knots, return_counts=True)
|
|
@@ -548,28 +597,43 @@ def trim(self, newDomain):
|
|
|
548
597
|
if not np.isnan(bounds[0]):
|
|
549
598
|
if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
550
599
|
leftBound = True
|
|
551
|
-
multiplicity = order
|
|
552
600
|
i = np.searchsorted(unique, bounds[0])
|
|
553
|
-
if unique[i]
|
|
554
|
-
|
|
555
|
-
|
|
601
|
+
if unique[i] - bounds[0] < epsilon:
|
|
602
|
+
bounds[0] = unique[i]
|
|
603
|
+
multiplicity = order - counts[i]
|
|
604
|
+
elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
|
|
605
|
+
bounds[0] = unique[i - 1]
|
|
606
|
+
multiplicity = order - counts[i - 1]
|
|
607
|
+
else:
|
|
608
|
+
multiplicity = order
|
|
609
|
+
if multiplicity > 0:
|
|
610
|
+
newKnots.append((bounds[0], multiplicity))
|
|
611
|
+
noChange = False
|
|
556
612
|
|
|
557
613
|
if not np.isnan(bounds[1]):
|
|
558
614
|
if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
559
615
|
if leftBound:
|
|
560
616
|
if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
|
|
561
|
-
multiplicity = order
|
|
562
617
|
i = np.searchsorted(unique, bounds[1])
|
|
563
|
-
if unique[i]
|
|
564
|
-
|
|
565
|
-
|
|
618
|
+
if unique[i] - bounds[1] < epsilon:
|
|
619
|
+
bounds[1] = unique[i]
|
|
620
|
+
multiplicity = order - counts[i]
|
|
621
|
+
elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
|
|
622
|
+
bounds[1] = unique[i - 1]
|
|
623
|
+
multiplicity = order - counts[i - i]
|
|
624
|
+
else:
|
|
625
|
+
multiplicity = order
|
|
626
|
+
if multiplicity > 0:
|
|
627
|
+
newKnots.append((bounds[1], multiplicity))
|
|
628
|
+
noChange = False
|
|
566
629
|
|
|
567
630
|
newKnotsList.append(newKnots)
|
|
568
631
|
|
|
632
|
+
if noChange:
|
|
633
|
+
return self
|
|
634
|
+
|
|
569
635
|
# Step 2: Insert the knots.
|
|
570
636
|
spline = self.insert_knots(newKnotsList)
|
|
571
|
-
if spline is self:
|
|
572
|
-
return spline
|
|
573
637
|
|
|
574
638
|
# Step 3: Trim the knots and coefficients.
|
|
575
639
|
knotsList = []
|
bspy/_spline_evaluation.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
2
3
|
|
|
3
4
|
def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
|
|
4
5
|
basis = np.zeros(splineOrder, knots.dtype)
|
|
@@ -25,10 +26,61 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
|
|
|
25
26
|
b += 1
|
|
26
27
|
return knot, basis
|
|
27
28
|
|
|
29
|
+
def composed_integral(self, integrand = None, domain = None):
|
|
30
|
+
# Determine domain and check its validity
|
|
31
|
+
actualDomain = self.domain()
|
|
32
|
+
if domain is None:
|
|
33
|
+
domain = actualDomain
|
|
34
|
+
else:
|
|
35
|
+
for iInd in range(self.nInd):
|
|
36
|
+
if domain[iInd, 0] < actualDomain[iInd, 0] or \
|
|
37
|
+
domain[iInd, 1] > actualDomain[iInd, 1]:
|
|
38
|
+
raise ValueError("Can't integrate beyond the domain of the spline")
|
|
39
|
+
|
|
40
|
+
# Determine breakpoints for quadrature intervals; require functions to be analytic
|
|
41
|
+
|
|
42
|
+
uniqueKnots = []
|
|
43
|
+
for iInd in range(self.nInd):
|
|
44
|
+
iStart = np.searchsorted(self.knots[iInd], domain[iInd, 0], side = 'right')
|
|
45
|
+
iEnd = np.searchsorted(self.knots[iInd], domain[iInd, 1], side = 'right')
|
|
46
|
+
uniqueKnots.append(np.unique(np.insert(self.knots[iInd], [iStart, iEnd], domain[iInd])[iStart : iEnd + 2]))
|
|
47
|
+
|
|
48
|
+
# Set integrand function if none is given
|
|
49
|
+
if integrand is None:
|
|
50
|
+
integrand = lambda x : 1.0
|
|
51
|
+
|
|
52
|
+
# Set tolerance
|
|
53
|
+
tolerance = 1.0e-13 / self.nInd
|
|
54
|
+
|
|
55
|
+
# Establish the callback function
|
|
56
|
+
def composedIntegrand(u, nIndSoFar, uValues):
|
|
57
|
+
uValues[nIndSoFar] = u
|
|
58
|
+
nIndSoFar += 1
|
|
59
|
+
if self.nInd == nIndSoFar:
|
|
60
|
+
total = integrand(self(uValues)) * \
|
|
61
|
+
np.prod(np.linalg.svd(self.jacobian(uValues), compute_uv = False))
|
|
62
|
+
else:
|
|
63
|
+
total = 0.0
|
|
64
|
+
for ix in range(len(uniqueKnots[nIndSoFar]) - 1):
|
|
65
|
+
value = sp.integrate.quad(composedIntegrand, uniqueKnots[nIndSoFar][ix],
|
|
66
|
+
uniqueKnots[nIndSoFar][ix + 1], (nIndSoFar, uValues),
|
|
67
|
+
epsabs = tolerance, epsrel = tolerance)
|
|
68
|
+
total += value[0]
|
|
69
|
+
return total
|
|
70
|
+
|
|
71
|
+
# Compute the value by calling the callback routine
|
|
72
|
+
total = composedIntegrand(0.0, -1, self.nInd * [0.0])
|
|
73
|
+
return total
|
|
74
|
+
|
|
75
|
+
def continuity(self):
|
|
76
|
+
multiplicity = np.array([np.max(np.unique(knots, return_counts = True)[1][1 : -1]) for knots in self.knots])
|
|
77
|
+
continuity = self.order - multiplicity - 1
|
|
78
|
+
return continuity
|
|
79
|
+
|
|
28
80
|
def curvature(self, uv):
|
|
81
|
+
if self.nDep == 1:
|
|
82
|
+
self = self.graph()
|
|
29
83
|
if self.nInd == 1:
|
|
30
|
-
if self.nDep == 1:
|
|
31
|
-
self = self.graph()
|
|
32
84
|
fp = self.derivative([1], uv)
|
|
33
85
|
fpp = self.derivative([2], uv)
|
|
34
86
|
fpDotFp = fp @ fp
|
|
@@ -38,7 +90,21 @@ def curvature(self, uv):
|
|
|
38
90
|
numerator = fp[0] * fpp[1] - fp[1] * fpp[0]
|
|
39
91
|
else:
|
|
40
92
|
numerator = np.sqrt((fpp @ fpp) * fpDotFp - fpDotFpp ** 2)
|
|
41
|
-
return numerator / denom
|
|
93
|
+
return numerator / denom
|
|
94
|
+
if self.nInd == 2:
|
|
95
|
+
su = self.derivative([1, 0], uv)
|
|
96
|
+
sv = self.derivative([0, 1], uv)
|
|
97
|
+
normal = self.normal(uv)
|
|
98
|
+
suu = self.derivative([2, 0], uv)
|
|
99
|
+
suv = self.derivative([1, 1], uv)
|
|
100
|
+
svv = self.derivative([0, 2], uv)
|
|
101
|
+
E = su @ su
|
|
102
|
+
F = su @ sv
|
|
103
|
+
G = sv @ sv
|
|
104
|
+
L = suu @ normal
|
|
105
|
+
M = suv @ normal
|
|
106
|
+
N = svv @ normal
|
|
107
|
+
return (L * N - M ** 2) / (E * G - F ** 2)
|
|
42
108
|
|
|
43
109
|
def derivative(self, with_respect_to, uvw):
|
|
44
110
|
# Make work for scalar valued functions
|
|
@@ -107,6 +173,8 @@ def greville(self, ind = 0):
|
|
|
107
173
|
for ix in range(1, self.order[ind]):
|
|
108
174
|
knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
|
|
109
175
|
knotAverages /= (self.order[ind] - 1)
|
|
176
|
+
domain = self.domain()[ind]
|
|
177
|
+
knotAverages = np.minimum(domain[1], np.maximum(domain[0], knotAverages))
|
|
110
178
|
return knotAverages
|
|
111
179
|
|
|
112
180
|
def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
|
|
@@ -150,8 +218,8 @@ def normal(self, uvw, normalize=True, indices=None):
|
|
|
150
218
|
|
|
151
219
|
if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be one different than the number of dependent variables.")
|
|
152
220
|
|
|
153
|
-
# Evaluate the
|
|
154
|
-
tangentSpace = self.
|
|
221
|
+
# Evaluate the Jacobian at the point.
|
|
222
|
+
tangentSpace = self.jacobian(uvw)
|
|
155
223
|
|
|
156
224
|
# Record the larger dimension and ensure it comes first.
|
|
157
225
|
if self.nInd > self.nDep:
|
|
@@ -161,15 +229,15 @@ def normal(self, uvw, normalize=True, indices=None):
|
|
|
161
229
|
nDep = self.nDep
|
|
162
230
|
|
|
163
231
|
# Compute the normal using cofactors (determinants of subsets of the tangent space).
|
|
164
|
-
sign = -1 if self.metadata.get("flipNormal", False) else 1
|
|
232
|
+
sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
|
|
233
|
+
dtype = self.coefs.dtype if hasattr(self, "coefs") else self.coefsDtype
|
|
165
234
|
if indices is None:
|
|
166
235
|
indices = range(nDep)
|
|
167
|
-
normal = np.empty(nDep,
|
|
236
|
+
normal = np.empty(nDep, dtype)
|
|
168
237
|
else:
|
|
169
|
-
normal = np.empty(len(indices),
|
|
170
|
-
for i in indices:
|
|
171
|
-
normal[
|
|
172
|
-
sign *= -1
|
|
238
|
+
normal = np.empty(len(indices), dtype)
|
|
239
|
+
for ix, i in enumerate(indices):
|
|
240
|
+
normal[ix] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
|
|
173
241
|
|
|
174
242
|
# Normalize the result as needed.
|
|
175
243
|
if normalize:
|
|
@@ -180,13 +248,4 @@ def normal(self, uvw, normalize=True, indices=None):
|
|
|
180
248
|
def range_bounds(self):
|
|
181
249
|
# Assumes self.nDep is the first value in self.coefs.shape
|
|
182
250
|
bounds = [[coefficient.min(), coefficient.max()] for coefficient in self.coefs]
|
|
183
|
-
return np.array(bounds, self.coefs.dtype)
|
|
184
|
-
|
|
185
|
-
def tangent_space(self, uvw):
|
|
186
|
-
tangentSpace = np.empty((self.nDep, self.nInd), self.coefs.dtype)
|
|
187
|
-
wrt = [0] * self.nInd
|
|
188
|
-
for i in range(self.nInd):
|
|
189
|
-
wrt[i] = 1
|
|
190
|
-
tangentSpace[:, i] = self.derivative(wrt, uvw)
|
|
191
|
-
wrt[i] = 0
|
|
192
|
-
return tangentSpace
|
|
251
|
+
return np.array(bounds, self.coefs.dtype)
|