1use 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 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 let kind = match bond.kind.get() {
243 Some(symbol) => remap_optional(source, Some(symbol), &mut self.data.dictionary)?,
244 None => OptionalSymbol::NONE,
245 };
246 self.bonds.push(BondRecord {
247 atom_a: AtomIndex::new(checked_add(bond.atom_a.get(), self.atom_offset)?),
248 atom_b: AtomIndex::new(checked_add(bond.atom_b.get(), self.atom_offset)?),
249 order: bond.order,
250 provenance: bond.provenance,
251 kind,
252 });
253 }
254 Ok(())
255 }
256
257 fn append_anisotropy(&mut self, source: &Structure) -> Result<(), Diagnostic> {
258 for record in source.data().anisotropy.iter() {
259 self.anisotropy.push(AnisotropicDisplacement {
260 atom: AtomIndex::new(checked_add(record.atom.get(), self.atom_offset)?),
261 ..record
262 });
263 }
264 Ok(())
265 }
266
267 fn advance_offsets(&mut self, source: &Structure) -> Result<(), Diagnostic> {
268 self.atom_offset = checked_add(self.atom_offset, source.atom_count())?;
269 self.residue_offset = checked_add(self.residue_offset, u32_of(source.residue_count())?)?;
270 self.entity_offset = checked_add(self.entity_offset, u32_of(source.entity_count())?)?;
271 Ok(())
272 }
273
274 fn finish(mut self) -> Result<Structure, Vec<Diagnostic>> {
275 let annotations = merge_annotations(self.sources, &mut self.data.dictionary)?;
276 let (chunks, first) = self.chunks.finish();
277 self.data.entry = EntryMetadata::default();
278 self.data.chunks = chunks.into();
279 self.data.bonds = self.bonds.finish_with_availability(
280 self.sources
281 .iter()
282 .all(|source| source.data().bonds.is_available()),
283 );
284 self.data.anisotropy = self.anisotropy.finish_with_availability(
285 self.sources
286 .iter()
287 .all(|source| source.data().anisotropy.is_available()),
288 );
289 self.data.annotations = annotations;
290 self.data.coords = merged_coordinates(self.sources, self.frame_count, first)?;
291 self.data.cell = common_cell(self.sources);
292 self.data.generation = CoordinateGeneration::INITIAL;
293 add_models(
294 &mut self.data.topology,
295 self.sources,
296 self.frame_count,
297 self.chain_count,
298 )?;
299 let findings = validate(&self.data);
300 if findings.is_empty() {
301 Ok(Structure::new(self.data))
302 } else {
303 Err(findings)
304 }
305 }
306}
307
308fn validate_inputs(structures: &[Structure]) -> Result<(), Vec<Diagnostic>> {
309 let mut findings: Vec<_> = structures
310 .iter()
311 .enumerate()
312 .flat_map(|(source, structure)| {
313 validate(structure.data())
314 .into_iter()
315 .map(move |finding| finding.with_context("merge source", source.to_string()))
316 })
317 .collect();
318 findings.extend(
319 structures
320 .iter()
321 .enumerate()
322 .filter(|(_, structure)| !structure.extensions().is_empty())
323 .map(|(source, structure)| {
324 Diagnostic::new(Code::E3014)
325 .with_context("merge source", source.to_string())
326 .with_context(
327 "extensions",
328 structure.extensions().keys().collect::<Vec<_>>().join(","),
329 )
330 }),
331 );
332 if findings.is_empty() {
333 Ok(())
334 } else {
335 Err(findings)
336 }
337}
338
339fn compatible_frame_count(structures: &[Structure]) -> Result<usize, Vec<Diagnostic>> {
340 let expected = structures[0].model_count();
341 let compatible = structures.iter().all(|structure| {
342 !matches!(structure.data().coords, CoordinateStore::Ragged { .. })
343 && structure.model_count() == expected
344 });
345 if compatible {
346 Ok(expected)
347 } else {
348 Err(single(Diagnostic::new(Code::E3012)))
349 }
350}
351
352fn add_models(
353 topology: &mut Topology,
354 sources: &[Structure],
355 count: usize,
356 chains: u32,
357) -> Result<(), Vec<Diagnostic>> {
358 for model in 0..count {
359 let index = ModelIndex::new(u32_of(model).map_err(single)?);
360 let stored = sources[0].data().topology.models.model_num(index);
361 let Some(number) = stored else {
362 return Err(single(invariant()));
363 };
364 topology
365 .models
366 .push(number, 0..chains)
367 .map_err(|_| single(invariant()))?;
368 }
369 Ok(())
370}
371
372fn merged_coordinates(
373 sources: &[Structure],
374 frame_count: usize,
375 first: CoordinateBlock,
376) -> Result<CoordinateStore, Vec<Diagnostic>> {
377 if frame_count == 1 {
378 return Ok(CoordinateStore::Single(first));
379 }
380 let mut frames = Vec::with_capacity(frame_count);
381 frames.push(first);
382 for frame in 1..frame_count {
383 let mut merged = CoordinateBlock::new();
384 for source in sources {
385 let index = ModelIndex::new(u32_of(frame).map_err(single)?);
386 if let Some(positions) = source.model_positions(index) {
387 for position in positions {
388 merged.push(*position);
389 }
390 }
391 }
392 frames.push(merged);
393 }
394 Ok(CoordinateStore::Dense { frames })
395}
396
397fn u32_of(value: usize) -> Result<u32, Diagnostic> {
398 u32::try_from(value).map_err(|_| invariant())
399}
400
401fn common_cell(sources: &[Structure]) -> Option<UnitCell> {
402 let first = sources.first()?.data().cell?;
403 sources
404 .iter()
405 .all(|source| source.data().cell == Some(first))
406 .then_some(first)
407}
408
409fn entity_id(source: &Structure, entity: EntityIndex) -> Result<SymbolId, Diagnostic> {
410 source
411 .data()
412 .topology
413 .entities
414 .id(entity)
415 .ok_or_else(invariant)
416}
417
418fn remap_optional(
419 source: &Structure,
420 symbol: Option<SymbolId>,
421 dictionary: &mut Interner,
422) -> Result<OptionalSymbol, Diagnostic> {
423 symbol
424 .map(|symbol| remap_symbol(source, symbol, dictionary))
425 .transpose()
426 .map(OptionalSymbol::from)
427}
428
429pub(super) fn remap_symbol(
430 source: &Structure,
431 symbol: SymbolId,
432 dictionary: &mut Interner,
433) -> Result<SymbolId, Diagnostic> {
434 let text = source.resolve(symbol).ok_or_else(invariant)?;
435 dictionary.intern(text).map_err(|_| {
436 Diagnostic::new(Code::E1901)
437 .with_context("limit", "dictionary entries")
438 .with_context("identifier", text)
439 })
440}
441
442fn checked_add(left: u32, right: u32) -> Result<u32, Diagnostic> {
443 left.checked_add(right)
444 .ok_or_else(|| Diagnostic::new(Code::E1901).with_context("limit", "u32 table rows"))
445}
446
447fn invariant() -> Diagnostic {
448 Diagnostic::new(Code::E9001)
449}
450
451pub(super) fn single(finding: Diagnostic) -> Vec<Diagnostic> {
452 vec![finding]
453}
454
455#[cfg(test)]
456#[path = "merge_tests.rs"]
457mod tests;