synkit 1.4.2b1__py3-none-any.whl → 2.0.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. synkit/Chem/Fingerprint/transformation_fp.py +8 -4
  2. synkit/Chem/Molecule/atom_features.py +1 -0
  3. synkit/Chem/Reaction/__init__.py +15 -1
  4. synkit/Chem/Reaction/explicit_h_audit.py +237 -0
  5. synkit/Chem/Reaction/tautomerize.py +1 -4
  6. synkit/Data/Mech/README.md +46 -0
  7. synkit/Data/Mech/polar.json +15921 -0
  8. synkit/Data/Mech/radical.json +12062 -0
  9. synkit/Data/Mech/stereo.json +6306 -0
  10. synkit/Data/aldol.json.gz +1 -0
  11. synkit/Data/paracetamol.json.gz +1 -0
  12. synkit/Graph/Hyrogen/_misc.py +7 -1
  13. synkit/Graph/ITS/its_construction.py +6 -0
  14. synkit/Graph/ITS/its_expand.py +115 -46
  15. synkit/Graph/ITS/its_reverter.py +28 -0
  16. synkit/Graph/ITS/rc_extractor.py +9 -0
  17. synkit/Graph/Matcher/batch_cluster.py +12 -39
  18. synkit/Graph/Matcher/graph_cluster.py +42 -52
  19. synkit/Graph/Matcher/graph_matcher.py +5 -48
  20. synkit/Graph/Matcher/subgraph_matcher.py +37 -94
  21. synkit/Graph/Mech/conversion.py +162 -0
  22. synkit/Graph/Mech/electron_accounting.py +28 -137
  23. synkit/Graph/Stereo/__init__.py +108 -0
  24. synkit/Graph/Stereo/changes.py +177 -0
  25. synkit/Graph/Stereo/couplings.py +224 -0
  26. synkit/Graph/Stereo/descriptors.py +734 -0
  27. synkit/Graph/Stereo/identity.py +565 -0
  28. synkit/Graph/Stereo/interchange.py +496 -0
  29. synkit/Graph/Stereo/matching.py +436 -0
  30. synkit/Graph/Stereo/outcomes.py +99 -0
  31. synkit/Graph/Stereo/rdkit_adapter.py +638 -0
  32. synkit/Graph/Wildcard/its_merge.py +6 -6
  33. synkit/Graph/syn_graph.py +48 -6
  34. synkit/Graph/utils.py +131 -2
  35. synkit/IO/__init__.py +1 -0
  36. synkit/IO/chem_converter.py +30 -23
  37. synkit/IO/conversion.py +42 -0
  38. synkit/IO/data_io.py +0 -45
  39. synkit/IO/graph_to_mol.py +69 -4
  40. synkit/IO/mol_to_graph.py +73 -1
  41. synkit/Mechanism/__init__.py +134 -0
  42. synkit/Mechanism/adapters.py +136 -0
  43. synkit/Mechanism/audit.py +323 -0
  44. synkit/Mechanism/benchmark.py +618 -0
  45. synkit/Mechanism/compatibility.py +10 -0
  46. synkit/Mechanism/drawing.py +85 -0
  47. synkit/Mechanism/equivalence.py +443 -0
  48. synkit/Mechanism/evidence.py +255 -0
  49. synkit/Mechanism/interchange.py +124 -0
  50. synkit/Mechanism/model.py +1214 -0
  51. synkit/Mechanism/radical_data.py +238 -0
  52. synkit/Mechanism/replay.py +574 -0
  53. synkit/Mechanism/schema.py +206 -0
  54. synkit/Mechanism/stereo_state.py +348 -0
  55. synkit/Mechanism/symbols.py +91 -0
  56. synkit/Rule/Compose/_identity.py +43 -0
  57. synkit/Rule/stereo_identity.py +200 -0
  58. synkit/Rule/syn_rule.py +474 -3
  59. synkit/Synthesis/Reactor/batch_reactor.py +11 -36
  60. synkit/Synthesis/Reactor/benchmark.py +0 -4
  61. synkit/Synthesis/Reactor/rbl_engine.py +258 -859
  62. synkit/Synthesis/Reactor/rule_filter.py +3 -5
  63. synkit/Synthesis/Reactor/syn_reactor.py +1748 -81
  64. synkit/Synthesis/reactor_utils.py +0 -27
  65. synkit/Vis/epd/models.py +30 -1
  66. synkit/Vis/epd/render.py +29 -14
  67. synkit/Vis/epd/visualizer.py +27 -13
  68. synkit/Vis/its_drawer.py +615 -0
  69. synkit/Vis/molecule_drawer.py +565 -0
  70. synkit/Vis/mtg_drawer.py +769 -0
  71. synkit/Vis/reaction/rule.py +11 -49
  72. synkit/Vis/reaction_drawer.py +285 -0
  73. synkit/Vis/vis_synedu/Vis/__init__.py +7 -0
  74. synkit/Vis/vis_synedu/Vis/dpo.py +862 -0
  75. synkit/Vis/vis_synedu/rxn_vis.py +382 -0
  76. synkit/Vis/vis_synedu/vis.py +501 -0
  77. synkit/version.py +12 -3
  78. {synkit-1.4.2b1.dist-info → synkit-2.0.0.dev1.dist-info}/METADATA +66 -5
  79. {synkit-1.4.2b1.dist-info → synkit-2.0.0.dev1.dist-info}/RECORD +82 -54
  80. {synkit-1.4.2b1.dist-info → synkit-2.0.0.dev1.dist-info}/WHEEL +1 -1
  81. synkit-2.0.0.dev1.dist-info/licenses/LICENSES/StereoMolGraph-MIT.txt +30 -0
  82. synkit/CRN/Construct/DAG/crn.py +0 -207
  83. synkit/CRN/Construct/DAG/mod_crn.py +0 -160
  84. synkit/IO/dg_to_gml.py +0 -133
  85. synkit/Rule/Apply/reactor_rule.py +0 -90
  86. synkit/Rule/Apply/retro_reactor.py +0 -212
  87. synkit/Rule/Apply/rule_rbl.py +0 -86
  88. synkit/Rule/Compose/compose_rule.py +0 -225
  89. synkit/Rule/Compose/rule_compose.py +0 -236
  90. synkit/Rule/Compose/rule_mapping.py +0 -315
  91. synkit/Rule/Compose/seq_comp.py +0 -71
  92. synkit/Rule/Compose/valence_constrain.py +0 -107
  93. synkit/Synthesis/MSR/multi_steps.py +0 -137
  94. synkit/Synthesis/Reactor/mod_aam.py +0 -279
  95. synkit/Synthesis/Reactor/mod_reactor.py +0 -427
  96. synkit/Synthesis/Reactor/single_predictor.py +0 -90
  97. {synkit-1.4.2b1.dist-info → synkit-2.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -36,14 +36,18 @@ class TransformationFP:
36
36
 
37
37
  @staticmethod
38
38
  def convert_arr2vec(arr: np.ndarray) -> cDataStructs.ExplicitBitVect:
39
- """Convert a NumPy array of bits into an RDKit ExplicitBitVect.
39
+ """Convert a non-negative NumPy array into an RDKit bit vector.
40
40
 
41
- :param arr: Array of 0/1 values representing a fingerprint.
41
+ :param arr: Array of non-negative fingerprint values.
42
42
  :type arr: np.ndarray
43
- :returns: RDKit bit vector constructed from the bit string.
43
+ :returns: RDKit bit vector with positive values encoded as 1.
44
44
  :rtype: cDataStructs.ExplicitBitVect
45
+ :raises ValueError: If the array contains negative or non-finite values.
45
46
  """
46
- bitstr = "".join(str(int(x)) for x in arr.flatten())
47
+ flat = np.asarray(arr).flatten()
48
+ if not np.isfinite(flat).all() or (flat < 0).any():
49
+ raise ValueError("Bit-vector output requires non-negative finite values.")
50
+ bitstr = "".join("1" if x else "0" for x in flat)
47
51
  return cDataStructs.CreateFromBitString(bitstr)
48
52
 
49
53
  def fit(
@@ -184,6 +184,7 @@ class AtomFeatureExtractor:
184
184
 
185
185
  return {
186
186
  "element": atom.GetSymbol(),
187
+ "isotope": int(atom.GetIsotope()),
187
188
  "aromatic": bool(atom.GetIsAromatic()),
188
189
  "hcount": int(atom.GetTotalNumHs()),
189
190
  "charge": int(atom.GetFormalCharge()),
@@ -1,6 +1,20 @@
1
1
  from .aam_validator import AAMValidator
2
+ from .explicit_h_audit import (
3
+ ExplicitHydrogenReactionAudit,
4
+ MappedBondChange,
5
+ audit_explicit_h_reaction,
6
+ reaction_smiles_from_annotated_text,
7
+ )
8
+ from synkit.Chem.utils import remove_explicit_H_from_rsmi
2
9
 
3
10
  # from .standardize import Standardize
4
11
  # from .canon_rsmi import CanonRSMI
5
12
 
6
- __all__ = ["AAMValidator"]
13
+ __all__ = [
14
+ "AAMValidator",
15
+ "ExplicitHydrogenReactionAudit",
16
+ "MappedBondChange",
17
+ "audit_explicit_h_reaction",
18
+ "reaction_smiles_from_annotated_text",
19
+ "remove_explicit_H_from_rsmi",
20
+ ]
@@ -0,0 +1,237 @@
1
+ """Selection gate for mapped reactions with hydrogen in the reaction center.
2
+
3
+ This module deliberately uses a narrow definition suitable for regression and
4
+ conformance data. An accepted reaction must be fully atom mapped, conserve
5
+ mapped atom identities, molecular formula, and total formal charge, and contain
6
+ at least one explicit mapped hydrogen incident to a changed bond.
7
+
8
+ The check is stricter than merely searching for ``"[H:"``. A mapped hydrogen
9
+ which is present but remote from every bond change does not qualify.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections import Counter
15
+ from dataclasses import dataclass
16
+
17
+ from rdkit import Chem
18
+ from rdkit.Chem.rdMolDescriptors import CalcMolFormula
19
+
20
+
21
+ @dataclass(frozen=True, order=True)
22
+ class MappedBondChange:
23
+ """One mapped bond before and after a reaction."""
24
+
25
+ atom_maps: tuple[int, int]
26
+ before: float
27
+ after: float
28
+
29
+ @property
30
+ def kind(self) -> str:
31
+ if self.before == 0.0:
32
+ return "FORMED"
33
+ if self.after == 0.0:
34
+ return "BROKEN"
35
+ return "ORDER_CHANGED"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ExplicitHydrogenReactionAudit:
40
+ """Structured result returned by :func:`audit_explicit_h_reaction`."""
41
+
42
+ reaction: str
43
+ errors: tuple[str, ...] = ()
44
+ reactant_formula: str | None = None
45
+ product_formula: str | None = None
46
+ reactant_charge: int | None = None
47
+ product_charge: int | None = None
48
+ atom_maps: tuple[int, ...] = ()
49
+ explicit_hydrogen_maps: tuple[int, ...] = ()
50
+ changed_hydrogen_maps: tuple[int, ...] = ()
51
+ reaction_center_maps: tuple[int, ...] = ()
52
+ changed_bonds: tuple[MappedBondChange, ...] = ()
53
+
54
+ @property
55
+ def accepted(self) -> bool:
56
+ """Whether the reaction satisfies every strict selection criterion."""
57
+
58
+ return not self.errors
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class _ReactionSide:
63
+ formula: str
64
+ elemental_composition: tuple[tuple[tuple[int, int], int], ...]
65
+ charge: int
66
+ atoms_by_map: dict[int, tuple[int, int]]
67
+ bonds_by_maps: dict[tuple[int, int], float]
68
+ unmapped_atoms: tuple[int, ...]
69
+ duplicate_maps: tuple[int, ...]
70
+
71
+
72
+ def reaction_smiles_from_annotated_text(text: str) -> str:
73
+ """Return the reaction token from legacy ``SMIRKS arrow-code`` text.
74
+
75
+ SynKit's polar and radical CSV files append electron-flow notation after a
76
+ space. Ordinary reaction SMILES contains no whitespace, so the first token
77
+ is the complete reaction in both the annotated and unannotated forms.
78
+ """
79
+
80
+ stripped = str(text).strip()
81
+ return stripped.split(maxsplit=1)[0] if stripped else ""
82
+
83
+
84
+ def _parse_side(smiles: str) -> _ReactionSide | None:
85
+ parser = Chem.SmilesParserParams()
86
+ parser.removeHs = False
87
+ molecule = Chem.MolFromSmiles(smiles, parser)
88
+ if molecule is None:
89
+ return None
90
+
91
+ atoms_by_map: dict[int, tuple[int, int]] = {}
92
+ unmapped_atoms = []
93
+ duplicate_maps = []
94
+ for atom in molecule.GetAtoms():
95
+ atom_map = int(atom.GetAtomMapNum())
96
+ if atom_map <= 0:
97
+ unmapped_atoms.append(atom.GetIdx())
98
+ continue
99
+ if atom_map in atoms_by_map:
100
+ duplicate_maps.append(atom_map)
101
+ continue
102
+ # Charge, radical state, and hydrogen count may legitimately change.
103
+ # Element and isotope may not change under an atom mapping.
104
+ atoms_by_map[atom_map] = (atom.GetAtomicNum(), atom.GetIsotope())
105
+
106
+ bonds_by_maps = {}
107
+ for bond in molecule.GetBonds():
108
+ begin_map = int(bond.GetBeginAtom().GetAtomMapNum())
109
+ end_map = int(bond.GetEndAtom().GetAtomMapNum())
110
+ if begin_map <= 0 or end_map <= 0:
111
+ continue
112
+ key = tuple(sorted((begin_map, end_map)))
113
+ bonds_by_maps[key] = float(bond.GetBondTypeAsDouble())
114
+
115
+ # CalcMolFormula includes a charge suffix (for example ``H4N+``). Keep
116
+ # that human-readable value in the report, but compare mass balance using
117
+ # an isotope-aware elemental inventory so charge has its own error code.
118
+ composition: Counter[tuple[int, int]] = Counter()
119
+ for atom in molecule.GetAtoms():
120
+ composition[(atom.GetAtomicNum(), atom.GetIsotope())] += 1
121
+ if atom.GetAtomicNum() != 1:
122
+ hydrogen_count = atom.GetTotalNumHs(includeNeighbors=False)
123
+ if hydrogen_count:
124
+ composition[(1, 0)] += int(hydrogen_count)
125
+
126
+ return _ReactionSide(
127
+ formula=CalcMolFormula(molecule),
128
+ elemental_composition=tuple(sorted(composition.items())),
129
+ charge=int(Chem.GetFormalCharge(molecule)),
130
+ atoms_by_map=atoms_by_map,
131
+ bonds_by_maps=bonds_by_maps,
132
+ unmapped_atoms=tuple(unmapped_atoms),
133
+ duplicate_maps=tuple(sorted(set(duplicate_maps))),
134
+ )
135
+
136
+
137
+ def audit_explicit_h_reaction(text: str) -> ExplicitHydrogenReactionAudit:
138
+ """Audit a mapped reaction for strict explicit-H-center suitability.
139
+
140
+ Parameters
141
+ ----------
142
+ text:
143
+ A mapped reaction SMILES, optionally followed by SynKit's legacy
144
+ whitespace-separated electron-flow annotation.
145
+
146
+ Returns
147
+ -------
148
+ ExplicitHydrogenReactionAudit
149
+ A report with stable error codes and the detected mapped bond changes.
150
+ """
151
+
152
+ reaction = reaction_smiles_from_annotated_text(text)
153
+ if reaction.count(">>") != 1:
154
+ return ExplicitHydrogenReactionAudit(
155
+ reaction,
156
+ errors=("INVALID_REACTION_SEPARATOR",),
157
+ )
158
+
159
+ reactant_text, product_text = reaction.split(">>", 1)
160
+ reactants = _parse_side(reactant_text)
161
+ products = _parse_side(product_text)
162
+ if reactants is None or products is None:
163
+ errors = []
164
+ if reactants is None:
165
+ errors.append("REACTANT_PARSE_FAILED")
166
+ if products is None:
167
+ errors.append("PRODUCT_PARSE_FAILED")
168
+ return ExplicitHydrogenReactionAudit(reaction, errors=tuple(errors))
169
+
170
+ errors = []
171
+ if reactants.unmapped_atoms:
172
+ errors.append("UNMAPPED_REACTANT_ATOM")
173
+ if products.unmapped_atoms:
174
+ errors.append("UNMAPPED_PRODUCT_ATOM")
175
+ if reactants.duplicate_maps:
176
+ errors.append("DUPLICATE_REACTANT_MAP")
177
+ if products.duplicate_maps:
178
+ errors.append("DUPLICATE_PRODUCT_MAP")
179
+
180
+ reactant_maps = set(reactants.atoms_by_map)
181
+ product_maps = set(products.atoms_by_map)
182
+ if reactant_maps != product_maps:
183
+ errors.append("MAP_INVENTORY_MISMATCH")
184
+ elif reactants.atoms_by_map != products.atoms_by_map:
185
+ errors.append("MAPPED_ATOM_IDENTITY_MISMATCH")
186
+
187
+ if reactants.elemental_composition != products.elemental_composition:
188
+ errors.append("FORMULA_IMBALANCE")
189
+ if reactants.charge != products.charge:
190
+ errors.append("CHARGE_IMBALANCE")
191
+
192
+ changed_bonds = []
193
+ all_bonds = set(reactants.bonds_by_maps) | set(products.bonds_by_maps)
194
+ for atom_maps in sorted(all_bonds):
195
+ before = reactants.bonds_by_maps.get(atom_maps, 0.0)
196
+ after = products.bonds_by_maps.get(atom_maps, 0.0)
197
+ if before != after:
198
+ changed_bonds.append(MappedBondChange(atom_maps, before, after))
199
+
200
+ hydrogen_maps = tuple(
201
+ sorted(
202
+ atom_map
203
+ for atom_map, identity in reactants.atoms_by_map.items()
204
+ if identity[0] == 1
205
+ )
206
+ )
207
+ if not hydrogen_maps:
208
+ errors.append("NO_EXPLICIT_MAPPED_HYDROGEN")
209
+
210
+ center_maps = tuple(
211
+ sorted({atom_map for change in changed_bonds for atom_map in change.atom_maps})
212
+ )
213
+ changed_hydrogen_maps = tuple(sorted(set(hydrogen_maps) & set(center_maps)))
214
+ if not changed_hydrogen_maps:
215
+ errors.append("NO_CHANGED_EXPLICIT_HYDROGEN")
216
+
217
+ return ExplicitHydrogenReactionAudit(
218
+ reaction=reaction,
219
+ errors=tuple(errors),
220
+ reactant_formula=reactants.formula,
221
+ product_formula=products.formula,
222
+ reactant_charge=reactants.charge,
223
+ product_charge=products.charge,
224
+ atom_maps=tuple(sorted(reactant_maps & product_maps)),
225
+ explicit_hydrogen_maps=hydrogen_maps,
226
+ changed_hydrogen_maps=changed_hydrogen_maps,
227
+ reaction_center_maps=center_maps,
228
+ changed_bonds=tuple(changed_bonds),
229
+ )
230
+
231
+
232
+ __all__ = [
233
+ "ExplicitHydrogenReactionAudit",
234
+ "MappedBondChange",
235
+ "audit_explicit_h_reaction",
236
+ "reaction_smiles_from_annotated_text",
237
+ ]
@@ -121,10 +121,7 @@ class Tautomerize:
121
121
  mol = Chem.MolFromSmiles(smiles)
122
122
  if mol is None:
123
123
  return []
124
- try:
125
- graph, groups = smiles_to_graph_and_functional_groups(smiles)
126
- except ValueError:
127
- return []
124
+ graph, groups = smiles_to_graph_and_functional_groups(smiles)
128
125
  node_to_idx = {
129
126
  (
130
127
  atom.GetAtomMapNum() if atom.GetAtomMapNum() else atom.GetIdx() + 1
@@ -0,0 +1,46 @@
1
+ # MechanismBench
2
+
3
+ `Data/Mech/` is the canonical on-branch location for the three reviewed
4
+ MechanismBench partitions. The former large `polar.csv` and `radical.csv`
5
+ source pools are not vendored; all release cases and their review provenance
6
+ are embedded in the JSON manifests below.
7
+
8
+ The public, executable MechanismBench layout has exactly three partitions:
9
+
10
+ - `radical.json` — 80 chemistry-reviewed, macro-balanced radical mechanisms.
11
+ Every case embeds its correction history, strict forward/reverse replay, and
12
+ unmapped rule-reapplication evidence.
13
+ - `polar.json` — 80 reviewed two-electron mechanisms selected deterministically
14
+ from SynEPD's 1,915-record `polar.json` source pool. The manifest records the
15
+ source SHA-256, CC BY 4.0 provenance, eight top-level POLAR strata, and strict
16
+ replay/reapplication evidence for every selected record. The full source pool
17
+ is intentionally not copied here.
18
+ - `stereo.json` — 72 positive transformations: the original 40 reaction-SMILES
19
+ cases, four reviewed non-tetrahedral/atrop rewrite fixtures, seven Phase 2R
20
+ electron-flow × stereo fixtures, and 21 reviewed native-descriptor rewrites.
21
+ Its eight negative assertions are retained as corruption/specification
22
+ fixtures and do not count as positive cases.
23
+
24
+ The compact nine-case rewrite conformance suite is development-only data at
25
+ `Test/Synthesis/Reactor/fixtures/small_rewrite_conformance.json`. It deliberately
26
+ does not define a MechanismBench partition.
27
+
28
+ The current stereo baseline is 72 positives, not 80. Any future claim of an
29
+ 80-positive stereo partition requires eight further independently reviewed
30
+ positive cases; negative fixtures must never be counted to fill that gap.
31
+
32
+ Raw candidate pools, audit summaries, and release-owner checklists do not belong
33
+ in this directory. They should be regenerated from source data or kept as project
34
+ documentation rather than presented as benchmark cases.
35
+
36
+ Generate the current replay/corruption/runtime evidence with:
37
+
38
+ ```bash
39
+ python tools/mechanismbench_evidence.py \
40
+ --output Data/Mech/evidence/mechanismbench_evidence.json
41
+ ```
42
+
43
+ The report currently covers typed `MechanismRecord` fixtures. It records the
44
+ reaction-SMILES, non-tetrahedral, and promoted native-descriptor rewrite
45
+ fixtures that remain outside that shared replay representation instead of
46
+ assigning them fabricated metrics.