okstra 0.108.0 → 0.110.0

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 (648) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +3 -3
  4. package/docs/kr/cli.md +2 -2
  5. package/docs/project-structure-overview.md +3 -3
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/prompts/profiles/_common-contract.md +1 -1
  9. package/runtime/python/okstra_ctl/graphify_cmd.py +225 -0
  10. package/runtime/python/okstra_ctl/resolve_task_key.py +15 -9
  11. package/runtime/python/okstra_ctl/worktree.py +14 -13
  12. package/runtime/python/okstra_project/__init__.py +2 -0
  13. package/runtime/python/okstra_project/state.py +36 -0
  14. package/runtime/python/okstra_vendor/__init__.py +41 -0
  15. package/runtime/python/okstra_vendor/graphify/.vendored-version +1 -0
  16. package/runtime/python/okstra_vendor/graphify/__init__.py +28 -0
  17. package/runtime/python/okstra_vendor/graphify/__main__.py +1371 -0
  18. package/runtime/python/okstra_vendor/graphify/analyze.py +540 -0
  19. package/runtime/python/okstra_vendor/graphify/benchmark.py +129 -0
  20. package/runtime/python/okstra_vendor/graphify/build.py +107 -0
  21. package/runtime/python/okstra_vendor/graphify/cache.py +169 -0
  22. package/runtime/python/okstra_vendor/graphify/cluster.py +137 -0
  23. package/runtime/python/okstra_vendor/graphify/detect.py +510 -0
  24. package/runtime/python/okstra_vendor/graphify/export.py +1014 -0
  25. package/runtime/python/okstra_vendor/graphify/extract.py +3277 -0
  26. package/runtime/python/okstra_vendor/graphify/hooks.py +220 -0
  27. package/runtime/python/okstra_vendor/graphify/ingest.py +297 -0
  28. package/runtime/python/okstra_vendor/graphify/manifest.py +4 -0
  29. package/runtime/python/okstra_vendor/graphify/report.py +175 -0
  30. package/runtime/python/okstra_vendor/graphify/security.py +203 -0
  31. package/runtime/python/okstra_vendor/graphify/serve.py +373 -0
  32. package/runtime/python/okstra_vendor/graphify/skill-aider.md +1184 -0
  33. package/runtime/python/okstra_vendor/graphify/skill-claw.md +1184 -0
  34. package/runtime/python/okstra_vendor/graphify/skill-codex.md +1242 -0
  35. package/runtime/python/okstra_vendor/graphify/skill-copilot.md +1268 -0
  36. package/runtime/python/okstra_vendor/graphify/skill-droid.md +1239 -0
  37. package/runtime/python/okstra_vendor/graphify/skill-kiro.md +1183 -0
  38. package/runtime/python/okstra_vendor/graphify/skill-opencode.md +1238 -0
  39. package/runtime/python/okstra_vendor/graphify/skill-trae.md +1208 -0
  40. package/runtime/python/okstra_vendor/graphify/skill-vscode.md +253 -0
  41. package/runtime/python/okstra_vendor/graphify/skill-windows.md +1245 -0
  42. package/runtime/python/okstra_vendor/graphify/skill.md +1319 -0
  43. package/runtime/python/okstra_vendor/graphify/transcribe.py +182 -0
  44. package/runtime/python/okstra_vendor/graphify/validate.py +72 -0
  45. package/runtime/python/okstra_vendor/graphify/watch.py +188 -0
  46. package/runtime/python/okstra_vendor/graphify/wiki.py +214 -0
  47. package/runtime/python/okstra_vendor/networkx/__init__.py +62 -0
  48. package/runtime/python/okstra_vendor/networkx/algorithms/__init__.py +134 -0
  49. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/__init__.py +26 -0
  50. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clique.py +259 -0
  51. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clustering_coefficient.py +71 -0
  52. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/connectivity.py +412 -0
  53. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/density.py +396 -0
  54. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/distance_measures.py +150 -0
  55. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/dominating_set.py +149 -0
  56. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/kcomponents.py +369 -0
  57. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/matching.py +44 -0
  58. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/maxcut.py +143 -0
  59. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/ramsey.py +53 -0
  60. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/steinertree.py +265 -0
  61. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/__init__.py +0 -0
  62. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py +41 -0
  63. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_clique.py +112 -0
  64. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_connectivity.py +199 -0
  65. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_density.py +146 -0
  66. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_distance_measures.py +59 -0
  67. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_dominating_set.py +78 -0
  68. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_kcomponents.py +303 -0
  69. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_matching.py +8 -0
  70. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_maxcut.py +94 -0
  71. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_ramsey.py +31 -0
  72. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_steinertree.py +306 -0
  73. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_traveling_salesman.py +1014 -0
  74. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_treewidth.py +274 -0
  75. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_vertex_cover.py +68 -0
  76. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/traveling_salesman.py +1508 -0
  77. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/treewidth.py +255 -0
  78. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/vertex_cover.py +83 -0
  79. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/__init__.py +5 -0
  80. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/connectivity.py +122 -0
  81. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/correlation.py +302 -0
  82. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/mixing.py +255 -0
  83. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/neighbor_degree.py +160 -0
  84. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/pairs.py +127 -0
  85. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/__init__.py +0 -0
  86. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/base_test.py +81 -0
  87. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_connectivity.py +143 -0
  88. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_correlation.py +122 -0
  89. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_mixing.py +174 -0
  90. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_neighbor_degree.py +107 -0
  91. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_pairs.py +87 -0
  92. package/runtime/python/okstra_vendor/networkx/algorithms/asteroidal.py +164 -0
  93. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/__init__.py +88 -0
  94. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/basic.py +322 -0
  95. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/centrality.py +290 -0
  96. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/cluster.py +289 -0
  97. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/covering.py +57 -0
  98. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/edgelist.py +360 -0
  99. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/extendability.py +105 -0
  100. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/generators.py +603 -0
  101. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/link_analysis.py +316 -0
  102. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matching.py +590 -0
  103. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matrix.py +232 -0
  104. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/projection.py +526 -0
  105. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/redundancy.py +112 -0
  106. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/spectral.py +69 -0
  107. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/__init__.py +0 -0
  108. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_basic.py +125 -0
  109. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_centrality.py +192 -0
  110. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_cluster.py +84 -0
  111. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_covering.py +33 -0
  112. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_edgelist.py +240 -0
  113. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_extendability.py +334 -0
  114. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_generators.py +407 -0
  115. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_link_analysis.py +218 -0
  116. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_matching.py +327 -0
  117. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_matrix.py +138 -0
  118. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_project.py +409 -0
  119. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_redundancy.py +35 -0
  120. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py +80 -0
  121. package/runtime/python/okstra_vendor/networkx/algorithms/boundary.py +168 -0
  122. package/runtime/python/okstra_vendor/networkx/algorithms/bridges.py +205 -0
  123. package/runtime/python/okstra_vendor/networkx/algorithms/broadcasting.py +164 -0
  124. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/__init__.py +20 -0
  125. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness.py +591 -0
  126. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness_subset.py +236 -0
  127. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/closeness.py +282 -0
  128. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness.py +364 -0
  129. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness_subset.py +227 -0
  130. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_closeness.py +96 -0
  131. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/degree_alg.py +150 -0
  132. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/dispersion.py +107 -0
  133. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/eigenvector.py +357 -0
  134. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/flow_matrix.py +130 -0
  135. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/group.py +787 -0
  136. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/harmonic.py +88 -0
  137. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/katz.py +331 -0
  138. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/laplacian.py +150 -0
  139. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/load.py +200 -0
  140. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/percolation.py +128 -0
  141. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/reaching.py +209 -0
  142. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/second_order.py +141 -0
  143. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/subgraph_alg.py +361 -0
  144. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/__init__.py +0 -0
  145. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_betweenness_centrality.py +923 -0
  146. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_betweenness_centrality_subset.py +354 -0
  147. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_closeness_centrality.py +274 -0
  148. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_betweenness_centrality.py +259 -0
  149. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_betweenness_centrality_subset.py +147 -0
  150. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_closeness.py +43 -0
  151. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_degree_centrality.py +144 -0
  152. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_dispersion.py +73 -0
  153. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_eigenvector_centrality.py +186 -0
  154. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_group.py +277 -0
  155. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_harmonic_centrality.py +122 -0
  156. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_katz_centrality.py +345 -0
  157. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_laplacian_centrality.py +220 -0
  158. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_load_centrality.py +344 -0
  159. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_percolation_centrality.py +87 -0
  160. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_reaching.py +140 -0
  161. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_second_order_centrality.py +82 -0
  162. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_subgraph.py +110 -0
  163. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_trophic.py +302 -0
  164. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_voterank.py +64 -0
  165. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/trophic.py +181 -0
  166. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/voterank_alg.py +95 -0
  167. package/runtime/python/okstra_vendor/networkx/algorithms/chains.py +172 -0
  168. package/runtime/python/okstra_vendor/networkx/algorithms/chordal.py +443 -0
  169. package/runtime/python/okstra_vendor/networkx/algorithms/clique.py +818 -0
  170. package/runtime/python/okstra_vendor/networkx/algorithms/cluster.py +732 -0
  171. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/__init__.py +4 -0
  172. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/equitable_coloring.py +505 -0
  173. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/greedy_coloring.py +565 -0
  174. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/tests/__init__.py +0 -0
  175. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/tests/test_coloring.py +863 -0
  176. package/runtime/python/okstra_vendor/networkx/algorithms/communicability_alg.py +163 -0
  177. package/runtime/python/okstra_vendor/networkx/algorithms/community/__init__.py +28 -0
  178. package/runtime/python/okstra_vendor/networkx/algorithms/community/asyn_fluid.py +153 -0
  179. package/runtime/python/okstra_vendor/networkx/algorithms/community/bipartitions.py +354 -0
  180. package/runtime/python/okstra_vendor/networkx/algorithms/community/centrality.py +171 -0
  181. package/runtime/python/okstra_vendor/networkx/algorithms/community/community_utils.py +30 -0
  182. package/runtime/python/okstra_vendor/networkx/algorithms/community/divisive.py +216 -0
  183. package/runtime/python/okstra_vendor/networkx/algorithms/community/kclique.py +79 -0
  184. package/runtime/python/okstra_vendor/networkx/algorithms/community/label_propagation.py +338 -0
  185. package/runtime/python/okstra_vendor/networkx/algorithms/community/leiden.py +162 -0
  186. package/runtime/python/okstra_vendor/networkx/algorithms/community/local.py +220 -0
  187. package/runtime/python/okstra_vendor/networkx/algorithms/community/louvain.py +384 -0
  188. package/runtime/python/okstra_vendor/networkx/algorithms/community/lukes.py +227 -0
  189. package/runtime/python/okstra_vendor/networkx/algorithms/community/modularity_max.py +452 -0
  190. package/runtime/python/okstra_vendor/networkx/algorithms/community/quality.py +347 -0
  191. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/__init__.py +0 -0
  192. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_asyn_fluid.py +147 -0
  193. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_bipartitions.py +157 -0
  194. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_centrality.py +85 -0
  195. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_divisive.py +106 -0
  196. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_kclique.py +91 -0
  197. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_label_propagation.py +241 -0
  198. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_leiden.py +138 -0
  199. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_local.py +76 -0
  200. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_louvain.py +264 -0
  201. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_lukes.py +152 -0
  202. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_modularity_max.py +340 -0
  203. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_quality.py +139 -0
  204. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_utils.py +26 -0
  205. package/runtime/python/okstra_vendor/networkx/algorithms/components/__init__.py +6 -0
  206. package/runtime/python/okstra_vendor/networkx/algorithms/components/attracting.py +115 -0
  207. package/runtime/python/okstra_vendor/networkx/algorithms/components/biconnected.py +394 -0
  208. package/runtime/python/okstra_vendor/networkx/algorithms/components/connected.py +282 -0
  209. package/runtime/python/okstra_vendor/networkx/algorithms/components/semiconnected.py +71 -0
  210. package/runtime/python/okstra_vendor/networkx/algorithms/components/strongly_connected.py +359 -0
  211. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/__init__.py +0 -0
  212. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_attracting.py +70 -0
  213. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_biconnected.py +248 -0
  214. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_connected.py +138 -0
  215. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_semiconnected.py +55 -0
  216. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_strongly_connected.py +193 -0
  217. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_weakly_connected.py +96 -0
  218. package/runtime/python/okstra_vendor/networkx/algorithms/components/weakly_connected.py +196 -0
  219. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/__init__.py +11 -0
  220. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/connectivity.py +811 -0
  221. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/cuts.py +616 -0
  222. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/disjoint_paths.py +408 -0
  223. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_augmentation.py +1270 -0
  224. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_kcomponents.py +592 -0
  225. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcomponents.py +220 -0
  226. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcutsets.py +235 -0
  227. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/stoerwagner.py +152 -0
  228. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/__init__.py +0 -0
  229. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_connectivity.py +421 -0
  230. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_cuts.py +309 -0
  231. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_disjoint_paths.py +249 -0
  232. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_edge_augmentation.py +502 -0
  233. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py +488 -0
  234. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_kcomponents.py +323 -0
  235. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_kcutsets.py +280 -0
  236. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_stoer_wagner.py +102 -0
  237. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/utils.py +88 -0
  238. package/runtime/python/okstra_vendor/networkx/algorithms/core.py +588 -0
  239. package/runtime/python/okstra_vendor/networkx/algorithms/covering.py +142 -0
  240. package/runtime/python/okstra_vendor/networkx/algorithms/cuts.py +416 -0
  241. package/runtime/python/okstra_vendor/networkx/algorithms/cycles.py +1234 -0
  242. package/runtime/python/okstra_vendor/networkx/algorithms/d_separation.py +677 -0
  243. package/runtime/python/okstra_vendor/networkx/algorithms/dag.py +1392 -0
  244. package/runtime/python/okstra_vendor/networkx/algorithms/distance_measures.py +1095 -0
  245. package/runtime/python/okstra_vendor/networkx/algorithms/distance_regular.py +272 -0
  246. package/runtime/python/okstra_vendor/networkx/algorithms/dominance.py +142 -0
  247. package/runtime/python/okstra_vendor/networkx/algorithms/dominating.py +268 -0
  248. package/runtime/python/okstra_vendor/networkx/algorithms/efficiency_measures.py +167 -0
  249. package/runtime/python/okstra_vendor/networkx/algorithms/euler.py +470 -0
  250. package/runtime/python/okstra_vendor/networkx/algorithms/flow/__init__.py +11 -0
  251. package/runtime/python/okstra_vendor/networkx/algorithms/flow/boykovkolmogorov.py +370 -0
  252. package/runtime/python/okstra_vendor/networkx/algorithms/flow/capacityscaling.py +407 -0
  253. package/runtime/python/okstra_vendor/networkx/algorithms/flow/dinitz_alg.py +238 -0
  254. package/runtime/python/okstra_vendor/networkx/algorithms/flow/edmondskarp.py +241 -0
  255. package/runtime/python/okstra_vendor/networkx/algorithms/flow/gomory_hu.py +178 -0
  256. package/runtime/python/okstra_vendor/networkx/algorithms/flow/maxflow.py +611 -0
  257. package/runtime/python/okstra_vendor/networkx/algorithms/flow/mincost.py +356 -0
  258. package/runtime/python/okstra_vendor/networkx/algorithms/flow/networksimplex.py +662 -0
  259. package/runtime/python/okstra_vendor/networkx/algorithms/flow/preflowpush.py +425 -0
  260. package/runtime/python/okstra_vendor/networkx/algorithms/flow/shortestaugmentingpath.py +300 -0
  261. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/__init__.py +0 -0
  262. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/gl1.gpickle.bz2 +0 -0
  263. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/gw1.gpickle.bz2 +0 -0
  264. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/netgen-2.gpickle.bz2 +0 -0
  265. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_gomory_hu.py +128 -0
  266. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_maxflow.py +573 -0
  267. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_maxflow_large_graph.py +155 -0
  268. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_mincost.py +475 -0
  269. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_networksimplex.py +481 -0
  270. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/wlm3.gpickle.bz2 +0 -0
  271. package/runtime/python/okstra_vendor/networkx/algorithms/flow/utils.py +194 -0
  272. package/runtime/python/okstra_vendor/networkx/algorithms/graph_hashing.py +435 -0
  273. package/runtime/python/okstra_vendor/networkx/algorithms/graphical.py +483 -0
  274. package/runtime/python/okstra_vendor/networkx/algorithms/hierarchy.py +57 -0
  275. package/runtime/python/okstra_vendor/networkx/algorithms/hybrid.py +196 -0
  276. package/runtime/python/okstra_vendor/networkx/algorithms/isolate.py +107 -0
  277. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/__init__.py +7 -0
  278. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/ismags.py +1306 -0
  279. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorph.py +336 -0
  280. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorphvf2.py +1262 -0
  281. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/matchhelpers.py +352 -0
  282. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/temporalisomorphvf2.py +308 -0
  283. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/__init__.py +0 -0
  284. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 +0 -0
  285. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 +0 -0
  286. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 +0 -0
  287. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 +0 -0
  288. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_ismags.py +719 -0
  289. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_isomorphism.py +103 -0
  290. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py +490 -0
  291. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_match_helpers.py +64 -0
  292. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py +212 -0
  293. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py +202 -0
  294. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2pp.py +1655 -0
  295. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py +3118 -0
  296. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py +196 -0
  297. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tree_isomorphism.py +264 -0
  298. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2pp.py +1102 -0
  299. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2userfunc.py +192 -0
  300. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/__init__.py +2 -0
  301. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/hits_alg.py +337 -0
  302. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/pagerank_alg.py +498 -0
  303. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/__init__.py +0 -0
  304. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/test_hits.py +77 -0
  305. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/test_pagerank.py +213 -0
  306. package/runtime/python/okstra_vendor/networkx/algorithms/link_prediction.py +687 -0
  307. package/runtime/python/okstra_vendor/networkx/algorithms/lowest_common_ancestors.py +280 -0
  308. package/runtime/python/okstra_vendor/networkx/algorithms/matching.py +1148 -0
  309. package/runtime/python/okstra_vendor/networkx/algorithms/minors/__init__.py +27 -0
  310. package/runtime/python/okstra_vendor/networkx/algorithms/minors/contraction.py +738 -0
  311. package/runtime/python/okstra_vendor/networkx/algorithms/minors/tests/test_contraction.py +544 -0
  312. package/runtime/python/okstra_vendor/networkx/algorithms/mis.py +78 -0
  313. package/runtime/python/okstra_vendor/networkx/algorithms/moral.py +59 -0
  314. package/runtime/python/okstra_vendor/networkx/algorithms/node_classification.py +219 -0
  315. package/runtime/python/okstra_vendor/networkx/algorithms/non_randomness.py +155 -0
  316. package/runtime/python/okstra_vendor/networkx/algorithms/operators/__init__.py +4 -0
  317. package/runtime/python/okstra_vendor/networkx/algorithms/operators/all.py +324 -0
  318. package/runtime/python/okstra_vendor/networkx/algorithms/operators/binary.py +468 -0
  319. package/runtime/python/okstra_vendor/networkx/algorithms/operators/product.py +633 -0
  320. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/__init__.py +0 -0
  321. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_all.py +328 -0
  322. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_binary.py +451 -0
  323. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_product.py +491 -0
  324. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_unary.py +55 -0
  325. package/runtime/python/okstra_vendor/networkx/algorithms/operators/unary.py +77 -0
  326. package/runtime/python/okstra_vendor/networkx/algorithms/perfect_graph.py +73 -0
  327. package/runtime/python/okstra_vendor/networkx/algorithms/planar_drawing.py +464 -0
  328. package/runtime/python/okstra_vendor/networkx/algorithms/planarity.py +1463 -0
  329. package/runtime/python/okstra_vendor/networkx/algorithms/polynomials.py +306 -0
  330. package/runtime/python/okstra_vendor/networkx/algorithms/reciprocity.py +98 -0
  331. package/runtime/python/okstra_vendor/networkx/algorithms/regular.py +167 -0
  332. package/runtime/python/okstra_vendor/networkx/algorithms/richclub.py +138 -0
  333. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/__init__.py +5 -0
  334. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/astar.py +239 -0
  335. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/dense.py +264 -0
  336. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/generic.py +716 -0
  337. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/__init__.py +0 -0
  338. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_astar.py +254 -0
  339. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_dense.py +212 -0
  340. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_dense_numpy.py +88 -0
  341. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_generic.py +511 -0
  342. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_unweighted.py +149 -0
  343. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_weighted.py +983 -0
  344. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/unweighted.py +625 -0
  345. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/weighted.py +2542 -0
  346. package/runtime/python/okstra_vendor/networkx/algorithms/similarity.py +2107 -0
  347. package/runtime/python/okstra_vendor/networkx/algorithms/simple_paths.py +966 -0
  348. package/runtime/python/okstra_vendor/networkx/algorithms/smallworld.py +404 -0
  349. package/runtime/python/okstra_vendor/networkx/algorithms/smetric.py +30 -0
  350. package/runtime/python/okstra_vendor/networkx/algorithms/sparsifiers.py +296 -0
  351. package/runtime/python/okstra_vendor/networkx/algorithms/structuralholes.py +374 -0
  352. package/runtime/python/okstra_vendor/networkx/algorithms/summarization.py +564 -0
  353. package/runtime/python/okstra_vendor/networkx/algorithms/swap.py +406 -0
  354. package/runtime/python/okstra_vendor/networkx/algorithms/tests/__init__.py +0 -0
  355. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_asteroidal.py +23 -0
  356. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_boundary.py +154 -0
  357. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_bridges.py +144 -0
  358. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_broadcasting.py +109 -0
  359. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_chains.py +136 -0
  360. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_chordal.py +129 -0
  361. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_clique.py +300 -0
  362. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cluster.py +678 -0
  363. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_communicability.py +80 -0
  364. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_core.py +266 -0
  365. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_covering.py +85 -0
  366. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cuts.py +171 -0
  367. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cycles.py +984 -0
  368. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_d_separation.py +340 -0
  369. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dag.py +835 -0
  370. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_distance_measures.py +831 -0
  371. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_distance_regular.py +85 -0
  372. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dominance.py +299 -0
  373. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dominating.py +115 -0
  374. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_efficiency.py +58 -0
  375. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_euler.py +314 -0
  376. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_graph_hashing.py +872 -0
  377. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_graphical.py +163 -0
  378. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_hierarchy.py +46 -0
  379. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_hybrid.py +24 -0
  380. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_isolate.py +26 -0
  381. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_link_prediction.py +615 -0
  382. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_lowest_common_ancestors.py +459 -0
  383. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_matching.py +556 -0
  384. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_max_weight_clique.py +179 -0
  385. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_mis.py +62 -0
  386. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_moral.py +15 -0
  387. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_node_classification.py +140 -0
  388. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_non_randomness.py +60 -0
  389. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_perfect_graph.py +27 -0
  390. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_planar_drawing.py +274 -0
  391. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_planarity.py +556 -0
  392. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_polynomials.py +57 -0
  393. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_reciprocity.py +37 -0
  394. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_regular.py +88 -0
  395. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_richclub.py +149 -0
  396. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_similarity.py +1158 -0
  397. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_simple_paths.py +803 -0
  398. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_smallworld.py +76 -0
  399. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_smetric.py +8 -0
  400. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_sparsifiers.py +138 -0
  401. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_structuralholes.py +191 -0
  402. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_summarization.py +642 -0
  403. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_swap.py +179 -0
  404. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_threshold.py +270 -0
  405. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_time_dependent.py +431 -0
  406. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_tournament.py +161 -0
  407. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_triads.py +248 -0
  408. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_vitality.py +41 -0
  409. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_voronoi.py +103 -0
  410. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_walks.py +54 -0
  411. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_wiener.py +157 -0
  412. package/runtime/python/okstra_vendor/networkx/algorithms/threshold.py +981 -0
  413. package/runtime/python/okstra_vendor/networkx/algorithms/time_dependent.py +142 -0
  414. package/runtime/python/okstra_vendor/networkx/algorithms/tournament.py +406 -0
  415. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/__init__.py +5 -0
  416. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/beamsearch.py +90 -0
  417. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/breadth_first_search.py +576 -0
  418. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/depth_first_search.py +529 -0
  419. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgebfs.py +185 -0
  420. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgedfs.py +182 -0
  421. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/__init__.py +0 -0
  422. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_beamsearch.py +25 -0
  423. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_bfs.py +203 -0
  424. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_dfs.py +307 -0
  425. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_edgebfs.py +147 -0
  426. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_edgedfs.py +131 -0
  427. package/runtime/python/okstra_vendor/networkx/algorithms/tree/__init__.py +7 -0
  428. package/runtime/python/okstra_vendor/networkx/algorithms/tree/branchings.py +1042 -0
  429. package/runtime/python/okstra_vendor/networkx/algorithms/tree/coding.py +413 -0
  430. package/runtime/python/okstra_vendor/networkx/algorithms/tree/decomposition.py +88 -0
  431. package/runtime/python/okstra_vendor/networkx/algorithms/tree/distance_measures.py +219 -0
  432. package/runtime/python/okstra_vendor/networkx/algorithms/tree/mst.py +1281 -0
  433. package/runtime/python/okstra_vendor/networkx/algorithms/tree/operations.py +106 -0
  434. package/runtime/python/okstra_vendor/networkx/algorithms/tree/recognition.py +273 -0
  435. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/__init__.py +0 -0
  436. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_branchings.py +624 -0
  437. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_coding.py +114 -0
  438. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_decomposition.py +79 -0
  439. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_distance_measures.py +99 -0
  440. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_mst.py +934 -0
  441. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_operations.py +53 -0
  442. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_recognition.py +174 -0
  443. package/runtime/python/okstra_vendor/networkx/algorithms/triads.py +500 -0
  444. package/runtime/python/okstra_vendor/networkx/algorithms/vitality.py +76 -0
  445. package/runtime/python/okstra_vendor/networkx/algorithms/voronoi.py +86 -0
  446. package/runtime/python/okstra_vendor/networkx/algorithms/walks.py +77 -0
  447. package/runtime/python/okstra_vendor/networkx/algorithms/wiener.py +278 -0
  448. package/runtime/python/okstra_vendor/networkx/classes/__init__.py +13 -0
  449. package/runtime/python/okstra_vendor/networkx/classes/coreviews.py +435 -0
  450. package/runtime/python/okstra_vendor/networkx/classes/digraph.py +1363 -0
  451. package/runtime/python/okstra_vendor/networkx/classes/filters.py +95 -0
  452. package/runtime/python/okstra_vendor/networkx/classes/function.py +1549 -0
  453. package/runtime/python/okstra_vendor/networkx/classes/graph.py +2082 -0
  454. package/runtime/python/okstra_vendor/networkx/classes/graphviews.py +269 -0
  455. package/runtime/python/okstra_vendor/networkx/classes/multidigraph.py +977 -0
  456. package/runtime/python/okstra_vendor/networkx/classes/multigraph.py +1294 -0
  457. package/runtime/python/okstra_vendor/networkx/classes/reportviews.py +1447 -0
  458. package/runtime/python/okstra_vendor/networkx/classes/tests/__init__.py +0 -0
  459. package/runtime/python/okstra_vendor/networkx/classes/tests/dispatch_interface.py +192 -0
  460. package/runtime/python/okstra_vendor/networkx/classes/tests/historical_tests.py +476 -0
  461. package/runtime/python/okstra_vendor/networkx/classes/tests/test_coreviews.py +362 -0
  462. package/runtime/python/okstra_vendor/networkx/classes/tests/test_digraph.py +331 -0
  463. package/runtime/python/okstra_vendor/networkx/classes/tests/test_digraph_historical.py +110 -0
  464. package/runtime/python/okstra_vendor/networkx/classes/tests/test_filters.py +177 -0
  465. package/runtime/python/okstra_vendor/networkx/classes/tests/test_function.py +1045 -0
  466. package/runtime/python/okstra_vendor/networkx/classes/tests/test_graph.py +950 -0
  467. package/runtime/python/okstra_vendor/networkx/classes/tests/test_graph_historical.py +12 -0
  468. package/runtime/python/okstra_vendor/networkx/classes/tests/test_graphviews.py +349 -0
  469. package/runtime/python/okstra_vendor/networkx/classes/tests/test_multidigraph.py +459 -0
  470. package/runtime/python/okstra_vendor/networkx/classes/tests/test_multigraph.py +528 -0
  471. package/runtime/python/okstra_vendor/networkx/classes/tests/test_reportviews.py +1421 -0
  472. package/runtime/python/okstra_vendor/networkx/classes/tests/test_special.py +131 -0
  473. package/runtime/python/okstra_vendor/networkx/classes/tests/test_subgraphviews.py +371 -0
  474. package/runtime/python/okstra_vendor/networkx/conftest.py +261 -0
  475. package/runtime/python/okstra_vendor/networkx/convert.py +502 -0
  476. package/runtime/python/okstra_vendor/networkx/convert_matrix.py +1314 -0
  477. package/runtime/python/okstra_vendor/networkx/drawing/__init__.py +7 -0
  478. package/runtime/python/okstra_vendor/networkx/drawing/layout.py +2036 -0
  479. package/runtime/python/okstra_vendor/networkx/drawing/nx_agraph.py +470 -0
  480. package/runtime/python/okstra_vendor/networkx/drawing/nx_latex.py +570 -0
  481. package/runtime/python/okstra_vendor/networkx/drawing/nx_pydot.py +361 -0
  482. package/runtime/python/okstra_vendor/networkx/drawing/nx_pylab.py +2978 -0
  483. package/runtime/python/okstra_vendor/networkx/drawing/tests/__init__.py +0 -0
  484. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_complex.png +0 -0
  485. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_empty_graph.png +0 -0
  486. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_house_with_colors.png +0 -0
  487. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_labels_and_colors.png +0 -0
  488. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_shortest_path.png +0 -0
  489. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_house_with_colors.png +0 -0
  490. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_agraph.py +237 -0
  491. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_image_comparison_pylab_mpl.py +229 -0
  492. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_latex.py +285 -0
  493. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_layout.py +631 -0
  494. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_pydot.py +146 -0
  495. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_pylab.py +1582 -0
  496. package/runtime/python/okstra_vendor/networkx/exception.py +131 -0
  497. package/runtime/python/okstra_vendor/networkx/generators/__init__.py +34 -0
  498. package/runtime/python/okstra_vendor/networkx/generators/atlas.dat.gz +0 -0
  499. package/runtime/python/okstra_vendor/networkx/generators/atlas.py +227 -0
  500. package/runtime/python/okstra_vendor/networkx/generators/classic.py +1091 -0
  501. package/runtime/python/okstra_vendor/networkx/generators/cographs.py +68 -0
  502. package/runtime/python/okstra_vendor/networkx/generators/community.py +1070 -0
  503. package/runtime/python/okstra_vendor/networkx/generators/degree_seq.py +886 -0
  504. package/runtime/python/okstra_vendor/networkx/generators/directed.py +572 -0
  505. package/runtime/python/okstra_vendor/networkx/generators/duplication.py +174 -0
  506. package/runtime/python/okstra_vendor/networkx/generators/ego.py +66 -0
  507. package/runtime/python/okstra_vendor/networkx/generators/expanders.py +499 -0
  508. package/runtime/python/okstra_vendor/networkx/generators/geometric.py +1037 -0
  509. package/runtime/python/okstra_vendor/networkx/generators/harary_graph.py +163 -0
  510. package/runtime/python/okstra_vendor/networkx/generators/internet_as_graphs.py +443 -0
  511. package/runtime/python/okstra_vendor/networkx/generators/intersection.py +125 -0
  512. package/runtime/python/okstra_vendor/networkx/generators/interval_graph.py +70 -0
  513. package/runtime/python/okstra_vendor/networkx/generators/joint_degree_seq.py +664 -0
  514. package/runtime/python/okstra_vendor/networkx/generators/lattice.py +405 -0
  515. package/runtime/python/okstra_vendor/networkx/generators/line.py +501 -0
  516. package/runtime/python/okstra_vendor/networkx/generators/mycielski.py +110 -0
  517. package/runtime/python/okstra_vendor/networkx/generators/nonisomorphic_trees.py +259 -0
  518. package/runtime/python/okstra_vendor/networkx/generators/random_clustered.py +117 -0
  519. package/runtime/python/okstra_vendor/networkx/generators/random_graphs.py +1416 -0
  520. package/runtime/python/okstra_vendor/networkx/generators/small.py +1070 -0
  521. package/runtime/python/okstra_vendor/networkx/generators/social.py +554 -0
  522. package/runtime/python/okstra_vendor/networkx/generators/spectral_graph_forge.py +120 -0
  523. package/runtime/python/okstra_vendor/networkx/generators/stochastic.py +54 -0
  524. package/runtime/python/okstra_vendor/networkx/generators/sudoku.py +131 -0
  525. package/runtime/python/okstra_vendor/networkx/generators/tests/__init__.py +0 -0
  526. package/runtime/python/okstra_vendor/networkx/generators/tests/test_atlas.py +75 -0
  527. package/runtime/python/okstra_vendor/networkx/generators/tests/test_classic.py +642 -0
  528. package/runtime/python/okstra_vendor/networkx/generators/tests/test_cographs.py +20 -0
  529. package/runtime/python/okstra_vendor/networkx/generators/tests/test_community.py +362 -0
  530. package/runtime/python/okstra_vendor/networkx/generators/tests/test_degree_seq.py +224 -0
  531. package/runtime/python/okstra_vendor/networkx/generators/tests/test_directed.py +189 -0
  532. package/runtime/python/okstra_vendor/networkx/generators/tests/test_duplication.py +103 -0
  533. package/runtime/python/okstra_vendor/networkx/generators/tests/test_ego.py +39 -0
  534. package/runtime/python/okstra_vendor/networkx/generators/tests/test_expanders.py +182 -0
  535. package/runtime/python/okstra_vendor/networkx/generators/tests/test_geometric.py +488 -0
  536. package/runtime/python/okstra_vendor/networkx/generators/tests/test_harary_graph.py +133 -0
  537. package/runtime/python/okstra_vendor/networkx/generators/tests/test_internet_as_graphs.py +221 -0
  538. package/runtime/python/okstra_vendor/networkx/generators/tests/test_intersection.py +28 -0
  539. package/runtime/python/okstra_vendor/networkx/generators/tests/test_interval_graph.py +144 -0
  540. package/runtime/python/okstra_vendor/networkx/generators/tests/test_joint_degree_seq.py +125 -0
  541. package/runtime/python/okstra_vendor/networkx/generators/tests/test_lattice.py +264 -0
  542. package/runtime/python/okstra_vendor/networkx/generators/tests/test_line.py +316 -0
  543. package/runtime/python/okstra_vendor/networkx/generators/tests/test_mycielski.py +30 -0
  544. package/runtime/python/okstra_vendor/networkx/generators/tests/test_nonisomorphic_trees.py +82 -0
  545. package/runtime/python/okstra_vendor/networkx/generators/tests/test_random_clustered.py +33 -0
  546. package/runtime/python/okstra_vendor/networkx/generators/tests/test_random_graphs.py +495 -0
  547. package/runtime/python/okstra_vendor/networkx/generators/tests/test_small.py +220 -0
  548. package/runtime/python/okstra_vendor/networkx/generators/tests/test_spectral_graph_forge.py +49 -0
  549. package/runtime/python/okstra_vendor/networkx/generators/tests/test_stochastic.py +72 -0
  550. package/runtime/python/okstra_vendor/networkx/generators/tests/test_sudoku.py +92 -0
  551. package/runtime/python/okstra_vendor/networkx/generators/tests/test_time_series.py +64 -0
  552. package/runtime/python/okstra_vendor/networkx/generators/tests/test_trees.py +195 -0
  553. package/runtime/python/okstra_vendor/networkx/generators/tests/test_triads.py +15 -0
  554. package/runtime/python/okstra_vendor/networkx/generators/time_series.py +74 -0
  555. package/runtime/python/okstra_vendor/networkx/generators/trees.py +1070 -0
  556. package/runtime/python/okstra_vendor/networkx/generators/triads.py +94 -0
  557. package/runtime/python/okstra_vendor/networkx/lazy_imports.py +188 -0
  558. package/runtime/python/okstra_vendor/networkx/linalg/__init__.py +13 -0
  559. package/runtime/python/okstra_vendor/networkx/linalg/algebraicconnectivity.py +650 -0
  560. package/runtime/python/okstra_vendor/networkx/linalg/attrmatrix.py +466 -0
  561. package/runtime/python/okstra_vendor/networkx/linalg/bethehessianmatrix.py +77 -0
  562. package/runtime/python/okstra_vendor/networkx/linalg/graphmatrix.py +168 -0
  563. package/runtime/python/okstra_vendor/networkx/linalg/laplacianmatrix.py +512 -0
  564. package/runtime/python/okstra_vendor/networkx/linalg/modularitymatrix.py +166 -0
  565. package/runtime/python/okstra_vendor/networkx/linalg/spectrum.py +186 -0
  566. package/runtime/python/okstra_vendor/networkx/linalg/tests/__init__.py +0 -0
  567. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_algebraic_connectivity.py +400 -0
  568. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_attrmatrix.py +108 -0
  569. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_bethehessian.py +40 -0
  570. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_graphmatrix.py +275 -0
  571. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_laplacian.py +334 -0
  572. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_modularity.py +86 -0
  573. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_spectrum.py +70 -0
  574. package/runtime/python/okstra_vendor/networkx/readwrite/__init__.py +17 -0
  575. package/runtime/python/okstra_vendor/networkx/readwrite/adjlist.py +330 -0
  576. package/runtime/python/okstra_vendor/networkx/readwrite/edgelist.py +489 -0
  577. package/runtime/python/okstra_vendor/networkx/readwrite/gexf.py +1084 -0
  578. package/runtime/python/okstra_vendor/networkx/readwrite/gml.py +879 -0
  579. package/runtime/python/okstra_vendor/networkx/readwrite/graph6.py +427 -0
  580. package/runtime/python/okstra_vendor/networkx/readwrite/graphml.py +1053 -0
  581. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/__init__.py +19 -0
  582. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/adjacency.py +156 -0
  583. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/cytoscape.py +190 -0
  584. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/node_link.py +261 -0
  585. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/__init__.py +0 -0
  586. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_adjacency.py +78 -0
  587. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_cytoscape.py +78 -0
  588. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_node_link.py +109 -0
  589. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_tree.py +48 -0
  590. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tree.py +137 -0
  591. package/runtime/python/okstra_vendor/networkx/readwrite/leda.py +108 -0
  592. package/runtime/python/okstra_vendor/networkx/readwrite/multiline_adjlist.py +393 -0
  593. package/runtime/python/okstra_vendor/networkx/readwrite/p2g.py +113 -0
  594. package/runtime/python/okstra_vendor/networkx/readwrite/pajek.py +286 -0
  595. package/runtime/python/okstra_vendor/networkx/readwrite/sparse6.py +379 -0
  596. package/runtime/python/okstra_vendor/networkx/readwrite/tests/__init__.py +0 -0
  597. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_adjlist.py +354 -0
  598. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_edgelist.py +318 -0
  599. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_gexf.py +612 -0
  600. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_gml.py +744 -0
  601. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_graph6.py +181 -0
  602. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_graphml.py +1531 -0
  603. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_leda.py +30 -0
  604. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_p2g.py +63 -0
  605. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_pajek.py +128 -0
  606. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_sparse6.py +166 -0
  607. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_text.py +1742 -0
  608. package/runtime/python/okstra_vendor/networkx/readwrite/text.py +851 -0
  609. package/runtime/python/okstra_vendor/networkx/relabel.py +285 -0
  610. package/runtime/python/okstra_vendor/networkx/tests/__init__.py +0 -0
  611. package/runtime/python/okstra_vendor/networkx/tests/test_all_random_functions.py +248 -0
  612. package/runtime/python/okstra_vendor/networkx/tests/test_convert.py +321 -0
  613. package/runtime/python/okstra_vendor/networkx/tests/test_convert_numpy.py +531 -0
  614. package/runtime/python/okstra_vendor/networkx/tests/test_convert_pandas.py +349 -0
  615. package/runtime/python/okstra_vendor/networkx/tests/test_convert_scipy.py +281 -0
  616. package/runtime/python/okstra_vendor/networkx/tests/test_exceptions.py +40 -0
  617. package/runtime/python/okstra_vendor/networkx/tests/test_import.py +11 -0
  618. package/runtime/python/okstra_vendor/networkx/tests/test_lazy_imports.py +96 -0
  619. package/runtime/python/okstra_vendor/networkx/tests/test_relabel.py +349 -0
  620. package/runtime/python/okstra_vendor/networkx/tests/test_removed_functions_exception_messages.py +8 -0
  621. package/runtime/python/okstra_vendor/networkx/utils/__init__.py +8 -0
  622. package/runtime/python/okstra_vendor/networkx/utils/backends.py +2171 -0
  623. package/runtime/python/okstra_vendor/networkx/utils/configs.py +396 -0
  624. package/runtime/python/okstra_vendor/networkx/utils/decorators.py +1233 -0
  625. package/runtime/python/okstra_vendor/networkx/utils/heaps.py +338 -0
  626. package/runtime/python/okstra_vendor/networkx/utils/mapped_queue.py +297 -0
  627. package/runtime/python/okstra_vendor/networkx/utils/misc.py +703 -0
  628. package/runtime/python/okstra_vendor/networkx/utils/random_sequence.py +198 -0
  629. package/runtime/python/okstra_vendor/networkx/utils/rcm.py +159 -0
  630. package/runtime/python/okstra_vendor/networkx/utils/tests/__init__.py +0 -0
  631. package/runtime/python/okstra_vendor/networkx/utils/tests/test__init.py +11 -0
  632. package/runtime/python/okstra_vendor/networkx/utils/tests/test_backends.py +225 -0
  633. package/runtime/python/okstra_vendor/networkx/utils/tests/test_config.py +263 -0
  634. package/runtime/python/okstra_vendor/networkx/utils/tests/test_decorators.py +510 -0
  635. package/runtime/python/okstra_vendor/networkx/utils/tests/test_heaps.py +131 -0
  636. package/runtime/python/okstra_vendor/networkx/utils/tests/test_mapped_queue.py +268 -0
  637. package/runtime/python/okstra_vendor/networkx/utils/tests/test_misc.py +393 -0
  638. package/runtime/python/okstra_vendor/networkx/utils/tests/test_random_sequence.py +53 -0
  639. package/runtime/python/okstra_vendor/networkx/utils/tests/test_rcm.py +63 -0
  640. package/runtime/python/okstra_vendor/networkx/utils/tests/test_unionfind.py +55 -0
  641. package/runtime/python/okstra_vendor/networkx/utils/union_find.py +106 -0
  642. package/runtime/skills/okstra-graphify/SKILL.md +161 -0
  643. package/runtime/skills/okstra-inspect/SKILL.md +17 -9
  644. package/runtime/templates/reports/settings.template.json +4 -0
  645. package/src/cli-registry.mjs +7 -0
  646. package/src/commands/graphify.mjs +32 -0
  647. package/src/commands/lifecycle/doctor.mjs +9 -0
  648. package/src/lib/skill-catalog.mjs +1 -0
@@ -0,0 +1,1306 @@
1
+ """
2
+ ISMAGS Algorithm
3
+ ================
4
+
5
+ Provides a Python implementation of the ISMAGS algorithm. [1]_
6
+
7
+ ISMAGS does a symmetry analysis to find the constraints on isomorphisms if
8
+ we preclude yielding isomorphisms that differ by a symmetry of the subgraph.
9
+ For example, if the subgraph contains a 4-cycle, every isomorphism would have a
10
+ symmetric version with the nodes rotated relative to the original isomorphism.
11
+ By encoding these symmetries as constraints we reduce the search space for
12
+ isomorphisms and we also simplify processing the resulting isomorphisms.
13
+
14
+ ISMAGS finds (subgraph) isomorphisms between two graphs, taking the
15
+ symmetry of the subgraph into account. In most cases the VF2 algorithm is
16
+ faster (at least on small graphs) than this implementation, but in some cases
17
+ there are an exponential number of isomorphisms that are symmetrically
18
+ equivalent. In that case, the ISMAGS algorithm will provide only one isomorphism
19
+ per symmetry group, speeding up finding isomorphisms and avoiding the task of
20
+ post-processing many effectively identical isomorphisms.
21
+
22
+ >>> petersen = nx.petersen_graph()
23
+ >>> ismags = nx.isomorphism.ISMAGS(petersen, petersen)
24
+ >>> isomorphisms = list(ismags.isomorphisms_iter(symmetry=False))
25
+ >>> len(isomorphisms)
26
+ 120
27
+ >>> isomorphisms = list(ismags.isomorphisms_iter(symmetry=True))
28
+ >>> answer = [{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}]
29
+ >>> answer == isomorphisms
30
+ True
31
+
32
+ In addition, this implementation also provides an interface to find the
33
+ largest common induced subgraph [2]_ between any two graphs, again taking
34
+ symmetry into account. Given ``graph`` and ``subgraph`` the algorithm will remove
35
+ nodes from the ``subgraph`` until ``subgraph`` is isomorphic to a subgraph of
36
+ ``graph``. Since only the symmetry of ``subgraph`` is taken into account it is
37
+ worth thinking about how you provide your graphs:
38
+
39
+ >>> graph1 = nx.path_graph(4)
40
+ >>> graph2 = nx.star_graph(3)
41
+ >>> ismags = nx.isomorphism.ISMAGS(graph1, graph2)
42
+ >>> ismags.is_isomorphic()
43
+ False
44
+ >>> largest_common_subgraph = list(ismags.largest_common_subgraph())
45
+ >>> answer = [{1: 0, 0: 1, 2: 2}, {2: 0, 1: 1, 3: 2}]
46
+ >>> answer == largest_common_subgraph
47
+ True
48
+ >>> ismags2 = nx.isomorphism.ISMAGS(graph2, graph1)
49
+ >>> largest_common_subgraph = list(ismags2.largest_common_subgraph())
50
+ >>> answer = [
51
+ ... {1: 0, 0: 1, 2: 2},
52
+ ... {1: 0, 0: 1, 3: 2},
53
+ ... {2: 0, 0: 1, 1: 2},
54
+ ... {2: 0, 0: 1, 3: 2},
55
+ ... {3: 0, 0: 1, 1: 2},
56
+ ... {3: 0, 0: 1, 2: 2},
57
+ ... ]
58
+ >>> answer == largest_common_subgraph
59
+ True
60
+
61
+ However, when not taking symmetry into account, it doesn't matter:
62
+
63
+ >>> largest_common_subgraph = list(ismags.largest_common_subgraph(symmetry=False))
64
+ >>> answer = [
65
+ ... {1: 0, 0: 1, 2: 2},
66
+ ... {1: 0, 2: 1, 0: 2},
67
+ ... {2: 0, 1: 1, 3: 2},
68
+ ... {2: 0, 3: 1, 1: 2},
69
+ ... {1: 0, 0: 1, 2: 3},
70
+ ... {1: 0, 2: 1, 0: 3},
71
+ ... {2: 0, 1: 1, 3: 3},
72
+ ... {2: 0, 3: 1, 1: 3},
73
+ ... {1: 0, 0: 2, 2: 3},
74
+ ... {1: 0, 2: 2, 0: 3},
75
+ ... {2: 0, 1: 2, 3: 3},
76
+ ... {2: 0, 3: 2, 1: 3},
77
+ ... ]
78
+ >>> answer == largest_common_subgraph
79
+ True
80
+ >>> largest_common_subgraph = list(ismags2.largest_common_subgraph(symmetry=False))
81
+ >>> answer = [
82
+ ... {1: 0, 0: 1, 2: 2},
83
+ ... {1: 0, 0: 1, 3: 2},
84
+ ... {2: 0, 0: 1, 1: 2},
85
+ ... {2: 0, 0: 1, 3: 2},
86
+ ... {3: 0, 0: 1, 1: 2},
87
+ ... {3: 0, 0: 1, 2: 2},
88
+ ... {1: 1, 0: 2, 2: 3},
89
+ ... {1: 1, 0: 2, 3: 3},
90
+ ... {2: 1, 0: 2, 1: 3},
91
+ ... {2: 1, 0: 2, 3: 3},
92
+ ... {3: 1, 0: 2, 1: 3},
93
+ ... {3: 1, 0: 2, 2: 3},
94
+ ... ]
95
+ >>> answer == largest_common_subgraph
96
+ True
97
+
98
+ Notes
99
+ -----
100
+ - Node and edge equality is assumed to be transitive: if A is equal to B, and
101
+ B is equal to C, then A is equal to C.
102
+
103
+ - With a method that yields subgraph isomorphisms, we can construct functions like
104
+ ``is_subgraph_isomorphic`` by checking for any yielded mapping. And functions like
105
+ ``is_isomorphic`` by checking whether the subgraph has the same number of nodes as
106
+ the graph and is also subgraph isomorphic. This subpackage also allows a
107
+ ``symmetry`` bool keyword argument so you can find isomorphisms with or
108
+ without the symmetry constraints.
109
+
110
+ - For more information, see [2]_ and the documentation for :class:`ISMAGS`
111
+ which includes a description of the algorithm.
112
+
113
+ References
114
+ ----------
115
+ .. [1] M. Houbraken, S. Demeyer, T. Michoel, P. Audenaert, D. Colle,
116
+ M. Pickavet, "The Index-Based Subgraph Matching Algorithm with General
117
+ Symmetries (ISMAGS): Exploiting Symmetry for Faster Subgraph
118
+ Enumeration", PLoS One 9(5): e97896, 2014.
119
+ https://doi.org/10.1371/journal.pone.0097896
120
+ .. [2] https://en.wikipedia.org/wiki/Maximum_common_induced_subgraph
121
+ """
122
+
123
+ __all__ = ["ISMAGS"]
124
+
125
+ import itertools
126
+ from collections import Counter, defaultdict
127
+ from functools import reduce, wraps
128
+
129
+ import networkx as nx
130
+
131
+
132
+ def are_all_equal(iterable):
133
+ """
134
+ Returns ``True`` if and only if all elements in `iterable` are equal; and
135
+ ``False`` otherwise.
136
+
137
+ Parameters
138
+ ----------
139
+ iterable: collections.abc.Iterable
140
+ The container whose elements will be checked.
141
+
142
+ Returns
143
+ -------
144
+ bool
145
+ ``True`` iff all elements in `iterable` compare equal, ``False``
146
+ otherwise.
147
+ """
148
+ try:
149
+ shape = iterable.shape
150
+ except AttributeError:
151
+ pass
152
+ else:
153
+ if len(shape) > 1:
154
+ message = "The function does not works on multidimensional arrays."
155
+ raise NotImplementedError(message) from None
156
+
157
+ iterator = iter(iterable)
158
+ first = next(iterator, None)
159
+ return all(item == first for item in iterator)
160
+
161
+
162
+ def make_partition(items, test, check=True):
163
+ """
164
+ Partitions items into sets based on the outcome of ``test(item1, item2)``.
165
+ Pairs of items for which `test` returns `True` end up in the same set.
166
+
167
+ Parameters
168
+ ----------
169
+ items : collections.abc.Iterable[collections.abc.Hashable]
170
+ Items to partition
171
+ test : collections.abc.Callable[collections.abc.Hashable, collections.abc.Hashable]
172
+ A function that will be called with 2 arguments, taken from items.
173
+ Should return `True` if those 2 items match/tests so need to end up in the same
174
+ part of the partition, and `False` otherwise.
175
+ check : bool optional (default: True)
176
+ If ``True``, check that the resulting partition satisfies the match criteria.
177
+ Every item should match every item in its part and none outside the part.
178
+
179
+ Returns
180
+ -------
181
+ list[set]
182
+ A partition as a list of sets (the parts). Each set contains some of
183
+ the items in `items`, such that all items are in exactly one part and every
184
+ pair of items in each part matches. The following will be true:
185
+ ``all(thing_matcher(*pair) for pair in itertools.combinations(items, 2))``
186
+
187
+ Notes
188
+ -----
189
+ The function `test` is assumed to be transitive: if ``test(a, b)`` and
190
+ ``test(b, c)`` return ``True``, then ``test(a, c)`` must also be ``True``.
191
+ The function `test` is assumed to be commutative: if ``test(a, b)``
192
+ returns ``True`` then ``test(b, a)`` returns ``True``.
193
+ """
194
+ partition = []
195
+ for item in items:
196
+ for part in partition:
197
+ p_item = next(iter(part))
198
+ if test(item, p_item):
199
+ part.add(item)
200
+ break
201
+ else: # No break
202
+ partition.append({item})
203
+
204
+ if check:
205
+ if not all(
206
+ test(t1, t2) and test(t2, t1)
207
+ for part in partition
208
+ for t1, t2 in itertools.combinations(part, 2)
209
+ ):
210
+ raise nx.NetworkXError(
211
+ f"\nInvalid partition created with {test}.\n"
212
+ "Some items in a part do not match. This leads to\n"
213
+ f"{partition=}"
214
+ )
215
+ if not all(
216
+ not test(t1, t2) and not test(t2, t1)
217
+ for p1 in partition
218
+ for p2 in partition
219
+ if p1 != p2
220
+ for t1, t2 in itertools.product(p1, p2)
221
+ ):
222
+ raise nx.NetworkXError(
223
+ f"\nInvalid partition created with {test}.\n"
224
+ "Some items match multiple parts. This leads to\n"
225
+ f"{partition=}"
226
+ )
227
+ return [set(part) for part in partition]
228
+
229
+
230
+ def node_to_part_ID_dict(partition):
231
+ """
232
+ Creates a dictionary that maps each item in each part to the index of
233
+ the part to which it belongs.
234
+
235
+ Parameters
236
+ ----------
237
+ partition: collections.abc.Sequence[collections.abc.Iterable]
238
+ As returned by :func:`make_partition`.
239
+
240
+ Returns
241
+ -------
242
+ dict
243
+ """
244
+ return {node: ID for ID, part in enumerate(partition) for node in part}
245
+
246
+
247
+ def color_degree_by_node(G, n_colors, e_colors):
248
+ """Returns a dict by node to counts of edge and node color for that node.
249
+
250
+ This returns a dict by node to a 2-tuple of node color and degree by
251
+ (edge color and nbr color). E.g. ``{0: (1, {(0, 2): 5})}`` means that
252
+ node ``0`` has node type 1 and has 5 edges of type 0 that go to nodes of type 2.
253
+ Thus, this is a measure of degree (edge count) by color of edge and color
254
+ of the node on the other side of that edge.
255
+
256
+ For directed graphs the degree counts is a 2-tuple of (in, out) degree counts.
257
+
258
+ Ideally, if edge_match is None, this could get simplified to just the node
259
+ color on the other end of the edge. Similarly if node_match is None then only
260
+ edge color is tracked. And if both are None, we simply count the number of edges.
261
+ """
262
+ # n_colors might be incomplete when using `largest_common_subgraph()`
263
+ if len(n_colors) < len(G):
264
+ for n, nbrs in G.adjacency():
265
+ if n not in n_colors:
266
+ n_colors[n] = None
267
+ for v in nbrs:
268
+ e_colors[n, v] = None
269
+ # undirected colored degree
270
+ if not G.is_directed():
271
+ return {
272
+ u: (n_colors[u], Counter((e_colors[u, v], n_colors[v]) for v in nbrs))
273
+ for u, nbrs in G.adjacency()
274
+ }
275
+ # directed colored out and in degree
276
+ return {
277
+ u: (
278
+ n_colors[u],
279
+ Counter((e_colors[u, v], n_colors[v]) for v in nbrs),
280
+ Counter((e_colors[v, u], n_colors[v]) for v in G._pred[u]),
281
+ )
282
+ for u, nbrs in G.adjacency()
283
+ }
284
+
285
+
286
+ class EdgeLookup:
287
+ """Class to handle getitem for undirected edges.
288
+
289
+ Note that ``items()`` iterates over one of the two representations of the edge
290
+ (u, v) and (v, u). So this technically doesn't violate the Mapping
291
+ invariant that (k,v) pairs reported by ``items()`` satisfy ``.__getitem__(k) == v``.
292
+ But we are violating the spirit of the protocol by having keys available
293
+ for lookup by ``__getitem__`` that are not reported by ``items()``.
294
+
295
+ Note that if we used frozensets for undirected edges we would have the same
296
+ behavior we see here. You could ``__getitem__`` either ``{u, v}`` or ``{v, u}``
297
+ and get the same value -- yet ``items()`` would only report one of the two.
298
+ So from that perspective we *are* following the Mapping protocol. Our keys
299
+ are undirected edges. We are using 2-tuples as an imperfect representation
300
+ of these edges. We are not using 2-tuples as keys. Only as imperfect edges
301
+ and we use the edges as keys.
302
+ """
303
+
304
+ def __init__(self, edge_dict):
305
+ self.edge_dict = edge_dict
306
+
307
+ def __getitem__(self, edge):
308
+ if edge in self.edge_dict:
309
+ return self.edge_dict[edge]
310
+ return self.edge_dict[edge[::-1]]
311
+
312
+ def items(self):
313
+ return self.edge_dict.items()
314
+
315
+
316
+ class ISMAGS:
317
+ """
318
+ Implements the ISMAGS subgraph matching algorithm. [1]_ ISMAGS stands for
319
+ "Index-based Subgraph Matching Algorithm with General Symmetries". As the
320
+ name implies, it is symmetry aware and will only generate non-symmetric
321
+ isomorphisms.
322
+
323
+ Attributes
324
+ ----------
325
+ graph: networkx.Graph
326
+ subgraph: networkx.Graph
327
+
328
+ Notes
329
+ -----
330
+ ISMAGS does a symmetry analysis to find the constraints on isomorphisms if
331
+ we preclude yielding isomorphisms that differ by a symmetry of the subgraph.
332
+ For example, if the subgraph is a 4-cycle, every isomorphism would have a
333
+ symmetric version with the nodes rotated relative to the original isomorphism.
334
+ By encoding these symmetries as constraints we reduce the search space for
335
+ isomorphisms and we also simplify processing the resulting isomorphisms.
336
+
337
+ **Symmetry Analysis**
338
+
339
+ The constraints in ISMAGS are based off the handling in ``nauty`` and its many
340
+ variants, in particular ``saucy``, as discussed in the ISMAGS paper [1]_.
341
+ That paper cites [3]_ for details on symmetry handling. Figure 2 of [3]_
342
+ describes the DFS approach to symmetries used here and relying on a data structure
343
+ called an Ordered Pair Partitions(OPP). This consists of a pair of partitions
344
+ where each part represents nodes with the same degree-by-color over all colors.
345
+ We refine these partitions simultaneously in a way to result in permutations
346
+ of the nodes that preserve the graph structure. We thus find automorphisms
347
+ for the subgraph of interest. From those we identify pairs of nodes which
348
+ are structurally equivalent. We then constrain our problem by requiring the
349
+ first of the pair to always be assigned first in the isomorphism -- thus
350
+ constraining the isomorphisms reported to only one example from the set of all
351
+ symmetrically equivalent isomorphisms. These constraints are computed once
352
+ based on the subgraph symmetries and then used throughout the DFS search for
353
+ isomorphisms.
354
+
355
+ Finding the symmetries involves a DFS of the OPP wherein we "couple" a node
356
+ to a node in its degree-by-color part of the partition. This "coupling" is done
357
+ via assigning a new color in the top partition to the node being coupled,
358
+ and the same new color in the bottom partition to the node being coupled to.
359
+ This new color has only one node in each partition. The new color also requires
360
+ that we "refine" both top and bottom partitions by splitting parts until each
361
+ part represents a common degree-by-color value. Those refinements introduce
362
+ new colors as the parts are split during refinement. Parts do not get combined
363
+ during refinement. So the coupling/refining process always results in at least
364
+ one new part with only one node in both the top and bottom partition. In practice
365
+ we usually refine into many new one-node parts in both partitions.
366
+ We continue in this way until each node has its own part/color in the top partition
367
+ -- and the node in the bottom partition with that color is the symmetric node.
368
+ That is, an OPP represents an automorphism, and thus a symmetry
369
+ of the subgraph when each color has a single node in the top partition and a single
370
+ node in the bottom partition. From those automorphisms we build up a set of nodes
371
+ that can be obtained from each other by symmetry (they are mutually symmetric).
372
+ That set of nodes is called an "orbit" of the subgraph under symmetry.
373
+
374
+ After finding the orbits for one symmetry, we backtrack in the DFS by removing the
375
+ latest coupling and replacing it with a coupling from the same top node to a new
376
+ bottom node in its degree-by-color grouping. When all possible couplings for that
377
+ node are considered we backtrack to the previously coupled node and recouple in
378
+ a DFS manner.
379
+
380
+ We can prune the DFS search tree in helpful ways. The paper [2]_ demonstrates 6
381
+ situations of interest in the DFS where pruning is possible:
382
+
383
+ - An **Automorphism OPP** is an OPP where every part in both partitions
384
+ contains a single node. The mapping/automorphism is found by mapping
385
+ each top node to the bottom node in the same color part. For example,
386
+ ``[({1}, {2}, {3}); ({2}, {3}, {1})]`` represents the mapping of each
387
+ node to the next in a triangle. It rotates the nodes around the triangle.
388
+ - The **Identity OPP** is the first automorphism found during the DFS. It
389
+ appears on the left side of the DFS tree and is first due to our ordering of
390
+ coupling nodes to be in an arbitrary but fixed ordering of the nodes. This
391
+ automorphism does not show any symmetries, but it ensures the orbit for each
392
+ node includes itself and it sets us up for handling the symmetries. Note that
393
+ a subgraph with no symmetries will only have the identity automorphism.
394
+ - A **Non-isomorphic OPP** occurs when refinement creates a different number of
395
+ parts in the top partition than in the bottom partition. This means no symmetries
396
+ will be found during further processing of that subtree of the DFS. We prune
397
+ the subtree and continue.
398
+ - A **Matching OPP** is such that each top part that has more than one node is
399
+ in fact equal to the bottom part with the same color. The many-node-parts match
400
+ exactly. The single-node parts then represent symmetries that do not permute
401
+ any matching nodes. Matching OPPs arise while finding the Identity Mapping. But
402
+ the single-node parts are identical in the two partitions, so no useful symmetries
403
+ are found. But after the Identity Mapping is found, every Matching OPP encountered
404
+ will have different nodes in at least two single-node parts of the same color.
405
+ So these positions in the DFS provide us with symmetries without any
406
+ need to find the whole automorphism. We can prune the subtree, update the orbits
407
+ and backtrack. Any larger symmetries that combine these symmetries with symmetries
408
+ of the many-node-parts do not need to be explored because the symmetry "generators"
409
+ found in this way provide a basis for all symmetries. We will find the symmetry
410
+ generators of the many-node-parts at another subtree of the DFS.
411
+ - An **Orbit Pruning OPP** is an OPP where the node coupling to be considered next
412
+ has both nodes already known to be in the same orbit. We have already identified
413
+ those permutations when we discovered the orbit. So we can prune the resulting
414
+ subtree. This is the primary pruning discussed in [1]_.
415
+ - A **Coset Point** in the DFS is a point of the tree when a node is first
416
+ back-tracked. That is, its couplings have all been analyzed once and we backtrack
417
+ to its parent. So, said another way, when a node is backtracked to and is about to
418
+ be mapped to a different node for the first time, its child in the DFS has been
419
+ completely analysed. Thus the orbit for that child at this point in the DFS is
420
+ the full orbit for symmetries involving only that child or larger nodes in the
421
+ node order. All smaller nodes are mapped to themselves.
422
+ This orbit is due to symmetries not involving smaller nodes. Such an orbit is
423
+ called the "coset" of that node. The Coset Point does not lead to pruning or to
424
+ more symmetries. It is the point in the process where we store the **coset** of
425
+ the node being backtracked. We use the cosets to construct the symmetry
426
+ constraints.
427
+
428
+ Once the pruned DFS tree has been traversed, we have collected the cosets of some
429
+ special nodes. Often most nodes are not coupled during the progression down the left
430
+ side of the DFS. They are separated from other nodes during the partition refinement
431
+ process after coupling. So they never get coupled directly. Thus the number of cosets
432
+ we find is typically many fewer than the number of nodes.
433
+
434
+ We turn those cosets into constraints on the nodes when building non-symmetric
435
+ isomorphisms. The node whose coset is used is paired with each other node in the
436
+ coset. These node-pairs form the constraints. During isomorphism construction we
437
+ always select the first of the constraint before the other. This removes subtrees
438
+ from the DFS traversal space used to build isomorphisms.
439
+
440
+ The constraints we obtain via symmetry analysis of the subgraph are used for
441
+ finding non-symmetric isomorphisms. We prune the isomorphism tree based on
442
+ the constraints we obtain from the symmetry analysis.
443
+
444
+ **Isomorphism Construction**
445
+
446
+ Once we have symmetry constraints on the isomorphisms, ISMAGS constructs the allowed
447
+ isomorphisms by mapping each node of the subgraph to all possible nodes (with the
448
+ same degree-by-color) from the graph. We partition all nodes into degree-by-color
449
+ parts and order the subgraph nodes we consider using smallest part size first.
450
+ The idea is to try to map the most difficult subgraph nodes first (most difficult
451
+ here means least number of graph candidates).
452
+
453
+ By considering each potential subgraph node to graph candidate mapping image in turn,
454
+ we perform a DFS traversal of partial mappings. If the mapping is rejected due to
455
+ the graph neighbors not matching the degree-by-color of the subgraph neighbors, or
456
+ rejected due to the constraints imposed from symmetry, we prune that subtree and
457
+ consider a new graph candidate node for that subgraph node. When no more graph
458
+ candidates remain we backtrack to the previous node in the mapping and consider a
459
+ new graph candidate for that node. If we ever get to a depth where all subgraph nodes
460
+ are mapped and no structural requirements or symmetry constraints are violated,
461
+ we have found an isomorphism. We yield that mapping and backtrack to find other
462
+ isomorphisms.
463
+
464
+ As we visit more neighbors, the graph candidate nodes have to satisfy more structural
465
+ restrictions. As described in the ISMAGS paper, [1]_, we store each set of structural
466
+ restrictions separately as a set of possible candidate nodes rather than computing
467
+ the intersection of that set with the known graph candidates for the subgraph node.
468
+ We delay taking the intersection until that node is selected to be in the mapping.
469
+ While choosing the node with fewest candidates, we avoid computing the intersection
470
+ by using the size of the minimal set to be intersected rather than the size of the
471
+ intersection. This may make the node ordering slightly worse via a savings of
472
+ many intersections most of which are not ever needed.
473
+
474
+ References
475
+ ----------
476
+ .. [1] M. Houbraken, S. Demeyer, T. Michoel, P. Audenaert, D. Colle,
477
+ M. Pickavet, "The Index-Based Subgraph Matching Algorithm with General
478
+ Symmetries (ISMAGS): Exploiting Symmetry for Faster Subgraph
479
+ Enumeration", PLoS One 9(5): e97896, 2014.
480
+ https://doi.org/10.1371/journal.pone.0097896
481
+ .. [2] https://en.wikipedia.org/wiki/Maximum_common_induced_subgraph
482
+ .. [3] Hadi Katebi, Karem A. Sakallah and Igor L. Markov
483
+ "Graph Symmetry Detection and Canonical Labeling: Differences and Synergies"
484
+ in "Turing-100. The Alan Turing Centenary" Ed: A. Voronkov p. 181 -- 195, (2012).
485
+ https://doi.org/10.29007/gzc1 https://arxiv.org/abs/1208.6271
486
+ """
487
+
488
+ def __init__(self, graph, subgraph, node_match=None, edge_match=None, cache=None):
489
+ """
490
+ Parameters
491
+ ----------
492
+ graph: networkx.Graph
493
+ subgraph: networkx.Graph
494
+ node_match: collections.abc.Callable or None
495
+ Function used to determine whether two nodes are equivalent. Its
496
+ signature should look like ``f(n1: dict, n2: dict) -> bool``, with
497
+ `n1` and `n2` node property dicts. See also
498
+ :func:`~networkx.algorithms.isomorphism.categorical_node_match` and
499
+ friends.
500
+ If `None`, all nodes are considered equal.
501
+ edge_match: collections.abc.Callable or None
502
+ Function used to determine whether two edges are equivalent. Its
503
+ signature should look like ``f(e1: dict, e2: dict) -> bool``, with
504
+ `e1` and `e2` edge property dicts. See also
505
+ :func:`~networkx.algorithms.isomorphism.categorical_edge_match` and
506
+ friends.
507
+ If `None`, all edges are considered equal.
508
+ cache: collections.abc.Mapping
509
+ A cache used for caching graph symmetries.
510
+ """
511
+ if graph.is_directed() != subgraph.is_directed():
512
+ raise ValueError("Directed and undirected graphs cannot be compared.")
513
+
514
+ # TODO: allow for precomputed partitions and colors
515
+ self.graph = graph
516
+ self.subgraph = subgraph
517
+ self._symmetry_cache = cache
518
+ # Naming conventions are taken from the original paper.
519
+ # For your sanity:
520
+ # sg: subgraph
521
+ # g: graph
522
+ # e: edge(s)
523
+ # n: node(s)
524
+ # So: sgn means "subgraph nodes".
525
+ node_parts = self.create_aligned_partitions(
526
+ node_match, self.subgraph.nodes, self.graph.nodes
527
+ )
528
+ self._sgn_partition, self._gn_partition, self.N_node_colors = node_parts
529
+ self._sgn_colors = node_to_part_ID_dict(self._sgn_partition)
530
+ self._gn_colors = node_to_part_ID_dict(self._gn_partition)
531
+
532
+ edge_partitions = self.create_aligned_partitions(
533
+ edge_match, self.subgraph.edges(), self.graph.edges()
534
+ )
535
+ self._sge_partition, self._ge_partition, self.N_edge_colors = edge_partitions
536
+ if self.graph.is_directed():
537
+ self._sge_colors = node_to_part_ID_dict(self._sge_partition)
538
+ self._ge_colors = node_to_part_ID_dict(self._ge_partition)
539
+ else: # allow lookups (u, v) or (v, u)
540
+ self._sge_colors = EdgeLookup(node_to_part_ID_dict(self._sge_partition))
541
+ self._ge_colors = EdgeLookup(node_to_part_ID_dict(self._ge_partition))
542
+
543
+ def create_aligned_partitions(self, thing_matcher, sg_things, g_things):
544
+ """Partitions of "things" (nodes or edges) from subgraph and graph
545
+ based on function `thing_matcher`.
546
+
547
+ Returns: sg_partition, g_partition, number_of_matched_parts
548
+
549
+ The first `number_of_matched_parts` parts in each partition
550
+ match in order, e.g. 2nd part matches other's 2nd part.
551
+ Warning: nodes in parts after that have no matching nodes in the other graph.
552
+ For morphisms those nodes can't appear in the mapping.
553
+ """
554
+ if thing_matcher is None:
555
+ sg_partition = [set(sg_things)]
556
+ g_partition = [set(g_things)]
557
+ return sg_partition, g_partition, 1
558
+
559
+ # Use thing_matcher to create a partition
560
+ # Note: isinstance(G.edges(), OutEdgeDataView) is only true for multi(di)graph
561
+ sg_multiedge = isinstance(sg_things, nx.classes.reportviews.OutEdgeDataView)
562
+ g_multiedge = isinstance(g_things, nx.classes.reportviews.OutEdgeDataView)
563
+ if not sg_multiedge:
564
+
565
+ def sg_match(thing1, thing2):
566
+ return thing_matcher(sg_things[thing1], sg_things[thing2])
567
+
568
+ else: # multiedges (note nodes of multigraphs use simple case above)
569
+
570
+ def sg_match(thing1, thing2):
571
+ (u1, v1), (u2, v2) = thing1, thing2
572
+ return thing_matcher(self.subgraph[u1][v1], self.subgraph[u2][v2])
573
+
574
+ if not g_multiedge:
575
+
576
+ def g_match(thing1, thing2):
577
+ return thing_matcher(g_things[thing1], g_things[thing2])
578
+
579
+ else: # multiedges (note nodes of multigraphs use simple case above)
580
+
581
+ def g_match(thing1, thing2):
582
+ (u1, v1), (u2, v2) = thing1, thing2
583
+ return thing_matcher(self.graph[u1][v1], self.graph[u2][v2])
584
+
585
+ sg_partition = make_partition(sg_things, sg_match)
586
+ g_partition = make_partition(g_things, g_match)
587
+
588
+ # Align order of g_partition to that of sg_partition
589
+ sgc_to_gc = {}
590
+ gc_to_sgc = {}
591
+ sN, N = len(sg_partition), len(g_partition)
592
+ for sgc, gc in itertools.product(range(sN), range(N)):
593
+ sgt = next(iter(sg_partition[sgc]))
594
+ gt = next(iter(g_partition[gc]))
595
+ sgt_ = sg_things[sgt] if not sg_multiedge else self.subgraph[sgt[0]][sgt[1]]
596
+ gt_ = g_things[gt] if not g_multiedge else self.graph[gt[0]][gt[1]]
597
+ if thing_matcher(sgt_, gt_):
598
+ # TODO: remove these two if-checks when confident they never arise
599
+ # The `check` feature in match_partitions should ensure they do not
600
+ if sgc in sgc_to_gc:
601
+ raise nx.NetworkXError(
602
+ f"\nMatching function {thing_matcher} seems faulty.\n"
603
+ f"Partition found: {sg_partition=}\n"
604
+ f"So {sgt} in subgraph part {sg_partition[sgc]} matches two "
605
+ f"graph parts {g_partition[gc]} and "
606
+ f"{g_partition[sgc_to_gc[sgc]]}\n"
607
+ )
608
+ if gc in gc_to_sgc:
609
+ raise nx.NetworkXError(
610
+ f"\nMatching function seems broken: {thing_matcher}\n"
611
+ f"Partitions found: {g_partition=} {sg_partition=}\n"
612
+ f"So {gt} in graph part {g_partition[gc]} matches two "
613
+ f"subgraph parts {sg_partition[sgc]} and "
614
+ f"{sg_partition[gc_to_sgc[gc]]}\n"
615
+ )
616
+ sgc_to_gc[sgc] = gc
617
+ gc_to_sgc[gc] = sgc
618
+ ## return two lists and the number of partitions that match.
619
+ new_order = [
620
+ (sg_partition[sgc], g_partition[gc]) for sgc, gc in sgc_to_gc.items()
621
+ ]
622
+ Ncolors = len(new_order)
623
+ if Ncolors:
624
+ new_sg_p, new_g_p = [list(x) for x in zip(*new_order)]
625
+ else:
626
+ new_sg_p, new_g_p = [], []
627
+ if Ncolors < sN:
628
+ extra = [sg_partition[c] for c in range(sN) if c not in sgc_to_gc]
629
+ new_sg_p = list(new_sg_p) + extra
630
+ new_g_p = list(new_g_p) + [set()] * len(extra)
631
+ if Ncolors < N:
632
+ extra = [g_partition[c] for c in range(N) if c not in gc_to_sgc]
633
+ new_g_p = list(new_g_p) + extra
634
+ new_sg_p = list(new_sg_p) + [set()] * len(extra)
635
+
636
+ return new_sg_p, new_g_p, Ncolors
637
+
638
+ def find_isomorphisms(self, symmetry=True):
639
+ """Find all subgraph isomorphisms between subgraph and graph
640
+
641
+ Finds isomorphisms where :attr:`subgraph` <= :attr:`graph`.
642
+
643
+ Parameters
644
+ ----------
645
+ symmetry: bool
646
+ Whether symmetry should be taken into account. If False, found
647
+ isomorphisms may be symmetrically equivalent.
648
+
649
+ Yields
650
+ ------
651
+ dict
652
+ The found isomorphism mappings of {graph_node: subgraph_node}.
653
+ """
654
+ # The networkx VF2 algorithm is slightly funny in when it yields an
655
+ # empty dict and when not.
656
+ if not self.subgraph:
657
+ yield {}
658
+ return
659
+ elif not self.graph:
660
+ return
661
+ elif len(self.graph) < len(self.subgraph):
662
+ return
663
+ elif len(self._sgn_partition) > self.N_node_colors:
664
+ # some subgraph nodes have a color that doesn't occur in graph
665
+ return
666
+ elif len(self._sge_partition) > self.N_edge_colors:
667
+ # some subgraph edges have a color that doesn't occur in graph
668
+ return
669
+
670
+ if symmetry:
671
+ cosets = self.analyze_subgraph_symmetry()
672
+ # Turn cosets into constraints.
673
+ constraints = [(n, co) for n, cs in cosets.items() for co in cs if n != co]
674
+ else:
675
+ constraints = []
676
+
677
+ cand_sets = self._get_node_color_candidate_sets()
678
+
679
+ lookahead_candidates = self._get_color_degree_candidates()
680
+ for sgn, lookahead_cands in lookahead_candidates.items():
681
+ cand_sets[sgn].add(frozenset(lookahead_cands))
682
+
683
+ if any(cand_sets.values()):
684
+ # Choose start node based on a heuristic for the min # of candidates
685
+ # Heuristic here is length of smallest frozenset in candidates' set
686
+ # of frozensets for that node. Using the smallest length avoids
687
+ # computing the intersection of the frozensets for each node.
688
+ start_sgn = min(cand_sets, key=lambda n: min(len(x) for x in cand_sets[n]))
689
+ cand_sets[start_sgn] = (frozenset.intersection(*cand_sets[start_sgn]),)
690
+ yield from self._map_nodes(start_sgn, cand_sets, constraints)
691
+ return
692
+
693
+ def _get_color_degree_candidates(self):
694
+ """
695
+ Returns a mapping of {subgraph node: set of graph nodes} for
696
+ which the graph nodes are feasible mapping candidate_sets for the
697
+ subgraph node, as determined by looking ahead one edge.
698
+ """
699
+ g_deg = color_degree_by_node(self.graph, self._gn_colors, self._ge_colors)
700
+ sg_deg = color_degree_by_node(self.subgraph, self._sgn_colors, self._sge_colors)
701
+
702
+ return {
703
+ sgn: {
704
+ gn
705
+ for gn, (_, *g_counts) in g_deg.items()
706
+ if all(
707
+ sg_cnt <= g_counts[idx][color]
708
+ for idx, counts in enumerate(needed_counts)
709
+ for color, sg_cnt in counts.items()
710
+ )
711
+ }
712
+ for sgn, (_, *needed_counts) in sg_deg.items()
713
+ }
714
+
715
+ def largest_common_subgraph(self, symmetry=True):
716
+ """
717
+ Find the largest common induced subgraphs between :attr:`subgraph` and
718
+ :attr:`graph`.
719
+
720
+ Parameters
721
+ ----------
722
+ symmetry: bool
723
+ Whether symmetry should be taken into account. If False, found
724
+ largest common subgraphs may be symmetrically equivalent.
725
+
726
+ Yields
727
+ ------
728
+ dict
729
+ The found isomorphism mappings of {graph_node: subgraph_node}.
730
+ """
731
+ # The networkx VF2 algorithm is slightly funny in when it yields an
732
+ # empty dict and when not.
733
+ if not self.subgraph:
734
+ yield {}
735
+ return
736
+ elif not self.graph:
737
+ return
738
+
739
+ if symmetry:
740
+ cosets = self.analyze_subgraph_symmetry()
741
+ # Turn cosets into constraints.
742
+ constraints = [(n, cn) for n, cs in cosets.items() for cn in cs if n != cn]
743
+ else:
744
+ constraints = []
745
+
746
+ candidate_sets = self._get_node_color_candidate_sets()
747
+
748
+ if any(candidate_sets.values()):
749
+ relevant_parts = self._sgn_partition[: self.N_node_colors]
750
+ to_be_mapped = {frozenset(n for p in relevant_parts for n in p)}
751
+ yield from self._largest_common_subgraph(
752
+ candidate_sets, constraints, to_be_mapped
753
+ )
754
+ else:
755
+ return
756
+
757
+ def analyze_subgraph_symmetry(self):
758
+ """
759
+ Find a minimal set of permutations and corresponding co-sets that
760
+ describe the symmetry of ``self.subgraph``, given the node and edge
761
+ equalities given by `node_partition` and `edge_colors`, respectively.
762
+
763
+ Returns
764
+ -------
765
+ dict[collections.abc.Hashable, set[collections.abc.Hashable]]
766
+ The found co-sets. The co-sets is a dictionary of
767
+ ``{node key: set of node keys}``.
768
+ Every key-value pair describes which ``values`` can be interchanged
769
+ without changing nodes less than ``key``.
770
+ """
771
+ partition, edge_colors = self._sgn_partition, self._sge_colors
772
+
773
+ if self._symmetry_cache is not None:
774
+ key = hash(
775
+ (
776
+ tuple(self.subgraph.nodes),
777
+ tuple(self.subgraph.edges),
778
+ tuple(map(tuple, node_partition)),
779
+ tuple(edge_colors.items()),
780
+ self.subgraph.is_directed(),
781
+ )
782
+ )
783
+ if key in self._symmetry_cache:
784
+ return self._symmetry_cache[key]
785
+ partition = self._refine_node_partition(self.subgraph, partition, edge_colors)
786
+ cosets = self._process_ordered_pair_partitions(
787
+ self.subgraph, partition, partition, edge_colors
788
+ )
789
+ if self._symmetry_cache is not None:
790
+ self._symmetry_cache[key] = cosets
791
+ return cosets
792
+
793
+ def is_isomorphic(self, symmetry=False):
794
+ """
795
+ Returns True if :attr:`graph` is isomorphic to :attr:`subgraph` and
796
+ False otherwise.
797
+
798
+ Returns
799
+ -------
800
+ bool
801
+ """
802
+ return len(self.subgraph) == len(self.graph) and self.subgraph_is_isomorphic(
803
+ symmetry
804
+ )
805
+
806
+ def subgraph_is_isomorphic(self, symmetry=False):
807
+ """
808
+ Returns True if a subgraph of :attr:`graph` is isomorphic to
809
+ :attr:`subgraph` and False otherwise.
810
+
811
+ Returns
812
+ -------
813
+ bool
814
+ """
815
+ # symmetry=False, since we only need to know whether there is any
816
+ # example; figuring out all symmetry elements probably costs more time
817
+ # than it gains.
818
+ isom = next(self.subgraph_isomorphisms_iter(symmetry=symmetry), None)
819
+ return isom is not None
820
+
821
+ def isomorphisms_iter(self, symmetry=True):
822
+ """
823
+ Does the same as :meth:`find_isomorphisms` if :attr:`graph` and
824
+ :attr:`subgraph` have the same number of nodes.
825
+ """
826
+ if len(self.graph) == len(self.subgraph):
827
+ yield from self.subgraph_isomorphisms_iter(symmetry=symmetry)
828
+
829
+ def subgraph_isomorphisms_iter(self, symmetry=True):
830
+ """Alternative name for :meth:`find_isomorphisms`."""
831
+ return self.find_isomorphisms(symmetry)
832
+
833
+ def _get_node_color_candidate_sets(self):
834
+ """
835
+ Per node in subgraph find all nodes in graph that have the same color.
836
+ Stored as a dict-of-set-of-frozenset. The dict is keyed by node to a
837
+ collection of frozensets of graph nodes. Each of these frozensets are
838
+ a restriction. The node can be mapped only to nodes in the frozenset.
839
+ Thus it must be mapped to nodes in the intersection of all these sets.
840
+ We store the sets to delay taking the intersection of them. This helps
841
+ for two reasons: Firstly any duplicate restriction sets can be ignored;
842
+ Secondly, some nodes will not need the intersection to be constructed.
843
+ Note: a dict-of-list-of-set would store duplicate sets in the list and
844
+ we want to avoid that. But I wonder if checking hash/equality when `add`ing
845
+ removes the benefit of avoiding computing intersections.
846
+ """
847
+ candidate_sets = defaultdict(set)
848
+ for sgn in self.subgraph.nodes:
849
+ sgn_color = self._sgn_colors[sgn]
850
+ if sgn_color >= self.N_node_colors: # color has no candidates
851
+ candidate_sets[sgn] # creates empty set entry in defaultdict
852
+ else:
853
+ candidate_sets[sgn].add(frozenset(self._gn_partition[sgn_color]))
854
+ return dict(candidate_sets)
855
+
856
+ @classmethod
857
+ def _refine_node_partition(cls, graph, partition, edge_colors):
858
+ def equal_color(node1, node2):
859
+ return color_degree[node1] == color_degree[node2]
860
+
861
+ node_colors = node_to_part_ID_dict(partition)
862
+ color_degree = color_degree_by_node(graph, node_colors, edge_colors)
863
+ while not all(are_all_equal(color_degree[n] for n in p) for p in partition):
864
+ partition = [
865
+ p
866
+ for part in partition
867
+ for p in (
868
+ [part]
869
+ if are_all_equal(color_degree[n] for n in part)
870
+ else sorted(make_partition(part, equal_color, check=False), key=len)
871
+ )
872
+ ]
873
+ node_colors = node_to_part_ID_dict(partition)
874
+ color_degree = color_degree_by_node(graph, node_colors, edge_colors)
875
+ return partition
876
+
877
+ def _map_nodes(self, sgn, candidate_sets, constraints, to_be_mapped=None):
878
+ """
879
+ Find all subgraph isomorphisms honoring constraints.
880
+ The collection `candidate_sets` is stored as a dict-of-set-of-frozenset.
881
+ The dict is keyed by node to a collection of candidate frozensets. Any
882
+ viable candidate must belong to all the frozensets in the collection.
883
+ So each frozenset added to the collection is a restriction on the candidates.
884
+
885
+ According to the paper, we store the collection of sets rather than their
886
+ intersection to delay computing many intersections with the hope of avoiding
887
+ them completely. Having the middle collection be a set also means that
888
+ duplicate restrictions on candidates are ignored, avoiding another intersection.
889
+ """
890
+ # shortcuts for speed
891
+ subgraph = self.subgraph
892
+ subgraph_adj = subgraph._adj
893
+ graph = self.graph
894
+ graph_adj = graph._adj
895
+ self_ge_partition = self._ge_partition
896
+ self_sge_colors = self._sge_colors
897
+ is_directed = subgraph.is_directed()
898
+
899
+ gn_ID_to_node = list(graph)
900
+ gn_node_to_ID = {n: id for id, n in enumerate(graph)}
901
+
902
+ mapping = {}
903
+ rev_mapping = {}
904
+ if to_be_mapped is None:
905
+ to_be_mapped = subgraph_adj.keys()
906
+
907
+ # Note that we don't copy candidates here. This means we leak
908
+ # information between the branches of the search. This is intentional!
909
+ # Specifically, we modify candidates here. That's OK because we substitute
910
+ # the set of frozensets with a set containing the frozenset intersection.
911
+ # So, it doesn't change the membership rule or the length rule for sorting.
912
+ # Membership: any candidate must be an element of each of the frozensets.
913
+ # Length: length of the intersection set. Use heuristic min(len of frozensets).
914
+ # This intersection improves future length heuristics which can only occur
915
+ # after this element of the queu is popped. But it means future additional
916
+ # restriction frozensets that duplicate previous ones are not ignored.
917
+ sgn_candidates = frozenset.intersection(*candidate_sets[sgn])
918
+ candidate_sets[sgn] = {sgn_candidates}
919
+ queue = [(sgn, candidate_sets, iter(sgn_candidates))]
920
+ while queue: # DFS over all possible mappings
921
+ sgn, candidate_sets, sgn_cand_iter = queue[-1]
922
+
923
+ for gn in sgn_cand_iter:
924
+ # We're going to try to map sgn to gn.
925
+ if gn in rev_mapping:
926
+ continue # pragma: no cover
927
+
928
+ # REDUCTION and COMBINATION
929
+ if sgn in mapping:
930
+ old_gn = mapping[sgn]
931
+ del rev_mapping[old_gn]
932
+ mapping[sgn] = gn
933
+ rev_mapping[gn] = sgn
934
+ # BASECASE
935
+ if len(mapping) == len(to_be_mapped):
936
+ yield rev_mapping.copy()
937
+ del mapping[sgn]
938
+ del rev_mapping[gn]
939
+ continue
940
+ left_to_map = to_be_mapped - mapping.keys()
941
+
942
+ # We copy the candidates dict. But it is not a deepcopy.
943
+ # This avoids inner set copies, yet still allows updates b/c setitem
944
+ # changes sgn in new dict without changing original set.
945
+ # Below be careful to not change the sets of frozensets.
946
+ cand_sets = candidate_sets.copy()
947
+
948
+ # update the candidate_sets for unmapped sgn based on sgn mapped
949
+ if not is_directed:
950
+ sgn_nbrs = subgraph_adj[sgn]
951
+ not_gn_nbrs = graph_adj.keys() - graph_adj[gn].keys()
952
+ for sgn2 in left_to_map:
953
+ # edge color must match when sgn2 connected to sgn
954
+ if sgn2 not in sgn_nbrs:
955
+ gn2_cands = not_gn_nbrs
956
+ else:
957
+ g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]]
958
+ gn2_cands = {n for e in g_edges if gn in e for n in e}
959
+ # Node color compatibility should be taken care of by the
960
+ # initial candidate lists made by find_subgraphs
961
+
962
+ # Add gn2_cands to the right collection.
963
+ # Do not change the original set. So do not use |= operator
964
+ cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)}
965
+ else: # directed
966
+ sgn_nbrs = subgraph_adj[sgn].keys()
967
+ sgn_preds = subgraph._pred[sgn].keys()
968
+ not_gn_nbrs = (
969
+ graph_adj.keys() - graph_adj[gn].keys() - graph._pred[gn].keys()
970
+ )
971
+ for sgn2 in left_to_map:
972
+ # edge color must match when sgn2 connected to sgn
973
+ if sgn2 not in sgn_nbrs:
974
+ if sgn2 not in sgn_preds:
975
+ gn2_cands = not_gn_nbrs
976
+ else: # sgn2 in sgn_preds
977
+ g_edges = self_ge_partition[self_sge_colors[sgn2, sgn]]
978
+ gn2_cands = {e[0] for e in g_edges if gn == e[1]}
979
+ else:
980
+ if sgn2 not in sgn_preds:
981
+ g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]]
982
+ gn2_cands = {e[1] for e in g_edges if gn == e[0]}
983
+ else:
984
+ # gn2 must have correct color in both directions
985
+ g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]]
986
+ gn2_cands = {e[1] for e in g_edges if gn == e[0]}
987
+ g_edges = self_ge_partition[self_sge_colors[sgn2, sgn]]
988
+ gn2_cands &= {e[0] for e in g_edges if gn == e[1]}
989
+ # Do not change the original set. So do not use |= operator
990
+ cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)}
991
+
992
+ for sgn2 in left_to_map:
993
+ # symmetry must match. constraints mean gn2>gn iff sgn2>sgn
994
+ if (sgn, sgn2) in constraints:
995
+ gn2_cands = set(gn_ID_to_node[gn_node_to_ID[gn] + 1 :])
996
+ elif (sgn2, sgn) in constraints:
997
+ gn2_cands = set(gn_ID_to_node[: gn_node_to_ID[gn]])
998
+ else:
999
+ continue # pragma: no cover
1000
+ # Do not change the original set. So do not use |= operator
1001
+ cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)}
1002
+
1003
+ # The next node is the one that is unmapped and has fewest candidates
1004
+ # Use the heuristic of the min size of the frozensets rather than
1005
+ # intersection of all frozensets to delay computing intersections.
1006
+ new_sgn = min(
1007
+ left_to_map, key=lambda n: min(len(x) for x in cand_sets[n])
1008
+ )
1009
+ new_sgn_candidates = frozenset.intersection(*cand_sets[new_sgn])
1010
+ if not new_sgn_candidates:
1011
+ continue
1012
+ cand_sets[new_sgn] = {new_sgn_candidates}
1013
+ queue.append((new_sgn, cand_sets, iter(new_sgn_candidates)))
1014
+ break
1015
+ else: # all gn candidates tried for sgn.
1016
+ queue.pop()
1017
+ if sgn in mapping:
1018
+ del rev_mapping[mapping[sgn]]
1019
+ del mapping[sgn]
1020
+
1021
+ def _largest_common_subgraph(self, candidates, constraints, to_be_mapped=None):
1022
+ """
1023
+ Find all largest common subgraphs honoring constraints.
1024
+ """
1025
+ # to_be_mapped is a set of frozensets of subgraph nodes
1026
+ if to_be_mapped is None:
1027
+ to_be_mapped = {frozenset(self.subgraph.nodes)}
1028
+
1029
+ # The LCS problem is basically a repeated subgraph isomorphism problem
1030
+ # with smaller and smaller subgraphs. We store the nodes that are
1031
+ # "part of" the subgraph in to_be_mapped, and we make it a little
1032
+ # smaller every iteration.
1033
+
1034
+ current_size = len(next(iter(to_be_mapped), []))
1035
+
1036
+ found_iso = False
1037
+ if current_size <= len(self.graph):
1038
+ # There's no point in trying to find isomorphisms of
1039
+ # graph >= subgraph if subgraph has more nodes than graph.
1040
+
1041
+ # Try the isomorphism first with the nodes with lowest ID. So sort
1042
+ # them. Those are more likely to be part of the final correspondence.
1043
+ # In theory, this makes finding the first answer(s) faster.
1044
+ for nodes in sorted(to_be_mapped, key=sorted):
1045
+ # Find the isomorphism between subgraph[to_be_mapped] <= graph
1046
+ next_sgn = min(nodes, key=lambda n: min(len(x) for x in candidates[n]))
1047
+ isomorphs = self._map_nodes(
1048
+ next_sgn, candidates, constraints, to_be_mapped=nodes
1049
+ )
1050
+
1051
+ # This is effectively `yield from isomorphs`, except that we look
1052
+ # whether an item was yielded.
1053
+ try:
1054
+ item = next(isomorphs)
1055
+ except StopIteration:
1056
+ pass
1057
+ else:
1058
+ yield item
1059
+ yield from isomorphs
1060
+ found_iso = True
1061
+
1062
+ # BASECASE
1063
+ if found_iso or current_size == 1:
1064
+ # Shrinking has no point because either 1) we end up with a smaller
1065
+ # common subgraph (and we want the largest), or 2) there'll be no
1066
+ # more subgraph.
1067
+ return
1068
+
1069
+ left_to_be_mapped = set()
1070
+ for nodes in to_be_mapped:
1071
+ for sgn in nodes:
1072
+ # We're going to remove sgn from to_be_mapped, but subject to
1073
+ # symmetry constraints. We know that for every constraint we
1074
+ # have those subgraph nodes are equal. So whenever we would
1075
+ # remove the lower part of a constraint, remove the higher
1076
+ # instead. This is all dealth with by _remove_node. And because
1077
+ # left_to_be_mapped is a set, we don't do double work.
1078
+
1079
+ # And finally, make the subgraph one node smaller.
1080
+ # REDUCTION
1081
+ new_nodes = self._remove_node(sgn, nodes, constraints)
1082
+ left_to_be_mapped.add(new_nodes)
1083
+ # COMBINATION
1084
+ yield from self._largest_common_subgraph(
1085
+ candidates, constraints, to_be_mapped=left_to_be_mapped
1086
+ )
1087
+
1088
+ @staticmethod
1089
+ def _remove_node(node, nodes, constraints):
1090
+ """
1091
+ Returns a new set where node has been removed from nodes, subject to
1092
+ symmetry constraints. We know, that for every constraint we have
1093
+ those subgraph nodes are equal. So whenever we would remove the
1094
+ lower part of a constraint, remove the higher instead.
1095
+ """
1096
+ while True:
1097
+ for low, high in constraints:
1098
+ if low == node and high in nodes:
1099
+ node = high
1100
+ break
1101
+ else: # no break, couldn't find node in constraints
1102
+ return frozenset(nodes - {node})
1103
+
1104
+ @staticmethod
1105
+ def _get_permutations_by_length(items):
1106
+ """
1107
+ Get all permutations of items, but only permute items with the same
1108
+ length.
1109
+
1110
+ >>> found = list(ISMAGS._get_permutations_by_length([{1}, {2}, {3, 4}, {4, 5}]))
1111
+ >>> answer = [
1112
+ ... (({1}, {2}), ({3, 4}, {4, 5})),
1113
+ ... (({1}, {2}), ({4, 5}, {3, 4})),
1114
+ ... (({2}, {1}), ({3, 4}, {4, 5})),
1115
+ ... (({2}, {1}), ({4, 5}, {3, 4})),
1116
+ ... ]
1117
+ >>> found == answer
1118
+ True
1119
+ """
1120
+ by_len = defaultdict(list)
1121
+ for item in items:
1122
+ by_len[len(item)].append(item)
1123
+
1124
+ return list(
1125
+ itertools.product(
1126
+ *(itertools.permutations(by_len[l]) for l in sorted(by_len))
1127
+ )
1128
+ )
1129
+
1130
+ def _refine_opp(cls, graph, top, bottom, edge_colors):
1131
+ def equal_color(node1, node2):
1132
+ return color_degree[node1] == color_degree[node2]
1133
+
1134
+ top = cls._refine_node_partition(graph, top, edge_colors)
1135
+
1136
+ possible_bottoms = [bottom]
1137
+ while possible_bottoms:
1138
+ bottom = possible_bottoms.pop()
1139
+ node_colors = node_to_part_ID_dict(bottom)
1140
+ color_degree = color_degree_by_node(graph, node_colors, edge_colors)
1141
+ if all(are_all_equal(color_degree[n] for n in p) for p in bottom):
1142
+ if len(top) == len(bottom):
1143
+ yield top, bottom
1144
+ # else Non-isomorphic OPP (pruned here)
1145
+ # either way continue to next possible bottom
1146
+ continue
1147
+ # refine bottom partition
1148
+ more_bottoms = [[]]
1149
+ for part in bottom:
1150
+ if len(part) == 1 or are_all_equal(color_degree[node] for node in part):
1151
+ for new_bottom in more_bottoms:
1152
+ new_bottom.append(part)
1153
+ else:
1154
+ # This part needs to be refined
1155
+ refined_part = make_partition(part, equal_color, check=False)
1156
+ R = len(refined_part)
1157
+ if R == 1 or R == len({len(p) for p in refined_part}):
1158
+ # no two parts have same length -- simple case
1159
+ for n_p in more_bottoms:
1160
+ n_p.extend(sorted(refined_part, key=len))
1161
+ else:
1162
+ # Any part might match any other part with the same size.
1163
+ # Before refinement they were the same color. So we need to
1164
+ # include all possible orderings/colors within each size.
1165
+ permutations = cls._get_permutations_by_length(refined_part)
1166
+ # Add all permutations of the refined parts to each possible
1167
+ # bottom. So the number of new possible bottoms is multiplied
1168
+ # by the number of permutations of the refined parts.
1169
+ new_partitions = []
1170
+ for new_partition in more_bottoms:
1171
+ for p in permutations:
1172
+ # p is tuple-of-tuples-of-sets. Flatten to list-of-sets
1173
+ flat_p = [s for tup in p for s in tup]
1174
+ new_partitions.append(new_partition + flat_p)
1175
+ more_bottoms = new_partitions
1176
+
1177
+ # reverse more_bottoms to keep the "finding identity" bottom first
1178
+ possible_bottoms.extend(more_bottoms[::-1])
1179
+
1180
+ @staticmethod
1181
+ def _find_permutations(top_partition, bottom_partition):
1182
+ """
1183
+ Return a set of 2-tuples of nodes. These nodes are not equal
1184
+ but are mapped to each other in the symmetry represented by this OPP.
1185
+ Swapping all the 2-tuples of nodes in this set permutes the nodes
1186
+ but retains the graph structure. Thus it is a symmetry of the subgraph.
1187
+ """
1188
+ # Find permutations
1189
+ permutations = set()
1190
+ for top, bot in zip(top_partition, bottom_partition):
1191
+ if len(top) > 1 or len(bot) > 1:
1192
+ # ignore parts with > 1 element when they are equal
1193
+ # These are called Matching OPPs in Katebi 2012.
1194
+ # Symmetries in matching partitions are built by considering
1195
+ # only parts that have 1 element.
1196
+ if top == bot:
1197
+ continue
1198
+ raise IndexError(
1199
+ "Not all nodes are matched. This is"
1200
+ f" impossible: {top_partition}, {bottom_partition}"
1201
+ )
1202
+ # top and bot have only one element
1203
+ elif top != bot:
1204
+ permutations.add(frozenset((next(iter(top)), next(iter(bot)))))
1205
+ return permutations
1206
+
1207
+ def _process_ordered_pair_partitions(
1208
+ self,
1209
+ graph,
1210
+ top_partition,
1211
+ bottom_partition,
1212
+ edge_colors,
1213
+ ):
1214
+ if all(len(top) <= 1 for top in top_partition):
1215
+ # no symmetries. Each node unique.
1216
+ return {}
1217
+
1218
+ # first mapping found is the identity mapping
1219
+ finding_identity = True
1220
+
1221
+ orbit_id = {node: orbit_i for orbit_i, node in enumerate(graph)}
1222
+ orbits = [{node} for node in graph]
1223
+ cosets = {}
1224
+
1225
+ node_to_ID = {n: i for i, n in enumerate(graph)}
1226
+ sort_by_ID = node_to_ID.__getitem__
1227
+
1228
+ def _load_next_queue_entry(queue, top_partition, bottom_partition):
1229
+ # find smallest node (by ID) in a |part|>1 and its partition index
1230
+ unmapped_nodes = (
1231
+ (node_to_ID[node], node, idx)
1232
+ for idx, t_part in enumerate(top_partition)
1233
+ for node in t_part
1234
+ if len(t_part) > 1
1235
+ )
1236
+ _, node, part_i = min(unmapped_nodes)
1237
+ b_part = bottom_partition[part_i]
1238
+ node2_iter = iter(sorted(b_part, key=sort_by_ID))
1239
+
1240
+ queue.append([top_partition, bottom_partition, node, part_i, node2_iter])
1241
+
1242
+ queue = []
1243
+ _load_next_queue_entry(queue, top_partition, bottom_partition)
1244
+
1245
+ while queue:
1246
+ tops, bottoms, node, part_i, node2_iter = queue[-1]
1247
+
1248
+ for node2 in node2_iter:
1249
+ if node != node2 and orbit_id[node] == orbit_id[node2]:
1250
+ # Orbit prune
1251
+ continue
1252
+
1253
+ # couple node to node2
1254
+ new_top_part = {node}
1255
+ new_bot_part = {node2}
1256
+
1257
+ new_top = [top.copy() for top in tops]
1258
+ new_top[part_i] -= new_top_part
1259
+ new_top.insert(part_i, new_top_part)
1260
+
1261
+ new_bot = [bot.copy() for bot in bottoms]
1262
+ new_bot[part_i] -= new_bot_part
1263
+ new_bot.insert(part_i, new_bot_part)
1264
+
1265
+ # collect OPPs
1266
+ opps = self._refine_opp(graph, new_top, new_bot, edge_colors)
1267
+ new_q = []
1268
+ for opp in opps:
1269
+ # Use OPP to find any of: Identity, Automorphism or Matching OPPs
1270
+ # else load the OPP onto queue for further exploration
1271
+ # Note that we check for Orbit pruning later because orbits may
1272
+ # be updated while OPP is sitting on the queue.
1273
+ # Note that we check for Non-isomorphic OPPs in `_refine_opp`.
1274
+ if finding_identity:
1275
+ # Note: allow zero size parts in identity check
1276
+ # b/c largest_common_subgraph allows empty parts
1277
+ if all(len(top) <= 1 for top in opp[0]):
1278
+ # Identity found. Set flag. Can now prune Matching OPPs
1279
+ finding_identity = False
1280
+ continue
1281
+ elif all(len(t) <= 1 or t == b for t, b in zip(*opp)):
1282
+ # Found a symmetry! (Full mapping or Matching OPP)
1283
+ # update orbits using the permutations from the OPP.
1284
+ permutations = self._find_permutations(*opp)
1285
+ for n1, n2 in permutations:
1286
+ orb1 = orbit_id[n1]
1287
+ orb2 = orbit_id[n2]
1288
+ if orb1 != orb2:
1289
+ orbit_set2 = orbits[orb2]
1290
+ orbits[orb1].update(orbit_set2)
1291
+ orbits[orb2] = set()
1292
+ orbit_id.update((n, orb1) for n in orbit_set2)
1293
+ continue
1294
+
1295
+ _load_next_queue_entry(new_q, *opp)
1296
+ # reverse order to maintain node order DFS (Identity comes first)
1297
+ queue.extend(new_q[::-1])
1298
+ break
1299
+ else: # no more node2 options
1300
+ queue.pop()
1301
+ if node not in cosets:
1302
+ # coset of `node` is its orbit at the time `node` has completed
1303
+ # its first DFS traversal. DFS is about to go to previous node.
1304
+ # Make copy so future orbit changes do not change the coset.
1305
+ cosets[node] = orbits[orbit_id[node]].copy()
1306
+ return cosets