bspy.solid

class Boundary:

A portion of the boundary of a solid.

Parameters
  • manifold (Manifold): The differentiable function whose range is one dimension higher than its domain that defines the range of the boundary.
  • domain (Solid, optional): The region of the domain of the manifold that's within the boundary. The default is the full domain of the manifold.
See also

Solid : A region that separates space into an inside and outside, defined by a collection of boundaries. Manifold.full_domain : Return a solid that represents the full domain of the manifold.

Boundary(manifold, domain=None)
domain
def any_point(self):

Return an arbitrary point on the boundary.

Returns
  • point (numpy.array): A point on the boundary.
See Also

Solid.any_point: Return an arbitrary point on the solid.

Notes

The point is computed by evaluating the boundary manifold by an arbitrary point in the domain of the boundary.

class Solid:

A region that separates space into an inside and outside, defined by a collection of boundaries.

Parameters
  • dimension (int): The dimension of the solid (non-negative).
  • containsInfinity (bool): Indicates whether or not the solid contains infinity.
See also

Boundary : A portion of the boundary of a solid.

Notes

Solids also contain a list of boundaries. That list may be empty.

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.

Solid(dimension, containsInfinity)
dimension
containsInfinity
boundaries
bounds
def add_boundary(self, boundary):

Adds a boundary to a solid, recomputing the solid's bounds.

Parameters
  • boundary (Boundary): A boundary to add to the solid.
Notes

While you could just append the boundary to the solid's collection of boundaries, this method also recomputes the solid's bounds for faster intersection and containment operations. Adding the boundary directly to the solid's boundaries collection may result in faulty operations.

def any_point(self):

Return an arbitrary point on the solid.

Returns
  • point (numpy.array): A point on the solid.
See Also

Boundary.any_point: Return an arbitrary point on the boundary.

Notes

The point is computed by calling Boundary.any_point on the solid's first boundary. If the solid has no boundaries but contains infinity, any_point returns the origin. If the solid has no boundaries and doesn't contain infinity, any_point returns None.

def complement(self):

Return the complement of the solid: whatever was inside is outside and vice-versa.

Returns
  • solid (Solid): The complement of the solid.
See Also

intersection: Intersect two solids.
union: Union two solids.
difference: Subtract one solid from another.

def contains_point(self, point):

Test if a point lies within the solid.

Parameters
  • point (array-like): A point that may lie within the solid.
Returns
  • containment (bool): True if point lies within the solid. False otherwise.
See Also

winding_number: Compute the winding number for a point relative to the solid.

Notes

A point is considered contained if it's on the boundary of the solid or it's winding number is greater than 0.5.

def difference(self, other):

Subtract one solid from another.

Parameters
Returns
  • combinedSolid (Solid): A Solid that represents the subtraction of other from self.
See Also

intersection: Intersect two solids.
union: Union two solids.

@staticmethod
def disjoint_bounds(bounds1, bounds2):

Returns whether or not bounds1 and bounds2 are disjoint.

Parameters
  • bounds1 (array-like or None): An array with shape (dimension, 2) of lower and upper and lower bounds on each dimension. If bounds1 is None then then there are no bounds.
  • bounds2 (array-like or None): An array with shape (dimension, 2) of lower and upper and lower bounds on each dimension. If bounds2 is None then then there are no bounds.
Returns
  • disjoint (bool): Value is true if the bounds are disjoint. Value is false if either bounds is None.
def intersection(self, other, cache=None):

Intersect two solids.

Parameters
  • other (Solid): The Solid intersecting self.
  • cache (dict, optional): A dictionary to cache Manifold intersections, speeding computation. If no dictionary is passed, one is created.
Returns
  • combinedSolid (Solid): A Solid that represents the intersection between self and other.
See Also

slice: Slice a solid by a manifold.
union: Union two solids.
difference: Subtract one solid from another.

Notes

To intersect two solids, we slice each solid with the boundaries of the other solid. The slices are the region of the domain that intersect the solid. We then intersect the domain of each boundary with its slice of the other solid. Thus, the intersection of two solids becomes a set of intersections within the domains of their boundaries. This recursion continues until we are intersecting points whose domains have no boundaries.

The only subtlety is when two boundaries are coincident. To avoid overlapping the coincident region, we keep that region 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.

def is_empty(self):

Test if the solid is empty.

Returns
  • isEmpty (bool): True if the solid is empty, False otherwise.
Notes

Casting the solid to bool returns not is_empty.

@staticmethod
def load(fileName):

Load solids and/or manifolds in json format from the specified filename (full path).

Parameters
  • fileName (string): The full path to the file containing the solids and/or manifolds. Can be a relative path.
Returns
  • solidsAndManifolds (list of Solid and/or Manifold): The loaded solids and/or manifolds.
See Also

save: Save a solids and/or manifolds in json format to the specified filename (full path).

@staticmethod
def point_outside_bounds(point, bounds):

Returns whether or not point is outside bounds.

Parameters
  • point (array-like): A point whose dimension matches the bounds.
  • bounds (array-like or None): An array with shape (dimension, 2) of lower and upper and lower bounds on each dimension. If bounds is None then then there are no bounds.
Returns
  • within (bool): Value is true if the point is outside bounds or bounds is None.
@staticmethod
def save(fileName, *solids_or_manifolds):

Save a solids and/or manifolds in json format to the specified filename (full path).

Parameters
  • fileName (string): The full path to the file containing the solids and/or manifolds. Can be a relative path.
  • *solids_or_manifolds (Solid or Manifold): Solids and/or manifolds to save in the same file.
See Also

load: Load solids and/or manifolds in json format from the specified filename (full path).

def slice(self, manifold, cache=None, trimTwin=False):

Slice the solid by a manifold.

Parameters
  • manifold (Manifold): The Manifold used to slice the solid.
  • cache (dict, optional): A dictionary to cache Manifold intersections, speeding computation.
  • trimTwin : bool, default (False): Trim coincident boundary twins on subsequent calls to slice (avoids duplication of overlapping regions). Trimming twins is typically only used in conjunction with intersection.
Returns
  • slice (Solid): A region in the domain of manifold that intersects with the solid. The region may contain infinity.
See Also

intersection: Intersect two solids.
Manifold.intersect: Intersect two manifolds.
Manifold.cached_intersect: Intersect two manifolds, caching the result for twins.
Manifold.complete_slice: Add any missing inherent (implicit) boundaries of this manifold's domain to the given slice.

Notes

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.

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 between the manifold and the boundary. Each is either a crossing or a coincident region.

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 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, 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 until the slice is just a point with no domain.

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 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 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 in the same direction, we union them with the slice.

def surface_integral(self, f, args=(), epsabs=None, epsrel=None, *quadArgs):

Compute the surface integral of a vector field on the boundary of the solid.

Parameters
  • **f : python function f(point: numpy.array, normal: numpy.array, args** (user-defined) -> numpy.array): The vector field to be integrated on the boundary of the solid. It's passed a point on the boundary and its corresponding outward-pointing unit normal, as well as any optional user-defined arguments.
  • args (tuple, optional): Extra arguments to pass to f.
  • *quadArgs (Quadrature arguments passed to scipy.integrate.quad.):
Returns
  • sum (scalar value): The value of the surface integral.
See Also

volume_integral: Compute the volume integral of a function within the solid.
scipy.integrate.quad: Integrate func from a to b (possibly infinite interval) using a technique from the Fortran library QUADPACK.

Notes

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.

surface_integral sums the volume_integral over the domain of the solid's boundaries, using the integrand: numpy.dot(f(point, normal), normal), where normal is the cross-product of the boundary tangents (the normal before normalization).

def transform(self, matrix, matrixInverseTranspose=None):

Transform the range of the solid.

Parameters
  • matrix (numpy.array): A square matrix transformation.
  • matrixInverseTranspose (numpy.array, optional): The inverse transpose of matrix (computed if not provided).
Returns
  • solid (Solid): The transformed solid.
def translate(self, delta):

Translate the range of the solid.

Parameters
  • delta (numpy.array): A 1D array translation.
Returns
  • solid (Solid): The translated solid.
def union(self, other):

Union two solids.

Parameters
Returns
  • combinedSolid (Solid): A Solid that represents the union between self and other.
See Also

intersection: Intersect two solids.
difference: Subtract one solid from another.

def volume_integral(self, f, args=(), epsabs=None, epsrel=None, *quadArgs):

Compute the volume integral of a function within the solid.

Parameters
  • **f : python function f(point: numpy.array, args** (user-defined) -> scalar value): The function to be integrated within the solid. It's passed a point within the solid, as well as any optional user-defined arguments.
  • args (tuple, optional): Extra arguments to pass to f.
  • *quadArgs (Quadrature arguments passed to scipy.integrate.quad.):
Returns
  • sum (scalar value): The value of the volume integral.
See Also

surface_integral: Compute the surface integral of a vector field on the boundary of the solid.
scipy.integrate.quad: Integrate func from a to b (possibly infinite interval) using a technique from the Fortran library QUADPACK.

Notes

The volume integral is computed by recursive application of the divergence theorem: volume_integral(divergence(F)) = surface_integral(dot(F, n)), where F is a vector field and n is the outward boundary unit normal.

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]. 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.

Thus, we have volume_integral(f) = surface_integral(Integral(f) * n[0]). The outward boundary unit normal, n, is the cross product of the boundary manifold's tangent space divided by its length. 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. The length of the cross product appears in the numerator and denominator of the surface integral and cancels. What's left multiplying Integral(f) is the first coordinate of the cross product plus the domain differentials (volume integral). The first coordinate of the cross product of the boundary manifold's tangent space is the first cofactor of the tangent space. And so, surface_integral(Integral(f) * n[0]) = volume_integral(Integral(f) * first cofactor) over each boundary manifold's domain.

So, we have volume_integral(f) = volume_integral(Integral(f) * first cofactor) over each boundary manifold's domain. To compute the volume integral we sum volume_integral over the domain of the solid's boundaries, using the integrand: scipy.integrate.quad(f, x0, x [other coordinates fixed]) * first cofactor. This recursion continues until the boundaries are only points, where we can just sum the integrand.

def winding_number(self, point):

Compute the winding number for a point relative to the solid.

Parameters
  • point (array-like): A point that may lie within the solid.
Returns
  • windingNumber (scalar value): The windingNumber is 0 if the point is outside the solid, 1 if it's inside. Other values indicate issues:
    • A point on the boundary leads to an undefined (random) winding number;
    • Boundaries with gaps or overlaps lead to fractional winding numbers;
    • Interior-pointing normals lead to negative winding numbers;
    • Nested shells lead to winding numbers with absolute value 2 or greater.
  • onBoundaryNormal (numpy.array): The boundary normal if the point lies on a boundary, None otherwise.
See Also

contains_point: Test if a point lies within the solid.

Notes

If onBoundaryNormal is not None, windingNumber is undefined and should be ignored.

winding_number uses two different implementations:

  • A simple fast implementation if the solid is a number line (dimension <= 1). This is the default for dimension <= 1.
  • A surface integral with integrand: (x - point) / norm(x - point)**dimension.