1mod compat;
7
8use super::{AtomRef, ResidueRef, Structure};
9use crate::contract::{AltlocPolicy, Analysis, AnalysisPolicy, Coverage, Indeterminacy, Quality};
10use crate::diagnostic::{Code, Diagnostic};
11use crate::hashing::IdentityBuildHasher;
12use crate::selection::AtomSelection;
13use crate::symbol::{AltId, SymbolId};
14pub use compat::{altloc_compatible, compatible_pairs};
15use hashbrown::HashMap;
16
17type ScoreTable<K, V> = HashMap<K, V, IdentityBuildHasher>;
22
23#[derive(Clone, Copy)]
28struct LabelScore {
29 occupancy: f32,
30 order: usize,
31}
32
33#[derive(Clone, Copy)]
35struct LabelChoice {
36 selected: Option<AltId>,
37 has_altlocs: bool,
38}
39
40struct AltlocResolution {
46 selection: AtomSelection,
47 missing_label: bool,
48 has_altlocs: bool,
49}
50
51#[derive(Clone, Copy)]
53struct AtomChoice {
54 atom: u32,
55 occupancy: f32,
56}
57
58impl Structure {
59 #[must_use]
70 pub fn resolve_altlocs(&self, policy: &AnalysisPolicy) -> Analysis<AtomSelection> {
71 if let Err(finding) = self.require_resolved_model() {
72 return Analysis::indeterminate(
73 Indeterminacy::UnresolvedConformations,
74 Coverage {
75 intended: 0,
76 used: 0,
77 missing: 0,
78 ambiguous: 0,
79 },
80 policy,
81 )
82 .with_warning(finding);
83 }
84
85 let total = self.atom_count();
86
87 let AltlocResolution {
88 selection,
89 missing_label,
90 has_altlocs,
91 } = match &policy.altloc {
92 AltlocPolicy::KeepAll => AltlocResolution {
93 selection: AtomSelection::All(total),
94 missing_label: false,
95 has_altlocs: self.has_altlocs(),
96 },
97 AltlocPolicy::ConformerConsistent => self.conformer_consistent(),
98 AltlocPolicy::Label(label) => self.named_conformer(label),
99 AltlocPolicy::First => self.per_residue(&FirstLabel),
100 AltlocPolicy::HighestOccupancyPerResidue => self.per_residue(&HighestOccupancy),
101 AltlocPolicy::HighestOccupancyPerAtom => self.per_atom_occupancy(),
102 };
103
104 let selected_rows = selection.len();
105 let used = match u32::try_from(selected_rows) {
106 Ok(used) if used <= total => used,
107 _ => {
108 return Analysis::indeterminate(
109 Indeterminacy::UnresolvedConformations,
110 Coverage {
111 intended: total,
112 used: 0,
113 missing: total,
114 ambiguous: 0,
115 },
116 policy,
117 )
118 .with_warning(
119 Diagnostic::new(Code::E3001)
120 .with_context("selection rows", selected_rows.to_string())
121 .with_context("atoms", total.to_string()),
122 );
123 }
124 };
125
126 let mut result = Analysis::complete(
127 selection,
128 Coverage {
129 intended: total,
130 used,
131 missing: 0,
132 ambiguous: total - used,
133 },
134 policy,
135 );
136
137 if matches!(&policy.altloc, AltlocPolicy::KeepAll) && has_altlocs {
138 result.degrade(Quality::Ambiguous);
139 }
140
141 if policy.altloc.is_hazardous() && has_altlocs {
142 result.warnings.push(Diagnostic::new(Code::W6001));
143 }
144
145 if missing_label {
146 result.warnings.push(
147 Diagnostic::new(Code::W4003)
148 .with_context("altloc", named_policy_label(&policy.altloc)),
149 );
150 }
151
152 result
153 }
154
155 fn has_altlocs(&self) -> bool {
160 self.data()
161 .atoms()
162 .any(|atom| atom.alt_id().is_some_and(|label| !label.is_blank()))
163 }
164
165 fn named_conformer(&self, label: &str) -> AltlocResolution {
171 let wanted = self.data().dictionary.get(label).and_then(AltId::labelled);
172
173 let requested_valid = wanted.is_some();
174 let (selection, present, has_altlocs) = self.select_label(wanted);
175
176 AltlocResolution {
177 selection,
178 missing_label: !requested_valid || !present,
179 has_altlocs,
180 }
181 }
182
183 fn select_label(&self, selected: Option<AltId>) -> (AtomSelection, bool, bool) {
188 let mut positions = Vec::new();
189 let mut selected_present = false;
190 let mut has_altlocs = false;
191
192 for atom in self.data().atoms() {
193 let Some(label) = atom.alt_id() else {
194 continue;
195 };
196
197 if !label.is_blank() {
198 has_altlocs = true;
199 }
200
201 if Some(label) == selected {
202 selected_present = true;
203 }
204
205 if label.is_blank() || Some(label) == selected {
206 positions.push(atom.index().get());
207 }
208 }
209
210 (
211 AtomSelection::from_sorted(positions),
212 selected_present,
213 has_altlocs,
214 )
215 }
216
217 fn conformer_consistent(&self) -> AltlocResolution {
222 let mut positions = Vec::new();
223 let mut scores = ScoreTable::default();
224 let mut has_altlocs = false;
225
226 for chain in self.data().chains() {
227 let choice = best_label(chain.residues().flat_map(ResidueRef::atoms), &mut scores);
228
229 has_altlocs |= choice.has_altlocs;
230
231 for atom in chain.residues().flat_map(ResidueRef::atoms) {
232 if keeps_label(atom, choice.selected) {
233 positions.push(atom.index().get());
234 }
235 }
236 }
237
238 AltlocResolution {
239 selection: AtomSelection::from_sorted(positions),
240 missing_label: false,
241 has_altlocs,
242 }
243 }
244
245 fn per_residue(&self, chooser: &impl LabelChooser) -> AltlocResolution {
250 let mut positions = Vec::new();
251 let mut scores = ScoreTable::default();
252 let mut has_altlocs = false;
253
254 for residue in self.data().residues() {
255 let choice = chooser.choose(residue, &mut scores);
256 has_altlocs |= choice.has_altlocs;
257
258 positions.extend(
259 residue
260 .atoms()
261 .filter(|atom| keeps_label(*atom, choice.selected))
262 .map(|atom| atom.index().get()),
263 );
264 }
265
266 AltlocResolution {
267 selection: AtomSelection::from_sorted(positions),
268 missing_label: false,
269 has_altlocs,
270 }
271 }
272
273 fn per_atom_occupancy(&self) -> AltlocResolution {
278 let mut positions = Vec::new();
279 let mut choices: ScoreTable<SymbolId, AtomChoice> = ScoreTable::default();
280 let mut has_altlocs = false;
281
282 for residue in self.data().residues() {
283 choices.clear();
284
285 for atom in residue.atoms() {
286 let Some(label) = atom.alt_id() else {
287 continue;
288 };
289
290 if label.is_blank() {
291 continue;
292 }
293
294 has_altlocs = true;
295
296 let Some(name) = atom.name_symbol() else {
297 continue;
298 };
299
300 record_atom_choice(&mut choices, name, atom);
301 }
302
303 for atom in residue.atoms() {
304 let Some(label) = atom.alt_id() else {
305 continue;
306 };
307
308 if label.is_blank() {
309 positions.push(atom.index().get());
310 continue;
311 }
312
313 let Some(name) = atom.name_symbol() else {
314 continue;
315 };
316
317 let Some(choice) = choices.get(&name) else {
318 continue;
319 };
320
321 if choice.atom == atom.index().get() {
322 positions.push(choice.atom);
323 }
324 }
325 }
326
327 AltlocResolution {
328 selection: AtomSelection::from_sorted(positions),
329 missing_label: false,
330 has_altlocs,
331 }
332 }
333}
334
335trait LabelChooser {
340 fn choose(
342 &self,
343 residue: ResidueRef<'_>,
344 scores: &mut ScoreTable<AltId, LabelScore>,
345 ) -> LabelChoice;
346}
347
348struct FirstLabel;
350
351impl LabelChooser for FirstLabel {
352 fn choose(
354 &self,
355 residue: ResidueRef<'_>,
356 _scores: &mut ScoreTable<AltId, LabelScore>,
357 ) -> LabelChoice {
358 let selected = residue
359 .atoms()
360 .filter_map(AtomRef::alt_id)
361 .find(|label| !label.is_blank());
362
363 LabelChoice {
364 selected,
365 has_altlocs: selected.is_some(),
366 }
367 }
368}
369
370struct HighestOccupancy;
372
373impl LabelChooser for HighestOccupancy {
374 fn choose(
376 &self,
377 residue: ResidueRef<'_>,
378 scores: &mut ScoreTable<AltId, LabelScore>,
379 ) -> LabelChoice {
380 best_label(residue.atoms(), scores)
381 }
382}
383
384fn best_label<'a>(
390 atoms: impl Iterator<Item = AtomRef<'a>>,
391 scores: &mut ScoreTable<AltId, LabelScore>,
392) -> LabelChoice {
393 scores.clear();
394
395 for atom in atoms {
396 let Some(label) = atom.alt_id() else {
397 continue;
398 };
399
400 if label.is_blank() {
401 continue;
402 }
403
404 let occupancy = occupancy(atom);
405 let order = scores.len();
406
407 scores
408 .entry(label)
409 .and_modify(|score| score.occupancy += occupancy)
410 .or_insert(LabelScore { occupancy, order });
411 }
412
413 LabelChoice {
414 selected: best_scored_label(scores),
415 has_altlocs: !scores.is_empty(),
416 }
417}
418
419fn best_scored_label(scores: &ScoreTable<AltId, LabelScore>) -> Option<AltId> {
425 let mut earliest: Option<(AltId, LabelScore)> = None;
426 let mut best_finite: Option<(AltId, LabelScore)> = None;
427
428 for (&label, &score) in scores {
429 if earliest.is_none_or(|(_, current)| score.order < current.order) {
430 earliest = Some((label, score));
431 }
432
433 if score.occupancy.is_nan() {
434 continue;
435 }
436
437 if best_finite.is_none_or(|(_, current)| {
438 score.occupancy > current.occupancy
439 || (score.occupancy.to_bits() == current.occupancy.to_bits()
440 && score.order < current.order)
441 }) {
442 best_finite = Some((label, score));
443 }
444 }
445
446 match earliest {
447 Some((label, score)) if score.occupancy.is_nan() => Some(label),
448 Some(_) => best_finite.map(|(label, _)| label),
449 None => None,
450 }
451}
452
453fn record_atom_choice(
455 choices: &mut ScoreTable<SymbolId, AtomChoice>,
456 name: SymbolId,
457 atom: AtomRef<'_>,
458) {
459 let candidate = AtomChoice {
460 atom: atom.index().get(),
461 occupancy: occupancy(atom),
462 };
463
464 choices
465 .entry(name)
466 .and_modify(|current| {
467 if candidate.occupancy > current.occupancy {
468 *current = candidate;
469 }
470 })
471 .or_insert(candidate);
472}
473
474fn occupancy(atom: AtomRef<'_>) -> f32 {
476 let Some(value) = atom.occupancy() else {
477 return 1.0;
478 };
479 value
480}
481
482fn keeps_label(atom: AtomRef<'_>, selected: Option<AltId>) -> bool {
484 atom.alt_id()
485 .is_some_and(|label| label.is_blank() || Some(label) == selected)
486}
487
488fn named_policy_label(policy: &AltlocPolicy) -> Box<str> {
490 match policy {
491 AltlocPolicy::Label(label) => label.clone(),
492 _ => "".into(),
493 }
494}
495
496#[cfg(test)]
497#[path = "disorder_tests.rs"]
498mod tests;