// // Copyright (c) 2019-2025 Greg Landrum and other RDKit contributors // // @@ All Rights Reserved @@ // This file is part of the RDKit. // The contents are covered by the terms of the BSD license // which is included in the file license.txt, found at the root // of the RDKit source tree. /// #include #include #include #include #include #include #include #include #include #include #include using namespace RDKit; using std::unique_ptr; TEST_CASE("Github #1039") { const auto useLegacy = GENERATE(true, false); UseLegacyStereoPerceptionFixture fx(useLegacy); SECTION("double bond") { auto m1 = "C/C=C/C=C/C"_smiles; REQUIRE(m1); std::vector bonds = {2}; std::unique_ptr pieces(MolFragmenter::fragmentOnBonds(*m1, bonds)); REQUIRE(pieces); CHECK(pieces->getNumAtoms() == 8); REQUIRE(pieces->getBondBetweenAtoms(3, 6)); REQUIRE(pieces->getBondBetweenAtoms(2, 7)); CHECK(pieces->getBondBetweenAtoms(3, 6)->getBondType() == Bond::SINGLE); CHECK(pieces->getBondBetweenAtoms(3, 6)->getBondDir() == Bond::ENDUPRIGHT); CHECK(pieces->getBondBetweenAtoms(2, 7)->getBondType() == Bond::SINGLE); CHECK(pieces->getBondBetweenAtoms(2, 7)->getBondDir() == Bond::ENDUPRIGHT); CHECK(MolToSmiles(*pieces) == "[2*]/C=C/C.[3*]/C=C/C"); } SECTION("atomic stereo") { auto m1 = "C(C)(F)(Cl)O"_smiles; REQUIRE(m1); m1->getBondWithIdx(0)->setBondDir(Bond::BEGINWEDGE); std::vector bonds = {0}; std::unique_ptr pieces(MolFragmenter::fragmentOnBonds(*m1, bonds)); REQUIRE(pieces); CHECK(pieces->getNumAtoms() == 7); REQUIRE(pieces->getBondBetweenAtoms(0, 6)); REQUIRE(pieces->getBondBetweenAtoms(1, 5)); CHECK(pieces->getBondBetweenAtoms(0, 6)->getBondDir() == Bond::BEGINWEDGE); CHECK(pieces->getBondBetweenAtoms(1, 5)->getBondDir() == Bond::NONE); // no actual stereo in the SMILES here since we haven't assigned it (need a // conformer to do that using wedging) CHECK(MolToSmiles(*pieces) == "*C.[1*]C(O)(F)Cl"); } SECTION("bond stereo") { auto m = "O/C=N/C=C"_smiles; std::vector> dummyLabels{{1, 1}}; std::vector bonds{0}; auto resa = RDKit::MolFragmenter::fragmentOnBonds(*m, bonds); CHECK(MolToSmiles(*resa) == "*/C=N/C=C.[1*]O"); // make sure we still have stereo atoms std::vector> expected_stereo_atoms{ {5, 3}, // 5 is the new dummy atom, it was 0 before {}, {}, {}, {}, }; std::vector> received_stereo; for (auto *bond : resa->bonds()) { received_stereo.push_back(bond->getStereoAtoms()); } CHECK(received_stereo == expected_stereo_atoms); delete resa; } { // break non stereo atom bond auto m = "C/C(O)=N/C=C"_smiles; std::vector> dummyLabels{{1, 1}}; std::vector bonds{0}; auto resa = RDKit::MolFragmenter::fragmentOnBonds(*m, bonds); CHECK(MolToSmiles(*resa) == "*/C(O)=N/C=C.[1*]C"); // make sure we still have stereo atoms std::vector> expected_stereo_atoms; if (useLegacy) { expected_stereo_atoms = {{}, {2, 4}, {}, {}, {}, {}}; } else { expected_stereo_atoms = {{}, {6, 4}, {}, {}, {}, {}}; } std::vector> received_stereo; for (auto *bond : resa->bonds()) { received_stereo.push_back(bond->getStereoAtoms()); } CHECK(received_stereo == expected_stereo_atoms); delete resa; } { // bond stereo should only be removed when deleting the double bond with // E/Z auto m = "O/C=N/C=C"_smiles; std::vector> dummyLabels{{1, 1}}; std::vector expected = { "*/C=N/C=C.[1*]O", "[1*]=NC=C.[2*]=CO", // bond stereo gone "[2*]C=C.[3*]/N=C/O", "[3*]=C.[4*]=C/N=C/O"}; for (unsigned int i = 0; i < m->getNumBonds(); ++i) { std::vector bonds{i}; auto resa = RDKit::MolFragmenter::fragmentOnBonds(*m, bonds); auto smiles = MolToSmiles(*resa); CHECK(smiles == expected[i]); delete resa; } } { // bond stereo should only be removed when deleting the double bond with // E/Z // chiral stereo should stay auto m = "O/C=N/[C@H](I)F"_smiles; std::vector> dummyLabels{{1, 1}}; std::vector expected = { "*/C=N/[C@@H](F)I.[1*]O", "[1*]=N[C@@H](F)I.[2*]=CO", // bond stereo gone "[2*][C@@H](F)I.[3*]/N=C/O", "[3*]I.[4*][C@H](F)/N=C/O", "[3*]F.[5*][C@@H](I)/N=C/O"}; for (unsigned int i = 0; i < m->getNumBonds(); ++i) { std::vector bonds{i}; auto resa = RDKit::MolFragmenter::fragmentOnBonds(*m, bonds); auto smiles = MolToSmiles(*resa); CHECK(smiles == expected[i]); delete resa; } } } TEST_CASE("molzip") { const auto useLegacy = GENERATE(true, false); CAPTURE(useLegacy); UseLegacyStereoPerceptionFixture fx(useLegacy); SECTION("basic tests") { { auto a = "C[*:1]"_smiles; auto b = "N[*:1]"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "CN"); } { // 0 isotopes aren't mapped auto a = "C[*]"_smiles; auto b = "N[*]"_smiles; MolzipParams p; auto mol = molzip(*a, *b, p); CHECK(MolToSmiles(*mol) == "*C.*N"); } { auto a = "C[1*]"_smiles; auto b = "N[1*]"_smiles; MolzipParams p; p.label = MolzipLabel::Isotope; auto mol = molzip(*a, *b, p); CHECK(MolToSmiles(*mol) == "CN"); } { auto a = "[C@H](Br)([*:1])F"_smiles; auto b = "[*:1]N"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "N[C@@H](F)Br"); } { auto a = "[C@H]([*:1])(Br)F"_smiles; auto b = "[*:1]N"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "N[C@H](F)Br"); } { auto a = "[C@H]([*:1])(F)([*:2])"_smiles; auto b = "[*:1]N.[*:2]I"_smiles; auto mol = molzip(*a, *b); REQUIRE(MolToSmiles(*mol) == "N[C@@H](F)I"); } { auto a = "[C@H]([Xe])(F)([V])"_smiles; auto b = "[Xe]N.[V]I"_smiles; MolzipParams params; params.label = MolzipLabel::AtomType; params.atomSymbols = {"Xe", "V"}; auto mol = molzip(*a, *b, params); CHECK(MolToSmiles(*mol) == "N[C@@H](F)I"); } { auto m = "OOO[C@](F)(I)N"_smiles; std::vector> dummyLabels{{1, 1}, {2, 2}}; for (unsigned int i = 0; i < m->getNumBonds(); ++i) { for (unsigned int j = 0; j < m->getNumBonds(); ++j) { if (i != j) { std::vector bonds{i, j}; auto resa = RDKit::MolFragmenter::fragmentOnBonds(*m, bonds); MolzipParams p; p.label = MolzipLabel::FragmentOnBonds; CHECK(MolToSmiles(*molzip(*resa, p)) == MolToSmiles(*m)); delete resa; // Now try using atom labels auto res = RDKit::MolFragmenter::fragmentOnBonds(*m, bonds, true, &dummyLabels); for (auto *atom : res->atoms()) { if (atom->getIsotope()) { atom->setAtomMapNum(atom->getIsotope()); } } CHECK(MolToSmiles(*molzip(*res)) == MolToSmiles(*m)); delete res; } } } } } SECTION("use atom property as label") { auto a = "[C@H]([*])(F)([*])"_smiles; auto b = "[*]N.[*]I"_smiles; a->getAtomWithIdx(1)->setProp("foo", 1); a->getAtomWithIdx(3)->setProp("foo", 2); b->getAtomWithIdx(0)->setProp("foo", 1); b->getAtomWithIdx(2)->setProp("foo", 2); MolzipParams p; p.label = MolzipLabel::AtomProperty; p.atomProperty = "foo"; auto mol = molzip(*a, *b, p); // chirality is "lost" here because [C@H]([*])(F)([*]) is considered // achiral CHECK(MolToSmiles(*mol) == "NC(F)I"); } SECTION("test bond stereo") { { auto a = "F/C=C/[*:1]"_smiles; auto b = "[*:1]F"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "F/C=C/F"); } { auto a = "C=C/N=C/[*:1]"_smiles; auto b = "O[*:1]"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "C=C/N=C/O"); } { auto a = "C=C[*:1].O/C=N/[*:1]"_smiles; auto mol = molzip(*a); CHECK(MolToSmiles(*mol) == "C=C/N=C/O"); } { // test single mol isotope labels auto a = "C=C[1*].O/C=N/[1*]"_smiles; MolzipParams p; p.label = MolzipLabel::Isotope; auto mol = molzip(*a, p); CHECK(MolToSmiles(*mol) == "C=C/N=C/O"); } { // double bondd stereo not handled // auto m = "O/C=N/C=C/F"_smiles; auto m = "O/C=N/C=C"_smiles; std::vector> dummyLabels{{1, 1}}; for (unsigned int i = 0; i < m->getNumBonds(); ++i) { std::vector bonds{i}; { std::unique_ptr resa{ RDKit::MolFragmenter::fragmentOnBonds(*m, bonds)}; auto smiles = MolToSmiles(*resa); if (std::count(smiles.begin(), smiles.end(), '/') != 2) { continue; // we removed bond stereo in fragment to bonds! } MolzipParams p; p.label = MolzipLabel::FragmentOnBonds; CHECK(MolToSmiles(*molzip(*resa, p)) == MolToSmiles(*m)); } { // Now try using atom labels std::unique_ptr res{RDKit::MolFragmenter::fragmentOnBonds( *m, bonds, true, &dummyLabels)}; auto smiles = MolToSmiles(*res); if (std::count(smiles.begin(), smiles.end(), '/') != 2) { continue; // we removed bond stereo in fragment to bonds! } for (auto *atom : res->atoms()) { if (atom->getIsotope()) { atom->setAtomMapNum(atom->getIsotope()); } } CHECK(MolToSmiles(*molzip(*res)) == MolToSmiles(*m)); } } } } SECTION("unzippable molecules") { auto a = "C[*:1]"_smiles; auto b = "N[*:2]"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "C[*:1].N[*:2]"); } { auto a = "[*:2]OC[*:1]"_smiles; auto b = "N[*:1]"_smiles; auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "NCO[*:2]"); } { auto a = "[*:1]OC[*:1]"_smiles; auto b = "N[*:1]"_smiles; bool caught = false; try { auto mol = molzip(*a, *b); } catch (Invar::Invariant &e) { CHECK(e.toUserString().find( "molzip: bond info already exists for end atom with label:1") != std::string::npos); caught = true; } CHECK(caught == true); } { // check to see we can make a non sanitizable zipped mol MolzipParams p; p.enforceValenceRules = false; auto a = "CC(=[*:1])N"_smiles; auto b = "[*:1]-N=C"_smiles; auto mol = molzip(*a, *b, p); } { // check atom property zipping auto a = "C=C*.O/C=N/*"_smiles; a->getAtomWithIdx(2)->setProp("fuse", 1); a->getAtomWithIdx(6)->setProp("fuse", 1); MolzipParams p; p.atomProperty = "fuse"; p.label = MolzipLabel::AtomProperty; auto mol = molzip(*a, p); CHECK(MolToSmiles(*mol) == "C=C/N=C/O"); } SECTION("MolZip saves bonddir") { { // a-* b<* auto a = "CC[*:1]"_smiles; auto b = "N[*:1]"_smiles; b->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE); auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "CCN"); CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE); CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 2); } { // a>* b-* auto a = "[*:1]CC"_smiles; auto b = "N[*:1]"_smiles; a->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE); auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "CCN"); CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE); CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 2); } { // a<* b-* auto a = "CC[*:1]"_smiles; auto b = "N[*:1]"_smiles; a->getBondWithIdx(1)->setBondDir(Bond::BondDir::BEGINWEDGE); auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "CCN"); CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE); CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 1); } { // a>* b-* auto a = "[*:1]CC"_smiles; auto b = "N[*:1]"_smiles; a->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE); auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "CCN"); CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE); CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 2); } { // a-* b<* auto a = "CC[*:1]"_smiles; auto b = "[*:1]N"_smiles; b->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE); auto mol = molzip(*a, *b); CHECK(MolToSmiles(*mol) == "CCN"); CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE); CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 1); } } } TEST_CASE( "Github4825: ReplaceCore should set stereo on ring bonds when it breaks " "rings") { SECTION("basics") { auto m = "C1C=CCC2=C1C=CC=N2"_smiles; REQUIRE(m); auto core = "c1ncccc1"_smiles; REQUIRE(core); std::unique_ptr res{replaceCore(*m, *core)}; REQUIRE(res); auto mb = MolToV3KMolBlock(*res); CHECK(mb.find("CFG=2") == std::string::npos); } SECTION("adjacent") { auto m = "C1CC2=C(C=C1)C=CC=N2"_smiles; REQUIRE(m); auto core = "c1ncccc1"_smiles; REQUIRE(core); std::unique_ptr res{replaceCore(*m, *core)}; REQUIRE(res); auto mb = MolToV3KMolBlock(*res); CHECK(mb.find("CFG=2") == std::string::npos); } SECTION("don't do larger rings") { auto m = "C1C=CCCCC2=C1C=CC=N2"_smiles; REQUIRE(m); auto core = "c1ncccc1"_smiles; REQUIRE(core); std::unique_ptr res{replaceCore(*m, *core)}; REQUIRE(res); auto mb = MolToV3KMolBlock(*res); CHECK(mb.find("CFG=2") != std::string::npos); } } TEST_CASE( "Github5334: ReplaceCore should set stereo on ring bonds when it breaks " "rings") { SECTION("segfault") { auto a = "C([*:1])[*:2].[C@@H](Cl)([1*:1])[2*:2]"_smiles; bool caught = false; try { auto mol = molzip(*a); CHECK(false); } catch (Invar::Invariant &e) { CHECK( e.toUserString().find( "molzip: zipped Bond already exists, perhaps labels are duplicated") != std::string::npos); caught = true; } CHECK(caught); } } TEST_CASE( "ReplaceCore handles chiral center with multiple bonds from core to chiral center") { auto structure = "C1CSCN[C@@]12(NCCCO2)"_smiles; auto core = "NCSCC"_smarts; std::unique_ptr res{replaceCore(*structure, *core, true, true)}; REQUIRE(res); auto resultSmiles = MolToSmiles(*res); REQUIRE(resultSmiles == "*[C@]1([4*])NCCCO1"); } TEST_CASE("Molzip with 2D coordinates", "[molzip]") { std::vector frags = {"c1nc([1*:1])c([2*:2])c([3*:3])n1", "CC(C)(C#N)c1ccc([1*:1])cc1", "Nc1ccc(C#C[2*:2])cn1", "OCC[3*:3]"}; std::vector mols = { ROMOL_SPTR(SmilesToMol(frags[0])), ROMOL_SPTR(SmilesToMol(frags[1])), ROMOL_SPTR(SmilesToMol(frags[2])), ROMOL_SPTR(SmilesToMol(frags[3]))}; MolzipParams params; params.generateCoordinates = true; const auto zippedMol = molzip(mols, params); for (auto &mol : mols) { for (auto &atom : mol->atoms()) { atom->setIsotope(0); atom->setAtomMapNum(0); } } const auto &zippedConformer = zippedMol->getConformer(); for (size_t i = 0; i < frags.size(); i++) { const auto sma = std::regex_replace(frags[i], std::regex(R"(\[\d\*\:\d\])"), "*"); const auto query = SmartsToMol(sma); const auto &mol = mols[i]; const auto &molConformer = mol->getConformer(); const auto molMatches = SubstructMatch(*mol, *query); const auto zippedMatches = SubstructMatch(*zippedMol, *query); REQUIRE(molMatches.size() == 1); REQUIRE(zippedMatches.size() == 1); const auto molMatch = molMatches[0]; const auto zippedMatch = zippedMatches[0]; for (size_t j = 0; j < molMatch.size(); j++) { const auto &position1 = molConformer.getAtomPos(molMatch[i].second); const auto &position2 = zippedConformer.getAtomPos(zippedMatch[i].second); CHECK(position1.x == position2.x); CHECK(position1.y == position2.y); CHECK(position1.z == position2.z); } delete query; } } TEST_CASE("Molzip with split rings from rgroup", "[molzip]") { std::vector frags = {"[*:1]CC[*:2]", "[*:1]NN[*:2]", "[*:1]NN[*:2]"}; std::vector mols; for (auto smi : frags) { mols.push_back(ROMOL_SPTR(SmilesToMol(smi))); } const auto zippedMol = molzip(mols); CHECK(MolToSmiles(*zippedMol) == "C1CNN1"); } TEST_CASE("Github #6034: FragmentOnBonds may create unexpected radicals") { auto m = "C[C@H](Cl)c1ccccc1"_smiles; REQUIRE(m); REQUIRE(m->getNumAtoms() == 9); REQUIRE(m->getAtomWithIdx(1)->getNoImplicit() == true); bool add_dummies = false; std::vector bonds = {0}; std::vector> *dummyLabels = nullptr; const std::vector *bondTypes = nullptr; std::vector nCutsPerAtom(m->getNumAtoms(), 0); std::unique_ptr pieces(MolFragmenter::fragmentOnBonds( *m, bonds, add_dummies, dummyLabels, bondTypes, &nCutsPerAtom)); REQUIRE(pieces); REQUIRE(nCutsPerAtom == std::vector{1, 1, 0, 0, 0, 0, 0, 0, 0}); for (auto at : pieces->atoms()) { INFO("atom " + std::to_string(at->getIdx())); if (at->getAtomicNum() == 6) { CHECK(at->getNoImplicit() == (at->getIdx() == 1)); CHECK(at->getTotalValence() == 4); } } } TEST_CASE( "GitHub #7327: SaltRemover may clear computed properties even if no atoms are removed", "[bug]") { auto m = "C=CC=O"_smiles; REQUIRE(m); REQUIRE(m->getRingInfo()->isSymmSssr()); auto q = "[O,N]"_smarts; REQUIRE(q); bool onlyFrags = true; std::unique_ptr m2{deleteSubstructs(*m, *q, onlyFrags)}; REQUIRE(m2); CHECK(m2->getNumAtoms() == m->getNumAtoms()); // No atoms removed CHECK(m2->getRingInfo()->isSymmSssr()); } TEST_CASE( "Github #8288: molzip add linker bond functionality (fixes memory issue)", "[feature,bug]") { auto m = "[*:1][*:2].C[*:1].S[*:2]"_smiles; auto res = molzip(*m); CHECK(MolToSmiles(*res) == "CS"); } TEST_CASE( "Github #8389: FragmentOnBonds does not preserve bond stereo when useLegacy=False") { auto useLegacy = GENERATE(true, false); CAPTURE(useLegacy); UseLegacyStereoPerceptionFixture fx(useLegacy); SECTION("as reported ") { auto m = "CC/C=C/N"_smiles; REQUIRE(m); std::unique_ptr res( MolFragmenter::fragmentOnBonds(*m, std::vector{1})); REQUIRE(res); CHECK(res->getBondWithIdx(1)->getBondType() == Bond::BondType::DOUBLE); if (useLegacy) { CHECK(res->getBondWithIdx(1)->getStereo() == Bond::BondStereo::STEREOE); } else { CHECK(res->getBondWithIdx(1)->getStereo() == Bond::BondStereo::STEREOTRANS); } CHECK(MolToSmiles(*res) == "[1*]/C=C/N.[2*]CC"); } } TEST_CASE( "Github #9009: fragmentOnBonds should transfer queries on deleted bonds") { // Basic test auto m1 = "c1ccccc1-C1CCCCC1"_smarts; REQUIRE(m1); auto bond1 = m1->getBondWithIdx(1); REQUIRE(bond1); REQUIRE(bond1->hasQuery()); std::unique_ptr splitM1( MolFragmenter::fragmentOnBonds(*m1, std::vector{1, 5})); REQUIRE(splitM1); auto nb1 = splitM1->getBondWithIdx(11); REQUIRE(nb1); CHECK(nb1->hasQuery()); CHECK(nb1->getQuery()->getDescription() == "SingleOrAromaticBond"); auto nb2 = splitM1->getBondWithIdx(12); REQUIRE(nb2); CHECK(nb2->hasQuery()); CHECK(nb2->getQuery()->getDescription() == "SingleOrAromaticBond"); auto nb3 = splitM1->getBondWithIdx(13); REQUIRE(nb3); CHECK(nb3->hasQuery()); CHECK(nb3->getQuery()->getDescription() == "BondOrder"); auto nb4 = splitM1->getBondWithIdx(14); REQUIRE(nb4); CHECK(nb4->hasQuery()); CHECK(nb4->getQuery()->getDescription() == "BondOrder"); // Check it does nothing if there isn't a query auto m2 = "c1ccccc1"_smiles; REQUIRE(m2); auto bond2 = m2->getBondWithIdx(1); REQUIRE(bond2); REQUIRE(!bond2->hasQuery()); std::unique_ptr splitM2( MolFragmenter::fragmentOnBonds(*m2, std::vector{1})); REQUIRE(splitM2); auto nb5 = splitM2->getBondWithIdx(5); REQUIRE(nb5); CHECK(!nb5->hasQuery()); auto nb6 = splitM2->getBondWithIdx(6); REQUIRE(nb6); CHECK(!nb6->hasQuery()); // Check it does nothing if bond types are specified auto m3 = "c1ccccc1"_smiles; REQUIRE(m3); auto bond3 = m3->getBondWithIdx(1); REQUIRE(bond3); REQUIRE(!bond3->hasQuery()); auto dummyLabels = std::vector>{{25, 26}}; auto newTypes = std::vector{Bond::DOUBLE}; std::unique_ptr splitM3(MolFragmenter::fragmentOnBonds( *m3, std::vector{1}, true, &dummyLabels, &newTypes)); REQUIRE(splitM3); auto nb7 = splitM3->getBondWithIdx(5); REQUIRE(nb7); CHECK(!nb7->hasQuery()); auto nb8 = splitM3->getBondWithIdx(6); REQUIRE(nb8); CHECK(!nb8->hasQuery()); } TEST_CASE("align fragments") { SECTION("basics") { auto m = "CC[1*].O[1*] |(1,0,0;2,0,0;3.5,0,0;0,1,0;0,2.1,0)|"_smiles; REQUIRE(m); REQUIRE(m->getNumConformers() == 1); MolzipParams params; params.alignCoordinates = true; params.label = MolzipLabel::Isotope; auto res = molzip(*m, params); REQUIRE(res); REQUIRE(res->getNumConformers() == 1); CHECK(MolToXYZBlock(*res) == R"XYZ(3 C 1.000000 0.000000 0.000000 C 2.000000 0.000000 0.000000 O 3.100000 0.000000 0.000000 )XYZ"); } SECTION("2D multiple attachment points") { auto m = "[2*]CCC[1*].[2*]O[1*] |(2,1.2;1,1;1,0;2,0;3,0;\ 0,0;0,1;0,2.1)|"_smiles; REQUIRE(m); REQUIRE(m->getNumConformers() == 1); MolzipParams params; params.alignCoordinates = true; params.label = MolzipLabel::Isotope; auto res = molzip(*m, params); REQUIRE(res); REQUIRE(res->getNumConformers() == 1); CHECK(MolToXYZBlock(*res) == R"XYZ(4 C 1.000000 1.000000 0.000000 C 1.000000 0.000000 0.000000 C 2.000000 0.000000 0.000000 O 3.100000 0.000000 0.000000 )XYZ"); } SECTION("3D, more real") { auto m1 = "[6*]C1CCCCC1 |(-1.08968,0.169713,-0.700677;0.254816,-0.328544,-0.482737;0.56879,-1.01027,0.899618;1.09147,0.21675,1.40903;2.37628,0.102667,0.427592;2.29478,-0.272363,-1.13357;1.24328,0.638789,-1.31851)|"_smiles; REQUIRE(m1); auto conf1 = m1->getConformer(); auto m2 = "[6*]C1CCC1 |(3.18447,1.72863,2.51726;1.96576,1.94984,2.29932;1.43402,2.74418,3.09468;0.109542,1.58059,3.19096;0.655465,0.761837,2.3866)|"_smiles; REQUIRE(m2); auto conf2 = m2->getConformer(); auto origv = conf2.getAtomPos(0) - conf2.getAtomPos(1); MolzipParams params; params.alignCoordinates = true; params.label = MolzipLabel::Isotope; auto res = molzip(*m1, *m2, params); REQUIRE(res); REQUIRE(res->getNumConformers() == 1); auto resconf = res->getConformer(); // check that the first fragment's coords are unchanged CHECK_THAT(resconf.getAtomPos(0).x, Catch::Matchers::WithinAbs(conf1.getAtomPos(1).x, 1e-4)); CHECK_THAT(resconf.getAtomPos(0).y, Catch::Matchers::WithinAbs(conf1.getAtomPos(1).y, 1e-4)); CHECK_THAT(resconf.getAtomPos(0).z, Catch::Matchers::WithinAbs(conf1.getAtomPos(1).z, 1e-4)); // make sure the bond length is right: auto v = resconf.getAtomPos(0) - resconf.getAtomPos(6); CHECK_THAT(v.length(), Catch::Matchers::WithinAbs(origv.length(), 1e-2)); } SECTION("on top") { auto m3 = "[1*]c1ccccc1.[1*]c1ncccc1 |(2.57143,0,;1.07143,0,;0.321429,-1.29904,;-1.17857,-1.29904,;-1.92857,0,;-1.17857,1.29904,;0.321429,1.29904,;12.5714,0,;11.0714,0,;10.3214,1.29904,;8.82143,1.29904,;8.07143,0,;8.82143,-1.29904,;10.3214,-1.29904,)|"_smiles; MolzipParams params; params.alignCoordinates = true; params.label = MolzipLabel::Isotope; auto res = molzip(*m3, params); INFO(MolToCXSmiles(*res)); auto resconf = res->getConformer(); // check that we don't have overlapping atoms: for (unsigned int i = 0; i < res->getNumAtoms(); ++i) { auto api = resconf.getAtomPos(i); for (unsigned int j = i + 1; j < res->getNumAtoms(); ++j) { auto apj = resconf.getAtomPos(j); CHECK((api - apj).length() > 1.0); } } } SECTION("first fragment should not move") { auto m3 = "[1*]c1ccccc1.[1*]c1ncccc1 |(2.57143,0,;1.07143,0,;0.321429,-1.29904,;-1.17857,-1.29904,;-1.92857,0,;-1.17857,1.29904,;0.321429,1.29904,;12.5714,0,;11.0714,0,;10.3214,1.29904,;8.82143,1.29904,;8.07143,0,;8.82143,-1.29904,;10.3214,-1.29904,)|"_smiles; MolzipParams params; params.alignCoordinates = true; params.label = MolzipLabel::Isotope; auto res = molzip(*m3, params); INFO(MolToCXSmiles(*res)); auto resconf = res->getConformer(); auto molconf = m3->getConformer(); for (unsigned int i = 0; i < 6; ++i) { auto rap = resconf.getAtomPos(i); auto map = molconf.getAtomPos(i + 1); CHECK_THAT(rap.x, Catch::Matchers::WithinAbs(map.x, 1e-4)); CHECK_THAT(rap.y, Catch::Matchers::WithinAbs(map.y, 1e-4)); CHECK_THAT(rap.z, Catch::Matchers::WithinAbs(map.z, 1e-4)); } } }