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/__init__.py +16 -3
- bspy/_spline_domain.py +101 -9
- bspy/_spline_evaluation.py +82 -17
- bspy/_spline_fitting.py +244 -114
- bspy/_spline_intersection.py +467 -156
- bspy/_spline_operations.py +70 -49
- bspy/hyperplane.py +540 -0
- bspy/manifold.py +334 -31
- bspy/solid.py +842 -0
- bspy/spline.py +350 -71
- bspy/splineOpenGLFrame.py +262 -14
- bspy/spline_block.py +343 -0
- bspy/viewer.py +134 -90
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/METADATA +17 -6
- bspy-4.2.dist-info/RECORD +18 -0
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/WHEEL +1 -1
- bspy-4.0.dist-info/RECORD +0 -15
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/LICENSE +0 -0
- {bspy-4.0.dist-info → bspy-4.2.dist-info}/top_level.txt +0 -0
bspy/solid.py
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import json
|
|
3
|
+
import scipy.integrate as integrate
|
|
4
|
+
from bspy.manifold import Manifold
|
|
5
|
+
|
|
6
|
+
class Boundary:
|
|
7
|
+
"""
|
|
8
|
+
A portion of the boundary of a solid.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
manifold : `Manifold`
|
|
13
|
+
The differentiable function whose range is one dimension higher than its domain that defines the range of the boundary.
|
|
14
|
+
|
|
15
|
+
domain : `Solid`, optional
|
|
16
|
+
The region of the domain of the manifold that's within the boundary. The default is the full domain of the manifold.
|
|
17
|
+
|
|
18
|
+
See also
|
|
19
|
+
--------
|
|
20
|
+
`Solid` : A region that separates space into an inside and outside, defined by a collection of boundaries.
|
|
21
|
+
`Manifold.full_domain` : Return a solid that represents the full domain of the manifold.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, manifold, domain = None):
|
|
24
|
+
self.domain = manifold.full_domain() if domain is None else domain
|
|
25
|
+
if manifold.domain_dimension() != self.domain.dimension: raise ValueError("Domain dimensions don't match")
|
|
26
|
+
if manifold.domain_dimension() + 1 != manifold.range_dimension(): raise ValueError("Manifold range is not one dimension higher than domain")
|
|
27
|
+
self.manifold, self.bounds = manifold.trimmed_range_bounds(self.domain.bounds)
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return "Boundary({0}, {1})".format(self.manifold.__repr__(), self.domain.__repr__())
|
|
31
|
+
|
|
32
|
+
def any_point(self):
|
|
33
|
+
"""
|
|
34
|
+
Return an arbitrary point on the boundary.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
point : `numpy.array`
|
|
39
|
+
A point on the boundary.
|
|
40
|
+
|
|
41
|
+
See Also
|
|
42
|
+
--------
|
|
43
|
+
`Solid.any_point` : Return an arbitrary point on the solid.
|
|
44
|
+
|
|
45
|
+
Notes
|
|
46
|
+
-----
|
|
47
|
+
The point is computed by evaluating the boundary manifold by an arbitrary point in the domain of the boundary.
|
|
48
|
+
"""
|
|
49
|
+
return self.manifold.evaluate(self.domain.any_point())
|
|
50
|
+
|
|
51
|
+
class Solid:
|
|
52
|
+
"""
|
|
53
|
+
A region that separates space into an inside and outside, defined by a collection of boundaries.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
dimension : `int`
|
|
58
|
+
The dimension of the solid (non-negative).
|
|
59
|
+
|
|
60
|
+
containsInfinity : `bool`
|
|
61
|
+
Indicates whether or not the solid contains infinity.
|
|
62
|
+
|
|
63
|
+
See also
|
|
64
|
+
--------
|
|
65
|
+
`Boundary` : A portion of the boundary of a solid.
|
|
66
|
+
|
|
67
|
+
Notes
|
|
68
|
+
-----
|
|
69
|
+
Solids also contain a `list` of `boundaries`. That list may be empty.
|
|
70
|
+
|
|
71
|
+
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
|
+
"""
|
|
73
|
+
def __init__(self, dimension, containsInfinity):
|
|
74
|
+
assert dimension >= 0
|
|
75
|
+
self.dimension = dimension
|
|
76
|
+
self.containsInfinity = containsInfinity
|
|
77
|
+
self.boundaries = []
|
|
78
|
+
self.bounds = None
|
|
79
|
+
|
|
80
|
+
def __add__(self, other):
|
|
81
|
+
return self.union(other)
|
|
82
|
+
|
|
83
|
+
def __bool__(self):
|
|
84
|
+
return self.containsInfinity or len(self.boundaries) > 0
|
|
85
|
+
|
|
86
|
+
def __mul__(self, other):
|
|
87
|
+
return self.intersection(other)
|
|
88
|
+
|
|
89
|
+
def __neg__(self):
|
|
90
|
+
return self.complement()
|
|
91
|
+
|
|
92
|
+
def __repr__(self):
|
|
93
|
+
return "Solid({0}, {1})".format(self.dimension, self.containsInfinity)
|
|
94
|
+
|
|
95
|
+
def __sub__(self, other):
|
|
96
|
+
return self.difference(other)
|
|
97
|
+
|
|
98
|
+
def add_boundary(self, boundary):
|
|
99
|
+
"""
|
|
100
|
+
Adds a boundary to a solid, recomputing the solid's bounds.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
boundary : `Boundary`
|
|
105
|
+
A boundary to add to the solid.
|
|
106
|
+
|
|
107
|
+
Notes
|
|
108
|
+
-----
|
|
109
|
+
While you could just append the boundary to the solid's collection of boundaries,
|
|
110
|
+
this method also recomputes the solid's bounds for faster intersection and containment operations.
|
|
111
|
+
Adding the boundary directly to the solid's boundaries collection may result in faulty operations.
|
|
112
|
+
"""
|
|
113
|
+
if boundary.manifold.range_dimension() != self.dimension: raise ValueError("Dimensions don't match")
|
|
114
|
+
self.boundaries.append(boundary)
|
|
115
|
+
if self.bounds is None:
|
|
116
|
+
self.bounds = boundary.bounds.copy()
|
|
117
|
+
elif boundary.bounds is None:
|
|
118
|
+
raise ValueError("Mix of infinite and bounded boundaries")
|
|
119
|
+
else:
|
|
120
|
+
self.bounds[:, 0] = np.minimum(self.bounds[:, 0], boundary.bounds[:, 0])
|
|
121
|
+
self.bounds[:, 1] = np.maximum(self.bounds[:, 1], boundary.bounds[:, 1])
|
|
122
|
+
|
|
123
|
+
def any_point(self):
|
|
124
|
+
"""
|
|
125
|
+
Return an arbitrary point on the solid.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
point : `numpy.array`
|
|
130
|
+
A point on the solid.
|
|
131
|
+
|
|
132
|
+
See Also
|
|
133
|
+
--------
|
|
134
|
+
`Boundary.any_point` : Return an arbitrary point on the boundary.
|
|
135
|
+
|
|
136
|
+
Notes
|
|
137
|
+
-----
|
|
138
|
+
The point is computed by calling `Boundary.any_point` on the solid's first boundary.
|
|
139
|
+
If the solid has no boundaries but contains infinity, `any_point` returns the origin.
|
|
140
|
+
If the solid has no boundaries and doesn't contain infinity, `any_point` returns `None`.
|
|
141
|
+
"""
|
|
142
|
+
point = None
|
|
143
|
+
if self.boundaries:
|
|
144
|
+
point = self.boundaries[0].any_point()
|
|
145
|
+
elif self.containsInfinity:
|
|
146
|
+
if self.dimension > 0:
|
|
147
|
+
point = np.full((self.dimension), 0.0)
|
|
148
|
+
else:
|
|
149
|
+
point = 0.0
|
|
150
|
+
|
|
151
|
+
return point
|
|
152
|
+
|
|
153
|
+
def complement(self):
|
|
154
|
+
"""
|
|
155
|
+
Return the complement of the solid: whatever was inside is outside and vice-versa.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
solid : `Solid`
|
|
160
|
+
The complement of the solid.
|
|
161
|
+
|
|
162
|
+
See Also
|
|
163
|
+
--------
|
|
164
|
+
`intersection` : Intersect two solids.
|
|
165
|
+
`union` : Union two solids.
|
|
166
|
+
`difference` : Subtract one solid from another.
|
|
167
|
+
"""
|
|
168
|
+
solid = Solid(self.dimension, not self.containsInfinity)
|
|
169
|
+
for boundary in self.boundaries:
|
|
170
|
+
solid.add_boundary(Boundary(boundary.manifold.flip_normal(), boundary.domain))
|
|
171
|
+
return solid
|
|
172
|
+
|
|
173
|
+
def contains_point(self, point):
|
|
174
|
+
"""
|
|
175
|
+
Test if a point lies within the solid.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
point : array-like
|
|
180
|
+
A point that may lie within the solid.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
containment : `bool`
|
|
185
|
+
`True` if `point` lies within the solid. `False` otherwise.
|
|
186
|
+
|
|
187
|
+
See Also
|
|
188
|
+
--------
|
|
189
|
+
`winding_number` : Compute the winding number for a point relative to the solid.
|
|
190
|
+
|
|
191
|
+
Notes
|
|
192
|
+
-----
|
|
193
|
+
A point is considered contained if it's on the boundary of the solid or it's winding number is greater than 0.5.
|
|
194
|
+
"""
|
|
195
|
+
windingNumber, onBoundaryNormal = self.winding_number(point)
|
|
196
|
+
# The default is to include points on the boundary (onBoundaryNormal is not None).
|
|
197
|
+
containment = True
|
|
198
|
+
if onBoundaryNormal is None:
|
|
199
|
+
# windingNumber > 0.5 returns a np.bool_, not a bool, so we need to cast it.
|
|
200
|
+
containment = bool(windingNumber > 0.5)
|
|
201
|
+
return containment
|
|
202
|
+
|
|
203
|
+
def difference(self, other):
|
|
204
|
+
"""
|
|
205
|
+
Subtract one solid from another.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
other : `Solid`
|
|
210
|
+
The `Solid` subtracted from self.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
combinedSolid : `Solid`
|
|
215
|
+
A `Solid` that represents the subtraction of other from self.
|
|
216
|
+
|
|
217
|
+
See Also
|
|
218
|
+
--------
|
|
219
|
+
`intersection` : Intersect two solids.
|
|
220
|
+
`union` : Union two solids.
|
|
221
|
+
"""
|
|
222
|
+
return self.intersection(other.complement())
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def disjoint_bounds(bounds1, bounds2):
|
|
226
|
+
"""
|
|
227
|
+
Returns whether or not bounds1 and bounds2 are disjoint.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
bounds1 : array-like or `None`
|
|
232
|
+
An array with shape (dimension, 2) of lower and upper and lower bounds on each dimension.
|
|
233
|
+
If bounds1 is `None` then then there are no bounds.
|
|
234
|
+
|
|
235
|
+
bounds2 : array-like or `None`
|
|
236
|
+
An array with shape (dimension, 2) of lower and upper and lower bounds on each dimension.
|
|
237
|
+
If bounds2 is `None` then then there are no bounds.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
disjoint : `bool`
|
|
242
|
+
Value is true if the bounds are disjoint. Value is false if either bounds is `None`.
|
|
243
|
+
"""
|
|
244
|
+
if bounds1 is None or bounds2 is None:
|
|
245
|
+
return False
|
|
246
|
+
else:
|
|
247
|
+
return np.min(np.diff(bounds1).reshape(-1) + np.diff(bounds2).reshape(-1) -
|
|
248
|
+
np.abs(np.sum(bounds1, axis=1) - np.sum(bounds2, axis=1))) < -Manifold.minSeparation
|
|
249
|
+
|
|
250
|
+
def intersection(self, other, cache = None):
|
|
251
|
+
"""
|
|
252
|
+
Intersect two solids.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
other : `Solid`
|
|
257
|
+
The `Solid` intersecting self.
|
|
258
|
+
|
|
259
|
+
cache : `dict`, optional
|
|
260
|
+
A dictionary to cache `Manifold` intersections, speeding computation. If no dictionary is passed, one is created.
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
combinedSolid : `Solid`
|
|
265
|
+
A `Solid` that represents the intersection between self and other.
|
|
266
|
+
|
|
267
|
+
See Also
|
|
268
|
+
--------
|
|
269
|
+
`slice` : Slice a solid by a manifold.
|
|
270
|
+
`union` : Union two solids.
|
|
271
|
+
`difference` : Subtract one solid from another.
|
|
272
|
+
|
|
273
|
+
Notes
|
|
274
|
+
-----
|
|
275
|
+
To intersect two solids, we slice each solid with the boundaries of the other solid. The slices are the region
|
|
276
|
+
of the domain that intersect the solid. We then intersect the domain of each boundary with its slice of the other solid. Thus,
|
|
277
|
+
the intersection of two solids becomes a set of intersections within the domains of their boundaries. This recursion continues
|
|
278
|
+
until we are intersecting points whose domains have no boundaries.
|
|
279
|
+
|
|
280
|
+
The only subtlety is when two boundaries are coincident. To avoid overlapping the coincident region, we keep that region
|
|
281
|
+
for one slice and trim it away for the other. We use a manifold intersection cache to keep track of these pairs, as well as to reduce computation.
|
|
282
|
+
"""
|
|
283
|
+
assert self.dimension == other.dimension
|
|
284
|
+
|
|
285
|
+
# Manifold intersections are expensive and come in symmetric pairs (m1 intersect m2, m2 intersect m1).
|
|
286
|
+
# So, we create a manifold intersections cache (dictionary) to store and reuse intersection pairs.
|
|
287
|
+
# The cache is also used to avoid overlapping coincident regions by identifying twins that could be trimmed.
|
|
288
|
+
if cache is None:
|
|
289
|
+
cache = {}
|
|
290
|
+
|
|
291
|
+
# Start with a solid without boundaries.
|
|
292
|
+
combinedSolid = Solid(self.dimension, self.containsInfinity and other.containsInfinity)
|
|
293
|
+
|
|
294
|
+
# If the solids are disjoint, add the boundaries contained by infinity and return.
|
|
295
|
+
if Solid.disjoint_bounds(self.bounds, other.bounds):
|
|
296
|
+
if other.containsInfinity:
|
|
297
|
+
for boundary in self.boundaries:
|
|
298
|
+
combinedSolid.add_boundary(boundary)
|
|
299
|
+
if self.containsInfinity:
|
|
300
|
+
for boundary in other.boundaries:
|
|
301
|
+
combinedSolid.add_boundary(boundary)
|
|
302
|
+
return combinedSolid
|
|
303
|
+
|
|
304
|
+
for boundary in self.boundaries:
|
|
305
|
+
# Slice self boundary manifold by other.
|
|
306
|
+
slice = other.slice(boundary.manifold, cache, True)
|
|
307
|
+
# Intersect slice with the boundary's domain.
|
|
308
|
+
newDomain = boundary.domain.intersection(slice, cache)
|
|
309
|
+
if newDomain:
|
|
310
|
+
# Self boundary intersects other, so create a new boundary with the intersected domain.
|
|
311
|
+
combinedSolid.add_boundary(Boundary(boundary.manifold, newDomain))
|
|
312
|
+
|
|
313
|
+
for boundary in other.boundaries:
|
|
314
|
+
# Slice other boundary manifold by self.
|
|
315
|
+
slice = self.slice(boundary.manifold, cache, True)
|
|
316
|
+
# Intersect slice with the boundary's domain.
|
|
317
|
+
newDomain = boundary.domain.intersection(slice, cache)
|
|
318
|
+
if newDomain:
|
|
319
|
+
# Other boundary intersects self, so create a new boundary with the intersected domain.
|
|
320
|
+
combinedSolid.add_boundary(Boundary(boundary.manifold, newDomain))
|
|
321
|
+
|
|
322
|
+
return combinedSolid
|
|
323
|
+
|
|
324
|
+
def is_empty(self):
|
|
325
|
+
"""
|
|
326
|
+
Test if the solid is empty.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
isEmpty : `bool`
|
|
331
|
+
`True` if the solid is empty, `False` otherwise.
|
|
332
|
+
|
|
333
|
+
Notes
|
|
334
|
+
-----
|
|
335
|
+
Casting the solid to `bool` returns not `is_empty`.
|
|
336
|
+
"""
|
|
337
|
+
return not self
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def load(fileName):
|
|
341
|
+
"""
|
|
342
|
+
Load solids and/or manifolds in json format from the specified filename (full path).
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
fileName : `string`
|
|
347
|
+
The full path to the file containing the solids and/or manifolds. Can be a relative path.
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
solidsAndManifolds : list of `Solid` and/or `Manifold`
|
|
352
|
+
The loaded solids and/or manifolds.
|
|
353
|
+
|
|
354
|
+
See Also
|
|
355
|
+
--------
|
|
356
|
+
`save` : Save a solids and/or manifolds in json format to the specified filename (full path).
|
|
357
|
+
"""
|
|
358
|
+
def from_dict(dictionary):
|
|
359
|
+
solid = Solid(dictionary["dimension"], dictionary["containsInfinity"])
|
|
360
|
+
for boundary in dictionary["boundaries"]:
|
|
361
|
+
manifold = boundary["manifold"]
|
|
362
|
+
solid.add_boundary(Boundary(Manifold.factory[manifold.get("type", "Spline")].from_dict(manifold), from_dict(boundary["domain"])))
|
|
363
|
+
return solid
|
|
364
|
+
|
|
365
|
+
# Load json file.
|
|
366
|
+
with open(fileName, 'r', encoding='utf-8') as file:
|
|
367
|
+
data = json.load(file)
|
|
368
|
+
|
|
369
|
+
# Convert json data to solids and manifolds.
|
|
370
|
+
solidsAndManifolds = []
|
|
371
|
+
if isinstance(data, dict):
|
|
372
|
+
data = [data]
|
|
373
|
+
for dictionary in data:
|
|
374
|
+
className = dictionary.get("type", "Spline")
|
|
375
|
+
if className == "Solid":
|
|
376
|
+
solidsAndManifolds.append(from_dict(dictionary))
|
|
377
|
+
else:
|
|
378
|
+
solidsAndManifolds.append(Manifold.factory[className].from_dict(dictionary))
|
|
379
|
+
return solidsAndManifolds
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def point_outside_bounds(point, bounds):
|
|
383
|
+
"""
|
|
384
|
+
Returns whether or not point is outside bounds.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
point : array-like
|
|
389
|
+
A point whose dimension matches the bounds.
|
|
390
|
+
|
|
391
|
+
bounds : array-like or `None`
|
|
392
|
+
An array with shape (dimension, 2) of lower and upper and lower bounds on each dimension.
|
|
393
|
+
If bounds is `None` then then there are no bounds.
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
within : `bool`
|
|
398
|
+
Value is true if the point is outside bounds or bounds is `None`.
|
|
399
|
+
"""
|
|
400
|
+
if bounds is None:
|
|
401
|
+
return True
|
|
402
|
+
else:
|
|
403
|
+
return np.min(np.diff(bounds).reshape(-1) - np.abs(np.sum(bounds, axis=1) + -2.0 * point)) < -Manifold.minSeparation
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def save(fileName, *solids_or_manifolds):
|
|
407
|
+
"""
|
|
408
|
+
Save a solids and/or manifolds in json format to the specified filename (full path).
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
fileName : `string`
|
|
413
|
+
The full path to the file containing the solids and/or manifolds. Can be a relative path.
|
|
414
|
+
|
|
415
|
+
*solids_or_manifolds : `Solid` or `Manifold`
|
|
416
|
+
Solids and/or manifolds to save in the same file.
|
|
417
|
+
|
|
418
|
+
See Also
|
|
419
|
+
--------
|
|
420
|
+
`load` : Load solids and/or manifolds in json format from the specified filename (full path).
|
|
421
|
+
"""
|
|
422
|
+
class Encoder(json.JSONEncoder):
|
|
423
|
+
def default(self, obj):
|
|
424
|
+
if isinstance(obj, np.ndarray):
|
|
425
|
+
return obj.tolist()
|
|
426
|
+
if isinstance(obj, Manifold):
|
|
427
|
+
return obj.to_dict()
|
|
428
|
+
if isinstance(obj, Boundary):
|
|
429
|
+
return {"type" : "Boundary", "manifold" : obj.manifold, "domain" : obj.domain}
|
|
430
|
+
if isinstance(obj, Solid):
|
|
431
|
+
return {"type" : "Solid", "dimension" : obj.dimension, "containsInfinity" : obj.containsInfinity, "boundaries" : obj.boundaries}
|
|
432
|
+
return super().default(obj)
|
|
433
|
+
|
|
434
|
+
with open(fileName, 'w', encoding='utf-8') as file:
|
|
435
|
+
json.dump(solids_or_manifolds, file, indent=4, cls=Encoder)
|
|
436
|
+
|
|
437
|
+
def slice(self, manifold, cache = None, trimTwin = False):
|
|
438
|
+
"""
|
|
439
|
+
Slice the solid by a manifold.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
manifold : `Manifold`
|
|
444
|
+
The `Manifold` used to slice the solid.
|
|
445
|
+
|
|
446
|
+
cache : `dict`, optional
|
|
447
|
+
A dictionary to cache `Manifold` intersections, speeding computation.
|
|
448
|
+
|
|
449
|
+
trimTwin : `bool`, default: False
|
|
450
|
+
Trim coincident boundary twins on subsequent calls to slice (avoids duplication of overlapping regions).
|
|
451
|
+
Trimming twins is typically only used in conjunction with `intersection`.
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
slice : `Solid`
|
|
456
|
+
A region in the domain of `manifold` that intersects with the solid. The region may contain infinity.
|
|
457
|
+
|
|
458
|
+
See Also
|
|
459
|
+
--------
|
|
460
|
+
`intersection` : Intersect two solids.
|
|
461
|
+
`Manifold.intersect` : Intersect two manifolds.
|
|
462
|
+
`Manifold.cached_intersect` : Intersect two manifolds, caching the result for twins.
|
|
463
|
+
`Manifold.complete_slice` : Add any missing inherent (implicit) boundaries of this manifold's domain to the given slice.
|
|
464
|
+
|
|
465
|
+
Notes
|
|
466
|
+
-----
|
|
467
|
+
The dimension of the slice is always one less than the dimension of the solid, since the slice is a region in the domain of the manifold slicing the solid.
|
|
468
|
+
|
|
469
|
+
To compute the slice of a manifold intersecting the solid, we intersect the manifold with each boundary of the solid. There may be multiple intersections
|
|
470
|
+
between the manifold and the boundary. Each is either a crossing or a coincident region.
|
|
471
|
+
|
|
472
|
+
Crossings result in two intersection manifolds: one in the domain of the manifold and one in the domain of the boundary. By construction, both intersection manifolds have the
|
|
473
|
+
same domain and the same range of the manifold and boundary (the crossing itself). The intersection manifold in the domain of the manifold becomes a boundary of the slice,
|
|
474
|
+
but we must determine the intersection's domain. For that, we slice the boundary's intersection manifold with the boundary's domain. This recursion continues
|
|
475
|
+
until the slice is just a point with no domain.
|
|
476
|
+
|
|
477
|
+
Coincident regions appear in the domains of the manifold and the boundary. We intersect the boundary's coincident region with the domain of the boundary and then map
|
|
478
|
+
it to the domain of the manifold. If the coincident regions have normals in opposite directions, they cancel each other out, so we subtract them from the slice by
|
|
479
|
+
inverting the region and intersecting it with the slice. We use this same technique for removing overlapping coincident regions. If the coincident regions have normals
|
|
480
|
+
in the same direction, we union them with the slice.
|
|
481
|
+
"""
|
|
482
|
+
assert manifold.range_dimension() == self.dimension
|
|
483
|
+
|
|
484
|
+
# Start with an empty slice and no domain coincidences.
|
|
485
|
+
slice = Solid(self.dimension-1, self.containsInfinity)
|
|
486
|
+
bounds = manifold.range_bounds()
|
|
487
|
+
if Solid.disjoint_bounds(bounds, self.bounds):
|
|
488
|
+
manifold.complete_slice(slice, self)
|
|
489
|
+
return slice
|
|
490
|
+
coincidences = []
|
|
491
|
+
|
|
492
|
+
# Intersect each of this solid's boundaries with the manifold.
|
|
493
|
+
for boundary in self.boundaries:
|
|
494
|
+
if Solid.disjoint_bounds(boundary.bounds, bounds):
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
# Intersect manifolds, checking if the intersection is already in the cache.
|
|
498
|
+
intersections, isTwin = boundary.manifold.cached_intersect(manifold, cache)
|
|
499
|
+
if intersections is NotImplemented:
|
|
500
|
+
raise NotImplementedError()
|
|
501
|
+
|
|
502
|
+
# Each intersection is either a crossing (domain manifold) or a coincidence (solid within the domain).
|
|
503
|
+
for intersection in intersections:
|
|
504
|
+
(left, right) = (intersection.right, intersection.left) if isTwin else (intersection.left, intersection.right)
|
|
505
|
+
|
|
506
|
+
if isinstance(intersection, Manifold.Crossing):
|
|
507
|
+
domainSlice = boundary.domain.slice(left, cache)
|
|
508
|
+
if domainSlice:
|
|
509
|
+
slice.add_boundary(Boundary(right, domainSlice))
|
|
510
|
+
|
|
511
|
+
elif isinstance(intersection, Manifold.Coincidence):
|
|
512
|
+
# Intersect domain coincidence with the boundary's domain.
|
|
513
|
+
left = left.intersection(boundary.domain)
|
|
514
|
+
# Invert the domain coincidence (which will remove it) if this is a twin or if the normals point in opposite directions.
|
|
515
|
+
#invertCoincidence = trimTwin and (isTwin or intersection.alignment < 0.0)
|
|
516
|
+
invertCoincidence = (trimTwin and isTwin) or intersection.alignment < 0.0
|
|
517
|
+
# Create the coincidence to hold the trimmed and transformed domain coincidence (left).
|
|
518
|
+
coincidence = Solid(left.dimension, left.containsInfinity)
|
|
519
|
+
if invertCoincidence:
|
|
520
|
+
coincidence.containsInfinity = not coincidence.containsInfinity
|
|
521
|
+
# Next, transform the domain coincidence from the boundary to the given manifold.
|
|
522
|
+
# Create copies of the manifolds and boundaries, since we are changing them.
|
|
523
|
+
for coincidenceBoundary in left.boundaries:
|
|
524
|
+
coincidenceManifold = coincidenceBoundary.manifold
|
|
525
|
+
if invertCoincidence:
|
|
526
|
+
coincidenceManifold = coincidenceManifold.flip_normal()
|
|
527
|
+
if isTwin:
|
|
528
|
+
coincidenceManifold = coincidenceManifold.translate(-intersection.translation)
|
|
529
|
+
coincidenceManifold = coincidenceManifold.transform(intersection.inverse, intersection.transform.T)
|
|
530
|
+
else:
|
|
531
|
+
coincidenceManifold = coincidenceManifold.transform(intersection.transform, intersection.inverse.T)
|
|
532
|
+
coincidenceManifold = coincidenceManifold.translate(intersection.translation)
|
|
533
|
+
coincidence.add_boundary(Boundary(coincidenceManifold, coincidenceBoundary.domain))
|
|
534
|
+
# Finally, add the domain coincidence to the list of coincidences.
|
|
535
|
+
coincidences.append((invertCoincidence, coincidence))
|
|
536
|
+
|
|
537
|
+
# Ensure the slice includes the manifold's inherent (implicit) boundaries, making it valid and complete.
|
|
538
|
+
manifold.complete_slice(slice, self)
|
|
539
|
+
|
|
540
|
+
# Now that we have a complete manifold domain, join it with each domain coincidence.
|
|
541
|
+
for coincidence in coincidences:
|
|
542
|
+
if coincidence[0]:
|
|
543
|
+
# If the domain coincidence is inverted (coincidence[0]), intersect it with the slice, thus removing it.
|
|
544
|
+
slice = slice.intersection(coincidence[1], cache)
|
|
545
|
+
else:
|
|
546
|
+
# Otherwise, union the domain coincidence with the slice, thus adding it.
|
|
547
|
+
slice = slice.union(coincidence[1])
|
|
548
|
+
|
|
549
|
+
return slice
|
|
550
|
+
|
|
551
|
+
def surface_integral(self, f, args=(), epsabs=None, epsrel=None, *quadArgs):
|
|
552
|
+
"""
|
|
553
|
+
Compute the surface integral of a vector field on the boundary of the solid.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
f : python function `f(point: numpy.array, normal: numpy.array, args : user-defined) -> numpy.array`
|
|
558
|
+
The vector field to be integrated on the boundary of the solid.
|
|
559
|
+
It's passed a point on the boundary and its corresponding outward-pointing unit normal, as well as any optional user-defined arguments.
|
|
560
|
+
|
|
561
|
+
args : tuple, optional
|
|
562
|
+
Extra arguments to pass to `f`.
|
|
563
|
+
|
|
564
|
+
*quadArgs : Quadrature arguments passed to `scipy.integrate.quad`.
|
|
565
|
+
|
|
566
|
+
Returns
|
|
567
|
+
-------
|
|
568
|
+
sum : scalar value
|
|
569
|
+
The value of the surface integral.
|
|
570
|
+
|
|
571
|
+
See Also
|
|
572
|
+
--------
|
|
573
|
+
`volume_integral` : Compute the volume integral of a function within the solid.
|
|
574
|
+
`scipy.integrate.quad` : Integrate func from a to b (possibly infinite interval) using a technique from the Fortran library QUADPACK.
|
|
575
|
+
|
|
576
|
+
Notes
|
|
577
|
+
-----
|
|
578
|
+
To compute the surface integral of a scalar function on the boundary, have `f` return the product of the `normal` times the scalar function for the `point`.
|
|
579
|
+
|
|
580
|
+
`surface_integral` sums the `volume_integral` over the domain of the solid's boundaries, using the integrand: `numpy.dot(f(point, normal), normal)`,
|
|
581
|
+
where `normal` is the cross-product of the boundary tangents (the normal before normalization).
|
|
582
|
+
"""
|
|
583
|
+
if not isinstance(args, tuple):
|
|
584
|
+
args = (args,)
|
|
585
|
+
if epsabs is None:
|
|
586
|
+
epsabs = Manifold.minSeparation
|
|
587
|
+
if epsrel is None:
|
|
588
|
+
epsrel = Manifold.minSeparation
|
|
589
|
+
|
|
590
|
+
# Initialize the return value for the integral
|
|
591
|
+
sum = 0.0
|
|
592
|
+
|
|
593
|
+
for boundary in self.boundaries:
|
|
594
|
+
def integrand(domainPoint):
|
|
595
|
+
evalPoint = np.atleast_1d(domainPoint)
|
|
596
|
+
point = boundary.manifold.evaluate(evalPoint)
|
|
597
|
+
cofactorNormal = boundary.manifold.normal(evalPoint, False)
|
|
598
|
+
normal = cofactorNormal / np.linalg.norm(cofactorNormal)
|
|
599
|
+
fValue = f(point, normal, *args)
|
|
600
|
+
return np.dot(fValue, cofactorNormal)
|
|
601
|
+
|
|
602
|
+
if boundary.domain.dimension > 0:
|
|
603
|
+
# Add the contribution to the Volume integral from this boundary.
|
|
604
|
+
sum += boundary.domain.volume_integral(integrand)
|
|
605
|
+
else:
|
|
606
|
+
# This is a 1-D boundary (line interval, no domain), so just add the integrand.
|
|
607
|
+
sum += integrand(0.0)
|
|
608
|
+
|
|
609
|
+
return sum
|
|
610
|
+
|
|
611
|
+
def transform(self, matrix, matrixInverseTranspose = None):
|
|
612
|
+
"""
|
|
613
|
+
Transform the range of the solid.
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
matrix : `numpy.array`
|
|
618
|
+
A square matrix transformation.
|
|
619
|
+
|
|
620
|
+
matrixInverseTranspose : `numpy.array`, optional
|
|
621
|
+
The inverse transpose of matrix (computed if not provided).
|
|
622
|
+
|
|
623
|
+
Returns
|
|
624
|
+
-------
|
|
625
|
+
solid : `Solid`
|
|
626
|
+
The transformed solid.
|
|
627
|
+
"""
|
|
628
|
+
assert np.shape(matrix) == (self.dimension, self.dimension)
|
|
629
|
+
|
|
630
|
+
if matrixInverseTranspose is None:
|
|
631
|
+
matrixInverseTranspose = np.transpose(np.linalg.inv(matrix))
|
|
632
|
+
|
|
633
|
+
solid = Solid(self.dimension, self.containsInfinity)
|
|
634
|
+
for boundary in self.boundaries:
|
|
635
|
+
solid.add_boundary(Boundary(boundary.manifold.transform(matrix, matrixInverseTranspose), boundary.domain))
|
|
636
|
+
return solid
|
|
637
|
+
|
|
638
|
+
def translate(self, delta):
|
|
639
|
+
"""
|
|
640
|
+
Translate the range of the solid.
|
|
641
|
+
|
|
642
|
+
Parameters
|
|
643
|
+
----------
|
|
644
|
+
delta : `numpy.array`
|
|
645
|
+
A 1D array translation.
|
|
646
|
+
|
|
647
|
+
Returns
|
|
648
|
+
-------
|
|
649
|
+
solid : `Solid`
|
|
650
|
+
The translated solid.
|
|
651
|
+
"""
|
|
652
|
+
assert len(delta) == self.dimension
|
|
653
|
+
|
|
654
|
+
solid = Solid(self.dimension, self.containsInfinity)
|
|
655
|
+
for boundary in self.boundaries:
|
|
656
|
+
solid.add_boundary(Boundary(boundary.manifold.translate(delta), boundary.domain))
|
|
657
|
+
return solid
|
|
658
|
+
|
|
659
|
+
def union(self, other):
|
|
660
|
+
"""
|
|
661
|
+
Union two solids.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
other : `Solid`
|
|
666
|
+
The `Solid` unioning self.
|
|
667
|
+
|
|
668
|
+
Returns
|
|
669
|
+
-------
|
|
670
|
+
combinedSolid : `Solid`
|
|
671
|
+
A `Solid` that represents the union between self and other.
|
|
672
|
+
|
|
673
|
+
See Also
|
|
674
|
+
--------
|
|
675
|
+
`intersection` : Intersect two solids.
|
|
676
|
+
`difference` : Subtract one solid from another.
|
|
677
|
+
"""
|
|
678
|
+
return self.complement().intersection(other.complement()).complement()
|
|
679
|
+
|
|
680
|
+
def volume_integral(self, f, args=(), epsabs=None, epsrel=None, *quadArgs):
|
|
681
|
+
"""
|
|
682
|
+
Compute the volume integral of a function within the solid.
|
|
683
|
+
|
|
684
|
+
Parameters
|
|
685
|
+
----------
|
|
686
|
+
f : python function `f(point: numpy.array, args : user-defined) -> scalar value`
|
|
687
|
+
The function to be integrated within the solid.
|
|
688
|
+
It's passed a point within the solid, as well as any optional user-defined arguments.
|
|
689
|
+
|
|
690
|
+
args : tuple, optional
|
|
691
|
+
Extra arguments to pass to `f`.
|
|
692
|
+
|
|
693
|
+
*quadArgs : Quadrature arguments passed to `scipy.integrate.quad`.
|
|
694
|
+
|
|
695
|
+
Returns
|
|
696
|
+
-------
|
|
697
|
+
sum : scalar value
|
|
698
|
+
The value of the volume integral.
|
|
699
|
+
|
|
700
|
+
See Also
|
|
701
|
+
--------
|
|
702
|
+
`surface_integral` : Compute the surface integral of a vector field on the boundary of the solid.
|
|
703
|
+
`scipy.integrate.quad` : Integrate func from a to b (possibly infinite interval) using a technique from the Fortran library QUADPACK.
|
|
704
|
+
|
|
705
|
+
Notes
|
|
706
|
+
-----
|
|
707
|
+
The volume integral is computed by recursive application of the divergence theorem: `volume_integral(divergence(F)) = surface_integral(dot(F, n))`,
|
|
708
|
+
where `F` is a vector field and `n` is the outward boundary unit normal.
|
|
709
|
+
|
|
710
|
+
Let `F = [Integral(f) from x0 to x holding other coordinates fixed, 0..0]`. `divergence(F) = f` by construction, and `dot(F, n) = Integral(f) * n[0]`.
|
|
711
|
+
Note that the choice of `x0` is arbitrary as long as it's in the domain of f and doesn't change across all surface integral boundaries.
|
|
712
|
+
|
|
713
|
+
Thus, we have `volume_integral(f) = surface_integral(Integral(f) * n[0])`.
|
|
714
|
+
The outward boundary unit normal, `n`, is the cross product of the boundary manifold's tangent space divided by its length.
|
|
715
|
+
The surface differential, `dS`, is the length of cross product of the boundary manifold's tangent space times the differentials of the manifold's domain variables.
|
|
716
|
+
The length of the cross product appears in the numerator and denominator of the surface integral and cancels.
|
|
717
|
+
What's left multiplying `Integral(f)` is the first coordinate of the cross product plus the domain differentials (volume integral).
|
|
718
|
+
The first coordinate of the cross product of the boundary manifold's tangent space is the first cofactor of the tangent space.
|
|
719
|
+
And so, `surface_integral(Integral(f) * n[0]) = volume_integral(Integral(f) * first cofactor)` over each boundary manifold's domain.
|
|
720
|
+
|
|
721
|
+
So, we have `volume_integral(f) = volume_integral(Integral(f) * first cofactor)` over each boundary manifold's domain.
|
|
722
|
+
To compute the volume integral we sum `volume_integral` over the domain of the solid's boundaries, using the integrand:
|
|
723
|
+
`scipy.integrate.quad(f, x0, x [other coordinates fixed]) * first cofactor`.
|
|
724
|
+
This recursion continues until the boundaries are only points, where we can just sum the integrand.
|
|
725
|
+
"""
|
|
726
|
+
if not isinstance(args, tuple):
|
|
727
|
+
args = (args,)
|
|
728
|
+
if epsabs is None:
|
|
729
|
+
epsabs = Manifold.minSeparation
|
|
730
|
+
if epsrel is None:
|
|
731
|
+
epsrel = Manifold.minSeparation
|
|
732
|
+
|
|
733
|
+
# Initialize the return value for the integral
|
|
734
|
+
sum = 0.0
|
|
735
|
+
|
|
736
|
+
# Select the first coordinate of an arbitrary point within the volume boundary (the domain of f)
|
|
737
|
+
x0 = self.any_point()[0]
|
|
738
|
+
|
|
739
|
+
for boundary in self.boundaries:
|
|
740
|
+
def domainF(domainPoint):
|
|
741
|
+
evalPoint = np.atleast_1d(domainPoint)
|
|
742
|
+
point = boundary.manifold.evaluate(evalPoint)
|
|
743
|
+
|
|
744
|
+
# fHat passes the scalar given by integrate.quad into the first coordinate of the vector for f.
|
|
745
|
+
def fHat(x):
|
|
746
|
+
evalPoint = np.array(point)
|
|
747
|
+
evalPoint[0] = x
|
|
748
|
+
return f(evalPoint, *args)
|
|
749
|
+
|
|
750
|
+
# Calculate Integral(f) * first cofactor. Note that quad returns a tuple: (integral, error bound).
|
|
751
|
+
returnValue = 0.0
|
|
752
|
+
firstCofactor = boundary.manifold.normal(evalPoint, False, (0,))
|
|
753
|
+
if abs(x0 - point[0]) > epsabs and abs(firstCofactor) > epsabs:
|
|
754
|
+
returnValue = integrate.quad(fHat, x0, point[0], epsabs=epsabs, epsrel=epsrel, *quadArgs)[0] * firstCofactor
|
|
755
|
+
return returnValue
|
|
756
|
+
|
|
757
|
+
if boundary.domain.dimension > 0:
|
|
758
|
+
# Add the contribution to the Volume integral from this boundary.
|
|
759
|
+
sum += boundary.domain.volume_integral(domainF)
|
|
760
|
+
else:
|
|
761
|
+
# This is a 1-D boundary (line interval, no domain), so just add the integrand.
|
|
762
|
+
sum += domainF(0.0)
|
|
763
|
+
|
|
764
|
+
return sum
|
|
765
|
+
|
|
766
|
+
def winding_number(self, point):
|
|
767
|
+
"""
|
|
768
|
+
Compute the winding number for a point relative to the solid.
|
|
769
|
+
|
|
770
|
+
Parameters
|
|
771
|
+
----------
|
|
772
|
+
point : array-like
|
|
773
|
+
A point that may lie within the solid.
|
|
774
|
+
|
|
775
|
+
Returns
|
|
776
|
+
-------
|
|
777
|
+
windingNumber : scalar value
|
|
778
|
+
The `windingNumber` is 0 if the point is outside the solid, 1 if it's inside.
|
|
779
|
+
Other values indicate issues:
|
|
780
|
+
* A point on the boundary leads to an undefined (random) winding number;
|
|
781
|
+
* Boundaries with gaps or overlaps lead to fractional winding numbers;
|
|
782
|
+
* Interior-pointing normals lead to negative winding numbers;
|
|
783
|
+
* Nested shells lead to winding numbers with absolute value 2 or greater.
|
|
784
|
+
|
|
785
|
+
onBoundaryNormal : `numpy.array`
|
|
786
|
+
The boundary normal if the point lies on a boundary, `None` otherwise.
|
|
787
|
+
|
|
788
|
+
See Also
|
|
789
|
+
--------
|
|
790
|
+
`contains_point` : Test if a point lies within the solid.
|
|
791
|
+
|
|
792
|
+
Notes
|
|
793
|
+
-----
|
|
794
|
+
If `onBoundaryNormal` is not `None`, `windingNumber` is undefined and should be ignored.
|
|
795
|
+
|
|
796
|
+
`winding_number` uses two different implementations:
|
|
797
|
+
* A simple fast implementation if the solid is a number line (dimension <= 1). This is the default for dimension <= 1.
|
|
798
|
+
* A surface integral with integrand: `(x - point) / norm(x - point)**dimension`.
|
|
799
|
+
"""
|
|
800
|
+
point = np.atleast_1d(point)
|
|
801
|
+
windingNumber = 0.0
|
|
802
|
+
onBoundaryNormal = None
|
|
803
|
+
if self.containsInfinity:
|
|
804
|
+
# If the solid contains infinity, then the winding number starts as 1 to account for the boundary at infinity.
|
|
805
|
+
windingNumber = 1.0
|
|
806
|
+
|
|
807
|
+
if Solid.point_outside_bounds(point, self.bounds):
|
|
808
|
+
return windingNumber, onBoundaryNormal
|
|
809
|
+
|
|
810
|
+
if self.dimension <= 1:
|
|
811
|
+
# Fast winding number calculation for a number line specialized to catch boundary edges.
|
|
812
|
+
for boundary in self.boundaries:
|
|
813
|
+
normal = boundary.manifold.normal(0.0)
|
|
814
|
+
separation = np.dot(normal, boundary.manifold.evaluate(0.0) - point)
|
|
815
|
+
if -2.0 * Manifold.minSeparation < separation < Manifold.minSeparation:
|
|
816
|
+
onBoundaryNormal = normal
|
|
817
|
+
break
|
|
818
|
+
else:
|
|
819
|
+
windingNumber += np.sign(separation) / 2.0
|
|
820
|
+
else:
|
|
821
|
+
# Compute the winding number via a surface integral, normalizing by the hypersphere surface area.
|
|
822
|
+
nSphereArea = 2.0
|
|
823
|
+
if self.dimension % 2 == 0:
|
|
824
|
+
nSphereArea *= np.pi
|
|
825
|
+
dimension = self.dimension
|
|
826
|
+
while dimension > 2:
|
|
827
|
+
nSphereArea *= 2.0 * np.pi / (dimension - 2.0)
|
|
828
|
+
dimension -= 2
|
|
829
|
+
|
|
830
|
+
def windingIntegrand(boundaryPoint, boundaryNormal, onBoundaryNormalList):
|
|
831
|
+
vector = boundaryPoint - point
|
|
832
|
+
vectorLength = np.linalg.norm(vector)
|
|
833
|
+
if vectorLength < Manifold.minSeparation:
|
|
834
|
+
onBoundaryNormalList[0] = boundaryNormal
|
|
835
|
+
vectorLength = 1.0
|
|
836
|
+
return vector / (vectorLength**self.dimension)
|
|
837
|
+
|
|
838
|
+
onBoundaryNormalList = [onBoundaryNormal]
|
|
839
|
+
windingNumber += self.surface_integral(windingIntegrand, onBoundaryNormalList) / nSphereArea
|
|
840
|
+
onBoundaryNormal = onBoundaryNormalList[0]
|
|
841
|
+
|
|
842
|
+
return windingNumber, onBoundaryNormal
|