diff --git a/Cargo.toml b/Cargo.toml index 1086859..9896148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rdkit" -version = "0.4.12" +version = "0.4.13" edition = "2021" authors = ["Xavier Lange ", "Javier Pineda SharedPtr; @@ -39,5 +41,28 @@ pub mod ffi { pub type ScaffoldNetworkClass; pub fn default_scaffold_network() -> SharedPtr; + pub fn create_scaffold_network( + mol: &SharedPtr, + params: &SharedPtr, + ) -> Result>; + + pub fn update_scaffold_network( + network: &mut SharedPtr, + mol: &SharedPtr, + params: &SharedPtr, + ) -> Result<()>; + + pub fn scaffold_network_nodes(network: &SharedPtr) -> Vec; + pub fn scaffold_network_counts(network: &SharedPtr) -> Vec; + pub fn scaffold_network_mol_counts(network: &SharedPtr) -> Vec; + pub fn scaffold_network_edge_begin_indices( + network: &SharedPtr, + ) -> Vec; + pub fn scaffold_network_edge_end_indices( + network: &SharedPtr, + ) -> Vec; + pub fn scaffold_network_edge_types( + network: &SharedPtr, + ) -> Vec; } } diff --git a/rdkit-sys/tests/test_scaffold_network.rs b/rdkit-sys/tests/test_scaffold_network.rs index 9819a4d..8ecd1d5 100644 --- a/rdkit-sys/tests/test_scaffold_network.rs +++ b/rdkit-sys/tests/test_scaffold_network.rs @@ -1,5 +1,23 @@ use rdkit_sys::scaffold_network_ffi::*; +/// 2-phenylquinoline: one fused bicycle plus a pendant ring, so the network has +/// something to actually fragment. +const PHENYLQUINOLINE: &str = "c1ccc(-c2ccc3ncccc3c2)cc1"; + +fn mol(smiles: &str) -> cxx::SharedPtr { + cxx::let_cxx_string!(smiles_cxx_string = smiles); + rdkit_sys::ro_mol_ffi::smiles_to_mol(&smiles_cxx_string).unwrap() +} + +/// The stock parameters minus the dummy-atom scaffolds, which keeps the +/// assertions below readable. +fn params_without_generics() -> cxx::SharedPtr { + let mut params = default_scaffold_network_params(); + set_include_generic_scaffolds(&mut params, false); + include_generic_bond_scaffolds(&mut params, false); + params +} + #[test] fn test_scaffold_network() { default_scaffold_network_params(); @@ -18,3 +36,110 @@ fn test_scaffold_network() { let _scaffold_network = default_scaffold_network(); } + +#[test] +fn test_create_scaffold_network() { + let params = params_without_generics(); + let network = create_scaffold_network(&mol(PHENYLQUINOLINE), ¶ms).unwrap(); + + let nodes = scaffold_network_nodes(&network); + assert_eq!( + nodes, + [ + PHENYLQUINOLINE, + "*c1ccccc1", + "c1ccccc1", + "*c1ccc2ncccc2c1", + "c1ccc2ncccc2c1", + ] + ); + + // one entry per node, so the downstream can zip them against `nodes` + assert_eq!(scaffold_network_counts(&network).len(), nodes.len()); + assert_eq!(scaffold_network_mol_counts(&network).len(), nodes.len()); + assert_eq!(scaffold_network_mol_counts(&network), [1, 1, 1, 1, 1]); + + // edges run from the more specific scaffold to the more general one + let begin_indices = scaffold_network_edge_begin_indices(&network); + let end_indices = scaffold_network_edge_end_indices(&network); + let types = scaffold_network_edge_types(&network); + assert_eq!(begin_indices, [0, 1, 0, 3]); + assert_eq!(end_indices, [1, 2, 3, 4]); + assert_eq!( + types, + [ + "Fragment", + "RemoveAttachment", + "Fragment", + "RemoveAttachment" + ] + ); +} + +#[test] +fn test_create_scaffold_network_single_ring() { + let params = params_without_generics(); + let network = create_scaffold_network(&mol("c1ccccc1"), ¶ms).unwrap(); + + // nothing to strip: the molecule is already its own scaffold + assert_eq!(scaffold_network_nodes(&network), ["c1ccccc1"]); + assert!(scaffold_network_edge_begin_indices(&network).is_empty()); +} + +#[test] +fn test_create_scaffold_network_no_rings() { + let params = params_without_generics(); + let network = create_scaffold_network(&mol("CCO"), ¶ms).unwrap(); + + // RDKit still emits a node for a ring-free molecule, it is just the empty + // SMILES + assert_eq!(scaffold_network_nodes(&network), ["CCO", ""]); + assert_eq!(scaffold_network_edge_types(&network), ["Initialize"]); +} + +#[test] +fn test_update_scaffold_network() { + let params = params_without_generics(); + let mut network = create_scaffold_network(&mol("c1ccccc1"), ¶ms).unwrap(); + update_scaffold_network(&mut network, &mol("c1ccc2ncccc2c1"), ¶ms).unwrap(); + + assert_eq!( + scaffold_network_nodes(&network), + ["c1ccccc1", "c1ccc2ncccc2c1"] + ); + assert_eq!(scaffold_network_mol_counts(&network), [1, 1]); +} + +/// `new_scaffold_network_params` hands its argument straight to the +/// `ScaffoldNetworkParams(std::vector)` constructor, so an empty +/// vec leaves RDKit with no bond breaker reactions and nothing ever fragments. +/// Callers who want the stock fragmentation want +/// `default_scaffold_network_params`. +#[test] +fn test_empty_bond_breakers_do_not_fragment() { + let params = new_scaffold_network_params(&vec![]); + let network = create_scaffold_network(&mol(PHENYLQUINOLINE), ¶ms).unwrap(); + assert_eq!(scaffold_network_nodes(&network), [PHENYLQUINOLINE]); + + let params = + new_scaffold_network_params(&vec!["[!#0;R:1]-!@[!#0:2]>>[*:1]-[#0].[#0]-[*:2]".into()]); + let network = create_scaffold_network(&mol(PHENYLQUINOLINE), ¶ms).unwrap(); + assert!(scaffold_network_nodes(&network).len() > 1); +} + +/// RDKit throws when asked for neither flavor of scaffold; the bridge turns +/// that into an `Err` rather than letting it unwind through Rust. +#[test] +fn test_create_scaffold_network_bad_params() { + let mut params = default_scaffold_network_params(); + include_scaffolds_with_attachments(&mut params, false); + include_scaffolds_without_attachments(&mut params, false); + + match create_scaffold_network(&mol(PHENYLQUINOLINE), ¶ms) { + Err(e) => assert_eq!( + e.what(), + "must include at least one of scaffolds with attachments or scaffolds without attachments" + ), + Ok(_) => panic!("expected err variant"), + } +} diff --git a/rdkit-sys/wrapper/include/scaffold_network.h b/rdkit-sys/wrapper/include/scaffold_network.h index 72e9a99..41df54f 100644 --- a/rdkit-sys/wrapper/include/scaffold_network.h +++ b/rdkit-sys/wrapper/include/scaffold_network.h @@ -24,4 +24,24 @@ void collect_mol_counts(std::shared_ptr ¶ms, bool inp using ScaffoldNetworkClass = ScaffoldNetwork::ScaffoldNetwork; std::shared_ptr default_scaffold_network(); + +// RDKit::ScaffoldNetwork::createScaffoldNetwork is a template over a container of molecules, but only +// std::vector (boost::shared_ptr) and std::vector> are explicitly +// instantiated. cxx hands us one std::shared_ptr at a time, so we wrap it in a one element +// vector here rather than fighting cxx over Vec> (see BACKLOG.md). Callers who want +// a network spanning several molecules build it up with update_scaffold_network. +std::shared_ptr create_scaffold_network(const std::shared_ptr &mol, + const std::shared_ptr ¶ms); +void update_scaffold_network(std::shared_ptr &network, const std::shared_ptr &mol, + const std::shared_ptr ¶ms); + +// The ScaffoldNetwork members come back as plain data, one parallel vector per member, so that no +// C++ type crosses the bridge. edges[i] runs from edge_begin_indices[i] to edge_end_indices[i], both +// of which index into the nodes vector. +rust::Vec scaffold_network_nodes(const std::shared_ptr &network); +rust::Vec scaffold_network_counts(const std::shared_ptr &network); +rust::Vec scaffold_network_mol_counts(const std::shared_ptr &network); +rust::Vec scaffold_network_edge_begin_indices(const std::shared_ptr &network); +rust::Vec scaffold_network_edge_end_indices(const std::shared_ptr &network); +rust::Vec scaffold_network_edge_types(const std::shared_ptr &network); } // namespace RDKit \ No newline at end of file diff --git a/rdkit-sys/wrapper/src/scaffold_network.cc b/rdkit-sys/wrapper/src/scaffold_network.cc index 8ac1f77..a70d4e8 100644 --- a/rdkit-sys/wrapper/src/scaffold_network.cc +++ b/rdkit-sys/wrapper/src/scaffold_network.cc @@ -62,4 +62,79 @@ std::shared_ptr default_scaffold_network() { return std::shared_ptr(scaffold_network); } +std::shared_ptr create_scaffold_network(const std::shared_ptr &mol, + const std::shared_ptr ¶ms) { + std::vector> mols{mol}; + ScaffoldNetworkClass *scaffold_network = + new ScaffoldNetworkClass(ScaffoldNetwork::createScaffoldNetwork(mols, *params)); + return std::shared_ptr(scaffold_network); +} + +void update_scaffold_network(std::shared_ptr &network, const std::shared_ptr &mol, + const std::shared_ptr ¶ms) { + std::vector> mols{mol}; + ScaffoldNetwork::updateScaffoldNetwork(mols, *network, *params); +} + +rust::Vec scaffold_network_nodes(const std::shared_ptr &network) { + rust::Vec nodes; + nodes.reserve(network->nodes.size()); + + for (const auto &node : network->nodes) { nodes.push_back(rust::String(node)); } + + return nodes; +} + +rust::Vec scaffold_network_counts(const std::shared_ptr &network) { + rust::Vec counts; + counts.reserve(network->counts.size()); + + for (const auto count : network->counts) { counts.push_back(count); } + + return counts; +} + +rust::Vec scaffold_network_mol_counts(const std::shared_ptr &network) { + rust::Vec mol_counts; + mol_counts.reserve(network->molCounts.size()); + + for (const auto mol_count : network->molCounts) { mol_counts.push_back(mol_count); } + + return mol_counts; +} + +rust::Vec scaffold_network_edge_begin_indices(const std::shared_ptr &network) { + rust::Vec begin_indices; + begin_indices.reserve(network->edges.size()); + + for (const auto &edge : network->edges) { begin_indices.push_back(edge.beginIdx); } + + return begin_indices; +} + +rust::Vec scaffold_network_edge_end_indices(const std::shared_ptr &network) { + rust::Vec end_indices; + end_indices.reserve(network->edges.size()); + + for (const auto &edge : network->edges) { end_indices.push_back(edge.endIdx); } + + return end_indices; +} + +rust::Vec scaffold_network_edge_types(const std::shared_ptr &network) { + rust::Vec types; + types.reserve(network->edges.size()); + + // RDKit ships an operator<< for EdgeType; going through it keeps these strings in step with + // upstream rather than duplicating the enum names on our side. + std::ostringstream type_name; + for (const auto &edge : network->edges) { + type_name.str(""); + type_name << edge.type; + types.push_back(rust::String(type_name.str())); + } + + return types; +} + } // namespace RDKit \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 01adaa4..afe9e4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,9 @@ pub use mol_standardize::*; mod periodic_table; pub use periodic_table::*; +mod scaffold_network; +pub use scaffold_network::*; + mod substruct_match; pub use substruct_match::*; diff --git a/src/scaffold_network.rs b/src/scaffold_network.rs new file mode 100644 index 0000000..6b3f8d8 --- /dev/null +++ b/src/scaffold_network.rs @@ -0,0 +1,261 @@ +//! RDKit's scaffold network (`rdScaffoldNetwork`): the hierarchy of scaffolds a +//! molecule reduces to, built by iteratively breaking its non-ring bonds. +//! +//! Prior art for this class of hierarchy, though not the algorithm implemented +//! here: Wilkens, Janes & Su, "HierS: hierarchical scaffold clustering using +//! topological chemical graphs", J. Med. Chem. 2005, 48(9), 3182-93, +//! doi:10.1021/jm049032d. HierS enumerates ring-delimited substructures +//! directly, where RDKit applies a bond breaking reaction and also emits +//! generic and attachment point scaffolds that HierS has no equivalent of. +//! RDKit's own sources cite no paper for the module. + +use cxx::SharedPtr; +use rdkit_sys::scaffold_network_ffi; + +use crate::ROMol; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum ScaffoldNetworkError { + #[error("could not build scaffold network (exception): {0}")] + CreationException(String), +} + +pub struct ScaffoldNetworkParams { + pub ptr: SharedPtr, +} + +impl Default for ScaffoldNetworkParams { + fn default() -> Self { + ScaffoldNetworkParams::new() + } +} + +impl ScaffoldNetworkParams { + /// RDKit's stock parameters, including its default bond breaker reaction. + pub fn new() -> Self { + let ptr = scaffold_network_ffi::default_scaffold_network_params(); + + ScaffoldNetworkParams { ptr } + } + + /// Stock parameters with the bond breaker reaction(s) replaced. Note that + /// an empty `bond_breaker_smarts` leaves RDKit with nothing to fragment + /// on, so every network comes back as a single node. + pub fn with_bond_breakers(bond_breaker_smarts: &[String]) -> Self { + let ptr = scaffold_network_ffi::new_scaffold_network_params(&bond_breaker_smarts.to_vec()); + + ScaffoldNetworkParams { ptr } + } + + /// Include scaffolds with every atom replaced by a dummy. + pub fn set_include_generic_scaffolds(&mut self, what: bool) { + scaffold_network_ffi::set_include_generic_scaffolds(&mut self.ptr, what) + } + + /// Include scaffolds with every bond replaced by a single bond. + pub fn set_include_generic_bond_scaffolds(&mut self, what: bool) { + scaffold_network_ffi::include_generic_bond_scaffolds(&mut self.ptr, what) + } + + pub fn set_include_scaffolds_without_attachments(&mut self, what: bool) { + scaffold_network_ffi::include_scaffolds_without_attachments(&mut self.ptr, what) + } + + pub fn set_include_scaffolds_with_attachments(&mut self, what: bool) { + scaffold_network_ffi::include_scaffolds_with_attachments(&mut self.ptr, what) + } + + pub fn set_keep_only_first_fragment(&mut self, what: bool) { + scaffold_network_ffi::keep_only_first_fragment(&mut self.ptr, what) + } + + pub fn set_prune_before_fragmenting(&mut self, what: bool) { + scaffold_network_ffi::prune_before_fragmenting(&mut self.ptr, what) + } + + pub fn set_flatten_isotopes(&mut self, what: bool) { + scaffold_network_ffi::flatten_isotopes(&mut self.ptr, what) + } + + pub fn set_flatten_chirality(&mut self, what: bool) { + scaffold_network_ffi::flatten_chirality(&mut self.ptr, what) + } + + pub fn set_flatten_keep_largest(&mut self, what: bool) { + scaffold_network_ffi::flatten_keep_largest(&mut self.ptr, what) + } + + /// Track how many input molecules each scaffold was reached from. With this + /// off RDKit leaves `molCounts` empty and + /// [`ScaffoldNetworkNode::mol_count`] is `None`. + pub fn set_collect_mol_counts(&mut self, what: bool) { + scaffold_network_ffi::collect_mol_counts(&mut self.ptr, what) + } +} + +/// Why RDKit drew an edge between two scaffolds. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScaffoldNetworkEdgeType { + /// molecule -> fragment + Fragment, + /// molecule -> generic molecule (all atoms are dummies) + Generic, + /// molecule -> generic bond molecule (all bonds single) + GenericBond, + /// molecule -> molecule with no attachment points + RemoveAttachment, + /// molecule -> flattened molecule + Initialize, + /// an edge type this crate does not know about yet + Unknown, +} + +impl ScaffoldNetworkEdgeType { + pub fn as_str(&self) -> &'static str { + match self { + ScaffoldNetworkEdgeType::Fragment => "Fragment", + ScaffoldNetworkEdgeType::Generic => "Generic", + ScaffoldNetworkEdgeType::GenericBond => "GenericBond", + ScaffoldNetworkEdgeType::RemoveAttachment => "RemoveAttachment", + ScaffoldNetworkEdgeType::Initialize => "Initialize", + ScaffoldNetworkEdgeType::Unknown => "UNKNOWN", + } + } +} + +impl From<&str> for ScaffoldNetworkEdgeType { + fn from(value: &str) -> Self { + match value { + "Fragment" => ScaffoldNetworkEdgeType::Fragment, + "Generic" => ScaffoldNetworkEdgeType::Generic, + "GenericBond" => ScaffoldNetworkEdgeType::GenericBond, + "RemoveAttachment" => ScaffoldNetworkEdgeType::RemoveAttachment, + "Initialize" => ScaffoldNetworkEdgeType::Initialize, + _ => ScaffoldNetworkEdgeType::Unknown, + } + } +} + +impl std::fmt::Display for ScaffoldNetworkEdgeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScaffoldNetworkNode { + /// Canonical SMILES for the scaffold. Molecules with no rings at all + /// produce an empty scaffold, so this can legitimately be the empty + /// string. + pub scaffold_smiles: String, + /// How many times this scaffold was encountered. + pub count: u32, + /// How many input molecules this scaffold was found in, when the network + /// was built with [`ScaffoldNetworkParams::set_collect_mol_counts`] + /// left on. + pub mol_count: Option, +} + +/// An edge in the scaffold hierarchy, pointing from the more specific scaffold +/// to the more general one. See [`ScaffoldNetworkEdge::child_idx`] and +/// [`ScaffoldNetworkEdge::parent_idx`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScaffoldNetworkEdge { + /// Index into [`ScaffoldNetwork::nodes`] of the scaffold the edge starts + /// at. + pub begin_idx: usize, + /// Index into [`ScaffoldNetwork::nodes`] of the scaffold the edge ends at. + pub end_idx: usize, + pub edge_type: ScaffoldNetworkEdgeType, +} + +impl ScaffoldNetworkEdge { + /// The more specific end of the edge: further down the hierarchy, more + /// rings. + pub fn child_idx(&self) -> usize { + self.begin_idx + } + + /// The more general end of the edge: further up the hierarchy, fewer rings. + pub fn parent_idx(&self) -> usize { + self.end_idx + } +} + +/// The scaffold hierarchy for one or more molecules, as a DAG of scaffold +/// SMILES. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ScaffoldNetwork { + pub nodes: Vec, + pub edges: Vec, +} + +impl ScaffoldNetwork { + fn from_ffi(ptr: &SharedPtr) -> Self { + let scaffold_smiles = scaffold_network_ffi::scaffold_network_nodes(ptr); + let counts = scaffold_network_ffi::scaffold_network_counts(ptr); + let mol_counts = scaffold_network_ffi::scaffold_network_mol_counts(ptr); + + // molCounts is only filled in when collectMolCounts was set, and RDKit leaves + // it empty otherwise rather than zero filling it + let mol_counts_match = mol_counts.len() == scaffold_smiles.len(); + + let nodes = scaffold_smiles + .into_iter() + .enumerate() + .map(|(idx, smiles)| ScaffoldNetworkNode { + scaffold_smiles: smiles, + count: counts.get(idx).copied().unwrap_or_default(), + mol_count: match mol_counts_match { + true => Some(mol_counts[idx]), + false => None, + }, + }) + .collect::>(); + + let begin_indices = scaffold_network_ffi::scaffold_network_edge_begin_indices(ptr); + let end_indices = scaffold_network_ffi::scaffold_network_edge_end_indices(ptr); + let edge_types = scaffold_network_ffi::scaffold_network_edge_types(ptr); + + let edges = begin_indices + .into_iter() + .zip(end_indices) + .zip(edge_types) + .map(|((begin_idx, end_idx), edge_type)| ScaffoldNetworkEdge { + begin_idx, + end_idx, + edge_type: edge_type.as_str().into(), + }) + .collect::>(); + + ScaffoldNetwork { nodes, edges } + } +} + +/// Build the scaffold hierarchy for a single molecule. +pub fn scaffold_network_for_mol( + mol: &ROMol, + params: &ScaffoldNetworkParams, +) -> Result { + let ptr = scaffold_network_ffi::create_scaffold_network(&mol.ptr, ¶ms.ptr) + .map_err(|e| ScaffoldNetworkError::CreationException(e.what().to_owned()))?; + + Ok(ScaffoldNetwork::from_ffi(&ptr)) +} + +/// Build one scaffold hierarchy spanning several molecules, so that shared +/// scaffolds collapse onto a single node and the per-node counts mean +/// something. +pub fn scaffold_network_for_mols( + mols: &[ROMol], + params: &ScaffoldNetworkParams, +) -> Result { + let mut ptr = scaffold_network_ffi::default_scaffold_network(); + + for mol in mols { + scaffold_network_ffi::update_scaffold_network(&mut ptr, &mol.ptr, ¶ms.ptr) + .map_err(|e| ScaffoldNetworkError::CreationException(e.what().to_owned()))?; + } + + Ok(ScaffoldNetwork::from_ffi(&ptr)) +} diff --git a/tests/test_scaffold_network.rs b/tests/test_scaffold_network.rs new file mode 100644 index 0000000..50e7a04 --- /dev/null +++ b/tests/test_scaffold_network.rs @@ -0,0 +1,186 @@ +use rdkit::{ + scaffold_network_for_mol, scaffold_network_for_mols, ROMol, ScaffoldNetworkEdgeType, + ScaffoldNetworkError, ScaffoldNetworkParams, +}; + +/// 2-phenylquinoline: one fused bicycle plus a pendant ring, so the network has +/// something to actually fragment. +const PHENYLQUINOLINE: &str = "c1ccc(-c2ccc3ncccc3c2)cc1"; + +/// The stock parameters minus the dummy-atom scaffolds, which keeps the +/// assertions below readable. +fn params_without_generics() -> ScaffoldNetworkParams { + let mut params = ScaffoldNetworkParams::default(); + params.set_include_generic_scaffolds(false); + params.set_include_generic_bond_scaffolds(false); + params +} + +fn scaffold_smiles(network: &rdkit::ScaffoldNetwork) -> Vec<&str> { + network + .nodes + .iter() + .map(|n| n.scaffold_smiles.as_str()) + .collect() +} + +#[test] +fn test_scaffold_network_for_mol() { + let mol = ROMol::from_smiles(PHENYLQUINOLINE).unwrap(); + let network = scaffold_network_for_mol(&mol, ¶ms_without_generics()).unwrap(); + + assert_eq!( + scaffold_smiles(&network), + [ + PHENYLQUINOLINE, + "*c1ccccc1", + "c1ccccc1", + "*c1ccc2ncccc2c1", + "c1ccc2ncccc2c1", + ] + ); + assert!(network.nodes.iter().all(|n| n.count == 1)); + assert!(network.nodes.iter().all(|n| n.mol_count == Some(1))); + + // every edge runs from a more specific scaffold down to a more general one + let hierarchy = network + .edges + .iter() + .map(|e| { + ( + network.nodes[e.child_idx()].scaffold_smiles.as_str(), + network.nodes[e.parent_idx()].scaffold_smiles.as_str(), + e.edge_type, + ) + }) + .collect::>(); + + assert_eq!( + hierarchy, + [ + ( + PHENYLQUINOLINE, + "*c1ccccc1", + ScaffoldNetworkEdgeType::Fragment + ), + ( + "*c1ccccc1", + "c1ccccc1", + ScaffoldNetworkEdgeType::RemoveAttachment + ), + ( + PHENYLQUINOLINE, + "*c1ccc2ncccc2c1", + ScaffoldNetworkEdgeType::Fragment + ), + ( + "*c1ccc2ncccc2c1", + "c1ccc2ncccc2c1", + ScaffoldNetworkEdgeType::RemoveAttachment + ), + ] + ); +} + +#[test] +fn test_scaffold_network_default_params_include_generics() { + let mol = ROMol::from_smiles(PHENYLQUINOLINE).unwrap(); + let network = scaffold_network_for_mol(&mol, &ScaffoldNetworkParams::default()).unwrap(); + + // the dummy-atom scaffolds are on by default, and hang off their concrete + // counterparts + assert!(scaffold_smiles(&network).contains(&"*1:*:*:*:*:*:1")); + assert!(network + .edges + .iter() + .any(|e| e.edge_type == ScaffoldNetworkEdgeType::Generic)); +} + +#[test] +fn test_scaffold_network_single_ring() { + let mol = ROMol::from_smiles("c1ccccc1").unwrap(); + let network = scaffold_network_for_mol(&mol, ¶ms_without_generics()).unwrap(); + + // nothing to strip: the molecule is already its own scaffold + assert_eq!(scaffold_smiles(&network), ["c1ccccc1"]); + assert!(network.edges.is_empty()); +} + +#[test] +fn test_scaffold_network_no_rings() { + let mol = ROMol::from_smiles("CCO").unwrap(); + let network = scaffold_network_for_mol(&mol, ¶ms_without_generics()).unwrap(); + + // RDKit still emits a node for a ring-free molecule, it is just the empty + // SMILES + assert_eq!(scaffold_smiles(&network), ["CCO", ""]); + assert_eq!( + network.edges[0].edge_type, + ScaffoldNetworkEdgeType::Initialize + ); +} + +#[test] +fn test_scaffold_network_for_mols_shares_nodes() { + let mols = [ + ROMol::from_smiles(PHENYLQUINOLINE).unwrap(), + ROMol::from_smiles("c1ccc(-c2ccccc2)cc1").unwrap(), + ]; + let network = scaffold_network_for_mols(&mols, ¶ms_without_generics()).unwrap(); + + // benzene falls out of both molecules, so it is one node reached from two + // molecules + let benzene = network + .nodes + .iter() + .find(|n| n.scaffold_smiles == "c1ccccc1") + .unwrap(); + assert_eq!(benzene.mol_count, Some(2)); + assert!(benzene.count > 1); +} + +#[test] +fn test_scaffold_network_without_mol_counts() { + let mut params = params_without_generics(); + params.set_collect_mol_counts(false); + + let mol = ROMol::from_smiles(PHENYLQUINOLINE).unwrap(); + let network = scaffold_network_for_mol(&mol, ¶ms).unwrap(); + + assert!(network.nodes.iter().all(|n| n.mol_count.is_none())); + assert!(network.nodes.iter().all(|n| n.count == 1)); +} + +/// An empty bond breaker list leaves RDKit nothing to fragment on, so the whole +/// molecule comes back as a single node. +#[test] +fn test_scaffold_network_with_bond_breakers() { + let mol = ROMol::from_smiles(PHENYLQUINOLINE).unwrap(); + + let params = ScaffoldNetworkParams::with_bond_breakers(&[]); + let network = scaffold_network_for_mol(&mol, ¶ms).unwrap(); + assert_eq!(scaffold_smiles(&network), [PHENYLQUINOLINE]); + + let params = ScaffoldNetworkParams::with_bond_breakers(&[ + "[!#0;R:1]-!@[!#0:2]>>[*:1]-[#0].[#0]-[*:2]".to_string(), + ]); + let network = scaffold_network_for_mol(&mol, ¶ms).unwrap(); + assert!(network.nodes.len() > 1); +} + +/// RDKit throws when asked for neither flavor of scaffold; that surfaces as an +/// `Err` rather than unwinding through Rust. +#[test] +fn test_scaffold_network_bad_params() { + let mut params = ScaffoldNetworkParams::default(); + params.set_include_scaffolds_with_attachments(false); + params.set_include_scaffolds_without_attachments(false); + + let mol = ROMol::from_smiles(PHENYLQUINOLINE).unwrap(); + assert_eq!( + scaffold_network_for_mol(&mol, ¶ms), + Err(ScaffoldNetworkError::CreationException( + "must include at least one of scaffolds with attachments or scaffolds without attachments".to_string() + )) + ); +}