Trajectory¶
Analysis almost never cares about a single snapshot. How do you hold many frames in time order without inventing a second data model?
A Trajectory is an eager, ordered sequence of Frame objects. Each element
is still one frame — blocks, metadata, optional box. Time stacks snapshots; it
does not replace them.
What it is not: a lazy file cursor. Seekable readers live under
molpy.io trajectory readers; construct a Trajectory when you want
an in-memory sequence with len, indexing, and slicing.
Building a trajectory from a list¶
Pass a sequence of frames (a list); step= and time= label them:
import molpy as mp
frames = []
for i in range(5):
f = mp.Frame()
f["atoms"] = mp.Block({"x": [float(i)], "y": [0.0], "z": [0.0]})
f.meta = {"time": i * 10.0}
frames.append(f)
traj = mp.Trajectory(frames)
print(len(traj)) # 5
print(traj[0]["atoms"]["x"]) # [0.]
Frames are materialized¶
The constructor copies every frame into the native container, so frames from a
generator are listed first. Use molpy.io.read_lammps_dump_trajectory or
molpy.io.read_xyz_trajectory when data must remain lazy and seekable on disk.
def make_frames(n):
for i in range(n):
f = mp.Frame()
f["atoms"] = mp.Block({"x": [float(i)], "y": [0.0], "z": [0.0]})
f.meta = {"time": i * 0.5}
yield f
traj_from_iterable = mp.Trajectory(list(make_frames(1000)))
print(len(traj_from_iterable)) # 1000
The whole sequence lives in memory. File readers avoid that eager materialization.
Slicing and indexing¶
For list-backed trajectories, standard Python indexing and slicing work as expected. Indexing returns a Frame; slicing returns a new Trajectory.
first_two = traj[:2]
print(len(first_two)) # 2
strided = traj[::2]
print(len(strided)) # 3
last = traj[-1]
print(last.meta["time"]) # 40.0
Slicing with a stride (traj[::n]) is a convenient way to downsample for quick inspection.
Transforms with map¶
map applies a function to every frame immediately and returns a new trajectory. The original frames are unchanged.
def shift_x(frame):
new = mp.Frame()
x = frame["atoms"]["x"]
new["atoms"] = mp.Block(
{
"x": x + 10.0,
"y": frame["atoms"]["y"],
"z": frame["atoms"]["z"],
}
)
new.meta = frame.meta
return new
shifted = traj.map(shift_x)
shifted_list = list(shifted)
print(shifted_list[0]["atoms"]["x"]) # [10.]
print(traj[0]["atoms"]["x"]) # [0.] — original unchanged
When to use Trajectory¶
Use Trajectory when time is part of the scientific question — following an observable over many snapshots, computing time correlations, or iterating through an I/O stream. If you only need a single state, Frame remains the right abstraction.
The trajectory does not invent a new kind of system state. It keeps frame meaning intact while adding temporal ordering. That is the entire point: one structure, many times.
See also: Block and Frame, Box and Periodicity.