bspy 4.3__tar.gz → 4.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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: bspy
3
- Version: 4.3
3
+ Version: 4.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
@@ -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,16 +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
 
46
- 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
-
48
- The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
49
-
50
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.
51
46
 
52
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.
53
48
 
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.
50
+
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).
52
+
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.
54
+
54
55
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
55
- [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
56
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.
57
58
 
58
59
  The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
@@ -60,7 +61,7 @@ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
60
61
  [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
61
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.
62
63
 
63
- 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.
64
65
  It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
65
66
  in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
66
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,16 +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
 
15
- 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
-
17
- The [Solid](https://ericbrec.github.io/BSpy/bspy/solid.html) class has methods to construct n-dimensional solids from trimmed [Manifold](https://ericbrec.github.io/BSpy/bspy/manifold.html) boundaries. Each solid consists of a list of boundaries and a Boolean value that indicates if the solid contains infinity. Each [Boundary](https://ericbrec.github.io/BSpy/bspy/solid.html) consists of a manifold (currently a [Hyperplane](https://ericbrec.github.io/BSpy/bspy/hyperplane.html) or [Spline](https://ericbrec.github.io/BSpy/bspy/spline.html)) and a domain solid that trims the manifold. Solids have methods to form the intersection, union, difference, and complement of solids. There are methods to compute point containment, winding numbers, surface integrals, and volume integrals. There are also methods to translate, transform, and slice solids. Solids can be saved and loaded in json format.
18
-
19
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.
20
14
 
21
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.
22
16
 
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.
18
+
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).
20
+
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.
22
+
23
23
  The [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html) class is an
24
- [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
25
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.
26
26
 
27
27
  The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
@@ -29,7 +29,7 @@ The [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) class is a
29
29
  [SplineOpenGLFrame](https://ericbrec.github.io/BSpy/bspy/splineOpenGLFrame.html),
30
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.
31
31
 
32
- 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.
33
33
  It launches a [Viewer](https://ericbrec.github.io/BSpy/bspy/viewer.html) and issues commands to the viewer for use
34
34
  in [jupyter](https://jupyter.org/) notebooks and other scripting environments. Only tested on Windows systems.
35
35
 
@@ -1,6 +1,34 @@
1
1
  import numpy as np
2
+ import bspy
2
3
  from bspy.manifold import Manifold
3
4
 
5
+ def arc_length_map(self, tolerance):
6
+ if self.nInd != 1: raise ValueError("Spline doesn't have exactly one independent variable")
7
+
8
+ # Compute the length of the spline
9
+ curveLength = self.integral()
10
+ domain = self.domain()[0]
11
+ guess = bspy.Spline.line([0.0], [1.0])
12
+ guess = guess.elevate([4])
13
+
14
+ # Solve the ODE
15
+ def arcLengthF(t, uData):
16
+ uValue = (1.0 - uData[0][0]) * domain[0] + uData[0][0] * domain[1]
17
+ uValue = np.clip(uValue, domain[0], domain[1])
18
+ d1 = self.derivative([1], [uValue])
19
+ d2 = self.derivative([2], [uValue])
20
+ speed = np.sqrt(d1 @ d1)
21
+ d1d2 = d1 @ d2
22
+ return np.array([curveLength / speed]), np.array([-curveLength * d1d2 / speed ** 3]).reshape((1, 1, 1))
23
+ arcLengthMap = guess.solve_ode(1, 0, arcLengthF, tolerance)
24
+
25
+ # Adjust range to match domain
26
+
27
+ arcLengthMap *= (domain[1] - domain[0]) / arcLengthMap(1.0)[0]
28
+ arcLengthMap += domain[0]
29
+ arcLengthMap.coefs[0][-1] = domain[1]
30
+ return arcLengthMap
31
+
4
32
  def clamp(self, left, right):
5
33
  bounds = [[None, None] for i in range(self.nInd)]
6
34
 
@@ -330,6 +358,7 @@ def insert_knots(self, newKnotList):
330
358
  continue
331
359
 
332
360
  # Check if knot and its total multiplicity is valid.
361
+ knot = knots.dtype.type(knot) # Cast to correct type
333
362
  if knot < knots[degree] or knot > knots[-order]:
334
363
  raise ValueError(f"Knot insertion outside domain: {knot}")
335
364
  position = np.searchsorted(knots, knot, 'right')
@@ -403,12 +432,11 @@ def join(splineList):
403
432
  splDomain = spl.domain()[0]
404
433
  start2 = spl(splDomain[0])
405
434
  end2 = spl(splDomain[1])
406
- gaps = [np.linalg.norm(vecDiff) for vecDiff in [start1 - start2, start1 - end2, end1 - start2, end1 - end2]]
407
- minDist = min(*gaps)
408
- if minDist == gaps[0] or minDist == gaps[1]:
409
- workingSpline = workingSpline.reverse()
410
- if minDist == gaps[1] or minDist == gaps[3]:
435
+ ixMin = np.argmin([np.linalg.norm(vecDiff) for vecDiff in [end1 - start2, end1 - end2, start1 - start2, start1 - end2]])
436
+ if ixMin == 1 or ixMin == 3:
411
437
  spl = spl.reverse()
438
+ if ixMin == 2 or ixMin == 3:
439
+ workingSpline = workingSpline.reverse()
412
440
  maxOrder = max(workingSpline.order[0], spl.order[0])
413
441
  workingSpline = workingSpline.elevate([maxOrder - workingSpline.order[0]])
414
442
  spl = spl.elevate([maxOrder - spl.order[0]])
@@ -499,7 +527,7 @@ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
499
527
  foldedIndices = list(filter(lambda x: x != id, indIndex))
500
528
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
501
529
  while True:
502
- bestError = np.finfo(scaleDep[0].dtype).max
530
+ bestError = np.finfo(self.coefs.dtype).max
503
531
  bestSpline = currentFold
504
532
  ix = currentFold.order[0]
505
533
  while ix < currentFold.nCoef[0]:
@@ -609,6 +637,8 @@ def trim(self, newDomain):
609
637
  if multiplicity > 0:
610
638
  newKnots.append((bounds[0], multiplicity))
611
639
  noChange = False
640
+ if bounds[0] != knots[order - 1]:
641
+ noChange = False
612
642
 
613
643
  if not np.isnan(bounds[1]):
614
644
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
@@ -626,6 +656,8 @@ def trim(self, newDomain):
626
656
  if multiplicity > 0:
627
657
  newKnots.append((bounds[1], multiplicity))
628
658
  noChange = False
659
+ if bounds[1] != knots[-order]:
660
+ noChange = False
629
661
 
630
662
  newKnotsList.append(newKnots)
631
663
 
@@ -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
@@ -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:
@@ -319,6 +339,7 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
319
339
  nInd = len(domain)
320
340
  midPoint = f(0.5 * (domain.T[0] + domain.T[1]))
321
341
  if not type(midPoint) is bspy.Spline:
342
+ midPoint = np.array(midPoint).flatten()
322
343
  nDep = len(midPoint)
323
344
 
324
345
  # Make sure order and knots conform to this
@@ -366,9 +387,13 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
366
387
  indices = nInd * [0]
367
388
  iLast = nInd
368
389
  while iLast >= 0:
390
+ # Create a tuple for the u value (must be a tuple to use it as a dictionary key)
369
391
  uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
370
392
  if not uValue in fDictionary:
371
- fDictionary[uValue] = f(uValue)
393
+ newValue = f(np.array(uValue))
394
+ if not type(newValue) is bspy.Spline:
395
+ newValue = np.array(newValue).flatten()
396
+ fDictionary[uValue] = newValue
372
397
  fValues.append(fDictionary[uValue])
373
398
  iLast = nInd - 1
374
399
  while iLast >= 0:
@@ -518,7 +543,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
518
543
 
519
544
  return (1.0 - surfParam) * coons + surfParam * laplace
520
545
 
521
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
546
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
522
547
  # Check validity of input
523
548
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
524
549
  if len(uvStart) != 2: raise ValueError("uvStart must have two components")
@@ -616,7 +641,7 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
616
641
  initialGuess = line(uvStart, uvEnd).elevate([2])
617
642
 
618
643
  # Solve the ODE and return the geodesic
619
- solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
644
+ solution = initialGuess.solve_ode(1, 1, geodesicCallback, tolerance, (self, uvDomain))
620
645
  return solution
621
646
 
622
647
  def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
@@ -878,7 +903,7 @@ def section(xytk):
878
903
  # Join the pieces together and return
879
904
  return bspy.Spline.join(mySections)
880
905
 
881
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
906
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
882
907
  # Ensure that the ODE is properly formulated
883
908
 
884
909
  if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
@@ -970,7 +995,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
970
995
  residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
971
996
  collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
972
997
  for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
973
- uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
998
+ uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder + 1 if includeEstimate else nOrder)]).T
974
999
  F, F_u = FAndF_u(t, uData, *args)
975
1000
  residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
976
1001
  ix = None
@@ -1046,7 +1071,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
1046
1071
 
1047
1072
  # Is it time to give up?
1048
1073
 
1049
- if (not done or continuation < 1.0) and n > 1000:
1074
+ if (not done or continuation < 1.0) and n > 10000:
1050
1075
  raise RuntimeError("Can't find solution with given initial guess")
1051
1076
 
1052
1077
  # Estimate the error
@@ -174,7 +174,6 @@ def _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInter
174
174
  for p1 in hull[1:]:
175
175
  yDelta = p0[1] - p1[1]
176
176
  if p0[1] * p1[1] <= 0.0 and yDelta != 0.0:
177
- yDelta = p0[1] - p1[1]
178
177
  alpha = p0[1] / yDelta
179
178
  xNew = p0[0] * (1.0 - alpha) + p1[0] * alpha
180
179
  if sign * yDelta > 0.0:
@@ -255,14 +254,13 @@ def _refine_projected_polyhedron(interval):
255
254
 
256
255
  # Compute the coefficients for f(x) = x for the independent variable and its knots.
257
256
  xData = spline.greville(ind)
258
-
257
+ if len(xData) == 1:
258
+ xData = spline.domain()[ind]
259
+
259
260
  # Loop through each dependent variable in this row to refine the interval containing the root for this independent variable.
260
- for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(),
261
- interval.bounds[nDep:nDep + spline.nDep]):
261
+ for yData, ySplineBounds, yBounds in zip(coefs, spline.range_bounds(), interval.bounds[nDep:nDep + spline.nDep]):
262
262
  # Compute the 2D convex hull of the knot coefficients and the spline's coefficients
263
263
  lowerHull, upperHull = _convex_hull_2D(xData, yData.ravel(), yBounds, yBounds - ySplineBounds)
264
- if lowerHull is None or upperHull is None:
265
- return roots, intervals
266
264
 
267
265
  # Intersect the convex hull with the xInterval along the x axis (the knot coefficients axis).
268
266
  xInterval = _intersect_convex_hull_with_x_interval(lowerHull, upperHull, epsilon, xInterval)
@@ -943,7 +941,8 @@ def contours(self):
943
941
 
944
942
  def intersect(self, other):
945
943
  intersections = []
946
- nDep = self.nInd # The dimension of the intersection's range
944
+ # Compute the number of degrees of freedom of the intersection.
945
+ dof = self.nInd + other.domain_dimension() - self.nDep
947
946
 
948
947
  # Spline-Hyperplane intersection.
949
948
  if isinstance(other, Hyperplane):
@@ -953,11 +952,13 @@ def intersect(self, other):
953
952
  spline = self.dot(other._normal) - np.atleast_1d(np.dot(other._normal, other._point))
954
953
 
955
954
  # Curve-Line intersection.
956
- if nDep == 1:
955
+ if dof == 0:
957
956
  # Find the intersection points and intervals.
958
957
  zeros = spline.zeros()
959
958
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
960
959
  for zero in zeros:
960
+ if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
961
+ zero = 0.5 * (zero[0] + zero[1])
961
962
  if isinstance(zero, tuple):
962
963
  # Intersection is an interval, so create a Manifold.Coincidence.
963
964
  planeBounds = (projection @ (self((zero[0],)) - other._point), projection @ (self((zero[1],)) - other._point))
@@ -972,10 +973,10 @@ def intersect(self, other):
972
973
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1] + epsilon, 0.0), Hyperplane(1.0, planeBounds[1], 0.0)))
973
974
 
974
975
  # Now, create the coincidence.
975
- left = Solid(nDep, False)
976
+ left = Solid(1, False)
976
977
  left.add_boundary(Boundary(Hyperplane(-1.0, zero[0], 0.0), Solid(0, True)))
977
978
  left.add_boundary(Boundary(Hyperplane(1.0, zero[1], 0.0), Solid(0, True)))
978
- right = Solid(nDep, False)
979
+ right = Solid(1, False)
979
980
  if planeBounds[0] > planeBounds[1]:
980
981
  planeBounds = (planeBounds[1], planeBounds[0])
981
982
  right.add_boundary(Boundary(Hyperplane(-1.0, planeBounds[0], 0.0), Solid(0, True)))
@@ -990,7 +991,7 @@ def intersect(self, other):
990
991
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero, 0.0), Hyperplane(1.0, projection @ (self((zero,)) - other._point), 0.0)))
991
992
 
992
993
  # Surface-Plane intersection.
993
- elif nDep == 2:
994
+ elif dof == 1:
994
995
  # Find the intersection contours, which are returned as splines.
995
996
  contours = spline.contours()
996
997
  # Convert each contour into a Manifold.Crossing.
@@ -1013,12 +1014,14 @@ def intersect(self, other):
1013
1014
  # Construct a spline block that represents the intersection.
1014
1015
  block = bspy.spline_block.SplineBlock([[self, -other]])
1015
1016
 
1016
- # Curve-Curve intersection.
1017
- if nDep == 1:
1017
+ # Zero degrees of freedom, typically a Curve-Curve intersection.
1018
+ if dof == 0:
1018
1019
  # Find the intersection points and intervals.
1019
1020
  zeros = block.zeros()
1020
1021
  # Convert each intersection point into a Manifold.Crossing and each intersection interval into a Manifold.Coincidence.
1021
1022
  for zero in zeros:
1023
+ if isinstance(zero, tuple) and zero[1] - zero[0] < Manifold.minSeparation:
1024
+ zero = 0.5 * (zero[0] + zero[1])
1022
1025
  if isinstance(zero, tuple):
1023
1026
  # Intersection is an interval, so create a Manifold.Coincidence.
1024
1027
 
@@ -1037,10 +1040,10 @@ def intersect(self, other):
1037
1040
  intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[1][0], 0.0), Hyperplane(1.0, zero[1][1] + epsilon, 0.0)))
1038
1041
 
1039
1042
  # Now, create the coincidence.
1040
- left = Solid(nDep, False)
1043
+ left = Solid(self.nInd, False)
1041
1044
  left.add_boundary(Boundary(Hyperplane(-1.0, zero[0][0], 0.0), Solid(0, True)))
1042
1045
  left.add_boundary(Boundary(Hyperplane(1.0, zero[1][0], 0.0), Solid(0, True)))
1043
- right = Solid(nDep, False)
1046
+ right = Solid(other.nInd, False)
1044
1047
  right.add_boundary(Boundary(Hyperplane(-1.0, zero[0][1], 0.0), Solid(0, True)))
1045
1048
  right.add_boundary(Boundary(Hyperplane(1.0, zero[1][1], 0.0), Solid(0, True)))
1046
1049
  alignment = np.dot(self.normal(zero[0][0]), other.normal(zero[0][1])) # Use the first zeros, since B-splines are closed on the left
@@ -1050,10 +1053,10 @@ def intersect(self, other):
1050
1053
  intersections.append(Manifold.Coincidence(left, right, alignment, np.atleast_2d(transform), np.atleast_2d(1.0 / transform), np.atleast_1d(translation)))
1051
1054
  else:
1052
1055
  # Intersection is a point, so create a Manifold.Crossing.
1053
- intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:nDep], 0.0), Hyperplane(1.0, zero[nDep:], 0.0)))
1056
+ intersections.append(Manifold.Crossing(Hyperplane(1.0, zero[:self.nInd], 0.0), Hyperplane(1.0, zero[self.nInd:], 0.0)))
1054
1057
 
1055
- # Surface-Surface intersection.
1056
- elif nDep == 2:
1058
+ # One degree of freedom, typically a Surface-Surface intersection.
1059
+ elif dof == 1:
1057
1060
  if "Name" in self.metadata and "Name" in other.metadata:
1058
1061
  logging.info(f"intersect:{self.metadata['Name']}:{other.metadata['Name']}")
1059
1062
  # Find the intersection contours, which are returned as splines.
@@ -1071,32 +1074,34 @@ def intersect(self, other):
1071
1074
  # Convert each contour into a Manifold.Crossing, swapping the manifolds back.
1072
1075
  for contour in contours:
1073
1076
  # Swap left and right, compared to not swapped.
1074
- left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
1075
- right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
1077
+ left = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[other.nInd:], contour.metadata)
1078
+ right = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:other.nInd], contour.metadata)
1076
1079
  intersections.append(Manifold.Crossing(left, right))
1077
1080
  else:
1078
1081
  # Convert each contour into a Manifold.Crossing.
1079
1082
  for contour in contours:
1080
- left = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[:nDep], contour.metadata)
1081
- right = bspy.Spline(contour.nInd, nDep, contour.order, contour.nCoef, contour.knots, contour.coefs[nDep:], contour.metadata)
1083
+ left = bspy.Spline(contour.nInd, self.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[:self.nInd], contour.metadata)
1084
+ right = bspy.Spline(contour.nInd, other.nInd, contour.order, contour.nCoef, contour.knots, contour.coefs[self.nInd:], contour.metadata)
1082
1085
  intersections.append(Manifold.Crossing(left, right))
1083
1086
  else:
1084
1087
  return NotImplemented
1085
1088
  else:
1086
1089
  return NotImplemented
1087
1090
 
1088
- # Ensure the normals point outwards for both Manifolds in each crossing intersection.
1089
- # Note that evaluating left and right at 0.5 is always valid because either they are points or curves with [0.0, 1.0] domains.
1090
- domainPoint = np.atleast_1d(0.5)
1091
- for i, intersection in enumerate(intersections):
1092
- if isinstance(intersection, Manifold.Crossing):
1093
- left = intersection.left
1094
- right = intersection.right
1095
- if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
1096
- left = left.flip_normal()
1097
- if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
1098
- right = right.flip_normal()
1099
- intersections[i] = Manifold.Crossing(left, right)
1091
+ # If self and other have normals, ensure they are pointing in the correct direction.
1092
+ if self.nInd + 1 == self.nDep and other.domain_dimension() + 1 == self.nDep:
1093
+ # Ensure the normals point outwards for both Manifolds in each crossing intersection.
1094
+ # Note that evaluating left and right at 0.5 is always valid because either they are points or curves with [0.0, 1.0] domains.
1095
+ domainPoint = np.atleast_1d(0.5)
1096
+ for i, intersection in enumerate(intersections):
1097
+ if isinstance(intersection, Manifold.Crossing):
1098
+ left = intersection.left
1099
+ right = intersection.right
1100
+ if np.dot(self.tangent_space(left.evaluate(domainPoint)) @ left.normal(domainPoint), other.normal(right.evaluate(domainPoint))) < 0.0:
1101
+ left = left.flip_normal()
1102
+ if np.dot(other.tangent_space(right.evaluate(domainPoint)) @ right.normal(domainPoint), self.normal(left.evaluate(domainPoint))) < 0.0:
1103
+ right = right.flip_normal()
1104
+ intersections[i] = Manifold.Crossing(left, right)
1100
1105
 
1101
1106
  return intersections
1102
1107
 
@@ -1135,14 +1140,14 @@ def complete_slice(self, slice, solid):
1135
1140
  newBoundary.touched = False
1136
1141
 
1137
1142
  # Define function for adding slice points to full domain boundaries.
1138
- def process_domain_point(boundary, domainPoint):
1143
+ def process_domain_point(boundary, domainPoint, adjustment):
1139
1144
  point = boundary.manifold.evaluate(domainPoint)
1140
1145
  # See if and where point touches full domain.
1141
1146
  for newBoundary in fullDomain.boundaries:
1142
1147
  vector = point - newBoundary.manifold._point
1143
1148
  if abs(np.dot(newBoundary.manifold._normal, vector)) < Manifold.minSeparation:
1144
- # Add the point onto the new boundary.
1145
- normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint))
1149
+ # Add the point onto the new boundary (adjust normal evaluation point to move away from boundary).
1150
+ normal = np.sign(newBoundary.manifold._tangentSpace.T @ boundary.manifold.normal(domainPoint + adjustment))
1146
1151
  newBoundary.domain.add_boundary(Boundary(Hyperplane(normal, newBoundary.manifold._tangentSpace.T @ vector, 0.0), Solid(0, True)))
1147
1152
  newBoundary.touched = True
1148
1153
  break
@@ -1151,9 +1156,9 @@ def complete_slice(self, slice, solid):
1151
1156
  for boundary in slice.boundaries:
1152
1157
  domainBoundaries = boundary.domain.boundaries
1153
1158
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1154
- process_domain_point(boundary, domainBoundaries[0].manifold._point)
1159
+ process_domain_point(boundary, domainBoundaries[0].manifold._point, Manifold.minSeparation)
1155
1160
  if len(domainBoundaries) > 1:
1156
- process_domain_point(boundary, domainBoundaries[-1].manifold._point)
1161
+ process_domain_point(boundary, domainBoundaries[-1].manifold._point, -Manifold.minSeparation)
1157
1162
 
1158
1163
  # For touched boundaries, remove domain bounds that aren't needed and then add boundary to slice.
1159
1164
  boundaryWasTouched = False
@@ -1161,7 +1166,6 @@ def complete_slice(self, slice, solid):
1161
1166
  if newBoundary.touched:
1162
1167
  boundaryWasTouched = True
1163
1168
  domainBoundaries = newBoundary.domain.boundaries
1164
- assert len(domainBoundaries) > 2
1165
1169
  domainBoundaries.sort(key=lambda boundary: (boundary.manifold.evaluate(0.0), boundary.manifold.normal(0.0)))
1166
1170
  # Ensure domain endpoints don't overlap and their normals are consistent.
1167
1171
  if abs(domainBoundaries[0].manifold._point - domainBoundaries[1].manifold._point) < Manifold.minSeparation or \