diff --git a/Code/GraphMol/Substruct/SubstructMatch.cpp b/Code/GraphMol/Substruct/SubstructMatch.cpp index 9ae0d28329f..91ce283b47a 100644 --- a/Code/GraphMol/Substruct/SubstructMatch.cpp +++ b/Code/GraphMol/Substruct/SubstructMatch.cpp @@ -246,6 +246,9 @@ bool MolMatchFinalCheckFunctor::operator()(const std::uint32_t q_c[], } const Atom *mAt = d_mol.getAtomWithIdx(m_c[i]); if (!detail::hasChiralLabel(mAt)) { + if (d_params.specifiedStereoQueryMatchesUnspecified) { + continue; + } return false; } if (qAt->getDegree() > mAt->getDegree()) { @@ -327,10 +330,15 @@ bool MolMatchFinalCheckFunctor::operator()(const std::uint32_t q_c[], const Bond *mBnd = d_mol.getBondBetweenAtoms( q_to_mol[qBnd->getBeginAtomIdx()], q_to_mol[qBnd->getEndAtomIdx()]); CHECK_INVARIANT(mBnd, "Matching bond not found"); - if (mBnd->getBondType() != Bond::DOUBLE || - qBnd->getStereo() <= Bond::STEREOANY) { + if (mBnd->getBondType() != Bond::DOUBLE) { continue; } + + if (!d_params.specifiedStereoQueryMatchesUnspecified && + mBnd->getStereo() <= Bond::STEREOANY) { + return false; + } + // don't think this can actually happen, but check to be sure: if (mBnd->getStereoAtoms().size() != 2) { continue; @@ -386,6 +394,7 @@ class AtomLabelFunctor { AtomLabelFunctor(const ROMol &query, const ROMol &mol, const SubstructMatchParameters &ps) : d_query(query), d_mol(mol), d_params(ps) {}; + bool operator()(unsigned int i, unsigned int j) const { bool res = false; if (d_params.useChirality) { @@ -393,7 +402,8 @@ class AtomLabelFunctor { if (qAt->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW || qAt->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW) { const Atom *mAt = d_mol.getAtomWithIdx(j); - if (mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CW && + if (!d_params.specifiedStereoQueryMatchesUnspecified && + mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CW && mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CCW) { return false; } @@ -421,6 +431,7 @@ class BondLabelFunctor { qBnd->getStereo() > Bond::STEREOANY) { const Bond *mBnd = d_mol[j]; if (mBnd->getBondType() == Bond::DOUBLE && + !d_params.specifiedStereoQueryMatchesUnspecified && mBnd->getStereo() <= Bond::STEREOANY) { return false; } diff --git a/Code/GraphMol/Substruct/SubstructMatch.h b/Code/GraphMol/Substruct/SubstructMatch.h index c5dcb6645ac..883fd8a9079 100644 --- a/Code/GraphMol/Substruct/SubstructMatch.h +++ b/Code/GraphMol/Substruct/SubstructMatch.h @@ -67,7 +67,10 @@ struct RDKIT_SUBSTRUCTMATCH_EXPORT SubstructMatchParameters { //!< match unsigned int maxRecursiveMatches = 1000; //!< maximum number of matches that the recursive substructure - //!< matching should return + //!< matching should return + bool specifiedStereoQueryMatchesUnspecified = + false; //!< If set, query atoms and bonds with specified stereochemistry + //!< will match atoms and bonds with unspecified stereochemistry SubstructMatchParameters() {} }; diff --git a/Code/GraphMol/Substruct/SubstructUtils.cpp b/Code/GraphMol/Substruct/SubstructUtils.cpp index e4cdae7101a..af6d52bb255 100644 --- a/Code/GraphMol/Substruct/SubstructUtils.cpp +++ b/Code/GraphMol/Substruct/SubstructUtils.cpp @@ -273,6 +273,7 @@ void updateSubstructMatchParamsFromJSON(SubstructMatchParameters ¶ms, PT_OPT_GET(maxMatches); PT_OPT_GET(maxRecursiveMatches); PT_OPT_GET(numThreads); + PT_OPT_GET(specifiedStereoQueryMatchesUnspecified); } std::string substructMatchParamsToJSON(const SubstructMatchParameters ¶ms) { @@ -287,6 +288,7 @@ std::string substructMatchParamsToJSON(const SubstructMatchParameters ¶ms) { PT_OPT_PUT(maxMatches); PT_OPT_PUT(maxRecursiveMatches); PT_OPT_PUT(numThreads); + PT_OPT_PUT(specifiedStereoQueryMatchesUnspecified); std::stringstream ss; boost::property_tree::json_parser::write_json(ss, pt); diff --git a/Code/GraphMol/Substruct/catch_tests.cpp b/Code/GraphMol/Substruct/catch_tests.cpp index 2d4c2b02f6f..ce4a070ae73 100644 --- a/Code/GraphMol/Substruct/catch_tests.cpp +++ b/Code/GraphMol/Substruct/catch_tests.cpp @@ -710,7 +710,7 @@ TEST_CASE("pickling HasPropWithValue queries") { REQUIRE(pklmol.getAtomWithIdx(0)->hasQuery()); REQUIRE(pklmol.getBondWithIdx(0)->hasQuery()); CHECK(SubstructMatch(*target, pklmol, ps).size() == 1); - // make sure we are idempotent in pickling + // make sure we are idempotent in pickling CHECK(SubstructMatch(*target, *mol, ps).size() == 1); } { @@ -720,7 +720,7 @@ TEST_CASE("pickling HasPropWithValue queries") { REQUIRE(pklmol.getAtomWithIdx(0)->hasQuery()); REQUIRE(pklmol.getBondWithIdx(0)->hasQuery()); CHECK(SubstructMatch(*target, pklmol, ps).size() == 0); - // make sure we are idempotent in pickling + // make sure we are idempotent in pickling CHECK(SubstructMatch(*target, mol2, ps).size() == 0); } { @@ -730,8 +730,58 @@ TEST_CASE("pickling HasPropWithValue queries") { REQUIRE(pklmol.getAtomWithIdx(0)->hasQuery()); REQUIRE(pklmol.getBondWithIdx(0)->hasQuery()); CHECK(SubstructMatch(*target, pklmol, ps).size() == 0); - // make sure we are idempotent in pickling + // make sure we are idempotent in pickling CHECK(SubstructMatch(*target, mol3, ps).size() == 0); } } } + +TEST_CASE("specified query matches unspecified atom") { + SECTION("atom basics") { + auto q = "F[C@](Cl)(Br)C"_smarts; + REQUIRE(q); + + auto m1 = "F[C@](Cl)(Br)C"_smiles; + REQUIRE(m1); + auto m2 = "FC(Cl)(Br)C"_smiles; + REQUIRE(m2); + auto m3 = "F[C@@](Cl)(Br)C"_smiles; + REQUIRE(m3); + + SubstructMatchParameters ps; + ps.useChirality = true; + CHECK(SubstructMatch(*m1, *q, ps).size() == 1); + CHECK(SubstructMatch(*m2, *q, ps).empty()); + CHECK(SubstructMatch(*m3, *q, ps).empty()); + + ps.specifiedStereoQueryMatchesUnspecified = true; + CHECK(SubstructMatch(*m1, *q, ps).size() == 1); + CHECK(SubstructMatch(*m2, *q, ps).size() == 1); + CHECK(SubstructMatch(*m3, *q, ps).empty()); + } + SECTION("bond basics") { + auto q = "F/C=C/Br"_smarts; + REQUIRE(q); + + auto m1 = "F/C=C/Br"_smiles; + REQUIRE(m1); + auto m2 = "FC=CBr"_smiles; + REQUIRE(m2); + auto m3 = "F/C=C\\Br"_smiles; + REQUIRE(m3); + + SubstructMatchParameters ps; + ps.useChirality = true; + CHECK(SubstructMatch(*m1, *q, ps).size() == 1); + CHECK(SubstructMatch(*m2, *q, ps).empty()); + CHECK(SubstructMatch(*m3, *q, ps).empty()); + + ps.specifiedStereoQueryMatchesUnspecified = true; + std::cerr << "m1" << std::endl; + CHECK(SubstructMatch(*m1, *q, ps).size() == 1); + std::cerr << "m2" << std::endl; + CHECK(SubstructMatch(*m2, *q, ps).size() == 1); + std::cerr << "m3" << std::endl; + CHECK(SubstructMatch(*m3, *q, ps).empty()); + } +} \ No newline at end of file diff --git a/Code/GraphMol/Wrap/Mol.cpp b/Code/GraphMol/Wrap/Mol.cpp index c8cdea54a7a..6c5f99ef6e7 100644 --- a/Code/GraphMol/Wrap/Mol.cpp +++ b/Code/GraphMol/Wrap/Mol.cpp @@ -334,12 +334,17 @@ struct mol_wrapper { "0 selects the number of concurrent threads supported by the" "hardware. negative values are added to the number of concurrent" "threads supported by the hardware.") + .def_readwrite( + "bondProperties", &RDKit::SubstructMatchParameters::bondProperties, + "bond properties that must be equivalent in order to match.") .def_readwrite( "atomProperties", &RDKit::SubstructMatchParameters::atomProperties, "atom properties that must be equivalent in order to match.") .def_readwrite( - "bondProperties", &RDKit::SubstructMatchParameters::bondProperties, - "bond properties that must be equivalent in order to match.") + "specifiedStereoQueryMatchesUnspecified", + &RDKit::SubstructMatchParameters:: + specifiedStereoQueryMatchesUnspecified, + "If set, query atoms and bonds with specified stereochemistry will match atoms and bonds with unspecified stereochemistry.") .def("setExtraFinalCheck", setSubstructMatchFinalCheck, python::with_custodian_and_ward<1, 2>(), python::args("self", "func"),