Skip to main content

rsomics_dssp/
lib.rs

1pub mod dssp;
2pub mod geom;
3pub mod pdb;
4
5use std::io::{BufReader, Read, Write};
6use std::path::Path;
7
8use rsomics_common::{Result, RsomicsError};
9
10pub use dssp::assign;
11pub use pdb::Residue;
12
13/// A residue's secondary-structure assignment alongside its identity.
14#[derive(Clone, Debug)]
15pub struct Assignment {
16    pub chain: char,
17    pub seq: i32,
18    pub icode: char,
19    pub aa: char,
20    pub ss: char,
21}
22
23/// Parse a PDB (file path or `-` for stdin), assign secondary structure, and
24/// return one [`Assignment`] per protein residue.
25#[allow(clippy::missing_errors_doc)]
26pub fn assign_from_path(input: &Path) -> Result<Vec<Assignment>> {
27    let residues = if input.as_os_str() == "-" {
28        pdb::parse_pdb(BufReader::new(std::io::stdin().lock()))?
29    } else {
30        let f = std::fs::File::open(input)
31            .map_err(|e| RsomicsError::InvalidInput(format!("{}: {e}", input.display())))?;
32        pdb::parse_pdb(BufReader::new(f))?
33    };
34    if residues.is_empty() {
35        return Err(RsomicsError::InvalidInput(
36            "no protein ATOM records found".into(),
37        ));
38    }
39    let ss = assign(&residues);
40    Ok(residues
41        .iter()
42        .zip(ss)
43        .map(|(r, ss)| Assignment {
44            chain: r.chain,
45            seq: r.seq,
46            icode: r.icode,
47            aa: r.aa,
48            ss,
49        })
50        .collect())
51}
52
53/// Assign from already-parsed residues (used by callers that hold their own
54/// reader).
55#[allow(clippy::missing_errors_doc)]
56pub fn assign_from_reader<R: Read>(reader: R) -> Result<Vec<Assignment>> {
57    let residues = pdb::parse_pdb(BufReader::new(reader))?;
58    let ss = assign(&residues);
59    Ok(residues
60        .iter()
61        .zip(ss)
62        .map(|(r, ss)| Assignment {
63            chain: r.chain,
64            seq: r.seq,
65            icode: r.icode,
66            aa: r.aa,
67            ss,
68        })
69        .collect())
70}
71
72/// Output styles for the per-residue assignment table.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum Format {
75    /// One residue per line: chain, seq, icode, aa, ss.
76    Table,
77    /// A single per-residue SS string per chain (`> chain` header + line).
78    Fasta,
79    /// The bare concatenated SS string for the whole structure.
80    String,
81}
82
83#[allow(clippy::missing_errors_doc)]
84pub fn write_output(assignments: &[Assignment], format: Format, out: &mut dyn Write) -> Result<()> {
85    match format {
86        Format::Table => {
87            for a in assignments {
88                let icode = if a.icode == ' ' { ' ' } else { a.icode };
89                writeln!(
90                    out,
91                    "{}\t{}{}\t{}\t{}",
92                    a.chain,
93                    a.seq,
94                    if icode == ' ' {
95                        String::new()
96                    } else {
97                        icode.to_string()
98                    },
99                    a.aa,
100                    a.ss
101                )
102                .map_err(RsomicsError::Io)?;
103            }
104        }
105        Format::Fasta => {
106            let mut cur: Option<char> = None;
107            let mut buf = String::new();
108            for a in assignments {
109                if cur != Some(a.chain) {
110                    if cur.is_some() {
111                        writeln!(out, "{buf}").map_err(RsomicsError::Io)?;
112                    }
113                    writeln!(out, "> chain {}", a.chain).map_err(RsomicsError::Io)?;
114                    buf.clear();
115                    cur = Some(a.chain);
116                }
117                buf.push(a.ss);
118            }
119            if cur.is_some() {
120                writeln!(out, "{buf}").map_err(RsomicsError::Io)?;
121            }
122        }
123        Format::String => {
124            let s: String = assignments.iter().map(|a| a.ss).collect();
125            writeln!(out, "{s}").map_err(RsomicsError::Io)?;
126        }
127    }
128    Ok(())
129}