Skip to main content

molframe_pdb/header/
typed.rs

1//! Typed views over the preserved `SEQRES`, `SSBOND` and `LINK` records.
2//!
3//! The deposited lines stay the source of truth, so these views are computed on
4//! demand and writing a structure back out reproduces the header unchanged.
5
6use super::PdbHeaders;
7use crate::fixed;
8
9/// A residue named the way connectivity records name it.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ResidueId {
12    /// Component name, trimmed.
13    pub name: Box<str>,
14    /// Chain identifier; empty for a file that leaves the column blank.
15    pub chain: Box<str>,
16    /// Residue sequence number.
17    pub sequence: i32,
18    /// Insertion code, when one is given.
19    pub insertion: Option<char>,
20}
21
22/// The residue sequence one chain declares in its `SEQRES` records.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct SeqRes {
25    /// Chain identifier.
26    pub chain: Box<str>,
27    /// Residue count the first record declares.
28    pub declared: Option<u32>,
29    /// Component names in sequence order.
30    pub residues: Vec<Box<str>>,
31}
32
33/// A disulfide bond declared by `SSBOND`.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct SsBond {
36    /// First bonded residue.
37    pub first: ResidueId,
38    /// Second bonded residue.
39    pub second: ResidueId,
40}
41
42/// A covalent link declared by `LINK`.
43#[derive(Clone, Debug, PartialEq)]
44pub struct Link {
45    /// First atom name.
46    pub first_atom: Box<str>,
47    /// Residue of the first atom.
48    pub first: ResidueId,
49    /// Second atom name.
50    pub second_atom: Box<str>,
51    /// Residue of the second atom.
52    pub second: ResidueId,
53    /// Deposited link length in ångström, when given.
54    pub distance: Option<f64>,
55}
56
57/// Reads a residue from fixed columns: name, chain, sequence, insertion code.
58pub(super) fn residue_id(
59    line: &str,
60    name: (usize, usize),
61    chain: usize,
62    sequence: (usize, usize),
63    insertion: usize,
64) -> Option<ResidueId> {
65    let sequence = i32::try_from(fixed::integer(line, sequence.0, sequence.1)?).ok()?;
66    Some(ResidueId {
67        name: fixed::text(line, name.0, name.1).into(),
68        chain: fixed::text(line, chain, chain).into(),
69        sequence,
70        insertion: fixed::text(line, insertion, insertion).chars().next(),
71    })
72}
73
74impl PdbHeaders {
75    /// The `SEQRES` residue sequences, one per chain in first-appearance order.
76    #[must_use]
77    pub fn seqres(&self) -> Vec<SeqRes> {
78        let mut chains: Vec<SeqRes> = Vec::new();
79        for record in self.named("SEQRES") {
80            let line = record.line();
81            let chain = fixed::text(line, 12, 12);
82            if !chains.iter().any(|seen| &*seen.chain == chain) {
83                chains.push(SeqRes {
84                    chain: chain.into(),
85                    declared: fixed::integer(line, 14, 17).and_then(|n| u32::try_from(n).ok()),
86                    residues: Vec::new(),
87                });
88            }
89            let Some(entry) = chains.iter_mut().find(|seen| &*seen.chain == chain) else {
90                continue;
91            };
92            // Up to thirteen three-column names, four columns apart.
93            for slot in 0..13 {
94                let from = 20 + 4 * slot;
95                let name = fixed::text(line, from, from + 2);
96                if !name.is_empty() {
97                    entry.residues.push(name.into());
98                }
99            }
100        }
101        chains
102    }
103
104    /// The `SSBOND` disulfide bonds, in deposited order.
105    #[must_use]
106    pub fn ssbonds(&self) -> Vec<SsBond> {
107        self.named("SSBOND")
108            .filter_map(|record| {
109                let line = record.line();
110                Some(SsBond {
111                    first: residue_id(line, (12, 14), 16, (18, 21), 22)?,
112                    second: residue_id(line, (26, 28), 30, (32, 35), 36)?,
113                })
114            })
115            .collect()
116    }
117
118    /// The `LINK` records, in deposited order.
119    #[must_use]
120    pub fn links(&self) -> Vec<Link> {
121        self.named("LINK")
122            .filter_map(|record| {
123                let line = record.line();
124                Some(Link {
125                    first_atom: fixed::text(line, 13, 16).into(),
126                    first: residue_id(line, (18, 20), 22, (23, 26), 27)?,
127                    second_atom: fixed::text(line, 43, 46).into(),
128                    second: residue_id(line, (48, 50), 52, (53, 56), 57)?,
129                    distance: fixed::real(line, 74, 78),
130                })
131            })
132            .collect()
133    }
134}
135
136#[cfg(test)]
137#[path = "typed_tests.rs"]
138mod tests;