cosmolkit-core 0.2.1

Redesigned COSMolKit core with value-style molecule state and explicit topology operation contracts
Documentation
use crate::{AtomId, Bond, BondId};

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AdjacencyError {
    #[error("bond {bond} {endpoint} atom index {atom} is out of range for {atom_count} atoms")]
    BondAtomOutOfRange {
        bond: BondId,
        endpoint: &'static str,
        atom: AtomId,
        atom_count: usize,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NeighborRef {
    pub atom_index: usize,
    pub bond: BondId,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AdjacencyList {
    offsets: Vec<usize>,
    entries: Vec<NeighborRef>,
}

impl AdjacencyList {
    pub fn try_from_topology(atom_count: usize, bonds: &[Bond]) -> Result<Self, AdjacencyError> {
        let mut degrees = vec![0usize; atom_count];
        for bond in bonds {
            let begin = bond.begin();
            let end = bond.end();
            if begin.index() >= atom_count {
                return Err(AdjacencyError::BondAtomOutOfRange {
                    bond: bond.id(),
                    endpoint: "begin",
                    atom: begin,
                    atom_count,
                });
            }
            if end.index() >= atom_count {
                return Err(AdjacencyError::BondAtomOutOfRange {
                    bond: bond.id(),
                    endpoint: "end",
                    atom: end,
                    atom_count,
                });
            }
            degrees[begin.index()] += 1;
            degrees[end.index()] += 1;
        }

        let mut offsets = Vec::with_capacity(atom_count + 1);
        offsets.push(0);
        for degree in degrees {
            offsets.push(offsets.last().copied().expect("offsets is nonempty") + degree);
        }

        let mut entries = vec![
            NeighborRef {
                atom_index: 0,
                bond: BondId::new(0),
            };
            offsets.last().copied().unwrap_or(0)
        ];
        let mut cursor = offsets[..atom_count].to_vec();
        for bond in bonds {
            let begin = bond.begin().index();
            let end = bond.end().index();
            let begin_slot = cursor[begin];
            entries[begin_slot] = NeighborRef {
                atom_index: end,
                bond: bond.id(),
            };
            cursor[begin] += 1;

            let end_slot = cursor[end];
            entries[end_slot] = NeighborRef {
                atom_index: begin,
                bond: bond.id(),
            };
            cursor[end] += 1;
        }

        Ok(Self { offsets, entries })
    }

    #[must_use]
    pub fn from_topology(atom_count: usize, bonds: &[Bond]) -> Self {
        Self::try_from_topology(atom_count, bonds)
            .expect("topology must be valid before building adjacency")
    }

    #[must_use]
    pub fn neighbors_of(&self, atom_index: usize) -> &[NeighborRef] {
        let Some(window) = self.offsets.get(atom_index..atom_index.saturating_add(2)) else {
            return &[];
        };
        if window.len() != 2 {
            return &[];
        }
        &self.entries[window[0]..window[1]]
    }
}

#[cfg(test)]
mod tests {
    use crate::{AdjacencyList, AtomSpec, BondOrder, BondSpec, Element, MoleculeBuilder};

    #[test]
    fn adjacency_list_stores_incident_bonds_in_csr_layout() {
        let mut builder = MoleculeBuilder::new();
        let left = builder.add_atom(AtomSpec::new(Element::C));
        let middle = builder.add_atom(AtomSpec::new(Element::C));
        let right = builder.add_atom(AtomSpec::new(Element::O));
        builder
            .add_bond(BondSpec::new(left, middle, BondOrder::Single))
            .unwrap();
        builder
            .add_bond(BondSpec::new(middle, right, BondOrder::Double))
            .unwrap();
        let molecule = builder.build().unwrap();

        let adjacency =
            AdjacencyList::try_from_topology(molecule.num_atoms(), molecule.bonds()).unwrap();

        assert_eq!(adjacency.neighbors_of(left.index()).len(), 1);
        assert_eq!(adjacency.neighbors_of(middle.index()).len(), 2);
        assert_eq!(adjacency.neighbors_of(right.index()).len(), 1);
        assert!(adjacency.neighbors_of(usize::MAX).is_empty());
    }
}