Skip to content

Commit 66d9db2

Browse files
authored
Prepare release 0.0.14 (#31)
* Refractor (#26) * update refractor * fix reactor * fix lint * prepare bechmark * refractor MODReactor and SynReactor * update crn * refractor cluster, change to matcher, fix code synreactor, now resnow comparable to modreactor * test 3 os * test * test * fix workflow * fix lint * fix win * fix win * fix win again * update smart * add synreactor implicit hydrogen * fix mcsmatcher * refractor visualization * fix conflict rdkit, upgrade to 2025.3.1 * fix lint * move aam_validator to Chem submodule * fix lint * prepare benchmark matcher * change backend rule to mod * prepare doc * add doc * update fih * update graph module doc * update doc * prepare release * . * fix doc * clean doc * fix docstring * fix tutorial * update fig * update explicit_hydrogen for its * prepare release * build doc * fix lint * fix bug in explicit hydrogen for ITS * fix build * fix * fix * fix * fix doc * update nauty canon, rule filters, change benchmark * prepare release * fix bug in nauty alg * update doc * add features for expanding its * add rule_matcher.py * add testcase rule matcher * add wildcard for smiles * add partial engine * update new features * update document * add data * update Chem features * format docstring and refractor Chem module * add auto-test pypi * create dependabot * test run yml * test * test docker * add docker * add docker * . * add readme * rename * release docker * remove redundant file * Prepare release bump version (#27) * update refractor * fix reactor * fix lint * prepare bechmark * refractor MODReactor and SynReactor * update crn * refractor cluster, change to matcher, fix code synreactor, now resnow comparable to modreactor * test 3 os * test * test * fix workflow * fix lint * fix win * fix win * fix win again * update smart * add synreactor implicit hydrogen * fix mcsmatcher * refractor visualization * fix conflict rdkit, upgrade to 2025.3.1 * fix lint * move aam_validator to Chem submodule * fix lint * prepare benchmark matcher * change backend rule to mod * prepare doc * add doc * update fih * update graph module doc * update doc * prepare release * . * fix doc * clean doc * fix docstring * fix tutorial * update fig * update explicit_hydrogen for its * prepare release * build doc * fix lint * fix bug in explicit hydrogen for ITS * fix build * fix * fix * fix * fix doc * update nauty canon, rule filters, change benchmark * prepare release * fix bug in nauty alg * update doc * add features for expanding its * add rule_matcher.py * add testcase rule matcher * add wildcard for smiles * add partial engine * update new features * update document * add data * update Chem features * format docstring and refractor Chem module * add auto-test pypi * create dependabot * test run yml * test * test docker * add docker * add docker * . * add readme * rename * release docker * remove redundant file * fix lint * fix bug * fix lint * add partial its beta * test publising conda * test publising conda * fix workflow * fix workflow * fix workflow * fix workflow again * tes pre-release * tes pre-release * tes pre-release * tes pre-release * tes pre-release * tes pre-release * fix meta.yaml * fix meta.yaml * fix meta.yaml * fix meta.yaml * fix meta.yaml * fix meta.yaml * publish beta * publish beta * debug * debug * debug * debug * debug * debug * debug * debug * prepare release * pass test staging, prepare release * quick fix hydrogen * prepare release * Prepare release (#30) * add partial its construction and imbaengine * add wildcard and placeholder * add testcase * add mtg * fix bug mtg * prepare release
1 parent dc1ecc0 commit 66d9db2

42 files changed

Lines changed: 4633 additions & 601 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-and-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Test & Lint
22

33
on:
44
push:
5-
branches: [ "main", "dev", "staging", "refractor" ]
5+
branches: [ "main", "partialits", "staging", "refractor" ]
66
pull_request:
77
branches: [ "main" ]
88

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ run.sh
2222
docs/*
2323
run_rdcanon.py
2424
Data/Fragment/*
25+
test_partial.py
26+
Data/Benchmark/synthesis/*

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
# SynKit
2+
[![PyPI version](https://img.shields.io/pypi/v/synkit.svg)](https://pypi.org/project/synkit/)
3+
[![Conda version](https://img.shields.io/conda/vn/tieulongphan/synkit.svg)](https://anaconda.org/tieulongphan/synkit)
4+
[![Docker Pulls](https://img.shields.io/docker/pulls/tieulongphan/synkit.svg)](https://hub.docker.com/r/tieulongphan/synkit)
5+
[![Docker Image Version](https://img.shields.io/docker/v/tieulongphan/synkit/latest?label=container)](https://hub.docker.com/r/tieulongphan/synkit)
6+
[![License](https://img.shields.io/github/license/tieulongphan/synkit.svg)](https://github.com/tieulongphan/synkit/blob/main/LICENSE)
7+
[![Release](https://img.shields.io/github/v/release/tieulongphan/synkit.svg)](https://github.com/tieulongphan/synkit/releases)
8+
[![Last Commit](https://img.shields.io/github/last-commit/tieulongphan/synkit.svg)](https://github.com/tieulongphan/synkit/commits)
9+
[![Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.15269901.svg)](https://doi.org/10.5281/zenodo.15269901)
10+
[![CI](https://github.com/tieulongphan/synkit/actions/workflows/test-and-lint.yml/badge.svg?branch=main)](https://github.com/tieulongphan/synkit/actions/workflows/test-and-lint.yml)
11+
[![Dependency PRs](https://img.shields.io/github/issues-pr-raw/tieulongphan/synkit?label=dependency%20PRs)](https://github.com/tieulongphan/synkit/pulls?q=is%3Apr+label%3Adependencies)
12+
[![Stars](https://img.shields.io/github/stars/tieulongphan/synkit.svg?style=social&label=Star)](https://github.com/tieulongphan/synkit/stargazers)
213

314
**Toolkit for Synthesis Planning**
415

Test/Graph/MTG/test_mtg.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,17 @@ def setUp(self) -> None:
2020
self.test_graph_2 = [get_rc(rsmi_to_its(var)) for var in test_2]
2121

2222
def test_MTG_1(self):
23-
grp = GroupComp(self.test_graph_1[0], self.test_graph_1[1])
24-
candidates = grp.get_mapping()
25-
print(candidates)
26-
mtg = MTG(self.test_graph_1[0], self.test_graph_1[1], candidates[0])
27-
self.assertEqual(len(mtg.get_nodes()), 6)
28-
self.assertEqual(len(mtg.get_edges()), 7)
23+
mtg = MTG(self.test_graph_1[0:2], mcs_mol=True)
24+
self.assertEqual(mtg._graph.number_of_nodes(), 6)
25+
self.assertEqual(mtg._graph.number_of_edges(), 7)
2926

3027
def test_MTG_2(self):
3128
grp = GroupComp(self.test_graph_2[0], self.test_graph_2[1])
3229
candidates = grp.get_mapping()
33-
mtg = MTG(self.test_graph_2[0], self.test_graph_2[1], candidates[0])
34-
self.assertEqual(len(mtg.get_nodes()), 5)
35-
self.assertEqual(len(mtg.get_edges()), 4)
30+
# print(candidates)
31+
mtg = MTG(self.test_graph_2[0:], candidates)
32+
self.assertEqual(mtg._graph.number_of_nodes(), 5)
33+
self.assertEqual(mtg._graph.number_of_edges(), 4)
3634

3735

3836
if __name__ == "__main__":

Test/Graph/Wildcard/__init__.py

Whitespace-only changes.

Test/Graph/Wildcard/test_radwc.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import unittest
2+
from synkit.Graph.Wildcard.radwc import RadWC
3+
4+
5+
class TestRadWC(unittest.TestCase):
6+
def test_no_product_radicals(self):
7+
"""If product has no radicals, output should be unchanged."""
8+
rxn = "[CH3:1][OH:2]>>[CH3:1][OH:2]"
9+
self.assertEqual(RadWC.transform(rxn), rxn)
10+
11+
def test_single_radical_in_product(self):
12+
"""A single radical in product gets a wildcard."""
13+
rxn = "[CH3:1][OH:2]>>[CH2:1].[OH:2]"
14+
out = RadWC.transform(rxn)
15+
# Check [*:3] is attached to [CH2:1]
16+
self.assertIn("[CH2:1]([*:3])", out) # Atom-maps: 1,2 exist, so 3 is next
17+
18+
def test_multiple_radicals_in_product(self):
19+
"""Multiple radicals in product get multiple wildcards."""
20+
rxn = "[CH3:1][OH:2]>>[CH2:1].[O:2]"
21+
out = RadWC.transform(rxn)
22+
# [CH2:1] has *:3 and *:4, [O:2] has *:5
23+
self.assertIn("[CH2:1]([*:3])", out)
24+
self.assertIn("[O:2]([*:5])", out)
25+
26+
def test_radical_and_nonradical_mixture(self):
27+
"""Mixed radical/non-radical product fragments, only radicals get wildcard."""
28+
rxn = "[CH3:1][OH:2]>>[CH2:1].[OH:2]"
29+
out = RadWC.transform(rxn)
30+
# [CH2:1] gets *:3, [OH:2] is unchanged
31+
self.assertIn("[CH2:1]([*:3])", out)
32+
self.assertIn("[OH:2]", out)
33+
34+
def test_user_start_map(self):
35+
"""User-supplied map index is used for wildcards."""
36+
rxn = "[CH3:7][OH:8]>>[CH2:7].[OH:8]"
37+
out = RadWC.transform(rxn, start_map=50)
38+
self.assertIn("[CH2:7]([*:50])", out)
39+
40+
def test_empty_reaction(self):
41+
"""Empty input should raise ValueError."""
42+
with self.assertRaises(ValueError):
43+
RadWC.transform("")
44+
45+
def test_three_component(self):
46+
"""Agent block is preserved."""
47+
rxn = "[CH3:1][OH:2]>[Na+]>[CH2:1].[OH:2]"
48+
out = RadWC.transform(rxn)
49+
self.assertTrue(out.startswith("[CH3:1][OH:2]>[Na+]>"))
50+
self.assertIn("[CH2:1]([*:3])", out)
51+
self.assertIn("[OH:2]", out)
52+
53+
54+
if __name__ == "__main__":
55+
unittest.main()
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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()

Test/IO/combinatorial/__init__.py

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import unittest
2+
from synkit.IO.combinatorial.smarts_expander import SMARTSExpander
3+
4+
5+
class TestSMARTSExpander(unittest.TestCase):
6+
7+
def test_no_placeholders(self):
8+
s = "CCO"
9+
self.assertEqual(list(SMARTSExpander.expand_iter(s)), ["CCO"])
10+
self.assertEqual(SMARTSExpander.expand(s), ["CCO"])
11+
12+
def test_simple_expansion(self):
13+
s = "[C,N:1][O,P:2]"
14+
result = SMARTSExpander.expand(s)
15+
self.assertEqual(
16+
set(result), {"[C:1][O:2]", "[C:1][P:2]", "[N:1][O:2]", "[N:1][P:2]"}
17+
)
18+
19+
def test_disjoint_constraint(self):
20+
s = "[C,N:1][O:1]"
21+
with self.assertRaises(ValueError):
22+
list(SMARTSExpander.expand_iter(s))
23+
24+
def test_realistic_reaction(self):
25+
rxn = (
26+
"[H+:6].[C:7](-[O:8](-[H:12]))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])."
27+
"[C:2](-[S:4](-[C,N,O,P,S:5]))(-[C,N,O,P,S:1])(=[O:3])>>"
28+
"[S:4](-[H:6])(-[C,N,O,P,S:5]).[H+:12]."
29+
"[C:7](-[O:8](-[C:2](-[C,N,O,P,S:1])(=[O:3])))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])"
30+
)
31+
ex_list = list(SMARTSExpander.expand_iter(rxn))
32+
self.assertEqual(len(ex_list), 625)
33+
# Optional: Just check format or count, not endswith
34+
self.assertTrue(ex_list[0].startswith("[H+:6]"))
35+
36+
37+
if __name__ == "__main__":
38+
unittest.main()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import unittest
2+
from rdkit import Chem
3+
from synkit.IO.combinatorial.smarts_generalizer import SMARTSGeneralizer
4+
5+
6+
class TestSMARTSGeneralizer(unittest.TestCase):
7+
8+
def setUp(self):
9+
self.gen = SMARTSGeneralizer(sanity_check=True)
10+
11+
def test_basic_generalization(self):
12+
inputs = [
13+
"[C:1]-[N:2]>>[N:1]-[C:2]",
14+
"[N:1]-[N:2]>>[N:1]-[N:2]",
15+
"[O:1]-[N:2]>>[N:1]-[N:2]",
16+
]
17+
output = self.gen.generalize(inputs)
18+
# Instead of strict string match, check correct mapped elements
19+
self.assertIn("[C,N,O:1]", output)
20+
self.assertIn("[N:2]", output)
21+
self.assertIn(">>", output)
22+
23+
def test_single_smarts(self):
24+
inputs = ["[C:1]-[N:2]>>[N:1]-[C:2]"]
25+
output = self.gen.generalize(inputs)
26+
# Should match input exactly
27+
self.assertEqual(output, "[C:1]-[N:2]>>[N:1]-[C:2]")
28+
29+
def test_different_topology_raises(self):
30+
inputs = ["[C:1]-[N:2]>>[N:1]-[C:2]", "[N:1]-[N:2]-[C:3]>>[N:1]-[N:2]-[C:3]"]
31+
with self.assertRaises(ValueError):
32+
self.gen.generalize(inputs)
33+
34+
def test_empty_input_raises(self):
35+
with self.assertRaises(ValueError):
36+
self.gen.generalize([])
37+
38+
def test_molecule_smarts(self):
39+
gen = SMARTSGeneralizer(sanity_check=True)
40+
inputs = ["[C:1]-[N:2]", "[N:1]-[N:2]", "[O:1]-[N:2]"]
41+
out = gen.generalize(inputs)
42+
self.assertEqual(out, "[C,N,O:1]-[N:2]")
43+
44+
mol = Chem.MolFromSmarts(out)
45+
self.assertIsNotNone(mol)
46+
47+
def test_invalid_sanity_check(self):
48+
gen = SMARTSGeneralizer(sanity_check=True)
49+
# Using an obviously broken SMARTS (bad bracket placement)
50+
_ = [
51+
"[C:1]-[N:2]>>[N:1]-[X:2]"
52+
] # 'X' is a valid SMARTS wildcard! Use a real error
53+
with self.assertRaises(ValueError):
54+
gen.generalize(["[C:1][C:2]>>[N:1][C:2]["]) # broken SMARTS
55+
56+
def test_repr(self):
57+
gen = SMARTSGeneralizer()
58+
self.assertIn("sanity_check", repr(gen))
59+
60+
61+
if __name__ == "__main__":
62+
unittest.main()

0 commit comments

Comments
 (0)