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/__init__.py CHANGED
@@ -1,19 +1,26 @@
1
1
  """
2
- bspy is a python library for manipulating and rendering non-uniform B-splines.
2
+ BSpy is a python library for manipulating and rendering non-uniform B-splines.
3
3
 
4
4
  Available subpackages
5
5
  ---------------------
6
- `bspy.spline` : A class to model, represent, and process piecewise polynomial tensor product
7
- functions (spline functions) as linear combinations of B-splines.
6
+ `bspy.solid` : Provides the `Solid` and `Boundary` classes that model solids.
8
7
 
9
- `bspy.drawableSpline` : A `Spline` that can be drawn within a `SplineOpenGLFrame`.
8
+ `bspy.manifold` : Provides the `Manifold` base class for manifolds.
10
9
 
11
- `bspy.splineOpenGLFrame` : A tkinter `OpenGLFrame` with shaders to display a `DrawableSpline` list.
10
+ `bspy.hyperplane` : Provides the `Hyperplane` subclass of `Manifold` that models hyperplanes.
12
11
 
13
- `bspy.bspyApp` : A tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
14
- splines, and a set of controls to adjust and view the selected splines.
12
+ `bspy.spline` : Provides the `Spline` subclass of `Manifold` that models, represents, and processes
13
+ piecewise polynomial tensor product functions (spline functions) as linear combinations of B-splines.
14
+
15
+ `bspy.splineOpenGLFrame` : Provides the `SplineOpenGLFrame` class, a tkinter `OpenGLFrame` with shaders to display splines.
16
+
17
+ `bspy.viewer` : Provides the `Viewer` tkinter app (`tkinter.Tk`) that hosts a `SplineOpenGLFrame`, a listbox full of
18
+ splines, and a set of controls to adjust and view the selected splines. It also provides the `Graphics` engine that creates
19
+ an associated `Viewer`, allowing you to script splines and display them in the viewer.
15
20
  """
21
+ from bspy.solid import Solid, Boundary
22
+ from bspy.manifold import Manifold
23
+ from bspy.hyperplane import Hyperplane
16
24
  from bspy.spline import Spline
17
- from bspy.drawableSpline import DrawableSpline
18
25
  from bspy.splineOpenGLFrame import SplineOpenGLFrame
19
- from bspy.bspyApp import bspyApp, bspyGraphics
26
+ from bspy.viewer import Viewer, Graphics
bspy/_spline_domain.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ from bspy.manifold import Manifold
2
3
 
3
4
  def clamp(self, left, right):
4
5
  bounds = [[None, None] for i in range(self.nInd)]
@@ -12,10 +13,15 @@ def clamp(self, left, right):
12
13
  return self.trim(bounds)
13
14
 
14
15
  def common_basis(splines, indMap):
15
- # Step 1: Compute the order for each aligned independent variable.
16
- orders = []
16
+ # Fill out the default indMap.
17
17
  if indMap is None:
18
18
  indMap = [len(splines) * [iInd] for iInd in range(splines[0].nInd)]
19
+
20
+ # Ensure all splines are clamped at both ends.
21
+ splines = [spline.clamp(tuple(range(spline.nInd)), tuple(range(spline.nInd))) for spline in splines]
22
+
23
+ # Step 1: Compute the order for each aligned independent variable.
24
+ orders = []
19
25
  for map in indMap:
20
26
  if not(len(map) == len(splines)): raise ValueError("Invalid map")
21
27
  order = 0
@@ -269,7 +275,7 @@ def extrapolate(self, newDomain, continuityOrder):
269
275
  # Swap dependent and independent variables back.
270
276
  dCoefs = dCoefs.swapaxes(1, ind + 2)
271
277
 
272
- return type(self)(self.nInd, self.nDep, self.order, nCoef, knots, dCoefs[0], self.metadata)
278
+ return type(self)(self.nInd, self.nDep, self.order, nCoef, knots, dCoefs[0], self.metadata).remove_knots()
273
279
 
274
280
  def fold(self, foldedInd):
275
281
  if not(0 <= len(foldedInd) <= self.nInd): raise ValueError("Invalid foldedInd")
@@ -306,29 +312,29 @@ def fold(self, foldedInd):
306
312
 
307
313
  def insert_knots(self, newKnots):
308
314
  if not(len(newKnots) == self.nInd): raise ValueError("Invalid newKnots")
309
- knots = list(self.knots)
315
+ knotsList = list(self.knots)
310
316
  coefs = self.coefs
311
- for ind in range(self.nInd):
317
+ for ind, (order, knots) in enumerate(zip(self.order, self.knots)):
312
318
  # We can't reference self.nCoef[ind] in this loop because we are expanding the knots and coefs arrays.
313
319
  for knot in newKnots[ind]:
314
- if knot < knots[ind][self.order[ind]-1] or knot > knots[ind][-self.order[ind]]:
320
+ if knot < knots[order-1] or knot > knots[-order]:
315
321
  raise ValueError(f"Knot insertion outside domain: {knot}")
316
- if knot == knots[ind][-self.order[ind]]:
317
- position = len(knots[ind]) - self.order[ind]
322
+ if knot == knots[-order]:
323
+ position = len(knots) - order
318
324
  else:
319
- position = np.searchsorted(knots[ind], knot, 'right')
325
+ position = np.searchsorted(knots, knot, 'right')
320
326
  coefs = coefs.swapaxes(0, ind + 1) # Swap dependent and independent variable (swap back later)
321
327
  newCoefs = np.insert(coefs, position - 1, 0.0, axis=0)
322
- for i in range(position - self.order[ind] + 1, position):
323
- alpha = (knot - knots[ind][i]) / (knots[ind][i + self.order[ind] - 1] - knots[ind][i])
328
+ for i in range(position - order + 1, position):
329
+ alpha = (knot - knots[i]) / (knots[i + order - 1] - knots[i])
324
330
  newCoefs[i] = (1.0 - alpha) * coefs[i - 1] + alpha * coefs[i]
325
- knots[ind] = np.insert(knots[ind], position, knot)
331
+ knotsList[ind] = knots = np.insert(knots, position, knot)
326
332
  coefs = newCoefs.swapaxes(0, ind + 1)
327
333
 
328
334
  if self.coefs is coefs:
329
335
  return self
330
336
  else:
331
- return type(self)(self.nInd, self.nDep, self.order, coefs.shape[1:], knots, coefs, self.metadata)
337
+ return type(self)(self.nInd, self.nDep, self.order, coefs.shape[1:], knotsList, coefs, self.metadata)
332
338
 
333
339
  def join(splineList):
334
340
  # Make sure all the splines in the list are curves
@@ -368,13 +374,13 @@ def join(splineList):
368
374
  newKnots, newCoefs, workingSpline.metadata)
369
375
  return workingSpline.reparametrize([[0.0, 1.0]]).remove_knots()
370
376
 
371
- def remove_knot(self, iKnot):
377
+ def remove_knot(self, iKnot, nLeft = 0, nRight = 0):
372
378
  if self.nInd != 1: raise ValueError("Must have one independent variable")
373
379
  myOrder = self.order[0]
374
380
  if iKnot < myOrder or iKnot >= self.nCoef[0]: raise ValueError("Must specify interior knots for removal")
375
381
  diag0 = []
376
- diag1 = []
377
- rhs = []
382
+ diag1 = [1.0]
383
+ rhs = [np.ndarray.copy(self.coefs[:, iKnot - myOrder])]
378
384
  myKnots = self.knots[0]
379
385
  thisKnot = myKnots[iKnot]
380
386
 
@@ -384,45 +390,57 @@ def remove_knot(self, iKnot):
384
390
  diag0.append(alpha)
385
391
  diag1.append(1.0 - alpha)
386
392
  rhs.append(np.ndarray.copy(self.coefs[:, iKnot - myOrder + ix]))
387
-
388
- # Adjust right hand side because first and last coefficients are known
393
+ diag0.append(1.0)
394
+ diag1.append(0.0)
395
+ rhs.append(np.ndarray.copy(self.coefs[:, iKnot]))
389
396
  rhs = np.array(rhs)
390
- rhs[0] -= diag0[0] * self.coefs[:, iKnot - myOrder]
391
- rhs[-1] -= diag1[-1] * self.coefs[:, iKnot]
397
+
398
+ # Take care of the extra known conditions on the left
399
+ extraLeft = max(0, nLeft - iKnot + myOrder)
400
+ for ix in range(extraLeft):
401
+ rhs[ix] /= diag1[ix]
402
+ rhs[ix + 1] -= diag0[ix] * rhs[ix]
403
+
404
+ # Take care of the extra known conditions on the right
405
+ extraRight = max(0, nRight - self.nCoef[0] + iKnot + 1)
406
+ for ix in range(extraRight):
407
+ rhs[-1 - ix] /= diag0[-1 - ix]
408
+ rhs[-2 - ix] -= diag1[-2 - ix] * rhs[-1 - ix]
392
409
 
393
410
  # Use Givens rotations to factor the matrix and track right hand side
394
- for ix in range(1, myOrder - 1):
395
- cos = diag1[ix - 1]
411
+ for ix in range(extraLeft, myOrder - extraRight):
412
+ cos = diag1[ix]
396
413
  sin = diag0[ix]
397
414
  denom = np.sqrt(cos ** 2 + sin ** 2)
398
415
  cos /= denom
399
416
  sin /= denom
400
- diag1[ix - 1] = denom
401
- diag0[ix - 1] = sin * diag1[ix]
402
- diag1[ix] *= cos
403
- tempRow = cos * rhs[ix - 1] + sin * rhs[ix]
404
- rhs[ix] = cos * rhs[ix] - sin * rhs[ix - 1]
405
- rhs[ix - 1] = tempRow
417
+ diag1[ix] = denom
418
+ diag0[ix] = sin * diag1[ix + 1]
419
+ diag1[ix + 1] *= cos
420
+ tempRow = cos * rhs[ix] + sin * rhs[ix + 1]
421
+ rhs[ix + 1] = cos * rhs[ix + 1] - sin * rhs[ix]
422
+ rhs[ix] = tempRow
406
423
 
407
424
  # Perform back substitution
408
- rhs[-2] /= diag1[-2]
409
- for ix in range(1, myOrder - 2):
410
- rhs[-ix - 2] = (rhs[-ix - 2] - diag0[-ix - 2] * rhs[-ix - 1]) / diag1[-ix - 2]
425
+ for ix in range(1 + extraRight, myOrder - extraLeft):
426
+ rhs[-1 - ix] /= diag1[-1 - ix]
427
+ rhs[-2 - ix] -= diag0[-1 - ix] * rhs[-1 - ix]
428
+ rhs[-1 - myOrder + extraLeft] /= diag1[-1 - myOrder + extraLeft]
429
+
430
+ # Save residual and adjust solution
431
+ residual = abs(rhs[-1 - extraRight])
432
+ for ix in range(extraRight):
433
+ rhs[-1 - extraRight+ ix] = rhs[-extraRight + ix]
411
434
 
412
435
  # Create new spline
413
436
  newNCoef = [self.nCoef[0] - 1]
414
- newKnots = [list(self.knots[0][:iKnot]) + list(self.knots[0][iKnot + 1:])]
415
- newCoefs = []
416
- for ix in range(self.nDep):
417
- oldCoefs = self.coefs[ix]
418
- firstCoefs = oldCoefs[:iKnot - self.order[0] + 1]
419
- lastCoefs = oldCoefs[iKnot:]
420
- middleCoefs = rhs[:-1,ix]
421
- newCoefs.append(list(firstCoefs) + list(middleCoefs) + list(lastCoefs))
437
+ newKnots = [np.delete(self.knots[0], iKnot)]
438
+ newCoefs = np.delete(self.coefs, iKnot - self.order[0] + 1, 1)
439
+ newCoefs[: , iKnot - self.order[0] : iKnot] = rhs[: -1].T
422
440
  withoutKnot = type(self)(self.nInd, self.nDep, self.order, newNCoef, newKnots, newCoefs)
423
- return withoutKnot, abs(rhs[-1])
441
+ return withoutKnot, residual
424
442
 
425
- def remove_knots(self, tolerance):
443
+ def remove_knots(self, tolerance, nLeft = 0, nRight = 0):
426
444
  scaleDep = [max(np.abs(bound[0]), np.abs(bound[1])) for bound in self.range_bounds()]
427
445
  scaleDep = [1.0 if factor == 0.0 else factor for factor in scaleDep]
428
446
  rScaleDep = np.array([1.0 / factor for factor in scaleDep])
@@ -435,12 +453,20 @@ def remove_knots(self, tolerance):
435
453
  currentFold, foldedBasis = currentSpline.fold(foldedIndices)
436
454
  while True:
437
455
  bestError = np.finfo(scaleDep[0].dtype).max
438
- for ix in range(currentFold.order[0], currentFold.nCoef[0]):
439
- newSpline, residual = currentFold.remove_knot(ix)
456
+ bestSpline = currentFold
457
+ ix = currentFold.order[0]
458
+ while ix < currentFold.nCoef[0]:
459
+ newSpline, residual = currentFold.remove_knot(ix, nLeft, nRight)
440
460
  error = np.max(residual)
461
+ if error < 0.001 * tolerance:
462
+ currentFold = newSpline
463
+ continue
441
464
  if error < bestError:
442
465
  bestError = error
443
466
  bestSpline = newSpline
467
+ ix += 1
468
+ if currentFold.nCoef[0] < bestSpline.nCoef[0]:
469
+ continue
444
470
  if bestError > tolerance:
445
471
  break
446
472
  errorSpline = truthSpline - bestSpline.unfold(foldedIndices, foldedBasis)
@@ -508,6 +534,8 @@ def transpose(self, axes=None):
508
534
 
509
535
  def trim(self, newDomain):
510
536
  if not(len(newDomain) == self.nInd): raise ValueError("Invalid newDomain")
537
+ if self.nInd < 1: return self
538
+ newDomain = np.array(newDomain, self.knots[0].dtype) # Force dtype and convert None to nan
511
539
 
512
540
  # Step 1: Determine the knots to insert at the new domain bounds.
513
541
  newKnotsList = []
@@ -517,7 +545,7 @@ def trim(self, newDomain):
517
545
  leftBound = False # Do we have a left bound?
518
546
  newKnots = []
519
547
 
520
- if bounds[0] is not None and not np.isnan(bounds[0]):
548
+ if not np.isnan(bounds[0]):
521
549
  if not(knots[order - 1] <= bounds[0] <= knots[-order]): raise ValueError("Invalid newDomain")
522
550
  leftBound = True
523
551
  multiplicity = order
@@ -526,7 +554,7 @@ def trim(self, newDomain):
526
554
  multiplicity -= counts[i]
527
555
  newKnots += multiplicity * [bounds[0]]
528
556
 
529
- if bounds[1] is not None and not np.isnan(bounds[1]):
557
+ if not np.isnan(bounds[1]):
530
558
  if not(knots[order - 1] <= bounds[1] <= knots[-order]): raise ValueError("Invalid newDomain")
531
559
  if leftBound:
532
560
  if not(bounds[0] < bounds[1]): raise ValueError("Invalid newDomain")
@@ -547,14 +575,22 @@ def trim(self, newDomain):
547
575
  knotsList = []
548
576
  coefIndex = [slice(None)] # First index is for nDep
549
577
  for (order, knots, bounds) in zip(spline.order, spline.knots, newDomain):
550
- leftIndex = 0 if bounds[0] is None or np.isnan(bounds[0]) else np.searchsorted(knots, bounds[0])
551
- rightIndex = len(knots) - order if bounds[1] is None or np.isnan(bounds[1]) else np.searchsorted(knots, bounds[1])
578
+ leftIndex = 0 if np.isnan(bounds[0]) else np.searchsorted(knots, bounds[0])
579
+ rightIndex = len(knots) - order if np.isnan(bounds[1]) else np.searchsorted(knots, bounds[1])
552
580
  knotsList.append(knots[leftIndex:rightIndex + order])
553
581
  coefIndex.append(slice(leftIndex, rightIndex))
554
582
  coefs = spline.coefs[tuple(coefIndex)]
555
583
 
556
584
  return type(spline)(spline.nInd, spline.nDep, spline.order, coefs.shape[1:], knotsList, coefs, spline.metadata)
557
585
 
586
+ def trimmed_range_bounds(self, domainBounds):
587
+ domainBounds = np.array(domainBounds, copy=True)
588
+ for original, trim in zip(self.domain(), domainBounds):
589
+ trim[0] = max(original[0], trim[0] - Manifold.minSeparation)
590
+ trim[1] = min(original[1], trim[1] + Manifold.minSeparation)
591
+ trimmedSpline = self.trim(domainBounds)
592
+ return trimmedSpline, trimmedSpline.range_bounds()
593
+
558
594
  def unfold(self, foldedInd, coefficientlessSpline):
559
595
  if not(len(foldedInd) == coefficientlessSpline.nInd): raise ValueError("Invalid coefficientlessSpline")
560
596
  unfoldedOrder = []
@@ -1,43 +1,12 @@
1
1
  import numpy as np
2
2
 
3
- def blossom(self, uvw):
4
- def blossom_values(knot, knots, order, u):
5
- basis = np.zeros(order, knots.dtype)
6
- basis[-1] = 1.0
7
- for degree in range(1, order):
8
- b = order - degree
9
- for i in range(knot - degree, knot):
10
- alpha = (u[degree - 1] - knots[i]) / (knots[i + degree] - knots[i])
11
- basis[b - 1] += (1.0 - alpha) * basis[b]
12
- basis[b] *= alpha
13
- b += 1
14
- return basis
15
-
16
- # Make work for scalar valued functions
17
- uvw = np.atleast_1d(uvw)
18
-
19
- # Check for evaluation point inside domain
20
- dom = self.domain()
21
- for ix in range(self.nInd):
22
- if uvw[ix][0] < dom[ix][0] or uvw[ix][self.order[ix]-2] > dom[ix][1]:
23
- raise ValueError(f"Spline evaluation outside domain: {uvw}")
24
-
25
- # Grab all of the appropriate coefficients
26
- mySection = [slice(0, self.nDep)]
27
- myIndices = []
28
- for iv in range(self.nInd):
29
- ix = np.searchsorted(self.knots[iv], uvw[iv][0], 'right')
30
- ix = min(ix, self.nCoef[iv])
31
- myIndices.append(ix)
32
- mySection.append(slice(ix - self.order[iv], ix))
33
- myCoefs = self.coefs[tuple(mySection)]
34
- for iv in range(self.nInd - 1, -1, -1):
35
- bValues = blossom_values(myIndices[iv], self.knots[iv], self.order[iv], uvw[iv])
36
- myCoefs = myCoefs @ bValues
37
- return myCoefs
38
-
39
3
  def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs = False):
40
4
  basis = np.zeros(splineOrder, knots.dtype)
5
+ if knot is None:
6
+ knot = np.searchsorted(knots, u, side = 'right')
7
+ knot = min(knot, len(knots) - splineOrder)
8
+ if derivativeOrder >= splineOrder:
9
+ return knot, basis
41
10
  basis[-1] = 1.0
42
11
  for degree in range(1, splineOrder - derivativeOrder):
43
12
  b = splineOrder - degree
@@ -54,7 +23,7 @@ def bspline_values(knot, knots, splineOrder, u, derivativeOrder = 0, taylorCoefs
54
23
  basis[b - 1] += -alpha * basis[b]
55
24
  basis[b] *= alpha
56
25
  b += 1
57
- return basis
26
+ return knot, basis
58
27
 
59
28
  def curvature(self, uv):
60
29
  if self.nInd == 1:
@@ -87,16 +56,14 @@ def derivative(self, with_respect_to, uvw):
87
56
 
88
57
  # Grab all of the appropriate coefficients
89
58
  mySection = [slice(0, self.nDep)]
90
- myIndices = []
59
+ bValues = []
91
60
  for iv in range(self.nInd):
92
- ix = np.searchsorted(self.knots[iv], uvw[iv], 'right')
93
- ix = min(ix, self.nCoef[iv])
94
- myIndices.append(ix)
61
+ ix, indValues = bspline_values(None, self.knots[iv], self.order[iv], uvw[iv], with_respect_to[iv])
62
+ bValues.append(indValues)
95
63
  mySection.append(slice(ix - self.order[iv], ix))
96
64
  myCoefs = self.coefs[tuple(mySection)]
97
65
  for iv in range(self.nInd - 1, -1, -1):
98
- bValues = bspline_values(myIndices[iv], self.knots[iv], self.order[iv], uvw[iv], with_respect_to[iv])
99
- myCoefs = myCoefs @ bValues
66
+ myCoefs = myCoefs @ bValues[iv]
100
67
  return myCoefs
101
68
 
102
69
  def domain(self):
@@ -120,18 +87,28 @@ def evaluate(self, uvw):
120
87
 
121
88
  # Grab all of the appropriate coefficients
122
89
  mySection = [slice(0, self.nDep)]
123
- myIndices = []
90
+ bValues = []
124
91
  for iv in range(self.nInd):
125
- ix = np.searchsorted(self.knots[iv], uvw[iv], 'right')
126
- ix = min(ix, self.nCoef[iv])
127
- myIndices.append(ix)
92
+ ix, indValues = bspline_values(None, self.knots[iv], self.order[iv], uvw[iv])
93
+ bValues.append(indValues)
128
94
  mySection.append(slice(ix - self.order[iv], ix))
129
95
  myCoefs = self.coefs[tuple(mySection)]
130
96
  for iv in range(self.nInd - 1, -1, -1):
131
- bValues = bspline_values(myIndices[iv], self.knots[iv], self.order[iv], uvw[iv])
132
- myCoefs = myCoefs @ bValues
97
+ myCoefs = myCoefs @ bValues[iv]
133
98
  return myCoefs
134
99
 
100
+ def greville(self, ind = 0):
101
+ if ind < 0 or ind >= self.nInd: raise ValueError("Invalid independent variable")
102
+ myKnots = self.knots[ind]
103
+ knotAverages = 0
104
+ if self.order[ind] == 1:
105
+ knotAverages = 0.5 * (myKnots[1:] + myKnots[:-1])
106
+ else:
107
+ for ix in range(1, self.order[ind]):
108
+ knotAverages = knotAverages + myKnots[ix : ix + self.nCoef[ind]]
109
+ knotAverages /= (self.order[ind] - 1)
110
+ return knotAverages
111
+
135
112
  def integral(self, with_respect_to, uvw1, uvw2, returnSpline = False):
136
113
  # Make work for scalar valued functions
137
114
  uvw1 = np.atleast_1d(uvw1)
@@ -174,28 +151,24 @@ def normal(self, uvw, normalize=True, indices=None):
174
151
  if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be one different than the number of dependent variables.")
175
152
 
176
153
  # Evaluate the tangents at the point.
177
- tangentSpace = np.empty((self.nInd, self.nDep), self.coefs.dtype)
178
- with_respect_to = [0] * self.nInd
179
- for i in range(self.nInd):
180
- with_respect_to[i] = 1
181
- tangentSpace[i] = self.derivative(with_respect_to, uvw)
182
- with_respect_to[i] = 0
154
+ tangentSpace = self.tangent_space(uvw)
183
155
 
184
- # If self.nInd > self.nDep, transpose the tangent space and adjust the length of the normal.
185
- nDep = self.nDep
186
- if self.nInd > nDep:
187
- tangentSpace = tangentSpace.T
156
+ # Record the larger dimension and ensure it comes first.
157
+ if self.nInd > self.nDep:
188
158
  nDep = self.nInd
159
+ tangentSpace = tangentSpace.T
160
+ else:
161
+ nDep = self.nDep
189
162
 
190
163
  # Compute the normal using cofactors (determinants of subsets of the tangent space).
191
- sign = 1
164
+ sign = -1 if self.metadata.get("flipNormal", False) else 1
192
165
  if indices is None:
193
166
  indices = range(nDep)
194
167
  normal = np.empty(nDep, self.coefs.dtype)
195
168
  else:
196
169
  normal = np.empty(len(indices), self.coefs.dtype)
197
170
  for i in indices:
198
- normal[i] = sign * np.linalg.det(np.delete(tangentSpace, i, 1))
171
+ normal[i] = sign * np.linalg.det(tangentSpace[[j for j in range(nDep) if i != j]])
199
172
  sign *= -1
200
173
 
201
174
  # Normalize the result as needed.
@@ -207,4 +180,13 @@ def normal(self, uvw, normalize=True, indices=None):
207
180
  def range_bounds(self):
208
181
  # Assumes self.nDep is the first value in self.coefs.shape
209
182
  bounds = [[coefficient.min(), coefficient.max()] for coefficient in self.coefs]
210
- return np.array(bounds, self.coefs.dtype)
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