bspy 4.2__tar.gz → 4.3__tar.gz

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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bspy
3
- Version: 4.2
3
+ Version: 4.3
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
7
7
  Author-email: ericbrec@msn.com
8
8
  License: MIT
9
9
  Project-URL: Bug Tracker, http://github.com/ericbrec/BSpy/issues
10
- Keywords: opengl,bspline,B-spline,nub,tkinter
10
+ Keywords: bspline,B-spline,nub,solid,solid modeling,geometry,csg,opengl,tkinter
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Environment :: Win32 (MS Windows)
13
13
  Classifier: Environment :: Console
@@ -47,7 +47,9 @@ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has
47
47
 
48
48
  The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
49
49
 
50
- The [SplineBlock](https://ericbrec.github.io/BSpy/bspy/spline_block.html) class has methods to represent and process an array-like collection of splines, including ones to compute the contours and zeros of a spline block, as well as a variety of methods to evaluate a spline block and its derivatives. Spline blocks are useful for efficiently manipulating and solving systems of equations with splines.
50
+ The [SplineBlock](https://ericbrec.github.io/BSpy/bspy/spline_block.html) class has methods to process an array-like collection of splines that represent a system of equations. There are highly-optimized methods to compute the contours and zeros of a spline block, as well as a variety of methods to manipulate and evaluate a spline block and its derivatives.
51
+
52
+ The [BSpyConvert](https://pypi.org/project/BSpyConvert/) package converts BSpy splines and solid models to and from [OpenCascade (OCCT)](https://dev.opencascade.org/) equivalents and a variety of geometry and CAD file formats, including STEP, IGES, and STL.
51
53
 
52
54
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
53
55
  [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
@@ -16,7 +16,9 @@ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has
16
16
 
17
17
  The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
18
18
 
19
- The [SplineBlock](https://ericbrec.github.io/BSpy/bspy/spline_block.html) class has methods to represent and process an array-like collection of splines, including ones to compute the contours and zeros of a spline block, as well as a variety of methods to evaluate a spline block and its derivatives. Spline blocks are useful for efficiently manipulating and solving systems of equations with splines.
19
+ The [SplineBlock](https://ericbrec.github.io/BSpy/bspy/spline_block.html) class has methods to process an array-like collection of splines that represent a system of equations. There are highly-optimized methods to compute the contours and zeros of a spline block, as well as a variety of methods to manipulate and evaluate a spline block and its derivatives.
20
+
21
+ The [BSpyConvert](https://pypi.org/project/BSpyConvert/) package converts BSpy splines and solid models to and from [OpenCascade (OCCT)](https://dev.opencascade.org/) equivalents and a variety of geometry and CAD file formats, including STEP, IGES, and STL.
20
22
 
21
23
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
22
24
  [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
@@ -12,7 +12,7 @@ Available subpackages
12
12
  `bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
13
13
  piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
14
14
 
15
- `bspy.spline_block` : Provides the `SplineBlock` class that represents and processes an array-like collection of splines.
15
+ `bspy.spline_block` : Provides the `SplineBlock` class that processes an array-like collection of splines which represent a system of equations.
16
16
 
17
17
  `bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
18
18
 
@@ -310,26 +310,73 @@ def fold(self, foldedInd):
310
310
  coefficientlessSpline = type(self)(len(coefficientlessOrder), 0, coefficientlessOrder, coefficientlessNCoef, coefficientlessKnots, coefficientlessCoefs, self.metadata)
311
311
  return foldedSpline, coefficientlessSpline
312
312
 
313
- def insert_knots(self, newKnots):
314
- if not(len(newKnots) == self.nInd): raise ValueError("Invalid newKnots")
315
- knotsList = list(self.knots)
316
- coefs = self.coefs
317
- for ind, (order, knots) in enumerate(zip(self.order, self.knots)):
318
- # We can't reference self.nCoef[ind] in this loop because we are expanding the knots and coefs arrays.
319
- for knot in newKnots[ind]:
320
- if knot < knots[order-1] or knot > knots[-order]:
321
- raise ValueError(f"Knot insertion outside domain: {knot}")
322
- if knot == knots[-order]:
323
- position = len(knots) - order
313
+ def insert_knots(self, newKnotList):
314
+ if not(len(newKnotList) == self.nInd): raise ValueError("Invalid newKnots")
315
+ knotsList = list(self.knots) # Create a new knot list
316
+ coefs = self.coefs # Set initial value for coefs to check later if it's changed
317
+
318
+ # Insert new knots into each independent variable.
319
+ for ind, (order, knots, newKnots) in enumerate(zip(self.order, self.knots, newKnotList)):
320
+ coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
321
+ degree = order - 1
322
+ for knot in newKnots:
323
+ # Determine new knot multiplicity.
324
+ if np.isscalar(knot):
325
+ multiplicity = 1
324
326
  else:
325
- position = np.searchsorted(knots, knot, 'right')
326
- coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
327
- newCoefs = np.insert(coefs, position - 1, 0.0, axis=0)
328
- for i in range(position - order + 1, position):
329
- alpha = (knot - knots[i]) / (knots[i + order - 1] - knots[i])
330
- newCoefs[i] = (1.0 - alpha) * coefs[i - 1] + alpha * coefs[i]
331
- knotsList[ind] = knots = np.insert(knots, position, knot)
332
- coefs = newCoefs.swapaxes(0, ind + 1)
327
+ multiplicity = knot[1]
328
+ knot = knot[0]
329
+ if multiplicity < 1:
330
+ continue
331
+
332
+ # Check if knot and its total multiplicity is valid.
333
+ if knot < knots[degree] or knot > knots[-order]:
334
+ raise ValueError(f"Knot insertion outside domain: {knot}")
335
+ position = np.searchsorted(knots, knot, 'right')
336
+ oldMultiplicity = 0
337
+ for k in knots[position - 1::-1]:
338
+ if knot == k:
339
+ oldMultiplicity += 1
340
+ else:
341
+ break
342
+ if oldMultiplicity + multiplicity > order:
343
+ raise ValueError("Knot multiplicity > order")
344
+
345
+ # Initialize oldCoefs and expanded coefs array with multiplicity new coefficients, as well as some indices.
346
+ oldCoefs = coefs[position - order:position].copy()
347
+ lastKnotIndex = position - oldMultiplicity
348
+ firstCoefIndex = position - degree
349
+ coefs = np.insert(coefs, firstCoefIndex, oldCoefs[:multiplicity], axis=0)
350
+ # Compute inserted coefficients (multiplicity of them) and the degree - oldMultiplicity - 1 number of changed coefficients.
351
+ for j in range(multiplicity):
352
+ # Allocate new coefficients for the current multiplicity.
353
+ size = degree - oldMultiplicity - j
354
+ if size < 1:
355
+ # Full multiplicity knot, so use oldCoefs.
356
+ coefs[firstCoefIndex + j] = oldCoefs[0]
357
+ else:
358
+ # Otherwise, allocate space for newCoefs.
359
+ newCoefs = np.empty((size, *coefs.shape[1:]), coefs.dtype)
360
+
361
+ # Compute the new coefficients.
362
+ for i, k in zip(range(size), range(lastKnotIndex - size, lastKnotIndex)):
363
+ alpha = (knot - knots[k]) / (knots[k + degree - j] - knots[k])
364
+ newCoefs[i] = (1.0 - alpha) * oldCoefs[i] + alpha * oldCoefs[i + 1]
365
+
366
+ # Assign the ends of the new coefficients into their respective positions.
367
+ coefs[firstCoefIndex + j] = newCoefs[0]
368
+ if size > 1:
369
+ coefs[lastKnotIndex + multiplicity - j - 2] = newCoefs[-1]
370
+ oldCoefs = newCoefs
371
+
372
+ # Assign remaining computed coefficients (the ones in the middle).
373
+ if size > 2:
374
+ coefs[firstCoefIndex + multiplicity:firstCoefIndex + multiplicity + size - 2] = newCoefs[1:-1]
375
+
376
+ # Insert the inserted coefficients and inserted knots.
377
+ knotsList[ind] = knots = np.insert(knots, position, (knot,) * multiplicity)
378
+
379
+ coefs = coefs.swapaxes(0, ind + 1) # Swap back
333
380
 
334
381
  if self.coefs is coefs:
335
382
  return self
@@ -518,66 +565,6 @@ def reverse(self, variable = 0):
518
565
  newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
519
566
  return newFolded.unfold(myIndices, basisInfo)
520
567
 
521
- def split(self, minContinuity = 0, breaks = None):
522
- if minContinuity < 0: raise ValueError("minContinuity must be >= 0")
523
- if breaks is not None and len(breaks) != self.nInd: raise ValueError("Invalid breaks")
524
- if self.nInd < 1: return self
525
-
526
- # Step 1: Determine the knots to insert.
527
- newKnotsList = []
528
- for i, order, knots in zip(range(self.nInd), self.order, self.knots):
529
- unique, counts = np.unique(knots, return_counts=True)
530
- newKnots = []
531
- for knot, count in zip(unique, counts):
532
- assert count <= order
533
- if count > order - 1 - minContinuity:
534
- newKnots += [knot] * (order - count)
535
- if breaks is not None:
536
- for knot in breaks[i]:
537
- if knot not in unique:
538
- newKnots += [knot] * order
539
- newKnotsList.append(newKnots)
540
-
541
- # Step 2: Insert the knots.
542
- spline = self.insert_knots(newKnotsList)
543
- if spline is self:
544
- return np.full((1,) * spline.nInd, spline)
545
-
546
- # Step 3: Store the indices of the full order knots.
547
- indexList = []
548
- splineCount = []
549
- totalSplineCount = 1
550
- for order, knots in zip(spline.order, spline.knots):
551
- unique, counts = np.unique(knots, return_counts=True)
552
- indices = np.searchsorted(knots, unique)
553
- fullOrder = []
554
- for ix, count in zip(indices, counts):
555
- if count == order:
556
- fullOrder.append(ix)
557
- indexList.append(fullOrder)
558
- splines = len(fullOrder) - 1
559
- splineCount.append(splines)
560
- totalSplineCount *= splines
561
-
562
- # Step 4: Slice up the spline.
563
- splineArray = np.empty(totalSplineCount, object)
564
- for i in range(totalSplineCount):
565
- knotsList = []
566
- coefIndex = [slice(None)] # First index is for nDep
567
- ix = i
568
- for order, knots, splines, indices in zip(spline.order, spline.knots, splineCount, indexList):
569
- j = ix % splines
570
- ix = ix // splines
571
- leftIndex = indices[j]
572
- rightIndex = indices[j + 1]
573
- knotsList.append(knots[leftIndex:rightIndex + order])
574
- coefIndex.append(slice(leftIndex, rightIndex))
575
- coefs = spline.coefs[tuple(coefIndex)]
576
- splineArray[i] = type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
577
-
578
- # Return the transpose because we put the splines into splineArray dimensions in reverse order.
579
- return splineArray.reshape(tuple(reversed(splineCount))).T
580
-
581
568
  def transpose(self, axes=None):
582
569
  if axes is None:
583
570
  axes = range(self.nInd)[::-1]
@@ -614,17 +601,14 @@ def trim(self, newDomain):
614
601
  if unique[i] - bounds[0] < epsilon:
615
602
  bounds[0] = unique[i]
616
603
  multiplicity = order - counts[i]
617
- if i > 0:
618
- noChange = False
619
604
  elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
620
605
  bounds[0] = unique[i - 1]
621
606
  multiplicity = order - counts[i - 1]
622
- if i - 1 > 0:
623
- noChange = False
624
607
  else:
625
608
  multiplicity = order
626
-
627
- newKnots += multiplicity * [bounds[0]]
609
+ if multiplicity > 0:
610
+ newKnots.append((bounds[0], multiplicity))
611
+ noChange = False
628
612
 
629
613
  if not np.isnan(bounds[1]):
630
614
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
@@ -634,19 +618,16 @@ def trim(self, newDomain):
634
618
  if unique[i] - bounds[1] < epsilon:
635
619
  bounds[1] = unique[i]
636
620
  multiplicity = order - counts[i]
637
- if i < len(unique) - 1:
638
- noChange = False
639
621
  elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
640
622
  bounds[1] = unique[i - 1]
641
623
  multiplicity = order - counts[i - i]
642
- noChange = False # i < len(unique) - 1
643
624
  else:
644
625
  multiplicity = order
645
- newKnots += multiplicity * [bounds[1]]
626
+ if multiplicity > 0:
627
+ newKnots.append((bounds[1], multiplicity))
628
+ noChange = False
646
629
 
647
630
  newKnotsList.append(newKnots)
648
- if len(newKnots) > 0:
649
- noChange = False
650
631
 
651
632
  if noChange:
652
633
  return self
@@ -236,8 +236,8 @@ def normal(self, uvw, normalize=True, indices=None):
236
236
  normal = np.empty(nDep, dtype)
237
237
  else:
238
238
  normal = np.empty(len(indices), dtype)
239
- for i in indices:
240
- normal[i] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
239
+ for ix, i in enumerate(indices):
240
+ normal[ix] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
241
241
 
242
242
  # Normalize the result as needed.
243
243
  if normalize:
@@ -904,11 +904,11 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
904
904
  for i, knot in enumerate(uniqueKnots):
905
905
  howMany = currentGuess.order[0] - indices[i + 1] + indices[i] - nOrder
906
906
  if howMany > 0:
907
- knotsToAdd += howMany * [knot]
907
+ knotsToAdd.append((knot, howMany))
908
908
  if howMany < 0:
909
- knotsToRemove += [[knot, abs(howMany)]]
909
+ knotsToRemove.append((knot, abs(howMany)))
910
910
  currentGuess = currentGuess.insert_knots([knotsToAdd])
911
- for [knot, howMany] in knotsToRemove:
911
+ for (knot, howMany) in knotsToRemove:
912
912
  ix = np.searchsorted(currentGuess.knots[0], knot, side = 'left')
913
913
  for iy in range(howMany):
914
914
  currentGuess, residual = currentGuess.remove_knot(ix, nLeft, nRight)
@@ -1065,7 +1065,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
1065
1065
  knotsToAdd = []
1066
1066
  for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
1067
1067
  if knot0 < knot1:
1068
- knotsToAdd += (currentGuess.order[0] - nOrder) * [0.5 * (knot0 + knot1)]
1068
+ knotsToAdd.append((0.5 * (knot0 + knot1), currentGuess.order[0] - nOrder))
1069
1069
  previousGuess = currentGuess
1070
1070
  currentGuess = currentGuess.insert_knots([knotsToAdd])
1071
1071