├── .nvmrc ├── .prettierrc.json ├── crates ├── python │ ├── tmp │ │ └── .gitkeep │ ├── runtime.txt │ ├── docs │ │ ├── runtime.txt │ │ ├── source │ │ │ ├── _static │ │ │ │ └── .gitkeep │ │ │ ├── api │ │ │ │ ├── graph │ │ │ │ │ ├── graph.rst │ │ │ │ │ ├── digraph.rst │ │ │ │ │ └── graph_adapter.rst │ │ │ │ ├── layout │ │ │ │ │ ├── full_sgd.rst │ │ │ │ │ ├── pivot_mds.rst │ │ │ │ │ ├── sparse_sgd.rst │ │ │ │ │ ├── classical_mds.rst │ │ │ │ │ ├── kamada_kawai.rst │ │ │ │ │ ├── overwrap_removal.rst │ │ │ │ │ ├── stress_majorization.rst │ │ │ │ │ └── schedulers.rst │ │ │ │ ├── drawing │ │ │ │ │ ├── drawing_torus_2d.rst │ │ │ │ │ ├── drawing_euclidean.rst │ │ │ │ │ ├── drawing_euclidean_2d.rst │ │ │ │ │ ├── drawing_spherical_2d.rst │ │ │ │ │ └── drawing_hyperbolic_2d.rst │ │ │ │ ├── graph.rst │ │ │ │ ├── rng.rst │ │ │ │ ├── distance_matrix.rst │ │ │ │ ├── index.rst │ │ │ │ ├── algorithm.rst │ │ │ │ ├── drawing.rst │ │ │ │ ├── layout.rst │ │ │ │ └── quality_metrics.rst │ │ │ ├── getting_started │ │ │ │ ├── index.rst │ │ │ │ └── installation.rst │ │ │ ├── index.rst │ │ │ ├── tutorial │ │ │ │ └── index.rst │ │ │ └── examples │ │ │ │ ├── sgd.rst │ │ │ │ ├── index.rst │ │ │ │ ├── kamada_kawai.rst │ │ │ │ └── sgd_hyperbolic_2d.rst │ │ ├── requirements.txt │ │ ├── netlify.toml │ │ ├── Makefile │ │ └── make.bat │ ├── requirements.txt │ ├── netlify.toml │ ├── examples │ │ ├── kamada_kawai.py │ │ ├── sgd_hyperbolic_2d.py │ │ ├── sgd_spherical_2d.py │ │ ├── sgd.py │ │ ├── sgd_3d.py │ │ ├── stress_majorization.py │ │ ├── nonconnected_sgd.py │ │ ├── overwrap_removal.py │ │ └── sgd_torus.py │ ├── src │ │ ├── clustering │ │ │ ├── mod.rs │ │ │ ├── infomap.rs │ │ │ ├── spectral.rs │ │ │ ├── label_propagation.rs │ │ │ └── louvain.rs │ │ ├── algorithm │ │ │ ├── mod.rs │ │ │ ├── layering │ │ │ │ ├── mod.rs │ │ │ │ └── longest_path.rs │ │ │ └── triangulation.rs │ │ ├── layout │ │ │ ├── sgd │ │ │ │ ├── full.rs │ │ │ │ └── mod.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ └── drawing │ │ │ └── mod.rs │ ├── .gitignore │ ├── Cargo.toml │ ├── .github │ │ └── workflows │ │ │ └── CI.yml │ └── tests │ │ └── test_shortest_path.py ├── layout │ ├── random │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── separation-constraints │ │ ├── src │ │ │ ├── constraints.rs │ │ │ └── constraint_graph │ │ │ │ ├── variable.rs │ │ │ │ ├── block.rs │ │ │ │ └── constraint.rs │ │ ├── Cargo.toml │ │ └── tests │ │ │ └── rectangle_overlap.rs │ ├── overwrap-removal │ │ └── Cargo.toml │ ├── sgd │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── scheduler │ │ │ ├── scheduler_constant.rs │ │ │ └── scheduler_linear.rs │ ├── kamada-kawai │ │ └── Cargo.toml │ ├── omega │ │ └── Cargo.toml │ ├── mds │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── double_centering.rs │ ├── stress-majorization │ │ └── Cargo.toml │ └── kernel-sgd │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── wasm │ ├── build.js │ ├── src │ │ ├── layout.rs │ │ ├── graph │ │ │ ├── mod.rs │ │ │ └── types.rs │ │ ├── algorithm.rs │ │ ├── drawing.rs │ │ ├── lib.rs │ │ ├── layout │ │ │ └── sgd │ │ │ │ ├── full.rs │ │ │ │ └── sparse.rs │ │ ├── edge_bundling.rs │ │ ├── rng.rs │ │ └── drawing │ │ │ └── drawing_euclidean.rs │ ├── rollup.config.js │ ├── tests │ │ ├── sgd_full.js │ │ ├── sgd_full.rs │ │ ├── sgd_sparse.rs │ │ ├── sgd_sparse.js │ │ ├── util │ │ │ └── mod.rs │ │ ├── edge_bundling.rs │ │ ├── rng.rs │ │ ├── clustering.rs │ │ ├── graph.rs │ │ ├── drawing_euclidean_2d.rs │ │ ├── quality_metrics.rs │ │ ├── digraph.rs │ │ ├── stress_majorization.rs │ │ ├── kamada_kawai.rs │ │ ├── drawing_torus_2d.rs │ │ ├── drawing_hyperbolic_2d.rs │ │ ├── drawing_spherical_2d.rs │ │ ├── classical_mds.rs │ │ ├── sgd.rs │ │ └── node.rs │ ├── LICENSE │ ├── package.json │ └── Cargo.toml ├── drawing │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── metric.rs ├── algorithm │ ├── connected-components │ │ └── Cargo.toml │ ├── layering │ │ ├── Cargo.toml │ │ └── src │ │ │ └── algorithms │ │ │ └── mod.rs │ ├── shortest-path │ │ ├── src │ │ │ ├── lib.rs │ │ │ └── warshall_floyd.rs │ │ ├── Cargo.toml │ │ ├── benches │ │ │ └── apsp.rs │ │ └── tests │ │ │ └── apsp.rs │ └── triangulation │ │ └── Cargo.toml ├── clustering │ ├── src │ │ └── algorithms │ │ │ └── mod.rs │ └── Cargo.toml ├── edge-bundling │ └── fdeb │ │ └── Cargo.toml ├── linalg │ ├── rdmds │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── spmv │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── dataset │ ├── Cargo.toml │ └── src │ │ └── data │ │ └── karate.csv ├── quality-metrics │ ├── Cargo.toml │ └── src │ │ ├── edge_angle.rs │ │ ├── node_resolution.rs │ │ ├── gabriel_graph_property.rs │ │ ├── stress.rs │ │ ├── angular_resolution.rs │ │ ├── ideal_edge_lengths.rs │ │ └── aspect_ratio.rs └── cli │ └── Cargo.toml ├── js ├── examples │ ├── .eslintrc.json │ ├── public │ │ └── _redirects │ ├── src │ │ ├── styles.css │ │ ├── index.js │ │ ├── pages │ │ │ ├── index.js │ │ │ ├── Home.jsx │ │ │ ├── ExampleMds.jsx │ │ │ ├── ExampleKamadaKawai.jsx │ │ │ ├── ExampleStressMajorization.jsx │ │ │ └── ExampleEdgeBundling.jsx │ │ ├── wrapper.jsx │ │ └── App.jsx │ ├── vite.config.js │ ├── index.html │ └── package.json └── dataset │ ├── package.json │ ├── diamond.json │ ├── bull.json │ ├── house.json │ ├── house_x.json │ ├── sedgewick_maze.json │ ├── octahedral.json │ ├── cubical.json │ ├── petersen.json │ ├── krackhardt_kite.json │ ├── frucht.json │ ├── heawood.json │ ├── chvatal.json │ ├── moebius_kantor.json │ ├── pappus.json │ ├── icosahedral.json │ ├── florentine_families.json │ ├── desargues.json │ └── dodecahedral.json ├── .gitignore ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── maturin.yml ├── package.json └── memory-bank ├── projectbrief.md └── productContext.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /crates/python/tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/layout/random/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/python/runtime.txt: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /crates/python/docs/runtime.txt: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /crates/python/docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /js/examples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app"] 3 | } 4 | -------------------------------------------------------------------------------- /crates/wasm/build.js: -------------------------------------------------------------------------------- 1 | export { default as init } from "./dist/web/egraph_wasm"; 2 | export * from "./dist/web/egraph_wasm"; 3 | -------------------------------------------------------------------------------- /js/examples/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | http://egraph.netlify.com/* http://egraph.likr-lab.com/:splat 301! 3 | -------------------------------------------------------------------------------- /crates/wasm/src/layout.rs: -------------------------------------------------------------------------------- 1 | pub mod kamada_kawai; 2 | pub mod mds; 3 | pub mod overwrap_removal; 4 | pub mod sgd; 5 | pub mod stress_majorization; 6 | -------------------------------------------------------------------------------- /crates/python/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=4.0.0 2 | sphinx_rtd_theme>=1.0.0 3 | networkx>=2.6.0 4 | numpy>=1.20.0 5 | matplotlib>=3.4.0 6 | maturin>=1.0,<2.0 7 | -------------------------------------------------------------------------------- /crates/python/docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=4.0.0 2 | sphinx_rtd_theme>=1.0.0 3 | networkx>=2.6.0 4 | numpy>=1.20.0 5 | matplotlib>=3.4.0 6 | maturin>=1.0,<2.0 7 | -------------------------------------------------------------------------------- /crates/layout/separation-constraints/src/constraints.rs: -------------------------------------------------------------------------------- 1 | pub mod cluster_overlap; 2 | pub mod layered; 3 | pub mod rectangle_overlap; 4 | pub mod rectangle_overlap_2d; 5 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/graph/graph.rst: -------------------------------------------------------------------------------- 1 | Graph 2 | ============= 3 | 4 | .. autoclass:: egraph.Graph 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /js/examples/src/styles.css: -------------------------------------------------------------------------------- 1 | eg-renderer, 2 | svg { 3 | position: absolute; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | display: block; 9 | } 10 | -------------------------------------------------------------------------------- /crates/python/docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "crates/python/docs" 3 | publish = "_build/html" 4 | command = "rustup toolchain install stable && pip install .. && make html" 5 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/graph/digraph.rst: -------------------------------------------------------------------------------- 1 | DiGraph 2 | =============== 3 | 4 | .. autoclass:: egraph.DiGraph 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/wasm/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: "build.js", 3 | output: { 4 | file: "umd/egraph.js", 5 | format: "umd", 6 | name: "egraph", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/full_sgd.rst: -------------------------------------------------------------------------------- 1 | FullSgd 2 | ================== 3 | 4 | .. autoclass:: egraph.FullSgd 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/pivot_mds.rst: -------------------------------------------------------------------------------- 1 | PivotMds 2 | =================== 3 | 4 | .. autoclass:: egraph.PivotMds 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "crates/python" 3 | publish = "docs/_build/html" 4 | command = "rustup toolchain install stable && pip install . && make -C docs html" 5 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/sparse_sgd.rst: -------------------------------------------------------------------------------- 1 | SparseSgd 2 | ==================== 3 | 4 | .. autoclass:: egraph.SparseSgd 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/graph/graph_adapter.rst: -------------------------------------------------------------------------------- 1 | GraphAdapter 2 | ============== 3 | 4 | .. autoclass:: egraph.GraphAdapter 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/classical_mds.rst: -------------------------------------------------------------------------------- 1 | ClassicalMds 2 | ====================== 3 | 4 | .. autoclass:: egraph.ClassicalMds 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/kamada_kawai.rst: -------------------------------------------------------------------------------- 1 | KamadaKawai 2 | ======================= 3 | 4 | .. autoclass:: egraph.KamadaKawai 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/drawing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-drawing" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ndarray = "0.16" 8 | num-traits = "0.2" 9 | petgraph = "0.6" 10 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/drawing/drawing_torus_2d.rst: -------------------------------------------------------------------------------- 1 | DrawingTorus2d 2 | ========================== 3 | 4 | .. autoclass:: egraph.DrawingTorus2d 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/overwrap_removal.rst: -------------------------------------------------------------------------------- 1 | OverwrapRemoval 2 | =========================== 3 | 4 | .. autoclass:: egraph.OverwrapRemoval 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/drawing/drawing_euclidean.rst: -------------------------------------------------------------------------------- 1 | DrawingEuclidean 2 | =========================== 3 | 4 | .. autoclass:: egraph.DrawingEuclidean 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/stress_majorization.rst: -------------------------------------------------------------------------------- 1 | StressMajorization 2 | ============================= 3 | 4 | .. autoclass:: egraph.StressMajorization 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/drawing/drawing_euclidean_2d.rst: -------------------------------------------------------------------------------- 1 | DrawingEuclidean2d 2 | =============================== 3 | 4 | .. autoclass:: egraph.DrawingEuclidean2d 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/drawing/drawing_spherical_2d.rst: -------------------------------------------------------------------------------- 1 | DrawingSpherical2d 2 | =============================== 3 | 4 | .. autoclass:: egraph.DrawingSpherical2d 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | node_modules 4 | egraph-c-examples/example 5 | egraph-cpp-examples/example 6 | crates/wasm/dist 7 | crates/wasm/umd 8 | wasm-pack.log 9 | 10 | js/examples/dist 11 | 12 | .venv 13 | 14 | tmp/* -------------------------------------------------------------------------------- /crates/layout/overwrap-removal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-overwrap-removal" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | petgraph = "0.6" 8 | petgraph-drawing = { path = "../../drawing" } 9 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/drawing/drawing_hyperbolic_2d.rst: -------------------------------------------------------------------------------- 1 | DrawingHyperbolic2d 2 | ================================ 3 | 4 | .. autoclass:: egraph.DrawingHyperbolic2d 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /js/examples/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | build: { 6 | target: "esnext", 7 | }, 8 | plugins: [react()], 9 | }); 10 | -------------------------------------------------------------------------------- /js/dataset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egraph-dataset", 3 | "version": "5.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "author": "Yosuke Onoue", 7 | "license": "MIT", 8 | "files": [ 9 | "*.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /crates/algorithm/connected-components/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-algorithm-connected-components" 3 | version = "0.1.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | petgraph = "0.6" 9 | -------------------------------------------------------------------------------- /crates/algorithm/layering/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-algorithm-layering" 3 | version = "0.1.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | petgraph = "0.6" 9 | fixedbitset = "0.4" 10 | -------------------------------------------------------------------------------- /crates/clustering/src/algorithms/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod infomap; 2 | pub mod label_propagation; 3 | pub mod louvain; 4 | pub mod spectral; 5 | 6 | // Re-export all algorithms 7 | pub use infomap::*; 8 | pub use label_propagation::*; 9 | pub use louvain::*; 10 | pub use spectral::*; 11 | -------------------------------------------------------------------------------- /crates/edge-bundling/fdeb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-edge-bundling-fdeb" 3 | version = "0.1.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | petgraph = "0.6" 9 | petgraph-drawing = { path = "../../drawing" } 10 | -------------------------------------------------------------------------------- /crates/linalg/rdmds/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-linalg-rdmds" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ndarray = "0.16" 8 | num-traits = "0.2" 9 | petgraph = "0.6" 10 | petgraph-drawing = { path = "../../drawing" } 11 | rand = "0.8" 12 | -------------------------------------------------------------------------------- /crates/layout/random/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "random" 3 | version = "0.1.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/graph.rst: -------------------------------------------------------------------------------- 1 | Graph Module 2 | =========================== 3 | 4 | This module provides graph data structures for representing and manipulating graphs. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | graph/graph 10 | graph/digraph 11 | graph/graph_adapter 12 | -------------------------------------------------------------------------------- /crates/algorithm/shortest-path/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod bfs; 2 | mod dijkstra; 3 | mod distance_matrix; 4 | mod warshall_floyd; 5 | mod weighted_edge_length; 6 | 7 | pub use bfs::*; 8 | pub use dijkstra::*; 9 | pub use distance_matrix::*; 10 | pub use warshall_floyd::*; 11 | pub use weighted_edge_length::*; 12 | -------------------------------------------------------------------------------- /js/dataset/diamond.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "2"}, {"source": "1", "target": "2"}, {"source": "1", "target": "3"}, {"source": "2", "target": "3"}]} -------------------------------------------------------------------------------- /js/dataset/bull.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "2"}, {"source": "1", "target": "2"}, {"source": "1", "target": "3"}, {"source": "2", "target": "4"}]} -------------------------------------------------------------------------------- /crates/python/docs/source/api/rng.rst: -------------------------------------------------------------------------------- 1 | Random Number Generation Module 2 | ===================================== 3 | 4 | This module provides utilities for random number generation. 5 | 6 | Rng 7 | --------- 8 | 9 | .. autoclass:: egraph.Rng 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | -------------------------------------------------------------------------------- /crates/algorithm/triangulation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-algorithm-triangulation" 3 | version = "0.1.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | petgraph = "0.6" 9 | petgraph-drawing = { path = "../../../crates/drawing" } 10 | spade = "2.2" 11 | -------------------------------------------------------------------------------- /crates/dataset/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egraph-dataset" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | 1138_bus = [] 8 | poli = [] 9 | USpowerGrid = [] 10 | qh882 = [] 11 | dwt_1005 = [] 12 | dwt_2680 = [] 13 | 3elt = [] 14 | karate = [] 15 | lesmis = [] 16 | 17 | [dependencies] 18 | petgraph = "0.6" 19 | -------------------------------------------------------------------------------- /js/dataset/house.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "2"}, {"source": "1", "target": "3"}, {"source": "2", "target": "3"}, {"source": "2", "target": "4"}, {"source": "3", "target": "4"}]} -------------------------------------------------------------------------------- /crates/algorithm/layering/src/algorithms/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module containing various layering algorithm implementations. 2 | //! 3 | //! This module provides different algorithms for assigning layers to nodes 4 | //! in a directed graph, which is a key step in hierarchical graph layout. 5 | 6 | pub mod longest_path; 7 | 8 | pub use longest_path::LongestPath; 9 | -------------------------------------------------------------------------------- /crates/clustering/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-clustering" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | petgraph = "0.6" 8 | petgraph-linalg-rdmds = { path = "../linalg/rdmds" } 9 | petgraph-drawing = { path = "../drawing" } 10 | ndarray = "0.16" 11 | rand = "0.8" 12 | linfa = "0.8" 13 | linfa-clustering = "0.8" 14 | -------------------------------------------------------------------------------- /crates/layout/sgd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-sgd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ndarray = "0.16" 8 | ordered-float = "3.0" 9 | petgraph = "0.6" 10 | petgraph-algorithm-shortest-path = { path = "../../algorithm/shortest-path" } 11 | petgraph-drawing = { path = "../../drawing" } 12 | rand = "0.8" 13 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/distance_matrix.rst: -------------------------------------------------------------------------------- 1 | Distance Matrix Module 2 | =============================== 3 | 4 | This module provides functionality for working with distance matrices. 5 | 6 | DistanceMatrix 7 | --------------------------- 8 | 9 | .. autoclass:: egraph.DistanceMatrix 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | -------------------------------------------------------------------------------- /crates/quality-metrics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-quality-metrics" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | linfa = "0.8" 8 | linfa-nn = "0.8" 9 | ndarray = "0.16" 10 | petgraph = "0.6" 11 | petgraph-algorithm-shortest-path = { path = "../algorithm/shortest-path" } 12 | petgraph-drawing = { path = "../drawing" } 13 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============================ 3 | 4 | This section provides detailed documentation for all modules and classes in the egraph library. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | graph 10 | drawing 11 | layout 12 | algorithm 13 | quality_metrics 14 | distance_matrix 15 | rng 16 | -------------------------------------------------------------------------------- /js/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Egraph Examples 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /crates/layout/kamada-kawai/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-kamada-kawai" 3 | version = "0.1.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | ndarray = "0.16" 9 | petgraph = "0.6" 10 | petgraph-algorithm-shortest-path = { path = "../../algorithm/shortest-path" } 11 | petgraph-drawing = { path = "../../drawing" } -------------------------------------------------------------------------------- /crates/linalg/spmv/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-linalg-spmv" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Yosuke Onoue "] 6 | license = "MIT" 7 | description = "Sparse symmetric matrix-vector product implementation" 8 | repository = "https://github.com/likr/egraph-rs" 9 | 10 | [dependencies] 11 | ndarray = "0.16" 12 | num-traits = "0.2" 13 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/algorithm.rst: -------------------------------------------------------------------------------- 1 | Algorithm Module 2 | =============================== 3 | 4 | This module provides graph algorithms for analyzing graph structures. 5 | 6 | Shortest Path Algorithms 7 | --------------------------- 8 | 9 | .. autofunction:: egraph.all_sources_bfs 10 | .. autofunction:: egraph.all_sources_dijkstra 11 | .. autofunction:: egraph.warshall_floyd 12 | -------------------------------------------------------------------------------- /crates/layout/omega/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-omega" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | ndarray = "0.16" 8 | petgraph = "0.6" 9 | petgraph-drawing = { path = "../../drawing" } 10 | petgraph-layout-sgd = { path = "../sgd" } 11 | rand = "0.8" 12 | 13 | [dev-dependencies] 14 | petgraph-linalg-rdmds = { path = "../../linalg/rdmds" } 15 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/drawing.rst: -------------------------------------------------------------------------------- 1 | Drawing Module 2 | =========================== 3 | 4 | This module provides drawing implementations for different geometric spaces. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | drawing/drawing_euclidean_2d 10 | drawing/drawing_euclidean 11 | drawing/drawing_spherical_2d 12 | drawing/drawing_hyperbolic_2d 13 | drawing/drawing_torus_2d 14 | -------------------------------------------------------------------------------- /crates/wasm/src/graph/mod.rs: -------------------------------------------------------------------------------- 1 | //! Graph data structures for WebAssembly. 2 | //! 3 | //! This module provides WebAssembly bindings for graph data structures 4 | //! based on petgraph, exposed via wasm-bindgen. 5 | 6 | mod base; 7 | mod directed; 8 | mod types; 9 | mod undirected; 10 | 11 | pub use directed::JsDiGraph; 12 | pub use types::{Edge, IndexType, Node}; 13 | pub use undirected::JsGraph; 14 | -------------------------------------------------------------------------------- /js/dataset/house_x.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "2"}, {"source": "0", "target": "3"}, {"source": "1", "target": "2"}, {"source": "1", "target": "3"}, {"source": "2", "target": "3"}, {"source": "2", "target": "4"}, {"source": "3", "target": "4"}]} -------------------------------------------------------------------------------- /crates/layout/mds/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-mds" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ndarray = "0.16" 8 | petgraph = "0.6" 9 | petgraph-algorithm-shortest-path = { path = "../../algorithm/shortest-path" } 10 | petgraph-drawing = { path = "../../drawing" } 11 | 12 | [dev-dependencies] 13 | egraph-dataset = { path = "../../dataset", features = ["1138_bus"] } 14 | -------------------------------------------------------------------------------- /crates/linalg/spmv/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Sparse symmetric matrix-vector product implementation. 2 | //! 3 | //! This crate provides efficient sparse matrix-vector multiplication (spmv) 4 | //! for symmetric matrices, which is a core operation for many graph algorithms 5 | //! including Chebyshev polynomial approximation. 6 | 7 | mod sparse_symmetric_matrix; 8 | 9 | pub use sparse_symmetric_matrix::SparseSymmetricMatrix; 10 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout.rst: -------------------------------------------------------------------------------- 1 | Layout Module 2 | ============================ 3 | 4 | This module provides layout algorithms for positioning graph nodes. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | layout/full_sgd 10 | layout/sparse_sgd 11 | layout/schedulers 12 | layout/stress_majorization 13 | layout/kamada_kawai 14 | layout/classical_mds 15 | layout/pivot_mds 16 | layout/overwrap_removal 17 | -------------------------------------------------------------------------------- /js/examples/src/index.js: -------------------------------------------------------------------------------- 1 | import "bulma/css/bulma.css"; 2 | import "./styles.css"; 3 | import egraph from "egraph/dist/web/egraph_wasm"; 4 | import egraphBinary from "egraph/dist/web/egraph_wasm_bg.wasm?url"; 5 | import egRenderer from "eg-renderer/umd/eg-renderer.js"; 6 | import egRendererBinary from "eg-renderer/umd/eg-renderer.wasm?url"; 7 | 8 | await egRenderer(egRendererBinary); 9 | await egraph(egraphBinary); 10 | import("./App"); 11 | -------------------------------------------------------------------------------- /crates/layout/stress-majorization/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-stress-majorization" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ndarray = "0.16" 10 | petgraph = "0.6" 11 | petgraph-algorithm-shortest-path = { path = "../../algorithm/shortest-path" } 12 | petgraph-drawing = { path = "../../drawing" } -------------------------------------------------------------------------------- /crates/algorithm/shortest-path/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-algorithm-shortest-path" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | ndarray = "0.16" 8 | ordered-float = "3.0" 9 | petgraph = "0.6" 10 | 11 | [dev-dependencies] 12 | criterion = { version = "0.4", features = ["html_reports"] } 13 | egraph-dataset = { path = "../../dataset", features = ["1138_bus", "lesmis"] } 14 | 15 | [[bench]] 16 | name = "apsp" 17 | harness = false 18 | -------------------------------------------------------------------------------- /crates/wasm/src/algorithm.rs: -------------------------------------------------------------------------------- 1 | pub mod biclustering; 2 | pub mod ranking; 3 | 4 | use egraph::algorithm::connected_components; 5 | use egraph_wasm_adapter::{JsGraph, JsGraphAdapter}; 6 | use wasm_bindgen::prelude::*; 7 | 8 | #[wasm_bindgen(js_name = connected_components)] 9 | pub fn js_connected_components(graph: JsGraph) -> JsValue { 10 | let graph = JsGraphAdapter::new(graph); 11 | let components = connected_components(&graph); 12 | JsValue::from_serde(&components).unwrap() 13 | } 14 | -------------------------------------------------------------------------------- /crates/wasm/tests/sgd_full.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const eg = require("wasm-bindgen-test"); 3 | const helpers = require("./util/test_helpers"); 4 | 5 | /** 6 | * Test basic instantiation of FullSgd class 7 | */ 8 | exports.testFullSgdConstructor = function () { 9 | // Create a FullSgd instance 10 | const sgd = new eg.FullSgd(); 11 | 12 | // Verify that the FullSGD instance exists 13 | assert(sgd instanceof eg.FullSgd, "Should create an instance of FullSgd"); 14 | }; 15 | -------------------------------------------------------------------------------- /crates/wasm/tests/sgd_full.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/sgd_full.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testFullSgdConstructor")] 11 | fn test_full_sgd_constructor(); 12 | } 13 | 14 | /// Test basic instantiation of FullSgd class 15 | #[wasm_bindgen_test] 16 | pub fn full_sgd_constructor() { 17 | test_full_sgd_constructor(); 18 | } 19 | -------------------------------------------------------------------------------- /crates/wasm/src/graph/types.rs: -------------------------------------------------------------------------------- 1 | //! Common type definitions for graph data structures. 2 | //! 3 | //! This module defines shared types used across graph implementations. 4 | 5 | use wasm_bindgen::prelude::*; 6 | 7 | /// Type alias for node data, can be any JavaScript value. 8 | pub type Node = JsValue; 9 | 10 | /// Type alias for edge data, can be any JavaScript value. 11 | pub type Edge = JsValue; 12 | 13 | /// Type alias for the index type used in the graph, maps to u32. 14 | pub type IndexType = u32; 15 | -------------------------------------------------------------------------------- /js/dataset/sedgewick_maze.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}], "links": [{"source": "0", "target": "2"}, {"source": "0", "target": "7"}, {"source": "0", "target": "5"}, {"source": "1", "target": "7"}, {"source": "2", "target": "6"}, {"source": "3", "target": "4"}, {"source": "3", "target": "5"}, {"source": "4", "target": "5"}, {"source": "4", "target": "7"}, {"source": "4", "target": "6"}]} -------------------------------------------------------------------------------- /crates/wasm/tests/sgd_sparse.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/sgd_sparse.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testSparseSgdConstructor")] 11 | fn test_sparse_sgd_constructor(); 12 | } 13 | 14 | /// Test basic instantiation of SparseSgd class 15 | #[wasm_bindgen_test] 16 | pub fn sparse_sgd_constructor() { 17 | test_sparse_sgd_constructor(); 18 | } 19 | -------------------------------------------------------------------------------- /js/dataset/octahedral.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "2"}, {"source": "0", "target": "3"}, {"source": "0", "target": "4"}, {"source": "1", "target": "2"}, {"source": "1", "target": "3"}, {"source": "1", "target": "5"}, {"source": "2", "target": "4"}, {"source": "2", "target": "5"}, {"source": "3", "target": "4"}, {"source": "3", "target": "5"}, {"source": "4", "target": "5"}]} -------------------------------------------------------------------------------- /crates/layout/separation-constraints/src/constraint_graph/variable.rs: -------------------------------------------------------------------------------- 1 | /// Represents a variable (typically a node's coordinate in one dimension) within the QPSC problem. 2 | /// Its position is determined by the block it belongs to and its offset within that block. 3 | /// See Section 3.2 and Figure 9 in the IPSEP-COLA paper [1]. 4 | pub struct Variable { 5 | /// Index of the [`Block`] this variable belongs to. 6 | pub block: usize, 7 | /// Displacement from the [`Block::position`] of its containing block. 8 | pub offset: S, 9 | } 10 | -------------------------------------------------------------------------------- /crates/wasm/tests/sgd_sparse.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const eg = require("wasm-bindgen-test"); 3 | const helpers = require("./util/test_helpers"); 4 | 5 | /** 6 | * Test basic instantiation of SparseSgd class 7 | */ 8 | exports.testSparseSgdConstructor = function () { 9 | // Create a SparseSgd instance with a simple length function and 1 pivot node 10 | const sgd = new eg.SparseSgd(); 11 | 12 | // Verify that the SGD instance exists 13 | assert(sgd instanceof eg.SparseSgd, "Should create an instance of SparseSgd"); 14 | }; 15 | -------------------------------------------------------------------------------- /js/dataset/cubical.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "3"}, {"source": "0", "target": "4"}, {"source": "1", "target": "2"}, {"source": "1", "target": "7"}, {"source": "2", "target": "3"}, {"source": "2", "target": "6"}, {"source": "3", "target": "5"}, {"source": "4", "target": "5"}, {"source": "4", "target": "7"}, {"source": "5", "target": "6"}, {"source": "6", "target": "7"}]} -------------------------------------------------------------------------------- /crates/layout/kernel-sgd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-kernel-sgd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Yosuke Onoue "] 6 | license = "MIT" 7 | description = "Kernel-based SGD layout algorithm using diffusion kernel approximation" 8 | repository = "https://github.com/likr/egraph-rs" 9 | 10 | [dependencies] 11 | ndarray = "0.16" 12 | num-traits = "0.2" 13 | petgraph = "0.6" 14 | petgraph-drawing = { path = "../../drawing" } 15 | petgraph-layout-sgd = { path = "../sgd" } 16 | petgraph-linalg-spmv = { path = "../../linalg/spmv" } 17 | rand = "0.8" 18 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egraph-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | argparse = "0.2.2" 8 | petgraph = "0.6" 9 | petgraph-algorithm-shortest-path = { path = "../algorithm/shortest-path" } 10 | petgraph-drawing = { path = "../drawing" } 11 | petgraph-layout-omega = { path = "../layout/omega" } 12 | petgraph-layout-sgd = { path = "../layout/sgd" } 13 | petgraph-linalg-rdmds = { path = "../linalg/rdmds" } 14 | petgraph-quality-metrics = { path = "../quality-metrics" } 15 | rand = "0.8" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | -------------------------------------------------------------------------------- /js/examples/src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { ExampleEdgeBundling } from "./ExampleEdgeBundling"; 2 | export { ExampleHyperbolicGeometry } from "./ExampleHyperbolicGeometry"; 3 | export { ExampleKamadaKawai } from "./ExampleKamadaKawai"; 4 | export { ExampleMds } from "./ExampleMds"; 5 | export { ExampleOverwrapRemoval } from "./ExampleOverwrapRemoval"; 6 | export { ExampleSgd } from "./ExampleSgd"; 7 | export { ExampleSphericalGeometry } from "./ExampleSphericalGeometry"; 8 | export { ExampleStressMajorization } from "./ExampleStressMajorization"; 9 | export { ExampleTorusGeometry as ExampleTorus } from "./ExampleTorusGeometry"; 10 | export { Home } from "./Home"; 11 | -------------------------------------------------------------------------------- /crates/python/docs/source/getting_started/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Welcome to egraph! This section will help you get started with the library. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | overview 10 | installation 11 | quickstart 12 | 13 | Overview 14 | -------- 15 | 16 | :doc:`overview` provides an introduction to egraph, its features, and use cases. 17 | 18 | Installation 19 | ------------ 20 | 21 | :doc:`installation` guides you through installing egraph and its dependencies. 22 | 23 | Quick Start 24 | ----------- 25 | 26 | :doc:`quickstart` shows you how to create your first graph layout in just a few minutes. 27 | -------------------------------------------------------------------------------- /js/dataset/petersen.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "4"}, {"source": "0", "target": "5"}, {"source": "1", "target": "2"}, {"source": "1", "target": "6"}, {"source": "2", "target": "3"}, {"source": "2", "target": "7"}, {"source": "3", "target": "4"}, {"source": "3", "target": "8"}, {"source": "4", "target": "9"}, {"source": "5", "target": "7"}, {"source": "5", "target": "8"}, {"source": "6", "target": "8"}, {"source": "6", "target": "9"}, {"source": "7", "target": "9"}]} -------------------------------------------------------------------------------- /js/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "5.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bulma": "^0.9.2", 16 | "d3": "^6.5.0", 17 | "eg-renderer": "^1.7.0", 18 | "egraph": "^6.0.0-alpha.0", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "react-router-dom": "^5.2.0" 22 | }, 23 | "devDependencies": { 24 | "@vitejs/plugin-react": "^1.2.0", 25 | "vite": "^4.3.9" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /crates/layout/kernel-sgd/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Kernel-based SGD layout algorithm using diffusion kernel approximation. 2 | //! 3 | //! This crate implements a graph layout algorithm that uses the diffusion kernel 4 | //! exp(-tL) to compute ideal distances between nodes, where L is the graph Laplacian. 5 | //! The kernel is approximated using Chebyshev polynomials and element queries are 6 | //! performed using the Hutchinson trace estimator with symmetry optimization. 7 | 8 | mod chebyshev; 9 | mod diffusion_kernel; 10 | mod hutchinson; 11 | mod kernel_sgd; 12 | mod power_method; 13 | 14 | pub use diffusion_kernel::DiffusionKernel; 15 | pub use hutchinson::HutchinsonEstimator; 16 | pub use kernel_sgd::KernelSgd; 17 | -------------------------------------------------------------------------------- /crates/wasm/tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use egraph_wasm::*; 3 | use serde::{Deserialize, Serialize}; 4 | use wasm_bindgen::prelude::*; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | struct NodeData { 8 | id: usize, 9 | group: usize, 10 | } 11 | 12 | #[derive(Serialize, Deserialize)] 13 | struct LinkData { 14 | source: usize, 15 | target: usize, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | struct GraphData { 20 | nodes: Vec, 21 | links: Vec, 22 | } 23 | 24 | pub fn example_data() -> JsValue { 25 | let s = include_str!("./miserables.json"); 26 | let data = serde_json::from_str::(s).unwrap(); 27 | serde_wasm_bindgen::to_value(&data).ok().unwrap() 28 | } 29 | -------------------------------------------------------------------------------- /js/dataset/krackhardt_kite.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "2"}, {"source": "0", "target": "3"}, {"source": "0", "target": "5"}, {"source": "1", "target": "3"}, {"source": "1", "target": "4"}, {"source": "1", "target": "6"}, {"source": "2", "target": "3"}, {"source": "2", "target": "5"}, {"source": "3", "target": "4"}, {"source": "3", "target": "5"}, {"source": "3", "target": "6"}, {"source": "4", "target": "6"}, {"source": "5", "target": "6"}, {"source": "5", "target": "7"}, {"source": "6", "target": "7"}, {"source": "7", "target": "8"}, {"source": "8", "target": "9"}]} -------------------------------------------------------------------------------- /js/examples/src/wrapper.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | 3 | export function Wrapper({ onResize, children }) { 4 | const wrapperRef = useRef(); 5 | useEffect(() => { 6 | function resize() { 7 | if (onResize) { 8 | const { clientWidth, clientHeight } = wrapperRef.current; 9 | onResize(clientWidth, clientHeight); 10 | } 11 | } 12 | 13 | resize(); 14 | window.addEventListener("resize", resize); 15 | 16 | return () => { 17 | window.removeEventListener("resize", resize); 18 | }; 19 | }, []); 20 | return ( 21 |
26 | {children} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /js/dataset/frucht.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "6"}, {"source": "0", "target": "7"}, {"source": "1", "target": "2"}, {"source": "1", "target": "7"}, {"source": "2", "target": "3"}, {"source": "2", "target": "8"}, {"source": "3", "target": "4"}, {"source": "3", "target": "9"}, {"source": "4", "target": "5"}, {"source": "4", "target": "9"}, {"source": "5", "target": "6"}, {"source": "5", "target": "10"}, {"source": "6", "target": "10"}, {"source": "7", "target": "11"}, {"source": "8", "target": "11"}, {"source": "8", "target": "9"}, {"source": "10", "target": "11"}]} -------------------------------------------------------------------------------- /crates/layout/separation-constraints/src/constraint_graph/block.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | /// Represents a set of variables that move rigidly together, connected by active constraints. 4 | /// The block structure evolves as constraints are satisfied during the projection process. 5 | /// See Section 3.2 and Figure 9 in the IPSEP-COLA paper [1]. 6 | pub struct Block { 7 | /// Indices (`usize`) of variables belonging to this block. 8 | pub variables: HashSet, 9 | /// The reference position of the block (`posn` in the paper). 10 | pub position: S, 11 | /// Indices (`usize`) of constraints active within this block, forming a spanning tree. 12 | /// An active constraint `c = (u, v, gap)` implies `position(u) + gap = position(v)`. 13 | pub active: HashSet, 14 | } 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/algorithm/connected-components", 5 | "crates/algorithm/layering", 6 | "crates/algorithm/shortest-path", 7 | "crates/algorithm/triangulation", 8 | "crates/cli", 9 | "crates/clustering", 10 | "crates/dataset", 11 | "crates/drawing", 12 | "crates/edge-bundling/fdeb", 13 | "crates/layout/kamada-kawai", 14 | "crates/layout/kernel-sgd", 15 | "crates/layout/omega", 16 | "crates/layout/overwrap-removal", 17 | "crates/layout/sgd", 18 | "crates/layout/mds", 19 | "crates/layout/separation-constraints", 20 | "crates/layout/stress-majorization", 21 | "crates/linalg/rdmds", 22 | "crates/linalg/spmv", 23 | "crates/python", 24 | "crates/quality-metrics", 25 | "crates/wasm", 26 | ] 27 | -------------------------------------------------------------------------------- /crates/python/examples/kamada_kawai.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from egraph import Graph, Coordinates, KamadaKawai 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | drawing = Coordinates.initial_placement(graph) 16 | kamada_kawai = KamadaKawai(graph, lambda _: 30) 17 | kamada_kawai.eps = 1e-3 18 | kamada_kawai.run(drawing) 19 | 20 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 21 | nx.draw(nx_graph, pos) 22 | plt.savefig('tmp/kamada_kawai.png') 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /crates/dataset/src/data/karate.csv: -------------------------------------------------------------------------------- 1 | 34,78 2 | 1,0 3 | 1,2 4 | 1,3 5 | 1,7 6 | 1,13 7 | 1,17 8 | 1,19 9 | 1,21 10 | 1,30 11 | 0,2 12 | 0,3 13 | 0,4 14 | 0,5 15 | 0,6 16 | 0,7 17 | 0,8 18 | 0,10 19 | 0,11 20 | 0,12 21 | 0,13 22 | 0,17 23 | 0,19 24 | 0,21 25 | 0,31 26 | 2,3 27 | 2,7 28 | 2,8 29 | 2,9 30 | 2,13 31 | 2,27 32 | 2,28 33 | 2,32 34 | 3,7 35 | 3,12 36 | 3,13 37 | 4,6 38 | 4,10 39 | 5,6 40 | 5,10 41 | 5,16 42 | 6,16 43 | 8,30 44 | 8,32 45 | 8,33 46 | 9,33 47 | 13,33 48 | 19,33 49 | 25,23 50 | 25,24 51 | 25,31 52 | 23,27 53 | 23,29 54 | 23,32 55 | 23,33 56 | 24,27 57 | 24,31 58 | 27,33 59 | 28,31 60 | 28,33 61 | 29,26 62 | 29,32 63 | 29,33 64 | 26,33 65 | 30,32 66 | 30,33 67 | 31,32 68 | 31,33 69 | 32,14 70 | 32,15 71 | 32,18 72 | 32,20 73 | 32,22 74 | 32,33 75 | 14,33 76 | 15,33 77 | 18,33 78 | 20,33 79 | 22,33 80 | -------------------------------------------------------------------------------- /js/dataset/heawood.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}, {"id": "12"}, {"id": "13"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "13"}, {"source": "0", "target": "5"}, {"source": "1", "target": "2"}, {"source": "1", "target": "10"}, {"source": "2", "target": "3"}, {"source": "2", "target": "7"}, {"source": "3", "target": "4"}, {"source": "3", "target": "12"}, {"source": "4", "target": "5"}, {"source": "4", "target": "9"}, {"source": "5", "target": "6"}, {"source": "6", "target": "7"}, {"source": "6", "target": "11"}, {"source": "7", "target": "8"}, {"source": "8", "target": "9"}, {"source": "8", "target": "13"}, {"source": "9", "target": "10"}, {"source": "10", "target": "11"}, {"source": "11", "target": "12"}, {"source": "12", "target": "13"}]} -------------------------------------------------------------------------------- /crates/python/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= python -m sphinx 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile clean doctest 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | # Clean build directory 23 | clean: 24 | rm -rf $(BUILDDIR)/* 25 | 26 | # Run doctests 27 | doctest: 28 | @$(SPHINXBUILD) -M doctest "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 29 | -------------------------------------------------------------------------------- /crates/python/src/clustering/mod.rs: -------------------------------------------------------------------------------- 1 | /// Python bindings for petgraph-clustering. 2 | /// 3 | /// This module provides Python bindings for the petgraph-clustering Rust crate, 4 | /// which implements various community detection algorithms for graphs. 5 | use pyo3::prelude::*; 6 | 7 | mod coarsen; 8 | mod infomap; 9 | mod label_propagation; 10 | mod louvain; 11 | mod spectral; 12 | 13 | use coarsen::py_coarsen; 14 | use infomap::PyInfoMap; 15 | use label_propagation::PyLabelPropagation; 16 | use louvain::PyLouvain; 17 | use spectral::PySpectralClustering; 18 | 19 | /// Register the clustering module and its classes with Python. 20 | pub fn register(_py: Python<'_>, m: &Bound) -> PyResult<()> { 21 | m.add_class::()?; 22 | m.add_class::()?; 23 | m.add_class::()?; 24 | m.add_class::()?; 25 | m.add_function(wrap_pyfunction!(py_coarsen, m)?)?; 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /crates/python/docs/source/api/quality_metrics.rst: -------------------------------------------------------------------------------- 1 | Quality Metrics Module 2 | =============================== 3 | 4 | This module provides metrics for evaluating the quality of graph layouts. 5 | 6 | Stress 7 | ------------- 8 | 9 | .. autofunction:: egraph.stress 10 | 11 | Crossing Number 12 | --------------------------- 13 | 14 | .. autofunction:: egraph.crossing_number 15 | 16 | Neighborhood Preservation 17 | ---------------------------------- 18 | 19 | .. autofunction:: egraph.neighborhood_preservation 20 | 21 | Aspect Ratio 22 | ---------------------- 23 | 24 | .. autofunction:: egraph.aspect_ratio 25 | 26 | Angular Resolution 27 | --------------------------- 28 | 29 | .. autofunction:: egraph.angular_resolution 30 | 31 | Node Resolution 32 | ------------------------ 33 | 34 | .. autofunction:: egraph.node_resolution 35 | 36 | Gabriel Graph Property 37 | ------------------------------- 38 | 39 | .. autofunction:: egraph.gabriel_graph_property 40 | -------------------------------------------------------------------------------- /crates/python/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to egraph documentation 2 | =============================== 3 | 4 | **egraph** is a Python library for graph visualization and layout algorithms, providing efficient implementations of various graph algorithms in Rust with Python bindings. 5 | 6 | .. toctree:: 7 | :maxdepth: 3 8 | :caption: Contents: 9 | 10 | getting_started/index 11 | tutorial/index 12 | api/index 13 | examples/index 14 | 15 | Features 16 | -------- 17 | 18 | * Graph data structures (Graph, DiGraph) 19 | * Drawing spaces (Euclidean, Hyperbolic, Spherical, Torus) 20 | * Layout algorithms: 21 | * Stochastic Gradient Descent (SGD) 22 | * Multidimensional Scaling (MDS) 23 | * Stress Majorization 24 | * Kamada-Kawai 25 | * Overlap Removal 26 | * Quality metrics for evaluating layout effectiveness 27 | * Integration with NetworkX 28 | 29 | Indices and tables 30 | ================== 31 | 32 | * :ref:`genindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /js/dataset/chvatal.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "4"}, {"source": "0", "target": "6"}, {"source": "0", "target": "9"}, {"source": "1", "target": "2"}, {"source": "1", "target": "5"}, {"source": "1", "target": "7"}, {"source": "2", "target": "3"}, {"source": "2", "target": "6"}, {"source": "2", "target": "8"}, {"source": "3", "target": "4"}, {"source": "3", "target": "7"}, {"source": "3", "target": "9"}, {"source": "4", "target": "5"}, {"source": "4", "target": "8"}, {"source": "5", "target": "10"}, {"source": "5", "target": "11"}, {"source": "6", "target": "10"}, {"source": "6", "target": "11"}, {"source": "7", "target": "8"}, {"source": "7", "target": "11"}, {"source": "8", "target": "10"}, {"source": "9", "target": "10"}, {"source": "9", "target": "11"}]} -------------------------------------------------------------------------------- /crates/python/docs/source/api/layout/schedulers.rst: -------------------------------------------------------------------------------- 1 | Schedulers 2 | ================= 3 | 4 | This module provides learning rate schedulers for SGD algorithms. 5 | 6 | SchedulerConstant 7 | ----------------- 8 | 9 | .. autoclass:: egraph.SchedulerConstant 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | 14 | SchedulerLinear 15 | --------------- 16 | 17 | .. autoclass:: egraph.SchedulerLinear 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | SchedulerQuadratic 23 | ----------------- 24 | 25 | .. autoclass:: egraph.SchedulerQuadratic 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | SchedulerExponential 31 | ------------------- 32 | 33 | .. autoclass:: egraph.SchedulerExponential 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | SchedulerReciprocal 39 | ------------------ 40 | 41 | .. autoclass:: egraph.SchedulerReciprocal 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | -------------------------------------------------------------------------------- /crates/python/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version 73 | 74 | tmp/* -------------------------------------------------------------------------------- /crates/algorithm/shortest-path/benches/apsp.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use egraph_dataset::dataset_1138_bus; 3 | use petgraph::prelude::*; 4 | use petgraph_algorithm_shortest_path::*; 5 | 6 | fn criterion_benchmark(c: &mut Criterion) { 7 | let graph: UnGraph<(), ()> = dataset_1138_bus(); 8 | let mut group = c.benchmark_group("1138_bus"); 9 | group.bench_with_input("all_sources_bfs", &graph, |bench, graph| { 10 | bench.iter(|| { 11 | let _ = all_sources_bfs(graph, 30.); 12 | }); 13 | }); 14 | group.bench_with_input("all_sources_dijkstra", &graph, |bench, graph| { 15 | bench.iter(|| { 16 | let _ = all_sources_dijkstra(graph, &mut |_| 30.); 17 | }); 18 | }); 19 | group.bench_with_input("warshall_floyd", &graph, |bench, graph| { 20 | bench.iter(|| { 21 | let _ = warshall_floyd(graph, &mut |_| 30.); 22 | }); 23 | }); 24 | } 25 | 26 | criterion_group!(benches, criterion_benchmark); 27 | criterion_main!(benches); 28 | -------------------------------------------------------------------------------- /js/dataset/moebius_kantor.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}, {"id": "12"}, {"id": "13"}, {"id": "14"}, {"id": "15"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "15"}, {"source": "0", "target": "5"}, {"source": "1", "target": "2"}, {"source": "1", "target": "12"}, {"source": "2", "target": "3"}, {"source": "2", "target": "7"}, {"source": "3", "target": "4"}, {"source": "3", "target": "14"}, {"source": "4", "target": "5"}, {"source": "4", "target": "9"}, {"source": "5", "target": "6"}, {"source": "6", "target": "7"}, {"source": "6", "target": "11"}, {"source": "7", "target": "8"}, {"source": "8", "target": "9"}, {"source": "8", "target": "13"}, {"source": "9", "target": "10"}, {"source": "10", "target": "11"}, {"source": "10", "target": "15"}, {"source": "11", "target": "12"}, {"source": "12", "target": "13"}, {"source": "13", "target": "14"}, {"source": "14", "target": "15"}]} -------------------------------------------------------------------------------- /crates/algorithm/shortest-path/tests/apsp.rs: -------------------------------------------------------------------------------- 1 | use egraph_dataset::dataset_lesmis; 2 | use petgraph::prelude::*; 3 | use petgraph_algorithm_shortest_path::*; 4 | 5 | fn run(f: F) 6 | where 7 | F: Fn(&UnGraph<(), ()>) -> D, 8 | D: DistanceMatrix, 9 | { 10 | let graph: UnGraph<(), ()> = dataset_lesmis(); 11 | let actual = f(&graph); 12 | let expected = petgraph::algo::floyd_warshall(&graph, |_| 1.).unwrap(); 13 | for u in graph.node_indices() { 14 | for v in graph.node_indices() { 15 | assert_eq!( 16 | actual.get(u, v).unwrap(), 17 | expected[&(u, v)], 18 | "d[{:?}, {:?}]", 19 | u, 20 | v 21 | ); 22 | } 23 | } 24 | } 25 | 26 | #[test] 27 | fn test_all_sources_bfs() { 28 | run(|graph| all_sources_bfs(graph, 1.)); 29 | } 30 | 31 | #[test] 32 | fn test_all_sources_dijkstra() { 33 | run(|graph| all_sources_dijkstra(graph, &mut |_| 1.)); 34 | } 35 | 36 | #[test] 37 | fn test_warshall_floyd() { 38 | run(|graph| warshall_floyd(graph, &mut |_| 1.)); 39 | } 40 | -------------------------------------------------------------------------------- /crates/python/examples/sgd_hyperbolic_2d.py: -------------------------------------------------------------------------------- 1 | import json 2 | import networkx as nx 3 | import egraph as eg 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = eg.Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | drawing = eg.DrawingHyperbolic2d.initial_placement(graph) 16 | rng = eg.Rng.seed_from(0) 17 | sgd = eg.FullSgd( 18 | graph, 19 | lambda _: 6, 20 | ) 21 | scheduler = sgd.scheduler( 22 | 100, 23 | 0.1, 24 | ) 25 | 26 | def step(eta): 27 | sgd.shuffle(rng) 28 | sgd.apply(drawing, eta) 29 | scheduler.run(step) 30 | 31 | for u, i in indices.items(): 32 | nx_graph.nodes[u]['x'] = drawing.x(i) 33 | nx_graph.nodes[u]['y'] = drawing.y(i) 34 | json.dump(nx.node_link_data(nx_graph), 35 | open('tmp/graph_hyperbolic.json', 'w'), 36 | ensure_ascii=False) 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /crates/python/examples/sgd_spherical_2d.py: -------------------------------------------------------------------------------- 1 | import json 2 | import networkx as nx 3 | import egraph as eg 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = eg.Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | drawing = eg.DrawingSpherical2d.initial_placement(graph) 16 | rng = eg.Rng.seed_from(0) 17 | sgd = eg.FullSgd( 18 | graph, 19 | lambda _: 0.6, 20 | ) 21 | scheduler = sgd.scheduler( 22 | 100, 23 | 0.1, 24 | ) 25 | 26 | def step(eta): 27 | sgd.shuffle(rng) 28 | sgd.apply(drawing, eta) 29 | scheduler.run(step) 30 | 31 | for u, i in indices.items(): 32 | nx_graph.nodes[u]['lon'] = drawing.lon(i) 33 | nx_graph.nodes[u]['lat'] = drawing.lat(i) 34 | json.dump(nx.node_link_data(nx_graph), 35 | open('tmp/graph_sphere.json', 'w'), 36 | ensure_ascii=False) 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /crates/python/src/algorithm/mod.rs: -------------------------------------------------------------------------------- 1 | /// Graph algorithm modules for the Python bindings 2 | /// 3 | /// This module exports various graph algorithms to Python, providing efficient 4 | /// implementations of common operations on graph data structures. 5 | /// 6 | /// # Submodules 7 | /// 8 | /// - `shortest_path`: Shortest path algorithms (BFS, Dijkstra, Warshall-Floyd) 9 | /// - `layering`: Graph layering algorithms for hierarchical layouts 10 | /// - `triangulation`: Delaunay triangulation for 2D Euclidean drawings 11 | mod layering; 12 | mod separation_constraints; 13 | mod shortest_path; 14 | mod triangulation; 15 | 16 | use pyo3::prelude::*; 17 | 18 | /// Registers algorithm-related functions with the Python module 19 | /// 20 | /// This function adds all the graph algorithm functions to the Python module, 21 | /// making them available to be called from Python code. 22 | pub fn register(py: Python<'_>, m: &Bound) -> PyResult<()> { 23 | shortest_path::register(py, m)?; 24 | layering::register(py, m)?; 25 | triangulation::register(py, m)?; 26 | separation_constraints::register(py, m)?; 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /crates/python/examples/sgd.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import egraph as eg 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = eg.Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | drawing = eg.DrawingEuclidean2d.initial_placement(graph) 16 | rng = eg.Rng.seed_from(0) # random seed 17 | sgd = eg.SparseSgd( 18 | graph, 19 | lambda _: 30, # edge length 20 | 50, # number of pivots 21 | rng, 22 | ) 23 | scheduler = sgd.scheduler( 24 | 100, # number of iterations 25 | 0.1, # eps: eta_min = eps * min d[i, j] ^ 2 26 | ) 27 | 28 | def step(eta): 29 | sgd.shuffle(rng) 30 | sgd.apply(drawing, eta) 31 | scheduler.run(step) 32 | 33 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 34 | nx.draw(nx_graph, pos) 35 | plt.savefig('tmp/sgd.png') 36 | 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /crates/wasm/src/drawing.rs: -------------------------------------------------------------------------------- 1 | //! Graph drawing utilities for WebAssembly. 2 | //! 3 | //! This module provides WebAssembly bindings for various types of graph drawings, 4 | //! each representing different geometric spaces in which graphs can be drawn. 5 | //! These drawings typically store node positions and provide methods for 6 | //! manipulating layouts in different spaces. 7 | //! 8 | //! The module includes: 9 | //! * Euclidean drawing: Regular Cartesian coordinate system 10 | //! * Euclidean 2D drawing: 2D Cartesian coordinate system 11 | //! * Hyperbolic 2D drawing: Drawing on a hyperbolic plane 12 | //! * Spherical 2D drawing: Drawing on the surface of a sphere 13 | //! * Torus 2D drawing: Drawing on the surface of a torus 14 | 15 | mod drawing_euclidean; 16 | mod drawing_euclidean_2d; 17 | mod drawing_hyperbolic_2d; 18 | mod drawing_spherical_2d; 19 | mod drawing_torus_2d; 20 | 21 | pub use drawing_euclidean::JsDrawingEuclidean; 22 | pub use drawing_euclidean_2d::JsDrawingEuclidean2d; 23 | pub use drawing_hyperbolic_2d::JsDrawingHyperbolic2d; 24 | pub use drawing_spherical_2d::JsDrawingSpherical2d; 25 | pub use drawing_torus_2d::JsDrawingTorus2d; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Yosuke Onoue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/edge_angle.rs: -------------------------------------------------------------------------------- 1 | use petgraph_drawing::DrawingValue; 2 | 3 | /// Calculates the angle between two vectors in 2D space. 4 | /// 5 | /// This function computes the angle (in radians) between two edges represented as 6 | /// vectors from the origin to points (x1, y1) and (x2, y2). The angle is calculated 7 | /// using the dot product formula: cos(θ) = (v1·v2)/(|v1|·|v2|). 8 | /// 9 | /// # Parameters 10 | /// 11 | /// * `x1`: The x-coordinate of the first vector 12 | /// * `y1`: The y-coordinate of the first vector 13 | /// * `x2`: The x-coordinate of the second vector 14 | /// * `y2`: The y-coordinate of the second vector 15 | /// 16 | /// # Returns 17 | /// 18 | /// * `Some(angle)`: The angle between the vectors in radians, if it can be computed 19 | /// * `None`: If the angle cannot be defined (e.g., when either vector has zero length) 20 | /// 21 | pub fn edge_angle(x1: S, y1: S, x2: S, y2: S) -> Option { 22 | let cos = (x1 * x2 + y1 * y2) / (x1.hypot(y1) * x2.hypot(y2)); 23 | let angle = cos.acos(); 24 | if angle.is_finite() { 25 | Some(angle) 26 | } else { 27 | None 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/python/examples/sgd_3d.py: -------------------------------------------------------------------------------- 1 | import json 2 | import networkx as nx 3 | import egraph as eg 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = eg.Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | drawing = eg.ClassicalMds(graph, lambda _: 30).run(3) 16 | rng = eg.Rng.seed_from(0) 17 | sgd = eg.FullSgd( 18 | graph, 19 | lambda _: 30, 20 | ) 21 | scheduler = sgd.scheduler( 22 | 100, 23 | 0.1, 24 | ) 25 | 26 | def step(eta): 27 | sgd.shuffle(rng) 28 | sgd.apply(drawing, eta) 29 | scheduler.run(step) 30 | 31 | for u, i in indices.items(): 32 | nx_graph.nodes[u]['x'] = drawing.get(i, 0) 33 | nx_graph.nodes[u]['y'] = drawing.get(i, 1) 34 | nx_graph.nodes[u]['z'] = drawing.get(i, 2) 35 | json.dump(nx.node_link_data(nx_graph), 36 | open('tmp/graph.json', 'w'), 37 | ensure_ascii=False) 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /crates/wasm/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Yosuke Onoue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/maturin.yml: -------------------------------------------------------------------------------- 1 | name: maturin 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | defaults: 8 | run: 9 | working-directory: crates/python 10 | 11 | jobs: 12 | linux: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: messense/maturin-action@v1 17 | with: 18 | manylinux: auto 19 | command: build 20 | args: --release -o dist 21 | working-directory: crates/python 22 | - name: Upload wheels 23 | uses: actions/upload-artifact@v4 24 | with: 25 | name: wheels 26 | path: crates/python/dist 27 | 28 | release: 29 | name: Release 30 | runs-on: ubuntu-latest 31 | if: "startsWith(github.ref, 'refs/tags/')" 32 | needs: [linux] 33 | steps: 34 | - uses: actions/download-artifact@v4 35 | with: 36 | name: wheels 37 | path: . 38 | - name: Publish to PyPI 39 | uses: messense/maturin-action@v1 40 | env: 41 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 42 | with: 43 | command: upload 44 | args: --skip-existing * 45 | -------------------------------------------------------------------------------- /crates/python/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | if "%1" == "clean" goto clean 28 | if "%1" == "doctest" goto doctest 29 | 30 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 31 | goto end 32 | 33 | :help 34 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 35 | goto end 36 | 37 | :clean 38 | rmdir /s /q %BUILDDIR% 39 | goto end 40 | 41 | :doctest 42 | %SPHINXBUILD% -M doctest %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 43 | goto end 44 | 45 | :end 46 | popd 47 | -------------------------------------------------------------------------------- /crates/python/examples/stress_majorization.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import egraph as eg 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def plot(nx_graph, indices, drawing, filename): 7 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 8 | ax = plt.subplot() 9 | ax.set_aspect("equal") 10 | nx.draw(nx_graph, pos, ax=ax) 11 | plt.savefig(filename) 12 | plt.close() 13 | 14 | 15 | def main(): 16 | nx_graph = nx.les_miserables_graph() 17 | graph = eg.Graph() 18 | indices = {} 19 | for u in nx_graph.nodes: 20 | indices[u] = graph.add_node(u) 21 | for u, v in nx_graph.edges: 22 | graph.add_edge(indices[u], indices[v], (u, v)) 23 | 24 | drawing = eg.Drawing2D.initial_placement(graph) 25 | d = eg.warshall_floyd(graph, lambda _: 100) 26 | s0 = eg.stress(drawing, d) 27 | stress_majorization = eg.StressMajorization.with_distance_matrix(drawing, d) 28 | stress_majorization.run(drawing) 29 | s = eg.stress(drawing, d) 30 | print(f"stress {s0:.2f} -> {s:.2f}") 31 | 32 | plot(nx_graph, indices, drawing, "tmp/stress_majorization.png") 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /js/dataset/pappus.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}, {"id": "12"}, {"id": "13"}, {"id": "14"}, {"id": "15"}, {"id": "16"}, {"id": "17"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "17"}, {"source": "0", "target": "5"}, {"source": "1", "target": "2"}, {"source": "1", "target": "8"}, {"source": "2", "target": "3"}, {"source": "2", "target": "13"}, {"source": "3", "target": "4"}, {"source": "3", "target": "10"}, {"source": "4", "target": "5"}, {"source": "4", "target": "15"}, {"source": "5", "target": "6"}, {"source": "6", "target": "7"}, {"source": "6", "target": "11"}, {"source": "7", "target": "8"}, {"source": "7", "target": "14"}, {"source": "8", "target": "9"}, {"source": "9", "target": "10"}, {"source": "9", "target": "16"}, {"source": "10", "target": "11"}, {"source": "11", "target": "12"}, {"source": "12", "target": "13"}, {"source": "12", "target": "17"}, {"source": "13", "target": "14"}, {"source": "14", "target": "15"}, {"source": "15", "target": "16"}, {"source": "16", "target": "17"}]} -------------------------------------------------------------------------------- /js/dataset/icosahedral.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "5"}, {"source": "0", "target": "7"}, {"source": "0", "target": "8"}, {"source": "0", "target": "11"}, {"source": "1", "target": "2"}, {"source": "1", "target": "5"}, {"source": "1", "target": "6"}, {"source": "1", "target": "8"}, {"source": "2", "target": "3"}, {"source": "2", "target": "6"}, {"source": "2", "target": "8"}, {"source": "2", "target": "9"}, {"source": "3", "target": "4"}, {"source": "3", "target": "6"}, {"source": "3", "target": "9"}, {"source": "3", "target": "10"}, {"source": "4", "target": "5"}, {"source": "4", "target": "6"}, {"source": "4", "target": "10"}, {"source": "4", "target": "11"}, {"source": "5", "target": "6"}, {"source": "5", "target": "11"}, {"source": "7", "target": "8"}, {"source": "7", "target": "9"}, {"source": "7", "target": "10"}, {"source": "7", "target": "11"}, {"source": "8", "target": "9"}, {"source": "9", "target": "10"}, {"source": "10", "target": "11"}]} -------------------------------------------------------------------------------- /crates/layout/mds/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Multidimensional Scaling (MDS) implementations for graph layout. 2 | //! 3 | //! This crate provides implementations of MDS algorithms for visualizing graph structures. 4 | //! MDS is a technique used to visualize the level of similarity of individual nodes in a graph 5 | //! by placing them in a lower-dimensional space (typically 2D or 3D) such that the distances 6 | //! between points in the resulting space approximate the graph-theoretic distances between nodes. 7 | //! 8 | //! Two implementations are provided: 9 | //! * [`ClassicalMds`] - Standard implementation that computes a full distance matrix 10 | //! * [`PivotMds`] - More efficient implementation that uses a subset of nodes as pivots 11 | //! 12 | //! # References 13 | //! 14 | //! * Cox, T. F., & Cox, M. A. (2000). Multidimensional scaling. Chapman & Hall/CRC. 15 | //! * Brandes, U., & Pich, C. (2007). Eigensolver methods for progressive multidimensional scaling of large data. 16 | //! In Graph Drawing (pp. 42-53). Springer. 17 | 18 | mod classical_mds; 19 | mod double_centering; 20 | mod eigendecomposition; 21 | mod pivot_mds; 22 | 23 | pub use classical_mds::ClassicalMds; 24 | pub use pivot_mds::PivotMds; 25 | -------------------------------------------------------------------------------- /crates/layout/sgd/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Stochastic Gradient Descent (SGD) Layout Algorithms 2 | //! 3 | //! This crate provides implementations of various Stochastic Gradient Descent (SGD) based 4 | //! graph layout algorithms. SGD layout is a force-directed graph drawing technique that 5 | //! positions nodes to minimize a stress function by following the gradient of the stress 6 | //! with a series of small adjustment steps. 7 | //! 8 | //! ## Features 9 | //! 10 | //! - Multiple SGD variant implementations (Full, Sparse, Distance-Adjusted) 11 | //! - Various learning rate schedulers with different decay patterns 12 | //! - Customizable distance and weight functions 13 | //! 14 | //! ## Algorithm 15 | //! 16 | //! SGD layout works by iteratively moving nodes to better positions by minimizing the difference 17 | //! between the graph-theoretical distances and the geometric distances in the layout. The algorithm 18 | //! uses a learning rate parameter that typically decreases over time according to a schedule. 19 | 20 | mod full_sgd; 21 | mod scheduler; 22 | mod sgd; 23 | mod sparse_sgd; 24 | 25 | pub use full_sgd::FullSgd; 26 | pub use scheduler::*; 27 | pub use sgd::Sgd; 28 | pub use sparse_sgd::SparseSgd; 29 | -------------------------------------------------------------------------------- /js/examples/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export class Home extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
    9 |
  • 10 | KamadaKawai 11 |
  • 12 |
  • 13 | StressMajorization 14 |
  • 15 |
  • 16 | SGD 17 |
  • 18 |
  • 19 | MDS 20 |
  • 21 |
  • 22 | Hyperbolic Geometry 23 |
  • 24 |
  • 25 | Spherical Geometry 26 |
  • 27 |
  • 28 | Torus Geometry 29 |
  • 30 |
  • 31 | Edge-bundling 32 |
  • 33 |
  • 34 | Overwrap Removal 35 |
  • 36 |
37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/layout/separation-constraints/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "petgraph-layout-separation-constraints" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fixedbitset = "0.4" 8 | ordered-float = "3.0" 9 | petgraph = "0.6" 10 | petgraph-clustering = { path = "../../clustering" } 11 | petgraph-drawing = { path = "../../drawing" } 12 | petgraph-algorithm-layering = { path = "../../algorithm/layering" } 13 | petgraph-algorithm-triangulation = { path = "../../algorithm/triangulation" } 14 | 15 | [dev-dependencies] 16 | egraph-dataset = { path = "../../dataset", features=["qh882", "lesmis"] } 17 | petgraph-layout-mds = { path = "../mds" } 18 | petgraph-layout-stress-majorization = { path = "../stress-majorization" } 19 | petgraph-layout-sgd = { path = "../sgd" } 20 | plotters = "0.3" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | rand = "0.8" 24 | 25 | [[example]] 26 | name = "qh882_separation_constraints" 27 | path = "examples/qh882_separation_constraints.rs" 28 | 29 | [[example]] 30 | name = "dependency_graph_layered" 31 | path = "examples/dependency_graph_layered.rs" 32 | 33 | [[example]] 34 | name = "lesmis_cluster_overlap" 35 | path = "examples/lesmis_cluster_overlap.rs" 36 | 37 | -------------------------------------------------------------------------------- /crates/wasm/tests/edge_bundling.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/edge_bundling.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testFdebBasic")] 11 | fn test_fdeb_basic(); 12 | #[wasm_bindgen(js_name = "testFdebWithComplexGraph")] 13 | fn test_fdeb_with_complex_graph(); 14 | #[wasm_bindgen(js_name = "testFdebResultStructure")] 15 | fn test_fdeb_result_structure(); 16 | #[wasm_bindgen(js_name = "testFdebIntegration")] 17 | fn test_fdeb_integration(); 18 | } 19 | 20 | /// Test basic functionality of the FDEB algorithm 21 | #[wasm_bindgen_test] 22 | pub fn fdeb_basic() { 23 | test_fdeb_basic(); 24 | } 25 | 26 | /// Test FDEB with a more complex graph 27 | #[wasm_bindgen_test] 28 | pub fn fdeb_with_complex_graph() { 29 | test_fdeb_with_complex_graph(); 30 | } 31 | 32 | /// Test the structure of the FDEB result 33 | #[wasm_bindgen_test] 34 | pub fn fdeb_result_structure() { 35 | test_fdeb_result_structure(); 36 | } 37 | 38 | /// Test integration of FDEB with other components 39 | #[wasm_bindgen_test] 40 | pub fn fdeb_integration() { 41 | test_fdeb_integration(); 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egraph", 3 | "author": "Yosuke Onoue ", 4 | "license": "MIT", 5 | "scripts": { 6 | "prestart": "npm run wasm-build:web", 7 | "start": "npm run dev -w examples", 8 | "wasm-build": "npm run wasm-build:bundler && npm run wasm-build:no-modules && npm run wasm-build:nodejs && npm run wasm-build:web", 9 | "wasm-build:bundler": "wasm-pack build --target bundler -d dist/bundler crates/wasm", 10 | "wasm-build:no-modules": "wasm-pack build --target no-modules -d dist/no-modules crates/wasm", 11 | "wasm-build:nodejs": "wasm-pack build --target nodejs -d dist/nodejs crates/wasm", 12 | "wasm-build:web": "wasm-pack build --target web -d dist/web crates/wasm" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/likr/egraph-rs.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/likr/egraph-rs/issues" 20 | }, 21 | "homepage": "https://github.com/likr/egraph-rs#readme", 22 | "devDependencies": { 23 | "prettier": "^2.3.1" 24 | }, 25 | "private": true, 26 | "workspaces": [ 27 | "crates/wasm", 28 | "js/*" 29 | ], 30 | "dependencies": { 31 | "egraph-dataset": "file:js/dataset", 32 | "example": "^0.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /js/dataset/florentine_families.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "Acciaiuoli"}, {"id": "Medici"}, {"id": "Castellani"}, {"id": "Peruzzi"}, {"id": "Strozzi"}, {"id": "Barbadori"}, {"id": "Ridolfi"}, {"id": "Tornabuoni"}, {"id": "Albizzi"}, {"id": "Salviati"}, {"id": "Pazzi"}, {"id": "Bischeri"}, {"id": "Guadagni"}, {"id": "Ginori"}, {"id": "Lamberteschi"}], "links": [{"source": "Acciaiuoli", "target": "Medici"}, {"source": "Medici", "target": "Barbadori"}, {"source": "Medici", "target": "Ridolfi"}, {"source": "Medici", "target": "Tornabuoni"}, {"source": "Medici", "target": "Albizzi"}, {"source": "Medici", "target": "Salviati"}, {"source": "Castellani", "target": "Peruzzi"}, {"source": "Castellani", "target": "Strozzi"}, {"source": "Castellani", "target": "Barbadori"}, {"source": "Peruzzi", "target": "Strozzi"}, {"source": "Peruzzi", "target": "Bischeri"}, {"source": "Strozzi", "target": "Ridolfi"}, {"source": "Strozzi", "target": "Bischeri"}, {"source": "Ridolfi", "target": "Tornabuoni"}, {"source": "Tornabuoni", "target": "Guadagni"}, {"source": "Albizzi", "target": "Ginori"}, {"source": "Albizzi", "target": "Guadagni"}, {"source": "Salviati", "target": "Pazzi"}, {"source": "Bischeri", "target": "Guadagni"}, {"source": "Guadagni", "target": "Lamberteschi"}]} -------------------------------------------------------------------------------- /crates/python/src/layout/sgd/full.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | distance_matrix::{DistanceMatrixType, PyDistanceMatrix}, 3 | graph::{GraphType, PyGraphAdapter}, 4 | layout::sgd::PySgd, 5 | }; 6 | use petgraph::visit::EdgeRef; 7 | use petgraph_layout_sgd::FullSgd; 8 | use pyo3::prelude::*; 9 | 10 | #[pyclass] 11 | #[pyo3(name = "FullSgd")] 12 | pub struct PyFullSgd { 13 | builder: FullSgd, 14 | } 15 | 16 | #[pymethods] 17 | impl PyFullSgd { 18 | #[new] 19 | fn new() -> Self { 20 | Self { 21 | builder: FullSgd::new(), 22 | } 23 | } 24 | 25 | fn build(&self, graph: &PyGraphAdapter, f: &Bound) -> PySgd { 26 | PySgd::new_with_sgd(match graph.graph() { 27 | GraphType::Graph(native_graph) => self.builder.build(native_graph, |e| { 28 | f.call1((e.id().index(),)).unwrap().extract().unwrap() 29 | }), 30 | _ => panic!("unsupported graph type"), 31 | }) 32 | } 33 | 34 | fn build_with_distance_matrix(&self, d: &PyDistanceMatrix) -> PySgd { 35 | PySgd::new_with_sgd(match d.distance_matrix() { 36 | DistanceMatrixType::Full(d) => self.builder.build_with_distance_matrix(d), 37 | _ => panic!("unsupported distance matrix type"), 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/wasm/tests/rng.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/rng.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testRngConstructor")] 11 | fn test_rng_constructor(); 12 | #[wasm_bindgen(js_name = "testRngSeedFrom")] 13 | fn test_rng_seed_from(); 14 | #[wasm_bindgen(js_name = "testRngDifferentSeeds")] 15 | fn test_rng_different_seeds(); 16 | #[wasm_bindgen(js_name = "testRngWithSgdLayout")] 17 | fn test_rng_with_sgd_layout(); 18 | } 19 | 20 | /// Test basic instantiation of Rng class 21 | #[wasm_bindgen_test] 22 | pub fn rng_constructor() { 23 | test_rng_constructor(); 24 | } 25 | 26 | /// Test seeded random number generation 27 | /// This test verifies that the same seed produces the same sequence of random numbers 28 | #[wasm_bindgen_test] 29 | pub fn rng_seed_from() { 30 | test_rng_seed_from(); 31 | } 32 | 33 | /// Test that different seeds produce different sequences 34 | #[wasm_bindgen_test] 35 | pub fn rng_different_seeds() { 36 | test_rng_different_seeds(); 37 | } 38 | 39 | /// Test integration with SGD layout algorithm 40 | #[wasm_bindgen_test] 41 | pub fn rng_with_sgd_layout() { 42 | test_rng_with_sgd_layout(); 43 | } 44 | -------------------------------------------------------------------------------- /crates/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | // #[macro_use] 2 | // extern crate serde_derive; 3 | 4 | //! WebAssembly binding for the egraph library. 5 | //! 6 | //! This crate provides JavaScript/WebAssembly bindings for the egraph library, enabling 7 | //! graph visualization and analysis capabilities in web environments. It exposes Rust 8 | //! implementations of graph data structures, layout algorithms, and drawing utilities 9 | //! through wasm-bindgen. 10 | //! 11 | //! The primary components of this crate include: 12 | //! 13 | //! * Graph data structures ([`graph`]): Undirected and directed graph implementations 14 | //! * Drawing utilities ([`drawing`]): Various geometric representations for graph drawings 15 | //! * Layout algorithms ([`layout`]): Force-directed and other layout algorithms 16 | //! * Edge bundling ([`edge_bundling`]): Methods to reduce visual clutter by bundling edges 17 | //! * Clustering ([`clustering`]): Graph clustering algorithms 18 | //! * Quality metrics ([`quality_metrics`]): Functions to evaluate drawing quality 19 | //! * Random number generation ([`rng`]): Seeded random number generation 20 | 21 | // pub mod algorithm; 22 | pub mod clustering; 23 | pub mod drawing; 24 | pub mod edge_bundling; 25 | pub mod graph; 26 | // pub mod grouping; 27 | pub mod layout; 28 | pub mod quality_metrics; 29 | pub mod rng; 30 | -------------------------------------------------------------------------------- /js/dataset/desargues.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}, {"id": "12"}, {"id": "13"}, {"id": "14"}, {"id": "15"}, {"id": "16"}, {"id": "17"}, {"id": "18"}, {"id": "19"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "19"}, {"source": "0", "target": "5"}, {"source": "1", "target": "2"}, {"source": "1", "target": "16"}, {"source": "2", "target": "3"}, {"source": "2", "target": "11"}, {"source": "3", "target": "4"}, {"source": "3", "target": "14"}, {"source": "4", "target": "5"}, {"source": "4", "target": "9"}, {"source": "5", "target": "6"}, {"source": "6", "target": "7"}, {"source": "6", "target": "15"}, {"source": "7", "target": "8"}, {"source": "7", "target": "18"}, {"source": "8", "target": "9"}, {"source": "8", "target": "13"}, {"source": "9", "target": "10"}, {"source": "10", "target": "11"}, {"source": "10", "target": "19"}, {"source": "11", "target": "12"}, {"source": "12", "target": "13"}, {"source": "12", "target": "17"}, {"source": "13", "target": "14"}, {"source": "14", "target": "15"}, {"source": "15", "target": "16"}, {"source": "16", "target": "17"}, {"source": "17", "target": "18"}, {"source": "18", "target": "19"}]} -------------------------------------------------------------------------------- /js/dataset/dodecahedral.json: -------------------------------------------------------------------------------- 1 | {"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "0"}, {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, {"id": "5"}, {"id": "6"}, {"id": "7"}, {"id": "8"}, {"id": "9"}, {"id": "10"}, {"id": "11"}, {"id": "12"}, {"id": "13"}, {"id": "14"}, {"id": "15"}, {"id": "16"}, {"id": "17"}, {"id": "18"}, {"id": "19"}], "links": [{"source": "0", "target": "1"}, {"source": "0", "target": "19"}, {"source": "0", "target": "10"}, {"source": "1", "target": "2"}, {"source": "1", "target": "8"}, {"source": "2", "target": "3"}, {"source": "2", "target": "6"}, {"source": "3", "target": "4"}, {"source": "3", "target": "19"}, {"source": "4", "target": "5"}, {"source": "4", "target": "17"}, {"source": "5", "target": "6"}, {"source": "5", "target": "15"}, {"source": "6", "target": "7"}, {"source": "7", "target": "8"}, {"source": "7", "target": "14"}, {"source": "8", "target": "9"}, {"source": "9", "target": "10"}, {"source": "9", "target": "13"}, {"source": "10", "target": "11"}, {"source": "11", "target": "12"}, {"source": "11", "target": "18"}, {"source": "12", "target": "13"}, {"source": "12", "target": "16"}, {"source": "13", "target": "14"}, {"source": "14", "target": "15"}, {"source": "15", "target": "16"}, {"source": "16", "target": "17"}, {"source": "17", "target": "18"}, {"source": "18", "target": "19"}]} -------------------------------------------------------------------------------- /crates/wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egraph", 3 | "collaborators": [ 4 | "Yosuke Onoue " 5 | ], 6 | "description": "WebAssembly binding of egraph.", 7 | "version": "6.0.0-alpha.3", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/likr/egraph-rs/tree/master/crates/wasm" 12 | }, 13 | "files": [ 14 | "dist/bundler/egraph_wasm_bg.wasm", 15 | "dist/bundler/egraph_wasm.js", 16 | "dist/bundler/egraph_wasm.d.ts", 17 | "dist/no-modules/egraph_wasm_bg.wasm", 18 | "dist/no-modules/egraph_wasm.js", 19 | "dist/no-modules/egraph_wasm.d.ts", 20 | "dist/nodejs/egraph_wasm_bg.wasm", 21 | "dist/nodejs/egraph_wasm.js", 22 | "dist/nodejs/egraph_wasm.d.ts", 23 | "dist/web/egraph_wasm_bg.wasm", 24 | "dist/web/egraph_wasm.js", 25 | "dist/web/egraph_wasm.d.ts", 26 | "umd" 27 | ], 28 | "main": "dist/nodejs/egraph_wasm.js", 29 | "module": "dist/bundler/egraph_wasm.js", 30 | "unpkg": "umd/egraph.js", 31 | "types": "dist/bundler/egraph_wasm.d.ts", 32 | "sideEffects": "false", 33 | "devDependencies": { 34 | "rollup": "^2.79.1" 35 | }, 36 | "scripts": { 37 | "build-umd": "rollup -c && cp dist/web/egraph_wasm_bg.wasm umd/egraph_bg.wasm", 38 | "prepublish": "npm run build-umd" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/wasm/tests/clustering.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/clustering.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testBasicCoarsening")] 11 | fn test_basic_coarsening(); 12 | #[wasm_bindgen(js_name = "testComplexGraphCoarsening")] 13 | fn test_complex_graph_coarsening(); 14 | #[wasm_bindgen(js_name = "testCustomNodeAndEdgeMerging")] 15 | fn test_custom_node_and_edge_merging(); 16 | #[wasm_bindgen(js_name = "testClusteringIntegration")] 17 | fn test_clustering_integration(); 18 | } 19 | 20 | /// Test basic graph coarsening functionality 21 | #[wasm_bindgen_test] 22 | pub fn basic_coarsening() { 23 | test_basic_coarsening(); 24 | } 25 | 26 | /// Test coarsening with a more complex graph 27 | #[wasm_bindgen_test] 28 | pub fn complex_graph_coarsening() { 29 | test_complex_graph_coarsening(); 30 | } 31 | 32 | /// Test custom node and edge merging functions 33 | #[wasm_bindgen_test] 34 | pub fn custom_node_and_edge_merging() { 35 | test_custom_node_and_edge_merging(); 36 | } 37 | 38 | /// Test integration of clustering with other components 39 | #[wasm_bindgen_test] 40 | pub fn clustering_integration() { 41 | test_clustering_integration(); 42 | } 43 | -------------------------------------------------------------------------------- /memory-bank/projectbrief.md: -------------------------------------------------------------------------------- 1 | # Project Brief: egraph-rs 2 | 3 | ## Project Overview 4 | 5 | egraph-rs is a Rust library providing graph data structures, algorithms, quality metrics, and drawing functionality. It offers a comprehensive toolkit for graph manipulation and visualization through multiple interfaces. 6 | 7 | ## Core Requirements 8 | 9 | - Provide robust graph data structures in Rust 10 | - Implement various graph algorithms and layout techniques 11 | - Support drawing capabilities in multiple geometric spaces (Euclidean, Spherical, Hyperbolic, Torus) 12 | - Enable access from different environments (Rust, Python, WebAssembly/JavaScript) 13 | - Maintain high-quality metrics for evaluating graph layouts 14 | 15 | ## Goals 16 | 17 | - Create a unified API across multiple platforms 18 | - Provide efficient implementations of graph algorithms 19 | - Support various graph visualization techniques 20 | - Ensure comprehensive documentation and examples 21 | - Maintain a modular architecture for extensibility 22 | 23 | ## Scope 24 | 25 | The project is organized as a Rust workspace comprising multiple crates, each focused on specific functionality: 26 | 27 | - Algorithm crates for graph algorithms 28 | - Layout crates for positioning nodes 29 | - Drawing capabilities for visualization 30 | - Quality metrics for evaluating layouts 31 | - Language bindings (Python, WebAssembly) 32 | -------------------------------------------------------------------------------- /crates/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egraph-wasm" 3 | version = "5.0.0" 4 | authors = ["Yosuke Onoue "] 5 | edition = "2018" 6 | description = "WebAssembly binding of egraph." 7 | repository = "https://github.com/likr/egraph-rs/tree/master/crates/wasm" 8 | license = "MIT" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | console_error_panic_hook = "0.1" 15 | getrandom = { version = "0.2", features = ["js"] } 16 | js-sys = "0.3" 17 | ndarray = "0.16" 18 | petgraph = "0.6" 19 | petgraph-algorithm-shortest-path = { path = "../algorithm/shortest-path" } 20 | petgraph-clustering = { path = "../clustering" } 21 | petgraph-drawing = { path = "../drawing" } 22 | petgraph-edge-bundling-fdeb = { path = "../edge-bundling/fdeb" } 23 | petgraph-layout-kamada-kawai = { path = "../layout/kamada-kawai" } 24 | petgraph-layout-mds = { path = "../layout/mds" } 25 | petgraph-layout-overwrap-removal = { path = "../layout/overwrap-removal" } 26 | petgraph-layout-sgd = { path = "../layout/sgd" } 27 | petgraph-layout-stress-majorization = { path = "../layout/stress-majorization" } 28 | petgraph-quality-metrics = { path = "../quality-metrics" } 29 | rand = "0.8" 30 | serde = { version = "1.0", features = ["derive"] } 31 | serde-wasm-bindgen = "0.4" 32 | wasm-bindgen = "0.2" 33 | 34 | [dev-dependencies] 35 | wasm-bindgen-test = "0.3.50" 36 | serde_json = "1.0" 37 | -------------------------------------------------------------------------------- /crates/python/examples/nonconnected_sgd.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from egraph import Graph, Coordinates, Rng, SparseSgd 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def main(): 7 | nx_graph = nx.watts_strogatz_graph(50, 3, 0.3, seed=0) 8 | components = nx.connected_components(nx_graph) 9 | 10 | graph = Graph() 11 | indices = {} 12 | for u in nx_graph.nodes: 13 | indices[u] = graph.add_node(u) 14 | for u, v in nx_graph.edges: 15 | graph.add_edge(indices[u], indices[v], (u, v)) 16 | c = graph.add_node(u) 17 | for nodes in components: 18 | u = list(nodes)[0] 19 | graph.add_edge(c, indices[u], None) 20 | 21 | drawing = Coordinates.initial_placement(graph) 22 | rng = Rng.seed_from(0) # random seed 23 | sgd = SparseSgd( 24 | graph, 25 | lambda _: 30, # edge length 26 | 50, # number of pivots 27 | rng, 28 | ) 29 | scheduler = sgd.scheduler( 30 | 100, # number of iterations 31 | 0.1, # eps: eta_min = eps * min d[i, j] ^ 2 32 | ) 33 | 34 | def step(eta): 35 | sgd.shuffle(rng) 36 | sgd.apply(drawing, eta) 37 | scheduler.run(step) 38 | 39 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 40 | nx.draw(nx_graph, pos) 41 | plt.savefig('tmp/nonconnected_sgd.png') 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /crates/python/examples/overwrap_removal.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import egraph as eg 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = eg.Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | drawing = eg.DrawingEuclidean2d.initial_placement(graph) 16 | rng = eg.Rng.seed_from(0) 17 | sgd = eg.FullSgd(graph, lambda _: 100) 18 | scheduler = sgd.scheduler(100, 0.1) 19 | overwrap_removal = eg.OverwrapRemoval(graph, lambda _: 25) 20 | overwrap_removal.iterations = 5 21 | 22 | def step(eta): 23 | sgd.shuffle(rng) 24 | sgd.apply(drawing, eta) 25 | overwrap_removal.apply_with_drawing_euclidean_2d(drawing) 26 | scheduler.run(step) 27 | drawing.centralize() 28 | 29 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 30 | fig, ax = plt.subplots(figsize=(8, 8)) 31 | ax.set_xlim(-400, 400) 32 | ax.set_ylim(-400, 400) 33 | nx.draw(nx_graph, pos, ax=ax, node_size=(72 / 100 * 50) ** 2, alpha=0.5) 34 | plt.axis('tight') 35 | plt.axis('off') 36 | fig.subplots_adjust(left=0, right=1, bottom=0, top=1) 37 | plt.savefig('tmp/overwrap_removal.png') 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /crates/python/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egraph-python" 3 | version = "6.0.0-alpha" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | name = "egraph" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | ndarray = "0.16" 13 | numpy = "0.26" 14 | pyo3 = { version = "0.26", features = ["abi3-py37", "extension-module"] } 15 | petgraph = "0.6" 16 | petgraph-algorithm-shortest-path = { path = "../algorithm/shortest-path" } 17 | petgraph-algorithm-layering = { path = "../algorithm/layering" } 18 | petgraph-algorithm-triangulation = { path = "../algorithm/triangulation" } 19 | petgraph-clustering = { path = "../clustering" } 20 | petgraph-drawing = { path = "../drawing" } 21 | petgraph-layout-kamada-kawai = { path = "../layout/kamada-kawai" } 22 | petgraph-layout-kernel-sgd = { path = "../layout/kernel-sgd" } 23 | petgraph-layout-mds = { path = "../layout/mds" } 24 | petgraph-layout-overwrap-removal = { path = "../layout/overwrap-removal" } 25 | petgraph-layout-sgd = { path = "../layout/sgd" } 26 | petgraph-layout-omega = { path = "../layout/omega" } 27 | petgraph-layout-stress-majorization = { path = "../layout/stress-majorization" } 28 | petgraph-layout-separation-constraints = { path = "../layout/separation-constraints" } 29 | petgraph-linalg-rdmds = { path = "../linalg/rdmds" } 30 | petgraph-quality-metrics = { path = "../quality-metrics" } 31 | rand = "0.8" 32 | -------------------------------------------------------------------------------- /crates/wasm/src/layout/sgd/full.rs: -------------------------------------------------------------------------------- 1 | //! Full SGD layout algorithm for WebAssembly. 2 | //! 3 | //! This module provides WebAssembly bindings for the Full SGD layout algorithm, 4 | //! which is a force-directed approach that uses all pairs of nodes for computing 5 | //! the layout, providing accurate results but with higher computational complexity. 6 | 7 | use crate::{graph::JsGraph, layout::sgd::sgd::JsSgd}; 8 | use js_sys::Function; 9 | use petgraph::visit::EdgeRef; 10 | use petgraph_layout_sgd::FullSgd; 11 | use wasm_bindgen::prelude::*; 12 | 13 | use super::extract_edge_lengths; 14 | 15 | /// WebAssembly binding for Full SGD layout algorithm. 16 | /// 17 | /// Full SGD is a force-directed layout algorithm that computes shortest-path 18 | /// distances between all pairs of nodes. While accurate, it can be 19 | /// computationally expensive for large graphs. 20 | #[wasm_bindgen(js_name = "FullSgd")] 21 | pub struct JsFullSgd { 22 | builder: FullSgd, 23 | } 24 | 25 | #[wasm_bindgen(js_class = "FullSgd")] 26 | impl JsFullSgd { 27 | #[wasm_bindgen(constructor)] 28 | pub fn new() -> Self { 29 | Self { 30 | builder: FullSgd::new(), 31 | } 32 | } 33 | 34 | pub fn build(&self, graph: &JsGraph, length: &Function) -> JsSgd { 35 | let length_map = extract_edge_lengths(graph.graph(), length); 36 | JsSgd::new_with_sgd(self.builder.build(graph.graph(), |e| length_map[&e.id()])) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/wasm/tests/graph.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/graph.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testGraphConstructor")] 11 | fn test_graph_constructor(); 12 | #[wasm_bindgen(js_name = "testNodeOperations")] 13 | fn test_node_operations(); 14 | #[wasm_bindgen(js_name = "testEdgeOperations")] 15 | fn test_edge_operations(); 16 | #[wasm_bindgen(js_name = "testGraphTraversal")] 17 | fn test_graph_traversal(); 18 | #[wasm_bindgen(js_name = "testGraphWithDrawing")] 19 | fn test_graph_with_drawing(); 20 | } 21 | 22 | /// Test basic instantiation of Graph class 23 | #[wasm_bindgen_test] 24 | pub fn graph_constructor() { 25 | test_graph_constructor(); 26 | } 27 | 28 | /// Test node operations (add, remove, get) 29 | #[wasm_bindgen_test] 30 | pub fn node_operations() { 31 | test_node_operations(); 32 | } 33 | 34 | /// Test edge operations (add, remove, get) 35 | #[wasm_bindgen_test] 36 | pub fn edge_operations() { 37 | test_edge_operations(); 38 | } 39 | 40 | /// Test graph traversal and iteration 41 | #[wasm_bindgen_test] 42 | pub fn graph_traversal() { 43 | test_graph_traversal(); 44 | } 45 | 46 | /// Test integration with drawing component 47 | #[wasm_bindgen_test] 48 | pub fn graph_with_drawing() { 49 | test_graph_with_drawing(); 50 | } 51 | -------------------------------------------------------------------------------- /crates/python/src/layout/sgd/mod.rs: -------------------------------------------------------------------------------- 1 | //! SGD (Stochastic Gradient Descent) layout algorithms 2 | //! 3 | //! This module provides Python bindings for various SGD-based layout algorithms. 4 | 5 | mod diffusion_kernel; 6 | mod full; 7 | mod kernel_sgd; 8 | mod omega; 9 | mod schedulers; 10 | mod sgd; 11 | mod sparse; 12 | 13 | use pyo3::prelude::*; 14 | 15 | pub use self::diffusion_kernel::PyDiffusionKernel; 16 | pub use self::full::PyFullSgd; 17 | pub use self::kernel_sgd::PyKernelSgd; 18 | pub use self::omega::PyOmega; 19 | pub use self::schedulers::{ 20 | PySchedulerConstant, PySchedulerExponential, PySchedulerLinear, PySchedulerQuadratic, 21 | PySchedulerReciprocal, 22 | }; 23 | pub use self::sgd::PySgd; 24 | pub use self::sparse::PySparseSgd; 25 | 26 | /// Register all SGD-related classes with the Python module 27 | pub fn register(_py: Python<'_>, m: &Bound) -> PyResult<()> { 28 | // Register scheduler classes 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | m.add_class::()?; 33 | m.add_class::()?; 34 | 35 | // Register SGD algorithm classes 36 | m.add_class::()?; 37 | m.add_class::()?; 38 | m.add_class::()?; 39 | m.add_class::()?; 40 | m.add_class::()?; 41 | m.add_class::()?; 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /crates/wasm/src/layout/sgd/sparse.rs: -------------------------------------------------------------------------------- 1 | //! Sparse SGD layout algorithm for WebAssembly. 2 | //! 3 | //! This module provides WebAssembly bindings for the Sparse SGD layout algorithm, 4 | //! which is a scalable force-directed approach that uses pivot nodes to approximate 5 | //! distances, making it suitable for large graphs. 6 | 7 | use crate::{graph::JsGraph, layout::sgd::sgd::JsSgd, rng::JsRng}; 8 | use js_sys::Function; 9 | use petgraph::visit::EdgeRef; 10 | use petgraph_layout_sgd::SparseSgd; 11 | use wasm_bindgen::prelude::*; 12 | 13 | use super::extract_edge_lengths; 14 | 15 | /// WebAssembly binding for Sparse SGD layout algorithm. 16 | /// 17 | /// Sparse SGD is an efficient variant of SGD that uses pivot-based 18 | /// approximation of graph distances, making it suitable for large graphs 19 | /// where computing all-pairs shortest paths would be too expensive. 20 | #[wasm_bindgen(js_name = "SparseSgd")] 21 | pub struct JsSparseSgd { 22 | builder: SparseSgd, 23 | } 24 | 25 | #[wasm_bindgen(js_class = "SparseSgd")] 26 | impl JsSparseSgd { 27 | #[wasm_bindgen(constructor)] 28 | pub fn new() -> Self { 29 | Self { 30 | builder: SparseSgd::new(), 31 | } 32 | } 33 | 34 | pub fn build(&self, graph: &JsGraph, length: &Function, rng: &mut JsRng) -> JsSgd { 35 | let length_map = extract_edge_lengths(graph.graph(), length); 36 | JsSgd::new_with_sgd(self.builder.build( 37 | graph.graph(), 38 | |e| length_map[&e.id()], 39 | rng.get_mut(), 40 | )) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/wasm/tests/drawing_euclidean_2d.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/drawing_euclidean_2d.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testDrawingEuclidean2dConstructor")] 11 | fn test_drawing_euclidean_2d_constructor(); 12 | #[wasm_bindgen(js_name = "testNodeCoordinates")] 13 | fn test_node_coordinates(); 14 | #[wasm_bindgen(js_name = "testDrawingManipulation")] 15 | fn test_drawing_manipulation(); 16 | #[wasm_bindgen(js_name = "testEdgeSegments")] 17 | fn test_edge_segments(); 18 | #[wasm_bindgen(js_name = "testDrawingWithGraph")] 19 | fn test_drawing_with_graph(); 20 | } 21 | 22 | /// Test basic instantiation of DrawingEuclidean2d class 23 | #[wasm_bindgen_test] 24 | pub fn drawing_euclidean_2d_constructor() { 25 | test_drawing_euclidean_2d_constructor(); 26 | } 27 | 28 | /// Test node coordinate operations (get/set x,y) 29 | #[wasm_bindgen_test] 30 | pub fn node_coordinates() { 31 | test_node_coordinates(); 32 | } 33 | 34 | /// Test drawing manipulation (centralize, clamp_region) 35 | #[wasm_bindgen_test] 36 | pub fn drawing_manipulation() { 37 | test_drawing_manipulation(); 38 | } 39 | 40 | /// Test edge segment representation 41 | #[wasm_bindgen_test] 42 | pub fn edge_segments() { 43 | test_edge_segments(); 44 | } 45 | 46 | /// Test integration with Graph class 47 | #[wasm_bindgen_test] 48 | pub fn drawing_with_graph() { 49 | test_drawing_with_graph(); 50 | } 51 | -------------------------------------------------------------------------------- /crates/layout/mds/src/double_centering.rs: -------------------------------------------------------------------------------- 1 | use ndarray::prelude::*; 2 | use petgraph_drawing::DrawingValue; 3 | 4 | /// Applies double centering transformation to a squared distance matrix. 5 | /// 6 | /// Double centering is a mathematical operation that transforms a squared distance matrix 7 | /// into a matrix of dot products (also known as a Gram matrix or a kernel matrix). 8 | /// This is a key step in the MDS algorithm, as it allows working with inner products 9 | /// rather than distances, which enables the use of eigendecomposition to find the 10 | /// optimal configuration of points. 11 | /// 12 | /// The double centering operation is defined as: 13 | /// B = -0.5 * (I - 1/n * 11ᵀ) * D * (I - 1/n * 11ᵀ) 14 | /// 15 | /// Where: 16 | /// - D is the squared distance matrix 17 | /// - I is the identity matrix 18 | /// - 1 is a vector of ones 19 | /// - n is the number of points 20 | /// 21 | /// # Parameters 22 | /// 23 | /// * `delta`: The squared distance matrix to center 24 | /// 25 | /// # Returns 26 | /// 27 | /// The double-centered matrix (kernel matrix) 28 | pub fn double_centering(delta: &Array2) -> Array2 { 29 | let n = delta.shape()[0]; 30 | let k = delta.shape()[1]; 31 | let sum_col = delta.mean_axis(Axis(1)).unwrap(); 32 | let sum_row = delta.mean_axis(Axis(0)).unwrap(); 33 | let sum_all = sum_col.mean().unwrap(); 34 | let mut c = Array::zeros((n, k)); 35 | for i in 0..n { 36 | for j in 0..k { 37 | c[[i, j]] = (sum_col[i] + sum_row[j] - delta[[i, j]] - sum_all) / (2.).into(); 38 | } 39 | } 40 | c 41 | } 42 | -------------------------------------------------------------------------------- /crates/wasm/tests/quality_metrics.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/quality_metrics.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testStress")] 11 | fn test_stress(); 12 | #[wasm_bindgen(js_name = "testCrossingNumber")] 13 | fn test_crossing_number(); 14 | #[wasm_bindgen(js_name = "testCrossingNumberWithDrawingTorus2d")] 15 | fn test_crossing_number_with_drawing_torus_2d(); 16 | #[wasm_bindgen(js_name = "testNeighborhoodPreservation")] 17 | fn test_neighborhood_preservation(); 18 | #[wasm_bindgen(js_name = "testQualityMetricsIntegration")] 19 | fn test_quality_metrics_integration(); 20 | } 21 | 22 | /// Test the stress metric calculation 23 | #[wasm_bindgen_test] 24 | pub fn stress() { 25 | test_stress(); 26 | } 27 | 28 | /// Test the crossing number calculation in Euclidean 2D space 29 | #[wasm_bindgen_test] 30 | pub fn crossing_number() { 31 | test_crossing_number(); 32 | } 33 | 34 | /// Test the crossing number calculation in torus 2D space 35 | #[wasm_bindgen_test] 36 | pub fn crossing_number_with_drawing_torus_2d() { 37 | test_crossing_number_with_drawing_torus_2d(); 38 | } 39 | 40 | /// Test the neighborhood preservation metric 41 | #[wasm_bindgen_test] 42 | pub fn neighborhood_preservation() { 43 | test_neighborhood_preservation(); 44 | } 45 | 46 | /// Test integration of quality metrics with layout algorithms 47 | #[wasm_bindgen_test] 48 | pub fn quality_metrics_integration() { 49 | test_quality_metrics_integration(); 50 | } 51 | -------------------------------------------------------------------------------- /crates/drawing/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod drawing; 2 | mod metric; 3 | 4 | use ndarray::prelude::*; 5 | use num_traits::{FloatConst, FromPrimitive, Signed}; 6 | use std::hash::Hash; 7 | 8 | /// A trait constraint for types that can be used as indices in a drawing (typically node identifiers). 9 | /// 10 | /// Requires the type to implement `Eq` and `Hash`. 11 | pub trait DrawingIndex: Eq + Hash {} 12 | impl DrawingIndex for T where T: Eq + Hash {} 13 | 14 | /// A trait constraint for types that can be used as coordinate values in a drawing. 15 | /// 16 | /// Requires the type to implement `NdFloat` (from `ndarray`) and `FromPrimitive` (from `num_traits`). 17 | pub trait DrawingValue: 18 | NdFloat + FromPrimitive + FloatConst + Signed + Into + From 19 | { 20 | } 21 | impl DrawingValue for T where 22 | T: NdFloat + FromPrimitive + FloatConst + Signed + Into + From 23 | { 24 | } 25 | 26 | /// Represents a drawing, mapping indices (nodes) to coordinate arrays. 27 | pub use drawing::{ 28 | drawing_euclidean::DrawingEuclidean, drawing_euclidean_2d::DrawingEuclidean2d, 29 | drawing_hyperbolic_2d::DrawingHyperbolic2d, drawing_spherical_2d::DrawingSpherical2d, 30 | drawing_torus2d::DrawingTorus2d, Drawing, 31 | }; 32 | 33 | pub use metric::{ 34 | metric_euclidean::{DeltaEuclidean, MetricEuclidean}, 35 | metric_euclidean_2d::{DeltaEuclidean2d, MetricEuclidean2d}, 36 | metric_hyperbolic_2d::{DeltaHyperbolic2d, MetricHyperbolic2d}, 37 | metric_spherical_2d::{DeltaSpherical2d, MetricSpherical2d}, 38 | metric_torus2d::{DeltaTorus2d, MetricTorus2d, TorusValue}, 39 | Delta, Metric, MetricCartesian, 40 | }; 41 | -------------------------------------------------------------------------------- /crates/python/src/layout/mod.rs: -------------------------------------------------------------------------------- 1 | /// Graph layout algorithms for the Python bindings 2 | /// 3 | /// This module provides various graph layout algorithms that compute positions for graph nodes 4 | /// in different geometric spaces, optimizing for various aesthetic criteria like distance preservation, 5 | /// stress minimization, and minimal edge crossings. 6 | /// 7 | /// # Submodules 8 | /// 9 | /// - `mds`: Multidimensional Scaling algorithms 10 | /// - `rdmds`: Resistance-distance MDS for spectral embeddings 11 | /// - `kamada_kawai`: Kamada-Kawai force-directed layout algorithm 12 | /// - `overwrap_removal`: Algorithms to remove overlaps between nodes 13 | /// - `stress_majorization`: Stress majorization layout algorithm 14 | /// - `sgd`: Stochastic Gradient Descent based layout algorithms 15 | mod kamada_kawai; 16 | mod mds; 17 | mod overwrap_removal; 18 | mod rdmds; 19 | mod sgd; 20 | mod stress_majorization; 21 | 22 | use pyo3::prelude::*; 23 | 24 | /// Registers layout-related classes and functions with the Python module 25 | /// 26 | /// This function adds all the graph layout algorithms to the Python module, 27 | /// making them available to be instantiated and used from Python code. 28 | /// These algorithms determine the positions of nodes in a graph drawing, 29 | /// optimizing for various aesthetic criteria. 30 | pub fn register(py: Python<'_>, m: &Bound) -> PyResult<()> { 31 | // Register various layout algorithm implementations 32 | mds::register(py, m)?; 33 | rdmds::register(py, m)?; 34 | kamada_kawai::register(py, m)?; 35 | overwrap_removal::register(py, m)?; 36 | stress_majorization::register(py, m)?; 37 | sgd::register(py, m)?; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /crates/wasm/src/edge_bundling.rs: -------------------------------------------------------------------------------- 1 | //! Edge bundling algorithms for WebAssembly. 2 | //! 3 | //! This module provides WebAssembly bindings for edge bundling algorithms, 4 | //! which improve the readability of dense graphs by routing related edges 5 | //! along similar paths to reduce visual clutter. 6 | //! 7 | //! The main algorithm provided is Force-Directed Edge Bundling (FDEB), 8 | //! which uses forces between edge segments to bundle similar edges together. 9 | 10 | use crate::{drawing::JsDrawingEuclidean2d, graph::JsGraph}; 11 | use petgraph_edge_bundling_fdeb::{fdeb, EdgeBundlingOptions}; 12 | use std::collections::HashMap; 13 | use wasm_bindgen::prelude::*; 14 | 15 | /// Applies the Force-Directed Edge Bundling (FDEB) algorithm to a graph drawing. 16 | /// 17 | /// The FDEB algorithm reduces visual clutter in dense graphs by bundling similar edges 18 | /// together, making the graph structure more apparent. The algorithm works by 19 | /// subdividing edges into segments and applying forces between these segments to 20 | /// bundle related edges together. 21 | /// 22 | /// Takes a graph and a drawing containing node positions, and returns a map from 23 | /// edge indices to arrays of line segments. Each line segment is an array of points 24 | /// representing the bundled edge path. 25 | #[wasm_bindgen(js_name = fdeb)] 26 | pub fn js_fdeb(graph: &JsGraph, drawing: JsDrawingEuclidean2d) -> JsValue { 27 | let options = EdgeBundlingOptions::::new(); 28 | let bends = fdeb(graph.graph(), drawing.drawing(), &options) 29 | .into_iter() 30 | .map(|(e, lines)| (e.index(), lines)) 31 | .collect::>(); 32 | serde_wasm_bindgen::to_value(&bends).unwrap() 33 | } 34 | -------------------------------------------------------------------------------- /memory-bank/productContext.md: -------------------------------------------------------------------------------- 1 | # Product Context: egraph-rs 2 | 3 | ## Why This Project Exists 4 | 5 | egraph-rs exists to provide a comprehensive, high-performance graph manipulation and visualization library in Rust. It fills the need for a native Rust implementation with cross-language bindings, enabling graph algorithms and visualizations across various platforms and use cases. 6 | 7 | ## Problems It Solves 8 | 9 | - **Performance Gap**: Provides efficient Rust implementations of graph algorithms that outperform many existing libraries 10 | - **Language Barrier**: Bridges multiple programming environments (Rust, Python, JavaScript) with consistent APIs 11 | - **Visualization Variety**: Offers multiple geometric spaces for graph visualization (Euclidean, Spherical, Hyperbolic, Torus) 12 | - **Quality Assessment**: Provides metrics to quantitatively evaluate the quality of graph layouts 13 | 14 | ## How It Should Work 15 | 16 | The library should provide: 17 | 18 | - Intuitive APIs for creating and manipulating graphs 19 | - Efficient implementations of common graph algorithms 20 | - Various layout algorithms to position graph nodes effectively 21 | - Quality metrics to evaluate visualization effectiveness 22 | - Seamless integration with other languages and environments 23 | 24 | ## User Experience Goals 25 | 26 | - **Consistent API**: Maintain similar interfaces across all platforms 27 | - **Performance**: Prioritize speed and memory efficiency 28 | - **Flexibility**: Support various graph types and visualization techniques 29 | - **Extensibility**: Allow users to extend the system with custom algorithms 30 | - **Documentation**: Provide clear examples and documentation across all parts of the library 31 | -------------------------------------------------------------------------------- /crates/wasm/tests/digraph.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/digraph.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testDiGraphConstructor")] 11 | fn test_digraph_constructor(); 12 | #[wasm_bindgen(js_name = "testNodeOperations")] 13 | fn test_node_operations(); 14 | #[wasm_bindgen(js_name = "testEdgeOperations")] 15 | fn test_edge_operations(); 16 | #[wasm_bindgen(js_name = "testGraphTraversal")] 17 | fn test_graph_traversal(); 18 | #[wasm_bindgen(js_name = "testInOutNeighbors")] 19 | fn test_in_out_neighbors(); 20 | #[wasm_bindgen(js_name = "testDiGraphWithDrawing")] 21 | fn test_digraph_with_drawing(); 22 | } 23 | 24 | /// Test basic instantiation of DiGraph class 25 | #[wasm_bindgen_test] 26 | pub fn digraph_constructor() { 27 | test_digraph_constructor(); 28 | } 29 | 30 | /// Test node operations (add, remove, get) 31 | #[wasm_bindgen_test] 32 | pub fn node_operations() { 33 | test_node_operations(); 34 | } 35 | 36 | /// Test edge operations (add, remove, get) 37 | #[wasm_bindgen_test] 38 | pub fn edge_operations() { 39 | test_edge_operations(); 40 | } 41 | 42 | /// Test graph traversal and iteration 43 | #[wasm_bindgen_test] 44 | pub fn graph_traversal() { 45 | test_graph_traversal(); 46 | } 47 | 48 | /// Test directed-specific functionality (in/out neighbors) 49 | #[wasm_bindgen_test] 50 | pub fn in_out_neighbors() { 51 | test_in_out_neighbors(); 52 | } 53 | 54 | /// Test integration with drawing component 55 | #[wasm_bindgen_test] 56 | pub fn digraph_with_drawing() { 57 | test_digraph_with_drawing(); 58 | } 59 | -------------------------------------------------------------------------------- /crates/python/.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | linux: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: messense/maturin-action@v1 13 | with: 14 | manylinux: auto 15 | command: build 16 | args: --release -o dist 17 | - name: Upload wheels 18 | uses: actions/upload-artifact@v2 19 | with: 20 | name: wheels 21 | path: dist 22 | 23 | windows: 24 | runs-on: windows-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: messense/maturin-action@v1 28 | with: 29 | command: build 30 | args: --release --no-sdist -o dist 31 | - name: Upload wheels 32 | uses: actions/upload-artifact@v2 33 | with: 34 | name: wheels 35 | path: dist 36 | 37 | macos: 38 | runs-on: macos-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: messense/maturin-action@v1 42 | with: 43 | command: build 44 | args: --release --no-sdist -o dist --universal2 45 | - name: Upload wheels 46 | uses: actions/upload-artifact@v2 47 | with: 48 | name: wheels 49 | path: dist 50 | 51 | release: 52 | name: Release 53 | runs-on: ubuntu-latest 54 | if: "startsWith(github.ref, 'refs/tags/')" 55 | needs: [ macos, windows, linux ] 56 | steps: 57 | - uses: actions/download-artifact@v2 58 | with: 59 | name: wheels 60 | - name: Publish to PyPI 61 | uses: messense/maturin-action@v1 62 | env: 63 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 64 | with: 65 | command: upload 66 | args: --skip-existing * -------------------------------------------------------------------------------- /crates/wasm/tests/stress_majorization.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/stress_majorization.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testStressMajorizationConstructor")] 11 | fn test_stress_majorization_constructor(); 12 | #[wasm_bindgen(js_name = "testStressMajorizationApply")] 13 | fn test_stress_majorization_apply(); 14 | #[wasm_bindgen(js_name = "testStressMajorizationRun")] 15 | fn test_stress_majorization_run(); 16 | #[wasm_bindgen(js_name = "testStressMajorizationIntegration")] 17 | fn test_stress_majorization_integration(); 18 | #[wasm_bindgen(js_name = "testStressMajorizationParameters")] 19 | fn test_stress_majorization_parameters(); 20 | } 21 | 22 | /// Test basic instantiation of StressMajorization class 23 | #[wasm_bindgen_test] 24 | pub fn stress_majorization_constructor() { 25 | test_stress_majorization_constructor(); 26 | } 27 | 28 | /// Test applying a single iteration of the stress majorization algorithm 29 | #[wasm_bindgen_test] 30 | pub fn stress_majorization_apply() { 31 | test_stress_majorization_apply(); 32 | } 33 | 34 | /// Test running the complete stress majorization algorithm 35 | #[wasm_bindgen_test] 36 | pub fn stress_majorization_run() { 37 | test_stress_majorization_run(); 38 | } 39 | 40 | /// Test integration with other components and stress reduction 41 | #[wasm_bindgen_test] 42 | pub fn stress_majorization_integration() { 43 | test_stress_majorization_integration(); 44 | } 45 | 46 | /// Test getter and setter methods for epsilon and max_iterations 47 | #[wasm_bindgen_test] 48 | pub fn stress_majorization_parameters() { 49 | test_stress_majorization_parameters(); 50 | } 51 | -------------------------------------------------------------------------------- /crates/linalg/rdmds/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # RdMds (Resistance-distance MDS) 2 | //! 3 | //! This crate provides spectral embedding computation for graphs using the 4 | //! Resistance-distance Multidimensional Scaling (RdMds) algorithm. 5 | //! 6 | //! RdMds computes d-dimensional coordinates by finding the smallest non-zero 7 | //! eigenvalues and eigenvectors of the graph Laplacian matrix. These spectral 8 | //! coordinates can be used as initial embeddings for graph layout algorithms. 9 | //! 10 | //! ## Example 11 | //! 12 | //! ```rust 13 | //! use petgraph::Graph; 14 | //! use petgraph_linalg_rdmds::RdMds; 15 | //! use rand::thread_rng; 16 | //! 17 | //! // Create a graph 18 | //! let mut graph = Graph::new_undirected(); 19 | //! let a = graph.add_node(()); 20 | //! let b = graph.add_node(()); 21 | //! let c = graph.add_node(()); 22 | //! graph.add_edge(a, b, ()); 23 | //! graph.add_edge(b, c, ()); 24 | //! graph.add_edge(c, a, ()); 25 | //! 26 | //! // Compute spectral embedding 27 | //! let mut rng = thread_rng(); 28 | //! let embedding = RdMds::new() 29 | //! .d(2) 30 | //! .shift(1e-3f32) 31 | //! .embedding(&graph, |_| 1.0f32, &mut rng); 32 | //! 33 | //! // embedding is an Array2 where embedding.row(i) contains the 2D coordinate for node i 34 | //! println!("Embedding shape: {:?}", embedding.dim()); 35 | //! ``` 36 | //! 37 | //! ## Computational Complexity 38 | //! 39 | //! - Eigenvalue computation: O(d(|V| + |E|)) 40 | //! - Total: O(d(|V| + |E|)) 41 | //! 42 | //! where d is the number of spectral dimensions, |V| is the number of vertices, 43 | //! and |E| is the number of edges. 44 | 45 | mod eigenvalue; 46 | mod rdmds; 47 | 48 | pub use eigenvalue::{ 49 | IncompleteCholeskyPreconditioner, LaplacianStructure, compute_smallest_eigenvalues, 50 | eigendecomposition, 51 | }; 52 | pub use rdmds::RdMds; 53 | -------------------------------------------------------------------------------- /crates/python/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Python bindings for the egraph-rs library. 2 | /// 3 | /// This module provides a Python interface to the egraph-rs library, which is a collection 4 | /// of graph visualization and layout algorithms implemented in Rust. The library is designed 5 | /// to provide efficient implementations of graph algorithms that can be used from Python. 6 | /// 7 | /// # Module Organization 8 | /// 9 | /// - `graph`: Graph data structures (Graph, DiGraph) 10 | /// - `drawing`: Drawing spaces (Euclidean, Hyperbolic, Spherical, Torus) 11 | /// - `distance_matrix`: Distance matrix operations 12 | /// - `layout`: Layout algorithms (SGD, MDS, Kamada-Kawai, etc.) 13 | /// - `algorithm`: Graph algorithms (shortest path) 14 | /// - `clustering`: Community detection algorithms (Louvain, Label Propagation, etc.) 15 | /// - `quality_metrics`: Layout quality evaluation metrics 16 | /// - `rng`: Random number generation utilities 17 | use pyo3::prelude::*; 18 | 19 | mod algorithm; 20 | mod array; 21 | mod clustering; 22 | mod distance_matrix; 23 | mod drawing; 24 | mod graph; 25 | mod layout; 26 | mod quality_metrics; 27 | mod rng; 28 | 29 | pub type FloatType = f64; 30 | 31 | /// Creates and initializes the egraph Python module. 32 | /// 33 | /// This function is the main entry point for the Python module. It registers all submodules 34 | /// and their associated classes and functions. 35 | #[pymodule] 36 | fn egraph(py: Python<'_>, m: &Bound) -> PyResult<()> { 37 | graph::register(py, m)?; 38 | drawing::register(py, m)?; 39 | distance_matrix::register(py, m)?; 40 | rng::register(py, m)?; 41 | layout::register(py, m)?; 42 | algorithm::register(py, m)?; 43 | quality_metrics::register(py, m)?; 44 | clustering::register(py, m)?; 45 | array::register(py, m)?; 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /crates/wasm/tests/kamada_kawai.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/kamada_kawai.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testKamadaKawaiConstructor")] 11 | fn test_kamada_kawai_constructor(); 12 | #[wasm_bindgen(js_name = "testKamadaKawaiEpsilon")] 13 | fn test_kamada_kawai_epsilon(); 14 | #[wasm_bindgen(js_name = "testKamadaKawaiSelectNode")] 15 | fn test_kamada_kawai_select_node(); 16 | #[wasm_bindgen(js_name = "testKamadaKawaiApplyToNode")] 17 | fn test_kamada_kawai_apply_to_node(); 18 | #[wasm_bindgen(js_name = "testKamadaKawaiRun")] 19 | fn test_kamada_kawai_run(); 20 | #[wasm_bindgen(js_name = "testKamadaKawaiIntegration")] 21 | fn test_kamada_kawai_integration(); 22 | } 23 | 24 | /// Test basic instantiation of KamadaKawai class 25 | #[wasm_bindgen_test] 26 | pub fn kamada_kawai_constructor() { 27 | test_kamada_kawai_constructor(); 28 | } 29 | 30 | /// Test epsilon parameter getter and setter 31 | #[wasm_bindgen_test] 32 | pub fn kamada_kawai_epsilon() { 33 | test_kamada_kawai_epsilon(); 34 | } 35 | 36 | /// Test node selection functionality 37 | #[wasm_bindgen_test] 38 | pub fn kamada_kawai_select_node() { 39 | test_kamada_kawai_select_node(); 40 | } 41 | 42 | /// Test applying the algorithm to a single node 43 | #[wasm_bindgen_test] 44 | pub fn kamada_kawai_apply_to_node() { 45 | test_kamada_kawai_apply_to_node(); 46 | } 47 | 48 | /// Test running the complete algorithm 49 | #[wasm_bindgen_test] 50 | pub fn kamada_kawai_run() { 51 | test_kamada_kawai_run(); 52 | } 53 | 54 | /// Test integration with other components 55 | #[wasm_bindgen_test] 56 | pub fn kamada_kawai_integration() { 57 | test_kamada_kawai_integration(); 58 | } 59 | -------------------------------------------------------------------------------- /crates/python/src/clustering/infomap.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::{GraphType, PyGraphAdapter}; 2 | use petgraph_clustering::{CommunityDetection, InfoMap}; 3 | use pyo3::prelude::*; 4 | use std::collections::HashMap; 5 | 6 | /// InfoMap community detection algorithm. 7 | /// 8 | /// InfoMap is an information-theoretic approach to community detection that 9 | /// minimizes the expected description length of a random walk on the graph. 10 | /// 11 | /// Parameters: 12 | /// num_trials (int, optional): Number of trials to perform. Default is 10. 13 | /// seed (int, optional): Seed for random number generation. Default is None. 14 | /// 15 | /// Example: 16 | /// >>> import egraph as eg 17 | /// >>> graph = eg.Graph() 18 | /// >>> # Add nodes and edges... 19 | /// >>> infomap = eg.clustering.InfoMap(num_trials=10, seed=42) 20 | /// >>> communities = infomap.detect_communities(graph) 21 | /// >>> # communities is a dict mapping node indices to community IDs 22 | #[pyclass(name = "InfoMap")] 23 | pub struct PyInfoMap { 24 | instance: InfoMap, 25 | } 26 | 27 | #[pymethods] 28 | impl PyInfoMap { 29 | #[new] 30 | #[pyo3(signature = ())] 31 | fn new() -> Self { 32 | PyInfoMap { 33 | instance: InfoMap::new(), 34 | } 35 | } 36 | 37 | /// Detect communities in the given graph. 38 | /// 39 | /// Returns: 40 | /// dict: A dictionary mapping node indices to community IDs. 41 | #[pyo3(signature = (graph))] 42 | fn detect_communities(&self, graph: &PyGraphAdapter) -> HashMap { 43 | let map = match graph.graph() { 44 | GraphType::Graph(graph) => self.instance.detect_communities(graph), 45 | GraphType::DiGraph(graph) => self.instance.detect_communities(graph), 46 | }; 47 | map.into_iter() 48 | .map(|(u, c)| (u.index(), c)) 49 | .collect::>() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/python/examples/sgd_torus.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import egraph as eg 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def main(): 7 | nx_graph = nx.les_miserables_graph() 8 | graph = eg.Graph() 9 | indices = {} 10 | for u in nx_graph.nodes: 11 | indices[u] = graph.add_node(u) 12 | for u, v in nx_graph.edges: 13 | graph.add_edge(indices[u], indices[v], (u, v)) 14 | 15 | size = nx.diameter(nx_graph) * 1.5 16 | d = eg.all_sources_bfs( 17 | graph, 18 | 1 / size, # edge length 19 | ) 20 | drawing = eg.DrawingTorus2d.initial_placement(graph) 21 | rng = eg.Rng.seed_from(0) # random seed 22 | sgd = eg.FullSgd.new_with_distance_matrix(d) 23 | scheduler = sgd.scheduler( 24 | 100, # number of iterations 25 | 0.1, # eps: eta_min = eps * min d[i, j] ^ 2 26 | ) 27 | 28 | def step(eta): 29 | print(eg.stress(drawing, d)) 30 | sgd.shuffle(rng) 31 | sgd.apply(drawing, eta) 32 | scheduler.run(step) 33 | 34 | pos = {u: (drawing.x(i) * size, drawing.y(i) * size) 35 | for u, i in indices.items()} 36 | nx_edge_graph = nx.Graph() 37 | edge_pos = {} 38 | for e in graph.edge_indices(): 39 | u, v = graph.edge_endpoints(e) 40 | segments = drawing.edge_segments(u, v) 41 | for i, ((x1, y1), (x2, y2)) in enumerate(segments): 42 | eu = f'{u}:{v}:{i}:0' 43 | ev = f'{u}:{v}:{i}:1' 44 | nx_edge_graph.add_edge(eu, ev) 45 | edge_pos[eu] = (x1 * size, y1 * size) 46 | edge_pos[ev] = (x2 * size, y2 * size) 47 | fig, ax = plt.subplots(figsize=(8, 8)) 48 | ax.set_xlim(0, size) 49 | ax.set_ylim(0, size) 50 | nx.draw_networkx_nodes(nx_graph, pos, ax=ax) 51 | nx.draw_networkx_edges(nx_edge_graph, edge_pos, ax=ax) 52 | plt.savefig('tmp/sgd_torus.png') 53 | 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /js/examples/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { BrowserRouter as Router, Route, Link } from "react-router-dom"; 3 | import { 4 | Home, 5 | ExampleEdgeBundling, 6 | ExampleKamadaKawai, 7 | ExampleHyperbolicGeometry, 8 | ExampleMds, 9 | ExampleSgd, 10 | ExampleSphericalGeometry, 11 | ExampleStressMajorization, 12 | ExampleTorus, 13 | ExampleOverwrapRemoval, 14 | } from "./pages"; 15 | 16 | createRoot(document.getElementById("content")).render( 17 | 18 |
19 |
20 | 29 |
30 |
31 |
32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | 46 | 50 | 51 |
52 |
53 |
54 |
55 | ); 56 | -------------------------------------------------------------------------------- /crates/python/docs/source/getting_started/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | This guide will help you install egraph and its dependencies. 5 | 6 | Basic Installation 7 | ------------------ 8 | 9 | The easiest way to install egraph is using pip: 10 | 11 | .. code-block:: bash 12 | 13 | pip install egraph 14 | 15 | This will install the latest stable version of egraph from PyPI. 16 | 17 | Requirements 18 | ------------ 19 | 20 | egraph requires: 21 | 22 | * Python 3.6 or later 23 | * NumPy (automatically installed as a dependency) 24 | 25 | Optional Dependencies 26 | --------------------- 27 | 28 | For visualization and working with examples, you may want to install: 29 | 30 | .. code-block:: bash 31 | 32 | pip install matplotlib networkx 33 | 34 | * **matplotlib**: For creating visualizations of graph layouts 35 | * **networkx**: For creating and manipulating graphs, and for using built-in graph datasets 36 | 37 | Development Installation 38 | ------------------------ 39 | 40 | If you want to contribute to egraph or use the latest development version, you can install from source: 41 | 42 | .. code-block:: bash 43 | 44 | # Clone the repository 45 | git clone https://github.com/likr/egraph-rs.git 46 | cd egraph-rs/crates/python 47 | 48 | # Install in development mode 49 | pip install maturin 50 | maturin develop 51 | 52 | This requires: 53 | 54 | * Rust toolchain (install from https://rustup.rs/) 55 | * maturin (Python package for building Rust extensions) 56 | 57 | Verifying Installation 58 | ---------------------- 59 | 60 | To verify that egraph is installed correctly, run: 61 | 62 | .. code-block:: python 63 | 64 | import egraph as eg 65 | print(eg.__version__) 66 | 67 | You should see the version number printed without any errors. 68 | 69 | Next Steps 70 | ---------- 71 | 72 | Now that you have egraph installed, proceed to the :doc:`quickstart` guide to learn how to use it. 73 | -------------------------------------------------------------------------------- /crates/python/docs/source/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | This tutorial series provides a comprehensive guide to using egraph for graph visualization. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | 01_graph_basics 10 | 02_layout_algorithms 11 | 03_drawing_and_visualization 12 | 13 | Learning Path 14 | ------------- 15 | 16 | Follow these tutorials in order to build your understanding: 17 | 18 | 1. **Graph Basics** 19 | 20 | :doc:`01_graph_basics` - Learn how to create and manipulate graphs, work with nodes and edges, and integrate with NetworkX. 21 | 22 | 2. **Layout Algorithms** 23 | 24 | :doc:`02_layout_algorithms` - Explore different layout algorithms, understand when to use each one, and customize layouts for your needs. 25 | 26 | 3. **Drawing and Visualization** 27 | 28 | :doc:`03_drawing_and_visualization` - Master different drawing spaces, create visualizations with matplotlib, and apply best practices. 29 | 30 | What You'll Learn 31 | ----------------- 32 | 33 | By completing these tutorials, you will be able to: 34 | 35 | * Create and manipulate graphs with egraph 36 | * Choose the right layout algorithm for your graph 37 | * Apply layouts in different geometric spaces 38 | * Integrate egraph with NetworkX for analysis 39 | * Create publication-quality visualizations 40 | * Optimize layouts for different graph types and sizes 41 | 42 | Prerequisites 43 | ------------- 44 | 45 | Before starting these tutorials, you should: 46 | 47 | * Have egraph installed (see :doc:`../getting_started/installation`) 48 | * Be familiar with basic Python programming 49 | * Have matplotlib and NetworkX installed (optional but recommended) 50 | 51 | Next Steps 52 | ---------- 53 | 54 | After completing the tutorials: 55 | 56 | * Explore :doc:`../examples/index` for more complex use cases 57 | * Check the :doc:`../api/index` for detailed API documentation 58 | * Review :doc:`../getting_started/overview` for advanced features 59 | -------------------------------------------------------------------------------- /crates/python/src/clustering/spectral.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::{GraphType, PyGraphAdapter}; 2 | use petgraph_clustering::{CommunityDetection, Spectral}; 3 | use pyo3::prelude::*; 4 | use std::collections::HashMap; 5 | 6 | /// Spectral Clustering community detection algorithm. 7 | /// 8 | /// Spectral Clustering uses the eigenvectors of the graph Laplacian matrix to 9 | /// partition the graph into communities. 10 | /// 11 | /// Parameters: 12 | /// n_clusters (int, optional): Number of clusters to identify. Default is 2. 13 | /// seed (int, optional): Seed for random number generation. Default is None. 14 | /// 15 | /// Example: 16 | /// >>> import egraph as eg 17 | /// >>> graph = eg.Graph() 18 | /// >>> # Add nodes and edges... 19 | /// >>> spectral = eg.clustering.SpectralClustering(n_clusters=5, seed=42) 20 | /// >>> communities = spectral.detect_communities(graph) 21 | /// >>> # communities is a dict mapping node indices to community IDs 22 | #[pyclass(name = "SpectralClustering")] 23 | pub struct PySpectralClustering { 24 | instance: Spectral, 25 | } 26 | 27 | #[pymethods] 28 | impl PySpectralClustering { 29 | #[new] 30 | #[pyo3(signature = (k))] 31 | fn new(k: usize) -> Self { 32 | PySpectralClustering { 33 | instance: Spectral::new(k), 34 | } 35 | } 36 | 37 | /// Detect communities in the given graph. 38 | /// 39 | /// Returns: 40 | /// dict: A dictionary mapping node indices to community IDs. 41 | #[pyo3(signature = (graph))] 42 | fn detect_communities(&self, graph: &PyGraphAdapter) -> HashMap { 43 | let map = match graph.graph() { 44 | GraphType::Graph(graph) => self.instance.detect_communities(graph), 45 | GraphType::DiGraph(graph) => self.instance.detect_communities(graph), 46 | }; 47 | map.into_iter() 48 | .map(|(u, c)| (u.index(), c)) 49 | .collect::>() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/wasm/src/rng.rs: -------------------------------------------------------------------------------- 1 | //! Random number generation for WebAssembly. 2 | //! 3 | //! This module provides a WebAssembly binding for random number generation 4 | //! based on the Rust `rand` crate. 5 | 6 | use rand::prelude::*; 7 | use wasm_bindgen::prelude::*; 8 | 9 | /// WebAssembly binding for random number generation. 10 | /// 11 | /// This struct provides a JavaScript interface to Rust's random number generation, 12 | /// exposing the capabilities of StdRng through WebAssembly. 13 | #[wasm_bindgen(js_name = Rng)] 14 | pub struct JsRng { 15 | rng: StdRng, 16 | } 17 | 18 | impl JsRng { 19 | /// Returns a mutable reference to the internal RNG. 20 | /// 21 | /// This method is intended for internal use by other Rust modules that need 22 | /// access to the random number generator. 23 | pub fn get_mut(&mut self) -> &mut StdRng { 24 | &mut self.rng 25 | } 26 | } 27 | 28 | impl Default for JsRng { 29 | fn default() -> Self { 30 | Self::new() 31 | } 32 | } 33 | 34 | #[wasm_bindgen(js_class = Rng)] 35 | impl JsRng { 36 | /// Creates a new random number generator using system entropy. 37 | /// 38 | /// This constructor creates a cryptographically secure random number generator 39 | /// that is suitable for most applications. 40 | #[wasm_bindgen(constructor)] 41 | pub fn new() -> JsRng { 42 | JsRng { 43 | rng: StdRng::from_entropy(), 44 | } 45 | } 46 | 47 | /// Creates a new random number generator with a specific seed. 48 | /// 49 | /// This method allows for reproducible random number sequences by 50 | /// providing a seed value. 51 | /// 52 | /// @param {number} seed - A 64-bit unsigned integer to use as the seed 53 | /// @returns {Rng} A new seeded random number generator 54 | #[wasm_bindgen(js_name = "seedFrom")] 55 | pub fn seed_from(seed: u64) -> JsRng { 56 | JsRng { 57 | rng: StdRng::seed_from_u64(seed), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/node_resolution.rs: -------------------------------------------------------------------------------- 1 | use petgraph_drawing::{Delta, Drawing, DrawingValue, Metric}; 2 | 3 | /// Calculates the node resolution metric for a graph layout. 4 | /// 5 | /// Node resolution evaluates how well nodes are distributed in the drawing space, 6 | /// assessing whether nodes are too close to each other which can hamper readability. 7 | /// The metric is based on a comparison of actual inter-node distances to an ideal 8 | /// minimum distance that depends on the number of nodes and the maximum distance 9 | /// between any two nodes. 10 | /// 11 | /// This implementation calculates the sum of squared violations of the minimum 12 | /// distance rule. A lower value indicates better node resolution (fewer violations). 13 | /// 14 | /// # Parameters 15 | /// 16 | /// * `drawing`: The layout of the graph 17 | /// 18 | /// # Returns 19 | /// 20 | /// A value of type `S` representing the node resolution metric. Higher values 21 | /// indicate better node spacing. 22 | /// 23 | /// # Type Parameters 24 | /// 25 | /// * `Diff`: A type for representing differences between metric values 26 | /// * `D`: A drawing type 27 | /// * `M`: Metric type used in the drawing 28 | /// * `S`: Numeric type for calculations 29 | pub fn node_resolution(drawing: &D) -> S 30 | where 31 | D: Drawing, 32 | Diff: Delta, 33 | M: Copy + Metric, 34 | S: DrawingValue, 35 | { 36 | let n = drawing.len(); 37 | let r = S::one() / S::from_usize(n).unwrap().sqrt(); 38 | 39 | let mut d_max = S::zero(); 40 | for i in 1..n { 41 | for j in 0..i { 42 | let delta = drawing.delta(i, j); 43 | d_max = d_max.max(delta.norm()); 44 | } 45 | } 46 | 47 | let mut s = S::zero(); 48 | for i in 1..n { 49 | for j in 0..i { 50 | let delta = drawing.delta(i, j); 51 | s += (S::one() - delta.norm() / (r * d_max)) 52 | .powi(2) 53 | .max(S::zero()); 54 | } 55 | } 56 | s 57 | } 58 | -------------------------------------------------------------------------------- /crates/wasm/tests/drawing_torus_2d.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/drawing_torus_2d.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testDrawingTorus2dConstructor")] 11 | fn test_drawing_torus_2d_constructor(); 12 | #[wasm_bindgen(js_name = "testNodeCoordinates")] 13 | fn test_node_coordinates(); 14 | #[wasm_bindgen(js_name = "testDrawingWithGraph")] 15 | fn test_drawing_with_graph(); 16 | #[wasm_bindgen(js_name = "testEdgeSegments")] 17 | fn test_edge_segments(); 18 | #[wasm_bindgen(js_name = "testTorusWrapping")] 19 | fn test_torus_wrapping(); 20 | #[wasm_bindgen(js_name = "testCoordinateValidation")] 21 | fn test_coordinate_validation(); 22 | #[wasm_bindgen(js_name = "testLayoutIntegration")] 23 | fn test_layout_integration(); 24 | } 25 | 26 | /// Test basic instantiation of DrawingTorus2d class 27 | #[wasm_bindgen_test] 28 | pub fn drawing_torus_2d_constructor() { 29 | test_drawing_torus_2d_constructor(); 30 | } 31 | 32 | /// Test node coordinate operations (get/set x,y) 33 | #[wasm_bindgen_test] 34 | pub fn node_coordinates() { 35 | test_node_coordinates(); 36 | } 37 | 38 | /// Test integration with Graph class 39 | #[wasm_bindgen_test] 40 | pub fn drawing_with_graph() { 41 | test_drawing_with_graph(); 42 | } 43 | 44 | /// Test edge segment representation on a torus surface 45 | #[wasm_bindgen_test] 46 | pub fn edge_segments() { 47 | test_edge_segments(); 48 | } 49 | 50 | /// Test torus wrapping behavior (coordinates wrapping around) 51 | #[wasm_bindgen_test] 52 | pub fn torus_wrapping() { 53 | test_torus_wrapping(); 54 | } 55 | 56 | /// Test coordinate validation and normalization 57 | #[wasm_bindgen_test] 58 | pub fn coordinate_validation() { 59 | test_coordinate_validation(); 60 | } 61 | 62 | /// Test integration with layout algorithms 63 | #[wasm_bindgen_test] 64 | pub fn layout_integration() { 65 | test_layout_integration(); 66 | } 67 | -------------------------------------------------------------------------------- /crates/python/src/drawing/mod.rs: -------------------------------------------------------------------------------- 1 | /// Drawing classes for different geometric spaces 2 | /// 3 | /// This module provides classes that represent graph drawings in various geometric spaces. 4 | /// A drawing maps nodes of a graph to coordinates in some geometric space, which can be 5 | /// 2D or higher dimensional, and can use different geometries (Euclidean, spherical, etc.). 6 | /// 7 | /// # Submodules 8 | /// 9 | /// - `drawing_base`: Base class for all drawing types 10 | /// - `drawing_euclidean_2d`: 2D Euclidean space drawings with (x,y) coordinates 11 | /// - `drawing_euclidean`: N-dimensional Euclidean space drawings 12 | /// - `drawing_hyperbolic_2d`: 2D Hyperbolic space drawings 13 | /// - `drawing_spherical_2d`: 2D Spherical space drawings 14 | /// - `drawing_torus_2d`: 2D Torus space drawings with periodic boundary conditions 15 | mod drawing_base; 16 | mod drawing_euclidean; 17 | mod drawing_euclidean_2d; 18 | mod drawing_hyperbolic_2d; 19 | mod drawing_spherical_2d; 20 | mod drawing_torus_2d; 21 | 22 | pub use drawing_base::*; 23 | pub use drawing_euclidean::*; 24 | pub use drawing_euclidean_2d::*; 25 | pub use drawing_hyperbolic_2d::*; 26 | pub use drawing_spherical_2d::*; 27 | pub use drawing_torus_2d::*; 28 | 29 | use pyo3::prelude::*; 30 | 31 | /// Registers drawing-related classes with the Python module 32 | /// 33 | /// This function adds all the drawing classes to the Python module, 34 | /// making them available to be instantiated and used from Python code. 35 | /// Drawing classes provide the foundation for graph visualization by 36 | /// mapping nodes to positions in various geometric spaces. 37 | pub fn register(_py: Python<'_>, m: &Bound) -> PyResult<()> { 38 | // Register the base drawing class 39 | m.add_class::()?; 40 | 41 | // Register specific drawing implementation classes 42 | m.add_class::()?; 43 | m.add_class::()?; 44 | m.add_class::()?; 45 | m.add_class::()?; 46 | m.add_class::()?; 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /js/examples/src/pages/ExampleMds.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import * as d3 from "d3"; 3 | import { Graph, ClassicalMds } from "egraph/dist/web/egraph_wasm"; 4 | import { Wrapper } from "../wrapper"; 5 | 6 | export function ExampleMds() { 7 | const rendererRef = useRef(); 8 | 9 | useEffect(() => { 10 | (async () => { 11 | const response = await fetch("/data/miserables.json"); 12 | const data = await response.json(); 13 | const color = d3.scaleOrdinal(d3.schemeCategory10); 14 | const graph = new Graph(); 15 | const indices = new Map(); 16 | for (const node of data.nodes) { 17 | node.fillColor = color(node.group); 18 | indices.set(node.id, graph.addNode(node)); 19 | } 20 | for (const link of data.links) { 21 | link.strokeWidth = Math.sqrt(link.value); 22 | const { source, target } = link; 23 | graph.addEdge(indices.get(source), indices.get(target), link); 24 | } 25 | 26 | const drawing = new ClassicalMds(graph, () => 100).run2d(); 27 | for (const u of graph.nodeIndices()) { 28 | const node = graph.nodeWeight(u); 29 | node.x = drawing.x(u); 30 | node.y = drawing.y(u); 31 | } 32 | 33 | rendererRef.current.load(data); 34 | rendererRef.current.focus(0, 0); 35 | })(); 36 | }, []); 37 | 38 | return ( 39 | { 41 | rendererRef.current.width = width; 42 | rendererRef.current.height = height; 43 | }} 44 | > 45 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /crates/python/src/clustering/label_propagation.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::{GraphType, PyGraphAdapter}; 2 | use petgraph_clustering::{CommunityDetection, LabelPropagation}; 3 | use pyo3::prelude::*; 4 | use std::collections::HashMap; 5 | 6 | /// Label Propagation community detection algorithm. 7 | /// 8 | /// Label Propagation is a fast, near-linear time algorithm for detecting communities 9 | /// in networks. It works by iteratively updating each node's label to the most common 10 | /// label among its neighbors until convergence. 11 | /// 12 | /// Parameters: 13 | /// max_iterations (int, optional): Maximum number of iterations to perform. Default is 100. 14 | /// seed (int, optional): Seed for random number generation. Default is None. 15 | /// 16 | /// Example: 17 | /// >>> import egraph as eg 18 | /// >>> graph = eg.Graph() 19 | /// >>> # Add nodes and edges... 20 | /// >>> lp = eg.clustering.LabelPropagation(max_iterations=100, seed=42) 21 | /// >>> communities = lp.detect_communities(graph) 22 | /// >>> # communities is a dict mapping node indices to community IDs 23 | #[pyclass(name = "LabelPropagation")] 24 | pub struct PyLabelPropagation { 25 | instance: LabelPropagation, 26 | } 27 | 28 | #[pymethods] 29 | impl PyLabelPropagation { 30 | #[new] 31 | #[pyo3(signature = ())] 32 | fn new() -> Self { 33 | PyLabelPropagation { 34 | instance: LabelPropagation::new(), 35 | } 36 | } 37 | 38 | /// Detect communities in the given graph. 39 | /// 40 | /// Returns: 41 | /// dict: A dictionary mapping node indices to community IDs. 42 | #[pyo3(signature = (graph))] 43 | fn detect_communities(&self, graph: &PyGraphAdapter) -> HashMap { 44 | let map = match graph.graph() { 45 | GraphType::Graph(graph) => self.instance.detect_communities(graph), 46 | GraphType::DiGraph(graph) => self.instance.detect_communities(graph), 47 | }; 48 | map.into_iter() 49 | .map(|(u, c)| (u.index(), c)) 50 | .collect::>() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/gabriel_graph_property.rs: -------------------------------------------------------------------------------- 1 | use petgraph::visit::{EdgeRef, IntoEdgeReferences}; 2 | use petgraph_drawing::{ 3 | Drawing, DrawingEuclidean2d, DrawingIndex, DrawingValue, MetricEuclidean2d, 4 | }; 5 | 6 | /// Evaluates how well a graph layout adheres to the Gabriel graph property. 7 | /// 8 | /// A Gabriel graph has the property that for any edge, the disk with the edge as 9 | /// diameter contains no other nodes. This metric measures violations of this condition. 10 | /// 11 | /// For each edge in the graph, this function computes the disk with that edge as its 12 | /// diameter, and then calculates how much each node violates this property by being 13 | /// inside the disk. The metric is the sum of squared violations. 14 | /// 15 | /// # Parameters 16 | /// 17 | /// * `graph`: The graph structure to evaluate 18 | /// * `drawing`: The 2D Euclidean layout of the graph 19 | /// 20 | /// # Returns 21 | /// 22 | /// An `S` value representing the Gabriel graph property violation. Lower values 23 | /// indicate better adherence to the Gabriel graph property (fewer violations). 24 | /// 25 | /// # Type Parameters 26 | /// 27 | /// * `G`: A graph type that implements the required traits 28 | pub fn gabriel_graph_property(graph: G, drawing: &DrawingEuclidean2d) -> S 29 | where 30 | G: IntoEdgeReferences, 31 | G::NodeId: DrawingIndex, 32 | S: DrawingValue, 33 | { 34 | let n = drawing.len(); 35 | let mut s = S::zero(); 36 | for e in graph.edge_references() { 37 | let u = e.source(); 38 | let v = e.target(); 39 | let MetricEuclidean2d(x1, y1) = *drawing.position(u).unwrap(); 40 | let MetricEuclidean2d(x2, y2) = *drawing.position(v).unwrap(); 41 | let cx = (x1 + x2) / (2.).into(); 42 | let cy = (y1 + y2) / (2.).into(); 43 | let r = (x1 - x2).hypot(y1 - y2) / (2.).into(); 44 | for i in 0..n { 45 | s += (r - (drawing.raw_entry(i).0 - cx).hypot(drawing.raw_entry(i).1 - cy)) 46 | .max(S::zero()) 47 | .powi(2); 48 | } 49 | } 50 | s 51 | } 52 | -------------------------------------------------------------------------------- /crates/layout/separation-constraints/tests/rectangle_overlap.rs: -------------------------------------------------------------------------------- 1 | use petgraph::prelude::*; 2 | use petgraph_drawing::DrawingEuclidean2d; 3 | use petgraph_layout_separation_constraints::*; 4 | 5 | #[test] 6 | fn test_project_rectangle_no_overlap_constraints_2d() { 7 | // Create a graph with 2 overlapping nodes 8 | let mut graph = Graph::<(), ()>::new(); 9 | let n1 = graph.add_node(()); 10 | let n2 = graph.add_node(()); 11 | let n3 = graph.add_node(()); 12 | let n4 = graph.add_node(()); 13 | let n5 = graph.add_node(()); 14 | let nodes = [n1, n2, n3, n4, n5]; 15 | 16 | // Create a drawing with the nodes positioned with overlap 17 | let mut drawing = DrawingEuclidean2d::<_, f32>::new(&graph); 18 | drawing.set_x(n1, 5.); 19 | drawing.set_y(n1, 5.); 20 | drawing.set_x(n2, 13.); 21 | drawing.set_y(n2, 7.); 22 | drawing.set_x(n3, 25.); 23 | drawing.set_y(n3, 7.); 24 | drawing.set_x(n4, 10.); 25 | drawing.set_y(n4, 8.); 26 | drawing.set_x(n5, 0.); 27 | drawing.set_y(n5, 13.); 28 | 29 | // Set node size so they overlap (each node is 10.0 wide) 30 | let size = [ 31 | vec![10.0, 10.0], 32 | vec![10.0, 10.0], 33 | vec![10.0, 10.0], 34 | vec![10.0, 10.0], 35 | vec![10.0, 10.0], 36 | ]; 37 | 38 | // Apply constraints to remove overlaps 39 | project_rectangle_no_overlap_constraints_2d(&mut drawing, |u, d| size[u.index()][d]); 40 | 41 | // Check that the nodes are no longer overlapping 42 | for j in 0..5 { 43 | for i in 0..j { 44 | let u = nodes[i]; 45 | let v = nodes[j]; 46 | let dx = (drawing.x(u).unwrap() - drawing.x(v).unwrap()).abs(); 47 | let dy = (drawing.y(u).unwrap() - drawing.y(v).unwrap()).abs(); 48 | let gap_x = (size[i][0] + size[j][0]) / 2.0; 49 | let gap_y = (size[i][1] + size[j][1]) / 2.0; 50 | assert!( 51 | dx >= gap_x || dy >= gap_y, 52 | "|x({i}) - x({j})| = {dx} >= {gap_x} or |y({i}) - y({j})| = {dy} >= {gap_y}" 53 | ); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/python/src/clustering/louvain.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::{GraphType, PyGraphAdapter}; 2 | use petgraph_clustering::{CommunityDetection, Louvain}; 3 | use pyo3::prelude::*; 4 | use std::collections::HashMap; 5 | 6 | /// Louvain community detection algorithm. 7 | /// 8 | /// The Louvain method is a heuristic algorithm for detecting communities in networks. 9 | /// It works by optimizing modularity in a greedy manner, where modularity measures 10 | /// the density of connections within communities compared to connections between them. 11 | /// 12 | /// Parameters: 13 | /// resolution (float, optional): Resolution parameter that affects the size of communities. 14 | /// Higher values lead to smaller communities. Default is 1.0. 15 | /// seed (int, optional): Seed for random number generation. Default is None. 16 | /// 17 | /// Example: 18 | /// >>> import egraph as eg 19 | /// >>> graph = eg.Graph() 20 | /// >>> # Add nodes and edges... 21 | /// >>> louvain = eg.clustering.Louvain(resolution=1.0, seed=42) 22 | /// >>> communities = louvain.detect_communities(graph) 23 | /// >>> # communities is a dict mapping node indices to community IDs 24 | #[pyclass(name = "Louvain")] 25 | pub struct PyLouvain { 26 | instance: Louvain, 27 | } 28 | 29 | #[pymethods] 30 | impl PyLouvain { 31 | #[new] 32 | #[pyo3(signature = ())] 33 | fn new() -> Self { 34 | PyLouvain { 35 | instance: Louvain::new(), 36 | } 37 | } 38 | 39 | /// Detect communities in the given graph. 40 | /// 41 | /// Returns: 42 | /// dict: A dictionary mapping node indices to community IDs. 43 | #[pyo3(signature = (graph))] 44 | fn detect_communities(&self, graph: &PyGraphAdapter) -> HashMap { 45 | let map = match graph.graph() { 46 | GraphType::Graph(graph) => self.instance.detect_communities(graph), 47 | GraphType::DiGraph(graph) => self.instance.detect_communities(graph), 48 | }; 49 | map.into_iter() 50 | .map(|(u, c)| (u.index(), c)) 51 | .collect::>() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/wasm/tests/drawing_hyperbolic_2d.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/drawing_hyperbolic_2d.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testDrawingHyperbolic2dConstructor")] 11 | fn test_drawing_hyperbolic_2d_constructor(); 12 | #[wasm_bindgen(js_name = "testNodeCoordinates")] 13 | fn test_node_coordinates(); 14 | #[wasm_bindgen(js_name = "testDrawingWithGraph")] 15 | fn test_drawing_with_graph(); 16 | #[wasm_bindgen(js_name = "testHyperbolicDistance")] 17 | fn test_hyperbolic_distance(); 18 | #[wasm_bindgen(js_name = "testPoincareDiscConstraint")] 19 | fn test_poincare_disc_constraint(); 20 | #[wasm_bindgen(js_name = "testCoordinateValidation")] 21 | fn test_coordinate_validation(); 22 | #[wasm_bindgen(js_name = "testLayoutIntegration")] 23 | fn test_layout_integration(); 24 | } 25 | 26 | /// Test basic instantiation of DrawingHyperbolic2d class 27 | #[wasm_bindgen_test] 28 | pub fn drawing_hyperbolic_2d_constructor() { 29 | test_drawing_hyperbolic_2d_constructor(); 30 | } 31 | 32 | /// Test node coordinate operations (get/set x,y) 33 | #[wasm_bindgen_test] 34 | pub fn node_coordinates() { 35 | test_node_coordinates(); 36 | } 37 | 38 | /// Test integration with Graph class 39 | #[wasm_bindgen_test] 40 | pub fn drawing_with_graph() { 41 | test_drawing_with_graph(); 42 | } 43 | 44 | /// Test hyperbolic distance calculations between nodes 45 | #[wasm_bindgen_test] 46 | pub fn hyperbolic_distance() { 47 | test_hyperbolic_distance(); 48 | } 49 | 50 | /// Test Poincaré disc model constraint (|x^2 + y^2| < 1) 51 | #[wasm_bindgen_test] 52 | pub fn poincare_disc_constraint() { 53 | test_poincare_disc_constraint(); 54 | } 55 | 56 | /// Test coordinate validation and normalization 57 | #[wasm_bindgen_test] 58 | pub fn coordinate_validation() { 59 | test_coordinate_validation(); 60 | } 61 | 62 | /// Test integration with layout algorithms 63 | #[wasm_bindgen_test] 64 | pub fn layout_integration() { 65 | test_layout_integration(); 66 | } 67 | -------------------------------------------------------------------------------- /crates/wasm/tests/drawing_spherical_2d.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/drawing_spherical_2d.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testDrawingSpherical2dConstructor")] 11 | fn test_drawing_spherical_2d_constructor(); 12 | #[wasm_bindgen(js_name = "testNodeCoordinates")] 13 | fn test_node_coordinates(); 14 | #[wasm_bindgen(js_name = "testDrawingWithGraph")] 15 | fn test_drawing_with_graph(); 16 | #[wasm_bindgen(js_name = "testEdgeSegments")] 17 | fn test_edge_segments(); 18 | #[wasm_bindgen(js_name = "testGreatCircleDistance")] 19 | fn test_great_circle_distance(); 20 | #[wasm_bindgen(js_name = "testCoordinateValidation")] 21 | fn test_coordinate_validation(); 22 | #[wasm_bindgen(js_name = "testLayoutIntegration")] 23 | fn test_layout_integration(); 24 | } 25 | 26 | /// Test basic instantiation of DrawingSpherical2d class 27 | #[wasm_bindgen_test] 28 | pub fn drawing_spherical_2d_constructor() { 29 | test_drawing_spherical_2d_constructor(); 30 | } 31 | 32 | /// Test node coordinate operations (get/set longitude,latitude) 33 | #[wasm_bindgen_test] 34 | pub fn node_coordinates() { 35 | test_node_coordinates(); 36 | } 37 | 38 | /// Test integration with Graph class 39 | #[wasm_bindgen_test] 40 | pub fn drawing_with_graph() { 41 | test_drawing_with_graph(); 42 | } 43 | 44 | /// Test edge segment representation on a spherical surface 45 | #[wasm_bindgen_test] 46 | pub fn edge_segments() { 47 | test_edge_segments(); 48 | } 49 | 50 | /// Test great circle distance calculations between nodes 51 | #[wasm_bindgen_test] 52 | pub fn great_circle_distance() { 53 | test_great_circle_distance(); 54 | } 55 | 56 | /// Test coordinate validation (longitude normalization, latitude clamping) 57 | #[wasm_bindgen_test] 58 | pub fn coordinate_validation() { 59 | test_coordinate_validation(); 60 | } 61 | 62 | /// Test integration with layout algorithms 63 | #[wasm_bindgen_test] 64 | pub fn layout_integration() { 65 | test_layout_integration(); 66 | } 67 | -------------------------------------------------------------------------------- /crates/python/docs/source/examples/sgd.rst: -------------------------------------------------------------------------------- 1 | Stochastic Gradient Descent (SGD) 2 | ================================== 3 | 4 | This example demonstrates how to use the Stochastic Gradient Descent (SGD) layout algorithm. 5 | 6 | Basic SGD Example 7 | ----------------------- 8 | 9 | .. testcode:: python 10 | 11 | import networkx as nx 12 | import egraph as eg 13 | import matplotlib.pyplot as plt 14 | 15 | # Create a graph from NetworkX 16 | nx_graph = nx.les_miserables_graph() 17 | graph = eg.Graph() 18 | indices = {} 19 | for u in nx_graph.nodes: 20 | indices[u] = graph.add_node(u) 21 | for u, v in nx_graph.edges: 22 | graph.add_edge(indices[u], indices[v], (u, v)) 23 | 24 | # Create an initial drawing 25 | drawing = eg.DrawingEuclidean2d.initial_placement(graph) 26 | 27 | # Create a random number generator with a seed for reproducibility 28 | rng = eg.Rng.seed_from(0) 29 | 30 | # Create a SparseSgd instance using the builder pattern 31 | sgd = eg.SparseSgd().h(50).build(graph, lambda _: 30, rng) 32 | 33 | # Create a scheduler for the SGD algorithm 34 | scheduler = sgd.scheduler( 35 | 100, # number of iterations 36 | 0.1, # eps: eta_min = eps * min d[i, j] ^ 2 37 | ) 38 | 39 | # Define a step function for the scheduler 40 | def step(eta): 41 | sgd.shuffle(rng) 42 | sgd.apply(drawing, eta) 43 | 44 | # Run the scheduler 45 | scheduler.run(step) 46 | 47 | # Extract node positions 48 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 49 | 50 | # Visualize with NetworkX 51 | nx.draw(nx_graph, pos) 52 | 53 | Using FullSgd 54 | ------------------- 55 | 56 | For smaller graphs, you can use `FullSgd` which computes all-pairs shortest path distances: 57 | 58 | .. testcode:: python 59 | 60 | # Create a FullSgd instance using the builder pattern 61 | sgd = eg.FullSgd().build(graph, lambda _: 30) 62 | 63 | # The rest of the code is the same as the SparseSgd example 64 | scheduler = sgd.scheduler(100, 0.1) 65 | 66 | def step(eta): 67 | sgd.shuffle(rng) 68 | sgd.apply(drawing, eta) 69 | 70 | scheduler.run(step) 71 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/stress.rs: -------------------------------------------------------------------------------- 1 | use petgraph_algorithm_shortest_path::{DistanceMatrix, FullDistanceMatrix}; 2 | use petgraph_drawing::{Delta, Drawing, DrawingIndex, DrawingValue, Metric}; 3 | 4 | /// Calculates the stress metric for a graph layout. 5 | /// 6 | /// Stress is a fundamental metric in graph drawing that measures how well the 7 | /// Euclidean distances in the layout match the graph-theoretical distances 8 | /// (typically shortest path distances). It is calculated as the sum of squared 9 | /// relative differences between these distances. 10 | /// 11 | /// This implementation computes stress as: 12 | /// Σ [(||pos(i) - pos(j)|| - d(i,j))² / d(i,j)²] 13 | /// where: 14 | /// - pos(i) is the position of node i in the layout 15 | /// - d(i,j) is the graph-theoretical distance between nodes i and j 16 | /// - ||.|| denotes the Euclidean norm 17 | /// 18 | /// A lower stress value indicates a better layout in terms of faithfully 19 | /// representing the graph distances in the embedding space. 20 | /// 21 | /// # Parameters 22 | /// 23 | /// * `drawing`: The layout of the graph 24 | /// * `d`: The full distance matrix containing shortest path distances between all node pairs 25 | /// 26 | /// # Returns 27 | /// 28 | /// A value of type `S` representing the stress metric. Lower values indicate 29 | /// better preservation of graph distances. 30 | /// 31 | /// # Type Parameters 32 | /// 33 | /// * `Diff`: A type for representing differences between metric values 34 | /// * `D`: A drawing type 35 | /// * `N`: Node ID type 36 | /// * `M`: Metric type used in the drawing 37 | /// * `S`: Numeric type for distance calculations 38 | pub fn stress(drawing: &D, d: &FullDistanceMatrix) -> S 39 | where 40 | D: Drawing, 41 | Diff: Delta, 42 | N: DrawingIndex, 43 | M: Copy + Metric, 44 | S: DrawingValue, 45 | { 46 | let n = drawing.len(); 47 | let mut s = S::zero(); 48 | for j in 1..n { 49 | for i in 0..j { 50 | let delta = drawing.delta(i, j); 51 | let norm = delta.norm(); 52 | let dij = d.get_by_index(i, j); 53 | let e = (norm - dij) / dij; 54 | s += e * e; 55 | } 56 | } 57 | s 58 | } 59 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/angular_resolution.rs: -------------------------------------------------------------------------------- 1 | use crate::edge_angle::edge_angle; 2 | use petgraph::visit::{IntoNeighbors, IntoNodeIdentifiers}; 3 | use petgraph_drawing::{ 4 | Drawing, DrawingEuclidean2d, DrawingIndex, DrawingValue, MetricEuclidean2d, 5 | }; 6 | 7 | /// Calculates the angular resolution metric for a graph layout. 8 | /// 9 | /// Angular resolution measures how well the angles between edges connected to the same node 10 | /// are distributed. Higher values of this metric indicate better readability, as edges 11 | /// are more evenly spaced around their common node. 12 | /// 13 | /// This implementation calculates a sum of exponential functions of negative angles 14 | /// between adjacent edges. Smaller angles contribute larger values to the sum, 15 | /// making the metric increase as angular resolution worsens. 16 | /// 17 | /// # Parameters 18 | /// 19 | /// * `graph`: The graph structure to evaluate 20 | /// * `drawing`: The 2D Euclidean layout of the graph 21 | /// 22 | /// # Returns 23 | /// 24 | /// An `S` value representing the angular resolution metric. Lower values indicate 25 | /// better angular resolution. 26 | /// 27 | /// # Type Parameters 28 | /// 29 | /// * `G`: A graph type that implements the required traits 30 | pub fn angular_resolution(graph: G, drawing: &DrawingEuclidean2d) -> S 31 | where 32 | G: IntoNodeIdentifiers + IntoNeighbors, 33 | G::NodeId: DrawingIndex, 34 | S: DrawingValue, 35 | { 36 | let mut s = S::zero(); 37 | for u in graph.node_identifiers() { 38 | let MetricEuclidean2d(x0, y0) = *drawing.position(u).unwrap(); 39 | let neighbors = graph.neighbors(u).collect::>(); 40 | let n = neighbors.len(); 41 | for i in 1..n { 42 | let v = neighbors[i]; 43 | let MetricEuclidean2d(x1, y1) = *drawing.position(v).unwrap(); 44 | for neighbor in neighbors.iter().take(i) { 45 | let w = *neighbor; 46 | let MetricEuclidean2d(x2, y2) = *drawing.position(w).unwrap(); 47 | if let Some(angle) = edge_angle(x1 - x0, y1 - y0, x2 - x0, y2 - y0) { 48 | s += (-angle).exp() 49 | } 50 | } 51 | } 52 | } 53 | s 54 | } 55 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/ideal_edge_lengths.rs: -------------------------------------------------------------------------------- 1 | use petgraph::visit::{EdgeRef, IntoEdgeReferences}; 2 | use petgraph_algorithm_shortest_path::{DistanceMatrix, FullDistanceMatrix}; 3 | use petgraph_drawing::{Delta, Drawing, DrawingIndex, DrawingValue, Metric}; 4 | 5 | /// Evaluates how well edge lengths in a drawing match their ideal lengths. 6 | /// 7 | /// This metric measures the sum of squared relative differences between the actual 8 | /// edge lengths in the layout and the ideal lengths defined by the graph structure. 9 | /// The ideal length of an edge is typically derived from the graph-theoretical 10 | /// distance between its endpoints. 11 | /// 12 | /// A lower value indicates better preservation of edge length proportions, meaning 13 | /// the visual distances in the drawing better reflect the underlying graph structure. 14 | /// 15 | /// # Parameters 16 | /// 17 | /// * `graph`: The graph structure to evaluate 18 | /// * `drawing`: The layout of the graph 19 | /// * `d`: The full distance matrix containing shortest path distances between all node pairs 20 | /// 21 | /// # Returns 22 | /// 23 | /// A value of type `S` representing the ideal edge lengths metric. Lower values 24 | /// indicate better adherence to ideal edge lengths. 25 | /// 26 | /// # Type Parameters 27 | /// 28 | /// * `G`: A graph type that implements the required traits 29 | /// * `Diff`: A type for representing differences between metric values 30 | /// * `D`: A drawing type 31 | /// * `N`: Node ID type 32 | /// * `M`: Metric type used in the drawing 33 | /// * `S`: Numeric type for distance calculations 34 | pub fn ideal_edge_lengths( 35 | graph: G, 36 | drawing: &D, 37 | d: &FullDistanceMatrix, 38 | ) -> S 39 | where 40 | G: IntoEdgeReferences, 41 | D: Drawing, 42 | Diff: Delta, 43 | N: Copy + DrawingIndex, 44 | M: Copy + Metric, 45 | S: DrawingValue, 46 | { 47 | let mut s = S::zero(); 48 | for e in graph.edge_references() { 49 | let u = e.source(); 50 | let v = e.target(); 51 | let delta = drawing.delta(drawing.index(u), drawing.index(v)); 52 | let l = d.get(u, v).unwrap(); 53 | s += ((delta.norm() - l) / l).powi(2); 54 | } 55 | s 56 | } 57 | -------------------------------------------------------------------------------- /crates/python/docs/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | This section provides practical examples demonstrating various features of egraph. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | sgd 10 | stress_majorization 11 | kamada_kawai 12 | sgd_3d 13 | sgd_hyperbolic_2d 14 | sgd_spherical_2d 15 | sgd_torus 16 | overwrap_removal 17 | 18 | Overview 19 | -------- 20 | 21 | These examples demonstrate real-world usage of egraph's features. Each example is self-contained and can be run independently. 22 | 23 | Layout Algorithms 24 | ----------------- 25 | 26 | * :doc:`sgd` - Stochastic Gradient Descent for fast, scalable layouts 27 | * :doc:`stress_majorization` - High-quality layouts through stress minimization 28 | * :doc:`kamada_kawai` - Spring-based layout algorithm 29 | 30 | Advanced Drawing Spaces 31 | ------------------------ 32 | 33 | * :doc:`sgd_3d` - Three-dimensional graph layouts 34 | * :doc:`sgd_hyperbolic_2d` - Hyperbolic space for hierarchical graphs 35 | * :doc:`sgd_spherical_2d` - Spherical layouts for global networks 36 | * :doc:`sgd_torus` - Torus layouts with periodic boundaries 37 | 38 | Specialized Features 39 | -------------------- 40 | 41 | * :doc:`overwrap_removal` - Eliminate node overlaps while preserving structure 42 | 43 | Quick Example 44 | ------------- 45 | 46 | Here's a simple example of creating a graph and applying a layout algorithm: 47 | 48 | .. testcode:: python 49 | 50 | import networkx as nx 51 | import egraph as eg 52 | 53 | # Create a graph from NetworkX 54 | nx_graph = nx.les_miserables_graph() 55 | graph = eg.Graph() 56 | indices = {} 57 | for u in nx_graph.nodes: 58 | indices[u] = graph.add_node(u) 59 | for u, v in nx_graph.edges: 60 | graph.add_edge(indices[u], indices[v], (u, v)) 61 | 62 | # Create an initial drawing 63 | drawing = eg.DrawingEuclidean2d.initial_placement(graph) 64 | 65 | # Apply a layout algorithm 66 | sm = eg.StressMajorization(graph, drawing, lambda _: 100) 67 | sm.run(drawing) 68 | 69 | # Extract node positions 70 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 71 | 72 | # Visualize with NetworkX 73 | import matplotlib.pyplot as plt 74 | nx.draw(nx_graph, pos) 75 | -------------------------------------------------------------------------------- /crates/wasm/tests/classical_mds.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/classical_mds.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testClassicalMdsConstructor")] 11 | fn test_classical_mds_constructor(); 12 | #[wasm_bindgen(js_name = "testClassicalMdsRun2d")] 13 | fn test_classical_mds_run_2d(); 14 | #[wasm_bindgen(js_name = "testClassicalMdsRun")] 15 | fn test_classical_mds_run(); 16 | #[wasm_bindgen(js_name = "testClassicalMdsWithDifferentGraphs")] 17 | fn test_classical_mds_with_different_graphs(); 18 | #[wasm_bindgen(js_name = "testClassicalMdsWithCustomLengthFunction")] 19 | fn test_classical_mds_with_custom_length_function(); 20 | #[wasm_bindgen(js_name = "testClassicalMdsHandlesHighDimensions")] 21 | fn test_classical_mds_handles_high_dimensions(); 22 | #[wasm_bindgen(js_name = "testClassicalMdsIntegration")] 23 | fn test_classical_mds_integration(); 24 | } 25 | 26 | /// Test basic instantiation of ClassicalMds class 27 | #[wasm_bindgen_test] 28 | pub fn classical_mds_constructor() { 29 | test_classical_mds_constructor(); 30 | } 31 | 32 | /// Test run2d method for 2D layout generation 33 | #[wasm_bindgen_test] 34 | pub fn classical_mds_run_2d() { 35 | test_classical_mds_run_2d(); 36 | } 37 | 38 | /// Test run method for n-dimensional layout generation 39 | #[wasm_bindgen_test] 40 | pub fn classical_mds_run() { 41 | test_classical_mds_run(); 42 | } 43 | 44 | /// Test with different graph structures 45 | #[wasm_bindgen_test] 46 | pub fn classical_mds_with_different_graphs() { 47 | test_classical_mds_with_different_graphs(); 48 | } 49 | 50 | /// Test with custom length function 51 | #[wasm_bindgen_test] 52 | pub fn classical_mds_with_custom_length_function() { 53 | test_classical_mds_with_custom_length_function(); 54 | } 55 | 56 | /// Test handling of high-dimensional embeddings 57 | #[wasm_bindgen_test] 58 | pub fn classical_mds_handles_high_dimensions() { 59 | test_classical_mds_handles_high_dimensions(); 60 | } 61 | 62 | /// Test integration with other components 63 | #[wasm_bindgen_test] 64 | pub fn classical_mds_integration() { 65 | test_classical_mds_integration(); 66 | } 67 | -------------------------------------------------------------------------------- /crates/python/docs/source/examples/kamada_kawai.rst: -------------------------------------------------------------------------------- 1 | Kamada-Kawai 2 | ============== 3 | 4 | This example demonstrates how to use the Kamada-Kawai layout algorithm. 5 | 6 | Basic Kamada-Kawai Example 7 | ---------------------------------- 8 | 9 | .. testcode:: python 10 | 11 | import networkx as nx 12 | import egraph as eg 13 | import matplotlib.pyplot as plt 14 | 15 | # Create a graph from NetworkX 16 | nx_graph = nx.les_miserables_graph() 17 | graph = eg.Graph() 18 | indices = {} 19 | for u in nx_graph.nodes: 20 | indices[u] = graph.add_node(u) 21 | for u, v in nx_graph.edges: 22 | graph.add_edge(indices[u], indices[v], (u, v)) 23 | 24 | # Create an initial drawing 25 | drawing = eg.DrawingEuclidean2d.initial_placement(graph) 26 | 27 | # Create a KamadaKawai instance 28 | # The lambda function defines the desired distance between nodes 29 | # Here we use a constant distance of 1.0 for all edges 30 | kk = eg.KamadaKawai(graph, lambda _: 1.0) 31 | 32 | # Set the convergence threshold 33 | kk.eps = 1e-3 34 | 35 | # Run the algorithm 36 | kk.run(drawing) 37 | 38 | # Extract node positions 39 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 40 | 41 | # Visualize with NetworkX 42 | nx.draw(nx_graph, pos) 43 | 44 | Using Custom Edge Distances 45 | ---------------------------------- 46 | 47 | You can customize the desired distances between nodes: 48 | 49 | .. testcode:: python 50 | 51 | # Create a KamadaKawai instance with custom edge distances 52 | # The lambda function takes an edge index and returns the desired distance 53 | # Note: We use a simple distance function to avoid graph borrow conflicts 54 | kk = eg.KamadaKawai(graph, lambda e: 2.0) 55 | 56 | # Run the algorithm 57 | kk.run(drawing) 58 | 59 | Applying to a Single Node 60 | ---------------------------------- 61 | 62 | You can also apply the algorithm to a single node: 63 | 64 | .. testcode:: python 65 | 66 | # Apply the algorithm to a specific node 67 | node_index = 0 68 | kk.apply_to_node(node_index, drawing) 69 | 70 | # Apply the algorithm to all nodes one by one 71 | for i in range(graph.node_count()): 72 | kk.apply_to_node(i, drawing) 73 | -------------------------------------------------------------------------------- /crates/layout/separation-constraints/src/constraint_graph/constraint.rs: -------------------------------------------------------------------------------- 1 | /// Represents a separation constraint `variables[left] + gap <= variables[right]`. 2 | /// Enforces minimum separation between node pairs in one dimension. 3 | /// 4 | /// Separation constraints are useful for enforcing layout properties such as: 5 | /// - Minimum distance between nodes 6 | /// - Hierarchical relationships (e.g., parent above child) 7 | /// - Alignment requirements (e.g., nodes at same level) 8 | /// - Non-overlap between elements with extent 9 | /// 10 | /// In graph layouts, variables typically correspond to node coordinates in a specific dimension. 11 | /// For example, in a 2D layout, a constraint might enforce that node A is at least 50 pixels 12 | /// to the left of node B. 13 | /// 14 | /// See Section 2 in the IPSEP-COLA paper [2]. 15 | #[derive(Clone, Debug)] 16 | pub struct Constraint { 17 | /// Index (`usize`) of the variable on the left side. 18 | /// This corresponds to node indices in the original graph. 19 | pub left: usize, 20 | 21 | /// Index (`usize`) of the variable on the right side. 22 | /// This corresponds to node indices in the original graph. 23 | pub right: usize, 24 | 25 | /// Minimum required separation (`a` in `u + a <= v`). 26 | /// Units are the same as the drawing coordinates (typically pixels or other distance units). 27 | pub gap: S, 28 | } 29 | 30 | impl Constraint { 31 | /// Creates a new separation constraint. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `left` - The index of the variable that should be on the left side of the constraint 36 | /// * `right` - The index of the variable that should be on the right side of the constraint 37 | /// * `gap` - The minimum required separation between the two variables 38 | /// 39 | /// # Returns 40 | /// 41 | /// A new `Constraint` instance. 42 | /// 43 | /// # Example 44 | /// 45 | /// ``` 46 | /// use petgraph_layout_separation_constraints::Constraint; 47 | /// 48 | /// // Create a constraint that node 0 must be at least 5.0 units to the left of node 1 49 | /// let constraint = Constraint::new(0, 1, 5.0); 50 | /// ``` 51 | pub fn new(left: usize, right: usize, gap: S) -> Self { 52 | Constraint { left, right, gap } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/python/src/algorithm/layering/mod.rs: -------------------------------------------------------------------------------- 1 | /// Python bindings for petgraph-algorithm-layering. 2 | /// 3 | /// This module provides Python bindings for the petgraph-algorithm-layering Rust crate, 4 | /// which implements algorithms for assigning layers to nodes in directed graphs. 5 | use pyo3::prelude::*; 6 | 7 | mod longest_path; 8 | 9 | use crate::graph::{GraphType, PyGraphAdapter}; 10 | use longest_path::PyLongestPath; 11 | 12 | /// Register the layering module and its classes with Python. 13 | pub fn register(_py: Python<'_>, m: &Bound) -> PyResult<()> { 14 | m.add_class::()?; 15 | 16 | // Register cycle detection and removal functions 17 | m.add_function(wrap_pyfunction!(cycle_edges, m)?)?; 18 | m.add_function(wrap_pyfunction!(remove_cycle, m)?)?; 19 | 20 | Ok(()) 21 | } 22 | 23 | /// Get edges that form cycles in a directed graph. 24 | /// 25 | /// Args: 26 | /// graph: A directed graph. 27 | /// 28 | /// Returns: 29 | /// list: A list of edge indices that form cycles. 30 | #[pyfunction] 31 | fn cycle_edges(graph: &PyGraphAdapter) -> PyResult> { 32 | match graph.graph() { 33 | GraphType::Graph(_) => Err(pyo3::exceptions::PyValueError::new_err( 34 | "cycle_edges only works with directed graphs", 35 | )), 36 | GraphType::DiGraph(graph) => { 37 | let edges = petgraph_algorithm_layering::cycle::cycle_edges(graph); 38 | Ok(edges 39 | .into_iter() 40 | .map(|(u, v)| (u.index(), v.index())) 41 | .collect()) 42 | } 43 | } 44 | } 45 | 46 | /// Remove cycles from a directed graph. 47 | /// 48 | /// This function removes the minimum number of edges to make the graph acyclic. 49 | /// 50 | /// Args: 51 | /// graph: A directed graph. 52 | /// 53 | /// Returns: 54 | /// None: The graph is modified in-place. 55 | #[pyfunction] 56 | fn remove_cycle(graph: &mut PyGraphAdapter) -> PyResult<()> { 57 | match graph.graph_mut() { 58 | GraphType::Graph(_) => Err(pyo3::exceptions::PyValueError::new_err( 59 | "remove_cycle only works with directed graphs", 60 | )), 61 | GraphType::DiGraph(graph) => { 62 | petgraph_algorithm_layering::cycle::remove_cycle(graph); 63 | Ok(()) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/quality-metrics/src/aspect_ratio.rs: -------------------------------------------------------------------------------- 1 | use petgraph_drawing::{Drawing, DrawingEuclidean2d, DrawingIndex, DrawingValue}; 2 | 3 | /// Calculates the aspect ratio metric for a graph layout. 4 | /// 5 | /// The aspect ratio metric evaluates the balance between width and height of the drawing. 6 | /// It is calculated as the ratio of the smaller to the larger eigenvalue of the covariance 7 | /// matrix of node positions. A value closer to 1 indicates a more balanced, circular layout, 8 | /// while a value closer to 0 indicates a highly elongated layout. 9 | /// 10 | /// This metric computes the principal components of the node positions and returns 11 | /// the ratio of the smaller to the larger eigenvalue, which represents how close 12 | /// the layout is to being uniformly distributed in all directions. 13 | /// 14 | /// # Parameters 15 | /// 16 | /// * `drawing`: The 2D Euclidean layout of the graph 17 | /// 18 | /// # Returns 19 | /// 20 | /// An `S` value in the range [0, 1] representing the aspect ratio metric. 21 | /// A value of 1 indicates a perfectly balanced layout (equal spread in all directions), 22 | /// while lower values indicate more elongated layouts. 23 | /// 24 | /// # Type Parameters 25 | /// 26 | /// * `N`: Node ID type that implements `DrawingIndex` 27 | pub fn aspect_ratio(drawing: &DrawingEuclidean2d) -> S 28 | where 29 | N: DrawingIndex, 30 | S: DrawingValue, 31 | { 32 | let n = drawing.len(); 33 | let mut cx = S::zero(); 34 | let mut cy = S::zero(); 35 | for i in 0..n { 36 | let xi = drawing.raw_entry(i).0; 37 | let yi = drawing.raw_entry(i).1; 38 | cx += xi; 39 | cy += yi; 40 | } 41 | cx /= S::from_usize(n).unwrap(); 42 | cy /= S::from_usize(n).unwrap(); 43 | 44 | let mut xx = S::zero(); 45 | let mut xy = S::zero(); 46 | let mut yy = S::zero(); 47 | for i in 0..n { 48 | let xi = drawing.raw_entry(i).0 - cx; 49 | let yi = drawing.raw_entry(i).1 - cy; 50 | xx += xi * xi; 51 | xy += xi * yi; 52 | yy += yi * yi; 53 | } 54 | 55 | let tr = xx + yy; 56 | let det = xx * yy - xy * xy; 57 | let sigma1 = ((tr + (tr * tr - det * (4.).into()).sqrt()) / (2.).into()).sqrt(); 58 | let sigma2 = ((tr - (tr * tr - det * (4.).into()).sqrt()) / (2.).into()).sqrt(); 59 | sigma2 / sigma1 60 | } 61 | -------------------------------------------------------------------------------- /crates/python/tests/test_shortest_path.py: -------------------------------------------------------------------------------- 1 | import egraph as eg 2 | import networkx as nx 3 | import unittest 4 | 5 | 6 | def create_graph(nx_graph): 7 | graph = eg.DiGraph() if nx.is_directed(nx_graph) else eg.Graph() 8 | indices = {} 9 | for u in nx_graph.nodes: 10 | indices[u] = graph.add_node(u) 11 | for u, v in nx_graph.edges: 12 | graph.add_edge(indices[u], indices[v], (u, v)) 13 | return (nx_graph, graph) 14 | 15 | 16 | class TestShortestPath(unittest.TestCase): 17 | @classmethod 18 | def setUpClass(cls): 19 | cls._graphs = [ 20 | create_graph(nx.les_miserables_graph().to_undirected()), 21 | ] 22 | cls._digraphs = [ 23 | create_graph(nx.les_miserables_graph().to_directed()), 24 | create_graph(nx.gn_graph(100, seed=0)) 25 | ] 26 | 27 | def check(self, nx_graph, d_actual): 28 | d_expected = nx.floyd_warshall_numpy(nx_graph, weight='dummy') 29 | n = nx_graph.number_of_nodes() 30 | for i in range(n): 31 | for j in range(n): 32 | self.assertEqual( 33 | d_actual.get(i, j), 34 | d_expected[i, j], 35 | f'({i},{j})' 36 | ) 37 | 38 | def test_all_sources_bfs(self): 39 | for nx_graph, graph in self._graphs: 40 | self.check(nx_graph, eg.all_sources_bfs(graph, 1)) 41 | 42 | def test_all_sources_bfs_directed(self): 43 | for nx_graph, graph in self._digraphs: 44 | self.check(nx_graph, eg.all_sources_bfs(graph, 1)) 45 | 46 | def test_all_sources_dijkstra(self): 47 | for nx_graph, graph in self._graphs: 48 | self.check(nx_graph, eg.all_sources_dijkstra(graph, lambda _: 1)) 49 | 50 | def test_all_sources_dijkstra_directed(self): 51 | for nx_graph, graph in self._digraphs: 52 | self.check(nx_graph, eg.all_sources_dijkstra(graph, lambda _: 1)) 53 | 54 | def test_warshall_floyd(self): 55 | for nx_graph, graph in self._graphs: 56 | self.check(nx_graph, eg.warshall_floyd(graph, lambda _: 1)) 57 | 58 | def test_warshall_floyd_directed(self): 59 | for nx_graph, graph in self._digraphs: 60 | self.check(nx_graph, eg.warshall_floyd(graph, lambda _: 1)) 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /crates/algorithm/shortest-path/src/warshall_floyd.rs: -------------------------------------------------------------------------------- 1 | use crate::distance_matrix::{DistanceMatrix, FullDistanceMatrix}; 2 | use ndarray::NdFloat; 3 | use petgraph::visit::{EdgeRef, IntoEdges, IntoNodeIdentifiers}; 4 | use std::hash::Hash; 5 | 6 | /// Computes the shortest path distances between all pairs of nodes using the Floyd-Warshall algorithm. 7 | /// 8 | /// This algorithm is capable of handling negative edge weights, but not negative cycles. 9 | /// If a negative cycle is detected, the distance for nodes involved in or reachable from the cycle 10 | /// might not be correctly represented (this implementation doesn't explicitly handle negative infinity). 11 | /// 12 | /// # Type Parameters 13 | /// 14 | /// * `G`: The graph type, implementing `IntoEdges` and `IntoNodeIdentifiers`. 15 | /// * `F`: The type of the function/closure used to get edge lengths. 16 | /// * `S`: The scalar type for distances, implementing `NdFloat`. 17 | /// 18 | /// # Arguments 19 | /// 20 | /// * `graph`: The graph to compute the all-pairs shortest paths for. 21 | /// * `length`: A function or closure that takes an `EdgeRef` and returns its length (`S`). 22 | /// 23 | /// # Returns 24 | /// 25 | /// A `FullDistanceMatrix` containing the shortest path distances between all pairs of nodes. 26 | /// Distances for unreachable pairs remain infinity. Distances on negative cycles might be incorrect. 27 | pub fn warshall_floyd(graph: G, length: F) -> FullDistanceMatrix 28 | where 29 | G: IntoEdges + IntoNodeIdentifiers, 30 | G::NodeId: Eq + Hash + Copy, // Added Copy trait bound 31 | F: FnMut(G::EdgeRef) -> S, 32 | S: NdFloat, 33 | { 34 | let mut distance = FullDistanceMatrix::new(graph); 35 | let mut length = length; 36 | let n = distance.shape().0; 37 | 38 | for u in graph.node_identifiers() { 39 | for e in graph.edges(u) { 40 | distance.set(e.source(), e.target(), length(e)); 41 | } 42 | } 43 | for i in 0..n { 44 | distance.set_by_index(i, i, S::zero()); 45 | } 46 | 47 | for k in 0..n { 48 | for i in 0..n { 49 | for j in 0..n { 50 | let d = distance.get_by_index(i, k) + distance.get_by_index(k, j); 51 | if d < distance.get_by_index(i, j) { 52 | distance.set_by_index(i, j, d); 53 | } 54 | } 55 | } 56 | } 57 | 58 | distance 59 | } 60 | -------------------------------------------------------------------------------- /crates/layout/sgd/src/scheduler/scheduler_constant.rs: -------------------------------------------------------------------------------- 1 | use crate::scheduler::Scheduler; 2 | use petgraph_drawing::DrawingValue; 3 | use std::marker::PhantomData; 4 | 5 | /// A learning rate scheduler that maintains a constant learning rate. 6 | /// 7 | /// Unlike other schedulers, `SchedulerConstant` does not decrease the learning rate 8 | /// over time. Instead, it provides a constant learning rate of 1.0 at each step. 9 | /// This scheduler is primarily useful for testing or for cases where a constant 10 | /// learning rate is desired. 11 | pub struct SchedulerConstant { 12 | /// Current iteration counter 13 | t: usize, 14 | /// Maximum number of iterations 15 | t_max: usize, 16 | /// Phantom data to use the generic parameter S 17 | phantom: PhantomData, 18 | } 19 | 20 | /// Implementation of the Scheduler trait for SchedulerConstant 21 | impl Scheduler for SchedulerConstant 22 | where 23 | S: DrawingValue, 24 | { 25 | /// Initializes a new constant scheduler. 26 | /// 27 | /// This method initializes a scheduler that provides a constant learning rate. 28 | /// The constant value is set to eta_max (ignoring eta_min). 29 | /// 30 | /// # Parameters 31 | /// * `t_max` - The maximum number of iterations 32 | /// * `eta_min` - The minimum learning rate (ignored for constant scheduler) 33 | /// * `eta_max` - The maximum learning rate (used as the constant value) 34 | /// 35 | /// # Returns 36 | /// A new SchedulerConstant instance 37 | fn init(t_max: usize, _eta_min: S, _eta_max: S) -> Self { 38 | Self { 39 | t: 0, 40 | t_max, 41 | phantom: PhantomData, 42 | } 43 | } 44 | 45 | /// Performs a single step of the scheduling process. 46 | /// 47 | /// This implementation always provides a learning rate of 1.0 to the callback 48 | /// and increments the iteration counter. 49 | /// 50 | /// # Parameters 51 | /// * `callback` - A function that will be called with the constant learning rate 52 | fn step(&mut self, callback: &mut F) { 53 | callback(S::one()); 54 | self.t += 1; 55 | } 56 | 57 | /// Checks if the scheduling process is complete. 58 | /// 59 | /// # Returns 60 | /// `true` if the current iteration count has reached the maximum, `false` otherwise 61 | fn is_finished(&self) -> bool { 62 | self.t >= self.t_max 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/wasm/tests/sgd.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen(module = "tests/sgd.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = "testSgdSchedulers")] 11 | fn test_sgd_schedulers(); 12 | #[wasm_bindgen(js_name = "testSgdWithEuclidean2d")] 13 | fn test_sgd_with_euclidean_2d(); 14 | #[wasm_bindgen(js_name = "testSgdWithHyperbolic2d")] 15 | fn test_sgd_with_hyperbolic_2d(); 16 | #[wasm_bindgen(js_name = "testSgdWithSpherical2d")] 17 | fn test_sgd_with_spherical_2d(); 18 | #[wasm_bindgen(js_name = "testSgdWithTorus2d")] 19 | fn test_sgd_with_torus_2d(); 20 | #[wasm_bindgen(js_name = "testSgdWithEuclidean")] 21 | fn test_sgd_with_euclidean(); 22 | #[wasm_bindgen(js_name = "testSgdUpdateDistance")] 23 | fn test_sgd_update_distance(); 24 | #[wasm_bindgen(js_name = "testSgdUpdateWeight")] 25 | fn test_sgd_update_weight(); 26 | #[wasm_bindgen(js_name = "testSgdShuffle")] 27 | fn test_sgd_shuffle(); 28 | } 29 | 30 | /// Test scheduler creation methods 31 | #[wasm_bindgen_test] 32 | pub fn sgd_schedulers() { 33 | test_sgd_schedulers(); 34 | } 35 | 36 | /// Test applying SGD to Euclidean 2D drawings 37 | #[wasm_bindgen_test] 38 | pub fn sgd_with_euclidean_2d() { 39 | test_sgd_with_euclidean_2d(); 40 | } 41 | 42 | /// Test applying SGD to Hyperbolic 2D drawings 43 | #[wasm_bindgen_test] 44 | pub fn sgd_with_hyperbolic_2d() { 45 | test_sgd_with_hyperbolic_2d(); 46 | } 47 | 48 | /// Test applying SGD to Spherical 2D drawings 49 | #[wasm_bindgen_test] 50 | pub fn sgd_with_spherical_2d() { 51 | test_sgd_with_spherical_2d(); 52 | } 53 | 54 | /// Test applying SGD to Torus 2D drawings 55 | #[wasm_bindgen_test] 56 | pub fn sgd_with_torus_2d() { 57 | test_sgd_with_torus_2d(); 58 | } 59 | 60 | /// Test applying SGD to n-dimensional Euclidean drawings 61 | #[wasm_bindgen_test] 62 | pub fn sgd_with_euclidean() { 63 | test_sgd_with_euclidean(); 64 | } 65 | 66 | /// Test updating distance function 67 | #[wasm_bindgen_test] 68 | pub fn sgd_update_distance() { 69 | test_sgd_update_distance(); 70 | } 71 | 72 | /// Test updating weight function 73 | #[wasm_bindgen_test] 74 | pub fn sgd_update_weight() { 75 | test_sgd_update_weight(); 76 | } 77 | 78 | /// Test shuffling node pairs 79 | #[wasm_bindgen_test] 80 | pub fn sgd_shuffle() { 81 | test_sgd_shuffle(); 82 | } 83 | -------------------------------------------------------------------------------- /crates/drawing/src/metric.rs: -------------------------------------------------------------------------------- 1 | pub mod metric_euclidean; 2 | pub mod metric_euclidean_2d; 3 | pub mod metric_hyperbolic_2d; 4 | pub mod metric_spherical_2d; 5 | pub mod metric_torus2d; 6 | 7 | use crate::DrawingValue; 8 | use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; 9 | 10 | /// Represents the difference (vector) between two points in a metric space. 11 | /// 12 | /// This trait defines the operations that must be supported by a type representing 13 | /// the difference between two points in a metric space, such as vector addition, 14 | /// subtraction, scalar multiplication, and computing the norm (distance). 15 | /// 16 | /// # Type Parameters 17 | /// 18 | /// * `S`: The scalar type used for coordinate values and distance calculations. 19 | pub trait Delta: 20 | Sized + Add + Sub + Mul + Div + Clone 21 | { 22 | /// The scalar type used for coordinate values and distance calculations. 23 | type S: DrawingValue; 24 | 25 | /// Computes the norm (distance) of this difference vector. 26 | /// 27 | /// In a Euclidean space, this would be the length of the vector. 28 | /// In other spaces, it represents the distance measure appropriate to that space. 29 | fn norm(&self) -> Self::S; 30 | } 31 | 32 | /// Defines a metric space where distances between points can be measured. 33 | /// 34 | /// A metric space is a set where a notion of distance between elements is defined. 35 | /// This trait defines the basic operations needed for types representing points in such spaces. 36 | pub trait Metric: Sized + AddAssign + SubAssign { 37 | /// The type representing the difference (vector) between two points in this metric space. 38 | type D: Delta; 39 | } 40 | 41 | /// A specialized metric for Cartesian coordinate systems. 42 | /// 43 | /// This trait extends the basic `Metric` trait with functionality specific to 44 | /// Cartesian coordinate systems, such as accessing individual dimensions. 45 | pub trait MetricCartesian: Metric { 46 | /// Returns the value of the `n`-th dimension of this point. 47 | /// 48 | /// # Parameters 49 | /// 50 | /// * `n`: The index of the dimension to access. 51 | /// 52 | /// # Returns 53 | /// 54 | /// The scalar value of the `n`-th dimension. 55 | fn nth(&self, n: usize) -> &<::D as Delta>::S; 56 | 57 | fn nth_mut(&mut self, n: usize) -> &mut <::D as Delta>::S; 58 | } 59 | -------------------------------------------------------------------------------- /crates/python/src/algorithm/triangulation.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::PyDrawingEuclidean2d; 2 | use crate::graph::PyGraphAdapter; 3 | use pyo3::prelude::*; 4 | 5 | /// Performs Delaunay triangulation based on node positions in a 2D Euclidean drawing. 6 | /// 7 | /// This function takes a drawing as input, extracts the node positions from the drawing, 8 | /// computes the Delaunay triangulation of these points, and returns a new 9 | /// graph with nodes corresponding to the drawing's nodes and edges representing the triangulation. 10 | /// 11 | /// Parameters 12 | /// ---------- 13 | /// drawing : DrawingEuclidean2d 14 | /// A 2D Euclidean drawing that contains the positions of the nodes. 15 | /// 16 | /// Returns 17 | /// ------- 18 | /// Graph 19 | /// A new undirected graph with nodes corresponding to the drawing's nodes, 20 | /// and with edges representing the Delaunay triangulation. 21 | /// 22 | /// Examples 23 | /// -------- 24 | /// >>> import egraph as eg 25 | /// >>> # Create a graph 26 | /// >>> graph = eg.Graph() 27 | /// >>> n1 = graph.add_node() 28 | /// >>> n2 = graph.add_node() 29 | /// >>> n3 = graph.add_node() 30 | /// >>> n4 = graph.add_node() 31 | /// >>> # Create a drawing 32 | /// >>> drawing = eg.DrawingEuclidean2d(graph) 33 | /// >>> drawing.set_position(n1, 0.0, 0.0) 34 | /// >>> drawing.set_position(n2, 1.0, 0.0) 35 | /// >>> drawing.set_position(n3, 0.0, 1.0) 36 | /// >>> drawing.set_position(n4, 1.0, 1.0) 37 | /// >>> # Compute the Delaunay triangulation 38 | /// >>> triangulated_graph = eg.triangulation(drawing) 39 | /// >>> # The triangulated graph should have 4 nodes and 5 edges 40 | /// >>> triangulated_graph.number_of_nodes() 41 | /// 4 42 | /// >>> triangulated_graph.number_of_edges() 43 | /// 5 44 | #[pyfunction] 45 | #[pyo3(name = "triangulation")] 46 | pub fn py_triangulation( 47 | _py: Python<'_>, 48 | drawing: &PyDrawingEuclidean2d, 49 | ) -> PyResult { 50 | let drawing_ref = drawing.drawing(); 51 | let triangulated = petgraph_algorithm_triangulation::triangulation(drawing_ref).map( 52 | |_, _| Python::attach(|py| py.None()), 53 | |_, _| Python::attach(|py| py.None()), 54 | ); 55 | 56 | // Create a new PyGraphAdapter from the triangulated graph 57 | Ok(PyGraphAdapter::new(triangulated)) 58 | } 59 | 60 | /// Register the triangulation module with Python 61 | pub fn register(_py: Python<'_>, m: &Bound) -> PyResult<()> { 62 | m.add_function(wrap_pyfunction!(py_triangulation, m)?)?; 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /js/examples/src/pages/ExampleKamadaKawai.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import * as d3 from "d3"; 3 | import { 4 | DrawingEuclidean2d as Drawing, 5 | Graph, 6 | KamadaKawai, 7 | } from "egraph/dist/web/egraph_wasm"; 8 | import { Wrapper } from "../wrapper"; 9 | 10 | export function ExampleKamadaKawai() { 11 | const rendererRef = useRef(); 12 | 13 | useEffect(() => { 14 | (async () => { 15 | const response = await fetch("/data/miserables.json"); 16 | const data = await response.json(); 17 | const color = d3.scaleOrdinal(d3.schemeCategory10); 18 | const graph = new Graph(); 19 | const indices = new Map(); 20 | for (const node of data.nodes) { 21 | node.fillColor = color(node.group); 22 | indices.set(node.id, graph.addNode(node)); 23 | } 24 | for (const link of data.links) { 25 | link.strokeWidth = Math.sqrt(link.value); 26 | const { source, target } = link; 27 | graph.addEdge(indices.get(source), indices.get(target), link); 28 | } 29 | 30 | const drawing = Drawing.initialPlacement(graph); 31 | const kamadaKawai = new KamadaKawai(graph, () => ({ distance: 100 })); 32 | kamadaKawai.eps = 1e-3; 33 | 34 | setInterval(() => { 35 | const u = kamadaKawai.selectNode(drawing); 36 | if (u != null) { 37 | kamadaKawai.applyToNode(u, drawing); 38 | for (const u of graph.nodeIndices()) { 39 | const node = graph.nodeWeight(u); 40 | node.x = drawing.x(u); 41 | node.y = drawing.y(u); 42 | } 43 | rendererRef.current.update(); 44 | } 45 | }, 200); 46 | 47 | rendererRef.current.load(data); 48 | rendererRef.current.focus(0, 0); 49 | })(); 50 | }, []); 51 | 52 | return ( 53 | { 55 | rendererRef.current.width = width; 56 | rendererRef.current.height = height; 57 | }} 58 | > 59 | 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /crates/wasm/src/drawing/drawing_euclidean.rs: -------------------------------------------------------------------------------- 1 | //! N-dimensional Euclidean drawing functionality for WebAssembly. 2 | //! 3 | //! This module provides a WebAssembly binding for representing graph drawings 4 | //! in n-dimensional Euclidean space. Unlike the 2D-specific implementations, 5 | //! this drawing type supports arbitrary dimensions, making it suitable for 6 | //! embedding graphs in higher-dimensional spaces. 7 | 8 | use crate::graph::IndexType; 9 | use petgraph::graph::{node_index, NodeIndex}; 10 | use petgraph_drawing::{Drawing, DrawingEuclidean}; 11 | use wasm_bindgen::prelude::*; 12 | 13 | type NodeId = NodeIndex; 14 | 15 | /// WebAssembly binding for n-dimensional Euclidean graph drawings. 16 | /// 17 | /// This struct provides a JavaScript interface for creating and manipulating 18 | /// graph drawings in n-dimensional Euclidean space. It allows for positioning 19 | /// nodes in arbitrary dimensions, which can be useful for specialized layout 20 | /// algorithms or for visualizing high-dimensional data. 21 | #[wasm_bindgen(js_name = DrawingEuclidean)] 22 | pub struct JsDrawingEuclidean { 23 | drawing: DrawingEuclidean, 24 | } 25 | 26 | impl JsDrawingEuclidean { 27 | pub fn new(drawing: DrawingEuclidean) -> Self { 28 | Self { drawing } 29 | } 30 | 31 | pub fn drawing(&self) -> &DrawingEuclidean { 32 | &self.drawing 33 | } 34 | 35 | pub fn drawing_mut(&mut self) -> &mut DrawingEuclidean { 36 | &mut self.drawing 37 | } 38 | } 39 | 40 | #[wasm_bindgen(js_class = DrawingEuclidean)] 41 | impl JsDrawingEuclidean { 42 | /// Gets the coordinate of the node at the given index in the specified dimension. 43 | /// 44 | /// Returns None if the node is not present in the drawing or if the dimension 45 | /// is out of bounds. 46 | pub fn get(&self, u: usize, d: usize) -> Option { 47 | let u = node_index(u); 48 | self.drawing.get(u, d) 49 | } 50 | 51 | /// Sets the coordinate of the node at the given index in the specified dimension. 52 | pub fn set(&mut self, u: usize, d: usize, value: f32) { 53 | let u = node_index(u); 54 | self.drawing.set(u, d, value); 55 | } 56 | 57 | /// Returns the number of nodes in the drawing. 58 | pub fn len(&self) -> usize { 59 | self.drawing.len() 60 | } 61 | 62 | /// Returns whether the drawing is empty (has no nodes). 63 | pub fn is_empty(&self) -> bool { 64 | self.drawing.is_empty() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/python/docs/source/examples/sgd_hyperbolic_2d.rst: -------------------------------------------------------------------------------- 1 | Hyperbolic 2D Stochastic Gradient Descent 2 | ========================================= 3 | 4 | This example demonstrates how to use the Stochastic Gradient Descent (SGD) layout algorithm with hyperbolic 2D drawings. 5 | 6 | Basic Hyperbolic SGD Example 7 | ---------------------------------- 8 | 9 | .. testcode:: python 10 | 11 | import networkx as nx 12 | import egraph as eg 13 | import matplotlib.pyplot as plt 14 | import numpy as np 15 | 16 | # Create a graph from NetworkX 17 | nx_graph = nx.les_miserables_graph() 18 | graph = eg.Graph() 19 | indices = {} 20 | for u in nx_graph.nodes: 21 | indices[u] = graph.add_node(u) 22 | for u, v in nx_graph.edges: 23 | graph.add_edge(indices[u], indices[v], (u, v)) 24 | 25 | # Create a hyperbolic drawing using the factory method 26 | drawing = eg.DrawingHyperbolic2d.initial_placement(graph) 27 | 28 | # Create a random number generator with a seed for reproducibility 29 | rng = eg.Rng.seed_from(0) 30 | 31 | # Create a SparseSgd instance using the builder pattern 32 | sgd = eg.SparseSgd().h(50).build(graph, lambda _: 0.3, rng) 33 | 34 | # Create a scheduler for the SGD algorithm 35 | scheduler = sgd.scheduler( 36 | 100, # number of iterations 37 | 0.1, # eps: eta_min = eps * min d[i, j] ^ 2 38 | ) 39 | 40 | # Define a step function for the scheduler 41 | def step(eta): 42 | sgd.shuffle(rng) 43 | sgd.apply(drawing, eta) 44 | 45 | # Run the scheduler 46 | scheduler.run(step) 47 | 48 | # Extract node positions 49 | pos = {u: (drawing.x(i), drawing.y(i)) for u, i in indices.items()} 50 | 51 | # Visualize with NetworkX and Matplotlib 52 | fig, ax = plt.subplots(figsize=(10, 10)) 53 | 54 | # Draw the Poincaré disk boundary 55 | circle = plt.Circle((0, 0), 1, fill=False, color='gray', linestyle='--') 56 | ax.add_patch(circle) 57 | 58 | # Draw the graph 59 | nx.draw(nx_graph, pos, ax=ax, node_size=50) 60 | 61 | # Set equal aspect ratio and limits 62 | ax.set_xlim(-1.1, 1.1) 63 | ax.set_ylim(-1.1, 1.1) 64 | ax.set_aspect('equal') 65 | 66 | Working with Hyperbolic Distances 67 | ---------------------------------- 68 | 69 | When working with hyperbolic space, it's important to understand that distances are different from Euclidean space. 70 | The hyperbolic distance formula in the Poincaré disk model can be calculated using the positions of nodes. 71 | -------------------------------------------------------------------------------- /js/examples/src/pages/ExampleStressMajorization.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import * as d3 from "d3"; 3 | import { 4 | DrawingEuclidean2d as Drawing, 5 | Graph, 6 | StressMajorization, 7 | } from "egraph/dist/web/egraph_wasm"; 8 | import { Wrapper } from "../wrapper"; 9 | 10 | export function ExampleStressMajorization() { 11 | const rendererRef = useRef(); 12 | 13 | useEffect(() => { 14 | (async () => { 15 | const response = await fetch("/data/miserables.json"); 16 | const data = await response.json(); 17 | 18 | const color = d3.scaleOrdinal(d3.schemeCategory10); 19 | const graph = new Graph(); 20 | const indices = new Map(); 21 | for (const node of data.nodes) { 22 | node.fillColor = color(node.group); 23 | indices.set(node.id, graph.addNode(node)); 24 | } 25 | for (const link of data.links) { 26 | link.strokeWidth = Math.sqrt(link.value); 27 | const { source, target } = link; 28 | graph.addEdge(indices.get(source), indices.get(target), link); 29 | } 30 | 31 | const drawing = Drawing.initialPlacement(graph); 32 | const stressMajorization = new StressMajorization(graph, drawing, () => ({ 33 | distance: 100, 34 | })); 35 | rendererRef.current.load(data); 36 | rendererRef.current.focus(0, 0); 37 | function draw() { 38 | if (stressMajorization.apply(drawing) > 1e-5) { 39 | drawing.centralize(); 40 | for (const u of graph.nodeIndices()) { 41 | const node = graph.nodeWeight(u); 42 | node.x = drawing.x(u); 43 | node.y = drawing.y(u); 44 | } 45 | rendererRef.current.update(); 46 | requestAnimationFrame(draw); 47 | } 48 | } 49 | draw(); 50 | })(); 51 | }, []); 52 | 53 | return ( 54 | { 56 | rendererRef.current.width = width; 57 | rendererRef.current.height = height; 58 | }} 59 | > 60 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /crates/python/src/algorithm/layering/longest_path.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::{GraphType, PyGraphAdapter}; 2 | use petgraph_algorithm_layering::LongestPath; 3 | use pyo3::prelude::*; 4 | use std::collections::HashMap; 5 | 6 | /// Longest Path layering algorithm. 7 | /// 8 | /// This algorithm assigns layers to nodes in a directed graph by determining the 9 | /// longest path from any source node (node with no incoming edges) to each node. 10 | /// It is commonly used in hierarchical graph layouts. 11 | /// 12 | /// Example: 13 | /// >>> import egraph as eg 14 | /// >>> graph = eg.DiGraph() 15 | /// >>> # Add nodes and edges... 16 | /// >>> lp = eg.algorithm.LongestPath() 17 | /// >>> layers = lp.assign_layers(graph) 18 | /// >>> # layers is a dict mapping node indices to layer numbers 19 | #[pyclass(name = "LongestPath")] 20 | pub struct PyLongestPath { 21 | instance: LongestPath, 22 | } 23 | 24 | #[pymethods] 25 | impl PyLongestPath { 26 | #[new] 27 | fn new() -> Self { 28 | PyLongestPath { 29 | instance: LongestPath::new(), 30 | } 31 | } 32 | 33 | /// Assign layers to nodes in the given directed graph. 34 | /// 35 | /// The algorithm assigns layer 0 to source nodes (those with no incoming edges), 36 | /// and then assigns other nodes to layers based on the longest path from any source. 37 | /// 38 | /// Args: 39 | /// graph: A directed graph. 40 | /// 41 | /// Returns: 42 | /// dict: A dictionary mapping node indices to layer numbers (starting from 0). 43 | /// 44 | /// Raises: 45 | /// ValueError: If the graph contains cycles. Use remove_cycle() first to make the graph acyclic. 46 | #[pyo3(signature = (graph))] 47 | fn assign_layers(&self, graph: &PyGraphAdapter) -> PyResult> { 48 | match graph.graph() { 49 | GraphType::Graph(_) => Err(pyo3::exceptions::PyValueError::new_err( 50 | "LongestPath only works with directed graphs", 51 | )), 52 | GraphType::DiGraph(graph) => { 53 | // Try to assign layers, which may fail if the graph has cycles 54 | let layers = self.instance.assign_layers(graph); 55 | 56 | // Convert from NodeIndex keys to usize keys for Python 57 | let result = layers 58 | .into_iter() 59 | .map(|(node_idx, layer)| (node_idx.index(), layer)) 60 | .collect(); 61 | 62 | Ok(result) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/layout/sgd/src/scheduler/scheduler_linear.rs: -------------------------------------------------------------------------------- 1 | use crate::scheduler::Scheduler; 2 | use petgraph_drawing::DrawingValue; 3 | 4 | /// A learning rate scheduler with linear decay. 5 | /// 6 | /// This scheduler decreases the learning rate linearly over time, 7 | /// following the formula: η(t) = a - b * t. 8 | /// 9 | /// Linear decay provides a steady, predictable decrease in the learning rate, 10 | /// which can be useful for many graph layout applications. 11 | pub struct SchedulerLinear { 12 | /// Current iteration counter 13 | t: usize, 14 | /// Maximum number of iterations 15 | t_max: usize, 16 | /// Initial learning rate (y-intercept in the linear formula) 17 | a: S, 18 | /// Rate of decrease per iteration (slope in the linear formula) 19 | b: S, 20 | } 21 | 22 | /// Implementation of the Scheduler trait for SchedulerLinear 23 | impl Scheduler for SchedulerLinear 24 | where 25 | S: DrawingValue, 26 | { 27 | /// Initializes a new linear scheduler. 28 | /// 29 | /// This method calculates the parameters for the linear decay formula 30 | /// based on the desired minimum and maximum learning rates and the number of iterations. 31 | /// 32 | /// # Parameters 33 | /// * `t_max` - The maximum number of iterations 34 | /// * `eta_min` - The minimum learning rate (reached at the end) 35 | /// * `eta_max` - The maximum learning rate (used at the beginning) 36 | /// 37 | /// # Returns 38 | /// A new SchedulerLinear instance 39 | fn init(t_max: usize, eta_min: S, eta_max: S) -> Self { 40 | Self { 41 | t: 0, 42 | t_max, 43 | a: eta_max, 44 | b: (eta_max - eta_min) / S::from_usize(t_max - 1).unwrap(), 45 | } 46 | } 47 | 48 | /// Performs a single step of the scheduling process. 49 | /// 50 | /// This method calculates the learning rate using the linear decay formula, 51 | /// provides it to the callback function, and increments the iteration counter. 52 | /// 53 | /// # Parameters 54 | /// * `callback` - A function that will be called with the calculated learning rate 55 | fn step(&mut self, callback: &mut F) { 56 | let eta = self.a - self.b * S::from_usize(self.t).unwrap(); 57 | callback(eta); 58 | self.t += 1; 59 | } 60 | 61 | /// Checks if the scheduling process is complete. 62 | /// 63 | /// # Returns 64 | /// `true` if the current iteration count has reached the maximum, `false` otherwise 65 | fn is_finished(&self) -> bool { 66 | self.t >= self.t_max 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/wasm/tests/node.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | #[allow(unused_imports)] 4 | use egraph_wasm::*; 5 | use util::example_data; 6 | use wasm_bindgen::prelude::*; 7 | use wasm_bindgen_test::*; 8 | 9 | #[wasm_bindgen(module = "tests/node.js")] 10 | extern "C" { 11 | #[wasm_bindgen(js_name = "testConstructGraph")] 12 | fn test_construct_graph(data: JsValue); 13 | #[wasm_bindgen(js_name = "testKamadaKawai")] 14 | fn test_kamada_kawai(data: JsValue); 15 | #[wasm_bindgen(js_name = "testStressMajorization")] 16 | fn test_stress_majorization(data: JsValue); 17 | #[wasm_bindgen(js_name = "testClassicalMds")] 18 | fn test_classical_mds(data: JsValue); 19 | #[wasm_bindgen(js_name = "testPivotMds")] 20 | fn test_pivot_mds(data: JsValue); 21 | #[wasm_bindgen(js_name = "testFullSgd")] 22 | fn test_full_sgd(data: JsValue); 23 | #[wasm_bindgen(js_name = "testSparseSgd")] 24 | fn test_sparse_sgd(data: JsValue); 25 | #[wasm_bindgen(js_name = "testCrossingNumber")] 26 | fn test_crossing_number(data: JsValue); 27 | #[wasm_bindgen(js_name = "testNeighborhoodPreservation")] 28 | fn test_neighborhood_preservation(data: JsValue); 29 | #[wasm_bindgen(js_name = "testStress")] 30 | fn test_stress(data: JsValue); 31 | } 32 | 33 | #[wasm_bindgen_test] 34 | pub fn construct_graph() { 35 | let data = example_data(); 36 | test_construct_graph(data); 37 | } 38 | 39 | #[wasm_bindgen_test] 40 | pub fn kamada_kawai() { 41 | let data = example_data(); 42 | test_kamada_kawai(data); 43 | } 44 | 45 | #[wasm_bindgen_test] 46 | pub fn stress_majorization() { 47 | let data = example_data(); 48 | test_stress_majorization(data); 49 | } 50 | 51 | #[wasm_bindgen_test] 52 | pub fn classical_mds() { 53 | let data = example_data(); 54 | test_classical_mds(data); 55 | } 56 | 57 | #[wasm_bindgen_test] 58 | pub fn pivot_mds() { 59 | let data = example_data(); 60 | test_pivot_mds(data); 61 | } 62 | 63 | #[wasm_bindgen_test] 64 | pub fn full_sgd() { 65 | let data = example_data(); 66 | test_full_sgd(data); 67 | } 68 | 69 | #[wasm_bindgen_test] 70 | pub fn sparse_sgd() { 71 | let data = example_data(); 72 | test_sparse_sgd(data); 73 | } 74 | 75 | #[wasm_bindgen_test] 76 | pub fn crossing_number() { 77 | let data = example_data(); 78 | test_crossing_number(data); 79 | } 80 | 81 | #[wasm_bindgen_test] 82 | pub fn neighborhood_preservation() { 83 | let data = example_data(); 84 | test_neighborhood_preservation(data); 85 | } 86 | 87 | #[wasm_bindgen_test] 88 | pub fn stress() { 89 | let data = example_data(); 90 | test_stress(data); 91 | } 92 | -------------------------------------------------------------------------------- /js/examples/src/pages/ExampleEdgeBundling.jsx: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | import { 3 | DrawingEuclidean2d as Drawing, 4 | Graph, 5 | FullSgd as Sgd, 6 | Rng, 7 | fdeb, 8 | } from "egraph/dist/web/egraph_wasm"; 9 | import { Wrapper } from "../wrapper"; 10 | import { useEffect, useRef } from "react"; 11 | 12 | export function ExampleEdgeBundling() { 13 | const rendererRef = useRef(); 14 | useEffect(() => { 15 | window 16 | .fetch("/data/miserables.json") 17 | .then((response) => response.json()) 18 | .then((data) => { 19 | const color = d3.scaleOrdinal(d3.schemeCategory10); 20 | const graph = new Graph(); 21 | const indices = new Map(); 22 | for (const node of data.nodes) { 23 | node.fillColor = color(node.group); 24 | indices.set(node.id, graph.addNode(node)); 25 | } 26 | for (const link of data.links) { 27 | link.strokeWidth = Math.sqrt(link.value); 28 | const { source, target } = link; 29 | graph.addEdge(indices.get(source), indices.get(target), link); 30 | } 31 | 32 | const rng = Rng.seedFrom(0n); 33 | const drawing = Drawing.initialPlacement(graph); 34 | const sgd = new Sgd(graph, () => 100); 35 | const scheduler = sgd.scheduler(100, 0.1); 36 | scheduler.run((eta) => { 37 | sgd.shuffle(rng); 38 | sgd.applyWithDrawingEuclidean2d(drawing, eta); 39 | }); 40 | 41 | for (const u of graph.nodeIndices()) { 42 | const node = graph.nodeWeight(u); 43 | node.x = drawing.x(u); 44 | node.y = drawing.y(u); 45 | } 46 | 47 | const bends = fdeb(graph, drawing); 48 | for (const e of graph.edgeIndices()) { 49 | graph.edgeWeight(e).bends = bends.get(e); 50 | } 51 | 52 | rendererRef.current.load(data); 53 | rendererRef.current.center(); 54 | }); 55 | }, []); 56 | 57 | return ( 58 | { 60 | rendererRef.current.width = width; 61 | rendererRef.current.height = height; 62 | }} 63 | > 64 | 77 | 78 | ); 79 | } 80 | --------------------------------------------------------------------------------