├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── label-check.yaml │ ├── lint.yml │ ├── milestone-merged-prs.yaml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Config.md ├── INSTALL.md ├── LICENSE.md ├── Makefile ├── README.md ├── RELEASE.md ├── _nx_parallel ├── __init__.py ├── config.py ├── script.sh └── update_get_info.py ├── assets └── images │ └── backend_box_ss.png ├── benchmarks ├── README.md ├── asv.conf.json └── benchmarks │ ├── __init__.py │ ├── bench_approximation.py │ ├── bench_bipartite.py │ ├── bench_centrality.py │ ├── bench_cluster.py │ ├── bench_connectivity.py │ ├── bench_efficiency_measures.py │ ├── bench_isolate.py │ ├── bench_shortest_paths.py │ ├── bench_tournament.py │ ├── bench_vitality.py │ └── common.py ├── nx_parallel ├── __init__.py ├── algorithms │ ├── __init__.py │ ├── approximation │ │ ├── __init__.py │ │ └── connectivity.py │ ├── bipartite │ │ ├── __init__.py │ │ └── redundancy.py │ ├── centrality │ │ ├── __init__.py │ │ ├── betweenness.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_betweenness_centrality.py │ ├── cluster.py │ ├── connectivity │ │ ├── __init__.py │ │ └── connectivity.py │ ├── efficiency_measures.py │ ├── isolate.py │ ├── shortest_paths │ │ ├── __init__.py │ │ ├── generic.py │ │ ├── unweighted.py │ │ └── weighted.py │ ├── tournament.py │ └── vitality.py ├── interface.py ├── tests │ ├── __init__.py │ ├── test_entry_points.py │ └── test_get_chunks.py └── utils │ ├── __init__.py │ ├── chunk.py │ ├── decorators.py │ └── tests │ ├── __init__.py │ └── test_chunk.py ├── pyproject.toml ├── requirements └── release.txt └── timing ├── heatmap_all_functions.png ├── heatmap_all_pairs_all_shortest_paths_timing.png ├── heatmap_all_pairs_bellman_ford_path_length_timing.png ├── heatmap_all_pairs_bellman_ford_path_timing.png ├── heatmap_all_pairs_dijkstra_path_length_timing.png ├── heatmap_all_pairs_dijkstra_path_timing.png ├── heatmap_all_pairs_dijkstra_timing.png ├── heatmap_all_pairs_node_connectivity_timing.png ├── heatmap_all_pairs_shortest_path_length_timing.png ├── heatmap_all_pairs_shortest_path_timing.png ├── heatmap_betweenness_centrality_timing.png ├── heatmap_closeness_vitality_timing.png ├── heatmap_is_reachable_timing.png ├── heatmap_is_strongly_connected_timing.png ├── heatmap_johnson_timing.png ├── heatmap_local_efficiency_timing.png ├── heatmap_node_redundancy_timing.png ├── heatmap_square_clustering_timing.png ├── timing_all_functions.py ├── timing_comparison.md └── timing_individual_function.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: "Please describe the problem you have encountered" 4 | --- 5 | 6 | 7 | 8 | 9 | ### Current Behavior 10 | 11 | 12 | 13 | ### Expected Behavior 14 | 15 | 16 | 17 | ### Steps to Reproduce 18 | 19 | 20 | 21 | ### Environment 22 | 23 | 24 | 25 | Python version: 26 | nx-parallel version: 27 | joblib version: 28 | NetworkX version: 29 | 30 | ### Additional context 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - If you are using a LLM or any other AI model (ChatGPT, deepseek, llama, mistral, claude ...), 2 | please read and follow the [Contributor's Guide](https://github.com/networkx/nx-parallel/blob/main/CONTRIBUTING.md) 3 | before submitting this PR. 4 | 5 | - Please make sure that these changes are not already being worked on in a currently open PR. 6 | 7 | ## What does this PR change? 8 | (Describe the main functionality or fix introduced by this PR.) 9 | 10 | 11 | ## What is your approach, and why did you choose it? 12 | (Explain your reasoning and any key design decisions. Mention alternative approaches you considered and why you didn’t choose them.) 13 | 14 | 15 | ## Brief summary of changes (1–2 lines) 16 | 17 | 18 | ## Additional comments/resources or notes for reviewers 19 | (Include resources or anything that may help reviewers better understand or evaluate your PR.) 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/label-check.yaml: -------------------------------------------------------------------------------- 1 | name: Labels 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - labeled 9 | - unlabeled 10 | - synchronize 11 | 12 | env: 13 | LABELS: ${{ join( github.event.pull_request.labels.*.name, ' ' ) }} 14 | 15 | jobs: 16 | check-type-label: 17 | name: core dev picked label & checked title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - if: "contains( env.LABELS, 'type: ' ) == false" 21 | run: exit 1 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: style 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | format: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install packages 25 | run: | 26 | pip install --upgrade pip 27 | pip install -e ."[developer]" 28 | 29 | - name: Lint 30 | run: pre-commit run --all-files --show-diff-on-failure --color always 31 | -------------------------------------------------------------------------------- /.github/workflows/milestone-merged-prs.yaml: -------------------------------------------------------------------------------- 1 | name: Milestone 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - closed 7 | branches: 8 | - "main" 9 | 10 | jobs: 11 | milestone_pr: 12 | name: attach to PR 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: scientific-python/attach-next-milestone-action@bc07be829f693829263e57d5e8489f4e57d3d420 16 | with: 17 | token: ${{ secrets.MILESTONE_LABELER_TOKEN }} 18 | force: true 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build Wheel and Release 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | pypi-publish: 9 | name: upload release to PyPI 10 | if: github.repository_owner == 'networkx' && startsWith(github.ref, 'refs/tags/v') && github.actor == 'jarrodmillman' && always() 11 | runs-on: ubuntu-latest 12 | # Specifying a GitHub environment is optional, but strongly encouraged 13 | environment: release 14 | permissions: 15 | # IMPORTANT: this permission is mandatory for trusted publishing 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - uses: actions/setup-python@v5 23 | name: Install Python 24 | with: 25 | python-version: "3.12" 26 | 27 | - name: Build wheels 28 | run: | 29 | git clean -fxd 30 | pip install -U build 31 | python -m build --sdist --wheel 32 | 33 | - name: Publish package distributions to PyPI 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | continue-on-error: true 12 | defaults: 13 | run: 14 | shell: bash -l {0} 15 | strategy: 16 | # fail-fast: true 17 | matrix: 18 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 19 | python-version: ["3.12", "3.13"] 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install git+https://github.com/networkx/networkx.git@main 32 | python -m pip install git+https://github.com/joblib/joblib.git@main 33 | python -m pip install ".[test]" 34 | echo "Done with installing" 35 | - name: PyTest 36 | run: | 37 | NETWORKX_TEST_BACKEND=parallel \ 38 | NETWORKX_FALLBACK_TO_NX=True \ 39 | python -m pytest --pyargs networkx 40 | - name: Additional Tests for nx-parallel 41 | run: | 42 | pytest nx_parallel 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | nxp-dev/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # asv 133 | results/ 134 | html/ 135 | 136 | # get_info update script 137 | temp__init__.py 138 | 139 | .DS_Store -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Install pre-commit hooks via 2 | # pre-commit install 3 | 4 | repos: 5 | - repo: https://github.com/adamchainz/blacken-docs 6 | rev: 1.18.0 7 | hooks: 8 | - id: blacken-docs 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.6.7 11 | hooks: 12 | - id: ruff 13 | args: 14 | - --fix 15 | - id: ruff-format 16 | - repo: local 17 | hooks: 18 | - id: update-get_info 19 | name: Update function info 20 | entry: sh _nx_parallel/script.sh 21 | language: system 22 | pass_filenames: false 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nx-parallel 0.3 2 | 3 | We're happy to announce the release of nx-parallel 0.3! 4 | 5 | ## Enhancements 6 | 7 | - Add parallel version of edge_betweenness_centrality ([#60](https://github.com/networkx/nx-parallel/pull/60)). 8 | - Adding a `pre-commit` hook to update `get_info` ([#55](https://github.com/networkx/nx-parallel/pull/55)). 9 | - ENH: Adding and documenting configs in nx-parallel ([#75](https://github.com/networkx/nx-parallel/pull/75)). 10 | 11 | ## Bug Fixes 12 | 13 | - Ignoring functions in `utils` for the `get_info` dict ([#78](https://github.com/networkx/nx-parallel/pull/78)). 14 | - Adding ruff as a developer dependency ([#85](https://github.com/networkx/nx-parallel/pull/85)). 15 | 16 | ## Documentation 17 | 18 | - Update release process ([#66](https://github.com/networkx/nx-parallel/pull/66)). 19 | - DOC: minor doc_string changes to check write access ([#70](https://github.com/networkx/nx-parallel/pull/70)). 20 | 21 | ## Infrastructure 22 | 23 | - ENH: Adding and documenting configs in nx-parallel ([#75](https://github.com/networkx/nx-parallel/pull/75)). 24 | 25 | ## Maintenance 26 | 27 | - Revisiting nxp algorithms ([#63](https://github.com/networkx/nx-parallel/pull/63)). 28 | - MAINT: Switching to setuptools ([#69](https://github.com/networkx/nx-parallel/pull/69)). 29 | - MAINT: updated readme with conda installation guide ([#71](https://github.com/networkx/nx-parallel/pull/71)). 30 | - Ignoring functions in `utils` for the `get_info` dict ([#78](https://github.com/networkx/nx-parallel/pull/78)). 31 | - Test on Python 3.13-dev ([#83](https://github.com/networkx/nx-parallel/pull/83)). 32 | - Chore/style cleanup utility functions ([#80](https://github.com/networkx/nx-parallel/pull/80)). 33 | - Clean up and bump deps, ruff, pre-commit ([#84](https://github.com/networkx/nx-parallel/pull/84)). 34 | - MAINT: Drop support for python 3.10, and update min supported networkx version to 3.4.1 ([#88](https://github.com/networkx/nx-parallel/pull/88)). 35 | - TST: Added tests checking entry_points discovery and config initialisation ([#89](https://github.com/networkx/nx-parallel/pull/89)). 36 | 37 | ## Contributors 38 | 39 | 7 authors added to this release (alphabetically): 40 | 41 | - Aditi Juneja ([@Schefflera-Arboricola](https://github.com/Schefflera-Arboricola)) 42 | - Dan Schult ([@dschult](https://github.com/dschult)) 43 | - Derek Alexander ([@dPys](https://github.com/dPys)) 44 | - Jakub Krajniak ([@jkrajniak](https://github.com/jkrajniak)) 45 | - Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman)) 46 | - Mridul Seth ([@MridulS](https://github.com/MridulS)) 47 | - Ratan Kulshreshtha ([@RatanShreshtha](https://github.com/RatanShreshtha)) 48 | 49 | 8 reviewers added to this release (alphabetically): 50 | 51 | - Aditi Juneja ([@Schefflera-Arboricola](https://github.com/Schefflera-Arboricola)) 52 | - Dan Schult ([@dschult](https://github.com/dschult)) 53 | - Derek Alexander ([@dPys](https://github.com/dPys)) 54 | - Erik Welch ([@eriknw](https://github.com/eriknw)) 55 | - Jakub Krajniak ([@jkrajniak](https://github.com/jkrajniak)) 56 | - Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman)) 57 | - Mridul Seth ([@MridulS](https://github.com/MridulS)) 58 | - Ross Barnowski ([@rossbar](https://github.com/rossbar)) 59 | 60 | _These lists are automatically generated, and may not be complete or may contain duplicates._ 61 | 62 | # nx-parallel 0.2 63 | 64 | We're happy to announce the release of nx-parallel 0.2! 65 | 66 | ## Enhancements 67 | 68 | - parallel implementation for all_pairs_bellman_ford_path ([#14](https://github.com/networkx/nx-parallel/pull/14)). 69 | - benchmarking infrastructure ([#24](https://github.com/networkx/nx-parallel/pull/24)). 70 | - ENH : Adding `backend_info` entry point ([#27](https://github.com/networkx/nx-parallel/pull/27)). 71 | - ENH : added `johnson` in `weighted.py` ([#37](https://github.com/networkx/nx-parallel/pull/37)). 72 | - ENH : added `square_clustering` ([#34](https://github.com/networkx/nx-parallel/pull/34)). 73 | - ENH : improved `all_pairs_bellman_ford_path` ([#49](https://github.com/networkx/nx-parallel/pull/49)). 74 | - ENH : added `node_redundancy` in `bipartite` ([#45](https://github.com/networkx/nx-parallel/pull/45)). 75 | - ENH : adding parallel implementations of `all_pairs_` algos ([#33](https://github.com/networkx/nx-parallel/pull/33)). 76 | - [ENH, BUG] : added `time_tournament_is_strongly_connected` benchmark, renamed `tournament_is_strongly_connected` to `is_strongly_connected` ([#32](https://github.com/networkx/nx-parallel/pull/32)). 77 | - ENH : Added `get_chunks` to `betweenness_centrality` and `create_iterables` in `utils` ([#29](https://github.com/networkx/nx-parallel/pull/29)). 78 | 79 | ## Bug Fixes 80 | 81 | - BUG: moved `get_info` from `backend.py` to `_nx_parallel` ([#53](https://github.com/networkx/nx-parallel/pull/53)). 82 | - BUG : included `_nx_parallel` in packages ([#54](https://github.com/networkx/nx-parallel/pull/54)). 83 | 84 | ## Documentation 85 | 86 | - Update release process ([#21](https://github.com/networkx/nx-parallel/pull/21)). 87 | - DOC: Adding CONTRIBUTING.md, updating readme, etc ([#46](https://github.com/networkx/nx-parallel/pull/46)). 88 | 89 | ## Maintenance 90 | 91 | - replaced **networkx_plugin** with **networkx_backend** ([#25](https://github.com/networkx/nx-parallel/pull/25)). 92 | - replaced networkx.plugins with networkx.backends and updated readme ([#26](https://github.com/networkx/nx-parallel/pull/26)). 93 | - Sync up min python version with networkx main repo ([#31](https://github.com/networkx/nx-parallel/pull/31)). 94 | - MAINT : updated `pre-commit-config.yaml` ([#35](https://github.com/networkx/nx-parallel/pull/35)). 95 | - MAINT : added lint workflow test ([#28](https://github.com/networkx/nx-parallel/pull/28)). 96 | - MAINT : styling fixes ([#38](https://github.com/networkx/nx-parallel/pull/38)). 97 | - MAINT : style fix - 2 ([#40](https://github.com/networkx/nx-parallel/pull/40)). 98 | - MAINT : added `default_benchmark_timeout` to `asv.conf.json` ([#43](https://github.com/networkx/nx-parallel/pull/43)). 99 | - MAINT : removed `NETWORKX_GRAPH_CONVERT` ([#48](https://github.com/networkx/nx-parallel/pull/48)). 100 | - MAINT: updating backend.get_info ([#47](https://github.com/networkx/nx-parallel/pull/47)). 101 | - MAINT: renamed backend function keys ([#50](https://github.com/networkx/nx-parallel/pull/50)). 102 | - MAINT: added script to update `_nx_parallel/__init__.py` ([#57](https://github.com/networkx/nx-parallel/pull/57)). 103 | - MAINT: Renaming functions with a different name while dispatching ([#56](https://github.com/networkx/nx-parallel/pull/56)). 104 | - MAINT: updated `README` and `_nx_parallel/__init__.py` ([#58](https://github.com/networkx/nx-parallel/pull/58)). 105 | - MAINT: Added LICENSE and updated README ([#59](https://github.com/networkx/nx-parallel/pull/59)). 106 | - Use pypi for tests ([#64](https://github.com/networkx/nx-parallel/pull/64)). 107 | 108 | ## Contributors 109 | 110 | 4 authors added to this release (alphabetically): 111 | 112 | - Aditi Juneja ([@Schefflera-Arboricola](https://github.com/Schefflera-Arboricola)) 113 | - Dan Schult ([@dschult](https://github.com/dschult)) 114 | - Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman)) 115 | - Mridul Seth ([@MridulS](https://github.com/MridulS)) 116 | 117 | 5 reviewers added to this release (alphabetically): 118 | 119 | - Aditi Juneja ([@Schefflera-Arboricola](https://github.com/Schefflera-Arboricola)) 120 | - Dan Schult ([@dschult](https://github.com/dschult)) 121 | - Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman)) 122 | - Mridul Seth ([@MridulS](https://github.com/MridulS)) 123 | - Ross Barnowski ([@rossbar](https://github.com/rossbar)) 124 | 125 | _These lists are automatically generated, and may not be complete or may contain duplicates._ 126 | 127 | # nx-parallel 0.1 128 | 129 | We're happy to announce the release of nx-parallel 0.1! 130 | 131 | ## Enhancements 132 | 133 | - first commit, isolates and betweenness ([#2](https://github.com/networkx/nx-parallel/pull/2)). 134 | - Reconfigure so ParallelGraph stores original nx graph ([#9](https://github.com/networkx/nx-parallel/pull/9)). 135 | 136 | ## Bug Fixes 137 | 138 | - bug fix : changed edge probability from 0.5 to p ([#13](https://github.com/networkx/nx-parallel/pull/13)). 139 | 140 | ## Documentation 141 | 142 | - Update README for Clarity and Comprehension ([#16](https://github.com/networkx/nx-parallel/pull/16)). 143 | - Add release process documentation ([#19](https://github.com/networkx/nx-parallel/pull/19)). 144 | 145 | ## Maintenance 146 | 147 | - add skeleton ([#1](https://github.com/networkx/nx-parallel/pull/1)). 148 | - Add basic test.yaml based on graphblas-algorithms ([#3](https://github.com/networkx/nx-parallel/pull/3)). 149 | - New name for repository ([#8](https://github.com/networkx/nx-parallel/pull/8)). 150 | - Clean up tests, add some minimal docs to readme ([#10](https://github.com/networkx/nx-parallel/pull/10)). 151 | - Use changelist ([#17](https://github.com/networkx/nx-parallel/pull/17)). 152 | - Use hatch as the build backend ([#20](https://github.com/networkx/nx-parallel/pull/20)). 153 | - Use trusted publisher ([#18](https://github.com/networkx/nx-parallel/pull/18)). 154 | 155 | ## Contributors 156 | 157 | 6 authors added to this release (alphabetically): 158 | 159 | - [@20kavishs](https://github.com/20kavishs) 160 | - Aditi Juneja ([@Schefflera-Arboricola](https://github.com/Schefflera-Arboricola)) 161 | - Dan Schult ([@dschult](https://github.com/dschult)) 162 | - Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman)) 163 | - MD ASIFUL ALAM ([@axif0](https://github.com/axif0)) 164 | - Mridul Seth ([@MridulS](https://github.com/MridulS)) 165 | 166 | 4 reviewers added to this release (alphabetically): 167 | 168 | - Aditi Juneja ([@Schefflera-Arboricola](https://github.com/Schefflera-Arboricola)) 169 | - Dan Schult ([@dschult](https://github.com/dschult)) 170 | - Jarrod Millman ([@jarrodmillman](https://github.com/jarrodmillman)) 171 | - Mridul Seth ([@MridulS](https://github.com/MridulS)) 172 | 173 | _These lists are automatically generated, and may not be complete or may contain 174 | duplicates._ 175 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to nx-parallel! 2 | 3 | Hi, Thanks for stopping by! 4 | 5 | This project is part of the larger NetworkX project. If you're interested in contributing to nx-parallel, you can first go through the [NetworkX's contributing guide](https://github.com/networkx/networkx/blob/main/CONTRIBUTING.rst) for general guidelines on contributing, setting up the development environment, and adding tests/docs, etc. 6 | 7 | ## Development workflow 8 | 9 | To set the local development environment: 10 | 11 | - Fork this repository. 12 | - Clone the forked repository locally: 13 | 14 | ```.sh 15 | git clone git@github.com:/nx-parallel.git 16 | ``` 17 | 18 | - Create a fresh conda/mamba virtualenv ([learn more](https://github.com/networkx/networkx/blob/main/CONTRIBUTING.rst#development-workflow)) 19 | 20 | ```.sh 21 | # Creating a virtual environment 22 | python -m venv nxp-dev 23 | 24 | # Activating the venv 25 | source nxp-dev/bin/activate 26 | ``` 27 | 28 | - Install the dependencies using the following command 29 | 30 | ```.sh 31 | make install 32 | ``` 33 | 34 | - Create a new branch for your changes using 35 | 36 | ```.sh 37 | git checkout -b 38 | ``` 39 | 40 | - Make the changes in this new feature branch and run all tests before pushing them ([learn more](https://networkx.org/documentation/latest/reference/backends.html#testing-the-custom-backend)): 41 | 42 | ```.sh 43 | make test 44 | ``` 45 | 46 | - To only run only networkx's test suite with nx-parallel as the backend run: 47 | 48 | ```.sh 49 | make run-networkx-tests 50 | ``` 51 | 52 | - To only run nx-parallel specific tests run: 53 | 54 | ```.sh 55 | make test-only-nx-parallel 56 | ``` 57 | 58 | - To run both nx-parallel specific test and networkx's test suite with the parallel backend run: 59 | 60 | ```.sh 61 | make test-backend 62 | ``` 63 | 64 | - If all the tests run successfully, stage your changes, then commit and push and then create a PR 65 | 66 | ```.sh 67 | git add . 68 | git commit -m"Your commit message" 69 | git push origin 70 | ``` 71 | 72 | ## Adding tests in nx-parallel 73 | 74 | - If an algorithm introduces an additional backend-specific argument make sure to add tests (using pytest) for it in a `tests` directory (in that algorithm's parent directory). For more, refer the networkx's tests. 75 | 76 | - `get_chunks` is the additional backend-specific argument for almost all the algorithms in nx-parallel and it's tested for all algorithms together in `nx_parallel/tests/test_get_chunks.py` file. 77 | 78 | - We use `@pytest.mark.order` for the tests that change the global configurations (i.e. `nx.config`) to make sure those tests run in the specified order and don't cause unexpected failures. 79 | 80 | ## Documentation syntax 81 | 82 | For displaying a small note about nx-parallel's implementation at the end of the main NetworkX documentation, we use the `backend_info` [entry_point](https://packaging.python.org/en/latest/specifications/entry-points/#entry-points) (in the `pyproject.toml` file). The [`get_info` function](./_nx_parallel/__init__.py) is used to parse the docstrings of all the algorithms in nx-parallel and display the nx-parallel specific documentation on the NetworkX's main docs, in the "Additional Backend implementations" box, as shown in the screenshot below. 83 | 84 | ![backend_box_ss](./assets/images/backend_box_ss.png) 85 | 86 | nx-parallel follows [sphinx docstring guidelines](https://the-ultimate-sphinx-tutorial.readthedocs.io/en/latest/_guide/_styleguides/docstrings-guidelines.html) for writing docstrings. But, while extracting the docstring to display on the main networkx docs, only the first paragraph of the function's description and the first paragraph of each parameter's description is extracted and displayed. So, make sure to include all the necessary information in the first paragraphs itself. And you only need to include the additional **backend** parameters in the `Parameters` section and not all the parameters. Also, it is recommended to include a link to the networkx function's documentation page in the docstring, at the end of the function description. 87 | 88 | Here is an example of how the docstrings should be formatted in nx-parallel: 89 | 90 | ```.py 91 | def parallel_func(G, nx_arg, additional_backend_arg_1, additional_backend_arg_2=None): 92 | """The parallel computation is implemented by dividing the 93 | nodes into chunks and ..... [ONLY THIS PARAGRAPH WILL BE DISPLAYED ON THE MAIN NETWORKX DOCS] 94 | 95 | Some more additional information about the function. 96 | 97 | networkx.func : 98 | 99 | Parameters 100 | ---------- 101 | additional_backend_arg_1 : int or float 102 | [YOU CAN HAVE MULTIPLE PARAGRAPHS BUT ONLY THE FIRST PARAGRAPH WILL BE DISPLAYED ON THE MAIN NETWORKX DOCS] 103 | 104 | additional_backend_arg_2 : None or str (default=None) 105 | .... 106 | """ 107 | ``` 108 | 109 | ## Chunking 110 | 111 | In parallel computing, "chunking" refers to dividing a large task into smaller, more manageable chunks that can be processed simultaneously by multiple computing units, such as CPU cores or distributed computing nodes. It's like breaking down a big task into smaller pieces so that multiple workers can work on different pieces at the same time, and in the case of nx-parallel, this usually speeds up the overall process. 112 | 113 | The default chunking in nx-parallel is done by slicing the list of nodes (or edges or any other iterator) into `n_jobs` number of chunks. (ref. [chunk.py](./nx_parallel/utils/chunk.py)). By default, `n_jobs` is `None`. To learn about how you can modify the value of `n_jobs` and other config options refer [`Config.md`](./Config.md). The default chunking can be overridden by the user by passing a custom `get_chunks` function to the algorithm as a kwarg. While adding a new algorithm, you can change this default chunking, if necessary (ref. [PR](https://github.com/networkx/nx-parallel/pull/33)). 114 | 115 | ## General guidelines on adding a new algorithm 116 | 117 | - To get started with adding a new algorithm, you can refer to the existing implementations in nx-parallel and also refer to the [joblib's documentation on embarrassingly parallel `for` loops](https://joblib.readthedocs.io/en/latest/parallel.html). 118 | - The algorithm that you are considering to add to nx-parallel should be in the main networkx repository and it should have the `_dispatchable` decorator. If not, you can consider adding a sequential implementation in networkx first. 119 | - check-list for adding a new function: 120 | - [ ] Add the parallel implementation(make sure API doesn't break), the file structure should be the same as that in networkx. 121 | - [ ] Include the `get_chunks` additional parameter. Currently, all algorithms in nx-parallel offer the user to pass their own custom chunks. Unless it is impossible to chunk, please do include this additional parameter. 122 | - [ ] add the function to the `ALGORITHMS` list in [interface.py](./nx_parallel/interface.py). Take care of the `name` parameter in `_dispatchable` for the algorithms with same name but different implementations. The `name` parameter is used distinguish such algorithms in a single namespace. (ref. [docs](https://networkx.org/documentation/latest/reference/backends.html)) 123 | - [ ] update the `__init__.py` files accordingly 124 | - [ ] docstring following the above format 125 | - [ ] add additional test, if needed. The smoke tests for the additional parameter `get_chunks` are done [here](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/tests/test_get_chunks.py) together for all the algorithms. 126 | - [ ] run the [timing script](./timing/timing_individual_function.py) to get the performance heatmap (ref. [Issue#51](https://github.com/networkx/nx-parallel/issues/51)) 127 | - [ ] add benchmark(s) for the new function(ref. the README in `benchmarks` folder for more details) 128 | 129 | Happy contributing! 🎉 130 | -------------------------------------------------------------------------------- /Config.md: -------------------------------------------------------------------------------- 1 | # Configuring nx-parallel 2 | 3 | `nx-parallel` provides flexible parallel computing capabilities, allowing you to control settings like `backend`, `n_jobs`, `verbose`, and more. This can be done through two configuration systems: `joblib` and `NetworkX`. This guide explains how to configure `nx-parallel` using both systems. 4 | 5 | ## 1. Setting configs using `joblib.parallel_config` 6 | 7 | `nx-parallel` relies on [`joblib.Parallel`](https://joblib.readthedocs.io/en/latest/generated/joblib.Parallel.html) for parallel computing. You can adjust its settings through the [`joblib.parallel_config`](https://joblib.readthedocs.io/en/latest/generated/joblib.parallel_config.html) class provided by `joblib`. For more details, check out the official [joblib documentation](https://joblib.readthedocs.io/en/latest/parallel.html). 8 | 9 | ### 1.1 Usage 10 | 11 | ```python 12 | from joblib import parallel_config 13 | 14 | # Setting global configs 15 | parallel_config(n_jobs=3, verbose=50) 16 | nx.square_clustering(H) 17 | 18 | # Setting configs in a context 19 | with parallel_config(n_jobs=7, verbose=0): 20 | nx.square_clustering(H) 21 | ``` 22 | 23 | Please refer the [official joblib's documentation](https://joblib.readthedocs.io/en/latest/generated/joblib.parallel_config.html) to better understand the config parameters. 24 | 25 | Note: Ensure that `nx.config.backends.parallel.active = False` when using `joblib` for configuration, as NetworkX configurations will override `joblib.parallel_config` settings if `active` is `True`. 26 | 27 | ## 2. Setting configs using `networkx`'s configuration system for backends 28 | 29 | To use NetworkX’s configuration system in `nx-parallel`, you must set the `active` flag (in `nx.config.backends.parallel`) to `True`. 30 | 31 | ### 2.1 Configs in NetworkX for backends 32 | 33 | When you import NetworkX, it automatically sets default configurations for all installed backends, including `nx-parallel`. 34 | 35 | ```python 36 | import networkx as nx 37 | 38 | print(nx.config) 39 | ``` 40 | 41 | Output: 42 | 43 | ``` 44 | NetworkXConfig( 45 | backend_priority=[], 46 | backends=Config( 47 | parallel=ParallelConfig( 48 | active=False, 49 | backend="loky", 50 | n_jobs=None, 51 | verbose=0, 52 | temp_folder=None, 53 | max_nbytes="1M", 54 | mmap_mode="r", 55 | prefer=None, 56 | require=None, 57 | inner_max_num_threads=None, 58 | backend_params={}, 59 | ) 60 | ), 61 | cache_converted_graphs=True, 62 | ) 63 | ``` 64 | 65 | As you can see in the above output, by default, `active` is set to `False`. So, to enable NetworkX configurations for `nx-parallel`, set `active` to `True`. Please refer the [NetworkX's official backend and config docs](https://networkx.org/documentation/latest/reference/backends.html) for more on networkx configuration system. 66 | 67 | ### 2.2 Usage 68 | 69 | ```python 70 | # enabling networkx's config for nx-parallel 71 | nx.config.backends.parallel.active = True 72 | 73 | # Setting global configs 74 | nxp_config = nx.config.backends.parallel 75 | nxp_config.n_jobs = 3 76 | nxp_config.verbose = 50 77 | 78 | nx.square_clustering(H) 79 | 80 | # Setting config in a context 81 | with nxp_config(n_jobs=7, verbose=0): 82 | nx.square_clustering(H) 83 | ``` 84 | 85 | The configuration parameters are the same as `joblib.parallel_config`, so you can refer to the [official joblib's documentation](https://joblib.readthedocs.io/en/latest/generated/joblib.parallel_config.html) to better understand these config parameters. 86 | 87 | ### 2.3 How Does NetworkX's Configuration Work in nx-parallel? 88 | 89 | In `nx-parallel`, there's a `_configure_if_nx_active` decorator applied to all algorithms. This decorator checks the value of `active`(in `nx.config.backends.parallel`) and then accordingly uses the appropriate configuration system (`joblib` or `networkx`). If `active=True`, it extracts the configs from `nx.config.backends.parallel` and passes them in a `joblib.parallel_config` context manager and calls the function in this context. Otherwise, it simply calls the function. 90 | 91 | ## 3. Comparing NetworkX and Joblib Configuration Systems 92 | 93 | ### 3.1 Using Both Systems Simultaneously 94 | 95 | You can use both NetworkX’s configuration system and `joblib.parallel_config` together in `nx-parallel`. However, it’s important to understand their interaction. 96 | 97 | Example: 98 | 99 | ```py 100 | # Enable NetworkX configuration 101 | nx.config.backends.parallel.active = True 102 | nx.config.backends.parallel.n_jobs = 6 103 | 104 | # Global Joblib configuration 105 | joblib.parallel_config(backend="threading") 106 | 107 | with joblib.parallel_config(n_jobs=4, verbose=55): 108 | # NetworkX config for nx-parallel 109 | # backend="loky", n_jobs=6, verbose=0 110 | nx.square_clustering(G, backend="parallel") 111 | 112 | # Joblib config for other parallel tasks 113 | # backend="threading", n_jobs=4, verbose=55 114 | joblib.Parallel()(joblib.delayed(sqrt)(i**2) for i in range(10)) 115 | ``` 116 | 117 | - **NetworkX Configurations for nx-parallel**: When calling functions within `nx-parallel`, NetworkX’s configurations will override those specified by Joblib. For example, the `nx.square_clustering` function will use the `n_jobs=6` setting from `nx.config.backends.parallel`, regardless of any Joblib settings within the same context. 118 | 119 | - **Joblib Configurations for Other Code**: For any other parallel code outside of `nx-parallel`, such as a direct call to `joblib.Parallel`, the configurations specified within the Joblib context will be applied. 120 | 121 | This behavior ensures that `nx-parallel` functions consistently use NetworkX’s settings when enabled, while still allowing Joblib configurations to apply to non-NetworkX parallel tasks. 122 | 123 | **Key Takeaway**: When both systems are used together, NetworkX's configuration (`nx.config.backends.parallel`) takes precedence for `nx-parallel` functions. To avoid unexpected behavior, ensure that the `active` setting aligns with your intended configuration system. 124 | 125 | ### 3.2 Key Differences 126 | 127 | - **Parameter Handling**: The main difference is how `backend_params` are passed. Since, in networkx configurations are stored as a [`@dataclass`](https://docs.python.org/3/library/dataclasses.html), we need to pass them as a dictionary, whereas in `joblib.parallel_config` you can just pass them along with the other configurations, as shown below: 128 | 129 | ```py 130 | nx.config.backends.parallel.backend_params = {"max_nbytes": None} 131 | joblib.parallel_config(backend="loky", max_nbytes=None) 132 | ``` 133 | 134 | - **Default Behavior**: By default, `nx-parallel` looks for configs in `joblib.parallel_config` unless `nx.config.backends.parallel.active` is set to `True`. 135 | 136 | ### 3.3 When Should You Use Which System? 137 | 138 | When the only networkx backend you're using is `nx-parallel`, then either of the NetworkX or `joblib` configuration systems can be used, depending on your preference. 139 | 140 | But, when working with multiple NetworkX backends, it's crucial to ensure compatibility among the backends to avoid conflicts between different configurations. In such cases, using NetworkX's configuration system to configure `nx-parallel` is recommended. This approach helps maintain consistency across backends. For example: 141 | 142 | ```python 143 | nx.config.backend_priority = ["another_nx_backend", "parallel"] 144 | nx.config.backends.another_nx_backend.config_1 = "xyz" 145 | joblib.parallel_config(n_jobs=7, verbose=50) 146 | 147 | nx.square_clustering(G) 148 | ``` 149 | 150 | In this example, if `another_nx_backend` also internally utilizes `joblib.Parallel` (without exposing it to the user) within its implementation of the `square_clustering` algorithm, then the `nx-parallel` configurations set by `joblib.parallel_config` will influence the internal `joblib.Parallel` used by `another_nx_backend`. To prevent unexpected behavior, it is advisable to configure these settings through the NetworkX configuration system. 151 | 152 | **Future Synchronization:** We are working on synchronizing both configuration systems so that changes in one system automatically reflect in the other. This started with [PR#68](https://github.com/networkx/nx-parallel/pull/68), which introduced a unified context manager for `nx-parallel`. For more details on the challenges of creating a compatibility layer to keep both systems in sync, refer to [Issue#76](https://github.com/networkx/nx-parallel/issues/76). 153 | 154 | If you have feedback or suggestions, feel free to open an issue or submit a pull request. 155 | 156 | Thank you :) 157 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | It is recommended to first refer the [NetworkX's INSTALL.rst](https://github.com/networkx/networkx/blob/main/INSTALL.rst). 4 | nx-parallel requires Python >=3.12. Right now, the only dependencies of nx-parallel are networkx and joblib. 5 | 6 | ### Installing nx-parallel using `pip` 7 | 8 | You can install the stable version of nx-parallel using pip: 9 | 10 | ```sh 11 | pip install nx-parallel 12 | ``` 13 | 14 | The above command also installs the two main dependencies of nx-parallel i.e. networkx 15 | and joblib. To upgrade to a newer release use the `--upgrade` flag: 16 | 17 | ```sh 18 | pip install --upgrade nx-parallel 19 | ``` 20 | 21 | ### Installing the development version 22 | 23 | Before installing the development version, you may need to uninstall the 24 | standard version of `nx-parallel` and other two dependencies using `pip`: 25 | 26 | ```sh 27 | pip uninstall nx-parallel networkx joblib 28 | ``` 29 | 30 | Then do: 31 | 32 | ```sh 33 | pip install git+https://github.com/networkx/nx-parallel.git@main 34 | ``` 35 | 36 | ### Installing nx-parallel with conda 37 | 38 | Installing `nx-parallel` from the `conda-forge` channel can be achieved by adding `conda-forge` to your channels with: 39 | 40 | ```sh 41 | conda config --add channels conda-forge 42 | conda config --set channel_priority strict 43 | ``` 44 | 45 | Once the `conda-forge` channel has been enabled, `nx-parallel` can be installed with `conda`: 46 | 47 | ```sh 48 | conda install nx-parallel 49 | ``` 50 | 51 | or with `mamba`: 52 | 53 | ```sh 54 | mamba install nx-parallel 55 | ``` 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024-2025, [Contributors of nx-parallel](https://github.com/networkx/nx-parallel/graphs/contributors) 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | python3 -m pip install -e '.[test]' 3 | python -m pip install git+https://github.com/networkx/networkx.git@main 4 | python -m pip install git+https://github.com/joblib/joblib.git@main 5 | 6 | lint: 7 | python3 -m pip install -e '.[developer]' 8 | python3 -m pre-commit run --all-files 9 | 10 | run-networkx-tests: 11 | export NETWORKX_TEST_BACKEND="parallel"; \ 12 | export NETWORKX_FALLBACK_TO_NX=True; \ 13 | python3 -m pytest --pyargs networkx 14 | 15 | test-only-nx-parallel: 16 | pytest nx_parallel 17 | 18 | test-backend: run-networkx-tests test-only-nx-parallel 19 | 20 | test: lint test-backend 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nx-parallel 2 | 3 | nx-parallel is a NetworkX backend that uses joblib for parallelization. This project aims to provide parallelized implementations of various NetworkX functions to improve performance. Refer [NetworkX backends documentation](https://networkx.org/documentation/latest/reference/backends.html) to learn more about the backend architecture in NetworkX. 4 | 5 | ## Algorithms in nx-parallel 6 | 7 | - [all_pairs_all_shortest_paths](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/generic.py#L11) 8 | - [all_pairs_bellman_ford_path](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L212) 9 | - [all_pairs_bellman_ford_path_length](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L168) 10 | - [all_pairs_dijkstra](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L29) 11 | - [all_pairs_dijkstra_path](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L124) 12 | - [all_pairs_dijkstra_path_length](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L73) 13 | - [all_pairs_node_connectivity](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/connectivity/connectivity.py#L18) 14 | - [all_pairs_shortest_path](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/unweighted.py#L63) 15 | - [all_pairs_shortest_path_length](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/unweighted.py#L19) 16 | - [approximate_all_pairs_node_connectivity](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/approximation/connectivity.py#L13) 17 | - [betweenness_centrality](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/centrality/betweenness.py#L20) 18 | - [closeness_vitality](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/vitality.py#L10) 19 | - [edge_betweenness_centrality](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/centrality/betweenness.py#L96) 20 | - [is_reachable](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/tournament.py#L13) 21 | - [johnson](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L256) 22 | - [local_efficiency](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/efficiency_measures.py#L10) 23 | - [node_redundancy](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/bipartite/redundancy.py#L12) 24 | - [number_of_isolates](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/isolate.py#L9) 25 | - [square_clustering](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/cluster.py#L11) 26 | - [tournament_is_strongly_connected](https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/tournament.py#L59) 27 | 28 |
29 | Script used to generate the above list 30 | 31 | ``` 32 | import _nx_parallel as nxp 33 | d = nxp.get_funcs_info() # temporarily add `from .update_get_info import *` to _nx_parallel/__init__.py 34 | for func in d: 35 | print(f"- [{func}]({d[func]['url']})") 36 | ``` 37 | 38 |
39 | 40 | ## Installation 41 | 42 | You can install the stable version of nx-parallel using pip or conda: 43 | 44 | ```sh 45 | pip install nx-parallel 46 | 47 | conda install nx-parallel 48 | ``` 49 | 50 | For more, see [INSTALL.md](./INSTALL.md). 51 | 52 | ## Backend usage 53 | 54 | You can run your networkx code with nx-parallel backend by: 55 | 56 | ```sh 57 | export NETWORKX_AUTOMATIC_BACKENDS="parallel" && python nx_code.py 58 | ``` 59 | 60 | Note that for all functions inside `nx_code.py` that do not have an nx-parallel implementation their original networkx implementation will be executed. You can also use the nx-parallel backend in your code for only some specific function calls in the following ways: 61 | 62 | ```py 63 | import networkx as nx 64 | import nx_parallel as nxp 65 | 66 | # enabling networkx's config for nx-parallel 67 | nx.config.backends.parallel.active = True 68 | 69 | # setting `n_jobs` (by default, `n_jobs=None`) 70 | nx.config.backends.parallel.n_jobs = 4 71 | 72 | G = nx.path_graph(4) 73 | H = nxp.ParallelGraph(G) 74 | 75 | # method 1 : passing ParallelGraph object in networkx function (Type-based dispatching) 76 | nx.betweenness_centrality(H) 77 | 78 | # method 2 : using the 'backend' kwarg 79 | nx.betweenness_centrality(G, backend="parallel") 80 | 81 | # method 3 : using nx-parallel implementation with networkx object 82 | nxp.betweenness_centrality(G) 83 | 84 | # method 4 : using nx-parallel implementation with ParallelGraph object 85 | nxp.betweenness_centrality(H) 86 | ``` 87 | 88 | For more on how to play with configurations in nx-parallel refer the [Config.md](./Config.md)! Additionally, refer the [NetworkX's official backend and config docs](https://networkx.org/documentation/latest/reference/backends.html) for more on functionalities provided by networkx for backends and configs like logging, `backend_priority`, etc. Another way to configure nx-parallel is by using [`joblib.parallel_config`](https://joblib.readthedocs.io/en/latest/generated/joblib.parallel_config.html). 89 | 90 | ### Notes 91 | 92 | 1. Some functions in networkx have the same name but different implementations, so to avoid these name conflicts at the time of dispatching networkx differentiates them by specifying the `name` parameter in the `_dispatchable` decorator of such algorithms. So, `method 3` and `method 4` are not recommended. But, you can use them if you know the correct `name`. For example: 93 | 94 | ```py 95 | # using `name` parameter - nx-parallel as an independent package 96 | 97 | # run the parallel implementation in `connectivity/connectivity` 98 | nxp.all_pairs_node_connectivity(H) 99 | 100 | # runs the parallel implementation in `approximation/connectivity` 101 | nxp.approximate_all_pairs_node_connectivity(H) 102 | ``` 103 | 104 | Also, if you are using nx-parallel as a backend then mentioning the subpackage to which the algorithm belongs is recommended to ensure that networkx dispatches to the correct implementation. For example: 105 | 106 | ```py 107 | # with subpackage - nx-parallel as a backend 108 | nx.all_pairs_node_connectivity(H) 109 | nx.approximation.all_pairs_node_connectivity(H) 110 | ``` 111 | 112 | 2. Right now there isn't much difference between `nx.Graph` and `nxp.ParallelGraph` so `method 3` would work fine but it is not recommended because in the future that might not be the case. 113 | 114 | Feel free to contribute to nx-parallel. You can find the contributing guidelines [here](./CONTRIBUTING.md). If you'd like to implement a feature or fix a bug, we'd be happy to review a pull request. Please make sure to explain the changes you made in the pull request description. And feel free to open issues for any problems you face, or for new features you'd like to see implemented. 115 | 116 | This project is managed under the NetworkX organisation, so the [code of conduct of NetworkX](https://github.com/networkx/networkx/blob/main/CODE_OF_CONDUCT.rst) applies here as well. 117 | 118 | All code in this repository is available under the Berkeley Software Distribution (BSD) 3-Clause License (see LICENSE). 119 | 120 | Thank you :) 121 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process for `nx-parallel` 2 | 3 | ## Introduction 4 | 5 | Example `version number` 6 | 7 | - 1.8.dev0 # development version of 1.8 (release candidate 1) 8 | - 1.8rc1 # 1.8 release candidate 1 9 | - 1.8rc2.dev0 # development version of 1.8 release candidate 2 10 | - 1.8 # 1.8 release 11 | - 1.9.dev0 # development version of 1.9 (release candidate 1) 12 | 13 | ## Process 14 | 15 | - Set release variables: 16 | 17 | export VERSION= 18 | export PREVIOUS= 19 | export ORG="networkx" 20 | export REPO="nx-parallel" 21 | export LOG="CHANGELOG.md" 22 | 23 | - Autogenerate release notes 24 | 25 | changelist ${ORG}/${REPO} v${PREVIOUS} main --version ${VERSION} --out ${VERSION}.md 26 | 27 | - Put the output of the above command at the top of `CHANGELOG.md` 28 | 29 | cat ${VERSION}.md | cat - ${LOG} > temp && mv temp ${LOG} 30 | 31 | - Update `__version__` in `nx_parallel/__init__.py`. 32 | 33 | - Commit changes: 34 | 35 | git add nx_parallel/__init__.py ${LOG} 36 | git commit -m "Designate ${VERSION} release" 37 | 38 | - Tag the release in git: 39 | 40 | git tag -s v${VERSION} -m "signed ${VERSION} tag" 41 | 42 | If you do not have a gpg key, use -u instead; it is important for 43 | Debian packaging that the tags are annotated 44 | 45 | - Push the new meta-data to github: 46 | 47 | git push --tags origin main 48 | 49 | where `origin` is the name of the `github.com:networkx/nx-parallel` 50 | repository 51 | 52 | - Review the github release page: 53 | 54 | https://github.com/networkx/nx-parallel/tags 55 | 56 | - Create release from tag 57 | 58 | - go to https://github.com/networkx/nx-parallel/releases/new?tag=v${VERSION} 59 | - add v${VERSION} for the `Release title` 60 | - paste contents (or upload) of ${VERSION}.md in the `Describe this release section` 61 | - if pre-release check the box labelled `Set as a pre-release` 62 | 63 | - Update https://github.com/networkx/nx-parallel/milestones: 64 | 65 | - close old milestone 66 | - ensure new milestone exists (perhaps setting due date) 67 | 68 | - Update `__version__` in `nx_parallel/__init__.py`. 69 | 70 | - Commit changes: 71 | 72 | git add nx_parallel/__init__.py 73 | git commit -m 'Bump version' 74 | git push origin main 75 | -------------------------------------------------------------------------------- /_nx_parallel/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by update_get_info.py 2 | 3 | from .config import _config 4 | 5 | 6 | def get_info(): 7 | """Return a dictionary with information about the package.""" 8 | return { 9 | "backend_name": "parallel", 10 | "project": "nx-parallel", 11 | "package": "nx_parallel", 12 | "url": "https://github.com/networkx/nx-parallel", 13 | "short_summary": "A networkx backend that uses joblib to run graph algorithms in parallel. Find the nx-parallel's configuration guide `here `_", 14 | "default_config": _config, 15 | "functions": { 16 | "all_pairs_all_shortest_paths": { 17 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/generic.py#L11", 18 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute all shortest paths between all nodes for each node in `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 19 | "additional_parameters": { 20 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 21 | }, 22 | }, 23 | "all_pairs_bellman_ford_path": { 24 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L208", 25 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths for each node_chunk, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 26 | "additional_parameters": { 27 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 28 | }, 29 | }, 30 | "all_pairs_bellman_ford_path_length": { 31 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L165", 32 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths lengths for each node in `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 33 | "additional_parameters": { 34 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 35 | }, 36 | }, 37 | "all_pairs_dijkstra": { 38 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L29", 39 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths and lengths for each `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 40 | "additional_parameters": { 41 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 42 | }, 43 | }, 44 | "all_pairs_dijkstra_path": { 45 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L122", 46 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths for each `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 47 | "additional_parameters": { 48 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 49 | }, 50 | }, 51 | "all_pairs_dijkstra_path_length": { 52 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L72", 53 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths lengths for each node in `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 54 | "additional_parameters": { 55 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 56 | }, 57 | }, 58 | "all_pairs_node_connectivity": { 59 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/connectivity/connectivity.py#L18", 60 | "additional_docs": "The parallel implementation first divides a list of all permutation (in case of directed graphs) and combinations (in case of undirected graphs) of `nbunch` into chunks and then creates a generator to lazily compute the local node connectivities for each chunk, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores. At the end, the results are aggregated into a single dictionary and returned.", 61 | "additional_parameters": { 62 | 'get_chunks : str, function (default = "chunks")': "A function that takes in `list(iter_func(nbunch, 2))` as input and returns an iterable `pairs_chunks`, here `iter_func` is `permutations` in case of directed graphs and `combinations` in case of undirected graphs. The default is to create chunks by slicing the list into `n_jobs` number of chunks, such that size of each chunk is atmost 10, and at least 1." 63 | }, 64 | }, 65 | "all_pairs_shortest_path": { 66 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/unweighted.py#L62", 67 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths for each `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 68 | "additional_parameters": { 69 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 70 | }, 71 | }, 72 | "all_pairs_shortest_path_length": { 73 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/unweighted.py#L19", 74 | "additional_docs": "The parallel implementation first divides the nodes into chunks and then creates a generator to lazily compute shortest paths lengths for each node in `node_chunk`, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores.", 75 | "additional_parameters": { 76 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 77 | }, 78 | }, 79 | "approximate_all_pairs_node_connectivity": { 80 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/approximation/connectivity.py#L14", 81 | "additional_docs": "The parallel implementation first divides the a list of all permutation (in case of directed graphs) and combinations (in case of undirected graphs) of `nbunch` into chunks and then creates a generator to lazily compute the local node connectivities for each chunk, and then employs joblib's `Parallel` function to execute these computations in parallel across `n_jobs` number of CPU cores. At the end, the results are aggregated into a single dictionary and returned.", 82 | "additional_parameters": { 83 | 'get_chunks : str, function (default = "chunks")': "A function that takes in `list(iter_func(nbunch, 2))` as input and returns an iterable `pairs_chunks`, here `iter_func` is `permutations` in case of directed graphs and `combinations` in case of undirected graphs. The default is to create chunks by slicing the list into `n_jobs` chunks, such that size of each chunk is atmost 10, and at least 1." 84 | }, 85 | }, 86 | "betweenness_centrality": { 87 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/centrality/betweenness.py#L20", 88 | "additional_docs": "The parallel computation is implemented by dividing the nodes into chunks and computing betweenness centrality for each chunk concurrently.", 89 | "additional_parameters": { 90 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 91 | }, 92 | }, 93 | "closeness_vitality": { 94 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/vitality.py#L10", 95 | "additional_docs": "The parallel computation is implemented only when the node is not specified. The closeness vitality for each node is computed concurrently.", 96 | "additional_parameters": { 97 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 98 | }, 99 | }, 100 | "edge_betweenness_centrality": { 101 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/centrality/betweenness.py#L103", 102 | "additional_docs": "The parallel computation is implemented by dividing the nodes into chunks and computing edge betweenness centrality for each chunk concurrently.", 103 | "additional_parameters": { 104 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 105 | }, 106 | }, 107 | "is_reachable": { 108 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/tournament.py#L13", 109 | "additional_docs": "The function parallelizes the calculation of two neighborhoods of vertices in `G` and checks closure conditions for each neighborhood subset in parallel.", 110 | "additional_parameters": { 111 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 112 | }, 113 | }, 114 | "johnson": { 115 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/shortest_paths/weighted.py#L251", 116 | "additional_docs": "The parallel computation is implemented by dividing the nodes into chunks and computing the shortest paths using Johnson's Algorithm for each chunk in parallel.", 117 | "additional_parameters": { 118 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` into `n_jobs` number of chunks." 119 | }, 120 | }, 121 | "local_efficiency": { 122 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/efficiency_measures.py#L11", 123 | "additional_docs": "The parallel computation is implemented by dividing the nodes into chunks and then computing and adding global efficiencies of all node in all chunks, in parallel, and then adding all these sums and dividing by the total number of nodes at the end.", 124 | "additional_parameters": { 125 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 126 | }, 127 | }, 128 | "node_redundancy": { 129 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/bipartite/redundancy.py#L12", 130 | "additional_docs": "In the parallel implementation we divide the nodes into chunks and compute the node redundancy coefficients for all `node_chunk` in parallel.", 131 | "additional_parameters": { 132 | 'get_chunks : str, function (default = "chunks")': "A function that takes in an iterable of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `G.nodes` (or `nodes`) into `n_jobs` number of chunks." 133 | }, 134 | }, 135 | "number_of_isolates": { 136 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/isolate.py#L9", 137 | "additional_docs": "The parallel computation is implemented by dividing the list of isolated nodes into chunks and then finding the length of each chunk in parallel and then adding all the lengths at the end.", 138 | "additional_parameters": { 139 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the isolated nodes as input and returns an iterable `isolate_chunks`. The default chunking is done by slicing the `isolates` into `n_jobs` number of chunks." 140 | }, 141 | }, 142 | "square_clustering": { 143 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/cluster.py#L11", 144 | "additional_docs": "The nodes are chunked into `node_chunks` and then the square clustering coefficient for all `node_chunks` are computed in parallel over `n_jobs` number of CPU cores.", 145 | "additional_parameters": { 146 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes (or nbunch) as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 147 | }, 148 | }, 149 | "tournament_is_strongly_connected": { 150 | "url": "https://github.com/networkx/nx-parallel/blob/main/nx_parallel/algorithms/tournament.py#L58", 151 | "additional_docs": "The parallel computation is implemented by dividing the nodes into chunks and then checking whether each node is reachable from each other node in parallel.", 152 | "additional_parameters": { 153 | 'get_chunks : str, function (default = "chunks")': "A function that takes in a list of all the nodes as input and returns an iterable `node_chunks`. The default chunking is done by slicing the `nodes` into `n_jobs` number of chunks." 154 | }, 155 | }, 156 | }, 157 | } 158 | -------------------------------------------------------------------------------- /_nx_parallel/config.py: -------------------------------------------------------------------------------- 1 | from networkx.utils.configs import Config 2 | from typing import Union 3 | from dataclasses import dataclass, field 4 | 5 | __all__ = [ 6 | "_config", 7 | ] 8 | 9 | 10 | @dataclass 11 | class ParallelConfig(Config): 12 | active: bool = False 13 | backend: str = "loky" 14 | n_jobs: int = None 15 | verbose: int = 0 16 | temp_folder: str = None 17 | max_nbytes: Union[int, str] = "1M" 18 | mmap_mode: str = "r" 19 | prefer: str = None 20 | require: str = None 21 | inner_max_num_threads: int = None 22 | backend_params: dict = field(default_factory=dict) 23 | 24 | 25 | _config = ParallelConfig() 26 | -------------------------------------------------------------------------------- /_nx_parallel/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 _nx_parallel/update_get_info.py 4 | 5 | ruff format "_nx_parallel/temp__init__.py" 6 | 7 | # Check if there's any difference between the original file and the formatted one 8 | if diff -q "_nx_parallel/__init__.py" "_nx_parallel/temp__init__.py" >/dev/null; then 9 | rm "_nx_parallel/temp__init__.py" 10 | else 11 | mv "_nx_parallel/temp__init__.py" "_nx_parallel/__init__.py" 12 | fi 13 | -------------------------------------------------------------------------------- /_nx_parallel/update_get_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ast 3 | 4 | __all__ = [ 5 | "get_funcs_info", 6 | "extract_docstrings_from_file", 7 | "extract_add_docs", 8 | "extract_add_params", 9 | "get_url", 10 | ] 11 | 12 | # Helper functions for get_info 13 | 14 | 15 | def get_funcs_info(): 16 | """Return a dictionary with function names as keys and function's details as values. 17 | The function's details include the URL to the function in the source code, the parallel 18 | computation description, and the additional parameters description.""" 19 | funcs = {} 20 | 21 | nx_parallel_dir = os.path.join(os.getcwd(), "nx_parallel") 22 | for root, dirs, files in os.walk(nx_parallel_dir): 23 | if "nx_parallel/utils" in root: 24 | continue 25 | for file in files: 26 | if ( 27 | file.endswith(".py") 28 | and file != "__init__.py" 29 | and not file.startswith("test_") 30 | ): 31 | path = os.path.join(root, file) 32 | d = extract_docstrings_from_file(path) 33 | for func in d: 34 | funcs[func] = { 35 | "url": get_url(path, func), 36 | "additional_docs": extract_add_docs(d[func]), 37 | "additional_parameters": extract_add_params(d[func]), 38 | } 39 | sorted_funcs = dict(sorted(funcs.items())) 40 | return sorted_funcs 41 | 42 | 43 | def extract_docstrings_from_file(file_path): 44 | """ 45 | Extract docstrings from functions listed in the __all__ list of a Python file. 46 | 47 | Args: 48 | - file_path: The path to the Python file. 49 | 50 | Returns: 51 | - A dictionary mapping function names to their docstrings. 52 | """ 53 | docstrings = {} 54 | with open(file_path, "r") as f: 55 | tree = ast.parse(f.read(), filename=file_path) 56 | all_list = None 57 | for node in tree.body: 58 | if isinstance(node, ast.Assign): 59 | if ( 60 | isinstance(node.targets[0], ast.Name) 61 | and node.targets[0].id == "__all__" 62 | ): 63 | all_list = [ 64 | expr.value 65 | for expr in node.value.elts 66 | if isinstance(expr, ast.Constant) 67 | ] 68 | elif isinstance(node, ast.FunctionDef): 69 | if all_list and node.name in all_list: 70 | docstring = ast.get_docstring(node) or "No docstring found." 71 | docstrings[node.name] = docstring 72 | return docstrings 73 | 74 | 75 | def extract_add_docs(docstring): 76 | """Extract the parallel documentation description from the given doctring.""" 77 | try: 78 | # Extracting Parallel Computation description 79 | # Assuming that the first para in docstring is the function's PC desc 80 | # "par" is short for "parallel" 81 | par_docs_ = docstring.split("\n\n")[0] 82 | par_docs_ = par_docs_.split("\n") 83 | par_docs_ = [line.strip() for line in par_docs_ if line.strip()] 84 | par_docs = " ".join(par_docs_) 85 | par_docs = par_docs.replace("\n", " ") 86 | except IndexError: 87 | par_docs = None 88 | except Exception as e: 89 | print(e) 90 | par_docs = None 91 | return par_docs 92 | 93 | 94 | def extract_add_params(docstring): 95 | """Extract the parallel parameter description from the given docstring.""" 96 | try: 97 | # Extracting extra parameters 98 | # Assuming that the last para in docstring is the function's extra params 99 | par_params = {} 100 | par_params_ = docstring.split("----------\n")[1] 101 | par_params_ = par_params_.split("\n") 102 | 103 | i = 0 104 | while i < len(par_params_): 105 | line = par_params_[i] 106 | if " : " in line: 107 | key = line.strip() 108 | n = par_params_.index(key) + 1 109 | par_desc = "" 110 | while n < len(par_params_) and par_params_[n] != "": 111 | par_desc += par_params_[n].strip() + " " 112 | n += 1 113 | par_params[key] = par_desc.strip() 114 | i = n + 1 115 | else: 116 | i += 1 117 | except IndexError: 118 | par_params = None 119 | except Exception as e: 120 | print(e) 121 | par_params = None 122 | return par_params 123 | 124 | 125 | def get_url(file_path, function_name): 126 | """Return the URL to the given function in the given file.""" 127 | file_url = ( 128 | "https://github.com/networkx/nx-parallel/blob/main/nx_parallel" 129 | + file_path.split("nx_parallel")[-1] 130 | + "#L" 131 | ) 132 | with open(file_path, "r") as f: 133 | tree = ast.parse(f.read(), filename=file_path) 134 | for node in ast.walk(tree): 135 | if isinstance(node, ast.FunctionDef) and node.name == function_name: 136 | return file_url + str(node.lineno) 137 | return file_url 138 | 139 | 140 | # Creating a temp__init__.py file 141 | 142 | string = '''# This file was automatically generated by update_get_info.py 143 | 144 | from .config import _config 145 | 146 | def get_info(): 147 | """Return a dictionary with information about the package.""" 148 | return { 149 | "backend_name": "parallel", 150 | "project": "nx-parallel", 151 | "package": "nx_parallel", 152 | "url": "https://github.com/networkx/nx-parallel", 153 | "short_summary": "A networkx backend that uses joblib to run graph algorithms in parallel. Find the nx-parallel's configuration guide `here `_", 154 | "default_config": _config, 155 | "functions": ''' 156 | 157 | with open("_nx_parallel/temp__init__.py", "w") as f: 158 | f.write(string + str(get_funcs_info()) + "}\n") 159 | -------------------------------------------------------------------------------- /assets/images/backend_box_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/assets/images/backend_box_ss.png -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | These asv benchmarks are not just good to see how parallel implementations are improving over the commits but you can also compare how much better nx-parallel implementations are as compared to the networkx implementations, by switching the x-axis parameter to be `backends`. There are also heatmaps in the `/timing` folder that show the speedups of the parallel and networkx implementations of the same function. 4 | 5 | ## Preview benchmarks locally 6 | 7 | 1. clone this repo and setup the development algorithm(ref. [README](https://github.com/networkx/nx-parallel?tab=readme-ov-file#development-install)) 8 | 2. run `pip install asv` 9 | 3. navigate using `cd benchmarks` 10 | 4. If you are working on a different branch then update the value of `branches` in the `asv.conf.json` file. 11 | 5. `asv run` will run the benchmarks on the last commit 12 | - or use `asv continuous base_commit_hash test_commit_hash` to run the benchmark to compare two commits 13 | - or `asv run -b -k ` to run a particular benchmark in a file. 14 | - or `asv run -b BenchmarkClassName.time_benchmark_func_name` to run a specific benchmark in a benchmark class. 15 | - if you are running benchmarks for the first time, you will be asked to enter your machine information after this command. 16 | 6. `asv publish` will create an `html` folder with the results. 17 | 7. `asv preview` will host the results locally at http://127.0.0.1:8080/ 18 | 19 |
20 | 21 | ## Structure of benchmarks 22 | 23 | - Each `bench_` file corresponds to a folder/file in the [networkx/algorithms](https://github.com/networkx/networkx/tree/main/networkx/algorithms) directory in NetworkX 24 | - Each class inside a `bench_` file corresponds to every file in a folder(one class if it’s a file) in networkx/algorithms 25 | - The class name corresponds to the file name and the `x` in `bench_x` corresponds to the folder name(class name and `x` are the same if it’s a file in networkx/algorithms) 26 | - Each `time_` function corresponds to each function in the file. 27 | - For other folders in [networkx/networkx](https://github.com/networkx/networkx/tree/main/networkx) like `generators`, `classes`, `linalg`, `utils` etc. we can have different `bench_` files for each of them having different classes corresponding to different files in each of these folders. 28 | - For example: `bench_centrality.py` corresponds to `networkx/algorithms/centrality` folder in NetworkX and the `Betweenness` class inside it corresponds to the `betweenness.py` file in `networkx/algorithms/centrality` folder in NetworkX. And the `time_betweenness_centrality` function corresponds to the `betweenness_centrality` function. 29 | -------------------------------------------------------------------------------- /benchmarks/asv.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "project": "nx-parallel", 4 | "project_url": "https://github.com/networkx/nx-parallel", 5 | "repo": "..", 6 | "branches": ["main"], 7 | "build_command": [ 8 | "python -m pip install build", 9 | "python -m build --wheel -o {build_cache_dir} {build_dir}" 10 | ], 11 | "dvcs": "git", 12 | "environment_type": "virtualenv", 13 | "show_commit_url": "https://github.com/networkx/nx-parallel/commit/", 14 | "matrix": {"networkx": ["3.4.2"], "nx-parallel": []}, 15 | "benchmark_dir": "benchmarks", 16 | "env_dir": "env", 17 | "results_dir": "results", 18 | "html_dir": "html", 19 | "build_cache_size": 8, 20 | "default_benchmark_timeout": 1200, 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_approximation.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | import nx_parallel as nxp 10 | 11 | 12 | class Connectivity(Benchmark): 13 | params = [(backends), (num_nodes), (edge_prob)] 14 | param_names = ["backend", "num_nodes", "edge_prob"] 15 | 16 | def time_approximate_all_pairs_node_connectivity( 17 | self, backend, num_nodes, edge_prob 18 | ): 19 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 20 | if backend == "parallel": 21 | G = nxp.ParallelGraph(G) 22 | _ = nx.algorithms.approximation.connectivity.all_pairs_node_connectivity(G) 23 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_bipartite.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | edge_prob, 4 | Benchmark, 5 | ) 6 | import networkx as nx 7 | 8 | # for an unbalanced bipartite random graph 9 | n = [50, 100, 200, 400, 800] 10 | m = [25, 50, 100, 200, 400] 11 | 12 | 13 | class Redundancy(Benchmark): 14 | params = [(backends), (n), (m), (edge_prob)] 15 | param_names = ["backend", "n", "m", "edge_prob"] 16 | 17 | def time_node_redundancy(self, backend, n, m, edge_prob): 18 | G = get_random_bipartite_graph(n, m, edge_prob) 19 | _ = nx.node_redundancy(G, backend=backend) 20 | 21 | 22 | def get_random_bipartite_graph(n, m, p, seed=42, directed=False): 23 | """Ref. https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.bipartite.generators.random_graph.html""" 24 | return nx.bipartite.random_graph(n, m, p, seed, directed) 25 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_centrality.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | 10 | 11 | class Betweenness(Benchmark): 12 | params = [(backends), (num_nodes), (edge_prob)] 13 | param_names = ["backend", "num_nodes", "edge_prob"] 14 | 15 | def time_betweenness_centrality(self, backend, num_nodes, edge_prob): 16 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 17 | _ = nx.betweenness_centrality(G, backend=backend) 18 | 19 | def time_edge_betweenness_centrality(self, backend, num_nodes, edge_prob): 20 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 21 | _ = nx.edge_betweenness_centrality(G, backend=backend) 22 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_cluster.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | 10 | 11 | class Cluster(Benchmark): 12 | params = [(backends), (num_nodes), (edge_prob)] 13 | param_names = ["backend", "num_nodes", "edge_prob"] 14 | 15 | def time_square_clustering(self, backend, num_nodes, edge_prob): 16 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 17 | _ = nx.square_clustering(G, backend=backend) 18 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_connectivity.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | import nx_parallel as nxp 10 | 11 | 12 | class Connectivity(Benchmark): 13 | params = [(backends), (num_nodes), (edge_prob)] 14 | param_names = ["backend", "num_nodes", "edge_prob"] 15 | 16 | def time_all_pairs_node_connectivity(self, backend, num_nodes, edge_prob): 17 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 18 | if backend == "parallel": 19 | G = nxp.ParallelGraph(G) 20 | _ = nx.algorithms.connectivity.connectivity.all_pairs_node_connectivity(G) 21 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_efficiency_measures.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | 10 | 11 | class EfficiencyMeasures(Benchmark): 12 | params = [(backends), (num_nodes), (edge_prob)] 13 | param_names = ["backend", "num_nodes", "edge_prob"] 14 | 15 | def time_local_efficiency(self, backend, num_nodes, edge_prob): 16 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 17 | _ = nx.local_efficiency(G, backend=backend) 18 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_isolate.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | 10 | 11 | class Isolate(Benchmark): 12 | params = [(backends), (num_nodes), (edge_prob)] 13 | param_names = ["backend", "num_nodes", "edge_prob"] 14 | 15 | def time_number_of_isolates(self, backend, num_nodes, edge_prob): 16 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 17 | _ = nx.number_of_isolates(G, backend=backend) 18 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_shortest_paths.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | 10 | 11 | class Generic(Benchmark): 12 | params = [(backends), (num_nodes), (edge_prob)] 13 | param_names = ["backend", "num_nodes", "edge_prob"] 14 | 15 | def time_all_pairs_all_shortest_paths(self, backend, num_nodes, edge_prob): 16 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 17 | _ = dict(nx.all_pairs_all_shortest_paths(G, weight="weight", backend=backend)) 18 | 19 | 20 | class Unweighted(Benchmark): 21 | params = [(backends), (num_nodes), (edge_prob)] 22 | param_names = ["backend", "num_nodes", "edge_prob"] 23 | 24 | def time_all_pairs_shortest_path_length(self, backend, num_nodes, edge_prob): 25 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 26 | _ = dict(nx.all_pairs_shortest_path_length(G, backend=backend)) 27 | 28 | def time_all_pairs_shortest_path(self, backend, num_nodes, edge_prob): 29 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 30 | _ = dict(nx.all_pairs_shortest_path(G, backend=backend)) 31 | 32 | 33 | class Weighted(Benchmark): 34 | params = [(backends), (num_nodes), (edge_prob)] 35 | param_names = ["backend", "num_nodes", "edge_prob"] 36 | 37 | def time_all_pairs_dijkstra(self, backend, num_nodes, edge_prob): 38 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 39 | _ = dict(nx.all_pairs_dijkstra(G, backend=backend)) 40 | 41 | def time_all_pairs_dijkstra_path_length(self, backend, num_nodes, edge_prob): 42 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 43 | _ = dict(nx.all_pairs_dijkstra_path_length(G, backend=backend)) 44 | 45 | def time_all_pairs_dijkstra_path(self, backend, num_nodes, edge_prob): 46 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 47 | _ = dict(nx.all_pairs_dijkstra_path(G, backend=backend)) 48 | 49 | def time_all_pairs_bellman_ford_path_length(self, backend, num_nodes, edge_prob): 50 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 51 | _ = dict(nx.all_pairs_bellman_ford_path_length(G, backend=backend)) 52 | 53 | def time_all_pairs_bellman_ford_path(self, backend, num_nodes, edge_prob): 54 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 55 | _ = dict(nx.all_pairs_bellman_ford_path(G, backend=backend)) 56 | 57 | def time_johnson(self, backend, num_nodes, edge_prob): 58 | G = get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=True) 59 | _ = nx.johnson(G, backend=backend) 60 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_tournament.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | Benchmark, 5 | ) 6 | import networkx as nx 7 | 8 | 9 | class Tournament(Benchmark): 10 | params = [(backends), (num_nodes)] 11 | param_names = ["backend", "num_nodes"] 12 | 13 | def time_is_reachable(self, backend, num_nodes): 14 | G = nx.tournament.random_tournament(num_nodes, seed=42) 15 | _ = nx.tournament.is_reachable(G, 1, num_nodes, backend=backend) 16 | 17 | def time_tournament_is_strongly_connected(self, backend, num_nodes): 18 | G = nx.tournament.random_tournament(num_nodes, seed=42) 19 | _ = nx.tournament.is_strongly_connected(G, backend=backend) 20 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/bench_vitality.py: -------------------------------------------------------------------------------- 1 | from .common import ( 2 | backends, 3 | num_nodes, 4 | edge_prob, 5 | get_cached_gnp_random_graph, 6 | Benchmark, 7 | ) 8 | import networkx as nx 9 | 10 | 11 | class Vitality(Benchmark): 12 | params = [(backends), (num_nodes), (edge_prob)] 13 | param_names = ["backend", "num_nodes", "edge_prob"] 14 | 15 | def time_closeness_vitality(self, backend, num_nodes, edge_prob): 16 | G = get_cached_gnp_random_graph(num_nodes, edge_prob) 17 | _ = nx.closeness_vitality(G, backend=backend) 18 | -------------------------------------------------------------------------------- /benchmarks/benchmarks/common.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from pathlib import Path 3 | import random 4 | 5 | import networkx as nx 6 | 7 | __all__ = [ 8 | "backends", 9 | "num_nodes", 10 | "edge_prob", 11 | "get_cached_gnp_random_graph", 12 | "Benchmark", 13 | ] 14 | 15 | CACHE_ROOT = Path(__file__).resolve().parent.parent / "env" / "nxp_benchdata" 16 | 17 | backends = ["parallel", None] 18 | num_nodes = [50, 100, 200, 400, 800] 19 | edge_prob = [0.8, 0.6, 0.4, 0.2] 20 | 21 | 22 | @lru_cache(typed=True) 23 | def get_cached_gnp_random_graph(num_nodes, edge_prob, is_weighted=False): 24 | G = nx.fast_gnp_random_graph(num_nodes, edge_prob, seed=42, directed=False) 25 | if is_weighted: 26 | random.seed(42) 27 | for u, v in G.edges(): 28 | G.edges[u, v]["weight"] = random.random() 29 | return G 30 | 31 | 32 | class Benchmark: 33 | pass 34 | -------------------------------------------------------------------------------- /nx_parallel/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils.decorators import _configure_if_nx_active 2 | from .utils import * 3 | from .algorithms import * 4 | from .interface import * 5 | 6 | __version__ = "0.4rc0.dev0" 7 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | # subpackages 2 | from .bipartite import * 3 | from .centrality import * 4 | from .shortest_paths import * 5 | from .approximation import * 6 | from .connectivity import * 7 | 8 | # modules 9 | from .efficiency_measures import * 10 | from .isolate import * 11 | from .tournament import * 12 | from .vitality import * 13 | from .cluster import * 14 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/approximation/__init__.py: -------------------------------------------------------------------------------- 1 | from .connectivity import * 2 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/approximation/connectivity.py: -------------------------------------------------------------------------------- 1 | """Parallel implementations of fast approximation for node connectivity""" 2 | 3 | import itertools 4 | from joblib import Parallel, delayed 5 | from networkx.algorithms.approximation.connectivity import local_node_connectivity 6 | import nx_parallel as nxp 7 | 8 | __all__ = [ 9 | "approximate_all_pairs_node_connectivity", 10 | ] 11 | 12 | 13 | @nxp._configure_if_nx_active() 14 | def approximate_all_pairs_node_connectivity( 15 | G, nbunch=None, cutoff=None, get_chunks="chunks" 16 | ): 17 | """The parallel implementation first divides the a list of all permutation (in case 18 | of directed graphs) and combinations (in case of undirected graphs) of `nbunch` 19 | into chunks and then creates a generator to lazily compute the local node 20 | connectivities for each chunk, and then employs joblib's `Parallel` function to 21 | execute these computations in parallel across `n_jobs` number of CPU cores. At the 22 | end, the results are aggregated into a single dictionary and returned. 23 | 24 | Note, this function uses the name `approximate_all_pairs_node_connectivity` while 25 | dispatching to the backend in=mplementation. So, `nxp.all_pairs_node_connectivity` 26 | will run the parallel implementation of `all_pairs_node_connectivity` present in the 27 | `connectivity/connectivity`. Use `nxp.approximate_all_pairs_node_connectivity` instead. 28 | 29 | networkx.all_pairs_node_connectivity : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.approximation.connectivity.all_pairs_node_connectivity.html 30 | 31 | Parameters 32 | ---------- 33 | get_chunks : str, function (default = "chunks") 34 | A function that takes in `list(iter_func(nbunch, 2))` as input and returns 35 | an iterable `pairs_chunks`, here `iter_func` is `permutations` in case of 36 | directed graphs and `combinations` in case of undirected graphs. The default 37 | is to create chunks by slicing the list into `n_jobs` chunks, such that size 38 | of each chunk is atmost 10, and at least 1. 39 | """ 40 | 41 | if hasattr(G, "graph_object"): 42 | G = G.graph_object 43 | 44 | if nbunch is None: 45 | nbunch = G 46 | else: 47 | nbunch = set(nbunch) 48 | 49 | directed = G.is_directed() 50 | if directed: 51 | iter_func = itertools.permutations 52 | else: 53 | iter_func = itertools.combinations 54 | 55 | all_pairs = {n: {} for n in nbunch} 56 | 57 | def _process_pair_chunk(pairs_chunk): 58 | return [ 59 | (u, v, local_node_connectivity(G, u, v, cutoff=cutoff)) 60 | for u, v in pairs_chunk 61 | ] 62 | 63 | pairs = list(iter_func(nbunch, 2)) 64 | n_jobs = nxp.get_n_jobs() 65 | if get_chunks == "chunks": 66 | pairs_chunks = nxp.chunks(pairs, n_jobs, max_chunk_size=10) 67 | else: 68 | pairs_chunks = get_chunks(pairs) 69 | 70 | nc_chunk_generator = ( # nc = node connectivity 71 | delayed(_process_pair_chunk)(pairs_chunk) for pairs_chunk in pairs_chunks 72 | ) 73 | 74 | for nc_chunk in Parallel()(nc_chunk_generator): 75 | for u, v, k in nc_chunk: 76 | all_pairs[u][v] = k 77 | if not directed: 78 | all_pairs[v][u] = k 79 | return all_pairs 80 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/bipartite/__init__.py: -------------------------------------------------------------------------------- 1 | from .redundancy import * 2 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/bipartite/redundancy.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | from networkx.algorithms.bipartite.redundancy import _node_redundancy 3 | import networkx as nx 4 | import nx_parallel as nxp 5 | from itertools import chain 6 | 7 | 8 | __all__ = ["node_redundancy"] 9 | 10 | 11 | @nxp._configure_if_nx_active() 12 | def node_redundancy(G, nodes=None, get_chunks="chunks"): 13 | """In the parallel implementation we divide the nodes into chunks and compute 14 | the node redundancy coefficients for all `node_chunk` in parallel. 15 | 16 | networkx.bipartite.node_redundancy : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.bipartite.redundancy.node_redundancy.html 17 | 18 | Parameters 19 | ---------- 20 | get_chunks : str, function (default = "chunks") 21 | A function that takes in an iterable of all the nodes as input and returns 22 | an iterable `node_chunks`. The default chunking is done by slicing the 23 | `G.nodes` (or `nodes`) into `n_jobs` number of chunks. 24 | """ 25 | 26 | if hasattr(G, "graph_object"): 27 | G = G.graph_object 28 | if nodes is None: 29 | nodes = G 30 | if any(len(G[v]) < 2 for v in nodes): 31 | raise nx.NetworkXError( 32 | "Cannot compute redundancy coefficient for a node" 33 | " that has fewer than two neighbors." 34 | ) 35 | n_jobs = nxp.get_n_jobs() 36 | if get_chunks == "chunks": 37 | node_chunks = nxp.chunks(nodes, n_jobs) 38 | else: 39 | node_chunks = get_chunks(nodes) 40 | node_redundancies = Parallel()( 41 | delayed( 42 | lambda G, node_chunk: [(v, _node_redundancy(G, v)) for v in node_chunk] 43 | )(G, node_chunk) 44 | for node_chunk in node_chunks 45 | ) 46 | return dict(chain.from_iterable((node_redundancies))) 47 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/centrality/__init__.py: -------------------------------------------------------------------------------- 1 | from .betweenness import * 2 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/centrality/betweenness.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | from networkx.algorithms.centrality.betweenness import ( 3 | _accumulate_basic, 4 | _accumulate_endpoints, 5 | _rescale, 6 | _single_source_dijkstra_path_basic, 7 | _single_source_shortest_path_basic, 8 | _rescale_e, 9 | _add_edge_keys, 10 | _accumulate_edges, 11 | ) 12 | from networkx.utils import py_random_state 13 | import nx_parallel as nxp 14 | 15 | __all__ = ["betweenness_centrality", "edge_betweenness_centrality"] 16 | 17 | 18 | @nxp._configure_if_nx_active() 19 | @py_random_state(5) 20 | def betweenness_centrality( 21 | G, 22 | k=None, 23 | normalized=True, 24 | weight=None, 25 | endpoints=False, 26 | seed=None, 27 | get_chunks="chunks", 28 | ): 29 | """The parallel computation is implemented by dividing the nodes into chunks and 30 | computing betweenness centrality for each chunk concurrently. 31 | 32 | networkx.betweenness_centrality : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.betweenness_centrality.html 33 | 34 | Parameters 35 | ---------- 36 | get_chunks : str, function (default = "chunks") 37 | A function that takes in a list of all the nodes as input and returns an 38 | iterable `node_chunks`. The default chunking is done by slicing the 39 | `nodes` into `n_jobs` number of chunks. 40 | """ 41 | if hasattr(G, "graph_object"): 42 | G = G.graph_object 43 | 44 | if not G: 45 | return {} 46 | 47 | if k == len(G): 48 | k = None 49 | 50 | if k is None: 51 | nodes = G.nodes 52 | else: 53 | nodes = seed.sample(list(G.nodes), k) 54 | 55 | n_jobs = nxp.get_n_jobs() 56 | 57 | if get_chunks == "chunks": 58 | node_chunks = nxp.create_iterables(G, "node", n_jobs, nodes) 59 | else: 60 | node_chunks = get_chunks(nodes) 61 | 62 | bt_cs = Parallel()( 63 | delayed(_betweenness_centrality_node_subset)(G, chunk, weight, endpoints) 64 | for chunk in node_chunks 65 | ) 66 | 67 | # Reducing partial solution 68 | bt_c = bt_cs[0] 69 | for bt in bt_cs[1:]: 70 | for n in bt: 71 | bt_c[n] += bt[n] 72 | 73 | betweenness = _rescale( 74 | bt_c, 75 | len(G), 76 | normalized=normalized, 77 | directed=G.is_directed(), 78 | k=k, 79 | endpoints=endpoints, 80 | sampled_nodes=nodes, 81 | ) 82 | return betweenness 83 | 84 | 85 | def _betweenness_centrality_node_subset(G, nodes, weight=None, endpoints=False): 86 | betweenness = dict.fromkeys(G, 0.0) 87 | for s in nodes: 88 | # single source shortest paths 89 | if weight is None: # use BFS 90 | S, P, sigma, _ = _single_source_shortest_path_basic(G, s) 91 | else: # use Dijkstra's algorithm 92 | S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight) 93 | # accumulation 94 | if endpoints: 95 | betweenness, delta = _accumulate_endpoints(betweenness, S, P, sigma, s) 96 | else: 97 | betweenness, delta = _accumulate_basic(betweenness, S, P, sigma, s) 98 | return betweenness 99 | 100 | 101 | @nxp._configure_if_nx_active() 102 | @py_random_state(4) 103 | def edge_betweenness_centrality( 104 | G, k=None, normalized=True, weight=None, seed=None, get_chunks="chunks" 105 | ): 106 | """The parallel computation is implemented by dividing the nodes into chunks and 107 | computing edge betweenness centrality for each chunk concurrently. 108 | 109 | networkx.edge_betweenness_centrality : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.centrality.edge_betweenness_centrality.html 110 | 111 | Parameters 112 | ---------- 113 | get_chunks : str, function (default = "chunks") 114 | A function that takes in a list of all the nodes as input and returns an 115 | iterable `node_chunks`. The default chunking is done by slicing the 116 | `nodes` into `n_jobs` number of chunks. 117 | """ 118 | if hasattr(G, "graph_object"): 119 | G = G.graph_object 120 | 121 | if not G: 122 | return {} 123 | 124 | if k is None: 125 | nodes = G.nodes 126 | else: 127 | nodes = seed.sample(list(G.nodes), k) 128 | 129 | n_jobs = nxp.get_n_jobs() 130 | 131 | if get_chunks == "chunks": 132 | node_chunks = nxp.create_iterables(G, "node", n_jobs, nodes) 133 | else: 134 | node_chunks = get_chunks(nodes) 135 | 136 | bt_cs = Parallel()( 137 | delayed(_edge_betweenness_centrality_node_subset)(G, chunk, weight) 138 | for chunk in node_chunks 139 | ) 140 | 141 | # Reducing partial solution 142 | bt_c = bt_cs[0] 143 | for bt in bt_cs[1:]: 144 | for e in bt: 145 | bt_c[e] += bt[e] 146 | 147 | for n in G: # remove nodes to only return edges 148 | del bt_c[n] 149 | 150 | betweenness = _rescale_e(bt_c, len(G), normalized=normalized, k=k) 151 | 152 | if G.is_multigraph(): 153 | betweenness = _add_edge_keys(G, betweenness, weight=weight) 154 | 155 | return betweenness 156 | 157 | 158 | def _edge_betweenness_centrality_node_subset(G, nodes, weight=None): 159 | betweenness = dict.fromkeys(G, 0.0) # b[v]=0 for v in G 160 | # b[e]=0 for e in G.edges() 161 | betweenness.update(dict.fromkeys(G.edges(), 0.0)) 162 | for s in nodes: 163 | # single source shortest paths 164 | if weight is None: # use BFS 165 | S, P, sigma, _ = _single_source_shortest_path_basic(G, s) 166 | else: # use Dijkstra's algorithm 167 | S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight) 168 | # accumulation 169 | betweenness = _accumulate_edges(betweenness, S, P, sigma, s) 170 | return betweenness 171 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/centrality/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/nx_parallel/algorithms/centrality/tests/__init__.py -------------------------------------------------------------------------------- /nx_parallel/algorithms/centrality/tests/test_betweenness_centrality.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import nx_parallel as nxp 3 | import math 4 | 5 | 6 | def test_betweenness_centrality_get_chunks(): 7 | def get_chunk(nodes): 8 | num_chunks = nxp.get_n_jobs() 9 | nodes_ebc = {i: 0 for i in nodes} 10 | for i in ebc: 11 | nodes_ebc[i[0]] += ebc[i] 12 | nodes_ebc[i[1]] += ebc[i] 13 | 14 | sorted_nodes = sorted(nodes_ebc.items(), key=lambda x: x[1], reverse=True) 15 | 16 | chunks = [[] for _ in range(num_chunks)] 17 | chunk_sums = [0] * num_chunks 18 | 19 | for node, value in sorted_nodes: 20 | min_chunk_index = chunk_sums.index(min(chunk_sums)) 21 | chunks[min_chunk_index].append(node) 22 | chunk_sums[min_chunk_index] += value 23 | 24 | return chunks 25 | 26 | G = nx.fast_gnp_random_graph(100, 0.1, directed=False) 27 | H = nxp.ParallelGraph(G) 28 | ebc = nx.edge_betweenness_centrality(G) 29 | par_bc_chunk = nxp.betweenness_centrality(H, get_chunks=get_chunk) # smoke test 30 | par_bc = nxp.betweenness_centrality(H) 31 | 32 | for i in range(len(G.nodes)): 33 | assert math.isclose(par_bc[i], par_bc_chunk[i], abs_tol=1e-16) 34 | # get_chunk is faster than default(for big graphs) 35 | # G = nx.bipartite.random_graph(400, 700, 0.8, seed=5, directed=False) 36 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/cluster.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations, chain 2 | from joblib import Parallel, delayed 3 | import nx_parallel as nxp 4 | 5 | __all__ = [ 6 | "square_clustering", 7 | ] 8 | 9 | 10 | @nxp._configure_if_nx_active() 11 | def square_clustering(G, nodes=None, get_chunks="chunks"): 12 | """The nodes are chunked into `node_chunks` and then the square clustering 13 | coefficient for all `node_chunks` are computed in parallel over `n_jobs` number 14 | of CPU cores. 15 | 16 | networkx.square_clustering: https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.cluster.square_clustering.html 17 | 18 | Parameters 19 | ---------- 20 | get_chunks : str, function (default = "chunks") 21 | A function that takes in a list of all the nodes (or nbunch) as input and 22 | returns an iterable `node_chunks`. The default chunking is done by slicing the 23 | `nodes` into `n_jobs` number of chunks. 24 | """ 25 | 26 | def _compute_clustering_chunk(node_iter_chunk): 27 | result_chunk = [] 28 | for v in node_iter_chunk: 29 | clustering = 0 30 | potential = 0 31 | v_nbrs = G_nbrs_as_sets[v] 32 | for u, w in combinations(v_nbrs, 2): 33 | u_nbrs = G_nbrs_as_sets[u] 34 | w_nbrs = G_nbrs_as_sets[w] 35 | squares = len((u_nbrs & w_nbrs) - {v}) 36 | clustering += squares 37 | degm = squares + 1 38 | if w in u_nbrs: 39 | degm += 1 40 | potential += (len(u_nbrs) - degm) + (len(w_nbrs) - degm) + squares 41 | if potential > 0: 42 | clustering /= potential 43 | result_chunk += [(v, clustering)] 44 | return result_chunk 45 | 46 | if hasattr(G, "graph_object"): 47 | G = G.graph_object 48 | 49 | # ignore self-loops as per networkx 3.5 50 | G_nbrs_as_sets = {node: set(G[node]) - {node} for node in G} 51 | 52 | if nodes is None: 53 | node_iter = list(G) 54 | else: 55 | node_iter = list(G.nbunch_iter(nodes)) 56 | 57 | n_jobs = nxp.get_n_jobs() 58 | 59 | if get_chunks == "chunks": 60 | node_iter_chunks = nxp.chunks(node_iter, n_jobs) 61 | else: 62 | node_iter_chunks = get_chunks(node_iter) 63 | 64 | result = Parallel()( 65 | delayed(_compute_clustering_chunk)(node_iter_chunk) 66 | for node_iter_chunk in node_iter_chunks 67 | ) 68 | clustering = dict(chain.from_iterable(result)) 69 | 70 | if nodes in G: 71 | return clustering[nodes] 72 | return clustering 73 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/connectivity/__init__.py: -------------------------------------------------------------------------------- 1 | from .connectivity import * 2 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/connectivity/connectivity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parallel flow based connectivity algorithms 3 | """ 4 | 5 | import itertools 6 | from networkx.algorithms.flow import build_residual_network 7 | from networkx.algorithms.connectivity.utils import build_auxiliary_node_connectivity 8 | from networkx.algorithms.connectivity.connectivity import local_node_connectivity 9 | from joblib import Parallel, delayed 10 | import nx_parallel as nxp 11 | 12 | __all__ = [ 13 | "all_pairs_node_connectivity", 14 | ] 15 | 16 | 17 | @nxp._configure_if_nx_active() 18 | def all_pairs_node_connectivity(G, nbunch=None, flow_func=None, get_chunks="chunks"): 19 | """The parallel implementation first divides a list of all permutation (in case 20 | of directed graphs) and combinations (in case of undirected graphs) of `nbunch` 21 | into chunks and then creates a generator to lazily compute the local node 22 | connectivities for each chunk, and then employs joblib's `Parallel` function to 23 | execute these computations in parallel across `n_jobs` number of CPU cores. At the end, 24 | the results are aggregated into a single dictionary and returned. 25 | 26 | networkx.all_pairs_node_connectivity : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.connectivity.all_pairs_node_connectivity.html 27 | 28 | Parameters 29 | ---------- 30 | get_chunks : str, function (default = "chunks") 31 | A function that takes in `list(iter_func(nbunch, 2))` as input and returns 32 | an iterable `pairs_chunks`, here `iter_func` is `permutations` in case of 33 | directed graphs and `combinations` in case of undirected graphs. The default 34 | is to create chunks by slicing the list into `n_jobs` number of chunks, such 35 | that size of each chunk is atmost 10, and at least 1. 36 | """ 37 | 38 | if hasattr(G, "graph_object"): 39 | G = G.graph_object 40 | 41 | if nbunch is None: 42 | nbunch = G 43 | else: 44 | nbunch = set(nbunch) 45 | 46 | directed = G.is_directed() 47 | if directed: 48 | iter_func = itertools.permutations 49 | else: 50 | iter_func = itertools.combinations 51 | 52 | all_pairs = {n: {} for n in nbunch} 53 | 54 | # Reuse auxiliary digraph and residual network 55 | H = build_auxiliary_node_connectivity(G) 56 | R = build_residual_network(H, "capacity") 57 | kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R} 58 | 59 | def _process_pair_chunk(pairs_chunk): 60 | return [ 61 | (u, v, local_node_connectivity(G, u, v, **kwargs)) for u, v in pairs_chunk 62 | ] 63 | 64 | pairs = list(iter_func(nbunch, 2)) 65 | n_jobs = nxp.get_n_jobs() 66 | if get_chunks == "chunks": 67 | pairs_chunks = nxp.chunks(pairs, n_jobs, max_chunk_size=10) 68 | else: 69 | pairs_chunks = get_chunks(pairs) 70 | 71 | nc_chunk_generator = ( # nc = node connectivity 72 | delayed(_process_pair_chunk)(pairs_chunk) for pairs_chunk in pairs_chunks 73 | ) 74 | 75 | for nc_chunk in Parallel()(nc_chunk_generator): 76 | for u, v, k in nc_chunk: 77 | all_pairs[u][v] = k 78 | if not directed: 79 | all_pairs[v][u] = k 80 | return all_pairs 81 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/efficiency_measures.py: -------------------------------------------------------------------------------- 1 | """Provides functions for computing the efficiency of nodes and graphs.""" 2 | 3 | import networkx as nx 4 | from joblib import Parallel, delayed 5 | import nx_parallel as nxp 6 | 7 | __all__ = ["local_efficiency"] 8 | 9 | 10 | @nxp._configure_if_nx_active() 11 | def local_efficiency(G, get_chunks="chunks"): 12 | """The parallel computation is implemented by dividing the 13 | nodes into chunks and then computing and adding global efficiencies of all node 14 | in all chunks, in parallel, and then adding all these sums and dividing by the 15 | total number of nodes at the end. 16 | 17 | networkx.local_efficiency : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.efficiency_measures.local_efficiency.html 18 | 19 | Parameters 20 | ---------- 21 | get_chunks : str, function (default = "chunks") 22 | A function that takes in a list of all the nodes as input and returns an 23 | iterable `node_chunks`. The default chunking is done by slicing the `nodes` 24 | into `n_jobs` number of chunks. 25 | """ 26 | 27 | def _local_efficiency_node_subset(G, chunk): 28 | return sum(nx.global_efficiency(G.subgraph(G[v])) for v in chunk) 29 | 30 | if hasattr(G, "graph_object"): 31 | G = G.graph_object 32 | 33 | n_jobs = nxp.get_n_jobs() 34 | 35 | if get_chunks == "chunks": 36 | node_chunks = list(nxp.chunks(G.nodes, n_jobs)) 37 | else: 38 | node_chunks = get_chunks(G.nodes) 39 | 40 | efficiencies = Parallel()( 41 | delayed(_local_efficiency_node_subset)(G, chunk) for chunk in node_chunks 42 | ) 43 | return sum(efficiencies) / len(G) 44 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/isolate.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from joblib import Parallel, delayed 3 | import nx_parallel as nxp 4 | 5 | __all__ = ["number_of_isolates"] 6 | 7 | 8 | @nxp._configure_if_nx_active() 9 | def number_of_isolates(G, get_chunks="chunks"): 10 | """The parallel computation is implemented by dividing the list 11 | of isolated nodes into chunks and then finding the length of each chunk in parallel 12 | and then adding all the lengths at the end. 13 | 14 | networkx.number_of_isolates : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.isolate.number_of_isolates.html 15 | 16 | Parameters 17 | ---------- 18 | get_chunks : str, function (default = "chunks") 19 | A function that takes in a list of all the isolated nodes as input and returns an 20 | iterable `isolate_chunks`. The default chunking is done by slicing the `isolates` 21 | into `n_jobs` number of chunks. 22 | """ 23 | if hasattr(G, "graph_object"): 24 | G = G.graph_object 25 | 26 | n_jobs = nxp.get_n_jobs() 27 | 28 | isolates_list = list(nx.isolates(G)) 29 | if get_chunks == "chunks": 30 | isolate_chunks = nxp.chunks(isolates_list, n_jobs) 31 | else: 32 | isolate_chunks = get_chunks(isolates_list) 33 | 34 | results = Parallel()(delayed(len)(chunk) for chunk in isolate_chunks) 35 | return sum(results) 36 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/shortest_paths/__init__.py: -------------------------------------------------------------------------------- 1 | from .generic import * 2 | from .weighted import * 3 | from .unweighted import * 4 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/shortest_paths/generic.py: -------------------------------------------------------------------------------- 1 | from networkx.algorithms.shortest_paths.generic import single_source_all_shortest_paths 2 | from joblib import Parallel, delayed 3 | import nx_parallel as nxp 4 | 5 | __all__ = [ 6 | "all_pairs_all_shortest_paths", 7 | ] 8 | 9 | 10 | @nxp._configure_if_nx_active() 11 | def all_pairs_all_shortest_paths( 12 | G, weight=None, method="dijkstra", get_chunks="chunks" 13 | ): 14 | """The parallel implementation first divides the nodes into chunks and then 15 | creates a generator to lazily compute all shortest paths between all nodes for 16 | each node in `node_chunk`, and then employs joblib's `Parallel` function to 17 | execute these computations in parallel across `n_jobs` number of CPU cores. 18 | 19 | networkx.single_source_all_shortest_paths : https://networkx.org/documentation/latest/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.single_source_all_shortest_paths.html 20 | 21 | Parameters 22 | ---------- 23 | get_chunks : str, function (default = "chunks") 24 | A function that takes in an iterable of all the nodes as input and returns 25 | an iterable `node_chunks`. The default chunking is done by slicing the 26 | `G.nodes` into `n_jobs` number of chunks. 27 | """ 28 | 29 | def _process_node_chunk(node_chunk): 30 | return [ 31 | ( 32 | n, 33 | dict( 34 | single_source_all_shortest_paths(G, n, weight=weight, method=method) 35 | ), 36 | ) 37 | for n in node_chunk 38 | ] 39 | 40 | if hasattr(G, "graph_object"): 41 | G = G.graph_object 42 | 43 | nodes = G.nodes 44 | n_jobs = nxp.get_n_jobs() 45 | 46 | if get_chunks == "chunks": 47 | node_chunks = nxp.chunks(nodes, n_jobs) 48 | else: 49 | node_chunks = get_chunks(nodes) 50 | 51 | paths_chunk_generator = ( 52 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 53 | ) 54 | 55 | for path_chunk in Parallel()(paths_chunk_generator): 56 | for path in path_chunk: 57 | yield path 58 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/shortest_paths/unweighted.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shortest path parallel algorithms for unweighted graphs. 3 | """ 4 | 5 | from joblib import Parallel, delayed 6 | import nx_parallel as nxp 7 | from networkx.algorithms.shortest_paths.unweighted import ( 8 | single_source_shortest_path_length, 9 | single_source_shortest_path, 10 | ) 11 | 12 | __all__ = [ 13 | "all_pairs_shortest_path", 14 | "all_pairs_shortest_path_length", 15 | ] 16 | 17 | 18 | @nxp._configure_if_nx_active() 19 | def all_pairs_shortest_path_length(G, cutoff=None, get_chunks="chunks"): 20 | """The parallel implementation first divides the nodes into chunks and then 21 | creates a generator to lazily compute shortest paths lengths for each node in 22 | `node_chunk`, and then employs joblib's `Parallel` function to execute these 23 | computations in parallel across `n_jobs` number of CPU cores. 24 | 25 | networkx.single_source_shortest_path_length : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path_length.html 26 | 27 | Parameters 28 | ---------- 29 | get_chunks : str, function (default = "chunks") 30 | A function that takes in an iterable of all the nodes as input and returns 31 | an iterable `node_chunks`. The default chunking is done by slicing the 32 | `G.nodes` into `n_jobs` number of chunks. 33 | """ 34 | 35 | def _process_node_chunk(node_chunk): 36 | return [ 37 | (node, single_source_shortest_path_length(G, node, cutoff=cutoff)) 38 | for node in node_chunk 39 | ] 40 | 41 | if hasattr(G, "graph_object"): 42 | G = G.graph_object 43 | 44 | nodes = G.nodes 45 | n_jobs = nxp.get_n_jobs() 46 | 47 | if get_chunks == "chunks": 48 | node_chunks = nxp.chunks(nodes, n_jobs) 49 | else: 50 | node_chunks = get_chunks(nodes) 51 | 52 | path_lengths_chunk_generator = ( 53 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 54 | ) 55 | 56 | for path_length_chunk in Parallel()(path_lengths_chunk_generator): 57 | for path_length in path_length_chunk: 58 | yield path_length 59 | 60 | 61 | @nxp._configure_if_nx_active() 62 | def all_pairs_shortest_path(G, cutoff=None, get_chunks="chunks"): 63 | """The parallel implementation first divides the nodes into chunks and then 64 | creates a generator to lazily compute shortest paths for each `node_chunk`, and 65 | then employs joblib's `Parallel` function to execute these computations in 66 | parallel across `n_jobs` number of CPU cores. 67 | 68 | networkx.single_source_shortest_path : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path.html 69 | 70 | Parameters 71 | ---------- 72 | get_chunks : str, function (default = "chunks") 73 | A function that takes in an iterable of all the nodes as input and returns 74 | an iterable `node_chunks`. The default chunking is done by slicing the 75 | `G.nodes` into `n_jobs` number of chunks. 76 | """ 77 | 78 | def _process_node_chunk(node_chunk): 79 | return [ 80 | (node, single_source_shortest_path(G, node, cutoff=cutoff)) 81 | for node in node_chunk 82 | ] 83 | 84 | if hasattr(G, "graph_object"): 85 | G = G.graph_object 86 | 87 | nodes = G.nodes 88 | n_jobs = nxp.get_n_jobs() 89 | 90 | if get_chunks == "chunks": 91 | node_chunks = nxp.chunks(nodes, n_jobs) 92 | else: 93 | node_chunks = get_chunks(nodes) 94 | 95 | paths_chunk_generator = ( 96 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 97 | ) 98 | 99 | for path_chunk in Parallel()(paths_chunk_generator): 100 | for path in path_chunk: 101 | yield path 102 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/shortest_paths/weighted.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shortest path parallel algorithms for weighted graphs. 3 | """ 4 | 5 | from joblib import Parallel, delayed 6 | import nx_parallel as nxp 7 | from networkx.algorithms.shortest_paths.weighted import ( 8 | single_source_dijkstra, 9 | single_source_dijkstra_path_length, 10 | single_source_dijkstra_path, 11 | single_source_bellman_ford_path, 12 | single_source_bellman_ford_path_length, 13 | _weight_function, 14 | _dijkstra, 15 | _bellman_ford, 16 | ) 17 | 18 | __all__ = [ 19 | "all_pairs_dijkstra", 20 | "all_pairs_dijkstra_path_length", 21 | "all_pairs_dijkstra_path", 22 | "all_pairs_bellman_ford_path_length", 23 | "all_pairs_bellman_ford_path", 24 | "johnson", 25 | ] 26 | 27 | 28 | @nxp._configure_if_nx_active() 29 | def all_pairs_dijkstra(G, cutoff=None, weight="weight", get_chunks="chunks"): 30 | """The parallel implementation first divides the nodes into chunks and then 31 | creates a generator to lazily compute shortest paths and lengths for each 32 | `node_chunk`, and then employs joblib's `Parallel` function to execute these 33 | computations in parallel across `n_jobs` number of CPU cores. 34 | 35 | networkx.all_pairs_dijkstra : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.all_pairs_dijkstra.html#all-pairs-dijkstra 36 | 37 | Parameters 38 | ---------- 39 | get_chunks : str, function (default = "chunks") 40 | A function that takes in an iterable of all the nodes as input and returns 41 | an iterable `node_chunks`. The default chunking is done by slicing the 42 | `G.nodes` into `n_jobs` number of chunks. 43 | """ 44 | 45 | def _process_node_chunk(node_chunk): 46 | return [ 47 | (node, (single_source_dijkstra(G, node, cutoff=cutoff, weight=weight))) 48 | for node in node_chunk 49 | ] 50 | 51 | if hasattr(G, "graph_object"): 52 | G = G.graph_object 53 | 54 | nodes = G.nodes 55 | n_jobs = nxp.get_n_jobs() 56 | 57 | if get_chunks == "chunks": 58 | node_chunks = nxp.chunks(nodes, n_jobs) 59 | else: 60 | node_chunks = get_chunks(nodes) 61 | 62 | paths_chunk_generator = ( 63 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 64 | ) 65 | 66 | for path_chunk in Parallel()(paths_chunk_generator): 67 | for path in path_chunk: 68 | yield path 69 | 70 | 71 | @nxp._configure_if_nx_active() 72 | def all_pairs_dijkstra_path_length( 73 | G, cutoff=None, weight="weight", get_chunks="chunks" 74 | ): 75 | """The parallel implementation first divides the nodes into chunks and then 76 | creates a generator to lazily compute shortest paths lengths for each node in 77 | `node_chunk`, and then employs joblib's `Parallel` function to execute these 78 | computations in parallel across `n_jobs` number of CPU cores. 79 | 80 | networkx.all_pairs_dijkstra_path_length : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.all_pairs_dijkstra_path_length.html 81 | 82 | Parameters 83 | ---------- 84 | get_chunks : str, function (default = "chunks") 85 | A function that takes in an iterable of all the nodes as input and returns 86 | an iterable `node_chunks`. The default chunking is done by slicing the 87 | `G.nodes` into `n_jobs` number of chunks. 88 | """ 89 | 90 | def _process_node_chunk(node_chunk): 91 | return [ 92 | ( 93 | node, 94 | single_source_dijkstra_path_length( 95 | G, node, cutoff=cutoff, weight=weight 96 | ), 97 | ) 98 | for node in node_chunk 99 | ] 100 | 101 | if hasattr(G, "graph_object"): 102 | G = G.graph_object 103 | 104 | nodes = G.nodes 105 | n_jobs = nxp.get_n_jobs() 106 | 107 | if get_chunks == "chunks": 108 | node_chunks = nxp.chunks(nodes, n_jobs) 109 | else: 110 | node_chunks = get_chunks(nodes) 111 | 112 | paths_chunk_generator = ( 113 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 114 | ) 115 | 116 | for path_chunk in Parallel()(paths_chunk_generator): 117 | for path in path_chunk: 118 | yield path 119 | 120 | 121 | @nxp._configure_if_nx_active() 122 | def all_pairs_dijkstra_path(G, cutoff=None, weight="weight", get_chunks="chunks"): 123 | """The parallel implementation first divides the nodes into chunks and then 124 | creates a generator to lazily compute shortest paths for each `node_chunk`, and 125 | then employs joblib's `Parallel` function to execute these computations in 126 | parallel across `n_jobs` number of CPU cores. 127 | 128 | networkx.all_pairs_dijkstra_path : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.all_pairs_dijkstra_path.html 129 | 130 | Parameters 131 | ---------- 132 | get_chunks : str, function (default = "chunks") 133 | A function that takes in an iterable of all the nodes as input and returns 134 | an iterable `node_chunks`. The default chunking is done by slicing the 135 | `G.nodes` into `n_jobs` number of chunks. 136 | """ 137 | 138 | def _process_node_chunk(node_chunk): 139 | return [ 140 | (node, single_source_dijkstra_path(G, node, cutoff=cutoff, weight=weight)) 141 | for node in node_chunk 142 | ] 143 | 144 | if hasattr(G, "graph_object"): 145 | G = G.graph_object 146 | 147 | nodes = G.nodes 148 | n_jobs = nxp.get_n_jobs() 149 | 150 | if get_chunks == "chunks": 151 | node_chunks = nxp.chunks(nodes, n_jobs) 152 | else: 153 | node_chunks = get_chunks(nodes) 154 | 155 | paths_chunk_generator = ( 156 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 157 | ) 158 | 159 | for path_chunk in Parallel()(paths_chunk_generator): 160 | for path in path_chunk: 161 | yield path 162 | 163 | 164 | @nxp._configure_if_nx_active() 165 | def all_pairs_bellman_ford_path_length(G, weight="weight", get_chunks="chunks"): 166 | """The parallel implementation first divides the nodes into chunks and then 167 | creates a generator to lazily compute shortest paths lengths for each node in 168 | `node_chunk`, and then employs joblib's `Parallel` function to execute these 169 | computations in parallel across `n_jobs` number of CPU cores. 170 | 171 | networkx.all_pairs_bellman_ford_path_length : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.all_pairs_bellman_ford_path_length.html 172 | 173 | Parameters 174 | ---------- 175 | get_chunks : str, function (default = "chunks") 176 | A function that takes in an iterable of all the nodes as input and returns 177 | an iterable `node_chunks`. The default chunking is done by slicing the 178 | `G.nodes` into `n_jobs` number of chunks. 179 | """ 180 | 181 | def _process_node_chunk(node_chunk): 182 | return [ 183 | (node, single_source_bellman_ford_path_length(G, node, weight=weight)) 184 | for node in node_chunk 185 | ] 186 | 187 | if hasattr(G, "graph_object"): 188 | G = G.graph_object 189 | 190 | nodes = G.nodes 191 | n_jobs = nxp.get_n_jobs() 192 | 193 | if get_chunks == "chunks": 194 | node_chunks = nxp.chunks(nodes, n_jobs) 195 | else: 196 | node_chunks = get_chunks(nodes) 197 | 198 | path_lengths_chunk_generator = ( 199 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 200 | ) 201 | 202 | for path_length_chunk in Parallel()(path_lengths_chunk_generator): 203 | for path_length in path_length_chunk: 204 | yield path_length 205 | 206 | 207 | @nxp._configure_if_nx_active() 208 | def all_pairs_bellman_ford_path(G, weight="weight", get_chunks="chunks"): 209 | """The parallel implementation first divides the nodes into chunks and then 210 | creates a generator to lazily compute shortest paths for each node_chunk, and 211 | then employs joblib's `Parallel` function to execute these computations in 212 | parallel across `n_jobs` number of CPU cores. 213 | 214 | networkx.all_pairs_bellman_ford_path : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.all_pairs_bellman_ford_path.html 215 | 216 | Parameters 217 | ---------- 218 | get_chunks : str, function (default = "chunks") 219 | A function that takes in an iterable of all the nodes as input and returns 220 | an iterable `node_chunks`. The default chunking is done by slicing the 221 | `G.nodes` into `n_jobs` number of chunks. 222 | """ 223 | 224 | def _process_node_chunk(node_chunk): 225 | return [ 226 | (node, single_source_bellman_ford_path(G, node, weight=weight)) 227 | for node in node_chunk 228 | ] 229 | 230 | if hasattr(G, "graph_object"): 231 | G = G.graph_object 232 | 233 | nodes = G.nodes 234 | n_jobs = nxp.get_n_jobs() 235 | 236 | if get_chunks == "chunks": 237 | node_chunks = nxp.chunks(nodes, n_jobs) 238 | else: 239 | node_chunks = get_chunks(nodes) 240 | 241 | paths_chunk_generator = ( 242 | delayed(_process_node_chunk)(node_chunk) for node_chunk in node_chunks 243 | ) 244 | 245 | for path_chunk in Parallel()(paths_chunk_generator): 246 | for path in path_chunk: 247 | yield path 248 | 249 | 250 | @nxp._configure_if_nx_active() 251 | def johnson(G, weight="weight", get_chunks="chunks"): 252 | """The parallel computation is implemented by dividing the 253 | nodes into chunks and computing the shortest paths using Johnson's Algorithm 254 | for each chunk in parallel. 255 | 256 | networkx.johnson : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.weighted.johnson.html 257 | 258 | Parameters 259 | ---------- 260 | get_chunks : str, function (default = "chunks") 261 | A function that takes in an iterable of all the nodes as input and returns 262 | an iterable `node_chunks`. The default chunking is done by slicing the 263 | `G.nodes` into `n_jobs` number of chunks. 264 | """ 265 | if hasattr(G, "graph_object"): 266 | G = G.graph_object 267 | 268 | dist = {v: 0 for v in G} 269 | pred = {v: [] for v in G} 270 | weight = _weight_function(G, weight) 271 | 272 | # Calculate distance of shortest paths 273 | dist_bellman = _bellman_ford(G, list(G), weight, pred=pred, dist=dist) 274 | 275 | # Update the weight function to take into account the Bellman--Ford 276 | # relaxation distances. 277 | def new_weight(u, v, d): 278 | return weight(u, v, d) + dist_bellman[u] - dist_bellman[v] 279 | 280 | def dist_path(v): 281 | paths = {v: [v]} 282 | _dijkstra(G, v, new_weight, paths=paths) 283 | return paths 284 | 285 | def _johnson_subset(chunk): 286 | return {node: dist_path(node) for node in chunk} 287 | 288 | n_jobs = nxp.get_n_jobs() 289 | if get_chunks == "chunks": 290 | node_chunks = nxp.chunks(G.nodes, n_jobs) 291 | else: 292 | node_chunks = get_chunks(G.nodes) 293 | 294 | results = Parallel()(delayed(_johnson_subset)(chunk) for chunk in node_chunks) 295 | return {v: d_path for result_chunk in results for v, d_path in result_chunk.items()} 296 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/tournament.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | import nx_parallel as nxp 3 | from networkx.algorithms.simple_paths import is_simple_path as is_path 4 | import networkx as nx 5 | 6 | __all__ = [ 7 | "is_reachable", 8 | "tournament_is_strongly_connected", 9 | ] 10 | 11 | 12 | @nxp._configure_if_nx_active() 13 | def is_reachable(G, s, t, get_chunks="chunks"): 14 | """The function parallelizes the calculation of two 15 | neighborhoods of vertices in `G` and checks closure conditions for each 16 | neighborhood subset in parallel. 17 | 18 | networkx.tournament.is_reachable : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.tournament.is_reachable.html 19 | 20 | Parameters 21 | ---------- 22 | get_chunks : str, function (default = "chunks") 23 | A function that takes in a list of all the nodes as input and returns an 24 | iterable `node_chunks`. The default chunking is done by slicing the `nodes` 25 | into `n_jobs` number of chunks. 26 | """ 27 | 28 | def two_neighborhood_close(G, chunk): 29 | tnc = [] 30 | for v in chunk: 31 | S = { 32 | x 33 | for x in G 34 | if x == v or x in G[v] or any(is_path(G, [v, z, x]) for z in G) 35 | } 36 | tnc.append(not (is_closed(G, S) and s in S and t not in S)) 37 | return all(tnc) 38 | 39 | def is_closed(G, nodes): 40 | return all(v in G[u] for u in set(G) - nodes for v in nodes) 41 | 42 | if hasattr(G, "graph_object"): 43 | G = G.graph_object 44 | 45 | n_jobs = nxp.get_n_jobs() 46 | 47 | if get_chunks == "chunks": 48 | node_chunks = nxp.chunks(G, n_jobs) 49 | else: 50 | node_chunks = get_chunks(G) 51 | 52 | return all( 53 | Parallel()(delayed(two_neighborhood_close)(G, chunk) for chunk in node_chunks) 54 | ) 55 | 56 | 57 | @nxp._configure_if_nx_active() 58 | def tournament_is_strongly_connected(G, get_chunks="chunks"): 59 | """The parallel computation is implemented by dividing the 60 | nodes into chunks and then checking whether each node is reachable from each 61 | other node in parallel. 62 | 63 | Note, this function uses the name `tournament_is_strongly_connected` while 64 | dispatching to the backend implementation. So, `nxp.tournament.is_strongly_connected` 65 | will result in an error. Use `nxp.tournament_is_strongly_connected` instead. 66 | 67 | networkx.tournament.is_strongly_connected : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.tournament.is_strongly_connected.html 68 | 69 | Parameters 70 | ---------- 71 | get_chunks : str, function (default = "chunks") 72 | A function that takes in a list of all the nodes as input and returns an 73 | iterable `node_chunks`. The default chunking is done by slicing the `nodes` 74 | into `n_jobs` number of chunks. 75 | """ 76 | if hasattr(G, "graph_object"): 77 | G = G.graph_object 78 | 79 | def is_reachable_subset(G, chunk): 80 | return all(nx.tournament.is_reachable(G, u, v) for v in chunk for u in G) 81 | 82 | n_jobs = nxp.get_n_jobs() 83 | 84 | if get_chunks == "chunks": 85 | node_chunks = nxp.chunks(G, n_jobs) 86 | else: 87 | node_chunks = get_chunks(G) 88 | 89 | results = Parallel()( 90 | delayed(is_reachable_subset)(G, chunk) for chunk in node_chunks 91 | ) 92 | return all(results) 93 | -------------------------------------------------------------------------------- /nx_parallel/algorithms/vitality.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import nx_parallel as nxp 3 | from joblib import Parallel, delayed 4 | import networkx as nx 5 | 6 | __all__ = ["closeness_vitality"] 7 | 8 | 9 | @nxp._configure_if_nx_active() 10 | def closeness_vitality( 11 | G, node=None, weight=None, wiener_index=None, get_chunks="chunks" 12 | ): 13 | """The parallel computation is implemented only when the node 14 | is not specified. The closeness vitality for each node is computed concurrently. 15 | 16 | networkx.closeness_vitality : https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.vitality.closeness_vitality.html 17 | 18 | Parameters 19 | ---------- 20 | get_chunks : str, function (default = "chunks") 21 | A function that takes in a list of all the nodes as input and 22 | returns an iterable `node_chunks`. The default chunking is done by slicing the 23 | `nodes` into `n_jobs` number of chunks. 24 | """ 25 | 26 | def closeness_vitality_chunk_subset(chunk): 27 | return {v: vitality(v) for v in chunk} 28 | 29 | if hasattr(G, "graph_object"): 30 | G = G.graph_object 31 | 32 | if wiener_index is None: 33 | wiener_index = nx.wiener_index(G, weight=weight) 34 | 35 | if node is not None: 36 | after = nx.wiener_index(G.subgraph(set(G) - {node}), weight=weight) 37 | return wiener_index - after 38 | 39 | n_jobs = nxp.get_n_jobs() 40 | 41 | if get_chunks == "chunks": 42 | node_chunks = nxp.chunks(G.nodes, n_jobs) 43 | else: 44 | node_chunks = get_chunks(G.nodes) 45 | 46 | vitality = partial( 47 | nx.closeness_vitality, G, weight=weight, wiener_index=wiener_index 48 | ) 49 | 50 | result = Parallel()( 51 | delayed(closeness_vitality_chunk_subset)(chunk) for chunk in node_chunks 52 | ) 53 | return {k: v for d in result for k, v in d.items()} 54 | -------------------------------------------------------------------------------- /nx_parallel/interface.py: -------------------------------------------------------------------------------- 1 | from operator import attrgetter 2 | import networkx as nx 3 | from nx_parallel import algorithms 4 | 5 | __all__ = ["BackendInterface", "ParallelGraph"] 6 | 7 | 8 | ALGORITHMS = [ 9 | # Bipartite 10 | "node_redundancy", 11 | # Isolates 12 | "number_of_isolates", 13 | # Vitality 14 | "closeness_vitality", 15 | # Tournament 16 | "is_reachable", 17 | "tournament_is_strongly_connected", 18 | # Centrality 19 | "betweenness_centrality", 20 | "edge_betweenness_centrality", 21 | # Efficiency 22 | "local_efficiency", 23 | # Shortest Paths : generic 24 | "all_pairs_all_shortest_paths", 25 | # Shortest Paths : weighted graphs 26 | "all_pairs_dijkstra", 27 | "all_pairs_dijkstra_path_length", 28 | "all_pairs_dijkstra_path", 29 | "all_pairs_bellman_ford_path_length", 30 | "all_pairs_bellman_ford_path", 31 | "johnson", 32 | # Clustering 33 | "square_clustering", 34 | # Shortest Paths : unweighted graphs 35 | "all_pairs_shortest_path", 36 | "all_pairs_shortest_path_length", 37 | # Approximation 38 | "approximate_all_pairs_node_connectivity", 39 | # Connectivity 40 | "connectivity.all_pairs_node_connectivity", 41 | ] 42 | 43 | 44 | class ParallelGraph: 45 | """A wrapper class for networkx.Graph, networkx.DiGraph, networkx.MultiGraph, 46 | and networkx.MultiDiGraph. 47 | """ 48 | 49 | __networkx_backend__ = "parallel" 50 | 51 | def __init__(self, graph_object=None): 52 | if graph_object is None: 53 | self.graph_object = nx.Graph() 54 | elif isinstance(graph_object, nx.Graph): 55 | self.graph_object = graph_object 56 | else: 57 | self.graph_object = nx.Graph(graph_object) 58 | 59 | def is_multigraph(self): 60 | return self.graph_object.is_multigraph() 61 | 62 | def is_directed(self): 63 | return self.graph_object.is_directed() 64 | 65 | def __str__(self): 66 | return f"Parallel{self.graph_object}" 67 | 68 | 69 | def assign_algorithms(cls): 70 | """Class decorator to assign algorithms to the class attributes.""" 71 | for attr in ALGORITHMS: 72 | # get the function name by parsing the module hierarchy 73 | func_name = attr.rsplit(".", 1)[-1] 74 | setattr(cls, func_name, attrgetter(attr)(algorithms)) 75 | return cls 76 | 77 | 78 | @assign_algorithms 79 | class BackendInterface: 80 | """BackendInterface class for parallel algorithms.""" 81 | 82 | @staticmethod 83 | def convert_from_nx(graph, *args, **kwargs): 84 | """Convert a networkx.Graph, networkx.DiGraph, networkx.MultiGraph, 85 | or networkx.MultiDiGraph to a ParallelGraph. 86 | """ 87 | if isinstance(graph, ParallelGraph): 88 | return graph 89 | return ParallelGraph(graph) 90 | 91 | @staticmethod 92 | def convert_to_nx(result, *, name=None): 93 | """Convert a ParallelGraph to a networkx.Graph, networkx.DiGraph, 94 | networkx.MultiGraph, or networkx.MultiDiGraph. 95 | """ 96 | if isinstance(result, ParallelGraph): 97 | return result.graph_object 98 | return result 99 | -------------------------------------------------------------------------------- /nx_parallel/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/nx_parallel/tests/__init__.py -------------------------------------------------------------------------------- /nx_parallel/tests/test_entry_points.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import entry_points, EntryPoint 2 | import pytest 3 | 4 | 5 | def test_backends_ep(): 6 | assert entry_points(group="networkx.backends")["parallel"] == EntryPoint( 7 | name="parallel", 8 | value="nx_parallel.interface:BackendInterface", 9 | group="networkx.backends", 10 | ) 11 | 12 | 13 | def test_backend_info_ep(): 14 | assert entry_points(group="networkx.backend_info")["parallel"] == EntryPoint( 15 | name="parallel", value="_nx_parallel:get_info", group="networkx.backend_info" 16 | ) 17 | 18 | 19 | @pytest.mark.order(1) 20 | def test_config_init(): 21 | import networkx as nx 22 | 23 | assert dict(nx.config.backends.parallel) == { 24 | "active": False, 25 | "backend": "loky", 26 | "n_jobs": None, 27 | "verbose": 0, 28 | "temp_folder": None, 29 | "max_nbytes": "1M", 30 | "mmap_mode": "r", 31 | "prefer": None, 32 | "require": None, 33 | "inner_max_num_threads": None, 34 | "backend_params": {}, 35 | } 36 | from _nx_parallel.config import _config 37 | 38 | assert nx.config.backends.parallel == _config 39 | -------------------------------------------------------------------------------- /nx_parallel/tests/test_get_chunks.py: -------------------------------------------------------------------------------- 1 | # smoke tests for all functions supporting `get_chunks` kwarg 2 | 3 | import inspect 4 | import importlib 5 | import random 6 | import types 7 | import math 8 | 9 | import networkx as nx 10 | import pytest 11 | 12 | import nx_parallel as nxp 13 | 14 | 15 | def get_functions_with_get_chunks(ignore_funcs=[], package_name="nx_parallel"): 16 | """Yields function names for functions with a `get_chunks` kwarg.""" 17 | 18 | package = importlib.import_module(package_name) 19 | 20 | for name, obj in inspect.getmembers(package, inspect.isfunction): 21 | if not name.startswith("_"): 22 | signature = inspect.signature(obj) 23 | arguments = [ 24 | param.name 25 | for param in signature.parameters.values() 26 | if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD 27 | ] 28 | if name not in ignore_funcs and "get_chunks" in arguments: 29 | yield name 30 | 31 | 32 | def test_get_functions_with_get_chunks(): 33 | # TODO: Instead of `expected` use ALGORTHMS from interface.py 34 | # take care of functions like `connectivity.all_pairs_node_connectivity` 35 | expected = { 36 | "all_pairs_all_shortest_paths", 37 | "all_pairs_bellman_ford_path", 38 | "all_pairs_bellman_ford_path_length", 39 | "all_pairs_dijkstra", 40 | "all_pairs_dijkstra_path", 41 | "all_pairs_dijkstra_path_length", 42 | "all_pairs_node_connectivity", 43 | "all_pairs_shortest_path", 44 | "all_pairs_shortest_path_length", 45 | "approximate_all_pairs_node_connectivity", 46 | "betweenness_centrality", 47 | "closeness_vitality", 48 | "edge_betweenness_centrality", 49 | "is_reachable", 50 | "johnson", 51 | "local_efficiency", 52 | "node_redundancy", 53 | "number_of_isolates", 54 | "square_clustering", 55 | "tournament_is_strongly_connected", 56 | } 57 | assert set(get_functions_with_get_chunks()) == expected 58 | 59 | 60 | ignore_funcs = [ 61 | "number_of_isolates", 62 | "is_reachable", 63 | ] 64 | 65 | 66 | @pytest.mark.parametrize("func", get_functions_with_get_chunks(ignore_funcs)) 67 | def test_get_chunks(func): 68 | def random_chunking(nodes): 69 | _nodes = list(nodes).copy() 70 | random.seed(42) 71 | random.shuffle(_nodes) 72 | n_jobs = nxp.get_n_jobs() 73 | return nxp.chunks(_nodes, n_jobs) 74 | 75 | tournament_funcs = [ 76 | "tournament_is_strongly_connected", 77 | ] 78 | check_dict_values_close = [ 79 | "betweenness_centrality", 80 | "edge_betweenness_centrality", 81 | ] 82 | 83 | if func in tournament_funcs: 84 | G = nx.tournament.random_tournament(15, seed=42) 85 | H = nxp.ParallelGraph(G) 86 | c1 = getattr(nxp, func)(H) 87 | c2 = getattr(nxp, func)(H, get_chunks=random_chunking) 88 | assert c1 == c2 89 | else: 90 | G = nx.fast_gnp_random_graph(40, 0.6, seed=42) 91 | H = nxp.ParallelGraph(G) 92 | c1 = getattr(nxp, func)(H) 93 | c2 = getattr(nxp, func)(H, get_chunks=random_chunking) 94 | if isinstance(c1, types.GeneratorType): 95 | c1, c2 = dict(c1), dict(c2) 96 | if func in check_dict_values_close: 97 | for key in c1: 98 | assert math.isclose(c1[key], c2[key], abs_tol=1e-16) 99 | else: 100 | assert c1 == c2 101 | else: 102 | if func in check_dict_values_close: 103 | for key in c1: 104 | assert math.isclose(c1[key], c2[key], abs_tol=1e-16) 105 | else: 106 | if isinstance(c1, float): 107 | assert math.isclose(c1, c2, abs_tol=1e-16) 108 | else: 109 | assert c1 == c2 110 | -------------------------------------------------------------------------------- /nx_parallel/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .chunk import * 2 | from .decorators import * 3 | -------------------------------------------------------------------------------- /nx_parallel/utils/chunk.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | import networkx as nx 4 | 5 | 6 | __all__ = ["chunks", "get_n_jobs", "create_iterables"] 7 | 8 | 9 | def chunks(iterable, n_chunks, *, max_chunk_size=None): 10 | """Yield chunks from input iterable. 11 | 12 | - If `max_chunk_size` is None (default), the iterable is split into 13 | exactly `n_chunks` equally sized chunks. 14 | - If `max_chunk_size` is specified and the default split would create 15 | chunks larger than this size, the iterable is instead divided into 16 | smaller chunks, each containing at most `max_chunk_size` items. 17 | 18 | Parameters 19 | ---------- 20 | iterable : Iterable 21 | An iterable of inputs to be divided. 22 | n_chunks : int 23 | The number of chunks the iterable is divided into. Ignored 24 | if chunks' size exceed the `max_chunk_size` value. 25 | max_chunk_size : int, optional (default = None) 26 | Maximum number of items allowed in each chunk. If None, it 27 | divides the iterable into `n_chunks` chunks. 28 | 29 | Examples 30 | -------- 31 | >>> import nx_parallel as nxp 32 | >>> data = list(range(10)) 33 | >>> list(nxp.chunks(data, 3)) 34 | [(0, 1, 2, 3), (4, 5, 6), (7, 8, 9)] 35 | >>> list(nxp.chunks(data, 3, max_chunk_size=2)) 36 | [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)] 37 | >>> list(nxp.chunks(data, 5, max_chunk_size=3)) 38 | [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)] 39 | """ 40 | iterable = list(iterable) 41 | base_chunk_size, extra_items = divmod(len(iterable), n_chunks) 42 | if max_chunk_size and base_chunk_size >= max_chunk_size: 43 | yield from itertools.batched(iterable, max_chunk_size) 44 | return 45 | 46 | it = iter(iterable) 47 | for _ in range(n_chunks): 48 | chunk_size = base_chunk_size + (1 if extra_items > 0 else 0) 49 | extra_items -= 1 50 | yield tuple(itertools.islice(it, chunk_size)) 51 | 52 | 53 | def get_n_jobs(n_jobs=None): 54 | """Get the positive value of `n_jobs` 55 | 56 | Returns the positive value of `n_jobs` by either extracting it from the 57 | active configuration system or modifying the passed-in value, similar to 58 | joblib's behavior. 59 | 60 | - If running under pytest, it returns 2 jobs. 61 | - If the `active` configuration in NetworkX's config is `True`, `n_jobs` 62 | is extracted from the NetworkX config. 63 | - Otherwise, `n_jobs` is obtained from joblib's active backend. 64 | - `ValueError` is raised if `n_jobs` is 0. 65 | """ 66 | if "PYTEST_CURRENT_TEST" in os.environ: 67 | return 2 68 | 69 | if n_jobs is None: 70 | if nx.config.backends.parallel.active: 71 | n_jobs = nx.config.backends.parallel.n_jobs 72 | else: 73 | from joblib.parallel import get_active_backend 74 | 75 | n_jobs = get_active_backend()[1] 76 | 77 | if n_jobs is None: 78 | return 1 79 | if n_jobs < 0: 80 | return os.cpu_count() + n_jobs + 1 81 | 82 | if n_jobs == 0: 83 | raise ValueError("n_jobs == 0 in Parallel has no meaning") 84 | 85 | return int(n_jobs) 86 | 87 | 88 | def create_iterables(G, iterator, n_jobs, list_of_iterator=None): 89 | """Create an iterable of function inputs for parallel computation 90 | based on the provided iterator type. 91 | 92 | Parameters 93 | ---------- 94 | G : NetworkX graph 95 | The NetworkX graph. 96 | iterator : str 97 | Type of iterator. Valid values are 'node', 'edge', 'isolate' 98 | n_jobs : int 99 | The number of parallel jobs to run. 100 | list_of_iterator : list, optional 101 | A precomputed list of items to iterate over. If None, it will 102 | be generated based on the iterator type. 103 | 104 | Returns 105 | ------- 106 | iterable : Iterable 107 | An iterable of function inputs. 108 | 109 | Raises 110 | ------ 111 | ValueError 112 | If the iterator type is not one of "node", "edge" or "isolate". 113 | """ 114 | 115 | if not list_of_iterator: 116 | if iterator == "node": 117 | list_of_iterator = list(G.nodes) 118 | elif iterator == "edge": 119 | list_of_iterator = list(G.edges) 120 | elif iterator == "isolate": 121 | list_of_iterator = list(nx.isolates(G)) 122 | else: 123 | raise ValueError(f"Invalid iterator type: {iterator}") 124 | 125 | if not list_of_iterator: 126 | return iter([]) 127 | 128 | return chunks(list_of_iterator, n_jobs) 129 | -------------------------------------------------------------------------------- /nx_parallel/utils/decorators.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import asdict 3 | from functools import wraps 4 | import networkx as nx 5 | from joblib import parallel_config 6 | 7 | 8 | __all__ = ["_configure_if_nx_active"] 9 | 10 | 11 | def _configure_if_nx_active(): 12 | """Decorator to set the configuration for the parallel computation 13 | of the nx-parallel algorithms. 14 | """ 15 | 16 | def decorator(func): 17 | @wraps(func) 18 | def wrapper(*args, **kwargs): 19 | if ( 20 | nx.config.backends.parallel.active 21 | or "PYTEST_CURRENT_TEST" in os.environ 22 | ): 23 | # Activate nx config system in nx_parallel with: 24 | # `nx.config.backends.parallel.active = True` 25 | config_dict = asdict(nx.config.backends.parallel) 26 | config_dict.update(config_dict.pop("backend_params")) 27 | config_dict.pop("active", None) 28 | with parallel_config(**config_dict): 29 | return func(*args, **kwargs) 30 | return func(*args, **kwargs) 31 | 32 | return wrapper 33 | 34 | return decorator 35 | -------------------------------------------------------------------------------- /nx_parallel/utils/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/nx_parallel/utils/tests/__init__.py -------------------------------------------------------------------------------- /nx_parallel/utils/tests/test_chunk.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | import networkx as nx 4 | import nx_parallel as nxp 5 | 6 | 7 | @pytest.mark.order(2) 8 | def test_get_n_jobs(): 9 | """Test for various scenarios in `get_n_jobs`.""" 10 | # Test with no n_jobs (default) 11 | with pytest.MonkeyPatch().context() as mp: 12 | mp.delitem(os.environ, "PYTEST_CURRENT_TEST", raising=False) 13 | assert nxp.get_n_jobs() == 1 14 | 15 | # Test with n_jobs set to positive value 16 | assert nxp.get_n_jobs(4) == 4 17 | 18 | # Test with n_jobs set to negative value 19 | assert nxp.get_n_jobs(-1) == os.cpu_count() 20 | nx.config.backends.parallel.active = False 21 | from joblib import parallel_config 22 | 23 | parallel_config(n_jobs=3) 24 | assert nxp.get_n_jobs() == 3 25 | nx.config.backends.parallel.active = True 26 | nx.config.backends.parallel.n_jobs = 5 27 | assert nxp.get_n_jobs() == 5 28 | # restore nx.config 29 | nx.config.backends.parallel.active = False 30 | nx.config.backends.parallel.n_jobs = None 31 | # Test with n_jobs = 0 to raise a ValueError 32 | try: 33 | nxp.get_n_jobs(0) 34 | except ValueError as e: 35 | assert str(e) == "n_jobs == 0 in Parallel has no meaning" 36 | 37 | 38 | def test_chunks(): 39 | """Test `chunks` for various input scenarios.""" 40 | data = list(range(10)) 41 | 42 | # Test chunking with exactly 2 larger chunks (balanced) 43 | chunks_list = list(nxp.chunks(data, 2)) 44 | assert chunks_list == [(0, 1, 2, 3, 4), (5, 6, 7, 8, 9)] 45 | 46 | # Test chunking into 5 smaller chunks 47 | chunks_list = list(nxp.chunks(data, 5)) 48 | assert chunks_list == [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)] 49 | 50 | # Test chunking with a maximum chunk size 51 | chunks_list = list(nxp.chunks(data, 2, max_chunk_size=3)) 52 | assert chunks_list == [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)] 53 | 54 | chunks_list = list(nxp.chunks(data, 5, max_chunk_size=3)) 55 | assert chunks_list == [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)] 56 | 57 | 58 | def test_create_iterables(): 59 | """Test `create_iterables` for different iterator types.""" 60 | G = nx.fast_gnp_random_graph(50, 0.6, seed=42) 61 | 62 | # Test node iterator 63 | iterable = nxp.create_iterables(G, "node", 4) 64 | assert len(list(iterable)) == 4 65 | 66 | # Test edge iterator 67 | iterable = nxp.create_iterables(G, "edge", 4) 68 | assert len(list(iterable)) == 4 69 | 70 | # Test isolate iterator (G has no isolates, so this should be empty) 71 | iterable = nxp.create_iterables(G, "isolate", 4) 72 | assert len(list(iterable)) == 0 73 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'setuptools.build_meta' 3 | requires = ['setuptools>=61.2'] 4 | 5 | [project] 6 | name = "nx-parallel" 7 | description = "A parallel backend for NetworkX. It uses joblib to run NetworkX algorithms on multiple CPU cores." 8 | readme = "README.md" 9 | requires-python = ">=3.12" 10 | dynamic = ['version'] 11 | keywords = ["networkx", "graphs", "algorithms", "parallel"] 12 | license = {text = "BSD-3-Clause"} 13 | classifiers = [ 14 | 'Intended Audience :: Developers', 15 | 'Intended Audience :: Science/Research', 16 | 'License :: OSI Approved :: BSD License', 17 | 'Operating System :: OS Independent', 18 | 'Programming Language :: Python :: 3', 19 | 'Programming Language :: Python :: 3.12', 20 | 'Programming Language :: Python :: 3.13', 21 | 'Programming Language :: Python :: 3 :: Only', 22 | ] 23 | 24 | dependencies = [ 25 | "networkx>=3.4.2", 26 | "joblib>=1.5.0" 27 | ] 28 | 29 | [[project.authors]] 30 | name = "NetworkX Devs" 31 | email = "networkx-core@discuss.scientific-python.org" 32 | 33 | [[project.maintainers]] 34 | name = 'NetworkX Developers' 35 | email = 'networkx-discuss@googlegroups.com' 36 | 37 | [project.urls] 38 | "Bug Tracker" = 'https://github.com/networkx/nx-parallel/issues' 39 | "Source Code" = 'https://github.com/networkx/nx-parallel' 40 | 41 | [project.entry-points."networkx.backends"] 42 | parallel = "nx_parallel.interface:BackendInterface" 43 | 44 | [project.entry-points."networkx.backend_info"] 45 | parallel = "_nx_parallel:get_info" 46 | 47 | [project.optional-dependencies] 48 | developer = [ 49 | 'pre-commit==3.8.0', 50 | 'ruff==0.6.7', 51 | ] 52 | test = [ 53 | 'pytest>=7.2', 54 | 'pytest-order>=1.3.0', 55 | 'numpy>=1.23', 56 | 'scipy>=1.9,!=1.11.0,!=1.11.1', 57 | ] 58 | 59 | [tool.pytest.ini_options] 60 | filterwarnings = [ 61 | # note the use of single quote below to denote "raw" strings in TOML 62 | 'ignore:the hashes produced', 63 | ] 64 | 65 | [tool.setuptools] 66 | zip-safe = false 67 | include-package-data = false 68 | packages = [ 69 | 'nx_parallel', 70 | '_nx_parallel', 71 | 'nx_parallel.algorithms', 72 | 'nx_parallel.algorithms.approximation', 73 | 'nx_parallel.algorithms.bipartite', 74 | 'nx_parallel.algorithms.centrality', 75 | 'nx_parallel.algorithms.connectivity', 76 | 'nx_parallel.algorithms.shortest_paths', 77 | 'nx_parallel.utils', 78 | ] 79 | 80 | platforms = [ 81 | 'Linux', 82 | 'Mac OSX', 83 | 'Windows', 84 | 'Unix', 85 | ] 86 | 87 | [tool.setuptools.dynamic.version] 88 | attr = 'nx_parallel.__version__' 89 | 90 | [tool.ruff] 91 | line-length = 88 92 | target-version = 'py312' 93 | 94 | [tool.ruff.lint.per-file-ignores] 95 | "__init__.py" = ['I', 'F403', 'F401'] 96 | 97 | [tool.ruff.format] 98 | docstring-code-format = true 99 | -------------------------------------------------------------------------------- /requirements/release.txt: -------------------------------------------------------------------------------- 1 | changelist==0.4 2 | -------------------------------------------------------------------------------- /timing/heatmap_all_functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_functions.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_all_shortest_paths_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_all_shortest_paths_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_bellman_ford_path_length_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_bellman_ford_path_length_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_bellman_ford_path_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_bellman_ford_path_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_dijkstra_path_length_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_dijkstra_path_length_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_dijkstra_path_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_dijkstra_path_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_dijkstra_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_dijkstra_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_node_connectivity_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_node_connectivity_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_shortest_path_length_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_shortest_path_length_timing.png -------------------------------------------------------------------------------- /timing/heatmap_all_pairs_shortest_path_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_all_pairs_shortest_path_timing.png -------------------------------------------------------------------------------- /timing/heatmap_betweenness_centrality_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_betweenness_centrality_timing.png -------------------------------------------------------------------------------- /timing/heatmap_closeness_vitality_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_closeness_vitality_timing.png -------------------------------------------------------------------------------- /timing/heatmap_is_reachable_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_is_reachable_timing.png -------------------------------------------------------------------------------- /timing/heatmap_is_strongly_connected_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_is_strongly_connected_timing.png -------------------------------------------------------------------------------- /timing/heatmap_johnson_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_johnson_timing.png -------------------------------------------------------------------------------- /timing/heatmap_local_efficiency_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_local_efficiency_timing.png -------------------------------------------------------------------------------- /timing/heatmap_node_redundancy_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_node_redundancy_timing.png -------------------------------------------------------------------------------- /timing/heatmap_square_clustering_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkx/nx-parallel/daef0c8ca3f8b52d5f53cb50ff07236cd87e5ee2/timing/heatmap_square_clustering_timing.png -------------------------------------------------------------------------------- /timing/timing_all_functions.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import networkx as nx 4 | import pandas as pd 5 | import seaborn as sns 6 | from matplotlib import pyplot as plt 7 | 8 | import nx_parallel 9 | 10 | # Code to create README heatmap for all functions in function_list 11 | heatmapDF = pd.DataFrame() 12 | function_list = [nx.betweenness_centrality, nx.closeness_vitality, nx.local_efficiency] 13 | number_of_nodes_list = [10, 20, 50, 300, 600] 14 | 15 | for i in range(0, len(function_list)): 16 | currFun = function_list[i] 17 | for j in range(0, len(number_of_nodes_list)): 18 | num = number_of_nodes_list[j] 19 | 20 | # create original and parallel graphs 21 | G = nx.fast_gnp_random_graph(num, 0.5, directed=False) 22 | H = nx_parallel.ParallelGraph(G) 23 | 24 | # time both versions and update heatmapDF 25 | t1 = time.time() 26 | c = currFun(H) 27 | t2 = time.time() 28 | parallelTime = t2 - t1 29 | t1 = time.time() 30 | c = currFun(G) 31 | t2 = time.time() 32 | stdTime = t2 - t1 33 | timesFaster = stdTime / parallelTime 34 | heatmapDF.at[j, i] = timesFaster 35 | print("Finished " + str(currFun)) 36 | 37 | # Code to create for row of heatmap specifically for tournaments 38 | for j in range(0, len(number_of_nodes_list)): 39 | num = number_of_nodes_list[j] 40 | G = nx.tournament.random_tournament(num) 41 | H = nx_parallel.ParallelDiGraph(G) 42 | t1 = time.time() 43 | c = nx.tournament.is_reachable(H, 1, num) 44 | t2 = time.time() 45 | parallelTime = t2 - t1 46 | t1 = time.time() 47 | c = nx.tournament.is_reachable(G, 1, num) 48 | t2 = time.time() 49 | stdTime = t2 - t1 50 | timesFaster = stdTime / parallelTime 51 | heatmapDF.at[j, 3] = timesFaster 52 | 53 | # plotting the heatmap with numbers and a green color scheme 54 | plt.figure(figsize=(20, 4)) 55 | hm = sns.heatmap(data=heatmapDF.T, annot=True, cmap="Greens", cbar=True) 56 | 57 | # Remove the tick labels on both axes 58 | hm.set_yticklabels( 59 | [ 60 | "betweenness_centrality", 61 | "closeness_vitality", 62 | "local_efficiency", 63 | "tournament is_reachable", 64 | ] 65 | ) 66 | 67 | # Adding x-axis labels 68 | hm.set_xticklabels(number_of_nodes_list) 69 | 70 | # Rotating the x-axis labels for better readability (optional) 71 | plt.xticks(rotation=45) 72 | plt.yticks(rotation=20) 73 | plt.title("Small Scale Demo: Times Speedups of nx_parallel compared to networkx") 74 | plt.xlabel("Number of Vertices (edge probability of 0.5 except for tournaments)") 75 | plt.ylabel("Algorithm") 76 | 77 | # displaying the plotted heatmap 78 | plt.tight_layout() 79 | -------------------------------------------------------------------------------- /timing/timing_comparison.md: -------------------------------------------------------------------------------- 1 | # Timing Comparisons 2 | 3 | --- 4 | 5 | Model: 13-inch MacBook Pro (2020) 6 | 7 | CPU: 2 GHz Quad-Core Intel Core i5 8 | 9 | RAM: 16 GB LPDDR4X at 3733 MHz 10 | 11 | Code to generate heatmaps in timing_individual_function.py and timing_all_functions.py. 12 | 13 | ## All parallelized functions at this time: 14 | 15 | ![alt text](heatmap_all_functions.png) 16 | 17 | ## Individual functions: 18 | 19 | betweenness_centrality 20 | ![alt text](heatmap_betweenness_centrality_timing.png) 21 | 22 | closeness_vitality 23 | ![alt text](heatmap_closeness_vitality_timing.png) 24 | 25 | local_efficiency 26 | ![alt text](heatmap_local_efficiency_timing.png) 27 | 28 | tournament is_reachable 29 | ![alt text](heatmap_is_reachable_timing.png) 30 | 31 | all_pairs_bellman_ford_path 32 | ![alt text](heatmap_all_pairs_bellman_ford_path_timing.png) 33 | -------------------------------------------------------------------------------- /timing/timing_individual_function.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import networkx as nx 4 | import pandas as pd 5 | import seaborn as sns 6 | from matplotlib import pyplot as plt 7 | 8 | import nx_parallel as nxp 9 | 10 | # Code to create README heatmaps for individual function currFun 11 | heatmapDF = pd.DataFrame() 12 | # for bipartite graphs 13 | # n = [50, 100, 200, 400] 14 | # m = [25, 50, 100, 200] 15 | number_of_nodes_list = [200, 400, 800, 1600] 16 | weighted = False 17 | pList = [1, 0.8, 0.6, 0.4, 0.2] 18 | currFun = nx.tournament.is_reachable 19 | """ 20 | for p in pList: 21 | for num in range(len(number_of_nodes_list)): 22 | # create original and parallel graphs 23 | G = nx.fast_gnp_random_graph( 24 | number_of_nodes_list[num], p, seed=42, directed=True 25 | ) 26 | 27 | 28 | # for bipartite.node_redundancy 29 | G = nx.bipartite.random_graph(n[num], m[num], p, seed=42, directed=True) 30 | for i in G.nodes: 31 | l = list(G.neighbors(i)) 32 | if len(l) == 0: 33 | v = random.choice(list(G.nodes) - [i,]) 34 | G.add_edge(i, v) 35 | G.add_edge(i, random.choice([node for node in G.nodes if node != i])) 36 | elif len(l) == 1: 37 | G.add_edge(i, random.choice([node for node in G.nodes if node != i and node not in list(G.neighbors(i))])) 38 | 39 | # for weighted graphs 40 | if weighted: 41 | random.seed(42) 42 | for u, v in G.edges(): 43 | G[u][v]["weight"] = random.random() 44 | 45 | H = nxp.ParallelGraph(G) 46 | 47 | # time both versions and update heatmapDF 48 | t1 = time.time() 49 | c1 = currFun(H) 50 | if isinstance(c1, types.GeneratorType): 51 | d = dict(c1) 52 | t2 = time.time() 53 | parallelTime = t2 - t1 54 | t1 = time.time() 55 | c2 = currFun(G) 56 | if isinstance(c2, types.GeneratorType): 57 | d = dict(c2) 58 | t2 = time.time() 59 | stdTime = t2 - t1 60 | timesFaster = stdTime / parallelTime 61 | heatmapDF.at[number_of_nodes_list[num], p] = timesFaster 62 | print("Finished " + str(currFun)) 63 | """ 64 | 65 | # Code to create for row of heatmap specifically for tournaments 66 | for num in number_of_nodes_list: 67 | print(num) 68 | G = nx.tournament.random_tournament(num, seed=42) 69 | H = nxp.ParallelGraph(G) 70 | t1 = time.time() 71 | c = currFun(H, 1, num) 72 | t2 = time.time() 73 | parallelTime = t2 - t1 74 | print(parallelTime) 75 | t1 = time.time() 76 | c = currFun(G, 1, num) 77 | t2 = time.time() 78 | stdTime = t2 - t1 79 | print(stdTime) 80 | timesFaster = stdTime / parallelTime 81 | heatmapDF.at[num, 3] = timesFaster 82 | print("Finished " + str(currFun)) 83 | 84 | # plotting the heatmap with numbers and a green color scheme 85 | plt.figure(figsize=(20, 4)) 86 | hm = sns.heatmap(data=heatmapDF.T, annot=True, cmap="Greens", cbar=True) 87 | 88 | # Remove the tick labels on both axes 89 | hm.set_yticklabels( 90 | [ 91 | 3, 92 | ] 93 | ) 94 | 95 | # Adding x-axis labels 96 | hm.set_xticklabels(number_of_nodes_list) 97 | 98 | # Rotating the x-axis labels for better readability (optional) 99 | plt.xticks(rotation=45) 100 | plt.yticks(rotation=20) 101 | plt.title( 102 | "Small Scale Demo: Times Speedups of " + currFun.__name__ + " compared to NetworkX" 103 | ) 104 | plt.xlabel("Number of Vertices") 105 | plt.ylabel("Edge Probability") 106 | print(currFun.__name__) 107 | 108 | # displaying the plotted heatmap 109 | plt.tight_layout() 110 | plt.savefig("timing/" + "heatmap_" + currFun.__name__ + "_timing.png") 111 | --------------------------------------------------------------------------------