pub mod adjacency;
pub mod atom;
pub mod batch;
pub mod bio;
pub mod bond;
pub mod canon_smiles;
pub mod distgeom;
pub mod draw;
pub mod fingerprint;
pub mod hydrogens;
pub mod io;
pub mod kekulize;
pub mod molecule;
mod periodic_table;
pub mod query;
mod query_helpers;
pub mod sanitize;
mod smiles;
pub mod smiles_write;
pub mod stereo;
pub mod valence;
pub use adjacency::{AdjacencyList, NeighborRef};
pub use atom::{Atom, ChiralTag};
pub use batch::{
BatchErrorMode, BatchExportReport, BatchProgress, BatchProgressBar, BatchRecord,
BatchRecordError, BatchValidationError, MoleculeBatch, batch_progress_bar,
};
pub use bond::{Bond, BondDirection, BondOrder, BondStereo};
pub use distgeom::DgBoundsError;
pub use draw::{PreparedDrawAtom, PreparedDrawBond, PreparedDrawMolecule, SvgDrawError};
pub use fingerprint::{
Fingerprint, FingerprintError, MorganAdditionalOutput, MorganAtomInvariantsGenerator,
MorganBondInvariantsGenerator, MorganFingerprintOutput, MorganFingerprintParams,
};
pub use hydrogens::{
AddHydrogensError, RemoveHydrogensError, add_hydrogens_in_place, remove_hydrogens_in_place,
};
pub use molecule::{
ConformerStore, CoordinateDimension, Molecule, PropertyStore, SmilesParseError,
SmilesWriteError, TopologyData,
};
pub use query::{AtomQueryPredicate, BondQueryPredicate, QueryNode};
pub use sanitize::{SanitizeError, SanitizeOps, SanitizeStep};
pub use smiles::assign_double_bond_stereo_from_directions;
pub use smiles_write::SmilesWriteParams;
pub use stereo::{LigandRef, TetrahedralStereo};
pub use valence::{
ValenceAssignment, ValenceError, ValenceModel, assign_radicals_rdkit_2025, assign_valence,
rdkit_valence_list,
};
#[must_use]
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[cfg(test)]
mod tests {
use super::{Atom, Bond, BondOrder, Molecule, version};
#[test]
fn version_is_not_empty() {
assert!(!version().is_empty());
}
#[test]
fn molecule_from_smiles_parses_basic_chain() {
let mol = Molecule::from_smiles("CCO").expect("basic SMILES should parse");
assert_eq!(mol.atomic_numbers(), vec![6, 6, 8]);
}
#[test]
fn api_surface_types_exist() {
let _atom = Atom {
index: 0,
atomic_num: 6,
is_aromatic: false,
formal_charge: 0,
explicit_hydrogens: 0,
no_implicit: false,
num_radical_electrons: 0,
chiral_tag: super::ChiralTag::Unspecified,
isotope: None,
atom_map_num: None,
props: Default::default(),
query: None,
rdkit_cip_rank: None,
};
let _bond = Bond {
index: 0,
begin_atom: 0,
end_atom: 1,
order: BondOrder::Single,
is_aromatic: false,
direction: super::BondDirection::None,
stereo: super::BondStereo::None,
stereo_atoms: Vec::new(),
molfile_query_bond_code: None,
props: Default::default(),
query: None,
};
let _mol = Molecule::default();
}
#[test]
fn molecule_from_smiles_parses_bracket_charge() {
let mol = Molecule::from_smiles("[N-]=[N+]=N").expect("charged atoms should parse");
assert_eq!(mol.atomic_numbers(), vec![7, 7, 7]);
assert_eq!(mol.atoms()[0].formal_charge, -1);
assert_eq!(mol.atoms()[1].formal_charge, 1);
}
#[test]
fn molecule_from_smiles_sanitize_flag_controls_cleanup_pipeline() {
let raw = Molecule::from_smiles_with_sanitize("CN(=O)=O", false)
.expect("unsanitized nitro SMILES should parse");
assert_eq!(
raw.atoms()
.iter()
.map(|atom| atom.formal_charge)
.collect::<Vec<_>>(),
vec![0, 0, 0, 0]
);
let sanitized = raw.sanitize().expect("sanitize should clean nitro form");
assert_eq!(
sanitized
.atoms()
.iter()
.map(|atom| atom.formal_charge)
.collect::<Vec<_>>(),
vec![0, 1, -1, 0]
);
assert_eq!(
raw.atoms()
.iter()
.map(|atom| atom.formal_charge)
.collect::<Vec<_>>(),
vec![0, 0, 0, 0],
"COW sanitize must not mutate the original molecule"
);
}
#[test]
fn molecule_from_smiles_parses_aromatic_ring_order() {
let mol = Molecule::from_smiles("O=c1cc(O)c2ccccc2o1").expect("aromatic ring should parse");
assert_eq!(
mol.atomic_numbers(),
vec![8, 6, 6, 6, 8, 6, 6, 6, 6, 6, 6, 8]
);
}
#[test]
fn molecule_from_smiles_unsanitized_keeps_non_ring_aromatic_atoms() {
let mol = Molecule::from_smiles_with_sanitize("cc", false)
.expect("unsanitized non-ring aromatic input should parse like RDKit");
assert_eq!(mol.atomic_numbers(), vec![6, 6]);
assert!(mol.atoms().iter().all(|atom| atom.is_aromatic));
}
#[test]
fn molecule_from_smiles_sanitized_rejects_non_ring_aromatic_atoms() {
Molecule::from_smiles("cc")
.expect_err("sanitized non-ring aromatic input should fail like RDKit");
}
#[test]
fn molecule_from_smiles_parses_fragment_dummy_and_dative_subset() {
let mol = Molecule::from_smiles("[*:1]C.[NH3]->[Cu+2]<-[NH3]")
.expect("representative special subset should parse");
assert_eq!(mol.atomic_numbers(), vec![0, 6, 7, 29, 7]);
}
#[test]
fn molecule_from_smiles_does_not_aromatize_saturated_n_heterocycle() {
let mol = Molecule::from_smiles("CN1CCCC1").expect("saturated ring should parse");
assert!(
mol.atoms().iter().all(|atom| !atom.is_aromatic),
"saturated N-heterocycle atoms should not be aromatic"
);
assert!(
mol.bonds().iter().all(|bond| !bond.is_aromatic),
"saturated N-heterocycle bonds should not be aromatic"
);
}
#[test]
fn kekulize_in_place_with_clear_aromatic_flags_true_clears_aromatic_marks() {
let mut mol = Molecule::from_smiles("c1ccccc1").expect("benzene should parse");
super::kekulize::kekulize_in_place(&mut mol, true).expect("kekulize should succeed");
assert!(
mol.atoms().iter().all(|atom| !atom.is_aromatic),
"clearAromaticFlags=true should clear aromatic atom flags"
);
assert!(
mol.bonds().iter().all(|bond| !bond.is_aromatic),
"clearAromaticFlags=true should remove aromatic bond order annotations"
);
}
#[test]
fn kekulize_in_place_with_clear_aromatic_flags_false_preserves_aromatic_marks() {
let mut mol = Molecule::from_smiles("c1ccccc1").expect("benzene should parse");
super::kekulize::kekulize_in_place(&mut mol, false).expect("kekulize should succeed");
assert!(
mol.atoms().iter().all(|atom| atom.is_aromatic),
"clearAromaticFlags=false should preserve aromatic atom flags"
);
assert!(
mol.bonds().iter().all(|bond| bond.is_aromatic),
"clearAromaticFlags=false should preserve aromatic bond flags"
);
assert!(
mol.bonds()
.iter()
.all(|bond| !matches!(bond.order, super::BondOrder::Aromatic)),
"clearAromaticFlags=false should still rewrite aromatic bond types to single/double"
);
}
#[test]
fn molecule_to_smiles_writes_basic_chain() {
let mol = Molecule::from_smiles("CCO").expect("SMILES parser should parse CCO");
let smiles = mol
.to_smiles(true)
.expect("SMILES writer should serialize CCO");
assert_eq!(smiles, "CCO");
}
}