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,843 @@
1
+ """Independent, witnessed CIP ligand ranking over molecular constitution.
2
+
3
+ The implementation follows the hierarchical ordering in IUPAC Blue Book
4
+ P-92: Sequence Rule 1a is exhausted before duplicate-node Rule 1b, followed
5
+ by isotope Rule 2. Sequence Rule 3 and the reference-independent, single-pair
6
+ subset of Rules 4c/5 are witnessed explicitly; multi-unit Rule 4b continues
7
+ to fail closed. No RDKit CIPCode or CIPRank property is read.
8
+
9
+ Reference: https://iupac.qmul.ac.uk/BlueBook/P9.html#92010000
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ from hashlib import sha256
17
+ from itertools import combinations
18
+ from typing import Any, Iterable
19
+
20
+ from rdkit import Chem
21
+
22
+ from synkit.Graph.Stereo import descriptor_id, parse_virtual_reference
23
+
24
+ Reference = int | str
25
+
26
+ _STEREO_HIGH_LABELS = frozenset({"R", "M", "Z", "r", "m", "z"})
27
+ _STEREO_LOW_LABELS = frozenset({"S", "P", "E", "s", "p", "e"})
28
+ _STEREO_LABEL_FAMILIES = {
29
+ "R": "center",
30
+ "S": "center",
31
+ "r": "center",
32
+ "s": "center",
33
+ "M": "axis",
34
+ "P": "axis",
35
+ "m": "axis",
36
+ "p": "axis",
37
+ "Z": "planar",
38
+ "E": "planar",
39
+ "z": "planar",
40
+ "e": "planar",
41
+ }
42
+ _REFLECTED_STEREO_LABEL = {
43
+ "R": "S",
44
+ "S": "R",
45
+ "M": "P",
46
+ "P": "M",
47
+ "r": "r",
48
+ "s": "s",
49
+ "m": "m",
50
+ "p": "p",
51
+ "Z": "Z",
52
+ "E": "E",
53
+ "z": "z",
54
+ "e": "e",
55
+ }
56
+
57
+
58
+ class CIPSequenceRule(str, Enum):
59
+ RULE_1A_ATOMIC_NUMBER = "1a_atomic_number"
60
+ RULE_1B_DUPLICATE_DISTANCE = "1b_duplicate_distance"
61
+ RULE_2_ISOTOPE_MASS = "2_isotope_mass"
62
+ RULE_3_SEQUENCE_GEOMETRY = "3_sequence_geometry"
63
+ RULE_4_STEREOGENIC_UNIT = "4_stereogenic_unit"
64
+ RULE_5_REFLECTION_VARIANT = "5_reflection_variant"
65
+
66
+
67
+ class CIPComparisonOutcome(str, Enum):
68
+ LEFT_HIGHER = "left_higher"
69
+ RIGHT_HIGHER = "right_higher"
70
+ TIE = "tie"
71
+ UNSUPPORTED = "unsupported"
72
+
73
+
74
+ class CIPTermination(str, Enum):
75
+ EXHAUSTED = "exhausted"
76
+ VIRTUAL_REFERENCE = "virtual_reference"
77
+ DEPTH_CAP = "depth_cap"
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class CIPNodeEvidence:
82
+ """One material, duplicate, or virtual node in a ligand digraph sphere."""
83
+
84
+ depth: int
85
+ atomic_number: float
86
+ mass: float
87
+ atom_index: int | None
88
+ path: tuple[int, ...]
89
+ duplicate: bool = False
90
+ duplicate_source_depth: int | None = None
91
+ duplicate_kind: str | None = None
92
+
93
+ @property
94
+ def atomic_key(self) -> float:
95
+ return self.atomic_number
96
+
97
+ @property
98
+ def isotope_key(self) -> tuple[float, float]:
99
+ return self.atomic_number, self.mass
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class CIPSphereEvidence:
104
+ depth: int
105
+ nodes: tuple[CIPNodeEvidence, ...]
106
+
107
+ @property
108
+ def atomic_signature(self) -> tuple[float, ...]:
109
+ return tuple(
110
+ sorted(
111
+ (node.atomic_number for node in self.nodes),
112
+ reverse=True,
113
+ )
114
+ )
115
+
116
+ @property
117
+ def isotope_signature(self) -> tuple[tuple[float, float], ...]:
118
+ return tuple(
119
+ sorted(
120
+ (node.isotope_key for node in self.nodes),
121
+ reverse=True,
122
+ )
123
+ )
124
+
125
+ @property
126
+ def duplicate_signature(self) -> tuple[tuple[float, int], ...]:
127
+ values = (
128
+ (node.atomic_number, -int(node.duplicate_source_depth))
129
+ for node in self.nodes
130
+ if node.duplicate and node.duplicate_source_depth is not None
131
+ )
132
+ return tuple(sorted(values, reverse=True))
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class CIPStereogenicUnitEvidence:
137
+ descriptor_class: str
138
+ identifier: str
139
+ parity: int | None
140
+ label: str | None
141
+ depth: int
142
+ encountered_atoms: tuple[int, ...]
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class CIPLigandEvidence:
147
+ center: int
148
+ reference: Reference
149
+ spheres: tuple[CIPSphereEvidence, ...]
150
+ stereogenic_units: tuple[CIPStereogenicUnitEvidence, ...]
151
+ unsupported_features: tuple[str, ...]
152
+ termination: CIPTermination
153
+
154
+ @property
155
+ def digest(self) -> str:
156
+ return sha256(repr(self).encode("utf-8")).hexdigest()
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class CIPComparison:
161
+ center: int
162
+ left_reference: Reference
163
+ right_reference: Reference
164
+ outcome: CIPComparisonOutcome
165
+ deciding_rule: CIPSequenceRule | None
166
+ depth: int | None
167
+ left_witness: tuple[Any, ...] | None
168
+ right_witness: tuple[Any, ...] | None
169
+ reason: str
170
+ left_evidence: CIPLigandEvidence
171
+ right_evidence: CIPLigandEvidence
172
+
173
+ @property
174
+ def decided(self) -> bool:
175
+ return self.outcome in {
176
+ CIPComparisonOutcome.LEFT_HIGHER,
177
+ CIPComparisonOutcome.RIGHT_HIGHER,
178
+ }
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class CIPRanking:
183
+ center: int
184
+ references: tuple[Reference, ...]
185
+ ordered_references: tuple[Reference, ...]
186
+ comparisons: tuple[CIPComparison, ...]
187
+ complete: bool
188
+
189
+ @property
190
+ def digest(self) -> str:
191
+ return sha256(repr(self).encode("utf-8")).hexdigest()
192
+
193
+
194
+ @dataclass(frozen=True)
195
+ class _Occurrence:
196
+ atom_index: int
197
+ parent: int
198
+ path: tuple[int, ...]
199
+ depth: int
200
+
201
+
202
+ def _effective_mass(atom: Chem.Atom) -> float:
203
+ isotope = int(atom.GetIsotope())
204
+ if isotope:
205
+ return float(
206
+ Chem.GetPeriodicTable().GetMassForIsotope(
207
+ atom.GetAtomicNum(),
208
+ isotope,
209
+ )
210
+ )
211
+ return float(Chem.GetPeriodicTable().GetAtomicWeight(atom.GetAtomicNum()))
212
+
213
+
214
+ def _node(
215
+ molecule: Chem.Mol,
216
+ atom_index: int,
217
+ *,
218
+ depth: int,
219
+ path: tuple[int, ...],
220
+ duplicate: bool = False,
221
+ duplicate_source_depth: int | None = None,
222
+ duplicate_kind: str | None = None,
223
+ ) -> CIPNodeEvidence:
224
+ atom = molecule.GetAtomWithIdx(atom_index)
225
+ return CIPNodeEvidence(
226
+ depth,
227
+ atom.GetAtomicNum(),
228
+ _effective_mass(atom),
229
+ atom_index,
230
+ path,
231
+ duplicate,
232
+ duplicate_source_depth,
233
+ duplicate_kind,
234
+ )
235
+
236
+
237
+ def _virtual_node(reference: str) -> CIPNodeEvidence:
238
+ virtual = parse_virtual_reference(reference)
239
+ if virtual is None:
240
+ raise ValueError(f"Invalid virtual CIP reference: {reference!r}.")
241
+ if virtual.kind == "H":
242
+ mass = float(Chem.GetPeriodicTable().GetAtomicWeight(1))
243
+ return CIPNodeEvidence(1, 1, mass, None, (), duplicate_kind="virtual_H")
244
+ return CIPNodeEvidence(1, 0, 0.0, None, (), duplicate_kind="virtual_LP")
245
+
246
+
247
+ def _bond_multiplicity(bond: Chem.Bond) -> tuple[int, str | None]:
248
+ if bond.GetIsAromatic():
249
+ return 1, None
250
+ order = int(round(float(bond.GetBondTypeAsDouble())))
251
+ return max(1, order), None
252
+
253
+
254
+ def _mancude_duplicate_values(
255
+ molecule: Chem.Mol,
256
+ atom_index: int,
257
+ ) -> tuple[float, float] | None:
258
+ """Return the averaged duplicate node for a simple mancude ring atom.
259
+
260
+ IUPAC P-92.1.4.4 averages the atomic number over the possible Kekulé
261
+ double-bond positions. A non-fused even aromatic ring has two such
262
+ neighbours at every atom, so its exact mean is available without choosing
263
+ an arbitrary Kekulé form. Fused and odd aromatic systems remain explicit
264
+ unsupported features until their full resonance ensemble is implemented.
265
+ """
266
+ atom = molecule.GetAtomWithIdx(atom_index)
267
+ aromatic_neighbours = tuple(
268
+ bond.GetOtherAtomIdx(atom_index)
269
+ for bond in atom.GetBonds()
270
+ if bond.GetIsAromatic()
271
+ )
272
+ rings = tuple(
273
+ ring
274
+ for ring in molecule.GetRingInfo().AtomRings()
275
+ if atom_index in ring
276
+ and all(molecule.GetAtomWithIdx(index).GetIsAromatic() for index in ring)
277
+ )
278
+ if len(aromatic_neighbours) != 2 or len(rings) != 1 or len(rings[0]) % 2:
279
+ return None
280
+ neighbours = tuple(molecule.GetAtomWithIdx(index) for index in aromatic_neighbours)
281
+ return (
282
+ sum(atom.GetAtomicNum() for atom in neighbours) / len(neighbours),
283
+ sum(_effective_mass(atom) for atom in neighbours) / len(neighbours),
284
+ )
285
+
286
+
287
+ def _sort_nodes(
288
+ nodes: Iterable[CIPNodeEvidence],
289
+ ) -> tuple[CIPNodeEvidence, ...]:
290
+ return tuple(
291
+ sorted(
292
+ nodes,
293
+ key=lambda node: (
294
+ -node.atomic_number,
295
+ -node.mass,
296
+ not node.duplicate,
297
+ (
298
+ node.duplicate_source_depth
299
+ if node.duplicate_source_depth is not None
300
+ else 10**9
301
+ ),
302
+ repr(node.path),
303
+ node.atom_index if node.atom_index is not None else -1,
304
+ ),
305
+ )
306
+ )
307
+
308
+
309
+ class CIPRanker:
310
+ """Rank ligands without consulting toolkit-assigned CIP properties."""
311
+
312
+ def __init__(
313
+ self,
314
+ molecule: Chem.Mol,
315
+ *,
316
+ configured_descriptors: Iterable[Any] = (),
317
+ configured_labels: dict[str, str] | None = None,
318
+ max_depth: int | None = None,
319
+ ) -> None:
320
+ if molecule is None:
321
+ raise ValueError("CIP ranking requires a molecule.")
322
+ self.molecule = Chem.Mol(molecule)
323
+ self.configured_descriptors = tuple(configured_descriptors)
324
+ self.configured_labels = dict(configured_labels or {})
325
+ self._mancude_duplicate_cache: dict[
326
+ int,
327
+ tuple[float, float] | None,
328
+ ] = {}
329
+ self.max_depth = (
330
+ max(4, self.molecule.GetNumAtoms() * 2 + 2)
331
+ if max_depth is None
332
+ else max_depth
333
+ )
334
+ if type(self.max_depth) is not int or self.max_depth < 1:
335
+ raise ValueError("CIP maximum depth must be a positive integer.")
336
+
337
+ def _validate_center_reference(
338
+ self,
339
+ center: int,
340
+ reference: Reference,
341
+ ) -> None:
342
+ if center < 0 or center >= self.molecule.GetNumAtoms():
343
+ raise ValueError(f"CIP center {center} is absent.")
344
+ if type(reference) is int:
345
+ if reference < 0 or reference >= self.molecule.GetNumAtoms():
346
+ raise ValueError(f"CIP reference {reference} is absent.")
347
+ if self.molecule.GetBondBetweenAtoms(center, reference) is None:
348
+ raise ValueError(
349
+ f"CIP reference {reference} is not adjacent to " f"center {center}."
350
+ )
351
+ return
352
+ virtual = parse_virtual_reference(reference)
353
+ if virtual is None or virtual.center != center:
354
+ raise ValueError(
355
+ f"Virtual CIP reference {reference!r} is not owned " f"by {center}."
356
+ )
357
+
358
+ def reflected(self) -> CIPRanker:
359
+ """Return the same constitutional ranker with reflected stereo labels."""
360
+ return CIPRanker(
361
+ self.molecule,
362
+ configured_descriptors=self.configured_descriptors,
363
+ configured_labels={
364
+ identifier: _REFLECTED_STEREO_LABEL.get(label, label)
365
+ for identifier, label in self.configured_labels.items()
366
+ },
367
+ max_depth=self.max_depth,
368
+ )
369
+
370
+ def _mancude_duplicate(
371
+ self,
372
+ atom_index: int,
373
+ *,
374
+ depth: int,
375
+ path: tuple[int, ...],
376
+ ) -> CIPNodeEvidence | None:
377
+ if atom_index not in self._mancude_duplicate_cache:
378
+ self._mancude_duplicate_cache[atom_index] = _mancude_duplicate_values(
379
+ self.molecule, atom_index
380
+ )
381
+ values = self._mancude_duplicate_cache[atom_index]
382
+ if values is None:
383
+ return None
384
+ atomic_number, mass = values
385
+ return CIPNodeEvidence(
386
+ depth,
387
+ atomic_number,
388
+ mass,
389
+ None,
390
+ path,
391
+ duplicate=True,
392
+ duplicate_source_depth=depth,
393
+ duplicate_kind="mancude_average",
394
+ )
395
+
396
+ def _stereo_evidence(
397
+ self, material_depths: dict[int, int]
398
+ ) -> tuple[CIPStereogenicUnitEvidence, ...]:
399
+ result = []
400
+ for descriptor in self.configured_descriptors:
401
+ encountered = tuple(sorted(descriptor.dependencies & set(material_depths)))
402
+ if not encountered:
403
+ continue
404
+ identifier = descriptor_id(descriptor)
405
+ result.append(
406
+ CIPStereogenicUnitEvidence(
407
+ descriptor.descriptor_class,
408
+ identifier,
409
+ descriptor.parity,
410
+ self.configured_labels.get(identifier),
411
+ min(material_depths[atom] for atom in encountered),
412
+ encountered,
413
+ )
414
+ )
415
+ return tuple(
416
+ sorted(
417
+ result,
418
+ key=lambda item: (
419
+ item.depth,
420
+ item.descriptor_class,
421
+ item.identifier,
422
+ ),
423
+ )
424
+ )
425
+
426
+ def build_evidence(
427
+ self,
428
+ center: int,
429
+ reference: Reference,
430
+ ) -> CIPLigandEvidence:
431
+ self._validate_center_reference(center, reference)
432
+ if isinstance(reference, str):
433
+ sphere = CIPSphereEvidence(1, (_virtual_node(reference),))
434
+ return CIPLigandEvidence(
435
+ center,
436
+ reference,
437
+ (sphere,),
438
+ (),
439
+ (),
440
+ CIPTermination.VIRTUAL_REFERENCE,
441
+ )
442
+
443
+ first = _node(
444
+ self.molecule,
445
+ reference,
446
+ depth=1,
447
+ path=(center, reference),
448
+ )
449
+ spheres = [CIPSphereEvidence(1, (first,))]
450
+ active = [_Occurrence(reference, center, (center, reference), 1)]
451
+ material_depths = {reference: 1}
452
+ unsupported: set[str] = set()
453
+ termination = CIPTermination.EXHAUSTED
454
+
455
+ while active:
456
+ next_nodes: list[CIPNodeEvidence] = []
457
+ next_active: list[_Occurrence] = []
458
+ next_depth = active[0].depth + 1
459
+ if next_depth > self.max_depth:
460
+ termination = CIPTermination.DEPTH_CAP
461
+ break
462
+ for occurrence in active:
463
+ atom = self.molecule.GetAtomWithIdx(occurrence.atom_index)
464
+ hidden_h = int(atom.GetNumExplicitHs()) + int(atom.GetNumImplicitHs())
465
+ next_nodes.extend(
466
+ CIPNodeEvidence(
467
+ next_depth,
468
+ 1,
469
+ float(Chem.GetPeriodicTable().GetAtomicWeight(1)),
470
+ None,
471
+ occurrence.path,
472
+ duplicate_kind="implicit_H",
473
+ )
474
+ for _ in range(hidden_h)
475
+ )
476
+ if atom.GetIsAromatic():
477
+ mancude_duplicate = self._mancude_duplicate(
478
+ occurrence.atom_index,
479
+ depth=next_depth,
480
+ path=occurrence.path,
481
+ )
482
+ if mancude_duplicate is None:
483
+ unsupported.add("unsupported_mancude_duplicate_model")
484
+ else:
485
+ next_nodes.append(mancude_duplicate)
486
+ for bond in atom.GetBonds():
487
+ neighbor = bond.GetOtherAtomIdx(occurrence.atom_index)
488
+ multiplicity, feature = _bond_multiplicity(bond)
489
+ if feature is not None:
490
+ unsupported.add(feature)
491
+ if neighbor != occurrence.parent:
492
+ if neighbor in occurrence.path:
493
+ source_depth = occurrence.path.index(neighbor)
494
+ next_nodes.append(
495
+ _node(
496
+ self.molecule,
497
+ neighbor,
498
+ depth=next_depth,
499
+ path=occurrence.path + (neighbor,),
500
+ duplicate=True,
501
+ duplicate_source_depth=source_depth,
502
+ duplicate_kind="ring_closure",
503
+ )
504
+ )
505
+ else:
506
+ path = occurrence.path + (neighbor,)
507
+ next_nodes.append(
508
+ _node(
509
+ self.molecule,
510
+ neighbor,
511
+ depth=next_depth,
512
+ path=path,
513
+ )
514
+ )
515
+ next_active.append(
516
+ _Occurrence(
517
+ neighbor,
518
+ occurrence.atom_index,
519
+ path,
520
+ next_depth,
521
+ )
522
+ )
523
+ material_depths.setdefault(neighbor, next_depth)
524
+ source_depth = (
525
+ occurrence.path.index(neighbor)
526
+ if neighbor in occurrence.path
527
+ else next_depth
528
+ )
529
+ next_nodes.extend(
530
+ _node(
531
+ self.molecule,
532
+ neighbor,
533
+ depth=next_depth,
534
+ path=occurrence.path + (neighbor,),
535
+ duplicate=True,
536
+ duplicate_source_depth=source_depth,
537
+ duplicate_kind="multiple_bond",
538
+ )
539
+ for _ in range(multiplicity - 1)
540
+ )
541
+ if next_nodes:
542
+ spheres.append(CIPSphereEvidence(next_depth, _sort_nodes(next_nodes)))
543
+ active = next_active
544
+
545
+ stereo = self._stereo_evidence(material_depths)
546
+ return CIPLigandEvidence(
547
+ center,
548
+ reference,
549
+ tuple(spheres),
550
+ stereo,
551
+ tuple(sorted(unsupported)),
552
+ termination,
553
+ )
554
+
555
+ @staticmethod
556
+ def _padded(
557
+ values: tuple[Any, ...],
558
+ length: int,
559
+ zero: Any,
560
+ ) -> tuple[Any, ...]:
561
+ return values + (zero,) * (length - len(values))
562
+
563
+ @classmethod
564
+ def _first_sphere_difference(
565
+ cls,
566
+ left: CIPLigandEvidence,
567
+ right: CIPLigandEvidence,
568
+ field: str,
569
+ zero: Any,
570
+ ) -> tuple[int, tuple[Any, ...], tuple[Any, ...]] | None:
571
+ depth_count = max(len(left.spheres), len(right.spheres))
572
+ for offset in range(depth_count):
573
+ left_values = (
574
+ getattr(left.spheres[offset], field)
575
+ if offset < len(left.spheres)
576
+ else ()
577
+ )
578
+ right_values = (
579
+ getattr(right.spheres[offset], field)
580
+ if offset < len(right.spheres)
581
+ else ()
582
+ )
583
+ width = max(len(left_values), len(right_values))
584
+ left_padded = cls._padded(left_values, width, zero)
585
+ right_padded = cls._padded(right_values, width, zero)
586
+ if left_padded != right_padded:
587
+ return offset + 1, left_padded, right_padded
588
+ return None
589
+
590
+ @staticmethod
591
+ def _outcome(left: tuple[Any, ...], right: tuple[Any, ...]) -> CIPComparisonOutcome:
592
+ return (
593
+ CIPComparisonOutcome.LEFT_HIGHER
594
+ if left > right
595
+ else CIPComparisonOutcome.RIGHT_HIGHER
596
+ )
597
+
598
+ @staticmethod
599
+ def _resolved_stereo_units(
600
+ evidence: CIPLigandEvidence,
601
+ ) -> tuple[CIPStereogenicUnitEvidence, ...] | None:
602
+ if any(unit.label is None for unit in evidence.stereogenic_units):
603
+ return None
604
+ return evidence.stereogenic_units
605
+
606
+ def _stereo_difference(
607
+ self,
608
+ left: CIPLigandEvidence,
609
+ right: CIPLigandEvidence,
610
+ ) -> (
611
+ tuple[
612
+ CIPSequenceRule,
613
+ tuple[Any, ...],
614
+ tuple[Any, ...],
615
+ str,
616
+ ]
617
+ | None
618
+ ):
619
+ left_units = self._resolved_stereo_units(left)
620
+ right_units = self._resolved_stereo_units(right)
621
+ if left_units is None or right_units is None:
622
+ return None
623
+ # A single corresponding pair is the closed, reference-independent
624
+ # subset of Rules 4c/5. Multi-unit Rule 4b requires the full
625
+ # reference-descriptor pairing algorithm and therefore still fails
626
+ # closed below.
627
+ if len(left_units) != 1 or len(right_units) != 1:
628
+ return None
629
+ left_unit, right_unit = left_units[0], right_units[0]
630
+ left_label, right_label = str(left_unit.label), str(right_unit.label)
631
+ if (
632
+ left_unit.depth != right_unit.depth
633
+ or _STEREO_LABEL_FAMILIES.get(left_label)
634
+ != _STEREO_LABEL_FAMILIES.get(right_label)
635
+ or (left_label in _STEREO_HIGH_LABELS)
636
+ == (right_label in _STEREO_HIGH_LABELS)
637
+ ):
638
+ return None
639
+ left_witness = (
640
+ -left_unit.depth,
641
+ _STEREO_LABEL_FAMILIES[left_label],
642
+ int(left_label in _STEREO_HIGH_LABELS),
643
+ )
644
+ right_witness = (
645
+ -right_unit.depth,
646
+ _STEREO_LABEL_FAMILIES[right_label],
647
+ int(right_label in _STEREO_HIGH_LABELS),
648
+ )
649
+ if (
650
+ _STEREO_LABEL_FAMILIES[left_label] == "planar"
651
+ and left_label.isupper()
652
+ and right_label.isupper()
653
+ ):
654
+ return (
655
+ CIPSequenceRule.RULE_3_SEQUENCE_GEOMETRY,
656
+ left_witness,
657
+ right_witness,
658
+ "Sequence Rule 3 ranks seqCis/Z before seqTrans/E.",
659
+ )
660
+ if left_label.islower() and right_label.islower():
661
+ return (
662
+ CIPSequenceRule.RULE_4_STEREOGENIC_UNIT,
663
+ left_witness,
664
+ right_witness,
665
+ "Sequence Rule 4c ranks r before s and m before p.",
666
+ )
667
+ if left_label.isupper() and right_label.isupper():
668
+ return (
669
+ CIPSequenceRule.RULE_5_REFLECTION_VARIANT,
670
+ left_witness,
671
+ right_witness,
672
+ "Sequence Rule 5 ranks R/M/seqCis before "
673
+ "S/P/seqTrans enantiomorphs.",
674
+ )
675
+ return None
676
+
677
+ def compare(
678
+ self,
679
+ center: int,
680
+ left_reference: Reference,
681
+ right_reference: Reference,
682
+ *,
683
+ left_evidence: CIPLigandEvidence | None = None,
684
+ right_evidence: CIPLigandEvidence | None = None,
685
+ ) -> CIPComparison:
686
+ left = left_evidence or self.build_evidence(center, left_reference)
687
+ right = right_evidence or self.build_evidence(center, right_reference)
688
+ atomic = self._first_sphere_difference(
689
+ left,
690
+ right,
691
+ "atomic_signature",
692
+ 0,
693
+ )
694
+ if atomic is not None:
695
+ depth, left_witness, right_witness = atomic
696
+ return CIPComparison(
697
+ center,
698
+ left_reference,
699
+ right_reference,
700
+ self._outcome(left_witness, right_witness),
701
+ CIPSequenceRule.RULE_1A_ATOMIC_NUMBER,
702
+ depth,
703
+ left_witness,
704
+ right_witness,
705
+ "First exhaustive digraph difference in atomic number.",
706
+ left,
707
+ right,
708
+ )
709
+
710
+ duplicate = self._first_sphere_difference(
711
+ left, right, "duplicate_signature", (0, 0)
712
+ )
713
+ if duplicate is not None:
714
+ depth, left_witness, right_witness = duplicate
715
+ return CIPComparison(
716
+ center,
717
+ left_reference,
718
+ right_reference,
719
+ self._outcome(left_witness, right_witness),
720
+ CIPSequenceRule.RULE_1B_DUPLICATE_DISTANCE,
721
+ depth,
722
+ left_witness,
723
+ right_witness,
724
+ "Nearer corresponding duplicate atom node has precedence.",
725
+ left,
726
+ right,
727
+ )
728
+
729
+ isotope = self._first_sphere_difference(
730
+ left, right, "isotope_signature", (0, 0.0)
731
+ )
732
+ if isotope is not None:
733
+ depth, left_witness, right_witness = isotope
734
+ return CIPComparison(
735
+ center,
736
+ left_reference,
737
+ right_reference,
738
+ self._outcome(left_witness, right_witness),
739
+ CIPSequenceRule.RULE_2_ISOTOPE_MASS,
740
+ depth,
741
+ left_witness,
742
+ right_witness,
743
+ "First exhaustive digraph difference in atomic mass.",
744
+ left,
745
+ right,
746
+ )
747
+
748
+ stereo = self._stereo_difference(left, right)
749
+ if stereo is not None:
750
+ rule, left_witness, right_witness, reason = stereo
751
+ return CIPComparison(
752
+ center,
753
+ left_reference,
754
+ right_reference,
755
+ self._outcome(left_witness, right_witness),
756
+ rule,
757
+ None,
758
+ left_witness,
759
+ right_witness,
760
+ reason,
761
+ left,
762
+ right,
763
+ )
764
+
765
+ if (
766
+ left.termination is CIPTermination.DEPTH_CAP
767
+ or right.termination is CIPTermination.DEPTH_CAP
768
+ ):
769
+ reason = "Ligand exploration reached the explicit depth cap."
770
+ elif left.unsupported_features or right.unsupported_features:
771
+ reason = "Tied ligands require an unsupported mancude duplicate model."
772
+ elif left.stereogenic_units or right.stereogenic_units:
773
+ reason = (
774
+ "Tied ligands require Sequence Rules 3-5 " "stereogenic-unit ordering."
775
+ )
776
+ else:
777
+ return CIPComparison(
778
+ center,
779
+ left_reference,
780
+ right_reference,
781
+ CIPComparisonOutcome.TIE,
782
+ None,
783
+ None,
784
+ None,
785
+ None,
786
+ "Ligands are constitutionally and isotopically " "indistinguishable.",
787
+ left,
788
+ right,
789
+ )
790
+ return CIPComparison(
791
+ center,
792
+ left_reference,
793
+ right_reference,
794
+ CIPComparisonOutcome.UNSUPPORTED,
795
+ (
796
+ CIPSequenceRule.RULE_4_STEREOGENIC_UNIT
797
+ if left.stereogenic_units or right.stereogenic_units
798
+ else None
799
+ ),
800
+ None,
801
+ None,
802
+ None,
803
+ reason,
804
+ left,
805
+ right,
806
+ )
807
+
808
+ def rank(self, center: int, references: Iterable[Reference]) -> CIPRanking:
809
+ values = tuple(references)
810
+ if len(values) != len(set(values)):
811
+ raise ValueError("CIP ranking references must be distinct.")
812
+ comparisons = tuple(
813
+ self.compare(center, left, right) for left, right in combinations(values, 2)
814
+ )
815
+ complete = all(comparison.decided for comparison in comparisons)
816
+ if not complete:
817
+ return CIPRanking(center, values, (), comparisons, False)
818
+ wins = {reference: 0 for reference in values}
819
+ for comparison in comparisons:
820
+ winner = (
821
+ comparison.left_reference
822
+ if comparison.outcome is CIPComparisonOutcome.LEFT_HIGHER
823
+ else comparison.right_reference
824
+ )
825
+ wins[winner] += 1
826
+ ordered = tuple(sorted(values, key=lambda value: (-wins[value], repr(value))))
827
+ if len(set(wins.values())) != len(values):
828
+ return CIPRanking(center, values, (), comparisons, False)
829
+ return CIPRanking(center, values, ordered, comparisons, True)
830
+
831
+
832
+ __all__ = [
833
+ "CIPComparison",
834
+ "CIPComparisonOutcome",
835
+ "CIPLigandEvidence",
836
+ "CIPNodeEvidence",
837
+ "CIPRanker",
838
+ "CIPRanking",
839
+ "CIPSequenceRule",
840
+ "CIPSphereEvidence",
841
+ "CIPStereogenicUnitEvidence",
842
+ "CIPTermination",
843
+ ]