bspy 4.0__py3-none-any.whl → 4.2__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 +16 -3
- bspy/_spline_domain.py +101 -9
- bspy/_spline_evaluation.py +82 -17
- bspy/_spline_fitting.py +244 -114
- bspy/_spline_intersection.py +467 -156
- bspy/_spline_operations.py +70 -49
- bspy/hyperplane.py +540 -0
- bspy/manifold.py +334 -31
- bspy/solid.py +842 -0
- bspy/spline.py +350 -71
- bspy/splineOpenGLFrame.py +262 -14
- bspy/spline_block.py +343 -0
- bspy/viewer.py +134 -90
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/METADATA +17 -6
- bspy-4.2.dist-info/RECORD +18 -0
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/WHEEL +1 -1
- bspy-4.0.dist-info/RECORD +0 -15
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/LICENSE +0 -0
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/top_level.txt +0 -0
bspy/__init__.py
CHANGED
|
@@ -3,14 +3,27 @@ BSpy is a python library for manipulating and rendering non-uniform B-splines.
|
|
|
3
3
|
|
|
4
4
|
Available subpackages
|
|
5
5
|
---------------------
|
|
6
|
-
`bspy.
|
|
7
|
-
|
|
6
|
+
`bspy.solid` : Provides the `Solid` and `Boundary` classes that model solids.
|
|
7
|
+
|
|
8
|
+
`bspy.manifold` : Provides the `Manifold` base class for manifolds.
|
|
9
|
+
|
|
10
|
+
`bspy.hyperplane` : Provides the `Hyperplane` subclass of `Manifold` that models hyperplanes.
|
|
11
|
+
|
|
12
|
+
`bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
|
|
13
|
+
piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
|
|
14
|
+
|
|
15
|
+
`bspy.spline_block` : Provides the `SplineBlock` class that represents and processes an array-like collection of splines.
|
|
8
16
|
|
|
9
17
|
`bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
|
|
10
18
|
|
|
11
19
|
`bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
|
|
12
20
|
splines, and a set of controls to adjust and view the selected splines. It also provides the `Graphics` engine that creates
|
|
13
|
-
an associated `Viewer`, allowing you to script splines and display them in the viewer.
|
|
21
|
+
an associated `Viewer`, allowing you to script splines and display them in the viewer.
|
|
22
|
+
"""
|
|
23
|
+
from bspy.solid import Solid, Boundary
|
|
24
|
+
from bspy.manifold import Manifold
|
|
25
|
+
from bspy.hyperplane import Hyperplane
|
|
14
26
|
from bspy.spline import Spline
|
|
27
|
+
from bspy.spline_block import SplineBlock
|
|
15
28
|
from bspy.splineOpenGLFrame import SplineOpenGLFrame
|
|
16
29
|
from bspy.viewer import Viewer, Graphics
|
bspy/_spline_domain.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
from bspy.manifold import Manifold
|
|
2
3
|
|
|
3
4
|
def clamp(self, left, right):
|
|
4
5
|
bounds = [[None, None] for i in range(self.nInd)]
|
|
@@ -517,6 +518,66 @@ def reverse(self, variable = 0):
|
|
|
517
518
|
newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
|
|
518
519
|
return newFolded.unfold(myIndices, basisInfo)
|
|
519
520
|
|
|
521
|
+
def split(self, minContinuity = 0, breaks = None):
|
|
522
|
+
if minContinuity < 0: raise ValueError("minContinuity must be >= 0")
|
|
523
|
+
if breaks is not None and len(breaks) != self.nInd: raise ValueError("Invalid breaks")
|
|
524
|
+
if self.nInd < 1: return self
|
|
525
|
+
|
|
526
|
+
# Step 1: Determine the knots to insert.
|
|
527
|
+
newKnotsList = []
|
|
528
|
+
for i, order, knots in zip(range(self.nInd), self.order, self.knots):
|
|
529
|
+
unique, counts = np.unique(knots, return_counts=True)
|
|
530
|
+
newKnots = []
|
|
531
|
+
for knot, count in zip(unique, counts):
|
|
532
|
+
assert count <= order
|
|
533
|
+
if count > order - 1 - minContinuity:
|
|
534
|
+
newKnots += [knot] * (order - count)
|
|
535
|
+
if breaks is not None:
|
|
536
|
+
for knot in breaks[i]:
|
|
537
|
+
if knot not in unique:
|
|
538
|
+
newKnots += [knot] * order
|
|
539
|
+
newKnotsList.append(newKnots)
|
|
540
|
+
|
|
541
|
+
# Step 2: Insert the knots.
|
|
542
|
+
spline = self.insert_knots(newKnotsList)
|
|
543
|
+
if spline is self:
|
|
544
|
+
return np.full((1,) * spline.nInd, spline)
|
|
545
|
+
|
|
546
|
+
# Step 3: Store the indices of the full order knots.
|
|
547
|
+
indexList = []
|
|
548
|
+
splineCount = []
|
|
549
|
+
totalSplineCount = 1
|
|
550
|
+
for order, knots in zip(spline.order, spline.knots):
|
|
551
|
+
unique, counts = np.unique(knots, return_counts=True)
|
|
552
|
+
indices = np.searchsorted(knots, unique)
|
|
553
|
+
fullOrder = []
|
|
554
|
+
for ix, count in zip(indices, counts):
|
|
555
|
+
if count == order:
|
|
556
|
+
fullOrder.append(ix)
|
|
557
|
+
indexList.append(fullOrder)
|
|
558
|
+
splines = len(fullOrder) - 1
|
|
559
|
+
splineCount.append(splines)
|
|
560
|
+
totalSplineCount *= splines
|
|
561
|
+
|
|
562
|
+
# Step 4: Slice up the spline.
|
|
563
|
+
splineArray = np.empty(totalSplineCount, object)
|
|
564
|
+
for i in range(totalSplineCount):
|
|
565
|
+
knotsList = []
|
|
566
|
+
coefIndex = [slice(None)] # First index is for nDep
|
|
567
|
+
ix = i
|
|
568
|
+
for order, knots, splines, indices in zip(spline.order, spline.knots, splineCount, indexList):
|
|
569
|
+
j = ix % splines
|
|
570
|
+
ix = ix // splines
|
|
571
|
+
leftIndex = indices[j]
|
|
572
|
+
rightIndex = indices[j + 1]
|
|
573
|
+
knotsList.append(knots[leftIndex:rightIndex + order])
|
|
574
|
+
coefIndex.append(slice(leftIndex, rightIndex))
|
|
575
|
+
coefs = spline.coefs[tuple(coefIndex)]
|
|
576
|
+
splineArray[i] = type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
|
|
577
|
+
|
|
578
|
+
# Return the transpose because we put the splines into splineArray dimensions in reverse order.
|
|
579
|
+
return splineArray.reshape(tuple(reversed(splineCount))).T
|
|
580
|
+
|
|
520
581
|
def transpose(self, axes=None):
|
|
521
582
|
if axes is None:
|
|
522
583
|
axes = range(self.nInd)[::-1]
|
|
@@ -534,10 +595,12 @@ def transpose(self, axes=None):
|
|
|
534
595
|
def trim(self, newDomain):
|
|
535
596
|
if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
|
|
536
597
|
if self.nInd < 1: return self
|
|
537
|
-
newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
|
|
598
|
+
newDomain = np.array(newDomain, self.knots[0].dtype, copy=True) # Force dtype and convert None to nan
|
|
599
|
+
epsilon = np.finfo(newDomain.dtype).eps
|
|
538
600
|
|
|
539
601
|
# Step 1: Determine the knots to insert at the new domain bounds.
|
|
540
602
|
newKnotsList = []
|
|
603
|
+
noChange = True
|
|
541
604
|
for (order, knots, bounds) in zip(self.order, self.knots, newDomain):
|
|
542
605
|
if not(len(bounds) == 2): raise ValueError("Invalid newDomain")
|
|
543
606
|
unique, counts = np.unique(knots, return_counts=True)
|
|
@@ -547,28 +610,49 @@ def trim(self, newDomain):
|
|
|
547
610
|
if not np.isnan(bounds[0]):
|
|
548
611
|
if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
549
612
|
leftBound = True
|
|
550
|
-
multiplicity = order
|
|
551
613
|
i = np.searchsorted(unique, bounds[0])
|
|
552
|
-
if unique[i]
|
|
553
|
-
|
|
614
|
+
if unique[i] - bounds[0] < epsilon:
|
|
615
|
+
bounds[0] = unique[i]
|
|
616
|
+
multiplicity = order - counts[i]
|
|
617
|
+
if i > 0:
|
|
618
|
+
noChange = False
|
|
619
|
+
elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
|
|
620
|
+
bounds[0] = unique[i - 1]
|
|
621
|
+
multiplicity = order - counts[i - 1]
|
|
622
|
+
if i - 1 > 0:
|
|
623
|
+
noChange = False
|
|
624
|
+
else:
|
|
625
|
+
multiplicity = order
|
|
626
|
+
|
|
554
627
|
newKnots += multiplicity * [bounds[0]]
|
|
555
628
|
|
|
556
629
|
if not np.isnan(bounds[1]):
|
|
557
630
|
if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
558
631
|
if leftBound:
|
|
559
632
|
if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
|
|
560
|
-
multiplicity = order
|
|
561
633
|
i = np.searchsorted(unique, bounds[1])
|
|
562
|
-
if unique[i]
|
|
563
|
-
|
|
634
|
+
if unique[i] - bounds[1] < epsilon:
|
|
635
|
+
bounds[1] = unique[i]
|
|
636
|
+
multiplicity = order - counts[i]
|
|
637
|
+
if i < len(unique) - 1:
|
|
638
|
+
noChange = False
|
|
639
|
+
elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
|
|
640
|
+
bounds[1] = unique[i - 1]
|
|
641
|
+
multiplicity = order - counts[i - i]
|
|
642
|
+
noChange = False # i < len(unique) - 1
|
|
643
|
+
else:
|
|
644
|
+
multiplicity = order
|
|
564
645
|
newKnots += multiplicity * [bounds[1]]
|
|
565
646
|
|
|
566
647
|
newKnotsList.append(newKnots)
|
|
648
|
+
if len(newKnots) > 0:
|
|
649
|
+
noChange = False
|
|
650
|
+
|
|
651
|
+
if noChange:
|
|
652
|
+
return self
|
|
567
653
|
|
|
568
654
|
# Step 2: Insert the knots.
|
|
569
655
|
spline = self.insert_knots(newKnotsList)
|
|
570
|
-
if spline is self:
|
|
571
|
-
return spline
|
|
572
656
|
|
|
573
657
|
# Step 3: Trim the knots and coefficients.
|
|
574
658
|
knotsList = []
|
|
@@ -582,6 +666,14 @@ def trim(self, newDomain):
|
|
|
582
666
|
|
|
583
667
|
return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
|
|
584
668
|
|
|
669
|
+
def trimmed_range_bounds(self, domainBounds):
|
|
670
|
+
domainBounds = np.array(domainBounds, copy=True)
|
|
671
|
+
for original, trim in zip(self.domain(), domainBounds):
|
|
672
|
+
trim[0] = max(original[0], trim[0] - Manifold.minSeparation)
|
|
673
|
+
trim[1] = min(original[1], trim[1] + Manifold.minSeparation)
|
|
674
|
+
trimmedSpline = self.trim(domainBounds)
|
|
675
|
+
return trimmedSpline, trimmedSpline.range_bounds()
|
|
676
|
+
|
|
585
677
|
def unfold(self, foldedInd, coefficientlessSpline):
|
|
586
678
|
if not(len(foldedInd) == coefficientlessSpline.nInd): raise ValueError("Invalid coefficientlessSpline")
|
|
587
679
|
unfoldedOrder = []
|
bspy/_spline_evaluation.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
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)
|
|
5
|
-
basis[-1] = 1.0
|
|
6
6
|
if knot is None:
|
|
7
7
|
knot = np.searchsorted(knots, u, side = 'right')
|
|
8
8
|
knot = min(knot, len(knots) - splineOrder)
|
|
9
|
+
if derivativeOrder >= splineOrder:
|
|
10
|
+
return knot, basis
|
|
11
|
+
basis[-1] = 1.0
|
|
9
12
|
for degree in range(1, splineOrder - derivativeOrder):
|
|
10
13
|
b = splineOrder - degree
|
|
11
14
|
for i in range(knot - degree, knot):
|
|
@@ -23,10 +26,61 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
|
|
|
23
26
|
b += 1
|
|
24
27
|
return knot, basis
|
|
25
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
|
+
|
|
26
80
|
def curvature(self, uv):
|
|
81
|
+
if self.nDep == 1:
|
|
82
|
+
self = self.graph()
|
|
27
83
|
if self.nInd == 1:
|
|
28
|
-
if self.nDep == 1:
|
|
29
|
-
self = self.graph()
|
|
30
84
|
fp = self.derivative([1], uv)
|
|
31
85
|
fpp = self.derivative([2], uv)
|
|
32
86
|
fpDotFp = fp @ fp
|
|
@@ -36,7 +90,21 @@ def curvature(self, uv):
|
|
|
36
90
|
numerator = fp[0] * fpp[1] - fp[1] * fpp[0]
|
|
37
91
|
else:
|
|
38
92
|
numerator = np.sqrt((fpp @ fpp) * fpDotFp - fpDotFpp ** 2)
|
|
39
|
-
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)
|
|
40
108
|
|
|
41
109
|
def derivative(self, with_respect_to, uvw):
|
|
42
110
|
# Make work for scalar valued functions
|
|
@@ -105,6 +173,8 @@ def greville(self, ind = 0):
|
|
|
105
173
|
for ix in range(1, self.order[ind]):
|
|
106
174
|
knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
|
|
107
175
|
knotAverages /= (self.order[ind] - 1)
|
|
176
|
+
domain = self.domain()[ind]
|
|
177
|
+
knotAverages = np.minimum(domain[1], np.maximum(domain[0], knotAverages))
|
|
108
178
|
return knotAverages
|
|
109
179
|
|
|
110
180
|
def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
|
|
@@ -148,31 +218,26 @@ def normal(self, uvw, normalize=True, indices=None):
|
|
|
148
218
|
|
|
149
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.")
|
|
150
220
|
|
|
151
|
-
# Evaluate the
|
|
152
|
-
tangentSpace =
|
|
153
|
-
with_respect_to = [0] * self.nInd
|
|
154
|
-
for i in range(self.nInd):
|
|
155
|
-
with_respect_to[i] = 1
|
|
156
|
-
tangentSpace[i] = self.derivative(with_respect_to, uvw)
|
|
157
|
-
with_respect_to[i] = 0
|
|
221
|
+
# Evaluate the Jacobian at the point.
|
|
222
|
+
tangentSpace = self.jacobian(uvw)
|
|
158
223
|
|
|
159
224
|
# Record the larger dimension and ensure it comes first.
|
|
160
225
|
if self.nInd > self.nDep:
|
|
161
226
|
nDep = self.nInd
|
|
227
|
+
tangentSpace = tangentSpace.T
|
|
162
228
|
else:
|
|
163
229
|
nDep = self.nDep
|
|
164
|
-
tangentSpace = tangentSpace.T
|
|
165
230
|
|
|
166
231
|
# Compute the normal using cofactors (determinants of subsets of the tangent space).
|
|
167
|
-
sign = 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
|
|
168
234
|
if indices is None:
|
|
169
235
|
indices = range(nDep)
|
|
170
|
-
normal = np.empty(nDep,
|
|
236
|
+
normal = np.empty(nDep, dtype)
|
|
171
237
|
else:
|
|
172
|
-
normal = np.empty(len(indices),
|
|
238
|
+
normal = np.empty(len(indices), dtype)
|
|
173
239
|
for i in indices:
|
|
174
|
-
normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
|
|
175
|
-
sign *= -1
|
|
240
|
+
normal[i] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
|
|
176
241
|
|
|
177
242
|
# Normalize the result as needed.
|
|
178
243
|
if normalize:
|