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 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.spline` : Provides the `Spline` class that models, represents, and processes piecewise polynomial tensor product
7
- functions (spline functions) as linear combinations of B-splines.
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 = []
@@ -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 = np.empty((self.nInd, self.nDep), self.coefs.dtype)
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
- z.append(0.0)
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
- rhos, gaussWeights = _legendre_polynomial_zeros(degree - 1)
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 rho in reversed(rhos):
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))
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 rho in reversed(rhos):
257
- tValues[i] = t = 0.5 * (previousKnot + newKnot - rho * (newKnot - previousKnot))
258
- GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
259
- FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
260
- i += 1
261
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
262
- tValues[i] = t = 0.5 * (previousKnot + newKnot + rho * (newKnot - previousKnot))
263
- GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
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[0, 0] = max(uvDomain[0, 0], min(uvDomain[0, 1], u[0, 0]))
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
- R_uuu = suu @ su
427
- R_uuv = suu @ sv
428
- R_uvu = suv @ su
429
- R_uvv = suv @ sv
430
- R_vvu = svv @ su
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
- rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
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
- alpha = 0.5 * (1.0 - gaussNode)
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
- continuationBest = bestGuess
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 = continuationBest
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
- continuationBest = bestGuess
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
@@ -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.manifold import Manifold, Hyperplane
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.spline.Spline.contour(self, points)
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
- if isinstance(other, bspy.Spline):
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.boundaries.append(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
819
- left.boundaries.append(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
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.boundaries.append(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
822
- right.boundaries.append(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
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
- #logging.info(f"intersect_manifold({self.metadata['Name']}, {other.metadata['Name']})")
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
- #logging.info(f"intersect_manifold({other.metadata['Name']}, {self.metadata['Name']})")
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
- if np.dot(self.tangent_space(intersection.left.point(domainPoint)) @ intersection.left.normal(domainPoint), other.normal(intersection.right.point(domainPoint))) < 0.0:
873
- intersection.left.flip_normal()
874
- if np.dot(other.tangent_space(intersection.right.point(domainPoint)) @ intersection.right.normal(domainPoint), self.normal(intersection.left.point(domainPoint))) < 0.0:
875
- intersection.right.flip_normal()
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
- return intersections
1041
+ def full_domain(self):
1042
+ return Hyperplane.create_hypercube(self.domain())