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 / molrs 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 list (or any iterable that is materialised on construction):
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.]
Iterables are materialized¶
The constructor accepts any iterable, but materializes it immediately into the native container. Use molpy.io.read_lammps_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(make_frames(1000))
print(len(traj_from_iterable)) # 1000
The generator is consumed during construction. 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.