molframe-core 0.1.1

Core types, storage and analysis contracts for molframe
Documentation
//! The compiled-in half of the identifier dictionary.
//!
//! These are the strings that recur in every structure ever deposited: element
//! symbols, the standard residue codes, and the atom names of the standard
//! components. Giving them fixed identifiers has a consequence beyond saving a
//! little memory — two structures agree on what `CA` means without consulting
//! each other, so comparing them is integer work, and a query compiled against
//! one structure can be replayed against another.
//!
//! The list is **append-only**. Reassigning an identifier would silently change
//! the meaning of a serialised query or a cached plan, so new strings go on the
//! end and nothing is ever removed or reordered.

use hashbrown::HashTable;
use std::convert::TryFrom;
use std::hash::{BuildHasher, RandomState};
use std::sync::OnceLock;

/// Strings with fixed identifiers, in identifier order. Never reorder this.
pub(super) static CANONICAL: &[&str] = &[
    // Element symbols as files spell them, upper-case.
    "H", "HE", "LI", "BE", "B", "C", "N", "O", "F", "NE", "NA", "MG", "AL", "SI", "P", "S", "CL",
    "AR", "K", "CA", "SC", "TI", "V", "CR", "MN", "FE", "CO", "NI", "CU", "ZN", "GA", "GE", "AS",
    "SE", "BR", "KR", "RB", "SR", "Y", "ZR", "NB", "MO", "TC", "RU", "RH", "PD", "AG", "CD", "IN",
    "SN", "SB", "TE", "I", "XE", "CS", "BA", "LA", "CE", "PR", "ND", "PM", "SM", "EU", "GD", "TB",
    "DY", "HO", "ER", "TM", "YB", "LU", "HF", "TA", "W", "RE", "OS", "IR", "PT", "AU", "HG", "TL",
    "PB", "BI", "PO", "AT", "RN", "FR", "RA", "AC", "TH", "PA", "U", "NP", "PU", "AM", "CM", "BK",
    "CF", "ES", "FM", "MD", "NO", "LR", "RF", "DB", "SG", "BH", "HS", "MT", "DS", "RG", "CN", "NH",
    "FL", "MC", "LV", "TS", "OG",
    // Standard amino acids, the common non-standard ones, and water.
    "ALA", "ARG", "ASN", "ASP", "CYS", "GLN", "GLU", "GLY", "HIS", "ILE", "LEU", "LYS", "MET",
    "PHE", "PRO", "SER", "THR", "TRP", "TYR", "VAL", "MSE", "SEC", "PYL", "ASX", "GLX", "UNK",
    "HOH", "WAT", "DOD", // Nucleotides, ribo and deoxy.
    "DA", "DC", "DG", "DT", "DU", "DI", "A", "G", "T", "UNL",
    // Protein backbone and side-chain atom names. `N`, `CA`, `C`, `O`, `CD`,
    // `CE`, `NE`, `OG` and `SG` are already declared above as element symbols:
    // one string is one identifier, and which reading applies is decided by the
    // column it was read from, not by the dictionary.
    "CB", "CG", "CG1", "CG2", "OG1", "CD1", "CD2", "ND1", "ND2", "OD1", "OD2", "SD", "CE1", "CE2",
    "CE3", "NE1", "NE2", "OE1", "OE2", "CZ", "CZ2", "CZ3", "NZ", "OH", "NH1", "NH2", "CH2", "OXT",
    "AD1", "AD2", "AE1", "AE2", // Nucleic acid atom names.
    "OP1", "OP2", "OP3", "O5'", "C5'", "C4'", "O4'", "C3'", "O3'", "C2'", "O2'", "C1'", "N1", "C2",
    "O2", "N3", "C4", "N4", "O4", "C5", "C6", "N6", "O6", "N7", "C8", "N9", "N2", "C7", "O1P",
    "O2P", "O3P",
    // Hydrogen names common enough to be worth a fixed identifier. `HG` is
    // already declared above as mercury's symbol.
    "H1", "H2", "H3", "HA", "HA2", "HA3", "HB", "HB1", "HB2", "HB3", "HG1", "HG2", "HG3", "HD1",
    "HD2", "HD3", "HE1", "HE2", "HE3", "HZ", "HZ1", "HZ2", "HZ3", "HH", "HH2", "HN", "HXT",
    // Alternate-location labels and single-character chain identifiers.
    "D", "E", "L", "M", "Q", "R", "X", "Z",
];

/// A type-safe index into [`CANONICAL`].
///
/// The transparent representation preserves the compact four-byte table
/// layout while preventing accidental mixing with unrelated `u32` values.
#[derive(Clone, Copy)]
#[repr(transparent)]
struct CanonicalOrdinal(u32);

impl CanonicalOrdinal {
    /// Converts a slice index into a canonical ordinal.
    ///
    /// Returns `None` when the index cannot be represented by the public
    /// 32-bit identifier format.
    ///
    /// Runs in `O(1)` time and allocates no memory.
    #[inline]
    fn from_index(index: usize) -> Option<Self> {
        u32::try_from(index).ok().map(Self)
    }

    /// Returns the raw canonical identifier.
    ///
    /// Runs in `O(1)` time and allocates no memory.
    #[inline]
    const fn get(self) -> u32 {
        self.0
    }

    /// Returns the canonical text represented by this ordinal.
    ///
    /// Returns `None` for an ordinal outside the compiled-in dictionary.
    /// Runs in `O(1)` time and allocates no memory.
    #[inline]
    fn text(self) -> Option<&'static str> {
        text_of(self.0)
    }
}

/// Lazily built lookup from string to canonical identifier.
///
/// The table cannot be a sorted array searched by comparison, because that would
/// force the list into alphabetical order and identifiers must follow
/// declaration order to stay stable. It is built once for the process.
static LOOKUP: OnceLock<Lookup> = OnceLock::new();

/// Process-wide canonical lookup state.
struct Lookup {
    table: HashTable<CanonicalEntry>,
    hasher: RandomState,
}

#[derive(Clone, Copy)]
struct CanonicalEntry {
    ordinal: CanonicalOrdinal,
    hash: u64,
}

/// Returns the initialized process-wide canonical lookup.
///
/// The first invocation builds the table in expected `O(N)` insertions and
/// `O(B)` hashing work, where `N` is the number of canonical strings and `B`
/// is their total byte length. Subsequent invocations run in `O(1)` time.
fn lookup() -> &'static Lookup {
    LOOKUP.get_or_init(|| {
        let hasher = RandomState::new();
        let mut table: HashTable<CanonicalEntry> = HashTable::with_capacity(CANONICAL.len());

        for (index, text) in CANONICAL.iter().enumerate() {
            let Some(ordinal) = CanonicalOrdinal::from_index(index) else {
                break;
            };

            let hash = hasher.hash_one(*text);

            // A duplicate would make one of the two identifiers unreachable; the
            // test below proves there are none, so the later entry is dropped.
            let entry = CanonicalEntry { ordinal, hash };
            table
                .entry(
                    hash,
                    |other| other.ordinal.text() == Some(*text),
                    |other| other.hash,
                )
                .or_insert(entry);
        }

        Lookup { table, hasher }
    })
}

/// Returns the canonical identifier for `text`, if it has one.
///
/// The lookup runs in expected `O(L)` time, where `L` is `text.len()`, and uses
/// `O(1)` auxiliary memory after the process-wide table has been initialized.
#[inline]
pub(super) fn ordinal_of(text: &str) -> Option<u32> {
    let lookup = lookup();
    let hash = lookup.hasher.hash_one(text);

    lookup
        .table
        .find(hash, |entry| entry.ordinal.text() == Some(text))
        .map(|entry| entry.ordinal.get())
}

/// Returns the string a canonical identifier names.
///
/// Runs in `O(1)` time and allocates no memory.
#[inline]
pub(super) fn text_of(ordinal: u32) -> Option<&'static str> {
    let Ok(index) = usize::try_from(ordinal) else {
        return None;
    };

    CANONICAL.get(index).copied()
}

#[cfg(test)]
#[path = "canonical_tests.rs"]
mod tests;