Skip to content

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

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

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.