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.
- package/README.kr.md +1 -1
- package/README.md +1 -1
- package/docs/kr/architecture.md +3 -3
- package/docs/kr/cli.md +2 -2
- package/docs/project-structure-overview.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/python/okstra_ctl/graphify_cmd.py +225 -0
- package/runtime/python/okstra_ctl/resolve_task_key.py +15 -9
- package/runtime/python/okstra_ctl/worktree.py +14 -13
- package/runtime/python/okstra_project/__init__.py +2 -0
- package/runtime/python/okstra_project/state.py +36 -0
- package/runtime/python/okstra_vendor/__init__.py +41 -0
- package/runtime/python/okstra_vendor/graphify/.vendored-version +1 -0
- package/runtime/python/okstra_vendor/graphify/__init__.py +28 -0
- package/runtime/python/okstra_vendor/graphify/__main__.py +1371 -0
- package/runtime/python/okstra_vendor/graphify/analyze.py +540 -0
- package/runtime/python/okstra_vendor/graphify/benchmark.py +129 -0
- package/runtime/python/okstra_vendor/graphify/build.py +107 -0
- package/runtime/python/okstra_vendor/graphify/cache.py +169 -0
- package/runtime/python/okstra_vendor/graphify/cluster.py +137 -0
- package/runtime/python/okstra_vendor/graphify/detect.py +510 -0
- package/runtime/python/okstra_vendor/graphify/export.py +1014 -0
- package/runtime/python/okstra_vendor/graphify/extract.py +3277 -0
- package/runtime/python/okstra_vendor/graphify/hooks.py +220 -0
- package/runtime/python/okstra_vendor/graphify/ingest.py +297 -0
- package/runtime/python/okstra_vendor/graphify/manifest.py +4 -0
- package/runtime/python/okstra_vendor/graphify/report.py +175 -0
- package/runtime/python/okstra_vendor/graphify/security.py +203 -0
- package/runtime/python/okstra_vendor/graphify/serve.py +373 -0
- package/runtime/python/okstra_vendor/graphify/skill-aider.md +1184 -0
- package/runtime/python/okstra_vendor/graphify/skill-claw.md +1184 -0
- package/runtime/python/okstra_vendor/graphify/skill-codex.md +1242 -0
- package/runtime/python/okstra_vendor/graphify/skill-copilot.md +1268 -0
- package/runtime/python/okstra_vendor/graphify/skill-droid.md +1239 -0
- package/runtime/python/okstra_vendor/graphify/skill-kiro.md +1183 -0
- package/runtime/python/okstra_vendor/graphify/skill-opencode.md +1238 -0
- package/runtime/python/okstra_vendor/graphify/skill-trae.md +1208 -0
- package/runtime/python/okstra_vendor/graphify/skill-vscode.md +253 -0
- package/runtime/python/okstra_vendor/graphify/skill-windows.md +1245 -0
- package/runtime/python/okstra_vendor/graphify/skill.md +1319 -0
- package/runtime/python/okstra_vendor/graphify/transcribe.py +182 -0
- package/runtime/python/okstra_vendor/graphify/validate.py +72 -0
- package/runtime/python/okstra_vendor/graphify/watch.py +188 -0
- package/runtime/python/okstra_vendor/graphify/wiki.py +214 -0
- package/runtime/python/okstra_vendor/networkx/__init__.py +62 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/__init__.py +134 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/__init__.py +26 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clique.py +259 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clustering_coefficient.py +71 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/connectivity.py +412 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/density.py +396 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/distance_measures.py +150 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/dominating_set.py +149 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/kcomponents.py +369 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/matching.py +44 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/maxcut.py +143 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/ramsey.py +53 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/steinertree.py +265 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py +41 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_clique.py +112 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_connectivity.py +199 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_density.py +146 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_distance_measures.py +59 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_dominating_set.py +78 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_kcomponents.py +303 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_matching.py +8 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_maxcut.py +94 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_ramsey.py +31 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_steinertree.py +306 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_traveling_salesman.py +1014 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_treewidth.py +274 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_vertex_cover.py +68 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/traveling_salesman.py +1508 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/treewidth.py +255 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/approximation/vertex_cover.py +83 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/__init__.py +5 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/connectivity.py +122 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/correlation.py +302 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/mixing.py +255 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/neighbor_degree.py +160 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/pairs.py +127 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/base_test.py +81 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_connectivity.py +143 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_correlation.py +122 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_mixing.py +174 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_neighbor_degree.py +107 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_pairs.py +87 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/asteroidal.py +164 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/__init__.py +88 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/basic.py +322 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/centrality.py +290 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/cluster.py +289 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/covering.py +57 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/edgelist.py +360 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/extendability.py +105 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/generators.py +603 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/link_analysis.py +316 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matching.py +590 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matrix.py +232 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/projection.py +526 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/redundancy.py +112 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/spectral.py +69 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_basic.py +125 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_centrality.py +192 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_cluster.py +84 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_covering.py +33 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_edgelist.py +240 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_extendability.py +334 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_generators.py +407 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_link_analysis.py +218 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_matching.py +327 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_matrix.py +138 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_project.py +409 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_redundancy.py +35 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py +80 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/boundary.py +168 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/bridges.py +205 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/broadcasting.py +164 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/__init__.py +20 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness.py +591 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness_subset.py +236 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/closeness.py +282 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness.py +364 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness_subset.py +227 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_closeness.py +96 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/degree_alg.py +150 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/dispersion.py +107 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/eigenvector.py +357 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/flow_matrix.py +130 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/group.py +787 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/harmonic.py +88 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/katz.py +331 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/laplacian.py +150 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/load.py +200 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/percolation.py +128 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/reaching.py +209 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/second_order.py +141 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/subgraph_alg.py +361 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_betweenness_centrality.py +923 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_betweenness_centrality_subset.py +354 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_closeness_centrality.py +274 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_betweenness_centrality.py +259 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_betweenness_centrality_subset.py +147 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_closeness.py +43 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_degree_centrality.py +144 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_dispersion.py +73 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_eigenvector_centrality.py +186 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_group.py +277 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_harmonic_centrality.py +122 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_katz_centrality.py +345 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_laplacian_centrality.py +220 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_load_centrality.py +344 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_percolation_centrality.py +87 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_reaching.py +140 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_second_order_centrality.py +82 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_subgraph.py +110 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_trophic.py +302 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_voterank.py +64 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/trophic.py +181 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/centrality/voterank_alg.py +95 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/chains.py +172 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/chordal.py +443 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/clique.py +818 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/cluster.py +732 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/__init__.py +4 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/equitable_coloring.py +505 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/greedy_coloring.py +565 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/coloring/tests/test_coloring.py +863 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/communicability_alg.py +163 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/__init__.py +28 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/asyn_fluid.py +153 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/bipartitions.py +354 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/centrality.py +171 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/community_utils.py +30 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/divisive.py +216 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/kclique.py +79 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/label_propagation.py +338 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/leiden.py +162 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/local.py +220 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/louvain.py +384 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/lukes.py +227 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/modularity_max.py +452 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/quality.py +347 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_asyn_fluid.py +147 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_bipartitions.py +157 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_centrality.py +85 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_divisive.py +106 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_kclique.py +91 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_label_propagation.py +241 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_leiden.py +138 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_local.py +76 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_louvain.py +264 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_lukes.py +152 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_modularity_max.py +340 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_quality.py +139 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_utils.py +26 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/__init__.py +6 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/attracting.py +115 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/biconnected.py +394 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/connected.py +282 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/semiconnected.py +71 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/strongly_connected.py +359 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_attracting.py +70 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_biconnected.py +248 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_connected.py +138 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_semiconnected.py +55 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_strongly_connected.py +193 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_weakly_connected.py +96 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/components/weakly_connected.py +196 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/__init__.py +11 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/connectivity.py +811 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/cuts.py +616 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/disjoint_paths.py +408 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_augmentation.py +1270 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_kcomponents.py +592 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcomponents.py +220 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcutsets.py +235 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/stoerwagner.py +152 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_connectivity.py +421 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_cuts.py +309 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_disjoint_paths.py +249 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_edge_augmentation.py +502 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py +488 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_kcomponents.py +323 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_kcutsets.py +280 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_stoer_wagner.py +102 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/utils.py +88 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/core.py +588 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/covering.py +142 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/cuts.py +416 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/cycles.py +1234 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/d_separation.py +677 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/dag.py +1392 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/distance_measures.py +1095 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/distance_regular.py +272 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/dominance.py +142 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/dominating.py +268 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/efficiency_measures.py +167 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/euler.py +470 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/__init__.py +11 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/boykovkolmogorov.py +370 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/capacityscaling.py +407 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/dinitz_alg.py +238 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/edmondskarp.py +241 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/gomory_hu.py +178 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/maxflow.py +611 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/mincost.py +356 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/networksimplex.py +662 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/preflowpush.py +425 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/shortestaugmentingpath.py +300 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/gl1.gpickle.bz2 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/gw1.gpickle.bz2 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/netgen-2.gpickle.bz2 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_gomory_hu.py +128 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_maxflow.py +573 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_maxflow_large_graph.py +155 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_mincost.py +475 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_networksimplex.py +481 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/wlm3.gpickle.bz2 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/flow/utils.py +194 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/graph_hashing.py +435 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/graphical.py +483 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/hierarchy.py +57 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/hybrid.py +196 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isolate.py +107 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/__init__.py +7 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/ismags.py +1306 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorph.py +336 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorphvf2.py +1262 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/matchhelpers.py +352 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/temporalisomorphvf2.py +308 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_ismags.py +719 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_isomorphism.py +103 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py +490 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_match_helpers.py +64 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py +212 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py +202 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2pp.py +1655 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py +3118 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py +196 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tree_isomorphism.py +264 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2pp.py +1102 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2userfunc.py +192 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/__init__.py +2 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/hits_alg.py +337 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/pagerank_alg.py +498 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/test_hits.py +77 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/test_pagerank.py +213 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/link_prediction.py +687 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/lowest_common_ancestors.py +280 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/matching.py +1148 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/__init__.py +27 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/contraction.py +738 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/minors/tests/test_contraction.py +544 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/mis.py +78 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/moral.py +59 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/node_classification.py +219 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/non_randomness.py +155 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/__init__.py +4 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/all.py +324 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/binary.py +468 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/product.py +633 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_all.py +328 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_binary.py +451 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_product.py +491 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_unary.py +55 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/operators/unary.py +77 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/perfect_graph.py +73 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/planar_drawing.py +464 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/planarity.py +1463 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/polynomials.py +306 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/reciprocity.py +98 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/regular.py +167 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/richclub.py +138 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/__init__.py +5 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/astar.py +239 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/dense.py +264 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/generic.py +716 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_astar.py +254 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_dense.py +212 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_dense_numpy.py +88 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_generic.py +511 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_unweighted.py +149 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_weighted.py +983 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/unweighted.py +625 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/weighted.py +2542 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/similarity.py +2107 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/simple_paths.py +966 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/smallworld.py +404 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/smetric.py +30 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/sparsifiers.py +296 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/structuralholes.py +374 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/summarization.py +564 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/swap.py +406 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_asteroidal.py +23 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_boundary.py +154 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_bridges.py +144 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_broadcasting.py +109 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_chains.py +136 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_chordal.py +129 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_clique.py +300 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cluster.py +678 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_communicability.py +80 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_core.py +266 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_covering.py +85 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cuts.py +171 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cycles.py +984 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_d_separation.py +340 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dag.py +835 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_distance_measures.py +831 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_distance_regular.py +85 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dominance.py +299 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dominating.py +115 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_efficiency.py +58 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_euler.py +314 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_graph_hashing.py +872 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_graphical.py +163 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_hierarchy.py +46 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_hybrid.py +24 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_isolate.py +26 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_link_prediction.py +615 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_lowest_common_ancestors.py +459 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_matching.py +556 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_max_weight_clique.py +179 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_mis.py +62 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_moral.py +15 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_node_classification.py +140 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_non_randomness.py +60 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_perfect_graph.py +27 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_planar_drawing.py +274 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_planarity.py +556 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_polynomials.py +57 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_reciprocity.py +37 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_regular.py +88 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_richclub.py +149 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_similarity.py +1158 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_simple_paths.py +803 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_smallworld.py +76 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_smetric.py +8 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_sparsifiers.py +138 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_structuralholes.py +191 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_summarization.py +642 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_swap.py +179 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_threshold.py +270 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_time_dependent.py +431 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_tournament.py +161 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_triads.py +248 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_vitality.py +41 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_voronoi.py +103 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_walks.py +54 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_wiener.py +157 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/threshold.py +981 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/time_dependent.py +142 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tournament.py +406 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/__init__.py +5 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/beamsearch.py +90 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/breadth_first_search.py +576 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/depth_first_search.py +529 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgebfs.py +185 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgedfs.py +182 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_beamsearch.py +25 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_bfs.py +203 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_dfs.py +307 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_edgebfs.py +147 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_edgedfs.py +131 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/__init__.py +7 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/branchings.py +1042 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/coding.py +413 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/decomposition.py +88 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/distance_measures.py +219 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/mst.py +1281 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/operations.py +106 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/recognition.py +273 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_branchings.py +624 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_coding.py +114 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_decomposition.py +79 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_distance_measures.py +99 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_mst.py +934 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_operations.py +53 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_recognition.py +174 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/triads.py +500 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/vitality.py +76 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/voronoi.py +86 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/walks.py +77 -0
- package/runtime/python/okstra_vendor/networkx/algorithms/wiener.py +278 -0
- package/runtime/python/okstra_vendor/networkx/classes/__init__.py +13 -0
- package/runtime/python/okstra_vendor/networkx/classes/coreviews.py +435 -0
- package/runtime/python/okstra_vendor/networkx/classes/digraph.py +1363 -0
- package/runtime/python/okstra_vendor/networkx/classes/filters.py +95 -0
- package/runtime/python/okstra_vendor/networkx/classes/function.py +1549 -0
- package/runtime/python/okstra_vendor/networkx/classes/graph.py +2082 -0
- package/runtime/python/okstra_vendor/networkx/classes/graphviews.py +269 -0
- package/runtime/python/okstra_vendor/networkx/classes/multidigraph.py +977 -0
- package/runtime/python/okstra_vendor/networkx/classes/multigraph.py +1294 -0
- package/runtime/python/okstra_vendor/networkx/classes/reportviews.py +1447 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/dispatch_interface.py +192 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/historical_tests.py +476 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_coreviews.py +362 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_digraph.py +331 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_digraph_historical.py +110 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_filters.py +177 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_function.py +1045 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_graph.py +950 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_graph_historical.py +12 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_graphviews.py +349 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_multidigraph.py +459 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_multigraph.py +528 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_reportviews.py +1421 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_special.py +131 -0
- package/runtime/python/okstra_vendor/networkx/classes/tests/test_subgraphviews.py +371 -0
- package/runtime/python/okstra_vendor/networkx/conftest.py +261 -0
- package/runtime/python/okstra_vendor/networkx/convert.py +502 -0
- package/runtime/python/okstra_vendor/networkx/convert_matrix.py +1314 -0
- package/runtime/python/okstra_vendor/networkx/drawing/__init__.py +7 -0
- package/runtime/python/okstra_vendor/networkx/drawing/layout.py +2036 -0
- package/runtime/python/okstra_vendor/networkx/drawing/nx_agraph.py +470 -0
- package/runtime/python/okstra_vendor/networkx/drawing/nx_latex.py +570 -0
- package/runtime/python/okstra_vendor/networkx/drawing/nx_pydot.py +361 -0
- package/runtime/python/okstra_vendor/networkx/drawing/nx_pylab.py +2978 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_complex.png +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_empty_graph.png +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_house_with_colors.png +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_labels_and_colors.png +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_shortest_path.png +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_house_with_colors.png +0 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/test_agraph.py +237 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/test_image_comparison_pylab_mpl.py +229 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/test_latex.py +285 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/test_layout.py +631 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/test_pydot.py +146 -0
- package/runtime/python/okstra_vendor/networkx/drawing/tests/test_pylab.py +1582 -0
- package/runtime/python/okstra_vendor/networkx/exception.py +131 -0
- package/runtime/python/okstra_vendor/networkx/generators/__init__.py +34 -0
- package/runtime/python/okstra_vendor/networkx/generators/atlas.dat.gz +0 -0
- package/runtime/python/okstra_vendor/networkx/generators/atlas.py +227 -0
- package/runtime/python/okstra_vendor/networkx/generators/classic.py +1091 -0
- package/runtime/python/okstra_vendor/networkx/generators/cographs.py +68 -0
- package/runtime/python/okstra_vendor/networkx/generators/community.py +1070 -0
- package/runtime/python/okstra_vendor/networkx/generators/degree_seq.py +886 -0
- package/runtime/python/okstra_vendor/networkx/generators/directed.py +572 -0
- package/runtime/python/okstra_vendor/networkx/generators/duplication.py +174 -0
- package/runtime/python/okstra_vendor/networkx/generators/ego.py +66 -0
- package/runtime/python/okstra_vendor/networkx/generators/expanders.py +499 -0
- package/runtime/python/okstra_vendor/networkx/generators/geometric.py +1037 -0
- package/runtime/python/okstra_vendor/networkx/generators/harary_graph.py +163 -0
- package/runtime/python/okstra_vendor/networkx/generators/internet_as_graphs.py +443 -0
- package/runtime/python/okstra_vendor/networkx/generators/intersection.py +125 -0
- package/runtime/python/okstra_vendor/networkx/generators/interval_graph.py +70 -0
- package/runtime/python/okstra_vendor/networkx/generators/joint_degree_seq.py +664 -0
- package/runtime/python/okstra_vendor/networkx/generators/lattice.py +405 -0
- package/runtime/python/okstra_vendor/networkx/generators/line.py +501 -0
- package/runtime/python/okstra_vendor/networkx/generators/mycielski.py +110 -0
- package/runtime/python/okstra_vendor/networkx/generators/nonisomorphic_trees.py +259 -0
- package/runtime/python/okstra_vendor/networkx/generators/random_clustered.py +117 -0
- package/runtime/python/okstra_vendor/networkx/generators/random_graphs.py +1416 -0
- package/runtime/python/okstra_vendor/networkx/generators/small.py +1070 -0
- package/runtime/python/okstra_vendor/networkx/generators/social.py +554 -0
- package/runtime/python/okstra_vendor/networkx/generators/spectral_graph_forge.py +120 -0
- package/runtime/python/okstra_vendor/networkx/generators/stochastic.py +54 -0
- package/runtime/python/okstra_vendor/networkx/generators/sudoku.py +131 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_atlas.py +75 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_classic.py +642 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_cographs.py +20 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_community.py +362 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_degree_seq.py +224 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_directed.py +189 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_duplication.py +103 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_ego.py +39 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_expanders.py +182 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_geometric.py +488 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_harary_graph.py +133 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_internet_as_graphs.py +221 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_intersection.py +28 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_interval_graph.py +144 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_joint_degree_seq.py +125 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_lattice.py +264 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_line.py +316 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_mycielski.py +30 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_nonisomorphic_trees.py +82 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_random_clustered.py +33 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_random_graphs.py +495 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_small.py +220 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_spectral_graph_forge.py +49 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_stochastic.py +72 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_sudoku.py +92 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_time_series.py +64 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_trees.py +195 -0
- package/runtime/python/okstra_vendor/networkx/generators/tests/test_triads.py +15 -0
- package/runtime/python/okstra_vendor/networkx/generators/time_series.py +74 -0
- package/runtime/python/okstra_vendor/networkx/generators/trees.py +1070 -0
- package/runtime/python/okstra_vendor/networkx/generators/triads.py +94 -0
- package/runtime/python/okstra_vendor/networkx/lazy_imports.py +188 -0
- package/runtime/python/okstra_vendor/networkx/linalg/__init__.py +13 -0
- package/runtime/python/okstra_vendor/networkx/linalg/algebraicconnectivity.py +650 -0
- package/runtime/python/okstra_vendor/networkx/linalg/attrmatrix.py +466 -0
- package/runtime/python/okstra_vendor/networkx/linalg/bethehessianmatrix.py +77 -0
- package/runtime/python/okstra_vendor/networkx/linalg/graphmatrix.py +168 -0
- package/runtime/python/okstra_vendor/networkx/linalg/laplacianmatrix.py +512 -0
- package/runtime/python/okstra_vendor/networkx/linalg/modularitymatrix.py +166 -0
- package/runtime/python/okstra_vendor/networkx/linalg/spectrum.py +186 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_algebraic_connectivity.py +400 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_attrmatrix.py +108 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_bethehessian.py +40 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_graphmatrix.py +275 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_laplacian.py +334 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_modularity.py +86 -0
- package/runtime/python/okstra_vendor/networkx/linalg/tests/test_spectrum.py +70 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/__init__.py +17 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/adjlist.py +330 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/edgelist.py +489 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/gexf.py +1084 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/gml.py +879 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/graph6.py +427 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/graphml.py +1053 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/__init__.py +19 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/adjacency.py +156 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/cytoscape.py +190 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/node_link.py +261 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_adjacency.py +78 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_cytoscape.py +78 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_node_link.py +109 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_tree.py +48 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tree.py +137 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/leda.py +108 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/multiline_adjlist.py +393 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/p2g.py +113 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/pajek.py +286 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/sparse6.py +379 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_adjlist.py +354 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_edgelist.py +318 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_gexf.py +612 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_gml.py +744 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_graph6.py +181 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_graphml.py +1531 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_leda.py +30 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_p2g.py +63 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_pajek.py +128 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_sparse6.py +166 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_text.py +1742 -0
- package/runtime/python/okstra_vendor/networkx/readwrite/text.py +851 -0
- package/runtime/python/okstra_vendor/networkx/relabel.py +285 -0
- package/runtime/python/okstra_vendor/networkx/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_all_random_functions.py +248 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_convert.py +321 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_convert_numpy.py +531 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_convert_pandas.py +349 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_convert_scipy.py +281 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_exceptions.py +40 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_import.py +11 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_lazy_imports.py +96 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_relabel.py +349 -0
- package/runtime/python/okstra_vendor/networkx/tests/test_removed_functions_exception_messages.py +8 -0
- package/runtime/python/okstra_vendor/networkx/utils/__init__.py +8 -0
- package/runtime/python/okstra_vendor/networkx/utils/backends.py +2171 -0
- package/runtime/python/okstra_vendor/networkx/utils/configs.py +396 -0
- package/runtime/python/okstra_vendor/networkx/utils/decorators.py +1233 -0
- package/runtime/python/okstra_vendor/networkx/utils/heaps.py +338 -0
- package/runtime/python/okstra_vendor/networkx/utils/mapped_queue.py +297 -0
- package/runtime/python/okstra_vendor/networkx/utils/misc.py +703 -0
- package/runtime/python/okstra_vendor/networkx/utils/random_sequence.py +198 -0
- package/runtime/python/okstra_vendor/networkx/utils/rcm.py +159 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/__init__.py +0 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test__init.py +11 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_backends.py +225 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_config.py +263 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_decorators.py +510 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_heaps.py +131 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_mapped_queue.py +268 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_misc.py +393 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_random_sequence.py +53 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_rcm.py +63 -0
- package/runtime/python/okstra_vendor/networkx/utils/tests/test_unionfind.py +55 -0
- package/runtime/python/okstra_vendor/networkx/utils/union_find.py +106 -0
- package/runtime/skills/okstra-graphify/SKILL.md +161 -0
- package/runtime/skills/okstra-inspect/SKILL.md +17 -9
- package/runtime/templates/reports/settings.template.json +4 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/graphify.mjs +32 -0
- package/src/commands/lifecycle/doctor.mjs +9 -0
- package/src/lib/skill-catalog.mjs +1 -0
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
"""
|
|
2
|
+
******
|
|
3
|
+
Layout
|
|
4
|
+
******
|
|
5
|
+
|
|
6
|
+
Node positioning algorithms for graph drawing.
|
|
7
|
+
|
|
8
|
+
For `random_layout()` the possible resulting shape
|
|
9
|
+
is a square of side [0, scale] (default: [0, 1])
|
|
10
|
+
Changing `center` shifts the layout by that amount.
|
|
11
|
+
|
|
12
|
+
For the other layout routines, the extent is
|
|
13
|
+
[center - scale, center + scale] (default: [-1, 1]).
|
|
14
|
+
|
|
15
|
+
Warning: Most layout routines have only been tested in 2-dimensions.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import networkx as nx
|
|
20
|
+
from networkx.utils import np_random_state
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"bipartite_layout",
|
|
24
|
+
"circular_layout",
|
|
25
|
+
"forceatlas2_layout",
|
|
26
|
+
"kamada_kawai_layout",
|
|
27
|
+
"random_layout",
|
|
28
|
+
"rescale_layout",
|
|
29
|
+
"rescale_layout_dict",
|
|
30
|
+
"shell_layout",
|
|
31
|
+
"spring_layout",
|
|
32
|
+
"spectral_layout",
|
|
33
|
+
"planar_layout",
|
|
34
|
+
"fruchterman_reingold_layout",
|
|
35
|
+
"spiral_layout",
|
|
36
|
+
"multipartite_layout",
|
|
37
|
+
"bfs_layout",
|
|
38
|
+
"arf_layout",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _process_params(G, center, dim):
|
|
43
|
+
# Some boilerplate code.
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
if not isinstance(G, nx.Graph):
|
|
47
|
+
empty_graph = nx.Graph()
|
|
48
|
+
empty_graph.add_nodes_from(G)
|
|
49
|
+
G = empty_graph
|
|
50
|
+
|
|
51
|
+
if center is None:
|
|
52
|
+
center = np.zeros(dim)
|
|
53
|
+
else:
|
|
54
|
+
center = np.asarray(center)
|
|
55
|
+
|
|
56
|
+
if len(center) != dim:
|
|
57
|
+
msg = "length of center coordinates must match dimension of layout"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
|
|
60
|
+
return G, center
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@np_random_state(3)
|
|
64
|
+
def random_layout(G, center=None, dim=2, seed=None, store_pos_as=None):
|
|
65
|
+
"""Position nodes uniformly at random in the unit square.
|
|
66
|
+
|
|
67
|
+
For every node, a position is generated by choosing each of dim
|
|
68
|
+
coordinates uniformly at random on the interval [0.0, 1.0).
|
|
69
|
+
|
|
70
|
+
NumPy (http://scipy.org) is required for this function.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
G : NetworkX graph or list of nodes
|
|
75
|
+
A position will be assigned to every node in G.
|
|
76
|
+
|
|
77
|
+
center : array-like or None
|
|
78
|
+
Coordinate pair around which to center the layout.
|
|
79
|
+
|
|
80
|
+
dim : int
|
|
81
|
+
Dimension of layout.
|
|
82
|
+
|
|
83
|
+
seed : int, RandomState instance or None optional (default=None)
|
|
84
|
+
Set the random state for deterministic node layouts.
|
|
85
|
+
If int, `seed` is the seed used by the random number generator,
|
|
86
|
+
if numpy.random.RandomState instance, `seed` is the random
|
|
87
|
+
number generator,
|
|
88
|
+
if None, the random number generator is the RandomState instance used
|
|
89
|
+
by numpy.random.
|
|
90
|
+
|
|
91
|
+
store_pos_as : str, default None
|
|
92
|
+
If non-None, the position of each node will be stored on the graph as
|
|
93
|
+
an attribute with this string as its name, which can be accessed with
|
|
94
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
pos : dict
|
|
99
|
+
A dictionary of positions keyed by node
|
|
100
|
+
|
|
101
|
+
Examples
|
|
102
|
+
--------
|
|
103
|
+
>>> from pprint import pprint
|
|
104
|
+
>>> G = nx.lollipop_graph(4, 3)
|
|
105
|
+
>>> pos = nx.random_layout(G)
|
|
106
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
107
|
+
>>> _ = nx.random_layout(G, seed=42, store_pos_as="pos")
|
|
108
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
109
|
+
{0: array([0.37454012, 0.9507143 ], dtype=float32),
|
|
110
|
+
1: array([0.7319939, 0.5986585], dtype=float32),
|
|
111
|
+
2: array([0.15601864, 0.15599452], dtype=float32),
|
|
112
|
+
3: array([0.05808361, 0.8661761 ], dtype=float32),
|
|
113
|
+
4: array([0.601115 , 0.7080726], dtype=float32),
|
|
114
|
+
5: array([0.02058449, 0.96990985], dtype=float32),
|
|
115
|
+
6: array([0.83244264, 0.21233912], dtype=float32)}
|
|
116
|
+
"""
|
|
117
|
+
import numpy as np
|
|
118
|
+
|
|
119
|
+
G, center = _process_params(G, center, dim)
|
|
120
|
+
pos = seed.rand(len(G), dim) + center
|
|
121
|
+
pos = pos.astype(np.float32)
|
|
122
|
+
pos = dict(zip(G, pos))
|
|
123
|
+
|
|
124
|
+
if store_pos_as is not None:
|
|
125
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
126
|
+
return pos
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def circular_layout(G, scale=1, center=None, dim=2, store_pos_as=None):
|
|
130
|
+
# dim=2 only
|
|
131
|
+
"""Position nodes on a circle.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
G : NetworkX graph or list of nodes
|
|
136
|
+
A position will be assigned to every node in G.
|
|
137
|
+
|
|
138
|
+
scale : number (default: 1)
|
|
139
|
+
Scale factor for positions.
|
|
140
|
+
|
|
141
|
+
center : array-like or None
|
|
142
|
+
Coordinate pair around which to center the layout.
|
|
143
|
+
|
|
144
|
+
dim : int
|
|
145
|
+
Dimension of layout.
|
|
146
|
+
If dim>2, the remaining dimensions are set to zero
|
|
147
|
+
in the returned positions.
|
|
148
|
+
If dim<2, a ValueError is raised.
|
|
149
|
+
|
|
150
|
+
store_pos_as : str, default None
|
|
151
|
+
If non-None, the position of each node will be stored on the graph as
|
|
152
|
+
an attribute with this string as its name, which can be accessed with
|
|
153
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
pos : dict
|
|
158
|
+
A dictionary of positions keyed by node
|
|
159
|
+
|
|
160
|
+
Raises
|
|
161
|
+
------
|
|
162
|
+
ValueError
|
|
163
|
+
If dim < 2
|
|
164
|
+
|
|
165
|
+
Examples
|
|
166
|
+
--------
|
|
167
|
+
>>> from pprint import pprint
|
|
168
|
+
>>> G = nx.path_graph(4)
|
|
169
|
+
>>> pos = nx.circular_layout(G)
|
|
170
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
171
|
+
>>> _ = nx.circular_layout(G, store_pos_as="pos")
|
|
172
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
173
|
+
{0: array([9.99999986e-01, 2.18556937e-08]),
|
|
174
|
+
1: array([-3.57647606e-08, 1.00000000e+00]),
|
|
175
|
+
2: array([-9.9999997e-01, -6.5567081e-08]),
|
|
176
|
+
3: array([ 1.98715071e-08, -9.99999956e-01])}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
Notes
|
|
180
|
+
-----
|
|
181
|
+
This algorithm currently only works in two dimensions and does not
|
|
182
|
+
try to minimize edge crossings.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
import numpy as np
|
|
186
|
+
|
|
187
|
+
if dim < 2:
|
|
188
|
+
raise ValueError("cannot handle dimensions < 2")
|
|
189
|
+
|
|
190
|
+
G, center = _process_params(G, center, dim)
|
|
191
|
+
|
|
192
|
+
paddims = max(0, (dim - 2))
|
|
193
|
+
|
|
194
|
+
if len(G) == 0:
|
|
195
|
+
pos = {}
|
|
196
|
+
elif len(G) == 1:
|
|
197
|
+
pos = {nx.utils.arbitrary_element(G): center}
|
|
198
|
+
else:
|
|
199
|
+
# Discard the extra angle since it matches 0 radians.
|
|
200
|
+
theta = np.linspace(0, 1, len(G) + 1)[:-1] * 2 * np.pi
|
|
201
|
+
theta = theta.astype(np.float32)
|
|
202
|
+
pos = np.column_stack(
|
|
203
|
+
[np.cos(theta), np.sin(theta), np.zeros((len(G), paddims))]
|
|
204
|
+
)
|
|
205
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
206
|
+
pos = dict(zip(G, pos))
|
|
207
|
+
|
|
208
|
+
if store_pos_as is not None:
|
|
209
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
210
|
+
|
|
211
|
+
return pos
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def shell_layout(
|
|
215
|
+
G, nlist=None, rotate=None, scale=1, center=None, dim=2, store_pos_as=None
|
|
216
|
+
):
|
|
217
|
+
"""Position nodes in concentric circles.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
G : NetworkX graph or list of nodes
|
|
222
|
+
A position will be assigned to every node in G.
|
|
223
|
+
|
|
224
|
+
nlist : list of lists
|
|
225
|
+
List of node lists for each shell.
|
|
226
|
+
|
|
227
|
+
rotate : angle in radians (default=pi/len(nlist))
|
|
228
|
+
Angle by which to rotate the starting position of each shell
|
|
229
|
+
relative to the starting position of the previous shell.
|
|
230
|
+
To recreate behavior before v2.5 use rotate=0.
|
|
231
|
+
|
|
232
|
+
scale : number (default: 1)
|
|
233
|
+
Scale factor for positions.
|
|
234
|
+
|
|
235
|
+
center : array-like or None
|
|
236
|
+
Coordinate pair around which to center the layout.
|
|
237
|
+
|
|
238
|
+
dim : int
|
|
239
|
+
Dimension of layout, currently only dim=2 is supported.
|
|
240
|
+
Other dimension values result in a ValueError.
|
|
241
|
+
|
|
242
|
+
store_pos_as : str, default None
|
|
243
|
+
If non-None, the position of each node will be stored on the graph as
|
|
244
|
+
an attribute with this string as its name, which can be accessed with
|
|
245
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
pos : dict
|
|
250
|
+
A dictionary of positions keyed by node
|
|
251
|
+
|
|
252
|
+
Raises
|
|
253
|
+
------
|
|
254
|
+
ValueError
|
|
255
|
+
If dim != 2
|
|
256
|
+
|
|
257
|
+
Examples
|
|
258
|
+
--------
|
|
259
|
+
>>> from pprint import pprint
|
|
260
|
+
>>> G = nx.path_graph(4)
|
|
261
|
+
>>> shells = [[0], [1, 2, 3]]
|
|
262
|
+
>>> pos = nx.shell_layout(G, shells)
|
|
263
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
264
|
+
>>> _ = nx.shell_layout(G, shells, store_pos_as="pos")
|
|
265
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
266
|
+
{0: array([0., 0.]),
|
|
267
|
+
1: array([-5.00000000e-01, -4.37113883e-08]),
|
|
268
|
+
2: array([ 0.24999996, -0.43301272]),
|
|
269
|
+
3: array([0.24999981, 0.43301281])}
|
|
270
|
+
|
|
271
|
+
Notes
|
|
272
|
+
-----
|
|
273
|
+
This algorithm currently only works in two dimensions and does not
|
|
274
|
+
try to minimize edge crossings.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
import numpy as np
|
|
278
|
+
|
|
279
|
+
if dim != 2:
|
|
280
|
+
raise ValueError("can only handle 2 dimensions")
|
|
281
|
+
|
|
282
|
+
G, center = _process_params(G, center, dim)
|
|
283
|
+
|
|
284
|
+
if len(G) == 0:
|
|
285
|
+
return {}
|
|
286
|
+
if len(G) == 1:
|
|
287
|
+
return {nx.utils.arbitrary_element(G): center}
|
|
288
|
+
|
|
289
|
+
if nlist is None:
|
|
290
|
+
# draw the whole graph in one shell
|
|
291
|
+
nlist = [list(G)]
|
|
292
|
+
|
|
293
|
+
radius_bump = scale / len(nlist)
|
|
294
|
+
|
|
295
|
+
if len(nlist[0]) == 1:
|
|
296
|
+
# single node at center
|
|
297
|
+
radius = 0.0
|
|
298
|
+
else:
|
|
299
|
+
# else start at r=1
|
|
300
|
+
radius = radius_bump
|
|
301
|
+
|
|
302
|
+
if rotate is None:
|
|
303
|
+
rotate = np.pi / len(nlist)
|
|
304
|
+
first_theta = rotate
|
|
305
|
+
npos = {}
|
|
306
|
+
for nodes in nlist:
|
|
307
|
+
# Discard the last angle (endpoint=False) since 2*pi matches 0 radians
|
|
308
|
+
theta = (
|
|
309
|
+
np.linspace(0, 2 * np.pi, len(nodes), endpoint=False, dtype=np.float32)
|
|
310
|
+
+ first_theta
|
|
311
|
+
)
|
|
312
|
+
pos = radius * np.column_stack([np.cos(theta), np.sin(theta)]) + center
|
|
313
|
+
npos.update(zip(nodes, pos))
|
|
314
|
+
radius += radius_bump
|
|
315
|
+
first_theta += rotate
|
|
316
|
+
|
|
317
|
+
if store_pos_as is not None:
|
|
318
|
+
nx.set_node_attributes(G, npos, store_pos_as)
|
|
319
|
+
return npos
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def bipartite_layout(
|
|
323
|
+
G,
|
|
324
|
+
nodes=None,
|
|
325
|
+
align="vertical",
|
|
326
|
+
scale=1,
|
|
327
|
+
center=None,
|
|
328
|
+
aspect_ratio=4 / 3,
|
|
329
|
+
store_pos_as=None,
|
|
330
|
+
):
|
|
331
|
+
"""Position nodes in two straight lines.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
G : NetworkX graph or list of nodes
|
|
336
|
+
A position will be assigned to every node in G.
|
|
337
|
+
|
|
338
|
+
nodes : collection of nodes
|
|
339
|
+
Nodes in one node set of the graph. This set will be placed on
|
|
340
|
+
left or top. If `None` (the default), a node set is chosen arbitrarily
|
|
341
|
+
if the graph if bipartite.
|
|
342
|
+
|
|
343
|
+
align : string (default='vertical')
|
|
344
|
+
The alignment of nodes. Vertical or horizontal.
|
|
345
|
+
|
|
346
|
+
scale : number (default: 1)
|
|
347
|
+
Scale factor for positions.
|
|
348
|
+
|
|
349
|
+
center : array-like or None
|
|
350
|
+
Coordinate pair around which to center the layout.
|
|
351
|
+
|
|
352
|
+
aspect_ratio : number (default=4/3):
|
|
353
|
+
The ratio of the width to the height of the layout.
|
|
354
|
+
|
|
355
|
+
store_pos_as : str, default None
|
|
356
|
+
If non-None, the position of each node will be stored on the graph as
|
|
357
|
+
an attribute with this string as its name, which can be accessed with
|
|
358
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
pos : dict
|
|
363
|
+
A dictionary of positions keyed by node.
|
|
364
|
+
|
|
365
|
+
Raises
|
|
366
|
+
------
|
|
367
|
+
NetworkXError
|
|
368
|
+
If ``nodes=None`` and `G` is not bipartite.
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
--------
|
|
372
|
+
>>> G = nx.complete_bipartite_graph(3, 3)
|
|
373
|
+
>>> pos = nx.bipartite_layout(G)
|
|
374
|
+
|
|
375
|
+
The ordering of the layout (i.e. which nodes appear on the left/top) can
|
|
376
|
+
be specified with the `nodes` parameter:
|
|
377
|
+
|
|
378
|
+
>>> top, bottom = nx.bipartite.sets(G)
|
|
379
|
+
>>> pos = nx.bipartite_layout(G, nodes=bottom) # "bottom" set appears on the left
|
|
380
|
+
|
|
381
|
+
`store_pos_as` can be used to store the node positions for the computed layout
|
|
382
|
+
directly on the nodes:
|
|
383
|
+
|
|
384
|
+
>>> _ = nx.bipartite_layout(G, nodes=bottom, store_pos_as="pos")
|
|
385
|
+
>>> from pprint import pprint
|
|
386
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
387
|
+
{0: array([ 1. , -0.75]),
|
|
388
|
+
1: array([1., 0.]),
|
|
389
|
+
2: array([1. , 0.75]),
|
|
390
|
+
3: array([-1. , -0.75]),
|
|
391
|
+
4: array([-1., 0.]),
|
|
392
|
+
5: array([-1. , 0.75])}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
The ``bipartite_layout`` function can be used with non-bipartite graphs
|
|
396
|
+
by explicitly specifying how the layout should be partitioned with `nodes`:
|
|
397
|
+
|
|
398
|
+
>>> G = nx.complete_graph(5) # Non-bipartite
|
|
399
|
+
>>> pos = nx.bipartite_layout(G, nodes={0, 1, 2})
|
|
400
|
+
|
|
401
|
+
Notes
|
|
402
|
+
-----
|
|
403
|
+
This algorithm currently only works in two dimensions and does not
|
|
404
|
+
try to minimize edge crossings.
|
|
405
|
+
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
import numpy as np
|
|
409
|
+
|
|
410
|
+
if align not in ("vertical", "horizontal"):
|
|
411
|
+
msg = "align must be either vertical or horizontal."
|
|
412
|
+
raise ValueError(msg)
|
|
413
|
+
|
|
414
|
+
G, center = _process_params(G, center=center, dim=2)
|
|
415
|
+
if len(G) == 0:
|
|
416
|
+
return {}
|
|
417
|
+
|
|
418
|
+
height = 1
|
|
419
|
+
width = aspect_ratio * height
|
|
420
|
+
offset = (width / 2, height / 2)
|
|
421
|
+
|
|
422
|
+
if nodes is None:
|
|
423
|
+
top, bottom = nx.bipartite.sets(G)
|
|
424
|
+
nodes = list(G)
|
|
425
|
+
else:
|
|
426
|
+
top = set(nodes)
|
|
427
|
+
bottom = set(G) - top
|
|
428
|
+
# Preserves backward-compatible node ordering in returned pos dict
|
|
429
|
+
nodes = list(top) + list(bottom)
|
|
430
|
+
|
|
431
|
+
left_xs = np.repeat(0, len(top))
|
|
432
|
+
right_xs = np.repeat(width, len(bottom))
|
|
433
|
+
left_ys = np.linspace(0, height, len(top))
|
|
434
|
+
right_ys = np.linspace(0, height, len(bottom))
|
|
435
|
+
|
|
436
|
+
top_pos = np.column_stack([left_xs, left_ys]) - offset
|
|
437
|
+
bottom_pos = np.column_stack([right_xs, right_ys]) - offset
|
|
438
|
+
|
|
439
|
+
pos = np.concatenate([top_pos, bottom_pos])
|
|
440
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
441
|
+
if align == "horizontal":
|
|
442
|
+
pos = pos[:, ::-1] # swap x and y coords
|
|
443
|
+
pos = dict(zip(nodes, pos))
|
|
444
|
+
|
|
445
|
+
if store_pos_as is not None:
|
|
446
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
447
|
+
|
|
448
|
+
return pos
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@np_random_state("seed")
|
|
452
|
+
def spring_layout(
|
|
453
|
+
G,
|
|
454
|
+
k=None,
|
|
455
|
+
pos=None,
|
|
456
|
+
fixed=None,
|
|
457
|
+
iterations=50,
|
|
458
|
+
threshold=1e-4,
|
|
459
|
+
weight="weight",
|
|
460
|
+
scale=1,
|
|
461
|
+
center=None,
|
|
462
|
+
dim=2,
|
|
463
|
+
seed=None,
|
|
464
|
+
store_pos_as=None,
|
|
465
|
+
*,
|
|
466
|
+
method="auto",
|
|
467
|
+
gravity=1.0,
|
|
468
|
+
):
|
|
469
|
+
"""Position nodes using Fruchterman-Reingold force-directed algorithm.
|
|
470
|
+
|
|
471
|
+
The algorithm simulates a force-directed representation of the network
|
|
472
|
+
treating edges as springs holding nodes close, while treating nodes
|
|
473
|
+
as repelling objects, sometimes called an anti-gravity force.
|
|
474
|
+
Simulation continues until the positions are close to an equilibrium.
|
|
475
|
+
|
|
476
|
+
There are some hard-coded values: minimal distance between
|
|
477
|
+
nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away.
|
|
478
|
+
During the simulation, `k` helps determine the distance between nodes,
|
|
479
|
+
though `scale` and `center` determine the size and place after
|
|
480
|
+
rescaling occurs at the end of the simulation.
|
|
481
|
+
|
|
482
|
+
Fixing some nodes doesn't allow them to move in the simulation.
|
|
483
|
+
It also turns off the rescaling feature at the simulation's end.
|
|
484
|
+
In addition, setting `scale` to `None` turns off rescaling.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
G : NetworkX graph or list of nodes
|
|
489
|
+
A position will be assigned to every node in G.
|
|
490
|
+
|
|
491
|
+
k : float (default=None)
|
|
492
|
+
Optimal distance between nodes. If None the distance is set to
|
|
493
|
+
1/sqrt(n) where n is the number of nodes. Increase this value
|
|
494
|
+
to move nodes farther apart.
|
|
495
|
+
|
|
496
|
+
pos : dict or None optional (default=None)
|
|
497
|
+
Initial positions for nodes as a dictionary with node as keys
|
|
498
|
+
and values as a coordinate list or tuple. If None, then use
|
|
499
|
+
random initial positions.
|
|
500
|
+
|
|
501
|
+
fixed : list or None optional (default=None)
|
|
502
|
+
Nodes to keep fixed at initial position.
|
|
503
|
+
Nodes not in ``G.nodes`` are ignored.
|
|
504
|
+
ValueError raised if `fixed` specified and `pos` not.
|
|
505
|
+
|
|
506
|
+
iterations : int optional (default=50)
|
|
507
|
+
Maximum number of iterations taken
|
|
508
|
+
|
|
509
|
+
threshold: float optional (default = 1e-4)
|
|
510
|
+
Threshold for relative error in node position changes.
|
|
511
|
+
The iteration stops if the error is below this threshold.
|
|
512
|
+
|
|
513
|
+
weight : string or None optional (default='weight')
|
|
514
|
+
The edge attribute that holds the numerical value used for
|
|
515
|
+
the edge weight. Larger means a stronger attractive force.
|
|
516
|
+
If None, then all edge weights are 1.
|
|
517
|
+
|
|
518
|
+
scale : number or None (default: 1)
|
|
519
|
+
Scale factor for positions. Not used unless `fixed is None`.
|
|
520
|
+
If scale is None, no rescaling is performed.
|
|
521
|
+
|
|
522
|
+
center : array-like or None
|
|
523
|
+
Coordinate pair around which to center the layout.
|
|
524
|
+
Not used unless `fixed is None`.
|
|
525
|
+
|
|
526
|
+
dim : int
|
|
527
|
+
Dimension of layout.
|
|
528
|
+
|
|
529
|
+
seed : int, RandomState instance or None optional (default=None)
|
|
530
|
+
Used only for the initial positions in the algorithm.
|
|
531
|
+
Set the random state for deterministic node layouts.
|
|
532
|
+
If int, `seed` is the seed used by the random number generator,
|
|
533
|
+
if numpy.random.RandomState instance, `seed` is the random
|
|
534
|
+
number generator,
|
|
535
|
+
if None, the random number generator is the RandomState instance used
|
|
536
|
+
by numpy.random.
|
|
537
|
+
|
|
538
|
+
store_pos_as : str, default None
|
|
539
|
+
If non-None, the position of each node will be stored on the graph as
|
|
540
|
+
an attribute with this string as its name, which can be accessed with
|
|
541
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
542
|
+
|
|
543
|
+
method : str optional (default='auto')
|
|
544
|
+
The method to compute the layout.
|
|
545
|
+
If 'force', the force-directed Fruchterman-Reingold algorithm [1]_ is used.
|
|
546
|
+
If 'energy', the energy-based optimization algorithm [2]_ is used with absolute
|
|
547
|
+
values of edge weights and gravitational forces acting on each connected component.
|
|
548
|
+
If 'auto', we use 'force' if ``len(G) < 500`` and 'energy' otherwise.
|
|
549
|
+
|
|
550
|
+
gravity: float optional (default=1.0)
|
|
551
|
+
Used only for the method='energy'.
|
|
552
|
+
The positive coefficient of gravitational forces per connected component.
|
|
553
|
+
|
|
554
|
+
Returns
|
|
555
|
+
-------
|
|
556
|
+
pos : dict
|
|
557
|
+
A dictionary of positions keyed by node
|
|
558
|
+
|
|
559
|
+
Examples
|
|
560
|
+
--------
|
|
561
|
+
>>> from pprint import pprint
|
|
562
|
+
>>> G = nx.path_graph(4)
|
|
563
|
+
>>> pos = nx.spring_layout(G)
|
|
564
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
565
|
+
>>> _ = nx.spring_layout(G, seed=123, store_pos_as="pos")
|
|
566
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
567
|
+
{0: array([-0.61495802, -1. ]),
|
|
568
|
+
1: array([-0.21789544, -0.35432583]),
|
|
569
|
+
2: array([0.21847843, 0.35527369]),
|
|
570
|
+
3: array([0.61437502, 0.99905215])}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# The same using longer but equivalent function name
|
|
574
|
+
>>> pos = nx.fruchterman_reingold_layout(G)
|
|
575
|
+
|
|
576
|
+
References
|
|
577
|
+
----------
|
|
578
|
+
.. [1] Fruchterman, Thomas MJ, and Edward M. Reingold.
|
|
579
|
+
"Graph drawing by force-directed placement."
|
|
580
|
+
Software: Practice and experience 21, no. 11 (1991): 1129-1164.
|
|
581
|
+
http://dx.doi.org/10.1002/spe.4380211102
|
|
582
|
+
.. [2] Hamaguchi, Hiroki, Naoki Marumo, and Akiko Takeda.
|
|
583
|
+
"Initial Placement for Fruchterman--Reingold Force Model With Coordinate Newton Direction."
|
|
584
|
+
arXiv preprint arXiv:2412.20317 (2024).
|
|
585
|
+
https://arxiv.org/abs/2412.20317
|
|
586
|
+
"""
|
|
587
|
+
import numpy as np
|
|
588
|
+
|
|
589
|
+
if method not in ("auto", "force", "energy"):
|
|
590
|
+
raise ValueError("the method must be either auto, force, or energy.")
|
|
591
|
+
if method == "auto":
|
|
592
|
+
method = "force" if len(G) < 500 else "energy"
|
|
593
|
+
|
|
594
|
+
G, center = _process_params(G, center, dim)
|
|
595
|
+
|
|
596
|
+
if fixed is not None:
|
|
597
|
+
if pos is None:
|
|
598
|
+
raise ValueError("nodes are fixed without positions given")
|
|
599
|
+
for node in fixed:
|
|
600
|
+
if node not in pos:
|
|
601
|
+
raise ValueError("nodes are fixed without positions given")
|
|
602
|
+
nfixed = {node: i for i, node in enumerate(G)}
|
|
603
|
+
fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed])
|
|
604
|
+
|
|
605
|
+
if pos is not None:
|
|
606
|
+
# Determine size of existing domain to adjust initial positions
|
|
607
|
+
dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup)
|
|
608
|
+
if dom_size == 0:
|
|
609
|
+
dom_size = 1
|
|
610
|
+
pos_arr = seed.rand(len(G), dim) * dom_size + center
|
|
611
|
+
|
|
612
|
+
for i, n in enumerate(G):
|
|
613
|
+
if n in pos:
|
|
614
|
+
pos_arr[i] = np.asarray(pos[n])
|
|
615
|
+
else:
|
|
616
|
+
pos_arr = None
|
|
617
|
+
dom_size = 1
|
|
618
|
+
|
|
619
|
+
if len(G) == 0:
|
|
620
|
+
return {}
|
|
621
|
+
if len(G) == 1:
|
|
622
|
+
pos = {nx.utils.arbitrary_element(G.nodes()): center}
|
|
623
|
+
if store_pos_as is not None:
|
|
624
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
625
|
+
return pos
|
|
626
|
+
|
|
627
|
+
# Sparse matrix
|
|
628
|
+
if len(G) >= 500 or method == "energy":
|
|
629
|
+
A = nx.to_scipy_sparse_array(G, weight=weight, dtype="f")
|
|
630
|
+
if k is None and fixed is not None:
|
|
631
|
+
# We must adjust k by domain size for layouts not near 1x1
|
|
632
|
+
nnodes, _ = A.shape
|
|
633
|
+
k = dom_size / np.sqrt(nnodes)
|
|
634
|
+
pos = _sparse_fruchterman_reingold(
|
|
635
|
+
A, k, pos_arr, fixed, iterations, threshold, dim, seed, method, gravity
|
|
636
|
+
)
|
|
637
|
+
else:
|
|
638
|
+
A = nx.to_numpy_array(G, weight=weight)
|
|
639
|
+
if k is None and fixed is not None:
|
|
640
|
+
# We must adjust k by domain size for layouts not near 1x1
|
|
641
|
+
nnodes, _ = A.shape
|
|
642
|
+
k = dom_size / np.sqrt(nnodes)
|
|
643
|
+
pos = _fruchterman_reingold(
|
|
644
|
+
A, k, pos_arr, fixed, iterations, threshold, dim, seed
|
|
645
|
+
)
|
|
646
|
+
if fixed is None and scale is not None:
|
|
647
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
648
|
+
pos = dict(zip(G, pos))
|
|
649
|
+
|
|
650
|
+
if store_pos_as is not None:
|
|
651
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
652
|
+
|
|
653
|
+
return pos
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
fruchterman_reingold_layout = spring_layout
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
@np_random_state(7)
|
|
660
|
+
def _fruchterman_reingold(
|
|
661
|
+
A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
|
|
662
|
+
):
|
|
663
|
+
# Position nodes in adjacency matrix A using Fruchterman-Reingold
|
|
664
|
+
# Entry point for NetworkX graph is fruchterman_reingold_layout()
|
|
665
|
+
import numpy as np
|
|
666
|
+
|
|
667
|
+
try:
|
|
668
|
+
nnodes, _ = A.shape
|
|
669
|
+
except AttributeError as err:
|
|
670
|
+
msg = "fruchterman_reingold() takes an adjacency matrix as input"
|
|
671
|
+
raise nx.NetworkXError(msg) from err
|
|
672
|
+
|
|
673
|
+
if pos is None:
|
|
674
|
+
# random initial positions
|
|
675
|
+
pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
|
|
676
|
+
else:
|
|
677
|
+
# make sure positions are of same type as matrix
|
|
678
|
+
pos = pos.astype(A.dtype)
|
|
679
|
+
|
|
680
|
+
# optimal distance between nodes
|
|
681
|
+
if k is None:
|
|
682
|
+
k = np.sqrt(1.0 / nnodes)
|
|
683
|
+
# the initial "temperature" is about .1 of domain area (=1x1)
|
|
684
|
+
# this is the largest step allowed in the dynamics.
|
|
685
|
+
# We need to calculate this in case our fixed positions force our domain
|
|
686
|
+
# to be much bigger than 1x1
|
|
687
|
+
t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
|
|
688
|
+
# simple cooling scheme.
|
|
689
|
+
# linearly step down by dt on each iteration so last iteration is size dt.
|
|
690
|
+
dt = t / (iterations + 1)
|
|
691
|
+
delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
|
|
692
|
+
# the inscrutable (but fast) version
|
|
693
|
+
# this is still O(V^2)
|
|
694
|
+
# could use multilevel methods to speed this up significantly
|
|
695
|
+
for iteration in range(iterations):
|
|
696
|
+
# matrix of difference between points
|
|
697
|
+
delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
|
|
698
|
+
# distance between points
|
|
699
|
+
distance = np.linalg.norm(delta, axis=-1)
|
|
700
|
+
# enforce minimum distance of 0.01
|
|
701
|
+
np.clip(distance, 0.01, None, out=distance)
|
|
702
|
+
# displacement "force"
|
|
703
|
+
displacement = np.einsum(
|
|
704
|
+
"ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
|
|
705
|
+
)
|
|
706
|
+
# update positions
|
|
707
|
+
length = np.linalg.norm(displacement, axis=-1)
|
|
708
|
+
# Threshold the minimum length prior to position scaling
|
|
709
|
+
# See gh-8113 for detailed discussion of the threshold
|
|
710
|
+
length = np.clip(length, a_min=0.01, a_max=None)
|
|
711
|
+
delta_pos = np.einsum("ij,i->ij", displacement, t / length)
|
|
712
|
+
if fixed is not None:
|
|
713
|
+
# don't change positions of fixed nodes
|
|
714
|
+
delta_pos[fixed] = 0.0
|
|
715
|
+
pos += delta_pos
|
|
716
|
+
# cool temperature
|
|
717
|
+
t -= dt
|
|
718
|
+
if (np.linalg.norm(delta_pos) / nnodes) < threshold:
|
|
719
|
+
break
|
|
720
|
+
return pos
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@np_random_state(7)
|
|
724
|
+
def _sparse_fruchterman_reingold(
|
|
725
|
+
A,
|
|
726
|
+
k=None,
|
|
727
|
+
pos=None,
|
|
728
|
+
fixed=None,
|
|
729
|
+
iterations=50,
|
|
730
|
+
threshold=1e-4,
|
|
731
|
+
dim=2,
|
|
732
|
+
seed=None,
|
|
733
|
+
method="energy",
|
|
734
|
+
gravity=1.0,
|
|
735
|
+
):
|
|
736
|
+
# Position nodes in adjacency matrix A using Fruchterman-Reingold
|
|
737
|
+
# Entry point for NetworkX graph is fruchterman_reingold_layout()
|
|
738
|
+
# Sparse version
|
|
739
|
+
import numpy as np
|
|
740
|
+
import scipy as sp
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
nnodes, _ = A.shape
|
|
744
|
+
except AttributeError as err:
|
|
745
|
+
msg = "fruchterman_reingold() takes an adjacency matrix as input"
|
|
746
|
+
raise nx.NetworkXError(msg) from err
|
|
747
|
+
|
|
748
|
+
if pos is None:
|
|
749
|
+
# random initial positions
|
|
750
|
+
pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
|
|
751
|
+
else:
|
|
752
|
+
# make sure positions are of same type as matrix
|
|
753
|
+
pos = pos.astype(A.dtype)
|
|
754
|
+
|
|
755
|
+
# no fixed nodes
|
|
756
|
+
if fixed is None:
|
|
757
|
+
fixed = []
|
|
758
|
+
|
|
759
|
+
# optimal distance between nodes
|
|
760
|
+
if k is None:
|
|
761
|
+
k = np.sqrt(1.0 / nnodes)
|
|
762
|
+
|
|
763
|
+
if method == "energy":
|
|
764
|
+
return _energy_fruchterman_reingold(
|
|
765
|
+
A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# make sure we have a LIst of Lists representation
|
|
769
|
+
try:
|
|
770
|
+
A = A.tolil()
|
|
771
|
+
except AttributeError:
|
|
772
|
+
A = (sp.sparse.coo_array(A)).tolil()
|
|
773
|
+
|
|
774
|
+
# the initial "temperature" is about .1 of domain area (=1x1)
|
|
775
|
+
# this is the largest step allowed in the dynamics.
|
|
776
|
+
t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
|
|
777
|
+
# simple cooling scheme.
|
|
778
|
+
# linearly step down by dt on each iteration so last iteration is size dt.
|
|
779
|
+
dt = t / (iterations + 1)
|
|
780
|
+
|
|
781
|
+
displacement = np.zeros((dim, nnodes))
|
|
782
|
+
for iteration in range(iterations):
|
|
783
|
+
displacement *= 0
|
|
784
|
+
# loop over rows
|
|
785
|
+
for i in range(A.shape[0]):
|
|
786
|
+
if i in fixed:
|
|
787
|
+
continue
|
|
788
|
+
# difference between this row's node position and all others
|
|
789
|
+
delta = (pos[i] - pos).T
|
|
790
|
+
# distance between points
|
|
791
|
+
distance = np.sqrt((delta**2).sum(axis=0))
|
|
792
|
+
# enforce minimum distance of 0.01
|
|
793
|
+
distance = np.clip(distance, a_min=0.01, a_max=None)
|
|
794
|
+
# the adjacency matrix row
|
|
795
|
+
Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container
|
|
796
|
+
# displacement "force"
|
|
797
|
+
displacement[:, i] += (
|
|
798
|
+
delta * (k * k / distance**2 - Ai * distance / k)
|
|
799
|
+
).sum(axis=1)
|
|
800
|
+
# update positions
|
|
801
|
+
length = np.sqrt((displacement**2).sum(axis=0))
|
|
802
|
+
# Threshold the minimum length prior to position scaling
|
|
803
|
+
# See gh-8113 for detailed discussion of the threshold
|
|
804
|
+
length = np.clip(length, a_min=0.01, a_max=None)
|
|
805
|
+
delta_pos = (displacement * t / length).T
|
|
806
|
+
pos += delta_pos
|
|
807
|
+
# cool temperature
|
|
808
|
+
t -= dt
|
|
809
|
+
if (np.linalg.norm(delta_pos) / nnodes) < threshold:
|
|
810
|
+
break
|
|
811
|
+
return pos
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _energy_fruchterman_reingold(
|
|
815
|
+
A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity
|
|
816
|
+
):
|
|
817
|
+
# Entry point for NetworkX graph is fruchterman_reingold_layout()
|
|
818
|
+
# energy-based version
|
|
819
|
+
import numpy as np
|
|
820
|
+
import scipy as sp
|
|
821
|
+
|
|
822
|
+
if gravity <= 0:
|
|
823
|
+
raise ValueError(f"the gravity must be positive.")
|
|
824
|
+
|
|
825
|
+
# make sure we have a Compressed Sparse Row format
|
|
826
|
+
try:
|
|
827
|
+
A = A.tocsr()
|
|
828
|
+
except AttributeError:
|
|
829
|
+
A = sp.sparse.csr_array(A)
|
|
830
|
+
|
|
831
|
+
# Take absolute values of edge weights and symmetrize it
|
|
832
|
+
A = np.abs(A)
|
|
833
|
+
A = (A + A.T) / 2
|
|
834
|
+
|
|
835
|
+
n_components, labels = sp.sparse.csgraph.connected_components(A, directed=False)
|
|
836
|
+
bincount = np.bincount(labels)
|
|
837
|
+
batchsize = 500
|
|
838
|
+
|
|
839
|
+
def _cost_FR(x):
|
|
840
|
+
pos = x.reshape((nnodes, dim))
|
|
841
|
+
grad = np.zeros((nnodes, dim))
|
|
842
|
+
cost = 0.0
|
|
843
|
+
for l in range(0, nnodes, batchsize):
|
|
844
|
+
r = min(l + batchsize, nnodes)
|
|
845
|
+
# difference between selected node positions and all others
|
|
846
|
+
delta = pos[l:r, np.newaxis, :] - pos[np.newaxis, :, :]
|
|
847
|
+
# distance between points with a minimum distance of 1e-5
|
|
848
|
+
distance2 = np.sum(delta * delta, axis=2)
|
|
849
|
+
distance2 = np.maximum(distance2, 1e-10)
|
|
850
|
+
distance = np.sqrt(distance2)
|
|
851
|
+
# temporary variable for calculation
|
|
852
|
+
Ad = A[l:r] * distance
|
|
853
|
+
# attractive forces and repulsive forces
|
|
854
|
+
grad[l:r] = 2 * np.einsum("ij,ijk->ik", Ad / k - k**2 / distance2, delta)
|
|
855
|
+
# integrated attractive forces
|
|
856
|
+
cost += np.sum(Ad * distance2) / (3 * k)
|
|
857
|
+
# integrated repulsive forces
|
|
858
|
+
cost -= k**2 * np.sum(np.log(distance))
|
|
859
|
+
# gravitational force from the centroids of connected components to (0.5, ..., 0.5)^T
|
|
860
|
+
centers = np.zeros((n_components, dim))
|
|
861
|
+
np.add.at(centers, labels, pos)
|
|
862
|
+
delta0 = centers / bincount[:, np.newaxis] - 0.5
|
|
863
|
+
grad += gravity * delta0[labels]
|
|
864
|
+
cost += gravity * 0.5 * np.sum(bincount * np.linalg.norm(delta0, axis=1) ** 2)
|
|
865
|
+
# fix positions of fixed nodes
|
|
866
|
+
grad[fixed] = 0.0
|
|
867
|
+
return cost, grad.ravel()
|
|
868
|
+
|
|
869
|
+
# Optimization of the energy function by L-BFGS algorithm
|
|
870
|
+
options = {"maxiter": iterations, "gtol": threshold}
|
|
871
|
+
return sp.optimize.minimize(
|
|
872
|
+
_cost_FR, pos.ravel(), method="L-BFGS-B", jac=True, options=options
|
|
873
|
+
).x.reshape((nnodes, dim))
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def kamada_kawai_layout(
|
|
877
|
+
G,
|
|
878
|
+
dist=None,
|
|
879
|
+
pos=None,
|
|
880
|
+
weight="weight",
|
|
881
|
+
scale=1,
|
|
882
|
+
center=None,
|
|
883
|
+
dim=2,
|
|
884
|
+
store_pos_as=None,
|
|
885
|
+
):
|
|
886
|
+
"""Position nodes using Kamada-Kawai path-length cost-function.
|
|
887
|
+
|
|
888
|
+
Parameters
|
|
889
|
+
----------
|
|
890
|
+
G : NetworkX graph or list of nodes
|
|
891
|
+
A position will be assigned to every node in G.
|
|
892
|
+
|
|
893
|
+
dist : dict (default=None)
|
|
894
|
+
A two-level dictionary of optimal distances between nodes,
|
|
895
|
+
indexed by source and destination node.
|
|
896
|
+
If None, the distance is computed using shortest_path_length().
|
|
897
|
+
|
|
898
|
+
pos : dict or None optional (default=None)
|
|
899
|
+
Initial positions for nodes as a dictionary with node as keys
|
|
900
|
+
and values as a coordinate list or tuple. If None, then use
|
|
901
|
+
circular_layout() for dim >= 2 and a linear layout for dim == 1.
|
|
902
|
+
|
|
903
|
+
weight : string or None optional (default='weight')
|
|
904
|
+
The edge attribute that holds the numerical value used for
|
|
905
|
+
the edge weight. If None, then all edge weights are 1.
|
|
906
|
+
|
|
907
|
+
scale : number (default: 1)
|
|
908
|
+
Scale factor for positions.
|
|
909
|
+
|
|
910
|
+
center : array-like or None
|
|
911
|
+
Coordinate pair around which to center the layout.
|
|
912
|
+
|
|
913
|
+
dim : int
|
|
914
|
+
Dimension of layout.
|
|
915
|
+
|
|
916
|
+
store_pos_as : str, default None
|
|
917
|
+
If non-None, the position of each node will be stored on the graph as
|
|
918
|
+
an attribute with this string as its name, which can be accessed with
|
|
919
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
920
|
+
|
|
921
|
+
Returns
|
|
922
|
+
-------
|
|
923
|
+
pos : dict
|
|
924
|
+
A dictionary of positions keyed by node
|
|
925
|
+
|
|
926
|
+
Examples
|
|
927
|
+
--------
|
|
928
|
+
>>> from pprint import pprint
|
|
929
|
+
>>> G = nx.path_graph(4)
|
|
930
|
+
>>> pos = nx.kamada_kawai_layout(G)
|
|
931
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
932
|
+
>>> _ = nx.kamada_kawai_layout(G, store_pos_as="pos")
|
|
933
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
934
|
+
{0: array([0.99996577, 0.99366857]),
|
|
935
|
+
1: array([0.32913544, 0.33543827]),
|
|
936
|
+
2: array([-0.33544334, -0.32910684]),
|
|
937
|
+
3: array([-0.99365787, -1. ])}
|
|
938
|
+
"""
|
|
939
|
+
import numpy as np
|
|
940
|
+
|
|
941
|
+
G, center = _process_params(G, center, dim)
|
|
942
|
+
nNodes = len(G)
|
|
943
|
+
if nNodes == 0:
|
|
944
|
+
return {}
|
|
945
|
+
|
|
946
|
+
if dist is None:
|
|
947
|
+
dist = dict(nx.shortest_path_length(G, weight=weight))
|
|
948
|
+
dist_mtx = 1e6 * np.ones((nNodes, nNodes))
|
|
949
|
+
for row, nr in enumerate(G):
|
|
950
|
+
if nr not in dist:
|
|
951
|
+
continue
|
|
952
|
+
rdist = dist[nr]
|
|
953
|
+
for col, nc in enumerate(G):
|
|
954
|
+
if nc not in rdist:
|
|
955
|
+
continue
|
|
956
|
+
dist_mtx[row][col] = rdist[nc]
|
|
957
|
+
|
|
958
|
+
if pos is None:
|
|
959
|
+
if dim >= 3:
|
|
960
|
+
pos = random_layout(G, dim=dim)
|
|
961
|
+
elif dim == 2:
|
|
962
|
+
pos = circular_layout(G, dim=dim)
|
|
963
|
+
else:
|
|
964
|
+
pos = dict(zip(G, np.linspace(0, 1, len(G))))
|
|
965
|
+
pos_arr = np.array([pos[n] for n in G])
|
|
966
|
+
|
|
967
|
+
pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim)
|
|
968
|
+
|
|
969
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
970
|
+
pos = dict(zip(G, pos))
|
|
971
|
+
|
|
972
|
+
if store_pos_as is not None:
|
|
973
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
974
|
+
|
|
975
|
+
return pos
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _kamada_kawai_solve(dist_mtx, pos_arr, dim):
|
|
979
|
+
# Anneal node locations based on the Kamada-Kawai cost-function,
|
|
980
|
+
# using the supplied matrix of preferred inter-node distances,
|
|
981
|
+
# and starting locations.
|
|
982
|
+
|
|
983
|
+
import numpy as np
|
|
984
|
+
import scipy as sp
|
|
985
|
+
|
|
986
|
+
meanwt = 1e-3
|
|
987
|
+
costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim)
|
|
988
|
+
|
|
989
|
+
optresult = sp.optimize.minimize(
|
|
990
|
+
_kamada_kawai_costfn,
|
|
991
|
+
pos_arr.ravel(),
|
|
992
|
+
method="L-BFGS-B",
|
|
993
|
+
args=costargs,
|
|
994
|
+
jac=True,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
return optresult.x.reshape((-1, dim))
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim):
|
|
1001
|
+
# Cost-function and gradient for Kamada-Kawai layout algorithm
|
|
1002
|
+
nNodes = invdist.shape[0]
|
|
1003
|
+
pos_arr = pos_vec.reshape((nNodes, dim))
|
|
1004
|
+
|
|
1005
|
+
delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :]
|
|
1006
|
+
nodesep = np.linalg.norm(delta, axis=-1)
|
|
1007
|
+
direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3))
|
|
1008
|
+
|
|
1009
|
+
offset = nodesep * invdist - 1.0
|
|
1010
|
+
offset[np.diag_indices(nNodes)] = 0
|
|
1011
|
+
|
|
1012
|
+
cost = 0.5 * np.sum(offset**2)
|
|
1013
|
+
grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum(
|
|
1014
|
+
"ij,ij,ijk->jk", invdist, offset, direction
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
# Additional parabolic term to encourage mean position to be near origin:
|
|
1018
|
+
sumpos = np.sum(pos_arr, axis=0)
|
|
1019
|
+
cost += 0.5 * meanweight * np.sum(sumpos**2)
|
|
1020
|
+
grad += meanweight * sumpos
|
|
1021
|
+
|
|
1022
|
+
return (cost, grad.ravel())
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def spectral_layout(G, weight="weight", scale=1, center=None, dim=2, store_pos_as=None):
|
|
1026
|
+
"""Position nodes using the eigenvectors of the graph Laplacian.
|
|
1027
|
+
|
|
1028
|
+
Using the unnormalized Laplacian, the layout shows possible clusters of
|
|
1029
|
+
nodes which are an approximation of the ratio cut. If dim is the number of
|
|
1030
|
+
dimensions then the positions are the entries of the dim eigenvectors
|
|
1031
|
+
corresponding to the ascending eigenvalues starting from the second one.
|
|
1032
|
+
|
|
1033
|
+
Parameters
|
|
1034
|
+
----------
|
|
1035
|
+
G : NetworkX graph or list of nodes
|
|
1036
|
+
A position will be assigned to every node in G.
|
|
1037
|
+
|
|
1038
|
+
weight : string or None optional (default='weight')
|
|
1039
|
+
The edge attribute that holds the numerical value used for
|
|
1040
|
+
the edge weight. If None, then all edge weights are 1.
|
|
1041
|
+
|
|
1042
|
+
scale : number (default: 1)
|
|
1043
|
+
Scale factor for positions.
|
|
1044
|
+
|
|
1045
|
+
center : array-like or None
|
|
1046
|
+
Coordinate pair around which to center the layout.
|
|
1047
|
+
|
|
1048
|
+
dim : int
|
|
1049
|
+
Dimension of layout.
|
|
1050
|
+
|
|
1051
|
+
store_pos_as : str, default None
|
|
1052
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1053
|
+
an attribute with this string as its name, which can be accessed with
|
|
1054
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1055
|
+
|
|
1056
|
+
Returns
|
|
1057
|
+
-------
|
|
1058
|
+
pos : dict
|
|
1059
|
+
A dictionary of positions keyed by node
|
|
1060
|
+
|
|
1061
|
+
Examples
|
|
1062
|
+
--------
|
|
1063
|
+
>>> from pprint import pprint
|
|
1064
|
+
>>> G = nx.path_graph(4)
|
|
1065
|
+
>>> pos = nx.spectral_layout(G)
|
|
1066
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
1067
|
+
>>> _ = nx.spectral_layout(G, store_pos_as="pos")
|
|
1068
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
1069
|
+
{0: array([-1. , 0.76536686]),
|
|
1070
|
+
1: array([-0.41421356, -0.76536686]),
|
|
1071
|
+
2: array([ 0.41421356, -0.76536686]),
|
|
1072
|
+
3: array([1. , 0.76536686])}
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
Notes
|
|
1076
|
+
-----
|
|
1077
|
+
Directed graphs will be considered as undirected graphs when
|
|
1078
|
+
positioning the nodes.
|
|
1079
|
+
|
|
1080
|
+
For larger graphs (>500 nodes) this will use the SciPy sparse
|
|
1081
|
+
eigenvalue solver (ARPACK).
|
|
1082
|
+
"""
|
|
1083
|
+
# handle some special cases that break the eigensolvers
|
|
1084
|
+
import numpy as np
|
|
1085
|
+
|
|
1086
|
+
G, center = _process_params(G, center, dim)
|
|
1087
|
+
|
|
1088
|
+
if len(G) <= 2:
|
|
1089
|
+
if len(G) == 0:
|
|
1090
|
+
pos = np.array([])
|
|
1091
|
+
elif len(G) == 1:
|
|
1092
|
+
pos = np.array([center])
|
|
1093
|
+
else:
|
|
1094
|
+
pos = np.array([np.zeros(dim), np.array(center) * 2.0])
|
|
1095
|
+
return dict(zip(G, pos))
|
|
1096
|
+
try:
|
|
1097
|
+
# Sparse matrix
|
|
1098
|
+
if len(G) < 500: # dense solver is faster for small graphs
|
|
1099
|
+
raise ValueError
|
|
1100
|
+
A = nx.to_scipy_sparse_array(G, weight=weight, dtype="d")
|
|
1101
|
+
# Symmetrize directed graphs
|
|
1102
|
+
if G.is_directed():
|
|
1103
|
+
A = A + np.transpose(A)
|
|
1104
|
+
pos = _sparse_spectral(A, dim)
|
|
1105
|
+
except (ImportError, ValueError):
|
|
1106
|
+
# Dense matrix
|
|
1107
|
+
A = nx.to_numpy_array(G, weight=weight)
|
|
1108
|
+
# Symmetrize directed graphs
|
|
1109
|
+
if G.is_directed():
|
|
1110
|
+
A += A.T
|
|
1111
|
+
pos = _spectral(A, dim)
|
|
1112
|
+
|
|
1113
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
1114
|
+
pos = dict(zip(G, pos))
|
|
1115
|
+
|
|
1116
|
+
if store_pos_as is not None:
|
|
1117
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1118
|
+
|
|
1119
|
+
return pos
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _spectral(A, dim=2):
|
|
1123
|
+
# Input adjacency matrix A
|
|
1124
|
+
# Uses dense eigenvalue solver from numpy
|
|
1125
|
+
import numpy as np
|
|
1126
|
+
|
|
1127
|
+
try:
|
|
1128
|
+
nnodes, _ = A.shape
|
|
1129
|
+
except AttributeError as err:
|
|
1130
|
+
msg = "spectral() takes an adjacency matrix as input"
|
|
1131
|
+
raise nx.NetworkXError(msg) from err
|
|
1132
|
+
|
|
1133
|
+
# form Laplacian matrix where D is diagonal of degrees
|
|
1134
|
+
D = np.identity(nnodes, dtype=A.dtype) * np.sum(A, axis=1)
|
|
1135
|
+
L = D - A
|
|
1136
|
+
|
|
1137
|
+
eigenvalues, eigenvectors = np.linalg.eig(L)
|
|
1138
|
+
# sort and keep smallest nonzero
|
|
1139
|
+
index = np.argsort(eigenvalues)[1 : dim + 1] # 0 index is zero eigenvalue
|
|
1140
|
+
return np.real(eigenvectors[:, index])
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _sparse_spectral(A, dim=2):
|
|
1144
|
+
# Input adjacency matrix A
|
|
1145
|
+
# Uses sparse eigenvalue solver from scipy
|
|
1146
|
+
# Could use multilevel methods here, see Koren "On spectral graph drawing"
|
|
1147
|
+
import numpy as np
|
|
1148
|
+
import scipy as sp
|
|
1149
|
+
|
|
1150
|
+
try:
|
|
1151
|
+
nnodes, _ = A.shape
|
|
1152
|
+
except AttributeError as err:
|
|
1153
|
+
msg = "sparse_spectral() takes an adjacency matrix as input"
|
|
1154
|
+
raise nx.NetworkXError(msg) from err
|
|
1155
|
+
|
|
1156
|
+
# form Laplacian matrix
|
|
1157
|
+
D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(nnodes, nnodes)).tocsr()
|
|
1158
|
+
L = D - A
|
|
1159
|
+
|
|
1160
|
+
k = dim + 1
|
|
1161
|
+
# number of Lanczos vectors for ARPACK solver.What is the right scaling?
|
|
1162
|
+
ncv = max(2 * k + 1, int(np.sqrt(nnodes)))
|
|
1163
|
+
# return smallest k eigenvalues and eigenvectors
|
|
1164
|
+
eigenvalues, eigenvectors = sp.sparse.linalg.eigsh(L, k, which="SM", ncv=ncv)
|
|
1165
|
+
index = np.argsort(eigenvalues)[1:k] # 0 index is zero eigenvalue
|
|
1166
|
+
return np.real(eigenvectors[:, index])
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def planar_layout(G, scale=1, center=None, dim=2, store_pos_as=None):
|
|
1170
|
+
"""Position nodes without edge intersections.
|
|
1171
|
+
|
|
1172
|
+
Parameters
|
|
1173
|
+
----------
|
|
1174
|
+
G : NetworkX graph or list of nodes
|
|
1175
|
+
A position will be assigned to every node in G. If G is of type
|
|
1176
|
+
nx.PlanarEmbedding, the positions are selected accordingly.
|
|
1177
|
+
|
|
1178
|
+
scale : number (default: 1)
|
|
1179
|
+
Scale factor for positions.
|
|
1180
|
+
|
|
1181
|
+
center : array-like or None
|
|
1182
|
+
Coordinate pair around which to center the layout.
|
|
1183
|
+
|
|
1184
|
+
dim : int
|
|
1185
|
+
Dimension of layout.
|
|
1186
|
+
|
|
1187
|
+
store_pos_as : str, default None
|
|
1188
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1189
|
+
an attribute with this string as its name, which can be accessed with
|
|
1190
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1191
|
+
|
|
1192
|
+
Returns
|
|
1193
|
+
-------
|
|
1194
|
+
pos : dict
|
|
1195
|
+
A dictionary of positions keyed by node
|
|
1196
|
+
|
|
1197
|
+
Raises
|
|
1198
|
+
------
|
|
1199
|
+
NetworkXException
|
|
1200
|
+
If G is not planar
|
|
1201
|
+
|
|
1202
|
+
Examples
|
|
1203
|
+
--------
|
|
1204
|
+
>>> from pprint import pprint
|
|
1205
|
+
>>> G = nx.path_graph(4)
|
|
1206
|
+
>>> pos = nx.planar_layout(G)
|
|
1207
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
1208
|
+
>>> _ = nx.planar_layout(G, store_pos_as="pos")
|
|
1209
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
1210
|
+
{0: array([-0.77777778, -0.33333333]),
|
|
1211
|
+
1: array([ 1. , -0.33333333]),
|
|
1212
|
+
2: array([0.11111111, 0.55555556]),
|
|
1213
|
+
3: array([-0.33333333, 0.11111111])}
|
|
1214
|
+
"""
|
|
1215
|
+
import numpy as np
|
|
1216
|
+
|
|
1217
|
+
if dim != 2:
|
|
1218
|
+
raise ValueError("can only handle 2 dimensions")
|
|
1219
|
+
|
|
1220
|
+
G, center = _process_params(G, center, dim)
|
|
1221
|
+
|
|
1222
|
+
if len(G) == 0:
|
|
1223
|
+
return {}
|
|
1224
|
+
|
|
1225
|
+
if isinstance(G, nx.PlanarEmbedding):
|
|
1226
|
+
embedding = G
|
|
1227
|
+
else:
|
|
1228
|
+
is_planar, embedding = nx.check_planarity(G)
|
|
1229
|
+
if not is_planar:
|
|
1230
|
+
raise nx.NetworkXException("G is not planar.")
|
|
1231
|
+
pos = nx.combinatorial_embedding_to_pos(embedding)
|
|
1232
|
+
node_list = list(embedding)
|
|
1233
|
+
pos = np.vstack([pos[x] for x in node_list])
|
|
1234
|
+
pos = pos.astype(np.float64)
|
|
1235
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
1236
|
+
pos = dict(zip(node_list, pos))
|
|
1237
|
+
if store_pos_as is not None:
|
|
1238
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1239
|
+
return pos
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def spiral_layout(
|
|
1243
|
+
G,
|
|
1244
|
+
scale=1,
|
|
1245
|
+
center=None,
|
|
1246
|
+
dim=2,
|
|
1247
|
+
resolution=0.35,
|
|
1248
|
+
equidistant=False,
|
|
1249
|
+
store_pos_as=None,
|
|
1250
|
+
):
|
|
1251
|
+
"""Position nodes in a spiral layout.
|
|
1252
|
+
|
|
1253
|
+
Parameters
|
|
1254
|
+
----------
|
|
1255
|
+
G : NetworkX graph or list of nodes
|
|
1256
|
+
A position will be assigned to every node in G.
|
|
1257
|
+
|
|
1258
|
+
scale : number (default: 1)
|
|
1259
|
+
Scale factor for positions.
|
|
1260
|
+
|
|
1261
|
+
center : array-like or None
|
|
1262
|
+
Coordinate pair around which to center the layout.
|
|
1263
|
+
|
|
1264
|
+
dim : int, default=2
|
|
1265
|
+
Dimension of layout, currently only dim=2 is supported.
|
|
1266
|
+
Other dimension values result in a ValueError.
|
|
1267
|
+
|
|
1268
|
+
resolution : float, default=0.35
|
|
1269
|
+
The compactness of the spiral layout returned.
|
|
1270
|
+
Lower values result in more compressed spiral layouts.
|
|
1271
|
+
|
|
1272
|
+
equidistant : bool, default=False
|
|
1273
|
+
If True, nodes will be positioned equidistant from each other
|
|
1274
|
+
by decreasing angle further from center.
|
|
1275
|
+
If False, nodes will be positioned at equal angles
|
|
1276
|
+
from each other by increasing separation further from center.
|
|
1277
|
+
|
|
1278
|
+
store_pos_as : str, default None
|
|
1279
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1280
|
+
an attribute with this string as its name, which can be accessed with
|
|
1281
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1282
|
+
|
|
1283
|
+
Returns
|
|
1284
|
+
-------
|
|
1285
|
+
pos : dict
|
|
1286
|
+
A dictionary of positions keyed by node
|
|
1287
|
+
|
|
1288
|
+
Raises
|
|
1289
|
+
------
|
|
1290
|
+
ValueError
|
|
1291
|
+
If dim != 2
|
|
1292
|
+
|
|
1293
|
+
Examples
|
|
1294
|
+
--------
|
|
1295
|
+
>>> from pprint import pprint
|
|
1296
|
+
>>> G = nx.path_graph(4)
|
|
1297
|
+
>>> pos = nx.spiral_layout(G)
|
|
1298
|
+
>>> nx.draw(G, pos=pos)
|
|
1299
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
1300
|
+
>>> _ = nx.spiral_layout(G, store_pos_as="pos")
|
|
1301
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
1302
|
+
{0: array([-0.64153279, -0.68555087]),
|
|
1303
|
+
1: array([-0.03307913, -0.46344795]),
|
|
1304
|
+
2: array([0.34927952, 0.14899882]),
|
|
1305
|
+
3: array([0.32533239, 1. ])}
|
|
1306
|
+
|
|
1307
|
+
Notes
|
|
1308
|
+
-----
|
|
1309
|
+
This algorithm currently only works in two dimensions.
|
|
1310
|
+
|
|
1311
|
+
"""
|
|
1312
|
+
import numpy as np
|
|
1313
|
+
|
|
1314
|
+
if dim != 2:
|
|
1315
|
+
raise ValueError("can only handle 2 dimensions")
|
|
1316
|
+
|
|
1317
|
+
G, center = _process_params(G, center, dim)
|
|
1318
|
+
|
|
1319
|
+
if len(G) == 0:
|
|
1320
|
+
return {}
|
|
1321
|
+
if len(G) == 1:
|
|
1322
|
+
pos = {nx.utils.arbitrary_element(G): center}
|
|
1323
|
+
if store_pos_as is not None:
|
|
1324
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1325
|
+
return pos
|
|
1326
|
+
|
|
1327
|
+
pos = []
|
|
1328
|
+
if equidistant:
|
|
1329
|
+
chord = 1
|
|
1330
|
+
step = 0.5
|
|
1331
|
+
theta = resolution
|
|
1332
|
+
theta += chord / (step * theta)
|
|
1333
|
+
for _ in range(len(G)):
|
|
1334
|
+
r = step * theta
|
|
1335
|
+
theta += chord / r
|
|
1336
|
+
pos.append([np.cos(theta) * r, np.sin(theta) * r])
|
|
1337
|
+
|
|
1338
|
+
else:
|
|
1339
|
+
dist = np.arange(len(G), dtype=float)
|
|
1340
|
+
angle = resolution * dist
|
|
1341
|
+
pos = np.transpose(dist * np.array([np.cos(angle), np.sin(angle)]))
|
|
1342
|
+
|
|
1343
|
+
pos = rescale_layout(np.array(pos), scale=scale) + center
|
|
1344
|
+
|
|
1345
|
+
pos = dict(zip(G, pos))
|
|
1346
|
+
|
|
1347
|
+
if store_pos_as is not None:
|
|
1348
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1349
|
+
|
|
1350
|
+
return pos
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def multipartite_layout(
|
|
1354
|
+
G, subset_key="subset", align="vertical", scale=1, center=None, store_pos_as=None
|
|
1355
|
+
):
|
|
1356
|
+
"""Position nodes in layers of straight lines.
|
|
1357
|
+
|
|
1358
|
+
Parameters
|
|
1359
|
+
----------
|
|
1360
|
+
G : NetworkX graph or list of nodes
|
|
1361
|
+
A position will be assigned to every node in G.
|
|
1362
|
+
|
|
1363
|
+
subset_key : string or dict (default='subset')
|
|
1364
|
+
If a string, the key of node data in G that holds the node subset.
|
|
1365
|
+
If a dict, keyed by layer number to the nodes in that layer/subset.
|
|
1366
|
+
|
|
1367
|
+
align : string (default='vertical')
|
|
1368
|
+
The alignment of nodes. Vertical or horizontal.
|
|
1369
|
+
|
|
1370
|
+
scale : number (default: 1)
|
|
1371
|
+
Scale factor for positions.
|
|
1372
|
+
|
|
1373
|
+
center : array-like or None
|
|
1374
|
+
Coordinate pair around which to center the layout.
|
|
1375
|
+
|
|
1376
|
+
store_pos_as : str, default None
|
|
1377
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1378
|
+
an attribute with this string as its name, which can be accessed with
|
|
1379
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1380
|
+
|
|
1381
|
+
Returns
|
|
1382
|
+
-------
|
|
1383
|
+
pos : dict
|
|
1384
|
+
A dictionary of positions keyed by node.
|
|
1385
|
+
|
|
1386
|
+
Examples
|
|
1387
|
+
--------
|
|
1388
|
+
>>> G = nx.complete_multipartite_graph(28, 16, 10)
|
|
1389
|
+
>>> pos = nx.multipartite_layout(G)
|
|
1390
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
1391
|
+
>>> G = nx.complete_multipartite_graph(28, 16, 10)
|
|
1392
|
+
>>> _ = nx.multipartite_layout(G, store_pos_as="pos")
|
|
1393
|
+
|
|
1394
|
+
or use a dict to provide the layers of the layout
|
|
1395
|
+
|
|
1396
|
+
>>> G = nx.Graph([(0, 1), (1, 2), (1, 3), (3, 4)])
|
|
1397
|
+
>>> layers = {"a": [0], "b": [1], "c": [2, 3], "d": [4]}
|
|
1398
|
+
>>> pos = nx.multipartite_layout(G, subset_key=layers)
|
|
1399
|
+
|
|
1400
|
+
Notes
|
|
1401
|
+
-----
|
|
1402
|
+
This algorithm currently only works in two dimensions and does not
|
|
1403
|
+
try to minimize edge crossings.
|
|
1404
|
+
|
|
1405
|
+
Network does not need to be a complete multipartite graph. As long as nodes
|
|
1406
|
+
have subset_key data, they will be placed in the corresponding layers.
|
|
1407
|
+
|
|
1408
|
+
"""
|
|
1409
|
+
import numpy as np
|
|
1410
|
+
|
|
1411
|
+
if align not in ("vertical", "horizontal"):
|
|
1412
|
+
msg = "align must be either vertical or horizontal."
|
|
1413
|
+
raise ValueError(msg)
|
|
1414
|
+
|
|
1415
|
+
G, center = _process_params(G, center=center, dim=2)
|
|
1416
|
+
if len(G) == 0:
|
|
1417
|
+
return {}
|
|
1418
|
+
|
|
1419
|
+
try:
|
|
1420
|
+
# check if subset_key is dict-like
|
|
1421
|
+
if len(G) != sum(len(nodes) for nodes in subset_key.values()):
|
|
1422
|
+
raise nx.NetworkXError(
|
|
1423
|
+
"all nodes must be in one subset of `subset_key` dict"
|
|
1424
|
+
)
|
|
1425
|
+
except AttributeError:
|
|
1426
|
+
# subset_key is not a dict, hence a string
|
|
1427
|
+
node_to_subset = nx.get_node_attributes(G, subset_key)
|
|
1428
|
+
if len(node_to_subset) != len(G):
|
|
1429
|
+
raise nx.NetworkXError(
|
|
1430
|
+
f"all nodes need a subset_key attribute: {subset_key}"
|
|
1431
|
+
)
|
|
1432
|
+
subset_key = nx.utils.groups(node_to_subset)
|
|
1433
|
+
|
|
1434
|
+
# Sort by layer, if possible
|
|
1435
|
+
try:
|
|
1436
|
+
layers = dict(sorted(subset_key.items()))
|
|
1437
|
+
except TypeError:
|
|
1438
|
+
layers = subset_key
|
|
1439
|
+
|
|
1440
|
+
pos = None
|
|
1441
|
+
nodes = []
|
|
1442
|
+
width = len(layers)
|
|
1443
|
+
for i, layer in enumerate(layers.values()):
|
|
1444
|
+
height = len(layer)
|
|
1445
|
+
xs = np.repeat(i, height)
|
|
1446
|
+
ys = np.arange(0, height, dtype=float)
|
|
1447
|
+
offset = ((width - 1) / 2, (height - 1) / 2)
|
|
1448
|
+
layer_pos = np.column_stack([xs, ys]) - offset
|
|
1449
|
+
if pos is None:
|
|
1450
|
+
pos = layer_pos
|
|
1451
|
+
else:
|
|
1452
|
+
pos = np.concatenate([pos, layer_pos])
|
|
1453
|
+
nodes.extend(layer)
|
|
1454
|
+
pos = rescale_layout(pos, scale=scale) + center
|
|
1455
|
+
if align == "horizontal":
|
|
1456
|
+
pos = pos[:, ::-1] # swap x and y coords
|
|
1457
|
+
pos = dict(zip(nodes, pos))
|
|
1458
|
+
|
|
1459
|
+
if store_pos_as is not None:
|
|
1460
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1461
|
+
|
|
1462
|
+
return pos
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
@np_random_state("seed")
|
|
1466
|
+
def arf_layout(
|
|
1467
|
+
G,
|
|
1468
|
+
pos=None,
|
|
1469
|
+
scaling=1,
|
|
1470
|
+
a=1.1,
|
|
1471
|
+
etol=1e-6,
|
|
1472
|
+
dt=1e-3,
|
|
1473
|
+
max_iter=1000,
|
|
1474
|
+
*,
|
|
1475
|
+
seed=None,
|
|
1476
|
+
store_pos_as=None,
|
|
1477
|
+
):
|
|
1478
|
+
"""Arf layout for networkx
|
|
1479
|
+
|
|
1480
|
+
The attractive and repulsive forces (arf) layout [1] improves the spring
|
|
1481
|
+
layout in three ways. First, it prevents congestion of highly connected nodes
|
|
1482
|
+
due to strong forcing between nodes. Second, it utilizes the layout space
|
|
1483
|
+
more effectively by preventing large gaps that spring layout tends to create.
|
|
1484
|
+
Lastly, the arf layout represents symmetries in the layout better than the
|
|
1485
|
+
default spring layout.
|
|
1486
|
+
|
|
1487
|
+
Parameters
|
|
1488
|
+
----------
|
|
1489
|
+
G : nx.Graph or nx.DiGraph
|
|
1490
|
+
Networkx graph.
|
|
1491
|
+
pos : dict
|
|
1492
|
+
Initial position of the nodes. If set to None a
|
|
1493
|
+
random layout will be used.
|
|
1494
|
+
scaling : float
|
|
1495
|
+
Scales the radius of the circular layout space.
|
|
1496
|
+
a : float
|
|
1497
|
+
Strength of springs between connected nodes. Should be larger than 1.
|
|
1498
|
+
The greater a, the clearer the separation of unconnected sub clusters.
|
|
1499
|
+
etol : float
|
|
1500
|
+
Gradient sum of spring forces must be larger than `etol` before successful
|
|
1501
|
+
termination.
|
|
1502
|
+
dt : float
|
|
1503
|
+
Time step for force differential equation simulations.
|
|
1504
|
+
max_iter : int
|
|
1505
|
+
Max iterations before termination of the algorithm.
|
|
1506
|
+
seed : int, RandomState instance or None optional (default=None)
|
|
1507
|
+
Set the random state for deterministic node layouts.
|
|
1508
|
+
If int, `seed` is the seed used by the random number generator,
|
|
1509
|
+
if numpy.random.RandomState instance, `seed` is the random
|
|
1510
|
+
number generator,
|
|
1511
|
+
if None, the random number generator is the RandomState instance used
|
|
1512
|
+
by numpy.random.
|
|
1513
|
+
store_pos_as : str, default None
|
|
1514
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1515
|
+
an attribute with this string as its name, which can be accessed with
|
|
1516
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1517
|
+
|
|
1518
|
+
Returns
|
|
1519
|
+
-------
|
|
1520
|
+
pos : dict
|
|
1521
|
+
A dictionary of positions keyed by node.
|
|
1522
|
+
|
|
1523
|
+
Examples
|
|
1524
|
+
--------
|
|
1525
|
+
>>> G = nx.grid_graph((5, 5))
|
|
1526
|
+
>>> pos = nx.arf_layout(G)
|
|
1527
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
1528
|
+
>>> G = nx.grid_graph((5, 5))
|
|
1529
|
+
>>> _ = nx.arf_layout(G, store_pos_as="pos")
|
|
1530
|
+
|
|
1531
|
+
References
|
|
1532
|
+
----------
|
|
1533
|
+
.. [1] "Self-Organization Applied to Dynamic Network Layout", M. Geipel,
|
|
1534
|
+
International Journal of Modern Physics C, 2007, Vol 18, No 10,
|
|
1535
|
+
pp. 1537-1549.
|
|
1536
|
+
https://doi.org/10.1142/S0129183107011558 https://arxiv.org/abs/0704.1748
|
|
1537
|
+
"""
|
|
1538
|
+
import warnings
|
|
1539
|
+
|
|
1540
|
+
import numpy as np
|
|
1541
|
+
|
|
1542
|
+
if a <= 1:
|
|
1543
|
+
msg = "The parameter a should be larger than 1"
|
|
1544
|
+
raise ValueError(msg)
|
|
1545
|
+
|
|
1546
|
+
pos_tmp = nx.random_layout(G, seed=seed)
|
|
1547
|
+
if pos is None:
|
|
1548
|
+
pos = pos_tmp
|
|
1549
|
+
else:
|
|
1550
|
+
for node in G.nodes():
|
|
1551
|
+
if node not in pos:
|
|
1552
|
+
pos[node] = pos_tmp[node].copy()
|
|
1553
|
+
|
|
1554
|
+
# Initialize spring constant matrix
|
|
1555
|
+
N = len(G)
|
|
1556
|
+
# No nodes no computation
|
|
1557
|
+
if N == 0:
|
|
1558
|
+
return pos
|
|
1559
|
+
|
|
1560
|
+
# init force of springs
|
|
1561
|
+
K = np.ones((N, N)) - np.eye(N)
|
|
1562
|
+
node_order = {node: i for i, node in enumerate(G)}
|
|
1563
|
+
for x, y in G.edges():
|
|
1564
|
+
if x != y:
|
|
1565
|
+
idx, jdx = (node_order[i] for i in (x, y))
|
|
1566
|
+
K[idx, jdx] = a
|
|
1567
|
+
|
|
1568
|
+
# vectorize values
|
|
1569
|
+
p = np.asarray(list(pos.values()))
|
|
1570
|
+
|
|
1571
|
+
# equation 10 in [1]
|
|
1572
|
+
rho = scaling * np.sqrt(N)
|
|
1573
|
+
|
|
1574
|
+
# looping variables
|
|
1575
|
+
error = etol + 1
|
|
1576
|
+
n_iter = 0
|
|
1577
|
+
while error > etol:
|
|
1578
|
+
diff = p[:, np.newaxis] - p[np.newaxis]
|
|
1579
|
+
A = np.linalg.norm(diff, axis=-1)[..., np.newaxis]
|
|
1580
|
+
# attraction_force - repulsions force
|
|
1581
|
+
# suppress nans due to division; caused by diagonal set to zero.
|
|
1582
|
+
# Does not affect the computation due to nansum
|
|
1583
|
+
with warnings.catch_warnings():
|
|
1584
|
+
warnings.simplefilter("ignore")
|
|
1585
|
+
change = K[..., np.newaxis] * diff - rho / A * diff
|
|
1586
|
+
change = np.nansum(change, axis=0)
|
|
1587
|
+
p += change * dt
|
|
1588
|
+
|
|
1589
|
+
error = np.linalg.norm(change, axis=-1).sum()
|
|
1590
|
+
if n_iter > max_iter:
|
|
1591
|
+
break
|
|
1592
|
+
n_iter += 1
|
|
1593
|
+
|
|
1594
|
+
pos = dict(zip(G.nodes(), p))
|
|
1595
|
+
|
|
1596
|
+
if store_pos_as is not None:
|
|
1597
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1598
|
+
|
|
1599
|
+
return pos
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
@np_random_state("seed")
|
|
1603
|
+
@nx._dispatchable(edge_attrs="weight", mutates_input={"store_pos_as": 15})
|
|
1604
|
+
def forceatlas2_layout(
|
|
1605
|
+
G,
|
|
1606
|
+
pos=None,
|
|
1607
|
+
*,
|
|
1608
|
+
max_iter=100,
|
|
1609
|
+
jitter_tolerance=1.0,
|
|
1610
|
+
scaling_ratio=2.0,
|
|
1611
|
+
gravity=1.0,
|
|
1612
|
+
distributed_action=False,
|
|
1613
|
+
strong_gravity=False,
|
|
1614
|
+
node_mass=None,
|
|
1615
|
+
node_size=None,
|
|
1616
|
+
weight=None,
|
|
1617
|
+
linlog=False,
|
|
1618
|
+
seed=None,
|
|
1619
|
+
dim=2,
|
|
1620
|
+
store_pos_as=None,
|
|
1621
|
+
):
|
|
1622
|
+
"""Position nodes using the ForceAtlas2 force-directed layout algorithm.
|
|
1623
|
+
|
|
1624
|
+
This function applies the ForceAtlas2 layout algorithm [1]_ to a NetworkX graph,
|
|
1625
|
+
positioning the nodes in a way that visually represents the structure of the graph.
|
|
1626
|
+
The algorithm uses physical simulation to minimize the energy of the system,
|
|
1627
|
+
resulting in a more readable layout.
|
|
1628
|
+
|
|
1629
|
+
Parameters
|
|
1630
|
+
----------
|
|
1631
|
+
G : nx.Graph
|
|
1632
|
+
A NetworkX graph to be laid out.
|
|
1633
|
+
pos : dict or None, optional
|
|
1634
|
+
Initial positions of the nodes. If None, random initial positions are used.
|
|
1635
|
+
max_iter : int (default: 100)
|
|
1636
|
+
Number of iterations for the layout optimization.
|
|
1637
|
+
jitter_tolerance : float (default: 1.0)
|
|
1638
|
+
Controls the tolerance for adjusting the speed of layout generation.
|
|
1639
|
+
scaling_ratio : float (default: 2.0)
|
|
1640
|
+
Determines the scaling of attraction and repulsion forces.
|
|
1641
|
+
gravity : float (default: 1.0)
|
|
1642
|
+
Determines the amount of attraction on nodes to the center. Prevents islands
|
|
1643
|
+
(i.e. weakly connected or disconnected parts of the graph)
|
|
1644
|
+
from drifting away.
|
|
1645
|
+
distributed_action : bool (default: False)
|
|
1646
|
+
Distributes the attraction force evenly among nodes.
|
|
1647
|
+
strong_gravity : bool (default: False)
|
|
1648
|
+
Applies a strong gravitational pull towards the center.
|
|
1649
|
+
node_mass : dict or None, optional
|
|
1650
|
+
Maps nodes to their masses, influencing the attraction to other nodes.
|
|
1651
|
+
node_size : dict or None, optional
|
|
1652
|
+
Maps nodes to their sizes, preventing crowding by creating a halo effect.
|
|
1653
|
+
weight : string or None, optional (default: None)
|
|
1654
|
+
The edge attribute that holds the numerical value used for
|
|
1655
|
+
the edge weight. If None, then all edge weights are 1.
|
|
1656
|
+
linlog : bool (default: False)
|
|
1657
|
+
Uses logarithmic attraction instead of linear.
|
|
1658
|
+
seed : int, RandomState instance or None optional (default=None)
|
|
1659
|
+
Used only for the initial positions in the algorithm.
|
|
1660
|
+
Set the random state for deterministic node layouts.
|
|
1661
|
+
If int, `seed` is the seed used by the random number generator,
|
|
1662
|
+
if numpy.random.RandomState instance, `seed` is the random
|
|
1663
|
+
number generator,
|
|
1664
|
+
if None, the random number generator is the RandomState instance used
|
|
1665
|
+
by numpy.random.
|
|
1666
|
+
dim : int (default: 2)
|
|
1667
|
+
Sets the dimensions for the layout. Ignored if `pos` is provided.
|
|
1668
|
+
store_pos_as : str, default None
|
|
1669
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1670
|
+
an attribute with this string as its name, which can be accessed with
|
|
1671
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1672
|
+
|
|
1673
|
+
Examples
|
|
1674
|
+
--------
|
|
1675
|
+
>>> import networkx as nx
|
|
1676
|
+
>>> G = nx.florentine_families_graph()
|
|
1677
|
+
>>> pos = nx.forceatlas2_layout(G)
|
|
1678
|
+
>>> nx.draw(G, pos=pos)
|
|
1679
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
1680
|
+
>>> pos = nx.forceatlas2_layout(G, store_pos_as="pos")
|
|
1681
|
+
>>> _ = nx.forceatlas2_layout(G, store_pos_as="pos")
|
|
1682
|
+
|
|
1683
|
+
References
|
|
1684
|
+
----------
|
|
1685
|
+
.. [1] Jacomy, M., Venturini, T., Heymann, S., & Bastian, M. (2014).
|
|
1686
|
+
ForceAtlas2, a continuous graph layout algorithm for handy network
|
|
1687
|
+
visualization designed for the Gephi software. PloS one, 9(6), e98679.
|
|
1688
|
+
https://doi.org/10.1371/journal.pone.0098679
|
|
1689
|
+
"""
|
|
1690
|
+
import numpy as np
|
|
1691
|
+
|
|
1692
|
+
if len(G) == 0:
|
|
1693
|
+
return {}
|
|
1694
|
+
# parse optional pos positions
|
|
1695
|
+
if pos is None:
|
|
1696
|
+
pos = nx.random_layout(G, dim=dim, seed=seed)
|
|
1697
|
+
pos_arr = np.array(list(pos.values()))
|
|
1698
|
+
elif len(pos) == len(G):
|
|
1699
|
+
pos_arr = np.array([pos[node].copy() for node in G])
|
|
1700
|
+
else:
|
|
1701
|
+
# set random node pos within the initial pos values
|
|
1702
|
+
pos_init = np.array(list(pos.values()))
|
|
1703
|
+
max_pos = pos_init.max(axis=0)
|
|
1704
|
+
min_pos = pos_init.min(axis=0)
|
|
1705
|
+
dim = max_pos.size
|
|
1706
|
+
pos_arr = min_pos + seed.rand(len(G), dim) * (max_pos - min_pos)
|
|
1707
|
+
for idx, node in enumerate(G):
|
|
1708
|
+
if node in pos:
|
|
1709
|
+
pos_arr[idx] = pos[node].copy()
|
|
1710
|
+
|
|
1711
|
+
mass = np.zeros(len(G))
|
|
1712
|
+
size = np.zeros(len(G))
|
|
1713
|
+
|
|
1714
|
+
# Only adjust for size when the users specifies size other than default (1)
|
|
1715
|
+
adjust_sizes = False
|
|
1716
|
+
if node_size is None:
|
|
1717
|
+
node_size = {}
|
|
1718
|
+
else:
|
|
1719
|
+
adjust_sizes = True
|
|
1720
|
+
|
|
1721
|
+
if node_mass is None:
|
|
1722
|
+
node_mass = {}
|
|
1723
|
+
|
|
1724
|
+
for idx, node in enumerate(G):
|
|
1725
|
+
mass[idx] = node_mass.get(node, G.degree(node) + 1)
|
|
1726
|
+
size[idx] = node_size.get(node, 1)
|
|
1727
|
+
|
|
1728
|
+
n = len(G)
|
|
1729
|
+
gravities = np.zeros((n, dim))
|
|
1730
|
+
attraction = np.zeros((n, dim))
|
|
1731
|
+
repulsion = np.zeros((n, dim))
|
|
1732
|
+
A = nx.to_numpy_array(G, weight=weight)
|
|
1733
|
+
|
|
1734
|
+
def estimate_factor(n, swing, traction, speed, speed_efficiency, jitter_tolerance):
|
|
1735
|
+
"""Computes the scaling factor for the force in the ForceAtlas2 layout algorithm.
|
|
1736
|
+
|
|
1737
|
+
This helper function adjusts the speed and
|
|
1738
|
+
efficiency of the layout generation based on the
|
|
1739
|
+
current state of the system, such as the number of
|
|
1740
|
+
nodes, current swing, and traction forces.
|
|
1741
|
+
|
|
1742
|
+
Parameters
|
|
1743
|
+
----------
|
|
1744
|
+
n : int
|
|
1745
|
+
Number of nodes in the graph.
|
|
1746
|
+
swing : float
|
|
1747
|
+
The current swing, representing the oscillation of the nodes.
|
|
1748
|
+
traction : float
|
|
1749
|
+
The current traction force, representing the attraction between nodes.
|
|
1750
|
+
speed : float
|
|
1751
|
+
The current speed of the layout generation.
|
|
1752
|
+
speed_efficiency : float
|
|
1753
|
+
The efficiency of the current speed, influencing how fast the layout converges.
|
|
1754
|
+
jitter_tolerance : float
|
|
1755
|
+
The tolerance for jitter, affecting how much speed adjustment is allowed.
|
|
1756
|
+
|
|
1757
|
+
Returns
|
|
1758
|
+
-------
|
|
1759
|
+
tuple
|
|
1760
|
+
A tuple containing the updated speed and speed efficiency.
|
|
1761
|
+
|
|
1762
|
+
Notes
|
|
1763
|
+
-----
|
|
1764
|
+
This function is a part of the ForceAtlas2 layout algorithm and is used to dynamically adjust the
|
|
1765
|
+
layout parameters to achieve an optimal and stable visualization.
|
|
1766
|
+
|
|
1767
|
+
"""
|
|
1768
|
+
import numpy as np
|
|
1769
|
+
|
|
1770
|
+
# estimate jitter
|
|
1771
|
+
opt_jitter = 0.05 * np.sqrt(n)
|
|
1772
|
+
min_jitter = np.sqrt(opt_jitter)
|
|
1773
|
+
max_jitter = 10
|
|
1774
|
+
min_speed_efficiency = 0.05
|
|
1775
|
+
|
|
1776
|
+
other = min(max_jitter, opt_jitter * traction / n**2)
|
|
1777
|
+
jitter = jitter_tolerance * max(min_jitter, other)
|
|
1778
|
+
|
|
1779
|
+
if swing / traction > 2.0:
|
|
1780
|
+
if speed_efficiency > min_speed_efficiency:
|
|
1781
|
+
speed_efficiency *= 0.5
|
|
1782
|
+
jitter = max(jitter, jitter_tolerance)
|
|
1783
|
+
if swing == 0:
|
|
1784
|
+
target_speed = np.inf
|
|
1785
|
+
else:
|
|
1786
|
+
target_speed = jitter * speed_efficiency * traction / swing
|
|
1787
|
+
|
|
1788
|
+
if swing > jitter * traction:
|
|
1789
|
+
if speed_efficiency > min_speed_efficiency:
|
|
1790
|
+
speed_efficiency *= 0.7
|
|
1791
|
+
elif speed < 1000:
|
|
1792
|
+
speed_efficiency *= 1.3
|
|
1793
|
+
|
|
1794
|
+
max_rise = 0.5
|
|
1795
|
+
speed = speed + min(target_speed - speed, max_rise * speed)
|
|
1796
|
+
return speed, speed_efficiency
|
|
1797
|
+
|
|
1798
|
+
speed = 1
|
|
1799
|
+
speed_efficiency = 1
|
|
1800
|
+
swing = 1
|
|
1801
|
+
traction = 1
|
|
1802
|
+
for _ in range(max_iter):
|
|
1803
|
+
# compute pairwise difference
|
|
1804
|
+
diff = pos_arr[:, None] - pos_arr[None]
|
|
1805
|
+
# compute pairwise distance
|
|
1806
|
+
distance = np.linalg.norm(diff, axis=-1)
|
|
1807
|
+
|
|
1808
|
+
# linear attraction
|
|
1809
|
+
if linlog:
|
|
1810
|
+
attraction = -np.log(1 + distance) / distance
|
|
1811
|
+
np.fill_diagonal(attraction, 0)
|
|
1812
|
+
attraction = np.einsum("ij, ij -> ij", attraction, A)
|
|
1813
|
+
attraction = np.einsum("ijk, ij -> ik", diff, attraction)
|
|
1814
|
+
|
|
1815
|
+
else:
|
|
1816
|
+
attraction = -np.einsum("ijk, ij -> ik", diff, A)
|
|
1817
|
+
|
|
1818
|
+
if distributed_action:
|
|
1819
|
+
attraction /= mass[:, None]
|
|
1820
|
+
|
|
1821
|
+
# repulsion
|
|
1822
|
+
tmp = mass[:, None] @ mass[None]
|
|
1823
|
+
if adjust_sizes:
|
|
1824
|
+
distance += -size[:, None] - size[None]
|
|
1825
|
+
|
|
1826
|
+
d2 = distance**2
|
|
1827
|
+
# remove self-interaction
|
|
1828
|
+
np.fill_diagonal(tmp, 0)
|
|
1829
|
+
np.fill_diagonal(d2, 1)
|
|
1830
|
+
factor = (tmp / d2) * scaling_ratio
|
|
1831
|
+
repulsion = np.einsum("ijk, ij -> ik", diff, factor)
|
|
1832
|
+
|
|
1833
|
+
# gravity
|
|
1834
|
+
pos_centered = pos_arr - np.mean(pos_arr, axis=0)
|
|
1835
|
+
if strong_gravity:
|
|
1836
|
+
gravities = -gravity * mass[:, None] * pos_centered
|
|
1837
|
+
else:
|
|
1838
|
+
# hide warnings for divide by zero. Then change nan to 0
|
|
1839
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
1840
|
+
unit_vec = pos_centered / np.linalg.norm(pos_centered, axis=-1)[:, None]
|
|
1841
|
+
unit_vec = np.nan_to_num(unit_vec, nan=0)
|
|
1842
|
+
gravities = -gravity * mass[:, None] * unit_vec
|
|
1843
|
+
|
|
1844
|
+
# total forces
|
|
1845
|
+
update = attraction + repulsion + gravities
|
|
1846
|
+
|
|
1847
|
+
# compute total swing and traction
|
|
1848
|
+
swing += (mass * np.linalg.norm(pos_arr - update, axis=-1)).sum()
|
|
1849
|
+
traction += (0.5 * mass * np.linalg.norm(pos_arr + update, axis=-1)).sum()
|
|
1850
|
+
|
|
1851
|
+
speed, speed_efficiency = estimate_factor(
|
|
1852
|
+
n,
|
|
1853
|
+
swing,
|
|
1854
|
+
traction,
|
|
1855
|
+
speed,
|
|
1856
|
+
speed_efficiency,
|
|
1857
|
+
jitter_tolerance,
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
# update pos
|
|
1861
|
+
if adjust_sizes:
|
|
1862
|
+
df = np.linalg.norm(update, axis=-1)
|
|
1863
|
+
swinging = mass * df
|
|
1864
|
+
factor = 0.1 * speed / (1 + np.sqrt(speed * swinging))
|
|
1865
|
+
factor = np.minimum(factor * df, 10.0 * np.ones(df.shape)) / df
|
|
1866
|
+
else:
|
|
1867
|
+
swinging = mass * np.linalg.norm(update, axis=-1)
|
|
1868
|
+
factor = speed / (1 + np.sqrt(speed * swinging))
|
|
1869
|
+
|
|
1870
|
+
factored_update = update * factor[:, None]
|
|
1871
|
+
pos_arr += factored_update
|
|
1872
|
+
if abs(factored_update).sum() < 1e-10:
|
|
1873
|
+
break
|
|
1874
|
+
|
|
1875
|
+
pos = dict(zip(G, pos_arr))
|
|
1876
|
+
if store_pos_as is not None:
|
|
1877
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
1878
|
+
|
|
1879
|
+
return pos
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def rescale_layout(pos, scale=1):
|
|
1883
|
+
"""Returns scaled position array to (-scale, scale) in all axes.
|
|
1884
|
+
|
|
1885
|
+
The function acts on NumPy arrays which hold position information.
|
|
1886
|
+
Each position is one row of the array. The dimension of the space
|
|
1887
|
+
equals the number of columns. Each coordinate in one column.
|
|
1888
|
+
|
|
1889
|
+
To rescale, the mean (center) is subtracted from each axis separately.
|
|
1890
|
+
Then all values are scaled so that the largest magnitude value
|
|
1891
|
+
from all axes equals `scale` (thus, the aspect ratio is preserved).
|
|
1892
|
+
The resulting NumPy Array is returned (order of rows unchanged).
|
|
1893
|
+
|
|
1894
|
+
Parameters
|
|
1895
|
+
----------
|
|
1896
|
+
pos : numpy array
|
|
1897
|
+
positions to be scaled. Each row is a position.
|
|
1898
|
+
|
|
1899
|
+
scale : number (default: 1)
|
|
1900
|
+
The size of the resulting extent in all directions.
|
|
1901
|
+
|
|
1902
|
+
attribute : str, default None
|
|
1903
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1904
|
+
an attribute named `attribute` which can be accessed with
|
|
1905
|
+
`G.nodes[...][attribute]`. The function still returns the dictionary.
|
|
1906
|
+
|
|
1907
|
+
Returns
|
|
1908
|
+
-------
|
|
1909
|
+
pos : numpy array
|
|
1910
|
+
scaled positions. Each row is a position.
|
|
1911
|
+
|
|
1912
|
+
See Also
|
|
1913
|
+
--------
|
|
1914
|
+
rescale_layout_dict
|
|
1915
|
+
"""
|
|
1916
|
+
import numpy as np
|
|
1917
|
+
|
|
1918
|
+
# Find max length over all dimensions
|
|
1919
|
+
pos -= pos.mean(axis=0)
|
|
1920
|
+
lim = np.abs(pos).max() # max coordinate for all axes
|
|
1921
|
+
# rescale to (-scale, scale) in all directions, preserves aspect
|
|
1922
|
+
if lim > 0:
|
|
1923
|
+
pos *= scale / lim
|
|
1924
|
+
return pos
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
def rescale_layout_dict(pos, scale=1):
|
|
1928
|
+
"""Return a dictionary of scaled positions keyed by node
|
|
1929
|
+
|
|
1930
|
+
Parameters
|
|
1931
|
+
----------
|
|
1932
|
+
pos : A dictionary of positions keyed by node
|
|
1933
|
+
|
|
1934
|
+
scale : number (default: 1)
|
|
1935
|
+
The size of the resulting extent in all directions.
|
|
1936
|
+
|
|
1937
|
+
Returns
|
|
1938
|
+
-------
|
|
1939
|
+
pos : A dictionary of positions keyed by node
|
|
1940
|
+
|
|
1941
|
+
Examples
|
|
1942
|
+
--------
|
|
1943
|
+
>>> import numpy as np
|
|
1944
|
+
>>> pos = {0: np.array((0, 0)), 1: np.array((1, 1)), 2: np.array((0.5, 0.5))}
|
|
1945
|
+
>>> nx.rescale_layout_dict(pos)
|
|
1946
|
+
{0: array([-1., -1.]), 1: array([1., 1.]), 2: array([0., 0.])}
|
|
1947
|
+
|
|
1948
|
+
>>> pos = {0: np.array((0, 0)), 1: np.array((-1, 1)), 2: np.array((-0.5, 0.5))}
|
|
1949
|
+
>>> nx.rescale_layout_dict(pos, scale=2)
|
|
1950
|
+
{0: array([ 2., -2.]), 1: array([-2., 2.]), 2: array([0., 0.])}
|
|
1951
|
+
|
|
1952
|
+
See Also
|
|
1953
|
+
--------
|
|
1954
|
+
rescale_layout
|
|
1955
|
+
"""
|
|
1956
|
+
import numpy as np
|
|
1957
|
+
|
|
1958
|
+
if not pos: # empty_graph
|
|
1959
|
+
return {}
|
|
1960
|
+
pos_v = np.array(list(pos.values()))
|
|
1961
|
+
pos_v = rescale_layout(pos_v, scale=scale)
|
|
1962
|
+
return dict(zip(pos, pos_v))
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def bfs_layout(G, start, *, align="vertical", scale=1, center=None, store_pos_as=None):
|
|
1966
|
+
"""Position nodes according to breadth-first search algorithm.
|
|
1967
|
+
|
|
1968
|
+
Parameters
|
|
1969
|
+
----------
|
|
1970
|
+
G : NetworkX graph
|
|
1971
|
+
A position will be assigned to every node in G.
|
|
1972
|
+
|
|
1973
|
+
start : node in `G`
|
|
1974
|
+
Starting node for bfs
|
|
1975
|
+
|
|
1976
|
+
align : string (default='vertical')
|
|
1977
|
+
The alignment of nodes within a layer, either `"vertical"` or
|
|
1978
|
+
`"horizontal"`.
|
|
1979
|
+
|
|
1980
|
+
scale : number (default: 1)
|
|
1981
|
+
Scale factor for positions.
|
|
1982
|
+
|
|
1983
|
+
center : array-like or None
|
|
1984
|
+
Coordinate pair around which to center the layout.
|
|
1985
|
+
|
|
1986
|
+
store_pos_as : str, default None
|
|
1987
|
+
If non-None, the position of each node will be stored on the graph as
|
|
1988
|
+
an attribute with this string as its name, which can be accessed with
|
|
1989
|
+
``G.nodes[...][store_pos_as]``. The function still returns the dictionary.
|
|
1990
|
+
|
|
1991
|
+
Returns
|
|
1992
|
+
-------
|
|
1993
|
+
pos : dict
|
|
1994
|
+
A dictionary of positions keyed by node.
|
|
1995
|
+
|
|
1996
|
+
Examples
|
|
1997
|
+
--------
|
|
1998
|
+
>>> from pprint import pprint
|
|
1999
|
+
>>> G = nx.path_graph(4)
|
|
2000
|
+
>>> pos = nx.bfs_layout(G, 0)
|
|
2001
|
+
>>> # suppress the returned dict and store on the graph directly
|
|
2002
|
+
>>> _ = nx.bfs_layout(G, 0, store_pos_as="pos")
|
|
2003
|
+
>>> pprint(nx.get_node_attributes(G, "pos"))
|
|
2004
|
+
{0: array([-1., 0.]),
|
|
2005
|
+
1: array([-0.33333333, 0. ]),
|
|
2006
|
+
2: array([0.33333333, 0. ]),
|
|
2007
|
+
3: array([1., 0.])}
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
Notes
|
|
2012
|
+
-----
|
|
2013
|
+
This algorithm currently only works in two dimensions and does not
|
|
2014
|
+
try to minimize edge crossings.
|
|
2015
|
+
|
|
2016
|
+
"""
|
|
2017
|
+
G, center = _process_params(G, center, 2)
|
|
2018
|
+
|
|
2019
|
+
# Compute layers with BFS
|
|
2020
|
+
layers = dict(enumerate(nx.bfs_layers(G, start)))
|
|
2021
|
+
|
|
2022
|
+
if len(G) != sum(len(nodes) for nodes in layers.values()):
|
|
2023
|
+
raise nx.NetworkXError(
|
|
2024
|
+
"bfs_layout didn't include all nodes. Perhaps use input graph:\n"
|
|
2025
|
+
" G.subgraph(nx.node_connected_component(G, start))"
|
|
2026
|
+
)
|
|
2027
|
+
|
|
2028
|
+
# Compute node positions with multipartite_layout
|
|
2029
|
+
pos = multipartite_layout(
|
|
2030
|
+
G, subset_key=layers, align=align, scale=scale, center=center
|
|
2031
|
+
)
|
|
2032
|
+
|
|
2033
|
+
if store_pos_as is not None:
|
|
2034
|
+
nx.set_node_attributes(G, pos, store_pos_as)
|
|
2035
|
+
|
|
2036
|
+
return pos
|