Skip to main content

molframe_pdb/header/
remark.rs

1//! Typed views over the free-text `REMARK` records that carry data.
2//!
3//! `REMARK 2` holds the resolution, `REMARK 350` the biological assemblies and
4//! `REMARK 465` the residues missing from the model.
5
6use super::PdbHeaders;
7use super::typed::{ResidueId, residue_id};
8use crate::fixed;
9
10/// A residue the model leaves out, from `REMARK 465`.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct MissingResidue {
13    /// Model number for a multi-model entry.
14    pub model: Option<u32>,
15    /// The missing residue.
16    pub residue: ResidueId,
17}
18
19/// One rigid operation of a biological assembly, from `BIOMT1..3`.
20#[derive(Clone, Debug, PartialEq)]
21pub struct Biomt {
22    /// Operator serial number.
23    pub serial: u32,
24    /// Row-major rotation.
25    pub rotation: [[f64; 3]; 3],
26    /// Translation in ångström.
27    pub translation: [f64; 3],
28}
29
30/// Operations applied to a set of chains.
31#[derive(Clone, Debug, PartialEq)]
32pub struct BiomtGroup {
33    /// Chains the operations apply to.
34    pub chains: Vec<Box<str>>,
35    /// Operations in serial order.
36    pub operations: Vec<Biomt>,
37}
38
39/// One biological assembly, from `REMARK 350`.
40#[derive(Clone, Debug, PartialEq)]
41pub struct Biomolecule {
42    /// Assembly number.
43    pub id: u32,
44    /// One group per `APPLY THE FOLLOWING TO CHAINS` block.
45    pub groups: Vec<BiomtGroup>,
46}
47
48impl PdbHeaders {
49    /// The text of every `REMARK <number>` line, after the number.
50    fn remark_text<'a>(&'a self, number: &'a str) -> impl Iterator<Item = &'a str> {
51        self.named("REMARK").filter_map(move |record| {
52            let line = record.line();
53            (fixed::text(line, 8, 10) == number).then(|| fixed::raw(line, 11, line.len()))
54        })
55    }
56
57    /// The resolution in ångström that `REMARK 2` deposits, when it gives one.
58    #[must_use]
59    pub fn resolution(&self) -> Option<f64> {
60        self.remark_text("2").find_map(|text| {
61            let rest = text.trim().strip_prefix("RESOLUTION.")?;
62            rest.split_whitespace().next()?.parse().ok()
63        })
64    }
65
66    /// The residues `REMARK 465` lists as missing from the model.
67    #[must_use]
68    pub fn missing_residues(&self) -> Vec<MissingResidue> {
69        let mut in_table = false;
70        let mut missing = Vec::new();
71        for record in self.named("REMARK") {
72            let line = record.line();
73            if fixed::text(line, 8, 10) != "465" {
74                continue;
75            }
76            if line.contains("RES C SSSEQI") {
77                in_table = true;
78                continue;
79            }
80            if !in_table {
81                continue;
82            }
83            // Single-model entries leave the model column empty.
84            let model = fixed::integer(line, 12, 14).and_then(|n| u32::try_from(n).ok());
85            if let Some(residue) = residue_id(line, (16, 18), 20, (22, 26), 27) {
86                missing.push(MissingResidue { model, residue });
87            }
88        }
89        missing
90    }
91
92    /// The biological assemblies `REMARK 350` defines.
93    #[must_use]
94    pub fn biomolecules(&self) -> Vec<Biomolecule> {
95        let mut assemblies: Vec<Biomolecule> = Vec::new();
96        let mut continuing = false;
97        for text in self.remark_text("350") {
98            let text = text.trim();
99            if let Some(id) = text.strip_prefix("BIOMOLECULE:") {
100                continuing = false;
101                if let Ok(id) = id.trim().parse() {
102                    assemblies.push(Biomolecule {
103                        id,
104                        groups: Vec::new(),
105                    });
106                }
107            } else if let Some(chains) = chain_list(text) {
108                let Some(assembly) = assemblies.last_mut() else {
109                    continue;
110                };
111                // `AND CHAINS:` extends the group a long list began.
112                let append = continuing && text.starts_with("AND");
113                match assembly.groups.last_mut() {
114                    Some(group) if append => group.chains.extend(chains),
115                    _ => assembly.groups.push(BiomtGroup {
116                        chains,
117                        operations: Vec::new(),
118                    }),
119                }
120                continuing = true;
121            } else if let Some(row) = biomt_row(text) {
122                continuing = false;
123                if let Some(group) = assemblies.last_mut().and_then(|a| a.groups.last_mut()) {
124                    push_row(group, row);
125                }
126            }
127        }
128        assemblies
129    }
130}
131
132/// The chain names a `APPLY THE FOLLOWING TO CHAINS:` or `AND CHAINS:` line holds.
133fn chain_list(text: &str) -> Option<Vec<Box<str>>> {
134    let (_, list) = text.split_once("CHAINS:")?;
135    if !(text.starts_with("APPLY THE FOLLOWING TO") || text.starts_with("AND")) {
136        return None;
137    }
138    Some(
139        list.split(',')
140            .map(str::trim)
141            .filter(|chain| !chain.is_empty())
142            .map(Box::from)
143            .collect(),
144    )
145}
146
147/// One `BIOMTk` row: its index within the operator, serial, and four numbers.
148fn biomt_row(text: &str) -> Option<(usize, u32, [f64; 4])> {
149    let mut fields = text.split_whitespace();
150    let tag = fields.next()?.strip_prefix("BIOMT")?;
151    let row: usize = tag.parse().ok()?;
152    let serial = fields.next()?.parse().ok()?;
153    let mut numbers = [0.0; 4];
154    for slot in &mut numbers {
155        *slot = fields.next()?.parse().ok()?;
156    }
157    (1..=3).contains(&row).then_some((row - 1, serial, numbers))
158}
159
160fn push_row(group: &mut BiomtGroup, (row, serial, numbers): (usize, u32, [f64; 4])) {
161    if group
162        .operations
163        .last()
164        .is_none_or(|last| last.serial != serial)
165    {
166        group.operations.push(Biomt {
167            serial,
168            rotation: [[0.0; 3]; 3],
169            translation: [0.0; 3],
170        });
171    }
172    if let Some(operation) = group.operations.last_mut() {
173        operation.rotation[row] = [numbers[0], numbers[1], numbers[2]];
174        operation.translation[row] = numbers[3];
175    }
176}
177
178#[cfg(test)]
179#[path = "remark_tests.rs"]
180mod tests;