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,976 @@
1
+ """Binary DPO rule composition along one explicit certified overlap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass, field, replace
7
+ from enum import Enum
8
+ from typing import Any, Hashable, Mapping
9
+
10
+ import networkx as nx
11
+
12
+ from synkit.Graph.Morphism import (
13
+ ELECTRON_LLG_SCHEMA,
14
+ LLGError,
15
+ LLGMorphism,
16
+ LewisLabelledGraph,
17
+ derive_electron_labeled_graph,
18
+ is_electron_schema,
19
+ )
20
+ from synkit.Rule.Apply import (
21
+ DPOApplication,
22
+ DPOError,
23
+ EnvironmentToken,
24
+ RuleEmbedding,
25
+ RuleSpan,
26
+ SystemBoundary,
27
+ apply_dpo,
28
+ )
29
+ from synkit.Rule.Apply.conditions import SequentialCondition
30
+
31
+
32
+ class CompositionIssueCode(str, Enum):
33
+ """Stable failures for explicit binary rule composition."""
34
+
35
+ SCHEMA_MISMATCH = "COMPOSITION_SCHEMA_MISMATCH"
36
+ OVERLAP_PARTIAL = "COMPOSITION_OVERLAP_PARTIAL"
37
+ OVERLAP_NON_INJECTIVE = "COMPOSITION_OVERLAP_NON_INJECTIVE"
38
+ OVERLAP_NODE_LABEL = "COMPOSITION_OVERLAP_NODE_LABEL"
39
+ OVERLAP_EDGE_LABEL = "COMPOSITION_OVERLAP_EDGE_LABEL"
40
+ OVERLAP_EDGE_CLOSURE = "COMPOSITION_OVERLAP_EDGE_CLOSURE"
41
+ PULLBACK_COMPLEMENT = "COMPOSITION_PULLBACK_COMPLEMENT"
42
+ PARALLEL_EDGE_CONFLICT = "COMPOSITION_PARALLEL_EDGE_CONFLICT"
43
+ FIRST_APPLICATION = "COMPOSITION_FIRST_APPLICATION"
44
+ SECOND_APPLICATION = "COMPOSITION_SECOND_APPLICATION"
45
+ RESOURCE_MISMATCH = "COMPOSITION_RESOURCE_MISMATCH"
46
+ OUTER_REPLAY = "COMPOSITION_OUTER_REPLAY"
47
+ NONCOMMUTATIVE = "COMPOSITION_NONCOMMUTATIVE"
48
+ INCOMPLETE_PROVENANCE = "COMPOSITION_INCOMPLETE_PROVENANCE"
49
+ SOURCE_MUTATED = "COMPOSITION_SOURCE_MUTATED"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class CompositionIssue:
54
+ code: CompositionIssueCode
55
+ message: str
56
+ context: Mapping[str, Any] = field(default_factory=dict)
57
+
58
+ def to_dict(self) -> dict[str, Any]:
59
+ return {
60
+ "code": self.code.value,
61
+ "message": self.message,
62
+ "context": dict(self.context),
63
+ }
64
+
65
+
66
+ class CompositionError(ValueError):
67
+ """Raised when the declared overlap has no admitted composite."""
68
+
69
+ def __init__(self, *issues: CompositionIssue):
70
+ self.issues = tuple(issues)
71
+ super().__init__("; ".join(issue.message for issue in issues))
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class RuleOverlap:
76
+ """An explicit overlap span ``R1 <- D -> L2``."""
77
+
78
+ interface: LewisLabelledGraph
79
+ first_arm: LLGMorphism
80
+ second_arm: LLGMorphism
81
+
82
+ def __post_init__(self) -> None:
83
+ if (
84
+ self.first_arm.source != self.interface
85
+ or self.second_arm.source != self.interface
86
+ ):
87
+ raise CompositionError(
88
+ CompositionIssue(
89
+ CompositionIssueCode.OVERLAP_PARTIAL,
90
+ "Both overlap arms must start at the declared interface.",
91
+ )
92
+ )
93
+ if self.first_arm.target.schema != self.second_arm.target.schema:
94
+ raise CompositionError(
95
+ CompositionIssue(
96
+ CompositionIssueCode.SCHEMA_MISMATCH,
97
+ "Overlap endpoints must use the same LLG schema.",
98
+ )
99
+ )
100
+
101
+ @classmethod
102
+ def from_mapping(
103
+ cls,
104
+ first_right: LewisLabelledGraph,
105
+ second_left: LewisLabelledGraph,
106
+ mapping: Mapping[Hashable, Hashable],
107
+ *,
108
+ overlap_edges: set[frozenset[Hashable]] | None = None,
109
+ name: str = "overlap",
110
+ ) -> "RuleOverlap":
111
+ """Create an overlap; edge keys, when supplied, belong to ``R1``."""
112
+ if first_right.schema != second_left.schema:
113
+ raise CompositionError(
114
+ CompositionIssue(
115
+ CompositionIssueCode.SCHEMA_MISMATCH,
116
+ "Overlap endpoints must use the same LLG schema.",
117
+ )
118
+ )
119
+ pairs = tuple(sorted(mapping.items(), key=repr))
120
+ first_nodes = tuple(left for left, _ in pairs)
121
+ second_nodes = tuple(right for _, right in pairs)
122
+ if len(set(first_nodes)) != len(first_nodes) or (
123
+ set(first_nodes) - set(first_right.node_ids)
124
+ ):
125
+ raise CompositionError(
126
+ CompositionIssue(
127
+ CompositionIssueCode.OVERLAP_PARTIAL,
128
+ "Every overlap source node must occur once in R1.",
129
+ )
130
+ )
131
+ if len(set(second_nodes)) != len(second_nodes):
132
+ raise CompositionError(
133
+ CompositionIssue(
134
+ CompositionIssueCode.OVERLAP_NON_INJECTIVE,
135
+ "The overlap mapping must be injective.",
136
+ )
137
+ )
138
+ if set(second_nodes) - set(second_left.node_ids):
139
+ raise CompositionError(
140
+ CompositionIssue(
141
+ CompositionIssueCode.OVERLAP_PARTIAL,
142
+ "Every overlap image must lie in L2.",
143
+ )
144
+ )
145
+
146
+ interface_nx = nx.Graph()
147
+ first_to_interface: dict[Hashable, int] = {}
148
+ for interface_node, (first_node, second_node) in enumerate(pairs):
149
+ first_labels = first_right.node_labels(first_node, semantic=True)
150
+ second_labels = second_left.node_labels(second_node, semantic=True)
151
+ if first_labels != second_labels:
152
+ raise CompositionError(
153
+ CompositionIssue(
154
+ CompositionIssueCode.OVERLAP_NODE_LABEL,
155
+ "Mapped overlap nodes disagree on semantic labels.",
156
+ {"first": repr(first_node), "second": repr(second_node)},
157
+ )
158
+ )
159
+ interface_nx.add_node(interface_node, **first_right.node_labels(first_node))
160
+ first_to_interface[first_node] = interface_node
161
+
162
+ candidates = {
163
+ edge
164
+ for edge in first_right.edge_keys
165
+ if edge <= set(first_nodes)
166
+ and frozenset(mapping[node] for node in edge) in second_left.edge_keys
167
+ }
168
+ selected = candidates if overlap_edges is None else set(overlap_edges)
169
+ if not selected <= candidates:
170
+ raise CompositionError(
171
+ CompositionIssue(
172
+ CompositionIssueCode.OVERLAP_EDGE_CLOSURE,
173
+ "Each declared overlap edge must exist under both arms.",
174
+ )
175
+ )
176
+ for first_edge in selected:
177
+ second_edge = frozenset(mapping[node] for node in first_edge)
178
+ if first_right.edge_labels(
179
+ first_edge, semantic=True
180
+ ) != second_left.edge_labels(second_edge, semantic=True):
181
+ raise CompositionError(
182
+ CompositionIssue(
183
+ CompositionIssueCode.OVERLAP_EDGE_LABEL,
184
+ "Mapped overlap edges disagree on semantic labels.",
185
+ {"edge": tuple(sorted(map(repr, first_edge)))},
186
+ )
187
+ )
188
+ left, right = tuple(first_edge)
189
+ interface_nx.add_edge(
190
+ first_to_interface[left],
191
+ first_to_interface[right],
192
+ **first_right.edge_labels(first_edge),
193
+ )
194
+
195
+ interface = LewisLabelledGraph.from_networkx(
196
+ interface_nx, first_right.schema, name=name
197
+ )
198
+ first_map = {
199
+ interface_node: first_node
200
+ for interface_node, (first_node, _) in enumerate(pairs)
201
+ }
202
+ second_map = {
203
+ interface_node: second_node
204
+ for interface_node, (_, second_node) in enumerate(pairs)
205
+ }
206
+ try:
207
+ return cls(
208
+ interface,
209
+ LLGMorphism(interface, first_right, first_map),
210
+ LLGMorphism(interface, second_left, second_map),
211
+ )
212
+ except LLGError as error:
213
+ raise CompositionError(
214
+ CompositionIssue(
215
+ CompositionIssueCode.OVERLAP_EDGE_LABEL,
216
+ "The overlap arms are not strict LLG morphisms.",
217
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
218
+ )
219
+ ) from error
220
+
221
+
222
+ @dataclass(frozen=True)
223
+ class ProvenanceRef:
224
+ rule: int
225
+ side: str
226
+ item: Hashable
227
+
228
+
229
+ @dataclass(frozen=True)
230
+ class CompositionProvenance:
231
+ """Source-rule and overlap origins for every composite carrier item."""
232
+
233
+ node_sources: tuple[tuple[str, Hashable, tuple[ProvenanceRef, ...]], ...]
234
+ edge_sources: tuple[tuple[str, frozenset[Hashable], tuple[ProvenanceRef, ...]], ...]
235
+ overlap_nodes: tuple[tuple[Hashable, Hashable, Hashable], ...]
236
+ overlap_edges: tuple[
237
+ tuple[frozenset[Hashable], frozenset[Hashable], frozenset[Hashable]], ...
238
+ ]
239
+
240
+
241
+ @dataclass(frozen=True)
242
+ class CompositionReplay:
243
+ valid: bool
244
+ issues: tuple[CompositionIssue, ...] = ()
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class CompositionCertificate:
249
+ """Replayable proof for one explicit-overlap composition."""
250
+
251
+ first: RuleSpan
252
+ second: RuleSpan
253
+ overlap: RuleOverlap
254
+ minimal_host: LewisLabelledGraph
255
+ first_application: DPOApplication
256
+ second_application: DPOApplication
257
+ composite: RuleSpan
258
+ outer_application: DPOApplication
259
+ second_match: tuple[tuple[Hashable, Hashable], ...]
260
+ provenance: CompositionProvenance
261
+
262
+ def replay(self) -> CompositionReplay:
263
+ issues = _application_link_issues(self)
264
+ if not self.first_application.certificate.replay().valid:
265
+ issues.append(
266
+ CompositionIssue(
267
+ CompositionIssueCode.NONCOMMUTATIVE,
268
+ "The first DPO certificate no longer replays.",
269
+ )
270
+ )
271
+ if not self.second_application.certificate.replay().valid:
272
+ issues.append(
273
+ CompositionIssue(
274
+ CompositionIssueCode.NONCOMMUTATIVE,
275
+ "The second DPO certificate no longer replays.",
276
+ )
277
+ )
278
+ if not self.outer_application.certificate.replay().valid:
279
+ issues.append(
280
+ CompositionIssue(
281
+ CompositionIssueCode.OUTER_REPLAY,
282
+ "The outer DPO certificate no longer replays.",
283
+ )
284
+ )
285
+ if not self.second_application.result.is_isomorphic(
286
+ self.outer_application.result
287
+ ):
288
+ issues.append(
289
+ CompositionIssue(
290
+ CompositionIssueCode.OUTER_REPLAY,
291
+ "Sequential and composed applications disagree.",
292
+ )
293
+ )
294
+ left_node_keys = {
295
+ (side, node)
296
+ for side, graph in (
297
+ ("left", self.composite.left),
298
+ ("right", self.composite.right),
299
+ )
300
+ for node in graph.node_ids
301
+ }
302
+ observed_nodes = {
303
+ (side, node) for side, node, refs in self.provenance.node_sources if refs
304
+ }
305
+ left_edge_keys = {
306
+ (side, edge)
307
+ for side, graph in (
308
+ ("left", self.composite.left),
309
+ ("right", self.composite.right),
310
+ )
311
+ for edge in graph.edge_keys
312
+ }
313
+ observed_edges = {
314
+ (side, edge) for side, edge, refs in self.provenance.edge_sources if refs
315
+ }
316
+ if left_node_keys != observed_nodes or left_edge_keys != observed_edges:
317
+ issues.append(
318
+ CompositionIssue(
319
+ CompositionIssueCode.INCOMPLETE_PROVENANCE,
320
+ "Composite carrier provenance is incomplete.",
321
+ )
322
+ )
323
+ return CompositionReplay(not issues, tuple(issues))
324
+
325
+
326
+ @dataclass(frozen=True)
327
+ class CompositionResult:
328
+ rule: RuleSpan
329
+ certificate: CompositionCertificate
330
+
331
+
332
+ def _application_link_issues(c: CompositionCertificate) -> list[CompositionIssue]:
333
+ """Tie retained proofs to the actual applications and dependency witness."""
334
+ first, second, outer = (
335
+ c.first_application,
336
+ c.second_application,
337
+ c.outer_application,
338
+ )
339
+ checks = (
340
+ first.certificate.rule == c.first,
341
+ first.certificate.host == c.minimal_host,
342
+ second.certificate.rule == c.second,
343
+ second.certificate.host == first.result,
344
+ outer.certificate.rule == c.composite,
345
+ outer.certificate.host == c.minimal_host,
346
+ c.composite.left == c.minimal_host,
347
+ c.composite.right == second.result,
348
+ dict(second.certificate.match) == dict(c.second_match),
349
+ c.overlap.first_arm.target == c.first.right,
350
+ c.overlap.second_arm.target == c.second.left,
351
+ )
352
+ expected_condition = SequentialCondition(
353
+ c.first,
354
+ c.second,
355
+ first.certificate.match,
356
+ first.certificate.right_to_result,
357
+ c.second_match,
358
+ )
359
+ right_result = dict(first.certificate.right_to_result)
360
+ second_match = dict(c.second_match)
361
+ overlap_consistent = all(
362
+ right_result.get(c.overlap.first_arm.mapping[n])
363
+ == second_match.get(c.overlap.second_arm.mapping[n])
364
+ for n in c.overlap.interface.node_ids
365
+ )
366
+ if (
367
+ not all(checks)
368
+ or not overlap_consistent
369
+ or c.composite.condition != expected_condition
370
+ or any(a.result != a.certificate.result for a in (first, second, outer))
371
+ ):
372
+ return [
373
+ CompositionIssue(
374
+ CompositionIssueCode.NONCOMMUTATIVE,
375
+ "Application objects, sequential condition, and retained proofs disagree.",
376
+ )
377
+ ]
378
+ return []
379
+
380
+
381
+ @dataclass
382
+ class _HostPlan:
383
+ graph: nx.Graph
384
+ second_left_to_initial: dict[Hashable, Hashable | None]
385
+ node_refs: dict[Hashable, set[ProvenanceRef]]
386
+ edge_refs: dict[frozenset[Hashable], set[ProvenanceRef]]
387
+
388
+
389
+ def _fresh_context_nodes(existing: set[Hashable], count: int) -> tuple[Hashable, ...]:
390
+ result = []
391
+ index = 0
392
+ while len(result) < count:
393
+ candidate = ("__composition_context__", index)
394
+ index += 1
395
+ if candidate not in existing:
396
+ result.append(candidate)
397
+ existing.add(candidate)
398
+ return tuple(result)
399
+
400
+
401
+ def _overlap_edge_closed(overlap: RuleOverlap) -> None:
402
+ first = overlap.first_arm.target
403
+ second = overlap.second_arm.target
404
+ first_map = overlap.first_arm.mapping
405
+ second_map = overlap.second_arm.mapping
406
+ inverse_second = {node: source for source, node in second_map.items()}
407
+ interface_edges = set(overlap.interface.edge_keys)
408
+ for second_edge in second.edge_keys:
409
+ if not second_edge <= set(inverse_second):
410
+ continue
411
+ interface_edge = frozenset(inverse_second[node] for node in second_edge)
412
+ first_edge = frozenset(first_map[node] for node in interface_edge)
413
+ if first_edge in first.edge_keys and interface_edge not in interface_edges:
414
+ raise CompositionError(
415
+ CompositionIssue(
416
+ CompositionIssueCode.OVERLAP_EDGE_CLOSURE,
417
+ "The declared overlap omits an edge shared by both endpoints.",
418
+ {"edge": tuple(sorted(map(repr, second_edge)))},
419
+ )
420
+ )
421
+
422
+
423
+ def _initial_node_plan(
424
+ first: RuleSpan,
425
+ second: RuleSpan,
426
+ overlap: RuleOverlap,
427
+ graph: nx.Graph,
428
+ ) -> tuple[
429
+ dict[Hashable, Hashable | None],
430
+ dict[Hashable, set[ProvenanceRef]],
431
+ dict[frozenset[Hashable], set[ProvenanceRef]],
432
+ dict[Hashable, Hashable],
433
+ ]:
434
+ node_refs = {node: {ProvenanceRef(1, "left", node)} for node in first.left.node_ids}
435
+ edge_refs = {
436
+ edge: {ProvenanceRef(1, "left", edge)} for edge in first.left.edge_keys
437
+ }
438
+ first_right_inverse = {
439
+ node: interface for interface, node in first.right_arm.mapping.items()
440
+ }
441
+ inverse_second = {
442
+ node: interface for interface, node in overlap.second_arm.mapping.items()
443
+ }
444
+ second_to_initial: dict[Hashable, Hashable | None] = {}
445
+ for second_node, overlap_node in inverse_second.items():
446
+ first_right_node = overlap.first_arm.mapping[overlap_node]
447
+ first_interface = first_right_inverse.get(first_right_node)
448
+ initial_node = (
449
+ first.left_arm.mapping[first_interface]
450
+ if first_interface is not None
451
+ else None
452
+ )
453
+ second_to_initial[second_node] = initial_node
454
+ if initial_node is not None:
455
+ node_refs[initial_node].add(ProvenanceRef(2, "left", second_node))
456
+
457
+ external = sorted(set(second.left.node_ids) - set(inverse_second), key=repr)
458
+ fresh = _fresh_context_nodes(set(graph.nodes), len(external))
459
+ for second_node, initial_node in zip(external, fresh):
460
+ second_to_initial[second_node] = initial_node
461
+ graph.add_node(initial_node, **second.left.node_labels(second_node))
462
+ node_refs[initial_node] = {ProvenanceRef(2, "left", second_node)}
463
+ return second_to_initial, node_refs, edge_refs, inverse_second
464
+
465
+
466
+ def _add_external_second_edge(
467
+ second: RuleSpan,
468
+ graph: nx.Graph,
469
+ second_to_initial: Mapping[Hashable, Hashable | None],
470
+ edge_refs: dict[frozenset[Hashable], set[ProvenanceRef]],
471
+ second_edge: frozenset[Hashable],
472
+ ) -> None:
473
+ initial_endpoints = tuple(second_to_initial[node] for node in second_edge)
474
+ if any(node is None for node in initial_endpoints):
475
+ raise CompositionError(
476
+ CompositionIssue(
477
+ CompositionIssueCode.PULLBACK_COMPLEMENT,
478
+ "L2 context is incident to an R1-created overlap node.",
479
+ {"edge": tuple(sorted(map(repr, second_edge)))},
480
+ )
481
+ )
482
+ initial_edge = frozenset(initial_endpoints)
483
+ if len(initial_edge) != 2:
484
+ raise CompositionError(
485
+ CompositionIssue(
486
+ CompositionIssueCode.PARALLEL_EDGE_CONFLICT,
487
+ "The minimal host would contain a collapsed loop.",
488
+ {"edge": tuple(sorted(map(repr, second_edge)))},
489
+ )
490
+ )
491
+ left, right = tuple(initial_edge)
492
+ if graph.has_edge(left, right):
493
+ raise CompositionError(
494
+ CompositionIssue(
495
+ CompositionIssueCode.PARALLEL_EDGE_CONFLICT,
496
+ "The minimal host would collapse two non-interface edges.",
497
+ {"edge": tuple(sorted(map(repr, initial_edge)))},
498
+ )
499
+ )
500
+ graph.add_edge(left, right, **second.left.edge_labels(second_edge))
501
+ edge_refs[initial_edge] = {ProvenanceRef(2, "left", second_edge)}
502
+
503
+
504
+ def _add_second_context_edges(
505
+ second: RuleSpan,
506
+ overlap: RuleOverlap,
507
+ graph: nx.Graph,
508
+ second_to_initial: Mapping[Hashable, Hashable | None],
509
+ inverse_second: Mapping[Hashable, Hashable],
510
+ edge_refs: dict[frozenset[Hashable], set[ProvenanceRef]],
511
+ ) -> None:
512
+ interface_edges = set(overlap.interface.edge_keys)
513
+ for second_edge in second.left.edge_keys:
514
+ if not second_edge <= set(inverse_second):
515
+ _add_external_second_edge(
516
+ second, graph, second_to_initial, edge_refs, second_edge
517
+ )
518
+ continue
519
+ interface_edge = frozenset(inverse_second[node] for node in second_edge)
520
+ if interface_edge not in interface_edges:
521
+ _add_external_second_edge(
522
+ second, graph, second_to_initial, edge_refs, second_edge
523
+ )
524
+ continue
525
+ initial_endpoints = tuple(second_to_initial[node] for node in second_edge)
526
+ if all(node is not None for node in initial_endpoints):
527
+ initial_edge = frozenset(initial_endpoints)
528
+ if initial_edge in edge_refs:
529
+ edge_refs[initial_edge].add(ProvenanceRef(2, "left", second_edge))
530
+
531
+
532
+ def _derive_initial_electron_state(graph: nx.Graph) -> nx.Graph:
533
+ for _, attrs in graph.nodes(data=True):
534
+ for key in ELECTRON_LLG_SCHEMA.node_derived:
535
+ attrs.pop(key, None)
536
+ for _, _, attrs in graph.edges(data=True):
537
+ for key in ELECTRON_LLG_SCHEMA.edge_derived:
538
+ attrs.pop(key, None)
539
+ try:
540
+ return derive_electron_labeled_graph(graph)
541
+ except LLGError as error:
542
+ raise CompositionError(
543
+ CompositionIssue(
544
+ CompositionIssueCode.PULLBACK_COMPLEMENT,
545
+ "The minimal electron host is not coherent.",
546
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
547
+ )
548
+ ) from error
549
+
550
+
551
+ def _minimal_host_plan(
552
+ first: RuleSpan,
553
+ second: RuleSpan,
554
+ overlap: RuleOverlap,
555
+ ) -> _HostPlan:
556
+ if (
557
+ overlap.first_arm.target != first.right
558
+ or overlap.second_arm.target != second.left
559
+ ):
560
+ raise CompositionError(
561
+ CompositionIssue(
562
+ CompositionIssueCode.OVERLAP_PARTIAL,
563
+ "The overlap endpoints must be exactly R1 and L2.",
564
+ )
565
+ )
566
+ _overlap_edge_closed(overlap)
567
+ graph = first.left.to_networkx()
568
+ second_to_initial, node_refs, edge_refs, inverse_second = _initial_node_plan(
569
+ first, second, overlap, graph
570
+ )
571
+ _add_second_context_edges(
572
+ second,
573
+ overlap,
574
+ graph,
575
+ second_to_initial,
576
+ inverse_second,
577
+ edge_refs,
578
+ )
579
+ if is_electron_schema(first.left.schema):
580
+ graph = _derive_initial_electron_state(graph)
581
+ return _HostPlan(graph, second_to_initial, node_refs, edge_refs)
582
+
583
+
584
+ def _first_match(first: RuleSpan, host: LewisLabelledGraph) -> dict[Hashable, Hashable]:
585
+ return {node: node for node in first.left.node_ids}
586
+
587
+
588
+ def _second_match(
589
+ second: RuleSpan,
590
+ overlap: RuleOverlap,
591
+ plan: _HostPlan,
592
+ first_application: DPOApplication,
593
+ ) -> dict[Hashable, Hashable]:
594
+ first_right_result = dict(first_application.certificate.right_to_result)
595
+ overlap_first = overlap.first_arm.mapping
596
+ overlap_second_inverse = {
597
+ node: interface for interface, node in overlap.second_arm.mapping.items()
598
+ }
599
+ result: dict[Hashable, Hashable] = {}
600
+ for second_node in second.left.node_ids:
601
+ if second_node in overlap_second_inverse:
602
+ interface = overlap_second_inverse[second_node]
603
+ result[second_node] = first_right_result[overlap_first[interface]]
604
+ else:
605
+ initial = plan.second_left_to_initial[second_node]
606
+ if initial is None or initial not in first_application.result.node_ids:
607
+ raise CompositionError(
608
+ CompositionIssue(
609
+ CompositionIssueCode.PULLBACK_COMPLEMENT,
610
+ "An L2 context item did not survive the first application.",
611
+ {"node": repr(second_node)},
612
+ )
613
+ )
614
+ result[second_node] = initial
615
+ return result
616
+
617
+
618
+ def _apply_or_translate(
619
+ rule: RuleSpan,
620
+ host: LewisLabelledGraph,
621
+ match: Mapping[Hashable, Hashable],
622
+ code: CompositionIssueCode,
623
+ ) -> DPOApplication:
624
+ try:
625
+ return apply_dpo(rule, host, match)
626
+ except DPOError as error:
627
+ raise CompositionError(
628
+ CompositionIssue(
629
+ code,
630
+ "A component DPO application is not admissible.",
631
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
632
+ )
633
+ ) from error
634
+
635
+
636
+ def _combine_boundary(
637
+ first: RuleSpan, second: RuleSpan
638
+ ) -> tuple[SystemBoundary, EnvironmentToken | None]:
639
+ if SystemBoundary.ABSTRACT in {first.boundary, second.boundary}:
640
+ return SystemBoundary.ABSTRACT, None
641
+ if (
642
+ first.boundary is SystemBoundary.CLOSED
643
+ and second.boundary is SystemBoundary.CLOSED
644
+ ):
645
+ return SystemBoundary.CLOSED, None
646
+ deltas = (first.resource_delta, second.resource_delta)
647
+ elements: Counter[str] = Counter()
648
+ isotopes = Counter()
649
+ electrons = 0.0
650
+ names = []
651
+ for rule, delta in zip((first, second), deltas):
652
+ if delta is None:
653
+ continue
654
+ elements.update(dict(delta.element_delta))
655
+ isotopes.update(dict(delta.isotope_delta))
656
+ electrons += delta.electron_delta
657
+ if rule.environment is not None:
658
+ names.append(rule.environment.name)
659
+ token = EnvironmentToken(
660
+ " + ".join(names) or "composed environment",
661
+ tuple((element, count) for element, count in elements.items() if count),
662
+ electrons,
663
+ tuple((isotope, count) for isotope, count in isotopes.items() if count),
664
+ )
665
+ return SystemBoundary.OPEN, token
666
+
667
+
668
+ def _outer_interface_from_contexts(
669
+ rule: RuleSpan,
670
+ first_application: DPOApplication,
671
+ second_application: DPOApplication,
672
+ ) -> RuleSpan:
673
+ """Preserve only coordinates that survive both realized natural contexts."""
674
+ contexts = (
675
+ first_application.certificate.context,
676
+ second_application.certificate.context,
677
+ )
678
+ graph = rule.interface.to_networkx()
679
+ for node, carrier in rule.left_arm.mapping.items():
680
+ labels = [context.node_labels(carrier, semantic=True) for context in contexts]
681
+ for key in rule.left.schema.semantic_node_keys:
682
+ if (
683
+ key not in labels[0]
684
+ or key not in labels[1]
685
+ or labels[0][key] != labels[1][key]
686
+ ):
687
+ graph.nodes[node].pop(key, None)
688
+ for edge, carrier in rule.left_arm.edge_mapping.items():
689
+ labels = [context.edge_labels(carrier, semantic=True) for context in contexts]
690
+ for key in rule.left.schema.semantic_edge_keys:
691
+ if (
692
+ key not in labels[0]
693
+ or key not in labels[1]
694
+ or labels[0][key] != labels[1][key]
695
+ ):
696
+ graph.edges[tuple(edge)].pop(key, None)
697
+ interface = LewisLabelledGraph.from_networkx(
698
+ graph, rule.interface.schema, name=rule.interface.name, partial=True
699
+ )
700
+ return replace(
701
+ rule,
702
+ interface=interface,
703
+ left_arm=RuleEmbedding(interface, rule.left, rule.left_arm.f),
704
+ right_arm=RuleEmbedding(interface, rule.right, rule.right_arm.f),
705
+ )
706
+
707
+
708
+ def _outer_rule(
709
+ first: RuleSpan,
710
+ second: RuleSpan,
711
+ host: LewisLabelledGraph,
712
+ first_application: DPOApplication,
713
+ second_application: DPOApplication,
714
+ ) -> RuleSpan:
715
+ removed_first = set(first_application.certificate.deleted_nodes)
716
+ removed_second = set(second_application.certificate.deleted_nodes)
717
+ preserved = {
718
+ node: node
719
+ for node in host.node_ids
720
+ if node not in removed_first
721
+ and node not in removed_second
722
+ and node in second_application.result.node_ids
723
+ }
724
+ removed_edges = set(first_application.certificate.deleted_edges) | set(
725
+ second_application.certificate.deleted_edges
726
+ )
727
+ preserved_edges = {
728
+ edge
729
+ for edge in host.edge_keys
730
+ if edge <= set(preserved)
731
+ and edge not in removed_edges
732
+ and edge in second_application.result.edge_keys
733
+ }
734
+ boundary, environment = _combine_boundary(first, second)
735
+ try:
736
+ rule = RuleSpan.from_mapping(
737
+ host,
738
+ second_application.result,
739
+ preserved,
740
+ preserved_edges=preserved_edges,
741
+ boundary=boundary,
742
+ environment=environment,
743
+ name=f"{first.name};{second.name}",
744
+ )
745
+ return _outer_interface_from_contexts(
746
+ rule, first_application, second_application
747
+ )
748
+ except DPOError as error:
749
+ raise CompositionError(
750
+ CompositionIssue(
751
+ CompositionIssueCode.RESOURCE_MISMATCH,
752
+ "The outer rule violates the composed resource contract.",
753
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
754
+ )
755
+ ) from error
756
+
757
+
758
+ def _mapped_edge(
759
+ mapping: Mapping[Hashable, Hashable], edge: frozenset[Hashable]
760
+ ) -> frozenset[Hashable]:
761
+ return frozenset(mapping[node] for node in edge)
762
+
763
+
764
+ def _provenance(
765
+ first: RuleSpan,
766
+ second: RuleSpan,
767
+ overlap: RuleOverlap,
768
+ host: LewisLabelledGraph,
769
+ plan: _HostPlan,
770
+ first_application: DPOApplication,
771
+ second_application: DPOApplication,
772
+ ) -> CompositionProvenance:
773
+ final = second_application.result
774
+ left_nodes = {node: set(refs) for node, refs in plan.node_refs.items()}
775
+ left_edges = {edge: set(refs) for edge, refs in plan.edge_refs.items()}
776
+ removed_nodes = set(first_application.certificate.deleted_nodes) | set(
777
+ second_application.certificate.deleted_nodes
778
+ )
779
+ removed_edges = set(first_application.certificate.deleted_edges) | set(
780
+ second_application.certificate.deleted_edges
781
+ )
782
+ final_nodes: dict[Hashable, set[ProvenanceRef]] = {
783
+ node: set() if node in removed_nodes else set(left_nodes.get(node, ()))
784
+ for node in final.node_ids
785
+ }
786
+ final_edges: dict[frozenset[Hashable], set[ProvenanceRef]] = {
787
+ edge: set() if edge in removed_edges else set(left_edges.get(edge, ()))
788
+ for edge in final.edge_keys
789
+ }
790
+
791
+ first_right = dict(first_application.certificate.right_to_result)
792
+ removed_second = set(second_application.certificate.deleted_nodes)
793
+ for node in first.right.node_ids:
794
+ result_node = first_right[node]
795
+ if result_node in final.node_ids and result_node not in removed_second:
796
+ final_nodes[result_node].add(ProvenanceRef(1, "right", node))
797
+ for edge in first.right.edge_keys:
798
+ result_edge = _mapped_edge(first_right, edge)
799
+ if (
800
+ result_edge in final.edge_keys
801
+ and result_edge not in second_application.certificate.deleted_edges
802
+ ):
803
+ final_edges[result_edge].add(ProvenanceRef(1, "right", edge))
804
+
805
+ second_right = dict(second_application.certificate.right_to_result)
806
+ for node in second.right.node_ids:
807
+ final_nodes[second_right[node]].add(ProvenanceRef(2, "right", node))
808
+ for edge in second.right.edge_keys:
809
+ result_edge = _mapped_edge(second_right, edge)
810
+ final_edges[result_edge].add(ProvenanceRef(2, "right", edge))
811
+
812
+ node_sources = tuple(
813
+ sorted(
814
+ [
815
+ (side, node, tuple(sorted(refs, key=repr)))
816
+ for side, values in (("left", left_nodes), ("right", final_nodes))
817
+ for node, refs in values.items()
818
+ ],
819
+ key=repr,
820
+ )
821
+ )
822
+ edge_sources = tuple(
823
+ sorted(
824
+ [
825
+ (side, edge, tuple(sorted(refs, key=repr)))
826
+ for side, values in (("left", left_edges), ("right", final_edges))
827
+ for edge, refs in values.items()
828
+ ],
829
+ key=repr,
830
+ )
831
+ )
832
+ overlap_nodes = tuple(
833
+ sorted(
834
+ (
835
+ (
836
+ node,
837
+ overlap.first_arm.mapping[node],
838
+ overlap.second_arm.mapping[node],
839
+ )
840
+ for node in overlap.interface.node_ids
841
+ ),
842
+ key=repr,
843
+ )
844
+ )
845
+ overlap_edges = tuple(
846
+ sorted(
847
+ (
848
+ (
849
+ edge,
850
+ _mapped_edge(overlap.first_arm.mapping, edge),
851
+ _mapped_edge(overlap.second_arm.mapping, edge),
852
+ )
853
+ for edge in overlap.interface.edge_keys
854
+ ),
855
+ key=repr,
856
+ )
857
+ )
858
+ return CompositionProvenance(
859
+ node_sources, edge_sources, overlap_nodes, overlap_edges
860
+ )
861
+
862
+
863
+ def compose_rules(
864
+ first: RuleSpan,
865
+ second: RuleSpan,
866
+ overlap: RuleOverlap,
867
+ ) -> CompositionResult:
868
+ """Compose exactly two rules along exactly one caller-supplied overlap."""
869
+ if first.right.schema != second.left.schema:
870
+ raise CompositionError(
871
+ CompositionIssue(
872
+ CompositionIssueCode.SCHEMA_MISMATCH,
873
+ "Consecutive rule endpoints must use one LLG schema.",
874
+ )
875
+ )
876
+ sources_before = (
877
+ first.left.to_networkx(),
878
+ first.right.to_networkx(),
879
+ second.left.to_networkx(),
880
+ second.right.to_networkx(),
881
+ )
882
+ plan = _minimal_host_plan(first, second, overlap)
883
+ try:
884
+ host = LewisLabelledGraph.from_networkx(
885
+ plan.graph, first.left.schema, name=f"{first.name};{second.name}:G"
886
+ )
887
+ except LLGError as error:
888
+ raise CompositionError(
889
+ CompositionIssue(
890
+ CompositionIssueCode.PULLBACK_COMPLEMENT,
891
+ "The minimal initial context is not an LLG object.",
892
+ {"issues": tuple(issue.to_dict() for issue in error.issues)},
893
+ )
894
+ ) from error
895
+ first_application = _apply_or_translate(
896
+ first, host, _first_match(first, host), CompositionIssueCode.FIRST_APPLICATION
897
+ )
898
+ second_match = _second_match(second, overlap, plan, first_application)
899
+ second_application = _apply_or_translate(
900
+ second,
901
+ first_application.result,
902
+ second_match,
903
+ CompositionIssueCode.SECOND_APPLICATION,
904
+ )
905
+ composite = _outer_rule(first, second, host, first_application, second_application)
906
+ composite = replace(
907
+ composite,
908
+ condition=SequentialCondition(
909
+ first,
910
+ second,
911
+ first_application.certificate.match,
912
+ first_application.certificate.right_to_result,
913
+ tuple(sorted(second_match.items(), key=repr)),
914
+ ),
915
+ )
916
+ outer_application = _apply_or_translate(
917
+ composite,
918
+ host,
919
+ {node: node for node in host.node_ids},
920
+ CompositionIssueCode.OUTER_REPLAY,
921
+ )
922
+ provenance = _provenance(
923
+ first,
924
+ second,
925
+ overlap,
926
+ host,
927
+ plan,
928
+ first_application,
929
+ second_application,
930
+ )
931
+ certificate = CompositionCertificate(
932
+ first,
933
+ second,
934
+ overlap,
935
+ host,
936
+ first_application,
937
+ second_application,
938
+ composite,
939
+ outer_application,
940
+ tuple(sorted(second_match.items(), key=repr)),
941
+ provenance,
942
+ )
943
+ replay = certificate.replay()
944
+ if not replay.valid:
945
+ raise CompositionError(*replay.issues)
946
+ sources_after = (
947
+ first.left.to_networkx(),
948
+ first.right.to_networkx(),
949
+ second.left.to_networkx(),
950
+ second.right.to_networkx(),
951
+ )
952
+ if any(
953
+ not nx.utils.graphs_equal(before, after)
954
+ for before, after in zip(sources_before, sources_after)
955
+ ):
956
+ raise CompositionError(
957
+ CompositionIssue(
958
+ CompositionIssueCode.SOURCE_MUTATED,
959
+ "Composition mutated a source rule.",
960
+ )
961
+ )
962
+ return CompositionResult(composite, certificate)
963
+
964
+
965
+ __all__ = [
966
+ "CompositionCertificate",
967
+ "CompositionError",
968
+ "CompositionIssue",
969
+ "CompositionIssueCode",
970
+ "CompositionProvenance",
971
+ "CompositionReplay",
972
+ "CompositionResult",
973
+ "ProvenanceRef",
974
+ "RuleOverlap",
975
+ "compose_rules",
976
+ ]