molframe_pdb/header/
remark.rs1use super::PdbHeaders;
7use super::typed::{ResidueId, residue_id};
8use crate::fixed;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct MissingResidue {
13 pub model: Option<u32>,
15 pub residue: ResidueId,
17}
18
19#[derive(Clone, Debug, PartialEq)]
21pub struct Biomt {
22 pub serial: u32,
24 pub rotation: [[f64; 3]; 3],
26 pub translation: [f64; 3],
28}
29
30#[derive(Clone, Debug, PartialEq)]
32pub struct BiomtGroup {
33 pub chains: Vec<Box<str>>,
35 pub operations: Vec<Biomt>,
37}
38
39#[derive(Clone, Debug, PartialEq)]
41pub struct Biomolecule {
42 pub id: u32,
44 pub groups: Vec<BiomtGroup>,
46}
47
48impl PdbHeaders {
49 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 #[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 #[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 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 #[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 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
132fn 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
147fn 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;