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,964 @@
1
+ """Descriptor registry comparison for molecules, ITS graphs, and rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Iterable, Mapping
7
+
8
+ import networkx as nx
9
+
10
+ from .descriptors import (
11
+ OctahedralStereo,
12
+ SUPPORTED_STEREO_DESCRIPTOR_CLASSES,
13
+ SquarePlanarStereo,
14
+ StereoValue,
15
+ TetrahedralStereo,
16
+ TrigonalBipyramidalStereo,
17
+ descriptor_id,
18
+ parse_virtual_reference,
19
+ virtual_reference,
20
+ )
21
+ from .orbits import Permutation, StereoRelation, StereoRelationKind
22
+
23
+ _ATOM_STEREO_TYPES = (
24
+ TetrahedralStereo,
25
+ SquarePlanarStereo,
26
+ TrigonalBipyramidalStereo,
27
+ OctahedralStereo,
28
+ )
29
+
30
+
31
+ class NonInvertibleStereoEffectError(ValueError):
32
+ """Raised when reverse-rule construction would invent stereochemistry.
33
+
34
+ A known-to-unknown ``UNSPECIFIED`` endpoint relation is not one-to-one: a
35
+ specified reactant descriptor can map to an unknown product descriptor,
36
+ but that unknown state cannot identify which descriptor to recreate.
37
+ """
38
+
39
+ reason = "non_reversible_unspecified_descriptor"
40
+
41
+ def __init__(self, targets: Iterable[str] = ()) -> None:
42
+ self.targets = tuple(sorted(set(targets)))
43
+ detail = f"; targets={list(self.targets)!r}" if self.targets else ""
44
+ super().__init__(f"{self.reason}{detail}")
45
+
46
+
47
+ class StereoAlignmentError(ValueError):
48
+ """Raised when a reaction effect has no unambiguous reference alignment."""
49
+
50
+ def __init__(self, issue_code: str, detail: str = "") -> None:
51
+ self.issue_code = issue_code
52
+ self.detail = detail
53
+ suffix = f": {detail}" if detail else ""
54
+ super().__init__(f"{issue_code}{suffix}")
55
+
56
+
57
+ def _peripheral_references(descriptor: StereoValue) -> tuple[Any, ...]:
58
+ if isinstance(descriptor, _ATOM_STEREO_TYPES):
59
+ return descriptor.atoms[1:]
60
+ from .extended_descriptors import HelicalStereo, PlanarChiralityStereo
61
+ from .global_stereo import FrameworkStereo
62
+
63
+ if isinstance(
64
+ descriptor,
65
+ (HelicalStereo, PlanarChiralityStereo, FrameworkStereo),
66
+ ):
67
+ return ()
68
+ return (*descriptor.atoms[:2], *descriptor.atoms[4:])
69
+
70
+
71
+ def _mapping_sort_key(pair: tuple[Any, Any]) -> tuple[str, str, str, str]:
72
+ return type(pair[0]).__name__, repr(pair[0]), type(pair[1]).__name__, repr(pair[1])
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class StereoReferenceAlignment:
77
+ """Reference replacement evidence for one reaction-local stereo frame."""
78
+
79
+ mapping: tuple[tuple[Any, Any], ...] = ()
80
+ status: str = "identity"
81
+ removed: tuple[Any, ...] = ()
82
+ added: tuple[Any, ...] = ()
83
+ issue_code: str | None = None
84
+ detail: str | None = None
85
+
86
+ def __post_init__(self) -> None:
87
+ mapping = tuple(
88
+ sorted(
89
+ (tuple(pair) for pair in self.mapping),
90
+ key=_mapping_sort_key,
91
+ )
92
+ )
93
+ object.__setattr__(self, "mapping", mapping)
94
+ if self.status not in {"identity", "inferred", "explicit", "refused"}:
95
+ raise ValueError(f"Unsupported stereo alignment status: {self.status!r}.")
96
+ if self.status == "refused" and self.issue_code is None:
97
+ raise ValueError("A refused stereo alignment requires an issue code.")
98
+ if self.status != "refused" and self.issue_code is not None:
99
+ raise ValueError("Only a refused stereo alignment may carry an issue code.")
100
+ sources = [source for source, _ in mapping]
101
+ targets = [target for _, target in mapping]
102
+ if len(sources) != len(set(sources)) or len(targets) != len(set(targets)):
103
+ raise ValueError("A stereo reference mapping must be bijective.")
104
+
105
+ @property
106
+ def accepted(self) -> bool:
107
+ return self.status != "refused"
108
+
109
+ def reverse(self) -> "StereoReferenceAlignment":
110
+ if not self.accepted:
111
+ return StereoReferenceAlignment(
112
+ (),
113
+ "refused",
114
+ self.added,
115
+ self.removed,
116
+ self.issue_code,
117
+ self.detail,
118
+ )
119
+ status = "identity" if not self.mapping else self.status
120
+ return StereoReferenceAlignment(
121
+ tuple((target, source) for source, target in self.mapping),
122
+ status,
123
+ self.added,
124
+ self.removed,
125
+ )
126
+
127
+ def then(
128
+ self,
129
+ after: "StereoReferenceAlignment",
130
+ ) -> "StereoReferenceAlignment":
131
+ """Compose two endpoint-reference transports."""
132
+ if not self.accepted:
133
+ return self
134
+ if not after.accepted:
135
+ return after
136
+ first = dict(self.mapping)
137
+ second = dict(after.mapping)
138
+ composed = {
139
+ source: second.get(target, target) for source, target in first.items()
140
+ }
141
+ first_targets = set(first.values())
142
+ composed.update(
143
+ {
144
+ source: target
145
+ for source, target in second.items()
146
+ if source not in first_targets
147
+ }
148
+ )
149
+ composed = {
150
+ source: target for source, target in composed.items() if source != target
151
+ }
152
+ if not composed:
153
+ return StereoReferenceAlignment((), "identity")
154
+ status = "explicit" if "explicit" in {self.status, after.status} else "inferred"
155
+ return StereoReferenceAlignment(
156
+ tuple(composed.items()),
157
+ status,
158
+ tuple(composed),
159
+ tuple(composed.values()),
160
+ )
161
+
162
+ def relabel(
163
+ self,
164
+ mapping: Mapping[int, int],
165
+ ) -> "StereoReferenceAlignment":
166
+ """Relabel material and owner-qualified virtual references."""
167
+
168
+ def translate(reference: Any) -> Any:
169
+ if type(reference) is int:
170
+ return mapping.get(reference, reference)
171
+ virtual = parse_virtual_reference(reference)
172
+ if virtual is None:
173
+ return reference
174
+ return virtual_reference(
175
+ virtual.kind,
176
+ mapping.get(virtual.center, virtual.center),
177
+ )
178
+
179
+ return StereoReferenceAlignment(
180
+ tuple(
181
+ (translate(source), translate(target))
182
+ for source, target in self.mapping
183
+ ),
184
+ self.status,
185
+ tuple(translate(value) for value in self.removed),
186
+ tuple(translate(value) for value in self.added),
187
+ self.issue_code,
188
+ self.detail,
189
+ )
190
+
191
+ def to_dict(self) -> dict[str, Any]:
192
+ result: dict[str, Any] = {
193
+ "status": self.status,
194
+ "mapping": [list(pair) for pair in self.mapping],
195
+ "removed": list(self.removed),
196
+ "added": list(self.added),
197
+ }
198
+ if self.issue_code is not None:
199
+ result["issue_code"] = self.issue_code
200
+ if self.detail is not None:
201
+ result["detail"] = self.detail
202
+ return result
203
+
204
+ @classmethod
205
+ def from_dict(
206
+ cls,
207
+ value: Mapping[str, Any],
208
+ ) -> "StereoReferenceAlignment":
209
+ """Read one serialized endpoint-reference alignment."""
210
+ return cls(
211
+ mapping=tuple(tuple(pair) for pair in value.get("mapping", ())),
212
+ status=str(value.get("status", "identity")),
213
+ removed=tuple(value.get("removed", ())),
214
+ added=tuple(value.get("added", ())),
215
+ issue_code=value.get("issue_code"),
216
+ detail=value.get("detail"),
217
+ )
218
+
219
+
220
+ def _reference_delta(
221
+ old: StereoValue,
222
+ new: StereoValue,
223
+ ) -> tuple[tuple[Any, ...], tuple[Any, ...]]:
224
+ old_refs = _peripheral_references(old)
225
+ new_refs = _peripheral_references(new)
226
+ removed = tuple(reference for reference in old_refs if reference not in new_refs)
227
+ added = tuple(reference for reference in new_refs if reference not in old_refs)
228
+ return removed, added
229
+
230
+
231
+ def align_stereo_references(
232
+ old: StereoValue,
233
+ new: StereoValue,
234
+ reference_mapping: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None = None,
235
+ ) -> StereoReferenceAlignment:
236
+ """Align endpoint frames, inferring only a unique one-for-one replacement."""
237
+ if type(old) is not type(new) or descriptor_id(old) != descriptor_id(new):
238
+ return StereoReferenceAlignment(
239
+ (),
240
+ "refused",
241
+ issue_code="STEREO_ALIGNMENT_INCOMPATIBLE_LOCUS",
242
+ detail="Endpoint descriptors do not share a shape and locus.",
243
+ )
244
+
245
+ removed, added = _reference_delta(old, new)
246
+ if reference_mapping is not None:
247
+ raw_items = (
248
+ reference_mapping.items()
249
+ if isinstance(reference_mapping, Mapping)
250
+ else reference_mapping
251
+ )
252
+ mapping = tuple(raw_items)
253
+ try:
254
+ candidate = StereoReferenceAlignment(
255
+ mapping,
256
+ "explicit" if mapping else "identity",
257
+ removed,
258
+ added,
259
+ )
260
+ except (TypeError, ValueError) as exc:
261
+ return StereoReferenceAlignment(
262
+ (),
263
+ "refused",
264
+ removed,
265
+ added,
266
+ "STEREO_ALIGNMENT_INVALID_EXPLICIT_MAP",
267
+ str(exc),
268
+ )
269
+ if set(source for source, _ in candidate.mapping) != set(removed) or set(
270
+ target for _, target in candidate.mapping
271
+ ) != set(added):
272
+ return StereoReferenceAlignment(
273
+ (),
274
+ "refused",
275
+ removed,
276
+ added,
277
+ "STEREO_ALIGNMENT_INVALID_EXPLICIT_MAP",
278
+ "The explicit map must pair every removed reference with every added reference.",
279
+ )
280
+ return candidate
281
+
282
+ if not removed and not added:
283
+ return StereoReferenceAlignment((), "identity")
284
+ if len(removed) == len(added) == 1:
285
+ return StereoReferenceAlignment(
286
+ ((removed[0], added[0]),),
287
+ "inferred",
288
+ removed,
289
+ added,
290
+ )
291
+ issue = (
292
+ "STEREO_ALIGNMENT_AMBIGUOUS"
293
+ if len(removed) == len(added) and len(removed) > 1
294
+ else "STEREO_ALIGNMENT_INCOMPATIBLE_REFERENCES"
295
+ )
296
+ return StereoReferenceAlignment(
297
+ (),
298
+ "refused",
299
+ removed,
300
+ added,
301
+ issue,
302
+ (
303
+ "Multiple replacements require an explicit bijection."
304
+ if issue == "STEREO_ALIGNMENT_AMBIGUOUS"
305
+ else "Removed and added reference populations differ."
306
+ ),
307
+ )
308
+
309
+
310
+ def _relation_after_alignment(
311
+ old: StereoValue,
312
+ new: StereoValue,
313
+ alignment: StereoReferenceAlignment,
314
+ ) -> StereoRelation | None:
315
+ if not alignment.accepted:
316
+ return None
317
+ aligned = (
318
+ old.replace_references(dict(alignment.mapping)) if alignment.mapping else old
319
+ )
320
+ return aligned.relation_to(new)
321
+
322
+
323
+ def _change_from_relation(
324
+ old: StereoValue | None,
325
+ new: StereoValue | None,
326
+ transition: StereoValue | None,
327
+ relation: StereoRelation | None,
328
+ ) -> str:
329
+ if old is None and new is None:
330
+ return "FLEETING" if transition is not None else "UNSPECIFIED"
331
+ if old is None:
332
+ return "FORMED"
333
+ if new is None:
334
+ return "BROKEN"
335
+ if relation is None:
336
+ return "UNSPECIFIED"
337
+ if relation.kind is StereoRelationKind.EQUIVALENT:
338
+ return "RETAINED"
339
+ if relation.kind is StereoRelationKind.OPPOSITE:
340
+ return "INVERTED"
341
+ if relation.kind is StereoRelationKind.RECONFIGURED:
342
+ # Beta-2 projected every descriptor-level TBP/octahedral inversion onto
343
+ # the binary INVERTED label. Keep that public string when applicable,
344
+ # while ``relation`` remains the authoritative non-binary class.
345
+ from .legacy import legacy_classify_stereo_change
346
+
347
+ legacy_projection = legacy_classify_stereo_change(old, new, transition)
348
+ if legacy_projection in {"RETAINED", "INVERTED"}:
349
+ return legacy_projection
350
+ return "UNSPECIFIED"
351
+
352
+
353
+ def _descriptor_from_configuration(
354
+ prototype: StereoValue,
355
+ frame: tuple[Any, ...],
356
+ *,
357
+ unspecified: bool,
358
+ ) -> StereoValue:
359
+ parity = (
360
+ None
361
+ if unspecified
362
+ else (
363
+ 0 if prototype.descriptor_class in {"square_planar", "planar_bond"} else 1
364
+ )
365
+ )
366
+ return type(prototype)(frame, parity, prototype.provenance)
367
+
368
+
369
+ def _unspecified_descriptor(prototype: StereoValue) -> StereoValue:
370
+ value = prototype.to_dict()
371
+ if value["descriptor_class"] == "framework":
372
+ value["orientation"] = None
373
+ else:
374
+ value["parity"] = None
375
+ from .descriptors import stereo_from_dict
376
+
377
+ return stereo_from_dict(value)
378
+
379
+
380
+ @dataclass(frozen=True)
381
+ class StereoChange:
382
+ """One rule-local stereochemical state transition.
383
+
384
+ ``before`` and ``after`` are the stable molecular states. ``transition``
385
+ is optional ITS/transition-state stereo and is deliberately separate from
386
+ either endpoint. The textual ``change`` remains a derived, readable
387
+ classification for compatibility with existing SynKit rule consumers.
388
+ """
389
+
390
+ change: str
391
+ before: StereoValue | None
392
+ after: StereoValue | None
393
+ transition: StereoValue | None = None
394
+ relation: StereoRelation | None = None
395
+ reference_mapping: tuple[tuple[Any, Any], ...] | Mapping[Any, Any] | None = None
396
+ alignment: StereoReferenceAlignment | None = None
397
+
398
+ def __post_init__(self) -> None:
399
+ if self.before is None or self.after is None:
400
+ alignment = self.alignment or StereoReferenceAlignment((), "identity")
401
+ relation = self.relation
402
+ else:
403
+ alignment = self.alignment or align_stereo_references(
404
+ self.before,
405
+ self.after,
406
+ self.reference_mapping,
407
+ )
408
+ relation = self.relation or _relation_after_alignment(
409
+ self.before,
410
+ self.after,
411
+ alignment,
412
+ )
413
+ object.__setattr__(self, "alignment", alignment)
414
+ object.__setattr__(self, "reference_mapping", alignment.mapping)
415
+ object.__setattr__(self, "relation", relation)
416
+
417
+ @classmethod
418
+ def from_endpoints(
419
+ cls,
420
+ before: StereoValue | None,
421
+ after: StereoValue | None,
422
+ transition: StereoValue | None = None,
423
+ *,
424
+ reference_mapping: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None = None,
425
+ ) -> "StereoChange":
426
+ alignment = (
427
+ align_stereo_references(before, after, reference_mapping)
428
+ if before is not None and after is not None
429
+ else StereoReferenceAlignment((), "identity")
430
+ )
431
+ relation = (
432
+ _relation_after_alignment(before, after, alignment)
433
+ if before is not None and after is not None
434
+ else None
435
+ )
436
+ return cls(
437
+ _change_from_relation(before, after, transition, relation),
438
+ before,
439
+ after,
440
+ transition,
441
+ relation,
442
+ alignment.mapping,
443
+ alignment,
444
+ )
445
+
446
+ @property
447
+ def dependencies(self) -> frozenset[int]:
448
+ values = set()
449
+ if self.before:
450
+ values.update(self.before.dependencies)
451
+ if self.after:
452
+ values.update(self.after.dependencies)
453
+ if self.transition:
454
+ values.update(self.transition.dependencies)
455
+ return frozenset(values)
456
+
457
+ @property
458
+ def fleeting(self) -> StereoValue | None:
459
+ """Compatibility term for stereo present only at the transition state."""
460
+ return self.transition
461
+
462
+ @property
463
+ def evidence_kind(self) -> str:
464
+ """Return the rich relation label behind the compatibility projection."""
465
+ if self.before is None and self.after is None:
466
+ return "fleeting" if self.transition is not None else "unspecified"
467
+ if self.before is None:
468
+ return "formed"
469
+ if self.after is None:
470
+ return "broken"
471
+ if self.relation is None:
472
+ return "alignment_refused"
473
+ if self.relation.kind is StereoRelationKind.RECONFIGURED:
474
+ encoded = "-".join(map(str, self.relation.class_id or ()))
475
+ return f"reconfigured:{encoded}"
476
+ return self.relation.kind.value
477
+
478
+ def relation_evidence(self) -> dict[str, Any]:
479
+ """Return deterministic, machine-readable reaction relation evidence."""
480
+ result: dict[str, Any] = {
481
+ "kind": self.evidence_kind,
482
+ "alignment": self.alignment.to_dict(),
483
+ }
484
+ if self.relation is not None:
485
+ relation: dict[str, Any] = {
486
+ "kind": self.relation.kind.value,
487
+ "shape": self.relation.shape,
488
+ "class_id": (
489
+ list(self.relation.class_id)
490
+ if self.relation.class_id is not None
491
+ else None
492
+ ),
493
+ }
494
+ if self.relation.witness is not None:
495
+ relation["witness"] = list(self.relation.witness.permutation.image)
496
+ result["relation"] = relation
497
+ return result
498
+
499
+ @property
500
+ def non_invertible(self) -> bool:
501
+ """Whether reversing this change would fabricate endpoint parity."""
502
+ return (
503
+ self.change == "UNSPECIFIED"
504
+ and self.before is not None
505
+ and self.after is not None
506
+ and self.before.parity is not None
507
+ and self.after.parity is None
508
+ ) or self.alignment.issue_code == "STEREO_COMPOSITION_INFORMATION_LOSS"
509
+
510
+ def reverse(
511
+ self,
512
+ *,
513
+ semantics: str = "orbit",
514
+ diagnostics: list[Any] | None = None,
515
+ ) -> "StereoChange":
516
+ """Return the same state transition in the opposite direction."""
517
+ from .legacy import (
518
+ StereoSemanticComparison,
519
+ StereoSemanticsMode,
520
+ legacy_classify_stereo_change,
521
+ )
522
+
523
+ if self.non_invertible:
524
+ raise NonInvertibleStereoEffectError()
525
+ mode = StereoSemanticsMode(semantics)
526
+ reverse_mapping = (
527
+ {target: source for source, target in self.reference_mapping}
528
+ if self.alignment.status == "explicit"
529
+ else None
530
+ )
531
+ orbit_change = StereoChange.from_endpoints(
532
+ self.after,
533
+ self.before,
534
+ self.transition,
535
+ reference_mapping=reverse_mapping,
536
+ )
537
+ if mode is StereoSemanticsMode.ORBIT:
538
+ return orbit_change
539
+ legacy_change = legacy_classify_stereo_change(
540
+ self.after,
541
+ self.before,
542
+ self.transition,
543
+ )
544
+ if mode is StereoSemanticsMode.COMPARE and diagnostics is not None:
545
+ diagnostics.append(
546
+ StereoSemanticComparison.create(
547
+ "reaction_stereo_reverse",
548
+ orbit_change.change,
549
+ legacy_change,
550
+ )
551
+ )
552
+ if mode is StereoSemanticsMode.LEGACY:
553
+ return StereoChange(
554
+ legacy_change,
555
+ orbit_change.before,
556
+ orbit_change.after,
557
+ orbit_change.transition,
558
+ orbit_change.relation,
559
+ orbit_change.reference_mapping,
560
+ orbit_change.alignment,
561
+ )
562
+ return orbit_change
563
+
564
+ def _permutation_for_substrate(
565
+ self, descriptor: StereoValue, permutation: Permutation
566
+ ) -> Permutation:
567
+ """Express a specified endpoint witness in the matched frame basis.
568
+
569
+ :param descriptor: Specified input descriptor at the validated guard locus.
570
+ :param permutation: Concrete endpoint witness stored by this change.
571
+ :return: Rebased witness, or the same intrinsically defined operation.
572
+ :raises StereoAlignmentError: Input references or a different guard orbit
573
+ do not determine the operation's basis.
574
+ """
575
+ source_relation = self.before.relation_to(descriptor)
576
+ if source_relation.witness is None:
577
+ raise StereoAlignmentError(
578
+ "STEREO_ALIGNMENT_INCOMPATIBLE_REFERENCES",
579
+ "The substrate frame cannot be aligned to the rule guard frame.",
580
+ )
581
+ if source_relation.kind is StereoRelationKind.EQUIVALENT:
582
+ # Tuple actions are pullbacks. If u' = gamma.apply(u), the
583
+ # same operation in the new basis has slot image
584
+ # gamma^-1 o pi o gamma; its product differs only by gamma.
585
+ basis = source_relation.witness.permutation
586
+ return basis.inverse().then(permutation).then(basis)
587
+ group = self.before.configuration.definition.preserving_group
588
+ if any(
589
+ permutation.inverse().then(element).then(permutation) not in group
590
+ for element in group.elements
591
+ ):
592
+ raise StereoAlignmentError(
593
+ "STEREO_RELATION_AMBIGUOUS_BASIS",
594
+ "A non-normalizing witness requires a substrate in the "
595
+ "rule guard's configuration orbit.",
596
+ )
597
+ return permutation
598
+
599
+ def _apply_orbit_to(self, descriptor: StereoValue) -> StereoValue | None:
600
+ if self.before is None:
601
+ return self.after
602
+ if self.after is None:
603
+ return None
604
+ if type(descriptor) is not type(self.before) or descriptor_id(descriptor) != (
605
+ descriptor_id(self.before)
606
+ ):
607
+ raise StereoAlignmentError(
608
+ "STEREO_ALIGNMENT_INCOMPATIBLE_SUBSTRATE",
609
+ "The substrate descriptor does not share the rule effect locus.",
610
+ )
611
+ if not self.alignment.accepted:
612
+ raise StereoAlignmentError(
613
+ self.alignment.issue_code or "STEREO_ALIGNMENT_REFUSED",
614
+ self.alignment.detail or "",
615
+ )
616
+ if self.relation is None:
617
+ raise StereoAlignmentError(
618
+ "STEREO_RELATION_NOT_REPLAYABLE",
619
+ "The rule effect has no configuration relation.",
620
+ )
621
+ if (
622
+ self.relation.witness is None
623
+ or self.after.descriptor_class not in SUPPORTED_STEREO_DESCRIPTOR_CLASSES
624
+ ):
625
+ source_relation = self.before.relation_to(descriptor)
626
+ if source_relation.kind is StereoRelationKind.EQUIVALENT:
627
+ return self.after
628
+ if source_relation.kind is StereoRelationKind.OPPOSITE:
629
+ return self.after.invert()
630
+ if source_relation.kind is StereoRelationKind.UNSPECIFIED:
631
+ return _unspecified_descriptor(self.after)
632
+ raise StereoAlignmentError(
633
+ "STEREO_RELATION_NOT_REPLAYABLE",
634
+ "The substrate has no transportable relation to the rule guard.",
635
+ )
636
+ unspecified = (
637
+ descriptor.parity is None
638
+ or self.after.parity is None
639
+ or self.relation.kind is StereoRelationKind.UNSPECIFIED
640
+ )
641
+ permutation = self.relation.witness.permutation
642
+ if not unspecified:
643
+ permutation = self._permutation_for_substrate(descriptor, permutation)
644
+ aligned = descriptor.replace_references(dict(self.reference_mapping))
645
+ frame = permutation.apply(aligned.configuration.frame)
646
+ return _descriptor_from_configuration(
647
+ self.after,
648
+ frame,
649
+ unspecified=unspecified,
650
+ )
651
+
652
+ def apply_to(
653
+ self,
654
+ descriptor: StereoValue,
655
+ *,
656
+ semantics: str = "orbit",
657
+ diagnostics: list[Any] | None = None,
658
+ ) -> StereoValue | None:
659
+ """Apply this relation, optionally auditing the frozen Beta-2 result."""
660
+ from .legacy import (
661
+ StereoSemanticComparison,
662
+ StereoSemanticsMode,
663
+ legacy_apply_stereo_change,
664
+ )
665
+
666
+ mode = StereoSemanticsMode(semantics)
667
+ if mode is StereoSemanticsMode.LEGACY:
668
+ return legacy_apply_stereo_change(self, descriptor)
669
+ orbit_result = self._apply_orbit_to(descriptor)
670
+ if mode is StereoSemanticsMode.COMPARE:
671
+ legacy_result = legacy_apply_stereo_change(self, descriptor)
672
+ expected_divergence = None
673
+ if (
674
+ self.relation is not None
675
+ and self.relation.kind is StereoRelationKind.RECONFIGURED
676
+ and self.relation.shape in {"trigonal_bipyramidal", "octahedral"}
677
+ ):
678
+ expected_divergence = "nonbinary_orbit_reconfiguration"
679
+ if diagnostics is not None:
680
+ diagnostics.append(
681
+ StereoSemanticComparison.create(
682
+ "reaction_stereo_application",
683
+ orbit_result,
684
+ legacy_result,
685
+ expected_divergence=expected_divergence,
686
+ )
687
+ )
688
+ return orbit_result
689
+
690
+ def relabel(self, mapping: Mapping[int, int]) -> "StereoChange":
691
+ """Relabel endpoints and the explicit reference alignment together."""
692
+
693
+ def relabel_reference(reference: Any) -> Any:
694
+ if type(reference) is int:
695
+ return mapping.get(reference, reference)
696
+ virtual = parse_virtual_reference(reference)
697
+ if virtual is None:
698
+ return reference
699
+ return virtual_reference(
700
+ virtual.kind,
701
+ mapping.get(virtual.center, virtual.center),
702
+ )
703
+
704
+ translated_reference_mapping = (
705
+ {
706
+ relabel_reference(source): relabel_reference(target)
707
+ for source, target in self.reference_mapping
708
+ }
709
+ if self.alignment.status == "explicit"
710
+ else None
711
+ )
712
+ before = self.before.relabel(mapping) if self.before is not None else None
713
+ after = self.after.relabel(mapping) if self.after is not None else None
714
+ transition = (
715
+ self.transition.relabel(mapping) if self.transition is not None else None
716
+ )
717
+ if not self.alignment.accepted:
718
+ return StereoChange(
719
+ self.change,
720
+ before,
721
+ after,
722
+ transition,
723
+ alignment=self.alignment.relabel(mapping),
724
+ )
725
+ return StereoChange.from_endpoints(
726
+ before,
727
+ after,
728
+ transition,
729
+ reference_mapping=translated_reference_mapping,
730
+ )
731
+
732
+ def then(self, after: "StereoChange") -> "StereoChange":
733
+ """Compose two consecutive changes at the same stereo locus."""
734
+ if self.after != after.before:
735
+ raise StereoAlignmentError(
736
+ "STEREO_COMPOSITION_NONCOMPOSABLE",
737
+ "The first product descriptor must equal the second "
738
+ "reactant descriptor.",
739
+ )
740
+ if self.before is None and after.after is None:
741
+ transition = self.after or self.transition or after.transition
742
+ return StereoChange.from_endpoints(None, None, transition)
743
+ if self.after is None and after.before is None:
744
+ alignment = StereoReferenceAlignment(
745
+ (),
746
+ "refused",
747
+ issue_code="STEREO_COMPOSITION_INFORMATION_LOSS",
748
+ detail=(
749
+ "A broken descriptor was recreated without a continuous "
750
+ "configuration witness."
751
+ ),
752
+ )
753
+ return StereoChange(
754
+ "UNSPECIFIED",
755
+ self.before,
756
+ after.after,
757
+ alignment=alignment,
758
+ )
759
+ alignment = self.alignment.then(after.alignment)
760
+ reference_mapping = (
761
+ dict(alignment.mapping)
762
+ if alignment.status == "explicit" or len(alignment.mapping) > 1
763
+ else None
764
+ )
765
+ return StereoChange.from_endpoints(
766
+ self.before,
767
+ after.after,
768
+ reference_mapping=reference_mapping,
769
+ )
770
+
771
+ def to_dict(self) -> dict[str, Any]:
772
+ """Return the lossless version-independent value payload."""
773
+
774
+ def encode(value: StereoValue | None) -> dict[str, Any] | None:
775
+ return value.to_dict() if value is not None else None
776
+
777
+ return {
778
+ "change": self.change,
779
+ "before": encode(self.before),
780
+ "after": encode(self.after),
781
+ "transition": encode(self.transition),
782
+ "alignment": self.alignment.to_dict(),
783
+ "relation_evidence": self.relation_evidence(),
784
+ }
785
+
786
+ @classmethod
787
+ def from_dict(cls, value: Mapping[str, Any]) -> "StereoChange":
788
+ """Read a serialized change and rederive its orbit relation."""
789
+ from .descriptors import stereo_from_dict
790
+
791
+ def decode(item: Any) -> StereoValue | None:
792
+ return stereo_from_dict(item) if item is not None else None
793
+
794
+ alignment = StereoReferenceAlignment.from_dict(value.get("alignment", {}))
795
+ return cls(
796
+ change=str(value["change"]),
797
+ before=decode(value.get("before")),
798
+ after=decode(value.get("after")),
799
+ transition=decode(value.get("transition")),
800
+ reference_mapping=alignment.mapping,
801
+ alignment=alignment,
802
+ )
803
+
804
+ def signature(self) -> tuple[Any, ...]:
805
+ """Return a stable, hashable rule-identity signature."""
806
+ return (
807
+ self.change,
808
+ self.before,
809
+ self.after,
810
+ self.transition,
811
+ self.reference_mapping,
812
+ self.evidence_kind,
813
+ )
814
+
815
+
816
+ def classify_stereo_change(
817
+ old: StereoValue | None,
818
+ new: StereoValue | None,
819
+ transition: StereoValue | None = None,
820
+ *,
821
+ reference_mapping: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None = None,
822
+ semantics: str = "orbit",
823
+ diagnostics: list[Any] | None = None,
824
+ ) -> str:
825
+ """Derive the compatibility label from an aligned orbit relation."""
826
+ from .legacy import (
827
+ StereoSemanticComparison,
828
+ StereoSemanticsMode,
829
+ legacy_classify_stereo_change,
830
+ )
831
+
832
+ mode = StereoSemanticsMode(semantics)
833
+ legacy_result = None
834
+ if mode is not StereoSemanticsMode.ORBIT:
835
+ legacy_result = legacy_classify_stereo_change(old, new, transition)
836
+ if mode is StereoSemanticsMode.LEGACY:
837
+ return legacy_result
838
+ change = StereoChange.from_endpoints(
839
+ old,
840
+ new,
841
+ transition,
842
+ reference_mapping=reference_mapping,
843
+ )
844
+ if mode is StereoSemanticsMode.COMPARE and diagnostics is not None:
845
+ diagnostics.append(
846
+ StereoSemanticComparison.create(
847
+ "reaction_stereo_change",
848
+ change.change,
849
+ legacy_result,
850
+ )
851
+ )
852
+ if (
853
+ change.relation is not None
854
+ and change.relation.kind is StereoRelationKind.RECONFIGURED
855
+ and change.relation.shape in {"trigonal_bipyramidal", "octahedral"}
856
+ ):
857
+ diagnostics.append(
858
+ StereoSemanticComparison.create(
859
+ "reaction_stereo_relation",
860
+ change.evidence_kind,
861
+ str(legacy_result).lower(),
862
+ expected_divergence="nonbinary_orbit_reconfiguration",
863
+ )
864
+ )
865
+ return change.change
866
+
867
+
868
+ def stereo_registry(graph: nx.Graph) -> Mapping[str, StereoValue]:
869
+ return graph.graph.get("stereo_descriptors", {})
870
+
871
+
872
+ def compare_stereo_registries(
873
+ before: nx.Graph,
874
+ after: nx.Graph,
875
+ transition: nx.Graph | None = None,
876
+ *,
877
+ reference_mappings: (
878
+ Mapping[
879
+ str,
880
+ Mapping[Any, Any] | Iterable[tuple[Any, Any]],
881
+ ]
882
+ | None
883
+ ) = None,
884
+ semantics: str = "orbit",
885
+ diagnostics: list[Any] | None = None,
886
+ ) -> dict[str, StereoChange]:
887
+ left, right = stereo_registry(before), stereo_registry(after)
888
+ middle = stereo_registry(transition) if transition is not None else {}
889
+ changes: dict[str, StereoChange] = {}
890
+ for key in sorted(set(left) | set(right) | set(middle)):
891
+ old, new, transient = left.get(key), right.get(key), middle.get(key)
892
+ reference_mapping = (
893
+ None if reference_mappings is None else reference_mappings.get(key)
894
+ )
895
+ change = StereoChange.from_endpoints(
896
+ old,
897
+ new,
898
+ transient,
899
+ reference_mapping=reference_mapping,
900
+ )
901
+ if semantics != "orbit":
902
+ classify_stereo_change(
903
+ old,
904
+ new,
905
+ transient,
906
+ reference_mapping=reference_mapping,
907
+ semantics=semantics,
908
+ diagnostics=diagnostics,
909
+ )
910
+ changes[key] = change
911
+ return changes
912
+
913
+
914
+ def annotate_its_stereo(
915
+ its: nx.Graph,
916
+ before: nx.Graph,
917
+ after: nx.Graph,
918
+ transition: nx.Graph | None = None,
919
+ *,
920
+ reference_mappings: (
921
+ Mapping[
922
+ str,
923
+ Mapping[Any, Any] | Iterable[tuple[Any, Any]],
924
+ ]
925
+ | None
926
+ ) = None,
927
+ ) -> None:
928
+ changes = compare_stereo_registries(
929
+ before,
930
+ after,
931
+ transition,
932
+ reference_mappings=reference_mappings,
933
+ )
934
+ its.graph["stereo_changes"] = changes
935
+ its.graph["stereo_descriptors"] = {
936
+ "reactant": dict(stereo_registry(before)),
937
+ "product": dict(stereo_registry(after)),
938
+ }
939
+ if transition is not None:
940
+ its.graph["stereo_descriptors"]["transition"] = dict(
941
+ stereo_registry(transition)
942
+ )
943
+
944
+
945
+ def stereo_complete_reaction_center_nodes(its: nx.Graph) -> set[Any]:
946
+ changes = its.graph.get("stereo_changes", {})
947
+ if not changes:
948
+ return set()
949
+ atom_map_nodes = {}
950
+ for node, attrs in its.nodes(data=True):
951
+ atom_map = attrs.get("atom_map", node)
952
+ if isinstance(atom_map, int):
953
+ atom_map_nodes[atom_map] = node
954
+ elif isinstance(atom_map, tuple) and len(atom_map) == 2:
955
+ for side_map in atom_map:
956
+ if isinstance(side_map, int) and side_map > 0:
957
+ atom_map_nodes[side_map] = node
958
+ return {
959
+ atom_map_nodes[atom_map]
960
+ for change in changes.values()
961
+ if change.change != "RETAINED"
962
+ for atom_map in change.dependencies
963
+ if atom_map in atom_map_nodes
964
+ }