bspy 4.1__py3-none-any.whl → 4.3__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_fitting.py CHANGED
@@ -11,6 +11,16 @@ def circular_arc(radius, angle, tolerance = None):
11
11
  samples = int(max(np.ceil(((1.1536e-5 * radius / tolerance)**(1/8)) * angle / 90), 2.0)) + 1
12
12
  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
13
 
14
+ def composition(splines, tolerance):
15
+ # Define the callback function
16
+ def composition_of_splines(u):
17
+ for f in splines[::-1]:
18
+ u = f(u)
19
+ return u
20
+
21
+ # Approximate this composition
22
+ return bspy.Spline.fit(splines[-1].domain(), composition_of_splines, tolerance = tolerance)
23
+
14
24
  def cone(radius1, radius2, height, tolerance = None):
15
25
  if tolerance is None:
16
26
  tolerance = 1.0e-12
@@ -88,48 +98,57 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
88
98
  FValues = F(knownXValue)
89
99
  if not(len(FValues) == nDep - 1 and np.linalg.norm(FValues) < evaluationEpsilon):
90
100
  raise ValueError(f"F(known x) must be a zero vector of length {nDep - 1}.")
91
- coefsMin = knownXValues.min(axis=0)
92
- coefsMaxMinusMin = knownXValues.max(axis=0) - coefsMin
93
- coefsMaxMinusMin = np.where(coefsMaxMinusMin < 1.0, 1.0, coefsMaxMinusMin)
101
+
102
+ # Record domain of F and scaling of coefficients.
103
+ if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
104
+ FDomain = F.domain().T
105
+ coefsMin = FDomain[0]
106
+ coefsMaxMinusMin = FDomain[1] - FDomain[0]
107
+ else:
108
+ FDomain = np.array(nDep * [[-np.inf, np.inf]]).T
109
+ coefsMin = knownXValues.min(axis=0)
110
+ coefsMaxMinusMin = knownXValues.max(axis=0) - coefsMin
111
+ coefsMaxMinusMin = np.where(coefsMaxMinusMin < 1.0, 1.0, coefsMaxMinusMin)
112
+
113
+ # Rescale known values.
94
114
  coefsMaxMinMinReciprocal = np.reciprocal(coefsMaxMinusMin)
95
115
  knownXValues = (knownXValues - coefsMin) * coefsMaxMinMinReciprocal # Rescale to [0 , 1]
96
116
 
97
- # Establish the first derivatives of F.
117
+ # Establish the Jacobian of F.
98
118
  if dF is None:
99
- dF = []
100
- if isinstance(F, bspy.Spline):
101
- for i in range(nDep):
102
- def splineDerivative(x, i=i):
103
- wrt = [0] * nDep
104
- wrt[i] = 1
105
- return F.derivative(wrt, x)
106
- dF.append(splineDerivative)
107
- FDomain = F.domain().T
119
+ if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
120
+ dF = F.jacobian
108
121
  else:
109
- for i in range(nDep):
110
- def fDerivative(x, i=i):
122
+ def fJacobian(x):
123
+ value = np.empty((nDep - 1, nDep), float)
124
+ for i in range(nDep):
111
125
  h = epsilon * (1.0 + abs(x[i]))
112
126
  xShift = np.array(x, copy=True)
113
127
  xShift[i] -= h
114
128
  fLeft = np.array(F(xShift))
115
129
  h2 = h * 2.0
116
130
  xShift[i] += h2
117
- return (np.array(F(xShift)) - fLeft) / h2
118
- dF.append(fDerivative)
119
- FDomain = np.array(nDep * [[-np.inf, np.inf]]).T
120
- else:
131
+ value[:, i] = (np.array(F(xShift)) - fLeft) / h2
132
+ return value
133
+ dF = fJacobian
134
+ elif not callable(dF):
121
135
  if not(len(dF) == nDep): raise ValueError(f"Must provide {nDep} first derivatives.")
136
+ def fJacobian(x):
137
+ value = np.empty((nDep - 1, nDep), float)
138
+ for i in range(nDep):
139
+ value[:, i] = dF[i]
140
+ return value
141
+ dF = fJacobian
122
142
 
123
143
  # Construct knots, t values, and GSamples.
124
144
  tValues = np.empty(nUnknownCoefs, contourDtype)
125
145
  GSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
126
- t = 0.0 # We start with t measuring contour length.
146
+ t = 0.0 # t ranges from 0 to 1
147
+ dt = 1.0 / m
127
148
  knots = [t] * order
128
149
  i = 0
129
150
  previousPoint = knownXValues[0]
130
151
  for point in knownXValues[1:]:
131
- dt = np.linalg.norm(point - previousPoint)
132
- if not(dt > epsilon): raise ValueError("Points must be separated by at least epsilon.")
133
152
  for gaussNode in gaussNodes:
134
153
  tValues[i] = t + gaussNode * dt
135
154
  GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
@@ -138,8 +157,8 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
138
157
  knots += [t] * (order - 2)
139
158
  previousPoint = point
140
159
  knots += [t] * 2 # Clamp last knot
141
- knots = np.array(knots, contourDtype) / t # Rescale knots
142
- tValues /= t # Rescale t values
160
+ knots = np.array(knots, contourDtype)
161
+ knots[nCoef:] = 1.0 # Ensure last knot is exactly 1.0
143
162
  assert i == nUnknownCoefs
144
163
 
145
164
  # Start subdivision loop.
@@ -167,8 +186,6 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
167
186
  # Array to hold the Jacobian of the FSamples with respect to the coefficients.
168
187
  # The Jacobian is banded due to B-spline local support, so initialize it to zero.
169
188
  dFCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
170
- # Working array to hold the transpose of the Jacobian of F for a particular x(t).
171
- dFX = np.empty((nDep, nDep - 1), contourDtype)
172
189
 
173
190
  # Start Newton's method loop.
174
191
  previousFSamplesNorm = 0.0
@@ -201,9 +218,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
201
218
  FSamples[i, -1] = dotValues
202
219
 
203
220
  # Compute the Jacobian of FSamples with respect to the coefficients of x(t).
204
- for j in range(nDep):
205
- dFX[j] = dF[j](x) * coefsMaxMinusMin[j]
206
- FValues = np.outer(dFX.T, bValues).reshape(nDep - 1, nDep, order).swapaxes(1, 2)
221
+ FValues = np.outer(dF(x) * coefsMaxMinusMin, bValues).reshape(nDep - 1, nDep, order).swapaxes(1, 2)
207
222
  dotValues = (np.outer(d2Values, compactCoefs.T @ dValues) + np.outer(dValues, compactCoefs.T @ d2Values)).reshape(order, nDep)
208
223
  dFCoefs[i, :-1, ix - order:ix, :] = FValues
209
224
  dFCoefs[i, -1, ix - order:ix, :] = dotValues
@@ -274,7 +289,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
274
289
  previousKnot = knot
275
290
 
276
291
  # Test if F(GSamples) is close enough to zero.
277
- if FSamplesNorm / np.linalg.norm(dHCoefs, np.inf) < epsilon:
292
+ if FSamplesNorm < evaluationEpsilon:
278
293
  break # We're done! Exit subdivision loop and return x(t).
279
294
 
280
295
  # Otherwise, update nCoef and knots array, and then re-run Newton's method.
@@ -287,7 +302,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
287
302
  # Rescale x(t) back to original data points.
288
303
  coefs = (coefsMin + coefs * coefsMaxMinusMin).T
289
304
  spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
290
- if isinstance(F, bspy.Spline):
305
+ if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
291
306
  spline = spline.confine(F.domain())
292
307
  return spline
293
308
 
@@ -298,6 +313,122 @@ def cylinder(radius, height, tolerance = None):
298
313
  top = bottom + [0.0, 0.0, height]
299
314
  return bspy.Spline.ruled_surface(bottom, top)
300
315
 
316
+ def fit(domain, f, order = None, knots = None, tolerance = 1.0e-4):
317
+ # Determine number of independent variables
318
+ domain = np.array(domain)
319
+ nInd = len(domain)
320
+ midPoint = f(0.5 * (domain.T[0] + domain.T[1]))
321
+ if not type(midPoint) is bspy.Spline:
322
+ nDep = len(midPoint)
323
+
324
+ # Make sure order and knots conform to this
325
+ if order is None:
326
+ order = nInd * [4]
327
+ if len(order) != nInd:
328
+ raise ValueError("Inconsistent number of independent variables")
329
+
330
+ # Establish the initial knot sequence
331
+ if knots is None:
332
+ knots = np.array([order[iInd] * [domain[iInd, 0]] + order[iInd] * [domain[iInd, 1]] for iInd in range(nInd)])
333
+
334
+ # Determine initial nCoef
335
+ nCoef = [len(knotVector) - iOrder for iOrder, knotVector in zip(order, knots)]
336
+
337
+ # Define function to insert midpoints
338
+ def addMidPoints(u):
339
+ newArray = np.empty([2, len(u)])
340
+ newArray[0] = u
341
+ newArray[1, :-1] = 0.5 * (u[1:] + u[:-1])
342
+ return newArray.T.flatten()[:-1]
343
+
344
+ # Track the current spline space we're fitting in
345
+ currentSpace = bspy.Spline(nInd, 0, order, nCoef, knots, [])
346
+
347
+ # Generate the Greville points for these knots
348
+ uvw = [currentSpace.greville(iInd) for iInd in range(nInd)]
349
+
350
+ # Enrich the sample points
351
+ for iInd in range(nInd):
352
+ uvw[iInd][0] = knots[iInd][order[iInd] - 1]
353
+ uvw[iInd][-1] = knots[iInd][nCoef[iInd]]
354
+ for iLevel in range(1):
355
+ uvw[iInd] = addMidPoints(uvw[iInd])
356
+
357
+ # Initialize the dictionary of function values
358
+
359
+ fDictionary = {}
360
+
361
+ # Keep looping until done
362
+ while True:
363
+
364
+ # Evaluate the function on this data set
365
+ fValues = []
366
+ indices = nInd * [0]
367
+ iLast = nInd
368
+ while iLast >= 0:
369
+ uValue = tuple([uvw[i][indices[i]] for i in range(nInd)])
370
+ if not uValue in fDictionary:
371
+ fDictionary[uValue] = f(uValue)
372
+ fValues.append(fDictionary[uValue])
373
+ iLast = nInd - 1
374
+ while iLast >= 0:
375
+ indices[iLast] += 1
376
+ if indices[iLast] < len(uvw[iLast]):
377
+ break
378
+ indices[iLast] = 0
379
+ iLast -= 1
380
+
381
+ # Adjust the ordering
382
+ pointShape = [len(uvw[i]) for i in range(nInd)]
383
+ if type(midPoint) is bspy.Spline:
384
+ fValues = np.array(fValues).reshape(pointShape)
385
+ else:
386
+ fValues = np.array(fValues).reshape(pointShape + [nDep]).transpose([nInd] + list(range(nInd)))
387
+
388
+ # Call the least squares fitter on this data
389
+ bestSoFar = bspy.Spline.least_squares(uvw, fValues, order, currentSpace.knots, fixEnds = True)
390
+
391
+ # Determine the maximum error
392
+ maxError = 0.0
393
+ for key in fDictionary:
394
+ if type(midPoint) is bspy.Spline:
395
+ sampled = bestSoFar.contract(midPoint.nInd * [None] + list(key)).coefs
396
+ trueCoefs = fDictionary[key].coefs
397
+ thisError = np.max(np.linalg.norm(sampled - trueCoefs, axis = 0))
398
+ else:
399
+ thisError = np.linalg.norm(fDictionary[key] - bestSoFar(key))
400
+ if thisError > maxError:
401
+ maxError = thisError
402
+ maxKey = key
403
+ if maxError <= tolerance:
404
+ break
405
+
406
+ # Split the interval and try again
407
+ maxGap = 0.0
408
+ for iInd in range(nInd):
409
+ insert = bspy.Spline.bspline_values(None, currentSpace.knots[iInd], order[iInd], maxKey[iInd])[0]
410
+ leftKnot = currentSpace.knots[iInd][insert - 1]
411
+ rightKnot = currentSpace.knots[iInd][insert]
412
+ if rightKnot - leftKnot > maxGap:
413
+ maxGap = rightKnot - leftKnot
414
+ iFirst = np.searchsorted(uvw[iInd], leftKnot, side = 'right')
415
+ iLast = np.searchsorted(uvw[iInd], rightKnot, side = 'right')
416
+ maxLeft = leftKnot
417
+ maxRight = rightKnot
418
+ maxInd = iInd
419
+ splitAt = 0.5 * (maxLeft + maxRight)
420
+ newKnots = [[] for iInd in range(nInd)]
421
+ newKnots[maxInd] = [splitAt]
422
+ currentSpace = currentSpace.insert_knots(newKnots)
423
+
424
+ # Add samples for the new knot
425
+ uvw[maxInd] = np.array(list(uvw[maxInd][:iFirst - 1]) +
426
+ list(addMidPoints(uvw[maxInd][iFirst - 1:iLast])) +
427
+ list(uvw[maxInd][iLast:]))
428
+
429
+ # Return the best spline found so far
430
+ return bestSoFar
431
+
301
432
  def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
302
433
  if bottom.nInd != 1 or right.nInd != 1 or top.nInd != 1 or left.nInd != 1:
303
434
  raise ValueError("Input curves must have one independent variable")
@@ -414,26 +545,51 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
414
545
  suvv = surface.derivative([1, 2], u[:, 0])
415
546
  svvv = surface.derivative([0, 3], u[:, 0])
416
547
 
548
+ # Calculate inner products
549
+ su_su = su @ su
550
+ su_sv = su @ sv
551
+ sv_sv = sv @ sv
552
+ suu_su = suu @ su
553
+ suu_sv = suu @ sv
554
+ suv_su = suv @ su
555
+ suv_sv = suv @ sv
556
+ svv_su = svv @ su
557
+ svv_sv = svv @ sv
558
+ suu_suu = suu @ suu
559
+ suu_suv = suu @ suv
560
+ suu_svv = suu @ svv
561
+ suv_suv = suv @ suv
562
+ suv_svv = suv @ svv
563
+ svv_svv = svv @ svv
564
+ suuu_su = suuu @ su
565
+ suuu_sv = suuu @ sv
566
+ suuv_su = suuv @ su
567
+ suuv_sv = suuv @ sv
568
+ suvv_su = suvv @ su
569
+ suvv_sv = suvv @ sv
570
+ svvv_su = svvv @ su
571
+ svvv_sv = svvv @ sv
572
+
417
573
  # Calculate the first fundamental form and derivatives
418
- E = su @ su
419
- E_u = 2.0 * suu @ su
420
- E_v = 2.0 * suv @ su
421
- F = su @ sv
422
- F_u = suu @ sv + suv @ su
423
- F_v = suv @ sv + svv @ su
424
- G = sv @ sv
425
- G_u = 2.0 * suv @ sv
426
- G_v = 2.0 * svv @ sv
574
+ E = su_su
575
+ E_u = 2.0 * suu_su
576
+ E_v = 2.0 * suv_su
577
+ F = su_sv
578
+ F_u = suu_sv + suv_su
579
+ F_v = suv_sv + svv_su
580
+ G = sv_sv
581
+ G_u = 2.0 * suv_sv
582
+ G_v = 2.0 * svv_sv
427
583
  A = np.array([[E, F], [F, G]])
428
584
  A_u = np.array([[E_u, F_u], [F_u, G_u]])
429
585
  A_v = np.array([[E_v, F_v], [F_v, G_v]])
430
586
 
431
587
  # Compute right hand side entries
432
- R = np.array([[suu @ su, suv @ su, svv @ su], [suu @ sv, suv @ sv, svv @ sv]])
433
- R_u = np.array([[suuu @ su + suu @ suu, suuv @ su + suv @ suu, suvv @ su + svv @ suu],
434
- [suuu @ sv + suu @ suv, suuv @ sv + suv @ suv, suvv @ sv + svv @ suv]])
435
- R_v = np.array([[suuv @ su + suu @ suv, suvv @ su + suv @ suv, suvv @ su + svv @ suv],
436
- [suuv @ sv + suu @ svv, suvv @ sv + suv @ svv, svvv @ sv + svv @ svv]])
588
+ R = np.array([[suu_su, suv_su, svv_su], [suu_sv, suv_sv, svv_sv]])
589
+ R_u = np.array([[suuu_su + suu_suu, suuv_su + suu_suv, suvv_su + suu_svv],
590
+ [suuu_sv + suu_suv, suuv_sv + suv_suv, suvv_sv + suv_svv]])
591
+ R_v = np.array([[suuv_su + suu_suv, suvv_su + suv_suv, svvv_su + suv_svv],
592
+ [suuv_sv + suu_svv, suvv_sv + suv_svv, svvv_sv + svv_svv]])
437
593
 
438
594
  # Solve for the Christoffel symbols
439
595
  luAndPivot = sp.linalg.lu_factor(A)
@@ -748,11 +904,11 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
748
904
  for i, knot in enumerate(uniqueKnots):
749
905
  howMany = currentGuess.order[0] - indices[i + 1] + indices[i] - nOrder
750
906
  if howMany > 0:
751
- knotsToAdd += howMany * [knot]
907
+ knotsToAdd.append((knot, howMany))
752
908
  if howMany < 0:
753
- knotsToRemove += [[knot, abs(howMany)]]
909
+ knotsToRemove.append((knot, abs(howMany)))
754
910
  currentGuess = currentGuess.insert_knots([knotsToAdd])
755
- for [knot, howMany] in knotsToRemove:
911
+ for (knot, howMany) in knotsToRemove:
756
912
  ix = np.searchsorted(currentGuess.knots[0], knot, side = 'left')
757
913
  for iy in range(howMany):
758
914
  currentGuess, residual = currentGuess.remove_knot(ix, nLeft, nRight)
@@ -909,7 +1065,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
909
1065
  knotsToAdd = []
910
1066
  for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
911
1067
  if knot0 < knot1:
912
- knotsToAdd += (currentGuess.order[0] - nOrder) * [0.5 * (knot0 + knot1)]
1068
+ knotsToAdd.append((0.5 * (knot0 + knot1), currentGuess.order[0] - nOrder))
913
1069
  previousGuess = currentGuess
914
1070
  currentGuess = currentGuess.insert_knots([knotsToAdd])
915
1071