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 CHANGED
@@ -12,6 +12,8 @@ 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 processes an array-like collection of splines which represent a system of equations.
16
+
15
17
  `bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
16
18
 
17
19
  `bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
@@ -22,5 +24,6 @@ from bspy.solid import Solid, Boundary
22
24
  from bspy.manifold import Manifold
23
25
  from bspy.hyperplane import Hyperplane
24
26
  from bspy.spline import Spline
27
+ from bspy.spline_block import SplineBlock
25
28
  from bspy.splineOpenGLFrame import SplineOpenGLFrame
26
29
  from bspy.viewer import Viewer, Graphics
bspy/_spline_domain.py CHANGED
@@ -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
@@ -535,10 +582,12 @@ def transpose(self, axes=None):
535
582
  def trim(self, newDomain):
536
583
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
537
584
  if self.nInd < 1: return self
538
- newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
585
+ newDomain = np.array(newDomain, self.knots[0].dtype, copy=True) # Force dtype and convert None to nan
586
+ epsilon = np.finfo(newDomain.dtype).eps
539
587
 
540
588
  # Step 1: Determine the knots to insert at the new domain bounds.
541
589
  newKnotsList = []
590
+ noChange = True
542
591
  for (order, knots, bounds) in zip(self.order, self.knots, newDomain):
543
592
  if not(len(bounds) == 2): raise ValueError("Invalid newDomain")
544
593
  unique, counts = np.unique(knots, return_counts=True)
@@ -548,28 +597,43 @@ def trim(self, newDomain):
548
597
  if not np.isnan(bounds[0]):
549
598
  if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
550
599
  leftBound = True
551
- multiplicity = order
552
600
  i = np.searchsorted(unique, bounds[0])
553
- if unique[i] == bounds[0]:
554
- multiplicity -= counts[i]
555
- newKnots += multiplicity * [bounds[0]]
601
+ if unique[i] - bounds[0] < epsilon:
602
+ bounds[0] = unique[i]
603
+ multiplicity = order - counts[i]
604
+ elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
605
+ bounds[0] = unique[i - 1]
606
+ multiplicity = order - counts[i - 1]
607
+ else:
608
+ multiplicity = order
609
+ if multiplicity > 0:
610
+ newKnots.append((bounds[0], multiplicity))
611
+ noChange = False
556
612
 
557
613
  if not np.isnan(bounds[1]):
558
614
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
559
615
  if leftBound:
560
616
  if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
561
- multiplicity = order
562
617
  i = np.searchsorted(unique, bounds[1])
563
- if unique[i] == bounds[1]:
564
- multiplicity -= counts[i]
565
- newKnots += multiplicity * [bounds[1]]
618
+ if unique[i] - bounds[1] < epsilon:
619
+ bounds[1] = unique[i]
620
+ multiplicity = order - counts[i]
621
+ elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
622
+ bounds[1] = unique[i - 1]
623
+ multiplicity = order - counts[i - i]
624
+ else:
625
+ multiplicity = order
626
+ if multiplicity > 0:
627
+ newKnots.append((bounds[1], multiplicity))
628
+ noChange = False
566
629
 
567
630
  newKnotsList.append(newKnots)
568
631
 
632
+ if noChange:
633
+ return self
634
+
569
635
  # Step 2: Insert the knots.
570
636
  spline = self.insert_knots(newKnotsList)
571
- if spline is self:
572
- return spline
573
637
 
574
638
  # Step 3: Trim the knots and coefficients.
575
639
  knotsList = []
@@ -1,4 +1,5 @@
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)
@@ -25,10 +26,61 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
25
26
  b += 1
26
27
  return knot, basis
27
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
+
28
80
  def curvature(self, uv):
81
+ if self.nDep == 1:
82
+ self = self.graph()
29
83
  if self.nInd == 1:
30
- if self.nDep == 1:
31
- self = self.graph()
32
84
  fp = self.derivative([1], uv)
33
85
  fpp = self.derivative([2], uv)
34
86
  fpDotFp = fp @ fp
@@ -38,7 +90,21 @@ def curvature(self, uv):
38
90
  numerator = fp[0] * fpp[1] - fp[1] * fpp[0]
39
91
  else:
40
92
  numerator = np.sqrt((fpp @ fpp) * fpDotFp - fpDotFpp ** 2)
41
- 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)
42
108
 
43
109
  def derivative(self, with_respect_to, uvw):
44
110
  # Make work for scalar valued functions
@@ -107,6 +173,8 @@ def greville(self, ind = 0):
107
173
  for ix in range(1, self.order[ind]):
108
174
  knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
109
175
  knotAverages /= (self.order[ind] - 1)
176
+ domain = self.domain()[ind]
177
+ knotAverages = np.minimum(domain[1], np.maximum(domain[0], knotAverages))
110
178
  return knotAverages
111
179
 
112
180
  def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
@@ -150,8 +218,8 @@ def normal(self, uvw, normalize=True, indices=None):
150
218
 
151
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.")
152
220
 
153
- # Evaluate the tangents at the point.
154
- tangentSpace = self.tangent_space(uvw)
221
+ # Evaluate the Jacobian at the point.
222
+ tangentSpace = self.jacobian(uvw)
155
223
 
156
224
  # Record the larger dimension and ensure it comes first.
157
225
  if self.nInd > self.nDep:
@@ -161,15 +229,15 @@ def normal(self, uvw, normalize=True, indices=None):
161
229
  nDep = self.nDep
162
230
 
163
231
  # Compute the normal using cofactors (determinants of subsets of the tangent space).
164
- sign = -1 if self.metadata.get("flipNormal", False) else 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
165
234
  if indices is None:
166
235
  indices = range(nDep)
167
- normal = np.empty(nDep, self.coefs.dtype)
236
+ normal = np.empty(nDep, dtype)
168
237
  else:
169
- normal = np.empty(len(indices), self.coefs.dtype)
170
- for i in indices:
171
- normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
172
- sign *= -1
238
+ normal = np.empty(len(indices), dtype)
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]])
173
241
 
174
242
  # Normalize the result as needed.
175
243
  if normalize:
@@ -180,13 +248,4 @@ def normal(self, uvw, normalize=True, indices=None):
180
248
  def range_bounds(self):
181
249
  # Assumes self.nDep is the first value in self.coefs.shape
182
250
  bounds = [[coefficient.min(), coefficient.max()] for coefficient in self.coefs]
183
- return np.array(bounds, self.coefs.dtype)
184
-
185
- def tangent_space(self, uvw):
186
- tangentSpace = np.empty((self.nDep, self.nInd), self.coefs.dtype)
187
- wrt = [0] * self.nInd
188
- for i in range(self.nInd):
189
- wrt[i] = 1
190
- tangentSpace[:, i] = self.derivative(wrt, uvw)
191
- wrt[i] = 0
192
- return tangentSpace
251
+ return np.array(bounds, self.coefs.dtype)