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
synkit/CRN/analysis.py ADDED
@@ -0,0 +1,629 @@
1
+ """Scenario-based biochemical audits and independently replayed pathways.
2
+
3
+ Expensive enumeration is deliberately separate from the routine audit. Instances
4
+ own a detached snapshot; construct a new instance after editing a network.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from copy import deepcopy
10
+ from dataclasses import asdict, dataclass, field
11
+ from itertools import product
12
+ from math import prod
13
+ from time import monotonic
14
+ from typing import Any, Mapping
15
+
16
+ import numpy as np
17
+ from scipy.optimize import linprog
18
+
19
+ from .Structure import SynCRN
20
+ from .Props.stoich import _exact_null_space
21
+ from .Props.deficiency import crnt_summary
22
+ from .Pathway import PathwayRealizability
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class AnalysisResult:
27
+ value: Any
28
+ status: str = "complete"
29
+ complete: bool = True
30
+ method: str = "structural"
31
+ assumptions: dict = field(default_factory=dict)
32
+ certificate: Any = None
33
+ termination_reason: str = "complete"
34
+
35
+ def to_dict(self):
36
+ return asdict(self)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Scenario:
41
+ """Species selectors accept unique ids, source ids or labels.
42
+
43
+ Chemostats have unlimited supply in discrete execution. Currency deletion
44
+ removes incidences and changes the model. Neither happens implicitly.
45
+ """
46
+
47
+ chemostats: tuple[str, ...] = ()
48
+ drop_species: tuple[str, ...] = ()
49
+ initial_marking: Mapping[str, int] = field(default_factory=dict)
50
+
51
+ @classmethod
52
+ def from_model(cls, crn, *, initial_marking=None):
53
+ """Use declared SBML boundary species; never infer token counts from concentrations."""
54
+ return cls(
55
+ chemostats=tuple(
56
+ sid
57
+ for sid, sp in crn.species.items()
58
+ if str(sp.source_attrs.get("boundaryCondition", "")).lower() == "true"
59
+ ),
60
+ initial_marking={} if initial_marking is None else dict(initial_marking),
61
+ )
62
+
63
+
64
+ class CRNAnalysis:
65
+ """Prepared, detached model and explicit biochemical scenario."""
66
+
67
+ def __init__(self, crn: Any, scenario: Scenario | None = None):
68
+ self.scenario = deepcopy(scenario or Scenario())
69
+ self._original = deepcopy(
70
+ crn if isinstance(crn, SynCRN) else SynCRN.from_digraph(crn)
71
+ )
72
+ self._crn = deepcopy(self._original)
73
+ dropped = {self._resolve(x) for x in self.scenario.drop_species}
74
+ self.chemostats = {self._resolve(x) for x in self.scenario.chemostats}
75
+ if dropped & self.chemostats:
76
+ raise ValueError("A species cannot be both removed and chemostatted")
77
+ for reaction in self._crn.reactions.values():
78
+ for side in (reaction.lhs, reaction.rhs):
79
+ for sid in dropped:
80
+ side.counts.pop(sid, None)
81
+ for sid in dropped:
82
+ del self._crn.species[sid]
83
+ self._matrices = self._crn.to_stoichiometric_matrices()
84
+ self.species_order = tuple(self._matrices["species_order"])
85
+ self.reaction_order = tuple(self._matrices["reaction_order"])
86
+ self._structure = None
87
+
88
+ @property
89
+ def model(self):
90
+ return deepcopy(self._crn)
91
+
92
+ @property
93
+ def matrices(self):
94
+ return deepcopy(self._matrices)
95
+
96
+ def _resolve(self, token, *, reaction=False):
97
+ records = self._crn.reactions if reaction else self._crn.species
98
+ if token in records:
99
+ return token
100
+ matches = [
101
+ key
102
+ for key, record in records.items()
103
+ if str(token) in {str(record.label), str(record.source_node_id)}
104
+ ]
105
+ if len(matches) != 1:
106
+ raise ValueError(
107
+ f"Unknown or ambiguous {'reaction' if reaction else 'species'}: {token!r}"
108
+ )
109
+ return matches[0]
110
+
111
+ @staticmethod
112
+ def _counts(values):
113
+ if any(
114
+ isinstance(v, bool) or not isinstance(v, (int, np.integer)) or v < 0
115
+ for v in values.values()
116
+ ):
117
+ raise ValueError(
118
+ "Discrete inventories and flows require nonnegative integers"
119
+ )
120
+ return {k: int(v) for k, v in values.items() if v}
121
+
122
+ def _species_compositions(self):
123
+ from rdkit import Chem
124
+
125
+ compositions = {}
126
+ for sid, species in self._crn.species.items():
127
+ formula = species.source_attrs.get("chemicalFormula")
128
+ charge = species.source_attrs.get("charge")
129
+ if formula and charge is not None:
130
+ import re
131
+
132
+ parts = re.findall(r"([A-Z][a-z]?)([0-9]*)", formula)
133
+ if "".join(e + n for e, n in parts) == formula:
134
+ counts = {}
135
+ for element, number in parts:
136
+ counts[element] = counts.get(element, 0) + int(number or 1)
137
+ counts["charge"] = float(charge)
138
+ compositions[sid] = counts
139
+ mol = Chem.MolFromSmiles(species.smiles) if species.smiles else None
140
+ if mol is not None:
141
+ counts = {}
142
+ for atom in Chem.AddHs(mol).GetAtoms():
143
+ counts[atom.GetSymbol()] = counts.get(atom.GetSymbol(), 0) + 1
144
+ counts["charge"] = sum(a.GetFormalCharge() for a in mol.GetAtoms())
145
+ compositions[sid] = counts
146
+ return compositions
147
+
148
+ def audit(self):
149
+ """Cheap model diagnostics; chemical balance is unknown without structures."""
150
+ signatures, duplicates, empty, unbalanced, unknown = {}, [], [], [], []
151
+ compositions = self._species_compositions()
152
+ involved = set()
153
+ for rid, reaction in self._crn.reactions.items():
154
+ sig = (
155
+ tuple(sorted(reaction.lhs.items())),
156
+ tuple(sorted(reaction.rhs.items())),
157
+ )
158
+ if sig in signatures:
159
+ duplicates.append((signatures[sig], rid))
160
+ signatures[sig] = rid
161
+ if not reaction.lhs or not reaction.rhs:
162
+ empty.append(rid)
163
+ participants = set(dict(reaction.lhs.items())) | set(
164
+ dict(reaction.rhs.items())
165
+ )
166
+ involved |= participants
167
+ if not participants <= compositions.keys():
168
+ unknown.append(rid)
169
+ continue
170
+ balance = {}
171
+ for sign, side in ((-1, reaction.lhs), (1, reaction.rhs)):
172
+ for sid, coeff in side.items():
173
+ for element, count in compositions[sid].items():
174
+ balance[element] = (
175
+ balance.get(element, 0) + sign * coeff * count
176
+ )
177
+ if any(balance.values()):
178
+ unbalanced.append(
179
+ {
180
+ "reaction": rid,
181
+ "imbalance": {k: v for k, v in balance.items() if v},
182
+ }
183
+ )
184
+ return AnalysisResult(
185
+ dict(
186
+ n_species=len(self.species_order),
187
+ n_reactions=len(self.reaction_order),
188
+ duplicate_reactions=duplicates,
189
+ open_reactions=empty,
190
+ isolated_species=sorted(set(self.species_order) - involved),
191
+ unbalanced_reactions=unbalanced,
192
+ balance_unknown=unknown,
193
+ ),
194
+ assumptions=self._assumptions(),
195
+ )
196
+
197
+ def _assumptions(self):
198
+ return {
199
+ "chemostats": sorted(self.chemostats),
200
+ "drop_species": list(self.scenario.drop_species),
201
+ "inventory_semantics": "integer tokens; concentrations are not converted",
202
+ }
203
+
204
+ def conservation(self):
205
+ s = self._matrices["S"]
206
+ order = tuple(sid for sid in self.species_order if sid not in self.chemostats)
207
+ s = s[
208
+ [
209
+ i
210
+ for i, sid in enumerate(self.species_order)
211
+ if sid not in self.chemostats
212
+ ],
213
+ :,
214
+ ]
215
+ laws = _exact_null_space(s.T.tolist(), len(order))
216
+ return AnalysisResult(
217
+ {
218
+ "species_order": order,
219
+ "basis": laws,
220
+ "dimension": len(laws),
221
+ "rank": len(order) - len(laws),
222
+ },
223
+ method="rational elimination",
224
+ assumptions=self._assumptions(),
225
+ )
226
+
227
+ def structure(self):
228
+ if self._structure is None:
229
+ self._structure = crnt_summary(self._crn).to_dict()
230
+ value = deepcopy(self._structure)
231
+ value["scope"] = "reaction equations including external species"
232
+ if self.chemostats:
233
+ value["deficiency_zero"] = {
234
+ "applicable": None,
235
+ "reason": "Chemostatted dynamics require a separately reduced model",
236
+ }
237
+ value["deficiency_one"] = {
238
+ "applicable": None,
239
+ "reason": "Chemostatted dynamics require a separately reduced model",
240
+ }
241
+ return AnalysisResult(value, assumptions=self._assumptions())
242
+
243
+ def accessibility(self, seeds=()):
244
+ available = {self._resolve(s) for s in seeds} | self.chemostats
245
+ fired, witnesses = set(), {}
246
+ changed = True
247
+ while changed:
248
+ changed = False
249
+ for rid, reaction in self._crn.reactions.items():
250
+ if rid not in fired and set(dict(reaction.lhs.items())) <= available:
251
+ fired.add(rid)
252
+ for sid, _ in reaction.rhs.items():
253
+ if sid not in available:
254
+ witnesses[sid] = rid
255
+ available.add(sid)
256
+ changed = True
257
+ return AnalysisResult(
258
+ {
259
+ "reachable_species": sorted(available),
260
+ "enabled_reactions": sorted(fired),
261
+ "unreachable_species": sorted(set(self.species_order) - available),
262
+ },
263
+ method="qualitative closure",
264
+ certificate=witnesses,
265
+ assumptions=self._assumptions(),
266
+ )
267
+
268
+ def flux_feasibility(self, *, bounds=None, required=None, time_limit=30.0):
269
+ """Steady-state feasibility on internal species, not discrete execution.
270
+
271
+ Bounds default to nonnegative directed fluxes. Require a reaction to
272
+ exclude the zero solution. External supply must be declared explicitly.
273
+ """
274
+ if time_limit <= 0:
275
+ raise ValueError("time_limit must be positive")
276
+ bounds = (
277
+ {}
278
+ if bounds is None
279
+ else {self._resolve(k, reaction=True): v for k, v in bounds.items()}
280
+ )
281
+ required = (
282
+ {}
283
+ if required is None
284
+ else {self._resolve(k, reaction=True): v for k, v in required.items()}
285
+ )
286
+ limits = []
287
+ for rid in self.reaction_order:
288
+ lo, hi = bounds.get(rid, (0, None))
289
+ lo = max(0 if lo is None else lo, required.get(rid, 0))
290
+ if hi is not None and hi < lo:
291
+ return AnalysisResult(
292
+ False,
293
+ certificate={"conflicting_bounds": rid},
294
+ assumptions=self._assumptions(),
295
+ )
296
+ limits.append((lo, hi))
297
+ if not limits:
298
+ return AnalysisResult(True, certificate={}, assumptions=self._assumptions())
299
+ internal = [
300
+ i for i, sid in enumerate(self.species_order) if sid not in self.chemostats
301
+ ]
302
+ s = self._matrices["S"][internal, :]
303
+ answer = linprog(
304
+ np.zeros(len(limits)),
305
+ A_eq=s,
306
+ b_eq=np.zeros(len(internal)),
307
+ bounds=limits,
308
+ method="highs",
309
+ options={"time_limit": time_limit},
310
+ )
311
+ if answer.status == 2:
312
+ return AnalysisResult(
313
+ False, method="HiGHS numerical LP", assumptions=self._assumptions()
314
+ )
315
+ valid = answer.success and np.max(np.abs(s @ answer.x), initial=0) <= 1e-7
316
+ if valid:
317
+ valid = all(
318
+ x >= (lo * (1 - 1e-7) if lo > 0 else lo - 1e-7)
319
+ and (hi is None or x <= hi + 1e-7)
320
+ for x, (lo, hi) in zip(answer.x, limits)
321
+ )
322
+ return AnalysisResult(
323
+ True if valid else None,
324
+ "complete" if valid else "inconclusive",
325
+ bool(valid),
326
+ "HiGHS numerical LP",
327
+ self._assumptions(),
328
+ dict(zip(self.reaction_order, map(float, answer.x))) if valid else None,
329
+ "complete" if valid else str(answer.message),
330
+ )
331
+
332
+ def blocked_reactions(self, *, bounds=None, time_limit=30.0, threshold=1e-6):
333
+ """Classify ability to carry at least threshold forward steady-state flux."""
334
+ if threshold <= 0:
335
+ raise ValueError("threshold must be positive")
336
+ deadline = monotonic() + time_limit
337
+ blocked, active, unknown = [], [], []
338
+ for rid in self.reaction_order:
339
+ remaining = deadline - monotonic()
340
+ result = (
341
+ self.flux_feasibility(
342
+ bounds=bounds, required={rid: threshold}, time_limit=remaining
343
+ )
344
+ if remaining > 0
345
+ else AnalysisResult(None)
346
+ )
347
+ (
348
+ blocked
349
+ if result.value is False
350
+ else active if result.value is True else unknown
351
+ ).append(rid)
352
+ return AnalysisResult(
353
+ {"blocked": blocked, "active": active, "unknown": unknown},
354
+ "inconclusive" if unknown else "complete",
355
+ not unknown,
356
+ "threshold LP feasibility",
357
+ {**self._assumptions(), "threshold": threshold},
358
+ )
359
+
360
+ def verify_pathway(
361
+ self, flow, initial_marking=None, *, max_states=100000, max_depth=10000
362
+ ):
363
+ flow = {
364
+ self._resolve(k, reaction=True): v for k, v in self._counts(flow).items()
365
+ }
366
+ marking = (
367
+ self.scenario.initial_marking
368
+ if initial_marking is None
369
+ else initial_marking
370
+ )
371
+ marking = {self._resolve(k): v for k, v in self._counts(marking).items()}
372
+ model = deepcopy(self._crn)
373
+ for reaction in model.reactions.values():
374
+ for side in (reaction.lhs, reaction.rhs):
375
+ for sid in self.chemostats:
376
+ side.counts.pop(sid, None)
377
+ checker = PathwayRealizability().load_syncrn_and_flow(
378
+ model, flow=flow, initial_marking=marking, species="id", reaction="id"
379
+ )
380
+ checker.build_petri_net_from_flow()
381
+ outcome = checker.realizability_result(
382
+ max_states=max_states, max_depth=max_depth
383
+ ).to_dict()
384
+ inventory = dict(marking)
385
+ trajectory = [dict(inventory)]
386
+ counts = {}
387
+ for rid in outcome.get("certificate") or []:
388
+ reaction = model.reactions[rid]
389
+ if any(inventory.get(s, 0) < n for s, n in reaction.lhs.items()):
390
+ raise RuntimeError("Returned certificate is not enabled")
391
+ for sign, side in ((-1, reaction.lhs), (1, reaction.rhs)):
392
+ for sid, n in side.items():
393
+ inventory[sid] = inventory.get(sid, 0) + sign * n
394
+ counts[rid] = counts.get(rid, 0) + 1
395
+ trajectory.append(dict(inventory))
396
+ if outcome["status"] == "realizable" and counts != flow:
397
+ raise RuntimeError("Certificate does not match the requested flow")
398
+ outcome.update(
399
+ trajectory=trajectory,
400
+ replay_verified=outcome["status"] == "realizable",
401
+ assumptions=self._assumptions(),
402
+ )
403
+ return outcome
404
+
405
+ def pathways(
406
+ self,
407
+ seeds,
408
+ targets,
409
+ *,
410
+ initial_marking=None,
411
+ max_depth=20,
412
+ max_paths=10,
413
+ max_states=100000,
414
+ ):
415
+ """Suggest qualitative routes and verify each against finite inventory.
416
+
417
+ Candidate search is bounded and qualitative; absent candidates never
418
+ constitute a proof of discrete unreachability or route completeness.
419
+ """
420
+ from .Pathway import PathwayFinder
421
+
422
+ source = {self._resolve(s) for s in seeds} | self.chemostats
423
+ target = {self._resolve(s) for s in targets}
424
+ finder = PathwayFinder().load_syncrn(self._crn, species="id", reaction="id")
425
+ candidates = finder.find_paths_set(
426
+ source_species=source,
427
+ target_species=target,
428
+ max_depth=max_depth,
429
+ max_paths=max_paths,
430
+ stop_after_first=False,
431
+ )
432
+ rows = []
433
+ for candidate in candidates:
434
+ outcome = self.verify_pathway(
435
+ candidate.flow,
436
+ initial_marking,
437
+ max_states=max_states,
438
+ max_depth=max_depth,
439
+ )
440
+ rows.append(
441
+ {
442
+ "flow": candidate.flow,
443
+ "qualitative_sequence": candidate.reactions,
444
+ "execution": outcome,
445
+ }
446
+ )
447
+ return AnalysisResult(
448
+ rows,
449
+ "inconclusive",
450
+ False,
451
+ "bounded qualitative candidates plus discrete replay",
452
+ self._assumptions(),
453
+ termination_reason="candidate_search_not_exhaustive",
454
+ )
455
+
456
+ def to_cobra(self, *, bounds, exchange_bounds=None):
457
+ """Optional COBRApy bridge with caller-specified flux bounds.
458
+
459
+ Every reaction and chemostat exchange must have explicit bounds; no
460
+ physiological capacities or exchange directions are inferred.
461
+ """
462
+ try:
463
+ import cobra
464
+ except ImportError as exc:
465
+ raise ImportError(
466
+ "Install cobra separately to use the optional COBRApy bridge"
467
+ ) from exc
468
+ bounds = {self._resolve(k, reaction=True): value for k, value in bounds.items()}
469
+ if set(bounds) != set(self.reaction_order):
470
+ raise ValueError("Explicit bounds are required for every directed reaction")
471
+ exchanges = (
472
+ {}
473
+ if exchange_bounds is None
474
+ else {self._resolve(k): value for k, value in exchange_bounds.items()}
475
+ )
476
+ if set(exchanges) != self.chemostats:
477
+ raise ValueError(
478
+ "Explicit exchange bounds are required for every chemostat"
479
+ )
480
+ model = cobra.Model("synkit_crn")
481
+ metabolites = {}
482
+ for sid, sp in self._crn.species.items():
483
+ metabolites[sid] = cobra.Metabolite(
484
+ sid,
485
+ name=sp.label,
486
+ compartment=sp.source_attrs.get("compartment", "cell"),
487
+ )
488
+ model.add_metabolites(list(metabolites.values()))
489
+ for rid, record in self._crn.reactions.items():
490
+ reaction = cobra.Reaction(rid, name=record.label or rid)
491
+ reaction.bounds = bounds[rid]
492
+ counts = {}
493
+ for sign, side in ((-1, record.lhs), (1, record.rhs)):
494
+ for sid, coefficient in side.items():
495
+ counts[metabolites[sid]] = (
496
+ counts.get(metabolites[sid], 0) + sign * coefficient
497
+ )
498
+ reaction.add_metabolites(counts)
499
+ model.add_reactions([reaction])
500
+ for sid, limits in exchanges.items():
501
+ identifier = "EX_" + sid
502
+ if identifier in model.reactions:
503
+ raise ValueError("Generated exchange id conflicts with a reaction")
504
+ reaction = cobra.Reaction(identifier)
505
+ reaction.bounds = limits
506
+ reaction.add_metabolites({metabolites[sid]: -1})
507
+ model.add_reactions([reaction])
508
+ return model
509
+
510
+ def minimum_supplements(
511
+ self,
512
+ flow,
513
+ *,
514
+ allowed,
515
+ initial_marking=None,
516
+ costs=None,
517
+ max_added=4,
518
+ max_candidates=10000,
519
+ max_states=100000,
520
+ max_depth=10000,
521
+ ):
522
+ """Minimum weighted added tokens within a bounded, explicit search domain.
523
+
524
+ `allowed` lists species; each may receive 0..max_added tokens. A result
525
+ is optimal only within that box, and only if every cheaper candidate
526
+ was conclusively rejected. No global minimality is implied.
527
+ """
528
+ if max_added < 0 or max_candidates < 1:
529
+ raise ValueError("Invalid supplementation search limits")
530
+ ids = sorted({self._resolve(s) for s in allowed})
531
+ costs = {} if costs is None else {self._resolve(s): c for s, c in costs.items()}
532
+ if any(not np.isfinite(c) or c <= 0 for c in costs.values()):
533
+ raise ValueError("Supplement costs must be finite and positive")
534
+ domain = prod([max_added + 1] * len(ids))
535
+ if domain > max_candidates:
536
+ return AnalysisResult(
537
+ None,
538
+ "inconclusive",
539
+ False,
540
+ "bounded exhaustive supplementation",
541
+ termination_reason="candidate_limit",
542
+ )
543
+ base = (
544
+ self.scenario.initial_marking
545
+ if initial_marking is None
546
+ else initial_marking
547
+ )
548
+ base = {self._resolve(s): n for s, n in self._counts(base).items()}
549
+ candidates = sorted(
550
+ product(range(max_added + 1), repeat=len(ids)),
551
+ key=lambda values: (
552
+ sum(costs.get(s, 1) * n for s, n in zip(ids, values)),
553
+ values,
554
+ ),
555
+ )
556
+ uncertain = []
557
+ for values in candidates:
558
+ score = sum(costs.get(s, 1) * n for s, n in zip(ids, values))
559
+ added = {s: n for s, n in zip(ids, values) if n}
560
+ marking = dict(base)
561
+ for s, n in added.items():
562
+ marking[s] = marking.get(s, 0) + n
563
+ outcome = self.verify_pathway(
564
+ flow, marking, max_states=max_states, max_depth=max_depth
565
+ )
566
+ if outcome["status"] == "realizable":
567
+ optimal = not any(c < score for c in uncertain)
568
+ return AnalysisResult(
569
+ {
570
+ "supplements": added,
571
+ "cost": score,
572
+ "optimal_within_bounds": optimal,
573
+ "max_added_per_species": max_added,
574
+ },
575
+ "complete" if optimal else "inconclusive",
576
+ optimal,
577
+ "bounded exhaustive supplementation",
578
+ self._assumptions(),
579
+ outcome,
580
+ "optimum" if optimal else "unresolved_cheaper_candidates",
581
+ )
582
+ if outcome["status"] == "inconclusive":
583
+ uncertain.append(score)
584
+ return AnalysisResult(
585
+ None,
586
+ "inconclusive" if uncertain else "complete",
587
+ not uncertain,
588
+ "bounded exhaustive supplementation",
589
+ self._assumptions(),
590
+ termination_reason=(
591
+ "search_limit" if uncertain else "no_solution_within_bounds"
592
+ ),
593
+ )
594
+
595
+ def compare(self, other):
596
+ """Compare scenarios sharing the same stable species/reaction identities."""
597
+ if not isinstance(other, CRNAnalysis):
598
+ other = CRNAnalysis(other)
599
+ species = sorted(set(self.species_order) | set(other.species_order))
600
+ reactions = sorted(set(self.reaction_order) | set(other.reaction_order))
601
+
602
+ def aligned(analysis, name):
603
+ a = np.zeros((len(species), len(reactions)))
604
+ si, ri = {s: i for i, s in enumerate(species)}, {
605
+ r: i for i, r in enumerate(reactions)
606
+ }
607
+ for i, s in enumerate(analysis.species_order):
608
+ for j, r in enumerate(analysis.reaction_order):
609
+ a[si[s], ri[r]] = analysis._matrices[name][i, j]
610
+ return a
611
+
612
+ return AnalysisResult(
613
+ {
614
+ "same_net_matrix": bool(
615
+ np.array_equal(aligned(self, "S"), aligned(other, "S"))
616
+ ),
617
+ "same_reactants": bool(
618
+ np.array_equal(aligned(self, "S_minus"), aligned(other, "S_minus"))
619
+ ),
620
+ "same_products": bool(
621
+ np.array_equal(aligned(self, "S_plus"), aligned(other, "S_plus"))
622
+ ),
623
+ "before": self.structure().value,
624
+ "after": other.structure().value,
625
+ "conservation_before": self.conservation().value,
626
+ "conservation_after": other.conservation().value,
627
+ },
628
+ assumptions={"before": self._assumptions(), "after": other._assumptions()},
629
+ )
synkit/CRN/backend.py ADDED
@@ -0,0 +1,74 @@
1
+ """Explicit, task-local native backend selection; never compiles on import."""
2
+
3
+ from contextlib import contextmanager
4
+ from contextvars import ContextVar
5
+
6
+ _LIBRARY = ContextVar("synkit_crn_native_library", default=None)
7
+ _OPERATIONS = ContextVar("synkit_crn_native_operations", default=None)
8
+
9
+
10
+ def record_operation(name):
11
+ operations = _OPERATIONS.get()
12
+ if operations is not None:
13
+ operations[name] = operations.get(name, 0) + 1
14
+
15
+
16
+ def backend_metadata():
17
+ from .native import _library_manifest
18
+
19
+ library = _LIBRARY.get()
20
+ metadata = {
21
+ "production_backend": "cpp" if library is not None else "python",
22
+ "library_path": str(library) if library is not None else None,
23
+ "native_operations": dict(_OPERATIONS.get() or {}),
24
+ }
25
+ if library is not None:
26
+ manifest = _library_manifest(library)
27
+ metadata.update(
28
+ library_sha256=manifest["library_sha256"],
29
+ build_sha256=manifest["build_sha256"],
30
+ capabilities=manifest.get("capabilities", []),
31
+ )
32
+ return metadata
33
+
34
+
35
+ def resolve_backend(backend="auto", library_path=None):
36
+ if backend not in {"auto", "python", "cpp"}:
37
+ raise ValueError("backend must be auto, python or cpp")
38
+ library_path = library_path if library_path is not None else _LIBRARY.get()
39
+ if backend == "python":
40
+ return "python", None
41
+ if backend == "cpp" and library_path is None:
42
+ from .native import bundled_native_library
43
+
44
+ try:
45
+ library_path = bundled_native_library()
46
+ except FileNotFoundError as exc:
47
+ raise ValueError(str(exc)) from exc
48
+ return ("cpp", library_path) if library_path is not None else ("python", None)
49
+
50
+
51
+ @contextmanager
52
+ def native_backend(library_path=None):
53
+ """Use a verified native library throughout a Python analysis workflow."""
54
+ from .native import _load_library
55
+
56
+ _, library_path = resolve_backend("cpp", library_path)
57
+ _load_library(library_path)
58
+ token = _LIBRARY.set(library_path)
59
+ operations_token = _OPERATIONS.set({})
60
+ try:
61
+ yield
62
+ finally:
63
+ _LIBRARY.reset(token)
64
+ _OPERATIONS.reset(operations_token)
65
+
66
+
67
+ @contextmanager
68
+ def python_backend():
69
+ """Temporarily select the independent Python reference implementation."""
70
+ token = _LIBRARY.set(None)
71
+ try:
72
+ yield
73
+ finally:
74
+ _LIBRARY.reset(token)