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,703 @@
1
+ """Exact structural and stereochemical ITS deduplication helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter, defaultdict
6
+ from copy import deepcopy
7
+ from itertools import combinations
8
+ from typing import Any, Dict, List, Tuple
9
+
10
+ import networkx as nx
11
+ from networkx.algorithms.isomorphism import (
12
+ GraphMatcher,
13
+ categorical_edge_match,
14
+ categorical_node_match,
15
+ )
16
+
17
+ from synkit.Graph.ITS.its_reverter import ITSReverter
18
+ from ..core import product as _product_state
19
+
20
+ NodeId = Any
21
+ _PRIMITIVE_IDENTITY_TYPES = frozenset((str, int, float, bool, type(None)))
22
+
23
+
24
+ def _freeze_typed_identity(
25
+ value: Any,
26
+ cache: Dict[int, Tuple[Any, Any]] | None = None,
27
+ ) -> Any:
28
+ """Return a hashable identity without conflating container types.
29
+
30
+ This encoding is used by authoritative exact structural comparisons.
31
+ Unlike the coarser invariant helper above, equal encodings imply equal
32
+ typed attribute trees. Numeric/container coercions can therefore never
33
+ cause a false graph merge.
34
+ """
35
+ value_type = type(value)
36
+ if value_type in _PRIMITIVE_IDENTITY_TYPES:
37
+ # Primitive values need neither recursive dispatch nor identity-cache
38
+ # bookkeeping. The concrete type keeps bool/int/float distinct.
39
+ return value_type, value
40
+
41
+ cache_key = id(value)
42
+ if cache is not None:
43
+ cached = cache.get(cache_key)
44
+ if cached is not None and cached[0] is value:
45
+ return cached[1]
46
+
47
+ if isinstance(value, dict):
48
+ identity = (
49
+ dict,
50
+ frozenset(
51
+ (
52
+ _freeze_typed_identity(key, cache),
53
+ _freeze_typed_identity(item, cache),
54
+ )
55
+ for key, item in value.items()
56
+ ),
57
+ )
58
+ elif isinstance(value, list):
59
+ item_type = type(value[0]) if value else None
60
+ if item_type in _PRIMITIVE_IDENTITY_TYPES and all(
61
+ type(item) is item_type for item in value
62
+ ):
63
+ identity = (list, item_type, tuple(value))
64
+ else:
65
+ identity = (
66
+ list,
67
+ tuple(_freeze_typed_identity(item, cache) for item in value),
68
+ )
69
+ elif isinstance(value, tuple):
70
+ item_type = type(value[0]) if value else None
71
+ if item_type in _PRIMITIVE_IDENTITY_TYPES and all(
72
+ type(item) is item_type for item in value
73
+ ):
74
+ # This is exactly the same typed equality as recursively encoding
75
+ # each primitive item, but avoids two Python calls for the common
76
+ # paired endpoint representation.
77
+ identity = (tuple, item_type, value)
78
+ else:
79
+ identity = (
80
+ tuple,
81
+ tuple(_freeze_typed_identity(item, cache) for item in value),
82
+ )
83
+ elif isinstance(value, set):
84
+ identity = (
85
+ set,
86
+ frozenset(_freeze_typed_identity(item, cache) for item in value),
87
+ )
88
+ else:
89
+ try:
90
+ hash(value)
91
+ except TypeError:
92
+ # Unknown mutable objects fail closed: only the same live object
93
+ # may compare equal here. This can retain an extra representative
94
+ # but cannot authorize a false merge from a lossy representation.
95
+ identity = (type(value), cache_key)
96
+ else:
97
+ # The concrete type is part of the identity, so Python's numeric
98
+ # coercions (for example True == 1 == 1.0) cannot merge labels.
99
+ identity = (type(value), value)
100
+
101
+ if cache is not None:
102
+ # Keep the object itself beside the result: an id can be reused only
103
+ # after its previous object dies, which cannot happen while retained
104
+ # by this quotient-local cache.
105
+ cache[cache_key] = (value, identity)
106
+ return identity
107
+
108
+
109
+ def _merge_application_orbits(
110
+ representative: nx.Graph,
111
+ members: List[nx.Graph],
112
+ ) -> None:
113
+ """Retain every application contributing to one exact product orbit."""
114
+ contributions = []
115
+ aggregate_weight = 0.0
116
+ aggregate_multiplicity = 0
117
+ for member in members:
118
+ aggregate_weight += float(
119
+ member.graph.get(
120
+ "stereo_aggregate_weight",
121
+ member.graph.get("stereo_branch_weight", 1.0),
122
+ )
123
+ )
124
+ aggregate_multiplicity += int(member.graph.get("stereo_branch_multiplicity", 1))
125
+ existing = member.graph.get("stereo_branch_contributions")
126
+ if existing is not None:
127
+ contributions.extend(deepcopy(existing))
128
+ continue
129
+ contributions.append(
130
+ {
131
+ "weight": float(member.graph.get("stereo_branch_weight", 1.0)),
132
+ "branch_path": deepcopy(member.graph.get("stereo_branch_path", ())),
133
+ "branch": deepcopy(member.graph.get("stereo_branch", {})),
134
+ "coupling_branch": deepcopy(
135
+ member.graph.get("stereo_coupling_branch", {})
136
+ ),
137
+ "application": deepcopy(member.graph.get("application_provenance")),
138
+ }
139
+ )
140
+ representative.graph["stereo_aggregate_weight"] = aggregate_weight
141
+ representative.graph["stereo_branch_multiplicity"] = aggregate_multiplicity
142
+ representative.graph["stereo_branch_contributions"] = contributions
143
+
144
+ applications = []
145
+ for member in members:
146
+ orbit = member.graph.get("application_orbit")
147
+ if orbit is not None:
148
+ applications.extend(orbit.get("applications", ()))
149
+ continue
150
+ provenance = member.graph.get("application_provenance")
151
+ if provenance is not None:
152
+ applications.append(provenance)
153
+ if applications:
154
+ applications.sort(key=lambda value: value["application_index"])
155
+ representative.graph["application_orbit"] = {
156
+ "multiplicity": len(applications),
157
+ "applications": applications,
158
+ }
159
+
160
+
161
+ def _chemical_rewrite_role(role: Any) -> Any:
162
+ """Drop provenance-only atom-map identity from chemical rewrite roles."""
163
+ if isinstance(role, tuple) and len(role) >= 9:
164
+ chemical_role = role[:-1]
165
+ if chemical_role[0] == "H":
166
+ return chemical_role[:-1] + ((),)
167
+ return chemical_role
168
+ return role
169
+
170
+
171
+ def _rewrite_locus_nodes(reaction_center: nx.Graph) -> frozenset[Any]:
172
+ """Return rule nodes whose placement can alter a non-stereo product."""
173
+ active = set()
174
+ node_state_keys = (
175
+ "element",
176
+ "aromatic",
177
+ "hcount",
178
+ "charge",
179
+ "radical",
180
+ "lone_pairs",
181
+ "valence_electrons",
182
+ "present",
183
+ )
184
+ for node, attrs in reaction_center.nodes(data=True):
185
+ types = attrs.get("typesGH")
186
+ if (
187
+ isinstance(types, tuple)
188
+ and len(types) == 2
189
+ and _chemical_rewrite_role(types[0]) != _chemical_rewrite_role(types[1])
190
+ ):
191
+ active.add(node)
192
+ continue
193
+ if any(
194
+ isinstance(attrs.get(key), tuple)
195
+ and len(attrs[key]) == 2
196
+ and attrs[key][0] != attrs[key][1]
197
+ for key in node_state_keys
198
+ ):
199
+ active.add(node)
200
+
201
+ edge_state_keys = (
202
+ "order",
203
+ "kekule_order",
204
+ "sigma_order",
205
+ "pi_order",
206
+ "standard_order",
207
+ )
208
+ for left, right, attrs in reaction_center.edges(data=True):
209
+ if any(
210
+ isinstance(attrs.get(key), tuple)
211
+ and len(attrs[key]) == 2
212
+ and attrs[key][0] != attrs[key][1]
213
+ for key in edge_state_keys
214
+ ):
215
+ active.update((left, right))
216
+ return frozenset(active)
217
+
218
+
219
+ def _mapping_quotient_input_supported(
220
+ mappings: List[Dict[Any, Any]],
221
+ host: nx.Graph,
222
+ active: frozenset[Any],
223
+ ) -> bool:
224
+ """Return whether the exact component quotient supports this input."""
225
+ return (
226
+ len(mappings) >= 2
227
+ and not host.is_multigraph()
228
+ and not host.graph.get("stereo_descriptors")
229
+ and bool(active)
230
+ and all(active.issubset(mapping) for mapping in mappings)
231
+ )
232
+
233
+
234
+ def _project_mapped_rewrite_locus(
235
+ mappings: List[Dict[Any, Any]],
236
+ reaction_center: nx.Graph,
237
+ active: frozenset[Any],
238
+ ) -> frozenset[Any] | None:
239
+ """Project explicit-H roles onto their anchored implicit heavy roles.
240
+
241
+ Explicit hydrogen nodes are deliberately absent from the first matching
242
+ pass. Their extensions remain equivariant under an attributed host
243
+ isomorphism when each omitted H is source-bound to an already mapped
244
+ rewrite-locus node; the anchored explicit-H search then supplies exactly
245
+ the corresponding fibers on both applications.
246
+ """
247
+ if not mappings:
248
+ return None
249
+ common_domain = set.intersection(*(set(mapping) for mapping in mappings))
250
+ projected = frozenset(active & common_domain)
251
+ for node in active - projected:
252
+ element = reaction_center.nodes[node].get("element")
253
+ source_element = element[0] if isinstance(element, tuple) else element
254
+ if source_element != "H":
255
+ return None
256
+ anchored = False
257
+ for neighbor in reaction_center.neighbors(node):
258
+ if neighbor not in projected:
259
+ continue
260
+ order = reaction_center.edges[node, neighbor].get("order", 0.0)
261
+ source_order = order[0] if isinstance(order, tuple) else order
262
+ if source_order and float(source_order) > 0.0:
263
+ anchored = True
264
+ break
265
+ if not anchored:
266
+ return None
267
+ return projected
268
+
269
+
270
+ def _deduplicate_rewrite_equivalent_mappings( # noqa: C901
271
+ mappings: List[Dict[Any, Any]],
272
+ host: nx.Graph,
273
+ reaction_center: nx.Graph,
274
+ ) -> List[Dict[Any, Any]]:
275
+ """Quotient mappings that provably produce isomorphic products.
276
+
277
+ Each affected host component is coloured by the exact reaction-center
278
+ roles placed on it. Two mapping applications are merged only when exact
279
+ attributed-graph isomorphisms identify the resulting multiset of coloured
280
+ source components. Because the same fixed rule is then glued to the same
281
+ preserved roles, those applications have isomorphic non-stereo products.
282
+
283
+ Unchanged context nodes are deliberately absent from the colours: their
284
+ embeddings constrain matching but cannot affect the rewrite endpoint.
285
+ """
286
+ from .structural import (
287
+ _canonical_attributed_graph_certificate,
288
+ _refine_structural_colours_in_place,
289
+ )
290
+
291
+ active = _rewrite_locus_nodes(reaction_center)
292
+ active = _project_mapped_rewrite_locus(mappings, reaction_center, active)
293
+ if active is None:
294
+ return mappings
295
+ if not _mapping_quotient_input_supported(mappings, host, active):
296
+ return mappings
297
+
298
+ components = [
299
+ frozenset(component)
300
+ for component in (
301
+ nx.weakly_connected_components(host)
302
+ if host.is_directed()
303
+ else nx.connected_components(host)
304
+ )
305
+ ]
306
+ component_index = {
307
+ node: index for index, component in enumerate(components) for node in component
308
+ }
309
+ node_attrs = (
310
+ "element",
311
+ "aromatic",
312
+ "hcount",
313
+ "charge",
314
+ "radical",
315
+ "lone_pairs",
316
+ "valence_electrons",
317
+ "present",
318
+ "typesGH",
319
+ )
320
+ edge_attrs = (
321
+ "order",
322
+ "kekule_order",
323
+ "sigma_order",
324
+ "pi_order",
325
+ "standard_order",
326
+ )
327
+
328
+ # Build every affected source component once. A role-free WL colour is
329
+ # an inexpensive necessary invariant for a role-preserving component
330
+ # isomorphism. Mappings with distinct invariant signatures therefore
331
+ # cannot be rewrite-equivalent and bypass exact whole-component VF2.
332
+ mapping_placements: List[Dict[int, List[Tuple[Any, Any]]]] = []
333
+ affected_components = set()
334
+ for mapping in mappings:
335
+ by_component: Dict[int, List[Tuple[Any, Any]]] = defaultdict(list)
336
+ for pattern_node in active:
337
+ host_node = mapping[pattern_node]
338
+ index = component_index[host_node]
339
+ affected_components.add(index)
340
+ by_component[index].append((pattern_node, host_node))
341
+ mapping_placements.append(by_component)
342
+
343
+ base_components: Dict[int, nx.Graph] = {}
344
+ base_node_values: Dict[int, Dict[Any, Tuple[Any, ...]]] = {}
345
+ component_keys = {}
346
+ node_colours = {}
347
+ identity_cache: Dict[int, Tuple[Any, Any]] = {}
348
+ for index in affected_components:
349
+ base = host.subgraph(components[index]).copy()
350
+ values = {}
351
+ for node, attrs in base.nodes(data=True):
352
+ value = tuple(attrs.get(key) for key in node_attrs)
353
+ values[node] = value
354
+ attrs["_rewrite_mapping_node_sig"] = _freeze_typed_identity(
355
+ value + (None,),
356
+ identity_cache,
357
+ )
358
+ for _, _, attrs in base.edges(data=True):
359
+ attrs["_rewrite_mapping_edge_sig"] = _freeze_typed_identity(
360
+ tuple(attrs.get(key) for key in edge_attrs),
361
+ identity_cache,
362
+ )
363
+ base_components[index] = base
364
+ base_node_values[index] = values
365
+
366
+ _refine_structural_colours_in_place(
367
+ list(base_components.values()),
368
+ iterations=3,
369
+ node_signature_attr="_rewrite_mapping_node_sig",
370
+ edge_signature_attr="_rewrite_mapping_edge_sig",
371
+ node_colour_attr="_rewrite_mapping_node_colour",
372
+ edge_colour_attr="_rewrite_mapping_edge_colour",
373
+ )
374
+ for index, base in base_components.items():
375
+ colours = {
376
+ node: attrs["_rewrite_mapping_node_colour"]
377
+ for node, attrs in base.nodes(data=True)
378
+ }
379
+ node_colours[index] = colours
380
+ component_keys[index] = (
381
+ base.is_directed(),
382
+ len(base),
383
+ base.number_of_edges(),
384
+ tuple(sorted(colours.values())),
385
+ tuple(
386
+ sorted(
387
+ (
388
+ attrs["_rewrite_mapping_edge_colour"]
389
+ for _, _, attrs in base.edges(data=True)
390
+ ),
391
+ )
392
+ ),
393
+ )
394
+
395
+ fast_keys = []
396
+ for by_component in mapping_placements:
397
+ component_placements = []
398
+ for index, placements in by_component.items():
399
+ ordered = sorted(placements, key=lambda item: repr(item[0]))
400
+ roles = tuple(
401
+ (
402
+ _freeze_typed_identity(pattern_node, identity_cache),
403
+ node_colours[index][host_node],
404
+ )
405
+ for pattern_node, host_node in ordered
406
+ )
407
+ pair_edges = []
408
+ base = base_components[index]
409
+ for (left_role, left_host), (right_role, right_host) in combinations(
410
+ ordered,
411
+ 2,
412
+ ):
413
+ edge = (
414
+ base.edges[left_host, right_host].get("_rewrite_mapping_edge_sig")
415
+ if base.has_edge(left_host, right_host)
416
+ else None
417
+ )
418
+ pair_edges.append(
419
+ (
420
+ _freeze_typed_identity(left_role, identity_cache),
421
+ _freeze_typed_identity(right_role, identity_cache),
422
+ edge,
423
+ )
424
+ )
425
+ component_placements.append(
426
+ (component_keys[index], roles, tuple(pair_edges))
427
+ )
428
+ fast_keys.append(tuple(sorted(component_placements, key=repr)))
429
+
430
+ fast_counts = Counter(fast_keys)
431
+ cache: Dict[Any, int] = {}
432
+ representatives: Dict[Any, List[Tuple[int, nx.Graph]]] = defaultdict(list)
433
+ certificate_classes: Dict[str, int] = {}
434
+ certificate_node_palette: Dict[Any, int] = {}
435
+ certificate_edge_palette: Dict[Any, int] = {}
436
+ certificate_topology_cache: Dict[Any, Any] = {}
437
+ next_class = 0
438
+ node_match = categorical_node_match("_rewrite_mapping_node_sig", ())
439
+ edge_match = categorical_edge_match("_rewrite_mapping_edge_sig", ())
440
+
441
+ def classify_component(
442
+ index: int,
443
+ placements: Tuple[Tuple[Any, Any], ...],
444
+ ) -> int:
445
+ nonlocal next_class
446
+ cache_key = (index, placements)
447
+ cached = cache.get(cache_key)
448
+ if cached is not None:
449
+ return cached
450
+
451
+ decorated = base_components[index].copy()
452
+ for pattern_node, host_node in placements:
453
+ decorated.nodes[host_node]["_rewrite_mapping_node_sig"] = (
454
+ _freeze_typed_identity(
455
+ base_node_values[index][host_node] + (pattern_node,),
456
+ identity_cache,
457
+ )
458
+ )
459
+
460
+ certificate = _canonical_attributed_graph_certificate(
461
+ decorated,
462
+ node_attribute="_rewrite_mapping_node_sig",
463
+ edge_attribute="_rewrite_mapping_edge_sig",
464
+ node_palette=certificate_node_palette,
465
+ edge_palette=certificate_edge_palette,
466
+ topology_cache=certificate_topology_cache,
467
+ )
468
+ if certificate is not None:
469
+ component_class = certificate_classes.get(certificate)
470
+ if component_class is None:
471
+ component_class = next_class
472
+ next_class += 1
473
+ certificate_classes[certificate] = component_class
474
+ cache[cache_key] = component_class
475
+ return component_class
476
+
477
+ node_labels = [
478
+ attrs["_rewrite_mapping_node_sig"]
479
+ for _, attrs in decorated.nodes(data=True)
480
+ ]
481
+ edge_labels = [
482
+ attrs["_rewrite_mapping_edge_sig"]
483
+ for _, _, attrs in decorated.edges(data=True)
484
+ ]
485
+ environments = []
486
+ for node, attrs in decorated.nodes(data=True):
487
+ incident = Counter(
488
+ (
489
+ decorated.edges[node, neighbor]["_rewrite_mapping_edge_sig"],
490
+ decorated.nodes[neighbor]["_rewrite_mapping_node_sig"],
491
+ )
492
+ for neighbor in decorated.neighbors(node)
493
+ )
494
+ environments.append(
495
+ (
496
+ attrs["_rewrite_mapping_node_sig"],
497
+ frozenset(incident.items()),
498
+ )
499
+ )
500
+ fingerprint = (
501
+ decorated.number_of_nodes(),
502
+ decorated.number_of_edges(),
503
+ frozenset(Counter(node_labels).items()),
504
+ frozenset(Counter(edge_labels).items()),
505
+ frozenset(Counter(environments).items()),
506
+ )
507
+ for class_id, representative in representatives[fingerprint]:
508
+ if nx.is_isomorphic(
509
+ decorated,
510
+ representative,
511
+ node_match=node_match,
512
+ edge_match=edge_match,
513
+ ):
514
+ cache[cache_key] = class_id
515
+ return class_id
516
+
517
+ class_id = next_class
518
+ next_class += 1
519
+ representatives[fingerprint].append((class_id, decorated))
520
+ cache[cache_key] = class_id
521
+ return class_id
522
+
523
+ seen_by_fast_key = defaultdict(set)
524
+ unique = []
525
+ for mapping, by_component, fast_key in zip(
526
+ mappings,
527
+ mapping_placements,
528
+ fast_keys,
529
+ ):
530
+ if fast_counts[fast_key] == 1:
531
+ unique.append(mapping)
532
+ continue
533
+ signature = tuple(
534
+ sorted(
535
+ classify_component(
536
+ index,
537
+ tuple(sorted(placements, key=repr)),
538
+ )
539
+ for index, placements in by_component.items()
540
+ )
541
+ )
542
+ if signature in seen_by_fast_key[fast_key]:
543
+ continue
544
+ seen_by_fast_key[fast_key].add(signature)
545
+ unique.append(mapping)
546
+ return unique
547
+
548
+
549
+ def _finalize_product_electron_fields(
550
+ its_graphs: List[nx.Graph],
551
+ ) -> List[nx.Graph]:
552
+ """Finalize deferred tuple products and reject unperceivable candidates."""
553
+ finalized: List[nx.Graph] = []
554
+ for its in its_graphs:
555
+ try:
556
+ if its.graph.get("electron_aware_rewrite", False) and not its.graph.get(
557
+ "_product_electron_fields_current", False
558
+ ):
559
+ _product_state._refresh_product_electron_fields(its)
560
+ except _product_state.ProductStatePerceptionError:
561
+ # Reject only this application; other embeddings may remain valid.
562
+ continue
563
+ finalized.append(its)
564
+ return finalized
565
+
566
+
567
+ def _deduplicate_structural_its(its_graphs: List[nx.Graph]) -> List[nx.Graph]:
568
+ """Keep one representative per exact structural/stereo ITS identity.
569
+
570
+ Electron state is finalized before quotienting. This guarantees that the
571
+ deduplication relation is a congruence of endpoint serialization: two
572
+ provisional candidates may not be merged before every state field that
573
+ affects their serialized endpoints has been materialized.
574
+ """
575
+ from .structural import _cluster_structural_its
576
+
577
+ if not its_graphs:
578
+ return its_graphs
579
+
580
+ has_deferred_electrons = any(
581
+ its.graph.get("electron_aware_rewrite", False)
582
+ and not its.graph.get("_product_electron_fields_current", False)
583
+ for its in its_graphs
584
+ )
585
+ if not has_deferred_electrons:
586
+ return _cluster_structural_its(
587
+ its_graphs,
588
+ refresh_electrons=False,
589
+ )
590
+
591
+ its_graphs = _finalize_product_electron_fields(its_graphs)
592
+ if not its_graphs:
593
+ return []
594
+ return _cluster_structural_its(
595
+ its_graphs,
596
+ refresh_electrons=False,
597
+ hash_iterations=3,
598
+ )
599
+
600
+
601
+ def _deduplicate_coupling_face_products(
602
+ its_graphs: List[nx.Graph],
603
+ ) -> List[nx.Graph]:
604
+ """Collapse symmetry-identical coupled faces but keep enantiomers.
605
+
606
+ A meso product can be reached through both correlated face branches.
607
+ Atom-map labels make those ITS registries look different even though
608
+ a stereo-preserving molecular automorphism relates them. This pass is
609
+ limited to coupling branches without explicit population outcomes;
610
+ true enantiomers remain non-isomorphic and are retained.
611
+ """
612
+ from .structural import (
613
+ _EXACT_EDGE_SIG,
614
+ _EXACT_NODE_SIG,
615
+ _REFINED_NODE_COLOUR,
616
+ _prepare_its_for_structural_cluster,
617
+ )
618
+
619
+ if len(its_graphs) < 2:
620
+ return its_graphs
621
+
622
+ from synkit.Graph.Stereo import stereo_isomorphic
623
+
624
+ representatives: List[Tuple[nx.Graph, nx.Graph, nx.Graph, nx.Graph]] = []
625
+ unique = []
626
+ for its in its_graphs:
627
+ if not its.graph.get("stereo_coupling_branch") or its.graph.get(
628
+ "stereo_outcomes"
629
+ ):
630
+ unique.append(its)
631
+ continue
632
+ reverter = ITSReverter(its)
633
+ reactant = reverter.to_reactant_graph()
634
+ product = reverter.to_product_graph()
635
+ prepared = _prepare_its_for_structural_cluster(its)
636
+ duplicate = False
637
+ for (
638
+ other_its,
639
+ other_reactant,
640
+ other_product,
641
+ other_prepared,
642
+ ) in representatives:
643
+ if not nx.is_isomorphic(
644
+ prepared,
645
+ other_prepared,
646
+ node_match=categorical_node_match(
647
+ [_EXACT_NODE_SIG, _REFINED_NODE_COLOUR],
648
+ [(), ""],
649
+ ),
650
+ edge_match=categorical_edge_match(_EXACT_EDGE_SIG, ()),
651
+ ):
652
+ continue
653
+ if stereo_isomorphic(reactant, other_reactant) and stereo_isomorphic(
654
+ product, other_product
655
+ ):
656
+ retained = other_its.graph.get("stereo_coupling_branch", {})
657
+ duplicate_metadata = its.graph.get("stereo_coupling_branch", {})
658
+ for target, metadata in duplicate_metadata.items():
659
+ retained_metadata = retained.get(target)
660
+ if retained_metadata is None:
661
+ continue
662
+ branches = set(
663
+ retained_metadata.get(
664
+ "equivalent_face_branches",
665
+ [retained_metadata.get("face_branch")],
666
+ )
667
+ )
668
+ branches.add(metadata.get("face_branch"))
669
+ branches.discard(None)
670
+ retained_metadata["equivalent_face_branches"] = sorted(branches)
671
+ retained_metadata["symmetry_multiplicity"] = len(branches)
672
+ _merge_application_orbits(
673
+ other_its,
674
+ [other_its, its],
675
+ )
676
+ duplicate = True
677
+ break
678
+ if duplicate:
679
+ continue
680
+ representatives.append((its, reactant, product, prepared))
681
+ unique.append(its)
682
+ return unique
683
+
684
+
685
+ def _components_are_equivalent(
686
+ pattern: nx.Graph,
687
+ left: frozenset[NodeId],
688
+ right: frozenset[NodeId],
689
+ node_attrs: List[str],
690
+ edge_attrs: List[str],
691
+ ) -> bool:
692
+ """Return whether two disconnected pattern components have one role shape."""
693
+ left_graph = pattern.subgraph(left)
694
+ right_graph = pattern.subgraph(right)
695
+ node_defaults = [0 if attr == "charge" else "*" for attr in node_attrs]
696
+ edge_defaults = [1.0 for _ in edge_attrs]
697
+ matcher = GraphMatcher(
698
+ left_graph,
699
+ right_graph,
700
+ node_match=categorical_node_match(node_attrs, node_defaults),
701
+ edge_match=categorical_edge_match(edge_attrs, edge_defaults),
702
+ )
703
+ return matcher.is_isomorphic()