bspy 4.3__py3-none-any.whl → 4.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bspy/_spline_domain.py +38 -6
- bspy/_spline_evaluation.py +1 -1
- bspy/_spline_fitting.py +34 -9
- bspy/_spline_intersection.py +44 -40
- bspy/_spline_milling.py +288 -0
- bspy/_spline_operations.py +55 -42
- bspy/solid.py +1 -1
- bspy/spline.py +128 -18
- {bspy-4.3.dist-info → bspy-4.4.1.dist-info}/METADATA +13 -12
- bspy-4.4.1.dist-info/RECORD +19 -0
- {bspy-4.3.dist-info → bspy-4.4.1.dist-info}/WHEEL +1 -1
- bspy-4.3.dist-info/RECORD +0 -18
- {bspy-4.3.dist-info → bspy-4.4.1.dist-info/licenses}/LICENSE +0 -0
- {bspy-4.3.dist-info → bspy-4.4.1.dist-info}/top_level.txt +0 -0
bspy/_spline_domain.py
CHANGED
|
@@ -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
|
|
bspy/_spline_evaluation.py
CHANGED
|
@@ -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
|
bspy/_spline_fitting.py
CHANGED
|
@@ -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
|
bspy/_spline_intersection.py
CHANGED
|
@@ -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 \
|
bspy/_spline_milling.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import bspy.spline
|
|
3
|
+
import bspy.spline_block
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
|
|
7
|
+
if self.nInd != 2: raise ValueError("Surface must have two independent variables")
|
|
8
|
+
if len(uvStart) != 2: raise ValueError("uvStart must have two components")
|
|
9
|
+
uvDomain = self.domain()
|
|
10
|
+
if uvStart[0] < uvDomain[0, 0] or uvStart[0] > uvDomain[0, 1] or \
|
|
11
|
+
uvStart[1] < uvDomain[1, 0] or uvStart[1] > uvDomain[1, 1]:
|
|
12
|
+
raise ValueError("uvStart is outside domain of the surface")
|
|
13
|
+
is_max = bool(is_max) # Ensure is_max is a boolean for XNOR operation
|
|
14
|
+
|
|
15
|
+
# Define the callback function for the ODE solver
|
|
16
|
+
def curvatureLineCallback(t, u):
|
|
17
|
+
# Evaluate the surface information needed.
|
|
18
|
+
uv = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
|
|
19
|
+
su = self.derivative((1, 0), uv)
|
|
20
|
+
sv = self.derivative((0, 1), uv)
|
|
21
|
+
suu = self.derivative((2, 0), uv)
|
|
22
|
+
suv = self.derivative((1, 1), uv)
|
|
23
|
+
svv = self.derivative((0, 2), uv)
|
|
24
|
+
suuu = self.derivative((3, 0), uv)
|
|
25
|
+
suuv = self.derivative((2, 1), uv)
|
|
26
|
+
suvv = self.derivative((1, 2), uv)
|
|
27
|
+
svvv = self.derivative((0, 3), uv)
|
|
28
|
+
normal = self.normal(uv)
|
|
29
|
+
|
|
30
|
+
# Calculate curvature matrix and its derivatives.
|
|
31
|
+
sU = np.concatenate((su, sv)).reshape(2, -1)
|
|
32
|
+
sUu = np.concatenate((suu, suv)).reshape(2, -1)
|
|
33
|
+
sUv = np.concatenate((suv, svv)).reshape(2, -1)
|
|
34
|
+
sUU = np.concatenate((suu, suv, suv, svv)).reshape(2, 2, -1)
|
|
35
|
+
sUUu = np.concatenate((suuu, suuv, suuv, suvv)).reshape(2, 2, -1)
|
|
36
|
+
sUUv = np.concatenate((suuv, suvv, suvv, svvv)).reshape(2, 2, -1)
|
|
37
|
+
fffI = np.linalg.inv(sU @ sU.T) # Inverse of first fundamental form
|
|
38
|
+
k = fffI @ (sUU @ normal) # Curvature matrix
|
|
39
|
+
ku = fffI @ (sUUu @ normal - (sUu @ sU.T + sU @ sUu.T) @ k - sUU @ (sU.T @ k[:, 0]))
|
|
40
|
+
kv = fffI @ (sUUv @ normal - (sUv @ sU.T + sU @ sUv.T) @ k - sUU @ (sU.T @ k[:, 1]))
|
|
41
|
+
|
|
42
|
+
# Determine principle curvatures and directions, and assign new direction.
|
|
43
|
+
curvatures, directions = np.linalg.eig(k)
|
|
44
|
+
curvatureDelta = curvatures[1] - curvatures[0]
|
|
45
|
+
if abs(curvatureDelta) < tolerance:
|
|
46
|
+
# If we're at an umbilic, use the last direction (jacobian is zero at umbilic).
|
|
47
|
+
direction = u[:, 1]
|
|
48
|
+
jacobian = np.zeros((2,2,1), self.coefs.dtype)
|
|
49
|
+
else:
|
|
50
|
+
# Otherwise, compute the lhs inverse for the jacobian.
|
|
51
|
+
directionsInverse = np.linalg.inv(directions)
|
|
52
|
+
eigenIndex = 0 if bool(curvatures[0] > curvatures[1]) == is_max else 1
|
|
53
|
+
direction = directions[:, eigenIndex]
|
|
54
|
+
B = np.zeros((2, 2), self.coefs.dtype)
|
|
55
|
+
B[0, 1 - eigenIndex] = np.dot(directions[:, 1], direction) / curvatureDelta
|
|
56
|
+
B[1, 1 - eigenIndex] = -np.dot(directions[:, 0], direction) / curvatureDelta
|
|
57
|
+
lhsInv = directions @ B @ directionsInverse
|
|
58
|
+
|
|
59
|
+
# Adjust the direction for consistency.
|
|
60
|
+
if np.dot(direction, u[:, 1]) < -tolerance:
|
|
61
|
+
direction *= -1
|
|
62
|
+
|
|
63
|
+
# Compute the jacobian for the direction.
|
|
64
|
+
jacobian = np.empty((2,2,1), self.coefs.dtype)
|
|
65
|
+
jacobian[:,0,0] = lhsInv @ ku @ direction
|
|
66
|
+
jacobian[:,1,0] = lhsInv @ kv @ direction
|
|
67
|
+
|
|
68
|
+
return direction, jacobian
|
|
69
|
+
|
|
70
|
+
# Generate the initial guess for the line of curvature.
|
|
71
|
+
uvStart = np.atleast_1d(uvStart)
|
|
72
|
+
direction = 0.5 * (uvDomain[:,0] + uvDomain[:,1]) - uvStart # Initial guess toward center
|
|
73
|
+
distanceFromCenter = np.linalg.norm(direction)
|
|
74
|
+
if distanceFromCenter < 10 * tolerance:
|
|
75
|
+
# If we're at the center, just point to the far corner.
|
|
76
|
+
direction = np.array((1.0, 1.0)) / np.sqrt(2)
|
|
77
|
+
else:
|
|
78
|
+
direction /= distanceFromCenter
|
|
79
|
+
|
|
80
|
+
# Compute line of curvature direction at start.
|
|
81
|
+
direction, jacobian = curvatureLineCallback(0.0, np.array(((uvStart[0], direction[0]), (uvStart[1], direction[1]))))
|
|
82
|
+
|
|
83
|
+
# Calculate distance to the boundary in that direction.
|
|
84
|
+
if direction[0] < -tolerance:
|
|
85
|
+
uBoundaryDistance = (uvDomain[0, 0] - uvStart[0]) / direction[0]
|
|
86
|
+
elif direction[0] > tolerance:
|
|
87
|
+
uBoundaryDistance = (uvDomain[0, 1] - uvStart[0]) / direction[0]
|
|
88
|
+
else:
|
|
89
|
+
uBoundaryDistance = np.inf
|
|
90
|
+
if direction[1] < -tolerance:
|
|
91
|
+
vBoundaryDistance = (uvDomain[1, 0] - uvStart[1]) / direction[1]
|
|
92
|
+
elif direction[1] > tolerance:
|
|
93
|
+
vBoundaryDistance = (uvDomain[1, 1] - uvStart[1]) / direction[1]
|
|
94
|
+
else:
|
|
95
|
+
vBoundaryDistance = np.inf
|
|
96
|
+
boundaryDistance = min(uBoundaryDistance, vBoundaryDistance)
|
|
97
|
+
|
|
98
|
+
# Construct the initial guess from start point to boundary.
|
|
99
|
+
initialGuess = bspy.spline.Spline.line(uvStart, uvStart + boundaryDistance * direction).elevate([2])
|
|
100
|
+
|
|
101
|
+
# Solve the ODE and return the line of curvature confined to the surface's domain.
|
|
102
|
+
solution = initialGuess.solve_ode(1, 0, curvatureLineCallback, tolerance, includeEstimate = True)
|
|
103
|
+
return solution.confine(uvDomain)
|
|
104
|
+
|
|
105
|
+
def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
|
|
106
|
+
if self.nDep < 2 or self.nDep > 3 or self.nDep - self.nInd != 1: raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
|
|
107
|
+
if edgeRadius < 0:
|
|
108
|
+
raise ValueError("edgeRadius must be >= 0")
|
|
109
|
+
elif edgeRadius == 0:
|
|
110
|
+
return self
|
|
111
|
+
if bitRadius is None:
|
|
112
|
+
bitRadius = edgeRadius
|
|
113
|
+
elif bitRadius < edgeRadius:
|
|
114
|
+
raise ValueError("bitRadius must be >= edgeRadius")
|
|
115
|
+
if angle < 0 or angle >= np.pi / 2: raise ValueError("angle must in the range [0, pi/2)")
|
|
116
|
+
if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
|
|
117
|
+
raise ValueError("path must be a 2D curve and self must be a 3D surface")
|
|
118
|
+
|
|
119
|
+
# Compute new order, knots, and fillets for offset (ensure order is at least 4).
|
|
120
|
+
Fillet = namedtuple('Fillet', ('adjustment', 'isFillet'))
|
|
121
|
+
newOrder = []
|
|
122
|
+
newKnotList = []
|
|
123
|
+
newUniqueList = []
|
|
124
|
+
filletList = []
|
|
125
|
+
for order, knots in zip(self.order, self.knots):
|
|
126
|
+
min4Order = max(order, 4)
|
|
127
|
+
unique, counts = np.unique(knots, return_counts=True)
|
|
128
|
+
counts += min4Order - order # Ensure order is at least 4
|
|
129
|
+
newOrder.append(min4Order)
|
|
130
|
+
adjustment = 0
|
|
131
|
+
epsilon = np.finfo(unique.dtype).eps
|
|
132
|
+
|
|
133
|
+
# Add first knot.
|
|
134
|
+
newKnots = [unique[0]] * counts[0]
|
|
135
|
+
newUnique = [unique[0]]
|
|
136
|
+
fillets = [Fillet(adjustment, False)]
|
|
137
|
+
|
|
138
|
+
# Add internal knots, checking for C1 discontinuities needing fillets.
|
|
139
|
+
for knot, count in zip(unique[1:-1], counts[1:-1]):
|
|
140
|
+
knot += adjustment
|
|
141
|
+
newKnots += [knot] * count
|
|
142
|
+
newUnique.append(knot)
|
|
143
|
+
# Check for lack of C1 continuity (need for a fillet)
|
|
144
|
+
if count >= min4Order - 1:
|
|
145
|
+
fillets.append(Fillet(adjustment, True))
|
|
146
|
+
# Create parametric space for fillet.
|
|
147
|
+
adjustment += 1
|
|
148
|
+
knot += 1 + epsilon # Add additional adjustment and step slightly past discontinuity
|
|
149
|
+
newKnots += [knot] * (min4Order - 1)
|
|
150
|
+
newUnique.append(knot)
|
|
151
|
+
fillets.append(Fillet(adjustment, False))
|
|
152
|
+
|
|
153
|
+
# Add last knot.
|
|
154
|
+
newKnots += [unique[-1] + adjustment] * counts[-1]
|
|
155
|
+
newUnique.append(unique[-1] + adjustment)
|
|
156
|
+
fillets.append(Fillet(adjustment, False))
|
|
157
|
+
|
|
158
|
+
# Build fillet and knot lists.
|
|
159
|
+
newKnotList.append(np.array(newKnots, knots.dtype))
|
|
160
|
+
newUniqueList.append(np.array(newUnique, knots.dtype))
|
|
161
|
+
filletList.append(fillets)
|
|
162
|
+
|
|
163
|
+
if path is not None:
|
|
164
|
+
min4Order = max(path.order[0], 4)
|
|
165
|
+
newOrder = [min4Order]
|
|
166
|
+
unique, counts = np.unique(path.knots[0], return_counts=True)
|
|
167
|
+
counts += min4Order - path.order[0] # Ensure order is at least 4
|
|
168
|
+
newKnotList = [np.repeat(unique, counts)]
|
|
169
|
+
domain = path.domain()
|
|
170
|
+
else:
|
|
171
|
+
domain = [(unique[0], unique[-1]) for unique in newUniqueList]
|
|
172
|
+
|
|
173
|
+
# Determine geometry of drill bit.
|
|
174
|
+
if subtract:
|
|
175
|
+
edgeRadius *= -1
|
|
176
|
+
bitRadius *= -1
|
|
177
|
+
w = bitRadius - edgeRadius
|
|
178
|
+
h = w * np.tan(angle)
|
|
179
|
+
bottom = np.sin(angle)
|
|
180
|
+
bottomRadius = edgeRadius + h / bottom
|
|
181
|
+
|
|
182
|
+
# Define drill bit function.
|
|
183
|
+
if abs(w) < tolerance and path is None: # Simple offset curve or surface
|
|
184
|
+
def drillBit(normal):
|
|
185
|
+
return edgeRadius * normal
|
|
186
|
+
elif self.nDep == 2: # General offset curve
|
|
187
|
+
def drillBit(normal):
|
|
188
|
+
upward = np.sign(normal[1])
|
|
189
|
+
if upward * normal[1] <= bottom:
|
|
190
|
+
return np.array((edgeRadius * normal[0] + w * np.sign(normal[0]), edgeRadius * normal[1]))
|
|
191
|
+
else:
|
|
192
|
+
return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h))
|
|
193
|
+
elif self.nDep == 3: # General offset surface
|
|
194
|
+
def drillBit(normal):
|
|
195
|
+
upward = np.sign(normal[1])
|
|
196
|
+
if upward * normal[1] <= bottom:
|
|
197
|
+
norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
|
|
198
|
+
return np.array((edgeRadius * normal[0] + w * normal[0] / norm, edgeRadius * normal[1], edgeRadius * normal[2] + w * normal[2] / norm))
|
|
199
|
+
else:
|
|
200
|
+
return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h, bottomRadius * normal[2]))
|
|
201
|
+
else: # Should never get here (exception raised earlier)
|
|
202
|
+
raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
|
|
203
|
+
|
|
204
|
+
# Define function to pass to fit.
|
|
205
|
+
def fitFunction(uv):
|
|
206
|
+
if path is not None:
|
|
207
|
+
uv = path(uv)
|
|
208
|
+
|
|
209
|
+
# Compute adjusted spline uv values, accounting for fillets.
|
|
210
|
+
hasFillet = False
|
|
211
|
+
adjustedUV = uv.copy()
|
|
212
|
+
for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
|
|
213
|
+
ix = np.searchsorted(unique, u, 'right') - 1
|
|
214
|
+
fillet = fillets[ix]
|
|
215
|
+
if fillet.isFillet:
|
|
216
|
+
hasFillet = True
|
|
217
|
+
adjustedUV[i] = unique[ix] - fillet.adjustment
|
|
218
|
+
else:
|
|
219
|
+
adjustedUV[i] -= fillet.adjustment
|
|
220
|
+
|
|
221
|
+
# If we have fillets, compute the normal from their normal fan.
|
|
222
|
+
if hasFillet:
|
|
223
|
+
normal = np.zeros(self.nDep, self.coefs.dtype)
|
|
224
|
+
nudged = adjustedUV.copy()
|
|
225
|
+
for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
|
|
226
|
+
ix = np.searchsorted(unique, u, 'right') - 1
|
|
227
|
+
fillet = fillets[ix]
|
|
228
|
+
if fillet.isFillet:
|
|
229
|
+
epsilon = np.finfo(unique.dtype).eps
|
|
230
|
+
alpha = u - unique[ix]
|
|
231
|
+
np.copyto(nudged, adjustedUV)
|
|
232
|
+
nudged[i] -= epsilon
|
|
233
|
+
normal += (1 - alpha) * self.normal(nudged)
|
|
234
|
+
nudged[i] += 2 * epsilon
|
|
235
|
+
normal += alpha * self.normal(nudged)
|
|
236
|
+
normal = normal / np.linalg.norm(normal)
|
|
237
|
+
else:
|
|
238
|
+
normal = self.normal(adjustedUV)
|
|
239
|
+
|
|
240
|
+
# Return the offset based on the normal.
|
|
241
|
+
return self(adjustedUV) + drillBit(normal)
|
|
242
|
+
|
|
243
|
+
# Fit new spline to offset by drill bit.
|
|
244
|
+
offset = bspy.spline.Spline.fit(domain, fitFunction, newOrder, newKnotList, tolerance)
|
|
245
|
+
|
|
246
|
+
# Remove cusps as required (only applies to offset curves).
|
|
247
|
+
if removeCusps and (self.nInd == 1 or path is not None):
|
|
248
|
+
# Find the cusps by checking for tangent direction reversal between the spline and offset.
|
|
249
|
+
cusps = []
|
|
250
|
+
previousKnot = None
|
|
251
|
+
start = None
|
|
252
|
+
for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
|
|
253
|
+
if path is not None:
|
|
254
|
+
tangent = self.jacobian(path(knot)) @ path.derivative((1,), knot)
|
|
255
|
+
else:
|
|
256
|
+
tangent = self.derivative((1,), knot)
|
|
257
|
+
flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
|
|
258
|
+
if flipped and start is None:
|
|
259
|
+
start = knot
|
|
260
|
+
if not flipped and start is not None:
|
|
261
|
+
cusps.append((start, previousKnot))
|
|
262
|
+
start = None
|
|
263
|
+
previousKnot = knot
|
|
264
|
+
|
|
265
|
+
# Remove the cusps by intersecting the offset segments before and after each cusp.
|
|
266
|
+
segmentList = []
|
|
267
|
+
for cusp in cusps:
|
|
268
|
+
domain = offset.domain()
|
|
269
|
+
before = offset.trim(((domain[0][0], cusp[0]),))
|
|
270
|
+
after = -offset.trim(((cusp[1], domain[0][1]),))
|
|
271
|
+
if path is not None:
|
|
272
|
+
# Project before and after onto a 2D plane defined by the offset tangent
|
|
273
|
+
# and the surface normal at the start of the cusp.
|
|
274
|
+
# This is necessary to find the intersection point (2 equations, 2 unknowns).
|
|
275
|
+
tangent = offset.derivative((1,), cusp[0])
|
|
276
|
+
projection = np.concatenate((tangent / np.linalg.norm(tangent),
|
|
277
|
+
self.normal(path(cusp[0])))).reshape((2,3))
|
|
278
|
+
before = before.transform(projection)
|
|
279
|
+
after = after.transform(projection)
|
|
280
|
+
block = bspy.spline_block.SplineBlock([[before, after]])
|
|
281
|
+
intersections = block.zeros()
|
|
282
|
+
for intersection in intersections:
|
|
283
|
+
segmentList.append(offset.trim(((domain[0][0], intersection[0]),)))
|
|
284
|
+
offset = offset.trim(((intersection[1], domain[0][1]),))
|
|
285
|
+
segmentList.append(offset)
|
|
286
|
+
offset = bspy.spline.Spline.join(segmentList)
|
|
287
|
+
|
|
288
|
+
return offset
|
bspy/_spline_operations.py
CHANGED
|
@@ -71,16 +71,20 @@ def confine(self, range_bounds):
|
|
|
71
71
|
if self.nInd != 1: raise ValueError("Confine only works on curves (nInd == 1)")
|
|
72
72
|
if len(range_bounds) != self.nDep: raise ValueError("len(range_bounds) must equal nDep")
|
|
73
73
|
spline = self.clamp((0,), (0,))
|
|
74
|
+
if spline is self:
|
|
75
|
+
spline = self.copy()
|
|
74
76
|
order = spline.order[0]
|
|
75
77
|
degree = order - 1
|
|
76
78
|
domain = spline.domain()
|
|
79
|
+
dtype = spline.knots[0].dtype
|
|
77
80
|
unique, counts = np.unique(spline.knots[0], return_counts=True)
|
|
78
81
|
machineEpsilon = np.finfo(self.coefs.dtype).eps
|
|
79
82
|
epsilon = np.sqrt(machineEpsilon)
|
|
80
83
|
intersections = [] # List of tuples (u, boundaryPoint, headingOutside)
|
|
81
84
|
|
|
82
85
|
def addIntersection(u, headedOutside = False):
|
|
83
|
-
|
|
86
|
+
u = dtype.type(u) # Cast to spline domain type
|
|
87
|
+
boundaryPoint = spline(u)
|
|
84
88
|
for i in range(spline.nDep):
|
|
85
89
|
if boundaryPoint[i] < range_bounds[i][0]:
|
|
86
90
|
headedOutside = True if boundaryPoint[i] < range_bounds[i][0] - epsilon else headedOutside
|
|
@@ -88,18 +92,18 @@ def confine(self, range_bounds):
|
|
|
88
92
|
if boundaryPoint[i] > range_bounds[i][1]:
|
|
89
93
|
headedOutside = True if boundaryPoint[i] > range_bounds[i][1] + epsilon else headedOutside
|
|
90
94
|
boundaryPoint[i] = range_bounds[i][1]
|
|
91
|
-
intersections.append(
|
|
95
|
+
intersections.append([u, boundaryPoint, headedOutside])
|
|
92
96
|
|
|
93
97
|
def intersectBoundary(i, j):
|
|
94
98
|
zeros = type(spline)(1, 1, spline.order, spline.nCoef, spline.knots, (spline.coefs[i] - range_bounds[i][j],)).zeros()
|
|
95
99
|
for zero in zeros:
|
|
96
100
|
if isinstance(zero, tuple):
|
|
97
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
101
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero[0])[i] > 0
|
|
98
102
|
addIntersection(zero[0], headedOutside)
|
|
99
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
103
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero[1])[i] > 0
|
|
100
104
|
addIntersection(zero[1], headedOutside)
|
|
101
105
|
else:
|
|
102
|
-
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,),
|
|
106
|
+
headedOutside = (-1 if j == 0 else 1) * spline.derivative((1,), zero)[i] > 0
|
|
103
107
|
addIntersection(zero, headedOutside)
|
|
104
108
|
|
|
105
109
|
addIntersection(domain[0][0]) # Confine starting point
|
|
@@ -112,21 +116,22 @@ def confine(self, range_bounds):
|
|
|
112
116
|
# Put the intersection points in order.
|
|
113
117
|
intersections.sort(key=lambda intersection: intersection[0])
|
|
114
118
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Insert order-1 knots at each intersection point.
|
|
122
|
-
for (knot, boundaryPoint, headedOutside) in intersections:
|
|
123
|
-
ix = np.searchsorted(unique, knot)
|
|
124
|
-
if unique[ix] == knot:
|
|
125
|
-
count = (order - 1) - counts[ix]
|
|
126
|
-
if count > 0:
|
|
127
|
-
spline = spline.insert_knots(((knot, count),))
|
|
119
|
+
# Insert order-1 (degree) knots at each intersection point.
|
|
120
|
+
previousKnot, previousBoundaryPoint, previousHeadedOutside = intersections[0]
|
|
121
|
+
previousIx = 0
|
|
122
|
+
for i, (knot, boundaryPoint, headedOutside) in enumerate(intersections[1:]):
|
|
123
|
+
if knot - previousKnot < epsilon:
|
|
124
|
+
intersections[previousIx][2] = headedOutside # Keep last headed outside
|
|
128
125
|
else:
|
|
129
|
-
|
|
126
|
+
ix = np.searchsorted(unique, knot)
|
|
127
|
+
if unique[ix] == knot:
|
|
128
|
+
count = degree - counts[ix]
|
|
129
|
+
if count > 0:
|
|
130
|
+
spline = spline.insert_knots((((knot, count),),))
|
|
131
|
+
else:
|
|
132
|
+
spline = spline.insert_knots((((knot, degree),),))
|
|
133
|
+
previousKnot = knot
|
|
134
|
+
previousIx = i
|
|
130
135
|
|
|
131
136
|
# Go through the boundary points, assigning boundary coefficients, interpolating between boundary points,
|
|
132
137
|
# and removing knots and coefficients where the curve stalls.
|
|
@@ -139,29 +144,37 @@ def confine(self, range_bounds):
|
|
|
139
144
|
knotAdjustment = 0.0
|
|
140
145
|
for knot, boundaryPoint, headedOutside in intersections[1:]:
|
|
141
146
|
knot += knotAdjustment
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
147
|
+
if knot - previousKnot >= epsilon:
|
|
148
|
+
ix = np.searchsorted(knots, knot, 'right') - order
|
|
149
|
+
ix = min(ix, nCoef - 1)
|
|
150
|
+
coefs[:, ix] = boundaryPoint # Assign boundary coefficients
|
|
151
|
+
if previousHeadedOutside and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
|
|
152
|
+
# Curve has stalled, so remove intervening knots and coefficients, and adjust knot values.
|
|
153
|
+
nCoef -= ix - previousIx
|
|
154
|
+
knots = np.delete(knots, slice(previousIx + 1, ix + 1))
|
|
155
|
+
knots[previousIx + 1:] -= knot - previousKnot
|
|
156
|
+
knotAdjustment -= knot - previousKnot
|
|
157
|
+
coefs = np.delete(coefs, slice(previousIx, ix), axis=1)
|
|
158
|
+
previousHeadedOutside = headedOutside # The previous knot is unchanged, but inherits the new headedOutside value
|
|
159
|
+
else:
|
|
160
|
+
if previousHeadedOutside:
|
|
161
|
+
# If we were outside, linearly interpolate between the previous and current boundary points.
|
|
162
|
+
slope = (boundaryPoint - previousBoundaryPoint) / (knot - previousKnot)
|
|
163
|
+
for i in range(previousIx + 1, ix):
|
|
164
|
+
coefs[:, i] = coefs[:, i - 1] + ((knots[i + degree] - knots[i]) / degree) * slope
|
|
165
|
+
|
|
166
|
+
# Update previous knot
|
|
167
|
+
previousKnot = knot
|
|
168
|
+
previousBoundaryPoint = boundaryPoint
|
|
169
|
+
previousHeadedOutside = headedOutside
|
|
170
|
+
previousIx = ix
|
|
171
|
+
elif previousKnot != knot and knot == domain[0][1] and np.linalg.norm(boundaryPoint - previousBoundaryPoint) < epsilon:
|
|
172
|
+
# Curve stalled at the end. Remove the last knot and its associated coefficients.
|
|
173
|
+
# Keep the last knot if the previous and last knot are the same.
|
|
174
|
+
nCoef -= degree
|
|
175
|
+
knots = knots[:-degree]
|
|
176
|
+
knots[-1] = previousKnot
|
|
177
|
+
coefs = coefs[:,:-degree]
|
|
165
178
|
|
|
166
179
|
spline.nCoef = (nCoef,)
|
|
167
180
|
spline.knots = (knots,)
|
bspy/solid.py
CHANGED
|
@@ -753,7 +753,7 @@ class Solid:
|
|
|
753
753
|
|
|
754
754
|
# Calculate Integral(f) * first cofactor. Note that quad returns a tuple: (integral, error bound).
|
|
755
755
|
returnValue = 0.0
|
|
756
|
-
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))
|
|
756
|
+
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))[0]
|
|
757
757
|
if abs(x0 - point[0]) > epsabs and abs(firstCofactor) > epsabs:
|
|
758
758
|
returnValue = integrate.quad(fHat, x0, point[0], epsabs=epsabs, epsrel=epsrel, *quadArgs)[0] * firstCofactor
|
|
759
759
|
return returnValue
|
bspy/spline.py
CHANGED
|
@@ -5,8 +5,9 @@ from bspy.manifold import Manifold
|
|
|
5
5
|
import bspy.spline_block
|
|
6
6
|
import bspy._spline_domain
|
|
7
7
|
import bspy._spline_evaluation
|
|
8
|
-
import bspy._spline_intersection
|
|
9
8
|
import bspy._spline_fitting
|
|
9
|
+
import bspy._spline_intersection
|
|
10
|
+
import bspy._spline_milling
|
|
10
11
|
import bspy._spline_operations
|
|
11
12
|
|
|
12
13
|
@Manifold.register
|
|
@@ -59,8 +60,8 @@ class Spline(Manifold):
|
|
|
59
60
|
self.knots = tuple(np.array(kk) for kk in knots)
|
|
60
61
|
for knots, order, nCoef in zip(self.knots, self.order, self.nCoef):
|
|
61
62
|
for i in range(nCoef):
|
|
62
|
-
if not(knots[i] <= knots[i + 1] and knots[i]
|
|
63
|
-
raise ValueError("
|
|
63
|
+
if not(knots[i] <= knots[i + 1] and knots[i + order] - knots[i] > 0):
|
|
64
|
+
raise ValueError("Improper knot order or multiplicity")
|
|
64
65
|
totalCoefs = 1
|
|
65
66
|
for nCoef in self.nCoef:
|
|
66
67
|
totalCoefs *= nCoef
|
|
@@ -184,6 +185,25 @@ class Spline(Manifold):
|
|
|
184
185
|
indMap = [(mapping, mapping) if np.isscalar(mapping) else mapping for mapping in indMap]
|
|
185
186
|
return bspy._spline_operations.add(self, other, indMap)
|
|
186
187
|
|
|
188
|
+
def arc_length_map(self, tolerance = 1.0e-6):
|
|
189
|
+
"""
|
|
190
|
+
Determine a mapping s -> u such that any curve parametrized arbitrarily can be composed with the
|
|
191
|
+
computed mapping to get the curve parametrized by arc length. Specifically, given a curve x with
|
|
192
|
+
points on the curve given by x(u), determine a mapping u such that the composite curve x o u is
|
|
193
|
+
parametrized by arc length, i.e. x can be thought of as x(u(s)) for an arc length parameter s.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
tolerance : the accuracy to which the arc length map should be computed
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
spline : Spline
|
|
202
|
+
The spline mapping from R1 -> R1 which approximates the arc length map to within the specified
|
|
203
|
+
tolerance.
|
|
204
|
+
"""
|
|
205
|
+
return bspy._spline_domain.arc_length_map(self, tolerance)
|
|
206
|
+
|
|
187
207
|
@staticmethod
|
|
188
208
|
def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
|
|
189
209
|
"""
|
|
@@ -546,13 +566,13 @@ class Spline(Manifold):
|
|
|
546
566
|
|
|
547
567
|
def contract(self, uvw):
|
|
548
568
|
"""
|
|
549
|
-
Contract a spline by assigning a fixed value to one or more of its independent variables.
|
|
569
|
+
Contract a spline, reducing its number of independent variables, by assigning a fixed value to one or more of its independent variables.
|
|
550
570
|
|
|
551
571
|
Parameters
|
|
552
572
|
----------
|
|
553
573
|
uvw : `iterable`
|
|
554
574
|
An iterable of length `nInd` that specifies the values of each independent variable to contract.
|
|
555
|
-
A value of `None` for an independent variable
|
|
575
|
+
A value of `None` for an independent variable retains that independent variable in the contacted spline.
|
|
556
576
|
|
|
557
577
|
Returns
|
|
558
578
|
-------
|
|
@@ -974,15 +994,15 @@ class Spline(Manifold):
|
|
|
974
994
|
resulting spline function will have nInd + number of independent variables
|
|
975
995
|
in the splines returned independent variables and nDep dependent variables.
|
|
976
996
|
|
|
977
|
-
order : `array-like
|
|
997
|
+
order : `array-like`, optional
|
|
978
998
|
An optional integer array of length nInd which specifies the polynomial
|
|
979
999
|
order to use in each of the independent variables. It will default to order
|
|
980
1000
|
4 (degree 3) if None is specified (the default)
|
|
981
1001
|
|
|
982
|
-
knots : `array-like
|
|
1002
|
+
knots : `array-like`, optional
|
|
983
1003
|
The initial knot sequence to use, if given
|
|
984
1004
|
|
|
985
|
-
tolerance : `scalar
|
|
1005
|
+
tolerance : `scalar`, optional
|
|
986
1006
|
The maximum 2-norm of the difference between the given function and the
|
|
987
1007
|
spline fit. Defaults to 1.0e-4.
|
|
988
1008
|
|
|
@@ -1129,7 +1149,7 @@ class Spline(Manifold):
|
|
|
1129
1149
|
"""
|
|
1130
1150
|
return bspy._spline_intersection.full_domain(self)
|
|
1131
1151
|
|
|
1132
|
-
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-
|
|
1152
|
+
def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
|
|
1133
1153
|
"""
|
|
1134
1154
|
Determine a geodesic between two points on a surface
|
|
1135
1155
|
|
|
@@ -1141,9 +1161,9 @@ class Spline(Manifold):
|
|
|
1141
1161
|
uvEnd : `array-like`
|
|
1142
1162
|
The parameter values for the surface at the other end of the desired geodesic.
|
|
1143
1163
|
|
|
1144
|
-
tolerance : scalar
|
|
1164
|
+
tolerance : scalar, optional
|
|
1145
1165
|
The maximum error in parameter space to which the geodesic should get computed.
|
|
1146
|
-
Defaults to 1.0e-
|
|
1166
|
+
Defaults to 1.0e-5.
|
|
1147
1167
|
|
|
1148
1168
|
Returns
|
|
1149
1169
|
-------
|
|
@@ -1499,6 +1519,36 @@ class Spline(Manifold):
|
|
|
1499
1519
|
"""
|
|
1500
1520
|
return bspy._spline_fitting.line(startPoint, endPoint)
|
|
1501
1521
|
|
|
1522
|
+
def line_of_curvature(self, uvStart, is_max = True, tolerance = 1.0e-3):
|
|
1523
|
+
"""
|
|
1524
|
+
Determine a line of curvature along a surface
|
|
1525
|
+
|
|
1526
|
+
Parameters
|
|
1527
|
+
----------
|
|
1528
|
+
uvStart : `array-like`
|
|
1529
|
+
The parameter values for the surface at one end of the desired line of curvature.
|
|
1530
|
+
|
|
1531
|
+
is_max : `bool`, optional
|
|
1532
|
+
Boolean value indicating that the line of curvature should be the maximal curvature line.
|
|
1533
|
+
If False, the minimal curvature line is returned. Defaults to True.
|
|
1534
|
+
|
|
1535
|
+
tolerance : scalar, optional
|
|
1536
|
+
The maximum error in parameter space to which the geodesic should get computed.
|
|
1537
|
+
Defaults to 1.0e-3.
|
|
1538
|
+
|
|
1539
|
+
Returns
|
|
1540
|
+
-------
|
|
1541
|
+
spline : `Spline`
|
|
1542
|
+
A spline curve whose range is in the domain of the given surface. The range of the
|
|
1543
|
+
curve is the locus of points whose image under the surface map form the line of curvature
|
|
1544
|
+
starting at the given point.
|
|
1545
|
+
|
|
1546
|
+
See Also
|
|
1547
|
+
--------
|
|
1548
|
+
`solve_ode` : Solve an ordinary differential equation using spline collocation.
|
|
1549
|
+
"""
|
|
1550
|
+
return bspy._spline_milling.line_of_curvature(self, uvStart, is_max, tolerance)
|
|
1551
|
+
|
|
1502
1552
|
@staticmethod
|
|
1503
1553
|
def load(fileName):
|
|
1504
1554
|
"""
|
|
@@ -1653,6 +1703,61 @@ class Spline(Manifold):
|
|
|
1653
1703
|
the matrix formed by the tangents of the spline. If the null space is greater than one dimension, the normal will be zero.
|
|
1654
1704
|
"""
|
|
1655
1705
|
return bspy._spline_operations.normal_spline(bspy.spline_block.SplineBlock(self), indices)
|
|
1706
|
+
|
|
1707
|
+
def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtract=False, removeCusps=False, tolerance = 1.0e-4):
|
|
1708
|
+
"""
|
|
1709
|
+
Compute the offset of a spline to a given tolerance.
|
|
1710
|
+
|
|
1711
|
+
Parameters
|
|
1712
|
+
----------
|
|
1713
|
+
edgeRadius : scalar
|
|
1714
|
+
The radius of offset. If a bit radius is specified, the edge radius is the
|
|
1715
|
+
smaller radius of the cutting edge of the drill bit, whereas bit radius specifies
|
|
1716
|
+
half of the full width of the drill bit.
|
|
1717
|
+
|
|
1718
|
+
bitRadius : scalar, optional
|
|
1719
|
+
The radius of the drill bit (half its full width). For a ball nose cutter (the default),
|
|
1720
|
+
the bit radius is the same as the edge radius. For an end mill,
|
|
1721
|
+
the bit radius is larger (typically much larger) than the edge radius.
|
|
1722
|
+
|
|
1723
|
+
angle : scalar, optional
|
|
1724
|
+
The angle at which the drill bit transitions from the edge radius to the
|
|
1725
|
+
flatter bottom of the drill bit. The angle must be in the range [0, pi/2).
|
|
1726
|
+
Defaults to pi / 2.2.
|
|
1727
|
+
|
|
1728
|
+
path : `Spline`, optional
|
|
1729
|
+
The path along self that the drill bit should contact.
|
|
1730
|
+
If specified, the path must be a 2D curve in the domain of self, self must be a 3D surface,
|
|
1731
|
+
and the offset returned is a 3D curve providing the 3D position of the drill bit,
|
|
1732
|
+
rather than the full offset surface. Defaults to None.
|
|
1733
|
+
|
|
1734
|
+
subtract : boolean, optional
|
|
1735
|
+
Flag indicating if the drill bit should be subtracted from the spline instead of added.
|
|
1736
|
+
Subtracting the drill bit returns the tool path that cuts out the spline. Defaults to False.
|
|
1737
|
+
|
|
1738
|
+
removeCusps : boolean, optional
|
|
1739
|
+
Flag indicating if cusps and their associated self-intersections should be removed from the
|
|
1740
|
+
offset. Only applicable to offset curves and paths along offset surfaces. Defaults to False.
|
|
1741
|
+
|
|
1742
|
+
tolerance : `scalar`, optional
|
|
1743
|
+
The maximum 2-norm of the difference between the offset and the
|
|
1744
|
+
spline fit. Defaults to 1.0e-4.
|
|
1745
|
+
|
|
1746
|
+
Returns
|
|
1747
|
+
-------
|
|
1748
|
+
offset : `Spline`
|
|
1749
|
+
The spline that represents the offset.
|
|
1750
|
+
|
|
1751
|
+
See Also
|
|
1752
|
+
--------
|
|
1753
|
+
`fit` : Fit the function f with a spline to a given tolerance.
|
|
1754
|
+
|
|
1755
|
+
Notes
|
|
1756
|
+
-----
|
|
1757
|
+
The offset is only defined for 2D curves and 3D surfaces with well-defined normals.
|
|
1758
|
+
The bottom of the drill bit is tangent to its lowest y value.
|
|
1759
|
+
"""
|
|
1760
|
+
return bspy._spline_milling.offset(self, edgeRadius, bitRadius, angle, path, subtract, removeCusps, tolerance)
|
|
1656
1761
|
|
|
1657
1762
|
@staticmethod
|
|
1658
1763
|
def point(point):
|
|
@@ -1971,7 +2076,7 @@ class Spline(Manifold):
|
|
|
1971
2076
|
"""
|
|
1972
2077
|
return bspy._spline_fitting.section(xytk)
|
|
1973
2078
|
|
|
1974
|
-
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
|
|
2079
|
+
def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
|
|
1975
2080
|
"""
|
|
1976
2081
|
Numerically solve an ordinary differential equation with boundary conditions.
|
|
1977
2082
|
|
|
@@ -1994,26 +2099,31 @@ class Spline(Manifold):
|
|
|
1994
2099
|
FAndF_u : Python function
|
|
1995
2100
|
FAndF_u must have exactly this calling sequence: FAndF_u(t, uData, *args). t is a scalar set
|
|
1996
2101
|
to the desired value of the independent variable of the ODE. uData will be a numpy matrix of shape
|
|
1997
|
-
(self.nDep, nOrder) whose columns are
|
|
2102
|
+
(self.nDep, nOrder) whose columns are u, ... , u^(nOrder - 1). It must return a numpy
|
|
1998
2103
|
vector of length self.nDep and a numpy array whose shape is (self.nDep, self.nDep, nOrder).
|
|
1999
2104
|
The first output vector is the value of the forcing function F at (t, uData). The numpy
|
|
2000
2105
|
array is the array of partial derivatives with respect to all the numbers in uData. Thus, if
|
|
2001
2106
|
this array is called jacobian, then jacobian[:, i, j] is the gradient of the forcing function with
|
|
2002
2107
|
respect to uData[i, j].
|
|
2003
2108
|
|
|
2004
|
-
tolerance : scalar
|
|
2005
|
-
The relative error to which the ODE should get solved.
|
|
2109
|
+
tolerance : scalar, optional
|
|
2110
|
+
The relative error to which the ODE should get solved. Default is 1.0e-6.
|
|
2006
2111
|
|
|
2007
|
-
args : tuple
|
|
2112
|
+
args : tuple, optional
|
|
2008
2113
|
Additional arguments to pass to the user-defined function FAndF_u. For example, if FAndF_u has the
|
|
2009
|
-
FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3.
|
|
2114
|
+
FAndF_u(t, uData, a, b, c), then args must be a tuple of length 3. Default is ().
|
|
2115
|
+
|
|
2116
|
+
includeEstimate : bool, optional
|
|
2117
|
+
If `includeEstimate` is True, the uData passed to `FAndF_u` will be a numpy matrix of shape
|
|
2118
|
+
(self.nDep, nOrder + 1) whose columns are u, ... , u^(nOrder). The last column will be the most
|
|
2119
|
+
recent estimate of u^(nOrder)(t). Default is False.
|
|
2010
2120
|
|
|
2011
2121
|
Notes
|
|
2012
2122
|
=====
|
|
2013
2123
|
This method uses B-splines as finite elements. The ODE itself is discretized using
|
|
2014
2124
|
collocation.
|
|
2015
2125
|
"""
|
|
2016
|
-
return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args)
|
|
2126
|
+
return bspy._spline_fitting.solve_ode(self, nLeft, nRight, FAndF_u, tolerance, args, includeEstimate)
|
|
2017
2127
|
|
|
2018
2128
|
@staticmethod
|
|
2019
2129
|
def sphere(radius, tolerance = None):
|
|
@@ -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
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
|
|
2
|
+
bspy/_spline_domain.py,sha256=NW9BJell382gbogq4lRlcDcaTCy3UZEd_9RrDM_hEYg,34466
|
|
3
|
+
bspy/_spline_evaluation.py,sha256=aQ9w4N5MQDol7_OQ6HLljMtCbGVuS5iCAlM7Sg2q8B4,9656
|
|
4
|
+
bspy/_spline_fitting.py,sha256=Rd_75bCNrd4fpKOcA0ujPD2hS-pvUQewaZqmDtld5QM,52205
|
|
5
|
+
bspy/_spline_intersection.py,sha256=SukAnnS9AFcBNkJfUFrIGWtd7HCp8P-WR8BmLw2IbUA,67635
|
|
6
|
+
bspy/_spline_milling.py,sha256=RTxCIVzFiVyypx3-1R9ZjtQEokz_yyERPNdJRLhU36U,13899
|
|
7
|
+
bspy/_spline_operations.py,sha256=O2AsfJb0PvaNbM2Rp3_AbNXKo8X6X6RJDwDclqOwT7o,43564
|
|
8
|
+
bspy/hyperplane.py,sha256=gnVZ7rjisGpzHfm1moItyzq8mO7HguzzpY4dpFwyDiw,24840
|
|
9
|
+
bspy/manifold.py,sha256=vjgyz0M1mkgenUnTIbX7NFg1fUCgXtStr6ofF4oSLgg,14470
|
|
10
|
+
bspy/solid.py,sha256=tsO7fcGj-x3SWH2fBSkeu-Hx5vX0KMFOt1baH-OYNgQ,36995
|
|
11
|
+
bspy/spline.py,sha256=PSfOCSBzoxNY5tnkTv_6yKOKToFiEnBR2r9-S8JaHd8,106788
|
|
12
|
+
bspy/splineOpenGLFrame.py,sha256=N8elVJrt24_utOSoTaM5Ue5De2M4DxrquyB7o2lLLD4,96256
|
|
13
|
+
bspy/spline_block.py,sha256=O8MzfBEygVdAx57DoJMwzjkw349BQqht7_RVu8MO0Fg,20127
|
|
14
|
+
bspy/viewer.py,sha256=_iQCyEpsBFPBLLuHq7tc43IPVvlcqxdp0Hig0uvpQns,34349
|
|
15
|
+
bspy-4.4.1.dist-info/licenses/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
|
|
16
|
+
bspy-4.4.1.dist-info/METADATA,sha256=AHsaONNlgb7bZBRS-kyO4gmZTgmE7RjcdPZ_hgVQMt0,7140
|
|
17
|
+
bspy-4.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
bspy-4.4.1.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
|
|
19
|
+
bspy-4.4.1.dist-info/RECORD,,
|
bspy-4.3.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
bspy/__init__.py,sha256=LnJx7iHah7A4vud9y64LR61rBtYnuhV4Wno9O2IEK1I,1499
|
|
2
|
-
bspy/_spline_domain.py,sha256=bQQsJlKstIYdbEKIW7vr-7nTKat8y9thYc7jxzZHNFQ,33238
|
|
3
|
-
bspy/_spline_evaluation.py,sha256=WIv0tLZNLy0uHNj9YwR7vbputgT2Mn5QDXZzlD6ousk,9638
|
|
4
|
-
bspy/_spline_fitting.py,sha256=CEkmUQalXTT5N5ZOJpFF4Y2DI-ARlZap9IaCJ_Zg2Ws,50762
|
|
5
|
-
bspy/_spline_intersection.py,sha256=8FPTh4IDtzkRpieNtlnw8VhLabPzY4E_LWDaGxHIMTM,66973
|
|
6
|
-
bspy/_spline_operations.py,sha256=8yJGp4iVVvQ1zcUHAKgNq2TMIjDUhacf5XSoHp2jmVo,42799
|
|
7
|
-
bspy/hyperplane.py,sha256=gnVZ7rjisGpzHfm1moItyzq8mO7HguzzpY4dpFwyDiw,24840
|
|
8
|
-
bspy/manifold.py,sha256=vjgyz0M1mkgenUnTIbX7NFg1fUCgXtStr6ofF4oSLgg,14470
|
|
9
|
-
bspy/solid.py,sha256=ufNs5JV0jQ1A13pUY61N0pcW6Ep-DZmXUau7GHHcKk4,36992
|
|
10
|
-
bspy/spline.py,sha256=1USitAm1FIQg5JuWa1xLrVQQ2cWRGfP4VhQnS8XOKc8,101377
|
|
11
|
-
bspy/splineOpenGLFrame.py,sha256=N8elVJrt24_utOSoTaM5Ue5De2M4DxrquyB7o2lLLD4,96256
|
|
12
|
-
bspy/spline_block.py,sha256=O8MzfBEygVdAx57DoJMwzjkw349BQqht7_RVu8MO0Fg,20127
|
|
13
|
-
bspy/viewer.py,sha256=_iQCyEpsBFPBLLuHq7tc43IPVvlcqxdp0Hig0uvpQns,34349
|
|
14
|
-
bspy-4.3.dist-info/LICENSE,sha256=nLfJULN68Jw6GfCJp4xeMksGuRdyWNdgEsZGjw2twig,1091
|
|
15
|
-
bspy-4.3.dist-info/METADATA,sha256=lV6eciVqRf3FqWslbTD5OWb3gMD6-gv6WB-hkSRadPM,7044
|
|
16
|
-
bspy-4.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
17
|
-
bspy-4.3.dist-info/top_level.txt,sha256=fotZnJn6aCwgUbBEV3hslIko7Nw-eqtHLq2eyJLlFsY,5
|
|
18
|
-
bspy-4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|