bspy 3.0.1__tar.gz → 4.1__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bspy
3
- Version: 3.0.1
3
+ Version: 4.1
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
@@ -25,36 +25,38 @@ Requires-Python: >=3.0
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Requires-Dist: numpy
28
+ Requires-Dist: scipy
28
29
  Requires-Dist: PyOpenGL
29
30
  Requires-Dist: pyopengltk
30
31
 
31
32
  # BSpy
32
33
  Library for manipulating and rendering B-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
33
34
 
35
+ The [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) abstract base class for [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) and [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html).
36
+
34
37
  The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for
35
38
  scalar and vector functions of single and multiple variables. It also has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, and four-sided patches.
36
39
  Other methods add, subtract, and multiply splines, as well as confine spline curves to a given range.
37
40
  There are methods to evaluate spline values, derivatives, integrals, normals, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions. In addition, there are methods to manipulate the domain of splines, including trim, join, reparametrize, transpose, reverse, add and remove knots, elevate and extrapolate, and fold and unfold. There are methods to manipulate the range of splines, including dot product, cross product, translate, rotate, scale, and transform. Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
38
41
 
39
- The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
40
- [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Only tested on Windows systems.
42
+ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has methods to create individual hyperplanes in any dimension, along with axis-aligned hyperplanes and hypercubes.
41
43
 
42
- The [DrawableSpline](https://ericbrec.github.io/BSpy/bspy/drawableSpline.html) class converts a
43
- [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) to a curve, surface, or solid that can be drawn in a
44
- [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html). Only 1D, 2D, and 3D splines can be converted.
45
- Spline surfaces and solids with more than 3 dependent variables will have their added dimensions rendered as colors
46
- (up to 6 dependent variables are supported).
44
+ 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.
47
45
 
48
- The [bspyApp](https://ericbrec.github.io/BSpy/bspy/bspyApp.html) class is a
46
+ The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
47
+ [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
48
+ than 3 dependent variables will have their added dimensions rendered as colors (up to 6 dependent variables are supported). Only tested on Windows systems.
49
+
50
+ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
49
51
  [tkinter.Tk](https://docs.python.org/3/library/tkinter.html) app that hosts a
50
52
  [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
51
- a listbox full of splines, and a set of controls to adjust and view the selected splines. Only tested on Windows systems.
53
+ a tree view full of solids and splines, and a set of controls to adjust and view the selected solids and splines. Only tested on Windows systems.
52
54
 
53
- The [bspyGraphics](https://ericbrec.github.io/BSpy/bspy/bspyApp.html#bspyGraphics) class is a graphics engine to display splines.
54
- It launches a [bspyApp](https://ericbrec.github.io/BSpy/bspy/bspyApp.html) and issues commands to the app for use
55
+ The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display splines.
56
+ It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
55
57
  in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
56
58
 
57
- ![bspyApp rendering the Utah teapot](https://ericbrec.github.io/BSpy/bspyApp.png "bspyApp rendering the Utah teapot")
59
+ ![Viewer rendering the Utah teapot](https://ericbrec.github.io/BSpy/viewer.png "Viewer rendering the Utah teapot")
58
60
 
59
61
  The full documentation for BSpy can be found [here](https://ericbrec.github.io/BSpy), its GitHub project can be found
60
62
  [here](https://github.com/ericbrec/BSpy), a test suite can be found [here](https://github.com/ericbrec/BSpy/tree/main/tests), and
@@ -65,3 +67,13 @@ a set of examples, including a jupyter notebook, can be found [here](https://git
65
67
  * Spline.common_basis is now a static method (see documentation for details)
66
68
  * Spline.least_squares changed arguments (see documentation for details)
67
69
  * Spline.load always returns a list of splines
70
+
71
+ ### Release 4.0 breaking changes
72
+ * Removed Spline blossom method
73
+ * Removed DrawableSpline class
74
+ * Changed bspyApp class name to Viewer
75
+ * Changed Viewer listbox to use extended selection (shift and ctrl keys)
76
+ * Changed bspyGraphics class name to Graphics
77
+ * Moved DrawableSpine methods for adjusting spline appearance to Viewer (see documentation for details)
78
+ * Spline.bspline_values changed arguments (see documentation for details)
79
+ * Spline.intersect changed return values (see documentation for details)
bspy-4.1/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # BSpy
2
+ Library for manipulating and rendering B-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
3
+
4
+ The [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) abstract base class for [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) and [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html).
5
+
6
+ The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for
7
+ scalar and vector functions of single and multiple variables. It also has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, and four-sided patches.
8
+ Other methods add, subtract, and multiply splines, as well as confine spline curves to a given range.
9
+ There are methods to evaluate spline values, derivatives, integrals, normals, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions. In addition, there are methods to manipulate the domain of splines, including trim, join, reparametrize, transpose, reverse, add and remove knots, elevate and extrapolate, and fold and unfold. There are methods to manipulate the range of splines, including dot product, cross product, translate, rotate, scale, and transform. Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
10
+
11
+ The [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) class has methods to create individual hyperplanes in any dimension, along with axis-aligned hyperplanes and hypercubes.
12
+
13
+ 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.
14
+
15
+ The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
16
+ [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
17
+ than 3 dependent variables will have their added dimensions rendered as colors (up to 6 dependent variables are supported). Only tested on Windows systems.
18
+
19
+ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
20
+ [tkinter.Tk](https://docs.python.org/3/library/tkinter.html) app that hosts a
21
+ [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
22
+ a tree view full of solids and splines, and a set of controls to adjust and view the selected solids and splines. Only tested on Windows systems.
23
+
24
+ The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display splines.
25
+ It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
26
+ in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
27
+
28
+ ![Viewer rendering the Utah teapot](https://ericbrec.github.io/BSpy/viewer.png "Viewer rendering the Utah teapot")
29
+
30
+ The full documentation for BSpy can be found [here](https://ericbrec.github.io/BSpy), its GitHub project can be found
31
+ [here](https://github.com/ericbrec/BSpy), a test suite can be found [here](https://github.com/ericbrec/BSpy/tree/main/tests), and
32
+ a set of examples, including a jupyter notebook, can be found [here](https://github.com/ericbrec/BSpy/tree/main/examples).
33
+
34
+ ### Release 3.0 breaking changes
35
+ * Removed accuracy as a member of Spline
36
+ * Spline.common_basis is now a static method (see documentation for details)
37
+ * Spline.least_squares changed arguments (see documentation for details)
38
+ * Spline.load always returns a list of splines
39
+
40
+ ### Release 4.0 breaking changes
41
+ * Removed Spline blossom method
42
+ * Removed DrawableSpline class
43
+ * Changed bspyApp class name to Viewer
44
+ * Changed Viewer listbox to use extended selection (shift and ctrl keys)
45
+ * Changed bspyGraphics class name to Graphics
46
+ * Moved DrawableSpine methods for adjusting spline appearance to Viewer (see documentation for details)
47
+ * Spline.bspline_values changed arguments (see documentation for details)
48
+ * Spline.intersect changed return values (see documentation for details)
@@ -0,0 +1,26 @@
1
+ """
2
+ BSpy is a python library for manipulating and rendering non-uniform B-splines.
3
+
4
+ Available subpackages
5
+ ---------------------
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.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
16
+
17
+ `bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
18
+ splines, and a set of controls to adjust and view the selected splines. It also provides the `Graphics` engine that creates
19
+ an associated `Viewer`, allowing you to script splines and display them in the viewer.
20
+ """
21
+ from bspy.solid import Solid, Boundary
22
+ from bspy.manifold import Manifold
23
+ from bspy.hyperplane import Hyperplane
24
+ from bspy.spline import Spline
25
+ from bspy.splineOpenGLFrame import SplineOpenGLFrame
26
+ from bspy.viewer import Viewer, Graphics
@@ -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)]
@@ -12,10 +13,15 @@ def clamp(self, left, right):
12
13
  return self.trim(bounds)
13
14
 
14
15
  def common_basis(splines, indMap):
15
- # Step 1: Compute the order for each aligned independent variable.
16
- orders = []
16
+ # Fill out the default indMap.
17
17
  if indMap is None:
18
18
  indMap = [len(splines) * [iInd] for iInd in range(splines[0].nInd)]
19
+
20
+ # Ensure all splines are clamped at both ends.
21
+ splines = [spline.clamp(tuple(range(spline.nInd)), tuple(range(spline.nInd))) for spline in splines]
22
+
23
+ # Step 1: Compute the order for each aligned independent variable.
24
+ orders = []
19
25
  for map in indMap:
20
26
  if not(len(map) == len(splines)): raise ValueError("Invalid map")
21
27
  order = 0
@@ -269,7 +275,7 @@ def extrapolate(self, newDomain, continuityOrder):
269
275
  # Swap dependent and independent variables back.
270
276
  dCoefs = dCoefs.swapaxes(1, ind + 2)
271
277
 
272
- return type(self)(self.nInd, self.nDep, self.order, nCoef, knots, dCoefs[0], self.metadata)
278
+ return type(self)(self.nInd, self.nDep, self.order, nCoef, knots, dCoefs[0], self.metadata).remove_knots()
273
279
 
274
280
  def fold(self, foldedInd):
275
281
  if not(0 <= len(foldedInd) <= self.nInd): raise ValueError("Invalid foldedInd")
@@ -306,29 +312,29 @@ def fold(self, foldedInd):
306
312
 
307
313
  def insert_knots(self, newKnots):
308
314
  if not(len(newKnots) == self.nInd): raise ValueError("Invalid newKnots")
309
- knots = list(self.knots)
315
+ knotsList = list(self.knots)
310
316
  coefs = self.coefs
311
- for ind in range(self.nInd):
317
+ for ind, (order, knots) in enumerate(zip(self.order, self.knots)):
312
318
  # We can't reference self.nCoef[ind] in this loop because we are expanding the knots and coefs arrays.
313
319
  for knot in newKnots[ind]:
314
- if knot < knots[ind][self.order[ind]-1] or knot > knots[ind][-self.order[ind]]:
320
+ if knot < knots[order-1] or knot > knots[-order]:
315
321
  raise ValueError(f"Knot insertion outside domain: {knot}")
316
- if knot == knots[ind][-self.order[ind]]:
317
- position = len(knots[ind]) - self.order[ind]
322
+ if knot == knots[-order]:
323
+ position = len(knots) - order
318
324
  else:
319
- position = np.searchsorted(knots[ind], knot, 'right')
325
+ position = np.searchsorted(knots, knot, 'right')
320
326
  coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
321
327
  newCoefs = np.insert(coefs, position - 1, 0.0, axis=0)
322
- for i in range(position - self.order[ind] + 1, position):
323
- alpha = (knot - knots[ind][i]) / (knots[ind][i + self.order[ind] - 1] - knots[ind][i])
328
+ for i in range(position - order + 1, position):
329
+ alpha = (knot - knots[i]) / (knots[i + order - 1] - knots[i])
324
330
  newCoefs[i] = (1.0 - alpha) * coefs[i - 1] + alpha * coefs[i]
325
- knots[ind] = np.insert(knots[ind], position, knot)
331
+ knotsList[ind] = knots = np.insert(knots, position, knot)
326
332
  coefs = newCoefs.swapaxes(0, ind + 1)
327
333
 
328
334
  if self.coefs is coefs:
329
335
  return self
330
336
  else:
331
- return type(self)(self.nInd, self.nDep, self.order, coefs.shape[1:], knots, coefs, self.metadata)
337
+ return type(self)(self.nInd, self.nDep, self.order, coefs.shape[1:], knotsList, coefs, self.metadata)
332
338
 
333
339
  def join(splineList):
334
340
  # Make sure all the splines in the list are curves
@@ -368,13 +374,13 @@ def join(splineList):
368
374
  newKnots, newCoefs, workingSpline.metadata)
369
375
  return workingSpline.reparametrize([[0.0, 1.0]]).remove_knots()
370
376
 
371
- def remove_knot(self, iKnot):
377
+ def remove_knot(self, iKnot, nLeft = 0, nRight = 0):
372
378
  if self.nInd != 1: raise ValueError("Must have one independent variable")
373
379
  myOrder = self.order[0]
374
380
  if iKnot < myOrder or iKnot >= self.nCoef[0]: raise ValueError("Must specify interior knots for removal")
375
381
  diag0 = []
376
- diag1 = []
377
- rhs = []
382
+ diag1 = [1.0]
383
+ rhs = [np.ndarray.copy(self.coefs[:, iKnot - myOrder])]
378
384
  myKnots = self.knots[0]
379
385
  thisKnot = myKnots[iKnot]
380
386
 
@@ -384,45 +390,57 @@ def remove_knot(self, iKnot):
384
390
  diag0.append(alpha)
385
391
  diag1.append(1.0 - alpha)
386
392
  rhs.append(np.ndarray.copy(self.coefs[:, iKnot - myOrder + ix]))
387
-
388
- # Adjust right hand side because first and last coefficients are known
393
+ diag0.append(1.0)
394
+ diag1.append(0.0)
395
+ rhs.append(np.ndarray.copy(self.coefs[:, iKnot]))
389
396
  rhs = np.array(rhs)
390
- rhs[0] -= diag0[0] * self.coefs[:, iKnot - myOrder]
391
- rhs[-1] -= diag1[-1] * self.coefs[:, iKnot]
397
+
398
+ # Take care of the extra known conditions on the left
399
+ extraLeft = max(0, nLeft - iKnot + myOrder)
400
+ for ix in range(extraLeft):
401
+ rhs[ix] /= diag1[ix]
402
+ rhs[ix + 1] -= diag0[ix] * rhs[ix]
403
+
404
+ # Take care of the extra known conditions on the right
405
+ extraRight = max(0, nRight - self.nCoef[0] + iKnot + 1)
406
+ for ix in range(extraRight):
407
+ rhs[-1 - ix] /= diag0[-1 - ix]
408
+ rhs[-2 - ix] -= diag1[-2 - ix] * rhs[-1 - ix]
392
409
 
393
410
  # Use Givens rotations to factor the matrix and track right hand side
394
- for ix in range(1, myOrder - 1):
395
- cos = diag1[ix - 1]
411
+ for ix in range(extraLeft, myOrder - extraRight):
412
+ cos = diag1[ix]
396
413
  sin = diag0[ix]
397
414
  denom = np.sqrt(cos ** 2 + sin ** 2)
398
415
  cos /= denom
399
416
  sin /= denom
400
- diag1[ix - 1] = denom
401
- diag0[ix - 1] = sin * diag1[ix]
402
- diag1[ix] *= cos
403
- tempRow = cos * rhs[ix - 1] + sin * rhs[ix]
404
- rhs[ix] = cos * rhs[ix] - sin * rhs[ix - 1]
405
- rhs[ix - 1] = tempRow
417
+ diag1[ix] = denom
418
+ diag0[ix] = sin * diag1[ix + 1]
419
+ diag1[ix + 1] *= cos
420
+ tempRow = cos * rhs[ix] + sin * rhs[ix + 1]
421
+ rhs[ix + 1] = cos * rhs[ix + 1] - sin * rhs[ix]
422
+ rhs[ix] = tempRow
406
423
 
407
424
  # Perform back substitution
408
- rhs[-2] /= diag1[-2]
409
- for ix in range(1, myOrder - 2):
410
- rhs[-ix - 2] = (rhs[-ix - 2] - diag0[-ix - 2] * rhs[-ix - 1]) / diag1[-ix - 2]
425
+ for ix in range(1 + extraRight, myOrder - extraLeft):
426
+ rhs[-1 - ix] /= diag1[-1 - ix]
427
+ rhs[-2 - ix] -= diag0[-1 - ix] * rhs[-1 - ix]
428
+ rhs[-1 - myOrder + extraLeft] /= diag1[-1 - myOrder + extraLeft]
429
+
430
+ # Save residual and adjust solution
431
+ residual = abs(rhs[-1 - extraRight])
432
+ for ix in range(extraRight):
433
+ rhs[-1 - extraRight+ ix] = rhs[-extraRight + ix]
411
434
 
412
435
  # Create new spline
413
436
  newNCoef = [self.nCoef[0] - 1]
414
- newKnots = [list(self.knots[0][:iKnot]) + list(self.knots[0][iKnot + 1:])]
415
- newCoefs = []
416
- for ix in range(self.nDep):
417
- oldCoefs = self.coefs[ix]
418
- firstCoefs = oldCoefs[:iKnot - self.order[0] + 1]
419
- lastCoefs = oldCoefs[iKnot:]
420
- middleCoefs = rhs[:-1,ix]
421
- newCoefs.append(list(firstCoefs) + list(middleCoefs) + list(lastCoefs))
437
+ newKnots = [np.delete(self.knots[0], iKnot)]
438
+ newCoefs = np.delete(self.coefs, iKnot - self.order[0] + 1, 1)
439
+ newCoefs[: , iKnot - self.order[0] : iKnot] = rhs[: -1].T
422
440
  withoutKnot = type(self)(self.nInd, self.nDep, self.order, newNCoef, newKnots, newCoefs)
423
- return withoutKnot, abs(rhs[-1])
441
+ return withoutKnot, residual
424
442
 
425
- def remove_knots(self, tolerance):
443
+ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
426
444
  scaleDep = [max(np.abs(bound[0]), np.abs(bound[1])) for bound in self.range_bounds()]
427
445
  scaleDep = [1.0 if factor == 0.0 else factor for factor in scaleDep]
428
446
  rScaleDep = np.array([1.0 / factor for factor in scaleDep])
@@ -435,12 +453,20 @@ def remove_knots(self, tolerance):
435
453
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
436
454
  while True:
437
455
  bestError = np.finfo(scaleDep[0].dtype).max
438
- for ix in range(currentFold.order[0], currentFold.nCoef[0]):
439
- newSpline, residual = currentFold.remove_knot(ix)
456
+ bestSpline = currentFold
457
+ ix = currentFold.order[0]
458
+ while ix < currentFold.nCoef[0]:
459
+ newSpline, residual = currentFold.remove_knot(ix, nLeft, nRight)
440
460
  error = np.max(residual)
461
+ if error < 0.001 * tolerance:
462
+ currentFold = newSpline
463
+ continue
441
464
  if error < bestError:
442
465
  bestError = error
443
466
  bestSpline = newSpline
467
+ ix += 1
468
+ if currentFold.nCoef[0] < bestSpline.nCoef[0]:
469
+ continue
444
470
  if bestError > tolerance:
445
471
  break
446
472
  errorSpline = truthSpline - bestSpline.unfold(foldedIndices, foldedBasis)
@@ -508,6 +534,8 @@ def transpose(self, axes=None):
508
534
 
509
535
  def trim(self, newDomain):
510
536
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
537
+ if self.nInd < 1: return self
538
+ newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
511
539
 
512
540
  # Step 1: Determine the knots to insert at the new domain bounds.
513
541
  newKnotsList = []
@@ -517,7 +545,7 @@ def trim(self, newDomain):
517
545
  leftBound = False # Do we have a left bound?
518
546
  newKnots = []
519
547
 
520
- if bounds[0] is not None and not np.isnan(bounds[0]):
548
+ if not np.isnan(bounds[0]):
521
549
  if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
522
550
  leftBound = True
523
551
  multiplicity = order
@@ -526,7 +554,7 @@ def trim(self, newDomain):
526
554
  multiplicity -= counts[i]
527
555
  newKnots += multiplicity * [bounds[0]]
528
556
 
529
- if bounds[1] is not None and not np.isnan(bounds[1]):
557
+ if not np.isnan(bounds[1]):
530
558
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
531
559
  if leftBound:
532
560
  if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
@@ -547,14 +575,22 @@ def trim(self, newDomain):
547
575
  knotsList = []
548
576
  coefIndex = [slice(None)] # First index is for nDep
549
577
  for (order, knots, bounds) in zip(spline.order, spline.knots, newDomain):
550
- leftIndex = 0 if bounds[0] is None or np.isnan(bounds[0]) else np.searchsorted(knots, bounds[0])
551
- rightIndex = len(knots) - order if bounds[1] is None or np.isnan(bounds[1]) else np.searchsorted(knots, bounds[1])
578
+ leftIndex = 0 if np.isnan(bounds[0]) else np.searchsorted(knots, bounds[0])
579
+ rightIndex = len(knots) - order if np.isnan(bounds[1]) else np.searchsorted(knots, bounds[1])
552
580
  knotsList.append(knots[leftIndex:rightIndex + order])
553
581
  coefIndex.append(slice(leftIndex, rightIndex))
554
582
  coefs = spline.coefs[tuple(coefIndex)]
555
583
 
556
584
  return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
557
585
 
586
+ def trimmed_range_bounds(self, domainBounds):
587
+ domainBounds = np.array(domainBounds, copy=True)
588
+ for original, trim in zip(self.domain(), domainBounds):
589
+ trim[0] = max(original[0], trim[0] - Manifold.minSeparation)
590
+ trim[1] = min(original[1], trim[1] + Manifold.minSeparation)
591
+ trimmedSpline = self.trim(domainBounds)
592
+ return trimmedSpline, trimmedSpline.range_bounds()
593
+
558
594
  def unfold(self, foldedInd, coefficientlessSpline):
559
595
  if not(len(foldedInd) == coefficientlessSpline.nInd): raise ValueError("Invalid coefficientlessSpline")
560
596
  unfoldedOrder = []
@@ -1,43 +1,12 @@
1
1
  import numpy as np
2
2
 
3
- def blossom(self, uvw):
4
- def blossom_values(knot, knots, order, u):
5
- basis = np.zeros(order, knots.dtype)
6
- basis[-1] = 1.0
7
- for degree in range(1, order):
8
- b = order - degree
9
- for i in range(knot - degree, knot):
10
- alpha = (u[degree - 1] - knots[i]) / (knots[i + degree] - knots[i])
11
- basis[b - 1] += (1.0 - alpha) * basis[b]
12
- basis[b] *= alpha
13
- b += 1
14
- return basis
15
-
16
- # Make work for scalar valued functions
17
- uvw = np.atleast_1d(uvw)
18
-
19
- # Check for evaluation point inside domain
20
- dom = self.domain()
21
- for ix in range(self.nInd):
22
- if uvw[ix][0] < dom[ix][0] or uvw[ix][self.order[ix]-2] > dom[ix][1]:
23
- raise ValueError(f"Spline evaluation outside domain: {uvw}")
24
-
25
- # Grab all of the appropriate coefficients
26
- mySection = [slice(0, self.nDep)]
27
- myIndices = []
28
- for iv in range(self.nInd):
29
- ix = np.searchsorted(self.knots[iv], uvw[iv][0], 'right')
30
- ix = min(ix, self.nCoef[iv])
31
- myIndices.append(ix)
32
- mySection.append(slice(ix - self.order[iv], ix))
33
- myCoefs = self.coefs[tuple(mySection)]
34
- for iv in range(self.nInd - 1, -1, -1):
35
- bValues = blossom_values(myIndices[iv], self.knots[iv], self.order[iv], uvw[iv])
36
- myCoefs = myCoefs @ bValues
37
- return myCoefs
38
-
39
3
  def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
40
4
  basis = np.zeros(splineOrder, knots.dtype)
5
+ if knot is None:
6
+ knot = np.searchsorted(knots, u, side = 'right')
7
+ knot = min(knot, len(knots) - splineOrder)
8
+ if derivativeOrder >= splineOrder:
9
+ return knot, basis
41
10
  basis[-1] = 1.0
42
11
  for degree in range(1, splineOrder - derivativeOrder):
43
12
  b = splineOrder - degree
@@ -54,7 +23,7 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
54
23
  basis[b - 1] += -alpha * basis[b]
55
24
  basis[b] *= alpha
56
25
  b += 1
57
- return basis
26
+ return knot, basis
58
27
 
59
28
  def curvature(self, uv):
60
29
  if self.nInd == 1:
@@ -87,16 +56,14 @@ def derivative(self, with_respect_to, uvw):
87
56
 
88
57
  # Grab all of the appropriate coefficients
89
58
  mySection = [slice(0, self.nDep)]
90
- myIndices = []
59
+ bValues = []
91
60
  for iv in range(self.nInd):
92
- ix = np.searchsorted(self.knots[iv], uvw[iv], 'right')
93
- ix = min(ix, self.nCoef[iv])
94
- myIndices.append(ix)
61
+ ix, indValues = bspline_values(None, self.knots[iv], self.order[iv], uvw[iv], with_respect_to[iv])
62
+ bValues.append(indValues)
95
63
  mySection.append(slice(ix - self.order[iv], ix))
96
64
  myCoefs = self.coefs[tuple(mySection)]
97
65
  for iv in range(self.nInd - 1, -1, -1):
98
- bValues = bspline_values(myIndices[iv], self.knots[iv], self.order[iv], uvw[iv], with_respect_to[iv])
99
- myCoefs = myCoefs @ bValues
66
+ myCoefs = myCoefs @ bValues[iv]
100
67
  return myCoefs
101
68
 
102
69
  def domain(self):
@@ -120,18 +87,28 @@ def evaluate(self, uvw):
120
87
 
121
88
  # Grab all of the appropriate coefficients
122
89
  mySection = [slice(0, self.nDep)]
123
- myIndices = []
90
+ bValues = []
124
91
  for iv in range(self.nInd):
125
- ix = np.searchsorted(self.knots[iv], uvw[iv], 'right')
126
- ix = min(ix, self.nCoef[iv])
127
- myIndices.append(ix)
92
+ ix, indValues = bspline_values(None, self.knots[iv], self.order[iv], uvw[iv])
93
+ bValues.append(indValues)
128
94
  mySection.append(slice(ix - self.order[iv], ix))
129
95
  myCoefs = self.coefs[tuple(mySection)]
130
96
  for iv in range(self.nInd - 1, -1, -1):
131
- bValues = bspline_values(myIndices[iv], self.knots[iv], self.order[iv], uvw[iv])
132
- myCoefs = myCoefs @ bValues
97
+ myCoefs = myCoefs @ bValues[iv]
133
98
  return myCoefs
134
99
 
100
+ def greville(self, ind = 0):
101
+ if ind < 0 or ind >= self.nInd: raise ValueError("Invalid independent variable")
102
+ myKnots = self.knots[ind]
103
+ knotAverages = 0
104
+ if self.order[ind] == 1:
105
+ knotAverages = 0.5 * (myKnots[1:] + myKnots[:-1])
106
+ else:
107
+ for ix in range(1, self.order[ind]):
108
+ knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
109
+ knotAverages /= (self.order[ind] - 1)
110
+ return knotAverages
111
+
135
112
  def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
136
113
  # Make work for scalar valued functions
137
114
  uvw1 = np.atleast_1d(uvw1)
@@ -174,28 +151,24 @@ def normal(self, uvw, normalize=True, indices=None):
174
151
  if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be one different than the number of dependent variables.")
175
152
 
176
153
  # Evaluate the tangents at the point.
177
- tangentSpace = np.empty((self.nInd, self.nDep), self.coefs.dtype)
178
- with_respect_to = [0] * self.nInd
179
- for i in range(self.nInd):
180
- with_respect_to[i] = 1
181
- tangentSpace[i] = self.derivative(with_respect_to, uvw)
182
- with_respect_to[i] = 0
154
+ tangentSpace = self.tangent_space(uvw)
183
155
 
184
- # If self.nInd > self.nDep, transpose the tangent space and adjust the length of the normal.
185
- nDep = self.nDep
186
- if self.nInd > nDep:
187
- tangentSpace = tangentSpace.T
156
+ # Record the larger dimension and ensure it comes first.
157
+ if self.nInd > self.nDep:
188
158
  nDep = self.nInd
159
+ tangentSpace = tangentSpace.T
160
+ else:
161
+ nDep = self.nDep
189
162
 
190
163
  # Compute the normal using cofactors (determinants of subsets of the tangent space).
191
- sign = 1
164
+ sign = -1 if self.metadata.get("flipNormal", False) else 1
192
165
  if indices is None:
193
166
  indices = range(nDep)
194
167
  normal = np.empty(nDep, self.coefs.dtype)
195
168
  else:
196
169
  normal = np.empty(len(indices), self.coefs.dtype)
197
170
  for i in indices:
198
- normal[i] = sign * np.linalg.det(np.delete(tangentSpace, i, 1))
171
+ normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
199
172
  sign *= -1
200
173
 
201
174
  # Normalize the result as needed.
@@ -207,4 +180,13 @@ def normal(self, uvw, normalize=True, indices=None):
207
180
  def range_bounds(self):
208
181
  # Assumes self.nDep is the first value in self.coefs.shape
209
182
  bounds = [[coefficient.min(), coefficient.max()] for coefficient in self.coefs]
210
- return np.array(bounds, self.coefs.dtype)
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