Create geometry
Place and rotate the nine analytic primitives, choose an axis, export triangle meshes, and sample signed distances and soft occupancy masks.
Core has nine analytic primitives: Box, Sphere, Cylinder, Cone, Ellipsoid, Pyramid, Prism, Torus and HollowBox. Each one can return a triangle mesh, a signed distance and a soft occupancy mask. For triangle meshes, see Load a mesh.
Place a primitive#
All primitives take position (metres) and rotation. The remaining arguments set the size.
import math
from witwin.core import Box, Cone, Cylinder, Ellipsoid, HollowBox, Prism, Pyramid, Sphere, Torus
shapes = [
Box(position=(0.0, 0.0, 0.5), size=(2.0, 1.0, 1.0)),
Sphere(position=(3.0, 0.0, 0.5), radius=0.5),
Cylinder(position=(0.0, 3.0, 1.0), radius=0.2, height=2.0), # a pillar along z
Cone(position=(3.0, 3.0, 0.0), radius=0.5, height=1.0), # apex at z=0, base at z=1
Ellipsoid(position=(6.0, 0.0, 0.5), radii=(0.8, 0.4, 0.3)),
Pyramid(position=(6.0, 3.0, 0.0), base_size=1.0, height=1.0),
Prism(position=(9.0, 0.0, 0.5), radius=0.4, height=1.0, num_sides=6),
Torus(position=(9.0, 3.0, 0.5), major_radius=0.6, minor_radius=0.1),
HollowBox(position=(12.0, 0.0, 1.0), outer_size=(2.0, 2.0, 2.0), inner_size=(1.8, 1.8, 1.8)),
]
print([shape.kind for shape in shapes])['box', 'sphere', 'cylinder', 'cone', 'ellipsoid', 'pyramid', 'prism', 'torus', 'hollow_box']Cone and Pyramid put their apex at position and their base at +height along the axis. All others are centred on position. The full table is in Coordinates and units.
Orient a primitive#
Use axis for the four axial shapes (Cylinder, Cone, Pyramid, Prism) and Torus:
beam = Cylinder(position=(0.0, 0.0, 2.9), radius=0.1, height=6.0, axis="x") # a beam along xUse rotation for any direction. It takes a quaternion (w, x, y, z) or Euler angles (roll, pitch, yaw) in radians:
tilted = Box(size=(1.0, 0.2, 0.2), rotation=(0.0, 0.0, math.pi / 4)) # 45 degrees about z
quarter = Box(size=(1.0, 0.2, 0.2), rotation=(math.cos(math.pi / 8), 0.0, 0.0, math.sin(math.pi / 8))) # same, as a quaternionWarning
Geometry Euler angles are (roll, pitch, yaw). Antenna orientations use (yaw, pitch, roll). See Coordinates and units.
Export a triangle mesh#
to_mesh(segments=16) returns world-space vertices (N, 3) and faces (M, 3) as tensors. segments controls the tessellation of curved shapes; Box, HollowBox, Pyramid and Prism ignore it.
vertices, faces = Sphere(radius=0.5).to_mesh(segments=8)
print(vertices.shape, faces.shape, faces.dtype)torch.Size([144, 3]) torch.Size([224, 3]) torch.int64The vertices keep the autograd graph of the geometry's tensors, so a solver that consumes meshes can still pass gradients back to position, radius and the other parameters.
Note
In 0.4.0, meshes from axis="x" or axis="y" have their triangles wound inward (negative signed volume). The shape is right; only the face orientation is flipped. If your consumer relies on outward normals, reverse the faces with faces.flip(1).
Sample a signed distance#
signed_distance(x, y, z) evaluates element by element. Pass three tensors of the same shape:
import torch
box = Box(size=(2.0, 2.0, 2.0))
points_x = torch.tensor([0.0, 1.0, 1.5])
zeros = torch.zeros(3)
print(box.signed_distance(points_x, zeros, zeros))tensor([-1.0000, 0.0000, 0.5000])Negative is inside, zero is on the surface, positive is outside, in metres.
Make an occupancy grid#
to_mask(x, y, z, offset=0.0, beta=None) returns a soft occupancy in [0, 1]. Pass a meshgrid built with indexing="ij", so that Core can read the grid spacing and choose beta:
axis = torch.linspace(-1.0, 1.0, 101) # 2 cm spacing
xx, yy, zz = torch.meshgrid(axis, axis, axis, indexing="ij")
ball = Sphere(radius=0.5)
mask = ball.to_mask(xx, yy, zz) # beta = 0.05 * 0.02 m = 1 mm
grown = ball.to_mask(xx, yy, zz, offset=0.1) # a 0.6 m sphere
print(round(float(mask.sum()) * 0.02**3, 3), round(float(grown.sum()) * 0.02**3, 3))0.523 0.904The sums match 4/3 π r³ for r = 0.5 m (0.524 m³) and r = 0.6 m (0.905 m³). To get a binary mask, threshold at 0.5. For gradients, pass a wider beta; see Tensors and gradients.
Train a geometry parameter#
Pass tensors that require gradients. The primitive stores them as they are.
radius = torch.tensor(0.4, requires_grad=True)
ball = Sphere(radius=radius)
volume = ball.to_mask(xx, yy, zz, beta=0.01).sum() * 0.02**3
volume.backward()
print(round(radius.grad.item(), 2)) # about 4 * pi * r**2 = 2.012.01On a GPU, create the tensors on the device first, or pass device="cuda"; Core puts Python values on the same device as your tensors and refuses to mix devices.