1use super::buffer::{Column, ColumnData};
4use super::dictionary::StringDictionary;
5use super::error::{BoundaryError, BoundaryErrorKind, to_u32};
6use super::frame::FrameColumns;
7use super::gpu::{BufferLayout, GpuBuffer};
8use super::handles::{AtomHandle, ChainHandle, ResidueHandle};
9use crate::model::{AtomicHierarchy, Model, ResidueGroup};
10use std::borrow::Cow;
11use std::ops::Range;
12use std::sync::Arc;
13
14pub const NO_ALT_LOC: u32 = 0;
16pub const NO_INS_CODE: u32 = 0;
18pub const NO_FORMAL_CHARGE: i8 = i8::MIN;
20pub const RESIDUE_GROUP_POLYMER: u8 = 0;
22pub const RESIDUE_GROUP_NON_POLYMER: u8 = 1;
24
25#[derive(Clone, Debug)]
51pub struct TopologyColumns {
52 hierarchy: Arc<AtomicHierarchy>,
53 n_atoms: u32,
54 n_residues: u32,
55 n_chains: u32,
56 n_bonds: u32,
57 atom_name: StringDictionary,
58 auth_atom_name: StringDictionary,
59 element: Vec<u8>,
60 alt_loc: Vec<u32>,
61 formal_charge: Vec<i8>,
62 comp_id: StringDictionary,
63 auth_comp_id: StringDictionary,
64 ins_code: Vec<u32>,
65 residue_group: Vec<u8>,
66 label_asym_id: StringDictionary,
67 auth_asym_id: StringDictionary,
68 entity_id: StringDictionary,
69}
70
71fn encode_char(field: &str, row: usize, value: Option<char>) -> Result<u32, BoundaryError> {
72 match value {
73 None => Ok(0),
74 Some('\0') => Err(BoundaryError::new(
75 BoundaryErrorKind::InvalidModel,
76 format!("{field}[{row}] is NUL, which is reserved for 'absent'"),
77 )),
78 Some(c) => Ok(c as u32),
79 }
80}
81
82fn dictionary(values: &[String]) -> Result<StringDictionary, BoundaryError> {
83 StringDictionary::encode(values.iter().map(String::as_str))
84}
85
86impl TopologyColumns {
87 pub fn encode(hierarchy: &Arc<AtomicHierarchy>) -> Result<Self, BoundaryError> {
89 hierarchy.validate()?;
90 let atoms = &hierarchy.atoms;
91 let residues = &hierarchy.residues;
92 let chains = &hierarchy.chains;
93
94 let alt_loc = atoms
95 .alt_loc
96 .iter()
97 .enumerate()
98 .map(|(i, &c)| encode_char("atom.alt_loc", i, c))
99 .collect::<Result<_, _>>()?;
100 let formal_charge = atoms
101 .formal_charge
102 .iter()
103 .enumerate()
104 .map(|(i, &charge)| match charge {
105 None => Ok(NO_FORMAL_CHARGE),
106 Some(NO_FORMAL_CHARGE) => Err(BoundaryError::new(
107 BoundaryErrorKind::InvalidModel,
108 format!("atom.formal_charge[{i}] is {NO_FORMAL_CHARGE}, which is reserved for 'absent'"),
109 )),
110 Some(charge) => Ok(charge),
111 })
112 .collect::<Result<_, _>>()?;
113 let ins_code = residues
114 .ins_code
115 .iter()
116 .enumerate()
117 .map(|(i, &c)| encode_char("residue.ins_code", i, c))
118 .collect::<Result<_, _>>()?;
119 let residue_group = residues
120 .group
121 .iter()
122 .map(|group| match group {
123 ResidueGroup::Polymer => RESIDUE_GROUP_POLYMER,
124 ResidueGroup::NonPolymer => RESIDUE_GROUP_NON_POLYMER,
125 })
126 .collect();
127
128 Ok(Self {
129 hierarchy: Arc::clone(hierarchy),
130 n_atoms: to_u32("atom count", atoms.len())?,
132 n_residues: to_u32("residue count", residues.len())?,
133 n_chains: to_u32("chain count", chains.len())?,
134 n_bonds: to_u32("bond count", hierarchy.bonds.len())?,
135 atom_name: dictionary(&atoms.atom_name)?,
136 auth_atom_name: dictionary(&atoms.auth_atom_name)?,
137 element: atoms
138 .element
139 .iter()
140 .map(|e| e.atomic_number() as u8)
141 .collect(),
142 alt_loc,
143 formal_charge,
144 comp_id: dictionary(&residues.comp_id)?,
145 auth_comp_id: dictionary(&residues.auth_comp_id)?,
146 ins_code,
147 residue_group,
148 label_asym_id: dictionary(&chains.label_asym_id)?,
149 auth_asym_id: dictionary(&chains.auth_asym_id)?,
150 entity_id: dictionary(&chains.entity_id)?,
151 })
152 }
153
154 pub fn from_model(model: &Model) -> Result<Self, BoundaryError> {
156 Self::encode(&model.hierarchy)
157 }
158
159 pub fn hierarchy(&self) -> &Arc<AtomicHierarchy> {
161 &self.hierarchy
162 }
163
164 pub fn is_current_for(&self, model: &Model) -> bool {
166 Arc::ptr_eq(&self.hierarchy, &model.hierarchy)
167 }
168
169 pub fn frame<'m>(&self, model: &'m Model) -> Result<FrameColumns<'m>, BoundaryError> {
173 if !self.is_current_for(model) {
174 return Err(BoundaryError::new(
175 BoundaryErrorKind::StaleTopology,
176 "model does not share the hierarchy this topology snapshot was built from",
177 ));
178 }
179 FrameColumns::new(model)
180 }
181
182 pub fn n_atoms(&self) -> u32 {
184 self.n_atoms
185 }
186
187 pub fn n_residues(&self) -> u32 {
189 self.n_residues
190 }
191
192 pub fn n_chains(&self) -> u32 {
194 self.n_chains
195 }
196
197 pub fn n_bonds(&self) -> u32 {
199 self.n_bonds
200 }
201
202 pub fn atom_name(&self) -> &StringDictionary {
206 &self.atom_name
207 }
208
209 pub fn auth_atom_name(&self) -> &StringDictionary {
211 &self.auth_atom_name
212 }
213
214 pub fn element(&self) -> &[u8] {
216 &self.element
217 }
218
219 pub fn alt_loc(&self) -> &[u32] {
221 &self.alt_loc
222 }
223
224 pub fn formal_charge(&self) -> &[i8] {
226 &self.formal_charge
227 }
228
229 pub fn comp_id(&self) -> &StringDictionary {
233 &self.comp_id
234 }
235
236 pub fn auth_comp_id(&self) -> &StringDictionary {
238 &self.auth_comp_id
239 }
240
241 pub fn label_seq_id(&self) -> &[i32] {
243 &self.hierarchy.residues.label_seq_id
244 }
245
246 pub fn auth_seq_id(&self) -> &[i32] {
248 &self.hierarchy.residues.auth_seq_id
249 }
250
251 pub fn ins_code(&self) -> &[u32] {
253 &self.ins_code
254 }
255
256 pub fn residue_group(&self) -> &[u8] {
258 &self.residue_group
259 }
260
261 pub fn label_asym_id(&self) -> &StringDictionary {
265 &self.label_asym_id
266 }
267
268 pub fn auth_asym_id(&self) -> &StringDictionary {
270 &self.auth_asym_id
271 }
272
273 pub fn entity_id(&self) -> &StringDictionary {
275 &self.entity_id
276 }
277
278 pub fn residue_atom_offsets(&self) -> &[u32] {
282 self.hierarchy.atom_to_residue.offsets()
283 }
284
285 pub fn chain_residue_offsets(&self) -> &[u32] {
287 self.hierarchy.residue_to_chain.offsets()
288 }
289
290 pub fn bond_atom_a(&self) -> &[u32] {
292 &self.hierarchy.bonds.atom_a
293 }
294
295 pub fn bond_atom_b(&self) -> &[u32] {
297 &self.hierarchy.bonds.atom_b
298 }
299
300 pub fn bond_order(&self) -> &[u8] {
302 &self.hierarchy.bonds.order
303 }
304
305 pub fn atom_bond_offsets(&self) -> &[u32] {
307 &self.hierarchy.bonds.atom_bond_starts
308 }
309
310 pub fn columns(&self) -> Vec<Column<'_>> {
315 use ColumnData::{I8, I32, U8, U32};
316 [
317 ("atom.name", U32(self.atom_name.codes())),
318 ("atom.auth_name", U32(self.auth_atom_name.codes())),
319 ("atom.element", U8(&self.element)),
320 ("atom.alt_loc", U32(&self.alt_loc)),
321 ("atom.formal_charge", I8(&self.formal_charge)),
322 ("residue.comp_id", U32(self.comp_id.codes())),
323 ("residue.auth_comp_id", U32(self.auth_comp_id.codes())),
324 ("residue.label_seq_id", I32(self.label_seq_id())),
325 ("residue.auth_seq_id", I32(self.auth_seq_id())),
326 ("residue.ins_code", U32(&self.ins_code)),
327 ("residue.group", U8(&self.residue_group)),
328 ("residue.atom_offsets", U32(self.residue_atom_offsets())),
329 ("chain.label_asym_id", U32(self.label_asym_id.codes())),
330 ("chain.auth_asym_id", U32(self.auth_asym_id.codes())),
331 ("chain.entity_id", U32(self.entity_id.codes())),
332 ("chain.residue_offsets", U32(self.chain_residue_offsets())),
333 ("bond.atom_a", U32(self.bond_atom_a())),
334 ("bond.atom_b", U32(self.bond_atom_b())),
335 ("bond.order", U8(self.bond_order())),
336 ("atom.bond_offsets", U32(self.atom_bond_offsets())),
337 ]
338 .into_iter()
339 .map(|(name, data)| Column { name, data })
340 .collect()
341 }
342
343 pub fn bond_line_indices(&self) -> GpuBuffer<'static> {
346 let bonds = &self.hierarchy.bonds;
347 let mut bytes = Vec::with_capacity(bonds.len() * 8);
348 for (&a, &b) in bonds.atom_a.iter().zip(&bonds.atom_b) {
349 bytes.extend_from_slice(&a.to_le_bytes());
350 bytes.extend_from_slice(&b.to_le_bytes());
351 }
352 GpuBuffer {
353 label: "bond.line_indices",
354 layout: BufferLayout::new(super::ScalarType::U32, 2, 8, self.n_bonds),
355 bytes: Cow::Owned(bytes),
356 }
357 }
358
359 pub fn residue_of(&self, atom: AtomHandle) -> Result<ResidueHandle, BoundaryError> {
363 if atom.0 >= self.n_atoms {
364 return Err(BoundaryError::out_of_range("atom", atom.0, self.n_atoms));
365 }
366 Ok(ResidueHandle(
367 self.hierarchy.residue_of_atom(atom.index()) as u32
368 ))
369 }
370
371 pub fn chain_of(&self, residue: ResidueHandle) -> Result<ChainHandle, BoundaryError> {
373 if residue.0 >= self.n_residues {
374 return Err(BoundaryError::out_of_range(
375 "residue",
376 residue.0,
377 self.n_residues,
378 ));
379 }
380 Ok(ChainHandle(
381 self.hierarchy.chain_of_residue(residue.index()) as u32,
382 ))
383 }
384
385 pub fn atoms_of(&self, residue: ResidueHandle) -> Result<Range<u32>, BoundaryError> {
387 let offsets = self.residue_atom_offsets();
388 if residue.0 >= self.n_residues {
389 return Err(BoundaryError::out_of_range(
390 "residue",
391 residue.0,
392 self.n_residues,
393 ));
394 }
395 Ok(offsets[residue.index()]..offsets[residue.index() + 1])
396 }
397
398 pub fn residues_of(&self, chain: ChainHandle) -> Result<Range<u32>, BoundaryError> {
400 let offsets = self.chain_residue_offsets();
401 if chain.0 >= self.n_chains {
402 return Err(BoundaryError::out_of_range("chain", chain.0, self.n_chains));
403 }
404 Ok(offsets[chain.index()]..offsets[chain.index() + 1])
405 }
406}