pub use geometric_traits::traits::{
GraphSimilarities, InitialProductVertexOrdering, LargestFragmentMetric, McesBuilder,
McesResult, McesSearchMode,
};
use crate::smiles::Smiles;
impl Smiles {
#[inline]
#[must_use]
pub fn mces(&self, other: &Self) -> McesResult<usize> {
self.mces_with(other).compute()
}
#[inline]
#[must_use]
pub fn mces_with<'g>(&'g self, other: &'g Self) -> SmilesMces<'g> {
SmilesMces::new(self, other)
}
}
#[derive(Clone, Debug)]
pub struct SmilesMces<'g> {
first: &'g Smiles,
second: &'g Smiles,
largest_fragment_metric: LargestFragmentMetric,
product_vertex_ordering: InitialProductVertexOrdering,
partition: bool,
search_mode: McesSearchMode,
delta_y: bool,
ignore_bond_orders: bool,
similarity_threshold: Option<f64>,
distance_threshold: Option<f64>,
search_budget: Option<usize>,
}
impl<'g> SmilesMces<'g> {
#[inline]
#[must_use]
fn new(first: &'g Smiles, second: &'g Smiles) -> Self {
Self {
first,
second,
largest_fragment_metric: LargestFragmentMetric::Edges,
product_vertex_ordering: InitialProductVertexOrdering::None,
partition: true,
search_mode: McesSearchMode::PartialEnumeration,
delta_y: true,
ignore_bond_orders: false,
similarity_threshold: None,
distance_threshold: None,
search_budget: None,
}
}
#[inline]
#[must_use]
pub fn largest_fragment_metric(mut self, metric: LargestFragmentMetric) -> Self {
self.largest_fragment_metric = metric;
self
}
#[inline]
#[must_use]
pub fn product_vertex_ordering(mut self, ordering: InitialProductVertexOrdering) -> Self {
self.product_vertex_ordering = ordering;
self
}
#[inline]
#[must_use]
pub fn partition(mut self, enabled: bool) -> Self {
self.partition = enabled;
self
}
#[inline]
#[must_use]
pub fn search_mode(mut self, search_mode: McesSearchMode) -> Self {
self.search_mode = search_mode;
self
}
#[inline]
#[must_use]
pub fn delta_y(mut self, enabled: bool) -> Self {
self.delta_y = enabled;
self
}
#[inline]
#[must_use]
pub fn ignore_bond_orders(mut self, enabled: bool) -> Self {
self.ignore_bond_orders = enabled;
self
}
#[inline]
#[must_use]
pub fn similarity_threshold(mut self, threshold: f64) -> Self {
self.similarity_threshold = Some(threshold);
self
}
#[inline]
#[must_use]
pub fn distance_threshold(mut self, threshold: f64) -> Self {
self.distance_threshold = Some(threshold);
self
}
#[inline]
#[must_use]
pub fn search_budget(mut self, max_nodes: usize) -> Self {
self.search_budget = Some(max_nodes);
self
}
#[must_use]
pub fn compute(self) -> McesResult<usize> {
let mut builder = McesBuilder::new(self.first, self.second)
.with_largest_fragment_metric(self.largest_fragment_metric)
.with_initial_product_vertex_ordering(self.product_vertex_ordering)
.with_partition(self.partition)
.with_search_mode(self.search_mode)
.with_delta_y(self.delta_y)
.with_ignore_edge_values(self.ignore_bond_orders);
if let Some(threshold) = self.similarity_threshold {
builder = builder.with_similarity_threshold(threshold);
}
if let Some(threshold) = self.distance_threshold {
builder = builder.with_distance_threshold(threshold);
}
if let Some(max_nodes) = self.search_budget {
builder = builder.with_search_budget(max_nodes);
}
builder.compute_labeled()
}
}
#[cfg(test)]
mod tests {
use geometric_traits::traits::GraphSimilarities;
use super::{InitialProductVertexOrdering, LargestFragmentMetric, McesSearchMode};
use crate::smiles::Smiles;
fn smiles(input: &str) -> Smiles {
input.parse().unwrap()
}
#[test]
fn mces_of_identical_molecules_is_a_full_match() {
let benzene = smiles("c1ccccc1");
let result = benzene.mces(&smiles("c1ccccc1"));
assert_eq!(result.matched_edges().len(), 6);
assert!((result.johnson_similarity() - 1.0).abs() < 1.0e-9);
assert!(result.search_completed());
}
#[test]
fn mces_distinguishes_atom_identity() {
let ethanol = smiles("CCO");
let ethylamine = smiles("CCN");
let result = ethanol.mces(ðylamine);
assert_eq!(result.matched_edges().len(), 1);
assert!(result.johnson_similarity() < 1.0);
}
#[test]
fn ignore_bond_orders_collapses_non_aromatic_kekule_distinction() {
let single = smiles("CC");
let double = smiles("C=C");
assert_eq!(single.mces(&double).matched_edges().len(), 0);
assert_eq!(
single.mces_with(&double).ignore_bond_orders(true).compute().matched_edges().len(),
1
);
}
#[test]
fn zero_search_budget_returns_an_incomplete_lower_bound() {
let a = smiles("c1ccccc1C(=O)O");
let b = smiles("c1ccccc1CC(=O)O");
let result = a.mces_with(&b).search_budget(0).compute();
assert!(!result.search_completed());
}
#[test]
fn similarity_threshold_can_short_circuit_dissimilar_molecules() {
let a = smiles("CCCCCCCCCC");
let b = smiles("c1ccccc1");
let result = a.mces_with(&b).similarity_threshold(0.9).compute();
assert_eq!(result.matched_edges().len(), 0);
assert_eq!(result.search_nodes(), 0);
}
#[test]
fn distance_threshold_short_circuits_when_edit_distance_bound_is_exceeded() {
let hexane = smiles("CCCCCC");
let pentane = smiles("CCCCC");
let skipped = hexane.mces_with(&pentane).distance_threshold(0.0).compute();
assert_eq!(skipped.matched_edges().len(), 0);
assert_eq!(skipped.search_nodes(), 0);
let run = hexane.mces_with(&pentane).distance_threshold(100.0).compute();
assert_eq!(run.matched_edges().len(), 4);
assert!(run.search_nodes() > 0);
assert_eq!(hexane.mces(&pentane).matched_edges().len(), 4);
}
#[test]
fn delta_y_filters_the_whitney_triangle_versus_claw_false_positive() {
let cyclopropane = smiles("C1CC1");
let isobutane = smiles("CC(C)C");
let filtered = cyclopropane.mces_with(&isobutane).delta_y(true).compute();
let unfiltered = cyclopropane.mces_with(&isobutane).delta_y(false).compute();
assert_eq!(filtered.matched_edges().len(), 2);
assert_eq!(unfiltered.matched_edges().len(), 3);
}
#[test]
fn all_best_search_mode_enumerates_more_tied_cliques() {
let benzene = smiles("c1ccccc1");
let partial = benzene.mces_with(&smiles("c1ccccc1")).compute();
let all_best =
benzene.mces_with(&smiles("c1ccccc1")).search_mode(McesSearchMode::AllBest).compute();
assert_eq!(partial.matched_edges().len(), 6);
assert_eq!(all_best.matched_edges().len(), 6);
assert_eq!(partial.all_cliques().len(), 1);
assert!(all_best.all_cliques().len() > partial.all_cliques().len());
}
#[test]
fn search_path_tuning_knobs_preserve_the_optimum() {
let acid = smiles("c1ccccc1C(=O)O");
let amide = smiles("c1ccccc1CC(=O)N");
let optimum = acid.mces(&amide).matched_edges().len();
assert_eq!(optimum, 7);
assert_eq!(
acid.mces_with(&amide).partition(false).compute().matched_edges().len(),
optimum
);
assert_eq!(
acid.mces_with(&amide)
.largest_fragment_metric(LargestFragmentMetric::Atoms)
.compute()
.matched_edges()
.len(),
optimum
);
for ordering in [
InitialProductVertexOrdering::None,
InitialProductVertexOrdering::EdgeSignature,
InitialProductVertexOrdering::LineGraphWL,
InitialProductVertexOrdering::Degree,
InitialProductVertexOrdering::PageRank,
] {
let matched = acid
.mces_with(&amide)
.product_vertex_ordering(ordering)
.compute()
.matched_edges()
.len();
assert_eq!(matched, optimum, "ordering {ordering:?} changed the optimum");
}
}
}