Skip to main content

molframe_core/structure/
merge.rs

1//! Deterministic concatenation of compatible immutable structures.
2
3use super::merge_annotations::merge_annotations;
4use super::{CoordinateStore, EntryMetadata, Structure, StructureData, UnitCell, validate};
5use crate::bond::{BondRecord, BondTableBuilder};
6use crate::chunk::{AtomRecord, ChunkBuilder};
7use crate::coords::{CoordinateBlock, CoordinateGeneration};
8use crate::diagnostic::{Code, Diagnostic};
9use crate::index::{AtomIndex, EntityIndex, ModelIndex, ResidueIndex};
10use crate::optional::OptionalSymbol;
11use crate::symbol::{AltId, Interner, SymbolId};
12use crate::topology::{ChainRecord, ResidueRecord, Topology};
13
14impl Structure {
15    /// Concatenates compatible structures into one immutable system.
16    ///
17    /// Atom, residue, chain and entity order follows input order. Dense inputs
18    /// must have equal frame counts; their corresponding frames are
19    /// concatenated. Dictionary identifiers, bonds and symbolic annotations are
20    /// remapped rather than compared by structure-local integer value.
21    ///
22    /// # Errors
23    ///
24    /// Returns diagnostics for invalid inputs, ragged or unequal model axes,
25    /// incompatible same-named annotation types, or representational overflow.
26    pub fn merge(structures: &[Self]) -> Result<Self, Vec<Diagnostic>> {
27        if structures.is_empty() {
28            return Ok(Self::new(StructureData::empty()));
29        }
30        if structures.len() == 1 {
31            return Ok(structures[0].clone());
32        }
33        validate_inputs(structures)?;
34        let frame_count = compatible_frame_count(structures)?;
35        let mut merger = Merger::new(structures, frame_count);
36        merger.append_all()?;
37        merger.finish()
38    }
39}
40
41struct Merger<'a> {
42    sources: &'a [Structure],
43    frame_count: usize,
44    data: StructureData,
45    chunks: ChunkBuilder,
46    bonds: BondTableBuilder,
47    atom_offset: u32,
48    residue_offset: u32,
49    entity_offset: u32,
50    chain_count: u32,
51}
52
53impl<'a> Merger<'a> {
54    fn new(sources: &'a [Structure], frame_count: usize) -> Self {
55        Self {
56            sources,
57            frame_count,
58            data: StructureData::empty(),
59            chunks: ChunkBuilder::new(),
60            bonds: BondTableBuilder::new(),
61            atom_offset: 0,
62            residue_offset: 0,
63            entity_offset: 0,
64            chain_count: 0,
65        }
66    }
67
68    fn append_all(&mut self) -> Result<(), Vec<Diagnostic>> {
69        self.chunks.start_model(0);
70        for source in self.sources {
71            self.append_entities(source).map_err(single)?;
72            self.append_residues(source).map_err(single)?;
73            self.append_chains(source).map_err(single)?;
74            self.append_atoms(source).map_err(single)?;
75            self.append_bonds(source).map_err(single)?;
76            self.advance_offsets(source).map_err(single)?;
77        }
78        Ok(())
79    }
80
81    fn append_entities(&mut self, source: &Structure) -> Result<(), Diagnostic> {
82        for entity in source.data().topology.entities.iter() {
83            let id = remap_symbol(
84                source,
85                entity_id(source, entity)?,
86                &mut self.data.dictionary,
87            )?;
88            let description = remap_optional(
89                source,
90                source.data().topology.entities.description(entity),
91                &mut self.data.dictionary,
92            )?;
93            let sequence = source
94                .data()
95                .topology
96                .entities
97                .canonical_sequence(entity)
98                .iter()
99                .map(|symbol| remap_symbol(source, *symbol, &mut self.data.dictionary))
100                .collect::<Result<Vec<_>, _>>()?;
101            let kind = source
102                .data()
103                .topology
104                .entities
105                .kind(entity)
106                .ok_or_else(invariant)?;
107            self.data
108                .topology
109                .entities
110                .push(id, kind, description, &sequence)
111                .map_err(|_| invariant())?;
112        }
113        Ok(())
114    }
115
116    fn append_residues(&mut self, source: &Structure) -> Result<(), Diagnostic> {
117        for residue in 0..u32_of(source.residue_count())? {
118            let residue = ResidueIndex::new(residue);
119            let table = &source.data().topology.residues;
120            let atoms = table.atoms(residue).ok_or_else(invariant)?;
121            let record = ResidueRecord {
122                label_comp_id: remap_symbol(
123                    source,
124                    table.label_comp_id(residue).ok_or_else(invariant)?,
125                    &mut self.data.dictionary,
126                )?,
127                auth_comp_id: remap_optional(
128                    source,
129                    table.auth_comp_id(residue),
130                    &mut self.data.dictionary,
131                )?,
132                label_seq_id: table.label_seq_id(residue).into(),
133                auth_seq_id: table.auth_seq_id(residue).into(),
134                ins_code: remap_optional(
135                    source,
136                    table.ins_code(residue),
137                    &mut self.data.dictionary,
138                )?,
139                het: table.is_het(residue),
140            };
141            self.data
142                .topology
143                .residues
144                .push(
145                    record,
146                    checked_add(atoms.start, self.atom_offset)?
147                        ..checked_add(atoms.end, self.atom_offset)?,
148                )
149                .map_err(|_| invariant())?;
150        }
151        Ok(())
152    }
153
154    fn append_chains(&mut self, source: &Structure) -> Result<(), Diagnostic> {
155        for chain in source.data().topology.chains.iter() {
156            let table = &source.data().topology.chains;
157            let residues = table.residues(chain).ok_or_else(invariant)?;
158            let entity = table.entity(chain).ok_or_else(invariant)?;
159            let record = ChainRecord {
160                label_asym_id: remap_symbol(
161                    source,
162                    table.label_asym_id(chain).ok_or_else(invariant)?,
163                    &mut self.data.dictionary,
164                )?,
165                auth_asym_id: remap_optional(
166                    source,
167                    table.auth_asym_id(chain),
168                    &mut self.data.dictionary,
169                )?,
170                entity: EntityIndex::new(checked_add(entity.get(), self.entity_offset)?),
171                polymer_kind: table.polymer_kind(chain).ok_or_else(invariant)?,
172            };
173            self.data
174                .topology
175                .chains
176                .push(
177                    record,
178                    checked_add(residues.start, self.residue_offset)?
179                        ..checked_add(residues.end, self.residue_offset)?,
180                )
181                .map_err(|_| invariant())?;
182            self.chain_count = checked_add(self.chain_count, 1)?;
183        }
184        Ok(())
185    }
186
187    fn append_atoms(&mut self, source: &Structure) -> Result<(), Diagnostic> {
188        let positions = source.positions();
189        for chunk in source.data().chunks.iter() {
190            for old in chunk.atoms() {
191                let local = old.checked_sub(chunk.atoms().start).ok_or_else(invariant)?;
192                let position = positions.get(old as usize).copied();
193                let record = chunk
194                    .record(local, &source.data().topology.residues, position)
195                    .ok_or_else(invariant)?;
196                let remapped = self.remap_atom(source, record)?;
197                self.chunks.push(remapped);
198            }
199        }
200        Ok(())
201    }
202
203    fn remap_atom(
204        &mut self,
205        source: &Structure,
206        record: AtomRecord,
207    ) -> Result<AtomRecord, Diagnostic> {
208        Ok(AtomRecord {
209            atom_name: remap_symbol(source, record.atom_name, &mut self.data.dictionary)?,
210            auth_atom_name: remap_optional(
211                source,
212                record.auth_atom_name.get(),
213                &mut self.data.dictionary,
214            )?,
215            alternate_component_id: remap_optional(
216                source,
217                record.alternate_component_id.get(),
218                &mut self.data.dictionary,
219            )?,
220            alt_id: match record.alt_id.symbol() {
221                Some(symbol) => {
222                    AltId::labelled(remap_symbol(source, symbol, &mut self.data.dictionary)?)
223                        .ok_or_else(invariant)?
224                }
225                None => AltId::BLANK,
226            },
227            residue: ResidueIndex::new(checked_add(record.residue.get(), self.residue_offset)?),
228            ..record
229        })
230    }
231
232    fn append_bonds(&mut self, source: &Structure) -> Result<(), Diagnostic> {
233        for bond in source.data().bonds.iter() {
234            self.bonds.push(BondRecord {
235                atom_a: AtomIndex::new(checked_add(bond.atom_a.get(), self.atom_offset)?),
236                atom_b: AtomIndex::new(checked_add(bond.atom_b.get(), self.atom_offset)?),
237                ..bond
238            });
239        }
240        Ok(())
241    }
242
243    fn advance_offsets(&mut self, source: &Structure) -> Result<(), Diagnostic> {
244        self.atom_offset = checked_add(self.atom_offset, source.atom_count())?;
245        self.residue_offset = checked_add(self.residue_offset, u32_of(source.residue_count())?)?;
246        self.entity_offset = checked_add(self.entity_offset, u32_of(source.entity_count())?)?;
247        Ok(())
248    }
249
250    fn finish(mut self) -> Result<Structure, Vec<Diagnostic>> {
251        let annotations = merge_annotations(self.sources, &mut self.data.dictionary)?;
252        let (chunks, first) = self.chunks.finish();
253        self.data.entry = EntryMetadata::default();
254        self.data.chunks = chunks.into();
255        self.data.bonds = self.bonds.finish_with_availability(
256            self.sources
257                .iter()
258                .all(|source| source.data().bonds.is_available()),
259        );
260        self.data.annotations = annotations;
261        self.data.coords = merged_coordinates(self.sources, self.frame_count, first)?;
262        self.data.cell = common_cell(self.sources);
263        self.data.generation = CoordinateGeneration::INITIAL;
264        add_models(
265            &mut self.data.topology,
266            self.sources,
267            self.frame_count,
268            self.chain_count,
269        )?;
270        let findings = validate(&self.data);
271        if findings.is_empty() {
272            Ok(Structure::new(self.data))
273        } else {
274            Err(findings)
275        }
276    }
277}
278
279fn validate_inputs(structures: &[Structure]) -> Result<(), Vec<Diagnostic>> {
280    let mut findings: Vec<_> = structures
281        .iter()
282        .enumerate()
283        .flat_map(|(source, structure)| {
284            validate(structure.data())
285                .into_iter()
286                .map(move |finding| finding.with_context("merge source", source.to_string()))
287        })
288        .collect();
289    findings.extend(
290        structures
291            .iter()
292            .enumerate()
293            .filter(|(_, structure)| !structure.extensions().is_empty())
294            .map(|(source, structure)| {
295                Diagnostic::new(Code::E3014)
296                    .with_context("merge source", source.to_string())
297                    .with_context(
298                        "extensions",
299                        structure.extensions().keys().collect::<Vec<_>>().join(","),
300                    )
301            }),
302    );
303    if findings.is_empty() {
304        Ok(())
305    } else {
306        Err(findings)
307    }
308}
309
310fn compatible_frame_count(structures: &[Structure]) -> Result<usize, Vec<Diagnostic>> {
311    let expected = structures[0].model_count();
312    let compatible = structures.iter().all(|structure| {
313        !matches!(structure.data().coords, CoordinateStore::Ragged { .. })
314            && structure.model_count() == expected
315    });
316    if compatible {
317        Ok(expected)
318    } else {
319        Err(single(Diagnostic::new(Code::E3012)))
320    }
321}
322
323fn add_models(
324    topology: &mut Topology,
325    sources: &[Structure],
326    count: usize,
327    chains: u32,
328) -> Result<(), Vec<Diagnostic>> {
329    for model in 0..count {
330        let index = ModelIndex::new(u32_of(model).map_err(single)?);
331        let stored = sources[0].data().topology.models.model_num(index);
332        let Some(number) = stored else {
333            return Err(single(invariant()));
334        };
335        topology
336            .models
337            .push(number, 0..chains)
338            .map_err(|_| single(invariant()))?;
339    }
340    Ok(())
341}
342
343fn merged_coordinates(
344    sources: &[Structure],
345    frame_count: usize,
346    first: CoordinateBlock,
347) -> Result<CoordinateStore, Vec<Diagnostic>> {
348    if frame_count == 1 {
349        return Ok(CoordinateStore::Single(first));
350    }
351    let mut frames = Vec::with_capacity(frame_count);
352    frames.push(first);
353    for frame in 1..frame_count {
354        let mut merged = CoordinateBlock::new();
355        for source in sources {
356            let index = ModelIndex::new(u32_of(frame).map_err(single)?);
357            if let Some(positions) = source.model_positions(index) {
358                for position in positions {
359                    merged.push(*position);
360                }
361            }
362        }
363        frames.push(merged);
364    }
365    Ok(CoordinateStore::Dense { frames })
366}
367
368fn u32_of(value: usize) -> Result<u32, Diagnostic> {
369    u32::try_from(value).map_err(|_| invariant())
370}
371
372fn common_cell(sources: &[Structure]) -> Option<UnitCell> {
373    let first = sources.first()?.data().cell?;
374    sources
375        .iter()
376        .all(|source| source.data().cell == Some(first))
377        .then_some(first)
378}
379
380fn entity_id(source: &Structure, entity: EntityIndex) -> Result<SymbolId, Diagnostic> {
381    source
382        .data()
383        .topology
384        .entities
385        .id(entity)
386        .ok_or_else(invariant)
387}
388
389fn remap_optional(
390    source: &Structure,
391    symbol: Option<SymbolId>,
392    dictionary: &mut Interner,
393) -> Result<OptionalSymbol, Diagnostic> {
394    symbol
395        .map(|symbol| remap_symbol(source, symbol, dictionary))
396        .transpose()
397        .map(OptionalSymbol::from)
398}
399
400pub(super) fn remap_symbol(
401    source: &Structure,
402    symbol: SymbolId,
403    dictionary: &mut Interner,
404) -> Result<SymbolId, Diagnostic> {
405    let text = source.resolve(symbol).ok_or_else(invariant)?;
406    dictionary.intern(text).map_err(|_| {
407        Diagnostic::new(Code::E1901)
408            .with_context("limit", "dictionary entries")
409            .with_context("identifier", text)
410    })
411}
412
413fn checked_add(left: u32, right: u32) -> Result<u32, Diagnostic> {
414    left.checked_add(right)
415        .ok_or_else(|| Diagnostic::new(Code::E1901).with_context("limit", "u32 table rows"))
416}
417
418fn invariant() -> Diagnostic {
419    Diagnostic::new(Code::E9001)
420}
421
422pub(super) fn single(finding: Diagnostic) -> Vec<Diagnostic> {
423    vec![finding]
424}
425
426#[cfg(test)]
427#[path = "merge_tests.rs"]
428mod tests;