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 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;