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.
- {bspy-4.3 → bspy-4.4.1}/PKG-INFO +13 -12
- {bspy-4.3 → bspy-4.4.1}/README.md +10 -10
- {bspy-4.3 → bspy-4.4.1}/bspy/_spline_domain.py +38 -6
- {bspy-4.3 → bspy-4.4.1}/bspy/_spline_evaluation.py +1 -1
- {bspy-4.3 → bspy-4.4.1}/bspy/_spline_fitting.py +34 -9
- {bspy-4.3 → bspy-4.4.1}/bspy/_spline_intersection.py +44 -40
- bspy-4.4.1/bspy/_spline_milling.py +288 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/_spline_operations.py +55 -42
- {bspy-4.3 → bspy-4.4.1}/bspy/solid.py +1 -1
- {bspy-4.3 → bspy-4.4.1}/bspy/spline.py +128 -18
- {bspy-4.3 → bspy-4.4.1}/bspy.egg-info/PKG-INFO +13 -12
- {bspy-4.3 → bspy-4.4.1}/bspy.egg-info/SOURCES.txt +1 -0
- {bspy-4.3 → bspy-4.4.1}/setup.cfg +1 -1
- {bspy-4.3 → bspy-4.4.1}/LICENSE +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/__init__.py +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/hyperplane.py +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/manifold.py +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/splineOpenGLFrame.py +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/spline_block.py +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy/viewer.py +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy.egg-info/dependency_links.txt +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy.egg-info/requires.txt +0 -0
- {bspy-4.3 → bspy-4.4.1}/bspy.egg-info/top_level.txt +0 -0
- {bspy-4.3 → bspy-4.4.1}/pyproject.toml +0 -0
{bspy-4.3 → bspy-4.4.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bspy
|
|
3
|
-
Version: 4.
|
|
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 [
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
|
|
407
|
-
|
|
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(
|
|
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
|
|
18
|
-
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(
|
|
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
|
-
|
|
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-
|
|
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,
|
|
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 >
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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[:
|
|
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
|
|
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,
|
|
1075
|
-
right = bspy.Spline(contour.nInd,
|
|
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,
|
|
1081
|
-
right = bspy.Spline(contour.nInd,
|
|
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
|
-
#
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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 \
|