// // Copyright (C) 2020 Schrödinger, LLC // // @@ 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 "CIPLabeler.h" #include "Digraph.h" #include "rules/Pairlist.h" #include "rules/Rule1a.h" #include "rules/Rule2.h" #include "CIPMol.h" using namespace RDKit; using namespace RDKit::CIPLabeler; std::string toBinaryString(int value) { return std::bitset<32>(value).to_string(); } TEST_CASE("Descriptor lists", "[accurateCIP]") { auto descriptors = PairList(); SECTION("IgnoreConstructionNulls") { CHECK(!descriptors.add(Descriptor::NONE)); CHECK(!descriptors.add(Descriptor::UNKNOWN)); CHECK(!descriptors.add(Descriptor::ns)); } SECTION("IgnoreConstructionPseudo") { CHECK(!descriptors.add(Descriptor::r)); CHECK(!descriptors.add(Descriptor::s)); CHECK(descriptors.add(Descriptor::R)); CHECK(descriptors.add(Descriptor::S)); } SECTION("Pairing") { REQUIRE(descriptors.getPairing() == 0); CHECK("00000000000000000000000000000000" == toBinaryString(descriptors.getPairing())); descriptors.add(Descriptor::R); CHECK("00000000000000000000000000000000" == toBinaryString(descriptors.getPairing())); // like descriptors.add(Descriptor::R); CHECK("01000000000000000000000000000000" == toBinaryString(descriptors.getPairing())); // like descriptors.add(Descriptor::R); CHECK("01100000000000000000000000000000" == toBinaryString(descriptors.getPairing())); // unlike descriptors.add(Descriptor::S); CHECK("01100000000000000000000000000000" == toBinaryString(descriptors.getPairing())); // like descriptors.add(Descriptor::R); CHECK("01101000000000000000000000000000" == toBinaryString(descriptors.getPairing())); // like descriptors.add(Descriptor::R); CHECK("01101100000000000000000000000000" == toBinaryString(descriptors.getPairing())); // like descriptors.add(Descriptor::R); CHECK("01101110000000000000000000000000" == toBinaryString(descriptors.getPairing())); // unlike descriptors.add(Descriptor::S); CHECK("01101110000000000000000000000000" == toBinaryString(descriptors.getPairing())); // like descriptors.add(Descriptor::R); CHECK("01101110100000000000000000000000" == toBinaryString(descriptors.getPairing())); } SECTION("pairRM") { PairList list1 = PairList(); PairList list2 = PairList(); list1.add(Descriptor::R); list1.add(Descriptor::M); list1.add(Descriptor::R); list1.add(Descriptor::S); list2.add(Descriptor::R); list2.add(Descriptor::P); list2.add(Descriptor::S); list2.add(Descriptor::M); CHECK(list1.toString() == "R:llu"); CHECK(list2.toString() == "R:uul"); } } void check_incoming_edge_count(Node *root) { auto queue = std::list({root}); for (const auto &node : queue) { int incoming_edges = 0; for (const auto &e : node->getEdges()) { if (!e->isBeg(node)) { REQUIRE(e->isEnd(node)); ++incoming_edges; } else if (!e->getEnd()->isTerminal()) { queue.push_back(e->getEnd()); } } // Only the current root should have no incoming nodes. // All other nodes should have exactly one incoming edge. if (node == root) { CHECK(incoming_edges == 0); } else { CHECK(incoming_edges == 1); } } } void expandAll(Digraph &g) { auto queue = std::list({g.getOriginalRoot()}); for (const auto &node : queue) { for (const auto &e : node->getEdges()) { if (!e->isBeg(node)) { continue; } if (!e->getEnd()->isTerminal()) { queue.push_back(e->getEnd()); } } } } TEST_CASE("Digraph", "[accurateCIP]") { auto mol = R"(CC1(OC2=C(C=3NC[C@@]4(C3C=C2)C([C@@H]5C[C@@]67C(N([C@]5(C4)CN6CC[C@@]7(C)O)C)=O)(C)C)OC=C1)C)"_smiles; // VS040 CIPLabeler::CIPMol cipmol(*mol); auto initial_root_idx = 1u; auto initial_root_atom = cipmol.getAtom(initial_root_idx); Digraph g(cipmol, initial_root_atom); expandAll(g); REQUIRE(g.getNumNodes() == 3819); auto current_root = g.getCurrentRoot(); REQUIRE(current_root->getAtom()->getIdx() == initial_root_idx); check_incoming_edge_count(current_root); auto new_root_idx = 24u; auto new_root_atom = cipmol.getAtom(new_root_idx); auto new_root_nodes = g.getNodes(new_root_atom); CHECK(new_root_nodes.size() == 104); Node *new_root_node = *new_root_nodes.begin(); for (const auto &node : new_root_nodes) { if (!node->isDuplicate() && node->getDistance() > new_root_node->getDistance()) { new_root_node = node; } } REQUIRE(new_root_node->getDistance() == 25); g.changeRoot(new_root_node); REQUIRE(g.getNumNodes() == 3819); current_root = g.getCurrentRoot(); REQUIRE(current_root->getAtom()->getIdx() == new_root_idx); check_incoming_edge_count(current_root); } TEST_CASE("Rule1a", "[accurateCIP]") { SECTION("Compare equal") { auto mol = "COC"_smiles; CIPLabeler::CIPMol cipmol(*mol); Digraph g(cipmol, cipmol.getAtom(1)); auto origin = g.getOriginalRoot(); auto frac = origin->getAtomicNumFraction(); REQUIRE(frac.numerator() == 8); REQUIRE(frac.denominator() == 1); Rule1a rule; auto edges = origin->getEdges(); REQUIRE(edges.size() == 2); REQUIRE(edges[0] != edges[1]); CHECK(0 == rule.compare(edges[0], edges[1])); CHECK(!rule.getSorter()->prioritize(origin, edges).isUnique()); } SECTION("Compare different") { auto mol = "CON"_smiles; CIPLabeler::CIPMol cipmol(*mol); Digraph g(cipmol, cipmol.getAtom(1)); auto origin = g.getOriginalRoot(); auto frac = origin->getAtomicNumFraction(); REQUIRE(frac.numerator() == 8); REQUIRE(frac.denominator() == 1); Rule1a rule; auto edges = origin->getEdges(); REQUIRE(edges.size() == 2); REQUIRE(edges[0] != edges[1]); auto frac0 = edges[0]->getEnd()->getAtomicNumFraction(); REQUIRE(frac0.numerator() == 6); REQUIRE(frac0.denominator() == 1); auto frac1 = edges[1]->getEnd()->getAtomicNumFraction(); REQUIRE(frac1.numerator() == 7); REQUIRE(frac1.denominator() == 1); CHECK(rule.compare(edges[0], edges[1]) < 0); CHECK(rule.compare(edges[1], edges[0]) > 0); CHECK(rule.getSorter()->prioritize(origin, edges).isUnique()); } } TEST_CASE("Rule2", "[accurateCIP]") { SECTION("Compare equal") { auto mol = "COC"_smiles; CIPLabeler::CIPMol cipmol(*mol); Digraph g(cipmol, cipmol.getAtom(1)); auto origin = g.getOriginalRoot(); auto frac = origin->getAtomicNumFraction(); REQUIRE(frac.numerator() == 8); REQUIRE(frac.denominator() == 1); Rule2 rule; auto edges = origin->getEdges(); REQUIRE(edges.size() == 2); REQUIRE(edges[0] != edges[1]); CHECK(0 == rule.compare(edges[0], edges[1])); CHECK(!rule.getSorter()->prioritize(origin, edges).isUnique()); } SECTION("Compare different: Zero Mass Num") { auto mol = "CO[13C]"_smiles; CIPLabeler::CIPMol cipmol(*mol); Digraph g(cipmol, cipmol.getAtom(1)); auto origin = g.getOriginalRoot(); auto frac = origin->getAtomicNumFraction(); REQUIRE(frac.numerator() == 8); REQUIRE(frac.denominator() == 1); Rule2 rule; auto edges = origin->getEdges(); REQUIRE(edges.size() == 2); REQUIRE(edges[0] != edges[1]); REQUIRE(edges[0]->getEnd()->getMassNum() == 0); REQUIRE(edges[1]->getEnd()->getMassNum() == 13); CHECK(rule.compare(edges[0], edges[1]) < 0); CHECK(rule.compare(edges[1], edges[0]) > 0); CHECK(rule.getSorter()->prioritize(origin, edges).isUnique()); } SECTION("Compare different: Non-Zero Mass Num") { auto mol = "[13C]O[14C]"_smiles; CIPLabeler::CIPMol cipmol(*mol); Digraph g(cipmol, cipmol.getAtom(1)); auto origin = g.getOriginalRoot(); auto frac = origin->getAtomicNumFraction(); REQUIRE(frac.numerator() == 8); REQUIRE(frac.denominator() == 1); Rule2 rule; auto edges = origin->getEdges(); REQUIRE(edges.size() == 2); REQUIRE(edges[0] != edges[1]); REQUIRE(edges[0]->getEnd()->getMassNum() == 13); REQUIRE(edges[1]->getEnd()->getMassNum() == 14); CHECK(rule.compare(edges[0], edges[1]) < 0); CHECK(rule.compare(edges[1], edges[0]) > 0); CHECK(rule.getSorter()->prioritize(origin, edges).isUnique()); } } TEST_CASE("Tetrahedral assignment", "[accurateCIP]") { auto mol = "Br[C@H](Cl)F"_smiles; REQUIRE(mol->getNumAtoms() == 4); auto chiral_atom = mol->getAtomWithIdx(1); chiral_atom->clearProp(common_properties::_CIPCode); REQUIRE(chiral_atom->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW); CIPLabeler::assignCIPLabels(*mol); std::string chirality; CHECK(chiral_atom->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "S"); } TEST_CASE("Double bond stereo assignment", "[accurateCIP]") { auto mol = R"(CC\C(\C(\C)=N\O)=N\O)"_smiles; // VS013 REQUIRE(mol->getNumAtoms() == 9); auto bond_1 = mol->getBondWithIdx(4); auto bond_2 = mol->getBondWithIdx(6); REQUIRE(bond_1->getBondType() == Bond::DOUBLE); REQUIRE(bond_2->getBondType() == Bond::DOUBLE); REQUIRE(bond_1->getStereo() == Bond::STEREOE); REQUIRE(bond_2->getStereo() == Bond::STEREOZ); CIPLabeler::assignCIPLabels(*mol); std::string chirality; CHECK(bond_1->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "E"); CHECK(bond_2->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "Z"); } TEST_CASE("phosphine and arsine chirality", "[accurateCIP]") { const std::vector> mols{ {"C[P@](C1CCCC1)C1=CC=CC=C1", "R"}, {"C[As@@](C1CCCC1)C1=CC=CC=C1", "S"}, {"C[P@H]C1CCCCC1", "R"}, {"C[P@@H]C1CCCCC1", "S"}}; for (const auto &ref : mols) { std::unique_ptr mol{SmilesToMol(ref.first)}; REQUIRE(mol); CIPLabeler::assignCIPLabels(*mol); std::string chirality; CHECK(mol->getAtomWithIdx(1)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == ref.second); } } TEST_CASE("assign specific atoms and bonds", "[accurateCIP]") { SECTION("Assign atoms") { auto mol = "C[C@H](Cl)CC[C@H](Cl)C"_smiles; REQUIRE(mol); auto atom1 = mol->getAtomWithIdx(1); auto atom5 = mol->getAtomWithIdx(5); REQUIRE(atom1->hasProp(common_properties::_CIPCode)); REQUIRE(atom5->hasProp(common_properties::_CIPCode)); atom1->clearProp(common_properties::_CIPCode); atom5->clearProp(common_properties::_CIPCode); boost::dynamic_bitset<> atoms(mol->getNumAtoms()); boost::dynamic_bitset<> bonds; atoms.set(1); CIPLabeler::assignCIPLabels(*mol, atoms, bonds); std::string chirality; CHECK(atom1->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "S"); CHECK(!atom5->hasProp(common_properties::_CIPCode)); } SECTION("Assign bonds") { auto mol = R"(C\C=C\C=C/C)"_smiles; REQUIRE(mol); auto bond1 = mol->getBondWithIdx(1); auto bond3 = mol->getBondWithIdx(3); REQUIRE(bond1->getBondType() == Bond::DOUBLE); REQUIRE(bond3->getBondType() == Bond::DOUBLE); REQUIRE(!bond1->hasProp(common_properties::_CIPCode)); REQUIRE(!bond3->hasProp(common_properties::_CIPCode)); boost::dynamic_bitset<> atoms; boost::dynamic_bitset<> bonds(mol->getNumBonds()); bonds.set(3); CIPLabeler::assignCIPLabels(*mol, atoms, bonds); std::string stereo; CHECK(!bond1->hasProp(common_properties::_CIPCode)); CHECK(bond3->getPropIfPresent(common_properties::_CIPCode, stereo)); CHECK(stereo == "Z"); } } TEST_CASE("para-stereochemistry", "[accurateCIP]") { SECTION("example 1") { // slightly simplified complex example from Salome Rieder auto mol = "C\\C=C/[C@@H](\\C=C\\O)[C@H](C)[C@H](\\C=C/C)\\C=C\\O"_smiles; REQUIRE(mol); CIPLabeler::assignCIPLabels(*mol); std::string chirality; CHECK(mol->getAtomWithIdx(3)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "R"); CHECK(mol->getAtomWithIdx(7)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "r"); CHECK(mol->getAtomWithIdx(9)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "S"); } SECTION("example 2") { // lovely complex example from Salome Rieder auto mol = "C\\C=C/[C@@H](\\C=C\\C)[C@H](C)[C@H](\\C=C/C)\\C=C\\C"_smiles; REQUIRE(mol); CIPLabeler::assignCIPLabels(*mol); std::string chirality; CHECK(mol->getAtomWithIdx(3)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "R"); CHECK(mol->getAtomWithIdx(7)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "r"); CHECK(mol->getAtomWithIdx(9)->getPropIfPresent(common_properties::_CIPCode, chirality)); CHECK(chirality == "S"); } } TEST_CASE( "Github #4996: Bad handling of dummy atoms in the CIP assignment code", "[accurateCIP]") { SECTION("case 1") { auto m = "*[C@](F)(Cl)Br"_smiles; REQUIRE(m); bool cleanit = true; bool force = true; // original assignment: MolOps::assignStereochemistry(*m, cleanit, force); std::string cip; CHECK(m->getAtomWithIdx(1)->getPropIfPresent(common_properties::_CIPCode, cip)); CHECK(cip == "S"); m->getAtomWithIdx(1)->clearProp(common_properties::_CIPCode); CIPLabeler::assignCIPLabels(*m); CHECK(m->getAtomWithIdx(1)->getPropIfPresent(common_properties::_CIPCode, cip)); CHECK(cip == "S"); } SECTION("dummies can match dummies") { auto m = "*[C@](*)(Cl)Br"_smiles; REQUIRE(m); bool cleanit = true; bool force = true; // original assignment: MolOps::assignStereochemistry(*m, cleanit, force); CHECK(!m->getAtomWithIdx(1)->hasProp(common_properties::_CIPCode)); CIPLabeler::assignCIPLabels(*m); CHECK(!m->getAtomWithIdx(1)->hasProp(common_properties::_CIPCode)); } SECTION("case 2") { auto m = "C1CC[C@](*)2CCCC[C@H]2C1"_smiles; REQUIRE(m); bool cleanit = true; bool force = true; // original assignment doesn't work for these: MolOps::assignStereochemistry(*m, cleanit, force); CHECK(!m->getAtomWithIdx(3)->hasProp(common_properties::_CIPCode)); CHECK(!m->getAtomWithIdx(9)->hasProp(common_properties::_CIPCode)); CIPLabeler::assignCIPLabels(*m); std::string cip; CHECK(m->getAtomWithIdx(3)->getPropIfPresent(common_properties::_CIPCode, cip)); CHECK(cip == "s"); cip = ""; CHECK(m->getAtomWithIdx(9)->getPropIfPresent(common_properties::_CIPCode, cip)); CHECK(cip == "s"); } } TEST_CASE("CIP code errors on fragments which cannot be kekulized", "[accurateCIP]") { SECTION("fragment not affecting the stereochem") { auto m = "F[C@H](CNC)CCc(:c):c"_smarts; m->getAtomWithIdx(1)->clearProp(common_properties::_CIPCode); m->updatePropertyCache(); CIPLabeler::assignCIPLabels(*m); std::string cip; CHECK(m->getAtomWithIdx(1)->getPropIfPresent(common_properties::_CIPCode, cip)); CHECK(cip == "S"); } SECTION("fragment, unique bits") { auto m = "F[C@H](C(N)C)c(:c):c"_smarts; m->getAtomWithIdx(1)->clearProp(common_properties::_CIPCode); m->updatePropertyCache(); CIPLabeler::assignCIPLabels(*m); std::string cip; CHECK(m->getAtomWithIdx(1)->getPropIfPresent(common_properties::_CIPCode, cip)); } SECTION("fragment, non-unique bits") { auto m = "F[C@H]([C]([NH])[CH2])c(:n):c"_smarts; m->getAtomWithIdx(1)->clearProp(common_properties::_CIPCode); m->updatePropertyCache(); CIPLabeler::assignCIPLabels(*m); std::string cip; CHECK(!m->getAtomWithIdx(1)->getPropIfPresent(common_properties::_CIPCode, cip)); } } TEST_CASE("GitHub Issue #5142", "[bug][accurateCIP]") { auto mol = "*C1C[C@H](CCC)[C@@H](C)[C@H](C)C1"_smiles; REQUIRE(mol); CIPLabeler::assignCIPLabels(*mol); } TEST_CASE("CIP timeout test", "[accurateCIP]") { std::string molBlock = R"( Mrv2117 11112217353D 40 50 0 0 0 0 999 V2000 7.5483 -7.7451 -3.3419 H 0 0 0 0 0 0 0 0 0 0 0 0 9.3984 -5.8792 -3.4318 H 0 0 0 0 0 0 0 0 0 0 0 0 9.3399 -6.5425 -1.9898 C 0 0 2 0 0 0 0 0 0 0 0 0 8.6820 -3.9425 -1.6474 H 0 0 0 0 0 0 0 0 0 0 0 0 5.8163 -7.1034 -1.6635 H 0 0 0 0 0 0 0 0 0 0 0 0 6.5722 -4.7343 -0.6304 H 0 0 0 0 0 0 0 0 0 0 0 0 7.1701 -7.2380 -0.8890 C 0 0 1 0 0 0 0 0 0 0 0 0 11.8300 -6.9765 -2.5544 H 0 0 0 0 0 0 0 0 0 0 0 0 10.6631 -7.1690 -1.5375 C 0 0 1 0 0 0 0 0 0 0 0 0 11.4077 -9.5239 -2.1472 H 0 0 0 0 0 0 0 0 0 0 0 0 11.9006 -10.0139 0.4722 H 0 0 0 0 0 0 0 0 0 0 0 0 10.6600 -8.8749 0.2914 C 0 0 2 0 0 0 0 0 0 0 0 0 12.5136 -5.9115 -0.4190 H 0 0 0 0 0 0 0 0 0 0 0 0 12.5338 -7.8406 1.4379 H 0 0 0 0 0 0 0 0 0 0 0 0 8.6981 -8.8654 3.4177 H 0 0 0 0 0 0 0 0 0 0 0 0 9.4046 -10.6907 1.7143 H 0 0 0 0 0 0 0 0 0 0 0 0 9.3185 -9.3106 0.9700 C 0 0 1 0 0 0 0 0 0 0 0 0 7.4958 -10.8075 -0.1783 H 0 0 0 0 0 0 0 0 0 0 0 0 6.5552 -7.8780 2.6983 H 0 0 0 0 0 0 0 0 0 0 0 0 5.8522 -9.1147 0.4514 H 0 0 0 0 0 0 0 0 0 0 0 0 7.1601 -8.2997 0.2353 C 0 0 2 0 0 0 0 0 0 0 0 0 7.5774 -7.5989 1.5700 C 0 0 1 0 0 0 0 0 0 0 0 0 10.6577 -4.0099 0.1683 H 0 0 0 0 0 0 0 0 0 0 0 0 11.0808 -6.4894 -0.1982 C 0 0 2 0 0 0 0 0 0 0 0 0 8.9123 -5.4699 -0.9424 C 0 0 1 0 0 0 0 0 0 0 0 0 8.9155 -8.2329 2.0184 C 0 0 2 0 0 0 0 0 0 0 0 0 11.0766 -7.5513 0.9147 C 0 0 1 0 0 0 0 0 0 0 0 0 10.6780 -6.9857 3.3021 H 0 0 0 0 0 0 0 0 0 0 0 0 7.5861 -5.8939 -0.2667 C 0 0 2 0 0 0 0 0 0 0 0 0 6.9973 -5.0597 1.9875 H 0 0 0 0 0 0 0 0 0 0 0 0 9.3193 -5.8429 1.5048 C 0 0 2 0 0 0 0 0 0 0 0 0 9.4740 -4.7173 2.5743 H 0 0 0 0 0 0 0 0 0 0 0 0 9.9809 -7.1457 1.9811 C 0 0 1 0 0 0 0 0 0 0 0 0 7.8270 -6.1245 1.2483 C 0 0 2 0 0 0 0 0 0 0 0 0 10.0023 -5.4308 0.1366 C 0 0 1 0 0 0 0 0 0 0 0 0 8.9125 -8.9385 -1.4737 C 0 0 2 0 0 0 0 0 0 0 0 0 8.2374 -9.3522 -0.1173 C 0 0 1 0 0 0 0 0 0 0 0 0 10.4116 -8.6461 -1.2148 C 0 0 1 0 0 0 0 0 0 0 0 0 8.7726 -9.9758 -2.5047 H 0 0 0 0 0 0 0 0 0 0 0 0 8.2577 -7.6250 -1.9478 C 0 0 2 0 0 0 0 0 0 0 0 0 1 40 1 0 0 0 0 2 3 1 0 0 0 0 3 9 1 0 0 0 0 3 25 1 0 0 0 0 3 40 1 0 0 0 0 4 25 1 0 0 0 0 5 7 1 0 0 0 0 6 29 1 0 0 0 0 7 21 1 0 0 0 0 7 29 1 0 0 0 0 7 40 1 0 0 0 0 8 9 1 0 0 0 0 9 24 1 0 0 0 0 9 38 1 0 0 0 0 10 38 1 0 0 0 0 11 12 1 0 0 0 0 12 17 1 0 0 0 0 12 27 1 0 0 0 0 12 38 1 0 0 0 0 13 24 1 0 0 0 0 14 27 1 0 0 0 0 15 26 1 0 0 0 0 16 17 1 0 0 0 0 17 26 1 0 0 0 0 17 37 1 0 0 0 0 18 37 1 0 0 0 0 19 22 1 0 0 0 0 20 21 1 0 0 0 0 21 22 1 0 0 0 0 21 37 1 0 0 0 0 22 26 1 0 0 0 0 22 34 1 0 0 0 0 23 35 1 0 0 0 0 24 27 1 0 0 0 0 24 35 1 0 0 0 0 25 29 1 0 0 0 0 25 35 1 0 0 0 0 26 33 1 0 0 0 0 27 33 1 0 0 0 0 28 33 1 0 0 0 0 29 34 1 0 0 0 0 30 34 1 0 0 0 0 31 32 1 0 0 0 0 31 33 1 0 0 0 0 31 35 1 0 0 0 0 31 34 1 0 0 0 0 36 37 1 0 0 0 0 36 38 1 0 0 0 0 36 39 1 0 0 0 0 36 40 1 0 0 0 0 M END )"; auto mol = std::unique_ptr(MolBlockToMol(molBlock, true, false)); REQUIRE(mol); CHECK_THROWS_AS(CIPLabeler::assignCIPLabels(*mol, 100000), CIPLabeler::MaxIterationsExceeded); // try a second call to test that the timer is reset CHECK_THROWS_AS(CIPLabeler::assignCIPLabels(*mol, 100000), CIPLabeler::MaxIterationsExceeded); }