bspy 4.0__py3-none-any.whl → 4.2__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
@@ -23,7 +33,7 @@ def cone(radius1, radius2, height, tolerance = None):
23
33
  return bspy.Spline.ruled_surface(bottom, top)
24
34
 
25
35
  # Courtesy of Michael Epton - Translated from his F77 code lgnzro
26
- def _legendre_polynomial_zeros(degree):
36
+ def _legendre_polynomial_zeros(degree, mapToZeroOne = True):
27
37
  def legendre(degree, x):
28
38
  p = [1.0, x]
29
39
  pd = [0.0, 1.0]
@@ -35,6 +45,7 @@ def _legendre_polynomial_zeros(degree):
35
45
  return p, pd
36
46
  zval = 1.0
37
47
  z = []
48
+ zNegative = []
38
49
  for iRoot in range(degree // 2):
39
50
  done = False
40
51
  while True:
@@ -49,21 +60,28 @@ def _legendre_polynomial_zeros(degree):
49
60
  if dz < 1.0e-10:
50
61
  done = True
51
62
  z.append(zval)
63
+ zNegative.append(-zval)
52
64
  zval -= 0.001
53
65
  if degree % 2 == 1:
54
- z.append(0.0)
66
+ zNegative.append(0.0)
55
67
  z.reverse()
68
+ z = np.array(zNegative + z)
56
69
  w = []
57
70
  for zval in z:
58
71
  p, pd = legendre(degree, zval)
59
72
  w.append(2.0 / ((1.0 - zval ** 2) * pd[-1] ** 2))
73
+ w = np.array(w)
74
+ if mapToZeroOne:
75
+ z = 0.5 * (1.0 + z)
76
+ w = 0.5 * w
60
77
  return z, w
61
78
 
62
79
  def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
63
80
  # Set up parameters for initial guess of x(t) and validate arguments.
64
81
  order = 4
65
82
  degree = order - 1
66
- rhos, gaussWeights = _legendre_polynomial_zeros(degree - 1)
83
+ gaussNodes, gaussWeights = _legendre_polynomial_zeros(degree - 1)
84
+
67
85
  if not(len(knownXValues) >= 2): raise ValueError("There must be at least 2 known x values.")
68
86
  m = len(knownXValues) - 1
69
87
  nCoef = m * (degree - 1) + 2
@@ -80,60 +98,67 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
80
98
  FValues = F(knownXValue)
81
99
  if not(len(FValues) == nDep - 1 and np.linalg.norm(FValues) < evaluationEpsilon):
82
100
  raise ValueError(f"F(known x) must be a zero vector of length {nDep - 1}.")
83
- coefsMin = knownXValues.min(axis=0)
84
- coefsMaxMinusMin = knownXValues.max(axis=0) - coefsMin
85
- 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.
86
114
  coefsMaxMinMinReciprocal = np.reciprocal(coefsMaxMinusMin)
87
115
  knownXValues = (knownXValues - coefsMin) * coefsMaxMinMinReciprocal # Rescale to [0 , 1]
88
116
 
89
- # Establish the first derivatives of F.
117
+ # Establish the Jacobian of F.
90
118
  if dF is None:
91
- dF = []
92
- if isinstance(F, bspy.Spline):
93
- for i in range(nDep):
94
- def splineDerivative(x, i=i):
95
- wrt = [0] * nDep
96
- wrt[i] = 1
97
- return F.derivative(wrt, x)
98
- dF.append(splineDerivative)
119
+ if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
120
+ dF = F.jacobian
99
121
  else:
100
- for i in range(nDep):
101
- def fDerivative(x, i=i):
122
+ def fJacobian(x):
123
+ value = np.empty((nDep - 1, nDep), float)
124
+ for i in range(nDep):
102
125
  h = epsilon * (1.0 + abs(x[i]))
103
126
  xShift = np.array(x, copy=True)
104
127
  xShift[i] -= h
105
128
  fLeft = np.array(F(xShift))
106
129
  h2 = h * 2.0
107
130
  xShift[i] += h2
108
- return (np.array(F(xShift)) - fLeft) / h2
109
- dF.append(fDerivative)
110
- else:
131
+ value[:, i] = (np.array(F(xShift)) - fLeft) / h2
132
+ return value
133
+ dF = fJacobian
134
+ elif not callable(dF):
111
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
112
142
 
113
143
  # Construct knots, t values, and GSamples.
114
144
  tValues = np.empty(nUnknownCoefs, contourDtype)
115
145
  GSamples = np.empty((nUnknownCoefs, nDep), contourDtype)
116
- 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
117
148
  knots = [t] * order
118
149
  i = 0
119
150
  previousPoint = knownXValues[0]
120
151
  for point in knownXValues[1:]:
121
- dt = np.linalg.norm(point - previousPoint)
122
- if not(dt > epsilon): raise ValueError("Points must be separated by at least epsilon.")
123
- for rho in reversed(rhos):
124
- tValues[i] = t + 0.5 * dt * (1.0 - rho)
125
- GSamples[i] = 0.5 * (previousPoint + point - rho * (point - previousPoint))
126
- i += 1
127
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
128
- tValues[i] = t + 0.5 * dt * (1.0 + rho)
129
- GSamples[i] = 0.5 * (previousPoint + point + rho * (point - previousPoint))
152
+ for gaussNode in gaussNodes:
153
+ tValues[i] = t + gaussNode * dt
154
+ GSamples[i] = (1.0 - gaussNode) * previousPoint + gaussNode * point
130
155
  i += 1
131
156
  t += dt
132
157
  knots += [t] * (order - 2)
133
158
  previousPoint = point
134
159
  knots += [t] * 2 # Clamp last knot
135
- knots = np.array(knots, contourDtype) / t # Rescale knots
136
- tValues /= t # Rescale t values
160
+ knots = np.array(knots, contourDtype)
161
+ knots[nCoef:] = 1.0 # Ensure last knot is exactly 1.0
137
162
  assert i == nUnknownCoefs
138
163
 
139
164
  # Start subdivision loop.
@@ -161,8 +186,6 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
161
186
  # Array to hold the Jacobian of the FSamples with respect to the coefficients.
162
187
  # The Jacobian is banded due to B-spline local support, so initialize it to zero.
163
188
  dFCoefs = np.zeros((nUnknownCoefs, nDep, nCoef, nDep), contourDtype)
164
- # Working array to hold the transpose of the Jacobian of F for a particular x(t).
165
- dFX = np.empty((nDep, nDep - 1), contourDtype)
166
189
 
167
190
  # Start Newton's method loop.
168
191
  previousFSamplesNorm = 0.0
@@ -184,6 +207,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
184
207
 
185
208
  # Do the same for F(x(t)).
186
209
  x = coefsMin + (compactCoefs.T @ bValues) * coefsMaxMinusMin
210
+ x = np.maximum(FDomain[0], np.minimum(FDomain[1], x))
187
211
  FValues = F(x)
188
212
  FSamplesNorm = max(FSamplesNorm, np.linalg.norm(FValues, np.inf))
189
213
  if previousFSamplesNorm > 0.0 and FSamplesNorm > previousFSamplesNorm * (1.0 - evaluationEpsilon):
@@ -194,9 +218,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
194
218
  FSamples[i, -1] = dotValues
195
219
 
196
220
  # Compute the Jacobian of FSamples with respect to the coefficients of x(t).
197
- for j in range(nDep):
198
- dFX[j] = dF[j](x) * coefsMaxMinusMin[j]
199
- 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)
200
222
  dotValues = (np.outer(d2Values, compactCoefs.T @ dValues) + np.outer(dValues, compactCoefs.T @ d2Values)).reshape(order, nDep)
201
223
  dFCoefs[i, :-1, ix - order:ix, :] = FValues
202
224
  dFCoefs[i, -1, ix - order:ix, :] = dotValues
@@ -253,33 +275,21 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
253
275
  newKnot = 0.5 * (previousKnot + knot)
254
276
 
255
277
  # 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] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
259
- FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
260
- i += 1
261
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
262
- tValues[i] = t = 0.5 * (previousKnot + newKnot + rho * (newKnot - previousKnot))
263
- GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
264
- FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
265
- i += 1
266
- for rho in reversed(rhos):
267
- tValues[i] = t = 0.5 * (newKnot + knot - rho * (knot - newKnot))
268
- GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
269
- FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
270
- i += 1
271
- for rho in rhos[0 if degree % 2 == 1 else 1:]:
272
- tValues[i] = t = 0.5 * (newKnot + knot + rho * (knot - newKnot))
273
- GSamples[i] = x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
274
- FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
275
- i += 1
278
+ for knotInterval in [[previousKnot, newKnot], [newKnot, knot]]:
279
+ for gaussNode in gaussNodes:
280
+ tValues[i] = t = (1.0 - gaussNode) * knotInterval[0] + gaussNode * knotInterval[1]
281
+ x = compactCoefs.T @ bspy.Spline.bspline_values(ix, knots, order, t)[1]
282
+ x = np.array([max(0.0, min(1.0, xi)) for xi in x])
283
+ GSamples[i] = x
284
+ FSamplesNorm = max(FSamplesNorm, np.linalg.norm(F(coefsMin + x * coefsMaxMinusMin), np.inf))
285
+ i += 1
276
286
 
277
287
  newKnots += [newKnot] * (order - 2) # C1 continuity
278
288
  newKnots += [knot] * (order - 2) # C1 continuity
279
289
  previousKnot = knot
280
290
 
281
291
  # Test if F(GSamples) is close enough to zero.
282
- if FSamplesNorm / np.linalg.norm(dHCoefs, np.inf) < epsilon:
292
+ if FSamplesNorm < evaluationEpsilon:
283
293
  break # We're done! Exit subdivision loop and return x(t).
284
294
 
285
295
  # Otherwise, update nCoef and knots array, and then re-run Newton's method.
@@ -292,7 +302,7 @@ def contour(F, knownXValues, dF = None, epsilon = None, metadata = {}):
292
302
  # Rescale x(t) back to original data points.
293
303
  coefs = (coefsMin + coefs * coefsMaxMinusMin).T
294
304
  spline = bspy.Spline(1, nDep, (order,), (nCoef,), (knots,), coefs, metadata)
295
- if isinstance(F, bspy.Spline):
305
+ if isinstance(F, (bspy.Spline, bspy.SplineBlock)):
296
306
  spline = spline.confine(F.domain())
297
307
  return spline
298
308
 
@@ -303,6 +313,122 @@ def cylinder(radius, height, tolerance = None):
303
313
  top = bottom + [0.0, 0.0, height]
304
314
  return bspy.Spline.ruled_surface(bottom, top)
305
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
+
306
432
  def four_sided_patch(bottom, right, top, left, surfParam = 0.5):
307
433
  if bottom.nInd != 1 or right.nInd != 1 or top.nInd != 1 or left.nInd != 1:
308
434
  raise ValueError("Input curves must have one independent variable")
@@ -408,68 +534,72 @@ def geodesic(self, uvStart, uvEnd, tolerance = 1.0e-6):
408
534
  # Define the callback function for the ODE solver
409
535
  def geodesicCallback(t, u, surface, uvDomain):
410
536
  # Evaluate the surface information needed for the Christoffel symbols
411
- u[0, 0] = max(uvDomain[0, 0], min(uvDomain[0, 1], u[0, 0]))
412
- u[1, 0] = max(uvDomain[1, 0], min(uvDomain[1, 1], u[1, 0]))
537
+ u[:, 0] = np.maximum(uvDomain[:, 0], np.minimum(uvDomain[:, 1], u[:, 0]))
413
538
  su = surface.derivative([1, 0], u[:, 0])
414
539
  sv = surface.derivative([0, 1], u[:, 0])
415
540
  suu = surface.derivative([2, 0], u[:, 0])
416
541
  suv = surface.derivative([1, 1], u[:, 0])
417
542
  svv = surface.derivative([0, 2], u[:, 0])
543
+ suuu = surface.derivative([3, 0], u[:, 0])
544
+ suuv = surface.derivative([2, 1], u[:, 0])
545
+ suvv = surface.derivative([1, 2], u[:, 0])
546
+ svvv = surface.derivative([0, 3], u[:, 0])
418
547
 
419
- # Calculate the first fundamental form
420
- E = su @ su
421
- F = su @ sv
422
- G = sv @ sv
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
+
573
+ # Calculate the first fundamental form and derivatives
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
423
583
  A = np.array([[E, F], [F, G]])
584
+ A_u = np.array([[E_u, F_u], [F_u, G_u]])
585
+ A_v = np.array([[E_v, F_v], [F_v, G_v]])
424
586
 
425
587
  # Compute right hand side entries
426
- R_uuu = suu @ su
427
- R_uuv = suu @ sv
428
- R_uvu = suv @ su
429
- R_uvv = suv @ sv
430
- R_vvu = svv @ su
431
- R_vvv = svv @ sv
432
- R = np.array([[R_uuu, R_uvu, R_vvu], [R_uuv, R_uvv, R_vvv]])
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]])
433
593
 
434
594
  # Solve for the Christoffel symbols
435
595
  luAndPivot = sp.linalg.lu_factor(A)
436
596
  Gamma = sp.linalg.lu_solve(luAndPivot, R)
597
+ Gamma_u = sp.linalg.lu_solve(luAndPivot, R_u - A_u @ Gamma)
598
+ Gamma_v = sp.linalg.lu_solve(luAndPivot, R_v - A_v @ Gamma)
437
599
 
438
600
  # Compute the right hand side for the ODE
439
601
  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,
440
602
  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])
441
-
442
- # Now we need the derivatives of the Christoffel symbols with respect to u
443
- suuu = surface.derivative([3, 0], u[:, 0])
444
- suuv = surface.derivative([2, 1], u[:, 0])
445
- suvv = surface.derivative([1, 2], u[:, 0])
446
- svvv = surface.derivative([0, 3], u[:, 0])
447
- E_u = 2.0 * R_uuu
448
- F_u = R_uuv + R_uvu
449
- G_u = 2.0 * R_uvv
450
- A_u = np.array([[E_u, F_u], [F_u, G_u]])
451
- R_uuu_u = suuu @ su + suu @ suu
452
- R_uuv_u = suuu @ sv + suu @ suv
453
- R_uvu_u = suuv @ su + suv @ suu
454
- R_uvv_u = suuv @ sv + suv @ suv
455
- R_vvu_u = suvv @ su + svv @ suu
456
- R_vvv_u = suvv @ sv + svv @ suv
457
- R_u = np.array([[R_uuu_u, R_uvu_u, R_vvu_u], [R_uuv_u, R_uvv_u, R_vvv_u]])
458
- Gamma_u = sp.linalg.lu_solve(luAndPivot, R_u - A_u @ Gamma)
459
-
460
- # . . . And the derivatives of the Christoffel symbols with respect to v
461
- E_v = 2.0 * R_uvu
462
- F_v = R_uvv + R_vvu
463
- G_v = 2.0 * R_vvv
464
- A_v = np.array([[E_v, F_v], [F_v, G_v]])
465
- R_uuu_v = suuv @ su + suu @ suv
466
- R_uuv_v = suuv @ sv + suu @ svv
467
- R_uvu_v = suvv @ su + suv @ suv
468
- R_uvv_v = suvv @ sv + suv @ svv
469
- R_vvu_v = svvv @ su + svv @ suv
470
- R_vvv_v = svvv @ sv + svv @ svv
471
- R_v = np.array([[R_uuu_v, R_uvu_v, R_vvu_v], [R_uuv_v, R_uvv_v, R_vvv_v]])
472
- Gamma_v = sp.linalg.lu_solve(luAndPivot, R_v - A_v @ Gamma)
473
603
 
474
604
  # Compute the Jacobian matrix of the right hand side of the ODE
475
605
  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,
@@ -707,7 +837,10 @@ def section(xytk):
707
837
  onePlusCosTheta = 1.0 + math.cos(theta)
708
838
  r0 = 4.0 * startKappa * tangentDistances[0] ** 2 / (3.0 * tangentDistances[1] * crossTangents)
709
839
  r1 = 4.0 * endKappa * tangentDistances[1] ** 2 / (3.0 * tangentDistances[0] * crossTangents)
710
- rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
840
+ if r0 != 0.0 or r1 != 0.0:
841
+ rhoCrit = (math.sqrt(1.0 + 4.0 * (r0 + r1)) - 1.0) / (2.0 * (r0 + r1))
842
+ else:
843
+ rhoCrit = 1.0
711
844
  rhoCritOfTheta = 3.0 * (math.sqrt(1.0 + 32.0 / (3.0 * onePlusCosTheta)) - 1.0) * onePlusCosTheta / 16.0
712
845
 
713
846
  # Determine quadratic polynomial
@@ -797,11 +930,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
797
930
  for knot0, knot1 in zip(currentGuess.knots[0][:-1], currentGuess.knots[0][1:]):
798
931
  if knot0 < knot1:
799
932
  for gaussNode in gaussNodes:
800
- alpha = 0.5 * (1.0 - gaussNode)
801
- collocationPoints.append((1.0 - alpha) * knot0 + alpha * knot1)
802
- if abs(gaussNode) > 1.0e-5:
803
- collocationPoints.append(alpha * knot0 + (1.0 - alpha) * knot1)
804
- collocationPoints = np.sort(collocationPoints)
933
+ collocationPoints.append((1.0 - gaussNode) * knot0 + gaussNode * knot1)
805
934
  n = nDep * currentGuess.nCoef[0]
806
935
  bestGuess = np.reshape(currentGuess.coefs.T, (n,))
807
936
 
@@ -826,7 +955,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
826
955
  done = linear
827
956
  continuation = 1.0
828
957
  bestContinuation = 0.0
829
- continuationBest = bestGuess
958
+ inCaseOfEmergency = bestGuess.copy()
830
959
  previous = 0.5 * np.finfo(bestGuess[0]).max
831
960
  iteration = 0
832
961
 
@@ -869,9 +998,10 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
869
998
  update = sp.linalg.solve_banded((bandWidth, bandWidth), collocationMatrix, residuals)
870
999
  bestGuess[nDep * (iFirstPoint + nLeft) : nDep * (iNextPoint + nLeft)] += update[nDep * nLeft : nDep * (iNextPoint - iFirstPoint + nLeft)]
871
1000
  updateSize = np.linalg.norm(update)
872
- if updateSize > 1.25 * previous and iteration >= 4:
1001
+ if updateSize > 1.25 * previous and iteration >= 4 or \
1002
+ updateSize > 0.01 and iteration > 50:
873
1003
  continuation = 0.5 * (continuation + bestContinuation)
874
- bestGuess = continuationBest
1004
+ bestGuess = inCaseOfEmergency.copy()
875
1005
  if continuation - bestContinuation < 0.01:
876
1006
  break
877
1007
  previous = 0.5 * np.finfo(bestGuess[0]).max
@@ -886,7 +1016,7 @@ def solve_ode(self, nLeft, nRight, FAndF_u, tolerance = 1.0e-6, args = ()):
886
1016
  if updateSize < math.sqrt(n) * scale * math.sqrt(np.finfo(update.dtype).eps):
887
1017
  if continuation < 1.0:
888
1018
  bestContinuation = continuation
889
- continuationBest = bestGuess
1019
+ inCaseOfEmergency = bestGuess.copy()
890
1020
  continuation = min(1.0, 1.2 * continuation)
891
1021
  previous = 0.5 * np.finfo(bestGuess[0]).max
892
1022
  iteration = 0