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