1use super::{AtomRef, ResidueRef, Structure};
7use crate::contract::{AltlocPolicy, Analysis, AnalysisPolicy, Coverage, Status};
8use crate::diagnostic::{Code, Diagnostic};
9use crate::hashing::IdentityBuildHasher;
10use crate::selection::AtomSelection;
11use crate::symbol::{AltId, SymbolId};
12use hashbrown::HashMap;
13
14type ScoreTable<K, V> = HashMap<K, V, IdentityBuildHasher>;
19
20#[derive(Clone, Copy)]
25struct LabelScore {
26 occupancy: f32,
27 order: usize,
28}
29
30#[derive(Clone, Copy)]
32struct LabelChoice {
33 selected: Option<AltId>,
34 has_altlocs: bool,
35}
36
37struct AltlocResolution {
43 selection: AtomSelection,
44 missing_label: bool,
45 has_altlocs: bool,
46}
47
48#[derive(Clone, Copy)]
50struct AtomChoice {
51 atom: u32,
52 occupancy: f32,
53}
54
55impl Structure {
56 #[must_use]
67 pub fn resolve_altlocs(&self, policy: &AnalysisPolicy) -> Analysis<AtomSelection> {
68 let total = self.atom_count();
69
70 let AltlocResolution {
71 selection,
72 missing_label,
73 has_altlocs,
74 } = match &policy.altloc {
75 AltlocPolicy::KeepAll => AltlocResolution {
76 selection: AtomSelection::All(total),
77 missing_label: false,
78 has_altlocs: self.has_altlocs(),
79 },
80 AltlocPolicy::ConformerConsistent => self.conformer_consistent(),
81 AltlocPolicy::Label(label) => self.named_conformer(label),
82 AltlocPolicy::First => self.per_residue(&FirstLabel),
83 AltlocPolicy::HighestOccupancyPerResidue => self.per_residue(&HighestOccupancy),
84 AltlocPolicy::HighestOccupancyPerAtom => self.per_atom_occupancy(),
85 };
86
87 let selected_rows = selection.len();
88 let used = match u32::try_from(selected_rows) {
89 Ok(used) if used <= total => used,
90 _ => {
91 return Analysis::indeterminate(
92 selection,
93 Coverage {
94 intended: total,
95 used: 0,
96 missing: total,
97 ambiguous: 0,
98 },
99 policy,
100 )
101 .with_warning(
102 Diagnostic::new(Code::E3001)
103 .with_context("selection rows", selected_rows.to_string())
104 .with_context("atoms", total.to_string()),
105 );
106 }
107 };
108
109 let mut result = Analysis::complete(
110 selection,
111 Coverage {
112 intended: total,
113 used,
114 missing: 0,
115 ambiguous: total - used,
116 },
117 policy,
118 );
119
120 if matches!(&policy.altloc, AltlocPolicy::KeepAll) && has_altlocs {
121 result.status = Status::Ambiguous;
122 }
123
124 if policy.altloc.is_hazardous() && has_altlocs {
125 result.warnings.push(Diagnostic::new(Code::W6001));
126 }
127
128 if missing_label {
129 result.warnings.push(
130 Diagnostic::new(Code::W4003)
131 .with_context("altloc", named_policy_label(&policy.altloc)),
132 );
133 }
134
135 result
136 }
137
138 fn has_altlocs(&self) -> bool {
143 self.data()
144 .atoms()
145 .any(|atom| atom.alt_id().is_some_and(|label| !label.is_blank()))
146 }
147
148 fn named_conformer(&self, label: &str) -> AltlocResolution {
154 let wanted = self.data().dictionary.get(label).and_then(AltId::labelled);
155
156 let requested_valid = wanted.is_some();
157 let (selection, present, has_altlocs) = self.select_label(wanted);
158
159 AltlocResolution {
160 selection,
161 missing_label: !requested_valid || !present,
162 has_altlocs,
163 }
164 }
165
166 fn select_label(&self, selected: Option<AltId>) -> (AtomSelection, bool, bool) {
171 let mut positions = Vec::new();
172 let mut selected_present = false;
173 let mut has_altlocs = false;
174
175 for atom in self.data().atoms() {
176 let Some(label) = atom.alt_id() else {
177 continue;
178 };
179
180 if !label.is_blank() {
181 has_altlocs = true;
182 }
183
184 if Some(label) == selected {
185 selected_present = true;
186 }
187
188 if label.is_blank() || Some(label) == selected {
189 positions.push(atom.index().get());
190 }
191 }
192
193 (
194 AtomSelection::from_sorted(positions),
195 selected_present,
196 has_altlocs,
197 )
198 }
199
200 fn conformer_consistent(&self) -> AltlocResolution {
205 let mut positions = Vec::new();
206 let mut scores = ScoreTable::default();
207 let mut has_altlocs = false;
208
209 for chain in self.data().chains() {
210 let choice = best_label(chain.residues().flat_map(ResidueRef::atoms), &mut scores);
211
212 has_altlocs |= choice.has_altlocs;
213
214 for atom in chain.residues().flat_map(ResidueRef::atoms) {
215 if keeps_label(atom, choice.selected) {
216 positions.push(atom.index().get());
217 }
218 }
219 }
220
221 AltlocResolution {
222 selection: AtomSelection::from_sorted(positions),
223 missing_label: false,
224 has_altlocs,
225 }
226 }
227
228 fn per_residue(&self, chooser: &impl LabelChooser) -> AltlocResolution {
233 let mut positions = Vec::new();
234 let mut scores = ScoreTable::default();
235 let mut has_altlocs = false;
236
237 for residue in self.data().residues() {
238 let choice = chooser.choose(residue, &mut scores);
239 has_altlocs |= choice.has_altlocs;
240
241 positions.extend(
242 residue
243 .atoms()
244 .filter(|atom| keeps_label(*atom, choice.selected))
245 .map(|atom| atom.index().get()),
246 );
247 }
248
249 AltlocResolution {
250 selection: AtomSelection::from_sorted(positions),
251 missing_label: false,
252 has_altlocs,
253 }
254 }
255
256 fn per_atom_occupancy(&self) -> AltlocResolution {
261 let mut positions = Vec::new();
262 let mut choices: ScoreTable<SymbolId, AtomChoice> = ScoreTable::default();
263 let mut has_altlocs = false;
264
265 for residue in self.data().residues() {
266 choices.clear();
267
268 for atom in residue.atoms() {
269 let Some(label) = atom.alt_id() else {
270 continue;
271 };
272
273 if label.is_blank() {
274 continue;
275 }
276
277 has_altlocs = true;
278
279 let Some(name) = atom.name_symbol() else {
280 continue;
281 };
282
283 record_atom_choice(&mut choices, name, atom);
284 }
285
286 for atom in residue.atoms() {
287 let Some(label) = atom.alt_id() else {
288 continue;
289 };
290
291 if label.is_blank() {
292 positions.push(atom.index().get());
293 continue;
294 }
295
296 let Some(name) = atom.name_symbol() else {
297 continue;
298 };
299
300 let Some(choice) = choices.get(&name) else {
301 continue;
302 };
303
304 if choice.atom == atom.index().get() {
305 positions.push(choice.atom);
306 }
307 }
308 }
309
310 AltlocResolution {
311 selection: AtomSelection::from_sorted(positions),
312 missing_label: false,
313 has_altlocs,
314 }
315 }
316}
317
318trait LabelChooser {
323 fn choose(
325 &self,
326 residue: ResidueRef<'_>,
327 scores: &mut ScoreTable<AltId, LabelScore>,
328 ) -> LabelChoice;
329}
330
331struct FirstLabel;
333
334impl LabelChooser for FirstLabel {
335 fn choose(
337 &self,
338 residue: ResidueRef<'_>,
339 _scores: &mut ScoreTable<AltId, LabelScore>,
340 ) -> LabelChoice {
341 let selected = residue
342 .atoms()
343 .filter_map(AtomRef::alt_id)
344 .find(|label| !label.is_blank());
345
346 LabelChoice {
347 selected,
348 has_altlocs: selected.is_some(),
349 }
350 }
351}
352
353struct HighestOccupancy;
355
356impl LabelChooser for HighestOccupancy {
357 fn choose(
359 &self,
360 residue: ResidueRef<'_>,
361 scores: &mut ScoreTable<AltId, LabelScore>,
362 ) -> LabelChoice {
363 best_label(residue.atoms(), scores)
364 }
365}
366
367fn best_label<'a>(
373 atoms: impl Iterator<Item = AtomRef<'a>>,
374 scores: &mut ScoreTable<AltId, LabelScore>,
375) -> LabelChoice {
376 scores.clear();
377
378 for atom in atoms {
379 let Some(label) = atom.alt_id() else {
380 continue;
381 };
382
383 if label.is_blank() {
384 continue;
385 }
386
387 let occupancy = occupancy(atom);
388 let order = scores.len();
389
390 scores
391 .entry(label)
392 .and_modify(|score| score.occupancy += occupancy)
393 .or_insert(LabelScore { occupancy, order });
394 }
395
396 LabelChoice {
397 selected: best_scored_label(scores),
398 has_altlocs: !scores.is_empty(),
399 }
400}
401
402fn best_scored_label(scores: &ScoreTable<AltId, LabelScore>) -> Option<AltId> {
408 let mut earliest: Option<(AltId, LabelScore)> = None;
409 let mut best_finite: Option<(AltId, LabelScore)> = None;
410
411 for (&label, &score) in scores {
412 if earliest.is_none_or(|(_, current)| score.order < current.order) {
413 earliest = Some((label, score));
414 }
415
416 if score.occupancy.is_nan() {
417 continue;
418 }
419
420 if best_finite.is_none_or(|(_, current)| {
421 score.occupancy > current.occupancy
422 || (score.occupancy.to_bits() == current.occupancy.to_bits()
423 && score.order < current.order)
424 }) {
425 best_finite = Some((label, score));
426 }
427 }
428
429 match earliest {
430 Some((label, score)) if score.occupancy.is_nan() => Some(label),
431 Some(_) => best_finite.map(|(label, _)| label),
432 None => None,
433 }
434}
435
436fn record_atom_choice(
441 choices: &mut ScoreTable<SymbolId, AtomChoice>,
442 name: SymbolId,
443 atom: AtomRef<'_>,
444) {
445 let candidate = AtomChoice {
446 atom: atom.index().get(),
447 occupancy: occupancy(atom),
448 };
449
450 choices
451 .entry(name)
452 .and_modify(|current| {
453 if candidate.occupancy > current.occupancy {
454 *current = candidate;
455 }
456 })
457 .or_insert(candidate);
458}
459
460fn occupancy(atom: AtomRef<'_>) -> f32 {
462 let Some(value) = atom.occupancy() else {
463 return 1.0;
464 };
465 value
466}
467
468fn keeps_label(atom: AtomRef<'_>, selected: Option<AltId>) -> bool {
472 atom.alt_id()
473 .is_some_and(|label| label.is_blank() || Some(label) == selected)
474}
475
476fn named_policy_label(policy: &AltlocPolicy) -> Box<str> {
481 match policy {
482 AltlocPolicy::Label(label) => label.clone(),
483 _ => "".into(),
484 }
485}
486
487#[cfg(test)]
488#[path = "disorder_tests.rs"]
489mod tests;