bspy 4.1__py3-none-any.whl → 4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bspy/__init__.py +3 -0
- bspy/_spline_domain.py +92 -9
- bspy/_spline_evaluation.py +79 -20
- bspy/_spline_fitting.py +201 -45
- bspy/_spline_intersection.py +284 -137
- bspy/_spline_operations.py +63 -48
- bspy/hyperplane.py +6 -6
- bspy/manifold.py +2 -2
- bspy/solid.py +15 -12
- bspy/spline.py +180 -50
- bspy/spline_block.py +343 -0
- bspy/viewer.py +7 -6
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/METADATA +11 -5
- bspy-4.2.dist-info/RECORD +18 -0
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/WHEEL +1 -1
- bspy-4.1.dist-info/RECORD +0 -17
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/LICENSE +0 -0
- {bspy-4.1.dist-info → bspy-4.2.dist-info}/top_level.txt +0 -0
bspy/_spline_intersection.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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,18 +126,19 @@ 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, epsilon = 1.0e-8):
|
|
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
132
|
|
|
124
133
|
# Assign (x0, y0) to the lowest point.
|
|
125
134
|
yMinIndex = np.argmin(yData)
|
|
126
135
|
x0 = xData[yMinIndex % xData.shape[0]]
|
|
127
|
-
y0 = yData[yMinIndex]
|
|
136
|
+
y0 = yOtherBounds[0] + yData[yMinIndex]
|
|
128
137
|
|
|
129
138
|
# Calculate y adjustment as needed for values close to zero
|
|
130
139
|
yAdjustment = -yBounds[0] if yBounds[0] > 0.0 else -yBounds[1] if yBounds[1] < 0.0 else 0.0
|
|
131
140
|
y0 += yAdjustment
|
|
141
|
+
additionalPoint = yOtherBounds[1] > yOtherBounds[0] + epsilon
|
|
132
142
|
|
|
133
143
|
# Sort points by angle around p0.
|
|
134
144
|
sortedPoints = []
|
|
@@ -139,7 +149,9 @@ def _convex_hull_2D(xData, yData, yBounds, epsilon = 1.0e-8):
|
|
|
139
149
|
if x is None:
|
|
140
150
|
xIter = iter(xData)
|
|
141
151
|
x = next(xIter)
|
|
142
|
-
sortedPoints.append((math.atan2(y - y0, x - x0), x, y))
|
|
152
|
+
sortedPoints.append((math.atan2(yOtherBounds[0] + y - y0, x - x0), x, yOtherBounds[0] + y))
|
|
153
|
+
if additionalPoint:
|
|
154
|
+
sortedPoints.append((math.atan2(yOtherBounds[1] + y - y0, x - x0), x, yOtherBounds[1] + y))
|
|
143
155
|
sortedPoints.sort()
|
|
144
156
|
|
|
145
157
|
# Trim away points with the same angle (keep furthest point from p0), removing the angle from the list.
|
|
@@ -195,74 +207,113 @@ def _intersect_convex_hull_with_x_interval(hullPoints, epsilon, xInterval):
|
|
|
195
207
|
else:
|
|
196
208
|
return (min(max(xMin, xInterval[0]), xInterval[1]), max(min(xMax, xInterval[1]), xInterval[0]))
|
|
197
209
|
|
|
198
|
-
Interval = namedtuple('Interval', ('
|
|
210
|
+
Interval = namedtuple('Interval', ('block', 'unknowns', 'scale', 'bounds', 'slope', 'intercept', 'epsilon', 'atMachineEpsilon'))
|
|
211
|
+
|
|
212
|
+
def _create_interval(domain, block, unknowns, scale, slope, intercept, epsilon):
|
|
213
|
+
nDep = 0
|
|
214
|
+
bounds = np.zeros((len(scale), 2), scale.dtype)
|
|
215
|
+
newScale = np.empty_like(scale)
|
|
216
|
+
newBlock = []
|
|
217
|
+
for row in block:
|
|
218
|
+
newRow = []
|
|
219
|
+
nInd = 0
|
|
220
|
+
keepDep = []
|
|
221
|
+
# Trim and reparametrize splines, and sum bounds.
|
|
222
|
+
for spline in row:
|
|
223
|
+
spline = spline.trim(domain[nInd:nInd + spline.nInd]).reparametrize(((0.0, 1.0),) * spline.nInd)
|
|
224
|
+
bounds[nDep:nDep + spline.nDep] += spline.range_bounds()
|
|
225
|
+
nInd += spline.nInd
|
|
226
|
+
newRow.append(spline)
|
|
227
|
+
|
|
228
|
+
# Check row bounds for potential roots.
|
|
229
|
+
for dep in range(spline.nDep):
|
|
230
|
+
coefsMin = bounds[nDep, 0] * scale[nDep]
|
|
231
|
+
coefsMax = bounds[nDep, 1] * scale[nDep]
|
|
232
|
+
if coefsMax < -epsilon or coefsMin > epsilon:
|
|
233
|
+
# No roots in this interval.
|
|
234
|
+
return None
|
|
235
|
+
if coefsMin < -epsilon or coefsMax > epsilon:
|
|
236
|
+
# Dependent variable not near zero for entire interval.
|
|
237
|
+
keepDep.append(dep)
|
|
238
|
+
newScale[nDep] = max(-coefsMin, coefsMax)
|
|
239
|
+
# Rescale spline coefficients to max 1.0.
|
|
240
|
+
rescale = 1.0 / max(-bounds[nDep, 0], bounds[nDep, 1])
|
|
241
|
+
for spline in newRow:
|
|
242
|
+
spline.coefs[dep] *= rescale
|
|
243
|
+
bounds[nDep] *= rescale
|
|
244
|
+
nDep += 1
|
|
245
|
+
else:
|
|
246
|
+
# Dependent variable near zero for entire interval.
|
|
247
|
+
bounds = np.delete(bounds, nDep, 0)
|
|
248
|
+
scale = np.delete(scale, nDep, 0)
|
|
249
|
+
|
|
250
|
+
if keepDep:
|
|
251
|
+
# Remove dependent variables that are zero over the domain
|
|
252
|
+
for spline in newRow:
|
|
253
|
+
spline.nDep = len(keepDep)
|
|
254
|
+
spline.coefs = spline.coefs[keepDep]
|
|
255
|
+
|
|
256
|
+
newBlock.append(newRow)
|
|
257
|
+
|
|
258
|
+
return Interval(newBlock, unknowns, newScale[:nDep], bounds, slope, intercept, epsilon, np.dot(slope, slope) < np.finfo(slope.dtype).eps)
|
|
199
259
|
|
|
200
260
|
# We use multiprocessing.Pool to call this function in parallel, so it cannot be nested and must take a single argument.
|
|
201
261
|
def _refine_projected_polyhedron(interval):
|
|
202
262
|
Crit = 0.85 # Required percentage decrease in domain per iteration.
|
|
203
263
|
epsilon = interval.epsilon
|
|
204
|
-
evaluationEpsilon = np.sqrt(epsilon)
|
|
205
|
-
machineEpsilon = np.finfo(interval.spline.coefs.dtype).eps
|
|
206
264
|
roots = []
|
|
207
265
|
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
266
|
|
|
235
267
|
# Loop through each independent variable to determine a tighter domain around roots.
|
|
268
|
+
# The interval block's remaining number of independent variables (nInd) is len(interval.unknowns).
|
|
269
|
+
# The interval block's remaining number of dependent variables (nDep) is len(interval.scale).
|
|
236
270
|
domain = []
|
|
237
|
-
|
|
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)
|
|
241
|
-
|
|
242
|
-
# 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
|
|
248
|
-
|
|
271
|
+
for nInd in range(len(interval.unknowns)):
|
|
249
272
|
# Loop through each dependent variable to compute the interval containing the root for this independent variable.
|
|
250
273
|
xInterval = (0.0, 1.0)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
274
|
+
nDep = 0
|
|
275
|
+
for row in interval.block:
|
|
276
|
+
rowInd = 0
|
|
277
|
+
order = 0
|
|
278
|
+
for spline in row:
|
|
279
|
+
if rowInd <= nInd < rowInd + spline.nInd:
|
|
280
|
+
order = spline.order[nInd - rowInd]
|
|
281
|
+
nCoef = spline.nCoef[nInd - rowInd]
|
|
282
|
+
knots = spline.knots[nInd - rowInd]
|
|
283
|
+
# Move independent variable to the last (fastest) axis, adding 1 to account for the dependent variables.
|
|
284
|
+
coefs = np.moveaxis(spline.coefs, nInd - rowInd + 1, -1)
|
|
285
|
+
break
|
|
286
|
+
rowInd += spline.nInd
|
|
287
|
+
|
|
288
|
+
# Skip this row if it doesn't contains this independent variable.
|
|
289
|
+
if order < 1:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Compute the coefficients for f(x) = x for the independent variable and its knots.
|
|
293
|
+
degree = order - 1
|
|
294
|
+
xData = np.empty((nCoef,), knots.dtype)
|
|
295
|
+
xData[0] = knots[1]
|
|
296
|
+
for i in range(1, nCoef):
|
|
297
|
+
xData[i] = xData[i - 1] + (knots[i + degree] - knots[i])/degree
|
|
298
|
+
|
|
299
|
+
# Loop through each dependent variable in this row to refine the interval containing the root for this independent variable.
|
|
300
|
+
for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(), interval.bounds[nDep:nDep + spline.nDep]):
|
|
301
|
+
# Compute the 2D convex hull of the knot coefficients and the spline's coefficients
|
|
302
|
+
hull = _convex_hull_2D(xData, yData.ravel(), yBounds, yBounds - ySplineBounds, epsilon)
|
|
303
|
+
if hull is None:
|
|
304
|
+
return roots, intervals
|
|
305
|
+
|
|
306
|
+
# Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
|
|
307
|
+
xInterval = _intersect_convex_hull_with_x_interval(hull, epsilon, xInterval)
|
|
308
|
+
if xInterval is None:
|
|
309
|
+
return roots, intervals
|
|
256
310
|
|
|
257
|
-
|
|
258
|
-
xInterval = _intersect_convex_hull_with_x_interval(hull, epsilon, xInterval)
|
|
259
|
-
if xInterval is None:
|
|
260
|
-
return roots, intervals
|
|
311
|
+
nDep += spline.nDep
|
|
261
312
|
|
|
262
313
|
domain.append(xInterval)
|
|
263
314
|
|
|
264
315
|
# Compute new slope, intercept, and unknowns.
|
|
265
|
-
domain = np.array(domain,
|
|
316
|
+
domain = np.array(domain, interval.slope.dtype).T
|
|
266
317
|
width = domain[1] - domain[0]
|
|
267
318
|
newSlope = interval.slope.copy()
|
|
268
319
|
newIntercept = interval.intercept.copy()
|
|
@@ -282,17 +333,23 @@ def _refine_projected_polyhedron(interval):
|
|
|
282
333
|
nInd += 1
|
|
283
334
|
|
|
284
335
|
# 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
|
|
286
|
-
if interval.atMachineEpsilon or
|
|
336
|
+
# one iteration past being less than sqrt(machineEpsilon) or there are no remaining independent variables.
|
|
337
|
+
if interval.atMachineEpsilon or nInd == 0:
|
|
287
338
|
# Return the interval center and radius.
|
|
288
339
|
roots.append((newIntercept + 0.5 * newSlope, epsilon))
|
|
289
340
|
return roots, intervals
|
|
290
341
|
|
|
291
|
-
# Contract spline as needed.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
342
|
+
# Contract spline matrix as needed.
|
|
343
|
+
if newDomain.shape[1] < domain.shape[1]:
|
|
344
|
+
for row in interval.block:
|
|
345
|
+
rowInd = 0
|
|
346
|
+
for i, spline in enumerate(row):
|
|
347
|
+
row[i] = spline.contract(uvw[rowInd:rowInd + spline.nInd])
|
|
348
|
+
rowInd += spline.nInd
|
|
349
|
+
|
|
350
|
+
# Special case optimization: Use interval newton for one-dimensional splines.
|
|
351
|
+
if nInd == 1 and nDep == 1 and len(interval.block[0]) == 1:
|
|
352
|
+
spline = interval.block[0][0]
|
|
296
353
|
i = newUnknowns[0]
|
|
297
354
|
for root in zeros_using_interval_newton(spline):
|
|
298
355
|
if not isinstance(root, tuple):
|
|
@@ -310,7 +367,7 @@ def _refine_projected_polyhedron(interval):
|
|
|
310
367
|
# Split domain in dimensions that aren't decreasing in width sufficiently.
|
|
311
368
|
width = newDomain[1] - newDomain[0]
|
|
312
369
|
domains = [newDomain]
|
|
313
|
-
for nInd, w in
|
|
370
|
+
for nInd, w in enumerate(width):
|
|
314
371
|
if w > Crit:
|
|
315
372
|
# Didn't get the required decrease in width, so split the domain.
|
|
316
373
|
domainCount = len(domains) # Cache the domain list size, since we're increasing it mid loop
|
|
@@ -330,7 +387,12 @@ def _refine_projected_polyhedron(interval):
|
|
|
330
387
|
for i, w, d in zip(newUnknowns, width, domain.T):
|
|
331
388
|
splitSlope[i] = w * interval.slope[i]
|
|
332
389
|
splitIntercept[i] = d[0] * interval.slope[i] + interval.intercept[i]
|
|
333
|
-
|
|
390
|
+
newInterval = _create_interval(domain.T, interval.block, newUnknowns, interval.scale, splitSlope, splitIntercept, epsilon)
|
|
391
|
+
if newInterval:
|
|
392
|
+
if newInterval.block:
|
|
393
|
+
intervals.append(newInterval)
|
|
394
|
+
else:
|
|
395
|
+
roots.append((newInterval.intercept + 0.5 * newInterval.slope, 0.5 * np.linalg.norm(newInterval.slope)))
|
|
334
396
|
|
|
335
397
|
return roots, intervals
|
|
336
398
|
|
|
@@ -340,18 +402,27 @@ class _Region:
|
|
|
340
402
|
self.radius = radius
|
|
341
403
|
self.count = count
|
|
342
404
|
|
|
343
|
-
def zeros_using_projected_polyhedron(self, epsilon=None):
|
|
344
|
-
if
|
|
345
|
-
|
|
405
|
+
def zeros_using_projected_polyhedron(self, epsilon=None, initialScale=None):
|
|
406
|
+
if self.nInd != self.nDep: raise ValueError("The number of independent variables (nInd) must match the number of dependent variables (nDep).")
|
|
407
|
+
|
|
408
|
+
# Determine epsilon and initialize roots.
|
|
409
|
+
machineEpsilon = np.finfo(self.knotsDtype).eps
|
|
346
410
|
if epsilon is None:
|
|
347
411
|
epsilon = 0.0
|
|
348
|
-
epsilon = max(epsilon, np.sqrt(machineEpsilon))
|
|
349
|
-
evaluationEpsilon = np.sqrt(epsilon)
|
|
412
|
+
epsilon = max(epsilon, np.sqrt(machineEpsilon)) if epsilon else np.sqrt(machineEpsilon)
|
|
413
|
+
evaluationEpsilon = max(np.sqrt(epsilon), np.finfo(self.coefsDtype).eps ** 0.25)
|
|
414
|
+
intervals = []
|
|
350
415
|
roots = []
|
|
351
416
|
|
|
352
|
-
# Set initial
|
|
417
|
+
# Set initial interval.
|
|
353
418
|
domain = self.domain().T
|
|
354
|
-
|
|
419
|
+
initialScale = np.full(self.nDep, 1.0, self.coefsDtype) if initialScale is None else np.array(initialScale, self.coefsDtype)
|
|
420
|
+
newInterval = _create_interval(domain.T, self.block, [*range(self.nInd)], initialScale, domain[1] - domain[0], domain[0], epsilon)
|
|
421
|
+
if newInterval:
|
|
422
|
+
if newInterval.block:
|
|
423
|
+
intervals.append(newInterval)
|
|
424
|
+
else:
|
|
425
|
+
roots.append((newInterval.intercept + 0.5 * newInterval.slope, 0.5 * np.linalg.norm(newInterval.slope)))
|
|
355
426
|
chunkSize = 8
|
|
356
427
|
#pool = Pool() # Pool size matches CPU count
|
|
357
428
|
|
|
@@ -376,7 +447,8 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
|
|
|
376
447
|
rootRadius = root[1]
|
|
377
448
|
|
|
378
449
|
# Ensure we have a real root (not a boundary special case).
|
|
379
|
-
|
|
450
|
+
value = self.evaluate(rootCenter)
|
|
451
|
+
if np.linalg.norm(value) >= evaluationEpsilon:
|
|
380
452
|
continue
|
|
381
453
|
|
|
382
454
|
# Expand the radius of the root based on the approximate distance from the center needed
|
|
@@ -418,48 +490,47 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
|
|
|
418
490
|
|
|
419
491
|
return roots
|
|
420
492
|
|
|
421
|
-
def
|
|
422
|
-
if self.nInd - self.nDep != 1: raise ValueError("The number of free variables (self.nInd - self.nDep) must be one.")
|
|
423
|
-
|
|
493
|
+
def _contours_of_C1_spline_block(self, epsilon, evaluationEpsilon):
|
|
424
494
|
Point = namedtuple('Point', ('d', 'det', 'onUVBoundary', 'turningPoint', 'uvw'))
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
coefsMin = coefs.min()
|
|
431
|
-
coefsMax = coefs.max()
|
|
432
|
-
if coefsMax < -evaluationEpsilon or coefsMin > evaluationEpsilon:
|
|
495
|
+
|
|
496
|
+
# Go through each nDep of the spline block, checking bounds.
|
|
497
|
+
bounds = self.range_bounds()
|
|
498
|
+
for bound in bounds:
|
|
499
|
+
if bound[1] < -evaluationEpsilon or bound[0] > evaluationEpsilon:
|
|
433
500
|
# No contours for this spline.
|
|
434
501
|
return []
|
|
435
502
|
|
|
436
503
|
# Record self's original domain and then reparametrize self's domain to [0, 1]^nInd.
|
|
437
504
|
domain = self.domain().T
|
|
438
505
|
self = self.reparametrize(((0.0, 1.0),) * self.nInd)
|
|
506
|
+
|
|
507
|
+
# Rescale self in all dimensions.
|
|
508
|
+
nDep = 0
|
|
509
|
+
initialScale = np.max(np.abs(bounds), axis=1)
|
|
510
|
+
rescale = np.reciprocal(initialScale)
|
|
511
|
+
for row in self.block:
|
|
512
|
+
nInd = 0
|
|
513
|
+
for spline in row:
|
|
514
|
+
for coefs, scale in zip(spline.coefs, rescale):
|
|
515
|
+
coefs *= scale
|
|
516
|
+
nDep += spline.nDep
|
|
439
517
|
|
|
440
|
-
# Construct self's
|
|
441
|
-
tangents = []
|
|
442
|
-
for nInd in range(self.nInd):
|
|
443
|
-
tangents.append(self.differentiate(nInd))
|
|
518
|
+
# Construct self's normal.
|
|
444
519
|
normal = self.normal_spline((0, 1)) # We only need the first two indices
|
|
445
520
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
attempts = 3
|
|
449
|
-
while attempts > 0:
|
|
521
|
+
# Try arbitrary values for theta between [0, pi/2] that are unlikely to be a stationary points.
|
|
522
|
+
for theta in (1.0 / np.sqrt(2), np.pi / 6.0, 1.0/ np.e):
|
|
450
523
|
points = []
|
|
451
|
-
theta *= 0.607
|
|
452
524
|
cosTheta = np.cos(theta)
|
|
453
525
|
sinTheta = np.sin(theta)
|
|
454
526
|
abort = False
|
|
455
|
-
attempts -=1
|
|
456
527
|
|
|
457
528
|
# Construct the turning point determinant.
|
|
458
529
|
turningPointDeterminant = normal.dot((cosTheta, sinTheta))
|
|
459
530
|
|
|
460
531
|
# Find intersections with u and v boundaries.
|
|
461
532
|
def uvIntersections(nInd, boundary):
|
|
462
|
-
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros()
|
|
533
|
+
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros(epsilon, initialScale)
|
|
463
534
|
abort = False
|
|
464
535
|
for zero in zeros:
|
|
465
536
|
if isinstance(zero, tuple):
|
|
@@ -467,10 +538,24 @@ def contours(self):
|
|
|
467
538
|
break
|
|
468
539
|
uvw = np.insert(np.array(zero), nInd, boundary)
|
|
469
540
|
d = uvw[0] * cosTheta + uvw[1] * sinTheta
|
|
470
|
-
|
|
541
|
+
n = normal(uvw)
|
|
542
|
+
tpd = turningPointDeterminant(uvw)
|
|
543
|
+
det = (0.5 - boundary) * n[nInd] * tpd
|
|
471
544
|
if abs(det) < epsilon:
|
|
472
545
|
abort = True
|
|
473
546
|
break
|
|
547
|
+
# Check for literal corner case.
|
|
548
|
+
otherInd = 1 - nInd
|
|
549
|
+
otherValue = uvw[otherInd]
|
|
550
|
+
if otherValue < epsilon or otherValue + epsilon > 1.0:
|
|
551
|
+
otherDet = (0.5 - otherValue) * n[otherInd] * tpd
|
|
552
|
+
if det * otherDet < 0.0:
|
|
553
|
+
continue # Corner that starts and ends, ignore it
|
|
554
|
+
elif max(otherValue, boundary) < epsilon and det < 0.0:
|
|
555
|
+
continue # End point at (0, 0), ignore it
|
|
556
|
+
elif min(otherValue, boundary) + epsilon > 1.0 and det > 0.0:
|
|
557
|
+
continue # Start point at (1, 1), ignore it
|
|
558
|
+
# Append boundary point.
|
|
474
559
|
points.append(Point(d, det, True, False, uvw))
|
|
475
560
|
return abort
|
|
476
561
|
for nInd in range(2):
|
|
@@ -485,7 +570,7 @@ def contours(self):
|
|
|
485
570
|
|
|
486
571
|
# Find intersections with other boundaries.
|
|
487
572
|
def otherIntersections(nInd, boundary):
|
|
488
|
-
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros()
|
|
573
|
+
zeros = self.contract([None] * nInd + [boundary] + [None] * (self.nInd - nInd - 1)).zeros(epsilon, initialScale)
|
|
489
574
|
abort = False
|
|
490
575
|
for zero in zeros:
|
|
491
576
|
if isinstance(zero, tuple):
|
|
@@ -494,12 +579,13 @@ def contours(self):
|
|
|
494
579
|
uvw = np.insert(np.array(zero), nInd, boundary)
|
|
495
580
|
d = uvw[0] * cosTheta + uvw[1] * sinTheta
|
|
496
581
|
columns = np.empty((self.nDep, self.nInd - 1))
|
|
582
|
+
tangents = self.jacobian(uvw).T
|
|
497
583
|
i = 0
|
|
498
584
|
for j in range(self.nInd):
|
|
499
585
|
if j != nInd:
|
|
500
|
-
columns[:, i] = tangents[j]
|
|
586
|
+
columns[:, i] = tangents[j]
|
|
501
587
|
i += 1
|
|
502
|
-
duv = np.linalg.solve(columns, -tangents[nInd]
|
|
588
|
+
duv = np.linalg.solve(columns, -tangents[nInd])
|
|
503
589
|
det = np.arctan2((0.5 - boundary) * (duv[0] * cosTheta + duv[1] * sinTheta), (0.5 - boundary) * (duv[0] * cosTheta - duv[1] * sinTheta))
|
|
504
590
|
if abs(det) < epsilon:
|
|
505
591
|
abort = True
|
|
@@ -517,10 +603,9 @@ def contours(self):
|
|
|
517
603
|
continue # Try a different theta
|
|
518
604
|
|
|
519
605
|
# Find turning points by combining self and turningPointDeterminant into a system and processing its zeros.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
zeros = system.zeros()
|
|
606
|
+
turningPointBlock = self.block.copy()
|
|
607
|
+
turningPointBlock.append([turningPointDeterminant])
|
|
608
|
+
zeros = bspy.spline_block.SplineBlock(turningPointBlock).zeros(epsilon, np.append(initialScale, 1.0))
|
|
524
609
|
for uvw in zeros:
|
|
525
610
|
if isinstance(uvw, tuple):
|
|
526
611
|
abort = True
|
|
@@ -540,7 +625,7 @@ def contours(self):
|
|
|
540
625
|
if not abort:
|
|
541
626
|
break # We're done!
|
|
542
627
|
|
|
543
|
-
if
|
|
628
|
+
if abort: raise ValueError("No contours. Degenerate equations.")
|
|
544
629
|
|
|
545
630
|
if not points:
|
|
546
631
|
return [] # No contours
|
|
@@ -553,17 +638,14 @@ def contours(self):
|
|
|
553
638
|
# a panel boundary: u * cosTheta + v * sinTheta = d. Basically, we add this panel boundary plane
|
|
554
639
|
# to the contour condition. We'll define it for d = 0, and add the actual d later.
|
|
555
640
|
# We didn't construct the panel system earlier, because we didn't have theta.
|
|
556
|
-
panelCoefs = np.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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)
|
|
641
|
+
panelCoefs = np.array((((0.0, sinTheta), (cosTheta, cosTheta + sinTheta)),), self.coefsDtype)
|
|
642
|
+
panelSpline = bspy.spline.Spline(2, 1, (2, 2), (2, 2),
|
|
643
|
+
(np.array((0.0, 0.0, 1.0, 1.0), self.knotsDtype), np.array((0.0, 0.0, 1.0, 1.0), self.knotsDtype)),
|
|
644
|
+
panelCoefs)
|
|
645
|
+
panelBlock = self.block.copy()
|
|
646
|
+
panelBlock.append([panelSpline])
|
|
647
|
+
panel = bspy.spline_block.SplineBlock(panelBlock)
|
|
648
|
+
panelInitialScale = np.append(initialScale, 1.0)
|
|
567
649
|
|
|
568
650
|
# Okay, we have everything we need to determine the contour topology and points along each contour.
|
|
569
651
|
# We've done the first two steps of Grandine and Klein's algorithm:
|
|
@@ -574,10 +656,26 @@ def contours(self):
|
|
|
574
656
|
# (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
657
|
points.sort()
|
|
576
658
|
|
|
659
|
+
# Extra step not in paper.
|
|
660
|
+
# Remove duplicate points (typically appear at corners).
|
|
661
|
+
i = 0
|
|
662
|
+
while i < len(points):
|
|
663
|
+
previousPoint = points[i]
|
|
664
|
+
j = i + 1
|
|
665
|
+
while j < len(points):
|
|
666
|
+
point = points[j]
|
|
667
|
+
if point.d < previousPoint.d + epsilon:
|
|
668
|
+
if np.linalg.norm(point.uvw - previousPoint.uvw) < epsilon:
|
|
669
|
+
del points[j]
|
|
670
|
+
else:
|
|
671
|
+
j += 1
|
|
672
|
+
else:
|
|
673
|
+
break
|
|
674
|
+
i += 1
|
|
675
|
+
|
|
577
676
|
# Extra step not in paper.
|
|
578
677
|
# Run a checksum on the points, ensuring starting and ending points balance.
|
|
579
678
|
# Start by flipping endpoints as needed, since we can miss turning points near endpoints.
|
|
580
|
-
|
|
581
679
|
if points[0].det < 0.0:
|
|
582
680
|
point = points[0]
|
|
583
681
|
points[0] = Point(point.d, -point.det, point.onUVBoundary, point.turningPoint, point.uvw)
|
|
@@ -651,11 +749,11 @@ def contours(self):
|
|
|
651
749
|
# points. Either insert two new contours in the list or delete two existing ones from
|
|
652
750
|
# the list. Go back to Step (5).
|
|
653
751
|
# First, construct panel, whose zeros lie along the panel boundary, u * cosTheta + v * sinTheta - d = 0.
|
|
654
|
-
|
|
752
|
+
panelSpline.coefs = panelCoefs - point.d
|
|
655
753
|
|
|
656
754
|
if point.turningPoint and point.uvw is None:
|
|
657
755
|
# For an inserted panel between two consecutive turning points, just find zeros along the panel.
|
|
658
|
-
panelPoints = panel.zeros()
|
|
756
|
+
panelPoints = panel.zeros(epsilon, panelInitialScale)
|
|
659
757
|
elif point.turningPoint:
|
|
660
758
|
# Split panel below and above the known zero point.
|
|
661
759
|
# This avoids extra computation and the high-zero at the known zero point, while ensuring we match the turning point.
|
|
@@ -677,15 +775,15 @@ def contours(self):
|
|
|
677
775
|
np.linalg.norm(selfUU * sinTheta * sinTheta - 2.0 * selfUV * sinTheta * cosTheta + selfVV * cosTheta * cosTheta))
|
|
678
776
|
# Now, we can find the zeros of the split panel, checking to ensure each panel is within bounds first.
|
|
679
777
|
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()
|
|
778
|
+
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
779
|
expectedPanelPoints -= len(panelPoints) - 1 # Discount the turning point itself
|
|
682
780
|
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()
|
|
781
|
+
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
782
|
else: # It's an other-boundary point.
|
|
685
783
|
# Only find extra zeros along the panel if any are expected (> 0 for starting point, > 1 for ending one).
|
|
686
784
|
expectedPanelPoints = len(currentContourPoints) - (0 if point.det > 0.0 else 1)
|
|
687
785
|
if expectedPanelPoints > 0:
|
|
688
|
-
panelPoints = panel.zeros()
|
|
786
|
+
panelPoints = panel.zeros(epsilon, panelInitialScale)
|
|
689
787
|
panelPoints.sort(key=lambda uvw: np.linalg.norm(point.uvw - uvw)) # Sort by distance from boundary point
|
|
690
788
|
while len(panelPoints) > expectedPanelPoints:
|
|
691
789
|
panelPoints.pop(0) # Drop points closest to the boundary point
|
|
@@ -693,8 +791,6 @@ def contours(self):
|
|
|
693
791
|
else:
|
|
694
792
|
panelPoints = [point.uvw]
|
|
695
793
|
|
|
696
|
-
# Add d back to prepare for next turning point.
|
|
697
|
-
panel.coefs[self.nDep] += point.d
|
|
698
794
|
# Sort zero points by their position along the panel boundary (using vector orthogonal to its normal).
|
|
699
795
|
panelPoints.sort(key=lambda uvw: uvw[1] * cosTheta - uvw[0] * sinTheta)
|
|
700
796
|
# Go through panel points, adding them to existing contours, creating new ones, or closing old ones.
|
|
@@ -781,6 +877,58 @@ def contours(self):
|
|
|
781
877
|
|
|
782
878
|
return splineContours
|
|
783
879
|
|
|
880
|
+
def contours(self):
|
|
881
|
+
if self.nInd - self.nDep != 1: raise ValueError("The number of free variables (self.nInd - self.nDep) must be one.")
|
|
882
|
+
epsilon = np.sqrt(np.finfo(self.knotsDtype).eps)
|
|
883
|
+
evaluationEpsilon = max(np.sqrt(epsilon), np.finfo(self.coefsDtype).eps ** 0.25)
|
|
884
|
+
|
|
885
|
+
# Split the splines in the block to ensure C1 continuity within each block
|
|
886
|
+
blocks = [self]
|
|
887
|
+
for i, row in enumerate(self.block):
|
|
888
|
+
for j, spline in enumerate(row):
|
|
889
|
+
splines = spline.split(minContinuity = 1)
|
|
890
|
+
if splines.size == 1 and self.size == 1:
|
|
891
|
+
break # Special case of a block with one C1 spline
|
|
892
|
+
newBlocks = []
|
|
893
|
+
for spline in splines.ravel():
|
|
894
|
+
for block in blocks:
|
|
895
|
+
newBlock = block.block.copy()
|
|
896
|
+
newRow = newBlock[i].copy()
|
|
897
|
+
newBlock[i] = newRow
|
|
898
|
+
newRow[j] = spline
|
|
899
|
+
newBlocks.append(bspy.spline_block.SplineBlock(newBlock))
|
|
900
|
+
blocks = newBlocks
|
|
901
|
+
|
|
902
|
+
contours = []
|
|
903
|
+
for block in blocks:
|
|
904
|
+
splineContours = _contours_of_C1_spline_block(block, epsilon, evaluationEpsilon)
|
|
905
|
+
for newContour in splineContours:
|
|
906
|
+
newStart = newContour(0.0)
|
|
907
|
+
newFinish = newContour(1.0)
|
|
908
|
+
joined = False
|
|
909
|
+
for i, oldContour in enumerate(contours):
|
|
910
|
+
oldStart = oldContour(0.0)
|
|
911
|
+
oldFinish = oldContour(1.0)
|
|
912
|
+
if np.linalg.norm(newStart - oldFinish) < evaluationEpsilon:
|
|
913
|
+
contours[i] = bspy.Spline.join((oldContour, newContour))
|
|
914
|
+
joined = True
|
|
915
|
+
break
|
|
916
|
+
if np.linalg.norm(newStart - oldStart) < evaluationEpsilon:
|
|
917
|
+
contours[i] = bspy.Spline.join((oldContour, newContour.reverse()))
|
|
918
|
+
joined = True
|
|
919
|
+
break
|
|
920
|
+
if np.linalg.norm(newFinish - oldStart) < evaluationEpsilon:
|
|
921
|
+
contours[i] = bspy.Spline.join((newContour, oldContour))
|
|
922
|
+
joined = True
|
|
923
|
+
break
|
|
924
|
+
if np.linalg.norm(newFinish - oldFinish) < evaluationEpsilon:
|
|
925
|
+
contours[i] = bspy.Spline.join((newContour, oldContour.reverse()))
|
|
926
|
+
joined = True
|
|
927
|
+
break
|
|
928
|
+
if not joined:
|
|
929
|
+
contours.append(newContour)
|
|
930
|
+
return contours
|
|
931
|
+
|
|
784
932
|
def intersect(self, other):
|
|
785
933
|
intersections = []
|
|
786
934
|
nDep = self.nInd # The dimension of the intersection's range
|
|
@@ -850,13 +998,13 @@ def intersect(self, other):
|
|
|
850
998
|
|
|
851
999
|
# Spline-Spline intersection.
|
|
852
1000
|
elif isinstance(other, bspy.Spline):
|
|
853
|
-
# Construct a
|
|
854
|
-
|
|
1001
|
+
# Construct a spline block that represents the intersection.
|
|
1002
|
+
block = bspy.spline_block.SplineBlock([[self, -other]])
|
|
855
1003
|
|
|
856
1004
|
# Curve-Curve intersection.
|
|
857
1005
|
if nDep == 1:
|
|
858
1006
|
# Find the intersection points and intervals.
|
|
859
|
-
zeros =
|
|
1007
|
+
zeros = block.zeros()
|
|
860
1008
|
# Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
|
|
861
1009
|
for zero in zeros:
|
|
862
1010
|
if isinstance(zero, tuple):
|
|
@@ -895,28 +1043,27 @@ def intersect(self, other):
|
|
|
895
1043
|
# Surface-Surface intersection.
|
|
896
1044
|
elif nDep == 2:
|
|
897
1045
|
if "Name" in self.metadata and "Name" in other.metadata:
|
|
898
|
-
logging.info(f"intersect
|
|
1046
|
+
logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
|
|
899
1047
|
# Find the intersection contours, which are returned as splines.
|
|
900
1048
|
swap = False
|
|
901
1049
|
try:
|
|
902
1050
|
# First try the intersection as is.
|
|
903
|
-
contours =
|
|
904
|
-
except ValueError:
|
|
1051
|
+
contours = block.contours()
|
|
1052
|
+
except ValueError as e:
|
|
1053
|
+
logging.info(e)
|
|
905
1054
|
# If that fails, swap the manifolds. Worth a shot since intersections are touchy.
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
# Convert each contour into a Manifold.Crossing.
|
|
909
|
-
if swap:
|
|
910
|
-
spline = other.subtract(self)
|
|
1055
|
+
block = bspy.spline_block.SplineBlock([[other, -self]])
|
|
911
1056
|
if "Name" in self.metadata and "Name" in other.metadata:
|
|
912
|
-
logging.info(f"intersect
|
|
913
|
-
contours =
|
|
1057
|
+
logging.info(f"intersect:{other.metadata['Name']}:{self.metadata['Name']}")
|
|
1058
|
+
contours = block.contours()
|
|
1059
|
+
# Convert each contour into a Manifold.Crossing, swapping the manifolds back.
|
|
914
1060
|
for contour in contours:
|
|
915
1061
|
# Swap left and right, compared to not swapped.
|
|
916
1062
|
left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
|
|
917
1063
|
right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
|
|
918
1064
|
intersections.append(Manifold.Crossing(left, right))
|
|
919
1065
|
else:
|
|
1066
|
+
# Convert each contour into a Manifold.Crossing.
|
|
920
1067
|
for contour in contours:
|
|
921
1068
|
left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
|
|
922
1069
|
right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
|
|
@@ -950,7 +1097,7 @@ def complete_slice(self, slice, solid):
|
|
|
950
1097
|
if not slice.boundaries:
|
|
951
1098
|
if slice.dimension == 2:
|
|
952
1099
|
if "Name" in self.metadata:
|
|
953
|
-
logging.info(f"check containment:
|
|
1100
|
+
logging.info(f"check containment:{self.metadata['Name']}")
|
|
954
1101
|
domain = bounds.T
|
|
955
1102
|
if solid.contains_point(self(0.5 * (domain[0] + domain[1]))):
|
|
956
1103
|
for boundary in Hyperplane.create_hypercube(bounds).boundaries:
|