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,1032 @@
1
+ """Typed evidence for molecular stereo-element perception.
2
+
3
+ Perception answers whether the molecular input can support a stereo element.
4
+ It does not assign a configuration, a CIP label, configurational stability, or
5
+ whole-molecule chirality. Keeping this boundary explicit prevents potential
6
+ topology from being promoted to an invented stereochemical state.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ import hashlib
14
+ from typing import Iterable, Mapping
15
+
16
+ import networkx as nx
17
+ from networkx.algorithms.isomorphism import (
18
+ GraphMatcher,
19
+ categorical_edge_match,
20
+ categorical_node_match,
21
+ )
22
+ from rdkit import Chem
23
+
24
+ from synkit.Graph.Stereo.descriptors import (
25
+ Reference,
26
+ TetrahedralStereo,
27
+ virtual_reference,
28
+ )
29
+ from synkit.Graph.Stereo.supports import (
30
+ AtomStereoSupport,
31
+ AxisStereoSupport,
32
+ BondStereoSupport,
33
+ PathStereoSupport,
34
+ StereoSupport,
35
+ )
36
+
37
+ from ._stereo_axis_evidence import (
38
+ StereoCarrierStatus,
39
+ validated_carrier_status,
40
+ )
41
+ from ._stereo_orientation_constraints import (
42
+ LocalOrientationConstraints,
43
+ extract_local_orientation_constraints,
44
+ tetrahedral_stabilizer_result,
45
+ )
46
+ from ._tetrahedral_carriers import (
47
+ detect_tetrahedral_carriers,
48
+ hidden_hydrogen_count as _hidden_hydrogen_count,
49
+ is_tetrahedral_carrier as _is_tetrahedral_carrier,
50
+ lone_pair_count as _lone_pair_count,
51
+ )
52
+
53
+
54
+ class StereoElementType(str, Enum):
55
+ """Stereo-element classes currently recognized from molecular input."""
56
+
57
+ TETRAHEDRAL = "tetrahedral"
58
+ DOUBLE_BOND = "double_bond"
59
+ CUMULENE_AXIS = "cumulene_axis"
60
+ EXTENDED_CIS_TRANS = "extended_cis_trans"
61
+ ATROP_AXIS = "atrop_axis"
62
+ HELICAL = "helical"
63
+
64
+
65
+ class StereoConfigurationState(str, Enum):
66
+ """Whether the input supplies a configuration for a perceived element."""
67
+
68
+ SPECIFIED = "specified"
69
+ UNSPECIFIED = "unspecified"
70
+
71
+
72
+ class TetrahedralConstitutionStatus(str, Enum):
73
+ """Canonicalization conclusion for one tetrahedral carrier."""
74
+
75
+ CONSTITUTIONALLY_DISTINCT = "constitutionally_distinct"
76
+ CONSTITUTIONALLY_ORIENTATION_DISTINCT = "constitutionally_orientation_distinct"
77
+ STEREO_DEPENDENT_DISTINCT = "stereo_dependent_distinct"
78
+ SYMMETRY_RELATED = "symmetry_related"
79
+
80
+
81
+ class TetrahedralFrameStatus(str, Enum):
82
+ """Outcome of configuration-neutral tetrahedral frame construction."""
83
+
84
+ CANONICAL = "canonical"
85
+ ORIENTATION_ORBIT = "orientation_orbit"
86
+ SYMMETRY_RELATED = "symmetry_related"
87
+ NEIGHBORHOOD_KEY_COLLISION = "neighborhood_key_collision"
88
+
89
+
90
+ @dataclass(frozen=True, order=True)
91
+ class LocalNeighborKey:
92
+ """Recursive, configuration-neutral key for one ligand environment.
93
+
94
+ ``digest`` is derived only from constitutional atom/bond attributes and
95
+ recursively refined neighboring keys. It never includes atom indices,
96
+ supplied stereo, CIP properties, or coordinates. Exact carrier-fixed
97
+ automorphisms remain authoritative when two references share a key.
98
+ """
99
+
100
+ digest: str
101
+ depth: int
102
+ algorithm: str = "recursive_local_neighbor_sha256_v2"
103
+
104
+ def __post_init__(self) -> None:
105
+ if len(self.digest) != 64 or any(
106
+ character not in "0123456789abcdef" for character in self.digest
107
+ ):
108
+ raise ValueError("Local-neighbor keys require a SHA-256 hex digest.")
109
+ if type(self.depth) is not int or self.depth < 0:
110
+ raise ValueError("Local-neighbor key depth must be non-negative.")
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class LigandSymmetryClass:
115
+ """Ligand slots equivalent under automorphisms fixing their owner."""
116
+
117
+ references: tuple[Reference, ...]
118
+ multiplicity: int
119
+ neighborhood_key: LocalNeighborKey | None = None
120
+
121
+ def __post_init__(self) -> None:
122
+ object.__setattr__(self, "references", tuple(self.references))
123
+ if not self.references:
124
+ raise ValueError("Ligand symmetry classes cannot be empty.")
125
+ if type(self.multiplicity) is not int or self.multiplicity < 1:
126
+ raise ValueError("Ligand symmetry multiplicity must be positive.")
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class TetrahedralConstitution:
131
+ """Exact local-symmetry evidence for a tetrahedral carrier.
132
+
133
+ The authoritative criterion is the parity action of the exact center
134
+ stabilizer on the four coordination slots: the carrier is stereogenic
135
+ exactly when no allowed automorphism induces an odd slot permutation.
136
+ ``ligand_classes`` records the coarser slot orbits for diagnostics. Local
137
+ orientation constraints may restrict the stabilizer, but configuration at
138
+ the focal center, CIP labels, ranks, and atom indices never participate.
139
+ """
140
+
141
+ support: AtomStereoSupport
142
+ ligand_classes: tuple[LigandSymmetryClass, ...]
143
+ status: TetrahedralConstitutionStatus
144
+ automorphism_witness_checks: int
145
+ canonical_frame: tuple[Reference, ...] | None = None
146
+ frame_status: TetrahedralFrameStatus = TetrahedralFrameStatus.SYMMETRY_RELATED
147
+ refinement_depth: int = 0
148
+ dependency_depth: int = 0
149
+ stereo_marker_count: int = 0
150
+ method: str = "exact_center_stabilizer_slot_parity_v1"
151
+
152
+ @property
153
+ def ligand_count(self) -> int:
154
+ return sum(item.multiplicity for item in self.ligand_classes)
155
+
156
+ @property
157
+ def confirms_stereogenic_center(self) -> bool:
158
+ return self.status in {
159
+ TetrahedralConstitutionStatus.CONSTITUTIONALLY_DISTINCT,
160
+ TetrahedralConstitutionStatus.CONSTITUTIONALLY_ORIENTATION_DISTINCT,
161
+ TetrahedralConstitutionStatus.STEREO_DEPENDENT_DISTINCT,
162
+ }
163
+
164
+ @property
165
+ def is_stereo_dependent(self) -> bool:
166
+ return self.status is TetrahedralConstitutionStatus.STEREO_DEPENDENT_DISTINCT
167
+
168
+ @property
169
+ def has_canonical_frame(self) -> bool:
170
+ return self.frame_status is TetrahedralFrameStatus.CANONICAL
171
+
172
+
173
+ _EXPECTED_SUPPORT = {
174
+ StereoElementType.TETRAHEDRAL: AtomStereoSupport,
175
+ StereoElementType.DOUBLE_BOND: BondStereoSupport,
176
+ StereoElementType.CUMULENE_AXIS: AxisStereoSupport,
177
+ StereoElementType.EXTENDED_CIS_TRANS: AxisStereoSupport,
178
+ StereoElementType.ATROP_AXIS: AxisStereoSupport,
179
+ StereoElementType.HELICAL: PathStereoSupport,
180
+ }
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class PotentialStereoElement:
185
+ """One perceived stereo carrier, separate from configured stereo state.
186
+
187
+ Atom references use zero-based RDKit indices. ``source_identifier`` is a
188
+ diagnostic pointer into the perception source, not part of chemical
189
+ identity. In particular, a returned axis carries no barrier or handedness
190
+ claim.
191
+ """
192
+
193
+ element_type: StereoElementType
194
+ support: StereoSupport
195
+ configuration_state: StereoConfigurationState
196
+ evidence_provenance: str
197
+ source_identifier: str
198
+ constitutional_evidence: TetrahedralConstitution | None = None
199
+ configuration: TetrahedralStereo | None = None
200
+ carrier_status: StereoCarrierStatus = StereoCarrierStatus.CONFIRMED
201
+ carrier_reason: str | None = None
202
+
203
+ def __post_init__(self) -> None:
204
+ object.__setattr__(self, "element_type", StereoElementType(self.element_type))
205
+ object.__setattr__(
206
+ self,
207
+ "configuration_state",
208
+ StereoConfigurationState(self.configuration_state),
209
+ )
210
+ object.__setattr__(
211
+ self,
212
+ "carrier_status",
213
+ validated_carrier_status(self.carrier_status, self.carrier_reason),
214
+ )
215
+ expected = _EXPECTED_SUPPORT[self.element_type]
216
+ if not isinstance(self.support, expected):
217
+ raise TypeError(
218
+ f"{self.element_type.value} perception requires "
219
+ f"{expected.__name__}, not {type(self.support).__name__}."
220
+ )
221
+ if not self.evidence_provenance:
222
+ raise ValueError("Stereo perception provenance cannot be empty.")
223
+ if not self.source_identifier:
224
+ raise ValueError("Stereo perception source identifier cannot be empty.")
225
+ if self.constitutional_evidence is not None:
226
+ if self.element_type is not StereoElementType.TETRAHEDRAL:
227
+ raise TypeError(
228
+ "Constitutional evidence is currently supported only for "
229
+ "tetrahedral elements."
230
+ )
231
+ if self.constitutional_evidence.support != self.support:
232
+ raise ValueError(
233
+ "Constitutional evidence and perceived support disagree."
234
+ )
235
+ if self.configuration is not None:
236
+ if self.element_type is not StereoElementType.TETRAHEDRAL:
237
+ raise TypeError(
238
+ "Attached configuration is currently supported only for "
239
+ "tetrahedral elements."
240
+ )
241
+ if self.configuration.support != self.support:
242
+ raise ValueError("Configuration and perceived support disagree.")
243
+ if self.configuration_state is not StereoConfigurationState.SPECIFIED:
244
+ raise ValueError(
245
+ "An attached configuration requires specified input evidence."
246
+ )
247
+ if self.constitutional_evidence is None:
248
+ raise ValueError(
249
+ "Attached configuration requires constitutional evidence."
250
+ )
251
+ if self.constitutional_evidence.canonical_frame != self.configuration.atoms:
252
+ raise ValueError(
253
+ "Attached configuration must use the constitutional frame."
254
+ )
255
+
256
+ @property
257
+ def identifier(self) -> str:
258
+ """Return a deterministic support identifier for diagnostics."""
259
+ if isinstance(self.support, AtomStereoSupport):
260
+ carrier = str(self.support.center)
261
+ elif isinstance(self.support, BondStereoSupport):
262
+ carrier = "-".join(str(value) for value in self.support.endpoints)
263
+ else:
264
+ carrier = "-".join(str(value) for value in self.support.path)
265
+ return f"{self.element_type.value}:{carrier}"
266
+
267
+
268
+ @dataclass(frozen=True)
269
+ class TetrahedralPerceptionResult:
270
+ """Fixed-point result for primary and stereo-dependent tetrahedral centers."""
271
+
272
+ carrier_evidence: tuple[TetrahedralConstitution, ...]
273
+ elements: tuple[PotentialStereoElement, ...]
274
+ dependency_iterations: int
275
+ stereo_markers: tuple[tuple[int, tuple[str, int]], ...]
276
+
277
+ def __post_init__(self) -> None:
278
+ object.__setattr__(self, "carrier_evidence", tuple(self.carrier_evidence))
279
+ object.__setattr__(self, "elements", tuple(self.elements))
280
+ object.__setattr__(self, "stereo_markers", tuple(self.stereo_markers))
281
+ if (
282
+ type(self.dependency_iterations) is not int
283
+ or self.dependency_iterations < 0
284
+ ):
285
+ raise ValueError("Dependency iteration count must be non-negative.")
286
+
287
+
288
+ def _configuration_state(info: object) -> StereoConfigurationState:
289
+ specified = getattr(info, "specified", None)
290
+ if specified == Chem.StereoSpecified.Specified:
291
+ return StereoConfigurationState.SPECIFIED
292
+ return StereoConfigurationState.UNSPECIFIED
293
+
294
+
295
+ def _cumulated_double_bonds(molecule: Chem.Mol) -> frozenset[int]:
296
+ """Return double bonds that belong to a run of at least two double bonds."""
297
+ double_bonds = [
298
+ bond
299
+ for bond in molecule.GetBonds()
300
+ if bond.GetBondType() == Chem.BondType.DOUBLE
301
+ ]
302
+ incidence: dict[int, list[int]] = {}
303
+ for bond in double_bonds:
304
+ for atom in (bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()):
305
+ incidence.setdefault(atom, []).append(bond.GetIdx())
306
+ cumulated = {
307
+ bond_index
308
+ for bond_indices in incidence.values()
309
+ if len(bond_indices) > 1
310
+ for bond_index in bond_indices
311
+ }
312
+ return frozenset(cumulated)
313
+
314
+
315
+ def _constitutional_graph(
316
+ molecule: Chem.Mol,
317
+ center: int,
318
+ stereo_markers: Mapping[int, tuple[str, int]] | None = None,
319
+ ) -> nx.Graph:
320
+ """Build the attributed component used for exact center stabilizers."""
321
+ markers = stereo_markers or {}
322
+ graph = nx.Graph()
323
+ for atom in molecule.GetAtoms():
324
+ index = atom.GetIdx()
325
+ graph.add_node(
326
+ index,
327
+ atomic_number=atom.GetAtomicNum(),
328
+ isotope=atom.GetIsotope(),
329
+ formal_charge=atom.GetFormalCharge(),
330
+ radical_electrons=atom.GetNumRadicalElectrons(),
331
+ hcount=_hidden_hydrogen_count(atom),
332
+ aromatic=atom.GetIsAromatic(),
333
+ stereo_anchor=index == center,
334
+ stereo_marker=None if index == center else markers.get(index),
335
+ )
336
+ for bond in molecule.GetBonds():
337
+ graph.add_edge(
338
+ bond.GetBeginAtomIdx(),
339
+ bond.GetEndAtomIdx(),
340
+ bond_type=str(bond.GetBondType()),
341
+ aromatic=bond.GetIsAromatic(),
342
+ )
343
+ component = nx.node_connected_component(graph, center)
344
+ return graph.subgraph(component).copy()
345
+
346
+
347
+ def _local_key_digest(value: object) -> str:
348
+ return hashlib.sha256(repr(value).encode("utf-8")).hexdigest()
349
+
350
+
351
+ def _local_key_node_seed(attributes: Mapping[str, object]) -> tuple[object, ...]:
352
+ """Return the constitution-only seed; map IDs and stereo are absent."""
353
+ return (
354
+ int(attributes["atomic_number"]),
355
+ int(attributes["isotope"]),
356
+ int(attributes["formal_charge"]),
357
+ int(attributes["radical_electrons"]),
358
+ int(attributes["hcount"]),
359
+ bool(attributes["aromatic"]),
360
+ bool(attributes["stereo_anchor"]),
361
+ attributes["stereo_marker"],
362
+ )
363
+
364
+
365
+ def _local_key_edge_seed(attributes: Mapping[str, object]) -> tuple[object, ...]:
366
+ return (
367
+ str(attributes["bond_type"]),
368
+ bool(attributes["aromatic"]),
369
+ )
370
+
371
+
372
+ def _local_key_graph(
373
+ molecule: Chem.Mol,
374
+ center: int,
375
+ stereo_markers: Mapping[int, tuple[str, int]] | None = None,
376
+ ) -> tuple[nx.Graph, tuple[object, ...]]:
377
+ """Materialize center-owned hydrogens for local recursive refinement."""
378
+ graph = _constitutional_graph(molecule, center, stereo_markers)
379
+ hidden_hydrogens = _hidden_hydrogen_count(molecule.GetAtomWithIdx(center))
380
+ virtual_nodes: list[object] = []
381
+ if hidden_hydrogens:
382
+ # The count is represented by explicit virtual leaves below, so it must
383
+ # not also remain folded into the center seed.
384
+ graph.nodes[center]["hcount"] = 0
385
+ for occurrence in range(hidden_hydrogens):
386
+ node = ("virtual_H", center, occurrence)
387
+ virtual_nodes.append(node)
388
+ graph.add_node(
389
+ node,
390
+ atomic_number=1,
391
+ isotope=0,
392
+ formal_charge=0,
393
+ radical_electrons=0,
394
+ hcount=0,
395
+ aromatic=False,
396
+ stereo_anchor=False,
397
+ stereo_marker=None,
398
+ )
399
+ graph.add_edge(
400
+ center,
401
+ node,
402
+ bond_type=str(Chem.BondType.SINGLE),
403
+ aromatic=False,
404
+ )
405
+ return graph, tuple(virtual_nodes)
406
+
407
+
408
+ def tetrahedral_local_neighbor_keys(
409
+ molecule: Chem.Mol,
410
+ center: int,
411
+ *,
412
+ stereo_markers: Mapping[int, tuple[str, int]] | None = None,
413
+ ) -> Mapping[Reference, LocalNeighborKey]:
414
+ """Return recursive constitutional keys for a tetrahedral carrier's ligands.
415
+
416
+ Refinement runs for the size of the complete connected component, including
417
+ center-owned virtual hydrogens, so information can propagate across rings
418
+ and through the full ligand environment. SHA-256 values are ordering keys,
419
+ not equality proof: exact center-stabilizer orbits audit every tie.
420
+ """
421
+ if molecule is None:
422
+ raise ValueError("Tetrahedral local-key construction requires a molecule.")
423
+ if type(center) is not int or center < 0 or center >= molecule.GetNumAtoms():
424
+ raise ValueError(f"Tetrahedral center {center!r} is absent.")
425
+ working = Chem.Mol(molecule)
426
+ atom = working.GetAtomWithIdx(center)
427
+ if not _is_tetrahedral_carrier(atom):
428
+ raise ValueError(f"Atom {center} is not a supported tetrahedral carrier.")
429
+
430
+ graph, virtual_nodes = _local_key_graph(
431
+ working,
432
+ center,
433
+ stereo_markers,
434
+ )
435
+ colors = {
436
+ node: _local_key_digest(
437
+ ("local_neighbor_seed_v2", _local_key_node_seed(attributes))
438
+ )
439
+ for node, attributes in graph.nodes(data=True)
440
+ }
441
+ depth = len(graph)
442
+ for _round in range(depth):
443
+ colors = {
444
+ node: _local_key_digest(
445
+ (
446
+ "local_neighbor_refine_v2",
447
+ colors[node],
448
+ tuple(
449
+ sorted(
450
+ (
451
+ _local_key_edge_seed(graph.edges[node, neighbor]),
452
+ colors[neighbor],
453
+ )
454
+ for neighbor in graph.neighbors(node)
455
+ )
456
+ ),
457
+ )
458
+ )
459
+ for node in graph
460
+ }
461
+
462
+ keys: dict[Reference, LocalNeighborKey] = {
463
+ neighbor.GetIdx(): LocalNeighborKey(colors[neighbor.GetIdx()], depth)
464
+ for neighbor in atom.GetNeighbors()
465
+ }
466
+ if virtual_nodes:
467
+ virtual_digests = {colors[node] for node in virtual_nodes}
468
+ if len(virtual_digests) != 1:
469
+ raise RuntimeError(
470
+ "Equivalent center-owned hydrogens received unequal keys."
471
+ )
472
+ keys[virtual_reference("H", center)] = LocalNeighborKey(
473
+ virtual_digests.pop(),
474
+ depth,
475
+ )
476
+ if _lone_pair_count(atom):
477
+ keys[virtual_reference("LP", center)] = LocalNeighborKey(
478
+ _local_key_digest(("virtual_lone_pair_v2",)),
479
+ depth,
480
+ )
481
+ return keys
482
+
483
+
484
+ _CONSTITUTIONAL_NODE_ATTRIBUTES = (
485
+ "atomic_number",
486
+ "isotope",
487
+ "formal_charge",
488
+ "radical_electrons",
489
+ "hcount",
490
+ "aromatic",
491
+ "stereo_anchor",
492
+ "stereo_marker",
493
+ "ligand_probe",
494
+ )
495
+ _CONSTITUTIONAL_EDGE_ATTRIBUTES = ("bond_type", "aromatic")
496
+
497
+
498
+ def _center_fixed_ligands_are_equivalent(
499
+ graph: nx.Graph,
500
+ left: int,
501
+ right: int,
502
+ ) -> bool:
503
+ """Test one exact ligand-orbit relation without enumerating the group."""
504
+ left_rooted = graph.copy()
505
+ right_rooted = graph.copy()
506
+ nx.set_node_attributes(left_rooted, False, "ligand_probe")
507
+ nx.set_node_attributes(right_rooted, False, "ligand_probe")
508
+ left_rooted.nodes[left]["ligand_probe"] = True
509
+ right_rooted.nodes[right]["ligand_probe"] = True
510
+ matcher = GraphMatcher(
511
+ left_rooted,
512
+ right_rooted,
513
+ node_match=categorical_node_match(
514
+ _CONSTITUTIONAL_NODE_ATTRIBUTES,
515
+ (0, 0, 0, 0, 0, False, False, None, False),
516
+ ),
517
+ edge_match=categorical_edge_match(
518
+ _CONSTITUTIONAL_EDGE_ATTRIBUTES,
519
+ ("", False),
520
+ ),
521
+ )
522
+ return matcher.is_isomorphic()
523
+
524
+
525
+ def _exact_material_ligand_classes(
526
+ graph: nx.Graph,
527
+ neighbors: set[int],
528
+ local_keys: Mapping[Reference, LocalNeighborKey],
529
+ ) -> tuple[list[LigandSymmetryClass], int]:
530
+ """Partition only tied local-key groups by exact rooted isomorphism."""
531
+ by_key: dict[LocalNeighborKey, list[int]] = {}
532
+ for reference in neighbors:
533
+ by_key.setdefault(local_keys[reference], []).append(reference)
534
+
535
+ classes = []
536
+ witness_checks = 0
537
+ for key, references in by_key.items():
538
+ parent = {reference: reference for reference in references}
539
+
540
+ def find(reference: int) -> int:
541
+ while parent[reference] != reference:
542
+ parent[reference] = parent[parent[reference]]
543
+ reference = parent[reference]
544
+ return reference
545
+
546
+ def union(left: int, right: int) -> None:
547
+ left_root = find(left)
548
+ right_root = find(right)
549
+ if left_root != right_root:
550
+ parent[right_root] = left_root
551
+
552
+ for left_position, left in enumerate(references):
553
+ for right in references[left_position + 1 :]:
554
+ witness_checks += 1
555
+ if _center_fixed_ligands_are_equivalent(graph, left, right):
556
+ union(left, right)
557
+
558
+ members: dict[int, list[int]] = {}
559
+ for reference in references:
560
+ members.setdefault(find(reference), []).append(reference)
561
+ classes.extend(
562
+ LigandSymmetryClass(
563
+ tuple(sorted(values)),
564
+ len(values),
565
+ key,
566
+ )
567
+ for values in members.values()
568
+ )
569
+ return classes, witness_checks
570
+
571
+
572
+ def _validated_tetrahedral_atom(
573
+ molecule: Chem.Mol,
574
+ center: int,
575
+ dependency_depth: int,
576
+ ) -> tuple[Chem.Mol, Chem.Atom]:
577
+ """Validate one constitutional request and return an isolated atom view."""
578
+ if molecule is None:
579
+ raise ValueError("Tetrahedral constitutional analysis requires a molecule.")
580
+ if type(center) is not int or center < 0 or center >= molecule.GetNumAtoms():
581
+ raise ValueError(f"Tetrahedral center {center!r} is absent.")
582
+ if type(dependency_depth) is not int or dependency_depth < 0:
583
+ raise ValueError("Tetrahedral dependency depth must be non-negative.")
584
+ working = Chem.Mol(molecule)
585
+ atom = working.GetAtomWithIdx(center)
586
+ if not _is_tetrahedral_carrier(atom):
587
+ raise ValueError(f"Atom {center} is not a supported tetrahedral carrier.")
588
+ return working, atom
589
+
590
+
591
+ def canonicalize_tetrahedral_constitution(
592
+ molecule: Chem.Mol,
593
+ center: int,
594
+ *,
595
+ stereo_markers: Mapping[int, tuple[str, int]] | None = None,
596
+ neighbor_orientation_constraints: LocalOrientationConstraints | None = None,
597
+ dependency_depth: int = 0,
598
+ ) -> TetrahedralConstitution:
599
+ """Classify a tetrahedral carrier by exact center-stabilizer parity.
600
+
601
+ The depth-zero calculation uses constitution alone. A constrained pass
602
+ may additionally require graph maps to preserve supplied orientations at
603
+ other local elements, but never reads the focal center's configuration or
604
+ any CIP descriptor. A center is confirmed exactly when its allowed
605
+ stabilizer contains no odd permutation of the four coordination slots.
606
+ """
607
+ working, atom = _validated_tetrahedral_atom(
608
+ molecule,
609
+ center,
610
+ dependency_depth,
611
+ )
612
+ markers = {
613
+ index: marker
614
+ for index, marker in (stereo_markers or {}).items()
615
+ if index != center
616
+ }
617
+ graph = _constitutional_graph(working, center, markers)
618
+ local_keys = tetrahedral_local_neighbor_keys(
619
+ working,
620
+ center,
621
+ stereo_markers=markers,
622
+ )
623
+ neighbors = {neighbor.GetIdx() for neighbor in atom.GetNeighbors()}
624
+ classes, witness_checks = _exact_material_ligand_classes(
625
+ graph,
626
+ neighbors,
627
+ local_keys,
628
+ )
629
+ hidden_hydrogens = _hidden_hydrogen_count(atom)
630
+ if hidden_hydrogens:
631
+ classes.append(
632
+ LigandSymmetryClass(
633
+ (virtual_reference("H", center),),
634
+ hidden_hydrogens,
635
+ local_keys[virtual_reference("H", center)],
636
+ )
637
+ )
638
+ lone_pairs = _lone_pair_count(atom)
639
+ if lone_pairs:
640
+ classes.append(
641
+ LigandSymmetryClass(
642
+ (virtual_reference("LP", center),),
643
+ lone_pairs,
644
+ local_keys[virtual_reference("LP", center)],
645
+ )
646
+ )
647
+ classes.sort(
648
+ key=lambda item: (
649
+ item.neighborhood_key,
650
+ item.multiplicity,
651
+ repr(item.references),
652
+ )
653
+ )
654
+ references = tuple(
655
+ [
656
+ *(neighbor.GetIdx() for neighbor in atom.GetNeighbors()),
657
+ *(virtual_reference("H", center) for _ in range(hidden_hydrogens)),
658
+ *(virtual_reference("LP", center) for _ in range(lone_pairs)),
659
+ ]
660
+ )
661
+ if neighbor_orientation_constraints is not None and not isinstance(
662
+ neighbor_orientation_constraints,
663
+ LocalOrientationConstraints,
664
+ ):
665
+ raise TypeError("Neighbor orientation constraints have an unsupported type.")
666
+ singleton_ligands = len(classes) == 4 and all(
667
+ item.multiplicity == 1 for item in classes
668
+ )
669
+ stabilizer = (
670
+ None
671
+ if singleton_ligands
672
+ else tetrahedral_stabilizer_result(
673
+ graph,
674
+ references, # type: ignore[arg-type]
675
+ constraints=neighbor_orientation_constraints,
676
+ )
677
+ )
678
+ if stabilizer is not None and not stabilizer.is_stereogenic:
679
+ status = TetrahedralConstitutionStatus.SYMMETRY_RELATED
680
+ elif dependency_depth or neighbor_orientation_constraints is not None:
681
+ status = TetrahedralConstitutionStatus.STEREO_DEPENDENT_DISTINCT
682
+ elif singleton_ligands:
683
+ status = TetrahedralConstitutionStatus.CONSTITUTIONALLY_DISTINCT
684
+ else:
685
+ status = TetrahedralConstitutionStatus.CONSTITUTIONALLY_ORIENTATION_DISTINCT
686
+ class_keys = tuple(item.neighborhood_key for item in classes)
687
+ if status is TetrahedralConstitutionStatus.SYMMETRY_RELATED:
688
+ canonical_frame = None
689
+ frame_status = TetrahedralFrameStatus.SYMMETRY_RELATED
690
+ elif not singleton_ligands:
691
+ canonical_frame = None
692
+ frame_status = TetrahedralFrameStatus.ORIENTATION_ORBIT
693
+ elif len(set(class_keys)) != len(class_keys):
694
+ canonical_frame = None
695
+ frame_status = TetrahedralFrameStatus.NEIGHBORHOOD_KEY_COLLISION
696
+ else:
697
+ canonical_frame = (
698
+ center,
699
+ *(item.references[0] for item in classes),
700
+ )
701
+ frame_status = TetrahedralFrameStatus.CANONICAL
702
+ result = TetrahedralConstitution(
703
+ support=AtomStereoSupport(center),
704
+ ligand_classes=tuple(classes),
705
+ status=status,
706
+ automorphism_witness_checks=(
707
+ witness_checks
708
+ + (0 if stabilizer is None else stabilizer.automorphism_checks)
709
+ ),
710
+ canonical_frame=canonical_frame,
711
+ frame_status=frame_status,
712
+ refinement_depth=next(iter(local_keys.values())).depth,
713
+ dependency_depth=dependency_depth,
714
+ stereo_marker_count=sum(
715
+ index in graph and index != center for index in markers
716
+ ),
717
+ )
718
+ if result.ligand_count != 4:
719
+ raise RuntimeError("Tetrahedral constitutional evidence lost a ligand slot.")
720
+ return result
721
+
722
+
723
+ def _reference_permutation_sign(
724
+ values: tuple[Reference, ...],
725
+ ordered: tuple[Reference, ...],
726
+ ) -> int:
727
+ positions = {value: index for index, value in enumerate(ordered)}
728
+ permutation = tuple(positions[value] for value in values)
729
+ inversions = sum(
730
+ permutation[left] > permutation[right]
731
+ for left in range(len(permutation))
732
+ for right in range(left + 1, len(permutation))
733
+ )
734
+ return -1 if inversions % 2 else 1
735
+
736
+
737
+ def canonicalize_tetrahedral_configuration(
738
+ molecule: Chem.Mol,
739
+ descriptor: TetrahedralStereo,
740
+ *,
741
+ constitutional_evidence: TetrahedralConstitution | None = None,
742
+ ) -> TetrahedralStereo:
743
+ """Attach tetrahedral configuration to its constitution-derived frame.
744
+
745
+ Frame construction is completed before this function reads parity.
746
+ The returned parity is transported through the ligand permutation, so
747
+ relative configuration is preserved without allowing it to influence the
748
+ canonical ligand order.
749
+ """
750
+ if not isinstance(descriptor, TetrahedralStereo):
751
+ raise TypeError(
752
+ "Tetrahedral configuration attachment requires TetrahedralStereo."
753
+ )
754
+ if type(descriptor.center) is not int:
755
+ raise ValueError("Tetrahedral configuration centers must be atom indices.")
756
+ evidence = constitutional_evidence or canonicalize_tetrahedral_constitution(
757
+ molecule,
758
+ descriptor.center,
759
+ )
760
+ if evidence.support != descriptor.support:
761
+ raise ValueError(
762
+ "Tetrahedral constitutional evidence belongs to another center."
763
+ )
764
+ if evidence.canonical_frame is None:
765
+ raise ValueError(
766
+ "Tetrahedral configuration has no canonical constitutional frame: "
767
+ f"{evidence.frame_status.value}."
768
+ )
769
+ canonical_references = tuple(evidence.canonical_frame[1:])
770
+ supplied_references = tuple(descriptor.atoms[1:])
771
+ if set(supplied_references) != set(canonical_references):
772
+ raise ValueError(
773
+ "Tetrahedral descriptor references do not match the constitutional frame."
774
+ )
775
+ parity = (
776
+ None
777
+ if descriptor.parity is None
778
+ else descriptor.parity
779
+ * _reference_permutation_sign(supplied_references, canonical_references)
780
+ )
781
+ return TetrahedralStereo(
782
+ evidence.canonical_frame, # type: ignore[arg-type]
783
+ parity,
784
+ descriptor.provenance,
785
+ )
786
+
787
+
788
+ _TETRAHEDRAL_TAG_PARITY = {
789
+ Chem.ChiralType.CHI_TETRAHEDRAL_CW: 1,
790
+ Chem.ChiralType.CHI_TETRAHEDRAL_CCW: -1,
791
+ }
792
+
793
+
794
+ def _raw_tetrahedral_configuration(
795
+ atom: Chem.Atom,
796
+ ) -> TetrahedralStereo | None:
797
+ """Read supplied RDKit orientation in zero-based local-reference order."""
798
+ parity = _TETRAHEDRAL_TAG_PARITY.get(atom.GetChiralTag())
799
+ if parity is None:
800
+ return None
801
+ center = atom.GetIdx()
802
+ references: list[Reference] = [
803
+ neighbor.GetIdx() for neighbor in atom.GetNeighbors()
804
+ ]
805
+ hidden_hydrogens = _hidden_hydrogen_count(atom)
806
+ references.extend(virtual_reference("H", center) for _ in range(hidden_hydrogens))
807
+ references.extend(
808
+ virtual_reference("LP", center) for _ in range(_lone_pair_count(atom))
809
+ )
810
+ if len(references) != 4:
811
+ raise ValueError(
812
+ f"Configured tetrahedral center {center} does not expose four ligands."
813
+ )
814
+ return TetrahedralStereo(
815
+ (center, *references), # type: ignore[arg-type]
816
+ parity,
817
+ "rdkit",
818
+ )
819
+
820
+
821
+ def analyze_tetrahedral_carriers(
822
+ molecule: Chem.Mol,
823
+ ) -> tuple[TetrahedralConstitution, ...]:
824
+ """Return configuration-neutral evidence for every broad carrier."""
825
+ return tuple(
826
+ canonicalize_tetrahedral_constitution(molecule, support.center)
827
+ for support in detect_tetrahedral_carriers(molecule)
828
+ )
829
+
830
+
831
+ def _element_from_tetrahedral_evidence(
832
+ molecule: Chem.Mol,
833
+ evidence: TetrahedralConstitution,
834
+ ) -> PotentialStereoElement:
835
+ support = evidence.support
836
+ if not evidence.confirms_stereogenic_center:
837
+ raise ValueError("Tetrahedral evidence does not define a stereo element.")
838
+ raw_configuration = _raw_tetrahedral_configuration(
839
+ molecule.GetAtomWithIdx(support.center)
840
+ )
841
+ configuration = (
842
+ canonicalize_tetrahedral_configuration(
843
+ molecule,
844
+ raw_configuration,
845
+ constitutional_evidence=evidence,
846
+ )
847
+ if raw_configuration is not None and evidence.has_canonical_frame
848
+ else None
849
+ )
850
+ configuration_state = (
851
+ StereoConfigurationState.SPECIFIED
852
+ if raw_configuration is not None
853
+ else StereoConfigurationState.UNSPECIFIED
854
+ )
855
+ if evidence.is_stereo_dependent:
856
+ provenance = "synkit_orientation_constrained_center_stabilizer"
857
+ elif (
858
+ evidence.status
859
+ is TetrahedralConstitutionStatus.CONSTITUTIONALLY_ORIENTATION_DISTINCT
860
+ ):
861
+ provenance = "synkit_even_center_stabilizer"
862
+ else:
863
+ provenance = "synkit_recursive_neighborhood_and_exact_symmetry"
864
+ return PotentialStereoElement(
865
+ StereoElementType.TETRAHEDRAL,
866
+ support,
867
+ configuration_state,
868
+ provenance,
869
+ f"Atom_Tetrahedral:{support.center}",
870
+ evidence,
871
+ configuration,
872
+ )
873
+
874
+
875
+ def perceive_tetrahedral_stereo(
876
+ molecule: Chem.Mol,
877
+ *,
878
+ excluded_centers: Iterable[int] = (),
879
+ ) -> TetrahedralPerceptionResult:
880
+ """Resolve tetrahedral centers from constitution and local neighbor frames.
881
+
882
+ A constrained pass reads oriented neighbor frames directly from molecular
883
+ input. It never converts them to CIP or canonical local descriptors, and
884
+ the focal center's own frame is excluded.
885
+ """
886
+ if molecule is None:
887
+ raise ValueError("Tetrahedral perception requires a molecule.")
888
+ working = Chem.Mol(molecule)
889
+ excluded = frozenset(int(center) for center in excluded_centers)
890
+ if any(center < 0 or center >= working.GetNumAtoms() for center in excluded):
891
+ raise ValueError("Excluded tetrahedral center is absent.")
892
+ initial = tuple(
893
+ canonicalize_tetrahedral_constitution(working, support.center)
894
+ for support in detect_tetrahedral_carriers(working)
895
+ if support.center not in excluded
896
+ )
897
+ evidence_by_center = {evidence.support.center: evidence for evidence in initial}
898
+ elements: dict[int, PotentialStereoElement] = {}
899
+ pending = set(evidence_by_center)
900
+
901
+ for center, evidence in evidence_by_center.items():
902
+ if not evidence.confirms_stereogenic_center:
903
+ continue
904
+ element = _element_from_tetrahedral_evidence(working, evidence)
905
+ elements[center] = element
906
+ pending.discard(center)
907
+
908
+ dependency_iterations = 0
909
+ if pending:
910
+ constraints = extract_local_orientation_constraints(working)
911
+ if constraints.tetrahedral or constraints.planar or constraints.cumulene:
912
+ dependency_iterations = 1
913
+ for center in sorted(tuple(pending)):
914
+ focal_constraints = LocalOrientationConstraints(
915
+ tuple(
916
+ constraint
917
+ for constraint in constraints.tetrahedral
918
+ if constraint.center != center
919
+ ),
920
+ constraints.planar,
921
+ constraints.cumulene,
922
+ )
923
+ evidence = canonicalize_tetrahedral_constitution(
924
+ working,
925
+ center,
926
+ neighbor_orientation_constraints=focal_constraints,
927
+ dependency_depth=1,
928
+ )
929
+ evidence_by_center[center] = evidence
930
+ if not evidence.confirms_stereogenic_center:
931
+ continue
932
+ elements[center] = _element_from_tetrahedral_evidence(
933
+ working,
934
+ evidence,
935
+ )
936
+ pending.discard(center)
937
+
938
+ return TetrahedralPerceptionResult(
939
+ carrier_evidence=tuple(
940
+ evidence_by_center[center] for center in sorted(evidence_by_center)
941
+ ),
942
+ elements=tuple(elements[center] for center in sorted(elements)),
943
+ dependency_iterations=dependency_iterations,
944
+ stereo_markers=(),
945
+ )
946
+
947
+
948
+ def detect_tetrahedral_elements(
949
+ molecule: Chem.Mol,
950
+ *,
951
+ excluded_centers: Iterable[int] = (),
952
+ ) -> tuple[PotentialStereoElement, ...]:
953
+ """Return primary and fixed-point stereo-dependent tetrahedral elements."""
954
+ return perceive_tetrahedral_stereo(
955
+ molecule,
956
+ excluded_centers=excluded_centers,
957
+ ).elements
958
+
959
+
960
+ def detect_constitutionally_distinct_tetrahedral_centers(
961
+ molecule: Chem.Mol,
962
+ ) -> tuple[TetrahedralConstitution, ...]:
963
+ """Return tetrahedral centers confirmed by constitution alone."""
964
+ return tuple(
965
+ evidence
966
+ for evidence in analyze_tetrahedral_carriers(molecule)
967
+ if evidence.confirms_stereogenic_center
968
+ )
969
+
970
+
971
+ def detect_potential_stereo_elements(
972
+ molecule: Chem.Mol,
973
+ *,
974
+ include_extended_ring_axes: bool = False,
975
+ excluded_tetrahedral_centers: Iterable[int] = (),
976
+ ) -> tuple[PotentialStereoElement, ...]:
977
+ """Detect supported atom, bond, and axis stereo carriers.
978
+
979
+ The result is an intentionally non-promoting perception inventory. It
980
+ includes configured atom/bond elements but does not reconstruct their
981
+ descriptor frames. Cumulated double bonds are not emitted as independent
982
+ E/Z elements. Even-bond cumulenes are emitted as axes and odd-bond
983
+ extended cumulenes are emitted once as path-level cis/trans supports.
984
+ """
985
+ if molecule is None:
986
+ raise ValueError("Stereo-element perception requires a molecule.")
987
+ from ._stereo_element_detection import (
988
+ axis_elements,
989
+ extended_cis_trans_elements,
990
+ helical_elements,
991
+ rdkit_double_bond_elements,
992
+ )
993
+
994
+ working = Chem.Mol(molecule)
995
+ Chem.AssignStereochemistry(working, cleanIt=False, force=True)
996
+ elements = (
997
+ *detect_tetrahedral_elements(
998
+ working,
999
+ excluded_centers=excluded_tetrahedral_centers,
1000
+ ),
1001
+ *rdkit_double_bond_elements(working),
1002
+ *axis_elements(
1003
+ working,
1004
+ include_extended_ring_axes=include_extended_ring_axes,
1005
+ ),
1006
+ *extended_cis_trans_elements(working),
1007
+ *helical_elements(working),
1008
+ )
1009
+ return tuple(sorted(elements, key=lambda element: element.identifier))
1010
+
1011
+
1012
+ __all__ = [
1013
+ "LigandSymmetryClass",
1014
+ "LocalNeighborKey",
1015
+ "PotentialStereoElement",
1016
+ "StereoCarrierStatus",
1017
+ "StereoConfigurationState",
1018
+ "StereoElementType",
1019
+ "TetrahedralConstitution",
1020
+ "TetrahedralConstitutionStatus",
1021
+ "TetrahedralFrameStatus",
1022
+ "TetrahedralPerceptionResult",
1023
+ "analyze_tetrahedral_carriers",
1024
+ "canonicalize_tetrahedral_configuration",
1025
+ "canonicalize_tetrahedral_constitution",
1026
+ "detect_constitutionally_distinct_tetrahedral_centers",
1027
+ "detect_potential_stereo_elements",
1028
+ "detect_tetrahedral_carriers",
1029
+ "detect_tetrahedral_elements",
1030
+ "perceive_tetrahedral_stereo",
1031
+ "tetrahedral_local_neighbor_keys",
1032
+ ]