bspy 4.2__py3-none-any.whl → 4.4__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/__init__.py CHANGED
@@ -12,7 +12,7 @@ Available subpackages
12
12
  `bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
13
13
  piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
14
14
 
15
- `bspy.spline_block` : Provides the `SplineBlock` class that represents and processes an array-like collection of splines.
15
+ `bspy.spline_block` : Provides the `SplineBlock` class that processes an array-like collection of splines which represent a system of equations.
16
16
 
17
17
  `bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
18
18
 
bspy/_spline_domain.py CHANGED
@@ -310,26 +310,74 @@ def fold(self, foldedInd):
310
310
  coefficientlessSpline = type(self)(len(coefficientlessOrder), 0, coefficientlessOrder, coefficientlessNCoef, coefficientlessKnots, coefficientlessCoefs, self.metadata)
311
311
  return foldedSpline, coefficientlessSpline
312
312
 
313
- def insert_knots(self, newKnots):
314
- if not(len(newKnots) == self.nInd): raise ValueError("Invalid newKnots")
315
- knotsList = list(self.knots)
316
- coefs = self.coefs
317
- for ind, (order, knots) in enumerate(zip(self.order, self.knots)):
318
- # We can't reference self.nCoef[ind] in this loop because we are expanding the knots and coefs arrays.
319
- for knot in newKnots[ind]:
320
- if knot < knots[order-1] or knot > knots[-order]:
321
- raise ValueError(f"Knot insertion outside domain: {knot}")
322
- if knot == knots[-order]:
323
- position = len(knots) - order
313
+ def insert_knots(self, newKnotList):
314
+ if not(len(newKnotList) == self.nInd): raise ValueError("Invalid newKnots")
315
+ knotsList = list(self.knots) # Create a new knot list
316
+ coefs = self.coefs # Set initial value for coefs to check later if it's changed
317
+
318
+ # Insert new knots into each independent variable.
319
+ for ind, (order, knots, newKnots) in enumerate(zip(self.order, self.knots, newKnotList)):
320
+ coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
321
+ degree = order - 1
322
+ for knot in newKnots:
323
+ # Determine new knot multiplicity.
324
+ if np.isscalar(knot):
325
+ multiplicity = 1
324
326
  else:
325
- position = np.searchsorted(knots, knot, 'right')
326
- coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
327
- newCoefs = np.insert(coefs, position - 1, 0.0, axis=0)
328
- for i in range(position - order + 1, position):
329
- alpha = (knot - knots[i]) / (knots[i + order - 1] - knots[i])
330
- newCoefs[i] = (1.0 - alpha) * coefs[i - 1] + alpha * coefs[i]
331
- knotsList[ind] = knots = np.insert(knots, position, knot)
332
- coefs = newCoefs.swapaxes(0, ind + 1)
327
+ multiplicity = knot[1]
328
+ knot = knot[0]
329
+ if multiplicity < 1:
330
+ continue
331
+
332
+ # Check if knot and its total multiplicity is valid.
333
+ knot = knots.dtype.type(knot) # Cast to correct type
334
+ if knot < knots[degree] or knot > knots[-order]:
335
+ raise ValueError(f"Knot insertion outside domain: {knot}")
336
+ position = np.searchsorted(knots, knot, 'right')
337
+ oldMultiplicity = 0
338
+ for k in knots[position - 1::-1]:
339
+ if knot == k:
340
+ oldMultiplicity += 1
341
+ else:
342
+ break
343
+ if oldMultiplicity + multiplicity > order:
344
+ raise ValueError("Knot multiplicity > order")
345
+
346
+ # Initialize oldCoefs and expanded coefs array with multiplicity new coefficients, as well as some indices.
347
+ oldCoefs = coefs[position - order:position].copy()
348
+ lastKnotIndex = position - oldMultiplicity
349
+ firstCoefIndex = position - degree
350
+ coefs = np.insert(coefs, firstCoefIndex, oldCoefs[:multiplicity], axis=0)
351
+ # Compute inserted coefficients (multiplicity of them) and the degree - oldMultiplicity - 1 number of changed coefficients.
352
+ for j in range(multiplicity):
353
+ # Allocate new coefficients for the current multiplicity.
354
+ size = degree - oldMultiplicity - j
355
+ if size < 1:
356
+ # Full multiplicity knot, so use oldCoefs.
357
+ coefs[firstCoefIndex + j] = oldCoefs[0]
358
+ else:
359
+ # Otherwise, allocate space for newCoefs.
360
+ newCoefs = np.empty((size, *coefs.shape[1:]), coefs.dtype)
361
+
362
+ # Compute the new coefficients.
363
+ for i, k in zip(range(size), range(lastKnotIndex - size, lastKnotIndex)):
364
+ alpha = (knot - knots[k]) / (knots[k + degree - j] - knots[k])
365
+ newCoefs[i] = (1.0 - alpha) * oldCoefs[i] + alpha * oldCoefs[i + 1]
366
+
367
+ # Assign the ends of the new coefficients into their respective positions.
368
+ coefs[firstCoefIndex + j] = newCoefs[0]
369
+ if size > 1:
370
+ coefs[lastKnotIndex + multiplicity - j - 2] = newCoefs[-1]
371
+ oldCoefs = newCoefs
372
+
373
+ # Assign remaining computed coefficients (the ones in the middle).
374
+ if size > 2:
375
+ coefs[firstCoefIndex + multiplicity:firstCoefIndex + multiplicity + size - 2] = newCoefs[1:-1]
376
+
377
+ # Insert the inserted coefficients and inserted knots.
378
+ knotsList[ind] = knots = np.insert(knots, position, (knot,) * multiplicity)
379
+
380
+ coefs = coefs.swapaxes(0, ind + 1) # Swap back
333
381
 
334
382
  if self.coefs is coefs:
335
383
  return self
@@ -452,7 +500,7 @@ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
452
500
  foldedIndices = list(filter(lambda x: x != id, indIndex))
453
501
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
454
502
  while True:
455
- bestError = np.finfo(scaleDep[0].dtype).max
503
+ bestError = np.finfo(self.coefs.dtype).max
456
504
  bestSpline = currentFold
457
505
  ix = currentFold.order[0]
458
506
  while ix < currentFold.nCoef[0]:
@@ -518,66 +566,6 @@ def reverse(self, variable = 0):
518
566
  newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
519
567
  return newFolded.unfold(myIndices, basisInfo)
520
568
 
521
- def split(self, minContinuity = 0, breaks = None):
522
- if minContinuity < 0: raise ValueError("minContinuity must be >= 0")
523
- if breaks is not None and len(breaks) != self.nInd: raise ValueError("Invalid breaks")
524
- if self.nInd < 1: return self
525
-
526
- # Step 1: Determine the knots to insert.
527
- newKnotsList = []
528
- for i, order, knots in zip(range(self.nInd), self.order, self.knots):
529
- unique, counts = np.unique(knots, return_counts=True)
530
- newKnots = []
531
- for knot, count in zip(unique, counts):
532
- assert count <= order
533
- if count > order - 1 - minContinuity:
534
- newKnots += [knot] * (order - count)
535
- if breaks is not None:
536
- for knot in breaks[i]:
537
- if knot not in unique:
538
- newKnots += [knot] * order
539
- newKnotsList.append(newKnots)
540
-
541
- # Step 2: Insert the knots.
542
- spline = self.insert_knots(newKnotsList)
543
- if spline is self:
544
- return np.full((1,) * spline.nInd, spline)
545
-
546
- # Step 3: Store the indices of the full order knots.
547
- indexList = []
548
- splineCount = []
549
- totalSplineCount = 1
550
- for order, knots in zip(spline.order, spline.knots):
551
- unique, counts = np.unique(knots, return_counts=True)
552
- indices = np.searchsorted(knots, unique)
553
- fullOrder = []
554
- for ix, count in zip(indices, counts):
555
- if count == order:
556
- fullOrder.append(ix)
557
- indexList.append(fullOrder)
558
- splines = len(fullOrder) - 1
559
- splineCount.append(splines)
560
- totalSplineCount *= splines
561
-
562
- # Step 4: Slice up the spline.
563
- splineArray = np.empty(totalSplineCount, object)
564
- for i in range(totalSplineCount):
565
- knotsList = []
566
- coefIndex = [slice(None)] # First index is for nDep
567
- ix = i
568
- for order, knots, splines, indices in zip(spline.order, spline.knots, splineCount, indexList):
569
- j = ix % splines
570
- ix = ix // splines
571
- leftIndex = indices[j]
572
- rightIndex = indices[j + 1]
573
- knotsList.append(knots[leftIndex:rightIndex + order])
574
- coefIndex.append(slice(leftIndex, rightIndex))
575
- coefs = spline.coefs[tuple(coefIndex)]
576
- splineArray[i] = type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
577
-
578
- # Return the transpose because we put the splines into splineArray dimensions in reverse order.
579
- return splineArray.reshape(tuple(reversed(splineCount))).T
580
-
581
569
  def transpose(self, axes=None):
582
570
  if axes is None:
583
571
  axes = range(self.nInd)[::-1]
@@ -614,17 +602,14 @@ def trim(self, newDomain):
614
602
  if unique[i] - bounds[0] < epsilon:
615
603
  bounds[0] = unique[i]
616
604
  multiplicity = order - counts[i]
617
- if i > 0:
618
- noChange = False
619
605
  elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
620
606
  bounds[0] = unique[i - 1]
621
607
  multiplicity = order - counts[i - 1]
622
- if i - 1 > 0:
623
- noChange = False
624
608
  else:
625
609
  multiplicity = order
626
-
627
- newKnots += multiplicity * [bounds[0]]
610
+ if multiplicity > 0:
611
+ newKnots.append((bounds[0], multiplicity))
612
+ noChange = False
628
613
 
629
614
  if not np.isnan(bounds[1]):
630
615
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
@@ -634,19 +619,16 @@ def trim(self, newDomain):
634
619
  if unique[i] - bounds[1] < epsilon:
635
620
  bounds[1] = unique[i]
636
621
  multiplicity = order - counts[i]
637
- if i < len(unique) - 1:
638
- noChange = False
639
622
  elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
640
623
  bounds[1] = unique[i - 1]
641
624
  multiplicity = order - counts[i - i]
642
- noChange = False # i < len(unique) - 1
643
625
  else:
644
626
  multiplicity = order
645
- newKnots += multiplicity * [bounds[1]]
627
+ if multiplicity > 0:
628
+ newKnots.append((bounds[1], multiplicity))
629
+ noChange = False
646
630
 
647
631
  newKnotsList.append(newKnots)
648
- if len(newKnots) > 0:
649
- noChange = False
650
632
 
651
633
  if noChange:
652
634
  return self
@@ -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
@@ -236,8 +236,8 @@ def normal(self, uvw, normalize=True, indices=None):
236
236
  normal = np.empty(nDep, dtype)
237
237
  else:
238
238
  normal = np.empty(len(indices), dtype)
239
- for i in indices:
240
- normal[i] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
239
+ for ix, i in enumerate(indices):
240
+ normal[ix] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
241
241
 
242
242
  # Normalize the result as needed.
243
243
  if normalize:
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 f in splines[::-1]:
18
- u = f(u)
32
+ for spline, domain in zip(splines[::-1], domains[::-1]):
33
+ u = spline(u)
34
+ if domain is not None:
35
+ # We've already checked that the range of spline is within the domain
36
+ # of its successor, but numerics may cause the spline value to slightly
37
+ # exceed its range, so we clip the spline value accordingly.
38
+ u = np.clip(u, domain[:, 0], domain[:, 1])
19
39
  return u
20
40
 
21
41
  # Approximate this composition
22
- return bspy.Spline.fit(splines[-1].domain(), composition_of_splines, tolerance = tolerance)
42
+ return bspy.Spline.fit(domain, composition_of_splines, tolerance = tolerance)
23
43
 
24
44
  def cone(radius1, radius2, height, tolerance = None):
25
45
  if tolerance is None:
@@ -366,9 +386,10 @@ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
366
386
  indices = nInd * [0]
367
387
  iLast = nInd
368
388
  while iLast >= 0:
389
+ # Create a tuple for the u value (must be a tuple to use it as a dictionary key)
369
390
  uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
370
391
  if not uValue in fDictionary:
371
- fDictionary[uValue] = f(uValue)
392
+ fDictionary[uValue] = f(np.array(uValue))
372
393
  fValues.append(fDictionary[uValue])
373
394
  iLast = nInd - 1
374
395
  while iLast >= 0:
@@ -518,7 +539,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
518
539
 
519
540
  return (1.0 - surfParam) * coons + surfParam * laplace
520
541
 
521
- def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
542
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-5):
522
543
  # Check validity of input
523
544
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
524
545
  if len(uvStart) != 2: raise ValueError("uvStart must have two components")
@@ -616,7 +637,7 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
616
637
  initialGuess = line(uvStart, uvEnd).elevate([2])
617
638
 
618
639
  # Solve the ODE and return the geodesic
619
- solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
640
+ solution = initialGuess.solve_ode(1, 1, geodesicCallback, tolerance, (self, uvDomain))
620
641
  return solution
621
642
 
622
643
  def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
@@ -878,7 +899,7 @@ def section(xytk):
878
899
  # Join the pieces together and return
879
900
  return bspy.Spline.join(mySections)
880
901
 
881
- def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
902
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = (), includeEstimate = False):
882
903
  # Ensure that the ODE is properly formulated
883
904
 
884
905
  if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
@@ -904,11 +925,11 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
904
925
  for i, knot in enumerate(uniqueKnots):
905
926
  howMany = currentGuess.order[0] - indices[i + 1] + indices[i] - nOrder
906
927
  if howMany > 0:
907
- knotsToAdd += howMany * [knot]
928
+ knotsToAdd.append((knot, howMany))
908
929
  if howMany < 0:
909
- knotsToRemove += [[knot, abs(howMany)]]
930
+ knotsToRemove.append((knot, abs(howMany)))
910
931
  currentGuess = currentGuess.insert_knots([knotsToAdd])
911
- for [knot, howMany] in knotsToRemove:
932
+ for (knot, howMany) in knotsToRemove:
912
933
  ix = np.searchsorted(currentGuess.knots[0], knot, side = 'left')
913
934
  for iy in range(howMany):
914
935
  currentGuess, residual = currentGuess.remove_knot(ix, nLeft, nRight)
@@ -970,7 +991,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
970
991
  residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
971
992
  collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
972
993
  for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
973
- uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
994
+ uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder + 1 if includeEstimate else nOrder)]).T
974
995
  F, F_u = FAndF_u(t, uData, *args)
975
996
  residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
976
997
  ix = None
@@ -1065,7 +1086,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
1065
1086
  knotsToAdd = []
1066
1087
  for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
1067
1088
  if knot0 < knot1:
1068
- knotsToAdd += (currentGuess.order[0] - nOrder) * [0.5 * (knot0 + knot1)]
1089
+ knotsToAdd.append((0.5 * (knot0 + knot1), currentGuess.order[0] - nOrder))
1069
1090
  previousGuess = currentGuess
1070
1091
  currentGuess = currentGuess.insert_knots([knotsToAdd])
1071
1092