bspy 3.0.1__py3-none-any.whl → 4.1__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 -9
- bspy/_spline_domain.py +83 -47
- bspy/_spline_evaluation.py +44 -62
- bspy/_spline_fitting.py +353 -75
- bspy/_spline_intersection.py +332 -59
- bspy/_spline_operations.py +33 -38
- bspy/hyperplane.py +540 -0
- bspy/manifold.py +391 -0
- bspy/solid.py +839 -0
- bspy/spline.py +310 -77
- bspy/splineOpenGLFrame.py +683 -19
- bspy/viewer.py +795 -0
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/METADATA +25 -13
- bspy-4.1.dist-info/RECORD +17 -0
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/WHEEL +1 -1
- bspy/bspyApp.py +0 -426
- bspy/drawableSpline.py +0 -585
- bspy-3.0.1.dist-info/RECORD +0 -15
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/LICENSE +0 -0
- {bspy-3.0.1.dist-info → bspy-4.1.dist-info}/top_level.txt +0 -0
bspy/_spline_fitting.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
2
3
|
import bspy.spline
|
|
3
4
|
import math
|
|
4
5
|
|
|
@@ -22,7 +23,7 @@ def cone(radius1, radius2, height, tolerance = None):
|
|
|
22
23
|
return bspy.Spline.ruled_surface(bottom, top)
|
|
23
24
|
|
|
24
25
|
# Courtesy of Michael Epton - Translated from his F77 code lgnzro
|
|
25
|
-
def _legendre_polynomial_zeros(degree):
|
|
26
|
+
def _legendre_polynomial_zeros(degree, mapToZeroOne = True):
|
|
26
27
|
def legendre(degree, x):
|
|
27
28
|
p = [1.0, x]
|
|
28
29
|
pd = [0.0, 1.0]
|
|
@@ -34,6 +35,7 @@ def _legendre_polynomial_zeros(degree):
|
|
|
34
35
|
return p, pd
|
|
35
36
|
zval = 1.0
|
|
36
37
|
z = []
|
|
38
|
+
zNegative = []
|
|
37
39
|
for iRoot in range(degree // 2):
|
|
38
40
|
done = False
|
|
39
41
|
while True:
|
|
@@ -48,21 +50,28 @@ def _legendre_polynomial_zeros(degree):
|
|
|
48
50
|
if dz < 1.0e-10:
|
|
49
51
|
done = True
|
|
50
52
|
z.append(zval)
|
|
53
|
+
zNegative.append(-zval)
|
|
51
54
|
zval -= 0.001
|
|
52
55
|
if degree % 2 == 1:
|
|
53
|
-
|
|
56
|
+
zNegative.append(0.0)
|
|
54
57
|
z.reverse()
|
|
58
|
+
z = np.array(zNegative + z)
|
|
55
59
|
w = []
|
|
56
60
|
for zval in z:
|
|
57
61
|
p, pd = legendre(degree, zval)
|
|
58
62
|
w.append(2.0 / ((1.0 - zval ** 2) * pd[-1] ** 2))
|
|
63
|
+
w = np.array(w)
|
|
64
|
+
if mapToZeroOne:
|
|
65
|
+
z = 0.5 * (1.0 + z)
|
|
66
|
+
w = 0.5 * w
|
|
59
67
|
return z, w
|
|
60
68
|
|
|
61
69
|
def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
62
70
|
# Set up parameters for initial guess of x(t) and validate arguments.
|
|
63
71
|
order = 4
|
|
64
72
|
degree = order - 1
|
|
65
|
-
|
|
73
|
+
gaussNodes, gaussWeights = _legendre_polynomial_zeros(degree - 1)
|
|
74
|
+
|
|
66
75
|
if not(len(knownXValues) >= 2): raise ValueError("There must be at least 2 known x values.")
|
|
67
76
|
m = len(knownXValues) - 1
|
|
68
77
|
nCoef = m * (degree - 1) + 2
|
|
@@ -95,6 +104,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
95
104
|
wrt[i] = 1
|
|
96
105
|
return F.derivative(wrt, x)
|
|
97
106
|
dF.append(splineDerivative)
|
|
107
|
+
FDomain = F.domain().T
|
|
98
108
|
else:
|
|
99
109
|
for i in range(nDep):
|
|
100
110
|
def fDerivative(x, i=i):
|
|
@@ -106,6 +116,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
106
116
|
xShift[i] += h2
|
|
107
117
|
return (np.array(F(xShift)) - fLeft) / h2
|
|
108
118
|
dF.append(fDerivative)
|
|
119
|
+
FDomain = np.array(nDep * [[-np.inf, np.inf]]).T
|
|
109
120
|
else:
|
|
110
121
|
if not(len(dF) == nDep): raise ValueError(f"Must provide {nDep} first derivatives.")
|
|
111
122
|
|
|
@@ -119,13 +130,9 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
119
130
|
for point in knownXValues[1:]:
|
|
120
131
|
dt = np.linalg.norm(point - previousPoint)
|
|
121
132
|
if not(dt > epsilon): raise ValueError("Points must be separated by at least epsilon.")
|
|
122
|
-
for
|
|
123
|
-
tValues[i] = t +
|
|
124
|
-
GSamples[i] = 0
|
|
125
|
-
i += 1
|
|
126
|
-
for rho in rhos[0 if degree % 2 == 1 else 1:]:
|
|
127
|
-
tValues[i] = t + 0.5 * dt * (1.0 + rho)
|
|
128
|
-
GSamples[i] = 0.5 * (previousPoint + point + rho * (point - previousPoint))
|
|
133
|
+
for gaussNode in gaussNodes:
|
|
134
|
+
tValues[i] = t + gaussNode * dt
|
|
135
|
+
GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
|
|
129
136
|
i += 1
|
|
130
137
|
t += dt
|
|
131
138
|
knots += [t] * (order - 2)
|
|
@@ -141,27 +148,25 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
141
148
|
# where dGCoefs and GSamples are the B-spline values and sample points, respectively, for x(t).
|
|
142
149
|
# The dGCoefs matrix is banded due to B-spline local support, so initialize it to zero.
|
|
143
150
|
# Solving for coefs provides us our initial coefficients of x(t).
|
|
144
|
-
dGCoefs = np.zeros((nUnknownCoefs, nDep,
|
|
151
|
+
dGCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
|
|
145
152
|
i = 0
|
|
146
|
-
for
|
|
147
|
-
ix =
|
|
148
|
-
ix = min(ix, nCoef)
|
|
149
|
-
bValues = bspy.Spline.bspline_values(ix, knots, order, t)
|
|
153
|
+
for i, t in enumerate(tValues):
|
|
154
|
+
ix, bValues = bspy.Spline.bspline_values(None, knots, order, t)
|
|
150
155
|
for j in range(nDep):
|
|
151
|
-
dGCoefs[i, j,
|
|
152
|
-
GSamples -= dGCoefs[:, :,
|
|
156
|
+
dGCoefs[i, j, ix - order:ix, j] = bValues
|
|
157
|
+
GSamples -= dGCoefs[:, :, 0, :] @ knownXValues[0] + dGCoefs[:, :, -1, :] @ knownXValues[-1]
|
|
153
158
|
GSamples = GSamples.reshape(nUnknownCoefs * nDep)
|
|
154
|
-
dGCoefs = dGCoefs[:, :,
|
|
155
|
-
coefs = np.empty((
|
|
156
|
-
coefs[
|
|
157
|
-
coefs[
|
|
158
|
-
coefs[
|
|
159
|
+
dGCoefs = dGCoefs[:, :, 1:-1, :].reshape(nUnknownCoefs * nDep, nUnknownCoefs * nDep)
|
|
160
|
+
coefs = np.empty((nCoef, nDep), contourDtype)
|
|
161
|
+
coefs[0, :] = knownXValues[0]
|
|
162
|
+
coefs[-1, :] = knownXValues[-1]
|
|
163
|
+
coefs[1:-1, :] = np.linalg.solve(dGCoefs, GSamples).reshape(nUnknownCoefs, nDep)
|
|
159
164
|
|
|
160
165
|
# Array to hold the values of F and contour dot for each t, excluding endpoints.
|
|
161
166
|
FSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
|
|
162
167
|
# Array to hold the Jacobian of the FSamples with respect to the coefficients.
|
|
163
168
|
# The Jacobian is banded due to B-spline local support, so initialize it to zero.
|
|
164
|
-
dFCoefs = np.zeros((nUnknownCoefs, nDep,
|
|
169
|
+
dFCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
|
|
165
170
|
# Working array to hold the transpose of the Jacobian of F for a particular x(t).
|
|
166
171
|
dFX = np.empty((nDep, nDep - 1), contourDtype)
|
|
167
172
|
|
|
@@ -170,26 +175,24 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
170
175
|
while True:
|
|
171
176
|
FSamplesNorm = 0.0
|
|
172
177
|
# Fill in FSamples and its Jacobian (dFCoefs) with respect to the coefficients of x(t).
|
|
173
|
-
for
|
|
178
|
+
for i, t in enumerate(tValues):
|
|
174
179
|
# Isolate coefficients and compute bspline values and their first two derivatives at t.
|
|
175
|
-
ix =
|
|
176
|
-
ix =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
dValues = bspy.Spline.bspline_values(ix, knots, order, t, 1)
|
|
180
|
-
d2Values = bspy.Spline.bspline_values(ix, knots, order, t, 2)
|
|
180
|
+
ix, bValues = bspy.Spline.bspline_values(None, knots, order, t)
|
|
181
|
+
ix, dValues = bspy.Spline.bspline_values(ix, knots, order, t, 1)
|
|
182
|
+
ix, d2Values = bspy.Spline.bspline_values(ix, knots, order, t, 2)
|
|
183
|
+
compactCoefs = coefs[ix - order:ix, :]
|
|
181
184
|
|
|
182
185
|
# Compute the dot constraint for x(t) and check for divergence from solution.
|
|
183
|
-
dotValues = np.dot(compactCoefs @ d2Values, compactCoefs @ dValues)
|
|
186
|
+
dotValues = np.dot(compactCoefs.T @ d2Values, compactCoefs.T @ dValues)
|
|
184
187
|
FSamplesNorm = max(FSamplesNorm, abs(dotValues))
|
|
185
188
|
if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
|
|
186
189
|
break
|
|
187
190
|
|
|
188
191
|
# Do the same for F(x(t)).
|
|
189
|
-
x = coefsMin + (compactCoefs @ bValues) * coefsMaxMinusMin
|
|
192
|
+
x = coefsMin + (compactCoefs.T @ bValues) * coefsMaxMinusMin
|
|
193
|
+
x = np.maximum(FDomain[0], np.minimum(FDomain[1], x))
|
|
190
194
|
FValues = F(x)
|
|
191
|
-
|
|
192
|
-
FSamplesNorm = max(FSamplesNorm, abs(FValue))
|
|
195
|
+
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(FValues, np.inf))
|
|
193
196
|
if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
|
|
194
197
|
break
|
|
195
198
|
|
|
@@ -200,16 +203,16 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
200
203
|
# Compute the Jacobian of FSamples with respect to the coefficients of x(t).
|
|
201
204
|
for j in range(nDep):
|
|
202
205
|
dFX[j] = dF[j](x) * coefsMaxMinusMin[j]
|
|
203
|
-
FValues = np.outer(dFX.T, bValues).reshape(nDep - 1, nDep, order)
|
|
204
|
-
dotValues = (np.outer(compactCoefs @ dValues
|
|
205
|
-
dFCoefs[i, :-1,
|
|
206
|
-
dFCoefs[i, -1,
|
|
206
|
+
FValues = np.outer(dFX.T, bValues).reshape(nDep - 1, nDep, order).swapaxes(1, 2)
|
|
207
|
+
dotValues = (np.outer(d2Values, compactCoefs.T @ dValues) + np.outer(dValues, compactCoefs.T @ d2Values)).reshape(order, nDep)
|
|
208
|
+
dFCoefs[i, :-1, ix - order:ix, :] = FValues
|
|
209
|
+
dFCoefs[i, -1, ix - order:ix, :] = dotValues
|
|
207
210
|
|
|
208
211
|
# Check if we got closer to the solution.
|
|
209
212
|
if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
|
|
210
213
|
# No we didn't, take a dampened step.
|
|
211
214
|
coefDelta *= 0.5
|
|
212
|
-
coefs[
|
|
215
|
+
coefs[1:-1, :] += coefDelta # Don't update endpoints
|
|
213
216
|
else:
|
|
214
217
|
# Yes we did, rescale FSamples and its Jacobian.
|
|
215
218
|
if FSamplesNorm >= evaluationEpsilon:
|
|
@@ -217,10 +220,16 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
217
220
|
dFCoefs /= FSamplesNorm
|
|
218
221
|
|
|
219
222
|
# Perform a Newton iteration.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
n = nUnknownCoefs * nDep
|
|
224
|
+
HSamples = FSamples.reshape(n)
|
|
225
|
+
dHCoefs = dFCoefs[:, :, 1:-1, :].reshape((n, n))
|
|
226
|
+
bandWidth = order * nDep - 1
|
|
227
|
+
banded = np.zeros((2 * bandWidth + 1, n))
|
|
228
|
+
for iDiagonal in range(min(bandWidth + 1, n)):
|
|
229
|
+
banded[bandWidth - iDiagonal, iDiagonal : n]= np.diagonal(dHCoefs, iDiagonal)
|
|
230
|
+
banded[bandWidth + iDiagonal, : n - iDiagonal] = np.diagonal(dHCoefs, -iDiagonal)
|
|
231
|
+
coefDelta = sp.linalg.solve_banded((bandWidth, bandWidth), banded, HSamples).reshape(nUnknownCoefs, nDep)
|
|
232
|
+
coefs[1:-1, :] -= coefDelta # Don't update endpoints
|
|
224
233
|
|
|
225
234
|
# Record FSamples norm to ensure this Newton step is productive.
|
|
226
235
|
previousFSamplesNorm = FSamplesNorm
|
|
@@ -229,59 +238,54 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
229
238
|
if np.linalg.norm(coefDelta) < epsilon:
|
|
230
239
|
# If step didn't improve the solution, remove it.
|
|
231
240
|
if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
|
|
232
|
-
coefs[
|
|
241
|
+
coefs[1:-1, :] += coefDelta # Don't update endpoints
|
|
233
242
|
break
|
|
234
243
|
|
|
235
|
-
# Newton steps are done. Now check if we need to subdivide.
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# We need to subdivide, so build new knots, tValues, and GSamples arrays.
|
|
241
|
-
nCoef = 2 * (nCoef - 1)
|
|
242
|
-
nUnknownCoefs = nCoef - 2
|
|
244
|
+
# Newton steps are done. Now, check if we need to subdivide.
|
|
245
|
+
# We do this by building new subdivided knots, tValues, and GSamples (x(tValues)).
|
|
246
|
+
# If F(GSamples) is close enough to zero, we're done.
|
|
247
|
+
# Otherwise, re-run Newton's method using the new knots, tValues, and GSamples.
|
|
248
|
+
nUnknownCoefs = 2 * (nCoef - 2)
|
|
243
249
|
tValues = np.empty(nUnknownCoefs, contourDtype)
|
|
244
250
|
GSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
|
|
245
251
|
previousKnot = knots[degree]
|
|
246
252
|
newKnots = [previousKnot] * order
|
|
253
|
+
FSamplesNorm = 0.0
|
|
247
254
|
i = 0
|
|
248
255
|
for ix in range(order, len(knots) - degree, order - 2):
|
|
249
256
|
knot = knots[ix]
|
|
250
|
-
compactCoefs = coefs[
|
|
257
|
+
compactCoefs = coefs[ix - order:ix, :]
|
|
251
258
|
|
|
252
259
|
# New knots are at the midpoint between old knots.
|
|
253
260
|
newKnot = 0.5 * (previousKnot + knot)
|
|
254
261
|
|
|
255
262
|
# Place tValues at Gauss points for the intervals [previousKnot, newKnot] and [newKnot, knot].
|
|
256
|
-
for
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
for rho in reversed(rhos):
|
|
265
|
-
tValues[i] = t = 0.5 * (newKnot + knot - rho * (knot - newKnot))
|
|
266
|
-
GSamples[i] = compactCoefs @ bspy.Spline.bspline_values(ix, knots, order, t)
|
|
267
|
-
i += 1
|
|
268
|
-
for rho in rhos[0 if degree % 2 == 1 else 1:]:
|
|
269
|
-
tValues[i] = t = 0.5 * (newKnot + knot + rho * (knot - newKnot))
|
|
270
|
-
GSamples[i] = compactCoefs @ bspy.Spline.bspline_values(ix, knots, order, t)
|
|
271
|
-
i += 1
|
|
263
|
+
for knotInterval in [[previousKnot, newKnot], [newKnot, knot]]:
|
|
264
|
+
for gaussNode in gaussNodes:
|
|
265
|
+
tValues[i] = t = (1.0 - gaussNode) * knotInterval[0] + gaussNode * knotInterval[1]
|
|
266
|
+
x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
|
|
267
|
+
x = np.array([max(0.0, min(1.0, xi)) for xi in x])
|
|
268
|
+
GSamples[i] = x
|
|
269
|
+
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
|
|
270
|
+
i += 1
|
|
272
271
|
|
|
273
272
|
newKnots += [newKnot] * (order - 2) # C1 continuity
|
|
274
273
|
newKnots += [knot] * (order - 2) # C1 continuity
|
|
275
274
|
previousKnot = knot
|
|
276
275
|
|
|
277
|
-
#
|
|
276
|
+
# Test if F(GSamples) is close enough to zero.
|
|
277
|
+
if FSamplesNorm / np.linalg.norm(dHCoefs, np.inf) < epsilon:
|
|
278
|
+
break # We're done! Exit subdivision loop and return x(t).
|
|
279
|
+
|
|
280
|
+
# Otherwise, update nCoef and knots array, and then re-run Newton's method.
|
|
281
|
+
nCoef = nUnknownCoefs + 2
|
|
278
282
|
newKnots += [knot] * 2 # Clamp last knot
|
|
279
283
|
knots = np.array(newKnots, contourDtype)
|
|
280
284
|
assert i == nUnknownCoefs
|
|
281
285
|
assert len(knots) == nCoef + order
|
|
282
286
|
|
|
283
287
|
# Rescale x(t) back to original data points.
|
|
284
|
-
coefs = (coefsMin + coefs
|
|
288
|
+
coefs = (coefsMin + coefs * coefsMaxMinusMin).T
|
|
285
289
|
spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
|
|
286
290
|
if isinstance(F, bspy.Spline):
|
|
287
291
|
spline = spline.confine(F.domain())
|
|
@@ -383,6 +387,82 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
|
|
|
383
387
|
|
|
384
388
|
return (1.0 - surfParam) * coons + surfParam * laplace
|
|
385
389
|
|
|
390
|
+
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
|
|
391
|
+
# Check validity of input
|
|
392
|
+
if self.nInd != 2: raise ValueError("Surface must have two independent variables")
|
|
393
|
+
if len(uvStart) != 2: raise ValueError("uvStart must have two components")
|
|
394
|
+
if len(uvEnd) != 2: raise ValueError("uvEnd must have two components")
|
|
395
|
+
uvDomain = self.domain()
|
|
396
|
+
if uvStart[0] < uvDomain[0, 0] or uvStart[0] > uvDomain[0, 1] or \
|
|
397
|
+
uvStart[1] < uvDomain[1, 0] or uvStart[1] > uvDomain[1, 1]:
|
|
398
|
+
raise ValueError("uvStart is outside domain of the surface")
|
|
399
|
+
if uvEnd[0] < uvDomain[0, 0] or uvEnd[0] > uvDomain[0, 1] or \
|
|
400
|
+
uvEnd[1] < uvDomain[1, 0] or uvEnd[1] > uvDomain[1, 1]:
|
|
401
|
+
raise ValueError("uvEnd is outside domain of the surface")
|
|
402
|
+
|
|
403
|
+
# Define the callback function for the ODE solver
|
|
404
|
+
def geodesicCallback(t, u, surface, uvDomain):
|
|
405
|
+
# Evaluate the surface information needed for the Christoffel symbols
|
|
406
|
+
u[:, 0] = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
|
|
407
|
+
su = surface.derivative([1, 0], u[:, 0])
|
|
408
|
+
sv = surface.derivative([0, 1], u[:, 0])
|
|
409
|
+
suu = surface.derivative([2, 0], u[:, 0])
|
|
410
|
+
suv = surface.derivative([1, 1], u[:, 0])
|
|
411
|
+
svv = surface.derivative([0, 2], u[:, 0])
|
|
412
|
+
suuu = surface.derivative([3, 0], u[:, 0])
|
|
413
|
+
suuv = surface.derivative([2, 1], u[:, 0])
|
|
414
|
+
suvv = surface.derivative([1, 2], u[:, 0])
|
|
415
|
+
svvv = surface.derivative([0, 3], u[:, 0])
|
|
416
|
+
|
|
417
|
+
# Calculate the first fundamental form and derivatives
|
|
418
|
+
E = su @ su
|
|
419
|
+
E_u = 2.0 * suu @ su
|
|
420
|
+
E_v = 2.0 * suv @ su
|
|
421
|
+
F = su @ sv
|
|
422
|
+
F_u = suu @ sv + suv @ su
|
|
423
|
+
F_v = suv @ sv + svv @ su
|
|
424
|
+
G = sv @ sv
|
|
425
|
+
G_u = 2.0 * suv @ sv
|
|
426
|
+
G_v = 2.0 * svv @ sv
|
|
427
|
+
A = np.array([[E, F], [F, G]])
|
|
428
|
+
A_u = np.array([[E_u, F_u], [F_u, G_u]])
|
|
429
|
+
A_v = np.array([[E_v, F_v], [F_v, G_v]])
|
|
430
|
+
|
|
431
|
+
# Compute right hand side entries
|
|
432
|
+
R = np.array([[suu @ su, suv @ su, svv @ su], [suu @ sv, suv @ sv, svv @ sv]])
|
|
433
|
+
R_u = np.array([[suuu @ su + suu @ suu, suuv @ su + suv @ suu, suvv @ su + svv @ suu],
|
|
434
|
+
[suuu @ sv + suu @ suv, suuv @ sv + suv @ suv, suvv @ sv + svv @ suv]])
|
|
435
|
+
R_v = np.array([[suuv @ su + suu @ suv, suvv @ su + suv @ suv, suvv @ su + svv @ suv],
|
|
436
|
+
[suuv @ sv + suu @ svv, suvv @ sv + suv @ svv, svvv @ sv + svv @ svv]])
|
|
437
|
+
|
|
438
|
+
# Solve for the Christoffel symbols
|
|
439
|
+
luAndPivot = sp.linalg.lu_factor(A)
|
|
440
|
+
Gamma = sp.linalg.lu_solve(luAndPivot, R)
|
|
441
|
+
Gamma_u = sp.linalg.lu_solve(luAndPivot, R_u - A_u @ Gamma)
|
|
442
|
+
Gamma_v = sp.linalg.lu_solve(luAndPivot, R_v - A_v @ Gamma)
|
|
443
|
+
|
|
444
|
+
# Compute the right hand side for the ODE
|
|
445
|
+
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,
|
|
446
|
+
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])
|
|
447
|
+
|
|
448
|
+
# Compute the Jacobian matrix of the right hand side of the ODE
|
|
449
|
+
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,
|
|
450
|
+
2.0 * Gamma[0, 0] * u[0, 1] + 2.0 * Gamma[0, 1] * u[1, 1]],
|
|
451
|
+
[Gamma_v[0, 0] * u[0, 1] ** 2 + 2.0 * Gamma_v[0, 1] * u[0, 1] * u[1, 1] + Gamma_v[0, 2] * u[1, 1] ** 2,
|
|
452
|
+
2.0 * Gamma[0, 1] * u[0, 1] + 2.0 * Gamma[0, 2] * u[1, 1]]],
|
|
453
|
+
[[Gamma_u[1, 0] * u[0, 1] ** 2 + 2.0 * Gamma_u[1, 1] * u[0, 1] * u[1, 1] + Gamma_u[1, 2] * u[1, 1] ** 2,
|
|
454
|
+
2.0 * Gamma[1, 0] * u[0, 1] + 2.0 * Gamma[1, 1] * u[1, 1]],
|
|
455
|
+
[Gamma_v[1, 0] * u[0, 1] ** 2 + 2.0 * Gamma_v[1, 1] * u[0, 1] * u[1, 1] + Gamma_v[1, 2] * u[1, 1] ** 2,
|
|
456
|
+
2.0 * Gamma[1, 1] * u[1, 1] + 2.0 * Gamma[1, 2] * u[1, 1]]]])
|
|
457
|
+
return rhs, jacobian
|
|
458
|
+
|
|
459
|
+
# Generate the initial guess for the contour
|
|
460
|
+
initialGuess = line(uvStart, uvEnd).elevate([2])
|
|
461
|
+
|
|
462
|
+
# Solve the ODE and return the geodesic
|
|
463
|
+
solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
|
|
464
|
+
return solution
|
|
465
|
+
|
|
386
466
|
def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
|
|
387
467
|
tolerance = None, fixEnds = False, metadata = {}):
|
|
388
468
|
|
|
@@ -482,11 +562,10 @@ def least_squares(uValues, dataPoints, order = None, knots = None, compression =
|
|
|
482
562
|
if uNew != u:
|
|
483
563
|
iDerivative = 0
|
|
484
564
|
u = uNew
|
|
485
|
-
ix =
|
|
486
|
-
ix = min(ix, nCols)
|
|
565
|
+
ix = None
|
|
487
566
|
else:
|
|
488
567
|
iDerivative += 1
|
|
489
|
-
row = bspy.Spline.bspline_values(ix, knots[iInd], order[iInd], u, iDerivative)
|
|
568
|
+
ix, row = bspy.Spline.bspline_values(ix, knots[iInd], order[iInd], u, iDerivative)
|
|
490
569
|
A[iRow, ix - order[iInd] : ix] = row
|
|
491
570
|
if fixEnds and (u == uValues[iInd][0] or u == uValues[iInd][-1]):
|
|
492
571
|
fixedRows.append(iRow)
|
|
@@ -602,7 +681,10 @@ def section(xytk):
|
|
|
602
681
|
onePlusCosTheta = 1.0 + math.cos(theta)
|
|
603
682
|
r0 = 4.0 * startKappa * tangentDistances[0] ** 2 / (3.0 * tangentDistances[1] * crossTangents)
|
|
604
683
|
r1 = 4.0 * endKappa * tangentDistances[1] ** 2 / (3.0 * tangentDistances[0] * crossTangents)
|
|
605
|
-
|
|
684
|
+
if r0 != 0.0 or r1 != 0.0:
|
|
685
|
+
rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
|
|
686
|
+
else:
|
|
687
|
+
rhoCrit = 1.0
|
|
606
688
|
rhoCritOfTheta = 3.0 * (math.sqrt(1.0 + 32.0 / (3.0 * onePlusCosTheta)) - 1.0) * onePlusCosTheta / 16.0
|
|
607
689
|
|
|
608
690
|
# Determine quadratic polynomial
|
|
@@ -640,6 +722,202 @@ def section(xytk):
|
|
|
640
722
|
# Join the pieces together and return
|
|
641
723
|
return bspy.Spline.join(mySections)
|
|
642
724
|
|
|
725
|
+
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
726
|
+
# Ensure that the ODE is properly formulated
|
|
727
|
+
|
|
728
|
+
if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
|
|
729
|
+
if nRight < 0: raise ValueError("Invalid number of right hand boundary conditions")
|
|
730
|
+
if self.nInd != 1: raise ValueError("Initial guess must have exactly one independent variable")
|
|
731
|
+
nOrder = nLeft + nRight
|
|
732
|
+
|
|
733
|
+
# Make sure that there are full multiplicity knots on the ends
|
|
734
|
+
|
|
735
|
+
currentGuess = self.copy().clamp((0,), (0,))
|
|
736
|
+
scale = 1.0
|
|
737
|
+
for lower, upper in currentGuess.range_bounds():
|
|
738
|
+
scale = max(scale, upper - lower)
|
|
739
|
+
nDep = currentGuess.nDep
|
|
740
|
+
|
|
741
|
+
# Insert and remove knots so initial guess conforms to de Boor - Swartz setup
|
|
742
|
+
|
|
743
|
+
uniqueKnots, indices = np.unique(currentGuess.knots[0], True)
|
|
744
|
+
uniqueKnots = uniqueKnots[1 : -1]
|
|
745
|
+
indices = indices[1:]
|
|
746
|
+
knotsToAdd = []
|
|
747
|
+
knotsToRemove = []
|
|
748
|
+
for i, knot in enumerate(uniqueKnots):
|
|
749
|
+
howMany = currentGuess.order[0] - indices[i + 1] + indices[i] - nOrder
|
|
750
|
+
if howMany > 0:
|
|
751
|
+
knotsToAdd += howMany * [knot]
|
|
752
|
+
if howMany < 0:
|
|
753
|
+
knotsToRemove += [[knot, abs(howMany)]]
|
|
754
|
+
currentGuess = currentGuess.insert_knots([knotsToAdd])
|
|
755
|
+
for [knot, howMany] in knotsToRemove:
|
|
756
|
+
ix = np.searchsorted(currentGuess.knots[0], knot, side = 'left')
|
|
757
|
+
for iy in range(howMany):
|
|
758
|
+
currentGuess, residual = currentGuess.remove_knot(ix, nLeft, nRight)
|
|
759
|
+
previousGuess = 0.0 * currentGuess
|
|
760
|
+
bandWidth = nDep * currentGuess.order[0] - 1
|
|
761
|
+
|
|
762
|
+
# Determine whether an initial value problem or boundary value problem
|
|
763
|
+
|
|
764
|
+
IVP = nLeft == 0 or nRight == 0
|
|
765
|
+
|
|
766
|
+
# Use the Gauss Legendre points as the collocation points
|
|
767
|
+
|
|
768
|
+
perInterval = currentGuess.order[0] - nOrder
|
|
769
|
+
gaussNodes, weights = _legendre_polynomial_zeros(perInterval)
|
|
770
|
+
linear = False
|
|
771
|
+
refine = True
|
|
772
|
+
while refine:
|
|
773
|
+
collocationPoints = []
|
|
774
|
+
for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
|
|
775
|
+
if knot0 < knot1:
|
|
776
|
+
for gaussNode in gaussNodes:
|
|
777
|
+
collocationPoints.append((1.0 - gaussNode) * knot0 + gaussNode * knot1)
|
|
778
|
+
n = nDep * currentGuess.nCoef[0]
|
|
779
|
+
bestGuess = np.reshape(currentGuess.coefs.T, (n,))
|
|
780
|
+
|
|
781
|
+
# Set up for next pass at this refinement level
|
|
782
|
+
|
|
783
|
+
nCollocation = len(collocationPoints)
|
|
784
|
+
if IVP:
|
|
785
|
+
n = nDep * currentGuess.order[0]
|
|
786
|
+
if nLeft != 0:
|
|
787
|
+
iFirstPoint = 0
|
|
788
|
+
iNextPoint = perInterval
|
|
789
|
+
else:
|
|
790
|
+
iNextPoint = nCollocation
|
|
791
|
+
iFirstPoint = iNextPoint - perInterval
|
|
792
|
+
else:
|
|
793
|
+
iFirstPoint = 0
|
|
794
|
+
iNextPoint = nCollocation
|
|
795
|
+
|
|
796
|
+
# Perform the loop through all the IVP intervals
|
|
797
|
+
|
|
798
|
+
while True:
|
|
799
|
+
done = linear
|
|
800
|
+
continuation = 1.0
|
|
801
|
+
bestContinuation = 0.0
|
|
802
|
+
inCaseOfEmergency = bestGuess.copy()
|
|
803
|
+
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
804
|
+
iteration = 0
|
|
805
|
+
|
|
806
|
+
# Perform nonlinear Newton iteration
|
|
807
|
+
|
|
808
|
+
while True:
|
|
809
|
+
iteration += 1
|
|
810
|
+
collocationMatrix = np.zeros((2 * bandWidth + 1, n))
|
|
811
|
+
residuals = np.array([])
|
|
812
|
+
workingSpline = bspy.Spline(1, nDep, currentGuess.order, currentGuess.nCoef,
|
|
813
|
+
currentGuess.knots, np.reshape(bestGuess, (currentGuess.nCoef[0], nDep)).T)
|
|
814
|
+
residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
|
|
815
|
+
collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
|
|
816
|
+
for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
|
|
817
|
+
uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
|
|
818
|
+
F, F_u = FAndF_u(t, uData, *args)
|
|
819
|
+
residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
|
|
820
|
+
ix = None
|
|
821
|
+
bValues = np.array([])
|
|
822
|
+
for iDerivative in range(nOrder + 1):
|
|
823
|
+
ix, iValues = bspy.Spline.bspline_values(ix, workingSpline.knots[0], workingSpline.order[0],
|
|
824
|
+
t, derivativeOrder = iDerivative)
|
|
825
|
+
bValues = np.append(bValues, iValues)
|
|
826
|
+
bValues = np.reshape(bValues, (nOrder + 1, workingSpline.order[0]))
|
|
827
|
+
for iDep in range(nDep):
|
|
828
|
+
iRow = (nLeft + iPoint) * nDep + iDep
|
|
829
|
+
startSlice = nDep * (ix - workingSpline.order[0] - iFirstPoint)
|
|
830
|
+
endSlice = nDep * (ix - iFirstPoint)
|
|
831
|
+
indices = np.arange(startSlice + iDep, endSlice + iDep, nDep)
|
|
832
|
+
collocationMatrix[bandWidth + iRow - indices, indices] -= bValues[nOrder]
|
|
833
|
+
for iF_uRow in range(nDep):
|
|
834
|
+
indices = np.arange(startSlice + iF_uRow, endSlice + iF_uRow, nDep)
|
|
835
|
+
for iF_uColumn in range(nOrder):
|
|
836
|
+
collocationMatrix[bandWidth + iRow - indices, indices] += continuation * F_u[iDep, iF_uRow, iF_uColumn] * bValues[iF_uColumn]
|
|
837
|
+
residuals = np.append(residuals, np.zeros((nRight * nDep,)))
|
|
838
|
+
collocationMatrix[bandWidth, -1 : -(nRight * nDep + 1) : -1] = 1.0
|
|
839
|
+
|
|
840
|
+
# Solve the collocation linear system
|
|
841
|
+
|
|
842
|
+
update = sp.linalg.solve_banded((bandWidth, bandWidth), collocationMatrix, residuals)
|
|
843
|
+
bestGuess[nDep * (iFirstPoint + nLeft) : nDep * (iNextPoint + nLeft)] += update[nDep * nLeft : nDep * (iNextPoint - iFirstPoint + nLeft)]
|
|
844
|
+
updateSize = np.linalg.norm(update)
|
|
845
|
+
if updateSize > 1.25 * previous and iteration >= 4 or \
|
|
846
|
+
updateSize > 0.01 and iteration > 50:
|
|
847
|
+
continuation = 0.5 * (continuation + bestContinuation)
|
|
848
|
+
bestGuess = inCaseOfEmergency.copy()
|
|
849
|
+
if continuation - bestContinuation < 0.01:
|
|
850
|
+
break
|
|
851
|
+
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
852
|
+
iteration = 0
|
|
853
|
+
continue
|
|
854
|
+
previous = updateSize
|
|
855
|
+
if done or iteration > 50:
|
|
856
|
+
break
|
|
857
|
+
|
|
858
|
+
# Check to see if we're almost done
|
|
859
|
+
|
|
860
|
+
if updateSize < math.sqrt(n) * scale * math.sqrt(np.finfo(update.dtype).eps):
|
|
861
|
+
if continuation < 1.0:
|
|
862
|
+
bestContinuation = continuation
|
|
863
|
+
inCaseOfEmergency = bestGuess.copy()
|
|
864
|
+
continuation = min(1.0, 1.2 * continuation)
|
|
865
|
+
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
866
|
+
iteration = 0
|
|
867
|
+
else:
|
|
868
|
+
done = True
|
|
869
|
+
|
|
870
|
+
# Check to see if this is a linear problem
|
|
871
|
+
|
|
872
|
+
if not linear and iteration == 2 and updateSize < 100.0 * scale * np.finfo(update[0]).eps:
|
|
873
|
+
linear = True
|
|
874
|
+
done = True
|
|
875
|
+
|
|
876
|
+
# Set up for one more pass through an IVP
|
|
877
|
+
|
|
878
|
+
if IVP:
|
|
879
|
+
if nLeft != 0:
|
|
880
|
+
iFirstPoint = iNextPoint
|
|
881
|
+
iNextPoint += perInterval
|
|
882
|
+
if iFirstPoint < nCollocation:
|
|
883
|
+
continue
|
|
884
|
+
else:
|
|
885
|
+
iNextPoint = iFirstPoint
|
|
886
|
+
iFirstPoint -= perInterval
|
|
887
|
+
if iFirstPoint >= 0:
|
|
888
|
+
continue
|
|
889
|
+
break;
|
|
890
|
+
|
|
891
|
+
# Is it time to give up?
|
|
892
|
+
|
|
893
|
+
if (not done or continuation < 1.0) and n > 1000:
|
|
894
|
+
raise RuntimeError("Can't find solution with given initial guess")
|
|
895
|
+
|
|
896
|
+
# Estimate the error
|
|
897
|
+
|
|
898
|
+
currentGuess = bspy.Spline(1, nDep, currentGuess.order, currentGuess.nCoef, currentGuess.knots,
|
|
899
|
+
np.reshape(bestGuess, (currentGuess.nCoef[0], nDep)).T)
|
|
900
|
+
errorRange = (previousGuess - currentGuess).range_bounds()
|
|
901
|
+
refine = False
|
|
902
|
+
for lower, upper in errorRange:
|
|
903
|
+
if upper - lower > 2.0 * scale * tolerance:
|
|
904
|
+
refine = True
|
|
905
|
+
|
|
906
|
+
# Insert new knots if refinement is needed
|
|
907
|
+
|
|
908
|
+
if refine:
|
|
909
|
+
knotsToAdd = []
|
|
910
|
+
for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
|
|
911
|
+
if knot0 < knot1:
|
|
912
|
+
knotsToAdd += (currentGuess.order[0] - nOrder) * [0.5 * (knot0 + knot1)]
|
|
913
|
+
previousGuess = currentGuess
|
|
914
|
+
currentGuess = currentGuess.insert_knots([knotsToAdd])
|
|
915
|
+
|
|
916
|
+
# Simplify the result and return
|
|
917
|
+
|
|
918
|
+
currentGuess = currentGuess.remove_knots(0.1 * scale * tolerance, nLeft, nRight)
|
|
919
|
+
return currentGuess
|
|
920
|
+
|
|
643
921
|
def sphere(radius, tolerance = None):
|
|
644
922
|
if radius <= 0.0: raise ValueError("Radius must be positive")
|
|
645
923
|
if tolerance == None:
|