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/_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
|
|
@@ -23,7 +33,7 @@ def cone(radius1, radius2, height, tolerance = None):
|
|
|
23
33
|
return bspy.Spline.ruled_surface(bottom, top)
|
|
24
34
|
|
|
25
35
|
# Courtesy of Michael Epton - Translated from his F77 code lgnzro
|
|
26
|
-
def _legendre_polynomial_zeros(degree):
|
|
36
|
+
def _legendre_polynomial_zeros(degree, mapToZeroOne = True):
|
|
27
37
|
def legendre(degree, x):
|
|
28
38
|
p = [1.0, x]
|
|
29
39
|
pd = [0.0, 1.0]
|
|
@@ -35,6 +45,7 @@ def _legendre_polynomial_zeros(degree):
|
|
|
35
45
|
return p, pd
|
|
36
46
|
zval = 1.0
|
|
37
47
|
z = []
|
|
48
|
+
zNegative = []
|
|
38
49
|
for iRoot in range(degree // 2):
|
|
39
50
|
done = False
|
|
40
51
|
while True:
|
|
@@ -49,21 +60,28 @@ def _legendre_polynomial_zeros(degree):
|
|
|
49
60
|
if dz < 1.0e-10:
|
|
50
61
|
done = True
|
|
51
62
|
z.append(zval)
|
|
63
|
+
zNegative.append(-zval)
|
|
52
64
|
zval -= 0.001
|
|
53
65
|
if degree % 2 == 1:
|
|
54
|
-
|
|
66
|
+
zNegative.append(0.0)
|
|
55
67
|
z.reverse()
|
|
68
|
+
z = np.array(zNegative + z)
|
|
56
69
|
w = []
|
|
57
70
|
for zval in z:
|
|
58
71
|
p, pd = legendre(degree, zval)
|
|
59
72
|
w.append(2.0 / ((1.0 - zval ** 2) * pd[-1] ** 2))
|
|
73
|
+
w = np.array(w)
|
|
74
|
+
if mapToZeroOne:
|
|
75
|
+
z = 0.5 * (1.0 + z)
|
|
76
|
+
w = 0.5 * w
|
|
60
77
|
return z, w
|
|
61
78
|
|
|
62
79
|
def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
63
80
|
# Set up parameters for initial guess of x(t) and validate arguments.
|
|
64
81
|
order = 4
|
|
65
82
|
degree = order - 1
|
|
66
|
-
|
|
83
|
+
gaussNodes, gaussWeights = _legendre_polynomial_zeros(degree - 1)
|
|
84
|
+
|
|
67
85
|
if not(len(knownXValues) >= 2): raise ValueError("There must be at least 2 known x values.")
|
|
68
86
|
m = len(knownXValues) - 1
|
|
69
87
|
nCoef = m * (degree - 1) + 2
|
|
@@ -80,60 +98,67 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
80
98
|
FValues = F(knownXValue)
|
|
81
99
|
if not(len(FValues) == nDep - 1 and np.linalg.norm(FValues) < evaluationEpsilon):
|
|
82
100
|
raise ValueError(f"F(known x) must be a zero vector of length {nDep - 1}.")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
86
114
|
coefsMaxMinMinReciprocal = np.reciprocal(coefsMaxMinusMin)
|
|
87
115
|
knownXValues = (knownXValues - coefsMin) * coefsMaxMinMinReciprocal # Rescale to [0 , 1]
|
|
88
116
|
|
|
89
|
-
# Establish the
|
|
117
|
+
# Establish the Jacobian of F.
|
|
90
118
|
if dF is None:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
for i in range(nDep):
|
|
94
|
-
def splineDerivative(x, i=i):
|
|
95
|
-
wrt = [0] * nDep
|
|
96
|
-
wrt[i] = 1
|
|
97
|
-
return F.derivative(wrt, x)
|
|
98
|
-
dF.append(splineDerivative)
|
|
119
|
+
if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
|
|
120
|
+
dF = F.jacobian
|
|
99
121
|
else:
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
def fJacobian(x):
|
|
123
|
+
value = np.empty((nDep - 1, nDep), float)
|
|
124
|
+
for i in range(nDep):
|
|
102
125
|
h = epsilon * (1.0 + abs(x[i]))
|
|
103
126
|
xShift = np.array(x, copy=True)
|
|
104
127
|
xShift[i] -= h
|
|
105
128
|
fLeft = np.array(F(xShift))
|
|
106
129
|
h2 = h * 2.0
|
|
107
130
|
xShift[i] += h2
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
value[:, i] = (np.array(F(xShift)) - fLeft) / h2
|
|
132
|
+
return value
|
|
133
|
+
dF = fJacobian
|
|
134
|
+
elif not callable(dF):
|
|
111
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
|
|
112
142
|
|
|
113
143
|
# Construct knots, t values, and GSamples.
|
|
114
144
|
tValues = np.empty(nUnknownCoefs, contourDtype)
|
|
115
145
|
GSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
|
|
116
|
-
t = 0.0 #
|
|
146
|
+
t = 0.0 # t ranges from 0 to 1
|
|
147
|
+
dt = 1.0 / m
|
|
117
148
|
knots = [t] * order
|
|
118
149
|
i = 0
|
|
119
150
|
previousPoint = knownXValues[0]
|
|
120
151
|
for point in knownXValues[1:]:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
tValues[i] = t + 0.5 * dt * (1.0 - rho)
|
|
125
|
-
GSamples[i] = 0.5 * (previousPoint + point - rho * (point - previousPoint))
|
|
126
|
-
i += 1
|
|
127
|
-
for rho in rhos[0 if degree % 2 == 1 else 1:]:
|
|
128
|
-
tValues[i] = t + 0.5 * dt * (1.0 + rho)
|
|
129
|
-
GSamples[i] = 0.5 * (previousPoint + point + rho * (point - previousPoint))
|
|
152
|
+
for gaussNode in gaussNodes:
|
|
153
|
+
tValues[i] = t + gaussNode * dt
|
|
154
|
+
GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
|
|
130
155
|
i += 1
|
|
131
156
|
t += dt
|
|
132
157
|
knots += [t] * (order - 2)
|
|
133
158
|
previousPoint = point
|
|
134
159
|
knots += [t] * 2 # Clamp last knot
|
|
135
|
-
knots = np.array(knots, contourDtype)
|
|
136
|
-
|
|
160
|
+
knots = np.array(knots, contourDtype)
|
|
161
|
+
knots[nCoef:] = 1.0 # Ensure last knot is exactly 1.0
|
|
137
162
|
assert i == nUnknownCoefs
|
|
138
163
|
|
|
139
164
|
# Start subdivision loop.
|
|
@@ -161,8 +186,6 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
161
186
|
# Array to hold the Jacobian of the FSamples with respect to the coefficients.
|
|
162
187
|
# The Jacobian is banded due to B-spline local support, so initialize it to zero.
|
|
163
188
|
dFCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
|
|
164
|
-
# Working array to hold the transpose of the Jacobian of F for a particular x(t).
|
|
165
|
-
dFX = np.empty((nDep, nDep - 1), contourDtype)
|
|
166
189
|
|
|
167
190
|
# Start Newton's method loop.
|
|
168
191
|
previousFSamplesNorm = 0.0
|
|
@@ -184,6 +207,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
184
207
|
|
|
185
208
|
# Do the same for F(x(t)).
|
|
186
209
|
x = coefsMin + (compactCoefs.T @ bValues) * coefsMaxMinusMin
|
|
210
|
+
x = np.maximum(FDomain[0], np.minimum(FDomain[1], x))
|
|
187
211
|
FValues = F(x)
|
|
188
212
|
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(FValues, np.inf))
|
|
189
213
|
if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
|
|
@@ -194,9 +218,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
194
218
|
FSamples[i, -1] = dotValues
|
|
195
219
|
|
|
196
220
|
# Compute the Jacobian of FSamples with respect to the coefficients of x(t).
|
|
197
|
-
|
|
198
|
-
dFX[j] = dF[j](x) * coefsMaxMinusMin[j]
|
|
199
|
-
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)
|
|
200
222
|
dotValues = (np.outer(d2Values, compactCoefs.T @ dValues) + np.outer(dValues, compactCoefs.T @ d2Values)).reshape(order, nDep)
|
|
201
223
|
dFCoefs[i, :-1, ix - order:ix, :] = FValues
|
|
202
224
|
dFCoefs[i, -1, ix - order:ix, :] = dotValues
|
|
@@ -253,33 +275,21 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
253
275
|
newKnot = 0.5 * (previousKnot + knot)
|
|
254
276
|
|
|
255
277
|
# Place tValues at Gauss points for the intervals [previousKnot, newKnot] and [newKnot, knot].
|
|
256
|
-
for
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
|
|
265
|
-
i += 1
|
|
266
|
-
for rho in reversed(rhos):
|
|
267
|
-
tValues[i] = t = 0.5 * (newKnot + knot - rho * (knot - newKnot))
|
|
268
|
-
GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
|
|
269
|
-
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
|
|
270
|
-
i += 1
|
|
271
|
-
for rho in rhos[0 if degree % 2 == 1 else 1:]:
|
|
272
|
-
tValues[i] = t = 0.5 * (newKnot + knot + rho * (knot - newKnot))
|
|
273
|
-
GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
|
|
274
|
-
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
|
|
275
|
-
i += 1
|
|
278
|
+
for knotInterval in [[previousKnot, newKnot], [newKnot, knot]]:
|
|
279
|
+
for gaussNode in gaussNodes:
|
|
280
|
+
tValues[i] = t = (1.0 - gaussNode) * knotInterval[0] + gaussNode * knotInterval[1]
|
|
281
|
+
x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
|
|
282
|
+
x = np.array([max(0.0, min(1.0, xi)) for xi in x])
|
|
283
|
+
GSamples[i] = x
|
|
284
|
+
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
|
|
285
|
+
i += 1
|
|
276
286
|
|
|
277
287
|
newKnots += [newKnot] * (order - 2) # C1 continuity
|
|
278
288
|
newKnots += [knot] * (order - 2) # C1 continuity
|
|
279
289
|
previousKnot = knot
|
|
280
290
|
|
|
281
291
|
# Test if F(GSamples) is close enough to zero.
|
|
282
|
-
if FSamplesNorm
|
|
292
|
+
if FSamplesNorm < evaluationEpsilon:
|
|
283
293
|
break # We're done! Exit subdivision loop and return x(t).
|
|
284
294
|
|
|
285
295
|
# Otherwise, update nCoef and knots array, and then re-run Newton's method.
|
|
@@ -292,7 +302,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
292
302
|
# Rescale x(t) back to original data points.
|
|
293
303
|
coefs = (coefsMin + coefs * coefsMaxMinusMin).T
|
|
294
304
|
spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
|
|
295
|
-
if isinstance(F, bspy.Spline):
|
|
305
|
+
if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
|
|
296
306
|
spline = spline.confine(F.domain())
|
|
297
307
|
return spline
|
|
298
308
|
|
|
@@ -303,6 +313,122 @@ def cylinder(radius, height, tolerance = None):
|
|
|
303
313
|
top = bottom + [0.0, 0.0, height]
|
|
304
314
|
return bspy.Spline.ruled_surface(bottom, top)
|
|
305
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
|
+
|
|
306
432
|
def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
|
|
307
433
|
if bottom.nInd != 1 or right.nInd != 1 or top.nInd != 1 or left.nInd != 1:
|
|
308
434
|
raise ValueError("Input curves must have one independent variable")
|
|
@@ -408,68 +534,72 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
|
|
|
408
534
|
# Define the callback function for the ODE solver
|
|
409
535
|
def geodesicCallback(t, u, surface, uvDomain):
|
|
410
536
|
# Evaluate the surface information needed for the Christoffel symbols
|
|
411
|
-
u[
|
|
412
|
-
u[1, 0] = max(uvDomain[1, 0], min(uvDomain[1, 1], u[1, 0]))
|
|
537
|
+
u[:, 0] = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
|
|
413
538
|
su = surface.derivative([1, 0], u[:, 0])
|
|
414
539
|
sv = surface.derivative([0, 1], u[:, 0])
|
|
415
540
|
suu = surface.derivative([2, 0], u[:, 0])
|
|
416
541
|
suv = surface.derivative([1, 1], u[:, 0])
|
|
417
542
|
svv = surface.derivative([0, 2], u[:, 0])
|
|
543
|
+
suuu = surface.derivative([3, 0], u[:, 0])
|
|
544
|
+
suuv = surface.derivative([2, 1], u[:, 0])
|
|
545
|
+
suvv = surface.derivative([1, 2], u[:, 0])
|
|
546
|
+
svvv = surface.derivative([0, 3], u[:, 0])
|
|
418
547
|
|
|
419
|
-
# Calculate
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
|
|
573
|
+
# Calculate the first fundamental form and derivatives
|
|
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
|
|
423
583
|
A = np.array([[E, F], [F, G]])
|
|
584
|
+
A_u = np.array([[E_u, F_u], [F_u, G_u]])
|
|
585
|
+
A_v = np.array([[E_v, F_v], [F_v, G_v]])
|
|
424
586
|
|
|
425
587
|
# Compute right hand side entries
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
R_vvv = svv @ sv
|
|
432
|
-
R = np.array([[R_uuu, R_uvu, R_vvu], [R_uuv, R_uvv, R_vvv]])
|
|
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]])
|
|
433
593
|
|
|
434
594
|
# Solve for the Christoffel symbols
|
|
435
595
|
luAndPivot = sp.linalg.lu_factor(A)
|
|
436
596
|
Gamma = sp.linalg.lu_solve(luAndPivot, R)
|
|
597
|
+
Gamma_u = sp.linalg.lu_solve(luAndPivot, R_u - A_u @ Gamma)
|
|
598
|
+
Gamma_v = sp.linalg.lu_solve(luAndPivot, R_v - A_v @ Gamma)
|
|
437
599
|
|
|
438
600
|
# Compute the right hand side for the ODE
|
|
439
601
|
rhs = -np.array([Gamma[0, 0] * u[0, 1] ** 2 + 2.0 * Gamma[0, 1] * u[0, 1] * u[1, 1] + Gamma[0, 2] * u[1, 1] ** 2,
|
|
440
602
|
Gamma[1, 0] * u[0, 1] ** 2 + 2.0 * Gamma[1, 1] * u[0, 1] * u[1, 1] + Gamma[1, 2] * u[1, 1] ** 2])
|
|
441
|
-
|
|
442
|
-
# Now we need the derivatives of the Christoffel symbols with respect to u
|
|
443
|
-
suuu = surface.derivative([3, 0], u[:, 0])
|
|
444
|
-
suuv = surface.derivative([2, 1], u[:, 0])
|
|
445
|
-
suvv = surface.derivative([1, 2], u[:, 0])
|
|
446
|
-
svvv = surface.derivative([0, 3], u[:, 0])
|
|
447
|
-
E_u = 2.0 * R_uuu
|
|
448
|
-
F_u = R_uuv + R_uvu
|
|
449
|
-
G_u = 2.0 * R_uvv
|
|
450
|
-
A_u = np.array([[E_u, F_u], [F_u, G_u]])
|
|
451
|
-
R_uuu_u = suuu @ su + suu @ suu
|
|
452
|
-
R_uuv_u = suuu @ sv + suu @ suv
|
|
453
|
-
R_uvu_u = suuv @ su + suv @ suu
|
|
454
|
-
R_uvv_u = suuv @ sv + suv @ suv
|
|
455
|
-
R_vvu_u = suvv @ su + svv @ suu
|
|
456
|
-
R_vvv_u = suvv @ sv + svv @ suv
|
|
457
|
-
R_u = np.array([[R_uuu_u, R_uvu_u, R_vvu_u], [R_uuv_u, R_uvv_u, R_vvv_u]])
|
|
458
|
-
Gamma_u = sp.linalg.lu_solve(luAndPivot, R_u - A_u @ Gamma)
|
|
459
|
-
|
|
460
|
-
# . . . And the derivatives of the Christoffel symbols with respect to v
|
|
461
|
-
E_v = 2.0 * R_uvu
|
|
462
|
-
F_v = R_uvv + R_vvu
|
|
463
|
-
G_v = 2.0 * R_vvv
|
|
464
|
-
A_v = np.array([[E_v, F_v], [F_v, G_v]])
|
|
465
|
-
R_uuu_v = suuv @ su + suu @ suv
|
|
466
|
-
R_uuv_v = suuv @ sv + suu @ svv
|
|
467
|
-
R_uvu_v = suvv @ su + suv @ suv
|
|
468
|
-
R_uvv_v = suvv @ sv + suv @ svv
|
|
469
|
-
R_vvu_v = svvv @ su + svv @ suv
|
|
470
|
-
R_vvv_v = svvv @ sv + svv @ svv
|
|
471
|
-
R_v = np.array([[R_uuu_v, R_uvu_v, R_vvu_v], [R_uuv_v, R_uvv_v, R_vvv_v]])
|
|
472
|
-
Gamma_v = sp.linalg.lu_solve(luAndPivot, R_v - A_v @ Gamma)
|
|
473
603
|
|
|
474
604
|
# Compute the Jacobian matrix of the right hand side of the ODE
|
|
475
605
|
jacobian = -np.array([[[Gamma_u[0, 0] * u[0, 1] ** 2 + 2.0 * Gamma_u[0, 1] * u[0, 1] * u[1, 1] + Gamma_u[0, 2] * u[1, 1] ** 2,
|
|
@@ -707,7 +837,10 @@ def section(xytk):
|
|
|
707
837
|
onePlusCosTheta = 1.0 + math.cos(theta)
|
|
708
838
|
r0 = 4.0 * startKappa * tangentDistances[0] ** 2 / (3.0 * tangentDistances[1] * crossTangents)
|
|
709
839
|
r1 = 4.0 * endKappa * tangentDistances[1] ** 2 / (3.0 * tangentDistances[0] * crossTangents)
|
|
710
|
-
|
|
840
|
+
if r0 != 0.0 or r1 != 0.0:
|
|
841
|
+
rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
|
|
842
|
+
else:
|
|
843
|
+
rhoCrit = 1.0
|
|
711
844
|
rhoCritOfTheta = 3.0 * (math.sqrt(1.0 + 32.0 / (3.0 * onePlusCosTheta)) - 1.0) * onePlusCosTheta / 16.0
|
|
712
845
|
|
|
713
846
|
# Determine quadratic polynomial
|
|
@@ -797,11 +930,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
797
930
|
for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
|
|
798
931
|
if knot0 < knot1:
|
|
799
932
|
for gaussNode in gaussNodes:
|
|
800
|
-
|
|
801
|
-
collocationPoints.append((1.0 - alpha) * knot0 + alpha * knot1)
|
|
802
|
-
if abs(gaussNode) > 1.0e-5:
|
|
803
|
-
collocationPoints.append(alpha * knot0 + (1.0 - alpha) * knot1)
|
|
804
|
-
collocationPoints = np.sort(collocationPoints)
|
|
933
|
+
collocationPoints.append((1.0 - gaussNode) * knot0 + gaussNode * knot1)
|
|
805
934
|
n = nDep * currentGuess.nCoef[0]
|
|
806
935
|
bestGuess = np.reshape(currentGuess.coefs.T, (n,))
|
|
807
936
|
|
|
@@ -826,7 +955,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
826
955
|
done = linear
|
|
827
956
|
continuation = 1.0
|
|
828
957
|
bestContinuation = 0.0
|
|
829
|
-
|
|
958
|
+
inCaseOfEmergency = bestGuess.copy()
|
|
830
959
|
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
831
960
|
iteration = 0
|
|
832
961
|
|
|
@@ -869,9 +998,10 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
869
998
|
update = sp.linalg.solve_banded((bandWidth, bandWidth), collocationMatrix, residuals)
|
|
870
999
|
bestGuess[nDep * (iFirstPoint + nLeft) : nDep * (iNextPoint + nLeft)] += update[nDep * nLeft : nDep * (iNextPoint - iFirstPoint + nLeft)]
|
|
871
1000
|
updateSize = np.linalg.norm(update)
|
|
872
|
-
if updateSize > 1.25 * previous and iteration >= 4
|
|
1001
|
+
if updateSize > 1.25 * previous and iteration >= 4 or \
|
|
1002
|
+
updateSize > 0.01 and iteration > 50:
|
|
873
1003
|
continuation = 0.5 * (continuation + bestContinuation)
|
|
874
|
-
bestGuess =
|
|
1004
|
+
bestGuess = inCaseOfEmergency.copy()
|
|
875
1005
|
if continuation - bestContinuation < 0.01:
|
|
876
1006
|
break
|
|
877
1007
|
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
@@ -886,7 +1016,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
886
1016
|
if updateSize < math.sqrt(n) * scale * math.sqrt(np.finfo(update.dtype).eps):
|
|
887
1017
|
if continuation < 1.0:
|
|
888
1018
|
bestContinuation = continuation
|
|
889
|
-
|
|
1019
|
+
inCaseOfEmergency = bestGuess.copy()
|
|
890
1020
|
continuation = min(1.0, 1.2 * continuation)
|
|
891
1021
|
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
892
1022
|
iteration = 0
|