bspy 4.0__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 CHANGED
@@ -3,14 +3,27 @@ BSpy is a python library for manipulating and rendering non-uniform B-splines.
3
3
 
4
4
  Available subpackages
5
5
  ---------------------
6
- `bspy.spline` : Provides the `Spline` class that models, represents, and processes piecewise polynomial tensor product
7
- functions (spline functions) as linear combinations of B-splines.
6
+ `bspy.solid` : Provides the `Solid` and `Boundary` classes that model solids.
7
+
8
+ `bspy.manifold` : Provides the `Manifold` base class for manifolds.
9
+
10
+ `bspy.hyperplane` : Provides the `Hyperplane` subclass of `Manifold` that models hyperplanes.
11
+
12
+ `bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
13
+ piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
14
+
15
+ `bspy.spline_block` : Provides the `SplineBlock` class that represents and processes an array-like collection of splines.
8
16
 
9
17
  `bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
10
18
 
11
19
  `bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
12
20
  splines, and a set of controls to adjust and view the selected splines. It also provides the `Graphics` engine that creates
13
- an associated `Viewer`, allowing you to script splines and display them in the viewer."""
21
+ an associated `Viewer`, allowing you to script splines and display them in the viewer.
22
+ """
23
+ from bspy.solid import Solid, Boundary
24
+ from bspy.manifold import Manifold
25
+ from bspy.hyperplane import Hyperplane
14
26
  from bspy.spline import Spline
27
+ from bspy.spline_block import SplineBlock
15
28
  from bspy.splineOpenGLFrame import SplineOpenGLFrame
16
29
  from bspy.viewer import Viewer, Graphics
bspy/_spline_domain.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ from bspy.manifold import Manifold
2
3
 
3
4
  def clamp(self, left, right):
4
5
  bounds = [[None, None] for i in range(self.nInd)]
@@ -517,6 +518,66 @@ def reverse(self, variable = 0):
517
518
  newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
518
519
  return newFolded.unfold(myIndices, basisInfo)
519
520
 
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
+
520
581
  def transpose(self, axes=None):
521
582
  if axes is None:
522
583
  axes = range(self.nInd)[::-1]
@@ -534,10 +595,12 @@ def transpose(self, axes=None):
534
595
  def trim(self, newDomain):
535
596
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
536
597
  if self.nInd < 1: return self
537
- newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
598
+ newDomain = np.array(newDomain, self.knots[0].dtype, copy=True) # Force dtype and convert None to nan
599
+ epsilon = np.finfo(newDomain.dtype).eps
538
600
 
539
601
  # Step 1: Determine the knots to insert at the new domain bounds.
540
602
  newKnotsList = []
603
+ noChange = True
541
604
  for (order, knots, bounds) in zip(self.order, self.knots, newDomain):
542
605
  if not(len(bounds) == 2): raise ValueError("Invalid newDomain")
543
606
  unique, counts = np.unique(knots, return_counts=True)
@@ -547,28 +610,49 @@ def trim(self, newDomain):
547
610
  if not np.isnan(bounds[0]):
548
611
  if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
549
612
  leftBound = True
550
- multiplicity = order
551
613
  i = np.searchsorted(unique, bounds[0])
552
- if unique[i] == bounds[0]:
553
- multiplicity -= counts[i]
614
+ if unique[i] - bounds[0] < epsilon:
615
+ bounds[0] = unique[i]
616
+ multiplicity = order - counts[i]
617
+ if i > 0:
618
+ noChange = False
619
+ elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
620
+ bounds[0] = unique[i - 1]
621
+ multiplicity = order - counts[i - 1]
622
+ if i - 1 > 0:
623
+ noChange = False
624
+ else:
625
+ multiplicity = order
626
+
554
627
  newKnots += multiplicity * [bounds[0]]
555
628
 
556
629
  if not np.isnan(bounds[1]):
557
630
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
558
631
  if leftBound:
559
632
  if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
560
- multiplicity = order
561
633
  i = np.searchsorted(unique, bounds[1])
562
- if unique[i] == bounds[1]:
563
- multiplicity -= counts[i]
634
+ if unique[i] - bounds[1] < epsilon:
635
+ bounds[1] = unique[i]
636
+ multiplicity = order - counts[i]
637
+ if i < len(unique) - 1:
638
+ noChange = False
639
+ elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
640
+ bounds[1] = unique[i - 1]
641
+ multiplicity = order - counts[i - i]
642
+ noChange = False # i < len(unique) - 1
643
+ else:
644
+ multiplicity = order
564
645
  newKnots += multiplicity * [bounds[1]]
565
646
 
566
647
  newKnotsList.append(newKnots)
648
+ if len(newKnots) > 0:
649
+ noChange = False
650
+
651
+ if noChange:
652
+ return self
567
653
 
568
654
  # Step 2: Insert the knots.
569
655
  spline = self.insert_knots(newKnotsList)
570
- if spline is self:
571
- return spline
572
656
 
573
657
  # Step 3: Trim the knots and coefficients.
574
658
  knotsList = []
@@ -582,6 +666,14 @@ def trim(self, newDomain):
582
666
 
583
667
  return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
584
668
 
669
+ def trimmed_range_bounds(self, domainBounds):
670
+ domainBounds = np.array(domainBounds, copy=True)
671
+ for original, trim in zip(self.domain(), domainBounds):
672
+ trim[0] = max(original[0], trim[0] - Manifold.minSeparation)
673
+ trim[1] = min(original[1], trim[1] + Manifold.minSeparation)
674
+ trimmedSpline = self.trim(domainBounds)
675
+ return trimmedSpline, trimmedSpline.range_bounds()
676
+
585
677
  def unfold(self, foldedInd, coefficientlessSpline):
586
678
  if not(len(foldedInd) == coefficientlessSpline.nInd): raise ValueError("Invalid coefficientlessSpline")
587
679
  unfoldedOrder = []
@@ -1,11 +1,14 @@
1
1
  import numpy as np
2
+ import scipy as sp
2
3
 
3
4
  def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
4
5
  basis = np.zeros(splineOrder, knots.dtype)
5
- basis[-1] = 1.0
6
6
  if knot is None:
7
7
  knot = np.searchsorted(knots, u, side = 'right')
8
8
  knot = min(knot, len(knots) - splineOrder)
9
+ if derivativeOrder >= splineOrder:
10
+ return knot, basis
11
+ basis[-1] = 1.0
9
12
  for degree in range(1, splineOrder - derivativeOrder):
10
13
  b = splineOrder - degree
11
14
  for i in range(knot - degree, knot):
@@ -23,10 +26,61 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
23
26
  b += 1
24
27
  return knot, basis
25
28
 
29
+ def composed_integral(self, integrand = None, domain = None):
30
+ # Determine domain and check its validity
31
+ actualDomain = self.domain()
32
+ if domain is None:
33
+ domain = actualDomain
34
+ else:
35
+ for iInd in range(self.nInd):
36
+ if domain[iInd, 0] < actualDomain[iInd, 0] or \
37
+ domain[iInd, 1] > actualDomain[iInd, 1]:
38
+ raise ValueError("Can't integrate beyond the domain of the spline")
39
+
40
+ # Determine breakpoints for quadrature intervals; require functions to be analytic
41
+
42
+ uniqueKnots = []
43
+ for iInd in range(self.nInd):
44
+ iStart = np.searchsorted(self.knots[iInd], domain[iInd, 0], side = 'right')
45
+ iEnd = np.searchsorted(self.knots[iInd], domain[iInd, 1], side = 'right')
46
+ uniqueKnots.append(np.unique(np.insert(self.knots[iInd], [iStart, iEnd], domain[iInd])[iStart : iEnd + 2]))
47
+
48
+ # Set integrand function if none is given
49
+ if integrand is None:
50
+ integrand = lambda x : 1.0
51
+
52
+ # Set tolerance
53
+ tolerance = 1.0e-13 / self.nInd
54
+
55
+ # Establish the callback function
56
+ def composedIntegrand(u, nIndSoFar, uValues):
57
+ uValues[nIndSoFar] = u
58
+ nIndSoFar += 1
59
+ if self.nInd == nIndSoFar:
60
+ total = integrand(self(uValues)) * \
61
+ np.prod(np.linalg.svd(self.jacobian(uValues), compute_uv = False))
62
+ else:
63
+ total = 0.0
64
+ for ix in range(len(uniqueKnots[nIndSoFar]) - 1):
65
+ value = sp.integrate.quad(composedIntegrand, uniqueKnots[nIndSoFar][ix],
66
+ uniqueKnots[nIndSoFar][ix + 1], (nIndSoFar, uValues),
67
+ epsabs = tolerance, epsrel = tolerance)
68
+ total += value[0]
69
+ return total
70
+
71
+ # Compute the value by calling the callback routine
72
+ total = composedIntegrand(0.0, -1, self.nInd * [0.0])
73
+ return total
74
+
75
+ def continuity(self):
76
+ multiplicity = np.array([np.max(np.unique(knots, return_counts = True)[1][1 : -1]) for knots in self.knots])
77
+ continuity = self.order - multiplicity - 1
78
+ return continuity
79
+
26
80
  def curvature(self, uv):
81
+ if self.nDep == 1:
82
+ self = self.graph()
27
83
  if self.nInd == 1:
28
- if self.nDep == 1:
29
- self = self.graph()
30
84
  fp = self.derivative([1], uv)
31
85
  fpp = self.derivative([2], uv)
32
86
  fpDotFp = fp @ fp
@@ -36,7 +90,21 @@ def curvature(self, uv):
36
90
  numerator = fp[0] * fpp[1] - fp[1] * fpp[0]
37
91
  else:
38
92
  numerator = np.sqrt((fpp @ fpp) * fpDotFp - fpDotFpp ** 2)
39
- return numerator / denom
93
+ return numerator / denom
94
+ if self.nInd == 2:
95
+ su = self.derivative([1, 0], uv)
96
+ sv = self.derivative([0, 1], uv)
97
+ normal = self.normal(uv)
98
+ suu = self.derivative([2, 0], uv)
99
+ suv = self.derivative([1, 1], uv)
100
+ svv = self.derivative([0, 2], uv)
101
+ E = su @ su
102
+ F = su @ sv
103
+ G = sv @ sv
104
+ L = suu @ normal
105
+ M = suv @ normal
106
+ N = svv @ normal
107
+ return (L * N - M ** 2) / (E * G - F ** 2)
40
108
 
41
109
  def derivative(self, with_respect_to, uvw):
42
110
  # Make work for scalar valued functions
@@ -105,6 +173,8 @@ def greville(self, ind = 0):
105
173
  for ix in range(1, self.order[ind]):
106
174
  knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
107
175
  knotAverages /= (self.order[ind] - 1)
176
+ domain = self.domain()[ind]
177
+ knotAverages = np.minimum(domain[1], np.maximum(domain[0], knotAverages))
108
178
  return knotAverages
109
179
 
110
180
  def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
@@ -148,31 +218,26 @@ def normal(self, uvw, normalize=True, indices=None):
148
218
 
149
219
  if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be one different than the number of dependent variables.")
150
220
 
151
- # Evaluate the tangents at the point.
152
- tangentSpace = np.empty((self.nInd, self.nDep), self.coefs.dtype)
153
- with_respect_to = [0] * self.nInd
154
- for i in range(self.nInd):
155
- with_respect_to[i] = 1
156
- tangentSpace[i] = self.derivative(with_respect_to, uvw)
157
- with_respect_to[i] = 0
221
+ # Evaluate the Jacobian at the point.
222
+ tangentSpace = self.jacobian(uvw)
158
223
 
159
224
  # Record the larger dimension and ensure it comes first.
160
225
  if self.nInd > self.nDep:
161
226
  nDep = self.nInd
227
+ tangentSpace = tangentSpace.T
162
228
  else:
163
229
  nDep = self.nDep
164
- tangentSpace = tangentSpace.T
165
230
 
166
231
  # Compute the normal using cofactors (determinants of subsets of the tangent space).
167
- sign = 1
232
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
233
+ dtype = self.coefs.dtype if hasattr(self, "coefs") else self.coefsDtype
168
234
  if indices is None:
169
235
  indices = range(nDep)
170
- normal = np.empty(nDep, self.coefs.dtype)
236
+ normal = np.empty(nDep, dtype)
171
237
  else:
172
- normal = np.empty(len(indices), self.coefs.dtype)
238
+ normal = np.empty(len(indices), dtype)
173
239
  for i in indices:
174
- normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
175
- sign *= -1
240
+ normal[i] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
176
241
 
177
242
  # Normalize the result as needed.
178
243
  if normalize: