bspy 4.0__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 +13 -3
- bspy/_spline_domain.py +9 -0
- bspy/_spline_evaluation.py +16 -10
- bspy/_spline_fitting.py +56 -82
- bspy/_spline_intersection.py +187 -23
- bspy/_spline_operations.py +8 -2
- bspy/hyperplane.py +540 -0
- bspy/manifold.py +332 -29
- bspy/solid.py +839 -0
- bspy/spline.py +176 -27
- bspy/splineOpenGLFrame.py +262 -14
- bspy/viewer.py +130 -87
- {bspy-4.0.dist-info → bspy-4.1.dist-info}/METADATA +8 -3
- bspy-4.1.dist-info/RECORD +17 -0
- bspy-4.0.dist-info/RECORD +0 -15
- {bspy-4.0.dist-info → bspy-4.1.dist-info}/LICENSE +0 -0
- {bspy-4.0.dist-info → bspy-4.1.dist-info}/WHEEL +0 -0
- {bspy-4.0.dist-info → bspy-4.1.dist-info}/top_level.txt +0 -0
bspy/__init__.py
CHANGED
|
@@ -3,14 +3,24 @@ 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.
|
|
8
14
|
|
|
9
15
|
`bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
|
|
10
16
|
|
|
11
17
|
`bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
|
|
12
18
|
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.
|
|
19
|
+
an associated `Viewer`, allowing you to script splines and display them in the viewer.
|
|
20
|
+
"""
|
|
21
|
+
from bspy.solid import Solid, Boundary
|
|
22
|
+
from bspy.manifold import Manifold
|
|
23
|
+
from bspy.hyperplane import Hyperplane
|
|
14
24
|
from bspy.spline import Spline
|
|
15
25
|
from bspy.splineOpenGLFrame import SplineOpenGLFrame
|
|
16
26
|
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)]
|
|
@@ -582,6 +583,14 @@ def trim(self, newDomain):
|
|
|
582
583
|
|
|
583
584
|
return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
|
|
584
585
|
|
|
586
|
+
def trimmed_range_bounds(self, domainBounds):
|
|
587
|
+
domainBounds = np.array(domainBounds, copy=True)
|
|
588
|
+
for original, trim in zip(self.domain(), domainBounds):
|
|
589
|
+
trim[0] = max(original[0], trim[0] - Manifold.minSeparation)
|
|
590
|
+
trim[1] = min(original[1], trim[1] + Manifold.minSeparation)
|
|
591
|
+
trimmedSpline = self.trim(domainBounds)
|
|
592
|
+
return trimmedSpline, trimmedSpline.range_bounds()
|
|
593
|
+
|
|
585
594
|
def unfold(self, foldedInd, coefficientlessSpline):
|
|
586
595
|
if not(len(foldedInd) == coefficientlessSpline.nInd): raise ValueError("Invalid coefficientlessSpline")
|
|
587
596
|
unfoldedOrder = []
|
bspy/_spline_evaluation.py
CHANGED
|
@@ -2,10 +2,12 @@ import numpy as np
|
|
|
2
2
|
|
|
3
3
|
def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
|
|
4
4
|
basis = np.zeros(splineOrder, knots.dtype)
|
|
5
|
-
basis[-1] = 1.0
|
|
6
5
|
if knot is None:
|
|
7
6
|
knot = np.searchsorted(knots, u, side = 'right')
|
|
8
7
|
knot = min(knot, len(knots) - splineOrder)
|
|
8
|
+
if derivativeOrder >= splineOrder:
|
|
9
|
+
return knot, basis
|
|
10
|
+
basis[-1] = 1.0
|
|
9
11
|
for degree in range(1, splineOrder - derivativeOrder):
|
|
10
12
|
b = splineOrder - degree
|
|
11
13
|
for i in range(knot - degree, knot):
|
|
@@ -149,22 +151,17 @@ def normal(self, uvw, normalize=True, indices=None):
|
|
|
149
151
|
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
152
|
|
|
151
153
|
# Evaluate the tangents at the point.
|
|
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
|
|
154
|
+
tangentSpace = self.tangent_space(uvw)
|
|
158
155
|
|
|
159
156
|
# Record the larger dimension and ensure it comes first.
|
|
160
157
|
if self.nInd > self.nDep:
|
|
161
158
|
nDep = self.nInd
|
|
159
|
+
tangentSpace = tangentSpace.T
|
|
162
160
|
else:
|
|
163
161
|
nDep = self.nDep
|
|
164
|
-
tangentSpace = tangentSpace.T
|
|
165
162
|
|
|
166
163
|
# Compute the normal using cofactors (determinants of subsets of the tangent space).
|
|
167
|
-
sign = 1
|
|
164
|
+
sign = -1 if self.metadata.get("flipNormal", False) else 1
|
|
168
165
|
if indices is None:
|
|
169
166
|
indices = range(nDep)
|
|
170
167
|
normal = np.empty(nDep, self.coefs.dtype)
|
|
@@ -183,4 +180,13 @@ def normal(self, uvw, normalize=True, indices=None):
|
|
|
183
180
|
def range_bounds(self):
|
|
184
181
|
# Assumes self.nDep is the first value in self.coefs.shape
|
|
185
182
|
bounds = [[coefficient.min(), coefficient.max()] for coefficient in self.coefs]
|
|
186
|
-
return np.array(bounds, self.coefs.dtype)
|
|
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
|
bspy/_spline_fitting.py
CHANGED
|
@@ -23,7 +23,7 @@ def cone(radius1, radius2, height, tolerance = None):
|
|
|
23
23
|
return bspy.Spline.ruled_surface(bottom, top)
|
|
24
24
|
|
|
25
25
|
# Courtesy of Michael Epton - Translated from his F77 code lgnzro
|
|
26
|
-
def _legendre_polynomial_zeros(degree):
|
|
26
|
+
def _legendre_polynomial_zeros(degree, mapToZeroOne = True):
|
|
27
27
|
def legendre(degree, x):
|
|
28
28
|
p = [1.0, x]
|
|
29
29
|
pd = [0.0, 1.0]
|
|
@@ -35,6 +35,7 @@ def _legendre_polynomial_zeros(degree):
|
|
|
35
35
|
return p, pd
|
|
36
36
|
zval = 1.0
|
|
37
37
|
z = []
|
|
38
|
+
zNegative = []
|
|
38
39
|
for iRoot in range(degree // 2):
|
|
39
40
|
done = False
|
|
40
41
|
while True:
|
|
@@ -49,21 +50,28 @@ def _legendre_polynomial_zeros(degree):
|
|
|
49
50
|
if dz < 1.0e-10:
|
|
50
51
|
done = True
|
|
51
52
|
z.append(zval)
|
|
53
|
+
zNegative.append(-zval)
|
|
52
54
|
zval -= 0.001
|
|
53
55
|
if degree % 2 == 1:
|
|
54
|
-
|
|
56
|
+
zNegative.append(0.0)
|
|
55
57
|
z.reverse()
|
|
58
|
+
z = np.array(zNegative + z)
|
|
56
59
|
w = []
|
|
57
60
|
for zval in z:
|
|
58
61
|
p, pd = legendre(degree, zval)
|
|
59
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
|
|
60
67
|
return z, w
|
|
61
68
|
|
|
62
69
|
def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
63
70
|
# Set up parameters for initial guess of x(t) and validate arguments.
|
|
64
71
|
order = 4
|
|
65
72
|
degree = order - 1
|
|
66
|
-
|
|
73
|
+
gaussNodes, gaussWeights = _legendre_polynomial_zeros(degree - 1)
|
|
74
|
+
|
|
67
75
|
if not(len(knownXValues) >= 2): raise ValueError("There must be at least 2 known x values.")
|
|
68
76
|
m = len(knownXValues) - 1
|
|
69
77
|
nCoef = m * (degree - 1) + 2
|
|
@@ -96,6 +104,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
96
104
|
wrt[i] = 1
|
|
97
105
|
return F.derivative(wrt, x)
|
|
98
106
|
dF.append(splineDerivative)
|
|
107
|
+
FDomain = F.domain().T
|
|
99
108
|
else:
|
|
100
109
|
for i in range(nDep):
|
|
101
110
|
def fDerivative(x, i=i):
|
|
@@ -107,6 +116,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
107
116
|
xShift[i] += h2
|
|
108
117
|
return (np.array(F(xShift)) - fLeft) / h2
|
|
109
118
|
dF.append(fDerivative)
|
|
119
|
+
FDomain = np.array(nDep * [[-np.inf, np.inf]]).T
|
|
110
120
|
else:
|
|
111
121
|
if not(len(dF) == nDep): raise ValueError(f"Must provide {nDep} first derivatives.")
|
|
112
122
|
|
|
@@ -120,13 +130,9 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
120
130
|
for point in knownXValues[1:]:
|
|
121
131
|
dt = np.linalg.norm(point - previousPoint)
|
|
122
132
|
if not(dt > epsilon): raise ValueError("Points must be separated by at least epsilon.")
|
|
123
|
-
for
|
|
124
|
-
tValues[i] = t +
|
|
125
|
-
GSamples[i] = 0
|
|
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))
|
|
133
|
+
for gaussNode in gaussNodes:
|
|
134
|
+
tValues[i] = t + gaussNode * dt
|
|
135
|
+
GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
|
|
130
136
|
i += 1
|
|
131
137
|
t += dt
|
|
132
138
|
knots += [t] * (order - 2)
|
|
@@ -184,6 +190,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
184
190
|
|
|
185
191
|
# Do the same for F(x(t)).
|
|
186
192
|
x = coefsMin + (compactCoefs.T @ bValues) * coefsMaxMinusMin
|
|
193
|
+
x = np.maximum(FDomain[0], np.minimum(FDomain[1], x))
|
|
187
194
|
FValues = F(x)
|
|
188
195
|
FSamplesNorm = max(FSamplesNorm, np.linalg.norm(FValues, np.inf))
|
|
189
196
|
if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
|
|
@@ -253,26 +260,14 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
|
|
|
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
|
-
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
|
|
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
|
|
276
271
|
|
|
277
272
|
newKnots += [newKnot] * (order - 2) # C1 continuity
|
|
278
273
|
newKnots += [knot] * (order - 2) # C1 continuity
|
|
@@ -408,68 +403,47 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
|
|
|
408
403
|
# Define the callback function for the ODE solver
|
|
409
404
|
def geodesicCallback(t, u, surface, uvDomain):
|
|
410
405
|
# 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]))
|
|
406
|
+
u[:, 0] = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
|
|
413
407
|
su = surface.derivative([1, 0], u[:, 0])
|
|
414
408
|
sv = surface.derivative([0, 1], u[:, 0])
|
|
415
409
|
suu = surface.derivative([2, 0], u[:, 0])
|
|
416
410
|
suv = surface.derivative([1, 1], u[:, 0])
|
|
417
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])
|
|
418
416
|
|
|
419
|
-
# Calculate the first fundamental form
|
|
417
|
+
# Calculate the first fundamental form and derivatives
|
|
420
418
|
E = su @ su
|
|
419
|
+
E_u = 2.0 * suu @ su
|
|
420
|
+
E_v = 2.0 * suv @ su
|
|
421
421
|
F = su @ sv
|
|
422
|
+
F_u = suu @ sv + suv @ su
|
|
423
|
+
F_v = suv @ sv + svv @ su
|
|
422
424
|
G = sv @ sv
|
|
425
|
+
G_u = 2.0 * suv @ sv
|
|
426
|
+
G_v = 2.0 * svv @ sv
|
|
423
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]])
|
|
424
430
|
|
|
425
431
|
# 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]])
|
|
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]])
|
|
433
437
|
|
|
434
438
|
# Solve for the Christoffel symbols
|
|
435
439
|
luAndPivot = sp.linalg.lu_factor(A)
|
|
436
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)
|
|
437
443
|
|
|
438
444
|
# Compute the right hand side for the ODE
|
|
439
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,
|
|
440
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])
|
|
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
447
|
|
|
474
448
|
# Compute the Jacobian matrix of the right hand side of the ODE
|
|
475
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,
|
|
@@ -707,7 +681,10 @@ def section(xytk):
|
|
|
707
681
|
onePlusCosTheta = 1.0 + math.cos(theta)
|
|
708
682
|
r0 = 4.0 * startKappa * tangentDistances[0] ** 2 / (3.0 * tangentDistances[1] * crossTangents)
|
|
709
683
|
r1 = 4.0 * endKappa * tangentDistances[1] ** 2 / (3.0 * tangentDistances[0] * crossTangents)
|
|
710
|
-
|
|
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
|
|
711
688
|
rhoCritOfTheta = 3.0 * (math.sqrt(1.0 + 32.0 / (3.0 * onePlusCosTheta)) - 1.0) * onePlusCosTheta / 16.0
|
|
712
689
|
|
|
713
690
|
# Determine quadratic polynomial
|
|
@@ -797,11 +774,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
797
774
|
for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
|
|
798
775
|
if knot0 < knot1:
|
|
799
776
|
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)
|
|
777
|
+
collocationPoints.append((1.0 - gaussNode) * knot0 + gaussNode * knot1)
|
|
805
778
|
n = nDep * currentGuess.nCoef[0]
|
|
806
779
|
bestGuess = np.reshape(currentGuess.coefs.T, (n,))
|
|
807
780
|
|
|
@@ -826,7 +799,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
826
799
|
done = linear
|
|
827
800
|
continuation = 1.0
|
|
828
801
|
bestContinuation = 0.0
|
|
829
|
-
|
|
802
|
+
inCaseOfEmergency = bestGuess.copy()
|
|
830
803
|
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
831
804
|
iteration = 0
|
|
832
805
|
|
|
@@ -869,9 +842,10 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
869
842
|
update = sp.linalg.solve_banded((bandWidth, bandWidth), collocationMatrix, residuals)
|
|
870
843
|
bestGuess[nDep * (iFirstPoint + nLeft) : nDep * (iNextPoint + nLeft)] += update[nDep * nLeft : nDep * (iNextPoint - iFirstPoint + nLeft)]
|
|
871
844
|
updateSize = np.linalg.norm(update)
|
|
872
|
-
if updateSize > 1.25 * previous and iteration >= 4
|
|
845
|
+
if updateSize > 1.25 * previous and iteration >= 4 or \
|
|
846
|
+
updateSize > 0.01 and iteration > 50:
|
|
873
847
|
continuation = 0.5 * (continuation + bestContinuation)
|
|
874
|
-
bestGuess =
|
|
848
|
+
bestGuess = inCaseOfEmergency.copy()
|
|
875
849
|
if continuation - bestContinuation < 0.01:
|
|
876
850
|
break
|
|
877
851
|
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
@@ -886,7 +860,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
|
886
860
|
if updateSize < math.sqrt(n) * scale * math.sqrt(np.finfo(update.dtype).eps):
|
|
887
861
|
if continuation < 1.0:
|
|
888
862
|
bestContinuation = continuation
|
|
889
|
-
|
|
863
|
+
inCaseOfEmergency = bestGuess.copy()
|
|
890
864
|
continuation = min(1.0, 1.2 * continuation)
|
|
891
865
|
previous = 0.5 * np.finfo(bestGuess[0]).max
|
|
892
866
|
iteration = 0
|
bspy/_spline_intersection.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import math
|
|
3
3
|
import numpy as np
|
|
4
|
+
from bspy.manifold import Manifold
|
|
5
|
+
from bspy.hyperplane import Hyperplane
|
|
4
6
|
import bspy.spline
|
|
5
|
-
from bspy.
|
|
7
|
+
from bspy.solid import Solid, Boundary
|
|
6
8
|
from collections import namedtuple
|
|
7
9
|
from multiprocessing import Pool
|
|
8
10
|
|
|
@@ -575,6 +577,7 @@ def contours(self):
|
|
|
575
577
|
# Extra step not in paper.
|
|
576
578
|
# Run a checksum on the points, ensuring starting and ending points balance.
|
|
577
579
|
# Start by flipping endpoints as needed, since we can miss turning points near endpoints.
|
|
580
|
+
|
|
578
581
|
if points[0].det < 0.0:
|
|
579
582
|
point = points[0]
|
|
580
583
|
points[0] = Point(point.d, -point.det, point.onUVBoundary, point.turningPoint, point.uvw)
|
|
@@ -768,10 +771,10 @@ def contours(self):
|
|
|
768
771
|
currentContourPoints[i + adjustment].append(uvw)
|
|
769
772
|
|
|
770
773
|
# We've determined a bunch of points along all the contours, including starting and ending points.
|
|
771
|
-
# Now we just need to create splines for those contours using the Spline.contour method.
|
|
774
|
+
# Now we just need to create splines for those contours using the bspy.Spline.contour method.
|
|
772
775
|
splineContours = []
|
|
773
776
|
for points in contourPoints:
|
|
774
|
-
contour = bspy.
|
|
777
|
+
contour = bspy.Spline.contour(self, points)
|
|
775
778
|
# Transform the contour to self's original domain.
|
|
776
779
|
contour.coefs = (contour.coefs.T * (domain[1] - domain[0]) + domain[0]).T
|
|
777
780
|
splineContours.append(contour)
|
|
@@ -779,12 +782,74 @@ def contours(self):
|
|
|
779
782
|
return splineContours
|
|
780
783
|
|
|
781
784
|
def intersect(self, other):
|
|
782
|
-
#assert self.range_dimension() == other.range_dimension() TODO: Put back this assertion
|
|
783
785
|
intersections = []
|
|
784
786
|
nDep = self.nInd # The dimension of the intersection's range
|
|
785
787
|
|
|
788
|
+
# Spline-Hyperplane intersection.
|
|
789
|
+
if isinstance(other, Hyperplane):
|
|
790
|
+
# Compute the projection onto the hyperplane to map Spline-Hyperplane intersection points to the domain of the Hyperplane.
|
|
791
|
+
projection = np.linalg.inv(other._tangentSpace.T @ other._tangentSpace) @ other._tangentSpace.T
|
|
792
|
+
# Construct a new spline that represents the intersection.
|
|
793
|
+
spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
|
|
794
|
+
|
|
795
|
+
# Curve-Line intersection.
|
|
796
|
+
if nDep == 1:
|
|
797
|
+
# Find the intersection points and intervals.
|
|
798
|
+
zeros = spline.zeros()
|
|
799
|
+
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
800
|
+
for zero in zeros:
|
|
801
|
+
if isinstance(zero, tuple):
|
|
802
|
+
# Intersection is an interval, so create a Manifold.Coincidence.
|
|
803
|
+
planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
|
|
804
|
+
|
|
805
|
+
# First, check for crossings at the boundaries of the coincidence, since splines can have discontinuous tangents.
|
|
806
|
+
# We do this first because later we may change the order of the plane bounds.
|
|
807
|
+
(bounds,) = self.domain()
|
|
808
|
+
epsilon = 0.1 * Manifold.minSeparation
|
|
809
|
+
if zero[0] - epsilon > bounds[0]:
|
|
810
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[0] - epsilon, 0.0), Hyperplane(1.0, planeBounds[0], 0.0)))
|
|
811
|
+
if zero[1] + epsilon < bounds[1]:
|
|
812
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
|
|
813
|
+
|
|
814
|
+
# Now, create the coincidence.
|
|
815
|
+
left = Solid(nDep, False)
|
|
816
|
+
left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
|
|
817
|
+
left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
|
|
818
|
+
right = Solid(nDep, False)
|
|
819
|
+
if planeBounds[0] > planeBounds[1]:
|
|
820
|
+
planeBounds = (planeBounds[1], planeBounds[0])
|
|
821
|
+
right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
|
|
822
|
+
right.add_boundary(Boundary(Hyperplane(1.0, planeBounds[1], 0.0), Solid(0, True)))
|
|
823
|
+
alignment = np.dot(self.normal((zero[0],)), other._normal) # Use the first zero, since B-splines are closed on the left
|
|
824
|
+
width = zero[1] - zero[0]
|
|
825
|
+
transform = (planeBounds[1] - planeBounds[0]) / width
|
|
826
|
+
translation = (planeBounds[0] * zero[1] - planeBounds[1] * zero[0]) / width
|
|
827
|
+
intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
|
|
828
|
+
else:
|
|
829
|
+
# Intersection is a point, so create a Manifold.Crossing.
|
|
830
|
+
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
|
|
831
|
+
|
|
832
|
+
# Surface-Plane intersection.
|
|
833
|
+
elif nDep == 2:
|
|
834
|
+
# Find the intersection contours, which are returned as splines.
|
|
835
|
+
contours = spline.contours()
|
|
836
|
+
# Convert each contour into a Manifold.Crossing.
|
|
837
|
+
for contour in contours:
|
|
838
|
+
# The left portion is the contour returned for the spline-plane intersection.
|
|
839
|
+
left = contour
|
|
840
|
+
# The right portion is the contour projected onto the plane's domain, which we compute with samples and a least squares fit.
|
|
841
|
+
tValues = np.linspace(0.0, 1.0, contour.nCoef[0] + 5) # Over-sample a bit to reduce the condition number and avoid singular matrix
|
|
842
|
+
points = []
|
|
843
|
+
for t in tValues:
|
|
844
|
+
zero = contour((t,))
|
|
845
|
+
points.append(projection @ (self(zero) - other._point))
|
|
846
|
+
right = bspy.Spline.least_squares(tValues, np.array(points).T, contour.order, contour.knots)
|
|
847
|
+
intersections.append(Manifold.Crossing(left, right))
|
|
848
|
+
else:
|
|
849
|
+
return NotImplemented
|
|
850
|
+
|
|
786
851
|
# Spline-Spline intersection.
|
|
787
|
-
|
|
852
|
+
elif isinstance(other, bspy.Spline):
|
|
788
853
|
# Construct a new spline that represents the intersection.
|
|
789
854
|
spline = self.subtract(other)
|
|
790
855
|
|
|
@@ -812,27 +877,25 @@ def intersect(self, other):
|
|
|
812
877
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
|
|
813
878
|
|
|
814
879
|
# Now, create the coincidence.
|
|
815
|
-
# TODO: Remove the quoted section.
|
|
816
|
-
"""
|
|
817
880
|
left = Solid(nDep, False)
|
|
818
|
-
left.
|
|
819
|
-
left.
|
|
881
|
+
left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
|
|
882
|
+
left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
|
|
820
883
|
right = Solid(nDep, False)
|
|
821
|
-
right.
|
|
822
|
-
right.
|
|
884
|
+
right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
|
|
885
|
+
right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
|
|
823
886
|
alignment = np.dot(self.normal(zero[0][0]), other.normal(zero[0][1])) # Use the first zeros, since B-splines are closed on the left
|
|
824
887
|
width = zero[1][0] - zero[0][0]
|
|
825
888
|
transform = (zero[1][1] - zero[0][1]) / width
|
|
826
889
|
translation = (zero[0][1] * zero[1][0] - zero[1][1] * zero[0][0]) / width
|
|
827
890
|
intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
|
|
828
|
-
"""
|
|
829
891
|
else:
|
|
830
892
|
# Intersection is a point, so create a Manifold.Crossing.
|
|
831
893
|
intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:nDep], 0.0), Hyperplane(1.0, zero[nDep:], 0.0)))
|
|
832
894
|
|
|
833
895
|
# Surface-Surface intersection.
|
|
834
896
|
elif nDep == 2:
|
|
835
|
-
|
|
897
|
+
if "Name" in self.metadata and "Name" in other.metadata:
|
|
898
|
+
logging.info(f"intersect({self.metadata['Name']}, {other.metadata['Name']})")
|
|
836
899
|
# Find the intersection contours, which are returned as splines.
|
|
837
900
|
swap = False
|
|
838
901
|
try:
|
|
@@ -845,7 +908,8 @@ def intersect(self, other):
|
|
|
845
908
|
# Convert each contour into a Manifold.Crossing.
|
|
846
909
|
if swap:
|
|
847
910
|
spline = other.subtract(self)
|
|
848
|
-
|
|
911
|
+
if "Name" in self.metadata and "Name" in other.metadata:
|
|
912
|
+
logging.info(f"intersect({other.metadata['Name']}, {self.metadata['Name']})")
|
|
849
913
|
contours = spline.contours()
|
|
850
914
|
for contour in contours:
|
|
851
915
|
# Swap left and right, compared to not swapped.
|
|
@@ -864,15 +928,115 @@ def intersect(self, other):
|
|
|
864
928
|
|
|
865
929
|
# Ensure the normals point outwards for both Manifolds in each crossing intersection.
|
|
866
930
|
# Note that evaluating left and right at 0.5 is always valid because either they are points or curves with [0.0, 1.0] domains.
|
|
867
|
-
# TODO: Remove quoted section.
|
|
868
|
-
"""
|
|
869
931
|
domainPoint = np.atleast_1d(0.5)
|
|
870
|
-
for intersection in intersections:
|
|
932
|
+
for i, intersection in enumerate(intersections):
|
|
871
933
|
if isinstance(intersection, Manifold.Crossing):
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if np.dot(
|
|
875
|
-
|
|
876
|
-
|
|
934
|
+
left = intersection.left
|
|
935
|
+
right = intersection.right
|
|
936
|
+
if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
|
|
937
|
+
left = left.flip_normal()
|
|
938
|
+
if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
|
|
939
|
+
right = right.flip_normal()
|
|
940
|
+
intersections[i] = Manifold.Crossing(left, right)
|
|
941
|
+
|
|
942
|
+
return intersections
|
|
943
|
+
|
|
944
|
+
def complete_slice(self, slice, solid):
|
|
945
|
+
# Spline manifold domains have finite bounds.
|
|
946
|
+
slice.containsInfinity = False
|
|
947
|
+
bounds = self.domain()
|
|
948
|
+
|
|
949
|
+
# If manifold (self) has no intersections with solid, just check containment.
|
|
950
|
+
if not slice.boundaries:
|
|
951
|
+
if slice.dimension == 2:
|
|
952
|
+
if "Name" in self.metadata:
|
|
953
|
+
logging.info(f"check containment: {self.metadata['Name']}")
|
|
954
|
+
domain = bounds.T
|
|
955
|
+
if solid.contains_point(self(0.5 * (domain[0] + domain[1]))):
|
|
956
|
+
for boundary in Hyperplane.create_hypercube(bounds).boundaries:
|
|
957
|
+
slice.add_boundary(boundary)
|
|
958
|
+
return
|
|
959
|
+
|
|
960
|
+
# For curves, add domain bounds as needed.
|
|
961
|
+
if slice.dimension == 1:
|
|
962
|
+
slice.boundaries.sort(key=lambda b: (b.manifold.evaluate(0.0), b.manifold.normal(0.0)))
|
|
963
|
+
# First, check right end since we add new boundary to the end.
|
|
964
|
+
if abs(slice.boundaries[-1].manifold._point - bounds[0][1]) >= Manifold.minSeparation and \
|
|
965
|
+
slice.boundaries[-1].manifold._normal < 0.0:
|
|
966
|
+
slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[-1].manifold._normal, bounds[0][1], 0.0), Solid(0, True)))
|
|
967
|
+
# Next, check left end since it's still untouched.
|
|
968
|
+
if abs(slice.boundaries[0].manifold._point - bounds[0][0]) >= Manifold.minSeparation and \
|
|
969
|
+
slice.boundaries[0].manifold._normal > 0.0:
|
|
970
|
+
slice.add_boundary(Boundary(Hyperplane(-slice.boundaries[0].manifold._normal, bounds[0][0], 0.0), Solid(0, True)))
|
|
971
|
+
|
|
972
|
+
# For surfaces, intersect full spline domain with existing slice boundaries.
|
|
973
|
+
if slice.dimension == 2:
|
|
974
|
+
fullDomain = Hyperplane.create_hypercube(bounds)
|
|
975
|
+
for newBoundary in fullDomain.boundaries: # Mark full domain boundaries as untouched
|
|
976
|
+
newBoundary.touched = False
|
|
977
|
+
|
|
978
|
+
# Define function for adding slice points to full domain boundaries.
|
|
979
|
+
def process_domain_point(boundary, domainPoint):
|
|
980
|
+
point = boundary.manifold.evaluate(domainPoint)
|
|
981
|
+
# See if and where point touches full domain.
|
|
982
|
+
for newBoundary in fullDomain.boundaries:
|
|
983
|
+
vector = point - newBoundary.manifold._point
|
|
984
|
+
if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
|
|
985
|
+
# Add the point onto the new boundary.
|
|
986
|
+
normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint))
|
|
987
|
+
newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
|
|
988
|
+
newBoundary.touched = True
|
|
989
|
+
break
|
|
990
|
+
|
|
991
|
+
# Go through existing boundaries and check if either of their endpoints lies on the spline's bounds.
|
|
992
|
+
for boundary in slice.boundaries:
|
|
993
|
+
domainBoundaries = boundary.domain.boundaries
|
|
994
|
+
domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
|
|
995
|
+
process_domain_point(boundary, domainBoundaries[0].manifold._point)
|
|
996
|
+
if len(domainBoundaries) > 1:
|
|
997
|
+
process_domain_point(boundary, domainBoundaries[-1].manifold._point)
|
|
998
|
+
|
|
999
|
+
# For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
|
|
1000
|
+
boundaryWasTouched = False
|
|
1001
|
+
for newBoundary in fullDomain.boundaries:
|
|
1002
|
+
if newBoundary.touched:
|
|
1003
|
+
boundaryWasTouched = True
|
|
1004
|
+
domainBoundaries = newBoundary.domain.boundaries
|
|
1005
|
+
assert len(domainBoundaries) > 2
|
|
1006
|
+
domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
|
|
1007
|
+
# Ensure domain endpoints don't overlap and their normals are consistent.
|
|
1008
|
+
if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \
|
|
1009
|
+
domainBoundaries[1].manifold._normal < 0.0:
|
|
1010
|
+
del domainBoundaries[0]
|
|
1011
|
+
if abs(domainBoundaries[-1].manifold._point - domainBoundaries[-2].manifold._point) < Manifold.minSeparation or \
|
|
1012
|
+
domainBoundaries[-2].manifold._normal > 0.0:
|
|
1013
|
+
del domainBoundaries[-1]
|
|
1014
|
+
slice.add_boundary(newBoundary)
|
|
1015
|
+
|
|
1016
|
+
if boundaryWasTouched:
|
|
1017
|
+
# Touch untouched boundaries that are connected to touched boundary endpoints and add them to slice.
|
|
1018
|
+
boundaryMap = ((2, 3, 0), (2, 3, -1), (0, 1, 0), (0, 1, -1)) # Map of which full domain boundaries touch each other
|
|
1019
|
+
while True:
|
|
1020
|
+
noTouches = True
|
|
1021
|
+
for map, newBoundary, bound in zip(boundaryMap, fullDomain.boundaries, bounds.flatten()):
|
|
1022
|
+
if not newBoundary.touched:
|
|
1023
|
+
leftBoundary = fullDomain.boundaries[map[0]]
|
|
1024
|
+
rightBoundary = fullDomain.boundaries[map[1]]
|
|
1025
|
+
if leftBoundary.touched and abs(leftBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
|
|
1026
|
+
newBoundary.touched = True
|
|
1027
|
+
slice.add_boundary(newBoundary)
|
|
1028
|
+
noTouches = False
|
|
1029
|
+
elif rightBoundary.touched and abs(rightBoundary.domain.boundaries[map[2]].manifold._point - bound) < Manifold.minSeparation:
|
|
1030
|
+
newBoundary.touched = True
|
|
1031
|
+
slice.add_boundary(newBoundary)
|
|
1032
|
+
noTouches = False
|
|
1033
|
+
if noTouches:
|
|
1034
|
+
break
|
|
1035
|
+
else:
|
|
1036
|
+
# No slice boundaries touched the full domain (a hole), so only add full domain if it is contained in the solid.
|
|
1037
|
+
if solid.contains_point(self.evaluate(bounds[:,0])):
|
|
1038
|
+
for newBoundary in fullDomain.boundaries:
|
|
1039
|
+
slice.add_boundary(newBoundary)
|
|
877
1040
|
|
|
878
|
-
|
|
1041
|
+
def full_domain(self):
|
|
1042
|
+
return Hyperplane.create_hypercube(self.domain())
|