Skip to content
Projects

Building composite shapes

Download notebookView on GitHubRun when the site was built.

Build apertures and chambers from Boolean operations, position independent copies, and assign names to objects. Membership remains independent of any plotting resolution.

a | b, a & b, and a - b form union, intersection, and difference. Operations take snapshots of their operands. The named collection retains references to its objects.

import matplotlib.pyplot as plt
from simgeometry import Circle, Cuboid, GeometryCollection, Rectangle, Sphere
plate = Rectangle((0, 0), (2.0, 1.2))
holes = Circle((-0.5, 0), 0.2) | Circle((0.5, 0), 0.2)
aperture = plate - holes
assert aperture.contains((0, 0))
assert not aperture.contains((0.5, 0))
aperture.show(resolution=220, labels=False, display=False)
plt.title("Plate with two holes")
plt.show()
Notebook output

Use translated(), rotated() and scaled() for the same non-mutating API on every shape. Angles are in degrees. Rotation and scaling default to the world origin; pass origin= to choose another centre. Existing primitive translate() methods retain their in-place behavior.

General transformed(matrix, offset) uses world = matrix @ local + offset. Normals transform with the inverse transpose, so they remain correct under nonuniform scaling.

original = Rectangle((0, 0), (1.0, 0.35))
geometry = GeometryCollection(
{
"original": original,
"rotated": original.rotated(35).translated((0.1, 0.7)),
"scaled": original.scaled((0.7, 1.6)).translated((0.2, -0.7)),
}
)
geometry.show(resolution=180, display=False)
plt.title("Independent transformed shapes")
plt.show()
original.translate((5, 0))
assert geometry["rotated"].contains((0.1, 0.7))
Notebook output

The same methods transform 3D shapes; rotated() then takes an axis, and a negative factor in scaled() mirrors. The bracket below has a post at one end and a peg on one side, so no rotation turns it into its mirror image. Click Interactive 3-D view and turn the scene to compare the four copies.

import plasma_plots
import pyvista as pv
from simgeometry import Cylinder
pv.OFF_SCREEN = True # render without a window, e.g. on a docs server
bracket = (
Cuboid((0, 0, 0), (1.0, 0.3, 0.2))
| Cylinder((0.35, 0, 0.1), (0.35, 0, 0.8), 0.1) # the post
| Cylinder((-0.35, 0, 0), (-0.35, 0.45, 0), 0.08) # the peg
)
copies = GeometryCollection(
{
"original": bracket,
"rotated": bracket.rotated(90, axis=(1, 0, 0)).translated((0, 1.6, 0)),
"scaled": bracket.scaled((1, 1, 1.6)).translated((1.6, 0, 0)),
"mirrored": bracket.scaled((-1, 1, 1)).translated((1.6, 1.6, 0)),
}
)
plotter = copies.show_3d(
backend="pyvista", display=False, resolution=48, show_bounds=False
)
plasma_plots.scene(
plotter,
alt="A bracket with a post and a peg, next to rotated, stretched and mirrored copies",
)
A bracket with a post and a peg, next to rotated, stretched and mirrored copies
Open in a new tab

Collections return integer labels from evaluate(): zero is background, and labels 1 through N follow insertion order. The last listed object wins at overlapping points. quantity="occupancy" instead returns the union mask. Object names are stored in the result’s labels attribute.

overlap = GeometryCollection({"left": Circle((0, 0), 1), "right": Circle((1, 0), 1)})
labels = overlap.evaluate(x=[-0.5, 0.5, 2.5], y=0)
assert labels.values.tolist() == [1, 2, 0]
print(labels)
print(labels.attrs["labels"])
<xarray.DataArray 'labels' (x: 3)> Size: 24B
array([1, 2, 0])
Coordinates:
* x (x) float64 24B -0.5 0.5 2.5
y float64 8B 0.0
Attributes:
labels: {0: 'background', 1: 'left', 2: 'right'}
sampling: direct geometry queries; host arrays; no MPI gather
{0: 'background', 1: 'left', 2: 'right'}

Difference and intersection work for 3D objects too. Here a sphere cuts a cavity out of a block; an oblique section lets us inspect the interior. The faceted display is sampled, so refine resolution to check small features.

block = Cuboid((0, 0, 0), (2, 2, 2)) - Sphere((0, 0, 0), 0.75)
fig = plt.figure(figsize=(10, 4), layout="constrained")
ax = fig.add_subplot(121, projection="3d")
block.show_3d(
ax=ax, resolution=32, clip=((0, 0, 0), (1, 0, 0)), labels=False, display=False
)
ax.set_title("Clipped cavity")
ax = fig.add_subplot(122)
block.slice(origin=(0, 0, 0), normal=(1, 1, 1)).plot(
ax=ax, resolution=180, labels=False
)
ax.set_title("Oblique section")
plt.show()
Notebook output

All three operations side by side: the union of three rods, the intersection of a cube and a ball (a cube with its corners rounded off), and the difference of a ball and the rods, which drills three bores through it. Turn the scene to look down a bore, or zoom in where the bores meet at the centre.

from simgeometry import Cylinder
ball = Sphere((0, 0, 0), 1.0)
rods = (
Cylinder((-1.2, 0, 0), (1.2, 0, 0), 0.35)
| Cylinder((0, -1.2, 0), (0, 1.2, 0), 0.35)
| Cylinder((0, 0, -1.2), (0, 0, 1.2), 0.35)
)
booleans = GeometryCollection(
{
"union": rods,
"intersection": (
Cuboid((0, 0, 0), (1.6, 1.6, 1.6)) & Sphere((0, 0, 0), 1.1)
).translated((-2.0, 2.0, 0)),
"difference": (ball - rods).translated((-4.0, 4.0, 0)),
},
styles={"intersection": {"opacity": 1.0}}, # opaque, to show its flat faces
)
assert not booleans["difference"].contains((-4.0, 4.0, 0)) # the bores meet here
plotter = booleans.show_3d(
backend="pyvista", display=False, resolution=64, opacity=0.8, show_bounds=False
)
plasma_plots.scene(
plotter,
alt="Three crossed rods, a cube with rounded corners, and a ball with three bores",
)
Three crossed rods, a cube with rounded corners, and a ball with three bores
Open in a new tab

Try an intersection between the block and a rotated box. For inverted objects, supply a finite viewing/sampling region explicitly. Bounds on composed shapes conservatively enclose their operands; they do not certify that the resulting region is nonempty.