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