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,998 @@
1
+ """Typed DPO rule spans and application over finite Lewis-labelled graphs.
2
+
3
+ Rule interfaces use the endpoint schema with explicitly undefined coordinates.
4
+ Contexts erase these coordinates; labels on the two squares are checked by
5
+ coordinate unions and agreement pullbacks. Matches into total hosts are exact.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import Counter
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from numbers import Integral
14
+ from typing import Any, Hashable, Mapping
15
+
16
+ import networkx as nx
17
+
18
+ from synkit.Graph.Morphism import (
19
+ ISOTOPE_ELECTRON_LLG_SCHEMA,
20
+ LLGError,
21
+ LLGIssueCode,
22
+ LLGMorphism,
23
+ LewisLabelledGraph,
24
+ derive_electron_labeled_graph,
25
+ is_electron_schema,
26
+ llg_from_its,
27
+ )
28
+
29
+
30
+ class DPOIssueCode(str, Enum):
31
+ """Stable refusal codes for rule construction and application."""
32
+
33
+ SCHEMA_MISMATCH = "DPO_SCHEMA_MISMATCH"
34
+ INTERFACE_SCHEMA = "DPO_INTERFACE_SCHEMA"
35
+ INTERFACE_PARTIAL = "DPO_INTERFACE_PARTIAL"
36
+ INTERFACE_NON_INJECTIVE = "DPO_INTERFACE_NON_INJECTIVE"
37
+ INTERFACE_INCIDENCE = "DPO_INTERFACE_INCIDENCE"
38
+ INTERFACE_LABEL = "DPO_INTERFACE_LABEL"
39
+ MATCH_PARTIAL = "DPO_MATCH_PARTIAL"
40
+ IDENTIFICATION = "DPO_IDENTIFICATION_CONDITION"
41
+ MATCH_OUTSIDE_HOST = "DPO_MATCH_OUTSIDE_HOST"
42
+ MATCH_INCIDENCE = "DPO_MATCH_INCIDENCE"
43
+ MATCH_LABEL = "DPO_MATCH_LABEL"
44
+ DANGLING = "DPO_DANGLING_CONDITION"
45
+ RESULT_LABEL = "DPO_RESULT_LABEL_CONFLICT"
46
+ CLOSED_MATERIAL = "DPO_CLOSED_MATERIAL_IMBALANCE"
47
+ CLOSED_ELECTRON = "DPO_CLOSED_ELECTRON_IMBALANCE"
48
+ OPEN_TOKEN_REQUIRED = "DPO_OPEN_TOKEN_REQUIRED"
49
+ OPEN_TOKEN_MISMATCH = "DPO_OPEN_TOKEN_MISMATCH"
50
+ ELECTRON_SCHEMA_REQUIRED = "DPO_ELECTRON_SCHEMA_REQUIRED"
51
+ NONCOMMUTATIVE = "DPO_NONCOMMUTATIVE_SQUARE"
52
+ SOURCE_MUTATED = "DPO_SOURCE_MUTATED"
53
+ APPLICATION_CONDITION = "DPO_APPLICATION_CONDITION"
54
+ CLOSED_ISOTOPE = "DPO_CLOSED_ISOTOPE_IMBALANCE"
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class DPOIssue:
59
+ code: DPOIssueCode
60
+ message: str
61
+ context: Mapping[str, Any] = field(default_factory=dict)
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return {
65
+ "code": self.code.value,
66
+ "message": self.message,
67
+ "context": dict(self.context),
68
+ }
69
+
70
+
71
+ class DPOError(ValueError):
72
+ """Raised when a DPO premise or construction fails."""
73
+
74
+ def __init__(self, *issues: DPOIssue):
75
+ self.issues = tuple(issues)
76
+ super().__init__("; ".join(issue.message for issue in issues))
77
+
78
+
79
+ class SystemBoundary(str, Enum):
80
+ """Resource policy attached to a rule."""
81
+
82
+ ABSTRACT = "abstract"
83
+ CLOSED = "closed"
84
+ OPEN = "open"
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class EnvironmentToken:
89
+ """Explicit net resources supplied by an open environment.
90
+
91
+ Deltas use ``right - left`` convention. Positive values enter the graph
92
+ system; negative values leave it.
93
+ """
94
+
95
+ name: str
96
+ element_delta: tuple[tuple[str, int], ...] = ()
97
+ electron_delta: float = 0.0
98
+ isotope_delta: tuple[tuple[tuple[str, int], int], ...] = ()
99
+
100
+ def __post_init__(self) -> None:
101
+ if not self.name.strip():
102
+ raise ValueError("An environment token requires a non-empty name.")
103
+ normalized = tuple(
104
+ sorted(
105
+ (str(element), int(count))
106
+ for element, count in dict(self.element_delta).items()
107
+ if int(count) != 0
108
+ )
109
+ )
110
+ object.__setattr__(self, "element_delta", normalized)
111
+ object.__setattr__(self, "electron_delta", float(self.electron_delta))
112
+ object.__setattr__(
113
+ self, "isotope_delta", _normalize_isotope_delta(self.isotope_delta)
114
+ )
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class RuleEmbedding:
119
+ """An embedding preserving identity and every defined interface coordinate."""
120
+
121
+ interface: LewisLabelledGraph
122
+ endpoint: LewisLabelledGraph
123
+ f: tuple[tuple[Hashable, Hashable], ...]
124
+
125
+ def __post_init__(self) -> None:
126
+ pairs = tuple(self.f.items()) if isinstance(self.f, Mapping) else tuple(self.f)
127
+ object.__setattr__(self, "f", tuple(sorted(pairs, key=repr)))
128
+ if (
129
+ self.interface.schema != self.endpoint.schema
130
+ or not self.interface.partial
131
+ or self.endpoint.partial
132
+ ):
133
+ raise DPOError(
134
+ DPOIssue(
135
+ DPOIssueCode.INTERFACE_SCHEMA,
136
+ "The rule interface must be a partial pattern under the endpoint schema.",
137
+ )
138
+ )
139
+ source_keys = tuple(left for left, _ in pairs)
140
+ target_values = tuple(right for _, right in pairs)
141
+ missing = set(self.interface.node_ids) - set(source_keys)
142
+ outside = set(target_values) - set(self.endpoint.node_ids)
143
+ if (
144
+ len(set(source_keys)) != len(source_keys)
145
+ or missing
146
+ or outside
147
+ or set(source_keys) != set(self.interface.node_ids)
148
+ ):
149
+ raise DPOError(
150
+ DPOIssue(
151
+ DPOIssueCode.INTERFACE_PARTIAL,
152
+ "An interface arm is total and lands in its endpoint.",
153
+ {
154
+ "missing": tuple(sorted(map(repr, missing))),
155
+ "outside": tuple(sorted(map(repr, outside))),
156
+ },
157
+ )
158
+ )
159
+ if len(set(target_values)) != len(target_values):
160
+ raise DPOError(
161
+ DPOIssue(
162
+ DPOIssueCode.INTERFACE_NON_INJECTIVE,
163
+ "An interface arm must be injective.",
164
+ )
165
+ )
166
+ mapping = dict(pairs)
167
+ issues: list[DPOIssue] = []
168
+ for node, image in pairs:
169
+ interface_labels = self.interface.node_labels(node, semantic=True)
170
+ endpoint_labels = self.endpoint.node_labels(image, semantic=True)
171
+ if not set(self.endpoint.schema.node_identity) <= set(
172
+ interface_labels
173
+ ) or any(
174
+ endpoint_labels.get(key) != value
175
+ for key, value in interface_labels.items()
176
+ ):
177
+ issues.append(
178
+ DPOIssue(
179
+ DPOIssueCode.INTERFACE_LABEL,
180
+ "An interface node omits identity or does not preserve a defined coordinate.",
181
+ {"node": repr(node), "image": repr(image)},
182
+ )
183
+ )
184
+ for edge in self.interface.edge_keys:
185
+ image = frozenset(mapping[node] for node in edge)
186
+ if image not in self.endpoint.edge_keys:
187
+ issues.append(
188
+ DPOIssue(
189
+ DPOIssueCode.INTERFACE_INCIDENCE,
190
+ "An interface edge image is absent from its endpoint.",
191
+ {"edge": tuple(sorted(map(repr, edge)))},
192
+ )
193
+ )
194
+ continue
195
+ interface_labels = self.interface.edge_labels(edge, semantic=True)
196
+ endpoint_labels = self.endpoint.edge_labels(image, semantic=True)
197
+ if not set(self.endpoint.schema.edge_identity) <= set(
198
+ interface_labels
199
+ ) or any(
200
+ endpoint_labels.get(key) != value
201
+ for key, value in interface_labels.items()
202
+ ):
203
+ issues.append(
204
+ DPOIssue(
205
+ DPOIssueCode.INTERFACE_LABEL,
206
+ "An interface edge omits identity or does not preserve a defined coordinate.",
207
+ {"edge": tuple(sorted(map(repr, edge)))},
208
+ )
209
+ )
210
+ if issues:
211
+ raise DPOError(*issues)
212
+
213
+ @property
214
+ def mapping(self) -> dict[Hashable, Hashable]:
215
+ return dict(self.f)
216
+
217
+ @property
218
+ def edge_mapping(self) -> dict[frozenset[Hashable], frozenset[Hashable]]:
219
+ mapping = self.mapping
220
+ return {
221
+ edge: frozenset(mapping[node] for node in edge)
222
+ for edge in self.interface.edge_keys
223
+ }
224
+
225
+
226
+ @dataclass(frozen=True)
227
+ class ResourceDelta:
228
+ """Rule-level material and electron delta using right-minus-left sign."""
229
+
230
+ element_delta: tuple[tuple[str, int], ...]
231
+ electron_delta: float
232
+ isotope_delta: tuple[tuple[tuple[str, int], int], ...] = ()
233
+
234
+
235
+ def _normalize_isotope_delta(pairs):
236
+ pairs = tuple(pairs)
237
+ for (element, mass), count in pairs:
238
+ if (
239
+ not str(element)
240
+ or isinstance(mass, bool)
241
+ or not isinstance(mass, Integral)
242
+ or mass < 0
243
+ or isinstance(count, bool)
244
+ or not isinstance(count, Integral)
245
+ ):
246
+ raise ValueError(
247
+ "Isotope resource deltas need an element, integral mass and integral count."
248
+ )
249
+ return tuple(
250
+ sorted(
251
+ ((str(element), int(mass)), int(count))
252
+ for (element, mass), count in dict(pairs).items()
253
+ if count
254
+ )
255
+ )
256
+
257
+
258
+ def _isotope_inventory(graph):
259
+ if graph.schema != ISOTOPE_ELECTRON_LLG_SCHEMA:
260
+ return Counter()
261
+ values = Counter(
262
+ (
263
+ str(graph.node_labels(node)["element"]),
264
+ int(graph.node_labels(node)["isotope"]),
265
+ )
266
+ for node in graph.node_ids
267
+ )
268
+ values[("H", 0)] += sum(int(graph.node_labels(n)["hcount"]) for n in graph.node_ids)
269
+ return values
270
+
271
+
272
+ def _resource_delta(
273
+ left: LewisLabelledGraph, right: LewisLabelledGraph
274
+ ) -> ResourceDelta:
275
+ if not is_electron_schema(left.schema) or not is_electron_schema(right.schema):
276
+ raise DPOError(
277
+ DPOIssue(
278
+ DPOIssueCode.ELECTRON_SCHEMA_REQUIRED,
279
+ "Chemical resource policies require the electron-complete LLG schema.",
280
+ )
281
+ )
282
+
283
+ def inventory(graph: LewisLabelledGraph) -> tuple[Counter[str], float]:
284
+ elements: Counter[str] = Counter()
285
+ electrons = 0.0
286
+ for node in graph.node_ids:
287
+ labels = graph.node_labels(node)
288
+ elements[str(labels["element"])] += 1
289
+ hydrogen_count = float(labels["hcount"])
290
+ if not hydrogen_count.is_integer() or hydrogen_count < 0:
291
+ raise DPOError(
292
+ DPOIssue(
293
+ DPOIssueCode.CLOSED_MATERIAL,
294
+ "Unexpanded hydrogen inventory must be a nonnegative integer.",
295
+ )
296
+ )
297
+ elements["H"] += int(hydrogen_count)
298
+ electrons += (
299
+ float(labels["valence_electrons"])
300
+ + float(labels["hcount"])
301
+ - float(labels["charge"])
302
+ )
303
+ return elements, electrons
304
+
305
+ left_elements, left_electrons = inventory(left)
306
+ right_elements, right_electrons = inventory(right)
307
+ keys = sorted(set(left_elements) | set(right_elements))
308
+ element_delta = tuple(
309
+ (key, right_elements[key] - left_elements[key])
310
+ for key in keys
311
+ if right_elements[key] != left_elements[key]
312
+ )
313
+ isotope_delta = _isotope_inventory(right)
314
+ isotope_delta.subtract(_isotope_inventory(left))
315
+ return ResourceDelta(
316
+ element_delta,
317
+ right_electrons - left_electrons,
318
+ _normalize_isotope_delta(isotope_delta.items()),
319
+ )
320
+
321
+
322
+ def _closed_boundary_issues(delta, environment):
323
+ issues = []
324
+ if delta.isotope_delta:
325
+ issues.append(
326
+ DPOIssue(
327
+ DPOIssueCode.CLOSED_ISOTOPE,
328
+ "A closed rule changes the isotope-resolved material inventory.",
329
+ {"delta": delta.isotope_delta},
330
+ )
331
+ )
332
+ if delta.element_delta:
333
+ issues.append(
334
+ DPOIssue(
335
+ DPOIssueCode.CLOSED_MATERIAL,
336
+ "A closed rule changes the elemental inventory.",
337
+ {"delta": delta.element_delta},
338
+ )
339
+ )
340
+ if delta.electron_delta != 0:
341
+ issues.append(
342
+ DPOIssue(
343
+ DPOIssueCode.CLOSED_ELECTRON,
344
+ "A closed rule changes the electron inventory.",
345
+ {"delta": delta.electron_delta},
346
+ )
347
+ )
348
+ if environment is not None:
349
+ issues.append(
350
+ DPOIssue(
351
+ DPOIssueCode.OPEN_TOKEN_MISMATCH,
352
+ "A closed rule cannot carry an environment token.",
353
+ )
354
+ )
355
+ return issues
356
+
357
+
358
+ @dataclass(frozen=True)
359
+ class RuleSpan:
360
+ """A partially labelled DPO rule ``L <- K -> R``."""
361
+
362
+ left: LewisLabelledGraph
363
+ interface: LewisLabelledGraph
364
+ right: LewisLabelledGraph
365
+ left_arm: RuleEmbedding
366
+ right_arm: RuleEmbedding
367
+ boundary: SystemBoundary = SystemBoundary.ABSTRACT
368
+ environment: EnvironmentToken | None = None
369
+ name: str = "rule"
370
+ condition: Any = None
371
+
372
+ def __post_init__(self) -> None:
373
+ if self.left.partial or self.right.partial:
374
+ raise DPOError(
375
+ DPOIssue(
376
+ DPOIssueCode.INTERFACE_SCHEMA,
377
+ "Rule endpoints must have total semantic labels.",
378
+ )
379
+ )
380
+ if self.left.schema != self.right.schema:
381
+ raise DPOError(
382
+ DPOIssue(
383
+ DPOIssueCode.SCHEMA_MISMATCH,
384
+ "Rule endpoints must use the same LLG schema.",
385
+ )
386
+ )
387
+ if (
388
+ self.left_arm.interface != self.interface
389
+ or self.right_arm.interface != self.interface
390
+ or self.left_arm.endpoint != self.left
391
+ or self.right_arm.endpoint != self.right
392
+ ):
393
+ raise DPOError(
394
+ DPOIssue(
395
+ DPOIssueCode.INTERFACE_SCHEMA,
396
+ "Rule arms must have the declared interface and endpoints.",
397
+ )
398
+ )
399
+ boundary = SystemBoundary(self.boundary)
400
+ object.__setattr__(self, "boundary", boundary)
401
+ if boundary is SystemBoundary.ABSTRACT:
402
+ if self.environment is not None:
403
+ raise DPOError(
404
+ DPOIssue(
405
+ DPOIssueCode.OPEN_TOKEN_MISMATCH,
406
+ "Abstract rules do not consume chemical environment tokens.",
407
+ )
408
+ )
409
+ return
410
+ delta = _resource_delta(self.left, self.right)
411
+ if boundary is SystemBoundary.CLOSED:
412
+ issues = _closed_boundary_issues(delta, self.environment)
413
+ if issues:
414
+ raise DPOError(*issues)
415
+ return
416
+ if self.environment is None:
417
+ raise DPOError(
418
+ DPOIssue(
419
+ DPOIssueCode.OPEN_TOKEN_REQUIRED,
420
+ "An open rule requires an explicit environment token.",
421
+ )
422
+ )
423
+ if (
424
+ self.environment.element_delta != delta.element_delta
425
+ or self.environment.electron_delta != delta.electron_delta
426
+ or self.environment.isotope_delta != delta.isotope_delta
427
+ ):
428
+ raise DPOError(
429
+ DPOIssue(
430
+ DPOIssueCode.OPEN_TOKEN_MISMATCH,
431
+ "The environment token does not equal the rule resource delta.",
432
+ {
433
+ "expected_elements": delta.element_delta,
434
+ "observed_elements": self.environment.element_delta,
435
+ "expected_electrons": delta.electron_delta,
436
+ "observed_electrons": self.environment.electron_delta,
437
+ "expected_isotopes": delta.isotope_delta,
438
+ "observed_isotopes": self.environment.isotope_delta,
439
+ },
440
+ )
441
+ )
442
+
443
+ @classmethod
444
+ def from_mapping(
445
+ cls,
446
+ left: LewisLabelledGraph,
447
+ right: LewisLabelledGraph,
448
+ preserved_nodes: Mapping[Hashable, Hashable],
449
+ *,
450
+ preserved_edges: set[frozenset[Hashable]] | None = None,
451
+ boundary: SystemBoundary | str = SystemBoundary.ABSTRACT,
452
+ environment: EnvironmentToken | None = None,
453
+ name: str = "rule",
454
+ ) -> "RuleSpan":
455
+ """Build an explicit interface from a preserved endpoint mapping.
456
+
457
+ ``preserved_edges`` contains left-side edge keys. When omitted, every
458
+ left edge whose mapped right edge exists is structurally preserved.
459
+ """
460
+ if left.schema != right.schema:
461
+ raise DPOError(
462
+ DPOIssue(
463
+ DPOIssueCode.SCHEMA_MISMATCH,
464
+ "Rule endpoints must use the same LLG schema.",
465
+ )
466
+ )
467
+ pairs = tuple(sorted(preserved_nodes.items(), key=repr))
468
+ left_nodes = tuple(node for node, _ in pairs)
469
+ if len(set(left_nodes)) != len(left_nodes):
470
+ raise DPOError(
471
+ DPOIssue(
472
+ DPOIssueCode.INTERFACE_PARTIAL,
473
+ "Each preserved left node occurs once.",
474
+ )
475
+ )
476
+ interface_schema = left.schema
477
+ interface_graph = nx.Graph()
478
+ for interface_node, (left_node, right_node) in enumerate(pairs):
479
+ if left_node not in left.node_ids or right_node not in right.node_ids:
480
+ raise DPOError(
481
+ DPOIssue(
482
+ DPOIssueCode.INTERFACE_PARTIAL,
483
+ "A preserved node must exist in both endpoints.",
484
+ )
485
+ )
486
+ left_identity = {
487
+ key: left.node_labels(left_node, semantic=True)[key]
488
+ for key in left.schema.node_identity
489
+ }
490
+ right_identity = {
491
+ key: right.node_labels(right_node, semantic=True)[key]
492
+ for key in right.schema.node_identity
493
+ }
494
+ if left_identity != right_identity:
495
+ raise DPOError(
496
+ DPOIssue(
497
+ DPOIssueCode.INTERFACE_LABEL,
498
+ "Preserved nodes disagree on identity labels.",
499
+ {"left": repr(left_node), "right": repr(right_node)},
500
+ )
501
+ )
502
+ left_values = left.node_labels(left_node, semantic=True)
503
+ right_values = right.node_labels(right_node, semantic=True)
504
+ common = {
505
+ key: value
506
+ for key, value in left_values.items()
507
+ if key in right_values and right_values[key] == value
508
+ }
509
+ interface_graph.add_node(interface_node, **common)
510
+
511
+ left_to_interface = {
512
+ left_node: interface_node
513
+ for interface_node, (left_node, _) in enumerate(pairs)
514
+ }
515
+ right_by_left = dict(pairs)
516
+ candidates = {
517
+ edge
518
+ for edge in left.edge_keys
519
+ if edge <= set(left_to_interface)
520
+ and frozenset(right_by_left[node] for node in edge) in right.edge_keys
521
+ }
522
+ selected = candidates if preserved_edges is None else set(preserved_edges)
523
+ if not selected <= candidates:
524
+ raise DPOError(
525
+ DPOIssue(
526
+ DPOIssueCode.INTERFACE_INCIDENCE,
527
+ "A preserved edge must exist under both endpoint arms.",
528
+ )
529
+ )
530
+ for edge in selected:
531
+ left_a, left_b = tuple(edge)
532
+ right_edge = frozenset((right_by_left[left_a], right_by_left[left_b]))
533
+ left_identity = {
534
+ key: left.edge_labels(edge, semantic=True)[key]
535
+ for key in left.schema.edge_identity
536
+ }
537
+ right_identity = {
538
+ key: right.edge_labels(right_edge, semantic=True)[key]
539
+ for key in right.schema.edge_identity
540
+ }
541
+ if left_identity != right_identity:
542
+ raise DPOError(
543
+ DPOIssue(
544
+ DPOIssueCode.INTERFACE_LABEL,
545
+ "Preserved edges disagree on identity labels.",
546
+ )
547
+ )
548
+ left_values = left.edge_labels(edge, semantic=True)
549
+ right_values = right.edge_labels(right_edge, semantic=True)
550
+ common = {
551
+ key: value
552
+ for key, value in left_values.items()
553
+ if key in right_values and right_values[key] == value
554
+ }
555
+ interface_graph.add_edge(
556
+ left_to_interface[left_a], left_to_interface[left_b], **common
557
+ )
558
+
559
+ interface = LewisLabelledGraph.from_networkx(
560
+ interface_graph, interface_schema, name=f"{name}:K", partial=True
561
+ )
562
+ left_map = {
563
+ interface_node: left_node
564
+ for interface_node, (left_node, _) in enumerate(pairs)
565
+ }
566
+ right_map = {
567
+ interface_node: right_node
568
+ for interface_node, (_, right_node) in enumerate(pairs)
569
+ }
570
+ return cls(
571
+ left,
572
+ interface,
573
+ right,
574
+ RuleEmbedding(interface, left, left_map),
575
+ RuleEmbedding(interface, right, right_map),
576
+ SystemBoundary(boundary),
577
+ environment,
578
+ name,
579
+ )
580
+
581
+ @property
582
+ def resource_delta(self) -> ResourceDelta | None:
583
+ if self.boundary is SystemBoundary.ABSTRACT:
584
+ return None
585
+ return _resource_delta(self.left, self.right)
586
+
587
+
588
+ @dataclass(frozen=True)
589
+ class DPOReplay:
590
+ valid: bool
591
+ issues: tuple[DPOIssue, ...] = ()
592
+
593
+
594
+ @dataclass(frozen=True)
595
+ class DPOCertificate:
596
+ """Complete carrier-level certificate for the two DPO squares."""
597
+
598
+ rule: RuleSpan
599
+ host: LewisLabelledGraph
600
+ context: LewisLabelledGraph
601
+ result: LewisLabelledGraph
602
+ match: tuple[tuple[Hashable, Hashable], ...]
603
+ interface_to_context: tuple[tuple[Hashable, Hashable], ...]
604
+ context_to_host: tuple[tuple[Hashable, Hashable], ...]
605
+ context_to_result: tuple[tuple[Hashable, Hashable], ...]
606
+ right_to_result: tuple[tuple[Hashable, Hashable], ...]
607
+ deleted_nodes: tuple[Hashable, ...]
608
+ deleted_edges: tuple[frozenset[Hashable], ...]
609
+ added_nodes: tuple[Hashable, ...]
610
+ added_edges: tuple[frozenset[Hashable], ...]
611
+ resource_delta: ResourceDelta | None
612
+
613
+ def replay(self) -> DPOReplay:
614
+ from .validation import validate_certificate
615
+
616
+ issues = validate_certificate(self)
617
+ return DPOReplay(not issues, tuple(issues))
618
+
619
+
620
+ @dataclass(frozen=True)
621
+ class DPOApplication:
622
+ result: LewisLabelledGraph
623
+ certificate: DPOCertificate
624
+
625
+
626
+ def _translate_match_error(error: LLGError) -> DPOError:
627
+ translated = []
628
+ for issue in error.issues:
629
+ if issue.code is LLGIssueCode.PARTIAL_MAPPING:
630
+ code = DPOIssueCode.MATCH_PARTIAL
631
+ elif issue.code is LLGIssueCode.NON_INJECTIVE:
632
+ code = DPOIssueCode.IDENTIFICATION
633
+ elif issue.code in {LLGIssueCode.OUTSIDE_SOURCE, LLGIssueCode.OUTSIDE_TARGET}:
634
+ code = DPOIssueCode.MATCH_OUTSIDE_HOST
635
+ elif issue.code is LLGIssueCode.MISSING_EDGE:
636
+ code = DPOIssueCode.MATCH_INCIDENCE
637
+ else:
638
+ code = DPOIssueCode.MATCH_LABEL
639
+ translated.append(DPOIssue(code, issue.message, issue.context))
640
+ return DPOError(*translated)
641
+
642
+
643
+ def _map_edge(
644
+ mapping: Mapping[Hashable, Hashable],
645
+ edge: frozenset[Hashable] | None,
646
+ ) -> frozenset[Hashable] | None:
647
+ if edge is None or any(node not in mapping for node in edge):
648
+ return None
649
+ return frozenset(mapping[node] for node in edge)
650
+
651
+
652
+ def _fresh_nodes(existing: set[Hashable], count: int) -> tuple[tuple[str, int], ...]:
653
+ fresh = []
654
+ index = 0
655
+ while len(fresh) < count:
656
+ candidate = ("__dpo_fresh__", index)
657
+ index += 1
658
+ if candidate not in existing:
659
+ fresh.append(candidate)
660
+ existing.add(candidate)
661
+ return tuple(fresh)
662
+
663
+
664
+ @dataclass(frozen=True)
665
+ class _DeletionPlan:
666
+ nodes: frozenset[Hashable]
667
+ edges: frozenset[frozenset[Hashable]]
668
+
669
+
670
+ @dataclass
671
+ class _ResultPlan:
672
+ graph: nx.Graph
673
+ interface_to_context: dict[Hashable, Hashable]
674
+ right_to_result: dict[Hashable, Hashable]
675
+ added_nodes: tuple[Hashable, ...]
676
+ added_edges: set[frozenset[Hashable]]
677
+
678
+
679
+ def _validated_match(
680
+ rule: RuleSpan,
681
+ host: LewisLabelledGraph,
682
+ match: Mapping[Hashable, Hashable],
683
+ ) -> LLGMorphism:
684
+ if rule.left.schema != host.schema:
685
+ raise DPOError(
686
+ DPOIssue(
687
+ DPOIssueCode.SCHEMA_MISMATCH,
688
+ "The rule left side and host must use the same schema.",
689
+ )
690
+ )
691
+ try:
692
+ return LLGMorphism(rule.left, host, tuple(match.items()))
693
+ except LLGError as error:
694
+ raise _translate_match_error(error) from error
695
+
696
+
697
+ def _deletion_plan(
698
+ rule: RuleSpan,
699
+ host: LewisLabelledGraph,
700
+ match: Mapping[Hashable, Hashable],
701
+ ) -> _DeletionPlan:
702
+ preserved_nodes = set(rule.left_arm.mapping.values())
703
+ deleted_nodes = frozenset(
704
+ match[node] for node in set(rule.left.node_ids) - preserved_nodes
705
+ )
706
+ preserved_edges = set(rule.left_arm.edge_mapping.values())
707
+ deleted_edges = frozenset(
708
+ frozenset(match[node] for node in edge)
709
+ for edge in set(rule.left.edge_keys) - preserved_edges
710
+ )
711
+ matched_edges = {
712
+ frozenset(match[node] for node in edge) for edge in rule.left.edge_keys
713
+ }
714
+ dangling = {
715
+ edge
716
+ for edge in host.edge_keys
717
+ if edge & deleted_nodes and edge not in matched_edges
718
+ }
719
+ if dangling:
720
+ raise DPOError(
721
+ DPOIssue(
722
+ DPOIssueCode.DANGLING,
723
+ "Deleting a matched node would leave an incident host edge dangling.",
724
+ {
725
+ "edges": tuple(
726
+ sorted(
727
+ (tuple(sorted(map(repr, edge))) for edge in dangling),
728
+ key=repr,
729
+ )
730
+ )
731
+ },
732
+ )
733
+ )
734
+ return _DeletionPlan(deleted_nodes, deleted_edges)
735
+
736
+
737
+ def _construct_context(
738
+ rule: RuleSpan,
739
+ host: LewisLabelledGraph,
740
+ deletion: _DeletionPlan,
741
+ match: Mapping[Hashable, Hashable],
742
+ ) -> LewisLabelledGraph:
743
+ graph = host.to_networkx()
744
+ graph.remove_edges_from(tuple(tuple(edge) for edge in deletion.edges))
745
+ graph.remove_nodes_from(deletion.nodes)
746
+ # Naturality requires erasing precisely the coordinates undefined in K.
747
+ for node in rule.interface.node_ids:
748
+ image = match[rule.left_arm.mapping[node]]
749
+ labels = rule.interface.node_labels(node, semantic=True)
750
+ for key in host.schema.semantic_node_keys:
751
+ if key not in labels:
752
+ graph.nodes[image].pop(key, None)
753
+ for edge in rule.interface.edge_keys:
754
+ image = _map_edge(match, rule.left_arm.edge_mapping[edge])
755
+ left, right = tuple(image)
756
+ labels = rule.interface.edge_labels(edge, semantic=True)
757
+ for key in host.schema.semantic_edge_keys:
758
+ if key not in labels:
759
+ graph.edges[left, right].pop(key, None)
760
+ # Derived chemistry is meaningful on complete host states, not on D.
761
+ for _, labels in graph.nodes(data=True):
762
+ for key in host.schema.node_derived:
763
+ labels.pop(key, None)
764
+ for _, _, labels in graph.edges(data=True):
765
+ for key in host.schema.edge_derived:
766
+ labels.pop(key, None)
767
+ return LewisLabelledGraph.from_networkx(
768
+ graph, host.schema, name=f"{rule.name}:D", partial=True
769
+ )
770
+
771
+
772
+ def _initialize_result(
773
+ rule: RuleSpan,
774
+ context: LewisLabelledGraph,
775
+ match: Mapping[Hashable, Hashable],
776
+ ) -> _ResultPlan:
777
+ graph = context.to_networkx()
778
+ left_arm = rule.left_arm.mapping
779
+ right_arm = rule.right_arm.mapping
780
+ interface_to_context: dict[Hashable, Hashable] = {}
781
+ right_to_result: dict[Hashable, Hashable] = {}
782
+ for interface_node in rule.interface.node_ids:
783
+ host_node = match[left_arm[interface_node]]
784
+ interface_to_context[interface_node] = host_node
785
+ right_to_result[right_arm[interface_node]] = host_node
786
+
787
+ added_right = sorted(set(rule.right.node_ids) - set(right_arm.values()), key=repr)
788
+ fresh = _fresh_nodes(set(graph.nodes), len(added_right))
789
+ for right_node, result_node in zip(added_right, fresh):
790
+ right_to_result[right_node] = result_node
791
+ graph.add_node(result_node, **rule.right.node_labels(right_node))
792
+ return _ResultPlan(
793
+ graph,
794
+ interface_to_context,
795
+ right_to_result,
796
+ tuple(fresh),
797
+ set(),
798
+ )
799
+
800
+
801
+ def _update_preserved_values(rule: RuleSpan, plan: _ResultPlan) -> None:
802
+ right_arm = rule.right_arm.mapping
803
+ node_keys = rule.right.schema.semantic_node_keys + rule.right.schema.node_derived
804
+ for interface_node in rule.interface.node_ids:
805
+ right_node = right_arm[interface_node]
806
+ result_node = plan.right_to_result[right_node]
807
+ right_labels = rule.right.node_labels(right_node)
808
+ for key in node_keys:
809
+ if key in right_labels:
810
+ plan.graph.nodes[result_node][key] = right_labels[key]
811
+
812
+ edge_keys = rule.right.schema.semantic_edge_keys + rule.right.schema.edge_derived
813
+ right_edges = rule.right_arm.edge_mapping
814
+ for interface_edge in rule.interface.edge_keys:
815
+ right_edge = right_edges[interface_edge]
816
+ result_edge = frozenset(plan.right_to_result[node] for node in right_edge)
817
+ right_labels = rule.right.edge_labels(right_edge)
818
+ left, right = tuple(result_edge)
819
+ for key in edge_keys:
820
+ if key in right_labels:
821
+ plan.graph.edges[left, right][key] = right_labels[key]
822
+
823
+
824
+ def _add_right_edges(rule: RuleSpan, plan: _ResultPlan) -> None:
825
+ preserved = set(rule.right_arm.edge_mapping.values())
826
+ for right_edge in set(rule.right.edge_keys) - preserved:
827
+ result_edge = frozenset(plan.right_to_result[node] for node in right_edge)
828
+ left, right = tuple(result_edge)
829
+ if plan.graph.has_edge(left, right):
830
+ raise DPOError(
831
+ DPOIssue(
832
+ DPOIssueCode.RESULT_LABEL,
833
+ "An added rule edge collides with an unmatched host edge.",
834
+ {"edge": tuple(sorted(map(repr, result_edge)))},
835
+ )
836
+ )
837
+ plan.graph.add_edge(left, right, **rule.right.edge_labels(right_edge))
838
+ plan.added_edges.add(result_edge)
839
+
840
+
841
+ def _coherent_result(
842
+ rule: RuleSpan,
843
+ host: LewisLabelledGraph,
844
+ plan: _ResultPlan,
845
+ ) -> LewisLabelledGraph:
846
+ graph = plan.graph
847
+ if is_electron_schema(host.schema):
848
+ for _, attrs in graph.nodes(data=True):
849
+ for key in host.schema.node_derived:
850
+ attrs.pop(key, None)
851
+ for _, _, attrs in graph.edges(data=True):
852
+ for key in host.schema.edge_derived:
853
+ attrs.pop(key, None)
854
+ try:
855
+ graph = derive_electron_labeled_graph(graph)
856
+ except LLGError as error:
857
+ raise DPOError(
858
+ DPOIssue(
859
+ DPOIssueCode.RESULT_LABEL,
860
+ "The rewritten electron state is not coherent.",
861
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
862
+ )
863
+ ) from error
864
+ try:
865
+ return LewisLabelledGraph.from_networkx(
866
+ graph, host.schema, name=f"{rule.name}:H"
867
+ )
868
+ except LLGError as error:
869
+ raise DPOError(
870
+ DPOIssue(
871
+ DPOIssueCode.RESULT_LABEL,
872
+ "The rewritten graph is outside the LLG object class.",
873
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
874
+ )
875
+ ) from error
876
+
877
+
878
+ def _assert_unmutated(
879
+ host: LewisLabelledGraph,
880
+ rule: RuleSpan,
881
+ before: tuple[nx.Graph, nx.Graph, nx.Graph],
882
+ ) -> None:
883
+ observed = (host.to_networkx(), rule.left.to_networkx(), rule.right.to_networkx())
884
+ if any(
885
+ not nx.utils.graphs_equal(current, original)
886
+ for current, original in zip(observed, before)
887
+ ):
888
+ raise DPOError(
889
+ DPOIssue(
890
+ DPOIssueCode.SOURCE_MUTATED,
891
+ "DPO application mutated a source object.",
892
+ )
893
+ )
894
+
895
+
896
+ def apply_dpo(
897
+ rule: RuleSpan,
898
+ host: LewisLabelledGraph,
899
+ match: Mapping[Hashable, Hashable],
900
+ ) -> DPOApplication:
901
+ """Apply a rule at one explicit match or raise a typed DPO refusal."""
902
+ match_morphism = _validated_match(rule, host, match)
903
+ before = (host.to_networkx(), rule.left.to_networkx(), rule.right.to_networkx())
904
+ match_map = match_morphism.mapping
905
+ if rule.condition is not None:
906
+ rule.condition.validate(host, match_map)
907
+ deletion = _deletion_plan(rule, host, match_map)
908
+ context = _construct_context(rule, host, deletion, match_map)
909
+ plan = _initialize_result(rule, context, match_map)
910
+ _update_preserved_values(rule, plan)
911
+ _add_right_edges(rule, plan)
912
+ result = _coherent_result(rule, host, plan)
913
+ _assert_unmutated(host, rule, before)
914
+
915
+ context_identity = tuple((node, node) for node in context.node_ids)
916
+ certificate = DPOCertificate(
917
+ rule=rule,
918
+ host=host,
919
+ context=context,
920
+ result=result,
921
+ match=tuple(sorted(match_map.items(), key=repr)),
922
+ interface_to_context=tuple(sorted(plan.interface_to_context.items(), key=repr)),
923
+ context_to_host=context_identity,
924
+ context_to_result=context_identity,
925
+ right_to_result=tuple(sorted(plan.right_to_result.items(), key=repr)),
926
+ deleted_nodes=tuple(sorted(deletion.nodes, key=repr)),
927
+ deleted_edges=tuple(sorted(deletion.edges, key=repr)),
928
+ added_nodes=plan.added_nodes,
929
+ added_edges=tuple(sorted(plan.added_edges, key=repr)),
930
+ resource_delta=rule.resource_delta,
931
+ )
932
+ replay = certificate.replay()
933
+ if not replay.valid:
934
+ raise DPOError(*replay.issues)
935
+ return DPOApplication(result, certificate)
936
+
937
+
938
+ def rule_from_its(
939
+ its: nx.Graph,
940
+ *,
941
+ electron_complete: bool = False,
942
+ boundary: SystemBoundary | str = SystemBoundary.ABSTRACT,
943
+ environment: EnvironmentToken | None = None,
944
+ name: str = "its-rule",
945
+ ) -> RuleSpan:
946
+ """Adapt an ITS through two endpoint LLGs and an explicit carrier map."""
947
+ left = llg_from_its(its, "reactant", electron_complete=electron_complete)
948
+ right = llg_from_its(its, "product", electron_complete=electron_complete)
949
+ shared = set(left.node_ids) & set(right.node_ids)
950
+ return RuleSpan.from_mapping(
951
+ left,
952
+ right,
953
+ {node: node for node in shared},
954
+ boundary=boundary,
955
+ environment=environment,
956
+ name=name,
957
+ )
958
+
959
+
960
+ def rule_from_synrule(
961
+ rule: Any,
962
+ *,
963
+ electron_complete: bool = False,
964
+ boundary: SystemBoundary | str = SystemBoundary.ABSTRACT,
965
+ environment: EnvironmentToken | None = None,
966
+ ) -> RuleSpan:
967
+ """Adapt ``SynRule`` endpoints without treating paired ITS as a rule span."""
968
+ from synkit.Graph.ITS.its_construction import ITSConstruction
969
+ from synkit.Rule.syn_rule import SynRule
970
+
971
+ if not isinstance(rule, SynRule):
972
+ raise TypeError("rule_from_synrule expects a SynRule instance.")
973
+ its = ITSConstruction.construct(rule.left.raw, rule.right.raw)
974
+ return rule_from_its(
975
+ its,
976
+ electron_complete=electron_complete,
977
+ boundary=boundary,
978
+ environment=environment,
979
+ name=getattr(rule, "_name", "rule"),
980
+ )
981
+
982
+
983
+ __all__ = [
984
+ "DPOApplication",
985
+ "DPOCertificate",
986
+ "DPOError",
987
+ "DPOIssue",
988
+ "DPOIssueCode",
989
+ "DPOReplay",
990
+ "EnvironmentToken",
991
+ "ResourceDelta",
992
+ "RuleEmbedding",
993
+ "RuleSpan",
994
+ "SystemBoundary",
995
+ "apply_dpo",
996
+ "rule_from_its",
997
+ "rule_from_synrule",
998
+ ]