From 812bb8a470858244931059be9d9bf1310a7acd50 Mon Sep 17 00:00:00 2001 From: Mridul Seth Date: Tue, 20 Jun 2023 21:30:54 +1000 Subject: [PATCH] add skeleton --- .gitignore | 130 ++++++++++++++++++++++++++++++++++++++ README.md | 27 +++++++- nx_parallel/__init__.py | 3 + nx_parallel/centrality.py | 44 +++++++++++++ nx_parallel/graph.py | 13 ++++ nx_parallel/interface.py | 42 ++++++++++++ pyproject.toml | 25 ++++++++ 7 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 nx_parallel/__init__.py create mode 100644 nx_parallel/centrality.py create mode 100644 nx_parallel/graph.py create mode 100644 nx_parallel/interface.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa44ee2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + diff --git a/README.md b/README.md index d2c04fd..cd5079b 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# nx_parallel \ No newline at end of file +NX-Parallel +----------- + +A NetworkX backend plugin which uses dask for parallelization. + +``` python +In [1]: import networkx as nx; import nx_parallel + +In [2]: G = nx.erdos_renyi_graph(10, 0.5) + +In [3]: H = nx_parallel.ParallelGraph(G) + +In [4]: nx.betweenness_centrality(H) +Out[4]: +{0: 0.0, + 1: 0.0, + 2: 0.0, + 3: 0.0, + 4: 0.0, + 5: 0.0, + 6: 0.0, + 7: 0.0, + 8: 0.0, + 9: 0.0} + +``` \ No newline at end of file diff --git a/nx_parallel/__init__.py b/nx_parallel/__init__.py new file mode 100644 index 0000000..f16b848 --- /dev/null +++ b/nx_parallel/__init__.py @@ -0,0 +1,3 @@ +from .centrality import * +from .graph import * +from .interface import * diff --git a/nx_parallel/centrality.py b/nx_parallel/centrality.py new file mode 100644 index 0000000..abba84c --- /dev/null +++ b/nx_parallel/centrality.py @@ -0,0 +1,44 @@ +from joblib import Parallel, delayed +from networkx.algorithms.centrality.betweenness import ( + _single_source_shortest_path_basic, + _accumulate_endpoints, + _accumulate_basic, + _rescale, + _single_source_dijkstra_path_basic, +) + +__all__ = ["betweenness_centrality"] + + +def betweenness_centrality( + G, k=None, normalized=True, weight=None, endpoints=False, seed=None +): + betweenness = dict.fromkeys(G, 0.0) # b[v]=0 for v in G + if k is None: + nodes = G + else: + nodes = seed.sample(list(G.nodes()), k) + + def __node_loop(nodes): + for s in nodes: + # single source shortest paths + if weight is None: # use BFS + S, P, sigma, _ = _single_source_shortest_path_basic(G, s) + else: # use Dijkstra's algorithm + S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight) + # accumulation + if endpoints: + betweenness, _ = _accumulate_endpoints(betweenness, S, P, sigma, s) + else: + betweenness, _ = _accumulate_basic(betweenness, S, P, sigma, s) + + # rescaling + betweenness = _rescale( + betweenness, + len(G), + normalized=normalized, + directed=G.is_directed(), + k=k, + endpoints=endpoints, + ) + return betweenness diff --git a/nx_parallel/graph.py b/nx_parallel/graph.py new file mode 100644 index 0000000..b098bbe --- /dev/null +++ b/nx_parallel/graph.py @@ -0,0 +1,13 @@ +from networkx import Graph + +__all__ = ["ParallelGraph"] + + +class ParallelGraph(Graph): + __networkx_plugin__ = "parallel" + + def __init__(self, incoming_graph_data=None, **attr): + super().__init__(incoming_graph_data, **attr) + + def to_networkx(self): + return Graph(self) diff --git a/nx_parallel/interface.py b/nx_parallel/interface.py new file mode 100644 index 0000000..e05aee0 --- /dev/null +++ b/nx_parallel/interface.py @@ -0,0 +1,42 @@ +from .centrality import betweenness_centrality + +__all__ = ["Dispatcher"] + + +class Dispatcher: + betweenness_centrality = betweenness_centrality + + @staticmethod + def convert_from_nx(incoming_graph, weight=None, *, name=None): + import networkx as nx + from .graph import ParallelGraph + + if isinstance(incoming_graph, nx.Graph): + return ParallelGraph(incoming_graph) + raise TypeError(f"Unsupported type of graph: {type(incoming_graph)}") + + @staticmethod + def convert_to_nx(obj, *, name=None): + from .graph import ParallelGraph + + if isinstance(obj, Graph): + obj = obj.to_networkx() + return obj + + # @staticmethod + # def on_start_tests(items): + # try: + # import pytest + # except ImportError: # pragma: no cover (import) + # return + # skip = [ + # ("test_attributes", {"TestBoruvka", "test_mst.py"}), + # ("test_weight_attribute", {"TestBoruvka", "test_mst.py"}), + # ] + # for item in items: + # kset = set(item.keywords) + # for test_name, keywords in skip: + # if item.name == test_name and keywords.issubset(kset): + # item.add_marker( + # pytest.mark.xfail(reason="unable to handle multi-attributed graphs") + # ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d9a844a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "nx_parallel" +authors = [ + {name = "Mridul Seth", email = "mail@mriduls.com"}, +] +description = "An experimental parallel backend for NetworkX" +readme = "README.md" +requires-python = ">=3.10" +keywords = ["networkx", "algorithms", "parallel"] +license = {text = "BSD-3-Clause"} +classifiers = [ + "Programming Language :: Python :: 3", +] +dependencies = [ + "networkx", + "joblib" +] +version = '0.0.1' + +[project.entry-points."networkx.plugins"] +parallel = "nx_parallel.interface:Dispatcher"