Building composite shapes
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 - holesassert 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()
Transform snapshots
Section titled “Transform snapshots”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))
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_plotsimport 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",)
Names and overlap precedence
Section titled “Names and overlap precedence”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: 24Barray([1, 2, 0])Coordinates: * x (x) float64 24B -0.5 0.5 2.5 y float64 8B 0.0Attributes: labels: {0: 'background', 1: 'left', 2: 'right'} sampling: direct geometry queries; host arrays; no MPI gather{0: 'background', 1: 'left', 2: 'right'}Three-dimensional composition
Section titled “Three-dimensional composition”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()
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 hereplotter = 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",)
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.