(last updated: March 14th, 2021)

# Introduction to defSim

This short tutorial explains the basic functionality of defSim. First, we show the two main functions that create a single simulation run or a full experiment as a set of simulation runs with different parameter settings. The arguments are used to call a number of modules that are already implemented in defSim and can be combined to create a unique experiment. Then, we show the more modular use of defSim, where the modules are used independently or modified and fed back into the Simulation or Experiment classes.

## 1. Out of the box use

Using defSim ‘out of the box’ is the go-to use for quick replications, to illustrate well-known model dynamics, and for teaching purposes. All modules of defSim are called automatically, using reasonable default values or user specified parameters. There are two classes available for using defSim in this way: the Simulation <https://defsim.github.io/defSim/simulation.class.html>__ class and the Experiment <https://defsim.github.io/defSim/experiment.class.html>__ class. The Simulation class is used to specify a single simulation run, whereas the Experiment takes lists of parameter values to create a simulation experiment with a user-specified number of replications per parameter combination.

First, we import defSim and a couple other useful packages.

import defSim as ds

import networkx as nx              # used to handle the networkX object
import matplotlib.pyplot as plt    # for plotting
import numpy as np                 # includes some useful math tools
import random                      # used to set seeds for random but replicable drawing
import pandas as pd                # data manipulation


Then, we define a single simulation run, using the Simulation class

simrun = ds.Simulation()


The simulation object will run, even without specifying any arguments. It uses a long list of default values, which can be viewed using the class specific method return_values.

simrun.return_values()

network topology network_modifiers attributes_initializer focal_agent_selector neighbor_selector influence_function influenceable_attributes communication_regime dissimilarity_calculator ... seed network_provided agentIDs time_steps influence_steps output_realizations output_folder_path output_file_types tickwise tickwise_output_step_size
0 None grid None random_categorical random random similarity_adoption None one-to-one <defSim.dissimilarity_component.HammingDistanc... ... None False [] 0 0 [] None [] [] 1

1 rows × 22 columns

Let’s modify the object a little, to deviate from the default simulation, and run a simple bounded confidence model using a one-dimensional initially randomly distributed continuous opinion, on a complete graph network.

simrun = ds.Simulation(
seed=555,                                    # seed for replicability
attributes_initializer="random_continuous",  # continuous opinion with random start value
dissimilarity_measure="euclidean",           # distance calculator
topology="complete_graph",                   # graph where all agents are connected
influence_function="bounded_confidence",     # influence until dissimilarity > confidence
max_iterations=1000,                         # simulation stops after this # of ticks
parameter_dict={                             # dictionary for all arguments passed to modules
'n':20,                                  # size of the network
'num_features':1,                        # number of opinion features agents posess
'confidence_level':.8,                   # bounded confidence threshold value
'convergence_rate':.5                    # distance the receiver moves towards the sender
})
results = simrun.run()

/usr/local/lib/python3.9/site-packages/defSim/agents_init/agents_init.py:165: UserWarning: No Numpy Generator in parameter dictionary, creating default
warnings.warn("No Numpy Generator in parameter dictionary, creating default")
100%|██████████| 1000/1000 [00:00<00:00, 2177.03it/s]

Seed Ticks SuccessfulInfluence Topology n num_features confidence_level convergence_rate np_random_generator Regions Zones Homogeneity AverageDistance AverageOpinionf01
0 555 1000 998 complete_graph 20 1 0.8 0.5 Generator(PCG64) 20 1 0.05 3.411821e-08 0.442595

The simulation gives a pandas data frame as output, which we can print.

print(simrun)

     index   Seed  Ticks  SuccessfulInfluence        Topology   n  0        0  35342   1000                    0  complete_graph  20
1        0  45985   1000                    0  complete_graph  20
2        0  31109   1000                    0  complete_graph  20
3        0  26800   1000                    0  complete_graph  20
4        0  83560   1000                    0  complete_graph  20
..     ...    ...    ...                  ...             ...  ..
105      0  75306   1000                 1000  complete_graph  20
106      0  26744   1000                 1000  complete_graph  20
107      0  44321   1000                 1000  complete_graph  20
108      0  31496   1000                 1000  complete_graph  20
109      0  74249   1000                 1000  complete_graph  20

num_features communication_regime  confidence_level  convergence_rate  0               1           one-to-one               0.0               0.5
1               1           one-to-one               0.0               0.5
2               1           one-to-one               0.0               0.5
3               1           one-to-one               0.0               0.5
4               1           one-to-one               0.0               0.5
..            ...                  ...               ...               ...
105             1           one-to-one               1.0               0.5
106             1           one-to-one               1.0               0.5
107             1           one-to-one               1.0               0.5
108             1           one-to-one               1.0               0.5
109             1           one-to-one               1.0               0.5

seed np_random_generator  Regions  Zones  Homogeneity  AverageDistance  0    35342    Generator(PCG64)       20      1         0.05     3.474832e-01
1    45985    Generator(PCG64)       20      1         0.05     3.392755e-01
2    31109    Generator(PCG64)       20      1         0.05     3.537419e-01
3    26800    Generator(PCG64)       20      1         0.05     3.140546e-01
4    83560    Generator(PCG64)       20      1         0.05     3.101613e-01
..     ...                 ...      ...    ...          ...              ...
105  75306    Generator(PCG64)       20      1         0.05     4.944899e-08
106  26744    Generator(PCG64)       20      1         0.05     7.839228e-09
107  44321    Generator(PCG64)       20      1         0.05     1.218306e-07
108  31496    Generator(PCG64)       20      1         0.05     6.004397e-09
109  74249    Generator(PCG64)       20      1         0.05     3.752796e-08

AverageOpinionf01
0             0.563075
1             0.502311
2             0.488703
3             0.467109
4             0.457217
..                 ...
105           0.337614
106           0.496102
107           0.428962
108           0.655261
109           0.565978

[110 rows x 17 columns]

Now, we may ask a simple question: what is the effect of increasing the confidence level in the bounded confidence model? The confidence level is the proportion of dissimilarity that a receiving agent accepts from a sending agent in order for him to be influenced by the sending agent.

To answer that question, we can run the same model in the Experiment function. Instead of a float, we pass a list of floats to the confidence_level parameter. To get a robust estimate of the typical outcome at each setting, we replicate all our conditions 10 times (for 11 BC threshold values that makes 110 runs in total)

experiment = ds.Experiment(
seed=555,                                    # seed for replicability
attributes_initializer="random_continuous",  # continuous opinion with random start value
dissimilarity_measure="euclidean",           # distance calculator
topology="complete_graph",                   # graph where all agents are connected
influence_function="bounded_confidence",     # influence until dissimilarity > confidence
max_iterations=1000,                         # simulation stops after this # of ticks
network_parameters={                         # dictionary with arguments for 'network_init' module
'n':20},                                 # size of the network
attribute_parameters={                       # dictionary with arguments for 'attribute_init' module
'num_features':1},                       # number of opinion features agents posess
influence_parameters={                       # dictionary with arguments for 'influence_sim' module
'confidence_level':[x/10 for x in range(11)],   # list of BC threshold values (0.0 -> 1.0, incr=0.1)
'convergence_rate':0.5},                 # distance the receiver moves towards the sender
repetitions=10)                              # number of repetitions per condition

results = experiment.run()

0%|          | 0/110 [00:00<?, ?it/s]

110 different parameter combinations

/usr/local/lib/python3.9/site-packages/defSim/agents_init/agents_init.py:165: UserWarning: No Numpy Generator in parameter dictionary, creating default
warnings.warn("No Numpy Generator in parameter dictionary, creating default")
100%|██████████| 110/110 [00:35<00:00,  3.09it/s]


The experiment gave us a pandas data frame with 110 rows:

print(results)

     index   Seed  Ticks  SuccessfulInfluence        Topology   n  0        0  35342   1000                    0  complete_graph  20
1        0  45985   1000                    0  complete_graph  20
2        0  31109   1000                    0  complete_graph  20
3        0  26800   1000                    0  complete_graph  20
4        0  83560   1000                    0  complete_graph  20
..     ...    ...    ...                  ...             ...  ..
105      0  75306   1000                 1000  complete_graph  20
106      0  26744   1000                 1000  complete_graph  20
107      0  44321   1000                 1000  complete_graph  20
108      0  31496   1000                 1000  complete_graph  20
109      0  74249   1000                 1000  complete_graph  20

num_features communication_regime  confidence_level  convergence_rate  0               1           one-to-one               0.0               0.5
1               1           one-to-one               0.0               0.5
2               1           one-to-one               0.0               0.5
3               1           one-to-one               0.0               0.5
4               1           one-to-one               0.0               0.5
..            ...                  ...               ...               ...
105             1           one-to-one               1.0               0.5
106             1           one-to-one               1.0               0.5
107             1           one-to-one               1.0               0.5
108             1           one-to-one               1.0               0.5
109             1           one-to-one               1.0               0.5

seed np_random_generator  Regions  Zones  Homogeneity  AverageDistance  0    35342    Generator(PCG64)       20      1         0.05     3.162482e-01
1    45985    Generator(PCG64)       20      1         0.05     3.381264e-01
2    31109    Generator(PCG64)       20      1         0.05     3.239260e-01
3    26800    Generator(PCG64)       20      1         0.05     3.579149e-01
4    83560    Generator(PCG64)       20      1         0.05     2.987322e-01
..     ...                 ...      ...    ...          ...              ...
105  75306    Generator(PCG64)       20      1         0.05     2.157184e-08
106  26744    Generator(PCG64)       20      1         0.05     7.696255e-09
107  44321    Generator(PCG64)       20      1         0.05     9.459349e-08
108  31496    Generator(PCG64)       20      1         0.05     3.888918e-09
109  74249    Generator(PCG64)       20      1         0.05     9.271612e-08

AverageOpinionf01
0             0.549963
1             0.604544
2             0.441872
3             0.433244
4             0.543781
..                 ...
105           0.529282
106           0.615500
107           0.578355
108           0.375521
109           0.472806

[110 rows x 17 columns]

Using the plotting functionality in defSim (see here), we can quickly create a plot that shows the relationship between confidence level and the average distance in the plot.

plot = ds.RelPlot(ylim=[0,1])
plot.plot(data=results,x='confidence_level',y="AverageDistance")


## 2. Modular use

defSim consists of six different modules. Some modules take care of model initialization (network_init & agents_init), while other modules are called sequentially in a loop (focal_agent_sim, neighbor_selector_sim & influence_sim), until some convergence criterium is satisfied. The process is visualized in the flow chart right of this text.

The Simulation class (or its wrapper the Experiment class) calls all of these modules in the background, but defSim is designed in such a way that a modular use of the package is easy and user-friendly. All modules take a NetworkX object as in- and output. Writing an extension is easy, since we can just use this networkx object to manipulate outside of one of the modules. The best way to extend the functionality of defSim is to write your extension within the defSim framework. Adding your own (published) extension to the defSim package is a great way to transparently share your code and attract attention to your work.

Now, let’s turn to two of these examples. One in which we manipulate the networkx object, and one in which we write our own module-method.

### Example | Writing your own module implementation | Two groups attribute initializer

The true power of defSim becomes clear when we use custom module implementations within the defSim framework. All calls to realizations of a certain module in the Simulation and Experiment classes can be replaced with your own implementation of such a module. To succesfully pass an implementation, we need to inherit from the abstract base classes of that particular module.

Let’s show what we mean here with an example.

Say you want to model two groups that each have a different bias in their opinions at the start of the model. Under what conditions will these two groups still converge on one position? How quick do they converge relative to if there weren’t any groups? To answer these questions we need to code an attribute initializer that will be called only at the start of the simulation.

We create an attribute initializer called MyOwnAI for ‘my own attribute initializer’:

class MyOwnAI(ds.AttributesInitializer):
"""
This attribute initializer creates a group attribute and assigns one of two values (0 / 1) to an agent
with equal probability. Thereafter, the values for the num_features' desired features is drawn from a uniform
distribution. Limits of the uniform distribution are set for each group.
"""

def __init__(self, limits_0 = [0, 1], limits_1 = [0,1]):
self.limits_0 = limits_0
self.limits_1 = limits_1

def initialize_attributes(self, network, **kwargs):
ds.set_categorical_attribute(network, 'group', [0, 1])
for j in range(kwargs.get('num_features', 1)):
for i in network.nodes():
if network.nodes[i]['group'] == 0:
network.nodes[i]['f{:02d}'.format(j+1)] = np.random.uniform(self.limits_0[0], self.limits_0[1])
else:
network.nodes[i]['f{:02d}'.format(j+1)] = np.random.uniform(self.limits_1[0], self.limits_1[1])


The arguments that are unique to this attribute initializer have to be set in the __init__() part of the class. Here, we have specified defaults for the opinion limits of the two groups ([0,1]), but these defaults can be overwritten when we call the module later.

The initialize_attributes part of the code is simple. We set a categorical attribute (group) randomly, and then initialize a random opionion from a uniform distribution with limits limits_0 and limits_1.

The new attribute initializer can now be passed to the Simulation class:

simobj = ds.Simulation(
seed = 1414,
topology = 'complete_graph',
attributes_initializer = MyOwnAI(limits_0 = [0, 0.7], limits_1 = [0.3, 1]),  # here is our implementation
influence_function = 'bounded_confidence',
influenceable_attributes = ['f01'],         # important: only f01 can be influenced, not 'group'!
dissimilarity_measure = 'euclidean',
max_iterations = 3000,
output_realizations = ['Basic', ds.AttributeReporter('group')],  # basic output is asked, and we also want to get the values for group membership back (for plotting)
tickwise = ['f01'],
parameter_dict = {
'n': 100,
'num_features': 1,
'confidence_level': 0.6,
'convergence_rate':0.5
}
)

results = simobj.run()   # run the model

groups = results['group']
color_values = {0: 'blue', 1: 'red'}
colors = [color_values[group] for group in groups[0]]

ds.DynamicsPlot(colors=colors, linewidth=1, ylim=[0,1]).plot(data=results, y="Tickwise_f01")

  0%|          | 0/3000 [00:00<?, ?it/s]/usr/local/lib/python3.9/site-packages/defSim/influence_sim/BoundedConfidence.py:38: UserWarning: convergence_rate not specified, using default value 0.5
warnings.warn("convergence_rate not specified, using default value 0.5")
100%|██████████| 3000/3000 [00:03<00:00, 879.39it/s]


The agents neatly align into two groups. This happens based on opinion distance, but note that group membership is also taken into account when calculating distance!

### Example | Independent use of modules | Biased media influence

In this example we use the modules independently. We introduce a biased media platform that will randomly influence half the population one every ten rounds. Agents follow a weighted linear influence function, which means that they are influenced proportionally to their opinion similarity to the sending agent. Once opinion distance becomes too large, influence becomes negative and agents experience a push away from the source.

Note that this could easily be done inside of the defSim framework, but we showcase here an easy to understand example of how all the modules work.

    # the INITIALIZATION stage (where the NetworkX object is set up)

# we set a random seed to be able to replicate the run
random.seed(666)
np.random.seed(666)

# initialization of the network
ABM = ds.generate_network("grid",num_agents=20)
ds.initialize_attributes(ABM, realization="random_continuous", num_features=1)

# here we introduce the biased media agent. An agent connected to all others, with a biased opinion.
agents = list(ABM.nodes())              # store the original agentset to pass to the agent selectors
ABM.nodes['biased_media']['f01'] = 0.23 # fix the opinion
for i in agents:                        # add an edge between media source and all other agents

# initialize the dissimilarity calculator (= euclidean in the continuous opinion world)
calculator = ds.select_calculator("euclidean")
calculator.calculate_dissimilarity_networkwide(ABM)

## the SIMULATION stage (where we adjust the NetworkX object until convergence)

iterator = 0                       # to count the number of iterations
opinions_tickwise = []             # to record the opinions at each timestep

while iterator < 200:    # we stop after 200 iterations
if iterator in [x*10 for x in range(1000)]:    # once every 10 rounds we exert media influence
network = ABM,
realization = "weighted_linear",     # strong influence between close agents, may be negative when distance is large
agent_i = 'biased_media',            # the sending agent (i.e. media source)
agents_j = agents,                   # receiving agents
regime = "one-to-one",               # communication regime
dissimilarity_measure = calculator,  # dissimilarity calculator defined above
homophily=1.5)                       # parameter for the 'weighted_linear' influence module
else:                          # interaction between agents
agent_i = ds.select_focal_agent(
network = ABM,
realization = "random", # select random agent
agents=agents)          # agentset to select from
agent_j = ds.select_neighbors(
network = ABM,
realization = "random", # select random neighbor
focal_agent = agent_i,  # agent who's neighbors are eligible for selection
regime = "one-to-one")  # communication regime, needed here to tell the selector how many agents to select
if agent_j == ['biased_media']:        # what if the selected neighbor is the media source?
while agent_j == ['biased_media']: # select again if this is the case
agent_j = ds.select_neighbors(
network = ABM,
realization = "random",
focal_agent = agent_i,
regime = "one-to-one")
network = ABM,
realization = "weighted_linear",     # strong influence between close agents, may be negative when distance is large
agent_i = agent_i,                   # the sending agent (i.e. media source)
agents_j = agent_j,                  # receiving agents
regime = "one-to-one",               # communication regime
dissimilarity_measure = calculator,  # dissimilarity calculator defined above
homophily=1.5)                       # parameter for the 'weighted_linear' influence module
opinions_tickwise.append(list(nx.get_node_attributes(ABM,'f01').values()))  # store results
iterator += 1                  # increase iterator


The generated output is stored in opinions_tickwise – a list of lists with all agent opinions at each timestep.

We can plot the opinion trajectories using the DynamicsPlot function from defSim. This function normally takes a pandas data frame with a column Tickwise_fXX generated by the tickwise output option in the Simulation or Experiment class. If we pass a data frame with the opinions_tickwise to the DynamicsPlot function, we can get the same functionality:

plot = ds.DynamicsPlot(fast=True)
plot.plot(data=pd.DataFrame({'Tickwise_f01':[opinions_tickwise]}), y='Tickwise_f01')


There are a few things to note from these opinion trajectories. As one might expect, a large number of the agents (90%) converge on the opinion position of the biased media source. However, in the process, two agents rejected the stance of the medium and extremized into the other direction. Occassional meeting with the other agents furthermore create temporary extremization by others, which is again corrected by the media source. To compare, let’s look at a single run of the same model without the media source.

    # the INITIALIZATION stage (where the NetworkX object is set up)

# we set a random seed to be able to replicate the run
random.seed(14)
np.random.seed(14)

# initialization of the network
ABM = ds.generate_network("grid",num_agents=20)
ds.initialize_attributes(ABM, realization="random_continuous", num_features=1)

# initialize the dissimilarity calculator (= euclidean in the continuous opinion world)
calculator = ds.select_calculator("euclidean")
calculator.calculate_dissimilarity_networkwide(ABM)

## the SIMULATION stage (where we adjust the NetworkX object until convergence)

iterator = 0                       # to count the number of iterations
opinions_tickwise = []             # to record the opinions at each timestep

while iterator < 200:    # we stop after 200 iterations
agent_i = ds.select_focal_agent(
network = ABM,
realization = "random", # select random agent
agents=agents)          # agentset to select from
agent_j = ds.select_neighbors(
network = ABM,
realization = "random", # select random neighbor
focal_agent = agent_i,  # agent who's neighbors are eligible for selection
regime = "one-to-one")  # communication regime, needed here to tell the selector how many agents to select
network = ABM,
realization = "weighted_linear",     # strong influence between close agents, may be negative when distance is large
agent_i = agent_i,                   # the sending agent
agents_j = agent_j,                  # receiving agent
regime = "one-to-one",               # communication regime
dissimilarity_measure = calculator,  # dissimilarity calculator defined above
homophily=1.5)                       # parameter for the 'weighted_linear' influence module
opinions_tickwise.append(list(nx.get_node_attributes(ABM,'f01').values()))  # store results
iterator += 1                  # increase iterator

plot = ds.DynamicsPlot(fast=True)
plot.plot(data=pd.DataFrame({'Tickwise_f01':[opinions_tickwise]}), y='Tickwise_f01')
`

In this example the agents neatly converge to a central position