From fbe443d2be215f117681725228cd1e9447eac67e Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 5 Aug 2020 00:01:58 +0200 Subject: [PATCH 01/25] Add Continuous model file that allows for custom models with continuous statuses. Change delta function to track continuous variable changes and keep track of amounts. Update NodeNumericalAttribute compartiment to allow the testing of two attributes to each other. --- SIR_example.py | 39 ++++++++ continuous_custom_var.py | 54 ++++++++++ ndlib/models/ContinuousModel.py | 99 +++++++++++++++++++ ndlib/models/DiffusionModel.py | 29 +++++- .../compartments/NodeNumericalAttribute.py | 14 ++- 5 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 SIR_example.py create mode 100644 continuous_custom_var.py create mode 100644 ndlib/models/ContinuousModel.py diff --git a/SIR_example.py b/SIR_example.py new file mode 100644 index 0000000..81014ae --- /dev/null +++ b/SIR_example.py @@ -0,0 +1,39 @@ +import networkx as nx + +from ndlib_custom.ndlib.models.CompositeModel import CompositeModel +from ndlib_custom.ndlib.models.compartments.NodeStochastic import NodeStochastic +import ndlib_custom.ndlib.models.ModelConfig as mc + +from bokeh.io import show +from ndlib_custom.ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend + +# Network definition +g1 = nx.erdos_renyi_graph(n=1000, p=0.1) + +# Model definition +SIR = CompositeModel(g1) +SIR.add_status('Susceptible') +SIR.add_status('Infected') +SIR.add_status('Removed') + +# Compartments +c1 = NodeStochastic(rate=0.02, triggering_status='Infected') +c2 = NodeStochastic(rate=0.01) + +# Rules +SIR.add_rule('Susceptible', 'Infected', c1) +SIR.add_rule('Infected', 'Removed', c2) + +# Configuration +config = mc.Configuration() +config.add_model_parameter('percentage_infected', 0.1) +SIR.set_initial_status(config) + +# Simulation +iterations = SIR.iteration_bunch(300, node_status=False) +trends = SIR.build_trends(iterations) + +# Visualization +viz = DiffusionTrend(SIR, trends) +p = viz.plot(width=400, height=400) +show(p) \ No newline at end of file diff --git a/continuous_custom_var.py b/continuous_custom_var.py new file mode 100644 index 0000000..61f43c9 --- /dev/null +++ b/continuous_custom_var.py @@ -0,0 +1,54 @@ +import networkx as nx +import random + +from ndlib.models.CompositeModel import CompositeModel +from ndlib.models.ContinuousModel import ContinuousModel +from ndlib.models.compartments.NodeStochastic import NodeStochastic +from ndlib.models.compartments.NodeNumericalAttribute import NodeNumericalAttribute + +import ndlib.models.ModelConfig as mc + +from bokeh.io import show +from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend + +def intial_status(node, graph): + addiction = 0 + return addiction + +def craving_model(node, graph, current_val, variable): + craving = nx.get_node_attributes(graph, 'craving')[node] + self_control = nx.get_node_attributes(graph, 'self_control')[node] + return min(current_val + craving - self_control, 1) + +# Network definition +g = nx.erdos_renyi_graph(n=10, p=0.1) + +# Extra network setup +attr = {n: {'craving': random.random(), 'self_control': random.random()} for n in g.nodes()} +nx.set_node_attributes(g, attr) + +print('initial nodes:') +for n in g.nodes(data=True): + print(n) +print() + +# Model definition +addiction_model = ContinuousModel(g) +addiction_model.add_status('addiction') + +# Compartments +condition = NodeNumericalAttribute('self_control', value='craving', op='<') + +# Rules +addiction_model.add_rule('addiction', craving_model, condition) + +# Configuration +config = mc.Configuration() +config.add_model_parameter('fraction_infected', 0.1) +addiction_model.set_initial_status(intial_status, config) + +# Simulation +iterations = addiction_model.iteration_bunch(10, node_status=True) +print() +print(iterations) +print() \ No newline at end of file diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py new file mode 100644 index 0000000..ab487f5 --- /dev/null +++ b/ndlib/models/ContinuousModel.py @@ -0,0 +1,99 @@ +from ndlib.models.DiffusionModel import DiffusionModel +import future.utils + +__author__ = 'Mathijs Maijer' +__license__ = "BSD-2-Clause" +__email__ = "m.f.maijer@gmail.com" + + +class ContinuousModel(DiffusionModel): + + def __init__(self, graph): + """ + Model Constructor + :param graph: A networkx graph object + """ + super(self.__class__, self).__init__(graph) + self.compartment = {} + self.compartment_progressive = 0 + self.status_progressive = 0 + + self.discrete_state = False + + self.available_statuses = { + "Infected": 0 + } + + def add_status(self, status_name): + if status_name not in self.available_statuses: + self.available_statuses[status_name] = self.status_progressive + self.status_progressive += 1 + + def add_rule(self, status, function, rule): + self.compartment[self.compartment_progressive] = (status, function, rule) + self.compartment_progressive += 1 + + def set_initial_status(self, initial_status_fun, configuration=None): + """ + Override behaviour of methods in class DiffusionModel. + Overwrites initial status using given function + Generates node profiles + """ + super(ContinuousModel, self).set_initial_status(configuration) + + # set node status + for node in self.status: + self.status[node] = initial_status_fun(node, self.graph) + self.initial_status = self.status.copy() + + def clean_initial_status(self, valid_status=None): + for n, s in future.utils.iteritems(self.status): + if s > 1: + self.status[n] = 1 + elif s < 0: + self.status[n] = 0 + + 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: + u_status = self.status[u] + + # For all rules + for i in range(0, self.compartment_progressive): + # Get and test the condition + rule = self.compartment[i][2] + test = rule.execute(node=u, graph=self.graph, status=self.status, + status_map=self.available_statuses, params=self.params) + if test: + # Update status if test succeeds + val = self.compartment[i][1](u, self.graph, u_status, self.compartment[i][0]) + actual_status[u] = self.available_statuses[self.compartment[i][0]] = val + break + + delta, node_count, status_delta = self.status_delta_continuous(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()} diff --git a/ndlib/models/DiffusionModel.py b/ndlib/models/DiffusionModel.py index 0e0f077..1855895 100644 --- a/ndlib/models/DiffusionModel.py +++ b/ndlib/models/DiffusionModel.py @@ -135,7 +135,7 @@ def set_initial_status(self, configuration): for param, node_to_value in future.utils.iteritems(nodes_cfg): if len(node_to_value) < len(self.graph.nodes): raise ConfigurationException({"message": "Not all nodes have a configuration specified"}) - + self.params['nodes'][param] = node_to_value edges_cfg = configuration.get_edges_configuration() @@ -305,7 +305,7 @@ def status_delta(self, actual_status): if v != actual_status[n]: delta[n] = actual_status[n] - for st in self.available_statuses.values(): + for st in list(self.available_statuses.values()): actual_status_count[st] = len([x for x in actual_status if actual_status[x] == st]) old_status_count[st] = len([x for x in self.status if self.status[x] == st]) @@ -313,6 +313,31 @@ def status_delta(self, actual_status): return delta, actual_status_count, status_delta + def status_delta_continuous(self, actual_status): + """ + Compute the point-to-point variations for each status w.r.t. the previous system configuration + + Should be used for continuous statuses instead of discrete values + + :param actual_status: the actual simulation status + :return: nodes that have changed their statuses (dictionary status->nodes), + count of actual nodes per status (dictionary status->node count), + delta of nodes per status w.r.t the previous configuration (dictionary status->delta) + """ + actual_status_count = {} + delta = {} + status_delta = {} + for n, v in future.utils.iteritems(self.status): + if v != actual_status[n]: + delta[n] = actual_status[n] + status_delta[n] = actual_status[n] - v + + values = list(actual_status.values()) + actual_status_count = dict((l, values.count(l) ) for l in set(values)) + + return delta, actual_status_count, status_delta + + def build_trends(self, iterations): """ Build node status and node delta trends from model iteration bunch diff --git a/ndlib/models/compartments/NodeNumericalAttribute.py b/ndlib/models/compartments/NodeNumericalAttribute.py index 6408cdd..08112ed 100644 --- a/ndlib/models/compartments/NodeNumericalAttribute.py +++ b/ndlib/models/compartments/NodeNumericalAttribute.py @@ -32,20 +32,26 @@ def __init__(self, attribute, value=None, op=None, probability=1, **kwargs): else: if not isinstance(self.attribute_range, int): if not isinstance(self.attribute_range, float): - raise ValueError("A numeric value is required to test the selected condition") + if not isinstance(self.attribute_range, str): + raise ValueError("A numeric value or attribute is required to test the selected condition") else: raise ValueError("The operator provided '%s' is not valid" % operator) def execute(self, node, graph, status, status_map, *args, **kwargs): val = nx.get_node_attributes(graph, self.attribute)[node] + testVal = self.attribute_range + + if isinstance(self.attribute_range, str): + testVal = nx.get_node_attributes(graph, testVal)[node] + p = np.random.random_sample() if self.operator == "IN": - condition = self.__available_operators[self.operator][0](val, self.attribute_range[0]) and \ - self.__available_operators[self.operator][1](val, self.attribute_range[1]) + condition = self.__available_operators[self.operator][0](val, testVal[0]) and \ + self.__available_operators[self.operator][1](val, testVal[1]) else: - condition = self.__available_operators[self.operator](val, self.attribute_range) + condition = self.__available_operators[self.operator](val, testVal) test = condition and p <= self.probability From e3492e8c5311af5a3a1bd59a1df7fe2c346e2b3a Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 5 Aug 2020 15:12:26 +0200 Subject: [PATCH 02/25] Implement basic mean of change per iteration plot --- continuous_custom_var.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/continuous_custom_var.py b/continuous_custom_var.py index 61f43c9..bdd2967 100644 --- a/continuous_custom_var.py +++ b/continuous_custom_var.py @@ -1,5 +1,9 @@ +# TODO Dynamic graph, plots, attribute updating, multiple status implementation (?), optimize speed + import networkx as nx import random +import matplotlib.pyplot as plt +import numpy as np from ndlib.models.CompositeModel import CompositeModel from ndlib.models.ContinuousModel import ContinuousModel @@ -21,7 +25,7 @@ def craving_model(node, graph, current_val, variable): return min(current_val + craving - self_control, 1) # Network definition -g = nx.erdos_renyi_graph(n=10, p=0.1) +g = nx.erdos_renyi_graph(n=100, p=0.1) # Extra network setup attr = {n: {'craving': random.random(), 'self_control': random.random()} for n in g.nodes()} @@ -48,7 +52,23 @@ def craving_model(node, graph, current_val, variable): addiction_model.set_initial_status(intial_status, config) # Simulation -iterations = addiction_model.iteration_bunch(10, node_status=True) +iterations = addiction_model.iteration_bunch(200, node_status=True) + print() print(iterations) -print() \ No newline at end of file +print() + +### Plots / data manipulation +# Mean status delta per iterations +means = [] +for it in iterations: + deltas = list(it['status_delta'].values()) + if len(deltas) > 0: + means.append(sum(deltas) / len(deltas)) + else: + means.append(0) + +x = np.arange(0, len(iterations)) + +plt.plot(x, means) +plt.show() \ No newline at end of file From 4514aa5207a0458a609e25bc0a4f82fd7af7f7b6 Mon Sep 17 00:00:00 2001 From: Mathijs Maijer Date: Thu, 6 Aug 2020 00:24:48 +0200 Subject: [PATCH 03/25] Optimise attribute passing, add support for multiple continuous variable states --- SIR_example.html | 85 +++++++++++++++++++ SIR_example.py | 12 +-- continuous_custom_var.py | 62 ++++++++------ ndlib/models/ContinuousModel.py | 53 +++++++----- ndlib/models/DiffusionModel.py | 21 +++-- .../compartments/NodeNumericalAttribute.py | 6 +- 6 files changed, 175 insertions(+), 64 deletions(-) create mode 100644 SIR_example.html diff --git a/SIR_example.html b/SIR_example.html new file mode 100644 index 0000000..e3dfe2e --- /dev/null +++ b/SIR_example.html @@ -0,0 +1,85 @@ + + + + + + + + + + + Bokeh Plot + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/SIR_example.py b/SIR_example.py index 81014ae..7fc5c3f 100644 --- a/SIR_example.py +++ b/SIR_example.py @@ -1,11 +1,11 @@ import networkx as nx -from ndlib_custom.ndlib.models.CompositeModel import CompositeModel -from ndlib_custom.ndlib.models.compartments.NodeStochastic import NodeStochastic -import ndlib_custom.ndlib.models.ModelConfig as mc +from ndlib.models.CompositeModel import CompositeModel +from ndlib.models.compartments.NodeStochastic import NodeStochastic +import ndlib.models.ModelConfig as mc from bokeh.io import show -from ndlib_custom.ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend +from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend # Network definition g1 = nx.erdos_renyi_graph(n=1000, p=0.1) @@ -36,4 +36,6 @@ # Visualization viz = DiffusionTrend(SIR, trends) p = viz.plot(width=400, height=400) -show(p) \ No newline at end of file +show(p) + +print(SIR.available_statuses) \ No newline at end of file diff --git a/continuous_custom_var.py b/continuous_custom_var.py index bdd2967..3ad47f4 100644 --- a/continuous_custom_var.py +++ b/continuous_custom_var.py @@ -1,4 +1,4 @@ -# TODO Dynamic graph, plots, attribute updating, multiple status implementation (?), optimize speed +# TODO Dynamic graph, more plots, attribute updating, multiple status implementation (?), optimize speed, plotly/gephy visualization of graph import networkx as nx import random @@ -15,41 +15,52 @@ from bokeh.io import show from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend -def intial_status(node, graph): +def initial_addiction(node, graph): addiction = 0 return addiction -def craving_model(node, graph, current_val, variable): - craving = nx.get_node_attributes(graph, 'craving')[node] - self_control = nx.get_node_attributes(graph, 'self_control')[node] +def initial_self_confidence(node, graph): + self_confidence = 1 + return self_confidence + +initial_status = { + 'addiction': initial_addiction, + 'self_confidence': initial_self_confidence +} + +def craving_model(node, graph, status, attributes): + current_val = status[node]['addiction'] + craving = attributes[node]['craving'] + self_control = attributes[node]['self_control'] return min(current_val + craving - self_control, 1) +def self_confidence_impact(node, graph, status, attributes): + return max(status[node]['self_confidence'] - random.uniform(0, 0.2), 0) + # Network definition -g = nx.erdos_renyi_graph(n=100, p=0.1) +g = nx.erdos_renyi_graph(n=10000, p=0.1) # Extra network setup attr = {n: {'craving': random.random(), 'self_control': random.random()} for n in g.nodes()} nx.set_node_attributes(g, attr) -print('initial nodes:') -for n in g.nodes(data=True): - print(n) -print() - # Model definition addiction_model = ContinuousModel(g) addiction_model.add_status('addiction') +addiction_model.add_status('self_confidence') # Compartments -condition = NodeNumericalAttribute('self_control', value='craving', op='<') +condition = NodeNumericalAttribute('self_control', 'craving', op='<') +condition2 = NodeStochastic(0.5) # Rules addiction_model.add_rule('addiction', craving_model, condition) +addiction_model.add_rule('self_confidence', self_confidence_impact, condition2) # Configuration config = mc.Configuration() config.add_model_parameter('fraction_infected', 0.1) -addiction_model.set_initial_status(intial_status, config) +addiction_model.set_initial_status(initial_status, config) # Simulation iterations = addiction_model.iteration_bunch(200, node_status=True) @@ -59,16 +70,17 @@ def craving_model(node, graph, current_val, variable): print() ### Plots / data manipulation + # Mean status delta per iterations -means = [] -for it in iterations: - deltas = list(it['status_delta'].values()) - if len(deltas) > 0: - means.append(sum(deltas) / len(deltas)) - else: - means.append(0) - -x = np.arange(0, len(iterations)) - -plt.plot(x, means) -plt.show() \ No newline at end of file +# means = [] +# for it in iterations: +# deltas = list(it['status_delta'].values()) +# if len(deltas) > 0: +# means.append(sum(deltas) / len(deltas)) +# else: +# means.append(0) + +# x = np.arange(0, len(iterations)) + +# plt.plot(x, means) +# plt.show() \ No newline at end of file diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index ab487f5..8fe5b05 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,5 +1,6 @@ from ndlib.models.DiffusionModel import DiffusionModel import future.utils +import copy __author__ = 'Mathijs Maijer' __license__ = "BSD-2-Clause" @@ -33,7 +34,7 @@ def add_rule(self, status, function, rule): self.compartment[self.compartment_progressive] = (status, function, rule) self.compartment_progressive += 1 - def set_initial_status(self, initial_status_fun, configuration=None): + def set_initial_status(self, initial_status_funs, configuration=None): """ Override behaviour of methods in class DiffusionModel. Overwrites initial status using given function @@ -43,15 +44,20 @@ def set_initial_status(self, initial_status_fun, configuration=None): # set node status for node in self.status: - self.status[node] = initial_status_fun(node, self.graph) - self.initial_status = self.status.copy() + status = {} + for status_fun in initial_status_funs.items(): + status[status_fun[0]] = status_fun[1](node, self.graph) + self.status[node] = status + + self.initial_status = copy.deepcopy(self.status) def clean_initial_status(self, valid_status=None): for n, s in future.utils.iteritems(self.status): - if s > 1: - self.status[n] = 1 - elif s < 0: - self.status[n] = 0 + for var_val in s.items(): + if var_val[1] > 1: + self.status[n][var_val[0]] = 1 + elif var_val[1] < 0: + self.status[n][var_val[0]] = 0 def iteration(self, node_status=True): """ @@ -59,41 +65,44 @@ def iteration(self, node_status=True): :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)} + self.clean_initial_status() + + # actual_status = {node: nstatus for node, nstatus in future.utils.iteritems(self.status)} + actual_status = copy.deepcopy(self.status) if self.actual_iteration == 0: self.actual_iteration += 1 - delta, node_count, status_delta = self.status_delta(actual_status) + delta, status_delta = self.status_delta_continuous(self.status) if node_status: - return {"iteration": 0, "status": actual_status.copy(), - "node_count": node_count.copy(), "status_delta": status_delta.copy()} + return {"iteration": 0, "status": copy.deepcopy(self.status), + "status_delta": copy.deepcopy(status_delta)} else: return {"iteration": 0, "status": {}, - "node_count": node_count.copy(), "status_delta": status_delta.copy()} + "status_delta": copy.deepcopy(status_delta)} + + nodes_data = self.graph.nodes(data=True) for u in self.graph.nodes: - u_status = self.status[u] # For all rules for i in range(0, self.compartment_progressive): # Get and test the condition rule = self.compartment[i][2] test = rule.execute(node=u, graph=self.graph, status=self.status, - status_map=self.available_statuses, params=self.params) + status_map=self.available_statuses, attributes=nodes_data, + params=self.params) if test: # Update status if test succeeds - val = self.compartment[i][1](u, self.graph, u_status, self.compartment[i][0]) - actual_status[u] = self.available_statuses[self.compartment[i][0]] = val - break + val = self.compartment[i][1](u, self.graph, self.status, nodes_data) + actual_status[u][self.compartment[i][0]] = val - delta, node_count, status_delta = self.status_delta_continuous(actual_status) + delta, status_delta = self.status_delta_continuous(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()} + return {"iteration": self.actual_iteration - 1, "status": copy.deepcopy(delta), + "status_delta": copy.deepcopy(status_delta)} else: return {"iteration": self.actual_iteration - 1, "status": {}, - "node_count": node_count.copy(), "status_delta": status_delta.copy()} + "status_delta": copy.deepcopy(status_delta)} diff --git a/ndlib/models/DiffusionModel.py b/ndlib/models/DiffusionModel.py index 1855895..a80475e 100644 --- a/ndlib/models/DiffusionModel.py +++ b/ndlib/models/DiffusionModel.py @@ -324,18 +324,21 @@ def status_delta_continuous(self, actual_status): count of actual nodes per status (dictionary status->node count), delta of nodes per status w.r.t the previous configuration (dictionary status->delta) """ - actual_status_count = {} delta = {} status_delta = {} for n, v in future.utils.iteritems(self.status): - if v != actual_status[n]: - delta[n] = actual_status[n] - status_delta[n] = actual_status[n] - v - - values = list(actual_status.values()) - actual_status_count = dict((l, values.count(l) ) for l in set(values)) - - return delta, actual_status_count, status_delta + delta[n] = {} + status_delta[n] = {} + for var, val in v.items(): + if val != actual_status[n][var]: + delta[n][var] = actual_status[n][var] + status_delta[n][var] = actual_status[n][var] - val + if len(delta[n].values()) == 0: + del delta[n] + if len(status_delta[n].values()) == 0: + del status_delta[n] + + return delta, status_delta def build_trends(self, iterations): diff --git a/ndlib/models/compartments/NodeNumericalAttribute.py b/ndlib/models/compartments/NodeNumericalAttribute.py index 08112ed..ef13d98 100644 --- a/ndlib/models/compartments/NodeNumericalAttribute.py +++ b/ndlib/models/compartments/NodeNumericalAttribute.py @@ -37,13 +37,13 @@ def __init__(self, attribute, value=None, op=None, probability=1, **kwargs): else: raise ValueError("The operator provided '%s' is not valid" % operator) - def execute(self, node, graph, status, status_map, *args, **kwargs): + def execute(self, node, graph, status, status_map, attributes, *args, **kwargs): - val = nx.get_node_attributes(graph, self.attribute)[node] + val = attributes[node][self.attribute] testVal = self.attribute_range if isinstance(self.attribute_range, str): - testVal = nx.get_node_attributes(graph, testVal)[node] + testVal = attributes[node][testVal] p = np.random.random_sample() From 04d36ef51ed07a9fb9175c66da27ef79316bd974 Mon Sep 17 00:00:00 2001 From: Mathijs Maijer Date: Thu, 6 Aug 2020 01:54:43 +0200 Subject: [PATCH 04/25] Add numerical variable class that can be used to test a variable or attribute against another variable, attribute, or numerical value. Add enumeration interface for type checking. --- continuous_custom_var.py | 11 +-- .../compartments/NodeNumericalAttribute.py | 18 ++--- .../compartments/NodeNumericalVariable.py | 80 +++++++++++++++++++ .../compartments/enums/NumericalType.py | 9 +++ 4 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 ndlib/models/compartments/NodeNumericalVariable.py create mode 100644 ndlib/models/compartments/enums/NumericalType.py diff --git a/continuous_custom_var.py b/continuous_custom_var.py index 3ad47f4..f803145 100644 --- a/continuous_custom_var.py +++ b/continuous_custom_var.py @@ -1,4 +1,4 @@ -# TODO Dynamic graph, more plots, attribute updating, multiple status implementation (?), optimize speed, plotly/gephy visualization of graph +# TODO Dynamic graph, more plots, optimize speed (?), plotly/gephy visualization of graph import networkx as nx import random @@ -8,7 +8,8 @@ from ndlib.models.CompositeModel import CompositeModel from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic -from ndlib.models.compartments.NodeNumericalAttribute import NodeNumericalAttribute +from ndlib.models.compartments.enums.NumericalType import NumericalType +from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable import ndlib.models.ModelConfig as mc @@ -35,7 +36,7 @@ def craving_model(node, graph, status, attributes): return min(current_val + craving - self_control, 1) def self_confidence_impact(node, graph, status, attributes): - return max(status[node]['self_confidence'] - random.uniform(0, 0.2), 0) + return max(status[node]['self_confidence'] - random.uniform(0.2, 0.5), 0) # Network definition g = nx.erdos_renyi_graph(n=10000, p=0.1) @@ -50,8 +51,8 @@ def self_confidence_impact(node, graph, status, attributes): addiction_model.add_status('self_confidence') # Compartments -condition = NodeNumericalAttribute('self_control', 'craving', op='<') -condition2 = NodeStochastic(0.5) +condition = NodeNumericalVariable('self_control', var_type=NumericalType.ATTRIBUTE, value='craving', value_type=NumericalType.ATTRIBUTE, op='<') +condition2 = NodeNumericalVariable('addiction', var_type=NumericalType.STATUS, value=1, op='==') # Rules addiction_model.add_rule('addiction', craving_model, condition) diff --git a/ndlib/models/compartments/NodeNumericalAttribute.py b/ndlib/models/compartments/NodeNumericalAttribute.py index ef13d98..6408cdd 100644 --- a/ndlib/models/compartments/NodeNumericalAttribute.py +++ b/ndlib/models/compartments/NodeNumericalAttribute.py @@ -32,26 +32,20 @@ def __init__(self, attribute, value=None, op=None, probability=1, **kwargs): else: if not isinstance(self.attribute_range, int): if not isinstance(self.attribute_range, float): - if not isinstance(self.attribute_range, str): - raise ValueError("A numeric value or attribute is required to test the selected condition") + raise ValueError("A numeric value is required to test the selected condition") else: raise ValueError("The operator provided '%s' is not valid" % operator) - def execute(self, node, graph, status, status_map, attributes, *args, **kwargs): - - val = attributes[node][self.attribute] - testVal = self.attribute_range - - if isinstance(self.attribute_range, str): - testVal = attributes[node][testVal] + def execute(self, node, graph, status, status_map, *args, **kwargs): + val = nx.get_node_attributes(graph, self.attribute)[node] p = np.random.random_sample() if self.operator == "IN": - condition = self.__available_operators[self.operator][0](val, testVal[0]) and \ - self.__available_operators[self.operator][1](val, testVal[1]) + condition = self.__available_operators[self.operator][0](val, self.attribute_range[0]) and \ + self.__available_operators[self.operator][1](val, self.attribute_range[1]) else: - condition = self.__available_operators[self.operator](val, testVal) + condition = self.__available_operators[self.operator](val, self.attribute_range) test = condition and p <= self.probability diff --git a/ndlib/models/compartments/NodeNumericalVariable.py b/ndlib/models/compartments/NodeNumericalVariable.py new file mode 100644 index 0000000..a769823 --- /dev/null +++ b/ndlib/models/compartments/NodeNumericalVariable.py @@ -0,0 +1,80 @@ +from ndlib.models.compartments.Compartment import Compartiment +from ndlib.models.compartments.enums.NumericalType import NumericalType +import networkx as nx +import numpy as np +import operator + +__author__ = 'Mathijs Maijer' +__license__ = "BSD-2-Clause" +__email__ = "m.f.maijer@gmail.com" + + +class NodeNumericalVariable(Compartiment): + + def __init__(self, var, var_type=None, value=None, value_type=None, op=None, probability=1, **kwargs): + super(self.__class__, self).__init__(kwargs) + self.__available_operators = {"==": operator.__eq__, "<": operator.__lt__, + ">": operator.__gt__, "<=": operator.__le__, + ">=": operator.__ge__, "!=": operator.__ne__, + "IN": (operator.__ge__, operator.__le__)} + + self.variable = var + self.variable_type = var_type + self.value = value + self.value_type = value_type + self.probability = probability + self.operator = op + + if not isinstance(self.variable, str): + raise ValueError("The variable should be a string pointing to an attribute or status") + if self.variable_type is None: + raise ValueError("A type must be provided for the variable") + if not isinstance(self.variable_type, NumericalType): + raise ValueError("The provided variable type is not valid") + if self.value_type and not isinstance(self.value_type, NumericalType): + raise ValueError("The provided value type is not valid") + if self.value is None: + raise ValueError("A value must be provided") + + if self.operator is not None and self.operator in self.__available_operators: + if self.operator == "IN": + if not isinstance(self.value, list) or self.value[1] < self.value[0]: + raise ValueError("A range list is required to test IN condition") + else: + if self.value_type == None: + if not isinstance(self.value, int): + if not isinstance(self.value, float): + raise ValueError("When no value type is defined, the value should be numerical") + else: + if not isinstance(self.value, str): + raise ValueError("The value should be a string pointing to an attribute or status when value type is set") + else: + raise ValueError("The operator provided '%s' is not valid" % operator) + + def execute(self, node, graph, status, status_map, attributes, *args, **kwargs): + if self.variable_type == NumericalType.STATUS: + val = status[node][self.variable] + elif self.variable_type == NumericalType.ATTRIBUTE: + val = attributes[node][self.variable] + + testVal = self.value + + if self.value_type == NumericalType.STATUS: + testVal = status[node][self.value] + elif self.value_type == NumericalType.ATTRIBUTE: + testVal = attributes[node][self.value] + + p = np.random.random_sample() + + if self.operator == "IN": + condition = self.__available_operators[self.operator][0](val, testVal[0]) and \ + self.__available_operators[self.operator][1](val, testVal[1]) + else: + condition = self.__available_operators[self.operator](val, testVal) + + test = condition and p <= self.probability + + if test: + return self.compose(node, graph, status, status_map, kwargs) + + return False diff --git a/ndlib/models/compartments/enums/NumericalType.py b/ndlib/models/compartments/enums/NumericalType.py new file mode 100644 index 0000000..cb1b22f --- /dev/null +++ b/ndlib/models/compartments/enums/NumericalType.py @@ -0,0 +1,9 @@ +__author__ = 'Mathijs Maijer' +__license__ = "BSD-2-Clause" +__email__ = "m.f.maijer@gmail.com" + +from enum import Enum + +class NumericalType(Enum): + STATUS = 0 + ATTRIBUTE = 1 \ No newline at end of file From 8e05372f7f8a38da97c7a6ce7faffe9f99f44ddf Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Fri, 7 Aug 2020 01:15:53 +0200 Subject: [PATCH 05/25] Create basic visualization of iteration data --- continuous_custom_var.py | 20 +------ ndlib/models/ContinuousModel.py | 97 +++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/continuous_custom_var.py b/continuous_custom_var.py index f803145..e0250b0 100644 --- a/continuous_custom_var.py +++ b/continuous_custom_var.py @@ -2,7 +2,6 @@ import networkx as nx import random -import matplotlib.pyplot as plt import numpy as np from ndlib.models.CompositeModel import CompositeModel @@ -66,22 +65,7 @@ def self_confidence_impact(node, graph, status, attributes): # Simulation iterations = addiction_model.iteration_bunch(200, node_status=True) -print() -print(iterations) -print() +trends = addiction_model.build_trends(iterations) ### Plots / data manipulation - -# Mean status delta per iterations -# means = [] -# for it in iterations: -# deltas = list(it['status_delta'].values()) -# if len(deltas) > 0: -# means.append(sum(deltas) / len(deltas)) -# else: -# means.append(0) - -# x = np.arange(0, len(iterations)) - -# plt.plot(x, means) -# plt.show() \ No newline at end of file +addiction_model.visualize(trends, len(iterations), delta=True) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 8fe5b05..33fc8e8 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,6 +1,8 @@ from ndlib.models.DiffusionModel import DiffusionModel import future.utils +import matplotlib.pyplot as plt import copy +import numpy as np __author__ = 'Mathijs Maijer' __license__ = "BSD-2-Clause" @@ -106,3 +108,98 @@ def iteration(self, node_status=True): else: return {"iteration": self.actual_iteration - 1, "status": {}, "status_delta": copy.deepcopy(status_delta)} + + def get_mean_data(self, iterations, mean_type): + mean_changes = {} + for key in self.available_statuses.keys(): + mean_changes[key] = [] + del mean_changes['Infected'] # Todo, fix this line so that infected isn't even in available statuses + + for it in iterations: + delta = {} + + vals = list(it[mean_type].values()) + + for val in vals: + for key, v in val.items(): + if key not in delta.keys(): + delta[key] = {'v': v, 'n': 1} + else: + delta[key]['v'] += v + delta[key]['n'] += 1 + for key in mean_changes.keys(): + if key not in delta.keys(): + delta[key] = {} + delta[key]['v'] = 0 + delta[key]['n'] = 1 + + for k, v in delta.items(): + if k not in mean_changes.keys(): + mean_changes[k] = [] + mean_changes[k].append(delta[k]['v'] / delta[k]['n']) + + return mean_changes + + def build_full_status(self, iterations): + statuses = [] + status = {'iteration': 0, 'status': {}} + for key, val in iterations[0]['status'].items(): + status['status'][key] = val + statuses.append(status) + for it in iterations[1:]: + i = it['iteration'] + status = copy.deepcopy(statuses[-1]) + for node, d in it['status'].items(): + for var, val in d.items(): + status['status'][node][var] = val + status['iteration'] = i + statuses.append(status) + + return statuses + + def get_means(self, iterations): + full_status = self.build_full_status(iterations) + means = self.get_mean_data(full_status, 'status') + return means + + def build_trends(self, iterations): + """ + Overwrite build trends of diffusionmodel + """ + means = self.get_means(iterations) + means_status_delta_vals = self.get_mean_data(iterations, 'status') + status_delta = self.get_mean_data(iterations, 'status_delta') + + return {'mean_delta_status_vals': means_status_delta_vals, 'status_delta': status_delta, 'means': means} + + def visualize(self, trends, n, delta=None, delta_mean=None): + x = np.arange(0, n) + + sub_plots = 1 + if delta: + sub_plots = 2 if (delta and not delta_mean) else 3 + + fig, axs = plt.subplots(sub_plots) + + # Mean status delta per iterations + for status, values in trends['means'].items(): + axs[0].plot(x, values, label=status) + axs[0].set_title("Mean values per variable per iteration") + axs[0].legend() + + + if delta: + for status, values in trends['status_delta'].items(): + axs[1].plot(x, values, label=status) + axs[1].set_title("Mean change per variable per iteration") + axs[1].legend() + + i = 2 if delta else 1 + + if delta_mean: + for status, values in trends['mean_delta_status_vals'].items(): + axs[i].plot(x, values, label=status) + axs[i].set_title("Mean value of changed variables per iteration") + axs[i].legend() + + plt.show() \ No newline at end of file From 8f530c26278dfe622232394b53ca2981bc0f8126 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Fri, 7 Aug 2020 20:25:35 +0200 Subject: [PATCH 06/25] Optimize continuous model to allow constant variables and using ints or floats to set initial values. Implement self-control vs craving model from Grasman --- craving_vs_self_control.py | 117 ++++++++++++++++++ ...var.py => example_continuous_custom_var.py | 10 +- ndlib/models/ContinuousModel.py | 38 ++++-- 3 files changed, 151 insertions(+), 14 deletions(-) create mode 100644 craving_vs_self_control.py rename continuous_custom_var.py => example_continuous_custom_var.py (88%) diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py new file mode 100644 index 0000000..a1df7d1 --- /dev/null +++ b/craving_vs_self_control.py @@ -0,0 +1,117 @@ +# TODO Dynamic graph, more plots, optimize speed (?), plotly/gephy visualization of graph + +import networkx as nx +import random +import numpy as np +import matplotlib.pyplot as plt + +from ndlib.models.CompositeModel import CompositeModel +from ndlib.models.ContinuousModel import ContinuousModel +from ndlib.models.compartments.NodeStochastic import NodeStochastic +from ndlib.models.compartments.enums.NumericalType import NumericalType +from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable + +import ndlib.models.ModelConfig as mc + +from bokeh.io import show +from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend + +constants = { + 'q': 0.8, + 'b': 0.5, + 'd': 0.2, + 'h': 0.2, + 'k': 0.25, + 'S+': 0.5, +} +constants['p'] = 2*constants['d'] + +def initial_v(node, graph, status, constants): + return min(1, max(0, status['C']-status['S']-status['E'])) + +def initial_a(node, graph, status, constants): + return constants['q'] * status['V'] + (np.random.poisson(status['labda'])/7) + +initial_status = { + 'C': 0, + 'S': constants['S+'], + 'E': 1, + 'V': initial_v, + 'labda': 0.5, + 'A': initial_a +} + +def update_C(node, graph, status, attributes, constants): + return status[node]['C'] + constants['b'] * status[node]['A'] * min(1, 1-status[node]['C']) - constants['d'] * status[node]['C'] + +def update_S(node, graph, status, attributes, constants): + return status[node]['S'] + constants['p'] * max(0, constants['S+'] - status[node]['S']) - constants['h'] * status[node]['C'] - constants['k'] * status[node]['A'] + +def update_E(node, graph, status, attributes, constants): + return status[node]['E'] - 0.015 + +def update_V(node, graph, status, attributes, constants): + return min(1, max(0, status[node]['C']-status[node]['S']-status[node]['E'])) + +def update_labda(node, graph, status, attributes, constants): + return status[node]['labda'] + 0.01 + +def update_A(node, graph, status, attributes, constants): + return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['labda'])/7), constants['q']*(1 - status[node]['V'])) + +# Network definition +g = nx.erdos_renyi_graph(n=1, p=0.1) + +# Model definition +craving_control_model = ContinuousModel(g, constants=constants) +craving_control_model.add_status('C') +craving_control_model.add_status('S') +craving_control_model.add_status('E') +craving_control_model.add_status('V') +craving_control_model.add_status('labda') +craving_control_model.add_status('A') + +# Compartments +condition = NodeStochastic(1) + +# Rules +craving_control_model.add_rule('C', update_C, condition) +craving_control_model.add_rule('S', update_S, condition) +craving_control_model.add_rule('E', update_E, condition) +craving_control_model.add_rule('V', update_V, condition) +craving_control_model.add_rule('labda', update_labda, condition) +craving_control_model.add_rule('A', update_A, condition) + +# Configuration +config = mc.Configuration() +config.add_model_parameter('fraction_infected', 0.1) +craving_control_model.set_initial_status(initial_status, config) + +# Simulation +iterations = craving_control_model.iteration_bunch(100, node_status=True) +trends = craving_control_model.build_trends(iterations) + +### Plots / data manipulation + +# craving_control_model.visualize(trends, len(iterations), delta=True) + + +x = np.arange(0, len(iterations)) +plt.figure() + +plt.subplot(221) +plt.plot(x, trends['means']['E'], label='E') +plt.plot(x, trends['means']['labda'], label='labda') +plt.legend() + +plt.subplot(222) +plt.plot(x, trends['means']['A'], label='A') +plt.plot(x, trends['means']['C'], label='C') +plt.legend() + +plt.subplot(223) +plt.plot(x, trends['means']['S'], label='S') +plt.plot(x, trends['means']['V'], label='V') +plt.legend() + +plt.show() \ No newline at end of file diff --git a/continuous_custom_var.py b/example_continuous_custom_var.py similarity index 88% rename from continuous_custom_var.py rename to example_continuous_custom_var.py index e0250b0..3b63120 100644 --- a/continuous_custom_var.py +++ b/example_continuous_custom_var.py @@ -15,11 +15,11 @@ from bokeh.io import show from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend -def initial_addiction(node, graph): +def initial_addiction(node, graph, status, constants): addiction = 0 return addiction -def initial_self_confidence(node, graph): +def initial_self_confidence(node, graph, status, constants): self_confidence = 1 return self_confidence @@ -28,17 +28,17 @@ def initial_self_confidence(node, graph): 'self_confidence': initial_self_confidence } -def craving_model(node, graph, status, attributes): +def craving_model(node, graph, status, attributes, constants): current_val = status[node]['addiction'] craving = attributes[node]['craving'] self_control = attributes[node]['self_control'] return min(current_val + craving - self_control, 1) -def self_confidence_impact(node, graph, status, attributes): +def self_confidence_impact(node, graph, status, attributes, constants): return max(status[node]['self_confidence'] - random.uniform(0.2, 0.5), 0) # Network definition -g = nx.erdos_renyi_graph(n=10000, p=0.1) +g = nx.erdos_renyi_graph(n=1000, p=0.1) # Extra network setup attr = {n: {'craving': random.random(), 'self_control': random.random()} for n in g.nodes()} diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 33fc8e8..49eaff3 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -11,7 +11,7 @@ class ContinuousModel(DiffusionModel): - def __init__(self, graph): + def __init__(self, graph, constants=None, clean_status=None): """ Model Constructor :param graph: A networkx graph object @@ -27,6 +27,10 @@ def __init__(self, graph): "Infected": 0 } + self.clean = True if clean_status else False + + self.constants = constants + def add_status(self, status_name): if status_name not in self.available_statuses: self.available_statuses[status_name] = self.status_progressive @@ -44,11 +48,20 @@ def set_initial_status(self, initial_status_funs, configuration=None): """ super(ContinuousModel, self).set_initial_status(configuration) + if not isinstance(initial_status_funs, dict): + raise ValueError('The initial status should be a dictionary of form status (str): value (int/float/function)') + # set node status for node in self.status: status = {} for status_fun in initial_status_funs.items(): - status[status_fun[0]] = status_fun[1](node, self.graph) + if hasattr(status_fun[1], '__call__'): + status[status_fun[0]] = status_fun[1](node, self.graph, status, self.constants) + continue + if not isinstance(status_fun[1], float): + if not isinstance(status_fun[1], int): + raise ValueError('The initial status should be a function, integer, or float') + status[status_fun[0]] = status_fun[1] self.status[node] = status self.initial_status = copy.deepcopy(self.status) @@ -67,7 +80,8 @@ def iteration(self, node_status=True): :return: Iteration_id, Incremental node status (dictionary node->status) """ - self.clean_initial_status() + if self.clean: + self.clean_initial_status() # actual_status = {node: nstatus for node, nstatus in future.utils.iteritems(self.status)} actual_status = copy.deepcopy(self.status) @@ -92,10 +106,10 @@ def iteration(self, node_status=True): rule = self.compartment[i][2] test = rule.execute(node=u, graph=self.graph, status=self.status, status_map=self.available_statuses, attributes=nodes_data, - params=self.params) + params=self.params, constants=self.constants) if test: # Update status if test succeeds - val = self.compartment[i][1](u, self.graph, self.status, nodes_data) + val = self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) actual_status[u][self.compartment[i][0]] = val delta, status_delta = self.status_delta_continuous(actual_status) @@ -182,10 +196,16 @@ def visualize(self, trends, n, delta=None, delta_mean=None): fig, axs = plt.subplots(sub_plots) # Mean status delta per iterations - for status, values in trends['means'].items(): - axs[0].plot(x, values, label=status) - axs[0].set_title("Mean values per variable per iteration") - axs[0].legend() + if delta or delta_mean: + for status, values in trends['means'].items(): + axs[0].plot(x, values, label=status) + axs[0].set_title("Mean values per variable per iteration") + axs[0].legend() + else: + for status, values in trends['means'].items(): + plt.plot(x, values, label=status) + plt.title("Mean values per variable per iteration") + plt.legend() if delta: From 10b135058516ab93c9599f3b1fcdd7da55087327 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 12 Aug 2020 03:17:03 +0200 Subject: [PATCH 07/25] Add network visualization, animation, and configuration --- craving_vs_self_control.py | 65 +++++++--- ndlib/models/ContinuousModel.py | 216 +++++++++++++++++++++++++++++++- 2 files changed, 262 insertions(+), 19 deletions(-) diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py index a1df7d1..5580af5 100644 --- a/craving_vs_self_control.py +++ b/craving_vs_self_control.py @@ -1,4 +1,5 @@ -# TODO Dynamic graph, more plots, optimize speed (?), plotly/gephy visualization of graph +# TODO Dynamic graph, integrate with ndlib correctly +# Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido import networkx as nx import random @@ -16,6 +17,10 @@ from bokeh.io import show from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend + + +################### MODEL SPECIFICATIONS ################### + constants = { 'q': 0.8, 'b': 0.5, @@ -30,14 +35,14 @@ def initial_v(node, graph, status, constants): return min(1, max(0, status['C']-status['S']-status['E'])) def initial_a(node, graph, status, constants): - return constants['q'] * status['V'] + (np.random.poisson(status['labda'])/7) + return constants['q'] * status['V'] + (np.random.poisson(status['lambda'])/7) initial_status = { 'C': 0, 'S': constants['S+'], 'E': 1, 'V': initial_v, - 'labda': 0.5, + 'lambda': 0.5, 'A': initial_a } @@ -48,27 +53,48 @@ def update_S(node, graph, status, attributes, constants): return status[node]['S'] + constants['p'] * max(0, constants['S+'] - status[node]['S']) - constants['h'] * status[node]['C'] - constants['k'] * status[node]['A'] def update_E(node, graph, status, attributes, constants): - return status[node]['E'] - 0.015 + # return status[node]['E'] - 0.015 # Grasman calculation + + avg_neighbor_addiction = 0 + for n in graph.neighbors(node): + avg_neighbor_addiction += status[n]['A'] + + return max(-1.5, status[node]['E'] - avg_neighbor_addiction / 50) # Custom calculation def update_V(node, graph, status, attributes, constants): return min(1, max(0, status[node]['C']-status[node]['S']-status[node]['E'])) -def update_labda(node, graph, status, attributes, constants): - return status[node]['labda'] + 0.01 +def update_lambda(node, graph, status, attributes, constants): + return status[node]['lambda'] + 0.01 def update_A(node, graph, status, attributes, constants): - return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['labda'])/7), constants['q']*(1 - status[node]['V'])) + return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) + + + + +################### MODEL CONFIGURATION ################### # Network definition -g = nx.erdos_renyi_graph(n=1, p=0.1) +g = nx.random_geometric_graph(200, 0.125) + +# Visualization config +visualization_config = { + 'plot_interval': 5, + 'plot_variable': 'A', + 'save_plot': True, + 'plot_output': './animations/gg/network.gif', + 'plot_title': 'Self control vs craving simulation', + 'plot_annotation': 'The dynamics of addiction: Craving versus self-control, Johan Grasman, Raoul P.P.P. Grasman, Han L.J. van der Maas (2006)' +} # Model definition -craving_control_model = ContinuousModel(g, constants=constants) +craving_control_model = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config) craving_control_model.add_status('C') craving_control_model.add_status('S') craving_control_model.add_status('E') craving_control_model.add_status('V') -craving_control_model.add_status('labda') +craving_control_model.add_status('lambda') craving_control_model.add_status('A') # Compartments @@ -79,7 +105,7 @@ def update_A(node, graph, status, attributes, constants): craving_control_model.add_rule('S', update_S, condition) craving_control_model.add_rule('E', update_E, condition) craving_control_model.add_rule('V', update_V, condition) -craving_control_model.add_rule('labda', update_labda, condition) +craving_control_model.add_rule('lambda', update_lambda, condition) craving_control_model.add_rule('A', update_A, condition) # Configuration @@ -87,21 +113,28 @@ def update_A(node, graph, status, attributes, constants): config.add_model_parameter('fraction_infected', 0.1) craving_control_model.set_initial_status(initial_status, config) + + + +################### SIMULATION ################### + # Simulation iterations = craving_control_model.iteration_bunch(100, node_status=True) trends = craving_control_model.build_trends(iterations) -### Plots / data manipulation -# craving_control_model.visualize(trends, len(iterations), delta=True) +################### VISUALIZATION ################### + +# craving_control_model.plot(trends, len(iterations), delta=True) + x = np.arange(0, len(iterations)) plt.figure() plt.subplot(221) plt.plot(x, trends['means']['E'], label='E') -plt.plot(x, trends['means']['labda'], label='labda') +plt.plot(x, trends['means']['lambda'], label='lambda') plt.legend() plt.subplot(222) @@ -114,4 +147,6 @@ def update_A(node, graph, status, attributes, constants): plt.plot(x, trends['means']['V'], label='V') plt.legend() -plt.show() \ No newline at end of file +plt.show() + +craving_control_model.visualize() diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 49eaff3..986405a 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,8 +1,13 @@ from ndlib.models.DiffusionModel import DiffusionModel import future.utils +import os import matplotlib.pyplot as plt +import plotly.graph_objects as go +from plotly.subplots import make_subplots import copy import numpy as np +from PIL import Image +import io __author__ = 'Mathijs Maijer' __license__ = "BSD-2-Clause" @@ -11,7 +16,7 @@ class ContinuousModel(DiffusionModel): - def __init__(self, graph, constants=None, clean_status=None): + def __init__(self, graph, constants=None, clean_status=None, visualization_configuration=None): """ Model Constructor :param graph: A networkx graph object @@ -31,6 +36,70 @@ def __init__(self, graph, constants=None, clean_status=None): self.constants = constants + if visualization_configuration: + self.configure_visualization(visualization_configuration) + else: + self.visualization_configuration = None + + def configure_visualization(self, visualization_configuration): + if visualization_configuration: + self.visualizations = [] + self.visualization_configuration = visualization_configuration + vis_keys = visualization_configuration.keys() + if 'plot_interval' in vis_keys: + if isinstance(visualization_configuration['plot_interval'], int): + if visualization_configuration['plot_interval'] <= 0: + raise ValueError('plot_interval must be a positive integer') + else: + raise ValueError('plot_interval must be a positive integer') + else: + raise ValueError('plot_interval must be included for visualization') + + if 'plot_variable' in vis_keys: + if not isinstance(visualization_configuration['plot_variable'], str): + raise ValueError('Plot variable must be a string') + else: + self.visualization_configuration['plot_variable'] = None + + self.visualization_configuration['save_plot'] = True if self.visualization_configuration['save_plot'] else False + if self.visualization_configuration['save_plot']: + if 'plot_output' not in vis_keys: + self.visualization_configuration['plot_output'] = './visualization/network.gif' + elif not isinstance(self.visualization_configuration['plot_output'], str): + raise ValueError('plot_output must be a string') + # Todo create regex for plot output + + if 'plot_title' in self.visualization_configuration.keys(): + if not isinstance(self.visualization_configuration['plot_title'], str): + raise ValueError('Plot name must be a string') + else: + vis_var = self.visualization_configuration['plot_variable'] if self.visualization_configuration['plot_variable'] else '# Neighbours' + self.visualization_configuration['plot_title'] = 'Network simulation of ' + vis_var + + if 'plot_annotation' in vis_keys: + if not isinstance(self.visualization_configuration['plot_annotation'], str): + raise ValueError('Plot annotation must be a string') + else: + self.visualization_configuration['plot_annotation'] = None + + if 'cmin' in vis_keys: + if not isinstance(self.visualization_configuration['cmin'], int): + raise ValueError('cmin must be an integer') + else: + self.visualization_configuration['cmin'] = 0 + + if 'cmax' in vis_keys: + if not isinstance(self.visualization_configuration['cmax'], int): + raise ValueError('cmax must be an integer') + else: + self.visualization_configuration['cmax'] = 1 + + if 'color_scale' in vis_keys: + if not isinstance(self.visualization_configuration['color_scale'], str): + raise ValueError('Color scale must be a string') + else: + self.visualization_configuration['color_scale'] = 'YlGnBu' + def add_status(self, status_name): if status_name not in self.available_statuses: self.available_statuses[status_name] = self.status_progressive @@ -86,6 +155,9 @@ def iteration(self, node_status=True): # actual_status = {node: nstatus for node, nstatus in future.utils.iteritems(self.status)} actual_status = copy.deepcopy(self.status) + if self.visualization_configuration and self.actual_iteration % self.visualization_configuration['plot_interval'] == 0: + self.plot_graph() + if self.actual_iteration == 0: self.actual_iteration += 1 delta, status_delta = self.status_delta_continuous(self.status) @@ -186,7 +258,7 @@ def build_trends(self, iterations): return {'mean_delta_status_vals': means_status_delta_vals, 'status_delta': status_delta, 'means': means} - def visualize(self, trends, n, delta=None, delta_mean=None): + def plot(self, trends, n, delta=None, delta_mean=None): x = np.arange(0, n) sub_plots = 1 @@ -196,7 +268,7 @@ def visualize(self, trends, n, delta=None, delta_mean=None): fig, axs = plt.subplots(sub_plots) # Mean status delta per iterations - if delta or delta_mean: + if delta or delta_mean: for status, values in trends['means'].items(): axs[0].plot(x, values, label=status) axs[0].set_title("Mean values per variable per iteration") @@ -222,4 +294,140 @@ def visualize(self, trends, n, delta=None, delta_mean=None): axs[i].set_title("Mean value of changed variables per iteration") axs[i].legend() - plt.show() \ No newline at end of file + plt.show() + + def plot_graph(self): + edge_x = [] + edge_y = [] + for edge in self.graph.edges(): + x0, y0 = self.graph.nodes[edge[0]]['pos'] + x1, y1 = self.graph.nodes[edge[1]]['pos'] + edge_x.append(x0) + edge_x.append(x1) + edge_x.append(None) + edge_y.append(y0) + edge_y.append(y1) + edge_y.append(None) + + edge_trace = go.Scatter( + x=edge_x, y=edge_y, + line=dict(width=0.5, color='#888'), + hoverinfo='none', + mode='lines') + + node_x = [] + node_y = [] + for node in self.graph.nodes(): + x, y = self.graph.nodes[node]['pos'] + node_x.append(x) + node_y.append(y) + + node_trace = go.Scatter( + x=node_x, y=node_y, + mode='markers', + hoverinfo='text', + marker=dict( + showscale=True, + # colorscale options + #'Greys' | 'YlGnBu' | 'Greens' | 'YlOrRd' | 'Bluered' | 'RdBu' | + #'Reds' | 'Blues' | 'Picnic' | 'Rainbow' | 'Portland' | 'Jet' | + #'Hot' | 'Blackbody' | 'Earth' | 'Electric' | 'Viridis' | + colorscale=self.visualization_configuration['color_scale'], + reversescale=True, + color=[], + size=10, + cmin=self.visualization_configuration['cmin'], + cmax=self.visualization_configuration['cmax'], + colorbar=dict( + thickness=15, + title=self.visualization_configuration['plot_variable'], + xanchor='left', + titleside='right', + ), + line_width=2)) + + node_value = [] + node_text = [] + + if self.visualization_configuration['plot_variable']: + for node in self.graph.nodes(): + value = self.status[node][self.visualization_configuration['plot_variable']] + node_value.append(value) + node_text.append(self.visualization_configuration['plot_variable'] + ': ' + str(value)) + else: + for node in self.graph.nodes(): + n_neighbors = len(self.graph.neighbors(node)) + node_value.append(n_neighbors) + node_text.append('# of connections: ' + str(n_neighbors)) + + node_trace.marker.color = node_value + node_trace.text = node_text + + self.visualizations.append([edge_trace, node_trace]) + + def visualize(self): + if not self.visualization_configuration: + print('Visualization stopped because no configuration is defined') + return + self.plot_graph() # Also save last network state + + frames = [] + for v in self.visualizations: + frames.append(go.Frame(data=[v[0], v[1]])) + + fig = go.Figure(data=[self.visualizations[0][0], self.visualizations[0][1]], + layout=go.Layout( + title=self.visualization_configuration['plot_title'], + titlefont_size=16, + showlegend=False, + hovermode='closest', + margin=dict(b=20,l=5,r=5,t=40), + annotations=[ dict( + text=self.visualization_configuration['plot_annotation'], + showarrow=False, + xref="paper", yref="paper", + x=0.005, y=-0.002 ) ], + updatemenus=[dict( + type="buttons", + buttons=[dict(label="Play", + method="animate", + args=[None])])], + xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), + yaxis=dict(showgrid=False, zeroline=False, showticklabels=False) + ), + frames=frames + ) + + fig.show() + + if self.visualization_configuration['save_plot']: + # Create byte images + images = [] + for i, f in enumerate(self.visualizations): + fig = go.Figure(data=[self.visualizations[i][0], self.visualizations[i][1]], + layout=go.Layout( + title=self.visualization_configuration['plot_title'], + titlefont_size=16, + showlegend=False, + hovermode='closest', + margin=dict(b=20,l=5,r=5,t=40), + annotations=[ dict( + text=self.visualization_configuration['plot_annotation'], + showarrow=False, + xref="paper", yref="paper", + x=0.005, y=-0.002 ) ]) + ).to_image(format="png") + + images.append(Image.open(io.BytesIO(fig))) + # Create gif of byte image array + + split_dir = self.visualization_configuration['plot_output'].split('/') + path = '/'.join(split_dir[0:-1]) + filename = split_dir[-1] + + if not os.path.exists(path): + os.makedirs(path) + + images[0].save(self.visualization_configuration['plot_output'], + save_all=True, append_images=images[1:], duration=500) + print('Saved ' + self.visualization_configuration['plot_output']) From 6157a13f0b6c78e13b3eb0395b9bf3de3bf41a35 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 12 Aug 2020 06:25:29 +0200 Subject: [PATCH 08/25] Add support for graph updating using rules --- craving_vs_self_control.py | 34 ++++++++++++++++++++++++++------- ndlib/models/ContinuousModel.py | 23 ++++++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py index 5580af5..869dcdc 100644 --- a/craving_vs_self_control.py +++ b/craving_vs_self_control.py @@ -1,4 +1,4 @@ -# TODO Dynamic graph, integrate with ndlib correctly +# TODO Dynamic graph, integrate with ndlib correctly, plot speed optimizations for large node amount (intermediate images? / matplotlib?), write tests # Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido import networkx as nx @@ -17,8 +17,6 @@ from bokeh.io import show from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend - - ################### MODEL SPECIFICATIONS ################### constants = { @@ -70,20 +68,40 @@ def update_lambda(node, graph, status, attributes, constants): def update_A(node, graph, status, attributes, constants): return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) - - +# 50% chance of severing connections, move to random location, create new random connections +def update_network(node, graph, status, attributes, constants): + sever = [] + for n in graph.neighbors(node): + if random.random() < 0.5: + sever.append(n) + graph.remove_edges(node, sever) + + if random.random() < 0.05: + pos = (random.random(), random.random()) + graph.nodes[node]['pos'] = pos + + while True: + new_connections = [] + if random.random() < 0.5: + new_conn = random.choice(graph.nodes()) + if new_conn not in graph.neighbors(node): + new_connections.append(new_conn) + else: + graph.add_edges(node, new_connections) + return ################### MODEL CONFIGURATION ################### # Network definition -g = nx.random_geometric_graph(200, 0.125) +g = nx.random_geometric_graph(2000, 0.035) +# g = nx.random_geometric_graph(100, 0.125) # Visualization config visualization_config = { 'plot_interval': 5, 'plot_variable': 'A', 'save_plot': True, - 'plot_output': './animations/gg/network.gif', + 'plot_output': './animations/network.gif', 'plot_title': 'Self control vs craving simulation', 'plot_annotation': 'The dynamics of addiction: Craving versus self-control, Johan Grasman, Raoul P.P.P. Grasman, Han L.J. van der Maas (2006)' } @@ -99,6 +117,7 @@ def update_A(node, graph, status, attributes, constants): # Compartments condition = NodeStochastic(1) +condition2 = NodeStochastic(0.005) # Rules craving_control_model.add_rule('C', update_C, condition) @@ -107,6 +126,7 @@ def update_A(node, graph, status, attributes, constants): craving_control_model.add_rule('V', update_V, condition) craving_control_model.add_rule('lambda', update_lambda, condition) craving_control_model.add_rule('A', update_A, condition) +craving_control_model.add_rule('network', update_network, condition2) # Configuration config = mc.Configuration() diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 986405a..6b38ce8 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -180,9 +180,12 @@ def iteration(self, node_status=True): status_map=self.available_statuses, attributes=nodes_data, params=self.params, constants=self.constants) if test: - # Update status if test succeeds - val = self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) - actual_status[u][self.compartment[i][0]] = val + # Update status or network if test succeeds + if self.compartment[i][0] == 'network': + self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) + else: + val = self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) + actual_status[u][self.compartment[i][0]] = val delta, status_delta = self.status_delta_continuous(actual_status) self.status = actual_status @@ -391,7 +394,7 @@ def visualize(self): type="buttons", buttons=[dict(label="Play", method="animate", - args=[None])])], + args=[None, {"frame": {"duration": 500}, "transition": {"duration": 0}}])])], xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False) ), @@ -408,6 +411,8 @@ def visualize(self): layout=go.Layout( title=self.visualization_configuration['plot_title'], titlefont_size=16, + width=1920, + height=1080, showlegend=False, hovermode='closest', margin=dict(b=20,l=5,r=5,t=40), @@ -431,3 +436,13 @@ def visualize(self): images[0].save(self.visualization_configuration['plot_output'], save_all=True, append_images=images[1:], duration=500) print('Saved ' + self.visualization_configuration['plot_output']) + + def networkx_graphing(self): + pos = nx.get_node_attributes(self.graph.graph, 'pos') + plt.figure(figsize=(12, 12)) + nx.draw_networkx_edges(self.graph.graph, pos, alpha=0.2) + nx.draw_networkx_nodes(self.graph.graph, pos, node_size=20) + plt.xlim(-0.05, 1.05) + plt.ylim(-0.05, 1.05) + plt.axis('off') + plt.show() From 8564116e7559d6e56bf7b2c11d5da030e97b4429 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Mon, 14 Sep 2020 02:16:46 +0200 Subject: [PATCH 09/25] Create initial HIOM implementation --- HIOM.py | 129 ++++++++++++++++++++++++++++++++ craving_vs_self_control.py | 22 +++--- ndlib/models/ContinuousModel.py | 55 +++++++++----- 3 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 HIOM.py diff --git a/HIOM.py b/HIOM.py new file mode 100644 index 0000000..f9ee89a --- /dev/null +++ b/HIOM.py @@ -0,0 +1,129 @@ +# TODO Write tests +# Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido + +import networkx as nx +import random +import numpy as np +import matplotlib.pyplot as plt + +from ndlib.models.CompositeModel import CompositeModel +from ndlib.models.ContinuousModel import ContinuousModel +from ndlib.models.compartments.NodeStochastic import NodeStochastic +from ndlib.models.compartments.enums.NumericalType import NumericalType +from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable + +import ndlib.models.ModelConfig as mc + +from bokeh.io import show +from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend + +################### MODEL SPECIFICATIONS ################### + +constants = { + 'N': 500, + 'dt': 0.01, + 'A_min': -0.5, + 'A_star': 1, + 's_O': 0.01, + 's_I': 0, + 'd_A': 0, + 'p': 1, + 'r_min': 0, + 't_O': np.inf, +} + +def initial_I(node, graph, status, constants): + return np.random.normal(0, 0.3) + +def initial_O(node, graph, status, constants): + return np.random.normal(0, 0.3) + +initial_status = { + 'I': initial_I, + 'O': initial_O, + 'A': 1 +} + +def update_I(node, graph, status, attributes, constants): + nb = np.random.choice(graph.neighbors(node)) + if abs(status[node]['O'] - status[nb]['O']) > constants['t_O']: + return status[node]['I'] # Do nothing + else: + # Update information + r = constants['r_min'] + (1 - constants['r_min']) / (1 + np.exp(-1 * constants['p'] * (status[node]['A'] - status[nb]['A']))) + inf = r * status[node]['I'] + (1-r) * status[nb]['I'] + np.random.normal(0, constants['s_I']) + + # Update attention + status[node]['A'] = status[node]['A'] + constants['d_A'] * (2 * constants['A_star'] - status[node]['A']) + status[nb]['A'] = status[nb]['A'] + constants['d_A'] * (2 * constants['A_star'] - status[nb]['A']) + + return inf + + return # eq 7 + eq 6 + +def update_A(node, graph, status, attributes, constants): + return status[node]['A'] - 2 * constants['d_A'] * status[node]['A']/len(graph.nodes) + +def update_O(node, graph, status, attributes, constants): + # stoch_cusp(N,opinion,attention+min_attention,information,s_O,maxwell_convention) + noise = np.random.normal(0, constants['s_O']) + x = status[node]['O'] - constants['dt'] * (status[node]['O']**3 - (status[node]['A'] + constants['A_min']) * status[node]['O'] - status[node]['I']) + noise + return x + +def sample_attention_weighted(graph, status): + probs = [] + A = [stat['A'] for stat in list(status.values())] + factor = 1.0/sum(A) + for a in A: + probs.append(a * factor) + return np.random.choice(graph.nodes, size=1, replace=False, p=probs) + +schemes = [sample_attention_weighted, lambda graph, status: graph.nodes] + +################### MODEL CONFIGURATION ################### + +# Network definition +g = nx.watts_strogatz_graph(400, 2, 0.02) + +# Visualization config +visualization_config = { + 'layout': nx.drawing.fruchterman_reingold_layout, + 'plot_interval': 100, + 'plot_variable': 'O', + 'cmin': -1, + 'color_scale': 'RdBu', + 'save_plot': True, + 'plot_output': '../animations/HIOM.gif', + 'plot_title': 'HIERARCHICAL ISING OPINION MODEL', + 'plot_annotation': 'The polarization within and across individuals: the hierarchical Ising opinion model, Han L. J. van der Maas (2020)' +} + +# Model definition +HIOM = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config, iteration_schemes=schemes) +HIOM.add_status('I') +HIOM.add_status('A') +HIOM.add_status('O') + +# Compartments +condition = NodeStochastic(1) + +# Rules +HIOM.add_rule('I', update_I, condition, [0]) +HIOM.add_rule('A', update_A, condition, [1]) +HIOM.add_rule('O', update_O, condition, [1]) + +# Configuration +config = mc.Configuration() +config.add_model_parameter('fraction_infected', 0.1) +HIOM.set_initial_status(initial_status, config) + + +################### SIMULATION ################### + +# Simulation +iterations = HIOM.iteration_bunch(5000, node_status=True) +trends = HIOM.build_trends(iterations) + +################### VISUALIZATION ################### + +HIOM.visualize() diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py index 869dcdc..fcef5ad 100644 --- a/craving_vs_self_control.py +++ b/craving_vs_self_control.py @@ -1,4 +1,4 @@ -# TODO Dynamic graph, integrate with ndlib correctly, plot speed optimizations for large node amount (intermediate images? / matplotlib?), write tests +# TODO Write tests, Add more network visualizations # Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido import networkx as nx @@ -93,15 +93,15 @@ def update_network(node, graph, status, attributes, constants): ################### MODEL CONFIGURATION ################### # Network definition -g = nx.random_geometric_graph(2000, 0.035) -# g = nx.random_geometric_graph(100, 0.125) +# g = nx.random_geometric_graph(2000, 0.035) +g = nx.random_geometric_graph(200, 0.125) # Visualization config visualization_config = { 'plot_interval': 5, 'plot_variable': 'A', 'save_plot': True, - 'plot_output': './animations/network.gif', + 'plot_output': '../animations/c_vs_s.gif', 'plot_title': 'Self control vs craving simulation', 'plot_annotation': 'The dynamics of addiction: Craving versus self-control, Johan Grasman, Raoul P.P.P. Grasman, Han L.J. van der Maas (2006)' } @@ -120,13 +120,13 @@ def update_network(node, graph, status, attributes, constants): condition2 = NodeStochastic(0.005) # Rules -craving_control_model.add_rule('C', update_C, condition) -craving_control_model.add_rule('S', update_S, condition) -craving_control_model.add_rule('E', update_E, condition) -craving_control_model.add_rule('V', update_V, condition) -craving_control_model.add_rule('lambda', update_lambda, condition) -craving_control_model.add_rule('A', update_A, condition) -craving_control_model.add_rule('network', update_network, condition2) +craving_control_model.add_rule('C', update_C, condition, [0]) +craving_control_model.add_rule('S', update_S, condition, [0]) +craving_control_model.add_rule('E', update_E, condition, [0]) +craving_control_model.add_rule('V', update_V, condition, [0]) +craving_control_model.add_rule('lambda', update_lambda, condition, [0]) +craving_control_model.add_rule('A', update_A, condition, [0]) +# craving_control_model.add_rule('network', update_network, condition2) # Configuration config = mc.Configuration() diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 6b38ce8..615f5f1 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -3,6 +3,7 @@ import os import matplotlib.pyplot as plt import plotly.graph_objects as go +import networkx as nx from plotly.subplots import make_subplots import copy import numpy as np @@ -16,7 +17,7 @@ class ContinuousModel(DiffusionModel): - def __init__(self, graph, constants=None, clean_status=None, visualization_configuration=None): + def __init__(self, graph, constants=None, clean_status=None, visualization_configuration=None, iteration_schemes=None): """ Model Constructor :param graph: A networkx graph object @@ -36,6 +37,11 @@ def __init__(self, graph, constants=None, clean_status=None, visualization_confi self.constants = constants + if iteration_schemes: + self.iteration_schemes = iteration_schemes + else: + self.iteration_schemes = [lambda graph, status: graph.nodes] + if visualization_configuration: self.configure_visualization(visualization_configuration) else: @@ -99,14 +105,21 @@ def configure_visualization(self, visualization_configuration): raise ValueError('Color scale must be a string') else: self.visualization_configuration['color_scale'] = 'YlGnBu' + if 'pos' not in self.graph.nodes[0].keys(): + if 'layout' in vis_keys: + pos = self.visualization_configuration['layout'](self.graph.graph) + else: + pos = nx.drawing.kamada_kawai_layout(self.graph.graph) + positions = {key: {'pos': location} for key, location in pos.items()} + nx.set_node_attributes(self.graph, positions) def add_status(self, status_name): if status_name not in self.available_statuses: self.available_statuses[status_name] = self.status_progressive self.status_progressive += 1 - def add_rule(self, status, function, rule): - self.compartment[self.compartment_progressive] = (status, function, rule) + def add_rule(self, status, function, rule, schemes=[0]): + self.compartment[self.compartment_progressive] = (status, function, rule, schemes) self.compartment_progressive += 1 def set_initial_status(self, initial_status_funs, configuration=None): @@ -170,22 +183,24 @@ def iteration(self, node_status=True): nodes_data = self.graph.nodes(data=True) - for u in self.graph.nodes: - - # For all rules - for i in range(0, self.compartment_progressive): - # Get and test the condition - rule = self.compartment[i][2] - test = rule.execute(node=u, graph=self.graph, status=self.status, - status_map=self.available_statuses, attributes=nodes_data, - params=self.params, constants=self.constants) - if test: - # Update status or network if test succeeds - if self.compartment[i][0] == 'network': - self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) - else: - val = self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) - actual_status[u][self.compartment[i][0]] = val + for i, scheme in enumerate(self.iteration_schemes): + nodes = scheme(self.graph, self.status) + for u in nodes: + # For all rules + for j in range(0, self.compartment_progressive): + if i in self.compartment[j][3]: + # Get and test the condition + rule = self.compartment[j][2] + test = rule.execute(node=u, graph=self.graph, status=self.status, + status_map=self.available_statuses, attributes=nodes_data, + params=self.params, constants=self.constants) + if test: + # Update status or network if test succeeds + if self.compartment[j][0] == 'network': + self.compartment[j][1](u, self.graph, self.status, nodes_data, self.constants) + else: + val = self.compartment[j][1](u, self.graph, self.status, nodes_data, self.constants) + actual_status[u][self.compartment[j][0]] = val delta, status_delta = self.status_delta_continuous(actual_status) self.status = actual_status @@ -336,7 +351,7 @@ def plot_graph(self): #'Reds' | 'Blues' | 'Picnic' | 'Rainbow' | 'Portland' | 'Jet' | #'Hot' | 'Blackbody' | 'Earth' | 'Electric' | 'Viridis' | colorscale=self.visualization_configuration['color_scale'], - reversescale=True, + reversescale=False, color=[], size=10, cmin=self.visualization_configuration['cmin'], From d755713512d815e672c8b30b7828eb085ec82cf7 Mon Sep 17 00:00:00 2001 From: Mathijs Maijer Date: Tue, 15 Sep 2020 03:30:03 +0200 Subject: [PATCH 10/25] Finish implementing basic HIOM model --- HIOM.py | 55 ++++++++++++----- craving_vs_self_control.py | 21 +++---- ndlib/models/ContinuousModel.py | 101 ++++++++++++++++++++++++++++---- 3 files changed, 137 insertions(+), 40 deletions(-) diff --git a/HIOM.py b/HIOM.py index f9ee89a..bad628d 100644 --- a/HIOM.py +++ b/HIOM.py @@ -1,6 +1,3 @@ -# TODO Write tests -# Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido - import networkx as nx import random import numpy as np @@ -20,7 +17,6 @@ ################### MODEL SPECIFICATIONS ################### constants = { - 'N': 500, 'dt': 0.01, 'A_min': -0.5, 'A_star': 1, @@ -36,7 +32,7 @@ def initial_I(node, graph, status, constants): return np.random.normal(0, 0.3) def initial_O(node, graph, status, constants): - return np.random.normal(0, 0.3) + return np.random.normal(0, 0.2) initial_status = { 'I': initial_I, @@ -70,6 +66,12 @@ def update_O(node, graph, status, attributes, constants): x = status[node]['O'] - constants['dt'] * (status[node]['O']**3 - (status[node]['A'] + constants['A_min']) * status[node]['O'] - status[node]['I']) + noise return x +def shrink_I(node, graph, status, attributes, constants): + return status[node]['I'] * 0.999 + +def shrink_A(node, graph, status, attributes, constants): + return status[node]['A'] * 0.999 + def sample_attention_weighted(graph, status): probs = [] A = [stat['A'] for stat in list(status.values())] @@ -78,7 +80,26 @@ def sample_attention_weighted(graph, status): probs.append(a * factor) return np.random.choice(graph.nodes, size=1, replace=False, p=probs) -schemes = [sample_attention_weighted, lambda graph, status: graph.nodes] +schemes = [ + { + 'name': 'random agent', + 'function': sample_attention_weighted, + }, + { + 'name': 'all', + 'function': lambda graph, status: graph.nodes, + }, + { + 'name': 'shrink I', + 'function': lambda graph, status: graph.nodes, + 'lower': 5000 + }, + { + 'name': 'shrink A', + 'function': lambda graph, status: graph.nodes, + 'lower': 10000 + }, +] ################### MODEL CONFIGURATION ################### @@ -88,7 +109,7 @@ def sample_attention_weighted(graph, status): # Visualization config visualization_config = { 'layout': nx.drawing.fruchterman_reingold_layout, - 'plot_interval': 100, + 'plot_interval': 1000, 'plot_variable': 'O', 'cmin': -1, 'color_scale': 'RdBu', @@ -99,7 +120,7 @@ def sample_attention_weighted(graph, status): } # Model definition -HIOM = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config, iteration_schemes=schemes) +HIOM = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config, iteration_schemes=schemes, save_file='../data/hiom.npy') HIOM.add_status('I') HIOM.add_status('A') HIOM.add_status('O') @@ -108,9 +129,11 @@ def sample_attention_weighted(graph, status): condition = NodeStochastic(1) # Rules -HIOM.add_rule('I', update_I, condition, [0]) -HIOM.add_rule('A', update_A, condition, [1]) -HIOM.add_rule('O', update_O, condition, [1]) +HIOM.add_rule('I', update_I, condition, ['random agent']) +HIOM.add_rule('A', update_A, condition, ['all']) +HIOM.add_rule('O', update_O, condition, ['all']) +HIOM.add_rule('I', shrink_I, condition, ['shrink I']) +HIOM.add_rule('A', shrink_A, condition, ['shrink A']) # Configuration config = mc.Configuration() @@ -121,9 +144,13 @@ def sample_attention_weighted(graph, status): ################### SIMULATION ################### # Simulation -iterations = HIOM.iteration_bunch(5000, node_status=True) -trends = HIOM.build_trends(iterations) +# iterations = HIOM.iteration_bunch(15000, node_status=True) +# trends = HIOM.build_trends(iterations) +# HIOM.plot(trends, len(iterations), delta=True) +# HIOM.plot_bars(iterations) + +HIOM.plot_bars(np.load('../data/hiom.npy', allow_pickle=True)) ################### VISUALIZATION ################### -HIOM.visualize() +# HIOM.visualize() diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py index fcef5ad..a29ae93 100644 --- a/craving_vs_self_control.py +++ b/craving_vs_self_control.py @@ -107,7 +107,7 @@ def update_network(node, graph, status, attributes, constants): } # Model definition -craving_control_model = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config) +craving_control_model = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config, save_file='../data/c_vs_s.npy') craving_control_model.add_status('C') craving_control_model.add_status('S') craving_control_model.add_status('E') @@ -120,12 +120,12 @@ def update_network(node, graph, status, attributes, constants): condition2 = NodeStochastic(0.005) # Rules -craving_control_model.add_rule('C', update_C, condition, [0]) -craving_control_model.add_rule('S', update_S, condition, [0]) -craving_control_model.add_rule('E', update_E, condition, [0]) -craving_control_model.add_rule('V', update_V, condition, [0]) -craving_control_model.add_rule('lambda', update_lambda, condition, [0]) -craving_control_model.add_rule('A', update_A, condition, [0]) +craving_control_model.add_rule('C', update_C, condition) +craving_control_model.add_rule('S', update_S, condition) +craving_control_model.add_rule('E', update_E, condition) +craving_control_model.add_rule('V', update_V, condition) +craving_control_model.add_rule('lambda', update_lambda, condition) +craving_control_model.add_rule('A', update_A, condition) # craving_control_model.add_rule('network', update_network, condition2) # Configuration @@ -133,17 +133,12 @@ def update_network(node, graph, status, attributes, constants): config.add_model_parameter('fraction_infected', 0.1) craving_control_model.set_initial_status(initial_status, config) - - - ################### SIMULATION ################### # Simulation iterations = craving_control_model.iteration_bunch(100, node_status=True) trends = craving_control_model.build_trends(iterations) - - - +craving_control_model.plot_bars(iterations) ################### VISUALIZATION ################### diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 615f5f1..104a096 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,7 +1,9 @@ +# TODO Write tests, fix visualization logic, assert save_file +# Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido + from ndlib.models.DiffusionModel import DiffusionModel import future.utils import os -import matplotlib.pyplot as plt import plotly.graph_objects as go import networkx as nx from plotly.subplots import make_subplots @@ -10,6 +12,11 @@ from PIL import Image import io +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import matplotlib.path as path +import matplotlib.animation as animation + __author__ = 'Mathijs Maijer' __license__ = "BSD-2-Clause" __email__ = "m.f.maijer@gmail.com" @@ -17,7 +24,7 @@ class ContinuousModel(DiffusionModel): - def __init__(self, graph, constants=None, clean_status=None, visualization_configuration=None, iteration_schemes=None): + def __init__(self, graph, constants=None, clean_status=None, visualization_configuration=None, iteration_schemes=None, save_file=None): """ Model Constructor :param graph: A networkx graph object @@ -40,13 +47,16 @@ def __init__(self, graph, constants=None, clean_status=None, visualization_confi if iteration_schemes: self.iteration_schemes = iteration_schemes else: - self.iteration_schemes = [lambda graph, status: graph.nodes] + self.iteration_schemes = [{'name': '', 'function': lambda graph, status: graph.nodes}] if visualization_configuration: self.configure_visualization(visualization_configuration) else: self.visualization_configuration = None + self.save_file = save_file + + def configure_visualization(self, visualization_configuration): if visualization_configuration: self.visualizations = [] @@ -118,7 +128,7 @@ def add_status(self, status_name): self.available_statuses[status_name] = self.status_progressive self.status_progressive += 1 - def add_rule(self, status, function, rule, schemes=[0]): + def add_rule(self, status, function, rule, schemes=['']): self.compartment[self.compartment_progressive] = (status, function, rule, schemes) self.compartment_progressive += 1 @@ -183,24 +193,30 @@ def iteration(self, node_status=True): nodes_data = self.graph.nodes(data=True) - for i, scheme in enumerate(self.iteration_schemes): - nodes = scheme(self.graph, self.status) + for scheme in self.iteration_schemes: + + if 'lower' in scheme and scheme['lower'] > self.actual_iteration: + continue + if 'upper' in scheme and scheme['upper'] < self.actual_iteration: + continue + + nodes = scheme['function'](self.graph, self.status) for u in nodes: # For all rules - for j in range(0, self.compartment_progressive): - if i in self.compartment[j][3]: + for i in range(0, self.compartment_progressive): + if scheme['name'] in self.compartment[i][3]: # Get and test the condition - rule = self.compartment[j][2] + rule = self.compartment[i][2] test = rule.execute(node=u, graph=self.graph, status=self.status, status_map=self.available_statuses, attributes=nodes_data, params=self.params, constants=self.constants) if test: # Update status or network if test succeeds - if self.compartment[j][0] == 'network': - self.compartment[j][1](u, self.graph, self.status, nodes_data, self.constants) + if self.compartment[i][0] == 'network': + self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) else: - val = self.compartment[j][1](u, self.graph, self.status, nodes_data, self.constants) - actual_status[u][self.compartment[j][0]] = val + val = self.compartment[i][1](u, self.graph, self.status, nodes_data, self.constants) + actual_status[u][self.compartment[i][0]] = val delta, status_delta = self.status_delta_continuous(actual_status) self.status = actual_status @@ -213,6 +229,13 @@ def iteration(self, node_status=True): return {"iteration": self.actual_iteration - 1, "status": {}, "status_delta": copy.deepcopy(status_delta)} + def iteration_bunch(self, bunch_size, node_status=True): + iterations = super().iteration_bunch(bunch_size, node_status) + if self.save_file: + np.save(self.save_file, iterations) + print('Saved ' + self.save_file) + return iterations + def get_mean_data(self, iterations, mean_type): mean_changes = {} for key in self.available_statuses.keys(): @@ -314,6 +337,58 @@ def plot(self, trends, n, delta=None, delta_mean=None): plt.show() + def create_frames(self, iterations): + status = self.build_full_status(iterations) + statuses = list(self.available_statuses.keys()) + statuses.remove('Infected') + frames = {key: [] for key in statuses} + + for i in range(len(status)): + if i % self.visualization_configuration['plot_interval'] == 0: + status_list = {key: [] for key in statuses} + for node in status[i]['status'].keys(): + vals = status[i]['status'][node] + for key in statuses: + status_list[key].append(vals[key]) + + for key in statuses: + frames[key].append(status_list[key]) + return frames + + def plot_bars(self, iterations): + frames = self.create_frames(iterations) + + statuses = list(self.available_statuses.keys()) + statuses.remove('Infected') + + n_status = len(statuses) + + fig, axis = plt.subplots(1, n_status, figsize=(12,6)) + + n = int(len(iterations)/self.visualization_configuration['plot_interval']) + + cm = plt.cm.get_cmap('RdYlBu_r') + + def updateData(curr): + if curr <=2: return + for ax in axis: + ax.clear() + + for i, ax in enumerate(axis): + n, bins, patches = ax.hist(frames[statuses[i]][curr], range=[-1, 1], density=1, bins=25) + bin_centers = 0.5 * (bins[:-1] + bins[1:]) + col = bin_centers - min(bin_centers) + col /= max(col) + for c, p in zip(col, patches): + plt.setp(p, 'facecolor', cm(c)) + ax.set_title(statuses[i]) + ax.get_xaxis().set_ticks([]) + ax.get_yaxis().set_ticks([]) + + simulation = animation.FuncAnimation(fig, updateData, n, interval=200, repeat=True) + + plt.show() + def plot_graph(self): edge_x = [] edge_y = [] From d41de44c2679cc8844c31a71ebdcf60195331238 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 16 Sep 2020 06:12:59 +0200 Subject: [PATCH 11/25] Rework visualization --- HIOM.py | 31 ++-- craving_vs_self_control.py | 42 ++--- ndlib/models/ContinuousModel.py | 267 ++++++++++---------------------- 3 files changed, 108 insertions(+), 232 deletions(-) diff --git a/HIOM.py b/HIOM.py index bad628d..13b9311 100644 --- a/HIOM.py +++ b/HIOM.py @@ -55,13 +55,12 @@ def update_I(node, graph, status, attributes, constants): return inf - return # eq 7 + eq 6 + return def update_A(node, graph, status, attributes, constants): return status[node]['A'] - 2 * constants['d_A'] * status[node]['A']/len(graph.nodes) def update_O(node, graph, status, attributes, constants): - # stoch_cusp(N,opinion,attention+min_attention,information,s_O,maxwell_convention) noise = np.random.normal(0, constants['s_O']) x = status[node]['O'] - constants['dt'] * (status[node]['O']**3 - (status[node]['A'] + constants['A_min']) * status[node]['O'] - status[node]['I']) + noise return x @@ -108,19 +107,21 @@ def sample_attention_weighted(graph, status): # Visualization config visualization_config = { - 'layout': nx.drawing.fruchterman_reingold_layout, - 'plot_interval': 1000, + 'layout': 'fr', + 'plot_interval': 100, 'plot_variable': 'O', + 'variable_limits': { + 'A': [0, 1] + }, 'cmin': -1, + 'cmax': 1, 'color_scale': 'RdBu', - 'save_plot': True, - 'plot_output': '../animations/HIOM.gif', + 'plot_output': '../animations/HIOM_less.gif', 'plot_title': 'HIERARCHICAL ISING OPINION MODEL', - 'plot_annotation': 'The polarization within and across individuals: the hierarchical Ising opinion model, Han L. J. van der Maas (2020)' } # Model definition -HIOM = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config, iteration_schemes=schemes, save_file='../data/hiom.npy') +HIOM = ContinuousModel(g, constants=constants, iteration_schemes=schemes) HIOM.add_status('I') HIOM.add_status('A') HIOM.add_status('O') @@ -139,18 +140,18 @@ def sample_attention_weighted(graph, status): config = mc.Configuration() config.add_model_parameter('fraction_infected', 0.1) HIOM.set_initial_status(initial_status, config) - +HIOM.configure_visualization(visualization_config) ################### SIMULATION ################### # Simulation -# iterations = HIOM.iteration_bunch(15000, node_status=True) +iterations = HIOM.iteration_bunch(15000, node_status=True) # trends = HIOM.build_trends(iterations) -# HIOM.plot(trends, len(iterations), delta=True) -# HIOM.plot_bars(iterations) - -HIOM.plot_bars(np.load('../data/hiom.npy', allow_pickle=True)) ################### VISUALIZATION ################### -# HIOM.visualize() +# HIOM.plot(trends, len(iterations), delta=True) +HIOM.visualize(iterations) + + +# HIOM.visualize(np.load('../data/hiom.npy', allow_pickle=True)) \ No newline at end of file diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py index a29ae93..1868571 100644 --- a/craving_vs_self_control.py +++ b/craving_vs_self_control.py @@ -68,28 +68,6 @@ def update_lambda(node, graph, status, attributes, constants): def update_A(node, graph, status, attributes, constants): return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) -# 50% chance of severing connections, move to random location, create new random connections -def update_network(node, graph, status, attributes, constants): - sever = [] - for n in graph.neighbors(node): - if random.random() < 0.5: - sever.append(n) - graph.remove_edges(node, sever) - - if random.random() < 0.05: - pos = (random.random(), random.random()) - graph.nodes[node]['pos'] = pos - - while True: - new_connections = [] - if random.random() < 0.5: - new_conn = random.choice(graph.nodes()) - if new_conn not in graph.neighbors(node): - new_connections.append(new_conn) - else: - graph.add_edges(node, new_connections) - return - ################### MODEL CONFIGURATION ################### # Network definition @@ -98,16 +76,18 @@ def update_network(node, graph, status, attributes, constants): # Visualization config visualization_config = { - 'plot_interval': 5, + 'plot_interval': 1, 'plot_variable': 'A', - 'save_plot': True, - 'plot_output': '../animations/c_vs_s.gif', + 'variable_limits': { + 'A': [0, 0.8], + 'lambda': [0.5, 1.5] + }, + 'plot_output': '../animations/c_vs_s_slow.gif', 'plot_title': 'Self control vs craving simulation', - 'plot_annotation': 'The dynamics of addiction: Craving versus self-control, Johan Grasman, Raoul P.P.P. Grasman, Han L.J. van der Maas (2006)' } # Model definition -craving_control_model = ContinuousModel(g, constants=constants, visualization_configuration=visualization_config, save_file='../data/c_vs_s.npy') +craving_control_model = ContinuousModel(g, constants=constants, save_file='../data/c_vs_s.npy') craving_control_model.add_status('C') craving_control_model.add_status('S') craving_control_model.add_status('E') @@ -117,7 +97,6 @@ def update_network(node, graph, status, attributes, constants): # Compartments condition = NodeStochastic(1) -condition2 = NodeStochastic(0.005) # Rules craving_control_model.add_rule('C', update_C, condition) @@ -126,23 +105,22 @@ def update_network(node, graph, status, attributes, constants): craving_control_model.add_rule('V', update_V, condition) craving_control_model.add_rule('lambda', update_lambda, condition) craving_control_model.add_rule('A', update_A, condition) -# craving_control_model.add_rule('network', update_network, condition2) # Configuration config = mc.Configuration() config.add_model_parameter('fraction_infected', 0.1) craving_control_model.set_initial_status(initial_status, config) +craving_control_model.configure_visualization(visualization_config) ################### SIMULATION ################### # Simulation iterations = craving_control_model.iteration_bunch(100, node_status=True) trends = craving_control_model.build_trends(iterations) -craving_control_model.plot_bars(iterations) ################### VISUALIZATION ################### -# craving_control_model.plot(trends, len(iterations), delta=True) +craving_control_model.plot(trends, len(iterations), delta=True) x = np.arange(0, len(iterations)) plt.figure() @@ -164,4 +142,4 @@ def update_network(node, graph, status, attributes, constants): plt.show() -craving_control_model.visualize() +craving_control_model.visualize(iterations) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 104a096..adfae3b 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,5 +1,5 @@ # TODO Write tests, fix visualization logic, assert save_file -# Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido +# Requirements, networkx, numpy, matplotlib, PIL, pyintergraph from ndlib.models.DiffusionModel import DiffusionModel import future.utils @@ -12,9 +12,11 @@ from PIL import Image import io +import pyintergraph import matplotlib.pyplot as plt import matplotlib.patches as patches import matplotlib.path as path +import matplotlib as mpl import matplotlib.animation as animation __author__ = 'Mathijs Maijer' @@ -24,7 +26,7 @@ class ContinuousModel(DiffusionModel): - def __init__(self, graph, constants=None, clean_status=None, visualization_configuration=None, iteration_schemes=None, save_file=None): + def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=None, save_file=None): """ Model Constructor :param graph: A networkx graph object @@ -49,13 +51,12 @@ def __init__(self, graph, constants=None, clean_status=None, visualization_confi else: self.iteration_schemes = [{'name': '', 'function': lambda graph, status: graph.nodes}] - if visualization_configuration: - self.configure_visualization(visualization_configuration) - else: - self.visualization_configuration = None + self.visualization_configuration = None self.save_file = save_file + self.full_status = None + def configure_visualization(self, visualization_configuration): if visualization_configuration: @@ -77,14 +78,6 @@ def configure_visualization(self, visualization_configuration): else: self.visualization_configuration['plot_variable'] = None - self.visualization_configuration['save_plot'] = True if self.visualization_configuration['save_plot'] else False - if self.visualization_configuration['save_plot']: - if 'plot_output' not in vis_keys: - self.visualization_configuration['plot_output'] = './visualization/network.gif' - elif not isinstance(self.visualization_configuration['plot_output'], str): - raise ValueError('plot_output must be a string') - # Todo create regex for plot output - if 'plot_title' in self.visualization_configuration.keys(): if not isinstance(self.visualization_configuration['plot_title'], str): raise ValueError('Plot name must be a string') @@ -114,15 +107,30 @@ def configure_visualization(self, visualization_configuration): if not isinstance(self.visualization_configuration['color_scale'], str): raise ValueError('Color scale must be a string') else: - self.visualization_configuration['color_scale'] = 'YlGnBu' + self.visualization_configuration['color_scale'] = 'RdBu' if 'pos' not in self.graph.nodes[0].keys(): if 'layout' in vis_keys: - pos = self.visualization_configuration['layout'](self.graph.graph) + if self.visualization_configuration['layout'] == 'fr': + Graph = pyintergraph.InterGraph.from_networkx(self.graph.graph) + G = Graph.to_igraph() + layout = G.layout_fruchterman_reingold(niter=500) + positions = {node: {'pos': location} for node, location in enumerate(layout)} + else: + pos = nx.drawing.kamada_kawai_layout(self.graph.graph) + positions = {key: {'pos': location} for key, location in pos.items()} else: pos = nx.drawing.kamada_kawai_layout(self.graph.graph) - positions = {key: {'pos': location} for key, location in pos.items()} + positions = {key: {'pos': location} for key, location in pos.items()} + nx.set_node_attributes(self.graph, positions) + if 'variable_limits' not in vis_keys: + self.visualization_configuration['variable_limits'] = {key: [-1, 1] for key in list(self.available_statuses.keys())} + else: + for key in list(self.available_statuses.keys()): + if key not in list(self.visualization_configuration['variable_limits'].keys()): + self.visualization_configuration['variable_limits'][key] = [-1, 1] + def add_status(self, status_name): if status_name not in self.available_statuses: self.available_statuses[status_name] = self.status_progressive @@ -178,9 +186,6 @@ def iteration(self, node_status=True): # actual_status = {node: nstatus for node, nstatus in future.utils.iteritems(self.status)} actual_status = copy.deepcopy(self.status) - if self.visualization_configuration and self.actual_iteration % self.visualization_configuration['plot_interval'] == 0: - self.plot_graph() - if self.actual_iteration == 0: self.actual_iteration += 1 delta, status_delta = self.status_delta_continuous(self.status) @@ -285,8 +290,8 @@ def build_full_status(self, iterations): return statuses def get_means(self, iterations): - full_status = self.build_full_status(iterations) - means = self.get_mean_data(full_status, 'status') + self.full_status = self.build_full_status(iterations) + means = self.get_mean_data(self.full_status, 'status') return means def build_trends(self, iterations): @@ -338,201 +343,93 @@ def plot(self, trends, n, delta=None, delta_mean=None): plt.show() def create_frames(self, iterations): - status = self.build_full_status(iterations) + if not self.full_status: + self.full_status = self.build_full_status(iterations) statuses = list(self.available_statuses.keys()) statuses.remove('Infected') - frames = {key: [] for key in statuses} - - for i in range(len(status)): + histo_frames = {key: [] for key in statuses} + node_colors = [] + for i in range(len(self.full_status)): if i % self.visualization_configuration['plot_interval'] == 0: status_list = {key: [] for key in statuses} - for node in status[i]['status'].keys(): - vals = status[i]['status'][node] + for node in self.full_status[i]['status'].keys(): + vals = self.full_status[i]['status'][node] for key in statuses: status_list[key].append(vals[key]) for key in statuses: - frames[key].append(status_list[key]) - return frames + histo_frames[key].append(status_list[key]) + node_colors.append([self.full_status[i]['status'][node][self.visualization_configuration['plot_variable']] for node in self.graph.nodes]) + + return (histo_frames, node_colors) - def plot_bars(self, iterations): - frames = self.create_frames(iterations) + def visualize(self, iterations): + (histo_frames, node_colors) = self.create_frames(iterations) statuses = list(self.available_statuses.keys()) statuses.remove('Infected') n_status = len(statuses) - fig, axis = plt.subplots(1, n_status, figsize=(12,6)) + fig = plt.figure(figsize=(10,9), constrained_layout=True) + gs = fig.add_gridspec(6, n_status) + + network = fig.add_subplot(gs[:-1, :]) + + axis = [] + + for i in range(n_status): + ax = fig.add_subplot(gs[-1, i]) + ax.set_title(statuses[i]) + ax.get_xaxis().set_ticks([]) + ax.get_yaxis().set_ticks([]) + axis.append(ax) n = int(len(iterations)/self.visualization_configuration['plot_interval']) - cm = plt.cm.get_cmap('RdYlBu_r') + cm = plt.cm.get_cmap(self.visualization_configuration['color_scale']) + vmin = self.visualization_configuration['variable_limits'][self.visualization_configuration['plot_variable']][0] + vmax = self.visualization_configuration['variable_limits'][self.visualization_configuration['plot_variable']][1] def updateData(curr): - if curr <=2: return + # if curr <=2: return + # Clean previous graphs + network.clear() for ax in axis: ax.clear() + # Plot all variable histograms for i, ax in enumerate(axis): - n, bins, patches = ax.hist(frames[statuses[i]][curr], range=[-1, 1], density=1, bins=25) + n, bins, patches = ax.hist(histo_frames[statuses[i]][curr], range=self.visualization_configuration['variable_limits'][statuses[i]], density=1, bins=25, edgecolor='black') bin_centers = 0.5 * (bins[:-1] + bins[1:]) col = bin_centers - min(bin_centers) col /= max(col) for c, p in zip(col, patches): plt.setp(p, 'facecolor', cm(c)) ax.set_title(statuses[i]) + # ax.set_ylim([0, len(self.graph.nodes)]) ax.get_xaxis().set_ticks([]) ax.get_yaxis().set_ticks([]) - simulation = animation.FuncAnimation(fig, updateData, n, interval=200, repeat=True) - + # Plot network + pos = nx.get_node_attributes(self.graph.graph, 'pos') + nx.draw_networkx_edges(self.graph.graph, pos, alpha=0.2, ax=network) + nc = nx.draw_networkx_nodes(self.graph.graph, pos, nodelist=self.graph.nodes, node_color=node_colors[curr], vmin=vmin, vmax=vmax, cmap=cm, node_size=50, ax=network) + nc.set_edgecolor('black') + network.get_xaxis().set_ticks([]) + network.get_yaxis().set_ticks([]) + network.set_title('Iteration: ' + str(curr * self.visualization_configuration['plot_interval'])) + + simulation = animation.FuncAnimation(fig, updateData, n, interval=30, repeat=True) + + norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax) + sm = plt.cm.ScalarMappable(cmap=cm, norm=norm) + sm.set_array([]) + fig.colorbar(sm, ax=network) + fig.suptitle(self.visualization_configuration['plot_title'], fontsize=16) plt.show() - def plot_graph(self): - edge_x = [] - edge_y = [] - for edge in self.graph.edges(): - x0, y0 = self.graph.nodes[edge[0]]['pos'] - x1, y1 = self.graph.nodes[edge[1]]['pos'] - edge_x.append(x0) - edge_x.append(x1) - edge_x.append(None) - edge_y.append(y0) - edge_y.append(y1) - edge_y.append(None) - - edge_trace = go.Scatter( - x=edge_x, y=edge_y, - line=dict(width=0.5, color='#888'), - hoverinfo='none', - mode='lines') - - node_x = [] - node_y = [] - for node in self.graph.nodes(): - x, y = self.graph.nodes[node]['pos'] - node_x.append(x) - node_y.append(y) - - node_trace = go.Scatter( - x=node_x, y=node_y, - mode='markers', - hoverinfo='text', - marker=dict( - showscale=True, - # colorscale options - #'Greys' | 'YlGnBu' | 'Greens' | 'YlOrRd' | 'Bluered' | 'RdBu' | - #'Reds' | 'Blues' | 'Picnic' | 'Rainbow' | 'Portland' | 'Jet' | - #'Hot' | 'Blackbody' | 'Earth' | 'Electric' | 'Viridis' | - colorscale=self.visualization_configuration['color_scale'], - reversescale=False, - color=[], - size=10, - cmin=self.visualization_configuration['cmin'], - cmax=self.visualization_configuration['cmax'], - colorbar=dict( - thickness=15, - title=self.visualization_configuration['plot_variable'], - xanchor='left', - titleside='right', - ), - line_width=2)) - - node_value = [] - node_text = [] - - if self.visualization_configuration['plot_variable']: - for node in self.graph.nodes(): - value = self.status[node][self.visualization_configuration['plot_variable']] - node_value.append(value) - node_text.append(self.visualization_configuration['plot_variable'] + ': ' + str(value)) - else: - for node in self.graph.nodes(): - n_neighbors = len(self.graph.neighbors(node)) - node_value.append(n_neighbors) - node_text.append('# of connections: ' + str(n_neighbors)) - - node_trace.marker.color = node_value - node_trace.text = node_text - - self.visualizations.append([edge_trace, node_trace]) - - def visualize(self): - if not self.visualization_configuration: - print('Visualization stopped because no configuration is defined') - return - self.plot_graph() # Also save last network state - - frames = [] - for v in self.visualizations: - frames.append(go.Frame(data=[v[0], v[1]])) - - fig = go.Figure(data=[self.visualizations[0][0], self.visualizations[0][1]], - layout=go.Layout( - title=self.visualization_configuration['plot_title'], - titlefont_size=16, - showlegend=False, - hovermode='closest', - margin=dict(b=20,l=5,r=5,t=40), - annotations=[ dict( - text=self.visualization_configuration['plot_annotation'], - showarrow=False, - xref="paper", yref="paper", - x=0.005, y=-0.002 ) ], - updatemenus=[dict( - type="buttons", - buttons=[dict(label="Play", - method="animate", - args=[None, {"frame": {"duration": 500}, "transition": {"duration": 0}}])])], - xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), - yaxis=dict(showgrid=False, zeroline=False, showticklabels=False) - ), - frames=frames - ) - - fig.show() - - if self.visualization_configuration['save_plot']: - # Create byte images - images = [] - for i, f in enumerate(self.visualizations): - fig = go.Figure(data=[self.visualizations[i][0], self.visualizations[i][1]], - layout=go.Layout( - title=self.visualization_configuration['plot_title'], - titlefont_size=16, - width=1920, - height=1080, - showlegend=False, - hovermode='closest', - margin=dict(b=20,l=5,r=5,t=40), - annotations=[ dict( - text=self.visualization_configuration['plot_annotation'], - showarrow=False, - xref="paper", yref="paper", - x=0.005, y=-0.002 ) ]) - ).to_image(format="png") - - images.append(Image.open(io.BytesIO(fig))) - # Create gif of byte image array - - split_dir = self.visualization_configuration['plot_output'].split('/') - path = '/'.join(split_dir[0:-1]) - filename = split_dir[-1] - - if not os.path.exists(path): - os.makedirs(path) - - images[0].save(self.visualization_configuration['plot_output'], - save_all=True, append_images=images[1:], duration=500) - print('Saved ' + self.visualization_configuration['plot_output']) - - def networkx_graphing(self): - pos = nx.get_node_attributes(self.graph.graph, 'pos') - plt.figure(figsize=(12, 12)) - nx.draw_networkx_edges(self.graph.graph, pos, alpha=0.2) - nx.draw_networkx_nodes(self.graph.graph, pos, node_size=20) - plt.xlim(-0.05, 1.05) - plt.ylim(-0.05, 1.05) - plt.axis('off') - plt.show() + if 'plot_output' in self.visualization_configuration.keys(): + writergif = animation.PillowWriter(fps=5) + simulation.save(self.visualization_configuration['plot_output'], writer=writergif) + print('Saved: ' + self.visualization_configuration['plot_output']) \ No newline at end of file From 8317f9428fccae62740e94dfa0e8fae92d390a13 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 23 Sep 2020 05:06:22 +0200 Subject: [PATCH 12/25] Remove unnecessary imports, add sensitivity analysis possibility, add more visualization configuration --- HIOM.py | 6 -- craving_vs_self_control.py | 17 +--- craving_vs_self_control_multi.py | 116 ++++++++++++++++++++++++++ ndlib/models/ContinuousModel.py | 33 +++++--- ndlib/models/ContinuousModelRunner.py | 28 +++++++ 5 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 craving_vs_self_control_multi.py create mode 100644 ndlib/models/ContinuousModelRunner.py diff --git a/HIOM.py b/HIOM.py index 13b9311..42161e8 100644 --- a/HIOM.py +++ b/HIOM.py @@ -3,17 +3,11 @@ import numpy as np import matplotlib.pyplot as plt -from ndlib.models.CompositeModel import CompositeModel from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic -from ndlib.models.compartments.enums.NumericalType import NumericalType -from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable import ndlib.models.ModelConfig as mc -from bokeh.io import show -from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend - ################### MODEL SPECIFICATIONS ################### constants = { diff --git a/craving_vs_self_control.py b/craving_vs_self_control.py index 1868571..0dadda6 100644 --- a/craving_vs_self_control.py +++ b/craving_vs_self_control.py @@ -1,22 +1,13 @@ -# TODO Write tests, Add more network visualizations -# Requirements, networkx, numpy, matplotlib, bokeh, plotly, PIL, psutil, kaleido - import networkx as nx import random import numpy as np import matplotlib.pyplot as plt -from ndlib.models.CompositeModel import CompositeModel from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic -from ndlib.models.compartments.enums.NumericalType import NumericalType -from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable import ndlib.models.ModelConfig as mc -from bokeh.io import show -from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend - ################### MODEL SPECIFICATIONS ################### constants = { @@ -76,18 +67,19 @@ def update_A(node, graph, status, attributes, constants): # Visualization config visualization_config = { - 'plot_interval': 1, + 'plot_interval': 2, 'plot_variable': 'A', 'variable_limits': { 'A': [0, 0.8], 'lambda': [0.5, 1.5] }, - 'plot_output': '../animations/c_vs_s_slow.gif', + 'show_plot': True, + 'plot_output': '../animations/c_vs_s.gif', 'plot_title': 'Self control vs craving simulation', } # Model definition -craving_control_model = ContinuousModel(g, constants=constants, save_file='../data/c_vs_s.npy') +craving_control_model = ContinuousModel(g, constants=constants) craving_control_model.add_status('C') craving_control_model.add_status('S') craving_control_model.add_status('E') @@ -108,7 +100,6 @@ def update_A(node, graph, status, attributes, constants): # Configuration config = mc.Configuration() -config.add_model_parameter('fraction_infected', 0.1) craving_control_model.set_initial_status(initial_status, config) craving_control_model.configure_visualization(visualization_config) diff --git a/craving_vs_self_control_multi.py b/craving_vs_self_control_multi.py new file mode 100644 index 0000000..9d14761 --- /dev/null +++ b/craving_vs_self_control_multi.py @@ -0,0 +1,116 @@ +import networkx as nx +import random +import numpy as np +import matplotlib.pyplot as plt + +from ndlib.models.ContinuousModel import ContinuousModel +from ndlib.models.ContinuousModelRunner import ContinuousModelRunner +from ndlib.models.compartments.NodeStochastic import NodeStochastic + +import ndlib.models.ModelConfig as mc + +################### MODEL SPECIFICATIONS ################### + +constants = { + 'q': 0.8, + 'b': 0.5, + 'd': 0.2, + 'h': 0.2, + 'k': 0.25, + 'S+': 0.5, +} +constants['p'] = 2*constants['d'] + +def initial_v(node, graph, status, constants): + return min(1, max(0, status['C']-status['S']-status['E'])) + +def initial_a(node, graph, status, constants): + return constants['q'] * status['V'] + (np.random.poisson(status['lambda'])/7) + +initial_status = { + 'C': 0, + 'S': constants['S+'], + 'E': 1, + 'V': initial_v, + 'lambda': 0.5, + 'A': initial_a +} + +def update_C(node, graph, status, attributes, constants): + return status[node]['C'] + constants['b'] * status[node]['A'] * min(1, 1-status[node]['C']) - constants['d'] * status[node]['C'] + +def update_S(node, graph, status, attributes, constants): + return status[node]['S'] + constants['p'] * max(0, constants['S+'] - status[node]['S']) - constants['h'] * status[node]['C'] - constants['k'] * status[node]['A'] + +def update_E(node, graph, status, attributes, constants): + # return status[node]['E'] - 0.015 # Grasman calculation + + avg_neighbor_addiction = 0 + for n in graph.neighbors(node): + avg_neighbor_addiction += status[n]['A'] + + return max(-1.5, status[node]['E'] - avg_neighbor_addiction / 50) # Custom calculation + +def update_V(node, graph, status, attributes, constants): + return min(1, max(0, status[node]['C']-status[node]['S']-status[node]['E'])) + +def update_lambda(node, graph, status, attributes, constants): + return status[node]['lambda'] + 0.01 + +def update_A(node, graph, status, attributes, constants): + return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) + +################### MODEL CONFIGURATION ################### + +# Network definition +g = nx.random_geometric_graph(200, 0.125) + +# Visualization config +visualization_config = { + 'plot_interval': 10, + 'plot_variable': 'A', + 'variable_limits': { + 'A': [0, 0.8], + 'lambda': [0.5, 1.5] + }, + 'show_plot': True, + 'plot_title': 'Self control vs craving simulation', +} + +# Model definition +craving_control_model = ContinuousModel(g, constants=constants) +craving_control_model.add_status('C') +craving_control_model.add_status('S') +craving_control_model.add_status('E') +craving_control_model.add_status('V') +craving_control_model.add_status('lambda') +craving_control_model.add_status('A') + +# Compartments +condition = NodeStochastic(1) + +# Rules +craving_control_model.add_rule('C', update_C, condition) +craving_control_model.add_rule('S', update_S, condition) +craving_control_model.add_rule('E', update_E, condition) +craving_control_model.add_rule('V', update_V, condition) +craving_control_model.add_rule('lambda', update_lambda, condition) +craving_control_model.add_rule('A', update_A, condition) + +# Configuration +config = mc.Configuration() +craving_control_model.set_initial_status(initial_status, config) +craving_control_model.set_initial_status(initial_status, config) +craving_control_model.configure_visualization(visualization_config) + +################### SIMULATION ################### + +# Simulation +runner = ContinuousModelRunner(craving_control_model, config, 10, [100], [initial_status]) +results = runner.run() + +################### VISUALIZATION ################### + +for iterations in results: + trends = craving_control_model.build_trends(iterations) + craving_control_model.plot(trends, len(iterations), delta=True) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index adfae3b..ea205a7 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,5 +1,5 @@ -# TODO Write tests, fix visualization logic, assert save_file -# Requirements, networkx, numpy, matplotlib, PIL, pyintergraph +# TODO Write tests, fix visualization logic (overwrite vs update), assert save_file +# Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, tqdm from ndlib.models.DiffusionModel import DiffusionModel import future.utils @@ -31,7 +31,7 @@ def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=N Model Constructor :param graph: A networkx graph object """ - super(self.__class__, self).__init__(graph) + super(ContinuousModel, self).__init__(graph) self.compartment = {} self.compartment_progressive = 0 self.status_progressive = 0 @@ -60,7 +60,6 @@ def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=N def configure_visualization(self, visualization_configuration): if visualization_configuration: - self.visualizations = [] self.visualization_configuration = visualization_configuration vis_keys = visualization_configuration.keys() if 'plot_interval' in vis_keys: @@ -71,7 +70,11 @@ def configure_visualization(self, visualization_configuration): raise ValueError('plot_interval must be a positive integer') else: raise ValueError('plot_interval must be included for visualization') - + if 'show_plot' in vis_keys: + if not isinstance(visualization_configuration['show_plot'], bool): + raise ValueError('show_plot must be a boolean') + else: + self.visualization_configuration['show_plot'] = True if 'plot_variable' in vis_keys: if not isinstance(visualization_configuration['plot_variable'], str): raise ValueError('Plot variable must be a string') @@ -196,7 +199,7 @@ def iteration(self, node_status=True): return {"iteration": 0, "status": {}, "status_delta": copy.deepcopy(status_delta)} - nodes_data = self.graph.nodes(data=True) + nodes_data = self.graph.nodes for scheme in self.iteration_schemes: @@ -311,7 +314,7 @@ def plot(self, trends, n, delta=None, delta_mean=None): if delta: sub_plots = 2 if (delta and not delta_mean) else 3 - fig, axs = plt.subplots(sub_plots) + _, axs = plt.subplots(sub_plots) # Mean status delta per iterations if delta or delta_mean: @@ -392,7 +395,6 @@ def visualize(self, iterations): vmax = self.visualization_configuration['variable_limits'][self.visualization_configuration['plot_variable']][1] def updateData(curr): - # if curr <=2: return # Clean previous graphs network.clear() for ax in axis: @@ -400,7 +402,7 @@ def updateData(curr): # Plot all variable histograms for i, ax in enumerate(axis): - n, bins, patches = ax.hist(histo_frames[statuses[i]][curr], range=self.visualization_configuration['variable_limits'][statuses[i]], density=1, bins=25, edgecolor='black') + _, bins, patches = ax.hist(histo_frames[statuses[i]][curr], range=self.visualization_configuration['variable_limits'][statuses[i]], density=1, bins=25, edgecolor='black') bin_centers = 0.5 * (bins[:-1] + bins[1:]) col = bin_centers - min(bin_centers) col /= max(col) @@ -427,9 +429,14 @@ def updateData(curr): sm.set_array([]) fig.colorbar(sm, ax=network) fig.suptitle(self.visualization_configuration['plot_title'], fontsize=16) - plt.show() + + if self.visualization_configuration['show_plot']: + plt.show() if 'plot_output' in self.visualization_configuration.keys(): - writergif = animation.PillowWriter(fps=5) - simulation.save(self.visualization_configuration['plot_output'], writer=writergif) - print('Saved: ' + self.visualization_configuration['plot_output']) \ No newline at end of file + self.save_plot(simulation) + + def save_plot(self, simulation): + writergif = animation.PillowWriter(fps=5) + simulation.save(self.visualization_configuration['plot_output'], writer=writergif) + print('Saved: ' + self.visualization_configuration['plot_output']) diff --git a/ndlib/models/ContinuousModelRunner.py b/ndlib/models/ContinuousModelRunner.py new file mode 100644 index 0000000..d34471b --- /dev/null +++ b/ndlib/models/ContinuousModelRunner.py @@ -0,0 +1,28 @@ +class ContinuousModelRunner(object): + def __init__(self, ContinuousModel, config, N, iterations_list, initial_statuses, constants_list=None, node_status=True): + self.model = ContinuousModel + self.config = config + self.N = N + self.iterations_list = iterations_list + self.node_status = node_status + self.constants_list = constants_list + self.initial_statuses = initial_statuses + + def run(self): + results = [] + + n_iterations = len(self.iterations_list) + n_initial_statuses = len(self.initial_statuses) + + if self.constants_list: + n_constants = len(self.constants_list) + + for i in range(self.N): + print('\nRunning simulation ' + str(i + 1) + '/' + str(self.N) + '\n') + if self.constants_list: + self.model.constants = self.constants_list[i % n_constants] + self.model.set_initial_status(self.initial_statuses[i % n_initial_statuses], self.config) + output = self.model.iteration_bunch(self.iterations_list[i % n_iterations], node_status=self.node_status) + results.append(output) + + return results From c8be553f8c7fd3bd0df0f2b5a460c4ff82609877 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 30 Sep 2020 00:06:28 +0200 Subject: [PATCH 13/25] Document continuous model functions and move examples to folder --- HIOM.py => examples/HIOM.py | 0 .../craving_vs_self_control.py | 0 .../craving_vs_self_control_multi.py | 1 + .../example_continuous_custom_var.py | 17 ++- ndlib/models/ContinuousModel.py | 124 +++++++++++++++++- 5 files changed, 132 insertions(+), 10 deletions(-) rename HIOM.py => examples/HIOM.py (100%) rename craving_vs_self_control.py => examples/craving_vs_self_control.py (100%) rename craving_vs_self_control_multi.py => examples/craving_vs_self_control_multi.py (98%) rename example_continuous_custom_var.py => examples/example_continuous_custom_var.py (85%) diff --git a/HIOM.py b/examples/HIOM.py similarity index 100% rename from HIOM.py rename to examples/HIOM.py diff --git a/craving_vs_self_control.py b/examples/craving_vs_self_control.py similarity index 100% rename from craving_vs_self_control.py rename to examples/craving_vs_self_control.py diff --git a/craving_vs_self_control_multi.py b/examples/craving_vs_self_control_multi.py similarity index 98% rename from craving_vs_self_control_multi.py rename to examples/craving_vs_self_control_multi.py index 9d14761..cfc74a8 100644 --- a/craving_vs_self_control_multi.py +++ b/examples/craving_vs_self_control_multi.py @@ -106,6 +106,7 @@ def update_A(node, graph, status, attributes, constants): ################### SIMULATION ################### # Simulation +# N, iterations_list, initial_statuses, constants_list runner = ContinuousModelRunner(craving_control_model, config, 10, [100], [initial_status]) results = runner.run() diff --git a/example_continuous_custom_var.py b/examples/example_continuous_custom_var.py similarity index 85% rename from example_continuous_custom_var.py rename to examples/example_continuous_custom_var.py index 3b63120..af20e64 100644 --- a/example_continuous_custom_var.py +++ b/examples/example_continuous_custom_var.py @@ -1,5 +1,6 @@ # TODO Dynamic graph, more plots, optimize speed (?), plotly/gephy visualization of graph - +import sys +sys.path.append("..") import networkx as nx import random import numpy as np @@ -44,6 +45,15 @@ def self_confidence_impact(node, graph, status, attributes, constants): attr = {n: {'craving': random.random(), 'self_control': random.random()} for n in g.nodes()} nx.set_node_attributes(g, attr) +# Visualization config +visualization_config = { + 'plot_interval': 10, + 'plot_variable': 'addiction', + 'show_plot': True, + 'plot_title': 'Example model', + 'animation_interval': 500 +} + # Model definition addiction_model = ContinuousModel(g) addiction_model.add_status('addiction') @@ -59,13 +69,14 @@ def self_confidence_impact(node, graph, status, attributes, constants): # Configuration config = mc.Configuration() -config.add_model_parameter('fraction_infected', 0.1) addiction_model.set_initial_status(initial_status, config) +addiction_model.configure_visualization(visualization_config) # Simulation iterations = addiction_model.iteration_bunch(200, node_status=True) trends = addiction_model.build_trends(iterations) +addiction_model.plot(trends, len(iterations), delta=True) ### Plots / data manipulation -addiction_model.visualize(trends, len(iterations), delta=True) +addiction_model.visualize(iterations) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index ea205a7..bff4ccd 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,4 +1,4 @@ -# TODO Write tests, fix visualization logic (overwrite vs update), assert save_file +# TODO Write tests, fix visualization logic (overwrite vs update), assert save_file, add more visualization layout options # Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, tqdm from ndlib.models.DiffusionModel import DiffusionModel @@ -28,8 +28,14 @@ class ContinuousModel(DiffusionModel): def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=None, save_file=None): """ - Model Constructor - :param graph: A networkx graph object + Model Constructor + + :param graph: A networkx graph object + :param constants: dictionary containing state name as key and float or function returning a float as value + :param clean_status: boolean indicating whether to set all status values between 0 and 1 + :param iteration_schemes: list of dictionaries for each scheme + containing a name, function(graph, status), and optional lower and upper bound keys + :param save_file: string indicating path and file name to save the iterations output to """ super(ContinuousModel, self).__init__(graph) self.compartment = {} @@ -59,7 +65,13 @@ def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=N def configure_visualization(self, visualization_configuration): + """ + Configure and assert all visualization configuration parameters + + :param visualization_configuration: dictionary containing all visualization options + """ if visualization_configuration: + print('Configuring visualization...') self.visualization_configuration = visualization_configuration vis_keys = visualization_configuration.keys() if 'plot_interval' in vis_keys: @@ -119,10 +131,10 @@ def configure_visualization(self, visualization_configuration): layout = G.layout_fruchterman_reingold(niter=500) positions = {node: {'pos': location} for node, location in enumerate(layout)} else: - pos = nx.drawing.kamada_kawai_layout(self.graph.graph) + pos = nx.drawing.spring_layout(self.graph.graph) positions = {key: {'pos': location} for key, location in pos.items()} else: - pos = nx.drawing.kamada_kawai_layout(self.graph.graph) + pos = nx.drawing.spring_layout(self.graph.graph) positions = {key: {'pos': location} for key, location in pos.items()} nx.set_node_attributes(self.graph, positions) @@ -133,13 +145,34 @@ def configure_visualization(self, visualization_configuration): for key in list(self.available_statuses.keys()): if key not in list(self.visualization_configuration['variable_limits'].keys()): self.visualization_configuration['variable_limits'][key] = [-1, 1] + if 'animation_interval' not in vis_keys: + self.visualization_configuration['animation_interval'] = 30 + else: + if not isinstance(self.visualization_configuration['animation_interval'], int): + raise ValueError('animation interval must be an integer') + print('Done configuring the visualization') + else: + raise Exception('Provide a visualization configuration when using this function') def add_status(self, status_name): + """ + Add a status/state to the model + """ if status_name not in self.available_statuses: self.available_statuses[status_name] = self.status_progressive self.status_progressive += 1 def add_rule(self, status, function, rule, schemes=['']): + """ + Add a rule to the model + + :param status: string indicating the status + :param function: A function that updates the status value + it receives the parameters: node, graph, status, attributes, constants + :param rule: A condition that should be true before the status is updated + :param schemes: A list of strings matching the names of the schemes in which the rule should be assessed + If no schemes are provided, the default scheme '' is used, which is assessed for every iteration + """ self.compartment[self.compartment_progressive] = (status, function, rule, schemes) self.compartment_progressive += 1 @@ -170,6 +203,9 @@ def set_initial_status(self, initial_status_funs, configuration=None): self.initial_status = copy.deepcopy(self.status) def clean_initial_status(self, valid_status=None): + """ + For every status, set it to 0 if negative, or to 1 if > 1 + """ for n, s in future.utils.iteritems(self.status): for var_val in s.items(): if var_val[1] > 1: @@ -238,6 +274,14 @@ def iteration(self, node_status=True): "status_delta": copy.deepcopy(status_delta)} def iteration_bunch(self, bunch_size, node_status=True): + """ + Execute bunch_size of model iterations and save the result if save_file is set + + :param bunch_size: integer number of iterations to execute + :param node_status: boolean indicating whether to keep the statuses of the nodes + + :return: list of outputs for every iteration + """ iterations = super().iteration_bunch(bunch_size, node_status) if self.save_file: np.save(self.save_file, iterations) @@ -245,6 +289,16 @@ def iteration_bunch(self, bunch_size, node_status=True): return iterations def get_mean_data(self, iterations, mean_type): + """ + Create a dictionary with statuses as keys, and a list of average value per iteration as value + + :param iterations: iterations output from iteration_bunch + :param mean_type: A string containing the type to get the average data from + Should be 'status' or 'status_delta' + + :return: Dictionary containing all statuses as keys, + and as value a list of the average values of all nodes for that status per iteration + """ mean_changes = {} for key in self.available_statuses.keys(): mean_changes[key] = [] @@ -276,6 +330,14 @@ def get_mean_data(self, iterations, mean_type): return mean_changes def build_full_status(self, iterations): + """ + Create a list with objects that have all the nodes with their statuses per iteration + + :param iterations: iterations output from iteration_bunch + + :return: a list of status objects that contain an iteration key with its number as value and a status key + the status value contains per node all its states as keys with the corresponding state values + """ statuses = [] status = {'iteration': 0, 'status': {}} for key, val in iterations[0]['status'].items(): @@ -293,6 +355,14 @@ def build_full_status(self, iterations): return statuses def get_means(self, iterations): + """ + Create a full status and get the mean data for the status key + + :param iterations: iterations output from iteration_bunch + + :return: Dictionary containing all statuses as keys, + and as value a list of the average values of all nodes for the states per iteration + """ self.full_status = self.build_full_status(iterations) means = self.get_mean_data(self.full_status, 'status') return means @@ -308,6 +378,14 @@ def build_trends(self, iterations): return {'mean_delta_status_vals': means_status_delta_vals, 'status_delta': status_delta, 'means': means} def plot(self, trends, n, delta=None, delta_mean=None): + """ + Create and show different plots of the trends + + :param trends: output from the build_trends function + :param n: integer amount of iterations to show + :param delta: boolean indicating whether to show the mean change per variable per iteration + :param delta_mean: boolean indicating whether to show the mean value of changed variables per iteration + """ x = np.arange(0, n) sub_plots = 1 @@ -346,6 +424,18 @@ def plot(self, trends, n, delta=None, delta_mean=None): plt.show() def create_frames(self, iterations): + """ + Create frames every plot_interval iterations + by creating a dictionary that contains a list of all node values for a status key per iteration + + :param iterations: iterations output from iteration_bunch + + :return: tuple of a status value dictionary and a list of lists of node colors, + the dictionary has a status as key and as value a list of lists, the first dimension corresponds with the iteration, + the second dimension is a list that contains all the different node status values for that iteration, + the node_colors list first dimension corresponds with the iteration, + and the second dimension is a list of lists where each index holds the color value of the node at that index + """ if not self.full_status: self.full_status = self.build_full_status(iterations) statuses = list(self.available_statuses.keys()) @@ -367,6 +457,20 @@ def create_frames(self, iterations): return (histo_frames, node_colors) def visualize(self, iterations): + """ + Visualize the network and color the nodes using a status + Show histograms of the other statuses beneath the graph + All visualization options are specified in the visualization_configuration variable + If show_plot is set to True in the configuration, the visualization will be shown dynamically + If plot_output is set in the configuration, an animation will be saved + + Currently the graphs are cleared and then completely redrawn, a good optimization would be to only update the changed values + + :param iterations: iterations output from iteration_bunch + """ + if not self.visualization_configuration: + raise Exception("Specify a visualization configuration before you visualize the model") + (histo_frames, node_colors) = self.create_frames(iterations) statuses = list(self.available_statuses.keys()) @@ -422,14 +526,14 @@ def updateData(curr): network.get_yaxis().set_ticks([]) network.set_title('Iteration: ' + str(curr * self.visualization_configuration['plot_interval'])) - simulation = animation.FuncAnimation(fig, updateData, n, interval=30, repeat=True) + simulation = animation.FuncAnimation(fig, updateData, n, interval=self.visualization_configuration['animation_interval'], repeat=True) norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax) sm = plt.cm.ScalarMappable(cmap=cm, norm=norm) sm.set_array([]) fig.colorbar(sm, ax=network) fig.suptitle(self.visualization_configuration['plot_title'], fontsize=16) - + if self.visualization_configuration['show_plot']: plt.show() @@ -437,6 +541,12 @@ def updateData(curr): self.save_plot(simulation) def save_plot(self, simulation): + """ + Save the plot to a file specified in plot_output int he visualization configuration + The file is generated using the writer from the pillow library + + :param simulation: Output of the matplotlib animation.FuncAnimation function + """ writergif = animation.PillowWriter(fps=5) simulation.save(self.visualization_configuration['plot_output'], writer=writergif) print('Saved: ' + self.visualization_configuration['plot_output']) From fd2c47c9579f08496882ce3718972c9ddc940ff9 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Wed, 30 Sep 2020 03:44:09 +0200 Subject: [PATCH 14/25] Add basic sensitivity analysis --- examples/HIOM.py | 3 + examples/craving_vs_self_control.py | 3 + examples/craving_vs_self_control_multi.py | 21 ++++-- examples/example_continuous_custom_var.py | 1 - ndlib/models/ContinuousModel.py | 17 ++++- ndlib/models/ContinuousModelRunner.py | 92 +++++++++++++++++++---- 6 files changed, 108 insertions(+), 29 deletions(-) diff --git a/examples/HIOM.py b/examples/HIOM.py index 42161e8..b0f912d 100644 --- a/examples/HIOM.py +++ b/examples/HIOM.py @@ -3,6 +3,9 @@ import numpy as np import matplotlib.pyplot as plt +import sys +sys.path.append("..") + from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic diff --git a/examples/craving_vs_self_control.py b/examples/craving_vs_self_control.py index 0dadda6..598050c 100644 --- a/examples/craving_vs_self_control.py +++ b/examples/craving_vs_self_control.py @@ -3,6 +3,9 @@ import numpy as np import matplotlib.pyplot as plt +import sys +sys.path.append("..") + from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic diff --git a/examples/craving_vs_self_control_multi.py b/examples/craving_vs_self_control_multi.py index cfc74a8..cbf7124 100644 --- a/examples/craving_vs_self_control_multi.py +++ b/examples/craving_vs_self_control_multi.py @@ -3,6 +3,9 @@ import numpy as np import matplotlib.pyplot as plt +import sys +sys.path.append("..") + from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.ContinuousModelRunner import ContinuousModelRunner from ndlib.models.compartments.NodeStochastic import NodeStochastic @@ -100,18 +103,20 @@ def update_A(node, graph, status, attributes, constants): # Configuration config = mc.Configuration() craving_control_model.set_initial_status(initial_status, config) -craving_control_model.set_initial_status(initial_status, config) -craving_control_model.configure_visualization(visualization_config) +# craving_control_model.configure_visualization(visualization_config) ################### SIMULATION ################### # Simulation -# N, iterations_list, initial_statuses, constants_list -runner = ContinuousModelRunner(craving_control_model, config, 10, [100], [initial_status]) -results = runner.run() +runner = ContinuousModelRunner(craving_control_model, config) + +# results = runner.run(10, [100], [initial_status]) + +analysis = runner.analyze_sensitivity(initial_status, {'q': (0.5, 1), 'b': (0.25, 0.75), 'd': (0.0, 0.4)}, 5, 50) +print(analysis) ################### VISUALIZATION ################### -for iterations in results: - trends = craving_control_model.build_trends(iterations) - craving_control_model.plot(trends, len(iterations), delta=True) +# for iterations in results: +# trends = craving_control_model.build_trends(iterations) +# craving_control_model.plot(trends, len(iterations), delta=True) diff --git a/examples/example_continuous_custom_var.py b/examples/example_continuous_custom_var.py index af20e64..2b5fdaa 100644 --- a/examples/example_continuous_custom_var.py +++ b/examples/example_continuous_custom_var.py @@ -1,4 +1,3 @@ -# TODO Dynamic graph, more plots, optimize speed (?), plotly/gephy visualization of graph import sys sys.path.append("..") import networkx as nx diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index bff4ccd..a2cc7c9 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,4 +1,6 @@ -# TODO Write tests, fix visualization logic (overwrite vs update), assert save_file, add more visualization layout options +# TODO Write tests, fix visualization logic (overwrite vs update), +# assert save_file, add more visualization layout options, add sensitivity analysis, +# write documentation, add history span for states? # Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, tqdm from ndlib.models.DiffusionModel import DiffusionModel @@ -273,16 +275,23 @@ def iteration(self, node_status=True): return {"iteration": self.actual_iteration - 1, "status": {}, "status_delta": copy.deepcopy(status_delta)} - def iteration_bunch(self, bunch_size, node_status=True): + def iteration_bunch(self, bunch_size, node_status=True, tqdm=True): """ Execute bunch_size of model iterations and save the result if save_file is set :param bunch_size: integer number of iterations to execute :param node_status: boolean indicating whether to keep the statuses of the nodes + :param tqdm: boolean indicating whether to use tqdm to show the estimated duration :return: list of outputs for every iteration """ - iterations = super().iteration_bunch(bunch_size, node_status) + if tqdm: + iterations = super().iteration_bunch(bunch_size, node_status) + else: + iterations = [] + for _ in range(bunch_size): + iterations.append(self.iteration(node_status)) + if self.save_file: np.save(self.save_file, iterations) print('Saved ' + self.save_file) @@ -361,7 +370,7 @@ def get_means(self, iterations): :param iterations: iterations output from iteration_bunch :return: Dictionary containing all statuses as keys, - and as value a list of the average values of all nodes for the states per iteration + and as value a list of the average value over all nodes for that state per iteration """ self.full_status = self.build_full_status(iterations) means = self.get_mean_data(self.full_status, 'status') diff --git a/ndlib/models/ContinuousModelRunner.py b/ndlib/models/ContinuousModelRunner.py index d34471b..0b2af67 100644 --- a/ndlib/models/ContinuousModelRunner.py +++ b/ndlib/models/ContinuousModelRunner.py @@ -1,28 +1,88 @@ +from SALib.sample import saltelli +from SALib.analyze import sobol +import numpy as np + class ContinuousModelRunner(object): - def __init__(self, ContinuousModel, config, N, iterations_list, initial_statuses, constants_list=None, node_status=True): + def __init__(self, ContinuousModel, config, node_status=True): self.model = ContinuousModel self.config = config - self.N = N - self.iterations_list = iterations_list self.node_status = node_status - self.constants_list = constants_list - self.initial_statuses = initial_statuses - def run(self): + def run(self, N, iterations_list, initial_statuses, constants_list=None): results = [] - n_iterations = len(self.iterations_list) - n_initial_statuses = len(self.initial_statuses) + n_iterations = len(iterations_list) + n_initial_statuses = len(initial_statuses) - if self.constants_list: - n_constants = len(self.constants_list) + if constants_list: + n_constants = len(constants_list) - for i in range(self.N): - print('\nRunning simulation ' + str(i + 1) + '/' + str(self.N) + '\n') - if self.constants_list: - self.model.constants = self.constants_list[i % n_constants] - self.model.set_initial_status(self.initial_statuses[i % n_initial_statuses], self.config) - output = self.model.iteration_bunch(self.iterations_list[i % n_iterations], node_status=self.node_status) + for i in range(N): + print('\nRunning simulation ' + str(i + 1) + '/' + str(N) + '\n') + if constants_list: + self.model.constants = constants_list[i % n_constants] + self.model.set_initial_status(initial_statuses[i % n_initial_statuses], self.config) + output = self.model.iteration_bunch(iterations_list[i % n_iterations], node_status=self.node_status) results.append(output) return results + + def analyze_sensitivity(self, initial_status, bounds, n, iterations, second_order=True): + """ + Compute the sensitivity indices for the constants of the model using sobol + Samples are generated using a Saltelli sampler + + :param initial_status: a dictionary with as key a state and as value, a number or function indicating the intial value for this state + :param bounds: a dictionary with a constant name as key and a tuple (lower bound, upper bound) as value + :param n: integer indicating the n for the function: N∗(2D+2) which is used to determine the amount of samples + :param iterations: amount of iterations to run the model per sample + :param second_order: bool indicating whether to include second order indices + + :return: a Python dict with the keys "S1", "S2", "ST", "S1_conf", "S2_conf", and "ST_conf" + """ + if not self.model.constants: + raise Exception('Please add constants when initializing the model to perform sensitivity analysis on') + + problem = { + 'num_vars': len(bounds.keys()), + 'names': [var for var in bounds.keys()], + 'bounds': [ + [lower, upper] for _, (lower, upper) in bounds.items() + ] + } + + param_values = saltelli.sample(problem, n, calc_second_order=second_order) + + outputs = [] + + # Set the constants and run the model + for i in range(len(param_values)): + print('Running simulation ' + str(i + 1) + '/' + str(len(param_values))) + # Set the constants + for j, name in enumerate(problem['names']): + self.model.constants[name] = param_values[i,j] + # Set intial values + self.model.set_initial_status(initial_status, self.config) + outputs.append(self.model.iteration_bunch(iterations, node_status=self.node_status, tqdm=False)) + + # Parse the outputs for every simulation (TODO: Optimize) + states = list(self.model.available_statuses.keys()) + states.remove('Infected') + averages = { + var: np.array([]) for var in states + } + + print('Parsing outputs...') + for output in outputs: + # Get the average value of all the nodes for every status at the last iteration of the simulation + means = self.model.get_means(output) + for status in averages.keys(): + averages[status] = np.append(averages[status], means[status][-1]) + + print('Running sensitivity analysis...') + # Perform the sobol analysis seperately for every status + analysis = { + var: sobol.analyze(problem, averages[var]) for var in states + } + + return analysis \ No newline at end of file From b979f965408328fcbcfd1b3647898778295fcdd9 Mon Sep 17 00:00:00 2001 From: Mathijs Maijer Date: Wed, 30 Sep 2020 23:47:24 +0200 Subject: [PATCH 15/25] :memo: Write docs and make several quality of life fixes --- .vscode/settings.json | 3 + .../compartments/NodeNumericalAttribute.rst | 4 +- .../compartments/NodeNumericalVariable.rst | 115 ++++++++ .../continuous_model/continuous_model.rst | 277 ++++++++++++++++++ .../examples/ControlVsCraving.rst | 184 ++++++++++++ .../custom/continuous_model/examples/HIOM.rst | 178 +++++++++++ .../continuous_model/optional/ModelRunner.rst | 164 +++++++++++ .../optional/Visualization.rst | 87 ++++++ docs/custom/custom.rst | 25 +- docs/index.rst | 2 + docs/installing.rst | 31 ++ examples/HIOM.py | 12 +- examples/craving_vs_self_control.py | 6 +- examples/numerical_variable_test.py | 43 +++ ndlib/models/ContinuousModel.py | 12 +- ndlib/models/ContinuousModelRunner.py | 2 +- .../compartments/NodeNumericalVariable.py | 14 +- 17 files changed, 1132 insertions(+), 27 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 docs/custom/compartments/NodeNumericalVariable.rst create mode 100644 docs/custom/continuous_model/continuous_model.rst create mode 100644 docs/custom/continuous_model/examples/ControlVsCraving.rst create mode 100644 docs/custom/continuous_model/examples/HIOM.rst create mode 100644 docs/custom/continuous_model/optional/ModelRunner.rst create mode 100644 docs/custom/continuous_model/optional/Visualization.rst create mode 100644 examples/numerical_variable_test.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a625774 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "restructuredtext.confPath": "${workspaceFolder}\\docs" +} \ No newline at end of file diff --git a/docs/custom/compartments/NodeNumericalAttribute.rst b/docs/custom/compartments/NodeNumericalAttribute.rst index c971376..73c6394 100644 --- a/docs/custom/compartments/NodeNumericalAttribute.rst +++ b/docs/custom/compartments/NodeNumericalAttribute.rst @@ -4,9 +4,9 @@ Node Numerical Attribute Node Numerical Attribute compartments are used to evaluate events attached to numeric edge attributes. -Consider the transition rule **Susceptible->Infected** that requires a that the susceptible node express a specific value +Consider the transition rule **Susceptible->Infected** that requires that the susceptible node expresses a specific value of an internal numeric attribute, *attr*, to be satisfied (e.g. "Age" == 18). -Such rule can be described by a simple compartment that models Node Numerical Attribute selection. Let's call il *NNA*. +Such a rule can be described by a simple compartment that models Node Numerical Attribute selection. Let's call it *NNA*. The rule will take as input the *initial* node status (Susceptible), the *final* one (Infected) and the *NNA* compartment. *NNA* will thus require a probability (*beta*) of activation. diff --git a/docs/custom/compartments/NodeNumericalVariable.rst b/docs/custom/compartments/NodeNumericalVariable.rst new file mode 100644 index 0000000..4be9af3 --- /dev/null +++ b/docs/custom/compartments/NodeNumericalVariable.rst @@ -0,0 +1,115 @@ +************************ +Node Numerical Variable +************************ + +Node Numerical Variable compartments are used to evaluate events attached to numeric edge attributes or statuses. + +Consider the transition rule **Addicted->Not addicted** that requires that the susceptible node satisfies a specific condition +of an internal numeric attribute, *attr*, to be satisfied (e.g. "Self control" *attr* < "Craving" *status*). +Such a rule can be described by a simple compartment that models Node Numerical Attribute and Status selection. Let's call it *NNV*. + +The rule will take as input the *initial* node status (Susceptible), the *final* one (Infected) and the *NNV* compartment. +*NNV* will thus require a probability (*beta*) of activation. + +During each rule evaluation, given a node *n* and one of its neighbors *m* + +- if the actual status of *n* equals the rule *initial* + - if *var(n)* **op** *var(n)* (where var(n) = attr(n) or status(n)) + - a random value *b* in [0,1] will be generated + - if *b <= beta*, then *NNV* is considered *satisfied* and the status of *n* changes from *initial* to *final*. + +**op** represent a logic operator and can assume one of the following values: +- equality: "==" +- less than: "<" +- greater than: ">" +- equal or less than: "<=" +- equal or greater than: ">=" +- not equal to: "!=" +- within: "IN" + +Moreover, *NNV* allows to specify a *triggering* status in order to restrain the compartment evaluation to those nodes that: + +1. match the rule *initial* state, and +2. have at least one neighbors in the *triggering* status. + +The type of the values that are compared have to be specified in advance, which is done using an enumerated type. +This is done to specify whether the first value to be compared is either a status or an attribute, +the same thing is done for the second value to be compared. +If the value type is not specified, the value to compare the variable to should be a number. + +---------- +Parameters +---------- + +================= ================= ======= ========= =================================== +Name Value Type Default Mandatory Description +================= ================= ======= ========= =================================== +variable string None True The name of the variable to compare +variable_type NumericalType None True Numerical type enumerated value +value numeric(*)|string None True Name of the testing value or number +value_type NumericalType None False Numerical type enumerated value +op string None True Logic operator +probability float in [0, 1] 1 False Event probability +triggering_status string None False Trigger +================= ================= ======= ========= =================================== + +(*) When *op* equals "IN" the attribute *value* is expected to be a tuple of two elements identifying a closed interval. + +------- +Example +------- + +In the code below the formulation of a model is shown using NodeNumericalVariable compartments. + +The first compartment, *condition*, is used to implement the transition rule *Susceptible->Infected*. +It restrains the rule evaluation to all those nodes having more "Friends" than 18. + +The second compartment, *condition2*, is used to implement the transition rule *Infected->Recovered*. +It restrains the rule evaluation to all those nodes where "Age" is less than the amount of "Friends" attributes. + +Note that instead of attributes, the states could have been used as well by using *NumericalType.STATUS* instead. +This would only be applicable for numerical states, which can be modelled when using the ``ContinuousModel`` instead of the ``CompositeModel``. + + +.. code-block:: python + + import networkx as nx + import random + import numpy as np + + from ndlib.models.CompositeModel import CompositeModel + from ndlib.models.compartments.NodeStochastic import NodeStochastic + from ndlib.models.compartments.enums.NumericalType import NumericalType + from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable + import ndlib.models.ModelConfig as mc + + # Network generation + g = nx.erdos_renyi_graph(1000, 0.1) + + # Setting edge attribute + attr = {n: {"Age": random.choice(range(0, 100)), "Friends": random.choice(range(0, 100))} for n in g.nodes()} + nx.set_node_attributes(g, attr) + + # Composite Model instantiation + model = CompositeModel(g) + + # Model statuses + model.add_status("Susceptible") + model.add_status("Infected") + model.add_status("Removed") + + # Compartment definition + condition = NodeNumericalVariable('Friends', var_type=NumericalType.ATTRIBUTE, value=18, op='>') + condition2 = NodeNumericalVariable('Age', var_type=NumericalType.ATTRIBUTE, value='Friends', value_type=NumericalType.ATTRIBUTE, op='<') + + # Rule definition + model.add_rule("Susceptible", "Infected", condition) + model.add_rule("Infected", "Removed", condition2) + + # Model initial status configuration + config = mc.Configuration() + config.add_model_parameter('fraction_infected', 0.5) + + # Simulation execution + model.set_initial_status(config) + iterations = model.iteration_bunch(100) \ No newline at end of file diff --git a/docs/custom/continuous_model/continuous_model.rst b/docs/custom/continuous_model/continuous_model.rst new file mode 100644 index 0000000..d5aad25 --- /dev/null +++ b/docs/custom/continuous_model/continuous_model.rst @@ -0,0 +1,277 @@ +================ +Continuous Model +================ + +The composite model only supports discrete states, but more advanced custom models might require continuous states and more options. +The general manner of creating a model remains the same as the ``CompositeModel``, but it allows for configuration by adding (optional) extra steps. + +The general modeling flow is as follows: + + 1. Define a graph + 2. Add (continuous) internal states + 3. Define constants and intial values + 4. Create update conditions + 5. Add iteration schemes (optional) + 6. Simulate + 7. Optional steps(Visualize/Sensitivity analysis) + +------------------------------------ +Graph, internal states and constants +------------------------------------ + +The graphs should still be ``Networkx`` graphs, either defined by yourself or generated using one of their built-in functions. +Attributes in the graph can still be accessed and used to update functions. + +After a graph is defined, the model can be initialized and internal states can be added to the model. When the model is initalized, +states can be added using ``add_status(status)`` function, where the `status` argument is a string. + +If the model requires certain constant values, these can be added using the ``constants`` parameter when initializing the model. +It should be a dictionary where the key corresponds to the constant name and the value to the constant value. +Adding constants is completely optional. + +Example: + +.. code-block:: python + + import networkx as nx + from ndlib.models.ContinuousModel import ContinuousModel + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + constants = { + 'constant_1': 0.1, + 'constant_2': 2.5 + } + + model = ContinuousModel(g, constants=constants) + model.add_status('status1') + model.add_status('status2') + +------------- +Intial values +------------- + +After the graph has been created, the model has been initalized, and the internal states have been added, +the next step is to define the intial values of the states. + +This is done by creating a dictionary, that maps a state name to an initial value. +This value has to be a continous value, that can be statically set, or it can be a function that will be executed for every node. +If the value is a function, it should take the following arguments: + + - node: the current node for which the initial state is being set + - graph: the full networkx graph containing all nodes + - status: a dictionary that contains all the previously set state (str) -> value (number) mappings for that node + - constants: the dictionary of specified constants, None if no constants are specified + +After creating the dictionary, it can be added to the model using the ``set_initial_status(dict, config)`` function. +The config argument is acquired by the ``Configuration()`` function from the ``ModelConfig`` class. + +The example below will create a model with 3 states. Every node in the model will initialize `status_1` with a value returned by the `initial_status_1` function, +which will results in all nodes getting a random uniform value between 0 and 0.5. The same happens for the internal state `status_2`. +The third state is constant and thus the same for every node. + +.. code-block:: python + + import networkx as nx + import numpy as np + from ndlib.models.ContinuousModel import ContinuousModel + import ndlib.models.ModelConfig as mc + + def initial_status_1(node, graph, status, constants): + return np.random.uniform(0, 0.5) + + def initial_status_2(node, graph, status, constants): + return status['status_1'] + np.random.uniform(0.5, 1) + + initial_status = { + 'status_1': initial_status_1, + 'status_2': initial_status_2, + 'status_3': 2 + } + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + model = ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + model.add_status('status_3') + + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + +----------------- +Update conditions +----------------- + +Another important part of the model is creating conditions and update rules. This follows the same principle as using the compartments, +which have already been explained. Every update condition has a: + + - State: A string matching an internal state + + - Update function: A function that should be used to update the state if the condition is true. Its arguments are: + + - node: The number of the node that is currently updated + - graph: The complete graph containing all the nodes + - status: A status dictionary that maps a node to a dictionary with State -> Value mappings + - attributes: The networkx attributes of the network + - constants: The specified constants, None if not defined + + - Condition: A compartment condition (Node/Edge/Time) + + - Scheme (optional): An iteration scheme to specify the iteration number(s) and node(s) + +In the example below, the states are updated when the condition ``NodeStochastic(1)`` is true, which is always the case, so the update functions are called every iteration. +Here the state `status_1` will be updated every iteration by setting it equal to `status_2` + 0.1. The same is done for `status_2`, but in this case it is set equal to `status_1` + 0.5. + +.. code-block:: python + + import networkx as nx + from ndlib.models.ContinuousModel import ContinuousModel + import ndlib.models.ModelConfig as mc + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + model = ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] + 0.1 + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + 0.5 + + # Rules + model.add_rule('status_1', update_1, condition) + model.add_rule('status_2', update_2, condition) + +----------------- +Iteration schemes +----------------- + +Another addition to the model, are iteration schemes. These can be used for two things: + +1. Specify nodes to update +2. Specify iteration range when updates should take place + +This allows to only update a select amount of nodes during a specific time in the iteration. +Under the hood, when schemes are not defined, +a default scheme is used for every rule that is active during each iteration and selects all nodes. + +To create a scheme, simply define a list where each element is a dictionary containing the following keys: + + - name: maps to a string that indicates the name of the scheme + - function: maps to a function that returns the nodes that should be updated if a condition is true. Its arguments are: + + - graph: the full networkx graph + - status: A status dictionary that maps a node to a dictionary with State -> Value mappings + + - lower (optional): maps to an integer indicating from which iteration the scheme should apply + - upper (optional): maps to an integer indicating until which iteration the scheme should apply + +After the scheme dictionary is created, it can be added to the model when the model is initalized: +``ContinuousModel(graph, constants=constants, iteration_schemes=schemes)``. + +Furthermore, if rules are added using the ``add_rule`` function, it should now be done as follows: +``model.add_rule('state_name', update_function, condition, scheme_list)``. +Here a rule can be added to multiple schemes. The scheme_list is a list, where every element should match a name of a scheme, +which means that updates can be done in multiple schemes. +If a scheme_list is not provided, the rule will be executed for every iteration, for every node, if the condition is true. + +In the example below, the previous model is executed in the same manner, +but this time, the update_1 function is only being evaluated when ``lower ≤ iteration < upper``, +in this case when the iterations are equal or bigger than 100 but lower than 200. +Furthermore, if the condition is true, the update function is then only executed for the nodes returned by the function specified in the scheme. +In this case a node is selected based on the weighted `status_1` value. +Because no scheme has been added to the second rule, it will be evaluated and executed for every node, each iteration. + + +.. code-block:: python + + import networkx as nx + import numpy as np + from ndlib.models.ContinuousModel import ContinuousModel + import ndlib.models.ModelConfig as mc + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + # Define schemes + def sample_state_weighted(graph, status): + probs = [] + status_1 = [stat['status_1'] for stat in list(status.values())] + factor = 1.0/sum(status_1) + for s in status_1: + probs.append(s * factor) + return np.random.choice(graph.nodes, size=1, replace=False, p=probs) + + schemes = [ + { + 'name': 'random weighted agent', + 'function': sample_state_weighted, + 'lower': 100, + 'upper': 200 + } + ] + + model = ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] + 0.1 + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + 0.5 + + # Rules + model.add_rule('status_1', update_1, condition, ['random weighted agent']) + model.add_rule('status_2', update_2, condition) + + +---------- +Simulation +---------- + +After everything has been specified and added to the model, it can be ran using the ``iteration_bunch(iterations)`` function. +It will run the model iterations amount of times and return the regular output as shown in other models before. + +------------------------ +Optional functionalities +------------------------ + +There are several extra configurations and options: + +.. toctree:: + :maxdepth: 2 + + optional/ModelRunner.rst + optional/Visualization.rst + +The ``ContinuousModelRunner`` can be used to simulate a model mutliple times using different parameters. +It also includes sensitivity analysis functionalities. + +The visualization section explains how visualizations can be configured, shown, and saved. + +------------------- +Continuous examples +------------------- + +Two examples have been added that reproduce models shown in two different papers. + +.. toctree:: + :maxdepth: 2 + + examples/ControlVsCraving.rst + examples/HIOM.rst \ No newline at end of file diff --git a/docs/custom/continuous_model/examples/ControlVsCraving.rst b/docs/custom/continuous_model/examples/ControlVsCraving.rst new file mode 100644 index 0000000..1e40a66 --- /dev/null +++ b/docs/custom/continuous_model/examples/ControlVsCraving.rst @@ -0,0 +1,184 @@ +******************************* +Self control vs craving example +******************************* + +----------- +Description +----------- + +This is an example of how a continuous model that uses multiple internal states can be modelled. +In this case, we have modelled the `The Dynamics of Addiction: Craving versus Self-Control (Johan Grasman, Raoul P P P Grasman, Han L J van der Maas) `_. +The model tries to model addiction by defining several interacting states; craving, self control, addiction, lambda, external influences, vulnerability, and addiction. + +It was slightly changed by using the average neighbour addiction to change the External influence variable to make it spread through the network. + +---- +Code +---- + +.. code-block:: python + + import networkx as nx + import random + import numpy as np + import matplotlib.pyplot as plt + + from ndlib.models.ContinuousModel import ContinuousModel + from ndlib.models.compartments.NodeStochastic import NodeStochastic + + import ndlib.models.ModelConfig as mc + + ################### MODEL SPECIFICATIONS ################### + + constants = { + 'q': 0.8, + 'b': 0.5, + 'd': 0.2, + 'h': 0.2, + 'k': 0.25, + 'S+': 0.5, + } + constants['p'] = 2*constants['d'] + + def initial_v(node, graph, status, constants): + return min(1, max(0, status['C']-status['S']-status['E'])) + + def initial_a(node, graph, status, constants): + return constants['q'] * status['V'] + (np.random.poisson(status['lambda'])/7) + + initial_status = { + 'C': 0, + 'S': constants['S+'], + 'E': 1, + 'V': initial_v, + 'lambda': 0.5, + 'A': initial_a + } + + def update_C(node, graph, status, attributes, constants): + return status[node]['C'] + constants['b'] * status[node]['A'] * min(1, 1-status[node]['C']) - constants['d'] * status[node]['C'] + + def update_S(node, graph, status, attributes, constants): + return status[node]['S'] + constants['p'] * max(0, constants['S+'] - status[node]['S']) - constants['h'] * status[node]['C'] - constants['k'] * status[node]['A'] + + def update_E(node, graph, status, attributes, constants): + # return status[node]['E'] - 0.015 # Grasman calculation + + avg_neighbor_addiction = 0 + for n in graph.neighbors(node): + avg_neighbor_addiction += status[n]['A'] + + return max(-1.5, status[node]['E'] - avg_neighbor_addiction / 50) # Custom calculation + + def update_V(node, graph, status, attributes, constants): + return min(1, max(0, status[node]['C']-status[node]['S']-status[node]['E'])) + + def update_lambda(node, graph, status, attributes, constants): + return status[node]['lambda'] + 0.01 + + def update_A(node, graph, status, attributes, constants): + return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) + + ################### MODEL CONFIGURATION ################### + + # Network definition + g = nx.random_geometric_graph(200, 0.125) + + # Visualization config + visualization_config = { + 'plot_interval': 2, + 'plot_variable': 'A', + 'variable_limits': { + 'A': [0, 0.8], + 'lambda': [0.5, 1.5] + }, + 'show_plot': True, + 'plot_output': './c_vs_s.gif', + 'plot_title': 'Self control vs craving simulation', + } + + # Model definition + craving_control_model = ContinuousModel(g, constants=constants) + craving_control_model.add_status('C') + craving_control_model.add_status('S') + craving_control_model.add_status('E') + craving_control_model.add_status('V') + craving_control_model.add_status('lambda') + craving_control_model.add_status('A') + + # Compartments + condition = NodeStochastic(1) + + # Rules + craving_control_model.add_rule('C', update_C, condition) + craving_control_model.add_rule('S', update_S, condition) + craving_control_model.add_rule('E', update_E, condition) + craving_control_model.add_rule('V', update_V, condition) + craving_control_model.add_rule('lambda', update_lambda, condition) + craving_control_model.add_rule('A', update_A, condition) + + # Configuration + config = mc.Configuration() + craving_control_model.set_initial_status(initial_status, config) + craving_control_model.configure_visualization(visualization_config) + + ################### SIMULATION ################### + + # Simulation + iterations = craving_control_model.iteration_bunch(100, node_status=True) + trends = craving_control_model.build_trends(iterations) + + ################### VISUALIZATION ################### + + # Show the trends of the model + craving_control_model.plot(trends, len(iterations), delta=True) + + # Recreate the plots shown in the paper to verify the implementation + x = np.arange(0, len(iterations)) + plt.figure() + + plt.subplot(221) + plt.plot(x, trends['means']['E'], label='E') + plt.plot(x, trends['means']['lambda'], label='lambda') + plt.legend() + + plt.subplot(222) + plt.plot(x, trends['means']['A'], label='A') + plt.plot(x, trends['means']['C'], label='C') + plt.legend() + + plt.subplot(223) + plt.plot(x, trends['means']['S'], label='S') + plt.plot(x, trends['means']['V'], label='V') + plt.legend() + + plt.show() + + # Show animated plot + craving_control_model.visualize(iterations) + + +------ +Output +------ + +After simulating the model, we get three outputs, the first figure shows the trends of the model per state. +It shows the average value per state per iteration and it shows the mean change per state per iteration. + +The second figure was created to compare it with a figure that is shown in the paper as verification. + +The last figure is an animation that is outputted when the visualize function is called. + +.. figure:: https://i.imgur.com/zXKkT6S.png + :align: center + :alt: Trends + +.. figure:: https://i.imgur.com/HfoWlnr.png + :align: center + :alt: Verification + +.. figure:: https://i.imgur.com/EeucXQt.gif + :align: center + :alt: Animation + + diff --git a/docs/custom/continuous_model/examples/HIOM.rst b/docs/custom/continuous_model/examples/HIOM.rst new file mode 100644 index 0000000..a33cdd7 --- /dev/null +++ b/docs/custom/continuous_model/examples/HIOM.rst @@ -0,0 +1,178 @@ +************ +HIOM example +************ + +----------- +Description +----------- + +This example will be slightly more complex, +as it involves different schemes and update functions to model the spread of opinion polarization within and across individuals. + +The paper: `The polarization within and across individuals: the hierarchical Ising opinion model (Han L J van der Maas, Jonas Dalege, Lourens Waldorp) `_ + +---- +Code +---- + +.. code-block:: python + + import networkx as nx + import random + import numpy as np + import matplotlib.pyplot as plt + + from ndlib.models.ContinuousModel import ContinuousModel + from ndlib.models.compartments.NodeStochastic import NodeStochastic + + import ndlib.models.ModelConfig as mc + + ################### MODEL SPECIFICATIONS ################### + + constants = { + 'dt': 0.01, + 'A_min': -0.5, + 'A_star': 1, + 's_O': 0.01, + 's_I': 0, + 'd_A': 0, + 'p': 1, + 'r_min': 0, + 't_O': np.inf, + } + + def initial_I(node, graph, status, constants): + return np.random.normal(0, 0.3) + + def initial_O(node, graph, status, constants): + return np.random.normal(0, 0.2) + + initial_status = { + 'I': initial_I, + 'O': initial_O, + 'A': 1 + } + + def update_I(node, graph, status, attributes, constants): + nb = np.random.choice(graph.neighbors(node)) + if abs(status[node]['O'] - status[nb]['O']) > constants['t_O']: + return status[node]['I'] # Do nothing + else: + # Update information + r = constants['r_min'] + (1 - constants['r_min']) / (1 + np.exp(-1 * constants['p'] * (status[node]['A'] - status[nb]['A']))) + inf = r * status[node]['I'] + (1-r) * status[nb]['I'] + np.random.normal(0, constants['s_I']) + + # Update attention + status[node]['A'] = status[node]['A'] + constants['d_A'] * (2 * constants['A_star'] - status[node]['A']) + status[nb]['A'] = status[nb]['A'] + constants['d_A'] * (2 * constants['A_star'] - status[nb]['A']) + + return inf + + return + + def update_A(node, graph, status, attributes, constants): + return status[node]['A'] - 2 * constants['d_A'] * status[node]['A']/len(graph.nodes) + + def update_O(node, graph, status, attributes, constants): + noise = np.random.normal(0, constants['s_O']) + x = status[node]['O'] - constants['dt'] * (status[node]['O']**3 - (status[node]['A'] + constants['A_min']) * status[node]['O'] - status[node]['I']) + noise + return x + + def shrink_I(node, graph, status, attributes, constants): + return status[node]['I'] * 0.999 + + def shrink_A(node, graph, status, attributes, constants): + return status[node]['A'] * 0.999 + + def sample_attention_weighted(graph, status): + probs = [] + A = [stat['A'] for stat in list(status.values())] + factor = 1.0/sum(A) + for a in A: + probs.append(a * factor) + return np.random.choice(graph.nodes, size=1, replace=False, p=probs) + + schemes = [ + { + 'name': 'random agent', + 'function': sample_attention_weighted, + }, + { + 'name': 'all', + 'function': lambda graph, status: graph.nodes, + }, + { + 'name': 'shrink I', + 'function': lambda graph, status: graph.nodes, + 'lower': 5000 + }, + { + 'name': 'shrink A', + 'function': lambda graph, status: graph.nodes, + 'lower': 10000 + }, + ] + + ################### MODEL CONFIGURATION ################### + + # Network definition + g = nx.watts_strogatz_graph(400, 2, 0.02) + + # Visualization config + visualization_config = { + 'layout': 'fr', + 'plot_interval': 100, + 'plot_variable': 'O', + 'variable_limits': { + 'A': [0, 1] + }, + 'cmin': -1, + 'cmax': 1, + 'color_scale': 'RdBu', + 'plot_output': './HIOM.gif', + 'plot_title': 'HIERARCHICAL ISING OPINION MODEL', + } + + # Model definition + HIOM = ContinuousModel(g, constants=constants, iteration_schemes=schemes) + HIOM.add_status('I') + HIOM.add_status('A') + HIOM.add_status('O') + + # Compartments + condition = NodeStochastic(1) + + # Rules + HIOM.add_rule('I', update_I, condition, ['random agent']) + HIOM.add_rule('A', update_A, condition, ['all']) + HIOM.add_rule('O', update_O, condition, ['all']) + HIOM.add_rule('I', shrink_I, condition, ['shrink I']) + HIOM.add_rule('A', shrink_A, condition, ['shrink A']) + + # Configuration + config = mc.Configuration() + HIOM.set_initial_status(initial_status, config) + HIOM.configure_visualization(visualization_config) + + ################### SIMULATION ################### + + iterations = HIOM.iteration_bunch(15000, node_status=True) + trends = HIOM.build_trends(iterations) + + ################### VISUALIZATION ################### + + HIOM.plot(trends, len(iterations), delta=True) + HIOM.visualize(iterations) + +------ +Output +------ + +.. figure:: https://i.imgur.com/xIpmL6X.png + :align: center + :alt: Verification + +.. figure:: https://i.imgur.com/emEFOlx.gif + :align: center + :alt: Verification + diff --git a/docs/custom/continuous_model/optional/ModelRunner.rst b/docs/custom/continuous_model/optional/ModelRunner.rst new file mode 100644 index 0000000..1fb6af9 --- /dev/null +++ b/docs/custom/continuous_model/optional/ModelRunner.rst @@ -0,0 +1,164 @@ +*********************** +Continuous Model Runner +*********************** + +Often, a model is not only executed once for n amount of iterations. Most of the time meaningful conclusions can be drawn after simulating the model multiple times and even using different inputs. +This is made possible using the ``ContinuousModelRunner``. + +It takes as input a ``ContinuousModel`` object and a ``Configuration`` object (created by using ``ModelConfig``). + +After instantiating the runner object, two functions can be used; +one runs the model multiple times for n amount of iterations using different parameters, +the other performs sensitivity analysis based on specified measures. + +------ +Runner +------ + +The model can be executed `N` amount with different parameters using the +``run(N, iterations_list, initial_statuses, constants_list=None)`` function. +If the length of a list is not equal to the amount of simulations, this is no problem, +as the index of the list will be selected using ``iteration number % list_length``. +This means if you want to only use one value for every simulation, simply provide a list as argument with only one value. + +================ ================ ======= ========= ===================================================================== +Run parameters Value Type Default Mandatory Description +================ ================ ======= ========= ===================================================================== +N number True The amount of times to run the simulation +iterations_list list[number] True A list containing the amount of iterations to use per simulation +initial_statuses list[dictionary] True A list containing `initial_status` dictionaries to use per simulation +constants_list list[dictionary] None False A list containing `constants` dictionaries to use per simulation +================ ================ ======= ========= ===================================================================== + +Example: + +.. code-block:: python + + import networkx as nx + import numpy as np + from ndlib.models.ContinuousModel import ContinuousModel + from ndlib.models.ContinuousModelRunner import ContinuousModelRunner + from ndlib.models.compartments.NodeStochastic import NodeStochastic + import ndlib.models.ModelConfig as mc + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + def initial_status_1(node, graph, status, constants): + return np.random.uniform(0, 0.5) + + def initial_status_2(node, graph, status, constants): + return status['status_1'] + np.random.uniform(0.5, 1) + + initial_status = { + 'status_1': initial_status_1, + 'status_2': initial_status_2, + 'status_3': 2 + } + + model = ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] + 0.1 + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + 0.5 + + # Rules + model.add_rule('status_1', update_1, condition) + model.add_rule('status_2', update_2, condition) + + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + # Simulation + runner = ContinuousModelRunner(craving_control_model, config) + # Simulate the model 10 times with 100 iterations + results = runner.run(10, [100], [initial_status]) + +-------------------- +Sensitivity Analysis +-------------------- + +Another important part of analysing a model is sensitivity analysis. +Custom analysis can be done using the run function, but an integrated SALib version is included +and can be ran using the ``analyze_sensitivity(initial_status, bounds, n, iterations, second_order=True)`` function. + +It requires the following parameters: + +============== =================================== ======= ========= ============================================================================= +parameters Value Type Default Mandatory Description +============== =================================== ======= ========= ============================================================================= +initial_status dictionary True A dictionary containing the initial status per state +bounds dictionary{status => (lower, upper) True A dictionary mapping a status string to a tuple in the form of [lower, upper] +n integer True The amount of samples to get from the SALib saltelli sampler +iterations integer True A list containing `constants` dictionaries to use per simulation +second_order boolean True False Boolean indicating whether to include second order indices +============== =================================== ======= ========= ============================================================================= + +At the moment, after every simulation, the mean value for a state is taken over all the nodes, which is seen as one output for the model. +After running the analysis, a dictionary is returned, mapping a state to a dictionary with the keys "S1", "S2", "ST", "S1_conf", "S2_conf", and "ST_conf" +which is acquired by using ``sobol.analyze()`` from SALib. + +Example: + +.. code-block:: python + + import networkx as nx + import numpy as np + from ndlib.models.ContinuousModel import ContinuousModel + from ndlib.models.ContinuousModelRunner import ContinuousModelRunner + from ndlib.models.compartments.NodeStochastic import NodeStochastic + import ndlib.models.ModelConfig as mc + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + constants = { + 'constant_1': 0.5, + 'constant_2': 0.8 + } + + def initial_status_1(node, graph, status, constants): + return np.random.uniform(0, 0.5) + + def initial_status_2(node, graph, status, constants): + return status['status_1'] + np.random.uniform(0.5, 1) + + initial_status = { + 'status_1': initial_status_1, + 'status_2': initial_status_2, + 'status_3': 2 + } + + model = ContinuousModel(g, constants=constants) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] * constants['constant_1'] + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + constants['constant_2'] + + # Rules + model.add_rule('status_1', update_1, condition) + model.add_rule('status_2', update_2, condition) + + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + # Simulation + runner = ContinuousModelRunner(model, config) + analysis = runner.analyze_sensitivity(initial_status, {'constant_1': (0, 1), 'constant_2': (-1, 1)}, 100, 50) + diff --git a/docs/custom/continuous_model/optional/Visualization.rst b/docs/custom/continuous_model/optional/Visualization.rst new file mode 100644 index 0000000..3928e9e --- /dev/null +++ b/docs/custom/continuous_model/optional/Visualization.rst @@ -0,0 +1,87 @@ +****************************** +Continuous Model Visualization +****************************** + +Visualization is often a very important section. The continuous model class allows for a lot of flexibility for what should be shown. +In general, the usual functions like creatings trends, and normally plotting work in the same manner as for the ``CompositeModel``. +But more features have been added to also visualize the specified network and color the nodes using an internal state. +The other states are shown beneath the network graph using histogram figures. + +------- +Example +------- + +This is an example generated visualization. It shows addiction as a node color, while the other states of the model are shown using histograms. + +.. figure:: https://i.imgur.com/pZBmhc3.gif + :align: center + :alt: Continuous Visualization + +------------- +Configuration +------------- + +To configure and enable visualization, a configuration dictionary should be created first. + +It should hold the following key -> value mappings: + +================== =============== ===================================== ========= ============================================================== +Key (string) Value Type Default Mandatory Description +================== =============== ===================================== ========= ============================================================== +plot_interval number True How many iterations should be between each plot +plot_variable string True The state to use as node color +show_plot boolean True False Whether a plot should be shown +plot_output string False Should be a path + file name, if set it will save a gif there +plot_title string Network simulation of `plot_variable` False The title of the visualization +plot_annotation string False The annotation of the visualization +cmin number 0 False The minimum color to display in the colorbar +cmax number 0 False The maximum color to display in the colorbar +color_scale string RdBu False Matplotlib colorscale colors to use +layout string|function nx.drawing.spring_layout False Name of the networkx layout to use +variable_limits dictionary {state: [-1, 1] for state in states} False Dictionary mapping state name to a list with min and max value +animation_interval integer 30 False Amount of miliseconds between each frame +================== =============== ===================================== ========= ============================================================== + +When the configuration dictionary has been initialized and the model has been initialized, it can be added to the model using the function ``configure_visualization(visualization_dictionary)``. + +.. note:: + + By default, if the nodes in the networkx graph already have positions (the pos attribute is set per node), + then the positions of the nodes will be used as a layout. If no positions are set, a spring layout will be used, or a specified layout will be used. + + The ``layout`` key currently supports some igraph layouts as well, but it requires the igraph and pyintergraph libraries installed. + + The following values are supported: + + - ``fr``: Creates an igraph layout using the fruchterman reingold algorithm + + +Example: + +.. code-block:: python + + import networkx as nx + from ndlib.models.ContinuousModel import ContinuousModel + + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + model = ContinuousModel(g) + model.add_status('status_1') + + # Visualization config + visualization_config = { + 'plot_interval': 5, + 'plot_variable': 'status_1', + 'variable_limits': { + 'status_1': [0, 0.8] + }, + 'show_plot': True, + 'plot_output': './animations/model_animation.gif', + 'plot_title': 'Animated network', + } + + model.configure_visualization(visualization_config) + +After running the model using the ``iteration_bunch`` function, the returned value can then be used to call the ``visualize(iterations)`` function, which will produce the plot shown in animation above. + +It is also possible to recreate the standard static plots using the ``plot(trends, len(iterations), delta=True)`` function. The first argument takes the trends created by the ``build_trends(iterations)`` function. \ No newline at end of file diff --git a/docs/custom/custom.rst b/docs/custom/custom.rst index f7f446b..a52fa95 100644 --- a/docs/custom/custom.rst +++ b/docs/custom/custom.rst @@ -21,12 +21,15 @@ The last step of such process can be easily decomposed into atomic operations th .. note:: - ``NDlib`` exposes two classes for defining custom diffusion models: + ``NDlib`` exposes three classes for defining custom diffusion models: - - ``CompositeModel`` describes diffusion models for static networks - - ``DynamicCompositeModel`` describes diffusion models for dynamic networks + - ``CompositeModel`` describes diffusion models for static networks - To avoid redundant documentation, here we will discuss only the former class, the latter behaving alike. + - ``DynamicCompositeModel`` describes diffusion models for dynamic networks + + - ``ContinuousModel`` describes diffusion models with continuous states for static and dynamic networks + + To avoid redundant documentation, here we will discuss only the former class, the second behaving alike. The ``ContinuousModel`` class will have a seperate section due to its extra complexity. ============ Compartments @@ -52,6 +55,7 @@ They model stochastic events as well as deterministic ones. compartments/NodeStochastic.rst compartments/NodeCategoricalAttribute.rst compartments/NodeNumericalAttribute.rst + compartments/NodeNumericalVariable.rst compartments/NodeThreshold.rst ----------------- @@ -147,3 +151,16 @@ SIR # Simulation execution model.set_initial_status(config) iterations = model.iteration_bunch(5) + + +======================= +Using continuous states +======================= + +The composite model only supports discrete states, but more advanced custom models might require continuous states and more options. +If continuous states are required, it might be better to use the continous model implementation. + +.. toctree:: + :maxdepth: 2 + + continuous_model/continuous_model.rst \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index d68ef3b..d4d78f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -35,6 +35,7 @@ NDlib Dev Team `Letizia Milli`_ Epidemic Models `Alina Sirbu`_ Opinion Dynamics Model `Salvatore Rinzivillo`_ Visual Platform +`Mathijs Maijer`_ Continuous Model ======================= ============================ @@ -59,5 +60,6 @@ NDlib Dev Team .. _`Letizia Milli`: https://github.com/letiziam .. _`Alina Sirbu`: https://github.com/alinasirbu .. _`Salvatore Rinzivillo`: https://github.com/rinziv +.. _`Mathijs Maijer`: https://github.com/Tensaiz .. _`Source`: https://github.com/GiulioRossetti/ndlib .. _`Distribution`: https://pypi.python.org/pypi/ndlib diff --git a/docs/installing.rst b/docs/installing.rst index 41abd18..11c706e 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -113,6 +113,37 @@ Provides support to the visualization facilities offered by ``NDlib``. Download: http://bokeh.pydata.org/en/latest/ +^^^ +PIL +^^^ +Enables matplotlib animations to be saved to a file, used only by ``Continuous Model`` implementations. + +Download: https://pillow.readthedocs.io/en/stable/installation.html + +^^^^^^ +igraph +^^^^^^ +Enables graphs to use layouts from the igraph library, used only by ``Continuous Model`` implementations. + +Download: https://igraph.org/python/#downloads + +^^^^^^^^^^^^ +pyintergraph +^^^^^^^^^^^^ +Enables graphs to use layouts from the igraph library, used only by ``Continuous Model`` implementations. + +It helps by transforming networkx graphs to igraphs and back + +Download: https://gitlab.com/luerhard/pyintergraph#installation + +^^^^^ +SALib +^^^^^ +Enables support for sensitivity analysis, used only by ``Continuous Model Runner`` implementations. + +Download: https://salib.readthedocs.io/en/latest/getting-started.html#installing-salib + + -------------- Other packages -------------- diff --git a/examples/HIOM.py b/examples/HIOM.py index b0f912d..014f1b4 100644 --- a/examples/HIOM.py +++ b/examples/HIOM.py @@ -113,7 +113,7 @@ def sample_attention_weighted(graph, status): 'cmin': -1, 'cmax': 1, 'color_scale': 'RdBu', - 'plot_output': '../animations/HIOM_less.gif', + # 'plot_output': '../animations/HIOM_less.gif', 'plot_title': 'HIERARCHICAL ISING OPINION MODEL', } @@ -135,7 +135,6 @@ def sample_attention_weighted(graph, status): # Configuration config = mc.Configuration() -config.add_model_parameter('fraction_infected', 0.1) HIOM.set_initial_status(initial_status, config) HIOM.configure_visualization(visualization_config) @@ -143,12 +142,9 @@ def sample_attention_weighted(graph, status): # Simulation iterations = HIOM.iteration_bunch(15000, node_status=True) -# trends = HIOM.build_trends(iterations) +trends = HIOM.build_trends(iterations) ################### VISUALIZATION ################### -# HIOM.plot(trends, len(iterations), delta=True) -HIOM.visualize(iterations) - - -# HIOM.visualize(np.load('../data/hiom.npy', allow_pickle=True)) \ No newline at end of file +HIOM.plot(trends, len(iterations), delta=True) +# HIOM.visualize(iterations) diff --git a/examples/craving_vs_self_control.py b/examples/craving_vs_self_control.py index 598050c..83145eb 100644 --- a/examples/craving_vs_self_control.py +++ b/examples/craving_vs_self_control.py @@ -1,3 +1,5 @@ +import sys +sys.path.append("..") import networkx as nx import random import numpy as np @@ -76,8 +78,8 @@ def update_A(node, graph, status, attributes, constants): 'A': [0, 0.8], 'lambda': [0.5, 1.5] }, - 'show_plot': True, - 'plot_output': '../animations/c_vs_s.gif', + 'show_plot': False, + 'plot_output': '../../animations/c_vs_s.gif', 'plot_title': 'Self control vs craving simulation', } diff --git a/examples/numerical_variable_test.py b/examples/numerical_variable_test.py new file mode 100644 index 0000000..cc8b341 --- /dev/null +++ b/examples/numerical_variable_test.py @@ -0,0 +1,43 @@ +import sys +sys.path.append("..") + +import networkx as nx +import random +import numpy as np + +from ndlib.models.CompositeModel import CompositeModel +from ndlib.models.compartments.NodeStochastic import NodeStochastic +from ndlib.models.compartments.enums.NumericalType import NumericalType +from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable +import ndlib.models.ModelConfig as mc + +# Network generation +g = nx.erdos_renyi_graph(1000, 0.1) + +# Setting edge attribute +attr = {n: {"Age": random.choice(range(0, 100)), "Friends": random.choice(range(0, 100))} for n in g.nodes()} +nx.set_node_attributes(g, attr) + +# Composite Model instantiation +model = CompositeModel(g) + +# Model statuses +model.add_status("Susceptible") +model.add_status("Infected") +model.add_status("Removed") + +# Compartment definition +condition = NodeNumericalVariable('Age', var_type=NumericalType.ATTRIBUTE, value='Friends', value_type=NumericalType.ATTRIBUTE, op='<') +condition2 = NodeNumericalVariable('Friends', var_type=NumericalType.ATTRIBUTE, value=18, op='>') + +# Rule definition +model.add_rule("Susceptible", "Infected", condition) +model.add_rule("Infected", "Removed", condition2) + +# Model initial status configuration +config = mc.Configuration() +config.add_model_parameter('fraction_infected', 0.5) + +# Simulation execution +model.set_initial_status(config) +iterations = model.iteration_bunch(100) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index a2cc7c9..d6261e5 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,20 +1,18 @@ # TODO Write tests, fix visualization logic (overwrite vs update), -# assert save_file, add more visualization layout options, add sensitivity analysis, -# write documentation, add history span for states? +# assert save_file and create directory, add more visualization layout options, add sensitivity analysis options, +# write documentation, add history span for states?, deal with unused/optional imports, +# Parallel execution for multi runner, numpy implementation instead of networkx nodes # Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, tqdm from ndlib.models.DiffusionModel import DiffusionModel import future.utils import os -import plotly.graph_objects as go import networkx as nx from plotly.subplots import make_subplots import copy import numpy as np -from PIL import Image import io -import pyintergraph import matplotlib.pyplot as plt import matplotlib.patches as patches import matplotlib.path as path @@ -99,7 +97,7 @@ def configure_visualization(self, visualization_configuration): if not isinstance(self.visualization_configuration['plot_title'], str): raise ValueError('Plot name must be a string') else: - vis_var = self.visualization_configuration['plot_variable'] if self.visualization_configuration['plot_variable'] else '# Neighbours' + vis_var = self.visualization_configuration['plot_variable'] self.visualization_configuration['plot_title'] = 'Network simulation of ' + vis_var if 'plot_annotation' in vis_keys: @@ -128,6 +126,7 @@ def configure_visualization(self, visualization_configuration): if 'pos' not in self.graph.nodes[0].keys(): if 'layout' in vis_keys: if self.visualization_configuration['layout'] == 'fr': + import pyintergraph Graph = pyintergraph.InterGraph.from_networkx(self.graph.graph) G = Graph.to_igraph() layout = G.layout_fruchterman_reingold(niter=500) @@ -556,6 +555,7 @@ def save_plot(self, simulation): :param simulation: Output of the matplotlib animation.FuncAnimation function """ + from PIL import Image writergif = animation.PillowWriter(fps=5) simulation.save(self.visualization_configuration['plot_output'], writer=writergif) print('Saved: ' + self.visualization_configuration['plot_output']) diff --git a/ndlib/models/ContinuousModelRunner.py b/ndlib/models/ContinuousModelRunner.py index 0b2af67..ad65b89 100644 --- a/ndlib/models/ContinuousModelRunner.py +++ b/ndlib/models/ContinuousModelRunner.py @@ -38,7 +38,7 @@ def analyze_sensitivity(self, initial_status, bounds, n, iterations, second_orde :param iterations: amount of iterations to run the model per sample :param second_order: bool indicating whether to include second order indices - :return: a Python dict with the keys "S1", "S2", "ST", "S1_conf", "S2_conf", and "ST_conf" + :return: a Python dict mapping state to a dictionary with the keys "S1", "S2", "ST", "S1_conf", "S2_conf", and "ST_conf" """ if not self.model.constants: raise Exception('Please add constants when initializing the model to perform sensitivity analysis on') diff --git a/ndlib/models/compartments/NodeNumericalVariable.py b/ndlib/models/compartments/NodeNumericalVariable.py index a769823..f003901 100644 --- a/ndlib/models/compartments/NodeNumericalVariable.py +++ b/ndlib/models/compartments/NodeNumericalVariable.py @@ -12,7 +12,7 @@ class NodeNumericalVariable(Compartiment): def __init__(self, var, var_type=None, value=None, value_type=None, op=None, probability=1, **kwargs): - super(self.__class__, self).__init__(kwargs) + super(NodeNumericalVariable, self).__init__(kwargs) self.__available_operators = {"==": operator.__eq__, "<": operator.__lt__, ">": operator.__gt__, "<=": operator.__le__, ">=": operator.__ge__, "!=": operator.__ne__, @@ -51,18 +51,24 @@ def __init__(self, var, var_type=None, value=None, value_type=None, op=None, pro else: raise ValueError("The operator provided '%s' is not valid" % operator) - def execute(self, node, graph, status, status_map, attributes, *args, **kwargs): + def execute(self, node, graph, status, status_map, attributes=None, *args, **kwargs): if self.variable_type == NumericalType.STATUS: val = status[node][self.variable] elif self.variable_type == NumericalType.ATTRIBUTE: - val = attributes[node][self.variable] + if attributes: + val = attributes[node][self.variable] + else: + val = nx.get_node_attributes(graph, self.variable)[node] testVal = self.value if self.value_type == NumericalType.STATUS: testVal = status[node][self.value] elif self.value_type == NumericalType.ATTRIBUTE: - testVal = attributes[node][self.value] + if attributes: + testVal = attributes[node][self.value] + else: + testVal = nx.get_node_attributes(graph, self.value)[node] p = np.random.random_sample() From 5256053bd5d76be00d1072ef6e1b7e39c59b73ca Mon Sep 17 00:00:00 2001 From: Mathijs Maijer Date: Thu, 1 Oct 2020 04:20:10 +0200 Subject: [PATCH 16/25] :white_check_mark: Add basic tests for continuous model and NodeNumericalVariable and fix some documentation --- .../continuous_model/continuous_model.rst | 6 +- ndlib/models/ContinuousModel.py | 3 +- ndlib/models/compartments/__init__.py | 2 + ndlib/test/test_compartment.py | 47 +++++ ndlib/test/test_continuous_model.py | 177 ++++++++++++++++++ 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 ndlib/test/test_continuous_model.py diff --git a/docs/custom/continuous_model/continuous_model.rst b/docs/custom/continuous_model/continuous_model.rst index d5aad25..399a024 100644 --- a/docs/custom/continuous_model/continuous_model.rst +++ b/docs/custom/continuous_model/continuous_model.rst @@ -2,6 +2,10 @@ Continuous Model ================ +.. Warning:: + + ``ContinuousModel`` requires python3 + The composite model only supports discrete states, but more advanced custom models might require continuous states and more options. The general manner of creating a model remains the same as the ``CompositeModel``, but it allows for configuration by adding (optional) extra steps. @@ -220,7 +224,7 @@ Because no scheme has been added to the second rule, it will be evaluated and ex } ] - model = ContinuousModel(g) + model = ContinuousModel(g, iteration_schemes=schemes) model.add_status('status_1') model.add_status('status_2') diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index d6261e5..41a9a20 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,6 +1,6 @@ # TODO Write tests, fix visualization logic (overwrite vs update), # assert save_file and create directory, add more visualization layout options, add sensitivity analysis options, -# write documentation, add history span for states?, deal with unused/optional imports, +# add history span for states?, check unused/optional imports, # Parallel execution for multi runner, numpy implementation instead of networkx nodes # Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, tqdm @@ -54,6 +54,7 @@ def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=N if iteration_schemes: self.iteration_schemes = iteration_schemes + self.iteration_schemes.append({'name': '', 'function': lambda graph, status: graph.nodes}) else: self.iteration_schemes = [{'name': '', 'function': lambda graph, status: graph.nodes}] diff --git a/ndlib/models/compartments/__init__.py b/ndlib/models/compartments/__init__.py index 31f04ee..3285c7a 100644 --- a/ndlib/models/compartments/__init__.py +++ b/ndlib/models/compartments/__init__.py @@ -6,6 +6,7 @@ from .NodeThreshold import NodeThreshold from .NodeCategoricalAttribute import NodeCategoricalAttribute from .NodeNumericalAttribute import NodeNumericalAttribute +from .NodeNumericalVariable import NodeNumericalVariable from .EdgeStochastic import EdgeStochastic from .EdgeCategoricalAttribute import EdgeCategoricalAttribute from .EdgeNumericalAttribute import EdgeNumericalAttribute @@ -17,6 +18,7 @@ 'NodeThreshold', 'NodeCategoricalAttribute', 'NodeNumericalAttribute', + 'NodeNumericalVariable' 'EdgeStochastic', 'EdgeCategoricalAttribute', 'EdgeNumericalAttribute', diff --git a/ndlib/test/test_compartment.py b/ndlib/test/test_compartment.py index 9aa5d44..c1ecf7a 100644 --- a/ndlib/test/test_compartment.py +++ b/ndlib/test/test_compartment.py @@ -6,6 +6,7 @@ import ndlib.models.ModelConfig as mc import ndlib.models.CompositeModel as gc import ndlib.models.compartments as cpm +from ndlib.models.compartments.enums.NumericalType import NumericalType __author__ = 'Giulio Rossetti' __license__ = "BSD-2-Clause" @@ -270,6 +271,52 @@ def test_node_num_attribute(self): iterations = model.iteration_bunch(10) self.assertEqual(len(iterations), 10) + def test_node_num_variable(self): + + g = nx.karate_club_graph() + attr = {n: {"even": int(n % 10)} for n in g.nodes()} + nx.set_node_attributes(g, attr) + + model = gc.CompositeModel(g) + model.add_status("Susceptible") + model.add_status("Infected") + + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0, op='==') + model.add_rule("Susceptible", "Infected", c) + + config = mc.Configuration() + config.add_model_parameter('fraction_infected', 0.1) + + model.set_initial_status(config) + iterations = model.iteration_bunch(10) + self.assertEqual(len(iterations), 10) + + model = gc.CompositeModel(g) + model.add_status("Susceptible") + model.add_status("Infected") + + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[3, 5], op='IN') + model.add_rule("Susceptible", "Infected", c) + + config = mc.Configuration() + config.add_model_parameter('fraction_infected', 0.1) + + model.set_initial_status(config) + iterations = model.iteration_bunch(10) + self.assertEqual(len(iterations), 10) + + with self.assertRaises(ValueError): + c = cpm.NodeNumericalVariable(5, var_type=NumericalType.ATTRIBUTE, value=0, op='==') + c = cpm.NodeNumericalVariable('even', value=0, op='==') + c = cpm.NodeNumericalVariable('even', var_type=3, value=0, value_type=3, op='==') + c = cpm.NodeNumericalVariable(var_type=NumericalType.ATTRIBUTE, value=0, op='==') + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0) + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0, op='IN') + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=['a', 3], op='IN') + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[3, 'a'], op='IN') + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') + c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') + def test_edge_num_attribute(self): g = nx.karate_club_graph() diff --git a/ndlib/test/test_continuous_model.py b/ndlib/test/test_continuous_model.py new file mode 100644 index 0000000..96a6ff3 --- /dev/null +++ b/ndlib/test/test_continuous_model.py @@ -0,0 +1,177 @@ +from __future__ import absolute_import + +import unittest +import networkx as nx +import numpy as np +import ndlib.models.ModelConfig as mc +import ndlib.models.ContinuousModel as gc +import ndlib.models.ContinuousModelRunner as gcr +import ndlib.models.compartments as cpm +from ndlib.models.compartments.enums.NumericalType import NumericalType + +__author__ = 'Mathijs Maijer' +__license__ = "BSD-2-Clause" +__email__ = "m.f.maijer@gmail.com" + +class NdlibContinuousModelTest(unittest.TestCase): + def test_bare_model(self): + def initial_addiction(node, graph, status, constants): + addiction = 0 + return addiction + + def initial_self_confidence(node, graph, status, constants): + self_confidence = 1 + return self_confidence + + initial_status = { + 'addiction': initial_addiction, + 'self_confidence': initial_self_confidence + } + + def craving_model(node, graph, status, attributes, constants): + current_val = status[node]['addiction'] + return min(current_val + 0.1, 1) + + def self_confidence_impact(node, graph, status, attributes, constants): + return max(status[node]['self_confidence'] - 0.25, 0) + + # Network definition + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + # Model definition + addiction_model = gc.ContinuousModel(g) + addiction_model.add_status('addiction') + addiction_model.add_status('self_confidence') + + # Compartments + condition = cpm.NodeNumericalVariable('addiction', var_type=NumericalType.STATUS, value=1, op='<') + + # Rules + addiction_model.add_rule('addiction', craving_model, condition) + addiction_model.add_rule('self_confidence', self_confidence_impact, condition) + + # Configuration + config = mc.Configuration() + addiction_model.set_initial_status(initial_status, config) + + # Simulation + iterations = addiction_model.iteration_bunch(50, node_status=True) + self.assertEqual(len(iterations), 50) + + def test_constants(self): + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + constants = { + 'constant_1': 0.1, + 'constant_2': 2.5 + } + model = gc.ContinuousModel(g, constants=constants) + + self.assertEqual(constants, model.constants) + + def test_states(self): + g = nx.erdos_renyi_graph(n=1000, p=0.1) + model = gc.ContinuousModel(g) + model.add_status('status1') + model.add_status('status2') + + self.assertTrue('status1' in model.available_statuses.keys() and 'status2' in model.available_statuses.keys()) + + def test_initial_values(self): + def initial_status_1(node, graph, status, constants): + return 1 + + initial_status = { + 'status_1': initial_status_1, + 'status_2': 2 + } + + g = nx.erdos_renyi_graph(n=100, p=0.1) + + model = gc.ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + self.assertEqual(model.initial_status[0]['status_1'], 1) + self.assertEqual(model.initial_status[0]['status_2'], 2) + + def test_conditions(self): + g = nx.erdos_renyi_graph(n=100, p=0.1) + + model = gc.ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = cpm.NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] + 0.1 + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + 0.5 + + # Rules + model.add_rule('status_1', update_1, condition) + model.add_rule('status_2', update_2, condition) + + self.assertEqual(model.compartment_progressive, 2) + self.assertEqual(model.compartment[0], ('status_1', update_1, condition, [''])) + self.assertEqual(model.compartment[1], ('status_2', update_2, condition, [''])) + + def test_schemes(self): + g = nx.erdos_renyi_graph(n=100, p=0.1) + + # Define schemes + def sample_state_weighted(graph, status): + probs = [] + status_1 = [stat['status_1'] for stat in list(status.values())] + factor = 1.0/sum(status_1) + for s in status_1: + probs.append(s * factor) + return np.random.choice(graph.nodes, size=1, replace=False, p=probs) + + schemes = [ + { + 'name': 'random weighted agent', + 'function': sample_state_weighted, + 'lower': 100, + 'upper': 200 + } + ] + + model = gc.ContinuousModel(g, iteration_schemes=schemes) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = cpm.NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] + 0.1 + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + 0.5 + + # Rules + model.add_rule('status_1', update_1, condition, ['random weighted agent']) + model.add_rule('status_2', update_2, condition) + + self.assertEqual(model.compartment_progressive, 2) + self.assertEqual(model.compartment[0], ('status_1', update_1, condition, ['random weighted agent'])) + self.assertEqual(model.compartment[1], ('status_2', update_2, condition, [''])) + self.assertTrue({ + 'name': 'random weighted agent', + 'function': sample_state_weighted, + 'lower': 100, + 'upper': 200 + } in model.iteration_schemes) + From 68e27c44b981a9ec75a583c802042692f73005f0 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Sat, 3 Oct 2020 02:31:52 +0200 Subject: [PATCH 17/25] Check output saving, modularize sensitivity analysis metrics, delete unused imports --- .gitignore | 3 ++ .vscode/settings.json | 3 -- .../continuous_model/optional/ModelRunner.rst | 19 +++++--- .../optional/Visualization.rst | 15 +++++-- examples/HIOM.py | 7 ++- examples/craving_vs_self_control.py | 5 +-- examples/craving_vs_self_control_multi.py | 11 +++-- examples/example_continuous_custom_var.py | 3 +- examples/numerical_variable_test.py | 43 ------------------- ndlib/models/ContinuousModel.py | 37 ++++++++++------ ndlib/models/ContinuousModelRunner.py | 43 ++++++++++++++----- ndlib/models/compartments/enums/SAType.py | 11 +++++ ndlib/test/test_continuous_model.py | 36 +++++++++++++++- 13 files changed, 140 insertions(+), 96 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 examples/numerical_variable_test.py create mode 100644 ndlib/models/compartments/enums/SAType.py diff --git a/.gitignore b/.gitignore index a69b54b..7e10ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ __pycache__/ # C extensions *.so +# IDE +.vscode/ + # Distribution / packaging .Python env/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a625774..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "restructuredtext.confPath": "${workspaceFolder}\\docs" -} \ No newline at end of file diff --git a/docs/custom/continuous_model/optional/ModelRunner.rst b/docs/custom/continuous_model/optional/ModelRunner.rst index 1fb6af9..7fc8749 100644 --- a/docs/custom/continuous_model/optional/ModelRunner.rst +++ b/docs/custom/continuous_model/optional/ModelRunner.rst @@ -88,24 +88,32 @@ Sensitivity Analysis Another important part of analysing a model is sensitivity analysis. Custom analysis can be done using the run function, but an integrated SALib version is included -and can be ran using the ``analyze_sensitivity(initial_status, bounds, n, iterations, second_order=True)`` function. +and can be ran using the ``analyze_sensitivity(sa_type, initial_status, bounds, n, iterations, second_order=True)`` function. It requires the following parameters: -============== =================================== ======= ========= ============================================================================= +============== =================================== ======= ========= ============================================================================== parameters Value Type Default Mandatory Description -============== =================================== ======= ========= ============================================================================= +============== =================================== ======= ========= ============================================================================== +sa_type SAType True SAType enumerated value indicating what metric to use for sensitivity analysis initial_status dictionary True A dictionary containing the initial status per state bounds dictionary{status => (lower, upper) True A dictionary mapping a status string to a tuple in the form of [lower, upper] n integer True The amount of samples to get from the SALib saltelli sampler iterations integer True A list containing `constants` dictionaries to use per simulation second_order boolean True False Boolean indicating whether to include second order indices -============== =================================== ======= ========= ============================================================================= +============== =================================== ======= ========= ============================================================================== At the moment, after every simulation, the mean value for a state is taken over all the nodes, which is seen as one output for the model. After running the analysis, a dictionary is returned, mapping a state to a dictionary with the keys "S1", "S2", "ST", "S1_conf", "S2_conf", and "ST_conf" which is acquired by using ``sobol.analyze()`` from SALib. +.. note:: + + Currently, the following sensitivity analysis metrics can be passed for the sa_type parameter (use the SAType enum): + + - SAType.MEAN + + Example: .. code-block:: python @@ -115,6 +123,7 @@ Example: from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.ContinuousModelRunner import ContinuousModelRunner from ndlib.models.compartments.NodeStochastic import NodeStochastic + from ndlib.models.compartments.enums.SAType import SAType import ndlib.models.ModelConfig as mc g = nx.erdos_renyi_graph(n=1000, p=0.1) @@ -160,5 +169,5 @@ Example: # Simulation runner = ContinuousModelRunner(model, config) - analysis = runner.analyze_sensitivity(initial_status, {'constant_1': (0, 1), 'constant_2': (-1, 1)}, 100, 50) + analysis = runner.analyze_sensitivity(SAType.MEAN, initial_status, {'constant_1': (0, 1), 'constant_2': (-1, 1)}, 100, 50) diff --git a/docs/custom/continuous_model/optional/Visualization.rst b/docs/custom/continuous_model/optional/Visualization.rst index 3928e9e..42c75f0 100644 --- a/docs/custom/continuous_model/optional/Visualization.rst +++ b/docs/custom/continuous_model/optional/Visualization.rst @@ -25,9 +25,9 @@ To configure and enable visualization, a configuration dictionary should be crea It should hold the following key -> value mappings: -================== =============== ===================================== ========= ============================================================== +================== =============== ===================================== ========= ================================================================= Key (string) Value Type Default Mandatory Description -================== =============== ===================================== ========= ============================================================== +================== =============== ===================================== ========= ================================================================= plot_interval number True How many iterations should be between each plot plot_variable string True The state to use as node color show_plot boolean True False Whether a plot should be shown @@ -38,9 +38,10 @@ cmin number 0 Fals cmax number 0 False The maximum color to display in the colorbar color_scale string RdBu False Matplotlib colorscale colors to use layout string|function nx.drawing.spring_layout False Name of the networkx layout to use +layout_params dictionary False Arguments to pass to layout function, takes argument name as key variable_limits dictionary {state: [-1, 1] for state in states} False Dictionary mapping state name to a list with min and max value animation_interval integer 30 False Amount of miliseconds between each frame -================== =============== ===================================== ========= ============================================================== +================== =============== ===================================== ========= ================================================================= When the configuration dictionary has been initialized and the model has been initialized, it can be added to the model using the function ``configure_visualization(visualization_dictionary)``. @@ -51,10 +52,16 @@ When the configuration dictionary has been initialized and the model has been in The ``layout`` key currently supports some igraph layouts as well, but it requires the igraph and pyintergraph libraries installed. - The following values are supported: + The following igraph layouts are supported: - ``fr``: Creates an igraph layout using the fruchterman reingold algorithm + It is possible to include any function, that takes the graph as argument and returns a dictionary of positions keyed by node, + just like how the networkx.drawing._layout functions work. This means all networkx layout functions can be included as layout value. + + If you wish to pass any specific arguments to the function included as layout, this can be done using the layout_params key. + Simply map it to a dict that has the parameter name as key and the desired value as value. + Example: diff --git a/examples/HIOM.py b/examples/HIOM.py index 014f1b4..43d316f 100644 --- a/examples/HIOM.py +++ b/examples/HIOM.py @@ -1,10 +1,9 @@ +import sys +sys.path.append("..") + import networkx as nx import random import numpy as np -import matplotlib.pyplot as plt - -import sys -sys.path.append("..") from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic diff --git a/examples/craving_vs_self_control.py b/examples/craving_vs_self_control.py index 83145eb..4e56ed4 100644 --- a/examples/craving_vs_self_control.py +++ b/examples/craving_vs_self_control.py @@ -1,13 +1,10 @@ import sys sys.path.append("..") + import networkx as nx -import random import numpy as np import matplotlib.pyplot as plt -import sys -sys.path.append("..") - from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.compartments.NodeStochastic import NodeStochastic diff --git a/examples/craving_vs_self_control_multi.py b/examples/craving_vs_self_control_multi.py index cbf7124..663a0fd 100644 --- a/examples/craving_vs_self_control_multi.py +++ b/examples/craving_vs_self_control_multi.py @@ -1,14 +1,13 @@ -import networkx as nx -import random -import numpy as np -import matplotlib.pyplot as plt - import sys sys.path.append("..") +import networkx as nx +import numpy as np + from ndlib.models.ContinuousModel import ContinuousModel from ndlib.models.ContinuousModelRunner import ContinuousModelRunner from ndlib.models.compartments.NodeStochastic import NodeStochastic +from ndlib.models.compartments.enums.SAType import SAType import ndlib.models.ModelConfig as mc @@ -112,7 +111,7 @@ def update_A(node, graph, status, attributes, constants): # results = runner.run(10, [100], [initial_status]) -analysis = runner.analyze_sensitivity(initial_status, {'q': (0.5, 1), 'b': (0.25, 0.75), 'd': (0.0, 0.4)}, 5, 50) +analysis = runner.analyze_sensitivity(SAType.MEAN, initial_status, {'q': (0.5, 1), 'b': (0.25, 0.75), 'd': (0.0, 0.4)}, 1, 50) print(analysis) ################### VISUALIZATION ################### diff --git a/examples/example_continuous_custom_var.py b/examples/example_continuous_custom_var.py index 2b5fdaa..8f5b099 100644 --- a/examples/example_continuous_custom_var.py +++ b/examples/example_continuous_custom_var.py @@ -1,12 +1,11 @@ import sys sys.path.append("..") + import networkx as nx import random import numpy as np -from ndlib.models.CompositeModel import CompositeModel from ndlib.models.ContinuousModel import ContinuousModel -from ndlib.models.compartments.NodeStochastic import NodeStochastic from ndlib.models.compartments.enums.NumericalType import NumericalType from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable diff --git a/examples/numerical_variable_test.py b/examples/numerical_variable_test.py deleted file mode 100644 index cc8b341..0000000 --- a/examples/numerical_variable_test.py +++ /dev/null @@ -1,43 +0,0 @@ -import sys -sys.path.append("..") - -import networkx as nx -import random -import numpy as np - -from ndlib.models.CompositeModel import CompositeModel -from ndlib.models.compartments.NodeStochastic import NodeStochastic -from ndlib.models.compartments.enums.NumericalType import NumericalType -from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable -import ndlib.models.ModelConfig as mc - -# Network generation -g = nx.erdos_renyi_graph(1000, 0.1) - -# Setting edge attribute -attr = {n: {"Age": random.choice(range(0, 100)), "Friends": random.choice(range(0, 100))} for n in g.nodes()} -nx.set_node_attributes(g, attr) - -# Composite Model instantiation -model = CompositeModel(g) - -# Model statuses -model.add_status("Susceptible") -model.add_status("Infected") -model.add_status("Removed") - -# Compartment definition -condition = NodeNumericalVariable('Age', var_type=NumericalType.ATTRIBUTE, value='Friends', value_type=NumericalType.ATTRIBUTE, op='<') -condition2 = NodeNumericalVariable('Friends', var_type=NumericalType.ATTRIBUTE, value=18, op='>') - -# Rule definition -model.add_rule("Susceptible", "Infected", condition) -model.add_rule("Infected", "Removed", condition2) - -# Model initial status configuration -config = mc.Configuration() -config.add_model_parameter('fraction_infected', 0.5) - -# Simulation execution -model.set_initial_status(config) -iterations = model.iteration_bunch(100) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 41a9a20..c6f576f 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,21 +1,19 @@ -# TODO Write tests, fix visualization logic (overwrite vs update), -# assert save_file and create directory, add more visualization layout options, add sensitivity analysis options, -# add history span for states?, check unused/optional imports, -# Parallel execution for multi runner, numpy implementation instead of networkx nodes -# Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, tqdm +# TODO +# - Parallel execution for multi runner, +# - Fix visualization logic (overwrite vs update), +# - numpy matrix implementation instead of networkx nodes/dict +# - Add sensitivity analysis options, +# Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, python-igraph, tqdm from ndlib.models.DiffusionModel import DiffusionModel import future.utils import os import networkx as nx -from plotly.subplots import make_subplots import copy import numpy as np -import io import matplotlib.pyplot as plt import matplotlib.patches as patches -import matplotlib.path as path import matplotlib as mpl import matplotlib.animation as animation @@ -60,11 +58,16 @@ def __init__(self, graph, constants=None, clean_status=None, iteration_schemes=N self.visualization_configuration = None - self.save_file = save_file + if save_file: + if isinstance(save_file, str): + self.save_file = save_file + else: + raise ValueError('save_file should be a string indicating path/and/filename') + else: + self.save_file = None self.full_status = None - def configure_visualization(self, visualization_configuration): """ Configure and assert all visualization configuration parameters @@ -133,7 +136,10 @@ def configure_visualization(self, visualization_configuration): layout = G.layout_fruchterman_reingold(niter=500) positions = {node: {'pos': location} for node, location in enumerate(layout)} else: - pos = nx.drawing.spring_layout(self.graph.graph) + if 'layout_params' in vis_keys: + pos = self.visualization_configuration['layout'](self.graph.graph, **self.visualization_configuration['layout_params']) + else: + pos = self.visualization_configuration['layout'](self.graph.graph) positions = {key: {'pos': location} for key, location in pos.items()} else: pos = nx.drawing.spring_layout(self.graph.graph) @@ -293,6 +299,11 @@ def iteration_bunch(self, bunch_size, node_status=True, tqdm=True): iterations.append(self.iteration(node_status)) if self.save_file: + split = self.save_file.split('/') + file_name = split[-1] + file_path = self.save_file.replace(file_name, '') + if not os.path.exists(file_path): + os.makedirs(file_path) np.save(self.save_file, iterations) print('Saved ' + self.save_file) return iterations @@ -305,7 +316,7 @@ def get_mean_data(self, iterations, mean_type): :param mean_type: A string containing the type to get the average data from Should be 'status' or 'status_delta' - :return: Dictionary containing all statuses as keys, + :return: Dictionary containing all statuses as keys, and as value a list of the average values of all nodes for that status per iteration """ mean_changes = {} @@ -369,7 +380,7 @@ def get_means(self, iterations): :param iterations: iterations output from iteration_bunch - :return: Dictionary containing all statuses as keys, + :return: Dictionary containing all statuses as keys, and as value a list of the average value over all nodes for that state per iteration """ self.full_status = self.build_full_status(iterations) diff --git a/ndlib/models/ContinuousModelRunner.py b/ndlib/models/ContinuousModelRunner.py index ad65b89..dc9a602 100644 --- a/ndlib/models/ContinuousModelRunner.py +++ b/ndlib/models/ContinuousModelRunner.py @@ -1,5 +1,6 @@ from SALib.sample import saltelli from SALib.analyze import sobol +from ndlib.models.compartments.enums.SAType import SAType import numpy as np class ContinuousModelRunner(object): @@ -27,11 +28,12 @@ def run(self, N, iterations_list, initial_statuses, constants_list=None): return results - def analyze_sensitivity(self, initial_status, bounds, n, iterations, second_order=True): + def analyze_sensitivity(self, sa_type, initial_status, bounds, n, iterations, second_order=True): """ Compute the sensitivity indices for the constants of the model using sobol Samples are generated using a Saltelli sampler + :param sa_type: SAType indicating the metric for the sensitivity analysis, possible values :param initial_status: a dictionary with as key a state and as value, a number or function indicating the intial value for this state :param bounds: a dictionary with a constant name as key and a tuple (lower bound, upper bound) as value :param n: integer indicating the n for the function: N∗(2D+2) which is used to determine the amount of samples @@ -42,6 +44,8 @@ def analyze_sensitivity(self, initial_status, bounds, n, iterations, second_orde """ if not self.model.constants: raise Exception('Please add constants when initializing the model to perform sensitivity analysis on') + if not isinstance(sa_type, SAType): + raise ValueError('Please use a SAType enum value for sa_type') problem = { 'num_vars': len(bounds.keys()), @@ -66,23 +70,40 @@ def analyze_sensitivity(self, initial_status, bounds, n, iterations, second_orde outputs.append(self.model.iteration_bunch(iterations, node_status=self.node_status, tqdm=False)) # Parse the outputs for every simulation (TODO: Optimize) + print('Parsing outputs...') states = list(self.model.available_statuses.keys()) states.remove('Infected') - averages = { + val_dict = { var: np.array([]) for var in states } - - print('Parsing outputs...') - for output in outputs: - # Get the average value of all the nodes for every status at the last iteration of the simulation - means = self.model.get_means(output) - for status in averages.keys(): - averages[status] = np.append(averages[status], means[status][-1]) + values = self.parse_outputs(sa_type, outputs, val_dict) print('Running sensitivity analysis...') # Perform the sobol analysis seperately for every status analysis = { - var: sobol.analyze(problem, averages[var]) for var in states + var: sobol.analyze(problem, values[var]) for var in states + } + + return analysis + + def parse_outputs(self, sa_type, outputs, val_dict): + mapping = { + SAType.MEAN: self.mean_outputs } + return mapping[sa_type](outputs, val_dict) + + def mean_outputs(self, outputs, val_dict): + for output in outputs: + # Get the average value of all the nodes for every status at the last iteration of the simulation + means = self.model.get_means(output) + for status in val_dict.keys(): + val_dict[status] = np.append(val_dict[status], means[status][-1]) + return val_dict - return analysis \ No newline at end of file + def variance_outputs(self, outputs, val_dict): + for output in outputs: + # Get the average value of all the nodes for every status at the last iteration of the simulation + means = self.model.get_means(output) + for status in val_dict.keys(): + val_dict[status] = np.append(val_dict[status], means[status][-1]) + return val_dict \ No newline at end of file diff --git a/ndlib/models/compartments/enums/SAType.py b/ndlib/models/compartments/enums/SAType.py new file mode 100644 index 0000000..63d64cf --- /dev/null +++ b/ndlib/models/compartments/enums/SAType.py @@ -0,0 +1,11 @@ +__author__ = 'Mathijs Maijer' +__license__ = "BSD-2-Clause" +__email__ = "m.f.maijer@gmail.com" + +from enum import Enum + +class SAType(Enum): + MEAN = 0 + VARIANCE = 1 + MIN = 2 + MAX = 3 diff --git a/ndlib/test/test_continuous_model.py b/ndlib/test/test_continuous_model.py index 96a6ff3..5dc45b2 100644 --- a/ndlib/test/test_continuous_model.py +++ b/ndlib/test/test_continuous_model.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import unittest +import os import networkx as nx import numpy as np import ndlib.models.ModelConfig as mc @@ -55,7 +56,7 @@ def self_confidence_impact(node, graph, status, attributes, constants): addiction_model.set_initial_status(initial_status, config) # Simulation - iterations = addiction_model.iteration_bunch(50, node_status=True) + iterations = addiction_model.iteration_bunch(50, node_status=True, tqdm=False) self.assertEqual(len(iterations), 50) def test_constants(self): @@ -175,3 +176,36 @@ def update_2(node, graph, status, attributes, constants): 'upper': 200 } in model.iteration_schemes) + def test_save_file(self): + def update(node, graph, status, attributes, constants): + return 0 + + initial_status = { + 'status': 0 + } + + # Network definition + g = nx.erdos_renyi_graph(n=10, p=0.5) + + # Model definition + path = './test_output/' + output_path = path + 'file' + model = gc.ContinuousModel(g, save_file=output_path) + model.add_status('status') + + # Compartments + condition = cpm.NodeStochastic(1) + + # Rules + model.add_rule('status_1', update, condition) + + # Configuration + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + # Simulation + iterations = model.iteration_bunch(10, node_status=True, tqdm=False) + self.assertEqual(len(iterations), 10) + self.assertTrue(os.path.isfile(output_path + '.npy')) + os.remove(output_path + '.npy') + os.rmdir(path) From b8fdcac9fd4634287ac956c79cdf07c47f7e3570 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Sat, 3 Oct 2020 04:14:23 +0200 Subject: [PATCH 18/25] :fire::bug: Remove examples, fix animation output not saving --- examples/HIOM.py | 149 ---------------------- examples/craving_vs_self_control.py | 138 -------------------- examples/craving_vs_self_control_multi.py | 121 ------------------ examples/example_continuous_custom_var.py | 80 ------------ ndlib/models/ContinuousModel.py | 9 +- ndlib/models/ContinuousModelRunner.py | 4 + 6 files changed, 11 insertions(+), 490 deletions(-) delete mode 100644 examples/HIOM.py delete mode 100644 examples/craving_vs_self_control.py delete mode 100644 examples/craving_vs_self_control_multi.py delete mode 100644 examples/example_continuous_custom_var.py diff --git a/examples/HIOM.py b/examples/HIOM.py deleted file mode 100644 index 43d316f..0000000 --- a/examples/HIOM.py +++ /dev/null @@ -1,149 +0,0 @@ -import sys -sys.path.append("..") - -import networkx as nx -import random -import numpy as np - -from ndlib.models.ContinuousModel import ContinuousModel -from ndlib.models.compartments.NodeStochastic import NodeStochastic - -import ndlib.models.ModelConfig as mc - -################### MODEL SPECIFICATIONS ################### - -constants = { - 'dt': 0.01, - 'A_min': -0.5, - 'A_star': 1, - 's_O': 0.01, - 's_I': 0, - 'd_A': 0, - 'p': 1, - 'r_min': 0, - 't_O': np.inf, -} - -def initial_I(node, graph, status, constants): - return np.random.normal(0, 0.3) - -def initial_O(node, graph, status, constants): - return np.random.normal(0, 0.2) - -initial_status = { - 'I': initial_I, - 'O': initial_O, - 'A': 1 -} - -def update_I(node, graph, status, attributes, constants): - nb = np.random.choice(graph.neighbors(node)) - if abs(status[node]['O'] - status[nb]['O']) > constants['t_O']: - return status[node]['I'] # Do nothing - else: - # Update information - r = constants['r_min'] + (1 - constants['r_min']) / (1 + np.exp(-1 * constants['p'] * (status[node]['A'] - status[nb]['A']))) - inf = r * status[node]['I'] + (1-r) * status[nb]['I'] + np.random.normal(0, constants['s_I']) - - # Update attention - status[node]['A'] = status[node]['A'] + constants['d_A'] * (2 * constants['A_star'] - status[node]['A']) - status[nb]['A'] = status[nb]['A'] + constants['d_A'] * (2 * constants['A_star'] - status[nb]['A']) - - return inf - - return - -def update_A(node, graph, status, attributes, constants): - return status[node]['A'] - 2 * constants['d_A'] * status[node]['A']/len(graph.nodes) - -def update_O(node, graph, status, attributes, constants): - noise = np.random.normal(0, constants['s_O']) - x = status[node]['O'] - constants['dt'] * (status[node]['O']**3 - (status[node]['A'] + constants['A_min']) * status[node]['O'] - status[node]['I']) + noise - return x - -def shrink_I(node, graph, status, attributes, constants): - return status[node]['I'] * 0.999 - -def shrink_A(node, graph, status, attributes, constants): - return status[node]['A'] * 0.999 - -def sample_attention_weighted(graph, status): - probs = [] - A = [stat['A'] for stat in list(status.values())] - factor = 1.0/sum(A) - for a in A: - probs.append(a * factor) - return np.random.choice(graph.nodes, size=1, replace=False, p=probs) - -schemes = [ - { - 'name': 'random agent', - 'function': sample_attention_weighted, - }, - { - 'name': 'all', - 'function': lambda graph, status: graph.nodes, - }, - { - 'name': 'shrink I', - 'function': lambda graph, status: graph.nodes, - 'lower': 5000 - }, - { - 'name': 'shrink A', - 'function': lambda graph, status: graph.nodes, - 'lower': 10000 - }, -] - -################### MODEL CONFIGURATION ################### - -# Network definition -g = nx.watts_strogatz_graph(400, 2, 0.02) - -# Visualization config -visualization_config = { - 'layout': 'fr', - 'plot_interval': 100, - 'plot_variable': 'O', - 'variable_limits': { - 'A': [0, 1] - }, - 'cmin': -1, - 'cmax': 1, - 'color_scale': 'RdBu', - # 'plot_output': '../animations/HIOM_less.gif', - 'plot_title': 'HIERARCHICAL ISING OPINION MODEL', -} - -# Model definition -HIOM = ContinuousModel(g, constants=constants, iteration_schemes=schemes) -HIOM.add_status('I') -HIOM.add_status('A') -HIOM.add_status('O') - -# Compartments -condition = NodeStochastic(1) - -# Rules -HIOM.add_rule('I', update_I, condition, ['random agent']) -HIOM.add_rule('A', update_A, condition, ['all']) -HIOM.add_rule('O', update_O, condition, ['all']) -HIOM.add_rule('I', shrink_I, condition, ['shrink I']) -HIOM.add_rule('A', shrink_A, condition, ['shrink A']) - -# Configuration -config = mc.Configuration() -HIOM.set_initial_status(initial_status, config) -HIOM.configure_visualization(visualization_config) - -################### SIMULATION ################### - -# Simulation -iterations = HIOM.iteration_bunch(15000, node_status=True) -trends = HIOM.build_trends(iterations) - -################### VISUALIZATION ################### - -HIOM.plot(trends, len(iterations), delta=True) -# HIOM.visualize(iterations) diff --git a/examples/craving_vs_self_control.py b/examples/craving_vs_self_control.py deleted file mode 100644 index 4e56ed4..0000000 --- a/examples/craving_vs_self_control.py +++ /dev/null @@ -1,138 +0,0 @@ -import sys -sys.path.append("..") - -import networkx as nx -import numpy as np -import matplotlib.pyplot as plt - -from ndlib.models.ContinuousModel import ContinuousModel -from ndlib.models.compartments.NodeStochastic import NodeStochastic - -import ndlib.models.ModelConfig as mc - -################### MODEL SPECIFICATIONS ################### - -constants = { - 'q': 0.8, - 'b': 0.5, - 'd': 0.2, - 'h': 0.2, - 'k': 0.25, - 'S+': 0.5, -} -constants['p'] = 2*constants['d'] - -def initial_v(node, graph, status, constants): - return min(1, max(0, status['C']-status['S']-status['E'])) - -def initial_a(node, graph, status, constants): - return constants['q'] * status['V'] + (np.random.poisson(status['lambda'])/7) - -initial_status = { - 'C': 0, - 'S': constants['S+'], - 'E': 1, - 'V': initial_v, - 'lambda': 0.5, - 'A': initial_a -} - -def update_C(node, graph, status, attributes, constants): - return status[node]['C'] + constants['b'] * status[node]['A'] * min(1, 1-status[node]['C']) - constants['d'] * status[node]['C'] - -def update_S(node, graph, status, attributes, constants): - return status[node]['S'] + constants['p'] * max(0, constants['S+'] - status[node]['S']) - constants['h'] * status[node]['C'] - constants['k'] * status[node]['A'] - -def update_E(node, graph, status, attributes, constants): - # return status[node]['E'] - 0.015 # Grasman calculation - - avg_neighbor_addiction = 0 - for n in graph.neighbors(node): - avg_neighbor_addiction += status[n]['A'] - - return max(-1.5, status[node]['E'] - avg_neighbor_addiction / 50) # Custom calculation - -def update_V(node, graph, status, attributes, constants): - return min(1, max(0, status[node]['C']-status[node]['S']-status[node]['E'])) - -def update_lambda(node, graph, status, attributes, constants): - return status[node]['lambda'] + 0.01 - -def update_A(node, graph, status, attributes, constants): - return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) - -################### MODEL CONFIGURATION ################### - -# Network definition -# g = nx.random_geometric_graph(2000, 0.035) -g = nx.random_geometric_graph(200, 0.125) - -# Visualization config -visualization_config = { - 'plot_interval': 2, - 'plot_variable': 'A', - 'variable_limits': { - 'A': [0, 0.8], - 'lambda': [0.5, 1.5] - }, - 'show_plot': False, - 'plot_output': '../../animations/c_vs_s.gif', - 'plot_title': 'Self control vs craving simulation', -} - -# Model definition -craving_control_model = ContinuousModel(g, constants=constants) -craving_control_model.add_status('C') -craving_control_model.add_status('S') -craving_control_model.add_status('E') -craving_control_model.add_status('V') -craving_control_model.add_status('lambda') -craving_control_model.add_status('A') - -# Compartments -condition = NodeStochastic(1) - -# Rules -craving_control_model.add_rule('C', update_C, condition) -craving_control_model.add_rule('S', update_S, condition) -craving_control_model.add_rule('E', update_E, condition) -craving_control_model.add_rule('V', update_V, condition) -craving_control_model.add_rule('lambda', update_lambda, condition) -craving_control_model.add_rule('A', update_A, condition) - -# Configuration -config = mc.Configuration() -craving_control_model.set_initial_status(initial_status, config) -craving_control_model.configure_visualization(visualization_config) - -################### SIMULATION ################### - -# Simulation -iterations = craving_control_model.iteration_bunch(100, node_status=True) -trends = craving_control_model.build_trends(iterations) - -################### VISUALIZATION ################### - -craving_control_model.plot(trends, len(iterations), delta=True) - -x = np.arange(0, len(iterations)) -plt.figure() - -plt.subplot(221) -plt.plot(x, trends['means']['E'], label='E') -plt.plot(x, trends['means']['lambda'], label='lambda') -plt.legend() - -plt.subplot(222) -plt.plot(x, trends['means']['A'], label='A') -plt.plot(x, trends['means']['C'], label='C') -plt.legend() - -plt.subplot(223) -plt.plot(x, trends['means']['S'], label='S') -plt.plot(x, trends['means']['V'], label='V') -plt.legend() - -plt.show() - -craving_control_model.visualize(iterations) diff --git a/examples/craving_vs_self_control_multi.py b/examples/craving_vs_self_control_multi.py deleted file mode 100644 index 663a0fd..0000000 --- a/examples/craving_vs_self_control_multi.py +++ /dev/null @@ -1,121 +0,0 @@ -import sys -sys.path.append("..") - -import networkx as nx -import numpy as np - -from ndlib.models.ContinuousModel import ContinuousModel -from ndlib.models.ContinuousModelRunner import ContinuousModelRunner -from ndlib.models.compartments.NodeStochastic import NodeStochastic -from ndlib.models.compartments.enums.SAType import SAType - -import ndlib.models.ModelConfig as mc - -################### MODEL SPECIFICATIONS ################### - -constants = { - 'q': 0.8, - 'b': 0.5, - 'd': 0.2, - 'h': 0.2, - 'k': 0.25, - 'S+': 0.5, -} -constants['p'] = 2*constants['d'] - -def initial_v(node, graph, status, constants): - return min(1, max(0, status['C']-status['S']-status['E'])) - -def initial_a(node, graph, status, constants): - return constants['q'] * status['V'] + (np.random.poisson(status['lambda'])/7) - -initial_status = { - 'C': 0, - 'S': constants['S+'], - 'E': 1, - 'V': initial_v, - 'lambda': 0.5, - 'A': initial_a -} - -def update_C(node, graph, status, attributes, constants): - return status[node]['C'] + constants['b'] * status[node]['A'] * min(1, 1-status[node]['C']) - constants['d'] * status[node]['C'] - -def update_S(node, graph, status, attributes, constants): - return status[node]['S'] + constants['p'] * max(0, constants['S+'] - status[node]['S']) - constants['h'] * status[node]['C'] - constants['k'] * status[node]['A'] - -def update_E(node, graph, status, attributes, constants): - # return status[node]['E'] - 0.015 # Grasman calculation - - avg_neighbor_addiction = 0 - for n in graph.neighbors(node): - avg_neighbor_addiction += status[n]['A'] - - return max(-1.5, status[node]['E'] - avg_neighbor_addiction / 50) # Custom calculation - -def update_V(node, graph, status, attributes, constants): - return min(1, max(0, status[node]['C']-status[node]['S']-status[node]['E'])) - -def update_lambda(node, graph, status, attributes, constants): - return status[node]['lambda'] + 0.01 - -def update_A(node, graph, status, attributes, constants): - return constants['q'] * status[node]['V'] + min((np.random.poisson(status[node]['lambda'])/7), constants['q']*(1 - status[node]['V'])) - -################### MODEL CONFIGURATION ################### - -# Network definition -g = nx.random_geometric_graph(200, 0.125) - -# Visualization config -visualization_config = { - 'plot_interval': 10, - 'plot_variable': 'A', - 'variable_limits': { - 'A': [0, 0.8], - 'lambda': [0.5, 1.5] - }, - 'show_plot': True, - 'plot_title': 'Self control vs craving simulation', -} - -# Model definition -craving_control_model = ContinuousModel(g, constants=constants) -craving_control_model.add_status('C') -craving_control_model.add_status('S') -craving_control_model.add_status('E') -craving_control_model.add_status('V') -craving_control_model.add_status('lambda') -craving_control_model.add_status('A') - -# Compartments -condition = NodeStochastic(1) - -# Rules -craving_control_model.add_rule('C', update_C, condition) -craving_control_model.add_rule('S', update_S, condition) -craving_control_model.add_rule('E', update_E, condition) -craving_control_model.add_rule('V', update_V, condition) -craving_control_model.add_rule('lambda', update_lambda, condition) -craving_control_model.add_rule('A', update_A, condition) - -# Configuration -config = mc.Configuration() -craving_control_model.set_initial_status(initial_status, config) -# craving_control_model.configure_visualization(visualization_config) - -################### SIMULATION ################### - -# Simulation -runner = ContinuousModelRunner(craving_control_model, config) - -# results = runner.run(10, [100], [initial_status]) - -analysis = runner.analyze_sensitivity(SAType.MEAN, initial_status, {'q': (0.5, 1), 'b': (0.25, 0.75), 'd': (0.0, 0.4)}, 1, 50) -print(analysis) - -################### VISUALIZATION ################### - -# for iterations in results: -# trends = craving_control_model.build_trends(iterations) -# craving_control_model.plot(trends, len(iterations), delta=True) diff --git a/examples/example_continuous_custom_var.py b/examples/example_continuous_custom_var.py deleted file mode 100644 index 8f5b099..0000000 --- a/examples/example_continuous_custom_var.py +++ /dev/null @@ -1,80 +0,0 @@ -import sys -sys.path.append("..") - -import networkx as nx -import random -import numpy as np - -from ndlib.models.ContinuousModel import ContinuousModel -from ndlib.models.compartments.enums.NumericalType import NumericalType -from ndlib.models.compartments.NodeNumericalVariable import NodeNumericalVariable - -import ndlib.models.ModelConfig as mc - -from bokeh.io import show -from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend - -def initial_addiction(node, graph, status, constants): - addiction = 0 - return addiction - -def initial_self_confidence(node, graph, status, constants): - self_confidence = 1 - return self_confidence - -initial_status = { - 'addiction': initial_addiction, - 'self_confidence': initial_self_confidence -} - -def craving_model(node, graph, status, attributes, constants): - current_val = status[node]['addiction'] - craving = attributes[node]['craving'] - self_control = attributes[node]['self_control'] - return min(current_val + craving - self_control, 1) - -def self_confidence_impact(node, graph, status, attributes, constants): - return max(status[node]['self_confidence'] - random.uniform(0.2, 0.5), 0) - -# Network definition -g = nx.erdos_renyi_graph(n=1000, p=0.1) - -# Extra network setup -attr = {n: {'craving': random.random(), 'self_control': random.random()} for n in g.nodes()} -nx.set_node_attributes(g, attr) - -# Visualization config -visualization_config = { - 'plot_interval': 10, - 'plot_variable': 'addiction', - 'show_plot': True, - 'plot_title': 'Example model', - 'animation_interval': 500 -} - -# Model definition -addiction_model = ContinuousModel(g) -addiction_model.add_status('addiction') -addiction_model.add_status('self_confidence') - -# Compartments -condition = NodeNumericalVariable('self_control', var_type=NumericalType.ATTRIBUTE, value='craving', value_type=NumericalType.ATTRIBUTE, op='<') -condition2 = NodeNumericalVariable('addiction', var_type=NumericalType.STATUS, value=1, op='==') - -# Rules -addiction_model.add_rule('addiction', craving_model, condition) -addiction_model.add_rule('self_confidence', self_confidence_impact, condition2) - -# Configuration -config = mc.Configuration() -addiction_model.set_initial_status(initial_status, config) -addiction_model.configure_visualization(visualization_config) - -# Simulation -iterations = addiction_model.iteration_bunch(200, node_status=True) - -trends = addiction_model.build_trends(iterations) -addiction_model.plot(trends, len(iterations), delta=True) - -### Plots / data manipulation -addiction_model.visualize(iterations) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index c6f576f..418ac4f 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -1,8 +1,6 @@ # TODO -# - Parallel execution for multi runner, # - Fix visualization logic (overwrite vs update), # - numpy matrix implementation instead of networkx nodes/dict -# - Add sensitivity analysis options, # Requirements, networkx, numpy, matplotlib, PIL, pyintergraph, python-igraph, tqdm from ndlib.models.DiffusionModel import DiffusionModel @@ -567,6 +565,13 @@ def save_plot(self, simulation): :param simulation: Output of the matplotlib animation.FuncAnimation function """ + print('Saving plot at: ' + self.visualization_configuration['plot_output'] + ' ...') + split = self.visualization_configuration['plot_output'].split('/') + file_name = split[-1] + file_path = self.visualization_configuration['plot_output'].replace(file_name, '') + if not os.path.exists(file_path): + os.makedirs(file_path) + from PIL import Image writergif = animation.PillowWriter(fps=5) simulation.save(self.visualization_configuration['plot_output'], writer=writergif) diff --git a/ndlib/models/ContinuousModelRunner.py b/ndlib/models/ContinuousModelRunner.py index dc9a602..9e78929 100644 --- a/ndlib/models/ContinuousModelRunner.py +++ b/ndlib/models/ContinuousModelRunner.py @@ -1,3 +1,7 @@ +# TODO +# - Parallel execution +# - Add sensitivity analysis options + from SALib.sample import saltelli from SALib.analyze import sobol from ndlib.models.compartments.enums.SAType import SAType From a3b083f5659f85758d97eee58aa1a12bcab74ae6 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Sat, 3 Oct 2020 04:19:59 +0200 Subject: [PATCH 19/25] :fire: Remove redundant examples --- SIR_example.html | 85 ------------------------------------------------ SIR_example.py | 41 ----------------------- 2 files changed, 126 deletions(-) delete mode 100644 SIR_example.html delete mode 100644 SIR_example.py diff --git a/SIR_example.html b/SIR_example.html deleted file mode 100644 index e3dfe2e..0000000 --- a/SIR_example.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - Bokeh Plot - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - \ No newline at end of file diff --git a/SIR_example.py b/SIR_example.py deleted file mode 100644 index 7fc5c3f..0000000 --- a/SIR_example.py +++ /dev/null @@ -1,41 +0,0 @@ -import networkx as nx - -from ndlib.models.CompositeModel import CompositeModel -from ndlib.models.compartments.NodeStochastic import NodeStochastic -import ndlib.models.ModelConfig as mc - -from bokeh.io import show -from ndlib.viz.bokeh.DiffusionTrend import DiffusionTrend - -# Network definition -g1 = nx.erdos_renyi_graph(n=1000, p=0.1) - -# Model definition -SIR = CompositeModel(g1) -SIR.add_status('Susceptible') -SIR.add_status('Infected') -SIR.add_status('Removed') - -# Compartments -c1 = NodeStochastic(rate=0.02, triggering_status='Infected') -c2 = NodeStochastic(rate=0.01) - -# Rules -SIR.add_rule('Susceptible', 'Infected', c1) -SIR.add_rule('Infected', 'Removed', c2) - -# Configuration -config = mc.Configuration() -config.add_model_parameter('percentage_infected', 0.1) -SIR.set_initial_status(config) - -# Simulation -iterations = SIR.iteration_bunch(300, node_status=False) -trends = SIR.build_trends(iterations) - -# Visualization -viz = DiffusionTrend(SIR, trends) -p = viz.plot(width=400, height=400) -show(p) - -print(SIR.available_statuses) \ No newline at end of file From 938d01be569fa5aa2878a0c741ff2a33d4572884 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Sat, 3 Oct 2020 05:17:25 +0200 Subject: [PATCH 20/25] :bug: Fix remaining bugs --- ndlib/models/ContinuousModel.py | 1 - .../compartments/NodeNumericalVariable.py | 2 +- ndlib/models/compartments/__init__.py | 2 +- ndlib/test/test_compartment.py | 20 +++++++++---------- ndlib/test/test_continuous_model.py | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index 418ac4f..ea549e6 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -11,7 +11,6 @@ import numpy as np import matplotlib.pyplot as plt -import matplotlib.patches as patches import matplotlib as mpl import matplotlib.animation as animation diff --git a/ndlib/models/compartments/NodeNumericalVariable.py b/ndlib/models/compartments/NodeNumericalVariable.py index f003901..ac82a33 100644 --- a/ndlib/models/compartments/NodeNumericalVariable.py +++ b/ndlib/models/compartments/NodeNumericalVariable.py @@ -41,7 +41,7 @@ def __init__(self, var, var_type=None, value=None, value_type=None, op=None, pro if not isinstance(self.value, list) or self.value[1] < self.value[0]: raise ValueError("A range list is required to test IN condition") else: - if self.value_type == None: + if self.value_type is None: if not isinstance(self.value, int): if not isinstance(self.value, float): raise ValueError("When no value type is defined, the value should be numerical") diff --git a/ndlib/models/compartments/__init__.py b/ndlib/models/compartments/__init__.py index 3285c7a..ab3c25f 100644 --- a/ndlib/models/compartments/__init__.py +++ b/ndlib/models/compartments/__init__.py @@ -18,7 +18,7 @@ 'NodeThreshold', 'NodeCategoricalAttribute', 'NodeNumericalAttribute', - 'NodeNumericalVariable' + 'NodeNumericalVariable', 'EdgeStochastic', 'EdgeCategoricalAttribute', 'EdgeNumericalAttribute', diff --git a/ndlib/test/test_compartment.py b/ndlib/test/test_compartment.py index c1ecf7a..4302f1f 100644 --- a/ndlib/test/test_compartment.py +++ b/ndlib/test/test_compartment.py @@ -306,16 +306,16 @@ def test_node_num_variable(self): self.assertEqual(len(iterations), 10) with self.assertRaises(ValueError): - c = cpm.NodeNumericalVariable(5, var_type=NumericalType.ATTRIBUTE, value=0, op='==') - c = cpm.NodeNumericalVariable('even', value=0, op='==') - c = cpm.NodeNumericalVariable('even', var_type=3, value=0, value_type=3, op='==') - c = cpm.NodeNumericalVariable(var_type=NumericalType.ATTRIBUTE, value=0, op='==') - c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0) - c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0, op='IN') - c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=['a', 3], op='IN') - c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[3, 'a'], op='IN') - c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') - c = cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') + cpm.NodeNumericalVariable(5, var_type=NumericalType.ATTRIBUTE, value=0, op='==') + cpm.NodeNumericalVariable('even', value=0, op='==') + cpm.NodeNumericalVariable('even', var_type=3, value=0, value_type=3, op='==') + cpm.NodeNumericalVariable(None, var_type=NumericalType.ATTRIBUTE, value=0, op='==') + cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0) + cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0, op='IN') + cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=['a', 3], op='IN') + cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[3, 'a'], op='IN') + cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') + cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') def test_edge_num_attribute(self): diff --git a/ndlib/test/test_continuous_model.py b/ndlib/test/test_continuous_model.py index 5dc45b2..a763c22 100644 --- a/ndlib/test/test_continuous_model.py +++ b/ndlib/test/test_continuous_model.py @@ -6,7 +6,6 @@ import numpy as np import ndlib.models.ModelConfig as mc import ndlib.models.ContinuousModel as gc -import ndlib.models.ContinuousModelRunner as gcr import ndlib.models.compartments as cpm from ndlib.models.compartments.enums.NumericalType import NumericalType @@ -76,7 +75,8 @@ def test_states(self): model.add_status('status1') model.add_status('status2') - self.assertTrue('status1' in model.available_statuses.keys() and 'status2' in model.available_statuses.keys()) + self.assertIn('status1', model.available_statuses.keys()) + self.assertIn('status2', model.available_statuses.keys()) def test_initial_values(self): def initial_status_1(node, graph, status, constants): From 199301cb3e1399a5f44ce15a84cfe637c1783fbb Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Sat, 3 Oct 2020 06:21:07 +0200 Subject: [PATCH 21/25] :white_check_mark: :art: Add more tests and refactor certain parts --- .../continuous_model/optional/ModelRunner.rst | 4 +- .../compartments/NodeNumericalVariable.py | 8 +- ndlib/test/test_compartment.py | 11 +- ndlib/test/test_continuous_model.py | 141 ++++++++++++++++++ 4 files changed, 159 insertions(+), 5 deletions(-) diff --git a/docs/custom/continuous_model/optional/ModelRunner.rst b/docs/custom/continuous_model/optional/ModelRunner.rst index 7fc8749..6c1052a 100644 --- a/docs/custom/continuous_model/optional/ModelRunner.rst +++ b/docs/custom/continuous_model/optional/ModelRunner.rst @@ -52,7 +52,6 @@ Example: initial_status = { 'status_1': initial_status_1, 'status_2': initial_status_2, - 'status_3': 2 } model = ContinuousModel(g) @@ -78,7 +77,7 @@ Example: model.set_initial_status(initial_status, config) # Simulation - runner = ContinuousModelRunner(craving_control_model, config) + runner = ContinuousModelRunner(model, config) # Simulate the model 10 times with 100 iterations results = runner.run(10, [100], [initial_status]) @@ -142,7 +141,6 @@ Example: initial_status = { 'status_1': initial_status_1, 'status_2': initial_status_2, - 'status_3': 2 } model = ContinuousModel(g, constants=constants) diff --git a/ndlib/models/compartments/NodeNumericalVariable.py b/ndlib/models/compartments/NodeNumericalVariable.py index ac82a33..9f1be3e 100644 --- a/ndlib/models/compartments/NodeNumericalVariable.py +++ b/ndlib/models/compartments/NodeNumericalVariable.py @@ -25,6 +25,9 @@ def __init__(self, var, var_type=None, value=None, value_type=None, op=None, pro self.probability = probability self.operator = op + self.validate() + + def validate(self): if not isinstance(self.variable, str): raise ValueError("The variable should be a string pointing to an attribute or status") if self.variable_type is None: @@ -38,7 +41,10 @@ def __init__(self, var, var_type=None, value=None, value_type=None, op=None, pro if self.operator is not None and self.operator in self.__available_operators: if self.operator == "IN": - if not isinstance(self.value, list) or self.value[1] < self.value[0]: + if not isinstance(self.value, list) or \ + not (isinstance(self.value[0], int) or isinstance(self.value[0], float)) or \ + not (isinstance(self.value[1], int) or isinstance(self.value[1], float)) \ + or self.value[1] < self.value[0]: raise ValueError("A range list is required to test IN condition") else: if self.value_type is None: diff --git a/ndlib/test/test_compartment.py b/ndlib/test/test_compartment.py index 4302f1f..676fe73 100644 --- a/ndlib/test/test_compartment.py +++ b/ndlib/test/test_compartment.py @@ -304,17 +304,26 @@ def test_node_num_variable(self): model.set_initial_status(config) iterations = model.iteration_bunch(10) self.assertEqual(len(iterations), 10) - + with self.assertRaises(ValueError): cpm.NodeNumericalVariable(5, var_type=NumericalType.ATTRIBUTE, value=0, op='==') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', value=0, op='==') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=3, value=0, value_type=3, op='==') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable(None, var_type=NumericalType.ATTRIBUTE, value=0, op='==') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0) + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=0, op='IN') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=['a', 3], op='IN') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[3, 'a'], op='IN') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') + with self.assertRaises(ValueError): cpm.NodeNumericalVariable('even', var_type=NumericalType.ATTRIBUTE, value=[5, 3], op='IN') def test_edge_num_attribute(self): diff --git a/ndlib/test/test_continuous_model.py b/ndlib/test/test_continuous_model.py index a763c22..4b5599b 100644 --- a/ndlib/test/test_continuous_model.py +++ b/ndlib/test/test_continuous_model.py @@ -6,8 +6,10 @@ import numpy as np import ndlib.models.ModelConfig as mc import ndlib.models.ContinuousModel as gc +import ndlib.models.ContinuousModelRunner as gcr import ndlib.models.compartments as cpm from ndlib.models.compartments.enums.NumericalType import NumericalType +from ndlib.models.compartments.enums.SAType import SAType __author__ = 'Mathijs Maijer' __license__ = "BSD-2-Clause" @@ -190,6 +192,8 @@ def update(node, graph, status, attributes, constants): # Model definition path = './test_output/' output_path = path + 'file' + with self.assertRaises(ValueError): + model = gc.ContinuousModel(g, save_file=5) model = gc.ContinuousModel(g, save_file=output_path) model.add_status('status') @@ -209,3 +213,140 @@ def update(node, graph, status, attributes, constants): self.assertTrue(os.path.isfile(output_path + '.npy')) os.remove(output_path + '.npy') os.rmdir(path) + + def test_visualization(self): + initial_status = { + 'status': 0, + } + + def update(node, graph, status, attributes, constants): + return -2 + + # Network definition + g = nx.erdos_renyi_graph(n=100, p=0.5) + + output_path = './test.gif' + + # Visualization config + visualization_config = { + 'plot_interval': 1, + 'plot_variable': 'status', + 'show_plot': False, + 'plot_title': 'Example model', + 'animation_interval': 200, + 'plot_output': output_path + } + + # Model definition + model = gc.ContinuousModel(g, clean_status=True) + model.add_status('status') + + # Compartments + condition = cpm.NodeStochastic(1) + + # Rules + model.add_rule('status', update, condition) + + # Configuration + config = mc.Configuration() + model.set_initial_status(initial_status, config) + model.configure_visualization(visualization_config) + + # Simulation + iterations = model.iteration_bunch(2, node_status=True) + + trends = model.build_trends(iterations) + model.plot(trends, len(iterations), delta=True, delta_mean=True) + + ### Plots / data manipulation + model.visualize(iterations) + + self.assertTrue(os.path.isfile(output_path)) + os.remove(output_path) + + def test_runner(self): + g = nx.erdos_renyi_graph(n=1000, p=0.1) + + def initial_status_1(node, graph, status, constants): + return np.random.uniform(0, 0.5) + + def initial_status_2(node, graph, status, constants): + return status['status_1'] + np.random.uniform(0.5, 1) + + initial_status = { + 'status_1': initial_status_1, + 'status_2': initial_status_2, + } + + model = gc.ContinuousModel(g) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = cpm.NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] + 0.1 + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + 0.5 + + # Rules + model.add_rule('status_1', update_1, condition) + model.add_rule('status_2', update_2, condition) + + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + # Simulation + runner = gcr.ContinuousModelRunner(model, config) + results = runner.run(2, [1], [initial_status]) + self.assertEqual(len(results), 2) + + def test_runner_sa(self): + g = nx.erdos_renyi_graph(n=10, p=0.5) + + constants = { + 'constant_1': 0.5, + 'constant_2': 0.8 + } + + def initial_status_1(node, graph, status, constants): + return np.random.uniform(0, 0.5) + + def initial_status_2(node, graph, status, constants): + return status['status_1'] + np.random.uniform(0.5, 1) + + initial_status = { + 'status_1': initial_status_1, + 'status_2': initial_status_2, + } + + model = gc.ContinuousModel(g, constants=constants) + + model.add_status('status_1') + model.add_status('status_2') + + # Compartments + condition = cpm.NodeStochastic(1) + + # Update functions + def update_1(node, graph, status, attributes, constants): + return status[node]['status_2'] * constants['constant_1'] + + def update_2(node, graph, status, attributes, constants): + return status[node]['status_1'] + constants['constant_2'] + + # Rules + model.add_rule('status_1', update_1, condition) + model.add_rule('status_2', update_2, condition) + + config = mc.Configuration() + model.set_initial_status(initial_status, config) + + # Simulation + runner = gcr.ContinuousModelRunner(model, config) + analysis = runner.analyze_sensitivity(SAType.MEAN, initial_status, {'constant_1': (0, 1), 'constant_2': (-1, 1)}, 1, 1) + self.assertEqual(len(analysis.keys()), 2) From 04f4728c39f72b1c693cd03b25f8eb5d7b66c939 Mon Sep 17 00:00:00 2001 From: Tensaiz Date: Sat, 3 Oct 2020 06:39:12 +0200 Subject: [PATCH 22/25] :art: Refactor type checking --- ndlib/models/ContinuousModel.py | 140 +++++++++++++++++--------------- 1 file changed, 75 insertions(+), 65 deletions(-) diff --git a/ndlib/models/ContinuousModel.py b/ndlib/models/ContinuousModel.py index ea549e6..aab8b50 100644 --- a/ndlib/models/ContinuousModel.py +++ b/ndlib/models/ContinuousModel.py @@ -75,74 +75,12 @@ def configure_visualization(self, visualization_configuration): print('Configuring visualization...') self.visualization_configuration = visualization_configuration vis_keys = visualization_configuration.keys() - if 'plot_interval' in vis_keys: - if isinstance(visualization_configuration['plot_interval'], int): - if visualization_configuration['plot_interval'] <= 0: - raise ValueError('plot_interval must be a positive integer') - else: - raise ValueError('plot_interval must be a positive integer') - else: - raise ValueError('plot_interval must be included for visualization') - if 'show_plot' in vis_keys: - if not isinstance(visualization_configuration['show_plot'], bool): - raise ValueError('show_plot must be a boolean') - else: - self.visualization_configuration['show_plot'] = True - if 'plot_variable' in vis_keys: - if not isinstance(visualization_configuration['plot_variable'], str): - raise ValueError('Plot variable must be a string') - else: - self.visualization_configuration['plot_variable'] = None - if 'plot_title' in self.visualization_configuration.keys(): - if not isinstance(self.visualization_configuration['plot_title'], str): - raise ValueError('Plot name must be a string') - else: - vis_var = self.visualization_configuration['plot_variable'] - self.visualization_configuration['plot_title'] = 'Network simulation of ' + vis_var - - if 'plot_annotation' in vis_keys: - if not isinstance(self.visualization_configuration['plot_annotation'], str): - raise ValueError('Plot annotation must be a string') - else: - self.visualization_configuration['plot_annotation'] = None - - if 'cmin' in vis_keys: - if not isinstance(self.visualization_configuration['cmin'], int): - raise ValueError('cmin must be an integer') - else: - self.visualization_configuration['cmin'] = 0 + self.validate_plot_config(visualization_configuration, vis_keys) + self.validate_color_config(vis_keys) - if 'cmax' in vis_keys: - if not isinstance(self.visualization_configuration['cmax'], int): - raise ValueError('cmax must be an integer') - else: - self.visualization_configuration['cmax'] = 1 - - if 'color_scale' in vis_keys: - if not isinstance(self.visualization_configuration['color_scale'], str): - raise ValueError('Color scale must be a string') - else: - self.visualization_configuration['color_scale'] = 'RdBu' if 'pos' not in self.graph.nodes[0].keys(): - if 'layout' in vis_keys: - if self.visualization_configuration['layout'] == 'fr': - import pyintergraph - Graph = pyintergraph.InterGraph.from_networkx(self.graph.graph) - G = Graph.to_igraph() - layout = G.layout_fruchterman_reingold(niter=500) - positions = {node: {'pos': location} for node, location in enumerate(layout)} - else: - if 'layout_params' in vis_keys: - pos = self.visualization_configuration['layout'](self.graph.graph, **self.visualization_configuration['layout_params']) - else: - pos = self.visualization_configuration['layout'](self.graph.graph) - positions = {key: {'pos': location} for key, location in pos.items()} - else: - pos = nx.drawing.spring_layout(self.graph.graph) - positions = {key: {'pos': location} for key, location in pos.items()} - - nx.set_node_attributes(self.graph, positions) + self.configure_layout(vis_keys) if 'variable_limits' not in vis_keys: self.visualization_configuration['variable_limits'] = {key: [-1, 1] for key in list(self.available_statuses.keys())} @@ -159,6 +97,78 @@ def configure_visualization(self, visualization_configuration): else: raise Exception('Provide a visualization configuration when using this function') + def configure_layout(self, vis_keys): + if 'layout' in vis_keys: + if self.visualization_configuration['layout'] == 'fr': + import pyintergraph + Graph = pyintergraph.InterGraph.from_networkx(self.graph.graph) + G = Graph.to_igraph() + layout = G.layout_fruchterman_reingold(niter=500) + positions = {node: {'pos': location} for node, location in enumerate(layout)} + else: + if 'layout_params' in vis_keys: + pos = self.visualization_configuration['layout'](self.graph.graph, **self.visualization_configuration['layout_params']) + else: + pos = self.visualization_configuration['layout'](self.graph.graph) + positions = {key: {'pos': location} for key, location in pos.items()} + else: + pos = nx.drawing.spring_layout(self.graph.graph) + positions = {key: {'pos': location} for key, location in pos.items()} + + nx.set_node_attributes(self.graph, positions) + + def validate_plot_config(self, visualization_configuration, vis_keys): + if 'plot_interval' in vis_keys: + if isinstance(visualization_configuration['plot_interval'], int): + if visualization_configuration['plot_interval'] <= 0: + raise ValueError('plot_interval must be a positive integer') + else: + raise ValueError('plot_interval must be a positive integer') + else: + raise ValueError('plot_interval must be included for visualization') + if 'show_plot' in vis_keys: + if not isinstance(visualization_configuration['show_plot'], bool): + raise ValueError('show_plot must be a boolean') + else: + self.visualization_configuration['show_plot'] = True + if 'plot_variable' in vis_keys: + if not isinstance(visualization_configuration['plot_variable'], str): + raise ValueError('Plot variable must be a string') + else: + self.visualization_configuration['plot_variable'] = None + + if 'plot_title' in self.visualization_configuration.keys(): + if not isinstance(self.visualization_configuration['plot_title'], str): + raise ValueError('Plot name must be a string') + else: + vis_var = self.visualization_configuration['plot_variable'] + self.visualization_configuration['plot_title'] = 'Network simulation of ' + vis_var + + if 'plot_annotation' in vis_keys: + if not isinstance(self.visualization_configuration['plot_annotation'], str): + raise ValueError('Plot annotation must be a string') + else: + self.visualization_configuration['plot_annotation'] = None + + def validate_color_config(self, vis_keys): + if 'cmin' in vis_keys: + if not isinstance(self.visualization_configuration['cmin'], int): + raise ValueError('cmin must be an integer') + else: + self.visualization_configuration['cmin'] = 0 + + if 'cmax' in vis_keys: + if not isinstance(self.visualization_configuration['cmax'], int): + raise ValueError('cmax must be an integer') + else: + self.visualization_configuration['cmax'] = 1 + + if 'color_scale' in vis_keys: + if not isinstance(self.visualization_configuration['color_scale'], str): + raise ValueError('Color scale must be a string') + else: + self.visualization_configuration['color_scale'] = 'RdBu' + def add_status(self, status_name): """ Add a status/state to the model From 3bded7579e3b9179daab269ac226833e7e505939 Mon Sep 17 00:00:00 2001 From: Giulio Rossetti Date: Mon, 5 Oct 2020 16:40:04 +0200 Subject: [PATCH 23/25] :arrow_up: dependency/mock update --- .idea/misc.xml | 2 +- docs/conf.py | 2 +- ndlib/models/ContinuousModelRunner.py | 1 + ndlib/test/test_continuous_model.py | 1 + requirements.txt | 6 +++++- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 3999087..ba2d895 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 8eff746..022ee56 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ def __getattr__(cls, name): MOCK_MODULES = ['ipython', 'pygtk', 'gtk', 'gobject', 'argparse', 'matplotlib', 'matplotlib.pyplot', 'numpy', 'pandas', 'dynetx', 'networkx', - 'scipy'] + 'scipy', 'salib', 'pillow', 'pyintergraph', 'python-igraph'] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) html_theme = "sphinx_rtd_theme" diff --git a/ndlib/models/ContinuousModelRunner.py b/ndlib/models/ContinuousModelRunner.py index 9e78929..1b7217e 100644 --- a/ndlib/models/ContinuousModelRunner.py +++ b/ndlib/models/ContinuousModelRunner.py @@ -7,6 +7,7 @@ from ndlib.models.compartments.enums.SAType import SAType import numpy as np + class ContinuousModelRunner(object): def __init__(self, ContinuousModel, config, node_status=True): self.model = ContinuousModel diff --git a/ndlib/test/test_continuous_model.py b/ndlib/test/test_continuous_model.py index 4b5599b..992f698 100644 --- a/ndlib/test/test_continuous_model.py +++ b/ndlib/test/test_continuous_model.py @@ -15,6 +15,7 @@ __license__ = "BSD-2-Clause" __email__ = "m.f.maijer@gmail.com" + class NdlibContinuousModelTest(unittest.TestCase): def test_bare_model(self): def initial_addiction(node, graph, status, constants): diff --git a/requirements.txt b/requirements.txt index bf472e1..a022cd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,8 @@ bokeh==1.3.4 matplotlib==3.0.* pytest==5.1.* pandas -tqdm \ No newline at end of file +tqdm +salib>=1.3 +pillow +pyintergraph +python-igraph From c7d6934809ad2b122e68b2e78810e31e542df565 Mon Sep 17 00:00:00 2001 From: Giulio Rossetti Date: Mon, 5 Oct 2020 16:49:50 +0200 Subject: [PATCH 24/25] :arrow_up: dependency/mock update --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a022cd6..4c109cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ mock==3.0.5 -python-igraph==0.8.2 +python-igraph netdispatch==0.0.5 sphinx_rtd_theme==0.4.3 numpy==1.17.* @@ -14,4 +14,3 @@ tqdm salib>=1.3 pillow pyintergraph -python-igraph From 73b4ab7f3b5525f2c433e4c20c0f839b361ac23f Mon Sep 17 00:00:00 2001 From: Giulio Rossetti Date: Mon, 5 Oct 2020 17:05:17 +0200 Subject: [PATCH 25/25] :arrow_up: minimum python version set to 3.6 (minor bug fix) --- .travis.yml | 3 ++- README.md | 1 - docs/index.rst | 2 +- ndlib/test/test_continuous_model.py | 7 ++++--- ndlib/viz/mpl/DiffusionViz.py | 2 +- ndlib/viz/mpl/OpinionEvolution.py | 3 --- requirements.txt | 1 + 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 67a32c3..d2ac82c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: python python: - - "3.5" - "3.6" + - "3.7" + - "3.8" before_install: - pip install pytest pytest-cov diff --git a/README.md b/README.md index f63afe1..caa0048 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![Updates](https://pyup.io/repos/github/GiulioRossetti/ndlib/shield.svg)](https://pyup.io/repos/github/GiulioRossetti/ndlib/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/GiulioRossetti/ndlib.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/GiulioRossetti/ndlib/context:python) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/ndlib_nx/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link) -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FGiulioRossetti%2Fndlib.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FGiulioRossetti%2Fndlib?ref=badge_shield) [![DOI](https://zenodo.org/badge/59556819.svg)](https://zenodo.org/badge/latestdoi/59556819) [![PyPI download month](https://img.shields.io/pypi/dm/ndlib.svg?color=blue&style=plastic)](https://pypi.python.org/pypi/ndlib/) diff --git a/docs/index.rst b/docs/index.rst index d4d78f9..14ccb78 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ NDlib - Network Diffusion Library ================ =================== ================== ========== =============== **Date** **Python Versions** **Main Author** **GitHub** **pypl** -|date| 3.x `Giulio Rossetti`_ `Source`_ `Distribution`_ +|date| >=3.6 `Giulio Rossetti`_ `Source`_ `Distribution`_ ================ =================== ================== ========== =============== diff --git a/ndlib/test/test_continuous_model.py b/ndlib/test/test_continuous_model.py index 992f698..637f195 100644 --- a/ndlib/test/test_continuous_model.py +++ b/ndlib/test/test_continuous_model.py @@ -17,6 +17,7 @@ class NdlibContinuousModelTest(unittest.TestCase): + def test_bare_model(self): def initial_addiction(node, graph, status, constants): addiction = 0 @@ -116,10 +117,10 @@ def test_conditions(self): # Update functions def update_1(node, graph, status, attributes, constants): - return status[node]['status_2'] + 0.1 + return status[node]['status_2'] + 0.1 def update_2(node, graph, status, attributes, constants): - return status[node]['status_1'] + 0.5 + return status[node]['status_1'] + 0.5 # Rules model.add_rule('status_1', update_1, condition) @@ -138,7 +139,7 @@ def sample_state_weighted(graph, status): status_1 = [stat['status_1'] for stat in list(status.values())] factor = 1.0/sum(status_1) for s in status_1: - probs.append(s * factor) + probs.append(s * factor) return np.random.choice(graph.nodes, size=1, replace=False, p=probs) schemes = [ diff --git a/ndlib/viz/mpl/DiffusionViz.py b/ndlib/viz/mpl/DiffusionViz.py index 5b8b77c..754f8ac 100644 --- a/ndlib/viz/mpl/DiffusionViz.py +++ b/ndlib/viz/mpl/DiffusionViz.py @@ -70,7 +70,7 @@ def plot(self, filename=None, percentile=90, statuses=None): #,color=cols[i]) else: plt.plot(range(0, mx), l[1], lw=2, label=self.srev[k], alpha=0.5) # , color=cols[i]) - plt.fill_between(range(0, mx), l[0], l[2], alpha="0.2") # ,color=cols[i]) + plt.fill_between(range(0, mx), l[0], l[2], alpha=0.2) # ,color=cols[i]) i += 1 diff --git a/ndlib/viz/mpl/OpinionEvolution.py b/ndlib/viz/mpl/OpinionEvolution.py index 338f447..09cfd69 100644 --- a/ndlib/viz/mpl/OpinionEvolution.py +++ b/ndlib/viz/mpl/OpinionEvolution.py @@ -41,8 +41,6 @@ def plot(self, filename=None): nodes2opinions = {} node2col = {} - mx = 0 - last_it = self.srev[-1]['iteration'] + 1 last_seen = {} @@ -83,7 +81,6 @@ def plot(self, filename=None): plt.ylabel(self.ylabel, fontsize=24) plt.legend(loc="best", fontsize=18) - plt.tight_layout() if filename is not None: plt.savefig(filename) diff --git a/requirements.txt b/requirements.txt index 4c109cf..4751f2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ tqdm salib>=1.3 pillow pyintergraph +six \ No newline at end of file