bspy 1.5.0__tar.gz → 2.1.0__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-1.5.0 → bspy-2.1.0}/PKG-INFO +4 -3
- {bspy-1.5.0 → bspy-2.1.0}/README.md +3 -2
- {bspy-1.5.0 → bspy-2.1.0}/bspy/_spline_domain.py +88 -159
- {bspy-1.5.0 → bspy-2.1.0}/bspy/_spline_evaluation.py +15 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy/_spline_fitting.py +203 -163
- {bspy-1.5.0 → bspy-2.1.0}/bspy/_spline_operations.py +40 -13
- {bspy-1.5.0 → bspy-2.1.0}/bspy/spline.py +238 -96
- {bspy-1.5.0 → bspy-2.1.0}/bspy.egg-info/PKG-INFO +4 -3
- {bspy-1.5.0 → bspy-2.1.0}/setup.cfg +1 -1
- {bspy-1.5.0 → bspy-2.1.0}/tests/test_bspy.py +89 -36
- {bspy-1.5.0 → bspy-2.1.0}/LICENSE +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy/__init__.py +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy/_spline_intersection.py +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy/bspyApp.py +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy/drawableSpline.py +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy/splineOpenGLFrame.py +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy.egg-info/SOURCES.txt +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy.egg-info/dependency_links.txt +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy.egg-info/requires.txt +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/bspy.egg-info/top_level.txt +0 -0
- {bspy-1.5.0 → bspy-2.1.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bspy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -33,8 +33,9 @@ Requires-Dist: pyopengltk
|
|
|
33
33
|
Library for manipulating and rendering b-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
|
|
34
34
|
|
|
35
35
|
The [Spline](https://ericbrec.github.io/bspy/bspy/spline.html) class has a method to fit multidimensional data for
|
|
36
|
-
scalar and vector functions of single and multiple variables.
|
|
37
|
-
|
|
36
|
+
scalar and vector functions of single and multiple variables. It also has methods to create circular arcs, ruled surfaces, and surfaces of revolution.
|
|
37
|
+
Other methods add, subtract, multiply, and linearly transform splines, as well as confine spline curves to a given range.
|
|
38
|
+
There are methods to evaluate spline values, derivatives, integrals, normals, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions. In addition, there are methods to manipulate the domain of splines, including trim, join, reparametrize, reverse, add and remove knots, elevate and extrapolate, and fold and unfold. Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
|
|
38
39
|
|
|
39
40
|
The [SplineOpenGLFrame](https://ericbrec.github.io/bspy/bspy/splineOpenGLFrame.html) class is an
|
|
40
41
|
[OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Library for manipulating and rendering b-spline curves, surfaces, and multidimensional manifolds with non-uniform knots in each dimension.
|
|
3
3
|
|
|
4
4
|
The [Spline](https://ericbrec.github.io/bspy/bspy/spline.html) class has a method to fit multidimensional data for
|
|
5
|
-
scalar and vector functions of single and multiple variables.
|
|
6
|
-
|
|
5
|
+
scalar and vector functions of single and multiple variables. It also has methods to create circular arcs, ruled surfaces, and surfaces of revolution.
|
|
6
|
+
Other methods add, subtract, multiply, and linearly transform splines, as well as confine spline curves to a given range.
|
|
7
|
+
There are methods to evaluate spline values, derivatives, integrals, normals, curvature, and the Jacobian, as well as methods that return spline representations of derivatives, normals, integrals, graphs, and convolutions. In addition, there are methods to manipulate the domain of splines, including trim, join, reparametrize, reverse, add and remove knots, elevate and extrapolate, and fold and unfold. Finally, there are methods to compute the zeros and contours of a spline and to intersect two splines.
|
|
7
8
|
|
|
8
9
|
The [SplineOpenGLFrame](https://ericbrec.github.io/bspy/bspy/splineOpenGLFrame.html) class is an
|
|
9
10
|
[OpenGLFrame](https://pypi.org/project/pyopengltk/) with custom shaders to render spline curves and surfaces.
|
|
@@ -49,9 +49,9 @@ def common_basis(self, splines, indMap):
|
|
|
49
49
|
|
|
50
50
|
# Step 3: Elevate and insert missing knots to each spline accordingly.
|
|
51
51
|
alignedSplines = []
|
|
52
|
-
for
|
|
52
|
+
for i, spline in enumerate(splines):
|
|
53
53
|
m = spline.nInd * [0]
|
|
54
|
-
newKnots =
|
|
54
|
+
newKnots = [[] for ix in spline.order]
|
|
55
55
|
for (map, order, multiplicities) in zip(indMap, orders, knots):
|
|
56
56
|
ind = map[i]
|
|
57
57
|
m[ind] = order - spline.order[ind]
|
|
@@ -361,165 +361,95 @@ def join(splineList):
|
|
|
361
361
|
speed1 = np.linalg.norm(workingSpline.derivative([1], workingDomain[1]))
|
|
362
362
|
speed2 = np.linalg.norm(spl.derivative([1], splDomain[0]))
|
|
363
363
|
spl = spl.reparametrize([[workingDomain[1], workingDomain[1] + speed2 * (splDomain[1] - splDomain[0]) / speed1]])
|
|
364
|
-
newKnots = [list(workingSpline.knots[0]
|
|
365
|
-
newCoefs = [list(workingCoefs) + list(splCoefs)
|
|
366
|
-
workingSpline = type(workingSpline)(1, numDep, workingSpline.order, [workingSpline.nCoef[0] + spl.nCoef[0]
|
|
364
|
+
newKnots = [list(workingSpline.knots[0]) + list(spl.knots[0][maxOrder:])]
|
|
365
|
+
newCoefs = [list(workingCoefs) + list(splCoefs) for workingCoefs, splCoefs in zip(workingSpline.coefs, spl.coefs)]
|
|
366
|
+
workingSpline = type(workingSpline)(1, numDep, workingSpline.order, [workingSpline.nCoef[0] + spl.nCoef[0]],
|
|
367
367
|
newKnots, newCoefs, max(workingSpline.accuracy, spl.accuracy), workingSpline.metadata)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
j = last
|
|
449
|
-
while j - i > removed:
|
|
450
|
-
coefs[i] = temp[i - offset]
|
|
451
|
-
coefs[j] = temp[j - offset]
|
|
452
|
-
i += 1
|
|
453
|
-
j -= 1
|
|
454
|
-
else:
|
|
455
|
-
break # Get out of removed < maxRemovals while-loop
|
|
456
|
-
|
|
457
|
-
first -= 1
|
|
458
|
-
last += 1
|
|
459
|
-
removed += 1
|
|
460
|
-
# End of removed < maxRemovals while-loop.
|
|
461
|
-
|
|
462
|
-
if removed > 0:
|
|
463
|
-
# Knots removed. Shift coefficients down.
|
|
464
|
-
j = firstCoefOut
|
|
465
|
-
i = j
|
|
466
|
-
# Pj thru Pi will be overwritten.
|
|
467
|
-
for k in range(1, removed):
|
|
468
|
-
if k % 2 == 1:
|
|
469
|
-
i += 1
|
|
470
|
-
else:
|
|
471
|
-
j -= 1
|
|
472
|
-
for k in range(i + 1, beforeGap):
|
|
473
|
-
coefs[j] = coefs[k] # shift
|
|
474
|
-
j += 1
|
|
475
|
-
else:
|
|
476
|
-
j = beforeGap + 1
|
|
477
|
-
|
|
478
|
-
if u >= highU:
|
|
479
|
-
gap += removed # No more knots, get out of endless while-loop
|
|
368
|
+
return workingSpline.reparametrize([[0.0, 1.0]]).remove_knots()
|
|
369
|
+
|
|
370
|
+
def remove_knot(self, iKnot):
|
|
371
|
+
if self.nInd != 1: raise ValueError("Must have one independent variable")
|
|
372
|
+
myOrder = self.order[0]
|
|
373
|
+
if iKnot < myOrder or iKnot >= self.nCoef[0]: raise ValueError("Must specify interior knots for removal")
|
|
374
|
+
diag0 = []
|
|
375
|
+
diag1 = []
|
|
376
|
+
rhs = []
|
|
377
|
+
myKnots = self.knots[0]
|
|
378
|
+
thisKnot = myKnots[iKnot]
|
|
379
|
+
|
|
380
|
+
# Form the bidiagonal system
|
|
381
|
+
for ix in range(1, myOrder):
|
|
382
|
+
alpha = (myKnots[iKnot + ix] - thisKnot) / (myKnots[iKnot + ix] - myKnots[iKnot + ix - myOrder])
|
|
383
|
+
diag0.append(alpha)
|
|
384
|
+
diag1.append(1.0 - alpha)
|
|
385
|
+
rhs.append(np.ndarray.copy(self.coefs[:, iKnot - myOrder + ix]))
|
|
386
|
+
|
|
387
|
+
# Adjust right hand side because first and last coefficients are known
|
|
388
|
+
rhs = np.array(rhs)
|
|
389
|
+
rhs[0] -= diag0[0] * self.coefs[:, iKnot - myOrder]
|
|
390
|
+
rhs[-1] -= diag1[-1] * self.coefs[:, iKnot]
|
|
391
|
+
|
|
392
|
+
# Use Givens rotations to factor the matrix and track right hand side
|
|
393
|
+
for ix in range(1, myOrder - 1):
|
|
394
|
+
cos = diag1[ix - 1]
|
|
395
|
+
sin = diag0[ix]
|
|
396
|
+
denom = np.sqrt(cos ** 2 + sin ** 2)
|
|
397
|
+
cos /= denom
|
|
398
|
+
sin /= denom
|
|
399
|
+
diag1[ix - 1] = denom
|
|
400
|
+
diag0[ix - 1] = sin * diag1[ix]
|
|
401
|
+
diag1[ix] *= cos
|
|
402
|
+
tempRow = cos * rhs[ix - 1] + sin * rhs[ix]
|
|
403
|
+
rhs[ix] = cos * rhs[ix] - sin * rhs[ix - 1]
|
|
404
|
+
rhs[ix - 1] = tempRow
|
|
405
|
+
|
|
406
|
+
# Perform back substitution
|
|
407
|
+
rhs[-2] /= diag1[-2]
|
|
408
|
+
for ix in range(1, myOrder - 2):
|
|
409
|
+
rhs[-ix - 2] = (rhs[-ix - 2] - diag0[-ix - 2] * rhs[-ix - 1]) / diag1[-ix - 2]
|
|
410
|
+
|
|
411
|
+
# Create new spline
|
|
412
|
+
newNCoef = [self.nCoef[0] - 1]
|
|
413
|
+
newKnots = [list(self.knots[0][:iKnot]) + list(self.knots[0][iKnot + 1:])]
|
|
414
|
+
newCoefs = []
|
|
415
|
+
for ix in range(self.nDep):
|
|
416
|
+
oldCoefs = self.coefs[ix]
|
|
417
|
+
firstCoefs = oldCoefs[:iKnot - self.order[0] + 1]
|
|
418
|
+
lastCoefs = oldCoefs[iKnot:]
|
|
419
|
+
middleCoefs = rhs[:-1,ix]
|
|
420
|
+
newCoefs.append(list(firstCoefs) + list(middleCoefs) + list(lastCoefs))
|
|
421
|
+
withoutKnot = type(self)(self.nInd, self.nDep, self.order, newNCoef, newKnots, newCoefs)
|
|
422
|
+
return withoutKnot, abs(rhs[-1])
|
|
423
|
+
|
|
424
|
+
def remove_knots(self, tolerance):
|
|
425
|
+
scaleDep = [max(np.abs(bound[0]), np.abs(bound[1])) for bound in self.range_bounds()]
|
|
426
|
+
scaleDep = [1.0 if factor == 0.0 else factor for factor in scaleDep]
|
|
427
|
+
rScaleDep = np.array([1.0 / factor for factor in scaleDep])
|
|
428
|
+
# Remove knots one at a time until done
|
|
429
|
+
currentSpline = self.scale(rScaleDep)
|
|
430
|
+
truthSpline = currentSpline
|
|
431
|
+
indIndex = list(range(currentSpline.nInd))
|
|
432
|
+
for id in indIndex:
|
|
433
|
+
foldedIndices = list(filter(lambda x: x != id, indIndex))
|
|
434
|
+
currentFold, foldedBasis = currentSpline.fold(foldedIndices)
|
|
435
|
+
while True:
|
|
436
|
+
bestError = np.finfo(scaleDep[0].dtype).max
|
|
437
|
+
for ix in range(currentFold.order[0], currentFold.nCoef[0]):
|
|
438
|
+
newSpline, residual = currentFold.remove_knot(ix)
|
|
439
|
+
error = np.max(residual)
|
|
440
|
+
if error < bestError:
|
|
441
|
+
bestError = error
|
|
442
|
+
bestSpline = newSpline
|
|
443
|
+
if bestError > tolerance:
|
|
444
|
+
break
|
|
445
|
+
errorSpline = truthSpline - bestSpline.unfold(foldedIndices, foldedBasis)
|
|
446
|
+
maxError = [max(np.abs(bound[0]), np.abs(bound[1])) for bound in errorSpline.range_bounds()]
|
|
447
|
+
if np.max(maxError) > tolerance:
|
|
480
448
|
break
|
|
481
449
|
else:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
i = k1
|
|
486
|
-
u = knots[k]
|
|
487
|
-
while u == knots[k]:
|
|
488
|
-
knots[i] = knots[k]
|
|
489
|
-
i += 1
|
|
490
|
-
k += 1
|
|
491
|
-
multiplicity = i - k1
|
|
492
|
-
knotIndex = i - 1
|
|
493
|
-
gap += removed
|
|
494
|
-
for k in range(0, multiplicity):
|
|
495
|
-
coefs[j] = coefs[afterGap]
|
|
496
|
-
j += 1
|
|
497
|
-
afterGap += 1
|
|
498
|
-
beforeGap = j - 1
|
|
499
|
-
firstCoefOut = (2 * knotIndex - degree - multiplicity) // 2
|
|
500
|
-
last = knotIndex - multiplicity
|
|
501
|
-
first = knotIndex - degree
|
|
502
|
-
# End of endless while-loop
|
|
503
|
-
|
|
504
|
-
# Shift remaining knots.
|
|
505
|
-
i = highSpan + 1
|
|
506
|
-
k = i - gap
|
|
507
|
-
for j in range(1, order + 1):
|
|
508
|
-
knots[k] = knots[i]
|
|
509
|
-
k += 1
|
|
510
|
-
i += 1
|
|
511
|
-
|
|
512
|
-
# Update totalRemoved, nCoef, knots, and coefs.
|
|
513
|
-
totalRemoved += gap
|
|
514
|
-
nCoef[ind] -= gap
|
|
515
|
-
knotList[ind] = knots[:order + nCoef[ind]]
|
|
516
|
-
coefs = coefs[:nCoef[ind]]
|
|
517
|
-
coefs = coefs.swapaxes(0, ind + 1)
|
|
518
|
-
temp = temp.swapaxes(0, ind + 1)
|
|
519
|
-
# End of ind loop
|
|
520
|
-
|
|
521
|
-
spline = type(self)(self.nInd, self.nDep, self.order, nCoef, knotList, coefs, self.accuracy + maxResidualError, self.metadata)
|
|
522
|
-
return spline, totalRemoved, maxResidualError
|
|
450
|
+
currentFold = bestSpline
|
|
451
|
+
currentSpline = currentFold.unfold(foldedIndices, foldedBasis)
|
|
452
|
+
return currentSpline.scale(scaleDep)
|
|
523
453
|
|
|
524
454
|
def reparametrize(self, newDomain):
|
|
525
455
|
if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
|
|
@@ -540,7 +470,6 @@ def reparametrize(self, newDomain):
|
|
|
540
470
|
for i in range(1-order, 0):
|
|
541
471
|
knots[i] = max(knots[i], nD[1])
|
|
542
472
|
knotList.append(knots)
|
|
543
|
-
|
|
544
473
|
return type(self)(self.nInd, self.nDep, self.order, self.nCoef, knotList, self.coefs, self.accuracy, self.metadata)
|
|
545
474
|
|
|
546
475
|
def reverse(self, variable = 0):
|
|
@@ -56,6 +56,21 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
|
|
|
56
56
|
b += 1
|
|
57
57
|
return basis
|
|
58
58
|
|
|
59
|
+
def curvature(self, uv):
|
|
60
|
+
if self.nInd == 1:
|
|
61
|
+
if self.nDep == 1:
|
|
62
|
+
self = self.graph()
|
|
63
|
+
fp = self.derivative([1], uv)
|
|
64
|
+
fpp = self.derivative([2], uv)
|
|
65
|
+
fpdotfp = fp @ fp
|
|
66
|
+
fpdotfpp = fp @ fpp
|
|
67
|
+
denom = fpdotfp ** 1.5
|
|
68
|
+
if self.nDep == 2:
|
|
69
|
+
numer = fp[0] * fpp[1] - fp[1] * fpp[0]
|
|
70
|
+
else:
|
|
71
|
+
numer = np.sqrt((fpp @ fpp) * fpdotfp - fpdotfpp ** 2)
|
|
72
|
+
return numer / denom
|
|
73
|
+
|
|
59
74
|
def derivative(self, with_respect_to, uvw):
|
|
60
75
|
# Make work for scalar valued functions
|
|
61
76
|
uvw = np.atleast_1d(uvw)
|