readcon-core 0.14.0

An oxidized single and multiple CON file reader and writer with FFI bindings for ergonomic C/C++ usage.
Documentation
//! Element symbol and atomic number lookup.
//!
//! Both lookups cover hydrogen through uranium (Z = 1..=92) plus the
//! hydrogen isotopes deuterium ("D") and tritium ("T") which both
//! map to Z = 1 since they share hydrogen's nucleon charge. The
//! reverse lookup ([`atomic_number_to_symbol`]) returns the standard
//! "H" for Z = 1; callers that need to distinguish isotopes do so out
//! of band (e.g. an `isotope` metadata key on the frame, or by
//! storing "D" / "T" as the per-atom symbol).
//!
//! The mapping is *informational, not binding*. The CON spec does not
//! mandate that an atom's symbol correspond to any periodic-table
//! element; consumers commonly store ghost atoms (e.g. for QM/MM
//! link atoms, virtual sites, dummy positions in NEB chains) with
//! whatever symbol fits their workflow. Unknown inputs return a
//! stable sentinel: [`symbol_to_atomic_number`] returns 0 for
//! unknown symbols and [`atomic_number_to_symbol`] returns "X" for
//! unknown atomic numbers.
//!
//! The same lookup is exposed to C/C++ via [`crate::ffi::rkr_symbol_to_z`]
//! and [`crate::ffi::rkr_z_to_symbol`] so downstream tools can drop
//! their own copies of the periodic table.

/// Returns the atomic number for a chemical symbol, or 0 if unknown.
pub fn symbol_to_atomic_number(symbol: &str) -> u64 {
    match symbol {
        "H" | "D" | "T" => 1,
        "He" => 2,
        "Li" => 3,
        "Be" => 4,
        "B" => 5,
        "C" => 6,
        "N" => 7,
        "O" => 8,
        "F" => 9,
        "Ne" => 10,
        "Na" => 11,
        "Mg" => 12,
        "Al" => 13,
        "Si" => 14,
        "P" => 15,
        "S" => 16,
        "Cl" => 17,
        "Ar" => 18,
        "K" => 19,
        "Ca" => 20,
        "Sc" => 21,
        "Ti" => 22,
        "V" => 23,
        "Cr" => 24,
        "Mn" => 25,
        "Fe" => 26,
        "Co" => 27,
        "Ni" => 28,
        "Cu" => 29,
        "Zn" => 30,
        "Ga" => 31,
        "Ge" => 32,
        "As" => 33,
        "Se" => 34,
        "Br" => 35,
        "Kr" => 36,
        "Rb" => 37,
        "Sr" => 38,
        "Y" => 39,
        "Zr" => 40,
        "Nb" => 41,
        "Mo" => 42,
        "Tc" => 43,
        "Ru" => 44,
        "Rh" => 45,
        "Pd" => 46,
        "Ag" => 47,
        "Cd" => 48,
        "In" => 49,
        "Sn" => 50,
        "Sb" => 51,
        "Te" => 52,
        "I" => 53,
        "Xe" => 54,
        "Cs" => 55,
        "Ba" => 56,
        "La" => 57,
        "Ce" => 58,
        "Pr" => 59,
        "Nd" => 60,
        "Pm" => 61,
        "Sm" => 62,
        "Eu" => 63,
        "Gd" => 64,
        "Tb" => 65,
        "Dy" => 66,
        "Ho" => 67,
        "Er" => 68,
        "Tm" => 69,
        "Yb" => 70,
        "Lu" => 71,
        "Hf" => 72,
        "Ta" => 73,
        "W" => 74,
        "Re" => 75,
        "Os" => 76,
        "Ir" => 77,
        "Pt" => 78,
        "Au" => 79,
        "Hg" => 80,
        "Tl" => 81,
        "Pb" => 82,
        "Bi" => 83,
        "Po" => 84,
        "At" => 85,
        "Rn" => 86,
        "Fr" => 87,
        "Ra" => 88,
        "Ac" => 89,
        "Th" => 90,
        "Pa" => 91,
        "U" => 92,
        _ => 0, // Unknown
    }
}

/// Returns the chemical symbol for an atomic number, or "X" if unknown.
pub fn atomic_number_to_symbol(atomic_number: u64) -> &'static str {
    match atomic_number {
        1 => "H",
        2 => "He",
        3 => "Li",
        4 => "Be",
        5 => "B",
        6 => "C",
        7 => "N",
        8 => "O",
        9 => "F",
        10 => "Ne",
        11 => "Na",
        12 => "Mg",
        13 => "Al",
        14 => "Si",
        15 => "P",
        16 => "S",
        17 => "Cl",
        18 => "Ar",
        19 => "K",
        20 => "Ca",
        21 => "Sc",
        22 => "Ti",
        23 => "V",
        24 => "Cr",
        25 => "Mn",
        26 => "Fe",
        27 => "Co",
        28 => "Ni",
        29 => "Cu",
        30 => "Zn",
        31 => "Ga",
        32 => "Ge",
        33 => "As",
        34 => "Se",
        35 => "Br",
        36 => "Kr",
        37 => "Rb",
        38 => "Sr",
        39 => "Y",
        40 => "Zr",
        41 => "Nb",
        42 => "Mo",
        43 => "Tc",
        44 => "Ru",
        45 => "Rh",
        46 => "Pd",
        47 => "Ag",
        48 => "Cd",
        49 => "In",
        50 => "Sn",
        51 => "Sb",
        52 => "Te",
        53 => "I",
        54 => "Xe",
        55 => "Cs",
        56 => "Ba",
        57 => "La",
        58 => "Ce",
        59 => "Pr",
        60 => "Nd",
        61 => "Pm",
        62 => "Sm",
        63 => "Eu",
        64 => "Gd",
        65 => "Tb",
        66 => "Dy",
        67 => "Ho",
        68 => "Er",
        69 => "Tm",
        70 => "Yb",
        71 => "Lu",
        72 => "Hf",
        73 => "Ta",
        74 => "W",
        75 => "Re",
        76 => "Os",
        77 => "Ir",
        78 => "Pt",
        79 => "Au",
        80 => "Hg",
        81 => "Tl",
        82 => "Pb",
        83 => "Bi",
        84 => "Po",
        85 => "At",
        86 => "Rn",
        87 => "Fr",
        88 => "Ra",
        89 => "Ac",
        90 => "Th",
        91 => "Pa",
        92 => "U",
        _ => "X", // Represents an unknown element
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn known_round_trip() {
        for z in 1u64..=92 {
            let symbol = atomic_number_to_symbol(z);
            assert_eq!(
                symbol_to_atomic_number(symbol),
                z,
                "round-trip failed for Z={z} (symbol={symbol})"
            );
        }
    }

    #[test]
    fn unknown_symbol_returns_zero() {
        assert_eq!(symbol_to_atomic_number(""), 0);
        assert_eq!(symbol_to_atomic_number("Xx"), 0);
        assert_eq!(symbol_to_atomic_number("h"), 0); // case-sensitive
    }

    #[test]
    fn hydrogen_isotopes_map_to_z_one() {
        // Deuterium and tritium share hydrogen's nucleon charge; the
        // reverse lookup uses canonical "H" so callers that need to
        // round-trip isotope identity store it out of band.
        assert_eq!(symbol_to_atomic_number("D"), 1);
        assert_eq!(symbol_to_atomic_number("T"), 1);
        // Ghost-atom / dummy-site placeholders (anything outside
        // H..U plus D/T) stay sentinel-0 so consumers can detect
        // and route them differently.
        assert_eq!(symbol_to_atomic_number("Gh"), 0);
        assert_eq!(symbol_to_atomic_number("Dum"), 0);
        assert_eq!(symbol_to_atomic_number("M"), 0);
    }

    #[test]
    fn unknown_z_returns_x() {
        assert_eq!(atomic_number_to_symbol(0), "X");
        assert_eq!(atomic_number_to_symbol(93), "X");
        assert_eq!(atomic_number_to_symbol(u64::MAX), "X");
    }
}