|
| 1 | +import unittest |
| 2 | +from synkit.IO import rsmi_to_graph |
| 3 | +from synkit.Graph.Wildcard.wildcard import WildCard |
| 4 | + |
| 5 | + |
| 6 | +class TestWildCard(unittest.TestCase): |
| 7 | + def setUp(self): |
| 8 | + # The main, complex test case with atom mapping |
| 9 | + self.rsmi_main = ( |
| 10 | + "[cH:1]1[cH:14][c:10]2[c:23]([cH:11][n:25]1)[cH:17][cH:12][cH:4][c:31]2[NH2:28]." |
| 11 | + "[cH:2]1[c:20]([C:22]([OH:7])=[O:21])[s:18][c:24]([S:6][c:29]2[c:15]([Cl:26])[cH:8]" |
| 12 | + "[n:19][cH:9][c:16]2[Cl:27])[c:30]1[N+:5]([O-:3])=[O:13]>>" |
| 13 | + "[cH:1]1[cH:14][c:10]2[c:23]([cH:11][n:25]1)[cH:17][cH:12][cH:4][c:31]2[NH:28]" |
| 14 | + "[C:22]([c:20]1[cH:2][c:30]([N+:5]([O-:3])=[O:13])[c:24]([S:6][c:29]2[c:15]([Cl:26])" |
| 15 | + "[cH:8][n:19][cH:9][c:16]2[Cl:27])[s:18]1)=[O:21]" |
| 16 | + ) |
| 17 | + # No atoms lost: R == P, should not add wildcards |
| 18 | + self.rsmi_no_loss = "CCO>>CCO" |
| 19 | + # All atoms lost: RSMI that loses everything (nonsense, but good test) |
| 20 | + self.rsmi_all_lost = "CCO>>" |
| 21 | + # Empty |
| 22 | + self.rsmi_empty = "" |
| 23 | + # Wildcard already present |
| 24 | + self.rsmi_existing_wildcard = "[CH3:1][CH2:2][OH:3]>>[CH2:1][CH2:2].[*:4][OH:3]" |
| 25 | + # No atom map (should raise error) |
| 26 | + self.rsmi_no_atom_map = "C(C)Cl>>CC" |
| 27 | + |
| 28 | + def test_main_case_wildcard_added(self): |
| 29 | + """Complex case: output product contains wildcard and roundtrip is valid.""" |
| 30 | + out_rsmi = WildCard.rsmi_with_wildcards(self.rsmi_main) |
| 31 | + _, product = out_rsmi.split(">>") |
| 32 | + self.assertIsInstance(out_rsmi, str) |
| 33 | + self.assertIn( |
| 34 | + "*", product, "Wildcard '*' should be present in the product side." |
| 35 | + ) |
| 36 | + # Roundtrip: should parse back without error |
| 37 | + r, p = rsmi_to_graph(out_rsmi) |
| 38 | + self.assertTrue(r.number_of_nodes() > 0) |
| 39 | + self.assertTrue(p.number_of_nodes() > 0) |
| 40 | + |
| 41 | + def test_no_atoms_lost(self): |
| 42 | + """No atoms lost: should raise ValueError if input is not atom-mapped.""" |
| 43 | + with self.assertRaises(ValueError): |
| 44 | + WildCard.rsmi_with_wildcards(self.rsmi_no_loss) |
| 45 | + |
| 46 | + def test_all_atoms_lost(self): |
| 47 | + """All atoms lost: should raise ValueError if input is not atom-mapped.""" |
| 48 | + with self.assertRaises(ValueError): |
| 49 | + WildCard.rsmi_with_wildcards(self.rsmi_all_lost) |
| 50 | + |
| 51 | + def test_empty_input(self): |
| 52 | + """Empty input: should raise ValueError.""" |
| 53 | + with self.assertRaises(ValueError): |
| 54 | + WildCard.rsmi_with_wildcards(self.rsmi_empty) |
| 55 | + |
| 56 | + def test_wildcard_not_duplicated(self): |
| 57 | + """Existing wildcards: should not create duplicate wildcards for same lost bond.""" |
| 58 | + out_rsmi = WildCard.rsmi_with_wildcards(self.rsmi_existing_wildcard) |
| 59 | + _, product = out_rsmi.split(">>") |
| 60 | + # At least one '*' in the product SMILES string |
| 61 | + self.assertIn("*", product) |
| 62 | + |
| 63 | + def test_no_false_positive_wildcards(self): |
| 64 | + """Wildcards are only added if there are truly lost subgraphs; non-atom-mapped input raises.""" |
| 65 | + rsmi = "C>>C" |
| 66 | + with self.assertRaises(ValueError): |
| 67 | + WildCard.rsmi_with_wildcards(rsmi) |
| 68 | + |
| 69 | + def test_output_is_str_and_split(self): |
| 70 | + """Should raise ValueError if input is not atom-mapped.""" |
| 71 | + with self.assertRaises(ValueError): |
| 72 | + WildCard.rsmi_with_wildcards(self.rsmi_no_loss) |
| 73 | + |
| 74 | + def test_missing_atom_map_raises(self): |
| 75 | + """Should raise ValueError if atom_map attributes are missing.""" |
| 76 | + with self.assertRaises(ValueError): |
| 77 | + WildCard.rsmi_with_wildcards(self.rsmi_no_atom_map) |
| 78 | + |
| 79 | + |
| 80 | +if __name__ == "__main__": |
| 81 | + unittest.main() |
0 commit comments