use crate::batch::MolBatch;
use crate::builder::{AtomData, BondData, MolBuilder};
#[derive(Debug, Clone, Copy)]
pub struct MolView<'a> {
batch: &'a MolBatch,
idx: u32,
atom_base: u32,
bond_base: u32,
n_atoms: u32,
n_bonds: u32,
}
impl<'a> MolView<'a> {
pub(crate) fn new(batch: &'a MolBatch, idx: u32) -> Self {
let i = idx as usize;
let atom_base = batch.mol_atom_offset[i];
let bond_base = batch.mol_bond_offset[i];
Self {
batch,
idx,
atom_base,
bond_base,
n_atoms: batch.mol_atom_offset[i + 1] - atom_base,
n_bonds: batch.mol_bond_offset[i + 1] - bond_base,
}
}
#[must_use]
pub fn index(&self) -> u32 {
self.idx
}
#[must_use]
pub fn batch(&self) -> &'a MolBatch {
self.batch
}
#[must_use]
pub fn num_atoms(&self) -> usize {
self.n_atoms as usize
}
#[must_use]
pub fn num_bonds(&self) -> usize {
self.n_bonds as usize
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.n_atoms == 0
}
#[must_use]
pub fn name(&self) -> Option<&'a str> {
self.batch.names[self.idx as usize].as_deref()
}
#[must_use]
pub fn global_atom(&self, local: u32) -> u32 {
self.atom_base + local
}
#[must_use]
pub fn local_atom(&self, global: u32) -> Option<u32> {
global
.checked_sub(self.atom_base)
.filter(|&l| l < self.n_atoms)
}
#[must_use]
pub fn atom(&self, local: u32) -> Option<AtomData> {
if local >= self.n_atoms {
return None;
}
let g = (self.atom_base + local) as usize;
let b = self.batch;
Some(AtomData {
atomic_num: b.atomic_num[g],
formal_charge: b.formal_charge[g],
isotope: b.isotope[g],
num_explicit_hs: b.num_explicit_hs[g],
num_implicit_hs: b.num_implicit_hs[g],
num_radical_electrons: b.num_radical_electrons[g],
atom_map: b.atom_map[g],
chiral_tag: b.chiral_tag[g],
stereo_perm: b.stereo_perm[g],
hybridization: b.hybridization[g],
flags: b.atom_flags[g],
})
}
#[must_use]
pub fn bond(&self, local: u32) -> Option<BondData> {
if local >= self.n_bonds {
return None;
}
let g = (self.bond_base + local) as usize;
let b = self.batch;
Some(BondData {
begin: b.bond_begin[g] - self.atom_base,
end: b.bond_end[g] - self.atom_base,
order: b.bond_order[g],
direction: b.bond_direction[g],
stereo: b.bond_stereo[g],
stereo_atoms: b.bond_stereo_atoms[g].map(|a| {
if a == BondData::NO_STEREO_ATOM {
a
} else {
a - self.atom_base
}
}),
flags: b.bond_flags[g],
})
}
#[must_use]
pub fn degree(&self, local: u32) -> usize {
if local >= self.n_atoms {
return 0;
}
let g = (self.atom_base + local) as usize;
(self.batch.nbr_offset[g + 1] - self.batch.nbr_offset[g]) as usize
}
pub fn neighbors(&self, local: u32) -> impl Iterator<Item = (u32, u32)> + 'a {
let (start, end) = if local < self.n_atoms {
let g = (self.atom_base + local) as usize;
(self.batch.nbr_offset[g], self.batch.nbr_offset[g + 1])
} else {
(0, 0)
};
let b = self.batch;
let (abase, bbase) = (self.atom_base, self.bond_base);
(start..end).map(move |k| {
let k = k as usize;
(b.nbr_atom[k] - abase, b.nbr_bond[k] - bbase)
})
}
pub fn atoms(&self) -> impl Iterator<Item = (u32, AtomData)> + '_ {
(0..self.n_atoms).map(move |i| (i, self.atom(i).expect("下标由 n_atoms 生成")))
}
pub fn bonds(&self) -> impl Iterator<Item = (u32, BondData)> + '_ {
(0..self.n_bonds).map(move |i| (i, self.bond(i).expect("下标由 n_bonds 生成")))
}
#[must_use]
pub fn to_builder(&self) -> MolBuilder {
let mut m = MolBuilder::with_capacity(self.num_atoms(), self.num_bonds());
for (_, a) in self.atoms() {
m.add_atom_data(a);
}
for (_, bd) in self.bonds() {
m.add_bond_data(bd).expect("视图中的键端点必然合法");
}
if let Some(n) = self.name() {
m.set_name(n);
}
m
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::batch::MolBatchBuilder;
use crate::types::{
AtomFlags, BondDirection, BondFlags, BondOrder, BondStereo, ChiralTag, Hybridization,
};
fn ethanol() -> MolBuilder {
let mut m = MolBuilder::new();
let c0 = m.add_atom(6);
let c1 = m.add_atom(6);
let o = m.add_atom(8);
m.add_bond(c0, c1, BondOrder::Single).unwrap();
m.add_bond(c1, o, BondOrder::Single).unwrap();
m.set_name("ethanol");
m
}
fn acetic_acid() -> MolBuilder {
let mut m = MolBuilder::new();
let c0 = m.add_atom(6);
let c1 = m.add_atom(6);
let o1 = m.add_atom(8);
let o2 = m.add_atom(8);
m.add_bond(c0, c1, BondOrder::Single).unwrap();
m.add_bond(c1, o1, BondOrder::Double).unwrap();
m.add_bond(c1, o2, BondOrder::Single).unwrap();
m
}
fn batch_of(mols: &[MolBuilder]) -> MolBatch {
let mut bb = MolBatchBuilder::new();
for m in mols {
bb.push(m).unwrap();
}
bb.finish()
}
#[test]
fn local_indices_are_zero_based_per_molecule() {
let b = batch_of(&[ethanol(), acetic_acid()]);
let m1 = b.mol(1).unwrap();
assert_eq!(m1.num_atoms(), 4);
assert_eq!(m1.num_bonds(), 3);
assert_eq!(m1.global_atom(0), 3);
assert_eq!(m1.local_atom(3), Some(0));
assert_eq!(m1.local_atom(2), None, "全局原子 2 属于第一个分子");
assert_eq!(m1.local_atom(7), None, "全局原子 7 已越过本分子");
let bond = m1.bond(1).unwrap();
assert_eq!((bond.begin, bond.end), (1, 2), "键端点应为局部下标");
assert_eq!(bond.order, BondOrder::Double);
}
#[test]
fn neighbors_are_local() {
let b = batch_of(&[ethanol(), acetic_acid()]);
let m1 = b.mol(1).unwrap();
let mut nbrs: Vec<u32> = m1.neighbors(1).map(|(a, _)| a).collect();
nbrs.sort_unstable();
assert_eq!(nbrs, vec![0, 2, 3]);
assert_eq!(m1.degree(1), 3);
assert_eq!(m1.degree(0), 1);
}
#[test]
fn neighbor_bond_indices_are_local() {
let b = batch_of(&[ethanol(), acetic_acid()]);
let m1 = b.mol(1).unwrap();
for (_, bond_local) in m1.neighbors(1) {
assert!(
(bond_local as usize) < m1.num_bonds(),
"键下标 {bond_local} 未换算为局部"
);
}
}
#[test]
fn out_of_range_access_is_none() {
let b = batch_of(&[ethanol()]);
let m = b.mol(0).unwrap();
assert!(m.atom(3).is_none());
assert!(m.bond(2).is_none());
assert_eq!(m.degree(99), 0);
assert_eq!(m.neighbors(99).count(), 0);
}
fn every_field_set() -> MolBuilder {
let mut m = MolBuilder::new();
m.add_atom_data(AtomData {
atomic_num: 7,
formal_charge: -1,
isotope: 15,
num_explicit_hs: 2,
num_implicit_hs: 3,
num_radical_electrons: 1,
atom_map: 7,
chiral_tag: ChiralTag::Cw,
stereo_perm: 0,
hybridization: Hybridization::Sp3d2,
flags: AtomFlags::AROMATIC | AtomFlags::NO_IMPLICIT | AtomFlags::IN_RING,
});
m.add_atom_data(AtomData {
atomic_num: 16,
formal_charge: 2,
isotope: 34,
num_explicit_hs: 1,
num_implicit_hs: 0,
num_radical_electrons: 2,
atom_map: 3,
chiral_tag: ChiralTag::Octahedral,
stereo_perm: 25,
hybridization: Hybridization::Sp2d,
flags: AtomFlags::CONJUGATED,
});
m.add_bond_data(BondData {
begin: 0,
end: 1,
order: BondOrder::Dative,
direction: BondDirection::DownRight,
stereo: BondStereo::Trans,
stereo_atoms: [1, 0],
flags: BondFlags::AROMATIC | BondFlags::IN_RING | BondFlags::CONJUGATED,
})
.expect("端点合法");
m.set_name("每字段非默认");
m
}
#[test]
fn roundtrip_through_batch_is_identity() {
let m = every_field_set();
let b = batch_of(&[ethanol(), m.clone(), ethanol()]);
let back = b.mol(1).unwrap().to_builder();
assert_eq!(back.atoms(), m.atoms(), "原子列往返不一致");
assert_eq!(back.bonds(), m.bonds(), "键列往返不一致");
assert_eq!(back.name(), m.name());
}
#[test]
fn every_field_is_actually_non_default() {
let m = every_field_set();
let default_atom = AtomData::default();
for (i, a) in m.atoms().iter().enumerate() {
assert_ne!(a.atomic_num, default_atom.atomic_num, "原子{i}.元素");
assert_ne!(a.formal_charge, default_atom.formal_charge, "原子{i}.电荷");
assert_ne!(a.isotope, default_atom.isotope, "原子{i}.同位素");
assert_ne!(a.atom_map, default_atom.atom_map, "原子{i}.映射号");
assert_ne!(a.chiral_tag, default_atom.chiral_tag, "原子{i}.手性");
assert_ne!(a.hybridization, default_atom.hybridization, "原子{i}.杂化");
assert_ne!(a.flags, AtomFlags::NONE, "原子{i}.标志");
}
assert!(m.atoms().iter().any(|a| a.num_explicit_hs != 0));
assert!(m.atoms().iter().any(|a| a.num_implicit_hs != 0));
assert!(m.atoms().iter().any(|a| a.num_radical_electrons != 0));
assert!(m.atoms().iter().any(|a| a.stereo_perm != 0));
assert!(m.atoms().iter().any(|a| a.chiral_tag.is_tetrahedral()));
assert!(m.atoms().iter().any(|a| !a.chiral_tag.is_tetrahedral()));
for (i, b) in m.bonds().iter().enumerate() {
assert_ne!(b.order, BondOrder::Unspecified, "键{i}.键级");
assert_ne!(b.direction, BondDirection::None, "键{i}.方向");
assert_ne!(b.stereo, BondStereo::None, "键{i}.立体");
assert_eq!(b.stereo_atoms, [1, 0], "键{i}.立体参照原子");
assert_ne!(b.flags, BondFlags::NONE, "键{i}.标志");
}
}
#[test]
fn iter_visits_every_molecule() {
let b = batch_of(&[ethanol(), acetic_acid(), ethanol()]);
let sizes: Vec<usize> = b.iter().map(|m| m.num_atoms()).collect();
assert_eq!(sizes, vec![3, 4, 3]);
assert_eq!(b.iter().count(), 3);
}
#[test]
fn try_mol_reports_out_of_range() {
let b = batch_of(&[ethanol()]);
assert!(b.try_mol(0).is_ok());
let e = b.try_mol(5).unwrap_err();
assert!(matches!(
e,
crate::error::Error::MolIndexOutOfRange {
index: 5,
num_mols: 1
}
));
}
}