bspy 4.1__py3-none-any.whl → 4.3__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.
@@ -4,6 +4,7 @@ import numpy as np
4
4
  from bspy.manifold import Manifold
5
5
  from bspy.hyperplane import Hyperplane
6
6
  import bspy.spline
7
+ import bspy.spline_block
7
8
  from bspy.solid import Solid, Boundary
8
9
  from collections import namedtuple
9
10
  from multiprocessing import Pool
@@ -75,14 +76,22 @@ def zeros_using_interval_newton(self):
75
76
  if derivativeBounds[0] * derivativeBounds[1] >= 0.0: # Refine interval
76
77
  projectedLeftStep = max(0.0, adjustedLeftStep)
77
78
  projectedRightStep = min(1.0, adjustedRightStep)
79
+ provisionalZero = [0.5 * (projectedLeftStep + projectedRightStep)]
78
80
  if projectedLeftStep <= projectedRightStep:
79
81
  if projectedRightStep - projectedLeftStep <= epsilon:
80
- myZeros = [0.5 * (projectedLeftStep + projectedRightStep)]
82
+ myZeros = provisionalZero
81
83
  else:
82
84
  trimmedSpline = mySpline.trim(((projectedLeftStep, projectedRightStep),))
83
85
  myZeros = refine(trimmedSpline, intervalSize, functionMax)
86
+ if len(myZeros) == 0 and mySpline.order[0] == mySpline.nCoef[0] and \
87
+ mySpline.coefs[0][0] * mySpline.coefs[0][-1] < 0.0:
88
+ myZeros = provisionalZero
84
89
  else:
85
- return []
90
+ if mySpline.order[0] == mySpline.nCoef[0] and \
91
+ mySpline.coefs[0][0] * mySpline.coefs[0][-1] < 0.0:
92
+ myZeros = provisionalZero
93
+ else:
94
+ return []
86
95
  else: # . . . or split as needed
87
96
  myZeros = []
88
97
  if adjustedLeftStep > 0.0:
@@ -117,220 +126,208 @@ def zeros_using_interval_newton(self):
117
126
  return mySolution
118
127
  return refine(spline, 1.0, 1.0)
119
128
 
120
- def _convex_hull_2D(xData, yData, yBounds, epsilon = 1.0e-8):
129
+ def _convex_hull_2D(xData, yData, yBounds, yOtherBounds):
121
130
  # Allow xData to be repeated for longer yData, but only if yData is a multiple.
122
131
  if not(yData.shape[0] % xData.shape[0] == 0): raise ValueError("Size of xData does not divide evenly in size of yData")
123
-
124
- # Assign (x0, y0) to the lowest point.
125
- yMinIndex = np.argmin(yData)
126
- x0 = xData[yMinIndex % xData.shape[0]]
127
- y0 = yData[yMinIndex]
132
+ yData = np.reshape(yData, (yData.shape[0] // xData.shape[0], xData.shape[0]))
128
133
 
129
134
  # Calculate y adjustment as needed for values close to zero
130
- yAdjustment = -yBounds[0] if yBounds[0] > 0.0 else -yBounds[1] if yBounds[1] < 0.0 else 0.0
131
- y0 += yAdjustment
132
-
133
- # Sort points by angle around p0.
134
- sortedPoints = []
135
- xIter = iter(xData)
136
- for y in yData:
137
- y += yAdjustment
138
- x = next(xIter, None)
139
- if x is None:
140
- xIter = iter(xData)
141
- x = next(xIter)
142
- sortedPoints.append((math.atan2(y - y0, x - x0), x, y))
143
- sortedPoints.sort()
144
-
145
- # Trim away points with the same angle (keep furthest point from p0), removing the angle from the list.
146
- trimmedPoints = [sortedPoints[0][1:]] # Ensure we keep the first point
147
- previousPoint = None
148
- previousDistance = -1.0
149
- for point in sortedPoints[1:]:
150
- if previousPoint is not None and abs(previousPoint[0] - point[0]) < epsilon:
151
- if previousDistance < 0.0:
152
- previousDistance = (previousPoint[1] - x0) ** 2 + (previousPoint[2] - y0) ** 2
153
- distance = (point[1] - x0) ** 2 + (point[2] - y0) ** 2
154
- if distance > previousDistance:
155
- trimmedPoints[-1] = point[1:]
156
- previousPoint = point
157
- previousDistance = distance
158
- else:
159
- trimmedPoints.append(point[1:])
160
- previousPoint = point
161
- previousDistance = -1.0
162
-
163
- # Build the convex hull by moving counterclockwise around trimmed sorted points.
164
- hullPoints = []
165
- for point in trimmedPoints:
166
- while len(hullPoints) > 1 and \
167
- (hullPoints[-1][0] - hullPoints[-2][0]) * (point[1] - hullPoints[-2][1]) - \
168
- (hullPoints[-1][1] - hullPoints[-2][1]) * (point[0] - hullPoints[-2][0]) <= 0.0:
169
- hullPoints.pop()
170
- hullPoints.append(point)
171
-
172
- return hullPoints
173
-
174
- def _intersect_convex_hull_with_x_interval(hullPoints, epsilon, xInterval):
175
- xMin = xInterval[1] + epsilon
176
- xMax = xInterval[0] - epsilon
177
- previousPoint = hullPoints[-1]
178
- for point in hullPoints:
179
- # Check for intersection with x axis.
180
- if previousPoint[1] * point[1] <= epsilon:
181
- determinant = point[1] - previousPoint[1]
182
- if abs(determinant) > epsilon:
183
- # Crosses x axis, determine intersection.
184
- x = previousPoint[0] - previousPoint[1] * (point[0] - previousPoint[0]) / determinant
185
- xMin = min(xMin, x)
186
- xMax = max(xMax, x)
187
- elif abs(point[1]) < epsilon:
188
- # Touches at endpoint. (Previous point is checked earlier.)
189
- xMin = min(xMin, point[0])
190
- xMax = max(xMax, point[0])
191
- previousPoint = point
192
-
193
- if xMin - epsilon > xInterval[1] or xMax + epsilon < xInterval[0]:
135
+ yMinAdjustment = -yBounds[0] if yBounds[0] > 0.0 else 0.0
136
+ yMaxAdjustment = -yBounds[1] if yBounds[1] < 0.0 else 0.0
137
+ yMinAdjustment += yOtherBounds[0]
138
+ yMaxAdjustment += yOtherBounds[1]
139
+
140
+ # Calculate the yMin and yMax arrays corresponding to xData
141
+ yMin = np.min(yData, axis = 0) + yMinAdjustment
142
+ yMax = np.max(yData, axis = 0) + yMaxAdjustment
143
+
144
+ # Initialize lower and upper hulls
145
+ lowerHull = [[xData[0], yMin[0]], [xData[1], yMin[1]]]
146
+ upperHull = [[xData[0], yMax[0]], [xData[1], yMax[1]]]
147
+
148
+ # Add additional lower points one at a time, throwing out intermediates if necessary
149
+ for xNext, yNext in zip(xData[2:], yMin[2:]):
150
+ lowerHull.append([xNext, yNext])
151
+ while len(lowerHull) > 2 and \
152
+ (lowerHull[-2][0] - lowerHull[-3][0]) * (lowerHull[-1][1] - lowerHull[-2][1]) <= \
153
+ (lowerHull[-1][0] - lowerHull[-2][0]) * (lowerHull[-2][1] - lowerHull[-3][1]):
154
+ del lowerHull[-2]
155
+
156
+ # Do the same for the upper points
157
+ for xNext, yNext in zip(xData[2:], yMax[2:]):
158
+ upperHull.append([xNext, yNext])
159
+ while len(upperHull) > 2 and \
160
+ (upperHull[-2][0] - upperHull[-3][0]) * (upperHull[-1][1] - upperHull[-2][1]) >= \
161
+ (upperHull[-1][0] - upperHull[-2][0]) * (upperHull[-2][1] - upperHull[-3][1]):
162
+ del upperHull[-2]
163
+
164
+ # Return the two hulls
165
+ return lowerHull, upperHull
166
+
167
+ def _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInterval):
168
+ xMin = xInterval[0]
169
+ xMax = xInterval[1]
170
+ sign = -1.0
171
+ for hull in [lowerHull, upperHull]:
172
+ sign = -sign
173
+ p0 = hull[0]
174
+ for p1 in hull[1:]:
175
+ yDelta = p0[1] - p1[1]
176
+ if p0[1] * p1[1] <= 0.0 and yDelta != 0.0:
177
+ yDelta = p0[1] - p1[1]
178
+ alpha = p0[1] / yDelta
179
+ xNew = p0[0] * (1.0 - alpha) + p1[0] * alpha
180
+ if sign * yDelta > 0.0:
181
+ xMin = max(xMin, xNew - epsilon)
182
+ else:
183
+ xMax = min(xMax, xNew + epsilon)
184
+ p0 = p1
185
+ if xMin > xMax:
194
186
  return None
195
187
  else:
196
- return (min(max(xMin, xInterval[0]), xInterval[1]), max(min(xMax, xInterval[1]), xInterval[0]))
197
-
198
- Interval = namedtuple('Interval', ('spline', 'unknowns', 'scale', 'slope', 'intercept', 'epsilon', 'atMachineEpsilon'))
188
+ return [xMin, xMax]
189
+
190
+ Interval = namedtuple('Interval', ('block', 'active', 'split', 'scale', 'bounds', 'xLeft', 'xRight', 'epsilon', 'atMachineEpsilon'))
191
+
192
+ def _create_interval(block, active, split, scale, xLeft, xRight, epsilon):
193
+ nDep = 0
194
+ nInd = len(scale)
195
+ bounds = np.zeros((nInd, 2), scale.dtype)
196
+ newScale = np.empty_like(scale)
197
+ newBlock = []
198
+ for row in block:
199
+ newRow = []
200
+ # Reparametrize splines and sum bounds
201
+ for map, spline in row:
202
+ spline = spline.reparametrize(((0.0, 1.0),) * spline.nInd)
203
+ bounds[nDep:nDep + spline.nDep] += spline.range_bounds()
204
+ newRow.append((map, spline))
205
+ newBlock.append(newRow)
206
+
207
+ # Check row bounds for potential roots.
208
+ for dep in range(spline.nDep):
209
+ coefsMin = bounds[nDep, 0] * scale[nDep]
210
+ coefsMax = bounds[nDep, 1] * scale[nDep]
211
+ if coefsMax < -epsilon or coefsMin > epsilon:
212
+ # No roots in this interval.
213
+ return None
214
+ newScale[nDep] = max(-coefsMin, coefsMax)
215
+ # Rescale spline coefficients to max 1.0.
216
+ rescale = 1.0 / max(-bounds[nDep, 0], bounds[nDep, 1])
217
+ for map, spline in newRow:
218
+ spline.coefs[dep] *= rescale
219
+ bounds[nDep] *= rescale
220
+ nDep += 1
221
+
222
+ for iInd in range(nInd):
223
+ newSplit = (split + iInd + 1) % nInd
224
+ if active[newSplit]:
225
+ return Interval(newBlock, active, newSplit, newScale, bounds, xLeft, xRight, epsilon, np.dot(xRight - xLeft, xRight - xLeft) < np.finfo(xLeft.dtype).eps)
226
+
227
+ # No active variables left
228
+ return None
199
229
 
200
230
  # We use multiprocessing.Pool to call this function in parallel, so it cannot be nested and must take a single argument.
201
231
  def _refine_projected_polyhedron(interval):
202
232
  Crit = 0.85 # Required percentage decrease in domain per iteration.
203
233
  epsilon = interval.epsilon
204
- evaluationEpsilon = np.sqrt(epsilon)
205
- machineEpsilon = np.finfo(interval.spline.coefs.dtype).eps
206
234
  roots = []
207
235
  intervals = []
208
-
209
- # Remove dependent variables that are near zero and compute newScale.
210
- spline = interval.spline.copy()
211
- bounds = spline.range_bounds()
212
- keepDep = []
213
- for nDep, (coefsMin, coefsMax) in enumerate(bounds * interval.scale):
214
- if coefsMax < -epsilon or coefsMin > epsilon:
215
- # No roots in this interval.
216
- return roots, intervals
217
- if coefsMin < -epsilon or coefsMax > epsilon:
218
- # Dependent variable not near zero for entire interval.
219
- keepDep.append(nDep)
220
-
221
- spline.nDep = len(keepDep)
222
- if spline.nDep == 0:
223
- # Return the interval center and radius.
224
- roots.append((interval.intercept + 0.5 * interval.slope, 0.5 * np.linalg.norm(interval.slope)))
225
- return roots, intervals
226
-
227
- # Rescale remaining spline coefficients to max 1.0.
228
- bounds = bounds[keepDep]
229
- newScale = np.abs(bounds).max()
230
- spline.coefs = spline.coefs[keepDep]
231
- spline.coefs *= 1.0 / newScale
232
- bounds *= 1.0 / newScale
233
- newScale *= interval.scale
234
236
 
235
- # Loop through each independent variable to determine a tighter domain around roots.
236
- domain = []
237
- coefs = spline.coefs
238
- for nInd, order, knots, nCoef, s in zip(range(spline.nInd), spline.order, spline.knots, spline.nCoef, interval.slope):
239
- # Move independent variable to the last (fastest) axis, adding 1 to account for the dependent variables.
240
- coefs = np.moveaxis(spline.coefs, nInd + 1, -1)
237
+ # Explore given independent variable to determine a tighter domain around roots.
238
+ xInterval = [0.0, 1.0]
239
+ iInd = interval.split
240
+ nDep = 0
241
+ for row in interval.block:
242
+ order = 0
243
+ for map, spline in row:
244
+ if iInd in map:
245
+ ind = map.index(iInd)
246
+ order = spline.order[ind]
247
+ # Move independent variable to the last (fastest) axis, adding 1 to account for the dependent variables.
248
+ coefs = np.moveaxis(spline.coefs, ind + 1, -1)
249
+ break
250
+
251
+ # Skip this row if it doesn't contain this independent variable.
252
+ if order < 1:
253
+ nDep += spline.nDep # Assumes there is at least one spline per block row
254
+ continue
241
255
 
242
256
  # Compute the coefficients for f(x) = x for the independent variable and its knots.
243
- degree = order - 1
244
- xData = np.empty((nCoef,), knots.dtype)
245
- xData[0] = knots[1]
246
- for i in range(1, nCoef):
247
- xData[i] = xData[i - 1] + (knots[i + degree] - knots[i])/degree
257
+ xData = spline.greville(ind)
248
258
 
249
- # Loop through each dependent variable to compute the interval containing the root for this independent variable.
250
- xInterval = (0.0, 1.0)
251
- for yData, yBounds in zip(coefs, bounds):
259
+ # Loop through each dependent variable in this row to refine the interval containing the root for this independent variable.
260
+ for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(),
261
+ interval.bounds[nDep:nDep + spline.nDep]):
252
262
  # Compute the 2D convex hull of the knot coefficients and the spline's coefficients
253
- hull = _convex_hull_2D(xData, yData.ravel(), yBounds, epsilon)
254
- if hull is None:
263
+ lowerHull, upperHull = _convex_hull_2D(xData, yData.ravel(), yBounds, yBounds - ySplineBounds)
264
+ if lowerHull is None or upperHull is None:
255
265
  return roots, intervals
256
266
 
257
267
  # Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
258
- xInterval = _intersect_convex_hull_with_x_interval(hull, epsilon, xInterval)
268
+ xInterval = _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInterval)
259
269
  if xInterval is None:
260
270
  return roots, intervals
261
-
262
- domain.append(xInterval)
271
+
272
+ nDep += spline.nDep
263
273
 
264
- # Compute new slope, intercept, and unknowns.
265
- domain = np.array(domain, spline.knots[0].dtype).T
266
- width = domain[1] - domain[0]
267
- newSlope = interval.slope.copy()
268
- newIntercept = interval.intercept.copy()
269
- newUnknowns = []
270
- newDomain = domain.copy()
271
- uvw = []
274
+ # Compute new interval bounds.
275
+
276
+ xNewLeft = interval.xLeft.copy()
277
+ xNewRight = interval.xRight.copy()
278
+ xNewLeft[iInd] = (1.0 - xInterval[0]) * interval.xLeft[iInd] + xInterval[0] * interval.xRight[iInd]
279
+ xNewRight[iInd] = (1.0 - xInterval[1]) * interval.xLeft[iInd] + xInterval[1] * interval.xRight[iInd]
280
+ newActive = interval.active.copy()
281
+ newActive[iInd] = (xNewRight[iInd] - xNewLeft[iInd] >= epsilon)
272
282
  nInd = 0
273
- for i, w, d in zip(interval.unknowns, width, domain.T):
274
- newSlope[i] = w * interval.slope[i]
275
- newIntercept[i] = d[0] * interval.slope[i] + interval.intercept[i]
276
- if newSlope[i] < epsilon:
277
- uvw.append(0.5 * (d[0] + d[1]))
278
- newDomain = np.delete(newDomain, nInd, axis=1)
279
- else:
280
- newUnknowns.append(i)
281
- uvw.append(None)
283
+ for active in newActive:
284
+ if active:
282
285
  nInd += 1
283
286
 
284
- # Iteration is complete if the interval actual width (slope) is either
285
- # one iteration past being less than sqrt(machineEpsilon) or there are no remaining unknowns.
286
- if interval.atMachineEpsilon or len(newUnknowns) == 0:
287
+ # Iteration is complete if the interval actual width is either
288
+ # one iteration past being less than sqrt(machineEpsilon) or there are no remaining independent variables.
289
+ if interval.atMachineEpsilon or nInd == 0:
287
290
  # Return the interval center and radius.
288
- roots.append((newIntercept + 0.5 * newSlope, epsilon))
291
+ roots.append((0.5 * (xNewLeft + xNewRight), epsilon))
289
292
  return roots, intervals
290
293
 
291
- # Contract spline as needed.
292
- spline = spline.contract(uvw)
293
-
294
- # Use interval newton for one-dimensional splines.
295
- if spline.nInd == 1 and spline.nDep == 1:
296
- i = newUnknowns[0]
297
- for root in zeros_using_interval_newton(spline):
298
- if not isinstance(root, tuple):
299
- root = (root, root)
300
- w = root[1] - root[0]
301
- slope = newSlope.copy()
302
- intercept = newIntercept.copy()
303
- slope[i] = w * interval.slope[i]
304
- intercept[i] = root[0] * interval.slope[i] + interval.intercept[i]
305
- # Return the interval center and radius.
306
- roots.append((intercept + 0.5 * slope, epsilon))
307
-
308
- return roots, intervals
294
+ # Split domain if not sufficient decrease in width
295
+ width = xInterval[1] - xInterval[0]
296
+ domains = [xInterval]
297
+ if width > Crit:
298
+ # Didn't get the required decrease in width, so split the domain.
299
+ leftDomain = xInterval
300
+ rightDomain = xInterval.copy()
301
+ leftDomain[1] = 0.5 * (leftDomain[0] + leftDomain[1])
302
+ rightDomain[0] = leftDomain[1]
303
+ domains = [leftDomain, rightDomain]
309
304
 
310
- # Split domain in dimensions that aren't decreasing in width sufficiently.
311
- width = newDomain[1] - newDomain[0]
312
- domains = [newDomain]
313
- for nInd, w in zip(range(spline.nInd), width):
314
- if w > Crit:
315
- # Didn't get the required decrease in width, so split the domain.
316
- domainCount = len(domains) # Cache the domain list size, since we're increasing it mid loop
317
- w *= 0.5 # Halve the domain width for this independent variable
318
- for i in range(domainCount):
319
- leftDomain = domains[i]
320
- rightDomain = leftDomain.copy()
321
- leftDomain[1][nInd] -= w # Alters domain in domains list
322
- rightDomain[0][nInd] += w
323
- domains.append(rightDomain)
324
-
325
305
  # Add new intervals to interval stack.
326
306
  for domain in domains:
327
- width = domain[1] - domain[0]
328
- splitSlope = newSlope.copy()
329
- splitIntercept = newIntercept.copy()
330
- for i, w, d in zip(newUnknowns, width, domain.T):
331
- splitSlope[i] = w * interval.slope[i]
332
- splitIntercept[i] = d[0] * interval.slope[i] + interval.intercept[i]
333
- intervals.append(Interval(spline.trim(domain.T).reparametrize(((0.0, 1.0),) * spline.nInd), newUnknowns, newScale, splitSlope, splitIntercept, epsilon, np.dot(splitSlope, splitSlope) < machineEpsilon))
307
+ xSplitLeft = xNewLeft.copy()
308
+ xSplitRight = xNewRight.copy()
309
+ xSplitLeft[iInd] = (1.0 - domain[0]) * interval.xLeft[iInd] + domain[0] * interval.xRight[iInd]
310
+ xSplitRight[iInd] = (1.0 - domain[1]) * interval.xLeft[iInd] + domain[1] * interval.xRight[iInd]
311
+ newBlock = []
312
+ for row in interval.block:
313
+ newRow = []
314
+ # Trim splines
315
+ for map, spline in row:
316
+ trimRegion = [(0.0, 1.0) for i in range(spline.nInd)]
317
+ if iInd in map:
318
+ ind = map.index(iInd)
319
+ trimRegion[ind] = domain
320
+ spline = spline.trim(trimRegion)
321
+ newRow.append((map, spline))
322
+ newBlock.append(newRow)
323
+ newInterval = _create_interval(newBlock, newActive, iInd,
324
+ interval.scale, xSplitLeft, xSplitRight, epsilon)
325
+ if newInterval:
326
+ if newInterval.block:
327
+ intervals.append(newInterval)
328
+ else:
329
+ roots.append((0.5 * (newInterval.xLeft + newInterval.xRight),
330
+ 0.5 * np.linalg.norm(newInterval.xRight - newInterval.xLeft)))
334
331
 
335
332
  return roots, intervals
336
333
 
@@ -340,33 +337,37 @@ class _Region:
340
337
  self.radius = radius
341
338
  self.count = count
342
339
 
343
- def zeros_using_projected_polyhedron(self, epsilon=None):
344
- if not(self.nInd == self.nDep): raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
345
- machineEpsilon = np.finfo(self.knots[0].dtype).eps
340
+ def zeros_using_projected_polyhedron(self, epsilon=None, initialScale=None):
341
+ if self.nInd != self.nDep: raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
342
+
343
+ # Determine epsilon and initialize roots.
344
+ machineEpsilon = np.finfo(self.knotsDtype).eps
346
345
  if epsilon is None:
347
346
  epsilon = 0.0
348
- epsilon = max(epsilon, np.sqrt(machineEpsilon))
349
- evaluationEpsilon = np.sqrt(epsilon)
347
+ epsilon = max(epsilon, np.sqrt(machineEpsilon)) if epsilon else np.sqrt(machineEpsilon)
348
+ evaluationEpsilon = max(np.sqrt(epsilon), np.finfo(self.coefsDtype).eps ** 0.25)
349
+ intervals = []
350
350
  roots = []
351
351
 
352
- # Set initial spline, domain, and interval.
352
+ # Set initial interval.
353
353
  domain = self.domain().T
354
- intervals = [Interval(self.trim(domain.T).reparametrize(((0.0, 1.0),) * self.nInd), [*range(self.nInd)], 1.0, domain[1] - domain[0], domain[0], epsilon, False)]
355
- chunkSize = 8
356
- #pool = Pool() # Pool size matches CPU count
354
+ initialScale = np.full(self.nDep, 1.0, self.coefsDtype) if initialScale is None else np.array(initialScale, self.coefsDtype)
355
+ newInterval = _create_interval(self.block, self.nInd * [True], -1, initialScale,
356
+ domain[0], domain[1], epsilon)
357
+ if newInterval:
358
+ if newInterval.block:
359
+ intervals.append(newInterval)
360
+ else:
361
+ roots.append(0.5 * (newInterval.xLeft + newInterval.xRight),
362
+ 0.5 * np.linalg.norm(newInterval.xRight - newInterval.xLeft))
357
363
 
358
364
  # Refine all the intervals, collecting roots as we go.
359
365
  while intervals:
360
- nextIntervals = []
361
- if False and len(intervals) > chunkSize:
362
- for (newRoots, newIntervals) in pool.imap_unordered(_refine_projected_polyhedron, intervals, chunkSize):
363
- roots += newRoots
364
- nextIntervals += newIntervals
365
- else:
366
- for (newRoots, newIntervals) in map(_refine_projected_polyhedron, intervals):
367
- roots += newRoots
368
- nextIntervals += newIntervals
369
- intervals = nextIntervals
366
+ interval = intervals.pop()
367
+ newRoots, newIntervals = _refine_projected_polyhedron(interval)
368
+ roots += newRoots
369
+ newIntervals.reverse()
370
+ intervals += newIntervals
370
371
 
371
372
  # Combine overlapping roots into regions.
372
373
  regions = []
@@ -375,16 +376,33 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
375
376
  rootCenter = root[0]
376
377
  rootRadius = root[1]
377
378
 
379
+ # Take one Newton step on each root
380
+ value = self.evaluate(rootCenter)
381
+ residualNorm = np.linalg.norm(value)
382
+ try:
383
+ update = np.linalg.solve(self.jacobian(rootCenter), value)
384
+ if np.linalg.norm(update) < rootRadius:
385
+ rootCenter -= update
386
+ except:
387
+ pass
388
+
389
+ # Project back onto spline domain
390
+ selfDomain = self.domain()
391
+ rootCenter = np.maximum(np.minimum(rootCenter, selfDomain.T[1]), selfDomain.T[0])
392
+ value = self.evaluate(rootCenter)
393
+ newResidualNorm = np.linalg.norm(value)
394
+ rootRadius *= newResidualNorm / residualNorm
395
+ residualNorm = newResidualNorm
396
+
378
397
  # Ensure we have a real root (not a boundary special case).
379
- if np.linalg.norm(self(rootCenter)) >= evaluationEpsilon:
398
+ if residualNorm >= evaluationEpsilon:
380
399
  continue
381
400
 
382
401
  # Expand the radius of the root based on the approximate distance from the center needed
383
402
  # to raise the value of the spline above evaluationEpsilon.
384
- jacobian = self.jacobian(rootCenter)
385
- minEigenvalue = np.sqrt(np.linalg.eigvalsh(jacobian.T @ jacobian)[0])
386
- if minEigenvalue > epsilon:
387
- rootRadius = max(rootRadius, evaluationEpsilon / minEigenvalue)
403
+ minSingularValue = np.linalg.svd(self.jacobian(rootCenter), False, False)[-1]
404
+ if minSingularValue > epsilon:
405
+ rootRadius = max(rootRadius, evaluationEpsilon / minSingularValue)
388
406
 
389
407
  # Intersect this root with the existing regions, expanding and combining them as appropriate.
390
408
  firstRegion = None
@@ -418,48 +436,68 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
418
436
 
419
437
  return roots
420
438
 
421
- def contours(self):
422
- if self.nInd - self.nDep != 1: raise ValueError("The number of free variables (self.nInd - self.nDep) must be one.")
439
+ def _turning_point_determinant(self, uvw, cosTheta, sinTheta):
440
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
441
+ tangentSpace = self.jacobian(uvw).T
442
+ return cosTheta * sign * np.linalg.det(tangentSpace[[j for j in range(self.nInd) if j != 0]]) - \
443
+ sinTheta * sign * np.linalg.det(tangentSpace[[j for j in range(self.nInd) if j != 1]])
444
+
445
+ def _turning_point_determinant_gradient(self, uvw, cosTheta, sinTheta):
446
+ dtype = self.coefs.dtype if hasattr(self, "coefs") else self.coefsDtype
447
+ gradient = np.zeros(self.nInd, dtype)
448
+
449
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
450
+ tangentSpace = self.jacobian(uvw).T
451
+ dTangentSpace = tangentSpace.copy()
452
+
453
+ wrt = [0] * self.nInd
454
+ for i in range(self.nInd):
455
+ wrt[i] = 1
456
+ for j in range(self.nInd):
457
+ wrt[j] = 1 if i != j else 2
458
+ dTangentSpace[j, :] = self.derivative(wrt, uvw) # tangentSpace and dTangentSpace are the transpose of the jacobian
459
+ gradient[i] += cosTheta * sign * np.linalg.det(dTangentSpace[[k for k in range(self.nInd) if k != 0]]) - \
460
+ sinTheta * sign * np.linalg.det(dTangentSpace[[k for k in range(self.nInd) if k != 1]])
461
+ dTangentSpace[j, :] = tangentSpace[j, :] # tangentSpace and dTangentSpace are the transpose of the jacobian
462
+ wrt[j] = 0 if i != j else 1
463
+ wrt[i] = 0
464
+
465
+ return gradient
423
466
 
467
+ def _contours_of_C1_spline_block(self, epsilon, evaluationEpsilon):
424
468
  Point = namedtuple('Point', ('d', 'det', 'onUVBoundary', 'turningPoint', 'uvw'))
425
- epsilon = np.sqrt(np.finfo(self.coefs.dtype).eps)
426
- evaluationEpsilon = np.sqrt(epsilon)
427
-
428
- # Go through each nDep of the spline, checking bounds.
429
- for coefs in self.coefs:
430
- coefsMin = coefs.min()
431
- coefsMax = coefs.max()
432
- if coefsMax < -evaluationEpsilon or coefsMin > evaluationEpsilon:
469
+
470
+ # Go through each nDep of the spline block, checking bounds.
471
+ bounds = self.range_bounds()
472
+ for bound in bounds:
473
+ if bound[1] < -evaluationEpsilon or bound[0] > evaluationEpsilon:
433
474
  # No contours for this spline.
434
475
  return []
435
476
 
436
477
  # Record self's original domain and then reparametrize self's domain to [0, 1]^nInd.
437
478
  domain = self.domain().T
438
479
  self = self.reparametrize(((0.0, 1.0),) * self.nInd)
439
-
440
- # Construct self's tangents and normal.
441
- tangents = []
442
- for nInd in range(self.nInd):
443
- tangents.append(self.differentiate(nInd))
444
- normal = self.normal_spline((0, 1)) # We only need the first two indices
445
-
446
- theta = np.sqrt(2) # Arbitrary starting value for theta (picked one in [0, pi/2] unlikely to be a stationary point)
447
- # Try different theta values until no border or turning points are degenerate or we run out of attempts.
448
- attempts = 3
449
- while attempts > 0:
480
+
481
+ # Rescale self in all dimensions.
482
+ initialScale = np.max(np.abs(bounds), axis=1)
483
+ rescale = np.reciprocal(initialScale)
484
+ nDep = 0
485
+ for row in self.block:
486
+ for map, spline in row:
487
+ for coefs, scale in zip(spline.coefs, rescale[nDep:nDep + spline.nDep]):
488
+ coefs *= scale
489
+ nDep += spline.nDep
490
+
491
+ # Try arbitrary values for theta between [0, pi/2] that are unlikely to be a stationary points.
492
+ for theta in (1.0 / np.sqrt(2), np.pi / 6.0, 1.0/ np.e):
450
493
  points = []
451
- theta *= 0.607
452
494
  cosTheta = np.cos(theta)
453
495
  sinTheta = np.sin(theta)
454
496
  abort = False
455
- attempts -=1
456
-
457
- # Construct the turning point determinant.
458
- turningPointDeterminant = normal.dot((cosTheta, sinTheta))
459
497
 
460
498
  # Find intersections with u and v boundaries.
461
499
  def uvIntersections(nInd, boundary):
462
- zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros()
500
+ zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros(epsilon, initialScale)
463
501
  abort = False
464
502
  for zero in zeros:
465
503
  if isinstance(zero, tuple):
@@ -467,10 +505,24 @@ def contours(self):
467
505
  break
468
506
  uvw = np.insert(np.array(zero), nInd, boundary)
469
507
  d = uvw[0] * cosTheta + uvw[1] * sinTheta
470
- det = (0.5 - boundary) * normal(uvw)[nInd] * turningPointDeterminant(uvw)
508
+ n = self.normal(uvw, False, (0, 1))
509
+ tpd = _turning_point_determinant(self, uvw, cosTheta, sinTheta)
510
+ det = (0.5 - boundary) * n[nInd] * tpd
471
511
  if abs(det) < epsilon:
472
512
  abort = True
473
513
  break
514
+ # Check for literal corner case.
515
+ otherInd = 1 - nInd
516
+ otherValue = uvw[otherInd]
517
+ if otherValue < epsilon or otherValue + epsilon > 1.0:
518
+ otherDet = (0.5 - otherValue) * n[otherInd] * tpd
519
+ if det * otherDet < 0.0:
520
+ continue # Corner that starts and ends, ignore it
521
+ elif max(otherValue, boundary) < epsilon and det < 0.0:
522
+ continue # End point at (0, 0), ignore it
523
+ elif min(otherValue, boundary) + epsilon > 1.0 and det > 0.0:
524
+ continue # Start point at (1, 1), ignore it
525
+ # Append boundary point.
474
526
  points.append(Point(d, det, True, False, uvw))
475
527
  return abort
476
528
  for nInd in range(2):
@@ -485,7 +537,7 @@ def contours(self):
485
537
 
486
538
  # Find intersections with other boundaries.
487
539
  def otherIntersections(nInd, boundary):
488
- zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros()
540
+ zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros(epsilon, initialScale)
489
541
  abort = False
490
542
  for zero in zeros:
491
543
  if isinstance(zero, tuple):
@@ -494,12 +546,13 @@ def contours(self):
494
546
  uvw = np.insert(np.array(zero), nInd, boundary)
495
547
  d = uvw[0] * cosTheta + uvw[1] * sinTheta
496
548
  columns = np.empty((self.nDep, self.nInd - 1))
549
+ tangents = self.jacobian(uvw).T
497
550
  i = 0
498
551
  for j in range(self.nInd):
499
552
  if j != nInd:
500
- columns[:, i] = tangents[j](uvw)
553
+ columns[:, i] = tangents[j]
501
554
  i += 1
502
- duv = np.linalg.solve(columns, -tangents[nInd](uvw))
555
+ duv = np.linalg.solve(columns, -tangents[nInd])
503
556
  det = np.arctan2((0.5 - boundary) * (duv[0] * cosTheta + duv[1] * sinTheta), (0.5 - boundary) * (duv[0] * cosTheta - duv[1] * sinTheta))
504
557
  if abs(det) < epsilon:
505
558
  abort = True
@@ -517,22 +570,79 @@ def contours(self):
517
570
  continue # Try a different theta
518
571
 
519
572
  # Find turning points by combining self and turningPointDeterminant into a system and processing its zeros.
520
- systemSelf, systemTurningPointDeterminant = bspy.Spline.common_basis((self, turningPointDeterminant))
521
- system = type(systemSelf)(self.nInd, self.nInd, systemSelf.order, systemSelf.nCoef, systemSelf.knots, \
522
- np.concatenate((systemSelf.coefs, systemTurningPointDeterminant.coefs)), systemSelf.metadata)
523
- zeros = system.zeros()
573
+
574
+ # First, add the null space constraint to the system: dot(self's gradient, (r * sinTheta, -r * cosTheta, c, d, ...) = 0.
575
+ # This introduces self.nInd - 1 new independent variables: r, c, d, ...
576
+ turningPointBlock = self.block.copy()
577
+ if self.nInd > 2:
578
+ rSpline = bspy.Spline(1, 1, (2,), (2,), ((0.0, 0.0, 1.0, 1.0),), ((0.0, 1.0),))
579
+ else:
580
+ rSpline = bspy.Spline.point([1.0])
581
+ otherSpline = bspy.Spline(1, 1, (2,), (2,), ((-1.0, -1.0, 1.0, 1.0),), ((-1.0, 1.0),))
582
+ # Track indices of other independent variables (c, d, ...).
583
+ otherNInd = self.nInd + 1 # Add one since r is always the first new variable (index for r is self.nInd)
584
+ otherDictionary = {}
585
+ # Go through each row building the null space constraint.
586
+ for row in self.block:
587
+ newRow = []
588
+ for map, spline in row:
589
+ newSpline = None # The spline's portion of the null space constraint starts with None
590
+ newMap = map.copy() # The map for spline's contribution to the null space constraint starts with its existing map
591
+ # Create addition indMap with existing independent variables for use in summing the dot product.
592
+ indMapForAdd = [(index, index) for index in range(spline.nInd)]
593
+ rIndex = None # Index of r in newSpline, which we need to track since rSpline may be added twice
594
+
595
+ # Add each term of spline's contribution to dot(self's gradient, (r * sinTheta, -r * cosTheta, c, d, ...).
596
+ for i in range(spline.nInd):
597
+ dSpline = spline.differentiate(i)
598
+ nInd = map[i]
599
+ if nInd < 2:
600
+ factor = sinTheta if nInd == 0 else -cosTheta
601
+ term = dSpline.multiply(factor * rSpline)
602
+ if rIndex is None:
603
+ # Adding rSpline for the first time, so add r to newMap and track its index.
604
+ newMap.append(self.nInd)
605
+ newSpline = term if newSpline is None else newSpline.add(term, indMapForAdd)
606
+ rIndex = newSpline.nInd - 1
607
+ else:
608
+ # The same rSpline is being added again, so enhance the indMapForAdd to associate the two rSplines.
609
+ newSpline = newSpline.add(term, indMapForAdd + [(rIndex, term.nInd - 1)])
610
+ else:
611
+ if nInd not in otherDictionary:
612
+ otherDictionary[nInd] = otherNInd
613
+ otherNInd += 1
614
+ newMap.append(otherDictionary[nInd])
615
+ term = dSpline.multiply(otherSpline)
616
+ newSpline = term if newSpline is None else newSpline.add(term, indMapForAdd)
617
+
618
+ newMap = newMap[:newSpline.nInd]
619
+ newRow.append((newMap, newSpline))
620
+ turningPointBlock.append(newRow)
621
+
622
+ # Second, add unit vector constrain to the system.
623
+ # r^2 + c^2 + d^2 + ... = 1
624
+ rSquaredMinus1 = bspy.Spline(1, 1, (3,), (3,), ((0.0, 0.0, 0.0, 1.0, 1.0, 1.0),), ((-1.0, -1.0, 0.0),))
625
+ otherSquared = bspy.Spline(1, 1, (3,), (3,), ((-1.0, -1.0, -1.0, 1.0, 1.0, 1.0),), ((1.0, -1.0, 1.0),))
626
+ newRow = [((self.nInd,), rSquaredMinus1)]
627
+ assert otherNInd == 2 * self.nInd - 1
628
+ for nInd in range(self.nInd + 1, otherNInd):
629
+ newRow.append(((nInd,), otherSquared))
630
+ if self.nInd > 2:
631
+ turningPointBlock.append(newRow)
632
+ if self.nDep > 1:
633
+ turningPointInitialScale = np.append(initialScale, (1.0,) * (self.nDep + 1))
634
+ else:
635
+ turningPointInitialScale = np.append(initialScale, (1.0,))
636
+
637
+ # Finally, find the zeros of the system (only the first self.nInd values are of interest).
638
+ zeros = bspy.spline_block.SplineBlock(turningPointBlock).zeros(epsilon, turningPointInitialScale)
524
639
  for uvw in zeros:
525
640
  if isinstance(uvw, tuple):
526
641
  abort = True
527
642
  break
643
+ uvw = uvw[:self.nInd] # Remove any new independent variables added by the turning point system
528
644
  d = uvw[0] * cosTheta + uvw[1] * sinTheta
529
- n = self.normal(uvw, False) # Computing all indices of the normal this time
530
- wrt = [0] * self.nInd
531
- det = 0.0
532
- for nInd in range(self.nInd):
533
- wrt[nInd] = 1
534
- det += turningPointDeterminant.derivative(wrt, uvw) * n[nInd]
535
- wrt[nInd] = 0
645
+ det = np.dot(self.normal(uvw, False), _turning_point_determinant_gradient(self, uvw, cosTheta, sinTheta))
536
646
  if abs(det) < epsilon:
537
647
  abort = True
538
648
  break
@@ -540,7 +650,7 @@ def contours(self):
540
650
  if not abort:
541
651
  break # We're done!
542
652
 
543
- if attempts <= 0: raise ValueError("No contours. Degenerate equations.")
653
+ if abort: raise ValueError("No contours. Degenerate equations.")
544
654
 
545
655
  if not points:
546
656
  return [] # No contours
@@ -553,17 +663,14 @@ def contours(self):
553
663
  # a panel boundary: u * cosTheta + v * sinTheta = d. Basically, we add this panel boundary plane
554
664
  # to the contour condition. We'll define it for d = 0, and add the actual d later.
555
665
  # We didn't construct the panel system earlier, because we didn't have theta.
556
- panelCoefs = np.empty((self.nDep + 1, *self.coefs.shape[1:]), self.coefs.dtype) # Note that self.nDep + 1 == self.nInd
557
- panelCoefs[:self.nDep] = self.coefs
558
- # The following value should be -d. We're setting it for d = 0 to start.
559
- panelCoefs[self.nDep, 0, 0] = 0.0
560
- degree = self.order[0] - 1
561
- for i in range(1, self.nCoef[0]):
562
- panelCoefs[self.nDep, i, 0] = panelCoefs[self.nDep, i - 1, 0] + ((self.knots[0][degree + i] - self.knots[0][i]) / degree) * cosTheta
563
- degree = self.order[1] - 1
564
- for i in range(1, self.nCoef[1]):
565
- panelCoefs[self.nDep, :, i] = panelCoefs[self.nDep, :, i - 1] + ((self.knots[1][degree + i] - self.knots[1][i]) / degree) * sinTheta
566
- panel = type(self)(self.nInd, self.nInd, self.order, self.nCoef, self.knots, panelCoefs, self.metadata)
666
+ panelCoefs = np.array((((0.0, sinTheta), (cosTheta, cosTheta + sinTheta)),), self.coefsDtype)
667
+ panelSpline = bspy.spline.Spline(2, 1, (2, 2), (2, 2),
668
+ (np.array((0.0, 0.0, 1.0, 1.0), self.knotsDtype), np.array((0.0, 0.0, 1.0, 1.0), self.knotsDtype)),
669
+ panelCoefs)
670
+ panelBlock = self.block.copy()
671
+ panelBlock.append([panelSpline])
672
+ panel = bspy.spline_block.SplineBlock(panelBlock)
673
+ panelInitialScale = np.append(initialScale, 1.0)
567
674
 
568
675
  # Okay, we have everything we need to determine the contour topology and points along each contour.
569
676
  # We've done the first two steps of Grandine and Klein's algorithm:
@@ -574,10 +681,26 @@ def contours(self):
574
681
  # (3) Take all the points found in Step (1) and Step (2) and order them by distance in the theta direction from the origin.
575
682
  points.sort()
576
683
 
684
+ # Extra step not in paper.
685
+ # Remove duplicate points (typically appear at corners).
686
+ i = 0
687
+ while i < len(points):
688
+ previousPoint = points[i]
689
+ j = i + 1
690
+ while j < len(points):
691
+ point = points[j]
692
+ if point.d < previousPoint.d + epsilon:
693
+ if np.linalg.norm(point.uvw - previousPoint.uvw) < epsilon:
694
+ del points[j]
695
+ else:
696
+ j += 1
697
+ else:
698
+ break
699
+ i += 1
700
+
577
701
  # Extra step not in paper.
578
702
  # Run a checksum on the points, ensuring starting and ending points balance.
579
703
  # Start by flipping endpoints as needed, since we can miss turning points near endpoints.
580
-
581
704
  if points[0].det < 0.0:
582
705
  point = points[0]
583
706
  points[0] = Point(point.d, -point.det, point.onUVBoundary, point.turningPoint, point.uvw)
@@ -651,11 +774,11 @@ def contours(self):
651
774
  # points. Either insert two new contours in the list or delete two existing ones from
652
775
  # the list. Go back to Step (5).
653
776
  # First, construct panel, whose zeros lie along the panel boundary, u * cosTheta + v * sinTheta - d = 0.
654
- panel.coefs[self.nDep] -= point.d
777
+ panelSpline.coefs = panelCoefs - point.d
655
778
 
656
779
  if point.turningPoint and point.uvw is None:
657
780
  # For an inserted panel between two consecutive turning points, just find zeros along the panel.
658
- panelPoints = panel.zeros()
781
+ panelPoints = panel.zeros(epsilon, panelInitialScale)
659
782
  elif point.turningPoint:
660
783
  # Split panel below and above the known zero point.
661
784
  # This avoids extra computation and the high-zero at the known zero point, while ensuring we match the turning point.
@@ -677,15 +800,15 @@ def contours(self):
677
800
  np.linalg.norm(selfUU * sinTheta * sinTheta - 2.0 * selfUV * sinTheta * cosTheta + selfVV * cosTheta * cosTheta))
678
801
  # Now, we can find the zeros of the split panel, checking to ensure each panel is within bounds first.
679
802
  if point.uvw[0] + sinTheta * offset < 1.0 - epsilon and epsilon < point.uvw[1] - cosTheta * offset:
680
- panelPoints += panel.trim(((point.uvw[0] + sinTheta * offset, 1.0), (0.0, point.uvw[1] - cosTheta * offset)) + ((None, None),) * (self.nInd - 2)).zeros()
803
+ panelPoints += panel.trim(((point.uvw[0] + sinTheta * offset, 1.0), (0.0, point.uvw[1] - cosTheta * offset)) + ((None, None),) * (self.nInd - 2)).zeros(epsilon, panelInitialScale)
681
804
  expectedPanelPoints -= len(panelPoints) - 1 # Discount the turning point itself
682
805
  if expectedPanelPoints > 0 and epsilon < point.uvw[0] - sinTheta * offset and point.uvw[1] + cosTheta * offset < 1.0 - epsilon:
683
- panelPoints += panel.trim(((0.0, point.uvw[0] - sinTheta * offset), (point.uvw[1] + cosTheta * offset, 1.0)) + ((None, None),) * (self.nInd - 2)).zeros()
806
+ panelPoints += panel.trim(((0.0, point.uvw[0] - sinTheta * offset), (point.uvw[1] + cosTheta * offset, 1.0)) + ((None, None),) * (self.nInd - 2)).zeros(epsilon, panelInitialScale)
684
807
  else: # It's an other-boundary point.
685
808
  # Only find extra zeros along the panel if any are expected (> 0 for starting point, > 1 for ending one).
686
809
  expectedPanelPoints = len(currentContourPoints) - (0 if point.det > 0.0 else 1)
687
810
  if expectedPanelPoints > 0:
688
- panelPoints = panel.zeros()
811
+ panelPoints = panel.zeros(epsilon, panelInitialScale)
689
812
  panelPoints.sort(key=lambda uvw: np.linalg.norm(point.uvw - uvw)) # Sort by distance from boundary point
690
813
  while len(panelPoints) > expectedPanelPoints:
691
814
  panelPoints.pop(0) # Drop points closest to the boundary point
@@ -693,8 +816,6 @@ def contours(self):
693
816
  else:
694
817
  panelPoints = [point.uvw]
695
818
 
696
- # Add d back to prepare for next turning point.
697
- panel.coefs[self.nDep] += point.d
698
819
  # Sort zero points by their position along the panel boundary (using vector orthogonal to its normal).
699
820
  panelPoints.sort(key=lambda uvw: uvw[1] * cosTheta - uvw[0] * sinTheta)
700
821
  # Go through panel points, adding them to existing contours, creating new ones, or closing old ones.
@@ -781,6 +902,45 @@ def contours(self):
781
902
 
782
903
  return splineContours
783
904
 
905
+ def contours(self):
906
+ if self.nInd - self.nDep != 1: raise ValueError("The number of free variables (self.nInd - self.nDep) must be one.")
907
+ epsilon = np.sqrt(np.finfo(self.knotsDtype).eps)
908
+ evaluationEpsilon = max(np.sqrt(epsilon), np.finfo(self.coefsDtype).eps ** 0.25)
909
+
910
+ # Split the splines in the block to ensure C1 continuity within each block
911
+ blocks = self.split(minContinuity=1).ravel()
912
+
913
+ # For each block, find its contours and join them to the contours from previous blocks.
914
+ contours = []
915
+ for block in blocks:
916
+ splineContours = _contours_of_C1_spline_block(block, epsilon, evaluationEpsilon)
917
+ for newContour in splineContours:
918
+ newStart = newContour(0.0)
919
+ newFinish = newContour(1.0)
920
+ joined = False
921
+ for i, oldContour in enumerate(contours):
922
+ oldStart = oldContour(0.0)
923
+ oldFinish = oldContour(1.0)
924
+ if np.linalg.norm(newStart - oldFinish) < evaluationEpsilon:
925
+ contours[i] = bspy.Spline.join((oldContour, newContour))
926
+ joined = True
927
+ break
928
+ if np.linalg.norm(newStart - oldStart) < evaluationEpsilon:
929
+ contours[i] = bspy.Spline.join((oldContour, newContour.reverse()))
930
+ joined = True
931
+ break
932
+ if np.linalg.norm(newFinish - oldStart) < evaluationEpsilon:
933
+ contours[i] = bspy.Spline.join((newContour, oldContour))
934
+ joined = True
935
+ break
936
+ if np.linalg.norm(newFinish - oldFinish) < evaluationEpsilon:
937
+ contours[i] = bspy.Spline.join((newContour, oldContour.reverse()))
938
+ joined = True
939
+ break
940
+ if not joined:
941
+ contours.append(newContour)
942
+ return contours
943
+
784
944
  def intersect(self, other):
785
945
  intersections = []
786
946
  nDep = self.nInd # The dimension of the intersection's range
@@ -850,13 +1010,13 @@ def intersect(self, other):
850
1010
 
851
1011
  # Spline-Spline intersection.
852
1012
  elif isinstance(other, bspy.Spline):
853
- # Construct a new spline that represents the intersection.
854
- spline = self.subtract(other)
1013
+ # Construct a spline block that represents the intersection.
1014
+ block = bspy.spline_block.SplineBlock([[self, -other]])
855
1015
 
856
1016
  # Curve-Curve intersection.
857
1017
  if nDep == 1:
858
1018
  # Find the intersection points and intervals.
859
- zeros = spline.zeros()
1019
+ zeros = block.zeros()
860
1020
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
861
1021
  for zero in zeros:
862
1022
  if isinstance(zero, tuple):
@@ -895,28 +1055,27 @@ def intersect(self, other):
895
1055
  # Surface-Surface intersection.
896
1056
  elif nDep == 2:
897
1057
  if "Name" in self.metadata and "Name" in other.metadata:
898
- logging.info(f"intersect({self.metadata['Name']}, {other.metadata['Name']})")
1058
+ logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
899
1059
  # Find the intersection contours, which are returned as splines.
900
1060
  swap = False
901
1061
  try:
902
1062
  # First try the intersection as is.
903
- contours = spline.contours()
904
- except ValueError:
1063
+ contours = block.contours()
1064
+ except ValueError as e:
1065
+ logging.info(e)
905
1066
  # If that fails, swap the manifolds. Worth a shot since intersections are touchy.
906
- swap = True
907
-
908
- # Convert each contour into a Manifold.Crossing.
909
- if swap:
910
- spline = other.subtract(self)
1067
+ block = bspy.spline_block.SplineBlock([[other, -self]])
911
1068
  if "Name" in self.metadata and "Name" in other.metadata:
912
- logging.info(f"intersect({other.metadata['Name']}, {self.metadata['Name']})")
913
- contours = spline.contours()
1069
+ logging.info(f"intersect:{other.metadata['Name']}:{self.metadata['Name']}")
1070
+ contours = block.contours()
1071
+ # Convert each contour into a Manifold.Crossing, swapping the manifolds back.
914
1072
  for contour in contours:
915
1073
  # Swap left and right, compared to not swapped.
916
1074
  left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
917
1075
  right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
918
1076
  intersections.append(Manifold.Crossing(left, right))
919
1077
  else:
1078
+ # Convert each contour into a Manifold.Crossing.
920
1079
  for contour in contours:
921
1080
  left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
922
1081
  right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
@@ -950,7 +1109,7 @@ def complete_slice(self, slice, solid):
950
1109
  if not slice.boundaries:
951
1110
  if slice.dimension == 2:
952
1111
  if "Name" in self.metadata:
953
- logging.info(f"check containment: {self.metadata['Name']}")
1112
+ logging.info(f"check containment:{self.metadata['Name']}")
954
1113
  domain = bounds.T
955
1114
  if solid.contains_point(self(0.5 * (domain[0] + domain[1]))):
956
1115
  for boundary in Hyperplane.create_hypercube(bounds).boundaries: