Skip to main content

molframe_core/structure/
chain_labels.rs

1//! Label identities that name one chain and one residue each.
2//!
3//! A format with no separate normalised label, such as PDB, can hold several
4//! chains under one identifier, a polymer and its waters after a `TER`. Formats
5//! that key chains by the normalised label would merge them on the way back in,
6//! so a writer asks for a view in which each chain has its own. Insertion
7//! codes raise the same problem for residues: `10` and `10A` share a number in
8//! a format whose normalised sequence position carries no code.
9
10use super::{CoordinateStore, Structure, StructureData};
11use crate::index::ChainIndex;
12use crate::symbol::SymbolId;
13use std::collections::{HashMap, HashSet};
14use std::sync::Arc;
15
16impl Structure {
17    /// This structure with repeated normalised chain labels and repeated
18    /// normalised residue numbers made distinct, or `None` when every chain and
19    /// residue already has its own.
20    ///
21    /// A repeated chain takes the label with the lowest unused number
22    /// appended. Within a chain that repeats a residue number, each later
23    /// residue steps the numbering on by one so the order is kept. The
24    /// depositor identifiers are untouched, so the original survives in its own
25    /// namespace.
26    #[must_use]
27    pub fn with_distinct_labels(&self) -> Option<Self> {
28        let mut data = self.0.as_ref().clone();
29        let mut changed = distinct_in(&mut data);
30        changed |= distinct_residues(&mut data);
31        if let CoordinateStore::Ragged { models } = &mut data.coords {
32            for model in models {
33                if let Some(distinct) = model.with_distinct_labels() {
34                    *model = distinct;
35                    changed = true;
36                }
37            }
38        }
39        changed.then(|| Self(Arc::new(data)))
40    }
41}
42
43fn distinct_in(data: &mut StructureData) -> bool {
44    let chains: Vec<(ChainIndex, SymbolId)> = data
45        .topology
46        .chains
47        .iter()
48        .filter_map(|chain| Some((chain, data.topology.chains.label_asym_id(chain)?)))
49        .collect();
50    let mut taken: HashSet<SymbolId> = chains.iter().map(|&(_, label)| label).collect();
51    let mut seen = HashSet::new();
52    let mut changed = false;
53    for (chain, label) in chains {
54        if seen.insert(label) {
55            continue;
56        }
57        let base = data
58            .dictionary
59            .resolve(label)
60            .map_or_else(String::new, str::to_owned);
61        let fresh = (2u32..100_000).find_map(|number| {
62            let symbol = data.dictionary.intern(&format!("{base}{number}")).ok()?;
63            taken.insert(symbol).then_some(symbol)
64        });
65        let Some(fresh) = fresh else { continue };
66        if data.topology.chains.relabel(chain, fresh).is_ok() {
67            seen.insert(fresh);
68            changed = true;
69        }
70    }
71    changed
72}
73
74fn distinct_residues(data: &mut StructureData) -> bool {
75    let mut renumbered = Vec::new();
76    for chain in data.chains() {
77        let numbers: Vec<_> = chain
78            .residues()
79            .filter_map(|residue| {
80                let code = residue.ins_code().map_or_else(String::new, str::to_owned);
81                Some((residue.index(), residue.label_seq_id()?, code))
82            })
83            .collect();
84        // Microheterogeneity repeats a number under one insertion code and is
85        // already unambiguous; only a differing code makes a number collide.
86        let mut first = HashMap::new();
87        let collides = numbers
88            .iter()
89            .any(|(_, number, code)| *first.entry(*number).or_insert(code) != code);
90        if !collides {
91            continue;
92        }
93        let mut shift = 0;
94        let mut previous: Option<(i32, &str)> = None;
95        for (residue, number, code) in &numbers {
96            if previous.is_some_and(|(n, c)| n == *number && c != code) {
97                shift += 1;
98            }
99            previous = Some((*number, code));
100            if shift > 0 {
101                renumbered.push((*residue, number.saturating_add(shift)));
102            }
103        }
104    }
105    let changed = !renumbered.is_empty();
106    for (residue, number) in renumbered {
107        let _ = data.topology.residues.set_label_seq_id(residue, number);
108    }
109    changed
110}
111
112#[cfg(test)]
113#[path = "chain_labels_tests.rs"]
114mod tests;