bspy 4.2__tar.gz → 4.4__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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: bspy
3
- Version: 4.2
3
+ Version: 4.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
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
@@ -28,14 +28,13 @@ Requires-Dist: numpy
28
28
  Requires-Dist: scipy
29
29
  Requires-Dist: PyOpenGL
30
30
  Requires-Dist: pyopengltk
31
+ Dynamic: license-file
31
32
 
32
33
  # BSpy
33
34
  Library for manipulating and rendering B-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
34
35
 
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
-
37
- The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), and to geodesics.
38
- Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, and four-sided patches.
36
+ The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), geodesics, offsets, and lines of curvature.
37
+ Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, four-sided patches, and compositions of splines.
39
38
  Other methods add, subtract, and multiply splines, as well as confine spline curves to a given range.
40
39
  There are methods to evaluate spline values, derivatives, normals, integrals, continuity, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions.
41
40
  In addition, there are methods to manipulate the domain of splines, including trim, join, split, reparametrize, transpose, reverse, add and remove knots, elevate and extrapolate, and fold and unfold.
@@ -43,14 +42,18 @@ There are methods to manipulate the range of splines, including dot product, cro
43
42
  Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
44
43
  Splines can be saved and loaded in json format.
45
44
 
45
+ 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.
46
+
47
+ 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.
48
+
46
49
  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.
47
50
 
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.
51
+ 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).
49
52
 
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.
53
+ 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.
51
54
 
52
55
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
53
- [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
56
+ [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
54
57
  than 3 dependent variables will have their added dimensions rendered as colors (up to 6 dependent variables are supported). Only tested on Windows systems.
55
58
 
56
59
  The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
@@ -58,7 +61,7 @@ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
58
61
  [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
59
62
  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.
60
63
 
61
- The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display splines.
64
+ The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display solids and splines.
62
65
  It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
63
66
  in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
64
67
 
@@ -1,10 +1,8 @@
1
1
  # BSpy
2
2
  Library for manipulating and rendering B-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
3
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 scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), and to geodesics.
7
- Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, and four-sided patches.
4
+ The [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html) class has a method to fit multidimensional data for scalar and vector functions of single and multiple variables. It also can fit splines to functions, to solutions for ordinary differential equations (ODEs), geodesics, offsets, and lines of curvature.
5
+ Spline has methods to create points, lines, circular arcs, spheres, cones, cylinders, tori, ruled surfaces, surfaces of revolution, four-sided patches, and compositions of splines.
8
6
  Other methods add, subtract, and multiply splines, as well as confine spline curves to a given range.
9
7
  There are methods to evaluate spline values, derivatives, normals, integrals, continuity, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions.
10
8
  In addition, there are methods to manipulate the domain of splines, including trim, join, split, reparametrize, transpose, reverse, add and remove knots, elevate and extrapolate, and fold and unfold.
@@ -12,14 +10,18 @@ There are methods to manipulate the range of splines, including dot product, cro
12
10
  Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
13
11
  Splines can be saved and loaded in json format.
14
12
 
13
+ 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.
14
+
15
+ 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.
16
+
15
17
  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.
16
18
 
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.
19
+ 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).
18
20
 
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.
21
+ 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.
20
22
 
21
23
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
22
- [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces. Spline surfaces with more
24
+ [OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves, surfaces, and solids. Spline surfaces with more
23
25
  than 3 dependent variables will have their added dimensions rendered as colors (up to 6 dependent variables are supported). Only tested on Windows systems.
24
26
 
25
27
  The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
@@ -27,7 +29,7 @@ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
27
29
  [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
28
30
  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.
29
31
 
30
- The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display splines.
32
+ The [Graphics](https://ericbrec.github.io/BSpy/bspy/viewer.html#Graphics) class is a graphics engine to display solids and splines.
31
33
  It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
32
34
  in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
33
35
 
@@ -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,74 @@ 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
+ knot = knots.dtype.type(knot) # Cast to correct type
334
+ if knot < knots[degree] or knot > knots[-order]:
335
+ raise ValueError(f"Knot insertion outside domain: {knot}")
336
+ position = np.searchsorted(knots, knot, 'right')
337
+ oldMultiplicity = 0
338
+ for k in knots[position - 1::-1]:
339
+ if knot == k:
340
+ oldMultiplicity += 1
341
+ else:
342
+ break
343
+ if oldMultiplicity + multiplicity > order:
344
+ raise ValueError("Knot multiplicity > order")
345
+
346
+ # Initialize oldCoefs and expanded coefs array with multiplicity new coefficients, as well as some indices.
347
+ oldCoefs = coefs[position - order:position].copy()
348
+ lastKnotIndex = position - oldMultiplicity
349
+ firstCoefIndex = position - degree
350
+ coefs = np.insert(coefs, firstCoefIndex, oldCoefs[:multiplicity], axis=0)
351
+ # Compute inserted coefficients (multiplicity of them) and the degree - oldMultiplicity - 1 number of changed coefficients.
352
+ for j in range(multiplicity):
353
+ # Allocate new coefficients for the current multiplicity.
354
+ size = degree - oldMultiplicity - j
355
+ if size < 1:
356
+ # Full multiplicity knot, so use oldCoefs.
357
+ coefs[firstCoefIndex + j] = oldCoefs[0]
358
+ else:
359
+ # Otherwise, allocate space for newCoefs.
360
+ newCoefs = np.empty((size, *coefs.shape[1:]), coefs.dtype)
361
+
362
+ # Compute the new coefficients.
363
+ for i, k in zip(range(size), range(lastKnotIndex - size, lastKnotIndex)):
364
+ alpha = (knot - knots[k]) / (knots[k + degree - j] - knots[k])
365
+ newCoefs[i] = (1.0 - alpha) * oldCoefs[i] + alpha * oldCoefs[i + 1]
366
+
367
+ # Assign the ends of the new coefficients into their respective positions.
368
+ coefs[firstCoefIndex + j] = newCoefs[0]
369
+ if size > 1:
370
+ coefs[lastKnotIndex + multiplicity - j - 2] = newCoefs[-1]
371
+ oldCoefs = newCoefs
372
+
373
+ # Assign remaining computed coefficients (the ones in the middle).
374
+ if size > 2:
375
+ coefs[firstCoefIndex + multiplicity:firstCoefIndex + multiplicity + size - 2] = newCoefs[1:-1]
376
+
377
+ # Insert the inserted coefficients and inserted knots.
378
+ knotsList[ind] = knots = np.insert(knots, position, (knot,) * multiplicity)
379
+
380
+ coefs = coefs.swapaxes(0, ind + 1) # Swap back
333
381
 
334
382
  if self.coefs is coefs:
335
383
  return self
@@ -452,7 +500,7 @@ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
452
500
  foldedIndices = list(filter(lambda x: x != id, indIndex))
453
501
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
454
502
  while True:
455
- bestError = np.finfo(scaleDep[0].dtype).max
503
+ bestError = np.finfo(self.coefs.dtype).max
456
504
  bestSpline = currentFold
457
505
  ix = currentFold.order[0]
458
506
  while ix < currentFold.nCoef[0]:
@@ -518,66 +566,6 @@ def reverse(self, variable = 0):
518
566
  newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
519
567
  return newFolded.unfold(myIndices, basisInfo)
520
568
 
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
569
  def transpose(self, axes=None):
582
570
  if axes is None:
583
571
  axes = range(self.nInd)[::-1]
@@ -614,17 +602,14 @@ def trim(self, newDomain):
614
602
  if unique[i] - bounds[0] < epsilon:
615
603
  bounds[0] = unique[i]
616
604
  multiplicity = order - counts[i]
617
- if i > 0:
618
- noChange = False
619
605
  elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
620
606
  bounds[0] = unique[i - 1]
621
607
  multiplicity = order - counts[i - 1]
622
- if i - 1 > 0:
623
- noChange = False
624
608
  else:
625
609
  multiplicity = order
626
-
627
- newKnots += multiplicity * [bounds[0]]
610
+ if multiplicity > 0:
611
+ newKnots.append((bounds[0], multiplicity))
612
+ noChange = False
628
613
 
629
614
  if not np.isnan(bounds[1]):
630
615
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
@@ -634,19 +619,16 @@ def trim(self, newDomain):
634
619
  if unique[i] - bounds[1] < epsilon:
635
620
  bounds[1] = unique[i]
636
621
  multiplicity = order - counts[i]
637
- if i < len(unique) - 1:
638
- noChange = False
639
622
  elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
640
623
  bounds[1] = unique[i - 1]
641
624
  multiplicity = order - counts[i - i]
642
- noChange = False # i < len(unique) - 1
643
625
  else:
644
626
  multiplicity = order
645
- newKnots += multiplicity * [bounds[1]]
627
+ if multiplicity > 0:
628
+ newKnots.append((bounds[1], multiplicity))
629
+ noChange = False
646
630
 
647
631
  newKnotsList.append(newKnots)
648
- if len(newKnots) > 0:
649
- noChange = False
650
632
 
651
633
  if noChange:
652
634
  return self
@@ -5,7 +5,7 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
5
5
  basis = np.zeros(splineOrder, knots.dtype)
6
6
  if knot is None:
7
7
  knot = np.searchsorted(knots, u, side = 'right')
8
- knot = min(knot, len(knots) - splineOrder)
8
+ knot = min(max(knot, splineOrder), len(knots) - splineOrder)
9
9
  if derivativeOrder >= splineOrder:
10
10
  return knot, basis
11
11
  basis[-1] = 1.0
@@ -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:
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  import scipy as sp
3
3
  import bspy.spline
4
+ import bspy.spline_block
4
5
  import math
5
6
 
6
7
  def circular_arc(radius, angle, tolerance = None):
@@ -12,14 +13,33 @@ def circular_arc(radius, angle, tolerance = None):
12
13
  return bspy.Spline.section([(radius * np.cos(u * angle * np.pi / 180), radius * np.sin(u * angle * np.pi / 180), 90 + u * angle, 1.0 / radius) for u in np.linspace(0.0, 1.0, samples)])
13
14
 
14
15
  def composition(splines, tolerance):
16
+ # Collect domains and check range bounds
17
+ domains = [None]
18
+ domain = None
19
+ for i, spline in enumerate(splines):
20
+ if domain is not None:
21
+ if len(domain) != spline.nDep:
22
+ raise ValueError(f"Domain dimension of spline {i-1} does not match range dimension of spline {i}")
23
+ rangeBounds = spline.range_bounds()
24
+ for ix in range(spline.nDep):
25
+ if rangeBounds[ix][0] < domain[ix][0] or rangeBounds[ix][1] > domain[ix][1]:
26
+ raise ValueError(f"Range of spline {i} exceeds domain of spline {i-1}")
27
+ domains.append(domain)
28
+ domain = spline.domain()
29
+
15
30
  # Define the callback function
16
31
  def composition_of_splines(u):
17
- for f in splines[::-1]:
18
- u = f(u)
32
+ for spline, domain in zip(splines[::-1], domains[::-1]):
33
+ u = spline(u)
34
+ if domain is not None:
35
+ # We've already checked that the range of spline is within the domain
36
+ # of its successor, but numerics may cause the spline value to slightly
37
+ # exceed its range, so we clip the spline value accordingly.
38
+ u = np.clip(u, domain[:, 0], domain[:, 1])
19
39
  return u
20
40
 
21
41
  # Approximate this composition
22
- return bspy.Spline.fit(splines[-1].domain(), composition_of_splines, tolerance = tolerance)
42
+ return bspy.Spline.fit(domain, composition_of_splines, tolerance = tolerance)
23
43
 
24
44
  def cone(radius1, radius2, height, tolerance = None):
25
45
  if tolerance is None:
@@ -366,9 +386,10 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
366
386
  indices = nInd * [0]
367
387
  iLast = nInd
368
388
  while iLast >= 0:
389
+ # Create a tuple for the u value (must be a tuple to use it as a dictionary key)
369
390
  uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
370
391
  if not uValue in fDictionary:
371
- fDictionary[uValue] = f(uValue)
392
+ fDictionary[uValue] = f(np.array(uValue))
372
393
  fValues.append(fDictionary[uValue])
373
394
  iLast = nInd - 1
374
395
  while iLast >= 0:
@@ -518,7 +539,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
518
539
 
519
540
  return (1.0 - surfParam) * coons + surfParam * laplace
520
541
 
521
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
542
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
522
543
  # Check validity of input
523
544
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
524
545
  if len(uvStart) != 2: raise ValueError("uvStart must have two components")
@@ -616,7 +637,7 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
616
637
  initialGuess = line(uvStart, uvEnd).elevate([2])
617
638
 
618
639
  # Solve the ODE and return the geodesic
619
- solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
640
+ solution = initialGuess.solve_ode(1, 1, geodesicCallback, tolerance, (self, uvDomain))
620
641
  return solution
621
642
 
622
643
  def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
@@ -878,7 +899,7 @@ def section(xytk):
878
899
  # Join the pieces together and return
879
900
  return bspy.Spline.join(mySections)
880
901
 
881
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
902
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
882
903
  # Ensure that the ODE is properly formulated
883
904
 
884
905
  if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
@@ -904,11 +925,11 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
904
925
  for i, knot in enumerate(uniqueKnots):
905
926
  howMany = currentGuess.order[0] - indices[i + 1] + indices[i] - nOrder
906
927
  if howMany > 0:
907
- knotsToAdd += howMany * [knot]
928
+ knotsToAdd.append((knot, howMany))
908
929
  if howMany < 0:
909
- knotsToRemove += [[knot, abs(howMany)]]
930
+ knotsToRemove.append((knot, abs(howMany)))
910
931
  currentGuess = currentGuess.insert_knots([knotsToAdd])
911
- for [knot, howMany] in knotsToRemove:
932
+ for (knot, howMany) in knotsToRemove:
912
933
  ix = np.searchsorted(currentGuess.knots[0], knot, side = 'left')
913
934
  for iy in range(howMany):
914
935
  currentGuess, residual = currentGuess.remove_knot(ix, nLeft, nRight)
@@ -970,7 +991,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
970
991
  residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
971
992
  collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
972
993
  for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
973
- uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
994
+ uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder + 1 if includeEstimate else nOrder)]).T
974
995
  F, F_u = FAndF_u(t, uData, *args)
975
996
  residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
976
997
  ix = None
@@ -1065,7 +1086,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
1065
1086
  knotsToAdd = []
1066
1087
  for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
1067
1088
  if knot0 < knot1:
1068
- knotsToAdd += (currentGuess.order[0] - nOrder) * [0.5 * (knot0 + knot1)]
1089
+ knotsToAdd.append((0.5 * (knot0 + knot1), currentGuess.order[0] - nOrder))
1069
1090
  previousGuess = currentGuess
1070
1091
  currentGuess = currentGuess.insert_knots([knotsToAdd])
1071
1092