Skip to main content

ferritin_core/columns/
frame.rs

1//! Zero-copy views of the per-frame columns of a [`Model`].
2
3use super::buffer::{Column, ColumnData};
4use super::error::{BoundaryError, to_u32};
5use super::gpu::{GpuBuffer, PositionLayout};
6use crate::model::Model;
7
8/// Borrowed per-frame columns: coordinates and per-atom scalars.
9///
10/// Every accessor returns a slice of the model's own
11/// [`AtomicConformation`](crate::AtomicConformation) storage; nothing is copied.
12/// The borrow checker keeps the model alive and unmodified for `'a`. Pointers
13/// handed across FFI from these slices carry the same rule, unenforced: they
14/// are valid until the model is dropped or its conformation is mutated.
15#[derive(Clone, Copy)]
16pub struct FrameColumns<'a> {
17    model: &'a Model,
18    n_atoms: u32,
19}
20
21impl std::fmt::Debug for FrameColumns<'_> {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("FrameColumns")
24            .field("n_atoms", &self.n_atoms)
25            .finish_non_exhaustive()
26    }
27}
28
29impl<'a> FrameColumns<'a> {
30    /// Borrow the per-frame columns of `model`, validating their lengths.
31    ///
32    /// Prefer [`TopologyColumns::frame`](super::TopologyColumns::frame), which
33    /// also checks that the frame belongs to a given topology snapshot.
34    pub fn new(model: &'a Model) -> Result<Self, BoundaryError> {
35        let n_atoms = model.n_atoms();
36        model.conformation.validate(n_atoms)?;
37        Ok(Self {
38            model,
39            n_atoms: to_u32("atom count", n_atoms)?,
40        })
41    }
42
43    /// Number of atoms.
44    pub fn n_atoms(&self) -> u32 {
45        self.n_atoms
46    }
47
48    /// X coordinates (Å).
49    pub fn x(&self) -> &'a [f32] {
50        &self.model.conformation.x
51    }
52
53    /// Y coordinates (Å).
54    pub fn y(&self) -> &'a [f32] {
55        &self.model.conformation.y
56    }
57
58    /// Z coordinates (Å).
59    pub fn z(&self) -> &'a [f32] {
60        &self.model.conformation.z
61    }
62
63    /// Occupancy, if recorded.
64    pub fn occupancy(&self) -> Option<&'a [f32]> {
65        self.model.conformation.occupancy.as_deref()
66    }
67
68    /// Isotropic B-factor, if recorded.
69    pub fn b_iso(&self) -> Option<&'a [f32]> {
70        self.model.conformation.b_iso.as_deref()
71    }
72
73    /// Per-atom confidence (e.g. pLDDT), if recorded.
74    pub fn confidence(&self) -> Option<&'a [f32]> {
75        self.model.conformation.confidence.as_deref()
76    }
77
78    /// Every present per-frame column by stable name. Absent optional columns
79    /// are omitted.
80    pub fn columns(&self) -> Vec<Column<'a>> {
81        [
82            ("x", Some(self.x())),
83            ("y", Some(self.y())),
84            ("z", Some(self.z())),
85            ("occupancy", self.occupancy()),
86            ("b_iso", self.b_iso()),
87            ("confidence", self.confidence()),
88        ]
89        .into_iter()
90        .filter_map(|(name, values)| {
91            values.map(|values| Column {
92                name,
93                data: ColumnData::F32(values),
94            })
95        })
96        .collect()
97    }
98
99    /// Interleave x/y/z into one position buffer. Always an owned copy, since
100    /// the model stores coordinates as separate SoA columns.
101    pub fn positions(&self, layout: PositionLayout) -> GpuBuffer<'static> {
102        let components = layout.components();
103        let mut bytes = Vec::with_capacity(self.n_atoms as usize * layout.stride_bytes() as usize);
104        for ((&x, &y), &z) in self.x().iter().zip(self.y()).zip(self.z()) {
105            bytes.extend_from_slice(&x.to_le_bytes());
106            bytes.extend_from_slice(&y.to_le_bytes());
107            bytes.extend_from_slice(&z.to_le_bytes());
108            if components == 4 {
109                bytes.extend_from_slice(&1.0f32.to_le_bytes());
110            }
111        }
112        GpuBuffer {
113            label: "positions",
114            layout: layout.buffer_layout(self.n_atoms),
115            bytes: bytes.into(),
116        }
117    }
118}