Sample a moving scene
Build a room with a walking person, sample DynamicScene snapshots over time, use versions to decide what to recompute, and differentiate with respect to speed.
This tutorial walks through what a solver sees when a scene moves. A person walks through a room between a transmitter and a receiver. You sample the scene at several times, compose the person's world position the way a solver does, check which versions change, and take the gradient of a distance with respect to the walking speed.
It uses Core only and runs on a CPU in under a second.
The full script#
import torch
from witwin.core import AntennaState, Box, DynamicScene, LinearTrajectory, PhysicalMaterial, Scene, Structure
# Fixed IDs, so the output is the same in every run.
concrete = PhysicalMaterial(eps_r=5.24, sigma_e=0.05, name="concrete", material_id=1)
body = PhysicalMaterial(eps_r=40.0, sigma_e=1.0, name="body", material_id=2)
wall = Structure(Box(position=(0.0, 4.0, 1.5), size=(10.0, 0.2, 3.0)), concrete,
name="wall", structure_id=1, assignment_id=1, surface_id=1)
walker = Structure(Box(position=(0.0, 0.0, 0.9), size=(0.4, 0.3, 1.8)), body,
name="walker", structure_id=2, assignment_id=2, surface_id=2)
tx = AntennaState(1, "tx", (-3.0, 0.0, 1.5), power_w=1.0)
rx = AntennaState(2, "rx", (3.0, 0.0, 1.5))
scene = Scene(structures=[wall, walker], endpoints=[tx, rx])
# The walker starts 2 m to the -y side of its authored pose and walks at 1.2 m/s along +y.
velocity = torch.tensor([0.0, 1.2, 0.0], requires_grad=True)
walk = LinearTrajectory(origin=torch.tensor([0.0, -2.0, 0.0]), velocity=velocity)
room = DynamicScene(scene, structure_trajectories={2: walk})
def walker_centre(snapshot):
state = snapshot.structures[1]
return state.structure.geometry.position + state.rigid_motion.translation
previous = None
for t in (0.0, 0.5, 1.0, 1.5):
snapshot = room.at(t)
centre = walker_centre(snapshot)
line = f"t={t:.1f} s walker centre {[round(v, 2) for v in centre.tolist()]}"
if previous is not None:
line += (f" geometry changed: {snapshot.geometry_version != previous.geometry_version}"
f" topology same: {snapshot.topology_version == previous.topology_version}"
f" material same: {snapshot.material_version == previous.material_version}")
print(line)
previous = snapshot
# Distance from the walker to the receiver at t = 1.5 s, and its gradient with respect to the velocity.
snapshot = room.at(1.5)
distance = torch.linalg.vector_norm(walker_centre(snapshot) - snapshot.endpoints[1].antenna.position)
distance.backward()
print(f"distance {distance.item():.3f} m")
print(f"d(distance)/d(velocity) = {[round(v, 3) for v in velocity.grad.tolist()]}")Expected output#
t=0.0 s walker centre [0.0, -2.0, 0.9]
t=0.5 s walker centre [0.0, -1.4, 0.9] geometry changed: True topology same: True material same: True
t=1.0 s walker centre [0.0, -0.8, 0.9] geometry changed: True topology same: True material same: True
t=1.5 s walker centre [0.0, -0.2, 0.9] geometry changed: True topology same: True material same: True
distance 3.066 m
d(distance)/d(velocity) = [-1.468, -0.098, -0.294]What happened#
The snapshot carries motion, not moved geometry. snapshot.structures[1].structure.geometry.position is still the authored (0, 0, 0.9). The motion is in rigid_motion.translation. A solver adds it after the authored transform; walker_centre does the same by hand. The wall has no trajectory, so its rigid_motion is None.
Only the geometry version moves. Each new time gives a new geometry_version. topology_version, material_version and assignment_version stay those of the static scene. A solver that caches per version can therefore keep its material tables and its topology-dependent data, and refresh only geometry.
Gradients reach the trajectory. The translation is origin + velocity * (t - reference_time_s), so the derivative of the walker's centre with respect to velocity is t times the identity. The distance gradient is that times the unit vector from the receiver to the walker, (-3, -0.2, -0.6) / 3.066 scaled by 1.5 s.
Variations#
- Make the receiver walk too: add
endpoint_trajectories={2: walk}. Endpoint motion composes the same way, after the antenna's orientation. - Pass a tensor time,
room.at(torch.tensor(1.5, requires_grad=True)), to get gradients with respect to time. - Replace
LinearTrajectorywith your own class that hasat(time_s); see Animate a scene.