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.