bspy 1.2.2__py3-none-any.whl → 1.2.4__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.
@@ -67,12 +67,15 @@ def zeros_using_interval_newton(self):
67
67
  adjustedLeftStep = min(leftNewtonStep, rightNewtonStep) - 0.5 * epsilon
68
68
  adjustedRightStep = max(leftNewtonStep, rightNewtonStep) + 0.5 * epsilon
69
69
  if derivativeBounds[0] * derivativeBounds[1] >= 0.0: # Refine interval
70
- projectedLeftStep = max(0.0, adjustedLeftStep)
71
- projectedRightStep = min(1.0, adjustedRightStep)
72
- if projectedLeftStep <= projectedRightStep:
73
- trimmedSpline = mySpline.trim(((projectedLeftStep, projectedRightStep),))
74
- myZeros = refine(trimmedSpline, intervalSize, functionMax)
75
- else:
70
+ projectedLeftStep = max(0.0, adjustedLeftStep)
71
+ projectedRightStep = min(1.0, adjustedRightStep)
72
+ if projectedLeftStep <= projectedRightStep:
73
+ if projectedRightStep - projectedLeftStep <= epsilon:
74
+ myZeros = [0.5 * (projectedLeftStep + projectedRightStep)]
75
+ else:
76
+ trimmedSpline = mySpline.trim(((projectedLeftStep, projectedRightStep),))
77
+ myZeros = refine(trimmedSpline, intervalSize, functionMax)
78
+ else:
76
79
  return []
77
80
  else: # . . . or split as needed
78
81
  myZeros = []
@@ -215,8 +218,9 @@ def _refine_projected_polyhedron(interval):
215
218
  # No roots in this interval.
216
219
  return root, intervals
217
220
  scale = max(scale, abs(coefsMin), abs(coefsMax))
218
-
219
- if scale < epsilon:
221
+ newScale = scale * interval.scale
222
+
223
+ if newScale < evaluationEpsilon:
220
224
  # Return the bounds of the interval within which the spline is zero.
221
225
  root = (interval.intercept, interval.slope + interval.intercept)
222
226
  else:
@@ -262,7 +266,6 @@ def _refine_projected_polyhedron(interval):
262
266
  domain.append(xInterval)
263
267
 
264
268
  if domain is not None:
265
- newScale = scale * interval.scale
266
269
  domain = np.array(domain).T
267
270
  width = domain[1] - domain[0]
268
271
  newSlope = np.multiply(width, interval.slope)
@@ -270,10 +273,9 @@ def _refine_projected_polyhedron(interval):
270
273
  # one iteration past being less than sqrt(machineEpsilon) or simply less than epsilon.
271
274
  if interval.atMachineEpsilon or newSlope.max() < epsilon:
272
275
  newIntercept = np.multiply(domain[0], interval.slope) + interval.intercept
273
- root = newIntercept + 0.5 * newSlope
274
276
  # Double-check that we're at an actual zero (avoids boundary case).
275
- if newScale * np.linalg.norm(spline(0.5 * (domain[0] + domain[1]))) >= evaluationEpsilon:
276
- root = None
277
+ if newScale * np.linalg.norm(spline(0.5 * (domain[0] + domain[1]))) < evaluationEpsilon:
278
+ root = (newIntercept, newIntercept + newSlope)
277
279
  else:
278
280
  # Split domain in dimensions that aren't decreasing in width sufficiently.
279
281
  domains = [domain]
@@ -296,7 +298,10 @@ def _refine_projected_polyhedron(interval):
296
298
  newIntercept = np.multiply(domain[0], interval.slope) + interval.intercept
297
299
  for i, w in zip(range(spline.nInd), width):
298
300
  if w < machineEpsilon:
299
- domain[1, i] = domain[0, i] + machineEpsilon
301
+ if domain[0, i] > machineEpsilon:
302
+ domain[0, i] = domain[1, i] - machineEpsilon
303
+ else:
304
+ domain[1, i] = domain[0, i] + machineEpsilon
300
305
  newDomain = [None if s < epsilon else (0.0, 1.0) for s in newSlope]
301
306
  intervals.append(Interval(spline.trim(domain.T).reparametrize(newDomain), newScale, newSlope, newIntercept, epsilon, np.dot(newSlope, newSlope) < machineEpsilon))
302
307
 
@@ -330,25 +335,48 @@ def zeros_using_projected_polyhedron(self, epsilon=None):
330
335
  nextIntervals += newIntervals
331
336
  intervals = nextIntervals
332
337
 
333
- # Check for duplicate roots. We test for a distance between roots of 2*epsilon to account for a left vs. right sided limit.
334
- i = 0
335
- rootCount = len(roots)
336
- while i < rootCount:
337
- root = roots[i]
338
- j = i + 1
339
- while j < rootCount:
340
- if np.linalg.norm(root - roots[j]) < 2.0 * epsilon:
341
- # For a duplicate root, return the average value.
342
- roots[i] = 0.5 * (root + roots[j])
343
- roots.pop(j)
344
- rootCount -= 1
345
- else:
346
- j += 1
347
- i += 1
338
+ # Connect intervals of zeros that overlap.
339
+ gotOverlap = True
340
+ while gotOverlap:
341
+ gotOverlap = False
342
+ i = 0
343
+ rootCount = len(roots)
344
+ while i < rootCount:
345
+ iRoot = roots[i]
346
+ root = (iRoot[0].copy(), iRoot[1].copy()) # Temporary storage for expanded interval
347
+ j = i + 1
348
+ # Check for overlap with other intervals.
349
+ while j < rootCount:
350
+ jRoot = roots[j]
351
+ overlapped = True
352
+ for d in range(self.nInd):
353
+ if iRoot[0][d] < jRoot[1][d] + epsilon and jRoot[0][d] < iRoot[1][d] + epsilon:
354
+ root[0][d] = min(iRoot[0][d], jRoot[0][d])
355
+ root[1][d] = max(iRoot[1][d], jRoot[1][d])
356
+ else:
357
+ overlapped = False
358
+ break
359
+ if overlapped:
360
+ # For an overlapped interval, expand original interval and toss overlapping interval.
361
+ iRoot[0][:] = root[0]
362
+ iRoot[1][:] = root[1]
363
+ roots.pop(j)
364
+ rootCount -= 1
365
+ gotOverlap = True
366
+ else:
367
+ j += 1
368
+ i += 1
369
+
370
+ # Collapse intervals to points as appropriate.
371
+ for i in range(len(roots)):
372
+ iRoot = roots[i]
373
+ # If interval is small, just return a single value (not an interval).
374
+ if True: # Skip small interval test, since it's typically a shallow point, not a flat section. np.linalg.norm(iRoot[1] - iRoot[0]) < 2.0 * epsilon:
375
+ roots[i] = 0.5 * (iRoot[0] + iRoot[1])
348
376
 
349
377
  # Sort roots if there's only one dimension.
350
378
  if self.nInd == 1:
351
- roots.sort(key=lambda root: root[0] if type(root) is tuple else root)
379
+ roots.sort(key=lambda root: root[0] if isinstance(root, tuple) else root)
352
380
 
353
381
  return roots
354
382
 
@@ -367,9 +395,15 @@ def contours(self):
367
395
  # No contours for this spline.
368
396
  return []
369
397
 
398
+ # Record self's original domain and then reparametrize self's domain to [0, 1]^nInd.
399
+ domain = self.domain().T
400
+ self = self.reparametrize(((0.0, 1.0),) * self.nInd)
401
+
402
+ # Construct self's tangents and normal.
370
403
  tangents = []
371
404
  for nInd in range(self.nInd):
372
405
  tangents.append(self.differentiate(nInd))
406
+ normal = self.normal_spline((0, 1)) # We only need the first two indices
373
407
 
374
408
  theta = np.sqrt(2) # Arbitrary starting value for theta (picked one in [0, pi/2] unlikely to be a stationary point)
375
409
  # Try different theta values until no border or turning points are degenerate.
@@ -381,7 +415,6 @@ def contours(self):
381
415
  abort = False
382
416
 
383
417
  # Construct the turning point determinant.
384
- normal = self.normal_spline((0, 1)) # We only need the first two indices
385
418
  turningPointDeterminant = normal.dot((cosTheta, sinTheta))
386
419
 
387
420
  # Find intersections with u and v boundaries.
@@ -496,6 +529,20 @@ def contours(self):
496
529
  # (3) Take all the points found in Step (1) and Step (2) and order them by distance in the theta direction from the origin.
497
530
  points.sort()
498
531
 
532
+ # Extra step not in the paper: Add a panel between two consecutive turning points to uniquely determine contours between them.
533
+ if len(points) > 1:
534
+ i = 0
535
+ previousPoint = points[i]
536
+ while i < len(points) - 1:
537
+ i += 1
538
+ point = points[i]
539
+ if not previousPoint.onBoundary and not point.onBoundary and point.d - previousPoint.d > epsilon:
540
+ # We have two consecutive turning points on separate panels.
541
+ # Insert a panel in between them, with the uvw value of None, since there is no zero associated.
542
+ points.insert(i, Point(0.5 * (previousPoint.d + point.d), 0.0, False, None))
543
+ i += 1
544
+ previousPoint = point
545
+
499
546
  # (4) Initialize an ordered list of contours. No contours will be on the list at first.
500
547
  currentContourPoints = [] # Holds contours currently being identified
501
548
  contourPoints = [] # Hold contours already identified
@@ -541,29 +588,34 @@ def contours(self):
541
588
  # the list. Go back to Step (5).
542
589
  # First, construct panel, whose zeros lie along the panel boundary, u * cosTheta + v * sinTheta - d = 0.
543
590
  panel.coefs[self.nDep] -= point.d
544
- # Split panel below and above the known zero point.
545
- # This avoids extra computation and the high-zero at the known zero point.
546
- panelPoints = [point.uvw]
547
- # To split the panel, we need to determine the offset from the point.
548
- # Since the objective function (self) is zero and its derivative is zero at the point,
549
- # we use second derivatives to determine when the objective function will likely grow
550
- # evaluationEpsilon above zero again.
551
- wrt = [0] * self.nInd
552
- wrt[0] = 2
553
- selfUU = self.derivative(wrt, point.uvw)
554
- wrt[0] = 1
555
- wrt[1] = 1
556
- selfUV = self.derivative(wrt, point.uvw)
557
- wrt[0] = 0
558
- wrt[1] = 2
559
- selfVV = self.derivative(wrt, point.uvw)
560
- offset = np.sqrt(2.0 * evaluationEpsilon / \
561
- np.linalg.norm(selfUU * sinTheta * sinTheta - 2.0 * selfUV * sinTheta * cosTheta + selfVV * cosTheta * cosTheta))
562
- # Now, we can find the zeros of the split panel, checking to ensure each panel is within bounds first.
563
- if point.uvw[0] + sinTheta * offset < 1.0 - epsilon and epsilon < point.uvw[1] - cosTheta * offset:
564
- panelPoints += panel.trim(((point.uvw[0] + sinTheta * offset, 1.0), (0.0, point.uvw[1] - cosTheta * offset)) + ((None, None),) * (self.nInd - 2)).zeros()
565
- if epsilon < point.uvw[0] - sinTheta * offset and point.uvw[1] + cosTheta * offset < 1.0 - epsilon:
566
- panelPoints += panel.trim(((0.0, point.uvw[0] - sinTheta * offset), (point.uvw[1] + cosTheta * offset, 1.0)) + ((None, None),) * (self.nInd - 2)).zeros()
591
+
592
+ if point.uvw is None:
593
+ # For an inserted panel between two consecutive turning points, just find zeros along the panel.
594
+ panelPoints = panel.zeros()
595
+ else:
596
+ # Split panel below and above the known zero point.
597
+ # This avoids extra computation and the high-zero at the known zero point.
598
+ panelPoints = [point.uvw]
599
+ # To split the panel, we need to determine the offset from the point.
600
+ # Since the objective function (self) is zero and its derivative is zero at the point,
601
+ # we use second derivatives to determine when the objective function will likely grow
602
+ # evaluationEpsilon above zero again.
603
+ wrt = [0] * self.nInd
604
+ wrt[0] = 2
605
+ selfUU = self.derivative(wrt, point.uvw)
606
+ wrt[0] = 1
607
+ wrt[1] = 1
608
+ selfUV = self.derivative(wrt, point.uvw)
609
+ wrt[0] = 0
610
+ wrt[1] = 2
611
+ selfVV = self.derivative(wrt, point.uvw)
612
+ offset = np.sqrt(2.0 * evaluationEpsilon / \
613
+ np.linalg.norm(selfUU * sinTheta * sinTheta - 2.0 * selfUV * sinTheta * cosTheta + selfVV * cosTheta * cosTheta))
614
+ # Now, we can find the zeros of the split panel, checking to ensure each panel is within bounds first.
615
+ if point.uvw[0] + sinTheta * offset < 1.0 - epsilon and epsilon < point.uvw[1] - cosTheta * offset:
616
+ panelPoints += panel.trim(((point.uvw[0] + sinTheta * offset, 1.0), (0.0, point.uvw[1] - cosTheta * offset)) + ((None, None),) * (self.nInd - 2)).zeros()
617
+ if epsilon < point.uvw[0] - sinTheta * offset and point.uvw[1] + cosTheta * offset < 1.0 - epsilon:
618
+ panelPoints += panel.trim(((0.0, point.uvw[0] - sinTheta * offset), (point.uvw[1] + cosTheta * offset, 1.0)) + ((None, None),) * (self.nInd - 2)).zeros()
567
619
  # Sort zero points by their position along the panel boundary (using vector orthogonal to its normal).
568
620
  panelPoints.sort(key=lambda uvw: uvw[1] * cosTheta - uvw[0] * sinTheta)
569
621
  # Add d back to prepare for next turning point.
@@ -571,7 +623,7 @@ def contours(self):
571
623
  # Go through panel points, adding them to existing contours, creating new ones, or closing old ones.
572
624
  adjustment = 0 # Adjust index after a contour point is added or removed.
573
625
  for i, uvw in zip(range(len(panelPoints)), panelPoints):
574
- if np.allclose(point.uvw, uvw):
626
+ if point.uvw is not None and np.allclose(point.uvw, uvw):
575
627
  if point.det > 0.0:
576
628
  # Insert the turning point twice (second one appears before the first one in the points list).
577
629
  currentContourPoints.insert(i, [True, point.uvw]) # True indicates end point
@@ -594,6 +646,9 @@ def contours(self):
594
646
  # Now we just need to create splines for those contours using the Spline.contour method.
595
647
  splineContours = []
596
648
  for points in contourPoints:
597
- splineContours.append(bspy.spline.Spline.contour(self, points[1:])) # Skip endPoint boolean at start of points list
649
+ contour = bspy.spline.Spline.contour(self, points[1:]) # Skip endPoint boolean at start of points list
650
+ # Transform the contour to self's original domain.
651
+ contour.coefs = (contour.coefs.T * (domain[1] - domain[0]) + domain[0]).T
652
+ splineContours.append(contour)
598
653
 
599
654
  return splineContours
@@ -573,10 +573,10 @@ def normal_spline(self, indices=None):
573
573
  ord += order - 1 if ccm[i, j] and index != j else 0
574
574
  newOrd = max(newOrd, ord)
575
575
  newOrder.append(newOrd)
576
- uniqueKnots, counts = np.unique(knots, return_counts=True)
576
+ uniqueKnots, counts = np.unique(knots[order - 1:self.nCoef[i] + 1], return_counts=True)
577
577
  counts += newOrd - order + 1 # Because we're multiplying all the tangents, the knot elevation is one more
578
- counts[0] -= 1 # But not at the endpoints, which get reduced by one when taking the derivative
579
- counts[-1] -= 1 # But not at the endpoints, which get reduced by one when taking the derivative
578
+ counts[0] = newOrd # But not at the endpoints, which are full order as usual
579
+ counts[-1] = newOrd # But not at the endpoints, which are full order as usual
580
580
  newKnots.append(np.repeat(uniqueKnots, counts))
581
581
  # Also calculate the total number of coefficients, capturing how it progressively increases, and
582
582
  # using that calculation to span uvw from the starting knot to the end for each variable.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bspy
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: Library for manipulating and rendering non-uniform b-splines
5
5
  Home-page: http://github.com/ericbrec/bspy
6
6
  Author: Eric Brechner
@@ -2,14 +2,14 @@ bspy/__init__.py,sha256=XRBelbYtbhB6AWKh8DTsWVMDIteVxUm7Cf_S1fkqeEs,851
2
2
  bspy/_spline_domain.py,sha256=29_Mp0bn9_8z4ltu__DG9_NZuCiSuol0tRgAeIuzXZI,27826
3
3
  bspy/_spline_evaluation.py,sha256=i0qPy6UNe1MeoCOxF5LjTtloP6HUnxtxPHseAUAlmJU,6376
4
4
  bspy/_spline_fitting.py,sha256=NKNUfHfHYkQLlK_4i5oF7MIS59wrCqVKkPXHDcDGwIc,20552
5
- bspy/_spline_intersection.py,sha256=J6OO3-pDz4ozklT8Mn-TXf0AaH_07C6DN9co_9xur4w,31442
6
- bspy/_spline_operations.py,sha256=mkp1FeFNRDRyWln6-9Z0GBHZUB9FnXYwjpJWYHccs5U,35722
5
+ bspy/_spline_intersection.py,sha256=Pe_TiVHB8gVjZ74gFgHeg0tfn7DhYLEMpPx-bfGcP6A,34402
6
+ bspy/_spline_operations.py,sha256=8VIVLGgYln7lLplgVBpnMVHMWbjigvir-LHE8tusZ-A,35715
7
7
  bspy/bspyApp.py,sha256=UxmNp_IGaG7gMVexUmA7YfuwX5iuMBr7ibW3DRan8R8,14524
8
8
  bspy/drawableSpline.py,sha256=Ha7wh__aRTpGTr3cEDG49SeAyRrKtfMUQYkEZ3w9OJw,17004
9
9
  bspy/spline.py,sha256=WIPhSQu09laxQdtjpp4XDO8RscYRin4hF8ERVHLeE1o,59087
10
10
  bspy/splineOpenGLFrame.py,sha256=77rikAbAood76cf5w1Idj7wciTwPV-Bn-yGSuRFvlJM,60616
11
- bspy-1.2.2.dist-info/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
12
- bspy-1.2.2.dist-info/METADATA,sha256=ZMkhzIDDgtW6glsa2CrQ1nruhy0-AAkdtHmliK6iwcE,3777
13
- bspy-1.2.2.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
14
- bspy-1.2.2.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
15
- bspy-1.2.2.dist-info/RECORD,,
11
+ bspy-1.2.4.dist-info/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
12
+ bspy-1.2.4.dist-info/METADATA,sha256=Lms9vTCdZnxc4xPJJ4Aya4c5brPr-tpK3Vs3QWlj8m0,3777
13
+ bspy-1.2.4.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
14
+ bspy-1.2.4.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
15
+ bspy-1.2.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.1)
2
+ Generator: bdist_wheel (0.41.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
File without changes