├── .gitattributes ├── sandbox ├── small_worlds.png ├── generate_lattice.py ├── small_world_figure.py ├── effective_medium.py ├── generation_test.py └── clustering.py ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── smallworld ├── __init__.py ├── metadata.py ├── tools.py ├── generate.py ├── draw.py └── theory.py ├── Makefile ├── setup.py ├── LICENSE ├── README.md └── README.rst /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-vendored 2 | -------------------------------------------------------------------------------- /sandbox/small_worlds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benmaier/smallworld/HEAD/sandbox/small_worlds.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include setup.py 3 | include README.rst 4 | include LICENSE 5 | include setup.cfg 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | long-description = file: README.rst 3 | license = MIT 4 | classifiers = 5 | Programming Language :: Python :: 3 6 | -------------------------------------------------------------------------------- /sandbox/generate_lattice.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as pl 2 | import numpy as np 3 | 4 | N = 31 5 | 6 | x = np.arange(N) 7 | 8 | for y in range(N): 9 | pl.plot(x, np.ones_like(x)*y,'o',mfc='w',c='k',ms=3,mew=0.5) 10 | 11 | pl.axis('square') 12 | pl.axis('off') 13 | 14 | pl.savefig('grid.pdf') 15 | pl.show() 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so* 2 | *.o* 3 | .DS_Store 4 | 5 | *_data 6 | *_data_* 7 | a.out 8 | *.npy 9 | *.mexmaci64 10 | *.m~ 11 | *.pickle 12 | *.pyc 13 | *.swp 14 | *.pdf 15 | 16 | /*.egg-info/ 17 | /build/ 18 | /tmp/ 19 | /dist/ 20 | 21 | /jupyter_notebooks/.ipynb_checkpoints/ 22 | 23 | /py_only_docs/ 24 | /docs/_build/ 25 | 26 | /.vscode/ 27 | /_vscode/ 28 | -------------------------------------------------------------------------------- /smallworld/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | `smallworld` offers routines to generate modified small-world-networks 4 | which interpolate between a k-nearest-neighbor-lattice and an Erdos-Renyi 5 | network (contrary to the traditional Watts-Strogatz model). 6 | """ 7 | 8 | from .metadata import __version__ 9 | from .generate import get_smallworld_graph 10 | -------------------------------------------------------------------------------- /smallworld/metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Contains a bunch of information about this package. 4 | """ 5 | 6 | __version__ = "0.1.0" 7 | 8 | __author__ = "Benjamin F. Maier" 9 | __copyright__ = "Copyright 2018-2021, Benjamin F. Maier" 10 | __credits__ = ["Benjamin F. Maier"] 11 | __license__ = "MIT" 12 | __maintainer__ = "Benjamin F. Maier" 13 | __email__ = "bfmaier@physik.hu-berlin.de" 14 | __status__ = "Development" 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | make python 3 | 4 | clean: 5 | -rm -f *.o 6 | make pyclean 7 | 8 | clean_all: 9 | make clean 10 | make pyclean 11 | 12 | pyclean: 13 | -rm -f *.so 14 | -rm -rf *.egg-info* 15 | -rm -rf ./tmp/ 16 | -rm -rf ./build/ 17 | 18 | python: 19 | pip install -e ../smallworld --no-binary :all: 20 | 21 | checkdocs: 22 | python setup.py checkdocs 23 | 24 | pypi: 25 | rm dist/* 26 | python setup.py sdist 27 | twine upload dist/* 28 | -------------------------------------------------------------------------------- /sandbox/small_world_figure.py: -------------------------------------------------------------------------------- 1 | from smallworld.draw import draw_network 2 | from smallworld import get_smallworld_graph 3 | 4 | import matplotlib.pyplot as pl 5 | 6 | # define network parameters 7 | N = 21 8 | k_over_2 = 2 9 | betas = [0, 0.025, 1.0] 10 | labels = [ r'$\beta=0$', r'$\beta=0.025$', r'$\beta=1$'] 11 | 12 | focal_node = 0 13 | 14 | fig, ax = pl.subplots(1,3,figsize=(9,3)) 15 | 16 | 17 | # scan beta values 18 | for ib, beta in enumerate(betas): 19 | 20 | # generate small-world graphs and draw 21 | G = get_smallworld_graph(N, k_over_2, beta) 22 | draw_network(G,k_over_2,focal_node=focal_node,ax=ax[ib]) 23 | 24 | ax[ib].set_title(labels[ib],fontsize=11) 25 | 26 | # show 27 | pl.subplots_adjust(wspace=0.3) 28 | pl.show() 29 | -------------------------------------------------------------------------------- /sandbox/effective_medium.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as pl 2 | import numpy as np 3 | 4 | from smallworld.theory import get_effective_medium_eigenvalue_gap, get_effective_medium_eigenvalue_gap_from_matrix 5 | 6 | 7 | N = 300 8 | k_over_2 = 2 9 | betas = np.logspace(-4,0,10) 10 | 11 | from_matrix = np.zeros_like(betas) 12 | 13 | for ib, beta in enumerate(betas): 14 | this_val = get_effective_medium_eigenvalue_gap_from_matrix(N, k_over_2, beta) 15 | from_matrix[ib] = this_val 16 | 17 | theory = get_effective_medium_eigenvalue_gap(N,k_over_2,betas) 18 | 19 | pl.plot(betas, 1./from_matrix,'s',c='k',mfc='w',label='from matrix') 20 | pl.plot(betas, 1./theory,'-',c='k',label='theory') 21 | 22 | pl.xscale('log') 23 | #pl.yscale('log') 24 | 25 | pl.show() 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | import setuptools 3 | import os, sys 4 | 5 | # get __version__, __author__, and __email__ 6 | exec(open("./smallworld/metadata.py").read()) 7 | 8 | setup( 9 | name = 'smallworld', 10 | version = __version__, 11 | author = __author__, 12 | author_email = __email__, 13 | url = 'https://github.com/benmaier/pysmallworld', 14 | license = __license__, 15 | description = "Generate modified small-world networks and compare with theoretical predictions.", 16 | long_description = '', 17 | packages = setuptools.find_packages(), 18 | setup_requires = [ 19 | ], 20 | install_requires = [ 21 | 'networkx>=2.4', 22 | 'numpy>=1.14', 23 | 'scipy>=1.1', 24 | ], 25 | include_package_data = True, 26 | zip_safe = False, 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Benjamin F. Maier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /sandbox/generation_test.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as pl 2 | import numpy as np 3 | from collections import Counter 4 | 5 | from smallworld import get_smallworld_graph 6 | from smallworld.theory import get_degree_distribution 7 | 8 | from time import time 9 | 10 | def get_counter_hist(ks): 11 | c = Counter(ks) 12 | kmax = np.max(ks) 13 | k = np.arange(kmax+1,dtype=int) 14 | s = 0 15 | P = np.array(np.zeros_like(k),dtype=float) 16 | for _k in k: 17 | P[_k] = c[_k] 18 | s += c[_k] 19 | P /= s 20 | 21 | return k, P 22 | 23 | N = 15 24 | k_over_2 = 3 25 | beta = 0.5 26 | 27 | N_meas = 10000 28 | 29 | ks_fast = [] 30 | ks_slow = [] 31 | 32 | t_fast = [] 33 | t_slow = [] 34 | 35 | for meas in range(N_meas): 36 | 37 | print(meas) 38 | 39 | start = time() 40 | G_fast = get_smallworld_graph(N, k_over_2, beta) 41 | end = time() 42 | t_fast.append(end-start) 43 | 44 | start = time() 45 | G_slow = get_smallworld_graph(N, k_over_2, beta, use_slow_algorithm = True) 46 | end = time() 47 | t_slow.append(end-start) 48 | 49 | ks_fast.extend([ d[1] for d in G_fast.degree()]) 50 | ks_slow.extend([ d[1] for d in G_slow.degree()]) 51 | 52 | print("needed t =", np.mean(t_fast), "s per run for the fast algorithm") 53 | print("needed t =", np.mean(t_slow), "s per run for the slow algorithm") 54 | 55 | k_f, P_f = get_counter_hist(ks_fast) 56 | k_s, P_s = get_counter_hist(ks_slow) 57 | k_t, P_t = get_degree_distribution(N, k_over_2, beta, kmax=max(k_f.max(), k_s.max())) 58 | 59 | pl.plot(k_f, P_f, 'o', label='fast algorithm',mfc='w') 60 | pl.plot(k_s, P_s, 's', label='slow algorithm',mfc='w') 61 | pl.plot(k_t, P_t, '-', label='theory',lw=1) 62 | pl.legend() 63 | 64 | pl.xlabel('node degree $k$') 65 | pl.ylabel('probability $P_k$') 66 | 67 | pl.show() 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smallworld 2 | 3 | Generate and analyze small-world networks according to the revised Watts-Strogatz model where the randomization 4 | at _β_ = 1 is truly equal to the Erdős-Rényi network model. 5 | 6 | In the Watts-Strogatz model each node rewires its _k_/2 rightmost edges with probality _β_. This means each node has halways minimum degree _k_/2. Also, at _β_ = 1, each edge has been rewired. Hence the probability of it existing is <_k_/(_N_-1), contrary to the ER model. 7 | 8 | In the adjusted model, each pair of nodes is connected with a certain connection probability. If the lattice distance between the potentially connected nodes is d(i,j) <= _k_/2 then they are connected with short-range probability `p_S = k / (k + β (N-1-k))`, otherwise they're connected with long-range probability `p_L = β * p_S`. 9 | 10 | ## Install 11 | 12 | pip install smallworld 13 | 14 | Beware: `smallworld` only works with Python 3! 15 | 16 | ## Example 17 | 18 | In the following example you can see how to generate and draw according to the model described above. 19 | 20 | ```python 21 | from smallworld.draw import draw_network 22 | from smallworld import get_smallworld_graph 23 | 24 | import matplotlib.pyplot as pl 25 | 26 | # define network parameters 27 | N = 21 28 | k_over_2 = 2 29 | betas = [0, 0.025, 1.0] 30 | labels = [ r'$\beta=0$', r'$\beta=0.025$', r'$\beta=1$'] 31 | 32 | focal_node = 0 33 | 34 | fig, ax = pl.subplots(1,3,figsize=(9,3)) 35 | 36 | 37 | # scan beta values 38 | for ib, beta in enumerate(betas): 39 | 40 | # generate small-world graphs and draw 41 | G = get_smallworld_graph(N, k_over_2, beta) 42 | draw_network(G,k_over_2,focal_node=focal_node,ax=ax[ib]) 43 | 44 | ax[ib].set_title(labels[ib],fontsize=11) 45 | 46 | # show 47 | pl.subplots_adjust(wspace=0.3) 48 | pl.show() 49 | ``` 50 | 51 | ![visualization example](https://github.com/benmaier/smallworld/raw/master/sandbox/small_worlds.png) 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | smallworld 2 | ========== 3 | 4 | Generate and analyze small-world networks according to the revised 5 | Watts-Strogatz model where the randomization at *β* = 1 is truly equal to the Erdős-Rényi network model. 6 | 7 | In the Watts-Strogatz model each node rewires its *k*/2 8 | rightmost edges with probality *β*. This means that each node has halways 9 | minimum degree *k*/2. Also, at *β* = 1, each edge has been rewired. 10 | Hence the probability of it existing is smaller than *k*/(*N*-1), contrary to the ER model. 11 | 12 | In the adjusted model, each pair of nodes is connected with a certain 13 | connection probability. If the lattice distance between the potentially 14 | connected nodes is d(i,j) <= *k*/2 then they are connected with 15 | short-range probability ``p_S = k / (k + β (N-1-k))``, otherwise they're 16 | connected with long-range probability ``p_L = β * p_S``. 17 | 18 | Install 19 | ------- 20 | 21 | :: 22 | 23 | pip install smallworld 24 | 25 | Beware: ``smallworld`` only works with Python 3! 26 | 27 | Example 28 | ------- 29 | 30 | In the following example you can see how to generate and draw according 31 | to the model described above. 32 | 33 | .. code:: python 34 | 35 | from smallworld.draw import draw_network 36 | from smallworld import get_smallworld_graph 37 | 38 | import matplotlib.pyplot as pl 39 | 40 | # define network parameters 41 | N = 21 42 | k_over_2 = 2 43 | betas = [0, 0.025, 1.0] 44 | labels = [ r'$\beta=0$', r'$\beta=0.025$', r'$\beta=1$'] 45 | 46 | focal_node = 0 47 | 48 | fig, ax = pl.subplots(1,3,figsize=(9,3)) 49 | 50 | 51 | # scan beta values 52 | for ib, beta in enumerate(betas): 53 | 54 | # generate small-world graphs and draw 55 | G = get_smallworld_graph(N, k_over_2, beta) 56 | draw_network(G,k_over_2,focal_node=focal_node,ax=ax[ib]) 57 | 58 | ax[ib].set_title(labels[ib],fontsize=11) 59 | 60 | # show 61 | pl.subplots_adjust(wspace=0.3) 62 | pl.show() 63 | 64 | .. figure:: https://github.com/benmaier/smallworld/raw/master/sandbox/small_worlds.png 65 | :alt: visualization example 66 | 67 | visualization example 68 | -------------------------------------------------------------------------------- /sandbox/clustering.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as pl 2 | import numpy as np 3 | from collections import Counter 4 | 5 | from smallworld import get_smallworld_graph 6 | from smallworld.theory import expected_number_of_unique_two_stars_per_node 7 | from smallworld.theory import expected_number_of_unique_triangles_per_node 8 | from smallworld.tools import get_number_of_unique_two_stars_for_each_node 9 | from smallworld.tools import get_number_of_unique_triangles_for_each_node 10 | 11 | from time import time 12 | 13 | N = 301 14 | k_over_2 = 2 15 | 16 | betas = np.logspace(-4,0,10) 17 | 18 | N_meas = 1000 19 | 20 | mean_two_stars = [] 21 | mean_triangles = [] 22 | 23 | 24 | start = time() 25 | for ib, beta in enumerate(betas): 26 | print(ib+1,"/",len(betas)) 27 | two_stars = [] 28 | triangles = [] 29 | for meas in range(N_meas): 30 | 31 | G = get_smallworld_graph(N, k_over_2, beta, use_slow_algorithm=False,verbose=False) 32 | two_stars.extend(list(get_number_of_unique_two_stars_for_each_node(G))) 33 | triangles.extend(list(get_number_of_unique_triangles_for_each_node(G))) 34 | 35 | mean_two_stars.append(np.mean(two_stars)) 36 | mean_triangles.append(np.mean(triangles)) 37 | 38 | end = time() 39 | 40 | print("needed", (end-start) / len(betas), "s per beta") 41 | 42 | 43 | mean_two_stars = np.array(mean_two_stars) 44 | mean_triangles = np.array(mean_triangles) 45 | 46 | pl.figure() 47 | pl.plot(betas, mean_two_stars,'s',mfc='w') 48 | S = expected_number_of_unique_two_stars_per_node(N, k_over_2, betas) 49 | pl.plot(betas, S,lw=1,c='k') 50 | 51 | pl.xscale('log') 52 | #pl.yscale('log') 53 | pl.xlabel(r'$\beta$') 54 | pl.ylabel('expected number of two stars per node') 55 | 56 | pl.figure() 57 | pl.plot(betas, mean_triangles,'s',mfc='w') 58 | T = expected_number_of_unique_triangles_per_node(N, k_over_2, betas) 59 | pl.plot(betas, T,lw=1,c='k') 60 | 61 | pl.xscale('log') 62 | #pl.yscale('log') 63 | pl.xlabel(r'$\beta$') 64 | pl.ylabel('expected number of triangles per node') 65 | 66 | pl.figure() 67 | pl.plot(betas, mean_triangles/mean_two_stars,'s',mfc='w') 68 | pl.plot(betas, T/S,lw=1,c='k') 69 | pl.xscale('log') 70 | #pl.yscale('log') 71 | pl.xlabel(r'$\beta$') 72 | pl.ylabel('clustering coefficient') 73 | 74 | pl.show() 75 | -------------------------------------------------------------------------------- /smallworld/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various handy things. 3 | """ 4 | 5 | import numpy as np 6 | import networkx as nx 7 | 8 | import scipy.sparse as sprs 9 | 10 | 11 | def assert_parameters(N,k_over_2,beta): 12 | """Assert that `N` is integer, `k_over_2` is integer and `0 <= beta <= 1`""" 13 | 14 | assert(k_over_2 == int(k_over_2)) 15 | assert(N == int(N)) 16 | assert(beta >= 0.0) 17 | assert(beta <= 1.0) 18 | 19 | 20 | def get_largest_component(G): 21 | """Return the largest connected component of graph `G`.""" 22 | 23 | new_G = max([G.subgraph(c) for c in nx.connected_components(G)], key=len) 24 | G = nx.convert_node_labels_to_integers(new_G) 25 | 26 | return G 27 | 28 | def get_number_of_unique_two_stars_for_each_node(G): 29 | k = np.array([ d[1] for d in G.degree() ], dtype=float) 30 | num = k*(k-1.0)/2.0 31 | return num 32 | 33 | def get_number_of_unique_two_stars_per_node(G): 34 | 35 | return np.mean(get_number_of_unique_two_stars_for_each_node(G)) 36 | 37 | def get_number_of_unique_triangles_for_each_node(G): 38 | 39 | A = nx.adjacency_matrix(G) 40 | 41 | A3 = A.dot(A).dot(A) 42 | T = np.array(A3.diagonal(),dtype=float) / 2.0 43 | #T = np.array(list(nx.triangles(G).values()), dtype=float) 44 | 45 | return T 46 | 47 | def get_number_of_unique_triangles_per_node(G): 48 | 49 | return np.mean(get_number_of_unique_triangles_for_each_node(G)) 50 | 51 | def get_sparse_matrix_from_rows_and_cols(N, rows, cols): 52 | 53 | A = sprs.csc_matrix((np.ones_like(rows),(rows,cols)), shape=(N,N),dtype=float) 54 | 55 | return A 56 | 57 | def get_random_walk_eigenvalue_gap(A,maxiter=10000): 58 | 59 | W = A.copy() 60 | W = W.astype(float) 61 | degree = np.array(W.sum(axis=1),dtype=float).flatten() 62 | 63 | for c in range(W.shape[1]): 64 | W.data[W.indptr[c]:W.indptr[c+1]] /= degree[c] 65 | 66 | lambda_max,_ = sprs.linalg.eigs(W,k=3,which='LR',maxiter=maxiter) 67 | lambda_max = np.abs(lambda_max) 68 | ind_zero = np.argmax(lambda_max) 69 | lambda_1 = lambda_max[ind_zero] 70 | lambda_max2 = np.delete(lambda_max,ind_zero) 71 | lambda_2 = max(lambda_max2) 72 | 73 | return 1 - lambda_2.real 74 | 75 | if __name__ == "__main__": 76 | from time import time 77 | 78 | G = nx.fast_gnp_random_graph(10000,5.0/10000) 79 | 80 | start = time() 81 | A = nx.adjacency_matrix(G) 82 | A3 = A.dot(A).dot(A) 83 | T = np.array(A3.diagonal(),dtype=float) / 2.0 84 | end = time() 85 | print(T) 86 | print(end-start) 87 | start = time() 88 | T = np.array(list(nx.triangles(G).values()), dtype=float) 89 | end = time() 90 | print(end-start) 91 | 92 | -------------------------------------------------------------------------------- /smallworld/generate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate small-world networks according to the modified model. 3 | """ 4 | 5 | import networkx as nx 6 | from numpy import random 7 | import numpy as np 8 | 9 | from smallworld.theory import get_connection_probabilities 10 | from smallworld.tools import assert_parameters 11 | from smallworld.tools import get_largest_component as _get_largest_component 12 | 13 | def get_fast_smallworld_graph(N, k_over_2, beta, verbose=False): 14 | """ 15 | Loop over all possibler short-range edges and add 16 | each with probability :math:`p_S`. 17 | 18 | Sample :math:`m_L` from a binomial distribution 19 | :math:`\mathcal B(N(N-1-k)/2, p_L`. For each edge 20 | m with :math:`0\leq m\leq m_L` sample a node :math:`u` 21 | and a possible long-range neighbor :math:`v` until 22 | egde :math:`(u,v)` has not yet been sampled. 23 | Then add :math:`(u,v)` to the network. 24 | """ 25 | 26 | assert_parameters(N,k_over_2,beta) 27 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 28 | 29 | G = nx.Graph() 30 | G.add_nodes_from(range(N)) 31 | 32 | N = int(N) 33 | k_over_2 = int(k_over_2) 34 | k = int(2*k_over_2) 35 | 36 | 37 | # add short range edges in order (Nk/2) 38 | for u in range(N): 39 | for v in range(u+1, u+k_over_2+1): 40 | if random.rand() < pS: 41 | G.add_edge(u,v % N) 42 | 43 | # sample number of long-range edges 44 | mL_max = N*(N-1-k) // 2 45 | mL = random.binomial(mL_max, pL) 46 | 47 | number_of_rejects = 0 48 | 49 | for m in range(mL): 50 | while True: 51 | # beware: upper bound non-inclusive in random.randint(a,b) 52 | u = random.randint(0,N) 53 | v = u + k_over_2 + random.randint(1, N - k) 54 | v %= N 55 | 56 | if not G.has_edge(u,v): 57 | G.add_edge(u,v) 58 | break 59 | else: 60 | number_of_rejects += 1 61 | 62 | if verbose: 63 | print("number_of_rejects =", number_of_rejects) 64 | 65 | return G 66 | 67 | 68 | def get_smallworld_graph(N,k_over_2,beta,use_slow_algorithm=False,get_largest_component=False,verbose=False): 69 | """ 70 | Get a modified small-world network with number of nodes `N`, 71 | mean degree `k=2*k_over_2` and long-range impact `0 <= beta <= 1`. 72 | At beta = 0, 73 | """ 74 | 75 | if use_slow_algorithm: 76 | G = nx.Graph() 77 | G.add_nodes_from(range(N)) 78 | 79 | G.add_edges_from(get_edgelist_slow(N,k_over_2,beta)) 80 | else: 81 | G = get_fast_smallworld_graph(N, k_over_2, beta,verbose=verbose) 82 | 83 | if get_largest_component: 84 | G = _get_largest_component(G) 85 | 86 | return G 87 | 88 | 89 | def get_edgelist_slow(N,k_over_2,beta): 90 | """ 91 | Loop over all pair of nodes, calculate their lattice 92 | distance and add an edge according to short-range 93 | or long-range connection probability, respectively 94 | """ 95 | 96 | assert_parameters(N,k_over_2,beta) 97 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 98 | 99 | N = int(N) 100 | k_over_2 = int(k_over_2) 101 | 102 | E = [] 103 | 104 | for i in range(N-1): 105 | for j in range(i+1,N): 106 | 107 | distance = j - i 108 | 109 | if (distance <= k_over_2) or ((N - distance) <= k_over_2): 110 | p = pS 111 | else: 112 | p = pL 113 | 114 | if random.rand() < p: 115 | E.append((i,j)) 116 | 117 | return E 118 | 119 | -------------------------------------------------------------------------------- /smallworld/draw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Methods to draw those networks. 3 | """ 4 | 5 | import numpy as np 6 | 7 | import matplotlib as mpl 8 | import matplotlib.pyplot as pl 9 | 10 | #def plot_edge(ax,N,u,v,phis,color=None): 11 | colors = [ 12 | '#666666', 13 | '#1b9e77', 14 | '#e7298a' 15 | ] 16 | 17 | mpl.rcParams['font.size'] = 9 18 | mpl.rcParams['legend.fontsize'] = 'medium' 19 | mpl.rcParams['figure.titlesize'] = 'medium' 20 | mpl.rcParams['axes.titlesize'] = 'medium' 21 | #mpl.rcParams['xtick.labelsize'] = 'small' 22 | #mpl.rcParams['ytick.labelsize'] = 'small' 23 | mpl.rcParams['xtick.labelsize'] = 'medium' 24 | mpl.rcParams['ytick.labelsize'] = 'medium' 25 | mpl.rcParams['lines.markersize'] = 4 26 | mpl.rcParams['lines.linewidth'] = 1.0 27 | 28 | 29 | def bezier_curve(P0,P1,P2,n=20): 30 | 31 | t = np.linspace(0,1,20) 32 | B = np.zeros((n,2)) 33 | for part in range(n): 34 | t_ = t[part] 35 | 36 | B[part,:] = (1-t_)**2 * P0 + 2*(1-t_)*t_*P1+t_**2*P2 37 | 38 | return B 39 | 40 | def is_shortrange(i,j,N,k_over_2): 41 | distance = np.abs(i-j) 42 | 43 | return distance <= k_over_2 or N-distance <= k_over_2 44 | 45 | def draw_network(G, k_over_2, R=10,focal_node=None, ax=None,markersize=None,linewidth=1.0,linkcolor=None): 46 | """ 47 | Draw a small world network. 48 | 49 | Parameters 50 | ========== 51 | G : network.Graph 52 | The network to be drawn 53 | R : float, default : 10.0 54 | Radius of the circle 55 | focal_node : int, default : None 56 | If this is given, highlight edges 57 | connected to this node. 58 | ax : matplotlib.Axes, default : None 59 | Axes to draw on. If `None`, will generate 60 | a new one. 61 | 62 | Returns 63 | ======= 64 | ax : matplotlib.Axes 65 | """ 66 | 67 | G_ = G.copy() 68 | 69 | 70 | if ax is None: 71 | fig, ax = pl.subplots(1,1,figsize=(3,3)) 72 | 73 | focal_alpha = 1 74 | 75 | if focal_node is None: 76 | non_focal_alpha = 1 77 | focal_lw = linewidth*1.0 78 | non_focal_lw = linewidth*1.0 79 | else: 80 | non_focal_alpha = 0.6 81 | focal_lw = linewidth*1.5 82 | non_focal_lw = linewidth*1.0 83 | 84 | 85 | N = G_.number_of_nodes() 86 | 87 | phis = 2*np.pi*np.arange(N)/N + np.pi/2 88 | 89 | x = R * np.cos(phis) 90 | y = R * np.sin(phis) 91 | 92 | points = np.zeros((N,2)) 93 | points[:,0] = x 94 | points[:,1] = y 95 | origin = np.zeros((2,)) 96 | 97 | col = list(colors) 98 | if linkcolor is not None: 99 | col[0] = linkcolor 100 | 101 | 102 | ax.axis('equal') 103 | ax.axis('off') 104 | 105 | if focal_node is None: 106 | edges = list(G_.edges(data=False)) 107 | else: 108 | focal_edges = [ e for e in G_.edges(data=False) if focal_node in e] 109 | G_.remove_edges_from(focal_edges) 110 | edges = list(G_.edges) + focal_edges 111 | 112 | for i, j in edges: 113 | 114 | phi0 = phis[i] 115 | phi1 = phis[j] 116 | dphi = phi1 - phi0 117 | 118 | if dphi > np.pi: 119 | dphi = 2*np.pi - dphi 120 | phi0, phi1 = phi1, phi0 121 | phi1 += 2*np.pi 122 | 123 | distance = np.abs(i-j) 124 | 125 | if i == focal_node or j == focal_node: 126 | if distance <= k_over_2 or N-distance <= k_over_2: 127 | this_color = col[2] 128 | else: 129 | this_color = col[1] 130 | this_alpha = focal_alpha 131 | this_lw = focal_lw 132 | else: 133 | this_color = col[0] 134 | this_alpha = non_focal_alpha 135 | this_lw = non_focal_lw 136 | 137 | if distance == 1 or N-distance == 1: 138 | 139 | these_phis = np.linspace(phi0, phi1,20) 140 | these_x = R * np.cos(these_phis) 141 | these_y = R * np.sin(these_phis) 142 | 143 | else: 144 | if is_shortrange(i,j,N,k_over_2): 145 | ophi = phi0 + dphi/2 146 | o = np.array([ 147 | 0.6*R*np.cos(ophi), 148 | 0.6*R*np.sin(ophi), 149 | ]) 150 | else: 151 | o = origin 152 | B = bezier_curve(points[i],o,points[j],n=20) 153 | these_x = B[:,0] 154 | these_y = B[:,1] 155 | 156 | 157 | ax.plot(these_x, these_y,c=this_color,alpha=this_alpha,lw=this_lw) 158 | 159 | ax.plot(x,y,'o',c='k',mec='#ffffff',ms=markersize) 160 | 161 | return ax 162 | 163 | if __name__ == "__main__": 164 | from smallworld import get_smallworld_graph 165 | 166 | N = 50 167 | k_over_2 = 2 168 | beta = 0.01 169 | 170 | focal_node = 0 171 | 172 | G = get_smallworld_graph(N, k_over_2, beta) 173 | draw_network(G,k_over_2,focal_node=0) 174 | 175 | pl.show() 176 | -------------------------------------------------------------------------------- /smallworld/theory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compute various things analytically. 3 | """ 4 | 5 | import numpy as np 6 | from scipy.stats import binom 7 | 8 | from smallworld.tools import assert_parameters 9 | 10 | from scipy.linalg import circulant 11 | 12 | def binomial_mean(n, p): 13 | """Does what it says it does.""" 14 | return n*p 15 | 16 | def binomial_variance(n, p): 17 | """Does what it says it does.""" 18 | return n*p*(1-p) 19 | 20 | def binomial_second_moment(n, p): 21 | """Does what it says it does.""" 22 | return binomial_variance(n, p) + binomial_mean(n, p)**2 23 | 24 | 25 | def get_connection_probabilities(N,k_over_2,beta): 26 | """ 27 | Return the connection probabilities :math:`p_S` and :math:`p_L`. 28 | """ 29 | 30 | assert_parameters(N,k_over_2,beta) 31 | 32 | k = float(int(k_over_2 * 2)) 33 | 34 | pS = k / (k + beta*(N-1.0-k)) 35 | pL = k * beta / (k + beta*(N-1.0-k)) 36 | 37 | return pS, pL 38 | 39 | def get_connection_probability_arrays(N, k_over_2, beta): 40 | """ 41 | Return the connection probabilities :math:`p_S` and :math:`p_L` 42 | but for beta being a `numpy.ndarray`. 43 | """ 44 | assert_parameters(N,k_over_2,0.0) 45 | 46 | assert(np.all(beta>=0.0)) 47 | assert(np.all(beta<=1.0)) 48 | 49 | k = float(int(k_over_2 * 2)) 50 | 51 | pS = k / (k + beta*(N-1.0-k)) 52 | pL = k * beta / (k + beta*(N-1.0-k)) 53 | 54 | return pS, pL 55 | 56 | 57 | def get_degree_distribution(N,k_over_2,beta,kmax=None): 58 | """ 59 | Return degrees `k` and corresponding probabilities math:`P_k` 60 | up to maximum degree `kmax` (= `N-1` if not provided). 61 | """ 62 | 63 | 64 | if kmax is None: 65 | kmax = N-1 66 | 67 | assert_parameters(N,k_over_2,beta) 68 | 69 | k = int(2*k_over_2) 70 | N = int(N) 71 | 72 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 73 | 74 | B_short = binom(k, pS).pmf 75 | B_long = binom(N-1-k, pL).pmf 76 | 77 | ks = np.arange(kmax+1) 78 | Pk = np.array(np.zeros_like(ks), dtype=float) 79 | 80 | for _k in ks: 81 | _P_k = 0.0 82 | for kS in range(min(k,_k)+1): 83 | _P_k += B_short(kS) * B_long(_k-kS) 84 | Pk[_k] = _P_k 85 | 86 | return ks, Pk 87 | 88 | 89 | def get_degree_second_moment(N,k_over_2,beta): 90 | """Does what it says it does.""" 91 | 92 | assert_parameters(N,k_over_2,beta) 93 | 94 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 95 | 96 | k = int(2*k_over_2) 97 | 98 | return binomial_second_moment(k, pS)\ 99 | + binomial_second_moment(N-1-k, pL) \ 100 | + 2 * binomial_mean(k, pS) * binomial_mean(N-1-k, pL) 101 | 102 | def get_degree_variance(N,k_over_2,beta): 103 | """Does what it says it does.""" 104 | 105 | if type(beta) == np.ndarray: 106 | pS, pL = get_connection_probability_arrays(N, k_over_2, beta) 107 | else: 108 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 109 | 110 | N = int(N) 111 | k = int(2*k_over_2) 112 | 113 | return binomial_variance(k, pS) + binomial_variance(N-1-k, pL) 114 | 115 | 116 | def expected_number_of_unique_triangles_per_node(N,k_over_2,beta): 117 | """Does what it says it does. If `beta` is an array, returns an array. Only works for odd N.""" 118 | 119 | if type(beta) == np.ndarray: 120 | pS, pL = get_connection_probability_arrays(N, k_over_2, beta) 121 | else: 122 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 123 | 124 | N = int(N) 125 | k = int(2*k_over_2) 126 | 127 | if N % 2 == 0: 128 | raise ValueError("This currently only works for odd number of nodes N") 129 | 130 | R = k_over_2 131 | L = (N-1) // 2 132 | 133 | big_triangle = (R**2 - R)/2 + R 134 | small_triangle = (R**2 - R)/2 135 | _S3 = small_triangle * 3 136 | _S2L = 3 * big_triangle 137 | _SL2 = 2 * ((L-R)*R - big_triangle) +\ 138 | big_triangle +\ 139 | 2*(L-R)*R +\ 140 | 2*((L-1)*R - big_triangle) 141 | 142 | _L3 = (L-R)**2 - (2*((L-1)*R - big_triangle)) - (L-R) +\ 143 | (L-R)**2 - big_triangle 144 | 145 | #print("whole area =", S3 + S2L + SL2 + L3) 146 | S3 = k*(k-2)*3/8. 147 | S2L = 3*k/8. * (k+2) 148 | SL2 = (k/8.)*(12*N-26-11*k) 149 | L3 = (1/8.) * (5*k**2 + k*(-12*N+26) + 4*(N**2-3*N+2)) 150 | 151 | #print(_S3, S3) 152 | #print(_S2L, S2L) 153 | #print(_SL2, SL2) 154 | #print(_L3, L3) 155 | 156 | return S3 * pS**3 + S2L * pS**2*pL + SL2 * pS*pL**2 + L3 * pL**3 157 | 158 | def expected_number_of_unique_two_stars_per_node(N,k_over_2,beta): 159 | """Does what it says it does. If `beta` is an array, returns an array. Only works for odd N.""" 160 | 161 | if type(beta) == np.ndarray: 162 | pS, pL = get_connection_probability_arrays(N, k_over_2, beta) 163 | else: 164 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 165 | 166 | N = int(N) 167 | k = int(2*k_over_2) 168 | 169 | if N % 2 == 0: 170 | raise ValueError("This currently only works for odd number of nodes N") 171 | 172 | R = k_over_2 173 | L = (N-1) // 2 174 | 175 | S2 = (R**2 - R) + R**2 176 | SL = 4 * (L-R) * R 177 | L2 = ((L-R)**2 - (L-R)) + (L-R)**2 178 | 179 | #print("whole area =", S2 + SL + L2) 180 | return S2 * pS**2 + SL * pS*pL + L2 * pL**2 181 | 182 | def expected_clustering(N,k_over_2,beta): 183 | 184 | return expected_number_of_unique_triangles_per_node(N,k_over_2,beta) / expected_number_of_unique_two_stars_per_node(N,k_over_2,beta) 185 | 186 | def get_effective_medium_eigenvalue_gap_from_matrix(N,k_over_2,beta): 187 | 188 | assert_parameters(N,k_over_2,beta) 189 | 190 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 191 | 192 | N = int(N) 193 | k_over_2 = int(k_over_2) 194 | k = int(2*k_over_2) 195 | 196 | 197 | P = np.array([0.0] + [pS]*k_over_2 + [pL]*(N-1-k) + [pS]*k_over_2) / k 198 | P = circulant(P) 199 | 200 | 201 | omega = np.sort(np.linalg.eigvalsh(P)) 202 | omega_N_m_1 = omega[-2] 203 | 204 | return 1 - omega_N_m_1 205 | 206 | def get_effective_medium_eigenvalue_gap(N,k_over_2,beta): 207 | 208 | if type(beta) == np.ndarray: 209 | pS, pL = get_connection_probability_arrays(N, k_over_2, beta) 210 | else: 211 | pS, pL = get_connection_probabilities(N,k_over_2,beta) 212 | 213 | N = int(N) 214 | k = int(2*k_over_2) 215 | 216 | 217 | j = 1.0 + np.arange(k_over_2) 218 | C = 2 * np.cos(2.0*np.pi/N*j).sum() 219 | 220 | return ( 1 - (C - beta * (1+C)) / (k + beta*(N-1-k))) 221 | --------------------------------------------------------------------------------