Animate a scene
Move structures and endpoints with trajectories, deform meshes over time, write your own Trajectory or Deformation, and read snapshots at any time.
A DynamicScene wraps a static Scene with motion. You attach a trajectory or a deformation to a structure ID, or a trajectory to an antenna ID, and call at(time_s) to get a SceneSnapshot for that time.
Move a structure#
LinearTrajectory moves at constant velocity from origin at reference_time_s (default 0):
from witwin.core import Box, DynamicScene, LinearTrajectory, PhysicalMaterial, Scene, Structure
car = Structure(Box(position=(0.0, 0.0, 0.75), size=(4.5, 1.8, 1.5)), PhysicalMaterial.perfect_conductor(), structure_id=1)
scene = Scene(structures=[car])
drive = LinearTrajectory(origin=(-20.0, 0.0, 0.0), velocity=(15.0, 0.0, 0.0))
street = DynamicScene(scene, structure_trajectories={1: drive})
for t in (0.0, 1.0, 2.0):
motion = street.at(t).structures[0].rigid_motion
print(t, motion.translation.tolist(), motion.velocity.tolist())0.0 [-20.0, 0.0, 0.0] [15.0, 0.0, 0.0]
1.0 [-5.0, 0.0, 0.0] [15.0, 0.0, 0.0]
2.0 [10.0, 0.0, 0.0] [15.0, 0.0, 0.0]The snapshot does not move the box. It attaches a RigidMotion next to the authored geometry, and the solver adds translation to the authored pose. At t = 1.0 the car's centre is therefore at (0, 0, 0.75) + (-5, 0, 0). A structure without a trajectory gets rigid_motion=None.
LinearTrajectory also takes a constant rotation and an angular_velocity. Core copies both into every RigidMotion it returns and does not integrate the angular velocity; a solver may use it, for example for Doppler.
Move an endpoint#
from witwin.core import AntennaState
scene = Scene(
structures=[car],
endpoints=[AntennaState(1, "tx", (0.0, -10.0, 6.0)), AntennaState(2, "rx", (0.0, 0.0, 1.5))],
)
street = DynamicScene(
scene,
structure_trajectories={1: drive},
endpoint_trajectories={2: drive}, # the receiver rides in the car
)
snapshot = street.at(1.0)
print(snapshot.endpoints[0].rigid_motion, snapshot.endpoints[1].rigid_motion.translation.tolist())None [-5.0, 0.0, 0.0]Keys are structure or antenna IDs (plain integers work). An ID that is not in the scene raises KeyError.
Write your own trajectory#
Any object with at(time_s) -> RigidMotion satisfies the Trajectory protocol. Use tensor maths so that gradients flow through time_s and your parameters:
import math
import torch
from witwin.core import RigidMotion
class Orbit:
"""Circle of a given radius in the xy plane, one turn per period."""
def __init__(self, radius, period_s):
self.radius = torch.as_tensor(radius, dtype=torch.float32)
self.period_s = period_s
def at(self, time_s):
angle = 2 * math.pi * torch.as_tensor(time_s, dtype=torch.float32) / self.period_s
offset = torch.stack((self.radius * torch.cos(angle), self.radius * torch.sin(angle), torch.zeros(())))
return RigidMotion(translation=offset, rotation=torch.stack((angle, torch.zeros(()), torch.zeros(()))))
drone = Structure(Box(size=(0.3, 0.3, 0.1)), PhysicalMaterial(), structure_id=5)
sky = DynamicScene(Scene(structures=[drone]), structure_trajectories={5: Orbit(10.0, 8.0)})
print([round(v, 2) for v in sky.at(2.0).structures[0].rigid_motion.translation.tolist()])[-0.0, 10.0, 0.0]RigidMotion takes translation, rotation, velocity and angular_velocity, all optional and keyword-only. rotation uses the antenna convention, (yaw, pitch, roll) or a quaternion, and is composed on the left of the authored rotation. All its tensors must share a device and a dtype.
Deform a mesh#
A deformation changes vertex positions without changing faces. Any object with at(time_s) -> DeformationState satisfies the Deformation protocol. DeformationState takes exactly one of vertices (absolute local vertices, (V, 3)) or offsets (added to the authored local vertices, (V, 3)):
from witwin.core import DeformationState, Mesh
sheet_vertices = torch.tensor([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]])
sheet = Structure(Mesh(sheet_vertices, torch.tensor([[0, 1, 2], [0, 2, 3]]), fill_mode="surface"), PhysicalMaterial(), structure_id=9)
class Flutter:
def at(self, time_s):
lift = 0.05 * torch.sin(torch.as_tensor(time_s, dtype=torch.float32) * 2 * math.pi)
return DeformationState(offsets=torch.stack([torch.zeros(3), torch.zeros(3), torch.stack((torch.zeros(()), torch.zeros(()), lift)), torch.zeros(3)]))
flag = DynamicScene(Scene(structures=[sheet]), structure_deformations={9: Flutter()})
print(flag.at(0.25).structures[0].deformation.offsets[2].tolist())[0.0, 0.0, 0.05000000074505806]The deformation is resolved first, then the authored transform, then any rigid motion. A structure can have both a deformation and a trajectory.
Gradients through time and motion#
Snapshots keep every tensor. Pass a tensor time_s, or trajectory tensors that require gradients, and differentiate whatever the solver computes:
velocity = torch.tensor([15.0, 0.0, 0.0], requires_grad=True)
street = DynamicScene(Scene(structures=[car]), structure_trajectories={1: LinearTrajectory(origin=torch.zeros(3), velocity=velocity)})
translation = street.at(0.5).structures[0].rigid_motion.translation
translation.sum().backward()
print(velocity.grad.tolist())[0.5, 0.5, 0.5]A tensor time_s must be 0-d. For LinearTrajectory it must share the trajectory's device and dtype.
Versions over time#
Every snapshot reports the four versions. Motion and time change only geometry_version; topology, material and assignment versions stay those of the static scene. Two calls with the same time and unchanged tensors give equal versions. dynamic_revision (an integer, default 0) lets you mark a change to the motion descriptors that Core cannot see, such as a new Python attribute in your own trajectory class. See Identity and versions and the moving scene tutorial.