use std::collections::HashMap;
use std::ops::{Index, IndexMut};
use slotmap::{Key, KeyData, SecondaryMap, SlotMap, new_key_type};
use smallvec::SmallVec;
use crate::error::MolRsError;
use crate::store::block::Block;
use crate::store::frame::Frame;
use crate::store::keys;
use crate::system::entity_table::{Cell, EntityTable};
use crate::types::{F, I, U};
#[derive(Debug, Clone, PartialEq)]
pub enum PropValue {
F64(f64),
Str(String),
Int(I),
Bool(bool),
}
impl PropValue {
pub fn as_f64(&self) -> Option<f64> {
match self {
PropValue::F64(v) => Some(*v),
PropValue::Int(v) => Some(*v as f64),
PropValue::Str(_) | PropValue::Bool(_) => None,
}
}
}
impl From<f64> for PropValue {
fn from(v: f64) -> Self {
PropValue::F64(v)
}
}
impl From<I> for PropValue {
fn from(v: I) -> Self {
PropValue::Int(v)
}
}
impl From<&str> for PropValue {
fn from(v: &str) -> Self {
PropValue::Str(v.to_owned())
}
}
impl From<bool> for PropValue {
fn from(v: bool) -> Self {
PropValue::Bool(v)
}
}
impl From<String> for PropValue {
fn from(v: String) -> Self {
PropValue::Str(v)
}
}
fn coerce_canonical(key: &str, pv: PropValue) -> PropValue {
match (keys::canonical_dtype(key), pv) {
(Some(crate::store::block::DType::Float), PropValue::Int(v)) => PropValue::F64(v as f64),
(_, pv) => pv,
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Atom {
props: HashMap<String, PropValue>,
}
impl Atom {
pub fn new() -> Self {
Self::default()
}
pub fn xyz(symbol: &str, x: f64, y: f64, z: f64) -> Self {
let mut a = Self::new();
a.set(keys::ELEMENT, symbol);
a.set(keys::X, x);
a.set(keys::Y, y);
a.set(keys::Z, z);
a
}
pub fn set(&mut self, key: &str, val: impl Into<PropValue>) {
self.props.insert(key.to_owned(), val.into());
}
pub fn get(&self, key: &str) -> Option<&PropValue> {
self.props.get(key)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut PropValue> {
self.props.get_mut(key)
}
pub fn get_f64(&self, key: &str) -> Option<f64> {
match self.props.get(key)? {
PropValue::F64(v) => Some(*v),
_ => None,
}
}
pub fn get_str(&self, key: &str) -> Option<&str> {
match self.props.get(key)? {
PropValue::Str(s) => Some(s.as_str()),
_ => None,
}
}
pub fn get_int(&self, key: &str) -> Option<I> {
match self.props.get(key)? {
PropValue::Int(v) => Some(*v),
_ => None,
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.props.contains_key(key)
}
pub fn remove(&mut self, key: &str) -> Option<PropValue> {
self.props.remove(key)
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.props.keys().map(|k| k.as_str())
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &PropValue)> {
self.props.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn len(&self) -> usize {
self.props.len()
}
pub fn is_empty(&self) -> bool {
self.props.is_empty()
}
}
impl Index<&str> for Atom {
type Output = PropValue;
fn index(&self, key: &str) -> &Self::Output {
self.props
.get(key)
.unwrap_or_else(|| panic!("Atom does not contain key '{}'", key))
}
}
impl IndexMut<&str> for Atom {
fn index_mut(&mut self, key: &str) -> &mut Self::Output {
self.props
.get_mut(key)
.unwrap_or_else(|| panic!("Atom does not contain key '{}'", key))
}
}
pub type Bead = Atom;
new_key_type! {
pub struct NodeId;
pub struct RelationId;
pub struct GroupId;
}
pub fn node_to_u64(id: NodeId) -> u64 {
id.data().as_ffi()
}
pub fn node_from_u64(h: u64) -> NodeId {
NodeId::from(KeyData::from_ffi(h))
}
pub fn relation_to_u64(id: RelationId) -> u64 {
id.data().as_ffi()
}
pub fn relation_from_u64(h: u64) -> RelationId {
RelationId::from(KeyData::from_ffi(h))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KindId(pub u16);
#[derive(Debug, Clone)]
pub struct Relation {
pub nodes: SmallVec<[NodeId; 4]>,
pub props: HashMap<String, PropValue>,
}
#[derive(Debug, Clone)]
struct RelationKind {
props: EntityTable<RelationId>,
endpoints: SecondaryMap<RelationId, SmallVec<[NodeId; 4]>>,
}
impl RelationKind {
fn new() -> Self {
Self {
props: EntityTable::new(),
endpoints: SecondaryMap::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Group {
pub members: Vec<NodeId>,
pub parent: Option<GroupId>,
pub props: HashMap<String, PropValue>,
}
#[derive(Debug, Clone)]
pub struct MolGraph {
nodes: EntityTable<NodeId>,
kinds: Vec<RelationKind>,
kind_arity: Vec<usize>,
kind_name: Vec<String>,
name_to_kind: HashMap<String, KindId>,
adjacency: HashMap<NodeId, Vec<(KindId, RelationId)>>,
#[allow(dead_code)]
groups: SlotMap<GroupId, Group>,
}
impl Default for MolGraph {
fn default() -> Self {
Self::new()
}
}
impl MolGraph {
pub fn new() -> Self {
Self {
nodes: EntityTable::new(),
kinds: Vec::new(),
kind_arity: Vec::new(),
kind_name: Vec::new(),
name_to_kind: HashMap::new(),
adjacency: HashMap::new(),
groups: SlotMap::with_key(),
}
}
pub fn register_kind(&mut self, name: &str, arity: usize) -> KindId {
if let Some(&kid) = self.name_to_kind.get(name) {
assert_eq!(
self.kind_arity[kid.0 as usize], arity,
"kind '{name}' already registered with a different arity"
);
return kid;
}
let kid = KindId(self.kinds.len() as u16);
self.kinds.push(RelationKind::new());
self.kind_arity.push(arity);
self.kind_name.push(name.to_owned());
self.name_to_kind.insert(name.to_owned(), kid);
kid
}
pub fn kind_id(&self, name: &str) -> Option<KindId> {
self.name_to_kind.get(name).copied()
}
pub fn kind_name(&self, kind: KindId) -> &str {
&self.kind_name[kind.0 as usize]
}
pub fn arity(&self, kind: KindId) -> usize {
self.kind_arity[kind.0 as usize]
}
pub fn kind_ids(&self) -> impl Iterator<Item = KindId> + '_ {
(0..self.kinds.len() as u16).map(KindId)
}
pub fn add_node(&mut self) -> NodeId {
let id = self.nodes.spawn();
self.adjacency.insert(id, Vec::new());
id
}
pub fn add_node_with(&mut self, payload: Atom) -> NodeId {
let id = self.add_node();
self.write_atom(id, &payload);
id
}
pub fn remove_node(&mut self, id: NodeId) -> Result<Atom, MolRsError> {
if !self.nodes.contains(id) {
return Err(MolRsError::not_found("node", format!("NodeId {:?}", id)));
}
let payload = self.read_atom(id);
for kid in 0..self.kinds.len() {
let doomed: Vec<RelationId> = self.kinds[kid]
.endpoints
.iter()
.filter(|(_, eps)| eps.contains(&id))
.map(|(rid, _)| rid)
.collect();
for rid in doomed {
self.detach_relation_from_adjacency(KindId(kid as u16), rid, Some(id));
let k = &mut self.kinds[kid];
k.props.despawn(rid);
k.endpoints.remove(rid);
}
}
self.adjacency.remove(&id);
self.nodes.despawn(id);
Ok(payload)
}
pub fn get_node(&self, id: NodeId) -> Result<Atom, MolRsError> {
if !self.nodes.contains(id) {
return Err(MolRsError::not_found("node", format!("NodeId {:?}", id)));
}
Ok(self.read_atom(id))
}
pub fn set_node(
&mut self,
id: NodeId,
key: &str,
val: impl Into<PropValue>,
) -> Result<(), MolRsError> {
match coerce_canonical(key, val.into()) {
PropValue::F64(v) => self.nodes.set_f64(id, key, v),
PropValue::Int(v) => self.nodes.set_i32(id, key, v),
PropValue::Str(s) => self.nodes.set_str(id, key, &s),
PropValue::Bool(v) => self.nodes.set_bool(id, key, v),
}
}
pub fn clear_node(&mut self, id: NodeId, key: &str) -> Result<(), MolRsError> {
self.nodes.clear(id, key)
}
pub fn nodes(&self) -> impl Iterator<Item = (NodeId, Atom)> + '_ {
self.nodes.handles().map(move |id| (id, self.read_atom(id)))
}
pub fn node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.nodes.handles()
}
pub fn node_table(&self) -> &EntityTable<NodeId> {
&self.nodes
}
pub fn node_table_mut(&mut self) -> &mut EntityTable<NodeId> {
&mut self.nodes
}
pub fn n_nodes(&self) -> usize {
self.nodes.len()
}
fn write_atom(&mut self, id: NodeId, atom: &Atom) {
for (key, val) in atom.iter() {
let r = match coerce_canonical(key, val.clone()) {
PropValue::F64(v) => self.nodes.set_f64(id, key, v),
PropValue::Int(v) => self.nodes.set_i32(id, key, v),
PropValue::Str(s) => self.nodes.set_str(id, key, &s),
PropValue::Bool(v) => self.nodes.set_bool(id, key, v),
};
debug_assert!(r.is_ok(), "component type conflict writing '{key}'");
let _ = r;
}
}
fn read_atom(&self, id: NodeId) -> Atom {
let mut atom = Atom::new();
for (key, cell) in self.nodes.row_cells(id) {
match cell {
Cell::F64(v) => atom.set(key, v),
Cell::I32(v) => atom.set(key, v),
Cell::Str(s) => atom.set(key, s),
Cell::Bool(v) => atom.set(key, v),
}
}
atom
}
pub fn neighbors(&self, id: NodeId) -> impl Iterator<Item = NodeId> + '_ {
self.neighbor_relations(id).map(|(_, _, other)| other)
}
pub fn neighbor_relations(
&self,
id: NodeId,
) -> impl Iterator<Item = (KindId, RelationId, NodeId)> + '_ {
self.adjacency
.get(&id)
.into_iter()
.flatten()
.filter_map(move |&(kind, rid)| {
let eps = self.kinds[kind.0 as usize].endpoints.get(rid)?;
let other = if eps[0] == id { eps[1] } else { eps[0] };
Some((kind, rid, other))
})
}
pub fn add_relation(
&mut self,
kind: KindId,
nodes: &[NodeId],
) -> Result<RelationId, MolRsError> {
let kidx = kind.0 as usize;
if kidx >= self.kinds.len() {
return Err(MolRsError::not_found("kind", format!("KindId {:?}", kind)));
}
let arity = self.kind_arity[kidx];
if nodes.len() != arity {
return Err(MolRsError::validation(format!(
"kind '{}' expects arity {}, got {} nodes",
self.kind_name[kidx],
arity,
nodes.len()
)));
}
for &n in nodes {
if !self.nodes.contains(n) {
return Err(MolRsError::not_found("node", format!("NodeId {:?}", n)));
}
}
let k = &mut self.kinds[kidx];
let rid = k.props.spawn();
k.endpoints.insert(rid, SmallVec::from_slice(nodes));
if arity == 2 {
self.adjacency
.entry(nodes[0])
.or_default()
.push((kind, rid));
self.adjacency
.entry(nodes[1])
.or_default()
.push((kind, rid));
}
Ok(rid)
}
pub fn get_relation(&self, kind: KindId, id: RelationId) -> Result<Relation, MolRsError> {
let k = self
.kinds
.get(kind.0 as usize)
.ok_or_else(|| MolRsError::not_found("kind", format!("KindId {:?}", kind)))?;
if !k.props.contains(id) {
return Err(MolRsError::not_found(
"relation",
format!("RelationId {:?}", id),
));
}
Ok(self.read_relation(kind, id))
}
pub fn relation_nodes(
&self,
kind: KindId,
id: RelationId,
) -> Result<SmallVec<[NodeId; 4]>, MolRsError> {
self.kinds
.get(kind.0 as usize)
.and_then(|k| k.endpoints.get(id).cloned())
.ok_or_else(|| MolRsError::not_found("relation", format!("RelationId {:?}", id)))
}
pub fn set_relation_prop(
&mut self,
kind: KindId,
id: RelationId,
key: &str,
val: impl Into<PropValue>,
) -> Result<(), MolRsError> {
let k = self
.kinds
.get_mut(kind.0 as usize)
.ok_or_else(|| MolRsError::not_found("kind", format!("KindId {:?}", kind)))?;
if !k.props.contains(id) {
return Err(MolRsError::not_found(
"relation",
format!("RelationId {:?}", id),
));
}
match coerce_canonical(key, val.into()) {
PropValue::F64(v) => k.props.set_f64(id, key, v),
PropValue::Int(v) => k.props.set_i32(id, key, v),
PropValue::Str(s) => k.props.set_str(id, key, &s),
PropValue::Bool(v) => k.props.set_bool(id, key, v),
}
}
pub fn clear_relation_prop(
&mut self,
kind: KindId,
id: RelationId,
key: &str,
) -> Result<(), MolRsError> {
let k = self
.kinds
.get_mut(kind.0 as usize)
.ok_or_else(|| MolRsError::not_found("kind", format!("KindId {:?}", kind)))?;
k.props.clear(id, key)
}
pub fn remove_relation(
&mut self,
kind: KindId,
id: RelationId,
) -> Result<Relation, MolRsError> {
let exists = self
.kinds
.get(kind.0 as usize)
.is_some_and(|k| k.props.contains(id));
if !exists {
return Err(MolRsError::not_found(
"relation",
format!("RelationId {:?}", id),
));
}
let rel = self.read_relation(kind, id);
self.detach_relation_from_adjacency(kind, id, None);
let k = &mut self.kinds[kind.0 as usize];
k.props.despawn(id);
k.endpoints.remove(id);
Ok(rel)
}
pub fn relations(&self, kind: KindId) -> impl Iterator<Item = (RelationId, Relation)> + '_ {
let ids: Vec<RelationId> = self.kinds[kind.0 as usize].props.handles().collect();
ids.into_iter()
.map(move |rid| (rid, self.read_relation(kind, rid)))
}
pub fn relation_ids(&self, kind: KindId) -> impl Iterator<Item = RelationId> + '_ {
self.kinds[kind.0 as usize].props.handles()
}
pub fn n_relations(&self, kind: KindId) -> usize {
self.kinds[kind.0 as usize].props.len()
}
fn read_relation(&self, kind: KindId, id: RelationId) -> Relation {
let k = &self.kinds[kind.0 as usize];
let nodes = k.endpoints.get(id).cloned().unwrap_or_default();
let mut props = HashMap::new();
for (key, cell) in k.props.row_cells(id) {
let pv = match cell {
Cell::F64(v) => PropValue::F64(v),
Cell::I32(v) => PropValue::Int(v),
Cell::Str(s) => PropValue::Str(s.to_owned()),
Cell::Bool(v) => PropValue::Bool(v),
};
props.insert(key.to_owned(), pv);
}
Relation { nodes, props }
}
fn write_relation_props(
&mut self,
kind: KindId,
id: RelationId,
props: &HashMap<String, PropValue>,
) {
let k = &mut self.kinds[kind.0 as usize];
for (key, val) in props {
let r = match coerce_canonical(key, val.clone()) {
PropValue::F64(v) => k.props.set_f64(id, key, v),
PropValue::Int(v) => k.props.set_i32(id, key, v),
PropValue::Str(s) => k.props.set_str(id, key, &s),
PropValue::Bool(v) => k.props.set_bool(id, key, v),
};
let _ = r;
}
}
fn detach_relation_from_adjacency(
&mut self,
kind: KindId,
id: RelationId,
skip: Option<NodeId>,
) {
if self.kind_arity[kind.0 as usize] != 2 {
return;
}
let endpoints: Option<[NodeId; 2]> = self.kinds[kind.0 as usize]
.endpoints
.get(id)
.map(|eps| [eps[0], eps[1]]);
if let Some(eps) = endpoints {
for ep in eps {
if Some(ep) == skip {
continue;
}
if let Some(adj) = self.adjacency.get_mut(&ep) {
adj.retain(|(_, rid)| *rid != id);
}
}
}
}
pub fn merge(&mut self, other: MolGraph) {
let mut node_map: HashMap<NodeId, NodeId> = HashMap::new();
for old_id in other.nodes.handles() {
let payload = other.read_atom(old_id);
let new_id = self.add_node_with(payload);
node_map.insert(old_id, new_id);
}
for okid in other.kind_ids() {
let oidx = okid.0 as usize;
let name = &other.kind_name[oidx];
let arity = other.kind_arity[oidx];
let self_kind = self.register_kind(name, arity);
let orids: Vec<RelationId> = other.relation_ids(okid).collect();
for orid in orids {
let rel = other.read_relation(okid, orid);
let mapped: SmallVec<[NodeId; 4]> = rel.nodes.iter().map(|n| node_map[n]).collect();
if let Ok(rid) = self.add_relation(self_kind, &mapped) {
self.write_relation_props(self_kind, rid, &rel.props);
}
}
}
}
pub(crate) fn to_frame(&self) -> Frame {
use ndarray::Array1;
let mut frame = Frame::new();
let node_ids: Vec<NodeId> = self.nodes.handles().collect();
let n = node_ids.len();
let id_to_row: HashMap<NodeId, usize> = node_ids
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let mut all_keys: Vec<String> = self.nodes.columns().map(|s| s.to_owned()).collect();
all_keys.sort();
let mut atoms_block = Block::new();
for key in &all_keys {
if let Ok((data, _)) = self.nodes.column_f64(key) {
let _ =
atoms_block.insert(key.as_str(), Array1::from_vec(data.to_vec()).into_dyn());
} else if let Ok((data, _)) = self.nodes.column_i32(key) {
let _ =
atoms_block.insert(key.as_str(), Array1::from_vec(data.to_vec()).into_dyn());
} else if let Ok((data, _)) = self.nodes.column_str(key) {
let _ =
atoms_block.insert(key.as_str(), Array1::from_vec(data.to_vec()).into_dyn());
}
}
if n > 0 {
frame.insert("atoms", atoms_block);
}
for kid in self.kind_ids() {
let kidx = kid.0 as usize;
let k = &self.kinds[kidx];
if k.props.is_empty() {
continue;
}
let arity = self.kind_arity[kidx];
let rids: Vec<RelationId> = k.props.handles().collect();
let mut block = Block::new();
for pos in 0..arity {
let col: Vec<U> = rids
.iter()
.map(|rid| id_to_row[&k.endpoints[*rid][pos]] as U)
.collect();
let _ = block.insert(rel_col_name(pos), Array1::from_vec(col).into_dyn());
}
let mut prop_keys: Vec<String> = k.props.columns().map(|s| s.to_owned()).collect();
prop_keys.sort();
for key in &prop_keys {
if let Ok((data, _)) = k.props.column_f64(key) {
let _ = block.insert(key.as_str(), Array1::from_vec(data.to_vec()).into_dyn());
} else if let Ok((data, _)) = k.props.column_i32(key) {
let _ = block.insert(key.as_str(), Array1::from_vec(data.to_vec()).into_dyn());
} else if let Ok((data, _)) = k.props.column_str(key) {
let _ = block.insert(key.as_str(), Array1::from_vec(data.to_vec()).into_dyn());
}
}
frame.insert(&self.kind_name[kidx], block);
}
frame
}
pub(crate) fn read_frame(&mut self, frame: &Frame) -> Result<(), MolRsError> {
let atoms_block = frame
.get("atoms")
.ok_or_else(|| MolRsError::parse("Frame missing 'atoms' block"))?;
let nrows = atoms_block.nrows().unwrap_or(0);
let col_keys: Vec<String> = atoms_block.keys().map(|k| k.to_owned()).collect();
let mut float_cols: Vec<(&str, &ndarray::ArrayD<F>)> = Vec::new();
let mut i64_cols: Vec<(&str, &ndarray::ArrayD<I>)> = Vec::new();
let mut str_cols: Vec<(&str, &ndarray::ArrayD<String>)> = Vec::new();
for key in &col_keys {
if let Some(arr) = atoms_block.get_float(key) {
float_cols.push((key.as_str(), arr));
} else if let Some(arr) = atoms_block.get_int(key) {
i64_cols.push((key.as_str(), arr));
} else if let Some(arr) = atoms_block.get_string(key) {
str_cols.push((key.as_str(), arr));
}
}
let mut node_ids: Vec<NodeId> = Vec::with_capacity(nrows);
for row in 0..nrows {
let mut node = Atom::new();
for &(key, arr) in &float_cols {
#[allow(clippy::unnecessary_cast)]
node.set(key, arr[[row]] as f64);
}
for &(key, arr) in &i64_cols {
node.set(key, PropValue::Int(arr[[row]]));
}
for &(key, arr) in &str_cols {
node.set(key, PropValue::Str(arr[[row]].clone()));
}
node_ids.push(self.add_node_with(node));
}
let kind_specs: Vec<(KindId, String, usize)> = self
.kind_ids()
.map(|kid| {
let i = kid.0 as usize;
(kid, self.kind_name[i].clone(), self.kind_arity[i])
})
.collect();
for (kid, block_name, arity) in kind_specs {
let Some(block) = frame.get(&block_name) else {
continue;
};
let mut endpoint_cols: Vec<&ndarray::ArrayD<U>> = Vec::with_capacity(arity);
let mut ok = true;
for pos in 0..arity {
match block.get_uint(&rel_col_name(pos)) {
Some(c) => endpoint_cols.push(c),
None => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
let nrel = block.nrows().unwrap_or(0);
let endpoint_names: Vec<String> = (0..arity).map(rel_col_name).collect();
let mut prop_f: Vec<(String, &ndarray::ArrayD<F>)> = Vec::new();
let mut prop_i: Vec<(String, &ndarray::ArrayD<I>)> = Vec::new();
let mut prop_s: Vec<(String, &ndarray::ArrayD<String>)> = Vec::new();
for k in block.keys() {
if endpoint_names.iter().any(|e| e == k) {
continue;
}
if let Some(a) = block.get_float(k) {
prop_f.push((k.to_owned(), a));
} else if let Some(a) = block.get_int(k) {
prop_i.push((k.to_owned(), a));
} else if let Some(a) = block.get_string(k) {
prop_s.push((k.to_owned(), a));
}
}
for row in 0..nrel {
let mut nodes: SmallVec<[NodeId; 4]> = SmallVec::new();
let mut valid = true;
for col in &endpoint_cols {
let idx = col[[row]] as usize;
if idx >= node_ids.len() {
valid = false;
break;
}
nodes.push(node_ids[idx]);
}
if !valid {
continue;
}
if let Ok(rid) = self.add_relation(kid, &nodes) {
for (k, arr) in &prop_f {
#[allow(clippy::unnecessary_cast)]
let _ = self.set_relation_prop(kid, rid, k, arr[[row]] as f64);
}
for (k, arr) in &prop_i {
let _ = self.set_relation_prop(kid, rid, k, PropValue::Int(arr[[row]]));
}
for (k, arr) in &prop_s {
let _ =
self.set_relation_prop(kid, rid, k, PropValue::Str(arr[[row]].clone()));
}
}
}
}
Ok(())
}
}
pub(crate) fn rel_col_name(pos: usize) -> String {
match crate::store::keys::ENDPOINTS.get(pos) {
Some(name) => (*name).to_owned(),
None => format!("atom{pos}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_propvalue_from() {
let v: PropValue = std::f64::consts::PI.into();
assert_eq!(v, PropValue::F64(std::f64::consts::PI));
let v: PropValue = (42 as I).into();
assert_eq!(v, PropValue::Int(42 as I));
let v: PropValue = "H".into();
assert_eq!(v, PropValue::Str("H".to_owned()));
}
#[test]
fn test_atom_dict_api() {
let mut a = Atom::new();
a.set("x", 1.5);
a.set("element", "C");
a.set("type_id", PropValue::Int(3 as I));
assert_eq!(a.get_f64("x"), Some(1.5));
assert_eq!(a.get_str("element"), Some("C"));
assert_eq!(a.get_int("type_id"), Some(3 as I));
assert_eq!(a.get_f64("missing"), None);
assert!(a.contains_key("x"));
assert_eq!(a.len(), 3);
a.remove("type_id");
assert_eq!(a.len(), 2);
}
#[test]
fn test_atom_index() {
let mut a = Atom::xyz("O", 1.0, 2.0, 3.0);
assert_eq!(a["x"], PropValue::F64(1.0));
a["x"] = PropValue::F64(99.0);
assert_eq!(a.get_f64("x"), Some(99.0));
}
#[test]
fn test_register_kind_dense_idempotent() {
let mut g = MolGraph::new();
let bond = g.register_kind("bond", 2);
let angle = g.register_kind("angle", 3);
assert_eq!(bond, KindId(0));
assert_eq!(angle, KindId(1));
assert_eq!(g.register_kind("bond", 2), bond);
assert_eq!(g.kind_id("angle"), Some(angle));
assert_eq!(g.arity(bond), 2);
assert_eq!(g.kind_name(angle), "angle");
}
#[test]
#[should_panic]
fn test_register_kind_conflicting_arity_panics() {
let mut g = MolGraph::new();
g.register_kind("bond", 2);
g.register_kind("bond", 3);
}
#[test]
fn test_add_node_field_less() {
let mut g = MolGraph::new();
let n = g.add_node();
assert!(g.get_node(n).unwrap().is_empty());
crate::spatial::geometry::translate(&mut g, [1.0, 2.0, 3.0]);
assert!(g.get_node(n).unwrap().get_f64("x").is_none());
g.set_node(n, "element", "C").unwrap();
assert_eq!(g.get_node(n).unwrap().get_str("element"), Some("C"));
}
#[test]
fn test_add_remove_node() {
let mut g = MolGraph::new();
assert_eq!(g.n_nodes(), 0);
let id = g.add_node_with(Atom::xyz("C", 0.0, 0.0, 0.0));
assert_eq!(g.n_nodes(), 1);
assert_eq!(g.get_node(id).unwrap().get_str("element"), Some("C"));
g.remove_node(id).unwrap();
assert_eq!(g.n_nodes(), 0);
assert!(g.get_node(id).is_err());
}
#[test]
fn test_generic_relation_crud() {
let mut g = MolGraph::new();
let angle = g.register_kind("angle", 3);
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let rid = g.add_relation(angle, &[a, b, c]).unwrap();
assert_eq!(g.n_relations(angle), 1);
assert_eq!(
g.get_relation(angle, rid).unwrap().nodes.as_slice(),
&[a, b, c][..]
);
assert!(g.add_relation(angle, &[a, b]).is_err()); g.remove_node(c).unwrap();
assert!(g.add_relation(angle, &[a, b, c]).is_err()); assert_eq!(g.n_relations(angle), 0); }
#[test]
fn test_same_arity_distinct_kind() {
let mut g = MolGraph::new();
let dih = g.register_kind("dihedral", 4);
let imp = g.register_kind("improper", 4);
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_relation(dih, &[a, b, c, d]).unwrap();
g.add_relation(imp, &[a, b, c, d]).unwrap();
assert_eq!(g.n_relations(dih), 1);
assert_eq!(g.n_relations(imp), 1);
}
#[test]
fn test_cascade_across_all_kinds() {
let mut g = MolGraph::new();
let bond = g.register_kind("bond", 2);
let angle = g.register_kind("angle", 3);
let dih = g.register_kind("dihedral", 4);
let imp = g.register_kind("improper", 4);
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_relation(bond, &[a, b]).unwrap();
g.add_relation(angle, &[b, a, c]).unwrap();
g.add_relation(dih, &[b, a, c, d]).unwrap();
g.add_relation(imp, &[b, a, c, d]).unwrap();
g.remove_node(a).unwrap();
assert_eq!(g.n_relations(bond), 0);
assert_eq!(g.n_relations(angle), 0);
assert_eq!(g.n_relations(dih), 0);
assert_eq!(g.n_relations(imp), 0);
}
#[test]
fn test_neighbors() {
let mut g = MolGraph::new();
let bond = g.register_kind("bond", 2);
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
g.add_relation(bond, &[a, b]).unwrap();
g.add_relation(bond, &[a, c]).unwrap();
let mut n: Vec<NodeId> = g.neighbors(a).collect();
n.sort_by_key(|id| id.0);
assert_eq!(n.len(), 2);
assert!(n.contains(&b) && n.contains(&c));
assert_eq!(g.neighbors(b).collect::<Vec<_>>(), vec![a]);
let bid = g.relations(bond).next().unwrap().0;
g.remove_relation(bond, bid).unwrap();
assert_eq!(g.neighbors(a).count(), 1);
}
#[test]
fn test_translate_and_rotate() {
let mut g = MolGraph::new();
let id = g.add_node_with(Atom::xyz("C", 1.0, 0.0, 0.0));
crate::spatial::geometry::translate(&mut g, [10.0, 20.0, 30.0]);
let a = g.get_node(id).unwrap();
assert!((a.get_f64("x").unwrap() - 11.0).abs() < 1e-12);
let id2 = g.add_node_with(Atom::xyz("C", 1.0, 0.0, 0.0));
crate::spatial::geometry::rotate(
&mut g,
[0.0, 0.0, 1.0],
std::f64::consts::FRAC_PI_2,
None,
);
let b = g.get_node(id2).unwrap();
assert!((b.get_f64("x").unwrap()).abs() < 1e-12);
assert!((b.get_f64("y").unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn test_to_read_frame_roundtrip() {
let mut g = MolGraph::new();
let bond = g.register_kind("bonds", 2);
let o = g.add_node_with(Atom::xyz("O", 0.0, 0.0, 0.0));
let h1 = g.add_node_with(Atom::xyz("H", 0.96, 0.0, 0.0));
let h2 = g.add_node_with(Atom::xyz("H", -0.24, 0.93, 0.0));
g.add_relation(bond, &[o, h1]).unwrap();
g.add_relation(bond, &[o, h2]).unwrap();
let frame = g.to_frame();
assert!(frame.contains_key("atoms"));
assert!(frame.contains_key("bonds"));
assert_eq!(frame["atoms"].nrows(), Some(3));
assert_eq!(frame["bonds"].nrows(), Some(2));
let mut g2 = MolGraph::new();
let bond2 = g2.register_kind("bonds", 2);
g2.read_frame(&frame).unwrap();
assert_eq!(g2.n_nodes(), 3);
assert_eq!(g2.n_relations(bond2), 2);
}
#[test]
fn test_read_frame_restores_int_and_str_relation_props() {
let mut g = MolGraph::new();
let bond = g.register_kind("bonds", 2);
let a = g.add_node_with(Atom::xyz("C", 0.0, 0.0, 0.0));
let b = g.add_node_with(Atom::xyz("C", 1.5, 0.0, 0.0));
let rid = g.add_relation(bond, &[a, b]).unwrap();
g.set_relation_prop(bond, rid, "order", 2.0_f64).unwrap();
g.set_relation_prop(bond, rid, "ring_size", PropValue::Int(6))
.unwrap();
g.set_relation_prop(bond, rid, "label", PropValue::Str("aromatic".to_owned()))
.unwrap();
let frame = g.to_frame();
let mut g2 = MolGraph::new();
let bond2 = g2.register_kind("bonds", 2);
g2.read_frame(&frame).unwrap();
let (_, r) = g2.relations(bond2).next().expect("relation round-trips");
assert_eq!(
r.props.get("ring_size"),
Some(&PropValue::Int(6)),
"int relation prop lost on round-trip"
);
assert_eq!(
r.props.get("label"),
Some(&PropValue::Str("aromatic".to_owned())),
"string relation prop lost on round-trip"
);
match r.props.get("order") {
Some(PropValue::F64(v)) => assert!((v - 2.0).abs() < 1e-12),
other => panic!("float relation prop lost: {other:?}"),
}
}
#[test]
fn test_merge_transfers_all_kinds() {
let mut src = MolGraph::new();
let bond = src.register_kind("bond", 2);
let imp = src.register_kind("improper", 4);
let a = src.add_node();
let b = src.add_node();
let c = src.add_node();
let d = src.add_node();
src.add_relation(bond, &[a, b]).unwrap();
src.add_relation(imp, &[a, b, c, d]).unwrap();
let mut dst = MolGraph::new();
let dbond = dst.register_kind("bond", 2);
let dimp = dst.register_kind("improper", 4);
let e = dst.add_node();
let f = dst.add_node();
dst.add_relation(dbond, &[e, f]).unwrap();
dst.merge(src);
assert_eq!(dst.n_nodes(), 6);
assert_eq!(dst.n_relations(dbond), 2);
assert_eq!(dst.n_relations(dimp), 1, "merge must carry impropers");
}
#[test]
fn test_clone_independence() {
let mut g = MolGraph::new();
let id = g.add_node_with(Atom::xyz("C", 0.0, 0.0, 0.0));
g.add_node_with(Atom::xyz("H", 1.0, 0.0, 0.0));
let g2 = g.clone();
g.set_node(id, "x", 99.0).unwrap();
assert_eq!(g2.get_node(id).unwrap().get_f64("x"), Some(0.0));
assert_eq!(g2.n_nodes(), 2);
}
#[test]
fn test_groups_reserved_empty() {
let g = MolGraph::new();
assert_eq!(g.groups.len(), 0);
}
}