Skip to content

Commit

Permalink
Merge pull request #135 from GiulioRossetti/community-aware
Browse files Browse the repository at this point in the history
🆕 community-aware IC
  • Loading branch information
GiulioRossetti committed Sep 13, 2019
2 parents f643821 + c3f98fd commit 8fc907e
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 6 deletions.
89 changes: 89 additions & 0 deletions ndlib/models/epidemics/ICEModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from ndlib.models.DiffusionModel import DiffusionModel
import numpy as np
import future.utils

__author__ = 'Giulio Rossetti'
__license__ = "BSD-2-Clause"
__email__ = "giulio.rossetti@gmail.com"


class ICEModel(DiffusionModel):
"""
Parameter free model: probability of diffusion tied to community embeddedness of individual nodes
"""

def __init__(self, graph):
"""
Model Constructor
:param graph: A networkx graph object
"""
super(self.__class__, self).__init__(graph)
self.available_statuses = {
"Susceptible": 0,
"Infected": 1,
"Removed": 2
}

self.parameters = {
"model": {},
"nodes": {},
"edges": {}
}

self.name = "Community Embeddedness"

def iteration(self, node_status=True):
"""
Execute a single model iteration
:return: Iteration_id, Incremental node status (dictionary node->status)
"""
self.clean_initial_status(self.available_statuses.values())
actual_status = {node: nstatus for node, nstatus in future.utils.iteritems(self.status)}

if self.actual_iteration == 0:
self.actual_iteration += 1
delta, node_count, status_delta = self.status_delta(actual_status)
if node_status:
return {"iteration": 0, "status": actual_status.copy(),
"node_count": node_count.copy(), "status_delta": status_delta.copy()}
else:
return {"iteration": 0, "status": {},
"node_count": node_count.copy(), "status_delta": status_delta.copy()}

for u in self.graph.nodes:
if self.status[u] != 1:
continue

neighbors = list(self.graph.neighbors(u)) # neighbors and successors (in DiGraph) produce the same result
same_community_neighbors = [n for n in neighbors if self.params['nodes']['com'][u] == self.params['nodes']['com'][n]]

# Standard threshold
if len(neighbors) > 0:

for v in neighbors:
if actual_status[v] == 0:

if self.params['nodes']['com'][u] == self.params['nodes']['com'][v]:
threshold = float(len(same_community_neighbors))/len(neighbors)

else: # across communities
threshold = 1 - float(len(same_community_neighbors))/len(neighbors)

flip = np.random.random_sample()
if flip <= threshold:
actual_status[v] = 1

actual_status[u] = 2

delta, node_count, status_delta = self.status_delta(actual_status)
self.status = actual_status
self.actual_iteration += 1

if node_status:
return {"iteration": self.actual_iteration - 1, "status": delta.copy(),
"node_count": node_count.copy(), "status_delta": status_delta.copy()}
else:
return {"iteration": self.actual_iteration - 1, "status": {},
"node_count": node_count.copy(), "status_delta": status_delta.copy()}
117 changes: 117 additions & 0 deletions ndlib/models/epidemics/ICPModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from ndlib.models.DiffusionModel import DiffusionModel
import numpy as np
import future.utils

__author__ = 'Giulio Rossetti'
__license__ = "BSD-2-Clause"
__email__ = "giulio.rossetti@gmail.com"


class ICPModel(DiffusionModel):
"""
Edge Parameters to be specified via ModelConfig
:param threshold: The edge threshold. As default a value of 0.1 is assumed for all edges.
:param permeability: The degree of permeability of a community toward outgoing diffusion processes
"""

def __init__(self, graph):
"""
Model Constructor
:param graph: A networkx graph object
"""
super(self.__class__, self).__init__(graph)
self.available_statuses = {
"Susceptible": 0,
"Infected": 1,
"Removed": 2
}

self.parameters = {
"model": {
"permeability":{
"descr": "Community permeability",
"range": [0,1],
"optional": False,
"default": 0.5
}
},
"nodes": {},
"edges": {
"threshold": {
"descr": "Edge threshold",
"range": [0, 1],
"optional": True,
"default": 0.1
}
},
}

self.name = "Community Permeability"

def iteration(self, node_status=True):
"""
Execute a single model iteration
:return: Iteration_id, Incremental node status (dictionary node->status)
"""
self.clean_initial_status(self.available_statuses.values())
actual_status = {node: nstatus for node, nstatus in future.utils.iteritems(self.status)}

if self.actual_iteration == 0:
self.actual_iteration += 1
delta, node_count, status_delta = self.status_delta(actual_status)
if node_status:
return {"iteration": 0, "status": actual_status.copy(),
"node_count": node_count.copy(), "status_delta": status_delta.copy()}
else:
return {"iteration": 0, "status": {},
"node_count": node_count.copy(), "status_delta": status_delta.copy()}

for u in self.graph.nodes:
if self.status[u] != 1:
continue

neighbors = list(self.graph.neighbors(u)) # neighbors and successors (in DiGraph) produce the same result

# Standard threshold
if len(neighbors) > 0:
threshold = 1.0/len(neighbors)

for v in neighbors:
if actual_status[v] == 0:
key = (u, v)

if self.params['nodes']['com'][u] == self.params['nodes']['com'][v]: # within same community
# Individual specified thresholds
if 'threshold' in self.params['edges']:
if key in self.params['edges']['threshold']:
threshold = self.params['edges']['threshold'][key]
elif (v, u) in self.params['edges']['threshold'] and not self.graph.directed:
threshold = self.params['edges']['threshold'][(v, u)]

else: # across communities
p = self.params['model']['permeability']
if 'threshold' in self.params['edges']:
if key in self.params['edges']['threshold']:
threshold = self.params['edges']['threshold'][key] * p
elif (v, u) in self.params['edges']['threshold'] and not self.graph.directed:
threshold = self.params['edges']['threshold'][(v, u)] * p

flip = np.random.random_sample()
if flip <= threshold:
actual_status[v] = 1

actual_status[u] = 2

delta, node_count, status_delta = self.status_delta(actual_status)
self.status = actual_status
self.actual_iteration += 1

if node_status:
return {"iteration": self.actual_iteration - 1, "status": delta.copy(),
"node_count": node_count.copy(), "status_delta": status_delta.copy()}
else:
return {"iteration": self.actual_iteration - 1, "status": {},
"node_count": node_count.copy(), "status_delta": status_delta.copy()}
4 changes: 4 additions & 0 deletions ndlib/models/epidemics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from .SISModel import SISModel
from .SWIRModel import SWIRModel
from .ThresholdModel import ThresholdModel
from .ICEModel import ICEModel
from .ICPModel import ICPModel

__all__ = [
'GeneralisedThresholdModel',
Expand All @@ -28,4 +30,6 @@
'SISModel',
'SWIRModel',
'ThresholdModel',
'ICEModel',
'ICPModel'
]
52 changes: 51 additions & 1 deletion ndlib/test/test_ndlib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import absolute_import

import unittest

import random
import future.utils
import networkx as nx
import igraph as ig
Expand Down Expand Up @@ -409,6 +409,56 @@ def test_independent_cascade_model(self):
iterations = model.iteration_bunch(10, node_status=False)
self.assertEqual(len(iterations), 10)

def test_ICE(self):
for g in get_graph(True):
model = epd.ICEModel(g)
config = mc.Configuration()
config.add_model_parameter('percentage_infected', 0.1)
if isinstance(g, nx.Graph):
node_to_com = {n: random.choice([0, 1])for n in g.nodes()}
for i in g.nodes():
config.add_node_configuration("com", i, node_to_com[i])
else:
node_to_com = {n: random.choice([0, 1]) for n in g.vs['name']}
for i in g.vs['name']:
config.add_node_configuration("com", i, node_to_com[i])

model.set_initial_status(config)
iterations = model.iteration_bunch(10)
self.assertEqual(len(iterations), 10)
iterations = model.iteration_bunch(10, node_status=False)
self.assertEqual(len(iterations), 10)

def test_ICP(self):

threshold = 0.1

for g in get_graph(True):
model = epd.ICPModel(g)
config = mc.Configuration()
config.add_model_parameter('percentage_infected', 0.1)
if isinstance(g, nx.Graph):
node_to_com = {n: random.choice([0, 1])for n in g.nodes()}
for i in g.nodes():
config.add_node_configuration("com", i, node_to_com[i])
for e in g.edges:
config.add_edge_configuration("threshold", e, threshold)
else:
node_to_com = {n: random.choice([0, 1]) for n in g.vs['name']}
for i in g.vs['name']:
config.add_node_configuration("com", i, node_to_com[i])
edges = [(g.vs[e.tuple[0]]['name'], g.vs[e.tuple[1]]['name']) for e in g.es]
for e in edges:
config.add_edge_configuration("threshold", e, threshold)

config.add_model_parameter('permeability', 0.1)

model.set_initial_status(config)
iterations = model.iteration_bunch(10)
self.assertEqual(len(iterations), 10)
iterations = model.iteration_bunch(10, node_status=False)
self.assertEqual(len(iterations), 10)

def test_kertesz_model_predefined_blocked(self):
for g in get_graph(True):
model = epd.KerteszThresholdModel(g)
Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ mock==3.0.5
python-igraph
netdispatch==0.0.3
sphinx_rtd_theme==0.4.3
numpy==1.17.1
future==0.17.1
numpy==1.17.*
future==0.17.*
networkx==2.3
dynetx==0.2.2
bokeh==1.3.4
matplotlib==3.0.3
pytest==5.1.1
matplotlib==3.0.*
pytest==5.1.*
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


setup(name='ndlib',
version='5.0.0',
version='5.0.1',
license='BSD-Clause-2',
description='Network Diffusion Library',
url='https://github.com/GiulioRossetti/ndlib',
Expand Down

0 comments on commit 8fc907e

Please sign in to comment.