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,999 @@
1
+ """Finite Lewis-labelled graphs and their strict embedding category.
2
+
3
+ This module defines the generic, non-stereo graph layer used by formal rule
4
+ composition. Objects are finite simple undirected attributed graphs. Arrows
5
+ are total injective node maps whose induced edge maps preserve incidence and
6
+ all declared identity and state labels. Derived labels are checked when an
7
+ object is built but are not independent matching data; annotations are never
8
+ matching data. Partial patterns use the same schema with omitted coordinates;
9
+ DPO embeddings and certificate maps preserve their defined coordinates.
10
+
11
+ The category deliberately makes no blanket adhesivity claim. Pullbacks of
12
+ strict embeddings are available as finite fibre products. A pushout along
13
+ strict embeddings is admitted only when quotienting creates no identity/state
14
+ label conflict. Pushout complements are partial and belong to the DPO layer,
15
+ where dangling and identification premises can be reported explicitly.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field, replace
21
+ from enum import Enum
22
+ from numbers import Integral, Real
23
+ from typing import Any, Hashable, Iterable, Mapping
24
+
25
+ import networkx as nx
26
+
27
+ FrozenValue = Hashable
28
+ LabelBundle = tuple[tuple[str, FrozenValue], ...]
29
+ EdgeKey = frozenset[Hashable]
30
+
31
+
32
+ class LLGIssueCode(str, Enum):
33
+ """Stable failures for LLG objects, adapters, and morphisms."""
34
+
35
+ SCHEMA_OVERLAP = "LLG_SCHEMA_OVERLAP"
36
+ DIRECTED_GRAPH = "LLG_DIRECTED_GRAPH"
37
+ MULTIGRAPH = "LLG_MULTIGRAPH"
38
+ SELF_LOOP = "LLG_SELF_LOOP"
39
+ DUPLICATE_NODE = "LLG_DUPLICATE_NODE"
40
+ DUPLICATE_EDGE = "LLG_DUPLICATE_EDGE"
41
+ UNKNOWN_ENDPOINT = "LLG_UNKNOWN_ENDPOINT"
42
+ MISSING_NODE_LABEL = "LLG_MISSING_NODE_LABEL"
43
+ MISSING_EDGE_LABEL = "LLG_MISSING_EDGE_LABEL"
44
+ DERIVED_LABEL_MISMATCH = "LLG_DERIVED_LABEL_MISMATCH"
45
+ OUTSIDE_SOURCE = "LLG_MORPHISM_OUTSIDE_SOURCE"
46
+ OUTSIDE_TARGET = "LLG_MORPHISM_OUTSIDE_TARGET"
47
+ PARTIAL_MAPPING = "LLG_MORPHISM_PARTIAL_MAPPING"
48
+ NON_INJECTIVE = "LLG_MORPHISM_NON_INJECTIVE"
49
+ MISSING_EDGE = "LLG_MORPHISM_MISSING_EDGE"
50
+ NODE_LABEL_MISMATCH = "LLG_MORPHISM_NODE_LABEL_MISMATCH"
51
+ EDGE_LABEL_MISMATCH = "LLG_MORPHISM_EDGE_LABEL_MISMATCH"
52
+ ENDPOINT_MISMATCH = "LLG_MORPHISM_ENDPOINT_MISMATCH"
53
+ INVALID_RELABELING = "LLG_INVALID_RELABELING"
54
+ UNSUPPORTED_ITS_FORMAT = "LLG_UNSUPPORTED_ITS_FORMAT"
55
+ LOSSY_ELECTRON_ADAPTER = "LLG_LOSSY_ELECTRON_ADAPTER"
56
+ INVALID_ISOTOPE = "LLG_INVALID_ISOTOPE"
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class LLGIssue:
61
+ """One typed LLG refusal."""
62
+
63
+ code: LLGIssueCode
64
+ message: str
65
+ context: Mapping[str, Any] = field(default_factory=dict)
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ return {
69
+ "code": self.code.value,
70
+ "message": self.message,
71
+ "context": dict(self.context),
72
+ }
73
+
74
+
75
+ class LLGError(ValueError):
76
+ """Raised when an invalid object or arrow would enter the category."""
77
+
78
+ def __init__(self, *issues: LLGIssue):
79
+ self.issues = tuple(issues)
80
+ super().__init__("; ".join(issue.message for issue in issues))
81
+
82
+
83
+ def _freeze(value: Any) -> FrozenValue:
84
+ """Recursively freeze common mutable attribute values."""
85
+ if isinstance(value, Mapping):
86
+ return tuple(
87
+ sorted(((str(key), _freeze(item)) for key, item in value.items()), key=repr)
88
+ )
89
+ if isinstance(value, (list, tuple)):
90
+ return tuple(_freeze(item) for item in value)
91
+ if isinstance(value, (set, frozenset)):
92
+ return frozenset(_freeze(item) for item in value)
93
+ try:
94
+ hash(value)
95
+ except TypeError as error:
96
+ raise TypeError(
97
+ f"LLG attribute values must be recursively hashable: {value!r}"
98
+ ) from error
99
+ return value
100
+
101
+
102
+ def _bundle(values: Mapping[str, Any]) -> LabelBundle:
103
+ return tuple(sorted(((str(key), _freeze(value)) for key, value in values.items())))
104
+
105
+
106
+ def _bundle_dict(values: LabelBundle) -> dict[str, FrozenValue]:
107
+ return dict(values)
108
+
109
+
110
+ def _carrier_key(value: Hashable) -> tuple[str, str, str]:
111
+ kind = type(value)
112
+ return (kind.__module__, kind.__qualname__, repr(value))
113
+
114
+
115
+ def _edge_sort_key(edge: EdgeKey) -> tuple[tuple[str, str, str], ...]:
116
+ return tuple(sorted((_carrier_key(node) for node in edge)))
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class LabelSchema:
121
+ """Classify labels by mathematical role.
122
+
123
+ Identity and state labels are preserved by every morphism. Derived labels
124
+ must be coherent with authoritative state but are not matched separately.
125
+ Annotation labels, including atom maps, names, and display fields, are
126
+ retained without affecting admissibility.
127
+ """
128
+
129
+ node_identity: tuple[str, ...] = ()
130
+ node_state: tuple[str, ...] = ()
131
+ edge_identity: tuple[str, ...] = ()
132
+ edge_state: tuple[str, ...] = ()
133
+ node_derived: tuple[str, ...] = ()
134
+ edge_derived: tuple[str, ...] = ()
135
+ node_annotations: tuple[str, ...] = ()
136
+ edge_annotations: tuple[str, ...] = ()
137
+ name: str = "llg"
138
+
139
+ def __post_init__(self) -> None:
140
+ groups = {
141
+ "node_identity": self.node_identity,
142
+ "node_state": self.node_state,
143
+ "node_derived": self.node_derived,
144
+ "node_annotations": self.node_annotations,
145
+ "edge_identity": self.edge_identity,
146
+ "edge_state": self.edge_state,
147
+ "edge_derived": self.edge_derived,
148
+ "edge_annotations": self.edge_annotations,
149
+ }
150
+ for key, values in groups.items():
151
+ normalized = tuple(dict.fromkeys(str(value) for value in values))
152
+ object.__setattr__(self, key, normalized)
153
+ issues: list[LLGIssue] = []
154
+ for prefix in ("node", "edge"):
155
+ owners: dict[str, list[str]] = {}
156
+ for key, values in groups.items():
157
+ if not key.startswith(prefix):
158
+ continue
159
+ for value in values:
160
+ owners.setdefault(str(value), []).append(key)
161
+ overlap = {key: value for key, value in owners.items() if len(value) > 1}
162
+ if overlap:
163
+ issues.append(
164
+ LLGIssue(
165
+ LLGIssueCode.SCHEMA_OVERLAP,
166
+ f"{prefix.title()} labels must have exactly one class.",
167
+ {"labels": overlap},
168
+ )
169
+ )
170
+ if issues:
171
+ raise LLGError(*issues)
172
+
173
+ @property
174
+ def semantic_node_keys(self) -> tuple[str, ...]:
175
+ return self.node_identity + self.node_state
176
+
177
+ @property
178
+ def semantic_edge_keys(self) -> tuple[str, ...]:
179
+ return self.edge_identity + self.edge_state
180
+
181
+ @property
182
+ def declared_node_keys(self) -> tuple[str, ...]:
183
+ return self.semantic_node_keys + self.node_derived + self.node_annotations
184
+
185
+ @property
186
+ def declared_edge_keys(self) -> tuple[str, ...]:
187
+ return self.semantic_edge_keys + self.edge_derived + self.edge_annotations
188
+
189
+
190
+ COMMON_CHEMICAL_SCHEMA = LabelSchema(
191
+ node_identity=("element",),
192
+ node_state=("aromatic", "hcount", "charge"),
193
+ edge_state=("order",),
194
+ node_annotations=("atom_map", "source_format"),
195
+ name="chemical-common/1",
196
+ )
197
+
198
+ ELECTRON_LLG_SCHEMA = LabelSchema(
199
+ node_identity=("element",),
200
+ node_state=(
201
+ "aromatic",
202
+ "hcount",
203
+ "radical",
204
+ "lone_pairs",
205
+ "valence_electrons",
206
+ ),
207
+ edge_state=("sigma_order", "pi_order"),
208
+ node_derived=("charge", "bond_order_sum"),
209
+ edge_derived=("order", "kekule_order"),
210
+ node_annotations=("atom_map", "source_format"),
211
+ name="lewis-electron/1",
212
+ )
213
+
214
+ ISOTOPE_ELECTRON_LLG_SCHEMA = replace(
215
+ ELECTRON_LLG_SCHEMA,
216
+ node_identity=("element", "isotope"),
217
+ name="lewis-electron-isotope/1",
218
+ )
219
+
220
+
221
+ def is_electron_schema(schema: LabelSchema) -> bool:
222
+ """Return whether the schema has the supported authoritative Lewis state."""
223
+ return schema in (ELECTRON_LLG_SCHEMA, ISOTOPE_ELECTRON_LLG_SCHEMA)
224
+
225
+
226
+ def _validate_isotopes(nodes, schema):
227
+ if schema != ISOTOPE_ELECTRON_LLG_SCHEMA:
228
+ return
229
+ for node, labels in nodes:
230
+ values = dict(labels)
231
+ if "isotope" not in values:
232
+ continue
233
+ mass = values["isotope"]
234
+ if isinstance(mass, bool) or not isinstance(mass, Integral) or mass < 0:
235
+ raise LLGError(
236
+ LLGIssue(
237
+ LLGIssueCode.INVALID_ISOTOPE,
238
+ "An isotope mass must be a nonnegative integer; zero is unspecified.",
239
+ {"node": repr(node), "isotope": repr(mass)},
240
+ )
241
+ )
242
+
243
+
244
+ def _select_labels(
245
+ values: Mapping[str, Any],
246
+ declared: Iterable[str],
247
+ *,
248
+ required: Iterable[str],
249
+ issue_code: LLGIssueCode,
250
+ owner: Any,
251
+ ) -> dict[str, Any]:
252
+ required_set = set(required)
253
+ missing = required_set - set(values)
254
+ if missing:
255
+ raise LLGError(
256
+ LLGIssue(
257
+ issue_code,
258
+ "A declared semantic label is missing.",
259
+ {"owner": repr(owner), "labels": tuple(sorted(missing))},
260
+ )
261
+ )
262
+ selected = {key: values[key] for key in declared if key in values}
263
+ declared_set = set(declared)
264
+ for key, value in values.items():
265
+ if key not in declared_set:
266
+ selected[key if key.startswith("annotation:") else f"annotation:{key}"] = (
267
+ value
268
+ )
269
+ return selected
270
+
271
+
272
+ @dataclass(frozen=True)
273
+ class LewisLabelledGraph:
274
+ """Immutable object of the finite LLG category."""
275
+
276
+ schema: LabelSchema
277
+ nodes: tuple[tuple[Hashable, LabelBundle], ...]
278
+ edges: tuple[tuple[EdgeKey, LabelBundle], ...]
279
+ name: Hashable | None = field(default=None, compare=False, hash=False)
280
+ partial: bool = False
281
+
282
+ def __post_init__(self) -> None:
283
+ normalized_nodes = tuple(
284
+ sorted(
285
+ ((node, tuple(labels)) for node, labels in self.nodes),
286
+ key=lambda item: _carrier_key(item[0]),
287
+ )
288
+ )
289
+ normalized_edges = tuple(
290
+ sorted(
291
+ ((frozenset(edge), tuple(labels)) for edge, labels in self.edges),
292
+ key=lambda item: _edge_sort_key(item[0]),
293
+ )
294
+ )
295
+ object.__setattr__(self, "nodes", normalized_nodes)
296
+ object.__setattr__(self, "edges", normalized_edges)
297
+ _validate_isotopes(normalized_nodes, self.schema)
298
+
299
+ issues: list[LLGIssue] = []
300
+ node_ids = tuple(node for node, _ in normalized_nodes)
301
+ if len(set(node_ids)) != len(node_ids):
302
+ issues.append(
303
+ LLGIssue(LLGIssueCode.DUPLICATE_NODE, "Duplicate carrier node.")
304
+ )
305
+ edge_keys = tuple(edge for edge, _ in normalized_edges)
306
+ if len(set(edge_keys)) != len(edge_keys):
307
+ issues.append(
308
+ LLGIssue(LLGIssueCode.DUPLICATE_EDGE, "Duplicate simple edge.")
309
+ )
310
+ for edge in edge_keys:
311
+ if len(edge) != 2:
312
+ issues.append(
313
+ LLGIssue(
314
+ LLGIssueCode.SELF_LOOP,
315
+ "LLG objects are loop-free simple graphs.",
316
+ {"edge": tuple(map(repr, edge))},
317
+ )
318
+ )
319
+ unknown = set(edge) - set(node_ids)
320
+ if unknown:
321
+ issues.append(
322
+ LLGIssue(
323
+ LLGIssueCode.UNKNOWN_ENDPOINT,
324
+ "An edge endpoint is outside the node carrier.",
325
+ {"nodes": tuple(sorted(map(repr, unknown)))},
326
+ )
327
+ )
328
+ if issues:
329
+ raise LLGError(*issues)
330
+
331
+ # Missing keys represent undefined coordinates only in partial patterns.
332
+ for node, labels in normalized_nodes:
333
+ if not self.partial:
334
+ _select_labels(
335
+ dict(labels),
336
+ self.schema.declared_node_keys,
337
+ required=self.schema.semantic_node_keys,
338
+ issue_code=LLGIssueCode.MISSING_NODE_LABEL,
339
+ owner=node,
340
+ )
341
+ for edge, labels in normalized_edges:
342
+ if not self.partial:
343
+ _select_labels(
344
+ dict(labels),
345
+ self.schema.declared_edge_keys,
346
+ required=self.schema.semantic_edge_keys,
347
+ issue_code=LLGIssueCode.MISSING_EDGE_LABEL,
348
+ owner=edge,
349
+ )
350
+
351
+ @classmethod
352
+ def from_networkx(
353
+ cls,
354
+ graph: nx.Graph,
355
+ schema: LabelSchema,
356
+ *,
357
+ name: Hashable | None = None,
358
+ partial: bool = False,
359
+ ) -> "LewisLabelledGraph":
360
+ """Freeze a NetworkX graph under an explicit label schema."""
361
+ if graph.is_directed():
362
+ raise LLGError(
363
+ LLGIssue(LLGIssueCode.DIRECTED_GRAPH, "LLG objects are undirected.")
364
+ )
365
+ if graph.is_multigraph():
366
+ raise LLGError(
367
+ LLGIssue(LLGIssueCode.MULTIGRAPH, "LLG objects are simple graphs.")
368
+ )
369
+ if nx.number_of_selfloops(graph):
370
+ raise LLGError(
371
+ LLGIssue(LLGIssueCode.SELF_LOOP, "LLG objects are loop-free.")
372
+ )
373
+
374
+ nodes = []
375
+ for node, attrs in graph.nodes(data=True):
376
+ labels = _select_labels(
377
+ attrs,
378
+ schema.declared_node_keys,
379
+ required=() if partial else schema.semantic_node_keys,
380
+ issue_code=LLGIssueCode.MISSING_NODE_LABEL,
381
+ owner=node,
382
+ )
383
+ nodes.append((node, _bundle(labels)))
384
+ edges = []
385
+ for left, right, attrs in graph.edges(data=True):
386
+ labels = _select_labels(
387
+ attrs,
388
+ schema.declared_edge_keys,
389
+ required=() if partial else schema.semantic_edge_keys,
390
+ issue_code=LLGIssueCode.MISSING_EDGE_LABEL,
391
+ owner=(left, right),
392
+ )
393
+ edges.append((frozenset((left, right)), _bundle(labels)))
394
+ return cls(schema, tuple(nodes), tuple(edges), name, partial)
395
+
396
+ @property
397
+ def node_ids(self) -> frozenset[Hashable]:
398
+ return self._index(self.nodes, "_node_index")[1]
399
+
400
+ @property
401
+ def edge_keys(self) -> frozenset[EdgeKey]:
402
+ return self._index(self.edges, "_edge_index")[1]
403
+
404
+ def _index(self, values, cache_name):
405
+ # Cache geometry/bundle references, never label values or validation results.
406
+ # Identity of the authoritative tuple also detects bypassed frozen-field edits.
407
+ cached = self.__dict__.get(cache_name)
408
+ if cached is None or cached[0] is not values:
409
+ lookup = dict(values)
410
+ cached = (values, lookup, frozenset(lookup))
411
+ object.__setattr__(self, cache_name, cached)
412
+ return cached[1:]
413
+
414
+ def node_labels(self, node: Hashable, *, semantic: bool = False) -> dict[str, Any]:
415
+ values = _bundle_dict(self._index(self.nodes, "_node_index")[0][node])
416
+ if semantic:
417
+ return {
418
+ key: values[key]
419
+ for key in self.schema.semantic_node_keys
420
+ if key in values
421
+ }
422
+ return values
423
+
424
+ def edge_labels(self, edge: EdgeKey, *, semantic: bool = False) -> dict[str, Any]:
425
+ values = _bundle_dict(self._index(self.edges, "_edge_index")[0][frozenset(edge)])
426
+ if semantic:
427
+ return {
428
+ key: values[key]
429
+ for key in self.schema.semantic_edge_keys
430
+ if key in values
431
+ }
432
+ return values
433
+
434
+ def _matching_node_labels(self, node: Hashable) -> dict[str, Any]:
435
+ """Return semantic node labels with aromatic-system invariants.
436
+
437
+ :param node: Carrier node.
438
+ :type node: Hashable
439
+ :return: Labels used by isomorphisms and morphisms.
440
+ :rtype: dict[str, Any]
441
+ """
442
+ values = self.node_labels(node, semantic=True)
443
+ if (
444
+ self.partial
445
+ or not is_electron_schema(self.schema)
446
+ or not values.get("aromatic", False)
447
+ ):
448
+ return values
449
+ pi_valence = 0.0
450
+ for edge in self.edge_keys:
451
+ if node not in edge or not self._is_kekule_aromatic_edge(edge):
452
+ continue
453
+ pi_valence += float(self.edge_labels(edge, semantic=True)["pi_order"])
454
+ values["aromatic_pi_valence"] = pi_valence
455
+ return values
456
+
457
+ def _matching_edge_labels(self, edge: EdgeKey) -> dict[str, Any]:
458
+ """Return semantic edge labels with Kekulé phase normalized.
459
+
460
+ :param edge: Simple undirected edge key.
461
+ :type edge: EdgeKey
462
+ :return: Labels used by isomorphisms and morphisms.
463
+ :rtype: dict[str, Any]
464
+ """
465
+ values = self.edge_labels(edge, semantic=True)
466
+ if self._is_kekule_aromatic_edge(edge):
467
+ values["sigma_order"] = "aromatic_delocalized"
468
+ values["pi_order"] = "aromatic_delocalized"
469
+ return values
470
+
471
+ def _is_kekule_aromatic_edge(self, edge: EdgeKey) -> bool:
472
+ """Return whether an edge belongs to a normal Kekulé aromatic system.
473
+
474
+ :param edge: Simple undirected edge key.
475
+ :type edge: EdgeKey
476
+ :return: Whether Kekulé phase may be normalized for matching.
477
+ :rtype: bool
478
+ """
479
+ if self.partial or not is_electron_schema(self.schema):
480
+ return False
481
+ endpoints = tuple(edge)
482
+ if len(endpoints) != 2 or not all(
483
+ self.node_labels(node, semantic=True).get("aromatic", False)
484
+ for node in endpoints
485
+ ):
486
+ return False
487
+ values = self.edge_labels(edge, semantic=True)
488
+ return _numbers_equal(values.get("sigma_order"), 1) and any(
489
+ _numbers_equal(values.get("pi_order"), phase) for phase in (0, 1)
490
+ )
491
+
492
+ def _aromatic_component(self, node: Hashable) -> frozenset[Hashable]:
493
+ """Return the normal Kekulé aromatic component containing a node.
494
+
495
+ :param node: Carrier node.
496
+ :type node: Hashable
497
+ :return: Aromatic component, or an empty set for a non-aromatic node.
498
+ :rtype: frozenset[Hashable]
499
+ """
500
+ if not self.node_labels(node, semantic=True).get("aromatic", False):
501
+ return frozenset()
502
+ graph = nx.Graph()
503
+ graph.add_node(node)
504
+ graph.add_edges_from(
505
+ tuple(edge)
506
+ for edge in self.edge_keys
507
+ if self._is_kekule_aromatic_edge(edge)
508
+ )
509
+ return frozenset(nx.node_connected_component(graph, node))
510
+
511
+ def _matching_graph(self) -> nx.Graph:
512
+ """Return a graph containing only normalized matching labels.
513
+
514
+ :return: Mutable semantic comparison graph.
515
+ :rtype: nx.Graph
516
+ """
517
+ graph = nx.Graph()
518
+ graph.add_nodes_from(
519
+ (node, {"labels": self._matching_node_labels(node)})
520
+ for node in self.node_ids
521
+ )
522
+ for edge in self.edge_keys:
523
+ left, right = tuple(edge)
524
+ graph.add_edge(
525
+ left,
526
+ right,
527
+ labels=self._matching_edge_labels(edge),
528
+ )
529
+ return graph
530
+
531
+ def to_networkx(self) -> nx.Graph:
532
+ """Return a mutable copy. Frozen container values remain immutable."""
533
+ graph = nx.Graph()
534
+ graph.add_nodes_from(
535
+ (node, _bundle_dict(labels)) for node, labels in self.nodes
536
+ )
537
+ for edge, labels in self.edges:
538
+ left, right = tuple(edge)
539
+ graph.add_edge(left, right, **_bundle_dict(labels))
540
+ return graph
541
+
542
+ def relabel(self, mapping: Mapping[Hashable, Hashable]) -> "LewisLabelledGraph":
543
+ """Transport an object along a total carrier bijection."""
544
+ if set(mapping) != set(self.node_ids) or len(set(mapping.values())) != len(
545
+ mapping
546
+ ):
547
+ raise LLGError(
548
+ LLGIssue(
549
+ LLGIssueCode.INVALID_RELABELING,
550
+ "An LLG relabeling must be a total carrier bijection.",
551
+ )
552
+ )
553
+ return LewisLabelledGraph(
554
+ self.schema,
555
+ tuple((mapping[node], labels) for node, labels in self.nodes),
556
+ tuple(
557
+ (frozenset(mapping[node] for node in edge), labels)
558
+ for edge, labels in self.edges
559
+ ),
560
+ self.name,
561
+ self.partial,
562
+ )
563
+
564
+ def is_isomorphic(self, other: "LewisLabelledGraph") -> bool:
565
+ """Decide labeled-graph isomorphism; no digest is a proof.
566
+
567
+ Kekulé single/double placement is phase-invariant inside electron LLG
568
+ aromatic systems. Aromatic node state and local pi valence remain part
569
+ of the match, so phase normalization does not erase electron counts.
570
+ """
571
+ if self.schema != other.schema:
572
+ return False
573
+ matcher = nx.algorithms.isomorphism.GraphMatcher(
574
+ self._matching_graph(),
575
+ other._matching_graph(),
576
+ node_match=lambda left, right: left["labels"] == right["labels"],
577
+ edge_match=lambda left, right: left["labels"] == right["labels"],
578
+ )
579
+ return matcher.is_isomorphic()
580
+
581
+
582
+ def _complete_aromatic_morphism_nodes(
583
+ source: LewisLabelledGraph,
584
+ target: LewisLabelledGraph,
585
+ mapping: Mapping[Hashable, Hashable],
586
+ ) -> frozenset[Hashable]:
587
+ """Return source nodes whose complete aromatic systems are mapped.
588
+
589
+ :param source: Morphism source.
590
+ :type source: LewisLabelledGraph
591
+ :param target: Morphism target.
592
+ :type target: LewisLabelledGraph
593
+ :param mapping: Total source-to-target node mapping.
594
+ :type mapping: Mapping[Hashable, Hashable]
595
+ :return: Nodes eligible for Kekulé-phase-invariant comparison.
596
+ :rtype: frozenset[Hashable]
597
+ """
598
+ complete: set[Hashable] = set()
599
+ for node in source.node_ids:
600
+ source_component = source._aromatic_component(node)
601
+ if not source_component or source_component & complete:
602
+ continue
603
+ target_component = target._aromatic_component(mapping[node])
604
+ mapped_component = frozenset(mapping[item] for item in source_component)
605
+ if mapped_component == target_component:
606
+ complete.update(source_component)
607
+ return frozenset(complete)
608
+
609
+
610
+ def _morphism_node_labels_preserved(
611
+ source: LewisLabelledGraph,
612
+ target: LewisLabelledGraph,
613
+ node: Hashable,
614
+ image: Hashable,
615
+ phase_invariant_nodes: frozenset[Hashable],
616
+ ) -> bool:
617
+ """Return whether one mapped node preserves matching semantics.
618
+
619
+ :param source: Morphism source.
620
+ :type source: LewisLabelledGraph
621
+ :param target: Morphism target.
622
+ :type target: LewisLabelledGraph
623
+ :param node: Source node.
624
+ :type node: Hashable
625
+ :param image: Target image node.
626
+ :type image: Hashable
627
+ :param phase_invariant_nodes: Complete mapped aromatic-system nodes.
628
+ :type phase_invariant_nodes: frozenset[Hashable]
629
+ :return: Whether node labels are preserved.
630
+ :rtype: bool
631
+ """
632
+ if node in phase_invariant_nodes:
633
+ return source._matching_node_labels(node) == target._matching_node_labels(image)
634
+ return source.node_labels(node, semantic=True) == target.node_labels(
635
+ image, semantic=True
636
+ )
637
+
638
+
639
+ def _morphism_edge_labels_preserved(
640
+ source: LewisLabelledGraph,
641
+ target: LewisLabelledGraph,
642
+ edge: EdgeKey,
643
+ image_edge: EdgeKey,
644
+ phase_invariant_nodes: frozenset[Hashable],
645
+ ) -> bool:
646
+ """Return whether one mapped edge preserves matching semantics.
647
+
648
+ :param source: Morphism source.
649
+ :type source: LewisLabelledGraph
650
+ :param target: Morphism target.
651
+ :type target: LewisLabelledGraph
652
+ :param edge: Source edge.
653
+ :type edge: EdgeKey
654
+ :param image_edge: Target image edge.
655
+ :type image_edge: EdgeKey
656
+ :param phase_invariant_nodes: Complete mapped aromatic-system nodes.
657
+ :type phase_invariant_nodes: frozenset[Hashable]
658
+ :return: Whether edge labels are preserved.
659
+ :rtype: bool
660
+ """
661
+ if edge <= phase_invariant_nodes:
662
+ return source._matching_edge_labels(edge) == target._matching_edge_labels(
663
+ image_edge
664
+ )
665
+ return source.edge_labels(edge, semantic=True) == target.edge_labels(
666
+ image_edge, semantic=True
667
+ )
668
+
669
+
670
+ @dataclass(frozen=True)
671
+ class LLGMorphism:
672
+ """A strict injective incidence- and label-preserving LLG morphism."""
673
+
674
+ source: LewisLabelledGraph
675
+ target: LewisLabelledGraph
676
+ f: tuple[tuple[Hashable, Hashable], ...]
677
+
678
+ def __post_init__(self) -> None:
679
+ pairs = tuple(self.f.items()) if isinstance(self.f, Mapping) else tuple(self.f)
680
+ object.__setattr__(
681
+ self,
682
+ "f",
683
+ tuple(
684
+ sorted(
685
+ pairs,
686
+ key=lambda item: (_carrier_key(item[0]), _carrier_key(item[1])),
687
+ )
688
+ ),
689
+ )
690
+ issues: list[LLGIssue] = []
691
+ if self.source.schema != self.target.schema:
692
+ issues.append(
693
+ LLGIssue(
694
+ LLGIssueCode.NODE_LABEL_MISMATCH,
695
+ "Morphism endpoints must use the same label schema.",
696
+ )
697
+ )
698
+ source_keys = tuple(left for left, _ in pairs)
699
+ target_values = tuple(right for _, right in pairs)
700
+ outside_source = set(source_keys) - set(self.source.node_ids)
701
+ outside_target = set(target_values) - set(self.target.node_ids)
702
+ missing = set(self.source.node_ids) - set(source_keys)
703
+ if outside_source:
704
+ issues.append(
705
+ LLGIssue(
706
+ LLGIssueCode.OUTSIDE_SOURCE,
707
+ "The map contains a node outside its source.",
708
+ {"nodes": tuple(sorted(map(repr, outside_source)))},
709
+ )
710
+ )
711
+ if outside_target:
712
+ issues.append(
713
+ LLGIssue(
714
+ LLGIssueCode.OUTSIDE_TARGET,
715
+ "The map contains an image outside its target.",
716
+ {"nodes": tuple(sorted(map(repr, outside_target)))},
717
+ )
718
+ )
719
+ if len(set(source_keys)) != len(source_keys) or missing:
720
+ issues.append(
721
+ LLGIssue(
722
+ LLGIssueCode.PARTIAL_MAPPING,
723
+ "An LLG morphism maps each source node exactly once.",
724
+ {"missing": tuple(sorted(map(repr, missing)))},
725
+ )
726
+ )
727
+ if len(set(target_values)) != len(target_values):
728
+ issues.append(
729
+ LLGIssue(
730
+ LLGIssueCode.NON_INJECTIVE,
731
+ "An LLG morphism is injective on material nodes.",
732
+ )
733
+ )
734
+ if issues:
735
+ raise LLGError(*issues)
736
+
737
+ mapping = dict(pairs)
738
+ phase_invariant_nodes = _complete_aromatic_morphism_nodes(
739
+ self.source, self.target, mapping
740
+ )
741
+ for node, image in pairs:
742
+ if not _morphism_node_labels_preserved(
743
+ self.source,
744
+ self.target,
745
+ node,
746
+ image,
747
+ phase_invariant_nodes,
748
+ ):
749
+ issues.append(
750
+ LLGIssue(
751
+ LLGIssueCode.NODE_LABEL_MISMATCH,
752
+ "A node identity or state label is not preserved.",
753
+ {"source": repr(node), "target": repr(image)},
754
+ )
755
+ )
756
+ target_edges = self.target.edge_keys
757
+ for edge in self.source.edge_keys:
758
+ image_edge = frozenset(mapping[node] for node in edge)
759
+ if image_edge not in target_edges:
760
+ issues.append(
761
+ LLGIssue(
762
+ LLGIssueCode.MISSING_EDGE,
763
+ "The induced edge image is absent from the target.",
764
+ {"edge": tuple(sorted(map(repr, edge)))},
765
+ )
766
+ )
767
+ continue
768
+ if not _morphism_edge_labels_preserved(
769
+ self.source,
770
+ self.target,
771
+ edge,
772
+ image_edge,
773
+ phase_invariant_nodes,
774
+ ):
775
+ issues.append(
776
+ LLGIssue(
777
+ LLGIssueCode.EDGE_LABEL_MISMATCH,
778
+ "An edge identity or state label is not preserved.",
779
+ {"edge": tuple(sorted(map(repr, edge)))},
780
+ )
781
+ )
782
+ if issues:
783
+ raise LLGError(*issues)
784
+
785
+ @property
786
+ def mapping(self) -> dict[Hashable, Hashable]:
787
+ return dict(self.f)
788
+
789
+ @property
790
+ def edge_mapping(self) -> dict[EdgeKey, EdgeKey]:
791
+ mapping = self.mapping
792
+ return {
793
+ edge: frozenset(mapping[node] for node in edge)
794
+ for edge in self.source.edge_keys
795
+ }
796
+
797
+ @property
798
+ def is_isomorphism(self) -> bool:
799
+ return len(self.source.node_ids) == len(self.target.node_ids) and len(
800
+ self.source.edge_keys
801
+ ) == len(self.target.edge_keys)
802
+
803
+ @classmethod
804
+ def identity(cls, graph: LewisLabelledGraph) -> "LLGMorphism":
805
+ return cls(graph, graph, tuple((node, node) for node in graph.node_ids))
806
+
807
+ def then(self, after: "LLGMorphism") -> "LLGMorphism":
808
+ """Return ``after ∘ self``."""
809
+ if self.target != after.source:
810
+ raise LLGError(
811
+ LLGIssue(
812
+ LLGIssueCode.ENDPOINT_MISMATCH,
813
+ "Composable LLG morphisms must share the same object.",
814
+ )
815
+ )
816
+ right = after.mapping
817
+ return LLGMorphism(
818
+ self.source,
819
+ after.target,
820
+ tuple((node, right[image]) for node, image in self.f),
821
+ )
822
+
823
+ def compose(self, after: "LLGMorphism") -> "LLGMorphism":
824
+ return self.then(after)
825
+
826
+ def relabel(
827
+ self,
828
+ source_labels: Mapping[Hashable, Hashable],
829
+ target_labels: Mapping[Hashable, Hashable],
830
+ ) -> "LLGMorphism":
831
+ source = self.source.relabel(source_labels)
832
+ target = self.target.relabel(target_labels)
833
+ return LLGMorphism(
834
+ source,
835
+ target,
836
+ tuple(
837
+ (source_labels[left], target_labels[right]) for left, right in self.f
838
+ ),
839
+ )
840
+
841
+
842
+ def derive_electron_labeled_graph(graph: nx.Graph) -> nx.Graph:
843
+ """Return a copy with deterministic Lewis-derived charge and bond orders.
844
+
845
+ Authoritative fields are ``valence_electrons``, ``hcount``, ``lone_pairs``,
846
+ ``radical``, ``sigma_order``, and ``pi_order``. A stored derived value may
847
+ be absent; if present and inconsistent, construction fails rather than
848
+ silently rewriting it.
849
+ """
850
+ result = graph.copy()
851
+ for left, right, attrs in result.edges(data=True):
852
+ try:
853
+ total = float(attrs["sigma_order"]) + float(attrs["pi_order"])
854
+ except KeyError as error:
855
+ raise LLGError(
856
+ LLGIssue(
857
+ LLGIssueCode.MISSING_EDGE_LABEL,
858
+ "Electron LLG edges require sigma_order and pi_order.",
859
+ {"edge": (repr(left), repr(right)), "label": str(error)},
860
+ )
861
+ ) from error
862
+ for key in ("order", "kekule_order"):
863
+ if key in attrs and not _numbers_equal(attrs[key], total):
864
+ raise LLGError(
865
+ LLGIssue(
866
+ LLGIssueCode.DERIVED_LABEL_MISMATCH,
867
+ f"Stored {key} disagrees with sigma+pi order.",
868
+ {
869
+ "edge": (repr(left), repr(right)),
870
+ "stored": attrs[key],
871
+ "derived": total,
872
+ },
873
+ )
874
+ )
875
+ attrs[key] = total
876
+
877
+ for node, attrs in result.nodes(data=True):
878
+ required = {"valence_electrons", "hcount", "lone_pairs", "radical"}
879
+ missing = required - set(attrs)
880
+ if missing:
881
+ raise LLGError(
882
+ LLGIssue(
883
+ LLGIssueCode.MISSING_NODE_LABEL,
884
+ "Electron LLG nodes lack authoritative electron state.",
885
+ {"node": repr(node), "labels": tuple(sorted(missing))},
886
+ )
887
+ )
888
+ bond_sum = sum(
889
+ float(edge_attrs["sigma_order"]) + float(edge_attrs["pi_order"])
890
+ for _, _, edge_attrs in result.edges(node, data=True)
891
+ )
892
+ charge = (
893
+ float(attrs["valence_electrons"])
894
+ - 2.0 * float(attrs["lone_pairs"])
895
+ - float(attrs["radical"])
896
+ - float(attrs["hcount"])
897
+ - bond_sum
898
+ )
899
+ charge = int(charge) if charge.is_integer() else charge
900
+ for key, derived in (("bond_order_sum", bond_sum), ("charge", charge)):
901
+ if key in attrs and not _numbers_equal(attrs[key], derived):
902
+ raise LLGError(
903
+ LLGIssue(
904
+ LLGIssueCode.DERIVED_LABEL_MISMATCH,
905
+ f"Stored {key} disagrees with authoritative electron state.",
906
+ {"node": repr(node), "stored": attrs[key], "derived": derived},
907
+ )
908
+ )
909
+ attrs[key] = derived
910
+ return result
911
+
912
+
913
+ def _numbers_equal(left: Any, right: Any) -> bool:
914
+ if isinstance(left, Real) and isinstance(right, Real):
915
+ return float(left) == float(right)
916
+ return left == right
917
+
918
+
919
+ def llg_from_its(
920
+ graph: nx.Graph,
921
+ side: str | int,
922
+ *,
923
+ electron_complete: bool = False,
924
+ ) -> LewisLabelledGraph:
925
+ """Adapt tuple or legacy ``typesGH`` ITS data to one LLG value contract.
926
+
927
+ The common schema is lossless for element, aromaticity, H count, charge,
928
+ and total bond order in both formats. Electron-complete adaptation is
929
+ available only for tuple ITS because legacy ``typesGH`` does not retain
930
+ side-specific lone pairs, radicals, or sigma/pi decomposition.
931
+ """
932
+ from synkit.Graph.ITS.its_decompose import its_decompose
933
+ from synkit.Graph.ITS.its_reverter import ITSReverter
934
+ from synkit.IO.chem_converter import detect_its_format
935
+
936
+ side_index = {
937
+ "reactant": 0,
938
+ "r": 0,
939
+ "left": 0,
940
+ 0: 0,
941
+ "product": 1,
942
+ "p": 1,
943
+ "right": 1,
944
+ 1: 1,
945
+ }.get(side)
946
+ if side_index is None:
947
+ raise ValueError(f"Unsupported ITS side: {side!r}")
948
+ representation = detect_its_format(graph)
949
+ if representation == "tuple":
950
+ molecular = ITSReverter(graph).to_graph(side_index)
951
+ elif representation == "typesGH":
952
+ if electron_complete:
953
+ raise LLGError(
954
+ LLGIssue(
955
+ LLGIssueCode.LOSSY_ELECTRON_ADAPTER,
956
+ "Legacy typesGH lacks side-specific electron fields.",
957
+ )
958
+ )
959
+ molecular = its_decompose(graph)[side_index]
960
+ else: # pragma: no cover - detect_its_format is a closed public contract
961
+ raise LLGError(
962
+ LLGIssue(
963
+ LLGIssueCode.UNSUPPORTED_ITS_FORMAT,
964
+ "Unsupported ITS representation.",
965
+ {"format": repr(representation)},
966
+ )
967
+ )
968
+
969
+ for node in molecular:
970
+ molecular.nodes[node]["source_format"] = representation
971
+ molecular.nodes[node].setdefault("atom_map", node)
972
+ if electron_complete:
973
+ molecular = derive_electron_labeled_graph(molecular)
974
+ return LewisLabelledGraph.from_networkx(
975
+ molecular, ELECTRON_LLG_SCHEMA, name=(representation, side_index)
976
+ )
977
+
978
+ for _, _, attrs in molecular.edges(data=True):
979
+ if "order" not in attrs and "kekule_order" in attrs:
980
+ attrs["order"] = attrs["kekule_order"]
981
+ return LewisLabelledGraph.from_networkx(
982
+ molecular, COMMON_CHEMICAL_SCHEMA, name=(representation, side_index)
983
+ )
984
+
985
+
986
+ __all__ = [
987
+ "COMMON_CHEMICAL_SCHEMA",
988
+ "ELECTRON_LLG_SCHEMA",
989
+ "ISOTOPE_ELECTRON_LLG_SCHEMA",
990
+ "LLGError",
991
+ "LLGIssue",
992
+ "LLGIssueCode",
993
+ "LLGMorphism",
994
+ "LabelSchema",
995
+ "LewisLabelledGraph",
996
+ "derive_electron_labeled_graph",
997
+ "is_electron_schema",
998
+ "llg_from_its",
999
+ ]