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, right)| left != right)
147 .count(),
148 changed_positions,
149 maximum_displacement,
150 })
151}
152
153fn changed_entity_count(left: &Structure, right: &Structure) -> usize {
154 let left_table = &left.data().topology.entities;
155 let right_table = &right.data().topology.entities;
156 left_table
157 .iter()
158 .zip(right_table.iter())
159 .filter(|(left_index, right_index)| {
160 left_table.kind(*left_index) != right_table.kind(*right_index)
161 || resolved(left, left_table.id(*left_index))
162 != resolved(right, right_table.id(*right_index))
163 || resolved(left, left_table.description(*left_index))
164 != resolved(right, right_table.description(*right_index))
165 || resolved_sequence(left, left_table.canonical_sequence(*left_index))
166 != resolved_sequence(right, right_table.canonical_sequence(*right_index))
167 })
168 .count()
169}
170
171fn changed_chain_count(left: &Structure, right: &Structure) -> usize {
172 (0..left.chain_count().min(right.chain_count()))
173 .filter(|position| {
174 let Ok(position) = u32::try_from(*position) else {
175 return true;
176 };
177 let index = ChainIndex::new(position);
178 let left_chain = left.chain(index);
179 let right_chain = right.chain(index);
180 match (left_chain, right_chain) {
181 (Some(left_chain), Some(right_chain)) => {
182 left_chain.label() != right_chain.label()
183 || left_chain.auth_label() != right_chain.auth_label()
184 || left_chain.entity() != right_chain.entity()
185 || left_chain.polymer_kind() != right_chain.polymer_kind()
186 }
187 _ => true,
188 }
189 })
190 .count()
191}
192
193fn changed_residue_count(left: &Structure, right: &Structure) -> usize {
194 (0..left.residue_count().min(right.residue_count()))
195 .filter(|position| {
196 let Ok(position) = u32::try_from(*position) else {
197 return true;
198 };
199 let index = ResidueIndex::new(position);
200 let left_residue = left.residue(index);
201 let right_residue = right.residue(index);
202 match (left_residue, right_residue) {
203 (Some(left_residue), Some(right_residue)) => {
204 left_residue.name() != right_residue.name()
205 || left_residue.auth_name() != right_residue.auth_name()
206 || left_residue.label_seq_id() != right_residue.label_seq_id()
207 || left_residue.auth_seq_id() != right_residue.auth_seq_id()
208 || left_residue.ins_code() != right_residue.ins_code()
209 || left_residue.is_het() != right_residue.is_het()
210 }
211 _ => true,
212 }
213 })
214 .count()
215}
216
217fn changed_atom_count(left: &Structure, right: &Structure) -> usize {
218 (0..left.atom_count().min(right.atom_count()))
219 .filter(|position| {
220 let left_atom = left.atom(AtomIndex::new(*position));
221 let right_atom = right.atom(AtomIndex::new(*position));
222 match (left_atom, right_atom) {
223 (Some(left_atom), Some(right_atom)) => {
224 left_atom.name() != right_atom.name()
225 || left_atom.auth_name() != right_atom.auth_name()
226 || left_atom.component_name() != right_atom.component_name()
227 || left_atom.element() != right_atom.element()
228 || left_atom.alt_label() != right_atom.alt_label()
229 || left_atom.b_factor() != right_atom.b_factor()
230 || left_atom.occupancy() != right_atom.occupancy()
231 || left_atom.formal_charge() != right_atom.formal_charge()
232 || left_atom.atom_site_id() != right_atom.atom_site_id()
233 }
234 _ => true,
235 }
236 })
237 .count()
238}
239
240fn coordinate_changes(left: &Structure, right: &Structure, tolerance: f32) -> (usize, Option<f32>) {
241 let mut changed = 0usize;
242 let mut maximum: Option<f32> = None;
243 for model in 0..left.model_count().min(right.model_count()) {
244 let Ok(model) = u32::try_from(model) else {
245 break;
246 };
247 let index = ModelIndex::new(model);
248 let left_snapshot = left.model_snapshot(index);
249 let right_snapshot = right.model_snapshot(index);
250 let left_positions: &[[f32; 3]] = match &left_snapshot {
251 Some((structure, local)) => structure
252 .model_positions(*local)
253 .map_or(&[], |positions| positions),
254 None => &[],
255 };
256 let right_positions: &[[f32; 3]] = match &right_snapshot {
257 Some((structure, local)) => structure
258 .model_positions(*local)
259 .map_or(&[], |positions| positions),
260 None => &[],
261 };
262 for (left_position, right_position) in left_positions.iter().zip(right_positions) {
263 let displacement = squared_distance(*left_position, *right_position).sqrt();
264 if !displacement.is_finite() || displacement > tolerance {
265 changed += 1;
266 }
267 if displacement.is_finite() {
268 maximum = Some(maximum.map_or(displacement, |value| value.max(displacement)));
269 }
270 }
271 changed += left_positions.len().abs_diff(right_positions.len());
272 }
273 (changed, maximum)
274}
275
276fn metadata_difference(left: &Structure, right: &Structure) -> MetadataDifference {
277 let left_entry = &left.data().entry;
278 let right_entry = &right.data().entry;
279 MetadataDifference {
280 id: different_value(left_entry.id.clone(), right_entry.id.clone()),
281 title: different_value(left_entry.title.clone(), right_entry.title.clone()),
282 method: different_value(left_entry.method.clone(), right_entry.method.clone()),
283 resolution: different_value(left_entry.resolution, right_entry.resolution),
284 cell: different_value(left.data().cell, right.data().cell),
285 }
286}
287
288fn different_count(left: usize, right: usize) -> Option<CountDifference> {
289 different_value(left, right)
290}
291
292fn different_value<T: PartialEq>(left: T, right: T) -> Option<ValueDifference<T>> {
293 (left != right).then_some(ValueDifference { left, right })
294}
295
296fn resolved(structure: &Structure, symbol: Option<crate::SymbolId>) -> Option<&str> {
297 symbol.and_then(|value| structure.resolve(value))
298}
299
300fn resolved_sequence<'a>(
301 structure: &'a Structure,
302 sequence: &'a [crate::SymbolId],
303) -> Vec<Option<&'a str>> {
304 sequence
305 .iter()
306 .map(|symbol| structure.resolve(*symbol))
307 .collect()
308}
309
310fn squared_distance(left: [f32; 3], right: [f32; 3]) -> f32 {
311 left.into_iter()
312 .zip(right)
313 .map(|(left, right)| {
314 let delta = left - right;
315 delta * delta
316 })
317 .sum()
318}
319
320#[cfg(test)]
321#[path = "diff_tests.rs"]
322mod tests;