bspy 4.1__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 +3 -0
- bspy/_spline_domain.py +92 -9
- bspy/_spline_evaluation.py +79 -20
- bspy/_spline_fitting.py +201 -45
- bspy/_spline_intersection.py +284 -137
- bspy/_spline_operations.py +63 -48
- bspy/hyperplane.py +6 -6
- bspy/manifold.py +2 -2
- bspy/solid.py +15 -12
- bspy/spline.py +180 -50
- bspy/spline_block.py +343 -0
- bspy/viewer.py +7 -6
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/METADATA +11 -5
- bspy-4.2.dist-info/RECORD +18 -0
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/WHEEL +1 -1
- bspy-4.1.dist-info/RECORD +0 -17
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/LICENSE +0 -0
- {bspy-4.1.dist-info → bspy-4.2.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 represents and processes an array-like collection of splines.
|
|
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
|
@@ -518,6 +518,66 @@ def reverse(self, variable = 0):
|
|
|
518
518
|
newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
|
|
519
519
|
return newFolded.unfold(myIndices, basisInfo)
|
|
520
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
|
+
|
|
521
581
|
def transpose(self, axes=None):
|
|
522
582
|
if axes is None:
|
|
523
583
|
axes = range(self.nInd)[::-1]
|
|
@@ -535,10 +595,12 @@ def transpose(self, axes=None):
|
|
|
535
595
|
def trim(self, newDomain):
|
|
536
596
|
if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
|
|
537
597
|
if self.nInd < 1: return self
|
|
538
|
-
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
|
|
539
600
|
|
|
540
601
|
# Step 1: Determine the knots to insert at the new domain bounds.
|
|
541
602
|
newKnotsList = []
|
|
603
|
+
noChange = True
|
|
542
604
|
for (order, knots, bounds) in zip(self.order, self.knots, newDomain):
|
|
543
605
|
if not(len(bounds) == 2): raise ValueError("Invalid newDomain")
|
|
544
606
|
unique, counts = np.unique(knots, return_counts=True)
|
|
@@ -548,28 +610,49 @@ def trim(self, newDomain):
|
|
|
548
610
|
if not np.isnan(bounds[0]):
|
|
549
611
|
if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
550
612
|
leftBound = True
|
|
551
|
-
multiplicity = order
|
|
552
613
|
i = np.searchsorted(unique, bounds[0])
|
|
553
|
-
if unique[i]
|
|
554
|
-
|
|
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
|
+
|
|
555
627
|
newKnots += multiplicity * [bounds[0]]
|
|
556
628
|
|
|
557
629
|
if not np.isnan(bounds[1]):
|
|
558
630
|
if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
|
|
559
631
|
if leftBound:
|
|
560
632
|
if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
|
|
561
|
-
multiplicity = order
|
|
562
633
|
i = np.searchsorted(unique, bounds[1])
|
|
563
|
-
if unique[i]
|
|
564
|
-
|
|
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
|
|
565
645
|
newKnots += multiplicity * [bounds[1]]
|
|
566
646
|
|
|
567
647
|
newKnotsList.append(newKnots)
|
|
648
|
+
if len(newKnots) > 0:
|
|
649
|
+
noChange = False
|
|
650
|
+
|
|
651
|
+
if noChange:
|
|
652
|
+
return self
|
|
568
653
|
|
|
569
654
|
# Step 2: Insert the knots.
|
|
570
655
|
spline = self.insert_knots(newKnotsList)
|
|
571
|
-
if spline is self:
|
|
572
|
-
return spline
|
|
573
656
|
|
|
574
657
|
# Step 3: Trim the knots and coefficients.
|
|
575
658
|
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),
|
|
238
|
+
normal = np.empty(len(indices), dtype)
|
|
170
239
|
for i in indices:
|
|
171
|
-
normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
|
|
172
|
-
sign *= -1
|
|
240
|
+
normal[i] = 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)
|
bspy/_spline_fitting.py
CHANGED
|
@@ -11,6 +11,16 @@ def circular_arc(radius, angle, tolerance = None):
|
|
|
11
11
|
samples = int(max(np.ceil(((1.1536e-5 * radius / tolerance)**(1/8)) * angle / 90), 2.0)) + 1
|
|
12
12
|
return bspy.Spline.section([(radius * np.cos(u * angle * np.pi / 180), radius * np.sin(u * angle * np.pi / 180), 90 + u * angle, 1.0 / radius) for u in np.linspace(0.0, 1.0, samples)])
|
|
13
13
|
|
|
14
|
+
def composition(splines, tolerance):
|
|
15
|
+
# Define the callback function
|
|
16
|
+
def composition_of_splines(u):
|
|
17
|
+
for f in splines[::-1]:
|
|
18
|
+
u = f(u)
|
|
19
|
+
return u
|
|
20
|
+
|
|
21
|
+
# Approximate this composition
|
|
22
|
+
return bspy.Spline.fit(splines[-1].domain(), composition_of_splines, tolerance = tolerance)
|
|
23
|
+
|
|
14
24
|
def cone(radius1, radius2, height, tolerance = None):
|
|
15
25
|
if tolerance is None:
|
|
16
26
|
tolerance = 1.0e-12
|
|
@@ -88,48 +98,57 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
88
98
|
FValues = F(knownXValue)
|
|
89
99
|
if not(len(FValues) == nDep - 1 and np.linalg.norm(FValues) < evaluationEpsilon):
|
|
90
100
|
raise ValueError(f"F(known x) must be a zero vector of length {nDep - 1}.")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
|
|
102
|
+
# Record domain of F and scaling of coefficients.
|
|
103
|
+
if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
|
|
104
|
+
FDomain = F.domain().T
|
|
105
|
+
coefsMin = FDomain[0]
|
|
106
|
+
coefsMaxMinusMin = FDomain[1] - FDomain[0]
|
|
107
|
+
else:
|
|
108
|
+
FDomain = np.array(nDep * [[-np.inf, np.inf]]).T
|
|
109
|
+
coefsMin = knownXValues.min(axis=0)
|
|
110
|
+
coefsMaxMinusMin = knownXValues.max(axis=0) - coefsMin
|
|
111
|
+
coefsMaxMinusMin = np.where(coefsMaxMinusMin < 1.0, 1.0, coefsMaxMinusMin)
|
|
112
|
+
|
|
113
|
+
# Rescale known values.
|
|
94
114
|
coefsMaxMinMinReciprocal = np.reciprocal(coefsMaxMinusMin)
|
|
95
115
|
knownXValues = (knownXValues - coefsMin) * coefsMaxMinMinReciprocal # Rescale to [0 , 1]
|
|
96
116
|
|
|
97
|
-
# Establish the
|
|
117
|
+
# Establish the Jacobian of F.
|
|
98
118
|
if dF is None:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for i in range(nDep):
|
|
102
|
-
def splineDerivative(x, i=i):
|
|
103
|
-
wrt = [0] * nDep
|
|
104
|
-
wrt[i] = 1
|
|
105
|
-
return F.derivative(wrt, x)
|
|
106
|
-
dF.append(splineDerivative)
|
|
107
|
-
FDomain = F.domain().T
|
|
119
|
+
if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
|
|
120
|
+
dF = F.jacobian
|
|
108
121
|
else:
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
def fJacobian(x):
|
|
123
|
+
value = np.empty((nDep - 1, nDep), float)
|
|
124
|
+
for i in range(nDep):
|
|
111
125
|
h = epsilon * (1.0 + abs(x[i]))
|
|
112
126
|
xShift = np.array(x, copy=True)
|
|
113
127
|
xShift[i] -= h
|
|
114
128
|
fLeft = np.array(F(xShift))
|
|
115
129
|
h2 = h * 2.0
|
|
116
130
|
xShift[i] += h2
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
value[:, i] = (np.array(F(xShift)) - fLeft) / h2
|
|
132
|
+
return value
|
|
133
|
+
dF = fJacobian
|
|
134
|
+
elif not callable(dF):
|
|
121
135
|
if not(len(dF) == nDep): raise ValueError(f"Must provide {nDep} first derivatives.")
|
|
136
|
+
def fJacobian(x):
|
|
137
|
+
value = np.empty((nDep - 1, nDep), float)
|
|
138
|
+
for i in range(nDep):
|
|
139
|
+
value[:, i] = dF[i]
|
|
140
|
+
return value
|
|
141
|
+
dF = fJacobian
|
|
122
142
|
|
|
123
143
|
# Construct knots, t values, and GSamples.
|
|
124
144
|
tValues = np.empty(nUnknownCoefs, contourDtype)
|
|
125
145
|
GSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
|
|
126
|
-
t = 0.0 #
|
|
146
|
+
t = 0.0 # t ranges from 0 to 1
|
|
147
|
+
dt = 1.0 / m
|
|
127
148
|
knots = [t] * order
|
|
128
149
|
i = 0
|
|
129
150
|
previousPoint = knownXValues[0]
|
|
130
151
|
for point in knownXValues[1:]:
|
|
131
|
-
dt = np.linalg.norm(point - previousPoint)
|
|
132
|
-
if not(dt > epsilon): raise ValueError("Points must be separated by at least epsilon.")
|
|
133
152
|
for gaussNode in gaussNodes:
|
|
134
153
|
tValues[i] = t + gaussNode * dt
|
|
135
154
|
GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
|
|
@@ -138,8 +157,8 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
138
157
|
knots += [t] * (order - 2)
|
|
139
158
|
previousPoint = point
|
|
140
159
|
knots += [t] * 2 # Clamp last knot
|
|
141
|
-
knots = np.array(knots, contourDtype)
|
|
142
|
-
|
|
160
|
+
knots = np.array(knots, contourDtype)
|
|
161
|
+
knots[nCoef:] = 1.0 # Ensure last knot is exactly 1.0
|
|
143
162
|
assert i == nUnknownCoefs
|
|
144
163
|
|
|
145
164
|
# Start subdivision loop.
|
|
@@ -167,8 +186,6 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
167
186
|
# Array to hold the Jacobian of the FSamples with respect to the coefficients.
|
|
168
187
|
# The Jacobian is banded due to B-spline local support, so initialize it to zero.
|
|
169
188
|
dFCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
|
|
170
|
-
# Working array to hold the transpose of the Jacobian of F for a particular x(t).
|
|
171
|
-
dFX = np.empty((nDep, nDep - 1), contourDtype)
|
|
172
189
|
|
|
173
190
|
# Start Newton's method loop.
|
|
174
191
|
previousFSamplesNorm = 0.0
|
|
@@ -201,9 +218,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
201
218
|
FSamples[i, -1] = dotValues
|
|
202
219
|
|
|
203
220
|
# Compute the Jacobian of FSamples with respect to the coefficients of x(t).
|
|
204
|
-
|
|
205
|
-
dFX[j] = dF[j](x) * coefsMaxMinusMin[j]
|
|
206
|
-
FValues = np.outer(dFX.T, bValues).reshape(nDep - 1, nDep, order).swapaxes(1, 2)
|
|
221
|
+
FValues = np.outer(dF(x) * coefsMaxMinusMin, bValues).reshape(nDep - 1, nDep, order).swapaxes(1, 2)
|
|
207
222
|
dotValues = (np.outer(d2Values, compactCoefs.T @ dValues) + np.outer(dValues, compactCoefs.T @ d2Values)).reshape(order, nDep)
|
|
208
223
|
dFCoefs[i, :-1, ix - order:ix, :] = FValues
|
|
209
224
|
dFCoefs[i, -1, ix - order:ix, :] = dotValues
|
|
@@ -274,7 +289,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
274
289
|
previousKnot = knot
|
|
275
290
|
|
|
276
291
|
# Test if F(GSamples) is close enough to zero.
|
|
277
|
-
if FSamplesNorm
|
|
292
|
+
if FSamplesNorm < evaluationEpsilon:
|
|
278
293
|
break # We're done! Exit subdivision loop and return x(t).
|
|
279
294
|
|
|
280
295
|
# Otherwise, update nCoef and knots array, and then re-run Newton's method.
|
|
@@ -287,7 +302,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
287
302
|
# Rescale x(t) back to original data points.
|
|
288
303
|
coefs = (coefsMin + coefs * coefsMaxMinusMin).T
|
|
289
304
|
spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
|
|
290
|
-
if isinstance(F, bspy.Spline):
|
|
305
|
+
if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
|
|
291
306
|
spline = spline.confine(F.domain())
|
|
292
307
|
return spline
|
|
293
308
|
|
|
@@ -298,6 +313,122 @@ def cylinder(radius, height, tolerance = None):
|
|
|
298
313
|
top = bottom + [0.0, 0.0, height]
|
|
299
314
|
return bspy.Spline.ruled_surface(bottom, top)
|
|
300
315
|
|
|
316
|
+
def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
|
|
317
|
+
# Determine number of independent variables
|
|
318
|
+
domain = np.array(domain)
|
|
319
|
+
nInd = len(domain)
|
|
320
|
+
midPoint = f(0.5 * (domain.T[0] + domain.T[1]))
|
|
321
|
+
if not type(midPoint) is bspy.Spline:
|
|
322
|
+
nDep = len(midPoint)
|
|
323
|
+
|
|
324
|
+
# Make sure order and knots conform to this
|
|
325
|
+
if order is None:
|
|
326
|
+
order = nInd * [4]
|
|
327
|
+
if len(order) != nInd:
|
|
328
|
+
raise ValueError("Inconsistent number of independent variables")
|
|
329
|
+
|
|
330
|
+
# Establish the initial knot sequence
|
|
331
|
+
if knots is None:
|
|
332
|
+
knots = np.array([order[iInd] * [domain[iInd, 0]] + order[iInd] * [domain[iInd, 1]] for iInd in range(nInd)])
|
|
333
|
+
|
|
334
|
+
# Determine initial nCoef
|
|
335
|
+
nCoef = [len(knotVector) - iOrder for iOrder, knotVector in zip(order, knots)]
|
|
336
|
+
|
|
337
|
+
# Define function to insert midpoints
|
|
338
|
+
def addMidPoints(u):
|
|
339
|
+
newArray = np.empty([2, len(u)])
|
|
340
|
+
newArray[0] = u
|
|
341
|
+
newArray[1, :-1] = 0.5 * (u[1:] + u[:-1])
|
|
342
|
+
return newArray.T.flatten()[:-1]
|
|
343
|
+
|
|
344
|
+
# Track the current spline space we're fitting in
|
|
345
|
+
currentSpace = bspy.Spline(nInd, 0, order, nCoef, knots, [])
|
|
346
|
+
|
|
347
|
+
# Generate the Greville points for these knots
|
|
348
|
+
uvw = [currentSpace.greville(iInd) for iInd in range(nInd)]
|
|
349
|
+
|
|
350
|
+
# Enrich the sample points
|
|
351
|
+
for iInd in range(nInd):
|
|
352
|
+
uvw[iInd][0] = knots[iInd][order[iInd] - 1]
|
|
353
|
+
uvw[iInd][-1] = knots[iInd][nCoef[iInd]]
|
|
354
|
+
for iLevel in range(1):
|
|
355
|
+
uvw[iInd] = addMidPoints(uvw[iInd])
|
|
356
|
+
|
|
357
|
+
# Initialize the dictionary of function values
|
|
358
|
+
|
|
359
|
+
fDictionary = {}
|
|
360
|
+
|
|
361
|
+
# Keep looping until done
|
|
362
|
+
while True:
|
|
363
|
+
|
|
364
|
+
# Evaluate the function on this data set
|
|
365
|
+
fValues = []
|
|
366
|
+
indices = nInd * [0]
|
|
367
|
+
iLast = nInd
|
|
368
|
+
while iLast >= 0:
|
|
369
|
+
uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
|
|
370
|
+
if not uValue in fDictionary:
|
|
371
|
+
fDictionary[uValue] = f(uValue)
|
|
372
|
+
fValues.append(fDictionary[uValue])
|
|
373
|
+
iLast = nInd - 1
|
|
374
|
+
while iLast >= 0:
|
|
375
|
+
indices[iLast] += 1
|
|
376
|
+
if indices[iLast] < len(uvw[iLast]):
|
|
377
|
+
break
|
|
378
|
+
indices[iLast] = 0
|
|
379
|
+
iLast -= 1
|
|
380
|
+
|
|
381
|
+
# Adjust the ordering
|
|
382
|
+
pointShape = [len(uvw[i]) for i in range(nInd)]
|
|
383
|
+
if type(midPoint) is bspy.Spline:
|
|
384
|
+
fValues = np.array(fValues).reshape(pointShape)
|
|
385
|
+
else:
|
|
386
|
+
fValues = np.array(fValues).reshape(pointShape + [nDep]).transpose([nInd] + list(range(nInd)))
|
|
387
|
+
|
|
388
|
+
# Call the least squares fitter on this data
|
|
389
|
+
bestSoFar = bspy.Spline.least_squares(uvw, fValues, order, currentSpace.knots, fixEnds = True)
|
|
390
|
+
|
|
391
|
+
# Determine the maximum error
|
|
392
|
+
maxError = 0.0
|
|
393
|
+
for key in fDictionary:
|
|
394
|
+
if type(midPoint) is bspy.Spline:
|
|
395
|
+
sampled = bestSoFar.contract(midPoint.nInd * [None] + list(key)).coefs
|
|
396
|
+
trueCoefs = fDictionary[key].coefs
|
|
397
|
+
thisError = np.max(np.linalg.norm(sampled - trueCoefs, axis = 0))
|
|
398
|
+
else:
|
|
399
|
+
thisError = np.linalg.norm(fDictionary[key] - bestSoFar(key))
|
|
400
|
+
if thisError > maxError:
|
|
401
|
+
maxError = thisError
|
|
402
|
+
maxKey = key
|
|
403
|
+
if maxError <= tolerance:
|
|
404
|
+
break
|
|
405
|
+
|
|
406
|
+
# Split the interval and try again
|
|
407
|
+
maxGap = 0.0
|
|
408
|
+
for iInd in range(nInd):
|
|
409
|
+
insert = bspy.Spline.bspline_values(None, currentSpace.knots[iInd], order[iInd], maxKey[iInd])[0]
|
|
410
|
+
leftKnot = currentSpace.knots[iInd][insert - 1]
|
|
411
|
+
rightKnot = currentSpace.knots[iInd][insert]
|
|
412
|
+
if rightKnot - leftKnot > maxGap:
|
|
413
|
+
maxGap = rightKnot - leftKnot
|
|
414
|
+
iFirst = np.searchsorted(uvw[iInd], leftKnot, side = 'right')
|
|
415
|
+
iLast = np.searchsorted(uvw[iInd], rightKnot, side = 'right')
|
|
416
|
+
maxLeft = leftKnot
|
|
417
|
+
maxRight = rightKnot
|
|
418
|
+
maxInd = iInd
|
|
419
|
+
splitAt = 0.5 * (maxLeft + maxRight)
|
|
420
|
+
newKnots = [[] for iInd in range(nInd)]
|
|
421
|
+
newKnots[maxInd] = [splitAt]
|
|
422
|
+
currentSpace = currentSpace.insert_knots(newKnots)
|
|
423
|
+
|
|
424
|
+
# Add samples for the new knot
|
|
425
|
+
uvw[maxInd] = np.array(list(uvw[maxInd][:iFirst - 1]) +
|
|
426
|
+
list(addMidPoints(uvw[maxInd][iFirst - 1:iLast])) +
|
|
427
|
+
list(uvw[maxInd][iLast:]))
|
|
428
|
+
|
|
429
|
+
# Return the best spline found so far
|
|
430
|
+
return bestSoFar
|
|
431
|
+
|
|
301
432
|
def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
|
|
302
433
|
if bottom.nInd != 1 or right.nInd != 1 or top.nInd != 1 or left.nInd != 1:
|
|
303
434
|
raise ValueError("Input curves must have one independent variable")
|
|
@@ -414,26 +545,51 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
|
|
|
414
545
|
suvv = surface.derivative([1, 2], u[:, 0])
|
|
415
546
|
svvv = surface.derivative([0, 3], u[:, 0])
|
|
416
547
|
|
|
548
|
+
# Calculate inner products
|
|
549
|
+
su_su = su @ su
|
|
550
|
+
su_sv = su @ sv
|
|
551
|
+
sv_sv = sv @ sv
|
|
552
|
+
suu_su = suu @ su
|
|
553
|
+
suu_sv = suu @ sv
|
|
554
|
+
suv_su = suv @ su
|
|
555
|
+
suv_sv = suv @ sv
|
|
556
|
+
svv_su = svv @ su
|
|
557
|
+
svv_sv = svv @ sv
|
|
558
|
+
suu_suu = suu @ suu
|
|
559
|
+
suu_suv = suu @ suv
|
|
560
|
+
suu_svv = suu @ svv
|
|
561
|
+
suv_suv = suv @ suv
|
|
562
|
+
suv_svv = suv @ svv
|
|
563
|
+
svv_svv = svv @ svv
|
|
564
|
+
suuu_su = suuu @ su
|
|
565
|
+
suuu_sv = suuu @ sv
|
|
566
|
+
suuv_su = suuv @ su
|
|
567
|
+
suuv_sv = suuv @ sv
|
|
568
|
+
suvv_su = suvv @ su
|
|
569
|
+
suvv_sv = suvv @ sv
|
|
570
|
+
svvv_su = svvv @ su
|
|
571
|
+
svvv_sv = svvv @ sv
|
|
572
|
+
|
|
417
573
|
# Calculate the first fundamental form and derivatives
|
|
418
|
-
E =
|
|
419
|
-
E_u = 2.0 *
|
|
420
|
-
E_v = 2.0 *
|
|
421
|
-
F =
|
|
422
|
-
F_u =
|
|
423
|
-
F_v =
|
|
424
|
-
G =
|
|
425
|
-
G_u = 2.0 *
|
|
426
|
-
G_v = 2.0 *
|
|
574
|
+
E = su_su
|
|
575
|
+
E_u = 2.0 * suu_su
|
|
576
|
+
E_v = 2.0 * suv_su
|
|
577
|
+
F = su_sv
|
|
578
|
+
F_u = suu_sv + suv_su
|
|
579
|
+
F_v = suv_sv + svv_su
|
|
580
|
+
G = sv_sv
|
|
581
|
+
G_u = 2.0 * suv_sv
|
|
582
|
+
G_v = 2.0 * svv_sv
|
|
427
583
|
A = np.array([[E, F], [F, G]])
|
|
428
584
|
A_u = np.array([[E_u, F_u], [F_u, G_u]])
|
|
429
585
|
A_v = np.array([[E_v, F_v], [F_v, G_v]])
|
|
430
586
|
|
|
431
587
|
# Compute right hand side entries
|
|
432
|
-
R = np.array([[
|
|
433
|
-
R_u = np.array([[
|
|
434
|
-
[
|
|
435
|
-
R_v = np.array([[
|
|
436
|
-
[
|
|
588
|
+
R = np.array([[suu_su, suv_su, svv_su], [suu_sv, suv_sv, svv_sv]])
|
|
589
|
+
R_u = np.array([[suuu_su + suu_suu, suuv_su + suu_suv, suvv_su + suu_svv],
|
|
590
|
+
[suuu_sv + suu_suv, suuv_sv + suv_suv, suvv_sv + suv_svv]])
|
|
591
|
+
R_v = np.array([[suuv_su + suu_suv, suvv_su + suv_suv, svvv_su + suv_svv],
|
|
592
|
+
[suuv_sv + suu_svv, suvv_sv + suv_svv, svvv_sv + svv_svv]])
|
|
437
593
|
|
|
438
594
|
# Solve for the Christoffel symbols
|
|
439
595
|
luAndPivot = sp.linalg.lu_factor(A)
|