bspy 1.5.0__py3-none-any.whl → 2.1.0__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 CHANGED
@@ -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 (spline, i) in zip(splines, range(len(splines))):
52
+ for i, spline in enumerate(splines):
53
53
  m = spline.nInd * [0]
54
- newKnots = spline.nInd * [[]]
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][:-1]) + list(spl.knots[0][maxOrder:])]
365
- newCoefs = [list(workingCoefs) + list(splCoefs)[1:] for workingCoefs, splCoefs in zip(workingSpline.coefs, spl.coefs)]
366
- workingSpline = type(workingSpline)(1, numDep, workingSpline.order, [workingSpline.nCoef[0] + spl.nCoef[0] - 1],
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
- # workingSpline, numRemoved, error = workingSpline.remove_knots() # Add this back in once remove_knots is fixed
369
- return workingSpline.reparametrize([[0.0, 1.0]])
370
-
371
- def remove_knots(self, oldKnots=((),), maxRemovalsPerKnot=0, tolerance=None):
372
- if not(len(oldKnots) == self.nInd): raise ValueError("Invalid oldKnots")
373
- nCoef = [*self.nCoef]
374
- knotList = list(self.knots)
375
- coefs = self.coefs.copy()
376
- temp = np.empty_like(coefs)
377
- totalRemoved = 0
378
- maxResidualError = 0.0
379
- for ind in range(self.nInd):
380
- order = self.order[ind]
381
- highSpan = nCoef[ind] - 1
382
- if highSpan < order:
383
- continue # no interior knots
384
-
385
- removeAll = len(oldKnots[ind]) == 0
386
- degree = order - 1
387
- knots = knotList[ind].copy()
388
- highU = knots[highSpan]
389
- gap = 0 # size of the gap
390
- u = knots[order]
391
- knotIndex = order
392
- while u == knots[knotIndex + 1]:
393
- knotIndex += 1
394
- multiplicity = knotIndex - degree
395
- firstCoefOut = (2 * knotIndex - degree - multiplicity) // 2 # first control point out
396
- last = knotIndex - multiplicity
397
- first = multiplicity
398
- beforeGap = knotIndex # control-point index before gap
399
- afterGap = beforeGap + 1 # control-point index after gap
400
- # Move the independent variable to the front of coefs and temp. We'll move it back at later.
401
- coefs = coefs.swapaxes(0, ind + 1)
402
- temp = temp.swapaxes(0, ind + 1)
403
-
404
- # Loop thru knots, stop after we process highU.
405
- while True:
406
- # Compute how many times to remove knot.
407
- removed = 0
408
- if removeAll or u in oldKnots[ind]:
409
- if maxRemovalsPerKnot > 0:
410
- maxRemovals = min(multiplicity, maxRemovalsPerKnot)
411
- else:
412
- maxRemovals = multiplicity
413
- else:
414
- maxRemovals = 0
415
-
416
- while removed < maxRemovals:
417
- offset = first - 1 # diff in index of temp and coefs
418
- temp[0] = coefs[offset]
419
- temp[last + 1 - offset] = coefs[last + 1]
420
- i = first
421
- j = last
422
- ii = first - offset
423
- jj = last - offset
424
-
425
- # Compute new coefficients for 1 removal step.
426
- while j - i > removed:
427
- alphaI = (u - knots[i]) / (knots[i + order + gap + removed] - knots[i])
428
- alphaJ = (u - knots[j - removed]) / (knots[j + order + gap] - knots[j - removed])
429
- temp[ii] = (coefs[i] - (1.0 - alphaI) * temp[ii - 1]) / alphaI
430
- temp[jj] = (coefs[j] - alphaJ * temp[jj + 1])/ (1.0 - alphaJ)
431
- i += 1
432
- ii += 1
433
- j -= 1
434
- j -= 1
435
-
436
- # Compute residual error.
437
- if j - i < removed:
438
- residualError = np.linalg.norm(temp[ii - 1] - temp[jj + 1], axis=ind).max()
439
- else:
440
- alphaI = (u - knots[i]) / (knots[i + order + gap + removed] - knots[i])
441
- residualError = np.linalg.norm(alphaI * temp[ii + removed + 1] + (1.0 - alphaI) * temp[ii - 1] - coefs[i], axis=ind).max()
442
-
443
- # Check if knot is removable.
444
- if tolerance is None or residualError <= tolerance:
445
- # Successful removal. Save new coefficients.
446
- maxResidualError = max(residualError, maxResidualError)
447
- i = first
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
- # Go to next knot, shift knots and coefficients down, and reset gaps.
483
- k1 = knotIndex - removed + 1
484
- k = knotIndex + gap + 1
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)