Skip to main content

molframe_core/annotation/
band.rs

1//! Named pLDDT confidence bands shared by selection, colouring and bindings.
2//!
3//! The four bands are the ones structure viewers report for predicted models.
4//! Boundaries are half-open from below so every finite score belongs to exactly
5//! one band: `very_high` is `> 90`, `confident` is `(70, 90]`, `low` is
6//! `(50, 70]` and `very_low` is `<= 50`.
7
8use super::AnnotationColumn;
9use crate::column::Presence;
10
11/// Code written for an atom that has no recorded score.
12pub const MISSING_BAND_CODE: u8 = u8::MAX;
13
14/// One of the four pLDDT confidence bands.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub enum PlddtBand {
17    /// Score of at most 50.
18    VeryLow,
19    /// Score above 50 and at most 70.
20    Low,
21    /// Score above 70 and at most 90.
22    Confident,
23    /// Score above 90.
24    VeryHigh,
25}
26
27impl PlddtBand {
28    /// All bands from least to most confident.
29    pub const ALL: [Self; 4] = [Self::VeryLow, Self::Low, Self::Confident, Self::VeryHigh];
30
31    /// Band of `score`, or `None` for a non-finite score.
32    #[must_use]
33    pub fn classify(score: f64) -> Option<Self> {
34        if !score.is_finite() {
35            return None;
36        }
37        Some(if score > 90.0 {
38            Self::VeryHigh
39        } else if score > 70.0 {
40            Self::Confident
41        } else if score > 50.0 {
42            Self::Low
43        } else {
44            Self::VeryLow
45        })
46    }
47
48    /// Stable lowercase name, also the selection macro suffix.
49    #[must_use]
50    pub const fn name(self) -> &'static str {
51        match self {
52            Self::VeryLow => "very_low",
53            Self::Low => "low",
54            Self::Confident => "confident",
55            Self::VeryHigh => "very_high",
56        }
57    }
58
59    /// Parses a name produced by [`Self::name`], ignoring ASCII case.
60    #[must_use]
61    pub fn from_name(name: &str) -> Option<Self> {
62        Self::ALL
63            .into_iter()
64            .find(|band| band.name().eq_ignore_ascii_case(name))
65    }
66
67    /// Conventional display colour as sRGB bytes.
68    #[must_use]
69    pub const fn rgb(self) -> [u8; 3] {
70        match self {
71            Self::VeryLow => [255, 125, 69],
72            Self::Low => [255, 219, 19],
73            Self::Confident => [101, 203, 243],
74            Self::VeryHigh => [0, 83, 214],
75        }
76    }
77
78    /// Stable small integer: 0 for `very_low` up to 3 for `very_high`.
79    #[must_use]
80    pub const fn code(self) -> u8 {
81        match self {
82            Self::VeryLow => 0,
83            Self::Low => 1,
84            Self::Confident => 2,
85            Self::VeryHigh => 3,
86        }
87    }
88
89    /// Band codes of every atom in a score column, [`MISSING_BAND_CODE`] where unscored.
90    #[must_use]
91    pub fn codes(column: &AnnotationColumn<f64>) -> Vec<u8> {
92        column
93            .values()
94            .iter()
95            .enumerate()
96            .map(|(row, score)| {
97                let recorded =
98                    u32::try_from(row).is_ok_and(|atom| column.presence(atom) == Presence::Present);
99                Self::classify(*score)
100                    .filter(|_| recorded)
101                    .map_or(MISSING_BAND_CODE, Self::code)
102            })
103            .collect()
104    }
105
106    /// Lowest score that can belong to this band, exclusive except for `VeryLow`.
107    #[must_use]
108    pub const fn lower_bound(self) -> f64 {
109        match self {
110            Self::VeryLow => f64::NEG_INFINITY,
111            Self::Low => 50.0,
112            Self::Confident => 70.0,
113            Self::VeryHigh => 90.0,
114        }
115    }
116}
117
118#[cfg(test)]
119#[path = "band_tests.rs"]
120mod tests;