Source code for CodeEntropy.trajectory.frames
"""Frame-selection primitives for trajectory-indexed execution.
Frame-index contract:
- FrameSelection.indices are absolute MDAnalysis trajectory indices.
- MDAnalysis trajectory access must use these absolute frame indices.
- Arrays produced by analyses over FrameSelection are indexed locally with
enumerate(FrameSelection.indices).
"""
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
[docs]
@dataclass(frozen=True)
class FrameSelection:
"""Absolute trajectory frame selection.
Attributes:
indices: Absolute source-trajectory frame indices selected for analysis.
"""
indices: tuple[int, ...]
[docs]
@classmethod
def from_bounds(cls, start: int, stop: int, step: int) -> FrameSelection:
"""Build a frame selection from Python range semantics.
Args:
start: Inclusive source-trajectory start frame.
stop: Exclusive source-trajectory stop frame.
step: Frame stride.
Returns:
FrameSelection containing absolute source-trajectory frame indices.
Raises:
ValueError: If ``step`` is not positive.
"""
if step <= 0:
raise ValueError(f"Frame step must be positive, got {step}")
return cls(indices=tuple(range(int(start), int(stop), int(step))))
def __len__(self) -> int:
"""Return the number of selected frames."""
return len(self.indices)
def __iter__(self) -> Iterator[int]:
"""Iterate over absolute source-trajectory frame indices."""
return iter(self.indices)
@property
def n_frames(self) -> int:
"""Return the number of selected frames."""
return len(self)
@property
def source_indices(self) -> tuple[int, ...]:
"""Return absolute source-trajectory frame indices.
This compatibility property is intentionally identical to ``indices``.
"""
return self.indices
@property
def analysis_indices(self) -> tuple[int, ...]:
"""Return active analysis frame indices.
Physical frame slicing has been removed, so analysis indices are absolute
source-trajectory indices.
"""
return self.indices
@property
def source_start(self) -> int | None:
"""Return the first selected source frame, or None if empty."""
return self.indices[0] if self.indices else None
@property
def source_stop_exclusive(self) -> int | None:
"""Return one past the final selected source frame, or None if empty."""
return self.indices[-1] + 1 if self.indices else None
[docs]
def iter_indices(self) -> Iterator[int]:
"""Yield absolute source-trajectory frame indices."""
yield from self.indices
[docs]
def iter_source_indices(self) -> Iterator[int]:
"""Yield absolute source-trajectory frame indices."""
yield from self.indices
[docs]
def iter_analysis_indices(self) -> Iterator[int]:
"""Yield active analysis frame indices.
Since physical frame slicing has been removed, these are absolute source
trajectory frame indices.
"""
yield from self.indices
[docs]
def iter_pairs(self) -> Iterator[tuple[int, int]]:
"""Yield ``(local_i, absolute_frame_index)`` pairs."""
yield from enumerate(self.indices)
[docs]
def infer_step(self) -> int:
"""Infer the regular stride in selected frame indices.
Returns:
Integer step between selected frames. Returns 1 for zero or one frame.
Raises:
ValueError: If the frame selection is not regularly spaced.
"""
if len(self.indices) <= 1:
return 1
step = self.indices[1] - self.indices[0]
if step <= 0:
raise ValueError("Frame indices must be strictly increasing.")
for left, right in zip(self.indices, self.indices[1:], strict=False):
if right - left != step:
raise ValueError("Frame selection is not regularly spaced.")
return step
[docs]
def infer_source_step(self) -> int:
"""Return the regular source-frame stride."""
return self.infer_step()
[docs]
def infer_analysis_step(self) -> int:
"""Return the regular analysis-frame stride."""
return self.infer_step()