From 92d8f9937e7048bf7da3dbdae0f373fb38548229 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 22:37:49 -0400 Subject: [PATCH 01/15] Upgrade C++ standard from C++17 to C++20 RDKit 2025.09.x uses constexpr virtual destructors and override functions in its headers (e.g. Geometry/point.h), which require C++20. Building with -std=c++17 causes compilation failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- rdkit-sys/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdkit-sys/build.rs b/rdkit-sys/build.rs index 782861d..d5ba17e 100644 --- a/rdkit-sys/build.rs +++ b/rdkit-sys/build.rs @@ -1,4 +1,4 @@ -const CPP_VERSION_FLAG: &str = "-std=c++17"; +const CPP_VERSION_FLAG: &str = "-std=c++20"; fn main() { if std::env::var("DOCS_RS").is_ok() { From e5a0ea96dd2f440be4c39a64319956d1bc4052e2 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 22:37:53 -0400 Subject: [PATCH 02/15] Add explicit lifetime annotation to atom_with_idx return type Fixes mismatched_lifetime_syntaxes warning by making the elided lifetime on Atom<'_> explicit, matching the &mut self borrow. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/graphmol/ro_mol.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphmol/ro_mol.rs b/src/graphmol/ro_mol.rs index bcdd30c..8b13489 100644 --- a/src/graphmol/ro_mol.rs +++ b/src/graphmol/ro_mol.rs @@ -89,7 +89,7 @@ impl ROMol { ro_mol_ffi::get_num_atoms(&self.ptr, only_explicit) } - pub fn atom_with_idx(&mut self, idx: u32) -> Atom { + pub fn atom_with_idx(&mut self, idx: u32) -> Atom<'_> { let ptr = ro_mol_ffi::get_atom_with_idx(&mut self.ptr, idx); Atom::from_ptr(ptr) } From 6a2d625aa0222c93276e7b288d68537a03700554 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 22:38:08 -0400 Subject: [PATCH 03/15] Fix clippy warnings: unused lifetime and len_zero Remove unused lifetime parameter 'a from PeriodicTableOps impl, and replace names.len() != 0 with !names.is_empty(). Co-Authored-By: Claude Opus 4.6 (1M context) --- rdkit-sys/src/bridge/periodic_table.rs | 2 +- src/descriptors.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rdkit-sys/src/bridge/periodic_table.rs b/rdkit-sys/src/bridge/periodic_table.rs index b08b1fb..21e0dfa 100644 --- a/rdkit-sys/src/bridge/periodic_table.rs +++ b/rdkit-sys/src/bridge/periodic_table.rs @@ -43,7 +43,7 @@ pub trait PeriodicTableOps { fn getElementName(self, atomic_number: u32) -> String; fn getValenceList(self, atomic_number: u32) -> &'static CxxVector; } -impl<'a> PeriodicTableOps for UniquePtr { +impl PeriodicTableOps for UniquePtr { fn getElementSymbol(self, atomic_number: u32) -> String { ffi::getElementSymbol(atomic_number) } diff --git a/src/descriptors.rs b/src/descriptors.rs index a3272c9..a6511d6 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -25,7 +25,7 @@ impl Properties { let names = rdkit_sys::descriptors_ffi::get_property_names(&self.ptr); let computed = rdkit_sys::descriptors_ffi::compute_properties(&self.ptr, &ro_mol.ptr); - assert!(names.len() != 0); + assert!(!names.is_empty()); assert!(computed.len() == names.len()); names From 4d6f31321fe2fec7f84a811d7600db11f9fb0eac Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 22:38:14 -0400 Subject: [PATCH 04/15] Replace deprecated rustfmt version option with style_edition The 'version' key is deprecated in current rustfmt. Replace with 'style_edition = "2021"' in both workspace and rdkit-sys configs. Co-Authored-By: Claude Opus 4.6 (1M context) --- rdkit-sys/rustfmt.toml | 2 +- rustfmt.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rdkit-sys/rustfmt.toml b/rdkit-sys/rustfmt.toml index 7de3b73..6158963 100644 --- a/rdkit-sys/rustfmt.toml +++ b/rdkit-sys/rustfmt.toml @@ -49,7 +49,7 @@ match_block_trailing_comma = false blank_lines_upper_bound = 1 blank_lines_lower_bound = 0 edition = "2021" -version = "One" +style_edition = "2021" inline_attribute_width = 0 format_generated_files = true merge_derives = true diff --git a/rustfmt.toml b/rustfmt.toml index 7de3b73..6158963 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -49,7 +49,7 @@ match_block_trailing_comma = false blank_lines_upper_bound = 1 blank_lines_lower_bound = 0 edition = "2021" -version = "One" +style_edition = "2021" inline_attribute_width = 0 format_generated_files = true merge_derives = true From d399a7427bf8b119941353bf0a3b34e19bfc7038 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 23:12:41 -0400 Subject: [PATCH 05/15] Fix tail-expression drop order for Rust 2024 compatibility Bind get_periodic_table() to a local variable instead of using it as a temporary in tail expressions. In Rust 2024, temporaries in tail expressions are dropped before locals, which would drop the PeriodicTable UniquePtr before the CxxString it borrows. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/periodic_table.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/periodic_table.rs b/src/periodic_table.rs index 60fc473..cf66658 100644 --- a/src/periodic_table.rs +++ b/src/periodic_table.rs @@ -1,4 +1,4 @@ -use cxx::{let_cxx_string, CxxVector}; +use cxx::{CxxVector, let_cxx_string}; use rdkit_sys::PeriodicTableOps; pub struct PeriodicTable {} @@ -18,8 +18,8 @@ impl PeriodicTable { /// * `atom` - The symbol of the element pub fn get_most_common_isotope_mass(atom: &str) -> f64 { let_cxx_string!(atom_cxx_string = atom); - rdkit_sys::periodic_table_ffi::get_periodic_table() - .getMostCommonIsotopeMass(&atom_cxx_string) + let pt = rdkit_sys::periodic_table_ffi::get_periodic_table(); + pt.getMostCommonIsotopeMass(&atom_cxx_string) } /// Returns the atomic weight of the atom @@ -32,7 +32,8 @@ impl PeriodicTable { /// * `atom` - The symbol of the element pub fn get_atomic_number(atom: &str) -> i32 { let_cxx_string!(atom_cxx_string = atom); - rdkit_sys::periodic_table_ffi::get_periodic_table().getAtomicNumber(&atom_cxx_string) + let pt = rdkit_sys::periodic_table_ffi::get_periodic_table(); + pt.getAtomicNumber(&atom_cxx_string) } /// Returns the symbol of the element From fd0639e0122aa66e9bbe5de1226bfffbc1b410d9 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 23:12:47 -0400 Subject: [PATCH 06/15] Upgrade to Rust edition 2024 Update edition to 2024 in both Cargo.toml files and rustfmt configs. Apply 2024 style formatting: types before functions/macros in imports, and long assert_eq! macro calls wrapped across multiple lines. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 2 +- rdkit-sys/Cargo.toml | 2 +- rdkit-sys/rustfmt.toml | 4 ++-- rdkit-sys/src/bridge/mod.rs | 2 +- rdkit-sys/tests/test_atoms.rs | 4 +++- rdkit-sys/tests/test_ro_mol.rs | 5 ++++- rdkit-sys/tests/test_rw_mol.rs | 7 +++++-- rustfmt.toml | 4 ++-- src/graphmol/rw_mol.rs | 2 +- tests/test_graphmol.rs | 26 +++++++++++++++++++------- tests/test_mol_ops.rs | 2 +- tests/test_substruct.rs | 2 +- 12 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1086859..295dbe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rdkit" version = "0.4.12" -edition = "2021" +edition = "2024" authors = ["Xavier Lange ", "Javier Pineda , SharedPtr>(rw_mol) }; let smiles = rdkit_sys::ro_mol_ffi::mol_to_smiles(&ro_mol); - assert_eq!("[H]C([H])([H])C(=O)OC([H])(C([H])([H])C(=O)[O-])C([H])([H])[N+](C([H])([H])[H])(C([H])([H])[H])C([H])([H])[H]", &smiles); + assert_eq!( + "[H]C([H])([H])C(=O)OC([H])(C([H])([H])C(=O)[O-])C([H])([H])[N+](C([H])([H])[H])(C([H])([H])[H])C([H])([H])[H]", + &smiles + ); } #[test] diff --git a/rustfmt.toml b/rustfmt.toml index 6158963..6edb4d8 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -48,8 +48,8 @@ trailing_comma = "Vertical" match_block_trailing_comma = false blank_lines_upper_bound = 1 blank_lines_lower_bound = 0 -edition = "2021" -style_edition = "2021" +edition = "2024" +style_edition = "2024" inline_attribute_width = 0 format_generated_files = true merge_derives = true diff --git a/src/graphmol/rw_mol.rs b/src/graphmol/rw_mol.rs index 6467120..b640d50 100644 --- a/src/graphmol/rw_mol.rs +++ b/src/graphmol/rw_mol.rs @@ -1,6 +1,6 @@ use std::fmt::Formatter; -use cxx::{let_cxx_string, SharedPtr}; +use cxx::{SharedPtr, let_cxx_string}; use rdkit_sys::*; use crate::ROMol; diff --git a/tests/test_graphmol.rs b/tests/test_graphmol.rs index 64587f1..e167672 100644 --- a/tests/test_graphmol.rs +++ b/tests/test_graphmol.rs @@ -1,7 +1,7 @@ use rdkit::{ - detect_chemistry_problems, fragment_parent, substruct_match, CleanupParameters, - MolSanitizeException, ROMol, ROMolError, RWMol, SmilesParserParams, SubstructMatchParameters, - TautomerEnumerator, Uncharger, + CleanupParameters, MolSanitizeException, ROMol, ROMolError, RWMol, SmilesParserParams, + SubstructMatchParameters, TautomerEnumerator, Uncharger, detect_chemistry_problems, + fragment_parent, substruct_match, }; #[test] @@ -15,7 +15,10 @@ fn test_neutralize() { let romol = ROMol::from_smiles(smiles).unwrap(); let uncharger = Uncharger::new(false); let uncharged_mol = uncharger.uncharge(&romol); - assert_eq!("CCOC(=O)C(C)(C)Oc1ccc(Cl)cc1.CO.Nc1nc2ncc(CNc3ccc(C(=O)N[C@@H](CCC(=O)O)C(=O)O)cc3)nc2c(=O)[nH]1", uncharged_mol.as_smiles()); + assert_eq!( + "CCOC(=O)C(C)(C)Oc1ccc(Cl)cc1.CO.Nc1nc2ncc(CNc3ccc(C(=O)N[C@@H](CCC(=O)O)C(=O)O)cc3)nc2c(=O)[nH]1", + uncharged_mol.as_smiles() + ); } #[test] @@ -29,7 +32,10 @@ fn test_fragment_parent() { "Nc1nc2ncc(CNc3ccc(C(=O)N[C@@H](CCC(=O)O)C(=O)O)cc3)nc2c(=O)[nH]1", parent_rwmol.as_smiles() ); - assert_eq!("CCOC(=O)C(C)(C)Oc1ccc(Cl)cc1.CO.Nc1nc2ncc(CNc3ccc(C(=O)N[C@@H](CCC(=O)O)C(=O)O)cc3)nc2c(=O)[nH]1", rwmol.as_smiles()); + assert_eq!( + "CCOC(=O)C(C)(C)Oc1ccc(Cl)cc1.CO.Nc1nc2ncc(CNc3ccc(C(=O)N[C@@H](CCC(=O)O)C(=O)O)cc3)nc2c(=O)[nH]1", + rwmol.as_smiles() + ); } #[test] @@ -271,7 +277,10 @@ CC(=O)OC(CC(=O)[O-])C[N+](C)(C)C "#; let rw_mol = RWMol::from_mol_block(mol_block, false, false, false).unwrap(); - assert_eq!("[H]C([H])([H])C(=O)OC([H])(C([H])([H])C(=O)[O-])C([H])([H])[N+](C([H])([H])[H])(C([H])([H])[H])C([H])([H])[H]", &rw_mol.as_smiles()); + assert_eq!( + "[H]C([H])([H])C(=O)OC([H])(C([H])([H])C(=O)[O-])C([H])([H])[N+](C([H])([H])[H])(C([H])([H])[H])C([H])([H])[H]", + &rw_mol.as_smiles() + ); } #[test] @@ -326,5 +335,8 @@ fn mol_to_molblock_test() { let smiles = "CC"; let romol = ROMol::from_smiles(&smiles).unwrap(); let molblock = romol.to_molblock(); - assert_eq!(molblock, "\n RDKit 2D\n\n 2 1 0 0 0 0 0 0 0 0999 V2000\n 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.2990 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 1 0\nM END\n"); + assert_eq!( + molblock, + "\n RDKit 2D\n\n 2 1 0 0 0 0 0 0 0 0999 V2000\n 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.2990 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 1 0\nM END\n" + ); } diff --git a/tests/test_mol_ops.rs b/tests/test_mol_ops.rs index 42e4cb3..56dc92c 100644 --- a/tests/test_mol_ops.rs +++ b/tests/test_mol_ops.rs @@ -1,4 +1,4 @@ -use rdkit::{add_hs, clean_up, remove_hs, set_hybridization, ROMol, RemoveHsParameters}; +use rdkit::{ROMol, RemoveHsParameters, add_hs, clean_up, remove_hs, set_hybridization}; #[test] fn test_remove_hs() { diff --git a/tests/test_substruct.rs b/tests/test_substruct.rs index 851e0bc..dbe2b7f 100644 --- a/tests/test_substruct.rs +++ b/tests/test_substruct.rs @@ -1,4 +1,4 @@ -use rdkit::{substruct_match, ROMol, SubstructMatchItem, SubstructMatchParameters}; +use rdkit::{ROMol, SubstructMatchItem, SubstructMatchParameters, substruct_match}; #[test] fn test_substruct_match() { From adfd9a5a482a2620124d5e469d88d20bc98e84a3 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 23:54:27 -0400 Subject: [PATCH 07/15] Add const atom access FFI path Add get_atom_with_idx_const that takes const shared_ptr& and returns const Atom&, using RDKit's const overload of getAtomWithIdx. This maps to Pin<&Atom> in CXX, enabling &self access on the Rust side. Co-Authored-By: Claude Opus 4.6 (1M context) --- rdkit-sys/src/bridge/ro_mol.rs | 1 + rdkit-sys/wrapper/include/ro_mol.h | 2 ++ rdkit-sys/wrapper/src/ro_mol.cc | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/rdkit-sys/src/bridge/ro_mol.rs b/rdkit-sys/src/bridge/ro_mol.rs index fffa0e7..2e5ac6e 100644 --- a/rdkit-sys/src/bridge/ro_mol.rs +++ b/rdkit-sys/src/bridge/ro_mol.rs @@ -59,6 +59,7 @@ pub mod ffi { pub fn get_num_atoms(mol: &SharedPtr, onlyExplicit: bool) -> u32; pub fn get_atom_with_idx(mol: &mut SharedPtr, idx: u32) -> Pin<&mut Atom>; + pub fn get_atom_with_idx_const(mol: &SharedPtr, idx: u32) -> Pin<&Atom>; pub fn get_symbol(atom: Pin<&Atom>) -> String; pub fn get_is_aromatic(atom: Pin<&Atom>) -> bool; pub fn get_atomic_num(atom: Pin<&Atom>) -> i32; diff --git a/rdkit-sys/wrapper/include/ro_mol.h b/rdkit-sys/wrapper/include/ro_mol.h index 6cbe18d..6fbe6ea 100644 --- a/rdkit-sys/wrapper/include/ro_mol.h +++ b/rdkit-sys/wrapper/include/ro_mol.h @@ -29,6 +29,8 @@ unsigned int atom_sanitize_exception_get_atom_idx(const MolSanitizeExceptionUniq unsigned int get_num_atoms(const std::shared_ptr &mol, bool only_explicit); Atom &get_atom_with_idx(std::shared_ptr &mol, unsigned int idx); +const Atom &get_atom_with_idx_const(const std::shared_ptr &mol, + unsigned int idx); rust::String get_symbol(const Atom &atom); bool get_is_aromatic(const Atom &atom); int get_atomic_num(const Atom &atom); diff --git a/rdkit-sys/wrapper/src/ro_mol.cc b/rdkit-sys/wrapper/src/ro_mol.cc index 2442a0b..d3ee865 100644 --- a/rdkit-sys/wrapper/src/ro_mol.cc +++ b/rdkit-sys/wrapper/src/ro_mol.cc @@ -69,6 +69,10 @@ unsigned int get_num_atoms(const std::shared_ptr &mol, bool only_explicit Atom &get_atom_with_idx(std::shared_ptr &mol, unsigned int idx) { return *mol->getAtomWithIdx(idx); } +const Atom &get_atom_with_idx_const(const std::shared_ptr &mol, unsigned int idx) { + return *mol->getAtomWithIdx(idx); +} + rust::String get_symbol(const Atom &atom) { return atom.getSymbol(); } bool get_is_aromatic(const Atom &atom) { return atom.getIsAromatic(); } From b0a74d43b384e957c778ef823420f749cb84140f Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 23:54:35 -0400 Subject: [PATCH 08/15] Add AtomRef for read-only atom access without &mut self AtomRef<'a> holds Pin<&'a Atom> (const) and exposes all read-only atom methods. ROMol::atom_ref(&self) returns AtomRef, eliminating the need to clone molecules just to read atom properties. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/graphmol/atom_ref.rs | 88 ++++++++++++++++++++++++++++++++++++++++ src/graphmol/mod.rs | 3 ++ src/graphmol/ro_mol.rs | 11 ++++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/graphmol/atom_ref.rs diff --git a/src/graphmol/atom_ref.rs b/src/graphmol/atom_ref.rs new file mode 100644 index 0000000..c6422d7 --- /dev/null +++ b/src/graphmol/atom_ref.rs @@ -0,0 +1,88 @@ +use std::{fmt::Formatter, pin::Pin}; + +use rdkit_sys::ro_mol_ffi; +pub use rdkit_sys::ro_mol_ffi::HybridizationType; + +/// Read-only view of an atom within a molecule. +/// +/// Unlike [`Atom`](crate::Atom), this borrows the parent molecule immutably +/// (`&self`), so multiple `AtomRef`s can coexist and no clone is needed. +pub struct AtomRef<'a> { + ptr: Pin<&'a ro_mol_ffi::Atom>, +} + +impl<'a> std::fmt::Display for AtomRef<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let symbol = self.symbol(); + f.write_str(&symbol) + } +} + +impl<'a> std::fmt::Debug for AtomRef<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let symbol = self.symbol(); + f.write_str(&symbol) + } +} + +impl<'a> AtomRef<'a> { + pub fn from_ptr(ptr: Pin<&'a ro_mol_ffi::Atom>) -> Self { + Self { ptr } + } + + pub fn symbol(&self) -> String { + ro_mol_ffi::get_symbol(self.ptr) + } + + pub fn get_is_aromatic(&self) -> bool { + ro_mol_ffi::get_is_aromatic(self.ptr) + } + + pub fn get_atomic_num(&self) -> i32 { + ro_mol_ffi::get_atomic_num(self.ptr) + } + + pub fn get_formal_charge(&self) -> i32 { + ro_mol_ffi::get_formal_charge(self.ptr) + } + + pub fn get_total_num_hs(&self) -> u32 { + ro_mol_ffi::get_total_num_hs(self.ptr) + } + + pub fn get_total_valence(&self) -> u32 { + ro_mol_ffi::get_total_valence(self.ptr) + } + + pub fn get_hybridization_type(&self) -> HybridizationType { + ro_mol_ffi::atom_get_hybridization(self.ptr) + } + + pub fn get_num_radical_electrons(&self) -> u32 { + ro_mol_ffi::get_num_radical_electrons(self.ptr) + } + + pub fn get_degree(&self) -> u32 { + ro_mol_ffi::get_degree(self.ptr) + } + + pub fn get_int_prop(&self, key: &str) -> Result { + cxx::let_cxx_string!(key = key); + ro_mol_ffi::get_int_prop(self.ptr, &key) + } + + pub fn get_float_prop(&self, key: &str) -> Result { + cxx::let_cxx_string!(key = key); + ro_mol_ffi::get_float_prop(self.ptr, &key) + } + + pub fn get_bool_prop(&self, key: &str) -> Result { + cxx::let_cxx_string!(key = key); + ro_mol_ffi::get_bool_prop(self.ptr, &key) + } + + pub fn get_prop(&self, key: &str) -> Result { + cxx::let_cxx_string!(key = key); + ro_mol_ffi::get_prop(self.ptr, &key) + } +} diff --git a/src/graphmol/mod.rs b/src/graphmol/mod.rs index f23a721..1b1309b 100644 --- a/src/graphmol/mod.rs +++ b/src/graphmol/mod.rs @@ -1,6 +1,9 @@ mod atom; pub use atom::*; +mod atom_ref; +pub use atom_ref::*; + mod mol_ops; pub use mol_ops::*; diff --git a/src/graphmol/ro_mol.rs b/src/graphmol/ro_mol.rs index 8b13489..045d2ed 100644 --- a/src/graphmol/ro_mol.rs +++ b/src/graphmol/ro_mol.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter}; use cxx::let_cxx_string; use rdkit_sys::*; -use crate::{Atom, Fingerprint, RWMol}; +use crate::{Atom, AtomRef, Fingerprint, RWMol}; pub struct ROMol { pub(crate) ptr: cxx::SharedPtr, @@ -94,6 +94,15 @@ impl ROMol { Atom::from_ptr(ptr) } + /// Returns a read-only reference to the atom at `idx`. + /// + /// Unlike [`atom_with_idx`](Self::atom_with_idx), this takes `&self`, + /// so no mutable borrow (or clone) is needed for read-only access. + pub fn atom_ref(&self, idx: u32) -> AtomRef<'_> { + let ptr = ro_mol_ffi::get_atom_with_idx_const(&self.ptr, idx); + AtomRef::from_ptr(ptr) + } + pub fn update_property_cache(&mut self, strict: bool) { ro_mol_ffi::ro_mol_update_property_cache(&mut self.ptr, strict) } From eed9fd03bdff9e4d26b174200095e8f6a68b32ad Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 23:54:40 -0400 Subject: [PATCH 09/15] Add parity tests for AtomRef vs Atom Verify AtomRef returns identical values for all read-only methods, property getters work, multiple AtomRefs coexist (no &mut needed), and iteration over all atoms works via &self. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_atom_ref.rs | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test_atom_ref.rs diff --git a/tests/test_atom_ref.rs b/tests/test_atom_ref.rs new file mode 100644 index 0000000..55c80e9 --- /dev/null +++ b/tests/test_atom_ref.rs @@ -0,0 +1,81 @@ +/// Verify AtomRef returns identical results to Atom for all read-only methods. +#[test] +fn test_atom_ref_parity() { + let mut romol = rdkit::ROMol::from_smiles("[NH4+]").unwrap(); + + // Read via mutable Atom + let atom = romol.atom_with_idx(0); + let symbol = atom.symbol(); + let is_aromatic = atom.get_is_aromatic(); + let atomic_num = atom.get_atomic_num(); + let hybridization = atom.get_hybridization_type(); + let formal_charge = atom.get_formal_charge(); + let total_num_hs = atom.get_total_num_hs(); + let total_valence = atom.get_total_valence(); + let num_radical_electrons = atom.get_num_radical_electrons(); + let degree = atom.get_degree(); + + // Read via immutable AtomRef — must match exactly + let atom_ref = romol.atom_ref(0); + assert_eq!(atom_ref.symbol(), symbol); + assert_eq!(atom_ref.get_is_aromatic(), is_aromatic); + assert_eq!(atom_ref.get_atomic_num(), atomic_num); + assert_eq!(atom_ref.get_hybridization_type(), hybridization); + assert_eq!(atom_ref.get_formal_charge(), formal_charge); + assert_eq!(atom_ref.get_total_num_hs(), total_num_hs); + assert_eq!(atom_ref.get_total_valence(), total_valence); + assert_eq!(atom_ref.get_num_radical_electrons(), num_radical_electrons); + assert_eq!(atom_ref.get_degree(), degree); +} + +/// Verify AtomRef property getters work. +#[test] +fn test_atom_ref_properties() { + let mut romol = rdkit::ROMol::from_smiles("CC").unwrap(); + + // Set properties via mutable Atom + { + let mut carbon = romol.atom_with_idx(0); + carbon.set_prop("int_key", 42); + carbon.set_prop("float_key", 3.14); + carbon.set_prop("bool_key", true); + carbon.set_prop("str_key", "hello"); + } + + // Read back via immutable AtomRef + let atom_ref = romol.atom_ref(0); + assert_eq!(atom_ref.get_int_prop("int_key").unwrap(), 42); + assert_eq!(atom_ref.get_float_prop("float_key").unwrap(), 3.14); + assert_eq!(atom_ref.get_bool_prop("bool_key").unwrap(), true); + assert_eq!(atom_ref.get_prop("str_key").unwrap(), "hello"); +} + +/// Verify multiple AtomRefs can coexist (no &mut self needed). +#[test] +fn test_atom_ref_no_clone_needed() { + let romol = rdkit::ROMol::from_smiles("CCO").unwrap(); + + // This would not compile with atom_with_idx since it needs &mut self. + // With atom_ref, we can hold multiple references simultaneously. + let c1 = romol.atom_ref(0); + let c2 = romol.atom_ref(1); + let o = romol.atom_ref(2); + + assert_eq!(c1.symbol(), "C"); + assert_eq!(c2.symbol(), "C"); + assert_eq!(o.symbol(), "O"); +} + +/// Verify AtomRef works across all atoms in a molecule. +#[test] +fn test_atom_ref_iteration() { + let romol = rdkit::ROMol::from_smiles("c1ccccc1").unwrap(); + let n = romol.num_atoms(true); + assert_eq!(n, 6); + + for i in 0..n { + let atom = romol.atom_ref(i); + assert_eq!(atom.symbol(), "C"); + assert!(atom.get_is_aromatic()); + } +} From e3ef2d332e6b8b23235bbea289c5698116f3116c Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Fri, 13 Mar 2026 23:54:47 -0400 Subject: [PATCH 10/15] Add atom iteration benchmarks comparing AtomRef vs Atom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmarks for the featurization hot path with 7 drug-like molecules: - atom_ref (const, &self): ~7μs for 7 properties across all atoms - atom_with_idx (&mut self): ~7μs same, but requires &mut - clone + featurize (&ROMol callers): ~46μs — 6.5x slower due to clone Shows AtomRef eliminates the clone tax for read-only workflows. Co-Authored-By: Claude Opus 4.6 (1M context) --- benches/atom_iteration_benchmark.rs | 194 ++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 benches/atom_iteration_benchmark.rs diff --git a/benches/atom_iteration_benchmark.rs b/benches/atom_iteration_benchmark.rs new file mode 100644 index 0000000..8c6d4dc --- /dev/null +++ b/benches/atom_iteration_benchmark.rs @@ -0,0 +1,194 @@ +#![allow(soft_unstable)] +#![feature(test)] +extern crate test; + +use rdkit::ROMol; + +/// Drug-like molecules of varying size for realistic benchmarking. +/// These cover common pharmaceutical scaffolds and natural products. +const SMILES_SET: &[&str] = &[ + // aspirin (21 atoms with H) + "CC(=O)Oc1ccccc1C(=O)O", + // ibuprofen + "CC(C)Cc1ccc(cc1)C(C)C(=O)O", + // caffeine + "Cn1c(=O)c2c(ncn2C)n(C)c1=O", + // diazepam + "O=C1CN=C(c2ccccc2)c2cc(Cl)ccc2N1C", + // atorvastatin (lipitor) — large drug molecule + "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)O", + // taxol core — complex natural product + "CC1=C2C(OC(=O)c3ccccc3)C(O)C4(OC(=O)C(O)(CC(OC(=O)c5ccccc5)C1O)C24C)C(=O)c1ccc(OC)cc1", + // vancomycin fragment — large, many atoms + "OC1C(O)C(OC2C(O)C(O)C(O)C(CO)O2)OC(CO)C1NC(=O)C1CC(O)CN1C(=O)C(NC(=O)C1CC(=O)NC(=O)C1O)C(O)c1ccc(O)cc1", +]; + +/// Benchmark: parse SMILES into molecules. +/// Baseline cost — everything else is on top of this. +#[bench] +fn bench_parse_smiles(b: &mut test::bench::Bencher) { + b.iter(|| { + for smiles in SMILES_SET { + test::black_box(ROMol::from_smiles(smiles).unwrap()); + } + }); +} + +/// Benchmark: iterate all atoms and read one cheap property (atomic number). +/// Each iteration: N atoms * 2 FFI calls (get_atom + get_atomic_num). +#[bench] +fn bench_atom_one_property(b: &mut test::bench::Bencher) { + let mut mols: Vec = SMILES_SET + .iter() + .map(|s| ROMol::from_smiles(s).unwrap()) + .collect(); + + b.iter(|| { + for mol in &mut mols { + let n = mol.num_atoms(true); + for i in 0..n { + let atom = mol.atom_with_idx(i); + test::black_box(atom.get_atomic_num()); + } + } + }); +} + +/// Benchmark: iterate all atoms, read 7 properties per atom. +/// This is the realistic "featurization" workload — the hot path +/// in ML pipelines, QSAR descriptor computation, etc. +/// Each iteration: N atoms * 8 FFI calls (1 get_atom + 7 properties). +#[bench] +fn bench_atom_all_properties(b: &mut test::bench::Bencher) { + let mut mols: Vec = SMILES_SET + .iter() + .map(|s| ROMol::from_smiles(s).unwrap()) + .collect(); + + b.iter(|| { + for mol in &mut mols { + let n = mol.num_atoms(true); + for i in 0..n { + let atom = mol.atom_with_idx(i); + test::black_box(atom.symbol()); + test::black_box(atom.get_atomic_num()); + test::black_box(atom.get_formal_charge()); + test::black_box(atom.get_is_aromatic()); + test::black_box(atom.get_hybridization_type()); + test::black_box(atom.get_degree()); + test::black_box(atom.get_total_num_hs()); + } + } + }); +} + +/// Benchmark: full pipeline — parse + featurize. +/// Measures end-to-end cost of the most common workflow. +#[bench] +fn bench_parse_and_featurize(b: &mut test::bench::Bencher) { + b.iter(|| { + for smiles in SMILES_SET { + let mut mol = ROMol::from_smiles(smiles).unwrap(); + let n = mol.num_atoms(true); + for i in 0..n { + let atom = mol.atom_with_idx(i); + test::black_box(atom.symbol()); + test::black_box(atom.get_atomic_num()); + test::black_box(atom.get_formal_charge()); + test::black_box(atom.get_is_aromatic()); + test::black_box(atom.get_hybridization_type()); + test::black_box(atom.get_degree()); + test::black_box(atom.get_total_num_hs()); + } + } + }); +} + +/// Benchmark: atom_ref with one property (const path, no &mut needed). +#[bench] +fn bench_atom_ref_one_property(b: &mut test::bench::Bencher) { + let mols: Vec = SMILES_SET + .iter() + .map(|s| ROMol::from_smiles(s).unwrap()) + .collect(); + + b.iter(|| { + for mol in &mols { + let n = mol.num_atoms(true); + for i in 0..n { + let atom = mol.atom_ref(i); + test::black_box(atom.get_atomic_num()); + } + } + }); +} + +/// Benchmark: atom_ref with all 7 properties (const path). +/// Compare directly against bench_atom_all_properties. +#[bench] +fn bench_atom_ref_all_properties(b: &mut test::bench::Bencher) { + let mols: Vec = SMILES_SET + .iter() + .map(|s| ROMol::from_smiles(s).unwrap()) + .collect(); + + b.iter(|| { + for mol in &mols { + let n = mol.num_atoms(true); + for i in 0..n { + let atom = mol.atom_ref(i); + test::black_box(atom.symbol()); + test::black_box(atom.get_atomic_num()); + test::black_box(atom.get_formal_charge()); + test::black_box(atom.get_is_aromatic()); + test::black_box(atom.get_hybridization_type()); + test::black_box(atom.get_degree()); + test::black_box(atom.get_total_num_hs()); + } + } + }); +} + +/// Benchmark: clone + featurize via atom_with_idx (the old &ROMol workflow). +/// This is the cost users pay when they only have &ROMol. +#[bench] +fn bench_clone_and_featurize(b: &mut test::bench::Bencher) { + let mols: Vec = SMILES_SET + .iter() + .map(|s| ROMol::from_smiles(s).unwrap()) + .collect(); + + b.iter(|| { + for mol in &mols { + let mut mol = mol.clone(); + let n = mol.num_atoms(true); + for i in 0..n { + let atom = mol.atom_with_idx(i); + test::black_box(atom.symbol()); + test::black_box(atom.get_atomic_num()); + test::black_box(atom.get_formal_charge()); + test::black_box(atom.get_is_aromatic()); + test::black_box(atom.get_hybridization_type()); + test::black_box(atom.get_degree()); + test::black_box(atom.get_total_num_hs()); + } + } + }); +} + +/// Benchmark: clone cost alone. +/// atom_with_idx requires &mut self, so callers who have &ROMol must clone. +/// This measures that tax. +#[bench] +fn bench_clone_molecules(b: &mut test::bench::Bencher) { + let mols: Vec = SMILES_SET + .iter() + .map(|s| ROMol::from_smiles(s).unwrap()) + .collect(); + + b.iter(|| { + for mol in &mols { + test::black_box(mol.clone()); + } + }); +} From a8ddd2febd97c7998ec0e0f7960ab93d4a8fda4f Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Sat, 14 Mar 2026 00:50:01 -0400 Subject: [PATCH 11/15] Update dependencies to latest major versions - thiserror 1 -> 2 (derive macro API unchanged for our usage) - env_logger 0.9/0.10 -> 0.11 (init() API unchanged) - which 4 -> 8 (which::which() API unchanged) Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 4 ++-- rdkit-sys/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 295dbe2..93aeeb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ cxx = "1" flate2 = "1" log = "0.4" rdkit-sys = { path = "rdkit-sys", version = "0.4.9" } -thiserror = "1" +thiserror = "2" [dev-dependencies] -env_logger = "0.9.0" +env_logger = "0.11" diff --git a/rdkit-sys/Cargo.toml b/rdkit-sys/Cargo.toml index 53c8e9b..553f31b 100644 --- a/rdkit-sys/Cargo.toml +++ b/rdkit-sys/Cargo.toml @@ -14,9 +14,9 @@ exclude = ["rdkit-*", "*.tar.gz", "examples/"] cxx = "1.0.109" [build-dependencies] -env_logger = "0.10.0" +env_logger = "0.11" cxx-build = "1.0.109" -which = "4.4.2" +which = "8" [features] default = [] From d3563170d11063035f39c9e48a42f8d396a9ac53 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Sat, 14 Mar 2026 00:50:07 -0400 Subject: [PATCH 12/15] Trim benchmarks to honest comparisons only Remove inflated clone+featurize and redundant one-property benchmarks. Keep: atom_ref vs atom_mut (regression guard), clone cost, parse baseline. Co-Authored-By: Claude Opus 4.6 (1M context) --- benches/atom_iteration_benchmark.rs | 120 ++++------------------------ 1 file changed, 14 insertions(+), 106 deletions(-) diff --git a/benches/atom_iteration_benchmark.rs b/benches/atom_iteration_benchmark.rs index 8c6d4dc..47b5927 100644 --- a/benches/atom_iteration_benchmark.rs +++ b/benches/atom_iteration_benchmark.rs @@ -7,7 +7,7 @@ use rdkit::ROMol; /// Drug-like molecules of varying size for realistic benchmarking. /// These cover common pharmaceutical scaffolds and natural products. const SMILES_SET: &[&str] = &[ - // aspirin (21 atoms with H) + // aspirin "CC(=O)Oc1ccccc1C(=O)O", // ibuprofen "CC(C)Cc1ccc(cc1)C(C)C(=O)O", @@ -15,16 +15,15 @@ const SMILES_SET: &[&str] = &[ "Cn1c(=O)c2c(ncn2C)n(C)c1=O", // diazepam "O=C1CN=C(c2ccccc2)c2cc(Cl)ccc2N1C", - // atorvastatin (lipitor) — large drug molecule + // atorvastatin (lipitor) "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)O", - // taxol core — complex natural product + // taxol core "CC1=C2C(OC(=O)c3ccccc3)C(O)C4(OC(=O)C(O)(CC(OC(=O)c5ccccc5)C1O)C24C)C(=O)c1ccc(OC)cc1", - // vancomycin fragment — large, many atoms + // vancomycin fragment "OC1C(O)C(OC2C(O)C(O)C(O)C(CO)O2)OC(CO)C1NC(=O)C1CC(O)CN1C(=O)C(NC(=O)C1CC(=O)NC(=O)C1O)C(O)c1ccc(O)cc1", ]; -/// Benchmark: parse SMILES into molecules. -/// Baseline cost — everything else is on top of this. +/// Baseline: SMILES parsing cost. #[bench] fn bench_parse_smiles(b: &mut test::bench::Bencher) { b.iter(|| { @@ -34,97 +33,8 @@ fn bench_parse_smiles(b: &mut test::bench::Bencher) { }); } -/// Benchmark: iterate all atoms and read one cheap property (atomic number). -/// Each iteration: N atoms * 2 FFI calls (get_atom + get_atomic_num). -#[bench] -fn bench_atom_one_property(b: &mut test::bench::Bencher) { - let mut mols: Vec = SMILES_SET - .iter() - .map(|s| ROMol::from_smiles(s).unwrap()) - .collect(); - - b.iter(|| { - for mol in &mut mols { - let n = mol.num_atoms(true); - for i in 0..n { - let atom = mol.atom_with_idx(i); - test::black_box(atom.get_atomic_num()); - } - } - }); -} - -/// Benchmark: iterate all atoms, read 7 properties per atom. -/// This is the realistic "featurization" workload — the hot path -/// in ML pipelines, QSAR descriptor computation, etc. -/// Each iteration: N atoms * 8 FFI calls (1 get_atom + 7 properties). -#[bench] -fn bench_atom_all_properties(b: &mut test::bench::Bencher) { - let mut mols: Vec = SMILES_SET - .iter() - .map(|s| ROMol::from_smiles(s).unwrap()) - .collect(); - - b.iter(|| { - for mol in &mut mols { - let n = mol.num_atoms(true); - for i in 0..n { - let atom = mol.atom_with_idx(i); - test::black_box(atom.symbol()); - test::black_box(atom.get_atomic_num()); - test::black_box(atom.get_formal_charge()); - test::black_box(atom.get_is_aromatic()); - test::black_box(atom.get_hybridization_type()); - test::black_box(atom.get_degree()); - test::black_box(atom.get_total_num_hs()); - } - } - }); -} - -/// Benchmark: full pipeline — parse + featurize. -/// Measures end-to-end cost of the most common workflow. -#[bench] -fn bench_parse_and_featurize(b: &mut test::bench::Bencher) { - b.iter(|| { - for smiles in SMILES_SET { - let mut mol = ROMol::from_smiles(smiles).unwrap(); - let n = mol.num_atoms(true); - for i in 0..n { - let atom = mol.atom_with_idx(i); - test::black_box(atom.symbol()); - test::black_box(atom.get_atomic_num()); - test::black_box(atom.get_formal_charge()); - test::black_box(atom.get_is_aromatic()); - test::black_box(atom.get_hybridization_type()); - test::black_box(atom.get_degree()); - test::black_box(atom.get_total_num_hs()); - } - } - }); -} - -/// Benchmark: atom_ref with one property (const path, no &mut needed). -#[bench] -fn bench_atom_ref_one_property(b: &mut test::bench::Bencher) { - let mols: Vec = SMILES_SET - .iter() - .map(|s| ROMol::from_smiles(s).unwrap()) - .collect(); - - b.iter(|| { - for mol in &mols { - let n = mol.num_atoms(true); - for i in 0..n { - let atom = mol.atom_ref(i); - test::black_box(atom.get_atomic_num()); - } - } - }); -} - -/// Benchmark: atom_ref with all 7 properties (const path). -/// Compare directly against bench_atom_all_properties. +/// Iterate all atoms via atom_ref (&self), read 7 properties per atom. +/// This is the realistic featurization workload. #[bench] fn bench_atom_ref_all_properties(b: &mut test::bench::Bencher) { let mols: Vec = SMILES_SET @@ -149,18 +59,17 @@ fn bench_atom_ref_all_properties(b: &mut test::bench::Bencher) { }); } -/// Benchmark: clone + featurize via atom_with_idx (the old &ROMol workflow). -/// This is the cost users pay when they only have &ROMol. +/// Same workload via atom_with_idx (&mut self). +/// Regression guard: should be the same speed as atom_ref. #[bench] -fn bench_clone_and_featurize(b: &mut test::bench::Bencher) { - let mols: Vec = SMILES_SET +fn bench_atom_mut_all_properties(b: &mut test::bench::Bencher) { + let mut mols: Vec = SMILES_SET .iter() .map(|s| ROMol::from_smiles(s).unwrap()) .collect(); b.iter(|| { - for mol in &mols { - let mut mol = mol.clone(); + for mol in &mut mols { let n = mol.num_atoms(true); for i in 0..n { let atom = mol.atom_with_idx(i); @@ -176,9 +85,8 @@ fn bench_clone_and_featurize(b: &mut test::bench::Bencher) { }); } -/// Benchmark: clone cost alone. -/// atom_with_idx requires &mut self, so callers who have &ROMol must clone. -/// This measures that tax. +/// Clone cost alone. Useful for understanding the cost of cloning +/// molecules when only &ROMol is available but mutation is needed. #[bench] fn bench_clone_molecules(b: &mut test::bench::Bencher) { let mols: Vec = SMILES_SET From d1a0fa2a37081f131570c2ec3e369bfb214114d2 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Sat, 14 Mar 2026 20:56:57 -0400 Subject: [PATCH 13/15] Fix broken rdkit-sys link in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crates.io/crate/ → crates.io/crates/ (missing plural) Closes #49 Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3a1a69..137cfa3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ RDKit --- A high-level library for performing common RDKit tasks such as SMILES parsing, molecule normalization, etc. Uses -the C++ API via bindings from [rdkit-sys](https://crates.io/crate/rdkit-sys). +the C++ API via bindings from [rdkit-sys](https://crates.io/crates/rdkit-sys). Notice: Requires rdkit 2023.09.1 or higher (like Ubuntu Noble 24.04) From 7cc300634b32ac6b0f72d00bb6e4b76b94db7357 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Sun, 15 Mar 2026 00:29:30 -0400 Subject: [PATCH 14/15] Add bond access with BondType, BondStereo, and BondDir enums Bond<'a> follows the same Pin<&'a mut> pattern as Atom<'a>. Provides 10 getters: bond_type, bond_type_as_double, begin/end_atom_idx, other_atom_idx, is_aromatic, is_conjugated, stereo, bond_dir, idx. ROMol gains num_bonds(), bond_with_idx(), and bond_between_atoms(). Co-Authored-By: Claude Opus 4.6 (1M context) --- rdkit-sys/src/bridge/bond.rs | 81 ++++++++++++++++++++++++++ rdkit-sys/src/bridge/mod.rs | 3 + rdkit-sys/wrapper/include/bond.h | 27 +++++++++ rdkit-sys/wrapper/src/bond.cc | 40 +++++++++++++ src/graphmol/bond.rs | 65 +++++++++++++++++++++ src/graphmol/mod.rs | 3 + src/graphmol/ro_mol.rs | 21 ++++++- tests/test_bond.rs | 99 ++++++++++++++++++++++++++++++++ 8 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 rdkit-sys/src/bridge/bond.rs create mode 100644 rdkit-sys/wrapper/include/bond.h create mode 100644 rdkit-sys/wrapper/src/bond.cc create mode 100644 src/graphmol/bond.rs create mode 100644 tests/test_bond.rs diff --git a/rdkit-sys/src/bridge/bond.rs b/rdkit-sys/src/bridge/bond.rs new file mode 100644 index 0000000..09ed4b9 --- /dev/null +++ b/rdkit-sys/src/bridge/bond.rs @@ -0,0 +1,81 @@ +#[cxx::bridge(namespace = "RDKit")] +pub mod ffi { + #[repr(i32)] + #[derive(Debug, PartialEq)] + pub enum BondType { + UNSPECIFIED, + SINGLE, + DOUBLE, + TRIPLE, + QUADRUPLE, + QUINTUPLE, + HEXTUPLE, + ONEANDAHALF, + TWOANDAHALF, + THREEANDAHALF, + FOURANDAHALF, + FIVEANDAHALF, + AROMATIC, + IONIC, + HYDROGEN, + THREECENTER, + DATIVEONE, + DATIVE, + DATIVEL, + DATIVER, + OTHER, + ZERO, + } + + #[repr(i32)] + #[derive(Debug, PartialEq)] + pub enum BondStereo { + STEREONONE, + STEREOANY, + STEREOZ, + STEREOE, + STEREOCIS, + STEREOTRANS, + } + + #[repr(i32)] + #[derive(Debug, PartialEq)] + pub enum BondDir { + NONE, + BEGINWEDGE, + BEGINDASH, + ENDDOWNRIGHT, + ENDUPRIGHT, + EITHERDOUBLE, + UNKNOWN, + } + + unsafe extern "C++" { + include!("wrapper/include/bond.h"); + + pub type ROMol = crate::ro_mol_ffi::ROMol; + pub type Bond; + pub type BondType; + pub type BondStereo; + pub type BondDir; + + pub fn get_num_bonds(mol: &SharedPtr, only_heavy: bool) -> u32; + pub fn get_bond_with_idx(mol: &mut SharedPtr, idx: u32) -> Pin<&mut Bond>; + pub fn get_bond_idx_between_atoms( + mol: &SharedPtr, + begin_idx: u32, + end_idx: u32, + ) -> i32; + + pub fn bond_get_bond_type(bond: Pin<&Bond>) -> BondType; + pub fn bond_get_bond_type_as_double(bond: Pin<&Bond>) -> f64; + pub fn bond_get_begin_atom_idx(bond: Pin<&Bond>) -> u32; + pub fn bond_get_end_atom_idx(bond: Pin<&Bond>) -> u32; + pub fn bond_get_other_atom_idx(bond: Pin<&Bond>, this_idx: u32) -> u32; + pub fn bond_get_is_aromatic(bond: Pin<&Bond>) -> bool; + pub fn bond_get_is_conjugated(bond: Pin<&Bond>) -> bool; + pub fn bond_get_stereo(bond: Pin<&Bond>) -> BondStereo; + pub fn bond_get_bond_dir(bond: Pin<&Bond>) -> BondDir; + pub fn bond_get_idx(bond: Pin<&Bond>) -> u32; + } +} diff --git a/rdkit-sys/src/bridge/mod.rs b/rdkit-sys/src/bridge/mod.rs index e020a81..c1b61d3 100644 --- a/rdkit-sys/src/bridge/mod.rs +++ b/rdkit-sys/src/bridge/mod.rs @@ -1,3 +1,6 @@ +mod bond; +pub use bond::ffi as bond_ffi; + mod descriptors; pub use descriptors::ffi as descriptors_ffi; diff --git a/rdkit-sys/wrapper/include/bond.h b/rdkit-sys/wrapper/include/bond.h new file mode 100644 index 0000000..4182fac --- /dev/null +++ b/rdkit-sys/wrapper/include/bond.h @@ -0,0 +1,27 @@ +#pragma once + +#include "rust/cxx.h" +#include + +namespace RDKit { + +using BondType = Bond::BondType; +using BondStereo = Bond::BondStereo; +using BondDir = Bond::BondDir; + +unsigned int get_num_bonds(const std::shared_ptr &mol, bool only_heavy); +Bond &get_bond_with_idx(std::shared_ptr &mol, unsigned int idx); +int get_bond_idx_between_atoms(const std::shared_ptr &mol, unsigned int begin_idx, unsigned int end_idx); + +BondType bond_get_bond_type(const Bond &bond); +double bond_get_bond_type_as_double(const Bond &bond); +unsigned int bond_get_begin_atom_idx(const Bond &bond); +unsigned int bond_get_end_atom_idx(const Bond &bond); +unsigned int bond_get_other_atom_idx(const Bond &bond, unsigned int this_idx); +bool bond_get_is_aromatic(const Bond &bond); +bool bond_get_is_conjugated(const Bond &bond); +BondStereo bond_get_stereo(const Bond &bond); +BondDir bond_get_bond_dir(const Bond &bond); +unsigned int bond_get_idx(const Bond &bond); + +} // namespace RDKit diff --git a/rdkit-sys/wrapper/src/bond.cc b/rdkit-sys/wrapper/src/bond.cc new file mode 100644 index 0000000..3c95ddc --- /dev/null +++ b/rdkit-sys/wrapper/src/bond.cc @@ -0,0 +1,40 @@ +#include "rust/cxx.h" +#include + +namespace RDKit { + +using BondType = Bond::BondType; +using BondStereo = Bond::BondStereo; +using BondDir = Bond::BondDir; + +unsigned int get_num_bonds(const std::shared_ptr &mol, bool only_heavy) { return mol->getNumBonds(only_heavy); } + +Bond &get_bond_with_idx(std::shared_ptr &mol, unsigned int idx) { return *mol->getBondWithIdx(idx); } + +int get_bond_idx_between_atoms(const std::shared_ptr &mol, unsigned int begin_idx, unsigned int end_idx) { + const Bond *bond = mol->getBondBetweenAtoms(begin_idx, end_idx); + if (bond == nullptr) { return -1; } + return static_cast(bond->getIdx()); +} + +BondType bond_get_bond_type(const Bond &bond) { return bond.getBondType(); } + +double bond_get_bond_type_as_double(const Bond &bond) { return bond.getBondTypeAsDouble(); } + +unsigned int bond_get_begin_atom_idx(const Bond &bond) { return bond.getBeginAtomIdx(); } + +unsigned int bond_get_end_atom_idx(const Bond &bond) { return bond.getEndAtomIdx(); } + +unsigned int bond_get_other_atom_idx(const Bond &bond, unsigned int this_idx) { return bond.getOtherAtomIdx(this_idx); } + +bool bond_get_is_aromatic(const Bond &bond) { return bond.getIsAromatic(); } + +bool bond_get_is_conjugated(const Bond &bond) { return bond.getIsConjugated(); } + +BondStereo bond_get_stereo(const Bond &bond) { return bond.getStereo(); } + +BondDir bond_get_bond_dir(const Bond &bond) { return bond.getBondDir(); } + +unsigned int bond_get_idx(const Bond &bond) { return bond.getIdx(); } + +} // namespace RDKit diff --git a/src/graphmol/bond.rs b/src/graphmol/bond.rs new file mode 100644 index 0000000..23ccfaf --- /dev/null +++ b/src/graphmol/bond.rs @@ -0,0 +1,65 @@ +use std::pin::Pin; + +use rdkit_sys::bond_ffi; +pub use rdkit_sys::bond_ffi::{BondDir, BondStereo, BondType}; + +pub struct Bond<'a> { + ptr: Pin<&'a mut bond_ffi::Bond>, +} + +impl<'a> Bond<'a> { + pub fn from_ptr(ptr: Pin<&'a mut bond_ffi::Bond>) -> Self { + Self { ptr } + } + + pub fn bond_type(&self) -> BondType { + bond_ffi::bond_get_bond_type(self.ptr.as_ref()) + } + + pub fn bond_type_as_double(&self) -> f64 { + bond_ffi::bond_get_bond_type_as_double(self.ptr.as_ref()) + } + + pub fn begin_atom_idx(&self) -> u32 { + bond_ffi::bond_get_begin_atom_idx(self.ptr.as_ref()) + } + + pub fn end_atom_idx(&self) -> u32 { + bond_ffi::bond_get_end_atom_idx(self.ptr.as_ref()) + } + + pub fn other_atom_idx(&self, this_idx: u32) -> u32 { + bond_ffi::bond_get_other_atom_idx(self.ptr.as_ref(), this_idx) + } + + pub fn is_aromatic(&self) -> bool { + bond_ffi::bond_get_is_aromatic(self.ptr.as_ref()) + } + + pub fn is_conjugated(&self) -> bool { + bond_ffi::bond_get_is_conjugated(self.ptr.as_ref()) + } + + pub fn stereo(&self) -> BondStereo { + bond_ffi::bond_get_stereo(self.ptr.as_ref()) + } + + pub fn bond_dir(&self) -> BondDir { + bond_ffi::bond_get_bond_dir(self.ptr.as_ref()) + } + + pub fn idx(&self) -> u32 { + bond_ffi::bond_get_idx(self.ptr.as_ref()) + } +} + +impl std::fmt::Debug for Bond<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Bond") + .field("idx", &self.idx()) + .field("type", &self.bond_type()) + .field("begin", &self.begin_atom_idx()) + .field("end", &self.end_atom_idx()) + .finish() + } +} diff --git a/src/graphmol/mod.rs b/src/graphmol/mod.rs index 1b1309b..4aca35b 100644 --- a/src/graphmol/mod.rs +++ b/src/graphmol/mod.rs @@ -4,6 +4,9 @@ pub use atom::*; mod atom_ref; pub use atom_ref::*; +mod bond; +pub use bond::*; + mod mol_ops; pub use mol_ops::*; diff --git a/src/graphmol/ro_mol.rs b/src/graphmol/ro_mol.rs index 045d2ed..f7a45d7 100644 --- a/src/graphmol/ro_mol.rs +++ b/src/graphmol/ro_mol.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter}; use cxx::let_cxx_string; use rdkit_sys::*; -use crate::{Atom, AtomRef, Fingerprint, RWMol}; +use crate::{Atom, AtomRef, Bond, Fingerprint, RWMol}; pub struct ROMol { pub(crate) ptr: cxx::SharedPtr, @@ -103,6 +103,25 @@ impl ROMol { AtomRef::from_ptr(ptr) } + pub fn num_bonds(&self, only_heavy: bool) -> u32 { + rdkit_sys::bond_ffi::get_num_bonds(&self.ptr, only_heavy) + } + + pub fn bond_with_idx(&mut self, idx: u32) -> Bond<'_> { + let ptr = rdkit_sys::bond_ffi::get_bond_with_idx(&mut self.ptr, idx); + Bond::from_ptr(ptr) + } + + pub fn bond_between_atoms(&mut self, begin: u32, end: u32) -> Option> { + let idx = rdkit_sys::bond_ffi::get_bond_idx_between_atoms(&self.ptr, begin, end); + if idx < 0 { + None + } else { + let ptr = rdkit_sys::bond_ffi::get_bond_with_idx(&mut self.ptr, idx as u32); + Some(Bond::from_ptr(ptr)) + } + } + pub fn update_property_cache(&mut self, strict: bool) { ro_mol_ffi::ro_mol_update_property_cache(&mut self.ptr, strict) } diff --git a/tests/test_bond.rs b/tests/test_bond.rs new file mode 100644 index 0000000..27663bd --- /dev/null +++ b/tests/test_bond.rs @@ -0,0 +1,99 @@ +use rdkit::{BondStereo, BondType, ROMol}; + +#[test] +fn test_bond_count() { + let mol = ROMol::from_smiles("CCO").unwrap(); + assert_eq!(mol.num_bonds(true), 2); +} + +#[test] +fn test_bond_type_single() { + let mut mol = ROMol::from_smiles("CC").unwrap(); + let bond = mol.bond_with_idx(0); + assert_eq!(bond.bond_type(), BondType::SINGLE); + assert_eq!(bond.bond_type_as_double(), 1.0); +} + +#[test] +fn test_bond_type_double() { + let mut mol = ROMol::from_smiles("C=C").unwrap(); + let bond = mol.bond_with_idx(0); + assert_eq!(bond.bond_type(), BondType::DOUBLE); +} + +#[test] +fn test_bond_type_triple() { + let mut mol = ROMol::from_smiles("C#C").unwrap(); + let bond = mol.bond_with_idx(0); + assert_eq!(bond.bond_type(), BondType::TRIPLE); +} + +#[test] +fn test_bond_aromatic() { + let mut mol = ROMol::from_smiles("c1ccccc1").unwrap(); + let bond = mol.bond_with_idx(0); + assert!(bond.is_aromatic()); + assert_eq!(bond.bond_type(), BondType::AROMATIC); +} + +#[test] +fn test_bond_atom_indices() { + let mut mol = ROMol::from_smiles("CCO").unwrap(); + let bond = mol.bond_with_idx(0); + assert_eq!(bond.begin_atom_idx(), 0); + assert_eq!(bond.end_atom_idx(), 1); +} + +#[test] +fn test_bond_other_atom_idx() { + let mut mol = ROMol::from_smiles("CCO").unwrap(); + let bond = mol.bond_with_idx(0); + assert_eq!(bond.other_atom_idx(0), 1); + assert_eq!(bond.other_atom_idx(1), 0); +} + +#[test] +fn test_bond_between_atoms() { + let mut mol = ROMol::from_smiles("CCO").unwrap(); + assert!(mol.bond_between_atoms(0, 1).is_some()); + let mut mol2 = ROMol::from_smiles("CCO").unwrap(); + assert!(mol2.bond_between_atoms(0, 2).is_none()); +} + +#[test] +fn test_bond_stereo_none() { + let mut mol = ROMol::from_smiles("CC").unwrap(); + let bond = mol.bond_with_idx(0); + assert_eq!(bond.stereo(), BondStereo::STEREONONE); +} + +#[test] +fn test_bond_conjugated() { + let mut mol = ROMol::from_smiles("C=CC=C").unwrap(); + let bond = mol.bond_with_idx(0); + assert!(bond.is_conjugated()); +} + +#[test] +fn test_bond_idx() { + let mut mol = ROMol::from_smiles("CCCO").unwrap(); + for i in 0..mol.num_bonds(true) { + let bond = mol.bond_with_idx(i); + assert_eq!(bond.idx(), i); + } +} + +#[test] +fn test_bond_count_benzene() { + let mol = ROMol::from_smiles("c1ccccc1").unwrap(); + assert_eq!(mol.num_bonds(true), 6); +} + +#[test] +fn test_bond_debug_format() { + let mut mol = ROMol::from_smiles("CC").unwrap(); + let bond = mol.bond_with_idx(0); + let debug = format!("{:?}", bond); + assert!(debug.contains("Bond")); + assert!(debug.contains("SINGLE")); +} From 05381eb588a23d0a9b08bbca9c80cc0ce69e8d84 Mon Sep 17 00:00:00 2001 From: Sharif Haason Date: Sun, 15 Mar 2026 00:32:07 -0400 Subject: [PATCH 15/15] Add RWMol programmatic molecule editing New: RWMol::new(), add_atom(), add_bond(), remove_atom(), remove_bond(), num_atoms(). Also adds sanitize_mol() and kekulize_mol() which are essential for validating programmatically constructed molecules. Co-Authored-By: Claude Opus 4.6 (1M context) --- rdkit-sys/src/bridge/mol_ops.rs | 3 ++ rdkit-sys/src/bridge/rw_mol.rs | 13 ++++++ rdkit-sys/wrapper/include/mol_ops.h | 2 + rdkit-sys/wrapper/include/rw_mol.h | 8 ++++ rdkit-sys/wrapper/src/mol_ops.cc | 3 ++ rdkit-sys/wrapper/src/rw_mol.cc | 21 ++++++++++ src/graphmol/mol_ops.rs | 8 ++++ src/graphmol/rw_mol.rs | 34 ++++++++++++++- tests/test_rw_mol_editing.rs | 65 +++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 tests/test_rw_mol_editing.rs diff --git a/rdkit-sys/src/bridge/mol_ops.rs b/rdkit-sys/src/bridge/mol_ops.rs index e7e89ce..0cc2657 100644 --- a/rdkit-sys/src/bridge/mol_ops.rs +++ b/rdkit-sys/src/bridge/mol_ops.rs @@ -83,5 +83,8 @@ pub mod ffi { pub fn romol_set_hybridization(mol: &mut SharedPtr); pub fn clean_up(rw_mol: &mut SharedPtr); + + pub fn sanitize_mol(mol: &mut SharedPtr) -> Result<()>; + pub fn kekulize_mol(mol: &mut SharedPtr) -> Result<()>; } } diff --git a/rdkit-sys/src/bridge/rw_mol.rs b/rdkit-sys/src/bridge/rw_mol.rs index 72e4d75..8e32d43 100644 --- a/rdkit-sys/src/bridge/rw_mol.rs +++ b/rdkit-sys/src/bridge/rw_mol.rs @@ -25,5 +25,18 @@ pub mod ffi { pub fn rw_mol_to_ro_mol(mol: SharedPtr) -> SharedPtr; pub fn smarts_to_mol(smarts: &CxxString) -> Result>; + + // Molecule editing + pub fn new_rw_mol() -> SharedPtr; + pub fn rw_mol_add_atom(mol: &mut SharedPtr, atomic_num: u32) -> u32; + pub fn rw_mol_add_bond( + mol: &mut SharedPtr, + begin_idx: u32, + end_idx: u32, + bond_order: i32, + ) -> u32; + pub fn rw_mol_remove_atom(mol: &mut SharedPtr, idx: u32); + pub fn rw_mol_remove_bond(mol: &mut SharedPtr, begin_idx: u32, end_idx: u32); + pub fn rw_mol_get_num_atoms(mol: &SharedPtr, only_explicit: bool) -> u32; } } diff --git a/rdkit-sys/wrapper/include/mol_ops.h b/rdkit-sys/wrapper/include/mol_ops.h index 654bce2..80b72cb 100644 --- a/rdkit-sys/wrapper/include/mol_ops.h +++ b/rdkit-sys/wrapper/include/mol_ops.h @@ -67,4 +67,6 @@ void romol_set_hybridization(std::shared_ptr &mol); // pub fn clean_up(rw_mol: &mut SharedPtr) void clean_up(std::shared_ptr &rw_mol); +void sanitize_mol(std::shared_ptr &mol); +void kekulize_mol(std::shared_ptr &mol); } // namespace RDKit diff --git a/rdkit-sys/wrapper/include/rw_mol.h b/rdkit-sys/wrapper/include/rw_mol.h index a368e8e..7d89041 100644 --- a/rdkit-sys/wrapper/include/rw_mol.h +++ b/rdkit-sys/wrapper/include/rw_mol.h @@ -1,6 +1,7 @@ #pragma once #include "rust/cxx.h" +#include namespace RDKit { std::shared_ptr rw_mol_from_mol_block(const std::string &mol_block, bool sanitize, bool remove_hs, @@ -13,4 +14,11 @@ std::shared_ptr rw_mol_from_rw_mol(const std::shared_ptr &mol); std::shared_ptr rw_mol_to_ro_mol(std::shared_ptr mol); std::shared_ptr smarts_to_mol(const std::string &smarts); +// Molecule editing +std::shared_ptr new_rw_mol(); +unsigned int rw_mol_add_atom(std::shared_ptr &mol, unsigned int atomic_num); +unsigned int rw_mol_add_bond(std::shared_ptr &mol, unsigned int begin_idx, unsigned int end_idx, int bond_order); +void rw_mol_remove_atom(std::shared_ptr &mol, unsigned int idx); +void rw_mol_remove_bond(std::shared_ptr &mol, unsigned int begin_idx, unsigned int end_idx); +unsigned int rw_mol_get_num_atoms(const std::shared_ptr &mol, bool only_explicit); } // namespace RDKit \ No newline at end of file diff --git a/rdkit-sys/wrapper/src/mol_ops.cc b/rdkit-sys/wrapper/src/mol_ops.cc index 66b11f5..1b3e81a 100644 --- a/rdkit-sys/wrapper/src/mol_ops.cc +++ b/rdkit-sys/wrapper/src/mol_ops.cc @@ -106,4 +106,7 @@ std::shared_ptr add_hs(const std::shared_ptr &mol, bool explicit_o void romol_set_hybridization(std::shared_ptr &mol) { MolOps::setHybridization(*mol); } void clean_up(std::shared_ptr &rw_mol) { MolOps::cleanUp(*rw_mol); } +void sanitize_mol(std::shared_ptr &mol) { MolOps::sanitizeMol(*mol); } + +void kekulize_mol(std::shared_ptr &mol) { MolOps::Kekulize(*mol); } } // namespace RDKit \ No newline at end of file diff --git a/rdkit-sys/wrapper/src/rw_mol.cc b/rdkit-sys/wrapper/src/rw_mol.cc index faf0165..b8d8fcd 100644 --- a/rdkit-sys/wrapper/src/rw_mol.cc +++ b/rdkit-sys/wrapper/src/rw_mol.cc @@ -29,4 +29,25 @@ std::shared_ptr rw_mol_from_rw_mol(const std::shared_ptr &mol) { std::shared_ptr rw_mol_to_ro_mol(std::shared_ptr mol) { return std::static_pointer_cast(mol); } std::shared_ptr smarts_to_mol(const std::string &smarts) { return std::shared_ptr(SmartsToMol(smarts)); } +std::shared_ptr new_rw_mol() { return std::shared_ptr(new RWMol()); } + +unsigned int rw_mol_add_atom(std::shared_ptr &mol, unsigned int atomic_num) { + Atom *atom = new Atom(atomic_num); + return mol->addAtom(atom, true, true); +} + +unsigned int rw_mol_add_bond(std::shared_ptr &mol, unsigned int begin_idx, unsigned int end_idx, + int bond_order) { + return mol->addBond(begin_idx, end_idx, static_cast(bond_order)); +} + +void rw_mol_remove_atom(std::shared_ptr &mol, unsigned int idx) { mol->removeAtom(idx); } + +void rw_mol_remove_bond(std::shared_ptr &mol, unsigned int begin_idx, unsigned int end_idx) { + mol->removeBond(begin_idx, end_idx); +} + +unsigned int rw_mol_get_num_atoms(const std::shared_ptr &mol, bool only_explicit) { + return mol->getNumAtoms(only_explicit); +} } // namespace RDKit \ No newline at end of file diff --git a/src/graphmol/mol_ops.rs b/src/graphmol/mol_ops.rs index 774cf85..5d02491 100644 --- a/src/graphmol/mol_ops.rs +++ b/src/graphmol/mol_ops.rs @@ -209,3 +209,11 @@ pub fn set_hybridization(romol: &mut ROMol) { pub fn clean_up(rw_mol: &mut RWMol) { rdkit_sys::mol_ops_ffi::clean_up(&mut rw_mol.ptr); } + +pub fn sanitize_mol(rw_mol: &mut RWMol) -> Result<(), cxx::Exception> { + rdkit_sys::mol_ops_ffi::sanitize_mol(&mut rw_mol.ptr) +} + +pub fn kekulize_mol(rw_mol: &mut RWMol) -> Result<(), cxx::Exception> { + rdkit_sys::mol_ops_ffi::kekulize_mol(&mut rw_mol.ptr) +} diff --git a/src/graphmol/rw_mol.rs b/src/graphmol/rw_mol.rs index b640d50..a13bcb0 100644 --- a/src/graphmol/rw_mol.rs +++ b/src/graphmol/rw_mol.rs @@ -3,13 +3,19 @@ use std::fmt::Formatter; use cxx::{SharedPtr, let_cxx_string}; use rdkit_sys::*; -use crate::ROMol; +use crate::{BondType, ROMol}; pub struct RWMol { pub(crate) ptr: SharedPtr, } impl RWMol { + pub fn new() -> Self { + RWMol { + ptr: rw_mol_ffi::new_rw_mol(), + } + } + pub fn from_mol_block( mol_block: &str, sanitize: bool, @@ -54,6 +60,26 @@ impl RWMol { let ptr = rdkit_sys::rw_mol_ffi::smarts_to_mol(&smarts)?; Ok(RWMol { ptr }) } + + pub fn add_atom(&mut self, atomic_num: u32) -> u32 { + rw_mol_ffi::rw_mol_add_atom(&mut self.ptr, atomic_num) + } + + pub fn add_bond(&mut self, begin: u32, end: u32, order: BondType) -> u32 { + rw_mol_ffi::rw_mol_add_bond(&mut self.ptr, begin, end, order.repr) + } + + pub fn remove_atom(&mut self, idx: u32) { + rw_mol_ffi::rw_mol_remove_atom(&mut self.ptr, idx) + } + + pub fn remove_bond(&mut self, begin: u32, end: u32) { + rw_mol_ffi::rw_mol_remove_bond(&mut self.ptr, begin, end) + } + + pub fn num_atoms(&self, only_explicit: bool) -> u32 { + rw_mol_ffi::rw_mol_get_num_atoms(&self.ptr, only_explicit) + } } impl Clone for RWMol { @@ -69,3 +95,9 @@ impl std::fmt::Debug for RWMol { f.debug_tuple("RWMol").field(&smiles).finish() } } + +impl Default for RWMol { + fn default() -> Self { + Self::new() + } +} diff --git a/tests/test_rw_mol_editing.rs b/tests/test_rw_mol_editing.rs new file mode 100644 index 0000000..0cf3821 --- /dev/null +++ b/tests/test_rw_mol_editing.rs @@ -0,0 +1,65 @@ +use rdkit::{BondType, RWMol, sanitize_mol}; + +#[test] +fn test_new_empty_rw_mol() { + let mol = RWMol::new(); + assert_eq!(mol.num_atoms(true), 0); +} + +#[test] +fn test_add_atoms() { + let mut mol = RWMol::new(); + let c_idx = mol.add_atom(6); + assert_eq!(c_idx, 0); + assert_eq!(mol.num_atoms(true), 1); + let o_idx = mol.add_atom(8); + assert_eq!(o_idx, 1); + assert_eq!(mol.num_atoms(true), 2); +} + +#[test] +fn test_build_ethanol() { + let mut mol = RWMol::new(); + mol.add_atom(6); + mol.add_atom(6); + mol.add_atom(8); + mol.add_bond(0, 1, BondType::SINGLE); + mol.add_bond(1, 2, BondType::SINGLE); + sanitize_mol(&mut mol).unwrap(); + assert_eq!(mol.as_smiles(), "CCO"); +} + +#[test] +fn test_build_ethene() { + let mut mol = RWMol::new(); + mol.add_atom(6); + mol.add_atom(6); + mol.add_bond(0, 1, BondType::DOUBLE); + sanitize_mol(&mut mol).unwrap(); + assert_eq!(mol.as_smiles(), "C=C"); +} + +#[test] +fn test_remove_atom() { + let mut mol = RWMol::new(); + mol.add_atom(6); + mol.add_atom(6); + mol.add_atom(8); + mol.add_bond(0, 1, BondType::SINGLE); + mol.add_bond(1, 2, BondType::SINGLE); + assert_eq!(mol.num_atoms(true), 3); + mol.remove_atom(2); + assert_eq!(mol.num_atoms(true), 2); +} + +#[test] +fn test_remove_bond() { + let mut mol = RWMol::new(); + mol.add_atom(6); + mol.add_atom(6); + mol.add_atom(8); + mol.add_bond(0, 1, BondType::SINGLE); + mol.add_bond(1, 2, BondType::SINGLE); + mol.remove_bond(1, 2); + sanitize_mol(&mut mol).unwrap(); +}