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.
- bspy/__init__.py +3 -0
- bspy/_spline_domain.py +94 -30
- bspy/_spline_evaluation.py +80 -21
- bspy/_spline_fitting.py +205 -49
- bspy/_spline_intersection.py +442 -283
- bspy/_spline_operations.py +93 -74
- bspy/hyperplane.py +13 -9
- bspy/manifold.py +10 -5
- bspy/solid.py +22 -15
- bspy/spline.py +195 -53
- bspy/splineOpenGLFrame.py +346 -303
- bspy/spline_block.py +460 -0
- bspy/viewer.py +26 -16
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/METADATA +14 -6
- bspy-4.3.dist-info/RECORD +18 -0
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/WHEEL +1 -1
- bspy-4.1.dist-info/RECORD +0 -17
- {bspy-4.1.dist-info → bspy-4.3.dist-info}/LICENSE +0 -0
- {bspy-4.1.dist-info → bspy-4.3.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,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,
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
Interval = namedtuple('Interval', ('
|
|
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
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
if
|
|
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(
|
|
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
|
-
|
|
271
|
+
|
|
272
|
+
nDep += spline.nDep
|
|
263
273
|
|
|
264
|
-
# Compute new
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
274
|
-
|
|
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
|
|
285
|
-
# one iteration past being less than sqrt(machineEpsilon) or there are no remaining
|
|
286
|
-
if interval.atMachineEpsilon or
|
|
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((
|
|
291
|
+
roots.append((0.5 * (xNewLeft + xNewRight), epsilon))
|
|
289
292
|
return roots, intervals
|
|
290
293
|
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
345
|
-
|
|
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
|
|
352
|
+
# Set initial interval.
|
|
353
353
|
domain = self.domain().T
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
#
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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]
|
|
553
|
+
columns[:, i] = tangents[j]
|
|
501
554
|
i += 1
|
|
502
|
-
duv = np.linalg.solve(columns, -tangents[nInd]
|
|
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
|
-
|
|
521
|
-
system
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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
|
|
854
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
913
|
-
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:
|
|
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:
|