bspy 2.2.2__py3-none-any.whl → 3.0.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/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- bspy is a python library for manipulating and rendering non-uniform b-splines.
2
+ bspy is a python library for manipulating and rendering non-uniform B-splines.
3
3
 
4
4
  Available subpackages
5
5
  ---------------------
bspy/_spline_domain.py CHANGED
@@ -11,10 +11,11 @@ def clamp(self, left, right):
11
11
 
12
12
  return self.trim(bounds)
13
13
 
14
- def common_basis(self, splines, indMap):
14
+ def common_basis(splines, indMap):
15
15
  # Step 1: Compute the order for each aligned independent variable.
16
16
  orders = []
17
- splines = (self, *splines)
17
+ if indMap is None:
18
+ indMap = [len(splines) * [iInd] for iInd in range(splines[0].nInd)]
18
19
  for map in indMap:
19
20
  if not(len(map) == len(splines)): raise ValueError("Invalid map")
20
21
  order = 0
@@ -161,7 +162,7 @@ def elevate_and_insert_knots(self, m, newKnots):
161
162
  # Set new coefs to the elevated zeroth derivative coefficients with variables swapped back.
162
163
  coefs = coefs[0].swapaxes(0, ind + 1)
163
164
 
164
- return type(self)(self.nInd, self.nDep, order, nCoef, knots, coefs, self.accuracy, self.metadata)
165
+ return type(self)(self.nInd, self.nDep, order, nCoef, knots, coefs, self.metadata)
165
166
 
166
167
  def extrapolate(self, newDomain, continuityOrder):
167
168
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
@@ -268,10 +269,10 @@ def extrapolate(self, newDomain, continuityOrder):
268
269
  # Swap dependent and independent variables back.
269
270
  dCoefs = dCoefs.swapaxes(1, ind + 2)
270
271
 
271
- return type(self)(self.nInd, self.nDep, self.order, nCoef, knots, dCoefs[0], self.accuracy, self.metadata)
272
+ return type(self)(self.nInd, self.nDep, self.order, nCoef, knots, dCoefs[0], self.metadata)
272
273
 
273
274
  def fold(self, foldedInd):
274
- if not(0 <= len(foldedInd) < self.nInd): raise ValueError("Invalid foldedInd")
275
+ if not(0 <= len(foldedInd) <= self.nInd): raise ValueError("Invalid foldedInd")
275
276
  foldedOrder = []
276
277
  foldedNCoef = []
277
278
  foldedKnots = []
@@ -299,8 +300,8 @@ def fold(self, foldedInd):
299
300
  foldedCoefs = np.moveaxis(self.coefs, coefficientMoveFrom, coefficientMoveTo).reshape((foldedNDep, *foldedNCoef))
300
301
  coefficientlessCoefs = np.empty((0, *coefficientlessNCoef), self.coefs.dtype)
301
302
 
302
- foldedSpline = type(self)(len(foldedOrder), foldedNDep, foldedOrder, foldedNCoef, foldedKnots, foldedCoefs, self.accuracy, self.metadata)
303
- coefficientlessSpline = type(self)(len(coefficientlessOrder), 0, coefficientlessOrder, coefficientlessNCoef, coefficientlessKnots, coefficientlessCoefs, self.accuracy, self.metadata)
303
+ foldedSpline = type(self)(len(foldedOrder), foldedNDep, foldedOrder, foldedNCoef, foldedKnots, foldedCoefs, self.metadata)
304
+ coefficientlessSpline = type(self)(len(coefficientlessOrder), 0, coefficientlessOrder, coefficientlessNCoef, coefficientlessKnots, coefficientlessCoefs, self.metadata)
304
305
  return foldedSpline, coefficientlessSpline
305
306
 
306
307
  def insert_knots(self, newKnots):
@@ -327,7 +328,7 @@ def insert_knots(self, newKnots):
327
328
  if self.coefs is coefs:
328
329
  return self
329
330
  else:
330
- return type(self)(self.nInd, self.nDep, self.order, coefs.shape[1:], knots, coefs, self.accuracy, self.metadata)
331
+ return type(self)(self.nInd, self.nDep, self.order, coefs.shape[1:], knots, coefs, self.metadata)
331
332
 
332
333
  def join(splineList):
333
334
  # Make sure all the splines in the list are curves
@@ -364,7 +365,7 @@ def join(splineList):
364
365
  newKnots = [list(workingSpline.knots[0]) + list(spl.knots[0][maxOrder:])]
365
366
  newCoefs = [list(workingCoefs) + list(splCoefs) for workingCoefs, splCoefs in zip(workingSpline.coefs, spl.coefs)]
366
367
  workingSpline = type(workingSpline)(1, numDep, workingSpline.order, [workingSpline.nCoef[0] + spl.nCoef[0]],
367
- newKnots, newCoefs, max(workingSpline.accuracy, spl.accuracy), workingSpline.metadata)
368
+ newKnots, newCoefs, workingSpline.metadata)
368
369
  return workingSpline.reparametrize([[0.0, 1.0]]).remove_knots()
369
370
 
370
371
  def remove_knot(self, iKnot):
@@ -377,7 +378,7 @@ def remove_knot(self, iKnot):
377
378
  myKnots = self.knots[0]
378
379
  thisKnot = myKnots[iKnot]
379
380
 
380
- # Form the bidiagonal system
381
+ # Form the bi-diagonal system
381
382
  for ix in range(1, myOrder):
382
383
  alpha = (myKnots[iKnot + ix] - thisKnot) / (myKnots[iKnot + ix] - myKnots[iKnot + ix - myOrder])
383
384
  diag0.append(alpha)
@@ -470,7 +471,7 @@ def reparametrize(self, newDomain):
470
471
  for i in range(1-order, 0):
471
472
  knots[i] = max(knots[i], nD[1])
472
473
  knotList.append(knots)
473
- return type(self)(self.nInd, self.nDep, self.order, self.nCoef, knotList, self.coefs, self.accuracy, self.metadata)
474
+ return type(self)(self.nInd, self.nDep, self.order, self.nCoef, knotList, self.coefs, self.metadata)
474
475
 
475
476
  def reverse(self, variable = 0):
476
477
  # Check to make sure variable is in range
@@ -488,8 +489,8 @@ def reverse(self, variable = 0):
488
489
  newCoefs = []
489
490
  for iDep in range(folded.nDep):
490
491
  newCoefs.append(self.coefs[iDep][::-1])
491
- newfolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.accuracy, folded.metadata)
492
- return newfolded.unfold(myIndices, basisInfo)
492
+ newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
493
+ return newFolded.unfold(myIndices, basisInfo)
493
494
 
494
495
  def transpose(self, axes=None):
495
496
  if axes is None:
@@ -503,7 +504,7 @@ def transpose(self, axes=None):
503
504
  nCoef.append(self.nCoef[axis])
504
505
  knots.append(self.knots[axis])
505
506
  coefAxes.append(axis + 1)
506
- return type(self)(self.nInd, self.nDep, order, nCoef, knots, np.transpose(self.coefs, coefAxes), self.accuracy, self.metadata)
507
+ return type(self)(self.nInd, self.nDep, order, nCoef, knots, np.transpose(self.coefs, coefAxes), self.metadata)
507
508
 
508
509
  def trim(self, newDomain):
509
510
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
@@ -552,7 +553,7 @@ def trim(self, newDomain):
552
553
  coefIndex.append(slice(leftIndex, rightIndex))
553
554
  coefs = spline.coefs[tuple(coefIndex)]
554
555
 
555
- return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.accuracy, spline.metadata)
556
+ return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
556
557
 
557
558
  def unfold(self, foldedInd, coefficientlessSpline):
558
559
  if not(len(foldedInd) == coefficientlessSpline.nInd): raise ValueError("Invalid coefficientlessSpline")
@@ -582,5 +583,5 @@ def unfold(self, foldedInd, coefficientlessSpline):
582
583
  coefficientMoveFrom = range(1, coefficientlessSpline.nInd + 1)
583
584
  unfoldedCoefs = np.moveaxis(self.coefs.reshape(unfoldedNDep, *coefficientlessSpline.nCoef, *self.nCoef), coefficientMoveFrom, coefficientMoveTo)
584
585
 
585
- unfoldedSpline = type(self)(len(unfoldedOrder), unfoldedNDep, unfoldedOrder, unfoldedNCoef, unfoldedKnots, unfoldedCoefs, self.accuracy, self.metadata)
586
+ unfoldedSpline = type(self)(len(unfoldedOrder), unfoldedNDep, unfoldedOrder, unfoldedNCoef, unfoldedKnots, unfoldedCoefs, self.metadata)
586
587
  return unfoldedSpline
@@ -62,19 +62,23 @@ def curvature(self, uv):
62
62
  self = self.graph()
63
63
  fp = self.derivative([1], uv)
64
64
  fpp = self.derivative([2], uv)
65
- fpdotfp = fp @ fp
66
- fpdotfpp = fp @ fpp
67
- denom = fpdotfp ** 1.5
65
+ fpDotFp = fp @ fp
66
+ fpDotFpp = fp @ fpp
67
+ denom = fpDotFp ** 1.5
68
68
  if self.nDep == 2:
69
- numer = fp[0] * fpp[1] - fp[1] * fpp[0]
69
+ numerator = fp[0] * fpp[1] - fp[1] * fpp[0]
70
70
  else:
71
- numer = np.sqrt((fpp @ fpp) * fpdotfp - fpdotfpp ** 2)
72
- return numer / denom
71
+ numerator = np.sqrt((fpp @ fpp) * fpDotFp - fpDotFpp ** 2)
72
+ return numerator / denom
73
73
 
74
74
  def derivative(self, with_respect_to, uvw):
75
75
  # Make work for scalar valued functions
76
76
  uvw = np.atleast_1d(uvw)
77
77
 
78
+ # Check for the correct number of independent variables
79
+ if len(uvw) != self.nInd:
80
+ raise ValueError(f"Incorrect number of parameter values: {len(uvw)}")
81
+
78
82
  # Check for evaluation point inside domain
79
83
  dom = self.domain()
80
84
  for ix in range(self.nInd):
@@ -104,6 +108,10 @@ def evaluate(self, uvw):
104
108
  # Make work for scalar valued functions
105
109
  uvw = np.atleast_1d(uvw)
106
110
 
111
+ # Check for the correct number of independent variables
112
+ if len(uvw) != self.nInd:
113
+ raise ValueError(f"Incorrect number of parameter values: {len(uvw)}")
114
+
107
115
  # Check for evaluation point inside domain
108
116
  dom = self.domain()
109
117
  for ix in range(self.nInd):
bspy/_spline_fitting.py CHANGED
@@ -10,6 +10,17 @@ def circular_arc(radius, angle, tolerance = None):
10
10
  samples = int(max(np.ceil(((1.1536e-5 * radius / tolerance)**(1/8)) * angle / 90), 2.0)) + 1
11
11
  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)])
12
12
 
13
+ def cone(radius1, radius2, height, tolerance = None):
14
+ if tolerance is None:
15
+ tolerance = 1.0e-12
16
+ bigRadius = max(radius1, radius2)
17
+ radius1 /= bigRadius
18
+ radius2 /= bigRadius
19
+ bottom = [[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]] @ bspy.Spline.circular_arc(bigRadius, 360.0, tolerance)
20
+ top = radius2 * bottom + [0.0, 0.0, height]
21
+ bottom = radius1 * bottom
22
+ return bspy.Spline.ruled_surface(bottom, top)
23
+
13
24
  # Courtesy of Michael Epton - Translated from his F77 code lgnzro
14
25
  def _legendre_polynomial_zeros(degree):
15
26
  def legendre(degree, x):
@@ -271,11 +282,18 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
271
282
 
272
283
  # Rescale x(t) back to original data points.
273
284
  coefs = (coefsMin + coefs.T * coefsMaxMinusMin).T
274
- spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, epsilon, metadata)
285
+ spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
275
286
  if isinstance(F, bspy.Spline):
276
287
  spline = spline.confine(F.domain())
277
288
  return spline
278
289
 
290
+ def cylinder(radius, height, tolerance = None):
291
+ if tolerance is None:
292
+ tolerance = 1.0e-12
293
+ bottom = [[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]] @ bspy.Spline.circular_arc(radius, 360.0, tolerance)
294
+ top = bottom + [0.0, 0.0, height]
295
+ return bspy.Spline.ruled_surface(bottom, top)
296
+
279
297
  def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
280
298
  if bottom.nInd != 1 or right.nInd != 1 or top.nInd != 1 or left.nInd != 1:
281
299
  raise ValueError("Input curves must have one independent variable")
@@ -328,7 +346,7 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
328
346
  biLinear = bspy.Spline.ruled_surface(bottomLine, topLine)
329
347
  coons = bottomTop.add(leftRight, ((0,1), (1,0))) - biLinear
330
348
 
331
- # Determine the Greville absiccae to use as collocation points
349
+ # Determine the Greville abscissae to use as collocation points
332
350
 
333
351
  uPts = coons.greville(0)[1 : -1]
334
352
  vPts = coons.greville(1)[1 : -1]
@@ -365,143 +383,153 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
365
383
 
366
384
  return (1.0 - surfParam) * coons + surfParam * laplace
367
385
 
368
- def least_squares(nInd, nDep, order, dataPoints, knotList = None, compression = 0, metadata = {}):
369
- if not(nInd >= 0): raise ValueError("nInd < 0")
370
- if not(nDep >= 0): raise ValueError("nDep < 0")
371
- if not(len(order) == nInd): raise ValueError("len(order) != nInd")
372
- if not(0 <= compression < 100): raise ValueError("compression not between 0 and 99")
373
- totalOrder = 1
374
- for ord in order:
375
- totalOrder *= ord
376
-
377
- totalDataPoints = len(dataPoints)
378
- for point in dataPoints:
379
- if not(len(point) == nInd + nDep or len(point) == nInd + nDep * (nInd + 1)): raise ValueError(f"Data points do not have {nInd + nDep} values")
380
- if len(point) == nInd + nDep * (nInd + 1):
381
- totalDataPoints += nInd
382
-
383
- if knotList is None:
384
- # Compute the target number of coefficients and the actual number of samples in each independent variable.
385
- targetTotalCoef = len(dataPoints) * (100 - compression) / 100.0
386
- totalCoef = 1
387
- knotSamples = np.array([point[:nInd] for point in dataPoints], type(dataPoints[0][0])).T
388
- knotList = []
389
- for knotSample in knotSamples:
390
- knots = np.unique(knotSample)
391
- knotList.append(knots)
392
- totalCoef *= len(knots)
393
-
394
- # Scale the number of coefficients for each independent variable so that the total closely matches the target.
395
- scaling = min((targetTotalCoef / totalCoef) ** (1.0 / nInd), 1.0)
396
- nCoef = []
397
- totalCoef = 1
398
- for knots in knotList:
399
- nCf = int(math.ceil(len(knots) * scaling))
400
- nCoef.append(nCf)
401
- totalCoef *= nCf
402
-
403
- # Compute "ideal" knots for each independent variable, based on the number of coefficients and the sample values.
404
- # Piegl, Les A., and Wayne Tiller. "Surface approximation to scanned data." The visual computer 16 (2000): 386-395.
405
- newKnotList = []
406
- for iInd, ord, nCf, knots in zip(range(nInd), order, nCoef, knotList):
407
- degree = ord - 1
408
- newKnots = [knots[0]] * ord
409
- inc = len(knots)/nCf
410
- low = 0
411
- d = -1
412
- w = np.empty((nCf,), float)
413
- for i in range(nCf):
414
- d += inc
415
- high = int(d + 0.5 + 1) # Paper's algorithm sets high to d + 0.5, but only references high + 1
416
- w[i] = np.mean(knots[low:high])
417
- low = high
418
- for i in range(1, nCf - degree):
419
- newKnots.append(np.mean(w[i:i + degree]))
420
- newKnots += [knots[-1]] * ord
421
- newKnotList.append(np.array(newKnots, knots.dtype))
422
- knotList = newKnotList
386
+ def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
387
+ tolerance = None, fixEnds = False, metadata = {}):
388
+
389
+ # Preprocess all the input if everything is a spline
390
+
391
+ dataPoints = np.array(dataPoints)
392
+ flatView = np.ravel(dataPoints)
393
+ splineInput = False
394
+ if type(flatView[0]) is bspy.Spline:
395
+ splineInput = True
396
+ nInd = flatView[0].nInd
397
+ nDep = flatView[0].nDep
398
+ splineDomain = flatView[0].domain()
399
+ for spline in flatView:
400
+ if spline.nInd != nInd: raise ValueError("Input splines have different number of independent variables")
401
+ if spline.nDep != nDep: raise ValueError("Input splines have different number of dependent variables")
402
+ if not np.array_equal(spline.domain(), splineDomain): raise ValueError("Input splines have different domains")
403
+ commonSplines = bspy.Spline.common_basis(flatView)
404
+ foldedData = []
405
+ for spline in commonSplines:
406
+ folded, unfoldInfo = spline.fold(list(range(nInd)))
407
+ foldedData.append(folded())
408
+ foldedData = np.array(foldedData).T
409
+ dataPoints = np.reshape(foldedData, (foldedData.shape[0],) + dataPoints.shape)
410
+
411
+ # Preprocess the parameters values for the points
412
+
413
+ if np.isscalar(uValues[0]):
414
+ nInd = 1
415
+ uValues = [uValues]
423
416
  else:
424
- if not(len(knotList) == nInd): raise ValueError("len(knots) != nInd") # The documented interface uses the argument 'knots' instead of 'knotList'
425
- nCoef = [len(knotList[i]) - order[i] for i in range(nInd)]
426
- totalCoef = 1
427
- newKnotList = []
428
- for knots, ord, nCf in zip(knotList, order, nCoef):
429
- for i in range(nCf):
430
- if not(knots[i] <= knots[i + 1] and knots[i] < knots[i + ord]): raise ValueError("Improperly ordered knot sequence")
431
- totalCoef *= nCf
432
- newKnotList.append(np.array(knots))
433
- if not(totalCoef <= totalDataPoints): raise ValueError(f"Insufficient number of data points. You need at least {totalCoef}.")
434
- knotList = newKnotList
417
+ nInd = len(uValues)
418
+ domain = []
419
+ for iInd in range(nInd):
420
+ uMin = np.min(uValues[iInd])
421
+ uMax = np.max(uValues[iInd])
422
+ domain.append([uMin, uMax])
423
+ for i in range(len(uValues[iInd]) - 1):
424
+ if uValues[iInd][i] > uValues[iInd][i + 1]: raise ValueError("Independent variable values are out of order")
435
425
 
436
- # Initialize A and b from the likely overdetermined equation, A x = b, where A contains the bspline values at the independent variables,
437
- # b contains point values for the dependent variables, and the x contains the desired coefficients.
438
- A = np.zeros((totalDataPoints, totalCoef), type(dataPoints[0][0]))
439
- b = np.empty((totalDataPoints, nDep), A.dtype)
440
-
441
- # Fill in the bspline values in A and the dependent point values in b at row at a time.
442
- # Note that if a data point also specifies first derivatives, it fills out nInd + 1 rows (the point and its derivatives).
443
- row = 0
444
- for point in dataPoints:
445
- hasDerivatives = len(point) == nInd + nDep * (nInd + 1)
446
-
447
- # Compute the bspline values (and their first derivatives as needed).
448
- bValueData = []
449
- for knots, ord, nCf, u in zip(knotList, order, nCoef, point[:nInd]):
450
- ix = np.searchsorted(knots, u, 'right')
451
- ix = min(ix, nCf)
452
- bValueData.append((ix, bspy.Spline.bspline_values(ix, knots, ord, u), \
453
- bspy.Spline.bspline_values(ix, knots, ord, u, 1) if hasDerivatives else None))
454
-
455
- # Compute the values for the A array.
456
- # It's a little tricky because we have to multiply nInd different bspline arrays of different sizes
457
- # and index into flattened A array. The solution is to loop through the total number of entries
458
- # being changed (totalOrder), and compute the array indices via mods and multiplies.
459
- indices = [0] * nInd
460
- for i in range(totalOrder):
461
- column = 0
462
- bValues = np.ones((nInd + 1,), A.dtype)
463
- for j, ord, nCf, index, (ix, values, dValues) in zip(range(1, nInd + 1), order, nCoef, indices, bValueData):
464
- column = column * nCf + ix - ord + index
465
- # Compute the bspline value for this specific element of A.
466
- bValues[0] *= values[index]
467
- if hasDerivatives:
468
- # Compute the first derivative values for each independent variable.
469
- for k in range(1, nInd + 1):
470
- bValues[k] *= dValues[index] if k == j else values[index]
471
-
472
- # Assign all the values and derivatives.
473
- A[row, column] = bValues[0]
474
- if hasDerivatives:
475
- for k in range(1, nInd + 1):
476
- A[row + k, column] = bValues[k]
477
-
478
- # Increment the bspline indices.
479
- for j in range(nInd - 1, -1, -1):
480
- indices[j] = (indices[j] + 1) % order[j]
481
- if indices[j] > 0:
482
- break
426
+ # Preprocess the data points
483
427
 
484
- # Assign values for the b array.
485
- b[row, :] = point[nInd:nInd + nDep]
486
- if hasDerivatives:
487
- for k in range(1, nInd + 1):
488
- b[row + k, :] = point[nInd + nDep * k:nInd + nDep * (k + 1)]
428
+ if len(dataPoints.shape) != nInd + 1: raise ValueError("dataPoints has the wrong shape")
429
+ nDep = dataPoints.shape[0]
430
+ pointsPerDirection = list(dataPoints.shape)[1:]
431
+ nPoints = 1
432
+ for i, nu in enumerate(pointsPerDirection):
433
+ if nu != len(uValues[i]): raise ValueError("Wrong number of parameter values in one or more directions")
434
+ nPoints *= nu
489
435
 
490
- # Increment the row before filling in the next data point
491
- row += nInd + 1 if hasDerivatives else 1
492
-
493
- # Yay, the A and b arrays are ready to solve.
494
- # Now, we call numpy's least squares solver.
495
- coefs, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
436
+ # Make sure the order makes sense
437
+
438
+ if order is None:
439
+ order = [min(4, nu) for nu in pointsPerDirection]
440
+ for nP, nOrder in zip(pointsPerDirection, order):
441
+ if nP < nOrder: raise ValueError("Not enough points in one or more directions")
442
+
443
+ # Determine the (initial) knots array
496
444
 
497
- # Reshape the coefs array to match nCoef (un-flatten) and move the dependent variables to the front.
498
- coefs = np.moveaxis(coefs.reshape((*nCoef, nDep)), -1, 0)
445
+ if not(0.0 <= compression <= 1.0): raise ValueError("compression not between 0.0 and 1.0")
446
+ if tolerance is None:
447
+ tolerance = np.finfo(float).max
448
+ else:
449
+ compression = 1.0
450
+ if knots is None:
451
+ knots = [np.array(order[iInd] * [domain[iInd][0]] + order[iInd] * [domain[iInd][1]]) for iInd in range(nInd)]
452
+ for iInd, (nP, nOrder) in enumerate(zip(pointsPerDirection, order)):
453
+ knotsToAdd = int((nP - nOrder) * (1.0 - compression) + 0.9999999999)
454
+ addSpots = np.linspace(0.0, nP - 1.0, knotsToAdd + 2)[1 : -1]
455
+ newKnots = []
456
+ for newSpot in addSpots:
457
+ ix = int(newSpot)
458
+ alpha = newSpot - ix
459
+ newKnots.append((1.0 - alpha) * uValues[iInd][ix] + alpha * uValues[iInd][ix + 1])
460
+ knots[iInd] = np.sort(np.append(knots[iInd], newKnots))
461
+ else:
462
+ knots = tuple(np.array(kk) for kk in knots)
463
+ for iInd in range(nInd):
464
+ if domain[iInd][0] < knots[iInd][order[iInd] - 1] or \
465
+ domain[iInd][1] > knots[iInd][-order[iInd]]: raise ValueError("One or more dataPoints are outside the domain of the spline")
499
466
 
500
- # Return the resulting spline, computing the accuracy based on system epsilon and the norm of the residuals.
501
- maxError = np.finfo(coefs.dtype).eps
502
- if residuals.size > 0:
503
- maxError = max(maxError, residuals.sum())
504
- return bspy.Spline(nInd, nDep, order, nCoef, knotList, coefs, np.sqrt(maxError), metadata)
467
+ # Loop through each independent variable and fit all of the data
468
+
469
+ for iInd in range(nInd):
470
+ nRows = pointsPerDirection[iInd]
471
+ b = np.swapaxes(dataPoints, 0, iInd + 1)
472
+ loopDep = nDep * nPoints // nRows
473
+ b = np.reshape(b, (nRows, loopDep))
474
+ done = False
475
+ while not done:
476
+ nCols = len(knots[iInd]) - order[iInd]
477
+ A = np.zeros((nRows, nCols))
478
+ u = -np.finfo(float).max
479
+ fixedRows = []
480
+ for iRow in range(nRows):
481
+ uNew = uValues[iInd][iRow]
482
+ if uNew != u:
483
+ iDerivative = 0
484
+ u = uNew
485
+ ix = np.searchsorted(knots[iInd], u, 'right')
486
+ ix = min(ix, nCols)
487
+ else:
488
+ iDerivative += 1
489
+ row = bspy.Spline.bspline_values(ix, knots[iInd], order[iInd], u, iDerivative)
490
+ A[iRow, ix - order[iInd] : ix] = row
491
+ if fixEnds and (u == uValues[iInd][0] or u == uValues[iInd][-1]):
492
+ fixedRows.append(iRow)
493
+ nInterpolationConditions = len(fixedRows)
494
+ if nInterpolationConditions != 0:
495
+ C = np.take(A, fixedRows, 0)
496
+ d = np.take(b, fixedRows, 0)
497
+ AUse = np.delete(A, fixedRows, 0)
498
+ bUse = np.delete(b, fixedRows, 0)
499
+ U, Sigma, VT = np.linalg.svd(C)
500
+ d = U.T @ d
501
+ for iRow in range(nInterpolationConditions):
502
+ d[iRow] = d[iRow] / Sigma[iRow]
503
+ rangeCols = np.take(VT.T, range(nInterpolationConditions), 1)
504
+ nullspace = np.delete(VT.T, range(nInterpolationConditions), 1)
505
+ x1 = rangeCols @ d
506
+ b1 = bUse - AUse @ x1
507
+ xNullspace, residuals, rank, s = np.linalg.lstsq(AUse @ nullspace, b1, rcond = None)
508
+ x = x1 + nullspace @ xNullspace
509
+ else:
510
+ x, residuals, rank, s = np.linalg.lstsq(A, b, rcond = None)
511
+ residuals = b - A @ x
512
+ maxError = 0.0
513
+ for iRow in range(nRows):
514
+ rowError = np.linalg.norm(residuals[iRow, :])
515
+ if rowError > maxError:
516
+ maxError = rowError
517
+ maxRow = iRow
518
+ if maxError <= tolerance / nInd:
519
+ done = True
520
+ else:
521
+ ix = np.searchsorted(knots[iInd], uValues[iInd][maxRow], 'right')
522
+ ix = min(ix, nCols)
523
+ knots[iInd] = np.sort(np.append(knots[iInd], 0.5 * (knots[iInd][ix - 1] + knots[iInd][ix])))
524
+ pointsPerDirection[iInd] = nDep
525
+ x = np.reshape(x, [nCols] + pointsPerDirection)
526
+ dataPoints = np.swapaxes(x, 0, iInd + 1)
527
+ pointsPerDirection[iInd] = nCols
528
+ nPoints = nCols * nPoints // nRows
529
+ splineFit = bspy.Spline(nInd, nDep, order, pointsPerDirection, knots, dataPoints, metadata = metadata)
530
+ if splineInput:
531
+ splineFit = splineFit.unfold(range(unfoldInfo.nInd), unfoldInfo)
532
+ return splineFit
505
533
 
506
534
  def line(startPoint, endPoint):
507
535
  startPoint = bspy.Spline.point(startPoint)
@@ -528,14 +556,15 @@ def ruled_surface(curve1, curve2):
528
556
  # Ensure that the splines are compatible
529
557
  if curve1.nInd != curve2.nInd: raise ValueError("Splines must have the same number of independent variables")
530
558
  if curve1.nDep != curve2.nDep: raise ValueError("Splines must have the same number of dependent variables")
531
- indMap = [(ix, ix) for ix in range(curve1.nInd)]
532
- [newCurve1, newCurve2] = curve1.common_basis([curve2], indMap)
559
+ [newCurve1, newCurve2] = bspy.Spline.common_basis([curve1, curve2])
533
560
 
534
561
  # Generate the ruled spline between them
562
+ myCoefs1 = np.reshape(newCurve1.coefs, newCurve1.coefs.shape + (1,))
563
+ myCoefs2 = np.reshape(newCurve2.coefs, newCurve2.coefs.shape + (1,))
564
+ newCoefs = np.append(myCoefs1, myCoefs2, newCurve1.nInd + 1)
535
565
  return bspy.Spline(curve1.nInd + 1, curve1.nDep, list(newCurve1.order) + [2],
536
566
  list(newCurve1.nCoef) + [2], list(newCurve1.knots) + [[0.0, 0.0, 1.0, 1.0]],
537
- [np.array([coef1, coef2]).T for coef1, coef2 in zip(newCurve1.coefs, newCurve2.coefs)],
538
- accuracy = max(newCurve1.accuracy, newCurve2.accuracy))
567
+ newCoefs)
539
568
 
540
569
  def section(xytk):
541
570
  def twoPointSection(startPointX, startPointY, startAngle, startKappa, endPointX, endPointY, endAngle, endKappa):
@@ -522,9 +522,9 @@ def contours(self):
522
522
  continue # Try a different theta
523
523
 
524
524
  # Find turning points by combining self and turningPointDeterminant into a system and processing its zeros.
525
- systemSelf, systemTurningPointDeterminant = self.common_basis((turningPointDeterminant,), [(nInd, nInd) for nInd in range(self.nInd)])
525
+ systemSelf, systemTurningPointDeterminant = bspy.Spline.common_basis((self, turningPointDeterminant))
526
526
  system = type(systemSelf)(self.nInd, self.nInd, systemSelf.order, systemSelf.nCoef, systemSelf.knots, \
527
- np.concatenate((systemSelf.coefs, systemTurningPointDeterminant.coefs)), systemSelf.accuracy, systemSelf.metadata)
527
+ np.concatenate((systemSelf.coefs, systemTurningPointDeterminant.coefs)), systemSelf.metadata)
528
528
  zeros = system.zeros()
529
529
  for uvw in zeros:
530
530
  if isinstance(uvw, tuple):
@@ -566,7 +566,7 @@ def contours(self):
566
566
  degree = self.order[1] - 1
567
567
  for i in range(1, self.nCoef[1]):
568
568
  panelCoefs[self.nDep, :, i] = panelCoefs[self.nDep, :, i - 1] + ((self.knots[1][degree + i] - self.knots[1][i]) / degree) * sinTheta
569
- panel = type(self)(self.nInd, self.nInd, self.order, self.nCoef, self.knots, panelCoefs, self.accuracy, self.metadata)
569
+ panel = type(self)(self.nInd, self.nInd, self.order, self.nCoef, self.knots, panelCoefs, self.metadata)
570
570
 
571
571
  # Okay, we have everything we need to determine the contour topology and points along each contour.
572
572
  # We've done the first two steps of Grandine and Klein's algorithm: