bspy 4.1__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/__init__.py CHANGED
@@ -12,6 +12,8 @@ Available subpackages
12
12
  `bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
13
13
  piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
14
14
 
15
+ `bspy.spline_block` : Provides the `SplineBlock` class that represents and processes an array-like collection of splines.
16
+
15
17
  `bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
16
18
 
17
19
  `bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
@@ -22,5 +24,6 @@ from bspy.solid import Solid, Boundary
22
24
  from bspy.manifold import Manifold
23
25
  from bspy.hyperplane import Hyperplane
24
26
  from bspy.spline import Spline
27
+ from bspy.spline_block import SplineBlock
25
28
  from bspy.splineOpenGLFrame import SplineOpenGLFrame
26
29
  from bspy.viewer import Viewer, Graphics
bspy/_spline_domain.py CHANGED
@@ -518,6 +518,66 @@ def reverse(self, variable = 0):
518
518
  newFolded = type(self)(folded.nInd, folded.nDep, folded.order, folded.nCoef, (newKnots,), newCoefs, folded.metadata)
519
519
  return newFolded.unfold(myIndices, basisInfo)
520
520
 
521
+ def split(self, minContinuity = 0, breaks = None):
522
+ if minContinuity < 0: raise ValueError("minContinuity must be >= 0")
523
+ if breaks is not None and len(breaks) != self.nInd: raise ValueError("Invalid breaks")
524
+ if self.nInd < 1: return self
525
+
526
+ # Step 1: Determine the knots to insert.
527
+ newKnotsList = []
528
+ for i, order, knots in zip(range(self.nInd), self.order, self.knots):
529
+ unique, counts = np.unique(knots, return_counts=True)
530
+ newKnots = []
531
+ for knot, count in zip(unique, counts):
532
+ assert count <= order
533
+ if count > order - 1 - minContinuity:
534
+ newKnots += [knot] * (order - count)
535
+ if breaks is not None:
536
+ for knot in breaks[i]:
537
+ if knot not in unique:
538
+ newKnots += [knot] * order
539
+ newKnotsList.append(newKnots)
540
+
541
+ # Step 2: Insert the knots.
542
+ spline = self.insert_knots(newKnotsList)
543
+ if spline is self:
544
+ return np.full((1,) * spline.nInd, spline)
545
+
546
+ # Step 3: Store the indices of the full order knots.
547
+ indexList = []
548
+ splineCount = []
549
+ totalSplineCount = 1
550
+ for order, knots in zip(spline.order, spline.knots):
551
+ unique, counts = np.unique(knots, return_counts=True)
552
+ indices = np.searchsorted(knots, unique)
553
+ fullOrder = []
554
+ for ix, count in zip(indices, counts):
555
+ if count == order:
556
+ fullOrder.append(ix)
557
+ indexList.append(fullOrder)
558
+ splines = len(fullOrder) - 1
559
+ splineCount.append(splines)
560
+ totalSplineCount *= splines
561
+
562
+ # Step 4: Slice up the spline.
563
+ splineArray = np.empty(totalSplineCount, object)
564
+ for i in range(totalSplineCount):
565
+ knotsList = []
566
+ coefIndex = [slice(None)] # First index is for nDep
567
+ ix = i
568
+ for order, knots, splines, indices in zip(spline.order, spline.knots, splineCount, indexList):
569
+ j = ix % splines
570
+ ix = ix // splines
571
+ leftIndex = indices[j]
572
+ rightIndex = indices[j + 1]
573
+ knotsList.append(knots[leftIndex:rightIndex + order])
574
+ coefIndex.append(slice(leftIndex, rightIndex))
575
+ coefs = spline.coefs[tuple(coefIndex)]
576
+ splineArray[i] = type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
577
+
578
+ # Return the transpose because we put the splines into splineArray dimensions in reverse order.
579
+ return splineArray.reshape(tuple(reversed(splineCount))).T
580
+
521
581
  def transpose(self, axes=None):
522
582
  if axes is None:
523
583
  axes = range(self.nInd)[::-1]
@@ -535,10 +595,12 @@ def transpose(self, axes=None):
535
595
  def trim(self, newDomain):
536
596
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
537
597
  if self.nInd < 1: return self
538
- newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
598
+ newDomain = np.array(newDomain, self.knots[0].dtype, copy=True) # Force dtype and convert None to nan
599
+ epsilon = np.finfo(newDomain.dtype).eps
539
600
 
540
601
  # Step 1: Determine the knots to insert at the new domain bounds.
541
602
  newKnotsList = []
603
+ noChange = True
542
604
  for (order, knots, bounds) in zip(self.order, self.knots, newDomain):
543
605
  if not(len(bounds) == 2): raise ValueError("Invalid newDomain")
544
606
  unique, counts = np.unique(knots, return_counts=True)
@@ -548,28 +610,49 @@ def trim(self, newDomain):
548
610
  if not np.isnan(bounds[0]):
549
611
  if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
550
612
  leftBound = True
551
- multiplicity = order
552
613
  i = np.searchsorted(unique, bounds[0])
553
- if unique[i] == bounds[0]:
554
- multiplicity -= counts[i]
614
+ if unique[i] - bounds[0] < epsilon:
615
+ bounds[0] = unique[i]
616
+ multiplicity = order - counts[i]
617
+ if i > 0:
618
+ noChange = False
619
+ elif i > 0 and bounds[0] - unique[i - 1] < epsilon:
620
+ bounds[0] = unique[i - 1]
621
+ multiplicity = order - counts[i - 1]
622
+ if i - 1 > 0:
623
+ noChange = False
624
+ else:
625
+ multiplicity = order
626
+
555
627
  newKnots += multiplicity * [bounds[0]]
556
628
 
557
629
  if not np.isnan(bounds[1]):
558
630
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
559
631
  if leftBound:
560
632
  if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
561
- multiplicity = order
562
633
  i = np.searchsorted(unique, bounds[1])
563
- if unique[i] == bounds[1]:
564
- multiplicity -= counts[i]
634
+ if unique[i] - bounds[1] < epsilon:
635
+ bounds[1] = unique[i]
636
+ multiplicity = order - counts[i]
637
+ if i < len(unique) - 1:
638
+ noChange = False
639
+ elif i > 0 and bounds[1] - unique[i - 1] < epsilon:
640
+ bounds[1] = unique[i - 1]
641
+ multiplicity = order - counts[i - i]
642
+ noChange = False # i < len(unique) - 1
643
+ else:
644
+ multiplicity = order
565
645
  newKnots += multiplicity * [bounds[1]]
566
646
 
567
647
  newKnotsList.append(newKnots)
648
+ if len(newKnots) > 0:
649
+ noChange = False
650
+
651
+ if noChange:
652
+ return self
568
653
 
569
654
  # Step 2: Insert the knots.
570
655
  spline = self.insert_knots(newKnotsList)
571
- if spline is self:
572
- return spline
573
656
 
574
657
  # Step 3: Trim the knots and coefficients.
575
658
  knotsList = []
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ import scipy as sp
2
3
 
3
4
  def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
4
5
  basis = np.zeros(splineOrder, knots.dtype)
@@ -25,10 +26,61 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
25
26
  b += 1
26
27
  return knot, basis
27
28
 
29
+ def composed_integral(self, integrand = None, domain = None):
30
+ # Determine domain and check its validity
31
+ actualDomain = self.domain()
32
+ if domain is None:
33
+ domain = actualDomain
34
+ else:
35
+ for iInd in range(self.nInd):
36
+ if domain[iInd, 0] < actualDomain[iInd, 0] or \
37
+ domain[iInd, 1] > actualDomain[iInd, 1]:
38
+ raise ValueError("Can't integrate beyond the domain of the spline")
39
+
40
+ # Determine breakpoints for quadrature intervals; require functions to be analytic
41
+
42
+ uniqueKnots = []
43
+ for iInd in range(self.nInd):
44
+ iStart = np.searchsorted(self.knots[iInd], domain[iInd, 0], side = 'right')
45
+ iEnd = np.searchsorted(self.knots[iInd], domain[iInd, 1], side = 'right')
46
+ uniqueKnots.append(np.unique(np.insert(self.knots[iInd], [iStart, iEnd], domain[iInd])[iStart : iEnd + 2]))
47
+
48
+ # Set integrand function if none is given
49
+ if integrand is None:
50
+ integrand = lambda x : 1.0
51
+
52
+ # Set tolerance
53
+ tolerance = 1.0e-13 / self.nInd
54
+
55
+ # Establish the callback function
56
+ def composedIntegrand(u, nIndSoFar, uValues):
57
+ uValues[nIndSoFar] = u
58
+ nIndSoFar += 1
59
+ if self.nInd == nIndSoFar:
60
+ total = integrand(self(uValues)) * \
61
+ np.prod(np.linalg.svd(self.jacobian(uValues), compute_uv = False))
62
+ else:
63
+ total = 0.0
64
+ for ix in range(len(uniqueKnots[nIndSoFar]) - 1):
65
+ value = sp.integrate.quad(composedIntegrand, uniqueKnots[nIndSoFar][ix],
66
+ uniqueKnots[nIndSoFar][ix + 1], (nIndSoFar, uValues),
67
+ epsabs = tolerance, epsrel = tolerance)
68
+ total += value[0]
69
+ return total
70
+
71
+ # Compute the value by calling the callback routine
72
+ total = composedIntegrand(0.0, -1, self.nInd * [0.0])
73
+ return total
74
+
75
+ def continuity(self):
76
+ multiplicity = np.array([np.max(np.unique(knots, return_counts = True)[1][1 : -1]) for knots in self.knots])
77
+ continuity = self.order - multiplicity - 1
78
+ return continuity
79
+
28
80
  def curvature(self, uv):
81
+ if self.nDep == 1:
82
+ self = self.graph()
29
83
  if self.nInd == 1:
30
- if self.nDep == 1:
31
- self = self.graph()
32
84
  fp = self.derivative([1], uv)
33
85
  fpp = self.derivative([2], uv)
34
86
  fpDotFp = fp @ fp
@@ -38,7 +90,21 @@ def curvature(self, uv):
38
90
  numerator = fp[0] * fpp[1] - fp[1] * fpp[0]
39
91
  else:
40
92
  numerator = np.sqrt((fpp @ fpp) * fpDotFp - fpDotFpp ** 2)
41
- return numerator / denom
93
+ return numerator / denom
94
+ if self.nInd == 2:
95
+ su = self.derivative([1, 0], uv)
96
+ sv = self.derivative([0, 1], uv)
97
+ normal = self.normal(uv)
98
+ suu = self.derivative([2, 0], uv)
99
+ suv = self.derivative([1, 1], uv)
100
+ svv = self.derivative([0, 2], uv)
101
+ E = su @ su
102
+ F = su @ sv
103
+ G = sv @ sv
104
+ L = suu @ normal
105
+ M = suv @ normal
106
+ N = svv @ normal
107
+ return (L * N - M ** 2) / (E * G - F ** 2)
42
108
 
43
109
  def derivative(self, with_respect_to, uvw):
44
110
  # Make work for scalar valued functions
@@ -107,6 +173,8 @@ def greville(self, ind = 0):
107
173
  for ix in range(1, self.order[ind]):
108
174
  knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
109
175
  knotAverages /= (self.order[ind] - 1)
176
+ domain = self.domain()[ind]
177
+ knotAverages = np.minimum(domain[1], np.maximum(domain[0], knotAverages))
110
178
  return knotAverages
111
179
 
112
180
  def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
@@ -150,8 +218,8 @@ def normal(self, uvw, normalize=True, indices=None):
150
218
 
151
219
  if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be one different than the number of dependent variables.")
152
220
 
153
- # Evaluate the tangents at the point.
154
- tangentSpace = self.tangent_space(uvw)
221
+ # Evaluate the Jacobian at the point.
222
+ tangentSpace = self.jacobian(uvw)
155
223
 
156
224
  # Record the larger dimension and ensure it comes first.
157
225
  if self.nInd > self.nDep:
@@ -161,15 +229,15 @@ def normal(self, uvw, normalize=True, indices=None):
161
229
  nDep = self.nDep
162
230
 
163
231
  # Compute the normal using cofactors (determinants of subsets of the tangent space).
164
- sign = -1 if self.metadata.get("flipNormal", False) else 1
232
+ sign = -1 if hasattr(self, "metadata") and self.metadata.get("flipNormal", False) else 1
233
+ dtype = self.coefs.dtype if hasattr(self, "coefs") else self.coefsDtype
165
234
  if indices is None:
166
235
  indices = range(nDep)
167
- normal = np.empty(nDep, self.coefs.dtype)
236
+ normal = np.empty(nDep, dtype)
168
237
  else:
169
- normal = np.empty(len(indices), self.coefs.dtype)
238
+ normal = np.empty(len(indices), dtype)
170
239
  for i in indices:
171
- normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
172
- sign *= -1
240
+ normal[i] = sign * ((-1) ** i) * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
173
241
 
174
242
  # Normalize the result as needed.
175
243
  if normalize:
@@ -180,13 +248,4 @@ def normal(self, uvw, normalize=True, indices=None):
180
248
  def range_bounds(self):
181
249
  # Assumes self.nDep is the first value in self.coefs.shape
182
250
  bounds = [[coefficient.min(), coefficient.max()] for coefficient in self.coefs]
183
- return np.array(bounds, self.coefs.dtype)
184
-
185
- def tangent_space(self, uvw):
186
- tangentSpace = np.empty((self.nDep, self.nInd), self.coefs.dtype)
187
- wrt = [0] * self.nInd
188
- for i in range(self.nInd):
189
- wrt[i] = 1
190
- tangentSpace[:, i] = self.derivative(wrt, uvw)
191
- wrt[i] = 0
192
- return tangentSpace
251
+ return np.array(bounds, self.coefs.dtype)
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)