synkit 1.6.5__py3-none-win_amd64.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 (636) hide show
  1. synkit/CRN/Benchmark/__init__.py +53 -0
  2. synkit/CRN/Benchmark/crosschecks.py +13 -0
  3. synkit/CRN/Benchmark/kegg.py +12 -0
  4. synkit/CRN/Benchmark/networks.py +13 -0
  5. synkit/CRN/Benchmark/scaling.py +13 -0
  6. synkit/CRN/Benchmark/validate.py +13 -0
  7. synkit/CRN/Construct/__init__.py +16 -0
  8. synkit/CRN/Construct/abstract.py +668 -0
  9. synkit/CRN/Construct/arity.py +36 -0
  10. synkit/CRN/Construct/builder.py +619 -0
  11. synkit/CRN/Construct/derivation.py +63 -0
  12. synkit/CRN/Construct/flattener.py +100 -0
  13. synkit/CRN/Construct/keys.py +14 -0
  14. synkit/CRN/Construct/mixtures.py +117 -0
  15. synkit/CRN/Construct/rdkit_worker.py +69 -0
  16. synkit/CRN/Construct/smiles.py +99 -0
  17. synkit/CRN/Construct/state.py +31 -0
  18. synkit/CRN/Construct/strategy.py +75 -0
  19. synkit/CRN/Construct/worker.py +42 -0
  20. synkit/CRN/IO/__init__.py +36 -0
  21. synkit/CRN/IO/sbml.py +925 -0
  22. synkit/CRN/Pathway/__init__.py +72 -0
  23. synkit/CRN/Pathway/_adapter.py +333 -0
  24. synkit/CRN/Pathway/pathfinder.py +500 -0
  25. synkit/CRN/Pathway/reachability.py +796 -0
  26. synkit/CRN/Pathway/realizability.py +1322 -0
  27. synkit/CRN/Petrinet/__init__.py +93 -0
  28. synkit/CRN/Petrinet/analyzer.py +486 -0
  29. synkit/CRN/Petrinet/minimal_semiflows.py +436 -0
  30. synkit/CRN/Petrinet/net.py +1041 -0
  31. synkit/CRN/Petrinet/persistence.py +97 -0
  32. synkit/CRN/Petrinet/semiflows.py +483 -0
  33. synkit/CRN/Petrinet/siphon_search.py +402 -0
  34. synkit/CRN/Petrinet/structure.py +286 -0
  35. synkit/CRN/Props/__init__.py +130 -0
  36. synkit/CRN/Props/_rank.py +29 -0
  37. synkit/CRN/Props/deficiency.py +953 -0
  38. synkit/CRN/Props/dynamics.py +832 -0
  39. synkit/CRN/Props/helper.py +151 -0
  40. synkit/CRN/Props/stoich.py +751 -0
  41. synkit/CRN/Props/thermo.py +584 -0
  42. synkit/CRN/Query/__init__.py +66 -0
  43. synkit/CRN/Query/cache.py +41 -0
  44. synkit/CRN/Query/kegg_api.py +99 -0
  45. synkit/CRN/Query/kegg_extract.py +714 -0
  46. synkit/CRN/Query/kegg_impute.py +707 -0
  47. synkit/CRN/Query/kegg_parse.py +480 -0
  48. synkit/CRN/Query/to_syncrn.py +290 -0
  49. synkit/CRN/README.md +234 -0
  50. synkit/CRN/Structure/__init__.py +12 -0
  51. synkit/CRN/Structure/_graph_io.py +713 -0
  52. synkit/CRN/Structure/_matrices.py +88 -0
  53. synkit/CRN/Structure/_parse.py +544 -0
  54. synkit/CRN/Structure/reaction.py +237 -0
  55. synkit/CRN/Structure/rule.py +55 -0
  56. synkit/CRN/Structure/species.py +46 -0
  57. synkit/CRN/Structure/syncrn.py +929 -0
  58. synkit/CRN/Symmetry/__init__.py +20 -0
  59. synkit/CRN/Symmetry/_common.py +1059 -0
  60. synkit/CRN/Symmetry/_ir.py +940 -0
  61. synkit/CRN/Symmetry/automorphism.py +472 -0
  62. synkit/CRN/Symmetry/canon.py +441 -0
  63. synkit/CRN/Symmetry/isomorphism.py +412 -0
  64. synkit/CRN/Symmetry/symmetry.py +61 -0
  65. synkit/CRN/Symmetry/wl_canon.py +967 -0
  66. synkit/CRN/Visualize/__init__.py +11 -0
  67. synkit/CRN/Visualize/labels.py +113 -0
  68. synkit/CRN/Visualize/layout.py +708 -0
  69. synkit/CRN/Visualize/palette.py +292 -0
  70. synkit/CRN/Visualize/validation.py +122 -0
  71. synkit/CRN/Visualize/vis.py +1152 -0
  72. synkit/CRN/__init__.py +233 -0
  73. synkit/CRN/_build.py +50 -0
  74. synkit/CRN/_native_build.py +175 -0
  75. synkit/CRN/analysis.py +629 -0
  76. synkit/CRN/backend.py +74 -0
  77. synkit/CRN/cpp/CMakeLists.txt +58 -0
  78. synkit/CRN/cpp/algebra.cpp +46 -0
  79. synkit/CRN/cpp/closure.cpp +29 -0
  80. synkit/CRN/cpp/exact.h +55 -0
  81. synkit/CRN/cpp/pathway.cpp +46 -0
  82. synkit/CRN/cpp/semiflows.cpp +209 -0
  83. synkit/CRN/cpp/structural.cpp +240 -0
  84. synkit/CRN/cpp/symmetry.cpp +45 -0
  85. synkit/CRN/cpp/write_manifest.cmake +28 -0
  86. synkit/CRN/kinds.py +137 -0
  87. synkit/CRN/lib/synkit_crn.dll +0 -0
  88. synkit/CRN/lib/synkit_crn.json +33 -0
  89. synkit/CRN/native.py +388 -0
  90. synkit/CRN/native_analysis.py +459 -0
  91. synkit/Chem/Cluster/__init__.py +0 -0
  92. synkit/Chem/Cluster/butina.py +140 -0
  93. synkit/Chem/Fingerprint/__init__.py +0 -0
  94. synkit/Chem/Fingerprint/fp_calculator.py +155 -0
  95. synkit/Chem/Fingerprint/smiles_featurizer.py +258 -0
  96. synkit/Chem/Fingerprint/transformation_fp.py +139 -0
  97. synkit/Chem/Mapper/README.rst +43 -0
  98. synkit/Chem/Mapper/__init__.py +255 -0
  99. synkit/Chem/Mapper/_compat.py +130 -0
  100. synkit/Chem/Mapper/adapters/__init__.py +1 -0
  101. synkit/Chem/Mapper/adapters/prediction.py +126 -0
  102. synkit/Chem/Mapper/adapters/template.py +412 -0
  103. synkit/Chem/Mapper/chem/__init__.py +52 -0
  104. synkit/Chem/Mapper/chem/aam.py +673 -0
  105. synkit/Chem/Mapper/chem/blind.py +160 -0
  106. synkit/Chem/Mapper/chem/its.py +309 -0
  107. synkit/Chem/Mapper/chem/smiles.py +564 -0
  108. synkit/Chem/Mapper/graph/__init__.py +66 -0
  109. synkit/Chem/Mapper/graph/automorphism.py +395 -0
  110. synkit/Chem/Mapper/graph/blockcut.py +98 -0
  111. synkit/Chem/Mapper/graph/labeled_graph.py +104 -0
  112. synkit/Chem/Mapper/graph/refine.py +176 -0
  113. synkit/Chem/Mapper/graph/synkit_adapter.py +110 -0
  114. synkit/Chem/Mapper/io/__init__.py +13 -0
  115. synkit/Chem/Mapper/io/idxmapstr.py +82 -0
  116. synkit/Chem/Mapper/mapper.py +685 -0
  117. synkit/Chem/Mapper/reaction_search.py +554 -0
  118. synkit/Chem/Mapper/reporting/__init__.py +1 -0
  119. synkit/Chem/Mapper/reporting/alternatives.py +451 -0
  120. synkit/Chem/Mapper/reporting/analysis.py +681 -0
  121. synkit/Chem/Mapper/reporting/native_analysis.py +485 -0
  122. synkit/Chem/Mapper/reporting/spectrum.py +642 -0
  123. synkit/Chem/Mapper/solvers/__init__.py +1 -0
  124. synkit/Chem/Mapper/solvers/exact/__init__.py +141 -0
  125. synkit/Chem/Mapper/solvers/exact/assignment/__init__.py +1 -0
  126. synkit/Chem/Mapper/solvers/exact/assignment/assignment_certificate.py +113 -0
  127. synkit/Chem/Mapper/solvers/exact/assignment/assignment_domains.py +162 -0
  128. synkit/Chem/Mapper/solvers/exact/assignment/assignment_verification.py +58 -0
  129. synkit/Chem/Mapper/solvers/exact/assignment/incremental_assignment.py +247 -0
  130. synkit/Chem/Mapper/solvers/exact/certificate.py +128 -0
  131. synkit/Chem/Mapper/solvers/exact/core.py +183 -0
  132. synkit/Chem/Mapper/solvers/exact/distance_search/__init__.py +1 -0
  133. synkit/Chem/Mapper/solvers/exact/distance_search/bounds.py +319 -0
  134. synkit/Chem/Mapper/solvers/exact/distance_search/exhaustive.py +179 -0
  135. synkit/Chem/Mapper/solvers/exact/distance_search/inputs.py +283 -0
  136. synkit/Chem/Mapper/solvers/exact/distance_search/records.py +242 -0
  137. synkit/Chem/Mapper/solvers/exact/distance_search/search.py +1025 -0
  138. synkit/Chem/Mapper/solvers/exact/distance_search/verify.py +378 -0
  139. synkit/Chem/Mapper/solvers/exact/edit_support.py +375 -0
  140. synkit/Chem/Mapper/solvers/exact/hybrid.py +224 -0
  141. synkit/Chem/Mapper/solvers/exact/hydrogen.py +264 -0
  142. synkit/Chem/Mapper/solvers/exact/kernel_solvers/__init__.py +1 -0
  143. synkit/Chem/Mapper/solvers/exact/kernel_solvers/branching.py +499 -0
  144. synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumerate.py +1050 -0
  145. synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumeration_result.py +30 -0
  146. synkit/Chem/Mapper/solvers/exact/kernel_solvers/kernel.py +256 -0
  147. synkit/Chem/Mapper/solvers/exact/kernel_solvers/milp.py +194 -0
  148. synkit/Chem/Mapper/solvers/exact/native/__init__.py +1 -0
  149. synkit/Chem/Mapper/solvers/exact/native/build.py +154 -0
  150. synkit/Chem/Mapper/solvers/exact/native/candidates.py +593 -0
  151. synkit/Chem/Mapper/solvers/exact/native/canonical.py +396 -0
  152. synkit/Chem/Mapper/solvers/exact/native/disk_frontier.py +415 -0
  153. synkit/Chem/Mapper/solvers/exact/native/frontier.py +390 -0
  154. synkit/Chem/Mapper/solvers/exact/native/hydrogen_quotient.py +213 -0
  155. synkit/Chem/Mapper/solvers/exact/native/its.py +528 -0
  156. synkit/Chem/Mapper/solvers/exact/native/native_distance.cpp +2803 -0
  157. synkit/Chem/Mapper/solvers/exact/native/orbit_aggregation.py +328 -0
  158. synkit/Chem/Mapper/solvers/exact/native/parallel.py +205 -0
  159. synkit/Chem/Mapper/solvers/exact/native/pmcd.py +203 -0
  160. synkit/Chem/Mapper/solvers/exact/native/runtime.py +40 -0
  161. synkit/Chem/Mapper/solvers/exact/native/search.py +294 -0
  162. synkit/Chem/Mapper/solvers/exact/native/separator.py +224 -0
  163. synkit/Chem/Mapper/solvers/exact/native_build.py +12 -0
  164. synkit/Chem/Mapper/solvers/exact/pabs/__init__.py +1 -0
  165. synkit/Chem/Mapper/solvers/exact/pabs/cost_lattice.py +43 -0
  166. synkit/Chem/Mapper/solvers/exact/pabs/factor_spectrum.py +110 -0
  167. synkit/Chem/Mapper/solvers/exact/pabs/multi_shell.py +368 -0
  168. synkit/Chem/Mapper/solvers/exact/pabs/propagation.py +679 -0
  169. synkit/Chem/Mapper/solvers/exact/pabs/propagation_bounds.py +436 -0
  170. synkit/Chem/Mapper/solvers/exact/pabs/propagation_limits.py +13 -0
  171. synkit/Chem/Mapper/solvers/exact/pabs/propagation_search.py +856 -0
  172. synkit/Chem/Mapper/solvers/exact/pabs/propagation_symmetry.py +66 -0
  173. synkit/Chem/Mapper/solvers/exact/pabs/residual_typed_bonds.py +136 -0
  174. synkit/Chem/Mapper/solvers/exact/pabs/reward_frontier.py +201 -0
  175. synkit/Chem/Mapper/solvers/exact/pabs/separator_bound.py +258 -0
  176. synkit/Chem/Mapper/solvers/exact/pabs/separator_spectrum.py +308 -0
  177. synkit/Chem/Mapper/solvers/exact/pabs/sparse_reward_bound.py +207 -0
  178. synkit/Chem/Mapper/solvers/exact/pabs/suffix_spectrum.py +299 -0
  179. synkit/Chem/Mapper/solvers/exact/search.py +55 -0
  180. synkit/Chem/Mapper/solvers/exact/seeds/__init__.py +1 -0
  181. synkit/Chem/Mapper/solvers/exact/seeds/seed.py +104 -0
  182. synkit/Chem/Mapper/solvers/exact/seeds/seed_fragments.py +148 -0
  183. synkit/Chem/Mapper/solvers/exact/seeds/seed_relaxation.py +181 -0
  184. synkit/Chem/Mapper/solvers/exact/symmetry.py +223 -0
  185. synkit/Chem/Mapper/solvers/native/__init__.py +4 -0
  186. synkit/Chem/Mapper/solvers/native/assignment.py +99 -0
  187. synkit/Chem/Mapper/solvers/native/astar.py +22 -0
  188. synkit/Chem/Mapper/solvers/native/build.py +385 -0
  189. synkit/Chem/Mapper/solvers/native/classification.py +641 -0
  190. synkit/Chem/Mapper/solvers/native/core.py +378 -0
  191. synkit/Chem/Mapper/solvers/native/cpp/CMakeLists.txt +120 -0
  192. synkit/Chem/Mapper/solvers/native/cpp/examples/map_reaction.cpp +29 -0
  193. synkit/Chem/Mapper/solvers/native/cpp/examples/slap_graph.cpp +63 -0
  194. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/assignment/assignment.h +18 -0
  195. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/astar.h +18 -0
  196. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/bound.h +33 -0
  197. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/pmcd_witness.h +16 -0
  198. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/runtime.h +130 -0
  199. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/hydrogen/hydrogen.h +73 -0
  200. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mapper.h +175 -0
  201. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mces/mces.h +9 -0
  202. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/pabs/seed.h +34 -0
  203. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/reaction/reaction.h +30 -0
  204. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/sepdp/sepdp.h +21 -0
  205. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/slap.h +72 -0
  206. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/solvers/solvers.h +16 -0
  207. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/classification.h +140 -0
  208. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/cpabs.h +33 -0
  209. synkit/Chem/Mapper/solvers/native/cpp/src/assignment/abb.cpp +302 -0
  210. synkit/Chem/Mapper/solvers/native/cpp/src/astar/CMakeLists.txt +12 -0
  211. synkit/Chem/Mapper/solvers/native/cpp/src/astar/README.md +47 -0
  212. synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar.cpp +228 -0
  213. synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar_graph.cpp +37 -0
  214. synkit/Chem/Mapper/solvers/native/cpp/src/astar/bound.cpp +212 -0
  215. synkit/Chem/Mapper/solvers/native/cpp/src/core/dispatch.cpp +272 -0
  216. synkit/Chem/Mapper/solvers/native/cpp/src/core/pmcd_witness.cpp +36 -0
  217. synkit/Chem/Mapper/solvers/native/cpp/src/hydrogen/hydrogen.cpp +380 -0
  218. synkit/Chem/Mapper/solvers/native/cpp/src/mces/README.md +57 -0
  219. synkit/Chem/Mapper/solvers/native/cpp/src/mces/mces.cpp +287 -0
  220. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/bridge.cpp +334 -0
  221. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/chemical_floor.inc +181 -0
  222. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/endpoint_symmetry.inc +104 -0
  223. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/exact_helpers.h +20 -0
  224. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_assignment_certificate.h +165 -0
  225. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_certificate.h +125 -0
  226. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_model.h +365 -0
  227. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_provider.inc +319 -0
  228. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_rounding.inc +170 -0
  229. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_test_hooks.h +342 -0
  230. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/objective_reduction.inc +137 -0
  231. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/overlap_dual.inc +351 -0
  232. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/profile_seed.inc +214 -0
  233. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/seed_heuristics.inc +640 -0
  234. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_embedding.cpp +430 -0
  235. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_root_lp.h +176 -0
  236. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/test_hooks.inc +262 -0
  237. synkit/Chem/Mapper/solvers/native/cpp/src/reaction/reaction.cpp +611 -0
  238. synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/README.md +61 -0
  239. synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/sepdp.cpp +253 -0
  240. synkit/Chem/Mapper/solvers/native/cpp/src/slap.cpp +810 -0
  241. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/formulation.h +138 -0
  242. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/maxsat.cpp +526 -0
  243. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/ortools.cpp +475 -0
  244. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/seed.h +80 -0
  245. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/weighted_cnf.h +256 -0
  246. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/README.md +43 -0
  247. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/dfs.cpp +123 -0
  248. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/edit_support.cpp +224 -0
  249. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification.cpp +553 -0
  250. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_pipeline.cpp +124 -0
  251. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_stream.h +13 -0
  252. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/cpabs.cpp +455 -0
  253. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/edge_deck.cpp +743 -0
  254. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
  255. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
  256. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naugraph.c +732 -0
  257. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.c +94 -0
  258. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.h +49 -0
  259. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.c +1771 -0
  260. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.h +133 -0
  261. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nautil.c +750 -0
  262. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naututil.h +355 -0
  263. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.c +1256 -0
  264. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.h +1544 -0
  265. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.c +1131 -0
  266. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.h +70 -0
  267. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/sorttemplates.c +580 -0
  268. synkit/Chem/Mapper/solvers/native/hydrogen.py +711 -0
  269. synkit/Chem/Mapper/solvers/native/implicit.py +506 -0
  270. synkit/Chem/Mapper/solvers/native/implicit_certificate.py +187 -0
  271. synkit/Chem/Mapper/solvers/native/implicit_preparation.py +309 -0
  272. synkit/Chem/Mapper/solvers/native/lib/synkit_mapper.dll +0 -0
  273. synkit/Chem/Mapper/solvers/native/mces.py +16 -0
  274. synkit/Chem/Mapper/solvers/native/reaction.py +179 -0
  275. synkit/Chem/Mapper/solvers/native/reaction_build.py +277 -0
  276. synkit/Chem/Mapper/solvers/native/reaction_text.py +26 -0
  277. synkit/Chem/Mapper/solvers/native/runtime.py +242 -0
  278. synkit/Chem/Mapper/solvers/native/sepdp.py +47 -0
  279. synkit/Chem/Mapper/solvers/native/slap.py +266 -0
  280. synkit/Chem/Mapper/solvers/native/solvers.py +67 -0
  281. synkit/Chem/Mapper/solvers/native/stereo.py +86 -0
  282. synkit/Chem/Mapper/solvers/native/supporting.py +34 -0
  283. synkit/Chem/Mapper/solvers/native/symmetry.py +91 -0
  284. synkit/Chem/Mapper/solvers/slap/__init__.py +29 -0
  285. synkit/Chem/Mapper/solvers/slap/lap.py +174 -0
  286. synkit/Chem/Mapper/solvers/slap/sequential.py +696 -0
  287. synkit/Chem/Mapper/validation/__init__.py +1 -0
  288. synkit/Chem/Mapper/validation/aam_validator.py +442 -0
  289. synkit/Chem/Mapper/validation/annotations.py +274 -0
  290. synkit/Chem/Mapper/validation/bonds.py +124 -0
  291. synkit/Chem/Mapper/validation/cohorts.py +72 -0
  292. synkit/Chem/Mapper/validation/identifiability.py +150 -0
  293. synkit/Chem/Mapper/validation/numerical_scope.py +36 -0
  294. synkit/Chem/Mapper/validation/orbits.py +137 -0
  295. synkit/Chem/Molecule/__init__.py +0 -0
  296. synkit/Chem/Molecule/_chirality_loci.py +165 -0
  297. synkit/Chem/Molecule/_chirality_types.py +231 -0
  298. synkit/Chem/Molecule/_helical_loci.py +167 -0
  299. synkit/Chem/Molecule/_stereo_axis_evidence.py +202 -0
  300. synkit/Chem/Molecule/_stereo_element_detection.py +193 -0
  301. synkit/Chem/Molecule/_stereo_orientation_constraints.py +511 -0
  302. synkit/Chem/Molecule/_tetrahedral_carriers.py +63 -0
  303. synkit/Chem/Molecule/atom_features.py +379 -0
  304. synkit/Chem/Molecule/chirality.py +1041 -0
  305. synkit/Chem/Molecule/cip_assignment.py +569 -0
  306. synkit/Chem/Molecule/cip_ranking.py +843 -0
  307. synkit/Chem/Molecule/coordinate_stereo.py +503 -0
  308. synkit/Chem/Molecule/derived_stereo_naming.py +165 -0
  309. synkit/Chem/Molecule/descriptors.py +452 -0
  310. synkit/Chem/Molecule/formula.py +249 -0
  311. synkit/Chem/Molecule/global_stereo.py +175 -0
  312. synkit/Chem/Molecule/graph_annotator.py +300 -0
  313. synkit/Chem/Molecule/standardize.py +817 -0
  314. synkit/Chem/Molecule/stereo_evidence.py +179 -0
  315. synkit/Chem/Molecule/stereo_perception.py +1032 -0
  316. synkit/Chem/Molecule/valence.py +123 -0
  317. synkit/Chem/Reaction/Mapper/__init__.py +44 -0
  318. synkit/Chem/Reaction/__init__.py +20 -0
  319. synkit/Chem/Reaction/aam_validator.py +5 -0
  320. synkit/Chem/Reaction/balance_check.py +138 -0
  321. synkit/Chem/Reaction/canon_rsmi.py +249 -0
  322. synkit/Chem/Reaction/cleaning.py +58 -0
  323. synkit/Chem/Reaction/deionize.py +199 -0
  324. synkit/Chem/Reaction/explicit_h_audit.py +232 -0
  325. synkit/Chem/Reaction/fix_aam.py +64 -0
  326. synkit/Chem/Reaction/neutralize.py +207 -0
  327. synkit/Chem/Reaction/radical_wildcard.py +224 -0
  328. synkit/Chem/Reaction/standardize.py +203 -0
  329. synkit/Chem/Reaction/tautomerize.py +205 -0
  330. synkit/Chem/__init__.py +1 -0
  331. synkit/Chem/utils.py +315 -0
  332. synkit/Data/__init__.py +0 -0
  333. synkit/Data/aldol.json.gz +1 -0
  334. synkit/Data/gen_partial_aam.py +147 -0
  335. synkit/Data/paracetamol.json.gz +1 -0
  336. synkit/Graph/Canon/__init__.py +16 -0
  337. synkit/Graph/Canon/_native/__init__.py +13 -0
  338. synkit/Graph/Canon/_native/build.py +93 -0
  339. synkit/Graph/Canon/_native/lib/synkit_canon.dll +0 -0
  340. synkit/Graph/Canon/_native/prepare.py +49 -0
  341. synkit/Graph/Canon/_native/runtime.py +140 -0
  342. synkit/Graph/Canon/_native/search.py +137 -0
  343. synkit/Graph/Canon/canon_algs.py +228 -0
  344. synkit/Graph/Canon/canon_graph.py +531 -0
  345. synkit/Graph/Canon/cpp/CMakeLists.txt +42 -0
  346. synkit/Graph/Canon/cpp/README.md +123 -0
  347. synkit/Graph/Canon/cpp/examples/canonical_graph.cpp +16 -0
  348. synkit/Graph/Canon/cpp/include/synkit_canon/canon.h +85 -0
  349. synkit/Graph/Canon/cpp/src/canonical.cpp +391 -0
  350. synkit/Graph/Canon/cpp/src/symmetry.h +83 -0
  351. synkit/Graph/Canon/cpp/tests/symmetry.cpp +93 -0
  352. synkit/Graph/Canon/exact.py +1020 -0
  353. synkit/Graph/Canon/exact_refinement.py +368 -0
  354. synkit/Graph/Canon/nauty.py +143 -0
  355. synkit/Graph/Context/__init__.py +0 -0
  356. synkit/Graph/Context/hier_context.py +244 -0
  357. synkit/Graph/Context/radius_expand.py +252 -0
  358. synkit/Graph/FG/__init__.py +19 -0
  359. synkit/Graph/FG/api.py +39 -0
  360. synkit/Graph/FG/audit.py +155 -0
  361. synkit/Graph/FG/catalog.py +919 -0
  362. synkit/Graph/FG/detector.py +288 -0
  363. synkit/Graph/FG/model.py +97 -0
  364. synkit/Graph/FG/recognizers.py +274 -0
  365. synkit/Graph/FG/ring_system.py +154 -0
  366. synkit/Graph/Feature/Descriptors/topology.py +857 -0
  367. synkit/Graph/Feature/Fingerprint/__init__.py +0 -0
  368. synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py +222 -0
  369. synkit/Graph/Feature/__init__.py +5 -0
  370. synkit/Graph/Feature/graph_descriptors.py +323 -0
  371. synkit/Graph/Feature/graph_fps.py +96 -0
  372. synkit/Graph/Feature/graph_signature.py +239 -0
  373. synkit/Graph/Feature/hash_fps.py +133 -0
  374. synkit/Graph/Feature/morgan_fps.py +88 -0
  375. synkit/Graph/Feature/path_fps.py +81 -0
  376. synkit/Graph/Feature/wl_hash.py +134 -0
  377. synkit/Graph/Fusion/__init__.py +85 -0
  378. synkit/Graph/Fusion/candidate.py +559 -0
  379. synkit/Graph/Fusion/constructor.py +877 -0
  380. synkit/Graph/Fusion/identity.py +184 -0
  381. synkit/Graph/Fusion/interface.py +700 -0
  382. synkit/Graph/Fusion/search.py +241 -0
  383. synkit/Graph/Hyrogen/__init__.py +6 -0
  384. synkit/Graph/Hyrogen/_misc.py +591 -0
  385. synkit/Graph/Hyrogen/hcomplete.py +995 -0
  386. synkit/Graph/Hyrogen/hcomplete_legacy.py +368 -0
  387. synkit/Graph/Hyrogen/hcompletion_algorithms.py +713 -0
  388. synkit/Graph/Hyrogen/hextend.py +570 -0
  389. synkit/Graph/Hyrogen/hextend_legacy.py +215 -0
  390. synkit/Graph/ITS/__init__.py +4 -0
  391. synkit/Graph/ITS/its_builder.py +120 -0
  392. synkit/Graph/ITS/its_construction.py +719 -0
  393. synkit/Graph/ITS/its_decompose.py +572 -0
  394. synkit/Graph/ITS/its_destruction.py +331 -0
  395. synkit/Graph/ITS/its_expand.py +1251 -0
  396. synkit/Graph/ITS/its_relabel.py +187 -0
  397. synkit/Graph/ITS/its_reverter.py +356 -0
  398. synkit/Graph/ITS/normalize_aam.py +145 -0
  399. synkit/Graph/ITS/partial_its.py +238 -0
  400. synkit/Graph/ITS/rc_extractor.py +656 -0
  401. synkit/Graph/ITS/stereo.py +74 -0
  402. synkit/Graph/MTG/__init__.py +107 -0
  403. synkit/Graph/MTG/gluing.py +451 -0
  404. synkit/Graph/MTG/history.py +928 -0
  405. synkit/Graph/MTG/inventory.py +118 -0
  406. synkit/Graph/MTG/local.py +447 -0
  407. synkit/Graph/MTG/mcs_matcher.py +246 -0
  408. synkit/Graph/MTG/mtg.py +611 -0
  409. synkit/Graph/MTG/mtg_explore.py +74 -0
  410. synkit/Graph/MTG/observation.py +209 -0
  411. synkit/Graph/MTG/process.py +577 -0
  412. synkit/Graph/MTG/queries.py +66 -0
  413. synkit/Graph/MTG/realization.py +95 -0
  414. synkit/Graph/MTG/series_parallel.py +107 -0
  415. synkit/Graph/MTG/utils.py +424 -0
  416. synkit/Graph/Matcher/__init__.py +10 -0
  417. synkit/Graph/Matcher/approx_mcs.py +1264 -0
  418. synkit/Graph/Matcher/auto_est.py +736 -0
  419. synkit/Graph/Matcher/automorphism.py +314 -0
  420. synkit/Graph/Matcher/batch_cluster.py +224 -0
  421. synkit/Graph/Matcher/dedup_matches.py +247 -0
  422. synkit/Graph/Matcher/graph_cluster.py +191 -0
  423. synkit/Graph/Matcher/graph_matcher.py +312 -0
  424. synkit/Graph/Matcher/graph_morphism.py +421 -0
  425. synkit/Graph/Matcher/mcs_matcher.py +804 -0
  426. synkit/Graph/Matcher/multi_turbo_iso.py +186 -0
  427. synkit/Graph/Matcher/orbit.py +284 -0
  428. synkit/Graph/Matcher/partial_matcher.py +694 -0
  429. synkit/Graph/Matcher/sing.py +544 -0
  430. synkit/Graph/Matcher/subgraph_matcher.py +812 -0
  431. synkit/Graph/Matcher/turbo_iso.py +272 -0
  432. synkit/Graph/Matcher/wl_sel.py +537 -0
  433. synkit/Graph/Mech/__init__.py +26 -0
  434. synkit/Graph/Mech/conversion.py +1466 -0
  435. synkit/Graph/Mech/conversion_debug.py +237 -0
  436. synkit/Graph/Mech/electron_accounting.py +200 -0
  437. synkit/Graph/Mech/lwg_editor.py +406 -0
  438. synkit/Graph/Mech/lwg_ops.py +313 -0
  439. synkit/Graph/Morphism/__init__.py +99 -0
  440. synkit/Graph/Morphism/constraints.py +460 -0
  441. synkit/Graph/Morphism/llg.py +999 -0
  442. synkit/Graph/Morphism/morphism.py +349 -0
  443. synkit/Graph/Morphism/stereo.py +258 -0
  444. synkit/Graph/Morphism/stereo_morphism.py +383 -0
  445. synkit/Graph/Morphism/stereo_morphism_helpers.py +957 -0
  446. synkit/Graph/Morphism/stereo_morphism_types.py +117 -0
  447. synkit/Graph/Stereo/__init__.py +376 -0
  448. synkit/Graph/Stereo/_descriptor_core.py +207 -0
  449. synkit/Graph/Stereo/_descriptor_serialization.py +138 -0
  450. synkit/Graph/Stereo/canonical.py +648 -0
  451. synkit/Graph/Stereo/changes.py +964 -0
  452. synkit/Graph/Stereo/composition.py +391 -0
  453. synkit/Graph/Stereo/configured.py +946 -0
  454. synkit/Graph/Stereo/couplings.py +245 -0
  455. synkit/Graph/Stereo/descriptors.py +1030 -0
  456. synkit/Graph/Stereo/enumeration.py +597 -0
  457. synkit/Graph/Stereo/extended_descriptors.py +406 -0
  458. synkit/Graph/Stereo/global_configured.py +152 -0
  459. synkit/Graph/Stereo/global_stereo.py +304 -0
  460. synkit/Graph/Stereo/identity.py +648 -0
  461. synkit/Graph/Stereo/interchange.py +496 -0
  462. synkit/Graph/Stereo/legacy.py +460 -0
  463. synkit/Graph/Stereo/matching.py +899 -0
  464. synkit/Graph/Stereo/native.py +154 -0
  465. synkit/Graph/Stereo/orbits.py +668 -0
  466. synkit/Graph/Stereo/outcomes.py +99 -0
  467. synkit/Graph/Stereo/pairwise.py +350 -0
  468. synkit/Graph/Stereo/rdkit_adapter.py +662 -0
  469. synkit/Graph/Stereo/rdkit_identity.py +118 -0
  470. synkit/Graph/Stereo/rdkit_resonance.py +140 -0
  471. synkit/Graph/Stereo/reaction_interchange.py +266 -0
  472. synkit/Graph/Stereo/semantics.py +296 -0
  473. synkit/Graph/Stereo/supports.py +257 -0
  474. synkit/Graph/Stereo/wire.py +339 -0
  475. synkit/Graph/Wildcard/__init__.py +0 -0
  476. synkit/Graph/Wildcard/fuse_graph.py +156 -0
  477. synkit/Graph/Wildcard/graph_wc.py +303 -0
  478. synkit/Graph/Wildcard/its_merge.py +857 -0
  479. synkit/Graph/Wildcard/radwc.py +114 -0
  480. synkit/Graph/Wildcard/wc_matcher.py +576 -0
  481. synkit/Graph/Wildcard/wildcard.py +225 -0
  482. synkit/Graph/__init__.py +17 -0
  483. synkit/Graph/canon_graph.py +14 -0
  484. synkit/Graph/syn_graph.py +199 -0
  485. synkit/Graph/utils.py +324 -0
  486. synkit/IO/__init__.py +4 -0
  487. synkit/IO/chem_converter.py +1059 -0
  488. synkit/IO/combinatorial/__init__.py +8 -0
  489. synkit/IO/combinatorial/gml_to_graph.py +223 -0
  490. synkit/IO/combinatorial/graph_to_gml.py +280 -0
  491. synkit/IO/combinatorial/graph_to_smarts.py +186 -0
  492. synkit/IO/combinatorial/smarts_expander.py +147 -0
  493. synkit/IO/combinatorial/smarts_generalizer.py +133 -0
  494. synkit/IO/combinatorial/smarts_to_graph.py +179 -0
  495. synkit/IO/conversion.py +42 -0
  496. synkit/IO/data_io.py +269 -0
  497. synkit/IO/data_process.py +143 -0
  498. synkit/IO/debug.py +73 -0
  499. synkit/IO/gml_to_nx.py +150 -0
  500. synkit/IO/graph_to_mol.py +199 -0
  501. synkit/IO/mol_to_graph.py +1105 -0
  502. synkit/IO/mol_to_graph_chemistry.py +545 -0
  503. synkit/IO/nx_to_gml.py +209 -0
  504. synkit/IO/smiles_to_id.py +123 -0
  505. synkit/Mechanism/__init__.py +151 -0
  506. synkit/Mechanism/adapters.py +136 -0
  507. synkit/Mechanism/audit.py +323 -0
  508. synkit/Mechanism/benchmark.py +638 -0
  509. synkit/Mechanism/compatibility.py +13 -0
  510. synkit/Mechanism/drawing.py +85 -0
  511. synkit/Mechanism/electron_models.py +725 -0
  512. synkit/Mechanism/equivalence.py +521 -0
  513. synkit/Mechanism/evidence.py +262 -0
  514. synkit/Mechanism/interchange.py +141 -0
  515. synkit/Mechanism/model.py +918 -0
  516. synkit/Mechanism/neighbor_stereo.py +312 -0
  517. synkit/Mechanism/radical_data.py +740 -0
  518. synkit/Mechanism/replay.py +986 -0
  519. synkit/Mechanism/schema.py +264 -0
  520. synkit/Mechanism/stereo_state.py +416 -0
  521. synkit/Mechanism/symbols.py +91 -0
  522. synkit/Rule/Apply/__init__.py +35 -0
  523. synkit/Rule/Apply/conditions.py +45 -0
  524. synkit/Rule/Apply/dpo.py +998 -0
  525. synkit/Rule/Apply/rule_matcher.py +218 -0
  526. synkit/Rule/Apply/validation.py +354 -0
  527. synkit/Rule/Compose/__init__.py +106 -0
  528. synkit/Rule/Compose/_identity.py +43 -0
  529. synkit/Rule/Compose/composition.py +976 -0
  530. synkit/Rule/Compose/laws.py +439 -0
  531. synkit/Rule/Compose/search.py +771 -0
  532. synkit/Rule/Compose/synrule.py +170 -0
  533. synkit/Rule/Modify/__init__.py +0 -0
  534. synkit/Rule/Modify/implict_rule.py +56 -0
  535. synkit/Rule/Modify/longest_path.py +92 -0
  536. synkit/Rule/Modify/molecule_rule.py +115 -0
  537. synkit/Rule/Modify/prune_templates.py +78 -0
  538. synkit/Rule/Modify/rule_utils.py +194 -0
  539. synkit/Rule/Modify/strip_rule.py +97 -0
  540. synkit/Rule/__init__.py +28 -0
  541. synkit/Rule/generic_stereo.py +884 -0
  542. synkit/Rule/stereo_identity.py +271 -0
  543. synkit/Rule/syn_rule.py +1090 -0
  544. synkit/Synthesis/MSR/__init__.py +0 -0
  545. synkit/Synthesis/MSR/path_finder.py +239 -0
  546. synkit/Synthesis/Metrics/__init__.py +0 -0
  547. synkit/Synthesis/Metrics/_base.py +52 -0
  548. synkit/Synthesis/Metrics/_plot.py +129 -0
  549. synkit/Synthesis/Metrics/_ranking.py +179 -0
  550. synkit/Synthesis/RBL/__init__.py +80 -0
  551. synkit/Synthesis/RBL/engine.py +1249 -0
  552. synkit/Synthesis/RBL/matching.py +652 -0
  553. synkit/Synthesis/RBL/overlap.py +336 -0
  554. synkit/Synthesis/RBL/pipeline.py +376 -0
  555. synkit/Synthesis/RBL/policy.py +139 -0
  556. synkit/Synthesis/RBL/proof.py +370 -0
  557. synkit/Synthesis/RBL/reaction.py +506 -0
  558. synkit/Synthesis/RBL/state.py +169 -0
  559. synkit/Synthesis/RBL/validation.py +1060 -0
  560. synkit/Synthesis/Reactor/__init__.py +60 -0
  561. synkit/Synthesis/Reactor/core/__init__.py +1 -0
  562. synkit/Synthesis/Reactor/core/engine.py +967 -0
  563. synkit/Synthesis/Reactor/core/product.py +621 -0
  564. synkit/Synthesis/Reactor/core/rewrite.py +561 -0
  565. synkit/Synthesis/Reactor/core/strategy.py +44 -0
  566. synkit/Synthesis/Reactor/matching/__init__.py +1 -0
  567. synkit/Synthesis/Reactor/matching/mixin.py +920 -0
  568. synkit/Synthesis/Reactor/matching/policy.py +345 -0
  569. synkit/Synthesis/Reactor/matching/symmetry.py +309 -0
  570. synkit/Synthesis/Reactor/output/__init__.py +1 -0
  571. synkit/Synthesis/Reactor/output/deduplication.py +703 -0
  572. synkit/Synthesis/Reactor/output/policy.py +16 -0
  573. synkit/Synthesis/Reactor/output/serialization.py +485 -0
  574. synkit/Synthesis/Reactor/output/structural.py +893 -0
  575. synkit/Synthesis/Reactor/stereo/__init__.py +1 -0
  576. synkit/Synthesis/Reactor/stereo/assignment.py +35 -0
  577. synkit/Synthesis/Reactor/stereo/mixin.py +780 -0
  578. synkit/Synthesis/Reactor/variants/__init__.py +1 -0
  579. synkit/Synthesis/Reactor/variants/imbalanced.py +180 -0
  580. synkit/Synthesis/Reactor/variants/partial.py +84 -0
  581. synkit/Synthesis/Reactor/workflow/__init__.py +1 -0
  582. synkit/Synthesis/Reactor/workflow/batch.py +550 -0
  583. synkit/Synthesis/Reactor/workflow/benchmark.py +143 -0
  584. synkit/Synthesis/Reactor/workflow/postprocess.py +267 -0
  585. synkit/Synthesis/Reactor/workflow/rule_filter.py +210 -0
  586. synkit/Synthesis/__init__.py +0 -0
  587. synkit/Synthesis/reactor_utils.py +316 -0
  588. synkit/Utils/__init__.py +0 -0
  589. synkit/Utils/utils.py +172 -0
  590. synkit/Vis/__init__.py +91 -0
  591. synkit/Vis/crn/__init__.py +40 -0
  592. synkit/Vis/epd/__init__.py +20 -0
  593. synkit/Vis/epd/arrows.py +425 -0
  594. synkit/Vis/epd/chem.py +197 -0
  595. synkit/Vis/epd/constants.py +83 -0
  596. synkit/Vis/epd/layout.py +283 -0
  597. synkit/Vis/epd/mapping.py +51 -0
  598. synkit/Vis/epd/models.py +136 -0
  599. synkit/Vis/epd/render.py +543 -0
  600. synkit/Vis/epd/utils.py +108 -0
  601. synkit/Vis/epd/visualizer.py +811 -0
  602. synkit/Vis/graph_visualizer.py +375 -0
  603. synkit/Vis/its/__init__.py +19 -0
  604. synkit/Vis/its/drawer.py +615 -0
  605. synkit/Vis/its_drawer.py +35 -0
  606. synkit/Vis/molecule/__init__.py +31 -0
  607. synkit/Vis/molecule/drawer.py +563 -0
  608. synkit/Vis/molecule/reaction.py +285 -0
  609. synkit/Vis/molecule_drawer.py +33 -0
  610. synkit/Vis/mtg/__init__.py +17 -0
  611. synkit/Vis/mtg/drawer.py +769 -0
  612. synkit/Vis/mtg_drawer.py +34 -0
  613. synkit/Vis/pdf_writer.py +146 -0
  614. synkit/Vis/reaction/__init__.py +19 -0
  615. synkit/Vis/reaction/rule.py +141 -0
  616. synkit/Vis/reaction/rxn.py +532 -0
  617. synkit/Vis/reaction_drawer.py +41 -0
  618. synkit/Vis/space/__init__.py +17 -0
  619. synkit/Vis/space/chemical.py +80 -0
  620. synkit/Vis/space/embedding.py +87 -0
  621. synkit/Vis/vis_synedu/Vis/__init__.py +7 -0
  622. synkit/Vis/vis_synedu/Vis/dpo.py +862 -0
  623. synkit/Vis/vis_synedu/rxn_vis.py +377 -0
  624. synkit/Vis/vis_synedu/vis.py +501 -0
  625. synkit/Vis/visual_drawer.py +215 -0
  626. synkit/Vis/visual_model.py +520 -0
  627. synkit/__init__.py +3 -0
  628. synkit/_native.py +91 -0
  629. synkit/examples.py +50 -0
  630. synkit/version.py +23 -0
  631. synkit-1.6.5.dist-info/METADATA +156 -0
  632. synkit-1.6.5.dist-info/RECORD +636 -0
  633. synkit-1.6.5.dist-info/WHEEL +4 -0
  634. synkit-1.6.5.dist-info/licenses/LICENSE +21 -0
  635. synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
  636. synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
@@ -0,0 +1,1041 @@
1
+ """Whole-molecule chirality classification by mirror automorphism.
2
+
3
+ This module is intentionally independent of reaction rules and reaction
4
+ stereo transport. It asks one molecular question: is a molecule isomorphic
5
+ to its mirror image?
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from functools import lru_cache
12
+ from typing import Any, Mapping
13
+
14
+ import networkx as nx
15
+ from rdkit import Chem
16
+ from rdkit.Chem.EnumerateStereoisomers import (
17
+ EnumerateStereoisomers,
18
+ GetStereoisomerCount,
19
+ StereoEnumerationOptions,
20
+ )
21
+
22
+ from synkit.Graph.Stereo.descriptors import (
23
+ AtropBondStereo,
24
+ CumuleneAxisStereo,
25
+ ExtendedCisTransStereo,
26
+ Reference,
27
+ TetrahedralStereo,
28
+ descriptor_id,
29
+ parse_virtual_reference,
30
+ virtual_reference,
31
+ )
32
+ from synkit.Graph.Stereo.extended_descriptors import HelicalStereo
33
+ from synkit.Graph.Stereo.identity import descriptor_relative_form
34
+ from synkit.IO.mol_to_graph import MolToGraph
35
+
36
+ from ._chirality_loci import detect_potential_axis_supports
37
+ from ._chirality_types import (
38
+ MolecularChirality,
39
+ MolecularChiralityAssessment,
40
+ MolecularChiralityOutcome,
41
+ MolecularChiralityResult,
42
+ PotentialStereoLocus,
43
+ PotentialStereoLocusType,
44
+ StereoOrientationState,
45
+ StereoStabilityStatus,
46
+ UnspecifiedMolecularStereoError,
47
+ connectivity_edge_match as _connectivity_edge_match,
48
+ molecular_node_colours as _molecular_node_colours,
49
+ molecular_node_match as _molecular_node_match,
50
+ )
51
+ from .stereo_evidence import (
52
+ ExtendedStereoStability,
53
+ MolecularStereoConfiguration,
54
+ MolecularStereoConfigurationSet,
55
+ StereoEvidenceSource,
56
+ StereoPopulationStatus,
57
+ )
58
+
59
+ __all__ = [
60
+ "MolecularChirality",
61
+ "MolecularChiralityAssessment",
62
+ "MolecularChiralityOutcome",
63
+ "MolecularChiralityResult",
64
+ "MolecularStereoConfiguration",
65
+ "MolecularStereoConfigurationSet",
66
+ "ExtendedStereoStability",
67
+ "PotentialStereoLocus",
68
+ "PotentialStereoLocusType",
69
+ "StereoOrientationState",
70
+ "StereoEvidenceSource",
71
+ "StereoPopulationStatus",
72
+ "StereoStabilityStatus",
73
+ "UnspecifiedMolecularStereoError",
74
+ "assess_molecular_chirality",
75
+ "classify_molecular_chirality",
76
+ "clear_molecular_chirality_cache",
77
+ "detect_potential_stereo_loci",
78
+ "is_molecular_chiral",
79
+ ]
80
+
81
+
82
+ def _resolved_cumulene_form(
83
+ descriptor: CumuleneAxisStereo,
84
+ resolve: Any,
85
+ ) -> tuple[Any, ...]:
86
+ """Resolve a complete cumulene path without entering rule semantics."""
87
+ path = tuple(resolve(atom) for atom in descriptor.axis_path)
88
+ frames = tuple(
89
+ tuple(resolve(reference) for reference in frame)
90
+ for frame in descriptor.terminal_frames
91
+ )
92
+ if descriptor.parity is None:
93
+ left, right = (tuple(sorted(frame, key=repr)) for frame in frames)
94
+ candidates = ((path, left, right), (tuple(reversed(path)), right, left))
95
+ return descriptor.descriptor_class, None, min(candidates, key=repr)
96
+ atoms = (*frames[0], path[0], path[-1], *frames[1])
97
+ if descriptor.parity == -1:
98
+ atoms = tuple(atoms[index] for index in descriptor._INVERSION)
99
+ candidates = []
100
+ for permutation in descriptor._PERMUTATIONS:
101
+ frame = tuple(atoms[index] for index in permutation)
102
+ oriented_path = tuple(reversed(path)) if permutation[2] == 3 else path
103
+ candidates.append((oriented_path, frame))
104
+ return descriptor.descriptor_class, 1, min(candidates, key=repr)
105
+
106
+
107
+ def _resolved_extended_cis_trans_form(
108
+ descriptor: ExtendedCisTransStereo,
109
+ resolve: Any,
110
+ ) -> tuple[Any, ...]:
111
+ """Resolve a complete odd-bond cumulene path and terminal frames."""
112
+ path = tuple(resolve(atom) for atom in descriptor.path)
113
+ frames = tuple(
114
+ tuple(resolve(reference) for reference in frame)
115
+ for frame in descriptor.terminal_frames
116
+ )
117
+ if descriptor.parity is None:
118
+ left, right = (tuple(sorted(frame, key=repr)) for frame in frames)
119
+ candidates = ((path, left, right), (tuple(reversed(path)), right, left))
120
+ return descriptor.descriptor_class, None, min(candidates, key=repr)
121
+ atoms = (*frames[0], path[0], path[-1], *frames[1])
122
+ candidates = []
123
+ for permutation in descriptor._PERMUTATIONS:
124
+ frame = tuple(atoms[index] for index in permutation)
125
+ oriented_path = tuple(reversed(path)) if permutation[2] == 3 else path
126
+ candidates.append((oriented_path, frame))
127
+ return descriptor.descriptor_class, 0, min(candidates, key=repr)
128
+
129
+
130
+ def _resolved_helical_form(
131
+ descriptor: HelicalStereo,
132
+ resolve: Any,
133
+ ) -> tuple[Any, ...]:
134
+ """Resolve open/cyclic path identity for molecular mirror matching."""
135
+ path = tuple(resolve(atom) for atom in descriptor.path)
136
+ if descriptor.cyclic:
137
+ reverse = tuple(reversed(path))
138
+ variants = tuple(
139
+ sequence[offset:] + sequence[:offset]
140
+ for sequence in (path, reverse)
141
+ for offset in range(len(path))
142
+ )
143
+ else:
144
+ variants = path, tuple(reversed(path))
145
+ return (
146
+ descriptor.descriptor_class,
147
+ min(variants, key=repr),
148
+ descriptor.cyclic,
149
+ descriptor.parity,
150
+ descriptor.coupling_id,
151
+ )
152
+
153
+
154
+ def _molecular_stereo_form(graph: Any) -> tuple[Any, ...]:
155
+ """Return a safe colour-refinement prefilter for mirror isomorphism."""
156
+ registry = graph.graph.get("stereo_descriptors", {})
157
+ if not registry:
158
+ return ()
159
+ colours = _molecular_node_colours(graph)
160
+
161
+ def resolve(reference: int | str) -> tuple[str, Any]:
162
+ if type(reference) is int:
163
+ if reference not in colours:
164
+ raise ValueError(f"Stereo reference {reference} is absent.")
165
+ return "atom", colours[reference]
166
+ virtual = parse_virtual_reference(reference)
167
+ if virtual is None or virtual.center not in colours:
168
+ raise ValueError(f"Invalid virtual stereo reference: {reference!r}.")
169
+ return "virtual", (virtual.kind, colours[virtual.center])
170
+
171
+ def descriptor_form(descriptor: Any) -> tuple[Any, ...]:
172
+ if isinstance(descriptor, CumuleneAxisStereo):
173
+ return _resolved_cumulene_form(descriptor, resolve)
174
+ if isinstance(descriptor, ExtendedCisTransStereo):
175
+ return _resolved_extended_cis_trans_form(descriptor, resolve)
176
+ if isinstance(descriptor, HelicalStereo):
177
+ return _resolved_helical_form(descriptor, resolve)
178
+ return descriptor_relative_form(descriptor, resolve)
179
+
180
+ return tuple(
181
+ sorted(
182
+ (descriptor_form(descriptor) for descriptor in registry.values()),
183
+ key=repr,
184
+ )
185
+ )
186
+
187
+
188
+ def _resolved_registry_matches(
189
+ source: tuple[Any, ...],
190
+ target: frozenset[Any],
191
+ mapping: Mapping[int, int],
192
+ ) -> bool:
193
+ """Reject any fully transported descriptor absent from the target."""
194
+ mapped_nodes = mapping.keys()
195
+ for descriptor in source:
196
+ if not descriptor.dependencies.issubset(mapped_nodes):
197
+ continue
198
+ if descriptor.relabel(mapping) not in target:
199
+ return False
200
+ return True
201
+
202
+
203
+ class _MolecularStereoGraphMatcher(nx.isomorphism.GraphMatcher):
204
+ """VF2 matcher that checks complete local frames during expansion."""
205
+
206
+ def __init__(self, left: Any, right: Any) -> None:
207
+ nx.set_node_attributes(
208
+ left,
209
+ _molecular_node_colours(left),
210
+ "_molecular_colour",
211
+ )
212
+ nx.set_node_attributes(
213
+ right,
214
+ _molecular_node_colours(right),
215
+ "_molecular_colour",
216
+ )
217
+ super().__init__(
218
+ left,
219
+ right,
220
+ node_match=_molecular_node_match,
221
+ edge_match=_connectivity_edge_match,
222
+ )
223
+ self._left_registry = tuple(left.graph.get("stereo_descriptors", {}).values())
224
+ self._right_registry = tuple(right.graph.get("stereo_descriptors", {}).values())
225
+ self._left_configurations = frozenset(self._left_registry)
226
+ self._right_configurations = frozenset(self._right_registry)
227
+
228
+ def semantic_feasibility(self, left_node: int, right_node: int) -> bool:
229
+ if not super().semantic_feasibility(left_node, right_node):
230
+ return False
231
+ forward = dict(self.core_1)
232
+ reverse = dict(self.core_2)
233
+ forward[left_node] = right_node
234
+ reverse[right_node] = left_node
235
+ return _resolved_registry_matches(
236
+ self._left_registry,
237
+ self._right_configurations,
238
+ forward,
239
+ ) and _resolved_registry_matches(
240
+ self._right_registry,
241
+ self._left_configurations,
242
+ reverse,
243
+ )
244
+
245
+
246
+ def _indexed_copy(molecule: Chem.Mol) -> Chem.Mol:
247
+ if molecule is None:
248
+ raise ValueError("Molecular chirality classification requires a molecule.")
249
+ working = Chem.Mol(molecule)
250
+ for atom in working.GetAtoms():
251
+ atom.SetAtomMapNum(atom.GetIdx() + 1)
252
+ Chem.AssignStereochemistry(working, cleanIt=False, force=True)
253
+ return working
254
+
255
+
256
+ def _configuration_covers_rdkit_locus(
257
+ molecule: Chem.Mol,
258
+ info: Any,
259
+ configuration: MolecularStereoConfiguration,
260
+ ) -> bool:
261
+ if info.type != Chem.StereoType.Bond_Double:
262
+ return False
263
+ bond = molecule.GetBondWithIdx(int(info.centeredOn))
264
+ locus = frozenset((bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()))
265
+ return any(
266
+ isinstance(
267
+ descriptor,
268
+ (CumuleneAxisStereo, ExtendedCisTransStereo),
269
+ )
270
+ and locus
271
+ in {
272
+ frozenset(pair)
273
+ for pair in zip(
274
+ (
275
+ descriptor.axis_path
276
+ if isinstance(descriptor, CumuleneAxisStereo)
277
+ else descriptor.path
278
+ ),
279
+ (
280
+ descriptor.axis_path[1:]
281
+ if isinstance(descriptor, CumuleneAxisStereo)
282
+ else descriptor.path[1:]
283
+ ),
284
+ )
285
+ }
286
+ for descriptor in configuration.descriptors
287
+ )
288
+
289
+
290
+ def _unspecified_stereo_loci(
291
+ molecule: Chem.Mol,
292
+ configurations: tuple[MolecularStereoConfiguration, ...] = (),
293
+ ) -> tuple[str, ...]:
294
+ def unresolved(info: Any) -> bool:
295
+ if info.specified == Chem.StereoSpecified.Specified:
296
+ return False
297
+ return not configurations or not all(
298
+ _configuration_covers_rdkit_locus(molecule, info, configuration)
299
+ for configuration in configurations
300
+ )
301
+
302
+ return tuple(
303
+ sorted(
304
+ f"{info.type}:{int(info.centeredOn)}"
305
+ for info in Chem.FindPotentialStereo(molecule)
306
+ if unresolved(info)
307
+ )
308
+ )
309
+
310
+
311
+ def _unsupported_stereo_loci(molecule: Chem.Mol) -> tuple[str, ...]:
312
+ supported = {
313
+ Chem.StereoType.Atom_Tetrahedral,
314
+ Chem.StereoType.Bond_Double,
315
+ }
316
+ return tuple(
317
+ sorted(
318
+ f"{info.type}:{int(info.centeredOn)}"
319
+ for info in Chem.FindPotentialStereo(molecule)
320
+ if info.specified != Chem.StereoSpecified.Specified
321
+ and info.type not in supported
322
+ )
323
+ )
324
+
325
+
326
+ def _complete_tetrahedral_topology(
327
+ molecule: Chem.Mol,
328
+ registry: dict[str, Any],
329
+ ) -> tuple[int, ...]:
330
+ """Add fixed probes at eligible unrepresented sp3 topologies.
331
+
332
+ These probes are not claims that each atom is a stereocentre. Their
333
+ arbitrary common orientation lets the whole-molecule automorphism test
334
+ decide which local probes cancel by symmetry and which collectively make
335
+ the molecular topology chiral. This is the role of ``stereo_complete`` in
336
+ the published StereoMolGraph validation protocol.
337
+ """
338
+ represented = {
339
+ descriptor.atoms[0]
340
+ for descriptor in registry.values()
341
+ if descriptor.descriptor_class == "tetrahedral"
342
+ }
343
+ completed = []
344
+ for atom in molecule.GetAtoms():
345
+ center = atom.GetIdx() + 1
346
+ if center in represented:
347
+ continue
348
+ if atom.GetHybridization() != Chem.HybridizationType.SP3:
349
+ continue
350
+ references: list[int | str] = [
351
+ neighbor.GetIdx() + 1 for neighbor in atom.GetNeighbors()
352
+ ]
353
+ hidden_hydrogens = int(atom.GetNumExplicitHs()) + int(atom.GetNumImplicitHs())
354
+ if len(references) == 3 and hidden_hydrogens == 1:
355
+ references.append(virtual_reference("H", center))
356
+ elif len(references) != 4:
357
+ continue
358
+ descriptor = TetrahedralStereo(
359
+ (center, *references),
360
+ 1,
361
+ "molecular_chirality:stereo_complete",
362
+ )
363
+ registry[descriptor_id(descriptor)] = descriptor
364
+ completed.append(center)
365
+ return tuple(completed)
366
+
367
+
368
+ def detect_potential_stereo_loci(
369
+ molecule: Chem.Mol,
370
+ *,
371
+ include_extended_ring_axes: bool = False,
372
+ ) -> tuple[PotentialStereoLocus, ...]:
373
+ """Return topology-supported, orientation-unspecified axial loci.
374
+
375
+ The detector intentionally over-approximates. A returned locus is not a
376
+ configured descriptor, a rotational-stability claim, or proof that the
377
+ complete molecule is chiral.
378
+ """
379
+ loci = tuple(
380
+ PotentialStereoLocus(
381
+ locus_type=PotentialStereoLocusType(locus_type),
382
+ support=support,
383
+ )
384
+ for locus_type, support in detect_potential_axis_supports(
385
+ molecule,
386
+ include_extended_ring_axes=include_extended_ring_axes,
387
+ )
388
+ )
389
+ return tuple(sorted(loci, key=lambda locus: locus.identifier))
390
+
391
+
392
+ def _validate_material_frame(
393
+ molecule: Chem.Mol,
394
+ owner: int,
395
+ references: tuple[Reference, ...],
396
+ ) -> None:
397
+ """Validate one zero-based molecular sidecar frame."""
398
+ owner_atom = molecule.GetAtomWithIdx(owner)
399
+ neighbors = {neighbor.GetIdx() for neighbor in owner_atom.GetNeighbors()}
400
+ virtual_counts = {"H": 0, "LP": 0}
401
+ for reference in references:
402
+ if type(reference) is int:
403
+ if reference < 0 or reference >= molecule.GetNumAtoms():
404
+ raise ValueError(f"Extended stereo reference {reference} is absent.")
405
+ if reference not in neighbors:
406
+ raise ValueError(
407
+ f"Extended stereo reference {reference} is not adjacent to {owner}."
408
+ )
409
+ continue
410
+ virtual = parse_virtual_reference(reference)
411
+ if virtual is None or virtual.center != owner:
412
+ raise ValueError(f"Invalid extended virtual reference: {reference!r}.")
413
+ virtual_counts[virtual.kind] += 1
414
+ available_h = int(owner_atom.GetNumExplicitHs()) + int(
415
+ owner_atom.GetNumImplicitHs()
416
+ )
417
+ if virtual_counts["H"] > available_h:
418
+ raise ValueError(f"Extended stereo requires unavailable hydrogen at {owner}.")
419
+
420
+
421
+ def _validate_molecular_configuration(
422
+ molecule: Chem.Mol,
423
+ configuration: MolecularStereoConfiguration,
424
+ ) -> None:
425
+ """Prove that declared zero-based descriptor supports exist in the molecule."""
426
+ atom_count = molecule.GetNumAtoms()
427
+ for descriptor in configuration.descriptors:
428
+ material = descriptor.dependencies
429
+ if any(atom < 0 or atom >= atom_count for atom in material):
430
+ raise ValueError("Extended stereo support contains an absent atom index.")
431
+ if isinstance(
432
+ descriptor,
433
+ (CumuleneAxisStereo, ExtendedCisTransStereo),
434
+ ):
435
+ path = (
436
+ descriptor.axis_path
437
+ if isinstance(descriptor, CumuleneAxisStereo)
438
+ else descriptor.path
439
+ )
440
+ for left, right in zip(path, path[1:]):
441
+ bond = molecule.GetBondBetweenAtoms(left, right)
442
+ if bond is None or bond.GetBondType() != Chem.BondType.DOUBLE:
443
+ raise ValueError(
444
+ "Cumulene evidence requires a continuous double-bond path."
445
+ )
446
+ for owner, frame in zip(
447
+ (path[0], path[-1]),
448
+ descriptor.terminal_frames,
449
+ ):
450
+ _validate_material_frame(molecule, owner, frame)
451
+ elif isinstance(descriptor, AtropBondStereo):
452
+ left, right = descriptor.atoms[2:4]
453
+ if molecule.GetBondBetweenAtoms(left, right) is None:
454
+ raise ValueError("Atrop evidence requires its central bond.")
455
+ _validate_material_frame(molecule, left, descriptor.atoms[:2])
456
+ _validate_material_frame(molecule, right, descriptor.atoms[4:])
457
+ elif isinstance(descriptor, HelicalStereo):
458
+ pairs = list(zip(descriptor.path, descriptor.path[1:]))
459
+ if descriptor.cyclic:
460
+ pairs.append((descriptor.path[-1], descriptor.path[0]))
461
+ if any(
462
+ molecule.GetBondBetweenAtoms(left, right) is None
463
+ for left, right in pairs
464
+ ):
465
+ raise ValueError(
466
+ "Helical evidence requires a continuous molecular path."
467
+ )
468
+
469
+
470
+ def _configuration_covers_locus(
471
+ configuration: MolecularStereoConfiguration,
472
+ locus: PotentialStereoLocus,
473
+ ) -> bool:
474
+ target = min(locus.support.path, tuple(reversed(locus.support.path)))
475
+ for descriptor in configuration.descriptors:
476
+ if isinstance(descriptor, (CumuleneAxisStereo, AtropBondStereo)):
477
+ path = descriptor.support.path
478
+ if min(path, tuple(reversed(path))) == target:
479
+ return True
480
+ return False
481
+
482
+
483
+ def _inject_molecular_configuration(
484
+ registry: dict[str, Any],
485
+ configuration: MolecularStereoConfiguration,
486
+ ) -> None:
487
+ for descriptor in configuration.graph_descriptors():
488
+ key = descriptor_id(descriptor)
489
+ previous = registry.get(key)
490
+ if previous is not None and previous != descriptor:
491
+ raise ValueError(f"Conflicting configured stereo evidence at {key}.")
492
+ registry[key] = descriptor
493
+
494
+
495
+ def _configuration_result_metadata(
496
+ configuration: MolecularStereoConfiguration | None,
497
+ ) -> dict[str, Any]:
498
+ if configuration is None:
499
+ return {}
500
+ return {
501
+ "configured_extended_descriptor_count": len(configuration.descriptors),
502
+ "stereo_evidence_source": configuration.evidence_source.value,
503
+ "extended_stability_status": configuration.stability.value,
504
+ "population_fraction": configuration.population_fraction,
505
+ }
506
+
507
+
508
+ def classify_molecular_chirality(
509
+ molecule: Chem.Mol,
510
+ *,
511
+ stereo_complete: bool = True,
512
+ require_specified: bool = False,
513
+ stereo_configuration: MolecularStereoConfiguration | None = None,
514
+ ) -> MolecularChiralityResult:
515
+ """Classify a molecule as globally chiral or achiral.
516
+
517
+ The molecular identity profile uses element, isotope, total hydrogen
518
+ count, and connectivity. It otherwise follows the published
519
+ StereoMolGraph validation protocol and deliberately avoids raw charge and
520
+ bond-order fields from a single Lewis/resonance form. Assigned local
521
+ descriptors are retained. When ``stereo_complete`` is true, eligible
522
+ unrepresented sp3 topologies receive the published protocol's completion
523
+ probes before all parity-bearing configurations are reflected. Cumulene
524
+ and biaryl topology is reported separately as orientation-unspecified
525
+ potential loci and never injected into the descriptor registry.
526
+
527
+ This is a molecule classifier. It does not extract, apply, or compare
528
+ reaction rules. The identity profile is a graph-topology convention, not a
529
+ quantum/geometric chirality proof: bond-order distinctions that do not
530
+ change hydrogen topology can be collapsed, as in validation case VS170.
531
+ ``stereo_complete`` cannot recover a stereochemical configuration erased
532
+ from an input SMILES. In particular, a topological biaryl candidate proves
533
+ neither orientation nor a high rotational barrier, and ordinary SMILES
534
+ cannot distinguish helicene handedness. Set ``require_specified`` to reject
535
+ RDKit-recognized unresolved input and detected unconfigured axial loci.
536
+ ``stereo_configuration`` may add fixed extended descriptors only through
537
+ the authorized evidence model; its references are zero-based RDKit indices.
538
+ """
539
+ if stereo_configuration is not None:
540
+ _validate_molecular_configuration(molecule, stereo_configuration)
541
+ working = _indexed_copy(molecule)
542
+ unspecified = _unspecified_stereo_loci(
543
+ working,
544
+ () if stereo_configuration is None else (stereo_configuration,),
545
+ )
546
+ potential_loci = detect_potential_stereo_loci(working)
547
+ potential_identifiers = tuple(
548
+ locus.identifier
549
+ for locus in potential_loci
550
+ if stereo_configuration is None
551
+ or not _configuration_covers_locus(stereo_configuration, locus)
552
+ )
553
+ unresolved = tuple(sorted((*unspecified, *potential_identifiers)))
554
+ if require_specified and unresolved:
555
+ raise UnspecifiedMolecularStereoError(unresolved)
556
+ graph = MolToGraph(attr_profile="minimal").transform(
557
+ working,
558
+ use_index_as_atom_map=True,
559
+ )
560
+ registry = dict(graph.graph.get("stereo_descriptors", {}))
561
+ completed = (
562
+ _complete_tetrahedral_topology(working, registry) if stereo_complete else ()
563
+ )
564
+ if stereo_configuration is not None:
565
+ _inject_molecular_configuration(registry, stereo_configuration)
566
+ graph.graph["stereo_descriptors"] = registry
567
+
568
+ mirror = graph.copy()
569
+ mirror.graph["stereo_descriptors"] = {
570
+ key: descriptor.invert() if descriptor.parity in {-1, 1} else descriptor
571
+ for key, descriptor in registry.items()
572
+ }
573
+ if _molecular_stereo_form(graph) != _molecular_stereo_form(mirror):
574
+ return MolecularChiralityResult(
575
+ classification=MolecularChirality.CHIRAL,
576
+ mirror_isomorphism=None,
577
+ descriptor_count=len(registry),
578
+ completed_tetrahedral_centers=completed,
579
+ potential_stereo_loci=potential_loci,
580
+ decision_method="stereo_colour_prefilter",
581
+ input_stereo_status=("underspecified" if unresolved else "specified"),
582
+ unspecified_stereo_loci=unresolved,
583
+ **_configuration_result_metadata(stereo_configuration),
584
+ )
585
+ matcher = _MolecularStereoGraphMatcher(graph, mirror)
586
+ mapping = dict(matcher.mapping) if matcher.is_isomorphic() else None
587
+ classification = (
588
+ MolecularChirality.ACHIRAL if mapping is not None else MolecularChirality.CHIRAL
589
+ )
590
+ return MolecularChiralityResult(
591
+ classification=classification,
592
+ mirror_isomorphism=(
593
+ None if mapping is None else tuple(sorted(mapping.items()))
594
+ ),
595
+ descriptor_count=len(registry),
596
+ completed_tetrahedral_centers=completed,
597
+ potential_stereo_loci=potential_loci,
598
+ input_stereo_status=("underspecified" if unresolved else "specified"),
599
+ unspecified_stereo_loci=unresolved,
600
+ **_configuration_result_metadata(stereo_configuration),
601
+ )
602
+
603
+
604
+ def is_molecular_chiral(
605
+ molecule: Chem.Mol,
606
+ *,
607
+ stereo_complete: bool = True,
608
+ require_specified: bool = False,
609
+ stereo_configuration: MolecularStereoConfiguration | None = None,
610
+ ) -> bool:
611
+ """Return the boolean whole-molecule chirality classification."""
612
+ return classify_molecular_chirality(
613
+ molecule,
614
+ stereo_complete=stereo_complete,
615
+ require_specified=require_specified,
616
+ stereo_configuration=stereo_configuration,
617
+ ).is_chiral
618
+
619
+
620
+ def _canonical_isomeric_smiles(molecule: Chem.Mol) -> str:
621
+ working = Chem.Mol(molecule)
622
+ for atom in working.GetAtoms():
623
+ atom.SetAtomMapNum(0)
624
+ return Chem.MolToSmiles(working, canonical=True, isomericSmiles=True)
625
+
626
+
627
+ @lru_cache(maxsize=4096)
628
+ def _cached_isomer_classification(
629
+ isomeric_smiles: str,
630
+ stereo_complete: bool,
631
+ ) -> MolecularChirality:
632
+ molecule = Chem.MolFromSmiles(isomeric_smiles)
633
+ if molecule is None:
634
+ raise ValueError(f"RDKit rejected cached isomeric SMILES: {isomeric_smiles}")
635
+ return classify_molecular_chirality(
636
+ molecule,
637
+ stereo_complete=stereo_complete,
638
+ require_specified=False,
639
+ ).classification
640
+
641
+
642
+ def clear_molecular_chirality_cache() -> None:
643
+ """Clear the bounded cache used by configuration-aware enumeration."""
644
+ _cached_isomer_classification.cache_clear()
645
+
646
+
647
+ @dataclass(frozen=True)
648
+ class _TetrahedralCompletionAnalysis:
649
+ """Exact achiral-existence result over unresolved tetrahedral assignments."""
650
+
651
+ achiral_witness: str | None
652
+ baseline_smiles: str
653
+ baseline_classification: MolecularChirality
654
+
655
+
656
+ def _parity_constraint_assignment(
657
+ descriptors: Mapping[int, TetrahedralStereo],
658
+ variable_centers: frozenset[int],
659
+ mapping: Mapping[int, int],
660
+ ) -> dict[int, int] | None:
661
+ """Solve mirror-compatibility XOR constraints for one automorphism."""
662
+ constraints: dict[int, list[tuple[int, int]]] = {
663
+ center: [] for center in descriptors
664
+ }
665
+ for center, source in descriptors.items():
666
+ target = descriptors.get(mapping[center])
667
+ if target is None:
668
+ return None
669
+ transported = source.relabel(mapping)
670
+ if transported == target.invert():
671
+ relation = 0
672
+ elif transported == target:
673
+ relation = 1
674
+ else:
675
+ return None
676
+ constraints[center].append((target.center, relation))
677
+ constraints[target.center].append((center, relation))
678
+
679
+ values = {center: 0 for center in descriptors if center not in variable_centers}
680
+ for root in descriptors:
681
+ if root not in values:
682
+ values[root] = 0
683
+ pending = [root]
684
+ while pending:
685
+ left = pending.pop()
686
+ for right, relation in constraints[left]:
687
+ expected = values[left] ^ relation
688
+ if right in values:
689
+ if values[right] != expected:
690
+ return None
691
+ continue
692
+ values[right] = expected
693
+ pending.append(right)
694
+ return values
695
+
696
+
697
+ def _flip_tetrahedral_assignments(
698
+ baseline: Chem.Mol,
699
+ variable_centers: frozenset[int],
700
+ values: Mapping[int, int],
701
+ ) -> Chem.Mol | None:
702
+ """Apply a solved descriptor-parity assignment to an RDKit baseline."""
703
+ witness = Chem.Mol(baseline)
704
+ for center in variable_centers:
705
+ if not values[center]:
706
+ continue
707
+ atom = witness.GetAtomWithIdx(center - 1)
708
+ tag = atom.GetChiralTag()
709
+ if tag == Chem.ChiralType.CHI_TETRAHEDRAL_CW:
710
+ atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CCW)
711
+ elif tag == Chem.ChiralType.CHI_TETRAHEDRAL_CCW:
712
+ atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CW)
713
+ else:
714
+ return None
715
+ return witness
716
+
717
+
718
+ def _analyze_unresolved_tetrahedral_completions(
719
+ molecule: Chem.Mol,
720
+ *,
721
+ stereo_complete: bool,
722
+ automorphism_limit: int = 4096,
723
+ ) -> _TetrahedralCompletionAnalysis | None:
724
+ """Prove whether any unresolved tetrahedral completion is achiral.
725
+
726
+ The raw assignment space grows as ``2**n``. Achirality, however, only
727
+ requires one constitutional automorphism whose transported local
728
+ configurations equal the mirror configurations. For each automorphism,
729
+ those requirements are binary XOR constraints. Exhausting the much
730
+ smaller automorphism set therefore proves nonexistence without enumerating
731
+ every stereoisomer.
732
+
733
+ ``None`` means this exact shortcut is outside its supported boundary or
734
+ exceeded its automorphism guard; callers must retain fail-closed capped
735
+ enumeration semantics.
736
+ """
737
+ unresolved = tuple(
738
+ info
739
+ for info in Chem.FindPotentialStereo(molecule)
740
+ if info.specified != Chem.StereoSpecified.Specified
741
+ )
742
+ if not unresolved or any(
743
+ info.type != Chem.StereoType.Atom_Tetrahedral for info in unresolved
744
+ ):
745
+ return None
746
+ variable_centers = frozenset(int(info.centeredOn) + 1 for info in unresolved)
747
+ options = StereoEnumerationOptions(
748
+ tryEmbedding=False,
749
+ onlyUnassigned=True,
750
+ maxIsomers=1,
751
+ rand=0x5A17,
752
+ unique=True,
753
+ )
754
+ baseline = next(iter(EnumerateStereoisomers(molecule, options=options)), None)
755
+ if baseline is None:
756
+ return None
757
+ baseline_smiles = _canonical_isomeric_smiles(baseline)
758
+ baseline_classification = classify_molecular_chirality(
759
+ baseline,
760
+ stereo_complete=stereo_complete,
761
+ require_specified=False,
762
+ ).classification
763
+
764
+ indexed = _indexed_copy(baseline)
765
+ graph = MolToGraph(attr_profile="minimal").transform(
766
+ indexed,
767
+ use_index_as_atom_map=True,
768
+ )
769
+ registry = dict(graph.graph.get("stereo_descriptors", {}))
770
+ if stereo_complete:
771
+ _complete_tetrahedral_topology(indexed, registry)
772
+ if any(not isinstance(value, TetrahedralStereo) for value in registry.values()):
773
+ return None
774
+ descriptors = {value.center: value for value in registry.values()}
775
+ if not variable_centers.issubset(descriptors):
776
+ return None
777
+
778
+ graph.graph["stereo_descriptors"] = {}
779
+ verification_failed = False
780
+ matcher = _MolecularStereoGraphMatcher(graph, graph)
781
+ for position, mapping in enumerate(matcher.isomorphisms_iter()):
782
+ if position >= automorphism_limit:
783
+ return None
784
+ values = _parity_constraint_assignment(
785
+ descriptors,
786
+ variable_centers,
787
+ mapping,
788
+ )
789
+ if values is None:
790
+ continue
791
+ witness = _flip_tetrahedral_assignments(
792
+ baseline,
793
+ variable_centers,
794
+ values,
795
+ )
796
+ if witness is None:
797
+ verification_failed = True
798
+ continue
799
+ witness_result = classify_molecular_chirality(
800
+ witness,
801
+ stereo_complete=stereo_complete,
802
+ require_specified=False,
803
+ )
804
+ if witness_result.classification is MolecularChirality.ACHIRAL:
805
+ return _TetrahedralCompletionAnalysis(
806
+ achiral_witness=_canonical_isomeric_smiles(witness),
807
+ baseline_smiles=baseline_smiles,
808
+ baseline_classification=baseline_classification,
809
+ )
810
+ verification_failed = True
811
+
812
+ if verification_failed or baseline_classification is MolecularChirality.ACHIRAL:
813
+ return None
814
+ return _TetrahedralCompletionAnalysis(
815
+ achiral_witness=None,
816
+ baseline_smiles=baseline_smiles,
817
+ baseline_classification=baseline_classification,
818
+ )
819
+
820
+
821
+ def _assessment_configuration_alternatives(
822
+ molecule: Chem.Mol,
823
+ configuration_set: MolecularStereoConfigurationSet | None,
824
+ ) -> tuple[MolecularStereoConfiguration | None, ...]:
825
+ if configuration_set is None:
826
+ return (None,)
827
+ for configuration in configuration_set.configurations:
828
+ _validate_molecular_configuration(molecule, configuration)
829
+ return configuration_set.configurations
830
+
831
+
832
+ def _covered_rdkit_binary_loci(
833
+ molecule: Chem.Mol,
834
+ configurations: tuple[MolecularStereoConfiguration | None, ...],
835
+ ) -> int:
836
+ configured = tuple(item for item in configurations if item is not None)
837
+ if len(configured) != len(configurations):
838
+ return 0
839
+ return sum(
840
+ info.specified != Chem.StereoSpecified.Specified
841
+ and all(
842
+ _configuration_covers_rdkit_locus(molecule, info, configuration)
843
+ for configuration in configured
844
+ )
845
+ for info in Chem.FindPotentialStereo(molecule)
846
+ )
847
+
848
+
849
+ def assess_molecular_chirality( # noqa: C901
850
+ molecule: Chem.Mol,
851
+ *,
852
+ max_isomers: int = 256,
853
+ stereo_complete: bool = True,
854
+ try_embedding: bool = False,
855
+ use_cache: bool = True,
856
+ stereo_configurations: MolecularStereoConfigurationSet | None = None,
857
+ ) -> MolecularChiralityAssessment:
858
+ """Assess chirality across supported completions of unresolved stereo.
859
+
860
+ RDKit-supported unassigned tetrahedral atoms and double bonds are
861
+ enumerated. A mixed chiral/achiral population proves
862
+ ``configuration_dependent`` even when the search cap truncates the full
863
+ population. For an oversized all-tetrahedral product, exact parity
864
+ constraints over the constitutional automorphisms can independently prove
865
+ that no achiral completion exists or construct an achiral witness. A
866
+ one-sided truncated population without such a proof is never promoted to
867
+ a necessary conclusion. Unsupported unresolved stereo types also fail
868
+ closed as ``unsupported_or_incomplete``.
869
+ """
870
+ if molecule is None:
871
+ raise ValueError("Molecular chirality assessment requires a molecule.")
872
+ if type(max_isomers) is not int or max_isomers < 1:
873
+ raise ValueError("max_isomers must be a positive integer.")
874
+
875
+ working = Chem.Mol(molecule)
876
+ configurations = _assessment_configuration_alternatives(
877
+ working, stereo_configurations
878
+ )
879
+ for atom in working.GetAtoms():
880
+ atom.SetAtomMapNum(0)
881
+ Chem.AssignStereochemistry(working, cleanIt=False, force=True)
882
+ configured = tuple(
883
+ configuration for configuration in configurations if configuration is not None
884
+ )
885
+ unspecified = _unspecified_stereo_loci(working, configured)
886
+ potential_loci = detect_potential_stereo_loci(working)
887
+ unsupported_potential = tuple(
888
+ locus.identifier
889
+ for locus in potential_loci
890
+ if any(
891
+ configuration is None
892
+ or not _configuration_covers_locus(configuration, locus)
893
+ for configuration in configurations
894
+ )
895
+ )
896
+ unsupported = tuple(
897
+ sorted((*_unsupported_stereo_loci(working),) + unsupported_potential)
898
+ )
899
+ if unsupported:
900
+ return MolecularChiralityAssessment(
901
+ outcome=MolecularChiralityOutcome.UNSUPPORTED_OR_INCOMPLETE,
902
+ observed_classifications=(),
903
+ input_stereo_status="underspecified",
904
+ unspecified_stereo_loci=unspecified,
905
+ unsupported_stereo_loci=unsupported,
906
+ theoretical_isomer_upper_bound=0,
907
+ evaluated_isomer_count=0,
908
+ enumeration_complete=False,
909
+ max_isomers=max_isomers,
910
+ configured_alternative_count=(
911
+ 0 if stereo_configurations is None else len(configurations)
912
+ ),
913
+ configured_population_status=(
914
+ None
915
+ if stereo_configurations is None
916
+ else stereo_configurations.population_status.value
917
+ ),
918
+ decision_method="unsupported_locus_detection",
919
+ )
920
+
921
+ options = StereoEnumerationOptions(
922
+ tryEmbedding=try_embedding,
923
+ onlyUnassigned=True,
924
+ maxIsomers=max_isomers,
925
+ rand=0x5A17,
926
+ unique=True,
927
+ )
928
+ raw_rdkit_theoretical = int(GetStereoisomerCount(working, options=options))
929
+ covered_binary = _covered_rdkit_binary_loci(working, configurations)
930
+ rdkit_theoretical = max(1, raw_rdkit_theoretical // (2**covered_binary))
931
+ theoretical = rdkit_theoretical * len(configurations)
932
+ classifications: set[MolecularChirality] = set()
933
+ representatives: dict[MolecularChirality, str] = {}
934
+ evaluated = 0
935
+ stopped_after_decisive_mixture = False
936
+ stopped_at_cap = False
937
+ decision_method = "stereo_completion_enumeration"
938
+
939
+ tetrahedral_analysis = None
940
+ if theoretical > max_isomers and configurations == (None,):
941
+ tetrahedral_analysis = _analyze_unresolved_tetrahedral_completions(
942
+ working,
943
+ stereo_complete=stereo_complete,
944
+ )
945
+ if tetrahedral_analysis is not None:
946
+ baseline_classification = tetrahedral_analysis.baseline_classification
947
+ classifications.add(baseline_classification)
948
+ representatives[baseline_classification] = tetrahedral_analysis.baseline_smiles
949
+ evaluated = 1
950
+ if tetrahedral_analysis.achiral_witness is None:
951
+ decision_method = "automorphism_parity_nonexistence_proof"
952
+ elif baseline_classification is MolecularChirality.CHIRAL:
953
+ classifications.add(MolecularChirality.ACHIRAL)
954
+ representatives[MolecularChirality.ACHIRAL] = (
955
+ tetrahedral_analysis.achiral_witness
956
+ )
957
+ evaluated += 1
958
+ stopped_after_decisive_mixture = True
959
+ decision_method = "automorphism_parity_achiral_witness"
960
+
961
+ analysis_is_decisive = tetrahedral_analysis is not None and (
962
+ tetrahedral_analysis.achiral_witness is None or len(classifications) > 1
963
+ )
964
+ if not analysis_is_decisive:
965
+ classifications.clear()
966
+ representatives.clear()
967
+ evaluated = 0
968
+ for isomer in EnumerateStereoisomers(working, options=options):
969
+ isomeric_smiles = _canonical_isomeric_smiles(isomer)
970
+ for configuration in configurations:
971
+ if evaluated >= max_isomers:
972
+ stopped_at_cap = True
973
+ break
974
+ classification = (
975
+ _cached_isomer_classification(isomeric_smiles, stereo_complete)
976
+ if use_cache and configuration is None
977
+ else classify_molecular_chirality(
978
+ isomer,
979
+ stereo_complete=stereo_complete,
980
+ stereo_configuration=configuration,
981
+ ).classification
982
+ )
983
+ evaluated += 1
984
+ classifications.add(classification)
985
+ representatives.setdefault(classification, isomeric_smiles)
986
+ if len(classifications) > 1:
987
+ stopped_after_decisive_mixture = True
988
+ break
989
+ if stopped_after_decisive_mixture or stopped_at_cap:
990
+ break
991
+
992
+ observed = tuple(sorted(classifications, key=lambda value: value.value))
993
+ evidence = tuple(
994
+ (representatives[classification], classification) for classification in observed
995
+ )
996
+ enumeration_exhausted = (
997
+ not analysis_is_decisive
998
+ and not stopped_after_decisive_mixture
999
+ and not stopped_at_cap
1000
+ and (theoretical <= max_isomers or evaluated < max_isomers)
1001
+ )
1002
+ if len(classifications) > 1:
1003
+ outcome = MolecularChiralityOutcome.CONFIGURATION_DEPENDENT
1004
+ if decision_method == "stereo_completion_enumeration":
1005
+ decision_method = "enumerated_mixed_population"
1006
+ elif (
1007
+ decision_method == "automorphism_parity_nonexistence_proof"
1008
+ and classifications == {MolecularChirality.CHIRAL}
1009
+ ):
1010
+ outcome = MolecularChiralityOutcome.NECESSARILY_CHIRAL
1011
+ elif not enumeration_exhausted or not classifications:
1012
+ outcome = MolecularChiralityOutcome.UNSUPPORTED_OR_INCOMPLETE
1013
+ decision_method = "capped_stereo_completion_enumeration"
1014
+ elif MolecularChirality.CHIRAL in classifications:
1015
+ outcome = MolecularChiralityOutcome.NECESSARILY_CHIRAL
1016
+ decision_method = "complete_stereo_completion_enumeration"
1017
+ else:
1018
+ outcome = MolecularChiralityOutcome.NECESSARILY_ACHIRAL
1019
+ decision_method = "complete_stereo_completion_enumeration"
1020
+
1021
+ return MolecularChiralityAssessment(
1022
+ outcome=outcome,
1023
+ observed_classifications=observed,
1024
+ input_stereo_status=("underspecified" if unspecified else "specified"),
1025
+ unspecified_stereo_loci=unspecified,
1026
+ unsupported_stereo_loci=(),
1027
+ theoretical_isomer_upper_bound=theoretical,
1028
+ evaluated_isomer_count=evaluated,
1029
+ enumeration_complete=enumeration_exhausted,
1030
+ max_isomers=max_isomers,
1031
+ representative_isomers=evidence,
1032
+ configured_alternative_count=(
1033
+ 0 if stereo_configurations is None else len(configurations)
1034
+ ),
1035
+ configured_population_status=(
1036
+ None
1037
+ if stereo_configurations is None
1038
+ else stereo_configurations.population_status.value
1039
+ ),
1040
+ decision_method=decision_method,
1041
+ )