├── 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 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 52 | 56 | 60 | 61 | 63 | 67 | div.MathJax_SVG_Display { position: static; } 69 | span.MathJax_SVG { position: static !important; } 70 | .MathJax_Hover_Frame {border-radius: .25em; -webkit-border-radius: .25em; -moz-border-radius: .25em; -khtml-border-radius: .25em; box-shadow: 0px 0px 15px #83A; -webkit-box-shadow: 0px 0px 15px #83A; -moz-box-shadow: 0px 0px 15px #83A; -khtml-box-shadow: 0px 0px 15px #83A; border: 1px solid #A6D ! important; display: inline-block; position: absolute} 72 | .MathJax_Menu_Button .MathJax_Hover_Arrow {position: absolute; cursor: pointer; display: inline-block; border: 2px solid #AAA; border-radius: 4px; -webkit-border-radius: 4px; -moz-border-radius: 4px; -khtml-border-radius: 4px; font-family: 'Courier New',Courier; font-size: 9px; color: #F0F0F0} 73 | .MathJax_Menu_Button .MathJax_Hover_Arrow span {display: block; background-color: #AAA; border: 1px solid; border-radius: 3px; line-height: 0; padding: 4px} 74 | .MathJax_Hover_Arrow:hover {color: white!important; border: 2px solid #CCC!important} 75 | .MathJax_Hover_Arrow:hover span {background-color: #CCC!important} 76 | 77 | #MathJax_About {position: fixed; left: 50%; width: auto; text-align: center; border: 3px outset; padding: 1em 2em; background-color: #DDDDDD; color: black; cursor: default; font-family: message-box; font-size: 120%; font-style: normal; text-indent: 0; text-transform: none; line-height: normal; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; z-index: 201; border-radius: 15px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -khtml-border-radius: 15px; box-shadow: 0px 10px 20px #808080; -webkit-box-shadow: 0px 10px 20px #808080; -moz-box-shadow: 0px 10px 20px #808080; -khtml-box-shadow: 0px 10px 20px #808080; filter: progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray', Positive='true')} 79 | #MathJax_About.MathJax_MousePost {outline: none} 80 | .MathJax_Menu {position: absolute; background-color: white; color: black; width: auto; padding: 5px 0px; border: 1px solid #CCCCCC; margin: 0; cursor: default; font: menu; text-align: left; text-indent: 0; text-transform: none; line-height: normal; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; z-index: 201; border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; -khtml-border-radius: 5px; box-shadow: 0px 10px 20px #808080; -webkit-box-shadow: 0px 10px 20px #808080; -moz-box-shadow: 0px 10px 20px #808080; -khtml-box-shadow: 0px 10px 20px #808080; filter: progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray', Positive='true')} 81 | .MathJax_MenuItem {padding: 1px 2em; background: transparent} 82 | .MathJax_MenuArrow {position: absolute; right: .5em; padding-top: .25em; color: #666666; font-size: .75em} 83 | .MathJax_MenuActive .MathJax_MenuArrow {color: white} 84 | .MathJax_MenuArrow.RTL {left: .5em; right: auto} 85 | .MathJax_MenuCheck {position: absolute; left: .7em} 86 | .MathJax_MenuCheck.RTL {right: .7em; left: auto} 87 | .MathJax_MenuRadioCheck {position: absolute; left: .7em} 88 | .MathJax_MenuRadioCheck.RTL {right: .7em; left: auto} 89 | .MathJax_MenuLabel {padding: 1px 2em 3px 1.33em; font-style: italic} 90 | .MathJax_MenuRule {border-top: 1px solid #DDDDDD; margin: 4px 3px} 91 | .MathJax_MenuDisabled {color: GrayText} 92 | .MathJax_MenuActive {background-color: #606872; color: white} 93 | .MathJax_MenuDisabled:focus, .MathJax_MenuLabel:focus {background-color: #E8E8E8} 94 | .MathJax_ContextMenu:focus {outline: none} 95 | .MathJax_ContextMenu .MathJax_MenuItem:focus {outline: none} 96 | #MathJax_AboutClose {top: .2em; right: .2em} 97 | .MathJax_Menu .MathJax_MenuClose {top: -10px; left: -10px} 98 | .MathJax_MenuClose {position: absolute; cursor: pointer; display: inline-block; border: 2px solid #AAA; border-radius: 18px; -webkit-border-radius: 18px; -moz-border-radius: 18px; -khtml-border-radius: 18px; font-family: 'Courier New',Courier; font-size: 24px; color: #F0F0F0} 99 | .MathJax_MenuClose span {display: block; background-color: #AAA; border: 1.5px solid; border-radius: 18px; -webkit-border-radius: 18px; -moz-border-radius: 18px; -khtml-border-radius: 18px; line-height: 0; padding: 8px 0 6px} 100 | .MathJax_MenuClose:hover {color: white!important; border: 2px solid #CCC!important} 101 | .MathJax_MenuClose:hover span {background-color: #CCC!important} 102 | .MathJax_MenuClose:hover:focus {outline: none} 103 | 104 | .MathJax_Preview .MJXf-math {color: inherit!important} 106 | 107 | #MathJax_Zoom {position: absolute; background-color: #F0F0F0; overflow: auto; display: block; z-index: 301; padding: .5em; border: 1px solid black; margin: 0; font-weight: normal; font-style: normal; text-align: left; text-indent: 0; text-transform: none; line-height: normal; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; box-shadow: 5px 5px 15px #AAAAAA; -webkit-box-shadow: 5px 5px 15px #AAAAAA; -moz-box-shadow: 5px 5px 15px #AAAAAA; -khtml-box-shadow: 5px 5px 15px #AAAAAA; filter: progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray', Positive='true')} 109 | #MathJax_ZoomOverlay {position: absolute; left: 0; top: 0; z-index: 300; display: inline-block; width: 100%; height: 100%; border: 0; padding: 0; margin: 0; background-color: white; opacity: 0; filter: alpha(opacity=0)} 110 | #MathJax_ZoomFrame {position: relative; display: inline-block; height: 0; width: 0} 111 | #MathJax_ZoomEventTrap {position: absolute; left: 0; top: 0; z-index: 302; display: inline-block; border: 0; padding: 0; margin: 0; background-color: white; opacity: 0; filter: alpha(opacity=0)} 112 | 113 | .MathJax_Preview {color: #888; display: contents} 115 | #MathJax_Message {position: fixed; left: 1em; bottom: 1.5em; background-color: #E6E6E6; border: 1px solid #959595; margin: 0px; padding: 2px 8px; z-index: 102; color: black; font-size: 80%; width: auto; white-space: nowrap} 116 | #MathJax_MSIE_Frame {position: absolute; top: 0; left: 0; width: 0px; z-index: 101; border: 0px; margin: 0px; padding: 0px} 117 | .MathJax_Error {color: #CC0000; font-style: italic} 118 | 119 | .MJXp-script {font-size: .8em} 121 | .MJXp-right {-webkit-transform-origin: right; -moz-transform-origin: right; -ms-transform-origin: right; -o-transform-origin: right; transform-origin: right} 122 | .MJXp-bold {font-weight: bold} 123 | .MJXp-italic {font-style: italic} 124 | .MJXp-scr {font-family: MathJax_Script,'Times New Roman',Times,STIXGeneral,serif} 125 | .MJXp-frak {font-family: MathJax_Fraktur,'Times New Roman',Times,STIXGeneral,serif} 126 | .MJXp-sf {font-family: MathJax_SansSerif,'Times New Roman',Times,STIXGeneral,serif} 127 | .MJXp-cal {font-family: MathJax_Caligraphic,'Times New Roman',Times,STIXGeneral,serif} 128 | .MJXp-mono {font-family: MathJax_Typewriter,'Times New Roman',Times,STIXGeneral,serif} 129 | .MJXp-largeop {font-size: 150%} 130 | .MJXp-largeop.MJXp-int {vertical-align: -.2em} 131 | .MJXp-math {display: inline-block; line-height: 1.2; text-indent: 0; font-family: 'Times New Roman',Times,STIXGeneral,serif; white-space: nowrap; border-collapse: collapse} 132 | .MJXp-display {display: block; text-align: center; margin: 1em 0} 133 | .MJXp-math span {display: inline-block} 134 | .MJXp-box {display: block!important; text-align: center} 135 | .MJXp-box:after {content: " "} 136 | .MJXp-rule {display: block!important; margin-top: .1em} 137 | .MJXp-char {display: block!important} 138 | .MJXp-mo {margin: 0 .15em} 139 | .MJXp-mfrac {margin: 0 .125em; vertical-align: .25em} 140 | .MJXp-denom {display: inline-table!important; width: 100%} 141 | .MJXp-denom > * {display: table-row!important} 142 | .MJXp-surd {vertical-align: top} 143 | .MJXp-surd > * {display: block!important} 144 | .MJXp-script-box > * {display: table!important; height: 50%} 145 | .MJXp-script-box > * > * {display: table-cell!important; vertical-align: top} 146 | .MJXp-script-box > *:last-child > * {vertical-align: bottom} 147 | .MJXp-script-box > * > * > * {display: block!important} 148 | .MJXp-mphantom {visibility: hidden} 149 | .MJXp-munderover, .MJXp-munder {display: inline-table!important} 150 | .MJXp-over {display: inline-block!important; text-align: center} 151 | .MJXp-over > * {display: block!important} 152 | .MJXp-munderover > *, .MJXp-munder > * {display: table-row!important} 153 | .MJXp-mtable {vertical-align: .25em; margin: 0 .125em} 154 | .MJXp-mtable > * {display: inline-table!important; vertical-align: middle} 155 | .MJXp-mtr {display: table-row!important} 156 | .MJXp-mtd {display: table-cell!important; text-align: center; padding: .5em 0 0 .5em} 157 | .MJXp-mtr > .MJXp-mtd:first-child {padding-left: 0} 158 | .MJXp-mtr:first-child > .MJXp-mtd {padding-top: 0} 159 | .MJXp-mlabeledtr {display: table-row!important} 160 | .MJXp-mlabeledtr > .MJXp-mtd:first-child {padding-left: 0} 161 | .MJXp-mlabeledtr:first-child > .MJXp-mtd {padding-top: 0} 162 | .MJXp-merror {background-color: #FFFF88; color: #CC0000; border: 1px solid #CC0000; padding: 1px 3px; font-style: normal; font-size: 90%} 163 | .MJXp-scale0 {-webkit-transform: scaleX(.0); -moz-transform: scaleX(.0); -ms-transform: scaleX(.0); -o-transform: scaleX(.0); transform: scaleX(.0)} 164 | .MJXp-scale1 {-webkit-transform: scaleX(.1); -moz-transform: scaleX(.1); -ms-transform: scaleX(.1); -o-transform: scaleX(.1); transform: scaleX(.1)} 165 | .MJXp-scale2 {-webkit-transform: scaleX(.2); -moz-transform: scaleX(.2); -ms-transform: scaleX(.2); -o-transform: scaleX(.2); transform: scaleX(.2)} 166 | .MJXp-scale3 {-webkit-transform: scaleX(.3); -moz-transform: scaleX(.3); -ms-transform: scaleX(.3); -o-transform: scaleX(.3); transform: scaleX(.3)} 167 | .MJXp-scale4 {-webkit-transform: scaleX(.4); -moz-transform: scaleX(.4); -ms-transform: scaleX(.4); -o-transform: scaleX(.4); transform: scaleX(.4)} 168 | .MJXp-scale5 {-webkit-transform: scaleX(.5); -moz-transform: scaleX(.5); -ms-transform: scaleX(.5); -o-transform: scaleX(.5); transform: scaleX(.5)} 169 | .MJXp-scale6 {-webkit-transform: scaleX(.6); -moz-transform: scaleX(.6); -ms-transform: scaleX(.6); -o-transform: scaleX(.6); transform: scaleX(.6)} 170 | .MJXp-scale7 {-webkit-transform: scaleX(.7); -moz-transform: scaleX(.7); -ms-transform: scaleX(.7); -o-transform: scaleX(.7); transform: scaleX(.7)} 171 | .MJXp-scale8 {-webkit-transform: scaleX(.8); -moz-transform: scaleX(.8); -ms-transform: scaleX(.8); -o-transform: scaleX(.8); transform: scaleX(.8)} 172 | .MJXp-scale9 {-webkit-transform: scaleX(.9); -moz-transform: scaleX(.9); -ms-transform: scaleX(.9); -o-transform: scaleX(.9); transform: scaleX(.9)} 173 | .MathJax_PHTML .noError {vertical-align: ; font-size: 90%; text-align: left; color: black; padding: 1px 3px; border: 1px solid} 174 | 175 | .MathJax_SVG_Display {text-align: center; margin: 1em 0em; position: relative; display: block!important; text-indent: 0; max-width: none; max-height: none; min-width: 0; min-height: 0; width: 100%} 177 | .MathJax_SVG .MJX-monospace {font-family: monospace} 178 | .MathJax_SVG .MJX-sans-serif {font-family: sans-serif} 179 | #MathJax_SVG_Tooltip {background-color: InfoBackground; color: InfoText; border: 1px solid black; box-shadow: 2px 2px 5px #AAAAAA; -webkit-box-shadow: 2px 2px 5px #AAAAAA; -moz-box-shadow: 2px 2px 5px #AAAAAA; -khtml-box-shadow: 2px 2px 5px #AAAAAA; padding: 3px 4px; z-index: 401; position: absolute; left: 0; top: 0; width: auto; height: auto; display: none} 180 | .MathJax_SVG {display: inline; font-style: normal; font-weight: normal; line-height: normal; font-size: 100%; font-size-adjust: none; text-indent: 0; text-align: left; text-transform: none; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0; min-height: 0; border: 0; padding: 0; margin: 0} 181 | .MathJax_SVG * {transition: none; -webkit-transition: none; -moz-transition: none; -ms-transition: none; -o-transition: none} 182 | .MathJax_SVG > div {display: inline-block} 183 | .mjx-svg-href {fill: blue; stroke: blue} 184 | .MathJax_SVG_Processing {visibility: hidden; position: absolute; top: 0; left: 0; width: 0; height: 0; overflow: hidden; display: block!important} 185 | .MathJax_SVG_Processed {display: none!important} 186 | .MathJax_SVG_test {font-style: normal; font-weight: normal; font-size: 100%; font-size-adjust: none; text-indent: 0; text-transform: none; letter-spacing: normal; word-spacing: normal; overflow: hidden; height: 1px} 187 | .MathJax_SVG_test.mjx-test-display {display: table!important} 188 | .MathJax_SVG_test.mjx-test-inline {display: inline!important; margin-right: -1px} 189 | .MathJax_SVG_test.mjx-test-default {display: block!important; clear: both} 190 | .MathJax_SVG_ex_box {display: inline-block!important; position: absolute; overflow: hidden; min-height: 0; max-height: none; padding: 0; border: 0; margin: 0; width: 1px; height: 60ex} 191 | .mjx-test-inline .MathJax_SVG_left_box {display: inline-block; width: 0; float: left} 192 | .mjx-test-inline .MathJax_SVG_right_box {display: inline-block; width: 0; float: right} 193 | .mjx-test-display .MathJax_SVG_right_box {display: table-cell!important; width: 10000em!important; min-width: 0; max-width: none; padding: 0; border: 0; margin: 0} 194 | .MathJax_SVG .noError {vertical-align: ; font-size: 90%; text-align: left; color: black; padding: 1px 3px; border: 1px solid} 195 | 196 | 197 | 200 | 210 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /docs/source/_static/img/NumGraph_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
NumGraph
NumGraph
Viewer does not support full SVG 1.1
--------------------------------------------------------------------------------