1use super::{Structure, UnitCell};
4use crate::index::{AtomIndex, ChainIndex, ModelIndex, ResidueIndex};
5
6#[derive(Clone, Debug, PartialEq)]
8pub struct ValueDifference<T> {
9 pub left: T,
11 pub right: T,
13}
14
15pub type CountDifference = ValueDifference<usize>;
17
18#[derive(Clone, Debug, Default, PartialEq)]
20pub struct MetadataDifference {
21 pub id: Option<ValueDifference<Option<Box<str>>>>,
23 pub title: Option<ValueDifference<Option<Box<str>>>>,
25 pub method: Option<ValueDifference<Option<Box<str>>>>,
27 pub resolution: Option<ValueDifference<Option<f32>>>,
29 pub cell: Option<ValueDifference<Option<UnitCell>>>,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct StructureDifferenceOptions {
36 pub coordinate_tolerance: f32,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum DifferenceError {
43 InvalidCoordinateTolerance,
45}
46
47impl std::fmt::Display for DifferenceError {
48 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::InvalidCoordinateTolerance => {
51 formatter.write_str("coordinate tolerance must be finite and non-negative")
52 }
53 }
54 }
55}
56
57impl std::error::Error for DifferenceError {}
58
59#[derive(Clone, Debug, PartialEq)]
61pub struct StructureDifference {
62 pub metadata: MetadataDifference,
64 pub models: Option<CountDifference>,
66 pub entities: Option<CountDifference>,
68 pub chains: Option<CountDifference>,
70 pub residues: Option<CountDifference>,
72 pub atoms: Option<CountDifference>,
74 pub bonds: Option<CountDifference>,
76 pub changed_entities: usize,
78 pub changed_chains: usize,
80 pub changed_residues: usize,
82 pub changed_atoms: usize,
84 pub changed_bonds: usize,
86 pub changed_positions: usize,
88 pub maximum_displacement: Option<f32>,
90}
91
92impl StructureDifference {
93 #[must_use]
95 pub fn is_empty(&self) -> bool {
96 self.metadata == MetadataDifference::default()
97 && self.models.is_none()
98 && self.entities.is_none()
99 && self.chains.is_none()
100 && self.residues.is_none()
101 && self.atoms.is_none()
102 && self.bonds.is_none()
103 && self.changed_entities == 0
104 && self.changed_chains == 0
105 && self.changed_residues == 0
106 && self.changed_atoms == 0
107 && self.changed_bonds == 0
108 && self.changed_positions == 0
109 }
110}
111
112pub fn structure_difference(
119 left: &Structure,
120 right: &Structure,
121 options: StructureDifferenceOptions,
122) -> Result<StructureDifference, DifferenceError> {
123 if !options.coordinate_tolerance.is_finite() || options.coordinate_tolerance < 0.0 {
124 return Err(DifferenceError::InvalidCoordinateTolerance);
125 }
126
127 let (changed_positions, maximum_displacement) =
128 coordinate_changes(left, right, options.coordinate_tolerance);
129 Ok(StructureDifference {
130 metadata: metadata_difference(left, right),
131 models: different_count(left.model_count(), right.model_count()),
132 entities: different_count(left.entity_count(), right.entity_count()),
133 chains: different_count(left.chain_count(), right.chain_count()),
134 residues: different_count(left.residue_count(), right.residue_count()),
135 atoms: different_count(left.atom_count() as usize, right.atom_count() as usize),
136 bonds: different_count(left.data().bonds.len(), right.data().bonds.len()),
137 changed_entities: changed_entity_count(left, right),
138 changed_chains: changed_chain_count(left, right),
139 changed_residues: changed_residue_count(left, right),
140 changed_atoms: changed_atom_count(left, right),
141 changed_bonds: left
142 .data()
143 .bonds
144 .iter()
145 .zip(right.data().bonds.iter())
146 .filter(|(left_bond, right_bond)| {
147 left_bond.atom_a != right_bond.atom_a
153 || left_bond.atom_b != right_bond.atom_b
154 || left_bond.order != right_bond.order
155 || left_bond.provenance != right_bond.provenance
156 || resolved(left, left_bond.kind.into())
157 != resolved(right, right_bond.kind.into())
158 })
159 .count(),
160 changed_positions,
161 maximum_displacement,
162 })
163}
164
165fn changed_entity_count(left: &Structure, right: &Structure) -> usize {
166 let left_table = &left.data().topology.entities;
167 let right_table = &right.data().topology.entities;
168 left_table
169 .iter()
170 .zip(right_table.iter())
171 .filter(|(left_index, right_index)| {
172 left_table.kind(*left_index) != right_table.kind(*right_index)
173 || resolved(left, left_table.id(*left_index))
174 != resolved(right, right_table.id(*right_index))
175 || resolved(left, left_table.description(*left_index))
176 != resolved(right, right_table.description(*right_index))
177 || resolved_sequence(left, left_table.canonical_sequence(*left_index))
178 != resolved_sequence(right, right_table.canonical_sequence(*right_index))
179 })
180 .count()
181}
182
183fn changed_chain_count(left: &Structure, right: &Structure) -> usize {
184 (0..left.chain_count().min(right.chain_count()))
185 .filter(|position| {
186 let Ok(position) = u32::try_from(*position) else {
187 return true;
188 };
189 let index = ChainIndex::new(position);
190 let left_chain = left.chain(index);
191 let right_chain = right.chain(index);
192 match (left_chain, right_chain) {
193 (Some(left_chain), Some(right_chain)) => {
194 left_chain.label() != right_chain.label()
195 || left_chain.auth_label() != right_chain.auth_label()
196 || left_chain.entity() != right_chain.entity()
197 || left_chain.polymer_kind() != right_chain.polymer_kind()
198 }
199 _ => true,
200 }
201 })
202 .count()
203}
204
205fn changed_residue_count(left: &Structure, right: &Structure) -> usize {
206 (0..left.residue_count().min(right.residue_count()))
207 .filter(|position| {
208 let Ok(position) = u32::try_from(*position) else {
209 return true;
210 };
211 let index = ResidueIndex::new(position);
212 let left_residue = left.residue(index);
213 let right_residue = right.residue(index);
214 match (left_residue, right_residue) {
215 (Some(left_residue), Some(right_residue)) => {
216 left_residue.name() != right_residue.name()
217 || left_residue.auth_name() != right_residue.auth_name()
218 || left_residue.label_seq_id() != right_residue.label_seq_id()
219 || left_residue.auth_seq_id() != right_residue.auth_seq_id()
220 || left_residue.ins_code() != right_residue.ins_code()
221 || left_residue.is_het() != right_residue.is_het()
222 }
223 _ => true,
224 }
225 })
226 .count()
227}
228
229fn changed_atom_count(left: &Structure, right: &Structure) -> usize {
230 (0..left.atom_count().min(right.atom_count()))
231 .filter(|position| {
232 let left_atom = left.atom(AtomIndex::new(*position));
233 let right_atom = right.atom(AtomIndex::new(*position));
234 match (left_atom, right_atom) {
235 (Some(left_atom), Some(right_atom)) => {
236 left_atom.name() != right_atom.name()
237 || left_atom.auth_name() != right_atom.auth_name()
238 || left_atom.component_name() != right_atom.component_name()
239 || left_atom.element() != right_atom.element()
240 || left_atom.alt_label() != right_atom.alt_label()
241 || left_atom.b_factor() != right_atom.b_factor()
242 || left_atom.occupancy() != right_atom.occupancy()
243 || left_atom.formal_charge() != right_atom.formal_charge()
244 || left_atom.atom_site_id() != right_atom.atom_site_id()
245 }
246 _ => true,
247 }
248 })
249 .count()
250}
251
252fn coordinate_changes(left: &Structure, right: &Structure, tolerance: f32) -> (usize, Option<f32>) {
253 let mut changed = 0usize;
254 let mut maximum: Option<f32> = None;
255 for model in 0..left.model_count().min(right.model_count()) {
256 let Ok(model) = u32::try_from(model) else {
257 break;
258 };
259 let index = ModelIndex::new(model);
260 let left_snapshot = left.model_snapshot(index);
261 let right_snapshot = right.model_snapshot(index);
262 let left_positions: &[[f32; 3]] = match &left_snapshot {
263 Some((structure, local)) => structure
264 .model_positions(*local)
265 .map_or(&[], |positions| positions),
266 None => &[],
267 };
268 let right_positions: &[[f32; 3]] = match &right_snapshot {
269 Some((structure, local)) => structure
270 .model_positions(*local)
271 .map_or(&[], |positions| positions),
272 None => &[],
273 };
274 for (left_position, right_position) in left_positions.iter().zip(right_positions) {
275 let displacement = squared_distance(*left_position, *right_position).sqrt();
276 if !displacement.is_finite() || displacement > tolerance {
277 changed += 1;
278 }
279 if displacement.is_finite() {
280 maximum = Some(maximum.map_or(displacement, |value| value.max(displacement)));
281 }
282 }
283 changed += left_positions.len().abs_diff(right_positions.len());
284 }
285 (changed, maximum)
286}
287
288fn metadata_difference(left: &Structure, right: &Structure) -> MetadataDifference {
289 let left_entry = &left.data().entry;
290 let right_entry = &right.data().entry;
291 MetadataDifference {
292 id: different_value(left_entry.id.clone(), right_entry.id.clone()),
293 title: different_value(left_entry.title.clone(), right_entry.title.clone()),
294 method: different_value(left_entry.method.clone(), right_entry.method.clone()),
295 resolution: different_value(left_entry.resolution, right_entry.resolution),
296 cell: different_value(left.data().cell, right.data().cell),
297 }
298}
299
300fn different_count(left: usize, right: usize) -> Option<CountDifference> {
301 different_value(left, right)
302}
303
304fn different_value<T: PartialEq>(left: T, right: T) -> Option<ValueDifference<T>> {
305 (left != right).then_some(ValueDifference { left, right })
306}
307
308fn resolved(structure: &Structure, symbol: Option<crate::SymbolId>) -> Option<&str> {
309 symbol.and_then(|value| structure.resolve(value))
310}
311
312fn resolved_sequence<'a>(
313 structure: &'a Structure,
314 sequence: &'a [crate::SymbolId],
315) -> Vec<Option<&'a str>> {
316 sequence
317 .iter()
318 .map(|symbol| structure.resolve(*symbol))
319 .collect()
320}
321
322fn squared_distance(left: [f32; 3], right: [f32; 3]) -> f32 {
323 left.into_iter()
324 .zip(right)
325 .map(|(left, right)| {
326 let delta = left - right;
327 delta * delta
328 })
329 .sum()
330}
331
332#[cfg(test)]
333#[path = "diff_tests.rs"]
334mod tests;