use crate::bond::{Bond, BondOrder};
type BondIdx = u32;
#[inline]
fn to_idx(i: usize) -> BondIdx {
BondIdx::try_from(i).expect("index exceeds the bond-storage index width (u32)")
}
#[derive(Debug, Default)]
pub struct BondStorage {
pairs: Vec<[BondIdx; 2]>,
orders: Option<Vec<BondOrder>>,
adj: Option<BondAdjacency>,
}
impl Clone for BondStorage {
fn clone(&self) -> Self {
Self { pairs: self.pairs.clone(), orders: self.orders.clone(), adj: None }
}
}
#[inline]
fn push_order(col: &mut Option<Vec<BondOrder>>, val: BondOrder, new_len: usize) {
match (col.as_mut(), val) {
(Some(v), x) => v.push(x),
(None, BondOrder::Unspecified) => {}
(None, x) => {
let mut v = vec![BondOrder::Unspecified; new_len - 1];
v.push(x);
*col = Some(v);
}
}
}
impl BondStorage {
pub fn len(&self) -> usize {
self.pairs.len()
}
pub fn is_empty(&self) -> bool {
self.pairs.is_empty()
}
pub fn has_orders(&self) -> bool {
self.orders.is_some()
}
pub fn push(&mut self, b: &Bond) {
self.pairs.push([to_idx(b.i1), to_idx(b.i2)]);
let n = self.pairs.len();
push_order(&mut self.orders, b.order, n);
self.adj = None;
debug_assert!(self.invariant_holds());
}
pub fn reserve(&mut self, n: usize) {
self.pairs.reserve(n);
if let Some(c) = self.orders.as_mut() {
c.reserve(n);
}
}
#[inline]
pub unsafe fn get_unchecked(&self, i: usize) -> BondRef<'_> {
BondRef { st: self, idx: i }
}
#[inline]
pub fn get(&self, i: usize) -> Option<BondRef<'_>> {
(i < self.len()).then_some(BondRef { st: self, idx: i })
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = BondRef<'_>> {
(0..self.len()).map(move |i| unsafe { self.get_unchecked(i) })
}
pub fn set_order(&mut self, i: usize, o: BondOrder) {
let n = self.pairs.len();
assert!(i < n, "bond index {i} out of bounds (n_bonds = {n})");
match (self.orders.as_mut(), o) {
(Some(v), x) => v[i] = x,
(None, BondOrder::Unspecified) => {}
(None, x) => {
let mut v = vec![BondOrder::Unspecified; n];
v[i] = x;
self.orders = Some(v);
}
}
debug_assert!(self.invariant_holds());
}
pub fn remove_by_index(&mut self, removed_atoms: &[usize], n_atoms: usize) {
let mut keep = vec![true; n_atoms];
for &i in removed_atoms {
if i < n_atoms {
keep[i] = false;
}
}
let mut remap = vec![0 as BondIdx; n_atoms];
let mut next: BondIdx = 0;
for i in 0..n_atoms {
if keep[i] {
remap[i] = next;
next += 1;
}
}
let mut orders = self.orders.take();
let mut w = 0usize;
for r in 0..self.pairs.len() {
let [a, b] = self.pairs[r];
let (a, b) = (a as usize, b as usize);
if a >= n_atoms || b >= n_atoms || !keep[a] || !keep[b] {
continue;
}
self.pairs[w] = [remap[a], remap[b]];
if let Some(v) = orders.as_mut() {
v[w] = v[r];
}
w += 1;
}
self.pairs.truncate(w);
if let Some(v) = orders.as_mut() {
v.truncate(w);
}
self.orders = orders;
self.adj = None;
debug_assert!(self.invariant_holds());
}
pub fn get_adjacency(&self) -> Option<&BondAdjacency> {
self.adj.as_ref()
}
pub fn ensure_adjacency(&mut self, n_atoms: usize) -> &BondAdjacency {
let stale = self.adj.as_ref().is_none_or(|a| a.n_atoms() != n_atoms);
if stale {
self.adj = Some(BondAdjacency::build(n_atoms, self.iter_pairs()));
}
self.adj.as_ref().unwrap()
}
pub fn invalidate_adjacency(&mut self) {
self.adj = None;
}
pub fn iter_pairs(&self) -> impl ExactSizeIterator<Item = [usize; 2]> + Clone + '_ {
self.pairs.iter().map(|&[a, b]| [a as usize, b as usize])
}
fn invariant_holds(&self) -> bool {
self.orders.as_ref().is_none_or(|c| c.len() == self.pairs.len())
}
}
impl Extend<Bond> for BondStorage {
fn extend<I: IntoIterator<Item = Bond>>(&mut self, iter: I) {
let it = iter.into_iter();
self.reserve(it.size_hint().0);
for b in it {
self.push(&b);
}
}
}
impl FromIterator<Bond> for BondStorage {
fn from_iter<I: IntoIterator<Item = Bond>>(iter: I) -> Self {
let mut s = BondStorage::default();
s.extend(iter);
s
}
}
#[derive(Debug, Clone, Copy)]
pub struct BondRef<'a> {
st: &'a BondStorage,
idx: usize,
}
impl BondRef<'_> {
#[inline]
pub fn i1(&self) -> usize {
unsafe { self.st.pairs.get_unchecked(self.idx)[0] as usize }
}
#[inline]
pub fn i2(&self) -> usize {
unsafe { self.st.pairs.get_unchecked(self.idx)[1] as usize }
}
#[inline]
pub fn pair(&self) -> [usize; 2] {
let p = unsafe { self.st.pairs.get_unchecked(self.idx) };
[p[0] as usize, p[1] as usize]
}
#[inline]
pub fn order(&self) -> BondOrder {
match self.st.orders.as_ref() {
Some(v) => unsafe { *v.get_unchecked(self.idx) },
None => BondOrder::Unspecified,
}
}
#[inline]
pub fn contains(&self, idx: usize) -> bool {
self.i1() == idx || self.i2() == idx
}
}
impl From<&BondRef<'_>> for Bond {
fn from(b: &BondRef<'_>) -> Self {
Bond { i1: b.i1(), i2: b.i2(), order: b.order() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BondNeighbor {
atom: BondIdx,
bond: BondIdx,
}
impl BondNeighbor {
#[inline]
pub fn atom(&self) -> usize {
self.atom as usize
}
#[inline]
pub fn bond(&self) -> usize {
self.bond as usize
}
}
#[derive(Debug, Clone)]
pub struct BondAdjacency {
offsets: Vec<BondIdx>,
entries: Vec<BondNeighbor>,
n_bonds: usize,
}
impl BondAdjacency {
pub fn build(n_atoms: usize, pairs: impl Iterator<Item = [usize; 2]> + Clone) -> Self {
let mut offsets = vec![0 as BondIdx; n_atoms + 1];
let mut n_bonds = 0usize;
for [a, b] in pairs.clone() {
n_bonds += 1;
if a >= n_atoms || b >= n_atoms || a == b {
continue;
}
offsets[a + 1] += 1;
offsets[b + 1] += 1;
}
for i in 0..n_atoms {
offsets[i + 1] += offsets[i];
}
let total = offsets[n_atoms] as usize;
let mut entries = vec![BondNeighbor { atom: 0, bond: 0 }; total];
let mut fill = vec![0 as BondIdx; n_atoms];
for (bi, [a, b]) in pairs.enumerate() {
if a >= n_atoms || b >= n_atoms || a == b {
continue;
}
let bond = to_idx(bi);
let pa = (offsets[a] + fill[a]) as usize;
entries[pa] = BondNeighbor { atom: to_idx(b), bond };
fill[a] += 1;
let pb = (offsets[b] + fill[b]) as usize;
entries[pb] = BondNeighbor { atom: to_idx(a), bond };
fill[b] += 1;
}
Self { offsets, entries, n_bonds }
}
pub fn n_atoms(&self) -> usize {
self.offsets.len() - 1
}
pub fn n_bonds(&self) -> usize {
self.n_bonds
}
pub fn bond_endpoints(&self) -> Vec<[usize; 2]> {
let mut ends = vec![[usize::MAX; 2]; self.n_bonds];
for a in 0..self.n_atoms() {
for nb in self.neighbors(a) {
let e = &mut ends[nb.bond()];
if e[0] == usize::MAX {
e[0] = a;
} else {
e[1] = a;
}
}
}
ends
}
#[inline]
pub fn neighbors(&self, atom: usize) -> &[BondNeighbor] {
let s = self.offsets[atom] as usize;
let e = self.offsets[atom + 1] as usize;
&self.entries[s..e]
}
}
#[cfg(test)]
mod tests {
use super::*;
const _: () = assert!(size_of::<[BondIdx; 2]>() == 8);
const _: () = assert!(size_of::<BondNeighbor>() == 8);
fn storage(bonds: &[(usize, usize, BondOrder)]) -> BondStorage {
bonds.iter().map(|&(i, j, o)| Bond::with_order(i, j, o)).collect()
}
fn nbr_atoms(adj: &BondAdjacency, a: usize) -> Vec<usize> {
adj.neighbors(a).iter().map(BondNeighbor::atom).collect()
}
fn nbr_bonds(adj: &BondAdjacency, a: usize) -> Vec<usize> {
adj.neighbors(a).iter().map(BondNeighbor::bond).collect()
}
#[test]
fn order_column_stays_absent_without_concrete_orders() {
let s = storage(&[(0, 1, BondOrder::Unspecified), (1, 2, BondOrder::Unspecified)]);
assert!(!s.has_orders(), "connectivity-only source must allocate no order column");
assert_eq!(s.get(0).unwrap().order(), BondOrder::Unspecified);
}
#[test]
fn order_column_materializes_and_backfills() {
let s = storage(&[
(0, 1, BondOrder::Unspecified),
(1, 2, BondOrder::Unspecified),
(2, 3, BondOrder::Double),
]);
assert!(s.has_orders());
assert_eq!(s.get(0).unwrap().order(), BondOrder::Unspecified);
assert_eq!(s.get(2).unwrap().order(), BondOrder::Double);
assert_eq!(s.len(), 3);
}
#[test]
fn set_order_materializes_a_missing_column() {
let mut s = storage(&[(0, 1, BondOrder::Unspecified), (1, 2, BondOrder::Unspecified)]);
assert!(!s.has_orders());
s.set_order(1, BondOrder::Aromatic);
assert!(s.has_orders());
assert_eq!(s.get(0).unwrap().order(), BondOrder::Unspecified);
assert_eq!(s.get(1).unwrap().order(), BondOrder::Aromatic);
}
#[test]
fn proxy_reads_endpoints_and_pair() {
let s = storage(&[(3, 7, BondOrder::Triple)]);
let b = s.get(0).unwrap();
assert_eq!((b.i1(), b.i2()), (3, 7));
assert_eq!(b.pair(), [3, 7]);
assert!(b.contains(3) && b.contains(7) && !b.contains(5));
assert_eq!(Bond::from(&b), Bond::with_order(3, 7, BondOrder::Triple));
}
#[test]
fn neighbor_order_matches_bond_insertion_order() {
let s = storage(&[
(0, 3, BondOrder::Single),
(0, 1, BondOrder::Single),
(0, 2, BondOrder::Single),
]);
let adj = BondAdjacency::build(4, s.iter_pairs());
assert_eq!(nbr_atoms(&adj, 0), vec![3, 1, 2], "neighbors follow bond order, not atom order");
assert_eq!(nbr_bonds(&adj, 0), vec![0, 1, 2], "and carry ascending bond indices");
assert_eq!(nbr_atoms(&adj, 3), vec![0]);
assert_eq!(nbr_bonds(&adj, 3), vec![0]);
}
#[test]
fn build_skips_self_bonds_and_out_of_range() {
let s = storage(&[
(0, 1, BondOrder::Single),
(2, 2, BondOrder::Single),
(0, 9, BondOrder::Single),
]);
let adj = BondAdjacency::build(3, s.iter_pairs());
assert_eq!(nbr_atoms(&adj, 0), vec![1], "only the valid bond survives");
assert_eq!(nbr_atoms(&adj, 2), Vec::<usize>::new());
assert_eq!(nbr_bonds(&adj, 1), vec![0]);
}
#[test]
fn adjacency_is_empty_for_no_atoms() {
let s = storage(&[]);
let adj = BondAdjacency::build(0, s.iter_pairs());
assert_eq!(adj.n_atoms(), 0);
}
#[test]
fn set_order_preserves_adjacency() {
let mut s = storage(&[(0, 1, BondOrder::Single), (1, 2, BondOrder::Single)]);
s.ensure_adjacency(3);
s.set_order(0, BondOrder::Aromatic);
assert!(
s.get_adjacency().is_some(),
"an order write must not cost the adjacency — this is why the columns are split"
);
}
#[test]
fn push_invalidates_adjacency() {
let mut s = storage(&[(0, 1, BondOrder::Single)]);
s.ensure_adjacency(3);
s.push(&Bond::new(1, 2));
assert!(s.get_adjacency().is_none());
}
#[test]
fn remove_by_index_invalidates_adjacency() {
let mut s = storage(&[(0, 1, BondOrder::Single), (1, 2, BondOrder::Single)]);
s.ensure_adjacency(3);
s.remove_by_index(&[2], 3);
assert!(s.get_adjacency().is_none());
}
#[test]
fn invalidate_adjacency_clears_the_cache() {
let mut s = storage(&[(0, 1, BondOrder::Single)]);
s.ensure_adjacency(2);
s.invalidate_adjacency();
assert!(s.get_adjacency().is_none());
}
#[test]
fn clone_drops_the_cache_but_keeps_the_columns() {
let mut s = storage(&[(0, 1, BondOrder::Double)]);
s.ensure_adjacency(2);
let c = s.clone();
assert!(c.get_adjacency().is_none(), "a derived cache is never worth copying");
assert_eq!(c.len(), 1);
assert_eq!(c.get(0).unwrap().order(), BondOrder::Double);
}
#[test]
fn ensure_adjacency_rebuilds_when_the_atom_count_changed() {
let mut s = storage(&[(0, 1, BondOrder::Single)]);
assert_eq!(s.ensure_adjacency(2).n_atoms(), 2);
assert_eq!(s.ensure_adjacency(5).n_atoms(), 5, "a different atom count is stale");
}
#[test]
fn remove_by_index_drops_incident_bonds_and_renumbers() {
let mut s = storage(&[
(0, 1, BondOrder::Single),
(1, 2, BondOrder::Double),
(2, 3, BondOrder::Triple),
(3, 4, BondOrder::Aromatic),
]);
s.remove_by_index(&[2], 5);
assert_eq!(s.len(), 2);
assert_eq!(s.get(0).unwrap().pair(), [0, 1]);
assert_eq!(s.get(0).unwrap().order(), BondOrder::Single);
assert_eq!(s.get(1).unwrap().pair(), [2, 3]);
assert_eq!(s.get(1).unwrap().order(), BondOrder::Aromatic);
}
#[test]
fn remove_by_index_handles_removing_everything() {
let mut s = storage(&[(0, 1, BondOrder::Single)]);
s.remove_by_index(&[0, 1], 2);
assert_eq!(s.len(), 0);
assert!(s.is_empty());
}
}