├── src
└── numgraph
│ ├── __init__.py
│ ├── utils
│ ├── __init__.py
│ ├── _utils.py
│ └── spikes_generator.py
│ ├── temporal
│ ├── __init__.py
│ ├── _heat_diffusion.py
│ ├── _susceptible_infected.py
│ └── _euler_diffusion.py
│ └── distributions
│ ├── __init__.py
│ ├── _clique.py
│ ├── _star.py
│ ├── _random_tree.py
│ ├── _erdos_renyi.py
│ ├── _barabasi_albert.py
│ ├── _sbm.py
│ └── _grid.py
├── pyproject.toml
├── docs
├── requirements.txt
├── source
│ ├── numgraph.temporal.rst
│ ├── numgraph.distributions.rst
│ ├── installation.rst
│ ├── numgraph.utils.rst
│ ├── index.rst
│ ├── conf.py
│ ├── usage.rst
│ └── _static
│ │ └── img
│ │ ├── NumGraph_favicon.svg
│ │ └── NumGraph_logo.svg
└── Makefile
├── .readthedocs.yaml
├── setup.cfg
├── LICENSE
├── test
├── plot_static.py
├── utils
│ ├── _community_layout.py
│ ├── _plot.py
│ ├── _dynamic_plot.py
│ └── __init__.py
├── plot_temporal.py
└── test.py
├── README.md
└── .gitignore
/src/numgraph/__init__.py:
--------------------------------------------------------------------------------
1 | from .distributions import *
2 |
3 | __version__ = '0.1.8'
4 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools>=42",
4 | "wheel"
5 | ]
6 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx==4.4.0
2 | sphinx_rtd_theme==1.0.0
3 | readthedocs-sphinx-search==0.1.1
4 | sphinxcontrib-napoleon==0.7
5 |
6 | matplotlib
7 | networkx
8 | seaborn
9 | tqdm
10 | numpy
11 | python-louvain
12 | numgraph
13 |
--------------------------------------------------------------------------------
/docs/source/numgraph.temporal.rst:
--------------------------------------------------------------------------------
1 | numgraph.temporal
2 | =================
3 |
4 | Temporal distributions
5 | ----------------------
6 |
7 | .. automodule:: numgraph.temporal
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
--------------------------------------------------------------------------------
/docs/source/numgraph.distributions.rst:
--------------------------------------------------------------------------------
1 | numgraph.distributions
2 | ======================
3 |
4 | Static distributions
5 | --------------------
6 |
7 |
8 | .. automodule:: numgraph.distributions
9 | :members:
10 | :undoc-members:
11 | :show-inheritance:
12 |
--------------------------------------------------------------------------------
/docs/source/installation.rst:
--------------------------------------------------------------------------------
1 | :github_url: https://github.com/gravins/NumGraph
2 |
3 | Installation
4 | ============
5 | For pip (and conda) users, the library can be easily installed by running the command
6 |
7 | .. code-block:: shell
8 |
9 | python3 -m pip install numgraph
10 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: "ubuntu-20.04"
5 | tools:
6 | python: "3.9"
7 |
8 | # Build from the docs/ directory with Sphinx
9 | sphinx:
10 | configuration: docs/source/conf.py
11 |
12 | # Explicitly set the version of Python and its requirements
13 | python:
14 | install:
15 | - requirements: docs/requirements.txt
16 |
--------------------------------------------------------------------------------
/src/numgraph/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from ._utils import coalesce, dense, to_dense, to_sparse, to_undirected, remove_self_loops, unsorted_coalesce
2 |
3 | _spikes_gen = [
4 | 'SpikeGenerator',
5 | 'HeatSpikeGenerator',
6 | 'ColdHeatSpikeGenerator'
7 | ]
8 |
9 | _utilities = [
10 | 'coalesce',
11 | 'dense',
12 | 'to_dense',
13 | 'to_sparse',
14 | 'to_undirected',
15 | 'remove_self_loops',
16 | 'unsorted_coalesce'
17 | ]
18 |
19 | __all__ = _utilities + _spikes_gen
--------------------------------------------------------------------------------
/docs/source/numgraph.utils.rst:
--------------------------------------------------------------------------------
1 | numgraph.utils
2 | ======================
3 |
4 | .. currentmodule:: numgraph.utils
5 |
6 |
7 | Utility functions
8 | -----------------
9 |
10 |
11 | .. automodule:: numgraph.utils
12 | :members:
13 | :undoc-members:
14 | :show-inheritance:
15 | :exclude-members: numgraph.utils._spike_gen
16 |
17 |
18 | Spikes Generator
19 | ----------------
20 |
21 |
22 | .. automodule:: numgraph.utils.spikes_generator
23 | :members:
24 | :undoc-members:
25 | :show-inheritance:
26 |
--------------------------------------------------------------------------------
/src/numgraph/temporal/__init__.py:
--------------------------------------------------------------------------------
1 | from ._susceptible_infected import susceptible_infected_coo, susceptible_infected_full
2 | from ._heat_diffusion import heat_graph_diffusion_coo, heat_graph_diffusion_full
3 | from ._euler_diffusion import euler_graph_diffusion_coo, euler_graph_diffusion_full
4 |
5 | __all__ = [
6 | 'susceptible_infected_coo',
7 | 'susceptible_infected_full',
8 | 'heat_graph_diffusion_coo',
9 | 'heat_graph_diffusion_full',
10 | 'euler_graph_diffusion_coo',
11 | 'euler_graph_diffusion_full'
12 |
13 | ]
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = numgraph
3 | version = attr: numgraph.__version__
4 | author = Alessio Gravina, Danilo Numeroso
5 | author_email = alessio.gravina@phd.unipi.it, danilo.numeroso@phd.unipi.it
6 | description = Utility package for generating graph structures
7 | long_description = file: README.md
8 | long_description_content_type = text/markdown
9 | url = https://github.com/gravins/NumGraph
10 | project_urls =
11 | Bug Tracker = https://github.com/gravins/NumGraph
12 | classifiers =
13 | Programming Language :: Python :: 3
14 | License :: OSI Approved :: MIT License
15 | Operating System :: OS Independent
16 |
17 | [options]
18 | package_dir =
19 | = src
20 | packages = find:
21 | python_requires = >=3.8
22 | install_requires =
23 | numpy >= 1.21
24 |
25 | [options.packages.find]
26 | where = src
27 |
--------------------------------------------------------------------------------
/src/numgraph/distributions/__init__.py:
--------------------------------------------------------------------------------
1 | from ._erdos_renyi import erdos_renyi_coo, erdos_renyi_full
2 | from ._barabasi_albert import barabasi_albert_coo, barabasi_albert_full
3 | from ._sbm import stochastic_block_model_coo, stochastic_block_model_full
4 | from ._clique import clique_coo, clique_full
5 | from ._grid import grid_coo, grid_full, simple_grid_coo, simple_grid_full
6 | from ._random_tree import random_tree_coo, random_tree_full
7 | from ._star import star_coo, star_full
8 |
9 | __all__ = [
10 | 'erdos_renyi_coo',
11 | 'erdos_renyi_full',
12 | 'barabasi_albert_coo',
13 | 'barabasi_albert_full',
14 | 'stochastic_block_model_coo',
15 | 'stochastic_block_model_full',
16 | 'clique_coo',
17 | 'clique_full',
18 | 'grid_coo',
19 | 'grid_full',
20 | 'simple_grid_coo',
21 | 'simple_grid_full',
22 | 'random_tree_coo',
23 | 'random_tree_full',
24 | 'star_coo',
25 | 'star_full',
26 | ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Alessio Gravina, Danilo Numeroso
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | :github_url: https://github.com/gravins/NumGraph
2 |
3 | NumGraph
4 | ========
5 |
6 | **Num(py)Graph** is a library for synthetic graph generation. The main principle of NumGraph is to be a lightweight library (i.e., ``numpy`` is the only dependency) that generates graphs from a broad range of distributions. Indeed, It implements several graph distributions in both the static and temporal domain.
7 |
8 |
9 | Implemented distributions
10 | -------------------------
11 |
12 | Static Graphs
13 | ~~~~~~~~~~~~~
14 |
15 | - Star graph
16 | - Clique
17 | - Two-dimensional rectangular grid lattice graph
18 | - Random Tree
19 | - Erdos Renyi
20 | - Barabasi Albert
21 | - Stochastic Block Model
22 |
23 | Temporal Graphs
24 | ~~~~~~~~~~~~~~~
25 |
26 | - Susceptible-Infected Dissemination Process Simulation
27 | - Heat diffusion over a graph (closed form solution)
28 | - Generic Euler's method approximation of a diffusion process over a graph
29 |
30 |
31 |
32 | .. toctree::
33 | :maxdepth: 2
34 | :caption: Contents:
35 |
36 | installation
37 | usage
38 |
39 | .. toctree::
40 | :glob:
41 | :maxdepth: 1
42 | :caption: Package Reference
43 |
44 | numgraph.distributions
45 | numgraph.temporal
46 | numgraph.utils
47 |
--------------------------------------------------------------------------------
/test/plot_static.py:
--------------------------------------------------------------------------------
1 | from numgraph.distributions import *
2 | import networkx as nx
3 | from utils import *
4 | from numpy.random import default_rng
5 |
6 | seed = 7
7 |
8 | # Erdos-Renyi
9 | print('Erdos-Renyi')
10 | num_nodes = 10
11 | prob = 0.4
12 | e, _ = erdos_renyi_coo(num_nodes, prob)
13 | G = nx.DiGraph()
14 | G.add_edges_from(e)
15 | plot_er(G, num_nodes)
16 |
17 | # SBM
18 | print('SBM')
19 | block_size = [15, 5, 3]
20 | probs = [[0.5, 0.01, 0.01], [0.01, 0.5, 0.01], [0.01, 0.01, 0.5]]
21 | generator = lambda b, p, rng: erdos_renyi_coo(b, p)
22 | e, _ = stochastic_block_model_coo(block_size, probs, generator, rng = default_rng(seed))
23 | G = nx.from_edgelist(e)
24 | plot_sbm(G, seed=seed)
25 |
26 | # Barabasi Albert
27 | print('Barabasi Albert')
28 | num_nodes = 10
29 | num_edges = 7
30 | rng = default_rng(seed)
31 | e, _ = barabasi_albert_coo(num_nodes, num_edges, rng)
32 | G = nx.DiGraph()
33 | G.add_edges_from(e)
34 | plot_ba(G, seed)
35 |
36 | # Clique
37 | print('Clique')
38 | num_nodes = 10
39 | e, _ = clique_coo(num_nodes)
40 | G = nx.DiGraph()
41 | G.add_edges_from(e)
42 | plot_clique(G)
43 |
44 | # Star
45 | print('Star')
46 | num_nodes = 10
47 | e, _ = star_coo(num_nodes)
48 | G = nx.DiGraph()
49 | G.add_edges_from(e)
50 | plot_star(G)
51 |
52 | # Simple Grid
53 | print('Simple Grid')
54 | height, width = 3, 5
55 | e, _ = simple_grid_coo(height, width)
56 | G = nx.DiGraph()
57 | G.add_edges_from(e)
58 | plot_grid(G)
59 |
60 | # Full Grid
61 | print('Full Grid')
62 | height, width = 3, 5
63 | e, _ = grid_coo(height, width)
64 | G = nx.DiGraph()
65 | G.add_edges_from(e)
66 | plot_grid(G)
67 |
68 | # Random Tree
69 | print('Random tree')
70 | num_nodes = 10
71 | e, _ = random_tree_coo(num_nodes, rng = default_rng(seed))
72 | G = nx.DiGraph()
73 | G.add_edges_from(e)
74 | plot_tree_on_terminal(G)
75 | plot_tree(G)
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | sys.path.insert(0, os.path.abspath('.'))
5 | sys.path.insert(0, os.path.abspath('..'))
6 | sys.path.insert(0, os.path.abspath('../src'))
7 |
8 |
9 | import datetime
10 | import numgraph
11 | import doctest
12 |
13 | project = 'NumGraph'
14 | author = 'Alessio Gravina and Danilo Numeroso'
15 | copyright = f'{datetime.datetime.now().year}, {author}'
16 | release = numgraph.__version__
17 | version = numgraph.__version__
18 |
19 | doctest_default_flags = doctest.NORMALIZE_WHITESPACE
20 | autodoc_member_order = 'bysource'
21 |
22 | extensions = ['sphinx.ext.autodoc',
23 | 'sphinx.ext.autodoc.typehints',
24 | 'sphinx.ext.autosummary',
25 | 'sphinx.ext.coverage',
26 | 'sphinx.ext.napoleon',
27 | 'sphinx.ext.intersphinx',
28 | 'sphinx.ext.viewcode'
29 | ]
30 |
31 | autosummary_generate = True
32 | autodoc_typehints = "none" #'description'
33 |
34 | napoleon_type_aliases = {
35 | 'NDArray': 'numpy.typing.NDArray',
36 | 'Generator': 'numpy.random.Generator',
37 | 'Optional': 'typing.Optional',
38 | #'Tuple': 'typing.Tuple',
39 | 'Callable': 'typing.Callable'
40 | }
41 |
42 | #napoleon_preprocess_types = True
43 | napoleon_numpy_docstring = True
44 | napoleon_use_ivar = True
45 | napoleon_use_rtype = False
46 |
47 | templates_path = ['_templates']
48 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
49 |
50 |
51 | html_theme = 'sphinx_rtd_theme'
52 | html_static_path = ['_static']
53 |
54 | html_logo = f'{html_static_path[0]}/img/NumGraph_logo.svg'
55 | html_favicon = f'{html_static_path[0]}/img/NumGraph_favicon.svg'
56 | html_theme_options = {
57 | 'logo_only': True,
58 | 'display_version': False,
59 | 'collapse_navigation': False,
60 | 'style_nav_header_background': '#EFEFEF',
61 | }
62 |
63 | intersphinx_mapping = {
64 | 'python': ('https://docs.python.org/3/', None),
65 | 'numpy': ('https://numpy.org/doc/stable/', None)
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [pypi-image]: https://github.com/gravins/NumGraph/blob/main/docs/source/_static/img/NumGraph_logo.svg
2 | [pypi-url]: https://pypi.org/project/numgraph/
3 |
4 |
5 |
6 |
7 |
8 |
9 | # NumGraph
10 | #### Read the [Documentation](https://numgraph.readthedocs.io/en/latest/index.html)
11 |
12 | **Num(py)Graph** is a library for synthetic graph generation. The main principle of NumGraph is to be a lightweight library (i.e., ``numpy`` is the only dependency) that generates graphs from a broad range of distributions. Indeed, It implements several graph distributions in both the static and temporal domain.
13 |
14 |
15 | ## Implemented distributions
16 | ### Static Graphs
17 | - Star graph
18 | - Clique
19 | - Two-dimensional rectangular grid lattice graph
20 | - Random Tree
21 | - Erdos Renyi
22 | - Barabasi Albert
23 | - Stochastic Block Model
24 |
25 | ### Temporal Graphs
26 | - Susceptible-Infected Dissemination Process Simulation
27 | - Heat diffusion over a graph (closed form solution)
28 | - Generic Euler's method approximation of a diffusion process over a graph
29 |
30 | ## Installation
31 |
32 | ``` python3 -m pip install numgraph ```
33 |
34 | ## Usage
35 | ```python
36 |
37 | >>> from numgraph import star_coo, star_full
38 | >>> coo_matrix, coo_weights = star_coo(num_nodes=5, weighted=True)
39 | >>> print(coo_matrix)
40 | array([[0, 1],
41 | [0, 2],
42 | [0, 3],
43 | [0, 4],
44 | [1, 0],
45 | [2, 0],
46 | [3, 0],
47 | [4, 0]]
48 |
49 | >>> print(coo_weights)
50 | array([[0.89292422],
51 | [0.3743427 ],
52 | [0.32810002],
53 | [0.97663266],
54 | [0.74940571],
55 | [0.89292422],
56 | [0.3743427 ],
57 | [0.32810002],
58 | [0.97663266],
59 | [0.74940571]])
60 |
61 | >>> adj_matrix = star_full(num_nodes=5, weighted=True)
62 | >>> print(adj_matrix)
63 | array([[0. , 0.72912008, 0.33964166, 0.30968042, 0.08774328],
64 | [0.72912008, 0. , 0. , 0. , 0. ],
65 | [0.33964166, 0. , 0. , 0. , 0. ],
66 | [0.30968042, 0. , 0. , 0. , 0. ],
67 | [0.08774328, 0. , 0. , 0. , 0. ]])
68 |
69 | ```
70 |
71 | Other examples can be found in ``` test/plot_static.py ``` and ``` test/plot_temporal.py ```.
72 |
--------------------------------------------------------------------------------
/.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 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/test/utils/_community_layout.py:
--------------------------------------------------------------------------------
1 | ''' Taken from https://stackoverflow.com/a/43541777 '''
2 |
3 | import networkx as nx
4 |
5 |
6 | def community_layout(g, partition):
7 | """
8 | Compute the layout for a modular graph.
9 |
10 | Arguments:
11 | ----------
12 | g -- networkx.Graph or networkx.DiGraph instance
13 | graph to plot
14 |
15 | partition -- dict mapping int node -> int community
16 | graph partitions
17 |
18 |
19 | Returns:
20 | --------
21 | pos -- dict mapping int node -> (float x, float y)
22 | node positions
23 |
24 | """
25 |
26 | pos_communities = _position_communities(g, partition, scale=3.)
27 |
28 | pos_nodes = _position_nodes(g, partition, scale=1.)
29 |
30 | # combine positions
31 | pos = dict()
32 | for node in g.nodes():
33 | pos[node] = pos_communities[node] + pos_nodes[node]
34 |
35 | return pos
36 |
37 | def _position_communities(g, partition, **kwargs):
38 |
39 | # create a weighted graph, in which each node corresponds to a community,
40 | # and each edge weight to the number of edges between communities
41 | between_community_edges = _find_between_community_edges(g, partition)
42 |
43 | communities = set(partition.values())
44 | hypergraph = nx.DiGraph()
45 | hypergraph.add_nodes_from(communities)
46 | for (ci, cj), edges in between_community_edges.items():
47 | hypergraph.add_edge(ci, cj, weight=len(edges))
48 |
49 | # find layout for communities
50 | pos_communities = nx.spring_layout(hypergraph, **kwargs)
51 |
52 | # set node positions to position of community
53 | pos = dict()
54 | for node, community in partition.items():
55 | pos[node] = pos_communities[community]
56 |
57 | return pos
58 |
59 | def _find_between_community_edges(g, partition):
60 |
61 | edges = dict()
62 |
63 | for (ni, nj) in g.edges():
64 | ci = partition[ni]
65 | cj = partition[nj]
66 |
67 | if ci != cj:
68 | try:
69 | edges[(ci, cj)] += [(ni, nj)]
70 | except KeyError:
71 | edges[(ci, cj)] = [(ni, nj)]
72 |
73 | return edges
74 |
75 | def _position_nodes(g, partition, **kwargs):
76 | """
77 | Positions nodes within communities.
78 | """
79 |
80 | communities = dict()
81 | for node, community in partition.items():
82 | try:
83 | communities[community] += [node]
84 | except KeyError:
85 | communities[community] = [node]
86 |
87 | pos = dict()
88 | for ci, nodes in communities.items():
89 | subgraph = g.subgraph(nodes)
90 | pos_subgraph = nx.spring_layout(subgraph, **kwargs)
91 | pos.update(pos_subgraph)
92 |
93 | return pos
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/numgraph/distributions/_clique.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | from typing import Optional, Tuple
5 | from numgraph.utils import to_undirected
6 |
7 | def _clique(num_nodes: int,
8 | weighted: bool = False,
9 | rng: Optional[Generator] = None) -> NDArray:
10 |
11 | adj_matrix = np.ones((num_nodes, num_nodes)) - np.eye(num_nodes)
12 |
13 | weights = None
14 | if weighted:
15 | if rng is None:
16 | rng = default_rng()
17 | weights = to_undirected(rng.uniform(low=0.0, high=1.0, size=(num_nodes, num_nodes)))
18 |
19 | return adj_matrix, weights
20 |
21 |
22 | def clique_full(num_nodes: int,
23 | weighted: bool = False,
24 | rng: Optional[Generator] = None) -> NDArray:
25 | """
26 | Returns a complete graph, a.k.a. a clique.
27 |
28 | Parameters
29 | ----------
30 | num_nodes : int
31 | The number of nodes
32 | weighted : bool, optional
33 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
34 | rng : Optional[Generator], optional
35 | Numpy random number generator, by default :obj:`None`
36 |
37 | Returns
38 | -------
39 | NDArray
40 | The clique in matrix representation :obj:`(num_nodes x num_nodes)`
41 | """
42 | adj_matrix, weights = _clique(num_nodes=num_nodes, weighted=weighted, rng=rng)
43 |
44 | adj_matrix = adj_matrix.astype(dtype=np.float32)
45 |
46 | return adj_matrix * weights if weighted else adj_matrix
47 |
48 |
49 | def clique_coo(num_nodes: int,
50 | weighted: bool = False,
51 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
52 | """
53 | Returns a complete graph, a.k.a. a clique.
54 |
55 | Parameters
56 | ----------
57 | num_nodes : int
58 | The number of nodes
59 | weighted : bool, optional
60 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
61 | rng : Optional[Generator], optional
62 | Numpy random number generator, by default :obj:`None`
63 |
64 | Returns
65 | -------
66 | NDArray
67 | The clique in COO representation :obj:`(num_edges x 2)`
68 | Optional[NDArray]
69 | Weights of the random graph.
70 | """
71 |
72 | adj_matrix, weights = _clique(num_nodes=num_nodes, weighted=weighted, rng=rng)
73 | if weighted:
74 | weights *= adj_matrix
75 |
76 | coo_matrix = np.argwhere(adj_matrix)
77 | coo_weights = np.expand_dims(weights[weights.nonzero()], -1) if weights is not None else None
78 |
79 | return coo_matrix, coo_weights
80 |
--------------------------------------------------------------------------------
/test/utils/_plot.py:
--------------------------------------------------------------------------------
1 | import community as community_louvain
2 | import networkx as nx
3 | import matplotlib.pyplot as plt
4 | from ._community_layout import community_layout
5 | from networkx.drawing.nx_pydot import graphviz_layout
6 |
7 | def plot_er(G, n):
8 | """
9 | Plots the Erdos-Renyi graph
10 |
11 | Parameters
12 | ----------
13 | G: nx.Graph
14 | The ER graph
15 | n: int
16 | Number of nodes
17 | """
18 | # Put the nodes in a circular shape
19 | pos = nx.circular_layout(G)
20 | nx.draw(G, pos, arrowstyle='-|>')
21 | plt.show()
22 |
23 |
24 | def plot_sbm(G, seed):
25 | """
26 | Plots the Stochastic Block Model graph
27 |
28 | Parameters
29 | ----------
30 | G: nx.Graph
31 | The SBM graph
32 | seed: int
33 | random seed
34 | """
35 | partition = community_louvain.best_partition(G, random_state=seed)
36 | pos = community_layout(G, partition)
37 | nx.draw(G, pos, node_color=list(partition.values()), arrowstyle='-|>')
38 | plt.show()
39 |
40 |
41 | def plot_ba(G, seed):
42 | """
43 | Plots the Barabasi Albert Model graph
44 | Parameters
45 | ----------
46 | G: nx.Graph
47 | The BA graph
48 | seed: int
49 | random seed
50 | """
51 | pos = nx.circular_layout(G)
52 | nx.draw(G, pos, arrowstyle='-|>')
53 | #partition = community_louvain.best_partition(G, random_state=seed)
54 | #pos = community_layout(G, partition)
55 | #nx.draw(G, pos, node_color=list(partition.values()))
56 | plt.show()
57 |
58 |
59 | def plot_tree_on_terminal(G):
60 | """
61 | Plots the random tree graph
62 |
63 | Parameters
64 | ----------
65 | G: nx.Graph
66 | The random tree graph
67 | """
68 | print(nx.forest_str(G))
69 |
70 |
71 | def plot_tree(G):
72 | """
73 | Plots the random tree graph
74 |
75 | Parameters
76 | ----------
77 | G: nx.Graph
78 | The random tree graph
79 | """
80 | pos = graphviz_layout(G, prog="twopi")
81 | nx.draw(G, pos, arrowstyle='-|>')
82 | plt.show()
83 |
84 |
85 | def plot_star(G):
86 | """
87 | Plots the star graph
88 |
89 | Parameters
90 | ----------
91 | G: nx.Graph
92 | The star graph
93 | """
94 | pos = nx.planar_layout(G)
95 | nx.draw(G, pos, arrowstyle='-|>')
96 | plt.show()
97 |
98 |
99 | def plot_clique(G):
100 | """
101 | Plots the clique graph
102 |
103 | Parameters
104 | ----------
105 | G: nx.Graph
106 | The clique graph
107 | """
108 | pos = nx.shell_layout(G)
109 | nx.draw(G, pos, arrowstyle='-|>')
110 | plt.show()
111 |
112 |
113 | def plot_grid(G):
114 | """
115 | Plots the simple grid graph
116 |
117 | Parameters
118 | ----------
119 | G: nx.Graph
120 | The simple grid graph
121 | """
122 | pos = nx.spring_layout(G, iterations=100, seed=9)
123 | nx.draw(G, pos, arrowstyle='-|>')
124 | plt.show()
125 |
--------------------------------------------------------------------------------
/src/numgraph/distributions/_star.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | from typing import Optional, Tuple
5 | from numgraph.utils import to_undirected, unsorted_coalesce
6 |
7 | def _star(num_nodes: int,
8 | directed: bool = False,
9 | weighted: bool = False,
10 | rng: Optional[Generator] = None) -> NDArray:
11 |
12 | edges = np.stack([np.zeros((num_nodes - 1, ), dtype = int), np.arange(1, num_nodes)],
13 | axis=1)
14 |
15 | weights = None
16 | if weighted:
17 | if rng is None:
18 | rng = default_rng()
19 | weights = rng.uniform(low=0.0, high=1.0, size=(len(edges), 1))
20 |
21 | if not directed:
22 | edges = to_undirected(edges)
23 | weights = np.vstack((weights, weights)) if weights is not None else None
24 |
25 | return unsorted_coalesce(edges, weights)
26 |
27 |
28 | def star_full(num_nodes: int,
29 | directed: bool = False,
30 | weighted: bool = False,
31 | rng: Optional[Generator] = None) -> NDArray:
32 | """
33 | Returns a star graph.
34 |
35 | Parameters
36 | ----------
37 | num_nodes : int
38 | The number of nodes
39 | directed : bool, optional
40 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
41 | weighted : bool, optional
42 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
43 | rng : Optional[Generator], optional
44 | Numpy random number generator, by default :obj:`None`
45 |
46 | Returns
47 | -------
48 | NDArray
49 | The star graph in matrix representaion :obj:`(num_nodes x num_nodes)`
50 | """
51 | coo_matrix, weights = _star(num_nodes=num_nodes, directed=directed, weighted=weighted, rng=rng)
52 |
53 | # Fill adj_matrix with the weights
54 | adj_matrix = np.zeros((num_nodes, num_nodes))
55 | adj_matrix[coo_matrix[:, 0], coo_matrix[:, 1]] = np.squeeze(weights) if weighted else 1
56 |
57 | return adj_matrix
58 |
59 |
60 | def star_coo(num_nodes: int,
61 | directed: bool = False,
62 | weighted: bool = False,
63 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
64 | """
65 | Returns a star graph.
66 |
67 | Parameters
68 | ----------
69 | num_nodes : int
70 | The number of nodes
71 | directed : bool, optional
72 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
73 | weighted : bool, optional
74 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
75 | rng : Optional[Generator], optional
76 | Numpy random number generator, by default :obj:`None`
77 |
78 | Returns
79 | -------
80 | NDArray
81 | The star graph in COO representation :obj:`(num_edges x 2)`
82 | Optional[NDArray]
83 | Weights of the random graph.
84 | """
85 | return _star(num_nodes=num_nodes, directed=directed, weighted=weighted, rng=rng)
86 |
--------------------------------------------------------------------------------
/test/plot_temporal.py:
--------------------------------------------------------------------------------
1 | from matplotlib import animation
2 | from utils import DynamicHeatmap, DynamicHeatGraph, DynamicNodeSignal
3 | from numgraph.distributions import *
4 | from numgraph.temporal import *
5 | import matplotlib.pyplot as plt
6 | import networkx as nx
7 | from numgraph.utils.spikes_generator import ColdHeatSpikeGenerator
8 |
9 |
10 | # Dissemination Process Simulation
11 | print('Susceptible-Infected')
12 | num_nodes = 15
13 | generator = lambda _: clique_coo(num_nodes)
14 | pos = nx.shell_layout(nx.from_edgelist(clique_coo(num_nodes)[0]))
15 |
16 | snapshots, xs = susceptible_infected_coo(generator, prob=0.4, mask_size=0.8, t_max = 100)
17 |
18 | for edge_list, x in zip(snapshots, xs):
19 | nodes = [(i, {'color': 'red' if v else 'green'}) for i, v in enumerate(x)]
20 | G = nx.DiGraph()
21 | G.add_nodes_from(nodes)
22 | G.add_edges_from(edge_list)
23 |
24 | nodes = G.nodes()
25 | sorted(nodes)
26 | c = nx.get_node_attributes(G,'color')
27 | colors = [c[n] for n in nodes]
28 | nx.draw(G, pos=pos, node_color=colors, arrowstyle='-|>')
29 |
30 | plt.pause(0.5)
31 | plt.clf()
32 | plt.close()
33 |
34 | # Heat Diffusion simulation (Euler's method)
35 | print("Euler's method heat diffusion")
36 | h, w = 3, 3
37 | generator = lambda _: simple_grid_coo(h,w, directed=False)
38 | t_max = 150
39 | spikegen = ColdHeatSpikeGenerator(t_max=t_max, prob_cold_spike=0.5, num_spikes=10)
40 | snapshots, xs = euler_graph_diffusion_coo(generator, spikegen, diffusion=None, t_max=t_max, num_nodes=h*w)
41 |
42 | dh = DynamicHeatmap(xs=xs, shape=(h,w), annot=True)
43 | dh.animate()
44 | #plt.show()
45 | f = "./EulerDynamicHeatmap.mp4"
46 | writervideo = animation.FFMpegWriter(fps=5)
47 | dh.anim.save(f, writer=writervideo)
48 |
49 |
50 | dh = DynamicHeatGraph(edges=snapshots, xs=xs, layout=lambda G: nx.spring_layout(G, iterations=100, seed=9))
51 | dh.animate()
52 | #plt.show()
53 | f = "./EulerDynamicGraph.mp4"
54 | writervideo = animation.FFMpegWriter(fps=5)
55 | dh.anim.save(f, writer=writervideo)
56 |
57 |
58 | dh = DynamicNodeSignal(xs=xs)
59 | dh.animate()
60 | #plt.show()
61 | f = "./EulerDynamicNodesignal.mp4"
62 | writervideo = animation.FFMpegWriter(fps=5)
63 | dh.anim.save(f, writer=writervideo)
64 |
65 |
66 |
67 | # Heat Diffusion simulation (Closed form solution)
68 | print("Closed form solution heat diffusion")
69 | h, w = 3, 3
70 | generator = lambda _: simple_grid_coo(h,w, directed=False)
71 | t_max = 150
72 | timestamps = [0.01 * i for i in range(100)]
73 | snapshots, xs = heat_graph_diffusion_coo(generator, timestamps, num_nodes=h*w)
74 |
75 | dh = DynamicHeatmap(xs=xs, shape=(h,w), annot=True)
76 | dh.animate()
77 | #plt.show()
78 | f = "./DynamicHeatmap.mp4"
79 | writervideo = animation.FFMpegWriter(fps=5)
80 | dh.anim.save(f, writer=writervideo)
81 |
82 |
83 | dh = DynamicHeatGraph(edges=snapshots, xs=xs, layout=lambda G: nx.spring_layout(G, iterations=100, seed=9))
84 | dh.animate()
85 | #plt.show()
86 | f = "./DynamicGraph.mp4"
87 | writervideo = animation.FFMpegWriter(fps=5)
88 | dh.anim.save(f, writer=writervideo)
89 |
90 |
91 | dh = DynamicNodeSignal(xs=xs)
92 | dh.animate()
93 | #plt.show()
94 | f = "./DynamicNodesignal.mp4"
95 | writervideo = animation.FFMpegWriter(fps=5)
96 | dh.anim.save(f, writer=writervideo)
--------------------------------------------------------------------------------
/test/utils/_dynamic_plot.py:
--------------------------------------------------------------------------------
1 | from matplotlib import animation
2 | import matplotlib.pyplot as plt
3 | import seaborn as sns
4 | import networkx as nx
5 |
6 | import numpy as np
7 | import tqdm
8 |
9 | class DynamicHeatmap:
10 |
11 | def __init__(self, xs, shape, annot=False):
12 | self.fig, self.ax = plt.subplots()
13 | self.anim = None
14 | self.pbar = tqdm.tqdm(total=len(xs))
15 | self.xs = xs
16 | self.shape = shape
17 | self.annot = annot
18 | self.cmap = sns.color_palette("magma", as_cmap=True)
19 |
20 | self.M, self.m = -np.inf, np.inf
21 | for x in xs:
22 | self.M = max(np.max(x), self.M)
23 | self.m = min(np.min(x), self.m)
24 |
25 | def animate(self):
26 | def init():
27 | sns.heatmap(np.zeros(self.shape), annot=self.annot, cmap=self.cmap, #linewidths=.5,
28 | yticklabels=False, xticklabels=False, cbar=False, ax=self.ax,
29 | vmin=self.m, vmax=self.M)
30 |
31 | def animate(i):
32 | self.pbar.update(1)
33 | self.ax.texts = []
34 | x = self.xs[i].reshape(self.shape)
35 | sns.heatmap(x, annot=self.annot, cmap=self.cmap, #linewidths=.5,
36 | yticklabels=False, xticklabels=False, cbar=False, ax=self.ax,
37 | vmin=self.m, vmax=self.M)
38 |
39 | self.anim = animation.FuncAnimation(self.fig, animate, init_func=init, frames=len(self.xs), repeat=False)
40 | sm = plt.cm.ScalarMappable(cmap=self.cmap, norm=plt.Normalize(vmin=self.m, vmax=self.M))
41 | sm.set_array([])
42 | plt.colorbar(sm)
43 |
44 |
45 |
46 | class DynamicHeatGraph:
47 |
48 | def __init__(self, edges, xs, layout):
49 | self.fig, self.ax = plt.subplots()
50 | self.anim = None
51 | self.pbar = tqdm.tqdm(total=len(xs))
52 | self.xs = xs
53 | self.edges = edges[0][0]
54 | self.G = nx.DiGraph()
55 | self.G.add_edges_from(self.edges)
56 | self.pos = layout(self.G)
57 |
58 | self.cmap = sns.color_palette("magma", as_cmap=True) #rocket
59 | self.M, self.m = -np.inf, np.inf
60 | for x in xs:
61 | self.M = max(np.max(x), self.M)
62 | self.m = min(np.min(x), self.m)
63 |
64 | def animate(self):
65 | def init():
66 | nx.draw(self.G, self.pos, node_color=self.xs[0], with_labels=True,
67 | cmap=self.cmap, arrowstyle='-|>', vmin=self.m, vmax=self.M)
68 |
69 | def animate(i):
70 | self.pbar.update(1)
71 | nx.draw(self.G, self.pos, node_color=self.xs[i], with_labels=True,
72 | cmap=self.cmap, arrowstyle='-|>', vmin=self.m, vmax=self.M)
73 |
74 | self.anim = animation.FuncAnimation(self.fig, animate, init_func=init, frames=len(self.xs), repeat = False)
75 | sm = plt.cm.ScalarMappable(cmap=self.cmap, norm=plt.Normalize(vmin=self.m, vmax=self.M))
76 | sm.set_array([])
77 | plt.colorbar(sm)
78 |
79 |
80 |
81 | class DynamicNodeSignal:
82 |
83 | def __init__(self, xs):
84 | self.fig, self.ax = plt.subplots()
85 | self.anim = None
86 | self.pbar = tqdm.tqdm(total=len(xs))
87 | self.xs = xs
88 | self.line=None
89 | self.M, self.m = -np.inf, np.inf
90 | for x in xs:
91 | self.M = max(np.max(x), self.M)
92 | self.m = min(np.min(x), self.m)
93 | self.ax.set_ylim([self.m - (0.1 * self.m), self.M + (0.1 * self.M)])
94 |
95 | def animate(self):
96 | def init():
97 | self.line, = self.ax.plot(self.xs[0], color='#1E88E5')
98 |
99 | def animate(i):
100 | self.pbar.update(1)
101 | self.line.set_ydata(self.xs[i])
102 |
103 | self.anim = animation.FuncAnimation(self.fig, animate, init_func=init, frames=len(self.xs), repeat = False)
104 |
--------------------------------------------------------------------------------
/src/numgraph/distributions/_random_tree.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from collections import Counter
3 | from numpy.typing import NDArray
4 | from typing import Optional, Tuple
5 | from numpy.random import Generator, default_rng
6 | from numgraph.utils import to_undirected, unsorted_coalesce
7 |
8 |
9 | def _random_tree(num_nodes: int,
10 | directed: bool = True,
11 | weighted: bool = False,
12 | rng: Optional[Generator] = None) -> NDArray:
13 |
14 | if rng is None:
15 | rng = default_rng()
16 |
17 | prufer_seq = [rng.choice(range(num_nodes)) for _ in range(num_nodes - 2)]
18 |
19 | # Node degree is equivalent to the number of times it appears in the sequence + 1
20 | degree = Counter(prufer_seq + list(range(num_nodes)))
21 |
22 | edges = []
23 | visited = set()
24 | for v in prufer_seq:
25 | for u in range(num_nodes):
26 | if degree[u] == 1:
27 | edges.append([v, u])
28 | degree[v] -= 1
29 | degree[u] -= 1
30 | visited.add(u)
31 | break
32 |
33 | u, v = degree.keys() - visited
34 | edges.append([u,v])
35 | edges = np.asarray(edges)
36 |
37 | weights = None
38 | if weighted:
39 | if rng is None:
40 | rng = default_rng()
41 | weights = rng.uniform(low=0.0, high=1.0, size=(len(edges), 1))
42 |
43 | if not directed:
44 | edges = to_undirected(edges)
45 | weights = np.vstack((weights, weights)) if weights is not None else None
46 |
47 | return unsorted_coalesce(edges, weights)
48 |
49 |
50 | def random_tree_coo(num_nodes: int,
51 | directed: bool = True,
52 | weighted: bool = False,
53 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
54 | """
55 | Returns a random tree computed using a random Prufer sequence.
56 |
57 | Parameters
58 | ----------
59 | num_nodes : int
60 | The number of nodes in the tree
61 | directed : bool, optional
62 | If set to :obj:`True`, will return a directed graph, by default :obj:`True`
63 | weighted : bool, optional
64 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
65 | rng : Optional[Generator], optional
66 | Numpy random number generator, by default :obj:`None`
67 |
68 | Returns
69 | -------
70 | NDArray
71 | The random tree in COO representation :obj:`(num_edges x 2)`
72 | Optional[NDArray]
73 | Weights of the random graph.
74 | """
75 |
76 | return _random_tree(num_nodes = num_nodes,
77 | directed = directed,
78 | weighted = weighted,
79 | rng = rng)
80 |
81 |
82 | def random_tree_full(num_nodes: int,
83 | directed: bool = True,
84 | weighted: bool = False,
85 | rng: Optional[Generator] = None) -> NDArray:
86 | """
87 | Returns a random tree computed using a random Prufer sequence.
88 |
89 | Parameters
90 | ----------
91 | num_nodes : int
92 | The number of nodes in the tree
93 | directed : bool, optional
94 | If set to :obj:`True`, will return a directed graph, by default :obj:`True`
95 | weighted : bool, optional
96 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
97 | rng : Optional[Generator], optional
98 | Numpy random number generator, by default :obj:`None`
99 |
100 | Returns
101 | -------
102 | NDArray
103 | The random tree in matrix representation :obj:`(num_edges x 2)`
104 | """
105 |
106 | coo_matrix, weights = _random_tree(num_nodes = num_nodes,
107 | directed = directed,
108 | weighted = weighted,
109 | rng = rng)
110 |
111 | # Fill adj_matrix with the weights
112 | adj_matrix = np.zeros((num_nodes, num_nodes))
113 | adj_matrix[coo_matrix[:, 0], coo_matrix[:, 1]] = np.squeeze(weights) if weighted else 1
114 |
115 | return adj_matrix
--------------------------------------------------------------------------------
/test/test.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numgraph.distributions import *
3 | import unittest
4 | from utils import get_all_matrices, get_directional_matrices, get_weighted_matrices
5 |
6 |
7 | N = 10
8 | block_size = [10, 5, 3]
9 | probs = [[0.5, 0.01, 0.01], [0.01, 0.5, 0.01], [0.01, 0.01, 0.5]]
10 | generator = lambda b, p, rng: erdos_renyi_coo(b, p, rng=rng)
11 | w = h = 3
12 | p = 0.35
13 | seed = 7
14 | rng = np.random.default_rng(seed)
15 | rng1 = np.random.default_rng(seed)
16 | rng2 = np.random.default_rng(seed)
17 |
18 | class TestStaticGraphDim(unittest.TestCase):
19 |
20 | def test_full_dim(self):
21 | matrices = get_all_matrices(N=N, p=p, block_size=block_size, probs=probs,
22 | h=h, w=w, generator=generator, rng=rng, coo=False)
23 |
24 | for num_nodes, matrix, _ in matrices:
25 | row, col = matrix.shape
26 | self.assertTrue(row == col and row == num_nodes)
27 |
28 |
29 | def test_full_weights(self):
30 | matrices = get_weighted_matrices(N=N, p=p, block_size=block_size, probs=probs,
31 | h=h, w=w, generator=generator, rng=rng, coo=False)
32 |
33 | for _, matrix, _ in matrices:
34 | self.assertTrue(np.all(matrix <= 1))
35 |
36 |
37 | def test_full_directed(self):
38 | rng = np.random.default_rng(seed)
39 | matrices = get_directional_matrices(directed=True, N=N, p=p, block_size=block_size,
40 | probs=probs, h=h, w=w, generator=generator,
41 | rng=rng, coo=False)
42 |
43 | for i, (_, matrix, _) in enumerate(matrices):
44 | j = 0
45 | while j < 10 and np.all(matrix == matrix.T): # check if return always an undirected graph or it is only an unluky sampling
46 | rng = np.random.default_rng(2**j)
47 | matrix = get_directional_matrices(directed=True, N=N, p=p, block_size=block_size,
48 | probs=probs, h=h, w=w, generator=generator,
49 | rng=rng, coo=False)[i]
50 | j += 1
51 | self.assertTrue(not np.all(matrix == matrix.T))
52 |
53 |
54 | def test_full_undirected(self):
55 | matrices = get_directional_matrices(directed=False, N=N, p=p, block_size=block_size, probs=probs, h=h, w=w, generator=generator, rng=rng, coo=False)
56 |
57 | for _, matrix, _ in matrices:
58 | self.assertTrue(np.all(matrix == matrix.T))
59 |
60 |
61 | def test_full_deterministic_sampling(self):
62 | matrices1 = get_all_matrices(N=N, p=p, block_size=block_size, probs=probs, h=h, w=w, generator=generator, rng=rng1, coo=False)
63 | matrices2 = get_all_matrices(N=N, p=p, block_size=block_size, probs=probs, h=h, w=w, generator=generator, rng=rng2, coo=False)
64 |
65 | for (_, matrix1, _), (_, matrix2, _) in zip(matrices1, matrices2):
66 | self.assertTrue(np.all(matrix1 == matrix2))
67 |
68 |
69 | def test_coo_dim(self):
70 | matrices = get_all_matrices(N=N, p=p, block_size=block_size, probs=probs, h=h, w=w, generator=generator, rng=rng, coo=True)
71 |
72 | for _, matrix, _ in matrices:
73 | self.assertTrue(isinstance(matrix, tuple))
74 | coo_matrix, coo_weights = matrix
75 | row1, col1 = coo_matrix.shape
76 | self.assertTrue(col1 == 2)
77 | if coo_weights is not None:
78 | row2, col2 = coo_weights.shape
79 | self.assertTrue(row1 == row2 and col2 == 1)
80 |
81 |
82 | def test_coo_deterministic_sampling(self):
83 | matrices1 = get_all_matrices(N=N, p=p, block_size=block_size, probs=probs, h=h, w=w, generator=generator, rng=rng1, coo=True)
84 | matrices2 = get_all_matrices(N=N, p=p, block_size=block_size, probs=probs, h=h, w=w, generator=generator, rng=rng2, coo=True)
85 |
86 | for (_, matrix1, _), (_, matrix2, _) in zip(matrices1, matrices2):
87 | self.assertTrue(isinstance(matrix1, tuple) and isinstance(matrix2, tuple))
88 | self.assertTrue(np.all(matrix1[0] == matrix2[0])) # check edges
89 | self.assertTrue(np.all(matrix1[1] == matrix2[1])) # check weights
90 |
91 |
92 |
93 | if __name__ == '__main__':
94 | unittest.main(verbosity=2)
--------------------------------------------------------------------------------
/src/numgraph/distributions/_erdos_renyi.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | from typing import Optional, Tuple
5 | from numgraph.utils import to_undirected, remove_self_loops
6 |
7 |
8 | def _erdos_renyi(num_nodes: int,
9 | prob: float,
10 | directed: bool = False,
11 | weighted: bool = False,
12 | rng: Optional[Generator] = None) -> NDArray:
13 |
14 | assert num_nodes >= 0 and 0 < prob <= 1
15 |
16 | if rng is None:
17 | rng = default_rng()
18 |
19 | adj_matrix = rng.random((num_nodes, num_nodes)) <= prob
20 | adj_matrix = remove_self_loops(adj_matrix)
21 |
22 | if not directed:
23 | adj_matrix = adj_matrix + adj_matrix.T
24 |
25 | weights = None
26 | if weighted:
27 | weights = rng.uniform(low=0.0, high=1.0, size=(num_nodes, num_nodes))
28 | if not directed:
29 | weights = to_undirected(weights)
30 |
31 | return adj_matrix, weights
32 |
33 |
34 | def erdos_renyi_coo(num_nodes: int,
35 | prob: float,
36 | directed: bool = False,
37 | weighted: bool = False,
38 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
39 |
40 | """
41 | Returns a random graph, also known as an Erdos-Renyi graph or a binomial graph.
42 | The model chooses each of the possible edges with a defined probability.
43 |
44 | Parameters
45 | ----------
46 | num_nodes : int
47 | The number of nodes
48 | prob : float
49 | Probability of an edge
50 | directed : bool, optional
51 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
52 | weighted : bool, optional
53 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
54 | rng : Generator, optional
55 | Numpy random number generator, by default :obj:`None`
56 |
57 | Returns
58 | -------
59 | NDArray
60 | The random graph in COO representation :obj:`(num_edges x 2)`.
61 | Optional[NDArray]
62 | Weights of the random graph.
63 | """
64 | adj_matrix, weights = _erdos_renyi(num_nodes=num_nodes,
65 | prob=prob,
66 | directed=directed,
67 | weighted=weighted,
68 | rng=rng)
69 |
70 | if weighted:
71 | weights *= adj_matrix
72 |
73 | coo_matrix = np.argwhere(adj_matrix)
74 | coo_weights = np.expand_dims(weights[weights.nonzero()], -1) if weights is not None else None
75 |
76 | return coo_matrix, coo_weights
77 |
78 |
79 | def erdos_renyi_full(num_nodes: int,
80 | prob: float,
81 | directed: bool = False,
82 | weighted: bool = False,
83 | rng: Optional[Generator] = None) -> NDArray:
84 |
85 | """
86 | Returns a random graph, also known as an Erdos-Renyi graph or a binomial graph.
87 | The model chooses each of the possible edges with a defined probability.
88 |
89 | Parameters
90 | ----------
91 | num_nodes : int
92 | The number of nodes
93 | prob : float
94 | Probability of an edge
95 | directed : bool, optional
96 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
97 | weighted : bool, optional
98 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
99 | rng : Generator, optional
100 | Numpy random number generator, by default :obj:`None`
101 |
102 | Returns
103 | -------
104 | NDArray
105 | The random graph in matrix representation :obj:`(num_nodes x num_nodes)`.
106 | """
107 | adj_matrix, weights = _erdos_renyi(num_nodes=num_nodes,
108 | prob=prob,
109 | directed=directed,
110 | weighted=weighted,
111 | rng=rng)
112 |
113 | adj_matrix = adj_matrix.astype(dtype=np.float32)
114 |
115 | return adj_matrix * weights if weighted else adj_matrix
116 |
--------------------------------------------------------------------------------
/docs/source/usage.rst:
--------------------------------------------------------------------------------
1 | Usage
2 | =====
3 |
4 | Here we provide some examples regarding how to use ``numgraph``.
5 |
6 |
7 | NumGraph in 2 steps
8 | -------------------
9 |
10 | .. code:: python
11 |
12 |
13 | >>> from numgraph import star_coo, star_full
14 | >>> coo_matrix, coo_weights = star_coo(num_nodes=5, weighted=True)
15 | >>> print(coo_matrix)
16 | array([[0, 1],
17 | [0, 2],
18 | [0, 3],
19 | [0, 4],
20 | [1, 0],
21 | [2, 0],
22 | [3, 0],
23 | [4, 0]]
24 |
25 | >>> print(coo_weights)
26 | array([[0.89292422],
27 | [0.3743427 ],
28 | [0.32810002],
29 | [0.97663266],
30 | [0.74940571],
31 | [0.89292422],
32 | [0.3743427 ],
33 | [0.32810002],
34 | [0.97663266],
35 | [0.74940571]])
36 |
37 | >>> adj_matrix = star_full(num_nodes=5, weighted=True)
38 | >>> print(adj_matrix)
39 | array([[0. , 0.72912008, 0.33964166, 0.30968042, 0.08774328],
40 | [0.72912008, 0. , 0. , 0. , 0. ],
41 | [0.33964166, 0. , 0. , 0. , 0. ],
42 | [0.30968042, 0. , 0. , 0. , 0. ],
43 | [0.08774328, 0. , 0. , 0. , 0. ]])
44 |
45 | Other examples can be found in `plot_static.py `_ and `plot_temporal.py `_.
46 |
47 |
48 | Stochastic Block Model
49 | ----------------------
50 |
51 | To generate a Stochastic Block Model graph, we need to define from which distribution the communities belong.
52 | Moreover, we need to identify the probability matrix:
53 | - the element ``i,j`` represents the edge probability between blocks i and j
54 | - the element ``i,i`` define the edge probability inside block i.
55 |
56 |
57 | .. code:: python
58 |
59 |
60 | >>> from numgraph import stochastic_block_model_coo
61 | >>> block_sizes = [15, 5, 3]
62 | >>> probs = [[0.5, 0.01, 0.01],
63 | >>> [0.01, 0.5, 0.01],
64 | >>> [0.01, 0.01, 0.5]]
65 | >>> generator = lambda b, p, rng: erdos_renyi_coo(b, p)
66 | >>> coo_matrix, coo_weights = stochastic_block_model_coo(block_sizes = block_sizes,
67 | >>> probs = probs,
68 | >>> generator = generator)
69 |
70 |
71 | .. note::
72 | The communities are generated with consecutive node ids. Let consider the previous example where ``block_sizes = [15, 5, 3]``. Here the first community has node ids in ``[0,15)``, the second in ``[15,20)``, and the third in ``[20,23)``.
73 |
74 |
75 | Heat Diffusion simulation
76 | -------------------------
77 |
78 | Similarly to the SBM generation, even the temporal distribution require the definition of a generator to compute the employed graph. In the case of the heat diffusion simulation leveraging the Euler's method, it is also important to define a ``SpikeGenerator``, which specifies how heat spikes are generated over time.
79 |
80 |
81 | .. code:: python
82 |
83 |
84 | >>> from numgraph import simple_grid_coo
85 | >>> from numgraph.utils.spikes_generator import ColdHeatSpikeGenerator
86 | >>> from numgraph.temporal import euler_graph_diffusion_coo
87 |
88 | >>> h, w = 3, 3
89 | >>> generator = lambda _: simple_grid_coo(h, w, directed=False)
90 | >>> t_max = 150
91 |
92 | >>> spikegen = ColdHeatSpikeGenerator(t_max=t_max, prob_cold_spike=0.5, num_spikes=10)
93 | >>> snapshots, xs = euler_graph_diffusion_coo(generator, spikegen, t_max=t_max, num_nodes=h*w)
94 | >>> print(snapshots[0]) # the topology of the graph at time 0
95 | array([[0., 1., 0., 1., 0., 0., 0., 0., 0.],
96 | [1., 0., 1., 0., 1., 0., 0., 0., 0.],
97 | [0., 1., 0., 0., 0., 1., 0., 0., 0.],
98 | [1., 0., 0., 0., 1., 0., 1., 0., 0.],
99 | [0., 1., 0., 1., 0., 1., 0., 1., 0.],
100 | [0., 0., 1., 0., 1., 0., 0., 0., 1.],
101 | [0., 0., 0., 1., 0., 0., 0., 1., 0.],
102 | [0., 0., 0., 0., 1., 0., 1., 0., 1.],
103 | [0., 0., 0., 0., 0., 1., 0., 1., 0.]])
104 |
105 | >>> print(xs[0]) # the temperature of each node at time 0
106 | array([[ 0.10196489],
107 | [-0.17995079],
108 | [ 0.04456628],
109 | [ 0.05386166],
110 | [ 0.03761498],
111 | [ 0.040233 ],
112 | [ 0.09440064],
113 | [ 0.17265226],
114 | [ 0.15886457]])
115 |
--------------------------------------------------------------------------------
/src/numgraph/distributions/_barabasi_albert.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | from typing import Optional, Tuple
5 | from numgraph.utils import remove_self_loops, to_undirected
6 |
7 |
8 | def _barabasi_albert(num_nodes: int,
9 | num_edges: int,
10 | weighted: bool = False,
11 | rng: Optional[Generator] = None) -> NDArray:
12 |
13 | assert num_nodes >= 0 and num_edges > 0 and num_edges < num_nodes
14 |
15 | if rng is None:
16 | rng = default_rng()
17 |
18 | sources, targets = np.arange(num_edges), rng.permutation(num_edges)
19 |
20 | for i in range(num_edges, num_nodes):
21 | sources = np.concatenate([sources, np.full((num_edges, ), i, dtype=np.int64)])
22 | choice = rng.choice(np.concatenate([sources, targets]), num_edges)
23 | targets = np.concatenate([targets, choice])
24 |
25 | sources, targets = sources.reshape((-1, 1)), targets.reshape((-1, 1))
26 | edge_list = np.concatenate([sources, targets], axis=1)
27 |
28 | edge_list = remove_self_loops(edge_list)
29 | edge_list = to_undirected(edge_list)
30 |
31 | adj_matrix = np.zeros((num_nodes, num_nodes))
32 | adj_matrix[edge_list[:, 0], edge_list[:, 1]] = 1
33 |
34 | weights = None
35 | if weighted:
36 | weights = rng.uniform(low=0.0, high=1.0, size=(num_nodes, num_nodes))
37 | weights = to_undirected(weights)
38 |
39 | return adj_matrix, weights
40 |
41 |
42 |
43 | def barabasi_albert_coo(num_nodes: int,
44 | num_edges: int,
45 | weighted: bool = False,
46 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
47 | """
48 | Returns a graph sampled from the Barabasi-Albert (BA) model. The graph is built
49 | incrementally by adding :obj:`num_edges` arcs from a new node to already existing ones with
50 | preferential attachment towards nodes with high degree.
51 |
52 | Parameters
53 | ----------
54 | num_nodes : int
55 | The number of nodes
56 | num_edges : int
57 | The number of edges
58 | weighted : bool, optional
59 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
60 | rng : Optional[Generator], optional
61 | Numpy random number generator, by default :obj:`None`
62 |
63 | Returns
64 | -------
65 | NDArray
66 | The Barabasi-Albert graph in COO representation :obj:`(num_edges x 2)`
67 | Optional[NDArray]
68 | Weights of the random graph.
69 | """
70 |
71 | adj_matrix, weights = _barabasi_albert(num_nodes=num_nodes,
72 | num_edges=num_edges,
73 | weighted=weighted,
74 | rng=rng)
75 |
76 | if weighted:
77 | weights *= adj_matrix
78 |
79 | coo_matrix = np.argwhere(adj_matrix)
80 | coo_weights = np.expand_dims(weights[weights.nonzero()], -1) if weights is not None else None
81 |
82 | return coo_matrix, coo_weights
83 |
84 |
85 | def barabasi_albert_full(num_nodes: int,
86 | num_edges: int,
87 | weighted: bool = False,
88 | rng: Optional[Generator] = None) -> NDArray:
89 | """
90 | Returns a graph sampled from the Barabasi-Albert (BA) model. The graph is built
91 | incrementally by adding `num_edges` arcs from a new node to already existing ones with
92 | preferential attachment towards nodes with high degree.
93 |
94 | Parameters
95 | ----------
96 | num_nodes : int
97 | The number of nodes
98 | num_edges : int
99 | The number of edges
100 | weighted : bool, optional
101 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
102 | rng : Optional[Generator], optional
103 | Numpy random number generator, by default :obj:`None`
104 |
105 | Returns
106 | -------
107 | NDArray
108 | The Barabasi-Albert graph in matrix representation :obj:`(num_nodes x num_nodes)`
109 | """
110 |
111 | adj_matrix, weights = _barabasi_albert(num_nodes=num_nodes,
112 | num_edges=num_edges,
113 | weighted=weighted,
114 | rng=rng)
115 |
116 | adj_matrix = adj_matrix.astype(dtype=np.float32)
117 |
118 | return adj_matrix * weights if weighted else adj_matrix
--------------------------------------------------------------------------------
/src/numgraph/utils/_utils.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple, Optional
2 | from numpy.typing import NDArray
3 | from numpy.random import default_rng, Generator
4 | import numpy as np
5 |
6 |
7 | def to_dense(edge_list: NDArray, num_nodes: int = None) -> NDArray:
8 | """
9 | Converts a list of edges in a squared adjacency matrix
10 |
11 | Parameters
12 | ----------
13 | edge_list : NDArray
14 | The list of edges :obj:`(num_edges x 2)`
15 | num_nodes : int, optional
16 | The number of nodes in the graph, by default :obj:`None`
17 |
18 | Returns
19 | -------
20 | NDArray
21 | The squared adjacency matrix :obj:`(num_nodes x num_nodes)`
22 | """
23 | if not num_nodes:
24 | num_nodes = np.max(edge_list) + 1
25 |
26 | dense_adj = np.zeros((num_nodes, num_nodes))
27 |
28 | for i, j in edge_list:
29 | dense_adj[i, j] = 1
30 |
31 | return dense_adj
32 |
33 |
34 | def to_sparse(adj_matrix: NDArray) -> NDArray:
35 |
36 | """
37 | Converts an adjacency matrix to a list of edges
38 |
39 | Parameters
40 | ----------
41 | adj_matrix : NDArray
42 | The squared adjacency matrix :obj:`(num_nodes x num_nodes)`
43 |
44 | Returns
45 | -------
46 | NDArray
47 | The list of edges :obj:`(num_edges x 2)`
48 | """
49 |
50 | return np.argwhere(adj_matrix > 0)
51 |
52 |
53 | def to_undirected(adj: NDArray) -> NDArray:
54 | """
55 | Turns a directed edge_list into a non-directed one
56 |
57 | Parameters
58 | ----------
59 | adj : NDArray
60 | A directed adjacency matrix :obj:`(num_nodes x num_nodes)`, or edge list :obj:`(num_edges x 2)`
61 |
62 | Returns
63 | -------
64 | NDArray
65 | An undirected adjacency matrix :obj:`(num_nodes x num_nodes)`, or the edge list :obj:`((2*num_edges) x 2)`
66 | """
67 | row, col = adj.shape
68 |
69 | if row == col:
70 | # Case of a squared dense adj matrix
71 | return np.triu(adj) + np.triu(adj, 1).T
72 |
73 | sources, targets = adj[:, 0], adj[:, 1]
74 | sources, targets = sources.reshape((-1, 1)), targets.reshape((-1, 1))
75 |
76 | new_edges = np.concatenate((targets, sources), axis=1)
77 | adj = np.concatenate((adj, new_edges), axis=0)
78 |
79 | return adj
80 |
81 |
82 | def coalesce(edge_list: NDArray) -> NDArray:
83 | """
84 | Polishes an edge list by removing duplicates and by sorting the edges
85 |
86 | Parameters
87 | ----------
88 | edge_list : NDArray
89 | An edge list :obj:`(num_edges x 2)`
90 |
91 | Returns
92 | -------
93 | NDArray
94 | A sorted edge list with no duplicated edges :obj:`(new_num_edges x 2)`
95 | """
96 | return np.unique(edge_list, axis=0)
97 |
98 |
99 | def unsorted_coalesce(edge_list: NDArray, weights: Optional[NDArray] = None) -> Tuple[NDArray, NDArray]:
100 | """
101 | Polishes an edge list by removing duplicates and by sorting the edges
102 |
103 | Parameters
104 | ----------
105 | edge_list : NDArray
106 | An edge list :obj:`(num_edges x 2)`
107 | weights : NDArray
108 | The weights :obj:`(num_edges x 1)`
109 | Returns
110 | -------
111 | NDArray
112 | An unsorted edge list with no duplicated edges :obj:`(new_num_edges x 2)`
113 | NDArray
114 | The unsorted weigths associated to the new edge list :obj:`(new_num_edges x 1)`
115 | """
116 | indexes = sorted(np.unique(edge_list, return_index=True, axis=0)[1])
117 | return edge_list[indexes], weights[indexes] if weights is not None else weights
118 |
119 |
120 | def dense(generator):
121 | """
122 | Transforms a sparse generator into its dense version
123 |
124 | Parameters
125 | ----------
126 | generator : Callable
127 | A callable that generates graphs
128 |
129 | Returns
130 | -------
131 | Callable
132 | A callable that generates the squared adjacency matrix :obj:`(num_nodes x num_nodes)` of a graph
133 | """
134 | return lambda *args, **kwargs: to_dense(generator(*args, **kwargs))
135 |
136 |
137 | def remove_self_loops(adj: NDArray) -> NDArray:
138 | """
139 | Removes every self-loop in the graph given by adj
140 |
141 | Parameters
142 | ----------
143 | adj : NDArray
144 | The adjancency matrix :obj:`(num_nodes x num_nodes)`, or the edge_list :obj:`(num_edges x 2)`
145 |
146 | Returns
147 | -------
148 | NDAarray
149 | The adjacency matrix :obj:`(num_nodes x num_nodes)`, or the list of edges :obj:`(new_num_edges x 2)`,
150 | without self-loops.
151 | """
152 | row, col = adj.shape
153 |
154 | if row == col:
155 | # Case of a squared dense adj matrix
156 | np.fill_diagonal(adj, 0)
157 | return adj
158 |
159 | sources, targets = adj[:, 0], adj[:, 1]
160 | mask = ~(sources == targets)
161 |
162 | return adj[mask]
163 |
--------------------------------------------------------------------------------
/src/numgraph/temporal/_heat_diffusion.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple, Callable, Optional, List, Union
2 | from numpy.random import Generator, default_rng
3 | from numpy.typing import NDArray
4 | import numpy as np
5 |
6 | def _heat_graph_diffusion(generator: Callable,
7 | timestamps: List,
8 | init_temp: Optional[Union[float, NDArray]] = None,
9 | num_nodes: Optional[int] = None,
10 | return_coo: float = True,
11 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
12 |
13 | assert init_temp is None or (isinstance(init_temp, float) and init_temp > 0), f'init_temp can be None or float > 0, not {type(init_temp)} with value {init_temp}'
14 |
15 | if rng is None:
16 | rng = default_rng()
17 |
18 | # Generate the graph
19 | edges = generator(rng)
20 | assert isinstance(edges, tuple) and edges[0].shape[1] == 2, 'The generator must return a graph in COO representation.'
21 | edges, weights = edges
22 |
23 | if num_nodes is None:
24 | num_nodes = edges.max() + 1
25 |
26 | if init_temp is None:
27 | x = rng.uniform(low=0.0, high=0.2, size=(num_nodes, 1))
28 | elif isinstance(init_temp, float):
29 | x = np.full((num_nodes,1), init_temp)
30 | else:
31 | x = init_temp
32 |
33 | # Compute the Laplacian matrix
34 | adj_mat = np.zeros((num_nodes, num_nodes))
35 | adj_mat[edges[:, 0], edges[:, 1]] = 1 if weights is None else weights
36 | self_loops = np.diag(adj_mat)
37 | np.fill_diagonal(adj_mat, 0)
38 | degree = np.diag(np.sum(adj_mat, axis=1))
39 | new_degree = np.linalg.inv(np.sqrt(degree))
40 | L = np.eye(num_nodes) - new_degree @ adj_mat @ new_degree # Normalized laplacian
41 |
42 | eigenvalues, eigenvectors = np.linalg.eig(L)
43 | Lambda = np.diag(eigenvalues)
44 | l = np.zeros_like(Lambda)
45 |
46 | xs = []
47 | for t in timestamps:
48 | # Closed form solution of the Graph Heat Diffusion equation (ie, e^{-tL}x_0)
49 | np.fill_diagonal(l, np.exp(-t * np.diag(Lambda)))
50 | xs.append(eigenvectors @ l @ np.linalg.inv(eigenvectors) @ x)
51 |
52 | if return_coo:
53 | return [(edges, weights)] * len(timestamps), xs
54 | else:
55 | adj_mat += self_loops
56 | return [adj_mat] * len(timestamps), xs
57 |
58 |
59 | def heat_graph_diffusion_coo(generator: Callable,
60 | timestamps: List,
61 | init_temp: Optional[Union[float, NDArray]] = None,
62 | num_nodes: Optional[int] = None,
63 | rng: Optional[Generator] = None) -> Tuple[List[Tuple[NDArray, NDArray]], List[NDArray]]:
64 | """
65 | Returns heat diffusion over a graph computed with the closed form solution.
66 | The model simulates the diffusion of heat on a given graph through the graph heat equation.
67 | Each node is characterized by the temperature. The process is evaluated on the predefined
68 | :obj:`timestamps`. The simulation graph has fixed nodes and edges along the temporal axis.
69 |
70 | Parameters
71 | ----------
72 | generator : Callable
73 | A callable that takes as input a rng and generates the simulation graph
74 | timestamps: List,
75 | The list of timestamps in which the diffusion is evaluated
76 | init_temp : Union[float, NDArray], optional
77 | The initial temperature of the nodes. If :obj:`None` it computes a random temperature between :obj:`0.` and :obj:`0.2`, by default :obj:`None`
78 | num_nodes : int, optional
79 | The number of nodes in the simulation graph, by default :obj:`None`
80 | rng : Generator, optional
81 | Numpy random number generator, by default :obj:`None`
82 |
83 | Returns
84 | -------
85 | List[Tuple[NDArray, NDArray]]
86 | The list of graph's snapshots :obj:`(T x 2)`: each snapshot is a tuple containing the graph
87 | in COO representation :obj:`(snapshot_num_edges x 2)` and the weights :obj:`(snapshot_num_edges x 1)`
88 | List[NDArray]
89 | The list of nodes' states :obj:`(T x (snapshot_num_nodes, ))`
90 | """
91 | return _heat_graph_diffusion(generator = generator,
92 | timestamps = timestamps,
93 | init_temp = init_temp,
94 | num_nodes = num_nodes,
95 | return_coo = True,
96 | rng = rng)
97 |
98 |
99 | def heat_graph_diffusion_full(generator: Callable,
100 | timestamps: List,
101 | init_temp: Optional[Union[float, NDArray]] = None,
102 | num_nodes: Optional[int] = None,
103 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
104 | """
105 |
106 | Returns heat diffusion over a graph computed with the closed form solution.
107 | The model simulates the diffusion of heat on a given graph through the graph heat equation.
108 | Each node is characterized by the temperature. The process is evaluated on the predefined
109 | :obj:`timestamps`. The simulation graph has fixed nodes and edges along the temporal axis.
110 |
111 | Parameters
112 | ----------
113 | generator : Callable
114 | A callable that takes as input a rng and generates the simulation graph
115 | timestamps: List,
116 | The list of timestamps in which the diffusion is evaluated
117 | init_temp : Union[float, NDArray], optional
118 | The initial temperature of the nodes. If :obj:`None` it computes a random temperature between :obj:`0.` and :obj:`0.2`, by default :obj:`None`
119 | num_nodes : int, optional
120 | The number of nodes in the simulation graph, by default :obj:`None`
121 | rng : Generator, optional
122 | Numpy random number generator, by default :obj:`None`
123 |
124 | Returns
125 | -------
126 | List[NDArray]
127 | the list of graph's snapshots in matrix representation :obj:`(T x (num_nodes x num_nodes))`
128 | List[NDArray]
129 | the list of nodes' states :obj:`(T x (num_nodes, ))`
130 | """
131 | return _heat_graph_diffusion(generator = generator,
132 | timestamps = timestamps,
133 | init_temp = init_temp,
134 | num_nodes = num_nodes,
135 | return_coo = False,
136 | rng = rng)
137 |
138 |
--------------------------------------------------------------------------------
/src/numgraph/distributions/_sbm.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | from typing import Optional, Tuple, List, Callable
5 | from numgraph.utils import coalesce, unsorted_coalesce
6 |
7 |
8 | def _stochastic_block_model(block_sizes: List[int],
9 | probs: List[List[float]],
10 | generator: Callable,
11 | directed: bool = False,
12 | weighted: bool = False,
13 | rng: Optional[Generator] = None) -> NDArray:
14 |
15 | assert all(block_sizes) and len(probs) == len(block_sizes)
16 | assert all([len(p) == len(probs) for p in probs]) and all([all(p) for p in probs])
17 |
18 | if rng is None:
19 | rng = default_rng()
20 |
21 | communities = []
22 | for i, b in enumerate(block_sizes):
23 | edges = generator(b, probs[i][i], rng)
24 | assert isinstance(edges, tuple) and edges[0].shape[1] == 2, 'The generator must return a graph in COO representation.'
25 | communities.append(edges[0])
26 |
27 | # Update communities's indices
28 | sizes = {}
29 | first_id = 0
30 | for i in range(len(block_sizes)):
31 | communities[i] += first_id
32 | sizes[i] = first_id
33 | first_id += block_sizes[i]
34 |
35 | # Compute inter-block links
36 | edges = []
37 | for i in range(len(probs)):
38 | for j in range(len(probs)):
39 | if i == j: continue
40 |
41 | p = probs[i][j]
42 | size_c1, size_c2 = block_sizes[i], block_sizes[j]
43 |
44 | mask = rng.random((size_c1, size_c2)) <= p
45 |
46 | inter_block_edges = np.argwhere(mask)
47 | inter_block_edges[:, 0] += sizes[i]
48 | inter_block_edges[:, 1] += sizes[j]
49 |
50 | edges.append(inter_block_edges)
51 |
52 | edges = np.concatenate(edges + communities)
53 | if not directed:
54 | edges = np.vstack((edges, edges[:, [1,0]]))
55 | edges = coalesce(edges)
56 |
57 | weights = None
58 | if weighted:
59 | weights = rng.uniform(low=0.0, high=1.0, size=(sum(block_sizes), sum(block_sizes)))
60 | if not directed:
61 | weights = np.tril(weights) + np.triu(weights.T, 1)
62 | weights = np.expand_dims(weights[edges[:,0], edges[:,1]], axis=-1)
63 |
64 | return edges, weights
65 |
66 |
67 |
68 | def stochastic_block_model_coo(block_sizes: List[int],
69 | probs: List[List[float]],
70 | generator: Callable,
71 | directed: bool = False,
72 | weighted: bool = False,
73 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
74 | """
75 | Returns a stochastic block model graph.
76 | This model partitions the nodes into blocks of defined sizes,
77 | and places edges between pairs of nodes depending on a probability matrix.
78 | Such a matrix specifies edge probabilities between and inside blocks.
79 |
80 | Parameters
81 | ----------
82 | block_sizes : List[int]
83 | Sizes of blocks
84 | probs : List[List[float]]
85 | The squared probability matrix :obj:`(num_blocks x num_blocks)`.
86 | The element :obj:`i,j` represents the edge probability between blocks :obj:`i` and :obj:`j`.
87 | The element :obj:`i,i` define the edge probability inside block :obj:`i`.
88 | generator : Callable
89 | A callable that generates communities with size depending on block_sizes
90 | directed : bool, optional
91 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
92 | weighted : bool, optional
93 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
94 | rng : Generator, optional
95 | Numpy random number generator, by default :obj:`None`
96 |
97 | Returns
98 | -------
99 | NDArray
100 | The stochastic block model graph in COO representation :obj:`(num_edges x 2)`.
101 | Optional[NDArray]
102 | Weights of the random graph.
103 |
104 | Note
105 | ----
106 | The generator takes as input the block size, the probability, and the rng
107 | """
108 |
109 | coo_matrix, coo_weights = _stochastic_block_model(block_sizes=block_sizes,
110 | probs=probs,
111 | generator=generator,
112 | directed=directed,
113 | weighted=weighted,
114 | rng=rng)
115 |
116 | return coo_matrix, coo_weights
117 |
118 |
119 | def stochastic_block_model_full(block_sizes: List[int],
120 | probs: List[List[float]],
121 | generator: Callable,
122 | directed: bool = False,
123 | weighted: bool = False,
124 | rng: Optional[Generator] = None) -> NDArray:
125 | """
126 | Returns a stochastic block model graph.
127 | This model partitions the nodes into blocks of defined sizes,
128 | and places edges between pairs of nodes depending on a probability matrix.
129 | Such a matrix specifies edge probabilities between and inside blocks.
130 |
131 | Parameters
132 | ----------
133 | block_sizes : List[int]
134 | Sizes of blocks
135 | probs : List[List[float]]
136 | The squared probability matrix :obj:`(num_blocks x num_blocks)`.
137 | The element :obj:`i,j` represents the edge probability between blocks :obj:`i` and :obj:`j`.
138 | The element :obj:`i,i` define the edge probability inside block :obj:`i`.
139 | generator : Callable
140 | A callable that generates communities with size depending on block_sizes
141 | directed : bool, optional
142 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
143 | weighted : bool, optional
144 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
145 | rng : Generator, optional
146 | Numpy random number generator, by default :obj:`None`
147 |
148 | Returns
149 | -------
150 | NDArray
151 | The stochastic block model graph in matrix representation :obj:`(num_nodes x num_nodes)`.
152 |
153 | Note
154 | ----
155 | The generator takes as input the block size, the probability, and the rng
156 | """
157 |
158 | coo_matrix, weights = _stochastic_block_model(block_sizes=block_sizes,
159 | probs=probs,
160 | generator=generator,
161 | directed=directed,
162 | weighted=weighted,
163 | rng=rng)
164 | # Fill adj_matrix with the weights
165 | num_nodes = sum(block_sizes)
166 | adj_matrix = np.zeros((num_nodes, num_nodes))
167 | adj_matrix[coo_matrix[:, 0], coo_matrix[:, 1]] = np.squeeze(weights) if weighted else 1
168 |
169 | return adj_matrix
--------------------------------------------------------------------------------
/src/numgraph/distributions/_grid.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | from typing import Optional, Tuple
5 | from numgraph.utils import to_undirected
6 |
7 |
8 | def _grid(height: int,
9 | width: int,
10 | kernel: NDArray,
11 | directed: bool = False,
12 | weighted: bool = False,
13 | rng: Optional[Generator] = None) -> NDArray:
14 |
15 | """
16 | Ausiliar function for grid graph generation.
17 | """
18 | num_nodes = height * width
19 | K = len(kernel)
20 |
21 | sources = np.arange(num_nodes, dtype=np.int64).repeat(K)
22 | targets = sources + np.tile(kernel, num_nodes)
23 | mask = (targets >= 0) & (targets < num_nodes)
24 |
25 | sources, targets = sources[mask].reshape((-1, 1)), targets[mask].reshape((-1, 1))
26 | edge_list = np.concatenate([sources, targets], axis=1)
27 |
28 | # Remove edges (u,v) from a boundary node to the first node of the new row.
29 | submask_1 = ((edge_list[:, 0] + 1) % width == 0) & ((edge_list[:, 1]) % width == 0)
30 | # As the graph is undirected, remove the corresponding edges (v, u).
31 | submask_2 = ((edge_list[:, 0]) % width == 0) & ((edge_list[:, 1] + 1) % width == 0)
32 |
33 | mask = ~(submask_1 | submask_2)
34 |
35 | edge_list = edge_list[mask]
36 | num_nodes = height * width
37 | adj_matrix = np.zeros((num_nodes, num_nodes))
38 | adj_matrix[edge_list[:,0], edge_list[:,1]] = 1
39 |
40 | if not directed:
41 | adj_matrix = adj_matrix + adj_matrix.T
42 | adj_matrix[adj_matrix.nonzero()] = 1
43 |
44 | weights = None
45 | if weighted:
46 | if rng is None:
47 | rng = default_rng()
48 | weights = rng.uniform(low=0.0, high=1.0, size=(num_nodes, num_nodes))
49 | weights = to_undirected(weights)
50 |
51 | return adj_matrix, weights
52 |
53 |
54 | def grid_full(height: int,
55 | width: int,
56 | directed: bool=False,
57 | weighted: bool = False,
58 | rng: Optional[Generator] = None) -> NDArray:
59 | """
60 | Returns a full undirected two-dimensional rectangular grid lattice graph.
61 |
62 | .. code-block:: python
63 |
64 | 1 - 2 - 3
65 | | X | X |
66 | 4 - 5 - 6
67 |
68 | Parameters
69 | ----------
70 | height : int
71 | Number of vertices in the vertical axis
72 | width : int
73 | Number of vertices in the horizontal axis
74 | directed : bool, optional
75 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
76 | weighted : bool, optional
77 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
78 | rng : Optional[Generator], optional
79 | Numpy random number generator, by default :obj:`None`
80 |
81 | Returns
82 | -------
83 | NDArray
84 | The full undirected two-dimensional rectangular grid lattice graph in matrix representation :obj:`(num_nodes x num_nodes)`
85 | """
86 | w = width
87 | kernel = np.array([-w - 1, -w, -w + 1, -1, w, w - 1, w, w + 1])
88 | adj_matrix, weights = _grid(height, width, kernel, directed, weighted, rng)
89 |
90 | adj_matrix = adj_matrix.astype(dtype=np.float32)
91 |
92 | return adj_matrix * weights if weighted else adj_matrix
93 |
94 |
95 | def grid_coo(height: int,
96 | width: int,
97 | directed: bool=False,
98 | weighted: bool = False,
99 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
100 | """
101 | Returns a full undirected two-dimensional rectangular grid lattice graph.
102 |
103 | .. code-block:: python
104 |
105 | 1 - 2 - 3
106 | | X | X |
107 | 4 - 5 - 6
108 |
109 | Parameters
110 | ----------
111 | height : int
112 | Number of vertices in the vertical axis
113 | width : int
114 | Number of vertices in the horizontal axis
115 | directed : bool, optional
116 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
117 | weighted : bool, optional
118 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
119 | rng : Optional[Generator], optional
120 | Numpy random number generator, by default :obj:`None`
121 |
122 | Returns
123 | -------
124 | NDArray
125 | The full undirected two-dimensional rectangular grid lattice graph in COO representation :obj:`(num_edges x 2)`
126 | Optional[NDArray]
127 | Weights of the random graph.
128 | """
129 | w = width
130 | kernel = np.array([-w - 1, -w, -w + 1, -1, w, w - 1, w, w + 1])
131 | adj_matrix, weights = _grid(height, width, kernel, directed, weighted, rng)
132 |
133 | if weighted:
134 | weights *= adj_matrix
135 |
136 | coo_matrix = np.argwhere(adj_matrix)
137 | coo_weights = np.expand_dims(weights[weights.nonzero()], -1) if weights is not None else None
138 |
139 | return coo_matrix, coo_weights
140 |
141 |
142 | def simple_grid_full(height: int,
143 | width: int,
144 | directed: bool=False,
145 | weighted: bool = False,
146 | rng: Optional[Generator] = None) -> NDArray:
147 | """
148 | Returns an undirected two-dimensional rectangular grid lattice graph.
149 |
150 | .. code-block:: python
151 |
152 | 1 -- 2 -- 3
153 | | | |
154 | 4 -- 5 -- 6
155 |
156 | Parameters
157 | ----------
158 | height : int
159 | Number of vertices in the vertical axis
160 | width : int
161 | Number of vertices in the horizontal axis
162 | directed : bool, optional
163 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
164 | weighted : bool, optional
165 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
166 | rng : Optional[Generator], optional
167 | Numpy random number generator, by default :obj:`None`
168 |
169 | Returns
170 | -------
171 | NDArray
172 | The undirected two-dimensional rectangular grid lattice graph in matrix representation :obj:`(num_nodes x num_nodes)`
173 | """
174 | w = width
175 | kernel = np.array([-w, -1, 1, w])
176 | adj_matrix, weights = _grid(height, width, kernel, directed, weighted, rng)
177 |
178 | adj_matrix = adj_matrix.astype(dtype=np.float32)
179 |
180 | return adj_matrix * weights if weighted else adj_matrix
181 |
182 |
183 | def simple_grid_coo(height: int,
184 | width: int,
185 | directed: bool=False,
186 | weighted: bool = False,
187 | rng: Optional[Generator] = None) -> Tuple[NDArray, Optional[NDArray]]:
188 | """
189 | Returns an undirected two-dimensional rectangular grid lattice graph.
190 |
191 | .. code-block:: python
192 |
193 | 1 -- 2 -- 3
194 | | | |
195 | 4 -- 5 -- 6
196 |
197 | Parameters
198 | ----------
199 | height : int
200 | Number of vertices in the vertical axis
201 | width : int
202 | Number of vertices in the horizontal axis
203 | directed : bool, optional
204 | If set to :obj:`True`, will return a directed graph, by default :obj:`False`
205 | weighted : bool, optional
206 | If set to :obj:`True`, will return a dense representation of the weighted graph, by default :obj:`False`
207 | rng : Optional[Generator], optional
208 | Numpy random number generator, by default :obj:`None`
209 |
210 | Returns
211 | -------
212 | NDArray
213 | The undirected two-dimensional rectangular grid lattice graph in COO representation :obj:`(num_edges x 2)`
214 | Optional[NDArray]
215 | Weights of the random graph.
216 | """
217 | w = width
218 | kernel = np.array([-w, -1, 1, w])
219 | adj_matrix, weights = _grid(height, width, kernel, directed, weighted, rng)
220 |
221 | if weighted:
222 | weights *= adj_matrix
223 |
224 | coo_matrix = np.argwhere(adj_matrix)
225 | coo_weights = np.expand_dims(weights[weights.nonzero()], -1) if weights is not None else None
226 |
227 | return coo_matrix, coo_weights
--------------------------------------------------------------------------------
/src/numgraph/temporal/_susceptible_infected.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple, Callable, Optional, Union, List
2 | from numpy.random import Generator, default_rng
3 | from scipy.sparse import coo_matrix
4 | from numpy.typing import NDArray
5 | import numpy as np
6 |
7 |
8 | def _susceptible_infected(generator: Callable,
9 | prob: float = 0.5,
10 | mask_size: float = 0.5,
11 | t_max: Optional[int] = None,
12 | infected_nodes: Union[int, float] = 0.1,
13 | num_nodes: Optional[int] = None,
14 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
15 |
16 | assert prob >= 0 and prob < 1, 'prob should be a probability in the range [0, 1)'
17 | assert mask_size >= 0 and mask_size < 1, 'mask_size should be a probability in the range [0, 1)'
18 |
19 | if rng is None:
20 | rng = default_rng()
21 |
22 | # Generate the graph
23 | edges = generator(rng)
24 | assert isinstance(edges, tuple) and edges[0].shape[1] == 2, 'The generator must return a graph in COO representation.'
25 | edges = edges[0]
26 |
27 | if num_nodes is None:
28 | num_nodes = edges.max() + 1
29 |
30 | assert (infected_nodes > 0 and infected_nodes <= 1) or infected_nodes < num_nodes, \
31 | 'infected_nodes should be a probability in the range (0, 1] or an integer < num_nodes'
32 |
33 | # Define the starting infected nodes
34 | x = np.zeros(num_nodes)
35 | infected_nodes = round(num_nodes * infected_nodes) if isinstance(infected_nodes, float) \
36 | else infected_nodes
37 | x[:infected_nodes] = 1
38 | rng.shuffle(x)
39 | infected = x > 0
40 |
41 | # Propagete the infection
42 | max_idx = round((1 - mask_size) * edges.shape[0])
43 | t_max = np.inf if t_max is None else t_max
44 | snapshots, xs = [], []
45 | t = 0
46 | while t < t_max and not all(x == 1):
47 | mask = np.arange(edges.shape[0])
48 | rng.shuffle(mask)
49 | mask = mask[:max_idx]
50 |
51 | A_t = edges[mask].T
52 | vals, row, col = np.ones(A_t.shape[1]), A_t[0], A_t[1]
53 | A_t = coo_matrix((vals, (row, col)), shape=(num_nodes, num_nodes))
54 |
55 | x = ((rng.uniform() * A_t.dot(x)) > prob).astype(int)
56 | x[infected] = 1
57 | infected = x > 0
58 |
59 | # Store info at time t
60 | snapshots.append(edges[mask])
61 | xs.append(x)
62 |
63 | t += 1
64 |
65 | return snapshots, xs
66 |
67 | def susceptible_infected_coo(generator: Callable,
68 | prob: float = 0.5,
69 | mask_size: float = 0.5,
70 | t_max: Optional[int] = None,
71 | infected_nodes: Union[int, float] = 0.1,
72 | num_nodes: Optional[int] = None,
73 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
74 | """
75 | Returns Dissemination Process Simulation (DPS). The model simulates a susceptible-infected scenario,
76 | e.g., epidemic spreading. Each node can be either infected (1) or susceptible (0). A node can
77 | change its state to infected depending on the number of infected neighbors and a fixed
78 | probability. When a node change its state, it stays infected indefinitely.
79 | The disseminaiton process is defined on discrite time, and last until each node is infected or
80 | :obj:`t_max` is reached. The simulation graph has fixed nodes and dynamic edges along the temporal axis.
81 |
82 | Parameters
83 | ----------
84 | generator : Callable
85 | A callable that takes as input a rng and generates the simulation graph
86 | prob : float, optional
87 | The probability of infection, by default :obj:`0.5`
88 | mask_size : float, optional
89 | The amount of edges to discard at each timestep, by default :obj:`0.5`
90 | t_max : int, optional
91 | The maximum number of timesteps in the simulation, by default :obj:`None`
92 | infected_nodes : Union[int, float], optional
93 | The amount of starting infected nodes, by default :obj:`0.1`
94 | num_nodes : int, optional
95 | The number of nodes in the simulation graph, by default :obj:`None`
96 | rng : Generator, optional
97 | Numpy random number generator, by default :obj:`None`
98 |
99 | Returns
100 | -------
101 | List[NDArray]
102 | The list of graph's snapshots in COO representation :obj:`(T x (snapshot_num_edges x 2))`
103 | List[NDArray]
104 | The list of nodes' states :obj:`(T x (num_nodes, ))`
105 |
106 | Note
107 | ----
108 | The weights computed by the :obj:`generator` are not used by the function
109 | """
110 | return _susceptible_infected(generator = generator,
111 | prob = prob,
112 | mask_size = mask_size,
113 | t_max = t_max,
114 | infected_nodes = infected_nodes,
115 | num_nodes = num_nodes,
116 | rng = rng)
117 |
118 |
119 |
120 | def susceptible_infected_full(generator: Callable,
121 | prob: float = 0.5,
122 | mask_size: float = 0.5,
123 | t_max: Optional[int] = None,
124 | infected_nodes: Union[int, float] = 0.1,
125 | num_nodes: Optional[int] = None,
126 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
127 | """
128 | Returns Dissemination Process Simulation (DPS). The model simulates a susceptible-infected scenario,
129 | e.g., epidemic spreading. Each node can be either infected :obj:`(1)` or susceptible :obj:`(0)`. A node can
130 | change its state to infected depending on the number of infected neighbors and a fixed
131 | probability. When a node change its state, it stays infected indefinitely.
132 | The disseminaiton process is defined on discrite time, and last until each node is infected or
133 | t_max is reached. The simulation graph has fixed nodes and dynamic edges along the temporal axis.
134 |
135 | Parameters
136 | ----------
137 | generator : Callable
138 | A callable that takes as input a rng and generates the simulation graph
139 | prob : float, optional
140 | The probability of infection, by default :obj:`0.5`
141 | mask_size : float, optional
142 | The amount of edges to discard at each timestep, by default :obj:`0.5`
143 | t_max : int, optional
144 | The maximum number of timesteps in the simulation, by default :obj:`None`
145 | infected_nodes : Union[int, float], optional
146 | The amount of starting infected nodes, by default :obj:`0.1`
147 | num_nodes : int, optional
148 | The number of nodes in the simulation graph, by default :obj:`None`
149 | rng : Generator, optional
150 | Numpy random number generator, by default :obj:`None`
151 |
152 | Returns
153 | -------
154 | List[NDArray]
155 | The list of graph's snapshots in matrix represnetation :obj:`(T x (num_nodes x num_nodes))`
156 | List[NDArray]
157 | The list of nodes' states :obj:`(T x (num_nodes, ))`
158 |
159 | Note
160 | ----
161 | The weights computed by the :obj:`generator` are not used by the function
162 |
163 | """
164 | edges_snapshots_coo, nodes_snapshots = _susceptible_infected(generator = generator,
165 | prob = prob,
166 | mask_size = mask_size,
167 | t_max = t_max,
168 | infected_nodes = infected_nodes,
169 | num_nodes = num_nodes,
170 | rng = rng)
171 | edges_snapshots = []
172 | for coo_matrix in edges_snapshots_coo:
173 | adj_matrix = np.zeros((num_nodes, num_nodes))
174 | adj_matrix[coo_matrix[:, 0], coo_matrix[:, 1]] = 1
175 | edges_snapshots.append(adj_matrix)
176 |
177 | return edges_snapshots, nodes_snapshots
178 |
--------------------------------------------------------------------------------
/src/numgraph/temporal/_euler_diffusion.py:
--------------------------------------------------------------------------------
1 | from numgraph.utils.spikes_generator import SpikeGenerator
2 | from typing import Tuple, Callable, Optional, List, Union
3 | from numpy.random import Generator, default_rng
4 | from numpy.typing import NDArray
5 | import numpy as np
6 |
7 | def _euler_graph_diffusion(generator: Callable,
8 | spike_generator: SpikeGenerator,
9 | diffusion: Optional[Callable] = None,
10 | t_max: int = 10,
11 | init_temp: Optional[Union[float, NDArray]] = None,
12 | num_nodes: Optional[int] = None,
13 | step_size: float = 0.1,
14 | return_coo: float = True,
15 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
16 |
17 | assert init_temp is None or (isinstance(init_temp, float) and init_temp > 0), f'init_temp can be None or float > 0, not {type(init_temp)} with value {init_temp}'
18 |
19 | if rng is None:
20 | rng = default_rng()
21 |
22 | # Generate the graph
23 | edges = generator(rng)
24 | assert isinstance(edges, tuple) and edges[0].shape[1] == 2, 'The generator must return a graph in COO representation.'
25 | edges, weights = edges
26 |
27 | if num_nodes is None:
28 | num_nodes = edges.max() + 1
29 |
30 | if init_temp is None:
31 | x = rng.uniform(low=0.0, high=0.2, size=(num_nodes, 1))
32 | elif isinstance(init_temp, float):
33 | x = np.full((num_nodes,1), init_temp)
34 | else:
35 | x = init_temp
36 |
37 | if diffusion is None:
38 | # Compute the Laplacian matrix
39 | adj_mat = np.zeros((num_nodes, num_nodes))
40 | adj_mat[edges[:, 0], edges[:, 1]] = 1 if weights is None else weights
41 | self_loops = np.diag(adj_mat)
42 | np.fill_diagonal(adj_mat, 0)
43 | degree = np.diag(np.sum(adj_mat, axis=1))
44 | new_degree = np.linalg.inv(np.sqrt(degree))
45 | L = np.eye(num_nodes) - new_degree @ adj_mat @ new_degree # Normalized laplacian
46 | diffusion = lambda _edges, _weights, _num_nodes, _x: -L @ _x
47 |
48 | xs = []
49 | for t in range(t_max):
50 | x = spike_generator.compute_spike(t, x)
51 |
52 | # Graph Heat Equation (Euler's method)
53 | xs.append(x)
54 | x = x + step_size * diffusion(edges, weights, num_nodes, x)
55 |
56 | if return_coo:
57 | return [(edges, weights)] * t_max, xs
58 | else:
59 | if diffusion is None:
60 | adj_mat += self_loops
61 | else:
62 | adj_mat = np.zeros((num_nodes, num_nodes))
63 | adj_mat[edges[:, 0], edges[:, 1]] = 1 if weights is None else weights
64 | return [adj_mat] * t_max, xs
65 |
66 |
67 | def euler_graph_diffusion_coo(generator: Callable,
68 | spike_generator: SpikeGenerator,
69 | diffusion: Optional[Callable] = None,
70 | t_max: int = 10,
71 | init_temp: Optional[Union[float, NDArray]] = None,
72 | step_size: float = 0.1,
73 | num_nodes: Optional[int] = None,
74 | rng: Optional[Generator] = None) -> Tuple[List[Tuple[NDArray, NDArray]], List[NDArray]]:
75 | """
76 | Returns the Euler's method approximation of a diffusion process over a graph defined
77 | by :obj:`diffusion`. Each node is characterized by the temperature.
78 | The process is defined on discrete time and last until :obj:`t_max` time is reached.
79 | The simulation graph has fixed nodes and edges along the temporal axis.
80 |
81 | Parameters
82 | ----------
83 | generator : Callable
84 | A callable that takes as input a rng and generates the simulation graph
85 | spike_generator : SpikeGenerator
86 | The spike generator, which implement the method :obj:`compute_spike(t, x)`
87 | diffusion: Callable, optional
88 | The function that implements the diffusion equation. It takes as input the graph snapshot in
89 | COO representation, the number of nodes, and nodes' states. In other words,the arguments of
90 | the diffusion function are :obj:`edges`, :obj:`weights`, :obj:`num_nodes`, :obj:`x`.
91 | If :obj:`None` it computes the standard graph heat equation, ie, -Lx(t), where L is the
92 | graph laplacian. By default :obj:`None`
93 | t_max : int, optional
94 | The maximum number of timesteps in the simulation, by default :obj:`10`
95 | init_temp : Union[float, NDArray], optional
96 | The initial temperature of the nodes. If :obj:`None` it computes a random temperature between :obj:`0.` and :obj:`0.2`, by default :obj:`None`
97 | step_size : float, optional
98 | The step size used in the Euler's method discretization, by default :obj:`0.1`
99 | num_nodes : int, optional
100 | The number of nodes in the simulation graph, by default :obj:`None`
101 | rng : Generator, optional
102 | Numpy random number generator, by default :obj:`None`
103 |
104 | Returns
105 | -------
106 | List[Tuple[NDArray, NDArray]]
107 | The list of graph's snapshots :obj:`(T x 2)`: each snapshot is a tuple containing the graph
108 | in COO representation :obj:`(snapshot_num_edges x 2)` and the weights :obj:`(snapshot_num_edges x 1)`
109 | List[NDArray]
110 | The list of nodes' states :obj:`(T x (snapshot_num_nodes, ))`
111 | """
112 | return _euler_graph_diffusion(generator = generator,
113 | spike_generator = spike_generator,
114 | diffusion = diffusion,
115 | t_max = t_max,
116 | init_temp = init_temp,
117 | num_nodes = num_nodes,
118 | step_size = step_size,
119 | return_coo = True,
120 | rng = rng)
121 |
122 |
123 | def euler_graph_diffusion_full(generator: Callable,
124 | spike_generator: SpikeGenerator,
125 | diffusion: Optional[Callable] = None,
126 | t_max: int = 10,
127 | init_temp: Optional[Union[float, NDArray]] = None,
128 | step_size: float = 0.1,
129 | num_nodes: Optional[int] = None,
130 | rng: Optional[Generator] = None) -> Tuple[List[NDArray], List[NDArray]]:
131 | """
132 | Returns the Euler's method approximation of a diffusion process over a graph defined
133 | by :obj:`diffusion`. Each node is characterized by the temperature.
134 | The process is defined on discrete time and last until :obj:`t_max` time is reached.
135 | The simulation graph has fixed nodes and edges along the temporal axis.
136 |
137 | Parameters
138 | ----------
139 | generator : Callable
140 | A callable that takes as input a rng and generates the simulation graph
141 | spike_generator : SpikeGenerator
142 | The spike generator, which implement the method :obj:`compute_spike(t, x)`
143 | diffusion: Callable, optional
144 | The function that implements the diffusion equation. It takes as input the graph snapshot in
145 | COO representation, the number of nodes, and nodes' states. In other words,the arguments of
146 | the diffusion function are :obj:`edges`, :obj:`weights`, :obj:`num_nodes`, :obj:`x`.
147 | If :obj:`None` it computes the standard graph heat equation, ie, -Lx(t), where L is the
148 | graph laplacian. By default :obj:`None`
149 | t_max : int, optional
150 | The maximum number of timesteps in the simulation, by default :obj:`10`
151 | init_temp : Union[float, NDArray], optional
152 | The initial temperature of the nodes. If :obj:`None` it computes a random temperature between :obj:`0.` and :obj:`0.2`, by default :obj:`None`
153 | step_size : float, optional
154 | The step size used in the Euler's method discretization, by default :obj:`0.1`
155 | num_nodes : int, optional
156 | The number of nodes in the simulation graph, by default :obj:`None`
157 | rng : Generator, optional
158 | Numpy random number generator, by default :obj:`None`
159 |
160 | Returns
161 | -------
162 | List[NDArray]
163 | the list of graph's snapshots in matrix representation :obj:`(T x (num_nodes x num_nodes))`
164 | List[NDArray]
165 | the list of nodes' states :obj:`(T x (num_nodes, ))`
166 | """
167 | return _euler_graph_diffusion(generator = generator,
168 | spike_generator = spike_generator,
169 | diffusion = diffusion,
170 | t_max = t_max,
171 | init_temp = init_temp,
172 | num_nodes = num_nodes,
173 | step_size = step_size,
174 | return_coo = False,
175 | rng = rng)
176 |
--------------------------------------------------------------------------------
/src/numgraph/utils/spikes_generator.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple, Optional
2 | from numpy.typing import NDArray
3 | from numpy.random import Generator, default_rng
4 | import numpy as np
5 |
6 |
7 | class SpikeGenerator:
8 | """
9 | The generator of the spikes in the heat diffusion over a graph. To each spike is associated an
10 | increase of temperature of the node. This class identify at each step the node with injected
11 | temperature and the new temperature.
12 |
13 | Parameters
14 | ----------
15 | t_max : int, optional
16 | The maximum number of timesteps in the simulation, by default :obj:`10`
17 | heat_spike : Tuple[float, float], optional
18 | A tuple containing the min and max temperature of a spike, by default :obj:`(0.7, 2.)`
19 | num_spikes : int, optional
20 | The number of heat spikes during the process, by default :obj:`1`
21 | rng : Generator, optional
22 | Numpy random number generator, by default :obj:`None`
23 | """
24 | def __init__(self,
25 | t_max: int = 10,
26 | heat_spike: Tuple[float, float] = (0.7, 2.),
27 | num_spikes: int = 1,
28 | rng: Optional[Generator] = None) -> None:
29 | """
30 | The generator of the spikes in the heat diffusion over a graph. To each spike is associated an
31 | increase of temperature of the node. This class identify at each step the node with injected
32 | temperature and the new temperature.
33 |
34 | Parameters
35 | ----------
36 | t_max : int, optional
37 | The maximum number of timesteps in the simulation, by default :obj:`10`
38 | heat_spike : Tuple[float, float], optional
39 | A tuple containing the min and max temperature of a spike, by default :obj:`(0.7, 2.)`
40 | num_spikes : int, optional
41 | The number of heat spikes during the process, by default :obj:`1`
42 | rng : Generator, optional
43 | Numpy random number generator, by default :obj:`None`
44 | """
45 | assert ((isinstance(heat_spike, tuple) and
46 | isinstance(heat_spike[0], float) and
47 | isinstance(heat_spike[0], float)),
48 | f'heat_spike can be Tuple[float, float], not {heat_spike}')
49 |
50 | assert num_spikes > 0 and num_spikes < t_max, 'num_spike must be in the range (0, t_max)'
51 |
52 | self.t_max = t_max
53 | self.heat_spike = heat_spike
54 | self.num_spikes = num_spikes
55 | if rng is None:
56 | self.rng = default_rng()
57 | else:
58 | self.rng = rng
59 |
60 | self.spike_timesteps = set([0])
61 | if num_spikes > 1:
62 | tmp = np.arange(1, t_max)
63 | self.rng.shuffle(tmp)
64 | self.spike_timesteps |= set(tmp[:self.num_spikes])
65 |
66 | def compute_spike(self, t: int, x: NDArray):
67 | """
68 | Computes the evolution of node temperature given a timestep :obj:`t`
69 |
70 | Parameters
71 | ----------
72 | t : int
73 | The timesteps
74 | x : NDArray
75 | The vector of node temperatures of shape :obj:`(num_nodes x 1)`
76 |
77 | Returns
78 | -------
79 | NDArray
80 | The vector of new node temperatures of shape :obj:`(num_nodes x 1)`
81 | """
82 | raise NotImplementedError()
83 |
84 |
85 | class HeatSpikeGenerator(SpikeGenerator):
86 | """
87 | The generator of the spikes in the heat diffusion over a graph. To each spike is associated an
88 | increase of temperature of the node. This class identify at each step the node with injected
89 | temperature and the new temperature.
90 |
91 | Parameters
92 | ----------
93 | t_max : int, optional
94 | The maximum number of timesteps in the simulation, by default :obj:`10`
95 | heat_spike : Tuple[float, float], optional
96 | A tuple containing the min and max temperature of a spike, by default :obj:`(0.7, 2.)`
97 | num_spikes : int, optional
98 | The number of heat spikes during the process, by default :obj:`1`
99 | rng : Generator, optional
100 | Numpy random number generator, by default :obj:`None`
101 | """
102 | def __init__(self,
103 | t_max: int = 10,
104 | heat_spike: Tuple[float, float] = (0.7, 2.),
105 | num_spikes: int = 1,
106 | rng: Optional[Generator] = None) -> None:
107 | """
108 | The generator of the spikes in the heat diffusion over a graph. To each spike is associated an
109 | increase of temperature of the node. This class identify at each step the node with injected
110 | temperature and the new temperature.
111 |
112 | Parameters
113 | ----------
114 | t_max : int, optional
115 | The maximum number of timesteps in the simulation, by default :obj:`10`
116 | heat_spike : Tuple[float, float], optional
117 | A tuple containing the min and max temperature of a spike, by default :obj:`(0.7, 2.)`
118 | num_spikes : int, optional
119 | The number of heat spikes during the process, by default :obj:`1`
120 | rng : Generator, optional
121 | Numpy random number generator, by default :obj:`None`
122 | """
123 | assert heat_spike[0] > 0, 'The minimum temperature of a spike must be greater than 0'
124 | super().__init__(t_max, heat_spike, num_spikes, rng)
125 |
126 | def compute_spike(self, t: int, x: NDArray):
127 | """
128 | Computes the evolution of node temperature given a timestep :obj:`t`
129 |
130 | Parameters
131 | ----------
132 | t : int
133 | The timesteps
134 | x : NDArray
135 | The vector of node temperatures of shape :obj:`(num_nodes x 1)`
136 |
137 | Returns
138 | -------
139 | NDArray
140 | The vector of new node temperatures of shape :obj:`(num_nodes x 1)`
141 | """
142 | if t in self.spike_timesteps:
143 | # Improve heat of a random node
144 | i = self.rng.integers(x.shape[0])
145 | x[i,0] = self.rng.uniform(low=self.heat_spike[0],
146 | high=self.heat_spike[1],
147 | size=(1, 1))
148 | return x
149 |
150 | class ColdHeatSpikeGenerator(SpikeGenerator):
151 | """
152 | The generator of the spikes in the heat diffusion over a graph. To each spike is associated an
153 | increase (or decrease) of temperature of the node. This class identify at each step the node with injected
154 | temperature and the new temperature.
155 |
156 | Parameters
157 | ----------
158 | t_max : int, optional
159 | The maximum number of timesteps in the simulation, by default :obj:`10`
160 | heat_spike : Tuple[float, float], optional
161 | A tuple containing the min and max temperature of a hot spike, by default :obj:`(0.7, 2.)`
162 | cold_spike : Tuple[float, float], optional
163 | A tuple containing the min and max temperature of a cold spike, by default :obj:`(-1., 0.)`
164 | prob_cold_spike : float, optional
165 | The probability of a cold spike to happen, by default :obj:`0.2`
166 | num_spikes : int, optional
167 | The number of heat spikes during the process, by default :obj:`1`
168 | rng : Generator, optional
169 | Numpy random number generator, by default :obj:`None`
170 | """
171 |
172 | def __init__(self,
173 | t_max: int = 10,
174 | heat_spike: Tuple[float, float] = (0.7, 2.),
175 | cold_spike: Tuple[float, float] = (-1., 0.),
176 | prob_cold_spike: float = 0.2,
177 | num_spikes: int = 2,
178 | rng: Optional[Generator] = None) -> None:
179 | """
180 | The generator of the spikes in the heat diffusion over a graph. To each spike is associated an
181 | increase (or decrease) of temperature of the node. This class identify at each step the node with injected
182 | temperature and the new temperature.
183 |
184 | Parameters
185 | ----------
186 | t_max : int, optional
187 | The maximum number of timesteps in the simulation, by default :obj:`10`
188 | heat_spike : Tuple[float, float], optional
189 | A tuple containing the min and max temperature of a hot spike, by default :obj:`(0.7, 2.)`
190 | cold_spike : Tuple[float, float], optional
191 | A tuple containing the min and max temperature of a cold spike, by default :obj:`(-1., 0.)`
192 | prob_cold_spike : float, optional
193 | The probability of a cold spike to happen, by default :obj:`0.2`
194 | num_spikes : int, optional
195 | The number of heat spikes during the process, by default :obj:`1`
196 | rng : Generator, optional
197 | Numpy random number generator, by default :obj:`None`
198 | """
199 | super().__init__(t_max, heat_spike, num_spikes, rng)
200 | assert prob_cold_spike >= 0 and prob_cold_spike < 1, 'prob_cold_spike is a probability in the range [0, 1)'
201 |
202 | self.cold_spike = cold_spike
203 | self.prob_cold_spike = prob_cold_spike
204 |
205 | def compute_spike(self, t: int, x: NDArray):
206 | """
207 | Computes the evolution of node temperature given a timestep :obj:`t`
208 |
209 | Parameters
210 | ----------
211 | t : int
212 | The timesteps
213 | x : NDArray
214 | The vector of node temperatures of shape :obj:`(num_nodes x 1)`
215 |
216 | Returns
217 | -------
218 | NDArray
219 | The vector of new node temperatures of shape :obj:`(num_nodes x 1)`
220 | """
221 | if t in self.spike_timesteps:
222 | # Improve heat of a random node
223 | i = self.rng.integers(x.shape[0])
224 | if self.rng.uniform(low=0, high=1) < self.prob_cold_spike:
225 | x[i,0] = self.rng.uniform(low=self.cold_spike[0],
226 | high=self.cold_spike[1],
227 | size=(1, 1))
228 | else:
229 | x[i,0] = self.rng.uniform(low=self.heat_spike[0],
230 | high=self.heat_spike[1],
231 | size=(1, 1))
232 | return x
--------------------------------------------------------------------------------
/test/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from numgraph.distributions import *
2 | from ._plot import plot_ba, plot_clique, plot_er, plot_grid, plot_sbm, plot_star, plot_tree, plot_tree_on_terminal
3 | from ._dynamic_plot import DynamicHeatGraph, DynamicHeatmap, DynamicNodeSignal
4 |
5 | def get_directional_matrices(directed, N, p, block_size, probs, h, w, generator, rng, coo=False):
6 | if coo:
7 | m = [
8 | [N, erdos_renyi_coo(num_nodes=N, prob=p, directed=directed, weighted=True, rng=rng), f"erdos_renyi_coo(num_nodes={N}, prob={p}, directed={directed}, weighted=True, rng=rng)"],
9 | [N, erdos_renyi_coo(num_nodes=N, prob=p, directed=directed, weighted=False, rng=rng), f"erdos_renyi_coo(num_nodes={N}, prob={p}, directed={directed}, weighted=False, rng=rng)"],
10 | [N, star_coo(num_nodes=N, directed=directed, weighted=False, rng=rng), f"star_coo(num_nodes={N}, directed={directed}, weighted=False, rng=rng)"],
11 | [N, star_coo(num_nodes=N, directed=directed, weighted=True, rng=rng), f"star_coo(num_nodes={N}, directed={directed}, weighted=True, rng=rng)"],
12 | [sum(block_size), stochastic_block_model_coo(block_size, probs, generator, directed=directed, weighted=False, rng=rng), f"stochastic_block_model_coo(block_size={block_size}, probs={probs}, generator, directed={directed}, weighted=False, rng=rng)"],
13 | [sum(block_size), stochastic_block_model_coo(block_size, probs, generator, directed=directed, weighted=True, rng=rng), f"stochastic_block_model_coo(block_size={block_size}, probs={probs}, generator, directed={directed}, weighted=True, rng=rng)"],
14 | [N, random_tree_coo(num_nodes=N, directed=directed, weighted=False, rng=rng), f"random_tree_coo(num_nodes={N}, directed={directed}, weighted=False, rng=rng)"],
15 | [N, random_tree_coo(num_nodes=N, directed=directed, weighted=True, rng=rng), f"random_tree_coo(num_nodes={N}, directed={directed}, weighted=True, rng=rng)"]
16 | ]
17 | if not directed:
18 | m += [
19 | [N, clique_coo(num_nodes=N, weighted=False, rng=rng), f"clique_coo(num_nodes={N}, weighted=False, rng=rng)"],
20 | [N, clique_coo(num_nodes=N, weighted=True, rng=rng), f"clique_coo(num_nodes={N}, weighted=True, rng=rng)"],
21 | [w*h, grid_coo(height=h, width=w, weighted=False, rng=rng), f"grid_coo(height={h}, width={w}, weighted=False, rng=rng)"],
22 | [w*h, grid_coo(height=h, width=w, weighted=True, rng=rng), f"grid_coo(height={h}, width={w}, weighted=True, rng=rng)"],
23 | [w*h, simple_grid_coo(height=h, width=w, weighted=False, rng=rng), f"simple_grid_coo(height={h}, width={w}, weighted=False, rng=rng)"],
24 | [w*h, simple_grid_coo(height=h, width=w, weighted=True, rng=rng), f"simple_grid_coo(height={h}, width={w}, weighted=True, rng=rng)"],
25 | [N, barabasi_albert_coo(num_nodes=N, num_edges=int(N/2), weighted=False, rng=rng), f"barabasi_albert_coo(num_nodes={N}, num_edges={int(N/2)}, weighted=False, rng=rng)"],
26 | [N, barabasi_albert_coo(num_nodes=N, num_edges=int(N/2), weighted=True, rng=rng), f"barabasi_albert_coo(num_nodes={N}, num_edges={int(N/2)}, weighted=True, rng=rng)"]
27 | ]
28 | else:
29 | m = [
30 | [N, erdos_renyi_full(num_nodes=N, prob=p, directed=directed, weighted=True, rng=rng), f"erdos_renyi_full(num_nodes={N}, prob={p}, directed={directed}, weighted=True, rng=rng)"],
31 | [N, erdos_renyi_full(num_nodes=N, prob=p, directed=directed, weighted=False, rng=rng), f"erdos_renyi_full(num_nodes={N}, prob={p}, directed={directed}, weighted=False, rng=rng)"],
32 | [N, star_full(num_nodes=N, directed=directed, weighted=False, rng=rng), f"star_full(num_nodes={N}, directed={directed}, weighted=False, rng=rng)"],
33 | [N, star_full(num_nodes=N, directed=directed, weighted=True, rng=rng), f"star_full(num_nodes={N}, directed={directed}, weighted=True, rng=rng)"],
34 | [sum(block_size), stochastic_block_model_full(block_size, probs, generator, directed=directed, weighted=False, rng=rng), f"stochastic_block_model_full(block_size={block_size}, probs={probs}, generator, directed={directed}, weighted=False, rng=rng)"],
35 | [sum(block_size), stochastic_block_model_full(block_size, probs, generator, directed=directed, weighted=True, rng=rng), f"stochastic_block_model_full(block_size={block_size}, probs={probs}, generator, directed={directed}, weighted=True, rng=rng)"],
36 | [N, random_tree_full(num_nodes=N, directed=directed, weighted=False, rng=rng), f"random_tree_full(num_nodes={N}, directed={directed}, weighted=False, rng=rng)"],
37 | [N, random_tree_full(num_nodes=N, directed=directed, weighted=True, rng=rng), f"random_tree_full(num_nodes={N}, directed={directed}, weighted=True, rng=rng)"]
38 | ]
39 |
40 | if not directed:
41 | m += [
42 | [N, clique_full(num_nodes=N, weighted=False, rng=rng), f"clique_full(num_nodes={N}, weighted=False, rng=rng)"],
43 | [N, clique_full(num_nodes=N, weighted=True, rng=rng), f"clique_full(num_nodes={N}, weighted=True, rng=rng)"],
44 | [w*h, grid_full(height=h, width=w, weighted=False, rng=rng), f"grid_full(height={h}, width={w}, weighted=False, rng=rng)"],
45 | [w*h, grid_full(height=h, width=w, weighted=True, rng=rng), f"grid_full(height={h}, width={w}, weighted=True, rng=rng)"],
46 | [w*h, simple_grid_full(height=h, width=w, weighted=False, rng=rng), f"simple_grid_full(height={h}, width={w}, weighted=False, rng=rng)"],
47 | [w*h, simple_grid_full(height=h, width=w, weighted=True, rng=rng), f"simple_grid_full(height={h}, width={w}, weighted=True, rng=rng)"],
48 | [N, barabasi_albert_full(num_nodes=N, num_edges=int(N/2), weighted=False, rng=rng), f"barabasi_albert_full(num_nodes={N}, num_edges={int(N/2)}, weighted=False, rng=rng)"],
49 | [N, barabasi_albert_full(num_nodes=N, num_edges=int(N/2), weighted=True, rng=rng), f"barabasi_albert_full(num_nodes={N}, num_edges={int(N/2)}, weighted=True, rng=rng)"]
50 | ]
51 | return m
52 |
53 | def get_weighted_matrices(N, p, block_size, probs, h, w, generator, rng, coo=False):
54 | if coo:
55 | return [
56 | [N, erdos_renyi_coo(num_nodes=N, prob=p, directed=True, weighted=True, rng=rng), f"erdos_renyi_coo(num_nodes={N}, prob={p}, directed=True, weighted=True, rng=rng)"],
57 | [N, erdos_renyi_coo(num_nodes=N, prob=p, directed=False, weighted=True, rng=rng), f"erdos_renyi_coo(num_nodes={N}, prob={p}, directed=False, weighted=True, rng=rng)"],
58 | [N, clique_coo(num_nodes=N, weighted=True, rng=rng), f"clique_coo(num_nodes={N}, weighted=True, rng=rng)"],
59 | [w*h, grid_coo(height=h, width=w, weighted=True, rng=rng), f"grid_coo(height={h}, width={w}, weighted=True, rng=rng)"],
60 | [w*h, simple_grid_coo(height=h, width=w, weighted=True, rng=rng), f"simple_grid_coo(height={h}, width={w}, weighted=True, rng=rng)"],
61 | [N, star_coo(num_nodes=N, directed=True, weighted=True, rng=rng), f"star_coo(num_nodes={N}, directed=True, weighted=True, rng=rng)"],
62 | [N, star_coo(num_nodes=N, directed=False, weighted=True, rng=rng), f"star_coo(num_nodes={N}, directed=False, weighted=True, rng=rng)"],
63 | [sum(block_size), stochastic_block_model_coo(block_size, probs, generator, directed=True, weighted=True, rng=rng), f"stochastic_block_model_coo(block_size={block_size}, probs={probs}, generator, directed=True, weighted=True, rng=rng)"],
64 | [sum(block_size), stochastic_block_model_coo(block_size, probs, generator, directed=False, weighted=True, rng=rng), f"stochastic_block_model_coo(block_size={block_size}, probs={probs}, generator, directed=False, weighted=True, rng=rng)"],
65 | [N, barabasi_albert_coo(num_nodes=N, num_edges=int(N/2), weighted=True, rng=rng), f"barabasi_albert_coo(num_nodes={N}, num_edges={int(N/2)}, weighted=True, rng=rng)"],
66 | [N, random_tree_coo(num_nodes=N, directed=True, weighted=True, rng=rng), f"random_tree_coo(num_nodes={N}, directed=True, weighted=True, rng=rng)"],
67 | [N, random_tree_coo(num_nodes=N, directed=False, weighted=True, rng=rng), f"random_tree_coo(num_nodes={N}, directed=False, weighted=True, rng=rng)"]
68 | ]
69 | else:
70 | return [
71 | [N, erdos_renyi_full(num_nodes=N, prob=p, directed=True, weighted=True, rng=rng), f"erdos_renyi_full(num_nodes={N}, prob={p}, directed=True, weighted=True, rng=rng)"],
72 | [N, erdos_renyi_full(num_nodes=N, prob=p, directed=False, weighted=True, rng=rng), f"erdos_renyi_full(num_nodes={N}, prob={p}, directed=False, weighted=True, rng=rng)"],
73 | [N, clique_full(num_nodes=N, weighted=True, rng=rng), f"clique_full(num_nodes={N}, weighted=True, rng=rng)"],
74 | [w*h, grid_full(height=h, width=w, weighted=True, rng=rng), f"grid_full(height={h}, width={w}, weighted=True, rng=rng)"],
75 | [w*h, simple_grid_full(height=h, width=w, weighted=True, rng=rng), f"simple_grid_full(height={h}, width={w}, weighted=True, rng=rng)"],
76 | [N, star_full(num_nodes=N, directed=True, weighted=True, rng=rng), f"star_full(num_nodes={N}, directed=True, weighted=True, rng=rng)"],
77 | [N, star_full(num_nodes=N, directed=False, weighted=True, rng=rng), f"star_full(num_nodes={N}, directed=False, weighted=True, rng=rng)"],
78 | [sum(block_size), stochastic_block_model_full(block_size, probs, generator, directed=True, weighted=True, rng=rng), f"stochastic_block_model_full(block_size={block_size}, probs={probs}, generator, directed=True, weighted=True, rng=rng)"],
79 | [sum(block_size), stochastic_block_model_full(block_size, probs, generator, directed=False, weighted=True, rng=rng), f"stochastic_block_model_full(block_size={block_size}, probs={probs}, generator, directed=False, weighted=True, rng=rng)"],
80 | [N, barabasi_albert_full(num_nodes=N, num_edges=int(N/2), weighted=True, rng=rng), f"barabasi_albert_full(num_nodes={N}, num_edges={int(N/2)}, weighted=True, rng=rng)"],
81 | [N, random_tree_full(num_nodes=N, directed=True, weighted=True, rng=rng), f"random_tree_full(num_nodes={N}, directed=True, weighted=True, rng=rng)"],
82 | [N, random_tree_full(num_nodes=N, directed=False, weighted=True, rng=rng), f"random_tree_full(num_nodes={N}, directed=False, weighted=True, rng=rng)"]
83 | ]
84 |
85 | def get_all_matrices(N, p, block_size, probs, h, w, generator, rng, coo=False):
86 |
87 | return (get_directional_matrices(False, N, p, block_size, probs, h, w, generator, rng, coo=coo) +
88 | get_directional_matrices(True, N, p, block_size, probs, h, w, generator, rng, coo=coo) +
89 | get_weighted_matrices(N, p, block_size, probs, h, w, generator, rng, coo=coo))
90 |
91 |
--------------------------------------------------------------------------------
/docs/source/_static/img/NumGraph_favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
223 |
--------------------------------------------------------------------------------
/docs/source/_static/img/NumGraph_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------