bspy 4.1__py3-none-any.whl → 4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,47 +13,55 @@ def _shiftPolynomial(polynomial, delta):
13
13
 
14
14
  def add(self, other, indMap = None):
15
15
  if not(self.nDep == other.nDep): raise ValueError("self and other must have same nDep")
16
- selfMapped = []
17
- otherMapped = []
16
+ selfMapped = set()
18
17
  otherToSelf = {}
19
18
  if indMap is not None:
20
19
  (self, other) = bspy.Spline.common_basis((self, other), indMap)
21
20
  for map in indMap:
22
- selfMapped.append(map[0])
23
- otherMapped.append(map[1])
21
+ selfMapped.add(map[0])
24
22
  otherToSelf[map[1]] = map[0]
25
23
 
26
24
  # Construct new spline parameters.
27
- # We index backwards because we're adding transposed coefficients (see below).
28
25
  nInd = self.nInd
29
26
  order = [*self.order]
30
27
  nCoef = [*self.nCoef]
31
28
  knots = list(self.knots)
32
- permutation = [] # Used to transpose coefs to match other.coefs.T.
33
- for i in range(self.nInd - 1, -1, -1):
34
- if i not in selfMapped:
35
- permutation.append(i + 1) # Add 1 to account for dependent variables.
36
- for i in range(other.nInd - 1, -1, -1):
37
- if i not in otherMapped:
38
- order.append(other.order[other.nInd - 1 - i])
39
- nCoef.append(other.nCoef[other.nInd - 1 - i])
40
- knots.append(other.knots[other.nInd - 1 - i])
41
- permutation.append(self.nInd + i + 1) # Add 1 to account for dependent variables.
29
+ for i in range(other.nInd):
30
+ if i not in otherToSelf:
31
+ order.append(other.order[i])
32
+ nCoef.append(other.nCoef[i])
33
+ knots.append(other.knots[i])
42
34
  nInd += 1
43
- else:
44
- permutation.append(otherToSelf[i] + 1) # Add 1 to account for dependent variables.
45
- permutation.append(0) # Account for dependent variables.
46
- permutation = np.array(permutation)
35
+
36
+ # Build coefs array.
47
37
  coefs = np.zeros((self.nDep, *nCoef), self.coefs.dtype)
48
38
 
49
- # Build coefs array by transposing the changing coefficients to the end, including the dependent variables.
50
- # First, add in self.coefs.
39
+ # Add in self.coefs (you need to transpose coefs for the addition to work properly).
51
40
  coefs = coefs.T
52
41
  coefs += self.coefs.T
53
- # Permutation for other.coefs.T accounts for coefs being transposed by subtracting permutation from ndim - 1.
54
- coefs = coefs.transpose((coefs.ndim - 1) - permutation)
55
- # Add in other.coefs.
42
+ coefs = coefs.T
43
+
44
+ # Construct permutation of coefs to transpose coefs to match other.coefs.
45
+ otherUnmappedCount = 0
46
+ permutation = [0] # Account for dependent variables
47
+ for i in range(other.nInd):
48
+ if i in otherToSelf:
49
+ permutation.append(otherToSelf[i] + 1) # Add 1 to account for dependent variables.
50
+ else:
51
+ permutation.append(self.nInd + otherUnmappedCount + 1) # Add 1 to account for dependent variables.
52
+ otherUnmappedCount += 1
53
+ for i in range(self.nInd):
54
+ if i not in selfMapped:
55
+ permutation.append(i + 1) # Add 1 to account for dependent variables.
56
+
57
+ # Permute coefs to match other.coefs
58
+ coefs = coefs.transpose(permutation)
59
+
60
+ # Add in other.coefs (you need to transpose coefs for the addition to work properly).
61
+ coefs = coefs.T
56
62
  coefs += other.coefs.T
63
+ coefs = coefs.T
64
+
57
65
  # Reverse the permutation.
58
66
  coefs = coefs.transpose(np.argsort(permutation))
59
67
 
@@ -105,9 +113,9 @@ def confine(self, range_bounds):
105
113
  intersections.sort(key=lambda intersection: intersection[0])
106
114
 
107
115
  # Remove repeat points at start and end.
108
- if intersections[1][0] - intersections[0][0] < epsilon:
116
+ while intersections[1][0] - intersections[0][0] < epsilon:
109
117
  del intersections[1]
110
- if intersections[-1][0] - intersections[-2][0] < epsilon:
118
+ while intersections[-1][0] - intersections[-2][0] < epsilon:
111
119
  del intersections[-2]
112
120
 
113
121
  # Insert order-1 knots at each intersection point.
@@ -116,9 +124,9 @@ def confine(self, range_bounds):
116
124
  if unique[ix] == knot:
117
125
  count = (order - 1) - counts[ix]
118
126
  if count > 0:
119
- spline = spline.insert_knots(([knot] * count,))
127
+ spline = spline.insert_knots(((knot, count),))
120
128
  else:
121
- spline = spline.insert_knots(([knot] * (order - 1),))
129
+ spline = spline.insert_knots(((knot, order - 1),))
122
130
 
123
131
  # Go through the boundary points, assigning boundary coefficients, interpolating between boundary points,
124
132
  # and removing knots and coefficients where the curve stalls.
@@ -630,79 +638,90 @@ def multiplyAndConvolve(self, other, indMap = None, productType = 'S'):
630
638
 
631
639
  return type(self)(nInd, nDep, order, nCoef, knots, coefs, self.metadata)
632
640
 
633
- # Return a matrix of booleans whose [i,j] value indicates if self's partial wrt variable i depends on variable j.
634
- def _cross_correlation_matrix(self):
635
- ccm = np.empty((self.nInd, self.nInd), bool)
636
- for i in range(self.nInd - 1):
637
- tangent = self.differentiate(i)
638
- totalCoefs = tangent.coefs.size // tangent.nDep
639
- ccm[i, i] = True
640
- for j in range(i + 1, self.nInd):
641
- coefs = np.moveaxis(tangent.coefs, (0, j + 1), (-1, -2))
642
- coefs = coefs.reshape(totalCoefs // tangent.nCoef[j], tangent.nCoef[j], tangent.nDep)
643
- match = True
644
- for row in coefs:
645
- first = row[0]
646
- for point in row[1:]:
647
- match = np.allclose(point, first)
648
- if not match:
649
- break
650
- if not match:
651
- break
652
- ccm[i, j] = ccm[j, i] = not match
653
- ccm[-1, -1] = True
654
- return ccm
655
-
656
641
  def normal_spline(self, indices=None):
657
- if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be one less than the number of dependent variables.")
642
+ if abs(self.nInd - self.nDep) != 1: raise ValueError("The number of independent variables must be different than the number of dependent variables.")
658
643
 
659
- # Construct order and knots for generalized cross product of the tangent space.
644
+ # Construct order, nCoef, knots, and sample values for generalized cross product of the tangent space.
660
645
  newOrder = []
661
646
  newKnots = []
662
647
  uvwValues = []
663
648
  nCoefs = []
664
649
  totalCoefs = [1]
665
- ccm = _cross_correlation_matrix(self)
666
- for i, (order, knots) in enumerate(zip(self.order, self.knots)):
667
- # First, calculate the order of the normal for this independent variable.
650
+ for nInd in range(self.nInd):
651
+ knots = None
652
+ counts = None
653
+ maxOrder = 0
654
+ maxMap = []
655
+ # First, collect the order, knots, and number of relevant columns for this independent variable.
656
+ for row in self.block:
657
+ for map, spline in row:
658
+ if nInd in map:
659
+ ind = map.index(nInd)
660
+ order = spline.order[ind]
661
+ k, c = np.unique(spline.knots[ind][order-1:spline.nCoef[ind]+1], return_counts=True)
662
+ if knots:
663
+ if maxOrder < order:
664
+ counts += order - maxOrder
665
+ maxOrder = order
666
+ if len(maxMap) < len(map):
667
+ maxMap = map
668
+ for knot, count in zip(k[1:-1], c[1:-1]):
669
+ ix = np.searchsorted(knots, knot)
670
+ if knots[ix] == knot:
671
+ counts[ix] = max(counts[ix], count + maxOrder - order)
672
+ else:
673
+ knots = np.insert(knots, ix, knot)
674
+ counts = np.insert(counts, ix, count + maxOrder - order)
675
+ else:
676
+ knots = k
677
+ counts = c
678
+ maxOrder = order
679
+ maxMap = map
680
+
681
+ break
682
+
683
+ # Next, calculate the order of the normal for this independent variable.
668
684
  # Note that the total order will be one less than usual, because one of
669
685
  # the tangents is the derivative with respect to that independent variable.
670
- newOrd = 0
671
686
  if self.nInd < self.nDep:
672
687
  # If this normal involves all tangents, simply add the degree of each,
673
- # so long as that tangent contains the independent variable.
674
- for j in range(self.nInd):
675
- newOrd += order - 1 if ccm[i, j] else 0
688
+ # so long as that tangent contains the independent variable.
689
+ order = (maxOrder - 1) * len(maxMap)
676
690
  else:
677
691
  # If this normal doesn't involve all tangents, find the max order of
678
692
  # each returned combination (as defined by the indices).
679
- for index in range(self.nInd) if indices is None else indices:
693
+ order = 0
694
+ for index in maxMap if indices is None else indices:
680
695
  # The order will be one larger if this independent variable's tangent is excluded by the index.
681
- ord = 0 if index != i else 1
696
+ ord = 0 if index != nInd else 1
682
697
  # Add the degree of each tangent, so long as that tangent contains the
683
698
  # independent variable and is not excluded by the index.
684
- for j in range(self.nInd):
685
- ord += order - 1 if ccm[i, j] and index != j else 0
686
- newOrd = max(newOrd, ord)
687
- newOrder.append(newOrd)
688
- uniqueKnots, counts = np.unique(knots[order - 1:self.nCoef[i] + 1], return_counts=True)
689
- counts += newOrd - order + 1 # Because we're multiplying all the tangents, the knot elevation is one more
690
- counts[0] = newOrd # But not at the endpoints, which are full order as usual
691
- counts[-1] = newOrd # But not at the endpoints, which are full order as usual
692
- newKnots.append(np.repeat(uniqueKnots, counts))
699
+ for ind in maxMap:
700
+ ord += maxOrder - 1 if index != ind else 0
701
+ order = max(order, ord)
702
+
703
+ # Now, record the order of this independent variable and adjust the knot counts.
704
+ newOrder.append(order)
705
+ counts += order - maxOrder + 1 # Because we're multiplying all the tangents, the knot elevation is one more
706
+ counts[0] = order # But not at the endpoints, which are full order as usual
707
+ counts[-1] = order # But not at the endpoints, which are full order as usual
708
+ newKnots.append(np.repeat(knots, counts))
709
+
693
710
  # Also calculate the total number of coefficients, capturing how it progressively increases, and
694
711
  # using that calculation to span uvw from the starting knot to the end for each variable.
695
712
  nCoef = len(newKnots[-1]) - newOrder[-1]
696
713
  totalCoefs.append(totalCoefs[-1] * nCoef)
697
- knotAverages = bspy.Spline(1, 0, [newOrd], [nCoef], [newKnots[-1]], []).greville()
714
+ knotAverages = bspy.Spline(1, 0, [order], [nCoef], [newKnots[-1]], []).greville()
698
715
  for iKnot in range(1, len(knotAverages) - 1):
699
716
  if knotAverages[iKnot] == knotAverages[iKnot + 1]:
700
717
  knotAverages[iKnot] = 0.5 * (knotAverages[iKnot - 1] + knotAverages[iKnot])
701
718
  knotAverages[iKnot + 1] = 0.5 * (knotAverages[iKnot + 1] + knotAverages[iKnot + 2])
702
719
  uvwValues.append(knotAverages)
703
720
  nCoefs.append(nCoef)
721
+
722
+ # Construct data points for normal.
704
723
  points = []
705
- ijk = [0 for order in self.order]
724
+ ijk = [0] * self.nInd
706
725
  for i in range(totalCoefs[-1]):
707
726
  uvw = [uvwValues[j][k] for j, k in enumerate(ijk)]
708
727
  points.append(self.normal(uvw, False, indices))
@@ -716,7 +735,7 @@ def normal_spline(self, indices=None):
716
735
  nCoefs.reverse()
717
736
  points = np.reshape(points, [nDep] + nCoefs)
718
737
  points = np.transpose(points, [0] + list(range(self.nInd, 0, -1)))
719
- return bspy.Spline.least_squares(uvwValues, points, order = newOrder, knots = newKnots, metadata = self.metadata)
738
+ return bspy.Spline.least_squares(uvwValues, points, order = newOrder, knots = newKnots)
720
739
 
721
740
  def rotate(self, vector, angle):
722
741
  vector = np.atleast_1d(vector)
bspy/hyperplane.py CHANGED
@@ -18,6 +18,9 @@ class Hyperplane(Manifold):
18
18
 
19
19
  tangentSpace : array-like
20
20
  A array of tangents that are linearly independent and orthogonal to the normal.
21
+
22
+ metadata : `dict`, optional
23
+ A dictionary of ancillary data to store with the hyperplane. Default is {}.
21
24
 
22
25
  Notes
23
26
  -----
@@ -25,14 +28,15 @@ class Hyperplane(Manifold):
25
28
  Thus the dimension of the domain is one less than that of the range.
26
29
  """
27
30
 
28
- maxAlignment = 0.99 # 1 - 1/10^2
29
- """ If a shift of 1 in the normal direction of one manifold yields a shift of 10 in the tangent plane intersection, the manifolds are parallel."""
31
+ maxAlignment = 0.9999 # 1 - 1/10^4
32
+ """ If the absolute value of the dot product of two unit normals is greater than maxAlignment, the manifolds are parallel."""
30
33
 
31
- def __init__(self, normal, point, tangentSpace):
32
- self._normal = np.atleast_1d(np.array(normal))
33
- self._point = np.atleast_1d(np.array(point))
34
- self._tangentSpace = np.atleast_1d(np.array(tangentSpace))
34
+ def __init__(self, normal, point, tangentSpace, metadata = {}):
35
+ self._normal = np.atleast_1d(normal)
36
+ self._point = np.atleast_1d(point)
37
+ self._tangentSpace = np.atleast_1d(tangentSpace)
35
38
  if not np.allclose(self._tangentSpace.T @ self._normal, 0.0): raise ValueError("normal must be orthogonal to tangent space")
39
+ self.metadata = dict(metadata)
36
40
 
37
41
  def __repr__(self):
38
42
  return "Hyperplane({0}, {1}, {2})".format(self._normal, self._point, self._tangentSpace)
@@ -172,7 +176,7 @@ class Hyperplane(Manifold):
172
176
  -------
173
177
  point : `numpy.array`
174
178
  """
175
- return np.dot(self._tangentSpace, domainPoint) + self._point
179
+ return np.dot(self._tangentSpace, np.atleast_1d(domainPoint)) + self._point
176
180
 
177
181
  def flip_normal(self):
178
182
  """
@@ -207,7 +211,7 @@ class Hyperplane(Manifold):
207
211
  --------
208
212
  `to_dict` : Return a `dict` with `Hyperplane` data.
209
213
  """
210
- return Hyperplane(dictionary["normal"], dictionary["point"], dictionary["tangentSpace"])
214
+ return Hyperplane(dictionary["normal"], dictionary["point"], dictionary["tangentSpace"], dictionary.get("metadata", {}))
211
215
 
212
216
  def full_domain(self):
213
217
  """
@@ -455,7 +459,7 @@ class Hyperplane(Manifold):
455
459
  --------
456
460
  `from_dict` : Create a `Hyperplane` from a data in a `dict`.
457
461
  """
458
- return {"type" : "Hyperplane", "normal" : self._normal, "point" : self._point, "tangentSpace" : self._tangentSpace}
462
+ return {"type" : "Hyperplane", "normal" : self._normal, "point" : self._point, "tangentSpace" : self._tangentSpace, "metadata" : self.metadata}
459
463
 
460
464
  def transform(self, matrix, matrixInverseTranspose = None):
461
465
  """
bspy/manifold.py CHANGED
@@ -5,10 +5,15 @@ class Manifold:
5
5
  """
6
6
  A manifold is an abstract base class for differentiable functions with
7
7
  normals and tangent spaces whose range is one dimension higher than their domain.
8
+
9
+ Parameters
10
+ ----------
11
+ metadata : `dict`, optional
12
+ A dictionary of ancillary data to store with the manifold. Default is {}.
8
13
  """
9
14
 
10
- minSeparation = 0.01
11
- """If two points are within 0.01 of each each other, they are coincident."""
15
+ minSeparation = 0.0001
16
+ """If two points are within minSeparation of each each other, they are coincident."""
12
17
 
13
18
  Crossing = namedtuple('Crossing', ('left','right'))
14
19
  Coincidence = namedtuple('Coincidence', ('left', 'right', 'alignment', 'transform', 'inverse', 'translation'))
@@ -17,8 +22,8 @@ class Manifold:
17
22
  factory = {}
18
23
  """Factory dictionary for creating manifolds."""
19
24
 
20
- def __init__(self):
21
- pass
25
+ def __init__(self, metadata = {}):
26
+ self.metadata = dict(metadata)
22
27
 
23
28
  def cached_intersect(self, other, cache = None):
24
29
  """
@@ -322,7 +327,7 @@ class Manifold:
322
327
  --------
323
328
  `from_dict` : Create a `Manifold` from a data in a `dict`.
324
329
  """
325
- return None
330
+ return {"metadata" : self.metadata}
326
331
 
327
332
  def transform(self, matrix, matrixInverseTranspose = None):
328
333
  """
bspy/solid.py CHANGED
@@ -59,6 +59,9 @@ class Solid:
59
59
 
60
60
  containsInfinity : `bool`
61
61
  Indicates whether or not the solid contains infinity.
62
+
63
+ metadata : `dict`, optional
64
+ A dictionary of ancillary data to store with the solid. Default is {}.
62
65
 
63
66
  See also
64
67
  --------
@@ -70,10 +73,11 @@ class Solid:
70
73
 
71
74
  Solids can be of zero dimension, typically acting as the domain of boundary endpoints. Zero-dimension solids have no boundaries, they only contain infinity or not.
72
75
  """
73
- def __init__(self, dimension, containsInfinity):
76
+ def __init__(self, dimension, containsInfinity, metadata = {}):
74
77
  assert dimension >= 0
75
78
  self.dimension = dimension
76
79
  self.containsInfinity = containsInfinity
80
+ self.metadata = dict(metadata)
77
81
  self.boundaries = []
78
82
  self.bounds = None
79
83
 
@@ -113,7 +117,7 @@ class Solid:
113
117
  if boundary.manifold.range_dimension() != self.dimension: raise ValueError("Dimensions don't match")
114
118
  self.boundaries.append(boundary)
115
119
  if self.bounds is None:
116
- self.bounds = boundary.bounds
120
+ self.bounds = boundary.bounds.copy()
117
121
  elif boundary.bounds is None:
118
122
  raise ValueError("Mix of infinite and bounded boundaries")
119
123
  else:
@@ -356,7 +360,7 @@ class Solid:
356
360
  `save` : Save a solids and/or manifolds in json format to the specified filename (full path).
357
361
  """
358
362
  def from_dict(dictionary):
359
- solid = Solid(dictionary["dimension"], dictionary["containsInfinity"])
363
+ solid = Solid(dictionary["dimension"], dictionary["containsInfinity"], dictionary.get("metadata", {}))
360
364
  for boundary in dictionary["boundaries"]:
361
365
  manifold = boundary["manifold"]
362
366
  solid.add_boundary(Boundary(Manifold.factory[manifold.get("type", "Spline")].from_dict(manifold), from_dict(boundary["domain"])))
@@ -428,7 +432,7 @@ class Solid:
428
432
  if isinstance(obj, Boundary):
429
433
  return {"type" : "Boundary", "manifold" : obj.manifold, "domain" : obj.domain}
430
434
  if isinstance(obj, Solid):
431
- return {"type" : "Solid", "dimension" : obj.dimension, "containsInfinity" : obj.containsInfinity, "boundaries" : obj.boundaries}
435
+ return {"type" : "Solid", "dimension" : obj.dimension, "containsInfinity" : obj.containsInfinity, "boundaries" : obj.boundaries, "metadata" : obj.metadata}
432
436
  return super().default(obj)
433
437
 
434
438
  with open(fileName, 'w', encoding='utf-8') as file:
@@ -509,25 +513,28 @@ class Solid:
509
513
  slice.add_boundary(Boundary(right, domainSlice))
510
514
 
511
515
  elif isinstance(intersection, Manifold.Coincidence):
512
- # First, intersect domain coincidence with the domain boundary.
513
- coincidence = left.intersection(boundary.domain)
514
- # Next, invert the domain coincidence (which will remove it) if this is a twin or if the normals point in opposite directions.
516
+ # Intersect domain coincidence with the boundary's domain.
517
+ left = left.intersection(boundary.domain)
518
+ # Invert the domain coincidence (which will remove it) if this is a twin or if the normals point in opposite directions.
519
+ #invertCoincidence = trimTwin and (isTwin or intersection.alignment < 0.0)
515
520
  invertCoincidence = (trimTwin and isTwin) or intersection.alignment < 0.0
521
+ # Create the coincidence to hold the trimmed and transformed domain coincidence (left).
522
+ coincidence = Solid(left.dimension, left.containsInfinity)
516
523
  if invertCoincidence:
517
524
  coincidence.containsInfinity = not coincidence.containsInfinity
518
525
  # Next, transform the domain coincidence from the boundary to the given manifold.
519
526
  # Create copies of the manifolds and boundaries, since we are changing them.
520
- for i in range(len(coincidence.boundaries)):
521
- domainManifold = coincidence.boundaries[i].manifold
527
+ for coincidenceBoundary in left.boundaries:
528
+ coincidenceManifold = coincidenceBoundary.manifold
522
529
  if invertCoincidence:
523
- domainManifold = domainManifold.flip_normal()
530
+ coincidenceManifold = coincidenceManifold.flip_normal()
524
531
  if isTwin:
525
- domainManifold = domainManifold.translate(-intersection.translation)
526
- domainManifold = domainManifold.transform(intersection.inverse, intersection.transform.T)
532
+ coincidenceManifold = coincidenceManifold.translate(-intersection.translation)
533
+ coincidenceManifold = coincidenceManifold.transform(intersection.inverse, intersection.transform.T)
527
534
  else:
528
- domainManifold = domainManifold.transform(intersection.transform, intersection.inverse.T)
529
- domainManifold = domainManifold.translate(intersection.translation)
530
- coincidence.boundaries[i] = Boundary(domainManifold, coincidence.boundaries[i].domain)
535
+ coincidenceManifold = coincidenceManifold.transform(intersection.transform, intersection.inverse.T)
536
+ coincidenceManifold = coincidenceManifold.translate(intersection.translation)
537
+ coincidence.add_boundary(Boundary(coincidenceManifold, coincidenceBoundary.domain))
531
538
  # Finally, add the domain coincidence to the list of coincidences.
532
539
  coincidences.append((invertCoincidence, coincidence))
533
540