bspy 3.0.1__py3-none-any.whl → 4.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/_spline_fitting.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ import scipy as sp
2
3
  import bspy.spline
3
4
  import math
4
5
 
@@ -22,7 +23,7 @@ def cone(radius1, radius2, height, tolerance = None):
22
23
  return bspy.Spline.ruled_surface(bottom, top)
23
24
 
24
25
  # Courtesy of Michael Epton - Translated from his F77 code lgnzro
25
- def _legendre_polynomial_zeros(degree):
26
+ def _legendre_polynomial_zeros(degree, mapToZeroOne = True):
26
27
  def legendre(degree, x):
27
28
  p = [1.0, x]
28
29
  pd = [0.0, 1.0]
@@ -34,6 +35,7 @@ def _legendre_polynomial_zeros(degree):
34
35
  return p, pd
35
36
  zval = 1.0
36
37
  z = []
38
+ zNegative = []
37
39
  for iRoot in range(degree // 2):
38
40
  done = False
39
41
  while True:
@@ -48,21 +50,28 @@ def _legendre_polynomial_zeros(degree):
48
50
  if dz < 1.0e-10:
49
51
  done = True
50
52
  z.append(zval)
53
+ zNegative.append(-zval)
51
54
  zval -= 0.001
52
55
  if degree % 2 == 1:
53
- z.append(0.0)
56
+ zNegative.append(0.0)
54
57
  z.reverse()
58
+ z = np.array(zNegative + z)
55
59
  w = []
56
60
  for zval in z:
57
61
  p, pd = legendre(degree, zval)
58
62
  w.append(2.0 / ((1.0 - zval ** 2) * pd[-1] ** 2))
63
+ w = np.array(w)
64
+ if mapToZeroOne:
65
+ z = 0.5 * (1.0 + z)
66
+ w = 0.5 * w
59
67
  return z, w
60
68
 
61
69
  def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
62
70
  # Set up parameters for initial guess of x(t) and validate arguments.
63
71
  order = 4
64
72
  degree = order - 1
65
- rhos, gaussWeights = _legendre_polynomial_zeros(degree - 1)
73
+ gaussNodes, gaussWeights = _legendre_polynomial_zeros(degree - 1)
74
+
66
75
  if not(len(knownXValues) >= 2): raise ValueError("There must be at least 2 known x values.")
67
76
  m = len(knownXValues) - 1
68
77
  nCoef = m * (degree - 1) + 2
@@ -95,6 +104,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
95
104
  wrt[i] = 1
96
105
  return F.derivative(wrt, x)
97
106
  dF.append(splineDerivative)
107
+ FDomain = F.domain().T
98
108
  else:
99
109
  for i in range(nDep):
100
110
  def fDerivative(x, i=i):
@@ -106,6 +116,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
106
116
  xShift[i] += h2
107
117
  return (np.array(F(xShift)) - fLeft) / h2
108
118
  dF.append(fDerivative)
119
+ FDomain = np.array(nDep * [[-np.inf, np.inf]]).T
109
120
  else:
110
121
  if not(len(dF) == nDep): raise ValueError(f"Must provide {nDep} first derivatives.")
111
122
 
@@ -119,13 +130,9 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
119
130
  for point in knownXValues[1:]:
120
131
  dt = np.linalg.norm(point - previousPoint)
121
132
  if not(dt > epsilon): raise ValueError("Points must be separated by at least epsilon.")
122
- for rho in reversed(rhos):
123
- tValues[i] = t + 0.5 * dt * (1.0 - rho)
124
- GSamples[i] = 0.5 * (previousPoint + point - rho * (point - previousPoint))
125
- i += 1
126
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
127
- tValues[i] = t + 0.5 * dt * (1.0 + rho)
128
- GSamples[i] = 0.5 * (previousPoint + point + rho * (point - previousPoint))
133
+ for gaussNode in gaussNodes:
134
+ tValues[i] = t + gaussNode * dt
135
+ GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
129
136
  i += 1
130
137
  t += dt
131
138
  knots += [t] * (order - 2)
@@ -141,27 +148,25 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
141
148
  # where dGCoefs and GSamples are the B-spline values and sample points, respectively, for x(t).
142
149
  # The dGCoefs matrix is banded due to B-spline local support, so initialize it to zero.
143
150
  # Solving for coefs provides us our initial coefficients of x(t).
144
- dGCoefs = np.zeros((nUnknownCoefs, nDep, nDep, nCoef), contourDtype)
151
+ dGCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
145
152
  i = 0
146
- for t, i in zip(tValues, range(nUnknownCoefs)):
147
- ix = np.searchsorted(knots, t, 'right')
148
- ix = min(ix, nCoef)
149
- bValues = bspy.Spline.bspline_values(ix, knots, order, t)
153
+ for i, t in enumerate(tValues):
154
+ ix, bValues = bspy.Spline.bspline_values(None, knots, order, t)
150
155
  for j in range(nDep):
151
- dGCoefs[i, j, j, ix - order:ix] = bValues
152
- GSamples -= dGCoefs[:, :, :, 0] @ knownXValues[0] + dGCoefs[:, :, :, -1] @ knownXValues[-1]
156
+ dGCoefs[i, j, ix - order:ix, j] = bValues
157
+ GSamples -= dGCoefs[:, :, 0, :] @ knownXValues[0] + dGCoefs[:, :, -1, :] @ knownXValues[-1]
153
158
  GSamples = GSamples.reshape(nUnknownCoefs * nDep)
154
- dGCoefs = dGCoefs[:, :, :, 1:-1].reshape(nUnknownCoefs * nDep, nDep * nUnknownCoefs)
155
- coefs = np.empty((nDep, nCoef), contourDtype)
156
- coefs[:, 0] = knownXValues[0]
157
- coefs[:, -1] = knownXValues[-1]
158
- coefs[:, 1:-1] = np.linalg.solve(dGCoefs, GSamples).reshape(nDep, nUnknownCoefs)
159
+ dGCoefs = dGCoefs[:, :, 1:-1, :].reshape(nUnknownCoefs * nDep, nUnknownCoefs * nDep)
160
+ coefs = np.empty((nCoef, nDep), contourDtype)
161
+ coefs[0, :] = knownXValues[0]
162
+ coefs[-1, :] = knownXValues[-1]
163
+ coefs[1:-1, :] = np.linalg.solve(dGCoefs, GSamples).reshape(nUnknownCoefs, nDep)
159
164
 
160
165
  # Array to hold the values of F and contour dot for each t, excluding endpoints.
161
166
  FSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
162
167
  # Array to hold the Jacobian of the FSamples with respect to the coefficients.
163
168
  # The Jacobian is banded due to B-spline local support, so initialize it to zero.
164
- dFCoefs = np.zeros((nUnknownCoefs, nDep, nDep, nCoef), contourDtype)
169
+ dFCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
165
170
  # Working array to hold the transpose of the Jacobian of F for a particular x(t).
166
171
  dFX = np.empty((nDep, nDep - 1), contourDtype)
167
172
 
@@ -170,26 +175,24 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
170
175
  while True:
171
176
  FSamplesNorm = 0.0
172
177
  # Fill in FSamples and its Jacobian (dFCoefs) with respect to the coefficients of x(t).
173
- for t, i in zip(tValues, range(nUnknownCoefs)):
178
+ for i, t in enumerate(tValues):
174
179
  # Isolate coefficients and compute bspline values and their first two derivatives at t.
175
- ix = np.searchsorted(knots, t, 'right')
176
- ix = min(ix, nCoef)
177
- compactCoefs = coefs[:, ix - order:ix]
178
- bValues = bspy.Spline.bspline_values(ix, knots, order, t)
179
- dValues = bspy.Spline.bspline_values(ix, knots, order, t, 1)
180
- d2Values = bspy.Spline.bspline_values(ix, knots, order, t, 2)
180
+ ix, bValues = bspy.Spline.bspline_values(None, knots, order, t)
181
+ ix, dValues = bspy.Spline.bspline_values(ix, knots, order, t, 1)
182
+ ix, d2Values = bspy.Spline.bspline_values(ix, knots, order, t, 2)
183
+ compactCoefs = coefs[ix - order:ix, :]
181
184
 
182
185
  # Compute the dot constraint for x(t) and check for divergence from solution.
183
- dotValues = np.dot(compactCoefs @ d2Values, compactCoefs @ dValues)
186
+ dotValues = np.dot(compactCoefs.T @ d2Values, compactCoefs.T @ dValues)
184
187
  FSamplesNorm = max(FSamplesNorm, abs(dotValues))
185
188
  if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
186
189
  break
187
190
 
188
191
  # Do the same for F(x(t)).
189
- x = coefsMin + (compactCoefs @ bValues) * coefsMaxMinusMin
192
+ x = coefsMin + (compactCoefs.T @ bValues) * coefsMaxMinusMin
193
+ x = np.maximum(FDomain[0], np.minimum(FDomain[1], x))
190
194
  FValues = F(x)
191
- for FValue in FValues:
192
- FSamplesNorm = max(FSamplesNorm, abs(FValue))
195
+ FSamplesNorm = max(FSamplesNorm, np.linalg.norm(FValues, np.inf))
193
196
  if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
194
197
  break
195
198
 
@@ -200,16 +203,16 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
200
203
  # Compute the Jacobian of FSamples with respect to the coefficients of x(t).
201
204
  for j in range(nDep):
202
205
  dFX[j] = dF[j](x) * coefsMaxMinusMin[j]
203
- FValues = np.outer(dFX.T, bValues).reshape(nDep - 1, nDep, order)
204
- dotValues = (np.outer(compactCoefs @ dValues, d2Values) + np.outer(compactCoefs @ d2Values, dValues)).reshape(nDep, order)
205
- dFCoefs[i, :-1, :, ix - order:ix] = FValues
206
- dFCoefs[i, -1, :, ix - order:ix] = dotValues
206
+ FValues = np.outer(dFX.T, bValues).reshape(nDep - 1, nDep, order).swapaxes(1, 2)
207
+ dotValues = (np.outer(d2Values, compactCoefs.T @ dValues) + np.outer(dValues, compactCoefs.T @ d2Values)).reshape(order, nDep)
208
+ dFCoefs[i, :-1, ix - order:ix, :] = FValues
209
+ dFCoefs[i, -1, ix - order:ix, :] = dotValues
207
210
 
208
211
  # Check if we got closer to the solution.
209
212
  if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
210
213
  # No we didn't, take a dampened step.
211
214
  coefDelta *= 0.5
212
- coefs[:, 1:-1] += coefDelta # Don't update endpoints
215
+ coefs[1:-1, :] += coefDelta # Don't update endpoints
213
216
  else:
214
217
  # Yes we did, rescale FSamples and its Jacobian.
215
218
  if FSamplesNorm >= evaluationEpsilon:
@@ -217,10 +220,16 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
217
220
  dFCoefs /= FSamplesNorm
218
221
 
219
222
  # Perform a Newton iteration.
220
- HSamples = FSamples.reshape(nUnknownCoefs * nDep)
221
- dHCoefs = dFCoefs[:, :, :, 1:-1].reshape((nUnknownCoefs * nDep, nDep * nUnknownCoefs))
222
- coefDelta = np.linalg.solve(dHCoefs, HSamples).reshape(nDep, nUnknownCoefs)
223
- coefs[:, 1:-1] -= coefDelta # Don't update endpoints
223
+ n = nUnknownCoefs * nDep
224
+ HSamples = FSamples.reshape(n)
225
+ dHCoefs = dFCoefs[:, :, 1:-1, :].reshape((n, n))
226
+ bandWidth = order * nDep - 1
227
+ banded = np.zeros((2 * bandWidth + 1, n))
228
+ for iDiagonal in range(min(bandWidth + 1, n)):
229
+ banded[bandWidth - iDiagonal, iDiagonal : n]= np.diagonal(dHCoefs, iDiagonal)
230
+ banded[bandWidth + iDiagonal, : n - iDiagonal] = np.diagonal(dHCoefs, -iDiagonal)
231
+ coefDelta = sp.linalg.solve_banded((bandWidth, bandWidth), banded, HSamples).reshape(nUnknownCoefs, nDep)
232
+ coefs[1:-1, :] -= coefDelta # Don't update endpoints
224
233
 
225
234
  # Record FSamples norm to ensure this Newton step is productive.
226
235
  previousFSamplesNorm = FSamplesNorm
@@ -229,59 +238,54 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
229
238
  if np.linalg.norm(coefDelta) < epsilon:
230
239
  # If step didn't improve the solution, remove it.
231
240
  if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
232
- coefs[:, 1:-1] += coefDelta # Don't update endpoints
241
+ coefs[1:-1, :] += coefDelta # Don't update endpoints
233
242
  break
234
243
 
235
- # Newton steps are done. Now check if we need to subdivide.
236
- # TODO: This would be FSamplesNorm / dHCoefs norm, but dHCoefs was divided by FSamplesNorm earlier.
237
- if FSamplesNorm / np.linalg.norm(dHCoefs, np.inf) < epsilon:
238
- break # We're done!
239
-
240
- # We need to subdivide, so build new knots, tValues, and GSamples arrays.
241
- nCoef = 2 * (nCoef - 1)
242
- nUnknownCoefs = nCoef - 2
244
+ # Newton steps are done. Now, check if we need to subdivide.
245
+ # We do this by building new subdivided knots, tValues, and GSamples (x(tValues)).
246
+ # If F(GSamples) is close enough to zero, we're done.
247
+ # Otherwise, re-run Newton's method using the new knots, tValues, and GSamples.
248
+ nUnknownCoefs = 2 * (nCoef - 2)
243
249
  tValues = np.empty(nUnknownCoefs, contourDtype)
244
250
  GSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
245
251
  previousKnot = knots[degree]
246
252
  newKnots = [previousKnot] * order
253
+ FSamplesNorm = 0.0
247
254
  i = 0
248
255
  for ix in range(order, len(knots) - degree, order - 2):
249
256
  knot = knots[ix]
250
- compactCoefs = coefs[:, ix - order:ix]
257
+ compactCoefs = coefs[ix - order:ix, :]
251
258
 
252
259
  # New knots are at the midpoint between old knots.
253
260
  newKnot = 0.5 * (previousKnot + knot)
254
261
 
255
262
  # Place tValues at Gauss points for the intervals [previousKnot, newKnot] and [newKnot, knot].
256
- for rho in reversed(rhos):
257
- tValues[i] = t = 0.5 * (previousKnot + newKnot - rho * (newKnot - previousKnot))
258
- GSamples[i] = compactCoefs @ bspy.Spline.bspline_values(ix, knots, order, t)
259
- i += 1
260
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
261
- tValues[i] = t = 0.5 * (previousKnot + newKnot + rho * (newKnot - previousKnot))
262
- GSamples[i] = compactCoefs @ bspy.Spline.bspline_values(ix, knots, order, t)
263
- i += 1
264
- for rho in reversed(rhos):
265
- tValues[i] = t = 0.5 * (newKnot + knot - rho * (knot - newKnot))
266
- GSamples[i] = compactCoefs @ bspy.Spline.bspline_values(ix, knots, order, t)
267
- i += 1
268
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
269
- tValues[i] = t = 0.5 * (newKnot + knot + rho * (knot - newKnot))
270
- GSamples[i] = compactCoefs @ bspy.Spline.bspline_values(ix, knots, order, t)
271
- i += 1
263
+ for knotInterval in [[previousKnot, newKnot], [newKnot, knot]]:
264
+ for gaussNode in gaussNodes:
265
+ tValues[i] = t = (1.0 - gaussNode) * knotInterval[0] + gaussNode * knotInterval[1]
266
+ x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
267
+ x = np.array([max(0.0, min(1.0, xi)) for xi in x])
268
+ GSamples[i] = x
269
+ FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
270
+ i += 1
272
271
 
273
272
  newKnots += [newKnot] * (order - 2) # C1 continuity
274
273
  newKnots += [knot] * (order - 2) # C1 continuity
275
274
  previousKnot = knot
276
275
 
277
- # Update knots array.
276
+ # Test if F(GSamples) is close enough to zero.
277
+ if FSamplesNorm / np.linalg.norm(dHCoefs, np.inf) < epsilon:
278
+ break # We're done! Exit subdivision loop and return x(t).
279
+
280
+ # Otherwise, update nCoef and knots array, and then re-run Newton's method.
281
+ nCoef = nUnknownCoefs + 2
278
282
  newKnots += [knot] * 2 # Clamp last knot
279
283
  knots = np.array(newKnots, contourDtype)
280
284
  assert i == nUnknownCoefs
281
285
  assert len(knots) == nCoef + order
282
286
 
283
287
  # Rescale x(t) back to original data points.
284
- coefs = (coefsMin + coefs.T * coefsMaxMinusMin).T
288
+ coefs = (coefsMin + coefs * coefsMaxMinusMin).T
285
289
  spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
286
290
  if isinstance(F, bspy.Spline):
287
291
  spline = spline.confine(F.domain())
@@ -383,6 +387,82 @@ def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
383
387
 
384
388
  return (1.0 - surfParam) * coons + surfParam * laplace
385
389
 
390
+ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
391
+ # Check validity of input
392
+ if self.nInd != 2: raise ValueError("Surface must have two independent variables")
393
+ if len(uvStart) != 2: raise ValueError("uvStart must have two components")
394
+ if len(uvEnd) != 2: raise ValueError("uvEnd must have two components")
395
+ uvDomain = self.domain()
396
+ if uvStart[0] < uvDomain[0, 0] or uvStart[0] > uvDomain[0, 1] or \
397
+ uvStart[1] < uvDomain[1, 0] or uvStart[1] > uvDomain[1, 1]:
398
+ raise ValueError("uvStart is outside domain of the surface")
399
+ if uvEnd[0] < uvDomain[0, 0] or uvEnd[0] > uvDomain[0, 1] or \
400
+ uvEnd[1] < uvDomain[1, 0] or uvEnd[1] > uvDomain[1, 1]:
401
+ raise ValueError("uvEnd is outside domain of the surface")
402
+
403
+ # Define the callback function for the ODE solver
404
+ def geodesicCallback(t, u, surface, uvDomain):
405
+ # Evaluate the surface information needed for the Christoffel symbols
406
+ u[:, 0] = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
407
+ su = surface.derivative([1, 0], u[:, 0])
408
+ sv = surface.derivative([0, 1], u[:, 0])
409
+ suu = surface.derivative([2, 0], u[:, 0])
410
+ suv = surface.derivative([1, 1], u[:, 0])
411
+ svv = surface.derivative([0, 2], u[:, 0])
412
+ suuu = surface.derivative([3, 0], u[:, 0])
413
+ suuv = surface.derivative([2, 1], u[:, 0])
414
+ suvv = surface.derivative([1, 2], u[:, 0])
415
+ svvv = surface.derivative([0, 3], u[:, 0])
416
+
417
+ # 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
427
+ A = np.array([[E, F], [F, G]])
428
+ A_u = np.array([[E_u, F_u], [F_u, G_u]])
429
+ A_v = np.array([[E_v, F_v], [F_v, G_v]])
430
+
431
+ # 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]])
437
+
438
+ # Solve for the Christoffel symbols
439
+ luAndPivot = sp.linalg.lu_factor(A)
440
+ Gamma = sp.linalg.lu_solve(luAndPivot, R)
441
+ Gamma_u = sp.linalg.lu_solve(luAndPivot, R_u - A_u @ Gamma)
442
+ Gamma_v = sp.linalg.lu_solve(luAndPivot, R_v - A_v @ Gamma)
443
+
444
+ # Compute the right hand side for the ODE
445
+ rhs = -np.array([Gamma[0, 0] * u[0, 1] ** 2 + 2.0 * Gamma[0, 1] * u[0, 1] * u[1, 1] + Gamma[0, 2] * u[1, 1] ** 2,
446
+ Gamma[1, 0] * u[0, 1] ** 2 + 2.0 * Gamma[1, 1] * u[0, 1] * u[1, 1] + Gamma[1, 2] * u[1, 1] ** 2])
447
+
448
+ # Compute the Jacobian matrix of the right hand side of the ODE
449
+ jacobian = -np.array([[[Gamma_u[0, 0] * u[0, 1] ** 2 + 2.0 * Gamma_u[0, 1] * u[0, 1] * u[1, 1] + Gamma_u[0, 2] * u[1, 1] ** 2,
450
+ 2.0 * Gamma[0, 0] * u[0, 1] + 2.0 * Gamma[0, 1] * u[1, 1]],
451
+ [Gamma_v[0, 0] * u[0, 1] ** 2 + 2.0 * Gamma_v[0, 1] * u[0, 1] * u[1, 1] + Gamma_v[0, 2] * u[1, 1] ** 2,
452
+ 2.0 * Gamma[0, 1] * u[0, 1] + 2.0 * Gamma[0, 2] * u[1, 1]]],
453
+ [[Gamma_u[1, 0] * u[0, 1] ** 2 + 2.0 * Gamma_u[1, 1] * u[0, 1] * u[1, 1] + Gamma_u[1, 2] * u[1, 1] ** 2,
454
+ 2.0 * Gamma[1, 0] * u[0, 1] + 2.0 * Gamma[1, 1] * u[1, 1]],
455
+ [Gamma_v[1, 0] * u[0, 1] ** 2 + 2.0 * Gamma_v[1, 1] * u[0, 1] * u[1, 1] + Gamma_v[1, 2] * u[1, 1] ** 2,
456
+ 2.0 * Gamma[1, 1] * u[1, 1] + 2.0 * Gamma[1, 2] * u[1, 1]]]])
457
+ return rhs, jacobian
458
+
459
+ # Generate the initial guess for the contour
460
+ initialGuess = line(uvStart, uvEnd).elevate([2])
461
+
462
+ # Solve the ODE and return the geodesic
463
+ solution = initialGuess.solve_ode(1, 1, geodesicCallback, 1.0e-5, (self, uvDomain))
464
+ return solution
465
+
386
466
  def least_squares(uValues, dataPoints, order = None, knots = None, compression = 0.0,
387
467
  tolerance = None, fixEnds = False, metadata = {}):
388
468
 
@@ -482,11 +562,10 @@ def least_squares(uValues, dataPoints, order = None, knots = None, compression =
482
562
  if uNew != u:
483
563
  iDerivative = 0
484
564
  u = uNew
485
- ix = np.searchsorted(knots[iInd], u, 'right')
486
- ix = min(ix, nCols)
565
+ ix = None
487
566
  else:
488
567
  iDerivative += 1
489
- row = bspy.Spline.bspline_values(ix, knots[iInd], order[iInd], u, iDerivative)
568
+ ix, row = bspy.Spline.bspline_values(ix, knots[iInd], order[iInd], u, iDerivative)
490
569
  A[iRow, ix - order[iInd] : ix] = row
491
570
  if fixEnds and (u == uValues[iInd][0] or u == uValues[iInd][-1]):
492
571
  fixedRows.append(iRow)
@@ -602,7 +681,10 @@ def section(xytk):
602
681
  onePlusCosTheta = 1.0 + math.cos(theta)
603
682
  r0 = 4.0 * startKappa * tangentDistances[0] ** 2 / (3.0 * tangentDistances[1] * crossTangents)
604
683
  r1 = 4.0 * endKappa * tangentDistances[1] ** 2 / (3.0 * tangentDistances[0] * crossTangents)
605
- rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
684
+ if r0 != 0.0 or r1 != 0.0:
685
+ rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
686
+ else:
687
+ rhoCrit = 1.0
606
688
  rhoCritOfTheta = 3.0 * (math.sqrt(1.0 + 32.0 / (3.0 * onePlusCosTheta)) - 1.0) * onePlusCosTheta / 16.0
607
689
 
608
690
  # Determine quadratic polynomial
@@ -640,6 +722,202 @@ def section(xytk):
640
722
  # Join the pieces together and return
641
723
  return bspy.Spline.join(mySections)
642
724
 
725
+ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
726
+ # Ensure that the ODE is properly formulated
727
+
728
+ if nLeft < 0: raise ValueError("Invalid number of left hand boundary conditions")
729
+ if nRight < 0: raise ValueError("Invalid number of right hand boundary conditions")
730
+ if self.nInd != 1: raise ValueError("Initial guess must have exactly one independent variable")
731
+ nOrder = nLeft + nRight
732
+
733
+ # Make sure that there are full multiplicity knots on the ends
734
+
735
+ currentGuess = self.copy().clamp((0,), (0,))
736
+ scale = 1.0
737
+ for lower, upper in currentGuess.range_bounds():
738
+ scale = max(scale, upper - lower)
739
+ nDep = currentGuess.nDep
740
+
741
+ # Insert and remove knots so initial guess conforms to de Boor - Swartz setup
742
+
743
+ uniqueKnots, indices = np.unique(currentGuess.knots[0], True)
744
+ uniqueKnots = uniqueKnots[1 : -1]
745
+ indices = indices[1:]
746
+ knotsToAdd = []
747
+ knotsToRemove = []
748
+ for i, knot in enumerate(uniqueKnots):
749
+ howMany = currentGuess.order[0] - indices[i + 1] + indices[i] - nOrder
750
+ if howMany > 0:
751
+ knotsToAdd += howMany * [knot]
752
+ if howMany < 0:
753
+ knotsToRemove += [[knot, abs(howMany)]]
754
+ currentGuess = currentGuess.insert_knots([knotsToAdd])
755
+ for [knot, howMany] in knotsToRemove:
756
+ ix = np.searchsorted(currentGuess.knots[0], knot, side = 'left')
757
+ for iy in range(howMany):
758
+ currentGuess, residual = currentGuess.remove_knot(ix, nLeft, nRight)
759
+ previousGuess = 0.0 * currentGuess
760
+ bandWidth = nDep * currentGuess.order[0] - 1
761
+
762
+ # Determine whether an initial value problem or boundary value problem
763
+
764
+ IVP = nLeft == 0 or nRight == 0
765
+
766
+ # Use the Gauss Legendre points as the collocation points
767
+
768
+ perInterval = currentGuess.order[0] - nOrder
769
+ gaussNodes, weights = _legendre_polynomial_zeros(perInterval)
770
+ linear = False
771
+ refine = True
772
+ while refine:
773
+ collocationPoints = []
774
+ for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
775
+ if knot0 < knot1:
776
+ for gaussNode in gaussNodes:
777
+ collocationPoints.append((1.0 - gaussNode) * knot0 + gaussNode * knot1)
778
+ n = nDep * currentGuess.nCoef[0]
779
+ bestGuess = np.reshape(currentGuess.coefs.T, (n,))
780
+
781
+ # Set up for next pass at this refinement level
782
+
783
+ nCollocation = len(collocationPoints)
784
+ if IVP:
785
+ n = nDep * currentGuess.order[0]
786
+ if nLeft != 0:
787
+ iFirstPoint = 0
788
+ iNextPoint = perInterval
789
+ else:
790
+ iNextPoint = nCollocation
791
+ iFirstPoint = iNextPoint - perInterval
792
+ else:
793
+ iFirstPoint = 0
794
+ iNextPoint = nCollocation
795
+
796
+ # Perform the loop through all the IVP intervals
797
+
798
+ while True:
799
+ done = linear
800
+ continuation = 1.0
801
+ bestContinuation = 0.0
802
+ inCaseOfEmergency = bestGuess.copy()
803
+ previous = 0.5 * np.finfo(bestGuess[0]).max
804
+ iteration = 0
805
+
806
+ # Perform nonlinear Newton iteration
807
+
808
+ while True:
809
+ iteration += 1
810
+ collocationMatrix = np.zeros((2 * bandWidth + 1, n))
811
+ residuals = np.array([])
812
+ workingSpline = bspy.Spline(1, nDep, currentGuess.order, currentGuess.nCoef,
813
+ currentGuess.knots, np.reshape(bestGuess, (currentGuess.nCoef[0], nDep)).T)
814
+ residuals = np.append(residuals, np.zeros((nLeft * nDep,)))
815
+ collocationMatrix[bandWidth, 0 : nLeft * nDep] = 1.0
816
+ for iPoint, t in enumerate(collocationPoints[iFirstPoint : iNextPoint]):
817
+ uData = np.array([workingSpline.derivative([i], t) for i in range(nOrder)]).T
818
+ F, F_u = FAndF_u(t, uData, *args)
819
+ residuals = np.append(residuals, workingSpline.derivative([nOrder], t) - continuation * F)
820
+ ix = None
821
+ bValues = np.array([])
822
+ for iDerivative in range(nOrder + 1):
823
+ ix, iValues = bspy.Spline.bspline_values(ix, workingSpline.knots[0], workingSpline.order[0],
824
+ t, derivativeOrder = iDerivative)
825
+ bValues = np.append(bValues, iValues)
826
+ bValues = np.reshape(bValues, (nOrder + 1, workingSpline.order[0]))
827
+ for iDep in range(nDep):
828
+ iRow = (nLeft + iPoint) * nDep + iDep
829
+ startSlice = nDep * (ix - workingSpline.order[0] - iFirstPoint)
830
+ endSlice = nDep * (ix - iFirstPoint)
831
+ indices = np.arange(startSlice + iDep, endSlice + iDep, nDep)
832
+ collocationMatrix[bandWidth + iRow - indices, indices] -= bValues[nOrder]
833
+ for iF_uRow in range(nDep):
834
+ indices = np.arange(startSlice + iF_uRow, endSlice + iF_uRow, nDep)
835
+ for iF_uColumn in range(nOrder):
836
+ collocationMatrix[bandWidth + iRow - indices, indices] += continuation * F_u[iDep, iF_uRow, iF_uColumn] * bValues[iF_uColumn]
837
+ residuals = np.append(residuals, np.zeros((nRight * nDep,)))
838
+ collocationMatrix[bandWidth, -1 : -(nRight * nDep + 1) : -1] = 1.0
839
+
840
+ # Solve the collocation linear system
841
+
842
+ update = sp.linalg.solve_banded((bandWidth, bandWidth), collocationMatrix, residuals)
843
+ bestGuess[nDep * (iFirstPoint + nLeft) : nDep * (iNextPoint + nLeft)] += update[nDep * nLeft : nDep * (iNextPoint - iFirstPoint + nLeft)]
844
+ updateSize = np.linalg.norm(update)
845
+ if updateSize > 1.25 * previous and iteration >= 4 or \
846
+ updateSize > 0.01 and iteration > 50:
847
+ continuation = 0.5 * (continuation + bestContinuation)
848
+ bestGuess = inCaseOfEmergency.copy()
849
+ if continuation - bestContinuation < 0.01:
850
+ break
851
+ previous = 0.5 * np.finfo(bestGuess[0]).max
852
+ iteration = 0
853
+ continue
854
+ previous = updateSize
855
+ if done or iteration > 50:
856
+ break
857
+
858
+ # Check to see if we're almost done
859
+
860
+ if updateSize < math.sqrt(n) * scale * math.sqrt(np.finfo(update.dtype).eps):
861
+ if continuation < 1.0:
862
+ bestContinuation = continuation
863
+ inCaseOfEmergency = bestGuess.copy()
864
+ continuation = min(1.0, 1.2 * continuation)
865
+ previous = 0.5 * np.finfo(bestGuess[0]).max
866
+ iteration = 0
867
+ else:
868
+ done = True
869
+
870
+ # Check to see if this is a linear problem
871
+
872
+ if not linear and iteration == 2 and updateSize < 100.0 * scale * np.finfo(update[0]).eps:
873
+ linear = True
874
+ done = True
875
+
876
+ # Set up for one more pass through an IVP
877
+
878
+ if IVP:
879
+ if nLeft != 0:
880
+ iFirstPoint = iNextPoint
881
+ iNextPoint += perInterval
882
+ if iFirstPoint < nCollocation:
883
+ continue
884
+ else:
885
+ iNextPoint = iFirstPoint
886
+ iFirstPoint -= perInterval
887
+ if iFirstPoint >= 0:
888
+ continue
889
+ break;
890
+
891
+ # Is it time to give up?
892
+
893
+ if (not done or continuation < 1.0) and n > 1000:
894
+ raise RuntimeError("Can't find solution with given initial guess")
895
+
896
+ # Estimate the error
897
+
898
+ currentGuess = bspy.Spline(1, nDep, currentGuess.order, currentGuess.nCoef, currentGuess.knots,
899
+ np.reshape(bestGuess, (currentGuess.nCoef[0], nDep)).T)
900
+ errorRange = (previousGuess - currentGuess).range_bounds()
901
+ refine = False
902
+ for lower, upper in errorRange:
903
+ if upper - lower > 2.0 * scale * tolerance:
904
+ refine = True
905
+
906
+ # Insert new knots if refinement is needed
907
+
908
+ if refine:
909
+ knotsToAdd = []
910
+ for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
911
+ if knot0 < knot1:
912
+ knotsToAdd += (currentGuess.order[0] - nOrder) * [0.5 * (knot0 + knot1)]
913
+ previousGuess = currentGuess
914
+ currentGuess = currentGuess.insert_knots([knotsToAdd])
915
+
916
+ # Simplify the result and return
917
+
918
+ currentGuess = currentGuess.remove_knots(0.1 * scale * tolerance, nLeft, nRight)
919
+ return currentGuess
920
+
643
921
  def sphere(radius, tolerance = None):
644
922
  if radius <= 0.0: raise ValueError("Radius must be positive")
645
923
  if tolerance == None: