diff --git a/README.md b/README.md index 5930c74..b9600d6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Soil is an extensible and user-friendly Agent-based Social Simulator for Social Networks. Learn how to run your own simulations with our [documentation](http://soilsim.readthedocs.io). -Follow our [tutorial](examples/tutorial/soil_tutorial.ipynb) to develop your own agent models. +Follow our [tutorial](docs/tutorial/soil_tutorial.ipynb) to develop your own agent models. > **Warning** > Soil 1.0 introduced many fundamental changes. Check the [documention on how to update your simulations to work with newer versions](docs/notes_v1.0.rst) diff --git a/docs/conf.py b/docs/conf.py index 7dde391..33adcec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,10 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['IPython.sphinxext.ipython_console_highlighting'] +extensions = [ + "IPython.sphinxext.ipython_console_highlighting", + "nbsphinx", +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -64,7 +67,7 @@ release = '0.1' # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -152,6 +155,3 @@ texinfo_documents = [ author, 'Soil', 'One line description of project.', 'Miscellaneous'), ] - - - diff --git a/docs/index.rst b/docs/index.rst index b589c06..23fc761 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,19 +2,20 @@ Welcome to Soil's documentation! ================================ Soil is an opinionated Agent-based Social Simulator in Python focused on Social Networks. +To get started developing your own simulations and agent behaviors, check out our :doc:`Tutorial ` and the `examples on GitHub `. + +Soil can be installed through pip (see more details in the :doc:`installation` page):. .. image:: soil.png :width: 80% :align: center -Soil can be installed through pip (see more details in the :doc:`installation` page): .. code:: bash pip install soil -To get started developing your own simulations and agent behaviors, check out our :doc:`Tutorial ` and the `examples on GitHub . If you use Soil in your research, do not forget to cite this paper: @@ -46,7 +47,8 @@ If you use Soil in your research, do not forget to cite this paper: :caption: Learn more about soil: installation - Tutorial + Tutorial + notes_v1.0 .. diff --git a/docs/notes_v1.0.rst b/docs/notes_v1.0.rst index 9f83a9c..4bfaeaa 100644 --- a/docs/notes_v1.0.rst +++ b/docs/notes_v1.0.rst @@ -1,3 +1,6 @@ +Upgrading to Soil 1.0 +--------------------- + What are the main changes in version 1.0? ######################################### diff --git a/docs/output_30_0.png b/docs/output_30_0.png deleted file mode 100644 index b5ed204..0000000 Binary files a/docs/output_30_0.png and /dev/null differ diff --git a/docs/output_34_0.png b/docs/output_34_0.png deleted file mode 100644 index 8ec88fe..0000000 Binary files a/docs/output_34_0.png and /dev/null differ diff --git a/docs/output_49_0.png b/docs/output_49_0.png deleted file mode 100644 index 3392810..0000000 Binary files a/docs/output_49_0.png and /dev/null differ diff --git a/docs/output_50_0.png b/docs/output_50_0.png deleted file mode 100644 index 914c4dd..0000000 Binary files a/docs/output_50_0.png and /dev/null differ diff --git a/docs/output_58_0.png b/docs/output_58_0.png deleted file mode 100644 index f8b44a6..0000000 Binary files a/docs/output_58_0.png and /dev/null differ diff --git a/docs/output_58_1.png b/docs/output_58_1.png deleted file mode 100644 index 26eb877..0000000 Binary files a/docs/output_58_1.png and /dev/null differ diff --git a/docs/output_58_2.png b/docs/output_58_2.png deleted file mode 100644 index 41c5676..0000000 Binary files a/docs/output_58_2.png and /dev/null differ diff --git a/docs/output_58_3.png b/docs/output_58_3.png deleted file mode 100644 index 90fba2e..0000000 Binary files a/docs/output_58_3.png and /dev/null differ diff --git a/docs/output_58_4.png b/docs/output_58_4.png deleted file mode 100644 index 747497b..0000000 Binary files a/docs/output_58_4.png and /dev/null differ diff --git a/docs/output_60_0.png b/docs/output_60_0.png deleted file mode 100644 index 88057fe..0000000 Binary files a/docs/output_60_0.png and /dev/null differ diff --git a/docs/output_60_1.png b/docs/output_60_1.png deleted file mode 100644 index 0b4eb13..0000000 Binary files a/docs/output_60_1.png and /dev/null differ diff --git a/docs/output_60_2.png b/docs/output_60_2.png deleted file mode 100644 index 576c44c..0000000 Binary files a/docs/output_60_2.png and /dev/null differ diff --git a/docs/output_60_3.png b/docs/output_60_3.png deleted file mode 100644 index 369382c..0000000 Binary files a/docs/output_60_3.png and /dev/null differ diff --git a/docs/output_60_4.png b/docs/output_60_4.png deleted file mode 100644 index c83fd1e..0000000 Binary files a/docs/output_60_4.png and /dev/null differ diff --git a/docs/output_62_0.png b/docs/output_62_0.png deleted file mode 100644 index 0345a2f..0000000 Binary files a/docs/output_62_0.png and /dev/null differ diff --git a/docs/output_62_1.png b/docs/output_62_1.png deleted file mode 100644 index 976a9e2..0000000 Binary files a/docs/output_62_1.png and /dev/null differ diff --git a/docs/output_62_2.png b/docs/output_62_2.png deleted file mode 100644 index 46561af..0000000 Binary files a/docs/output_62_2.png and /dev/null differ diff --git a/docs/output_62_3.png b/docs/output_62_3.png deleted file mode 100644 index 322ead4..0000000 Binary files a/docs/output_62_3.png and /dev/null differ diff --git a/docs/output_62_4.png b/docs/output_62_4.png deleted file mode 100644 index 93aee06..0000000 Binary files a/docs/output_62_4.png and /dev/null differ diff --git a/docs/output_68_1.png b/docs/output_68_1.png deleted file mode 100644 index f3127aa..0000000 Binary files a/docs/output_68_1.png and /dev/null differ diff --git a/docs/output_70_1.png b/docs/output_70_1.png deleted file mode 100644 index 27f18de..0000000 Binary files a/docs/output_70_1.png and /dev/null differ diff --git a/docs/output_77_0.png b/docs/output_77_0.png deleted file mode 100644 index 02b67cf..0000000 Binary files a/docs/output_77_0.png and /dev/null differ diff --git a/docs/output_81_0.png b/docs/output_81_0.png deleted file mode 100644 index 0f297ea..0000000 Binary files a/docs/output_81_0.png and /dev/null differ diff --git a/docs/output_82_1.png b/docs/output_82_1.png deleted file mode 100644 index 196c1c3..0000000 Binary files a/docs/output_82_1.png and /dev/null differ diff --git a/docs/output_83_0.png b/docs/output_83_0.png deleted file mode 100644 index a174a02..0000000 Binary files a/docs/output_83_0.png and /dev/null differ diff --git a/docs/requirements.txt b/docs/requirements.txt index 654cff4..290ff66 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ ipython>=7.31.1 +nbsphinx==0.9.1 diff --git a/docs/soil_tutorial.rst b/docs/soil_tutorial.rst deleted file mode 100644 index fc93303..0000000 --- a/docs/soil_tutorial.rst +++ /dev/null @@ -1,1405 +0,0 @@ -Soil Tutorial -============= - -Introduction ------------- - -This notebook is an introduction to the soil agent-based social network -simulation framework. In particular, we will focus on a specific use -case: studying the propagation of news in a social network. - -The steps we will follow are: - -- Modelling the behavior of agents -- Running the simulation using different configurations -- Analysing the results of each simulation - -But before that, let’s import the soil module and networkx. - -.. code:: ipython3 - - import soil - import networkx as nx - - %load_ext autoreload - %autoreload 2 - - import matplotlib.pyplot as plt - -Basic concepts --------------- - -There are three main elements in a soil simulation: - -- The environment or model. It assigns agents to nodes in the network, - and stores the environment parameters (shared state for all agents). -- The network topology. A simulation may use an existing NetworkX - topology, or generate one on the fly. -- Agents. There are several types of agents, depending on their - behavior and their capabilities. Some examples of built-in types of - agents are: - - - Network agents, which are linked to a node in the topology. They - have additional methods to access their neighbors. - - FSM (Finite state machine) agents. Their behavior is defined in - terms of states, and an agent will move from one state to another. - - Evented agents, an actor-based model of agents, which can - communicate with one another through message passing. - - For convenience, a general ``soil.Agent`` class is provided, which - inherits from Network, FSM and Evented at the same time. - -Soil provides several abstractions over events to make developing agents -easier. This means you can use events (timeouts, delays) in soil, but -for the most part we will assume your models will be step-based o. - -Modeling behaviour ------------------- - -Our first step will be to model how every person in the social network -reacts to hearing a piece of disinformation (news). We will follow a -very simple model based on a finite state machine. - -A person may be in one of two states: **neutral** (the default state) -and **infected**. A neutral person may hear about a piece of -disinformation either on the TV (with probability **prob_tv_spread**) or -through their friends. Once a person has heard the news, they will -spread it to their friends (with a probability -**prob_neighbor_spread**). Some users do not have a TV, so they will -only be infected by their friends. - -The spreading probabilities will change over time due to different -factors. We will represent this variance using an additional agent which -will not be a part of the social network. - -Modelling Agents -~~~~~~~~~~~~~~~~ - -The following sections will cover the basics of developing agents in -SOIL. - -For more advanced patterns, please check the **examples** folder in the -repository. - -Basic agents -^^^^^^^^^^^^ - -The most basic agent in Soil is ``soil.BaseAgent``. These agents -implement their behavior by overriding the ``step`` method, which will -be run in every simulation step. Only one agent will be running at any -given time, and it will be doing so until the ``step`` function returns. - -Agents can access their environment through their ``self.model`` -attribute. This is most commonly used to get access to the environment -parameters and methods. Here is a simple example of an agent: - -.. code:: python - - class ExampleAgent(BaseAgent): - def init(self): - self.is_infected = False - self.steps_neutral = 0 - - def step(self): - # Implement agent logic - if self.is_infected: - ... # Do something, like infecting other agents - return self.die("No need to do anything else") # Stop forever - else: - ... # Do something - self.steps_neutral += 1 - if self.steps_neutral > self.model.max_steps_neutral: - self.is_infected = True - -Any kind of agent behavior can be implemented with this ``step`` -function. However, it has two main drawbacks: 1) complex behaviors can -get difficult both write and understand; 2) these behaviors are not -composable. - -FSM agents -^^^^^^^^^^ - -One way to solve both issues is to model agents as `Finite-state -Machines `__ (FSM, -for short). FSM define a series of possible states for the agent, and -changes between these states. These states can be modelled and extended -independently. - -This is modelled in Soil through the ``soil.FSM`` class. Agents that -inherit from ``soil.FSM`` do not need to specify a ``step`` method. -Instead, we describe each finite state with a function. To change to -another state, a function may return the new state, or the ``id`` of a -state. If no state is returned, the state remains unchanged. - -The current state of the agent can be checked with ``agent.state_id``. -That state id can be used to look for other agents in that specific -state. - -Our previous example could be expressed like this: - -.. code:: python - - class FSMExample(FSM): - - def init(self): - self.steps_neutral = 0 - - @state(default=True) - def neutral(self): - ... # Do something - self.steps_neutral += 1 - if self.steps_neutral > self.model.max_steps_neutral: - return self.infected # Change state - - @state - def infected(self): - ... # Do something - return self.die("No need to do anything else") - -Generator-based agents -^^^^^^^^^^^^^^^^^^^^^^ - -Another design pattern that can be very useful in some cases is to model -each step (or a specific state) using generators (the ``yield`` -keyword). - -.. code:: python - - class GenExample(BaseAgent): - def step(self): - for i in range(self.model.max_steps_neutral): - ... # Do something - yield # Signal the scheduler that this step is done for now - ... # Do something - return self.die("No need to do anything else") - -Telling the scheduler when to wake up an agent -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -By default, every agent will be called in every simulation step, and the -time elapsed between two steps is controlled by the ``interval`` -attribute in the environment. - -But agents may signal the scheduler when they expect to be called again. -This is especially useful when an agent is going to be dormant for a -long time. To do so, an agent can return (or ``yield``) from a ``step`` -or a ``state`` a value of type ``soil.When`` (absolute time), -``soil.Delta`` (relative time) or ``soil.Cond``, telling the scheduler -when the agent will be ready to run again. If it returns nothing (i.e., -``None``), the agent will be ready to run at the next simulation step. - -Environment agents -~~~~~~~~~~~~~~~~~~ - -Environment agents allow us to control the state of the environment. In -this case, we will use an environment agent to simulate a very viral -event. - -When the event happens, the agent will modify the probability of -spreading the rumor. - -.. code:: ipython3 - - import logging - - class EventGenerator(soil.BaseAgent): - level = logging.INFO - - def step(self): - # Do nothing until the time of the event - yield soil.When(self.model.event_time) - self.info("TV event happened") - self.model.prob_tv_spread = 0.5 - self.model.prob_neighbor_spread *= 2 - self.model.prob_neighbor_spread = min(self.model.prob_neighbor_spread, 1) - yield - self.model.prob_tv_spread = 0 - - while self.alive: - self.model.prob_neighbor_spread = self.model.prob_neighbor_spread * self.model.neighbor_factor - if self.model.prob_neighbor_spread < 0.01: - return self.die("neighbors can no longer spread the rumour") - yield - -Environment (Model) -~~~~~~~~~~~~~~~~~~~ - -Let’s define a environment model to test our event generator agent. This -environment will have a single agent (the event generator). We will also -tell the environment to save the value of ``prob_tv_spread`` after every -step: - -.. code:: ipython3 - - class NewsEnv(soil.NetworkEnvironment): - - prob_tv_spread = 0.1 - prob_neighbor_spread = 0.1 - event_time = 10 - tv_factor = 0.5 - neighbor_factor = 0.9 - - - def init(self): - self.add_model_reporter("prob_tv_spread") - self.add_agent(EventGenerator) - -Once the environment has been defined, we can run a simulation - -.. code:: ipython3 - - it = NewsEnv.run(iterations=1, dump=False, max_time=14) - - it[0].model_df() - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, description='NewsEnv', max=1, style=ProgressStyle(description_width='initi… - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=1), HTML(value=''))) - - -.. parsed-literal:: - - - - - - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
stepagent_countprob_tv_spread
time
0010.1
10110.1
11210.5
12310.0
13410.0
14510.0
-
- - - -As we can see, the event occurred right after ``t=10``, so by ``t=11`` -the value of ``prob_tv_spread`` was already set to ``1.0``. - -You may notice nothing happened between ``t=0`` and ``t=1``. That is -because there aren’t any other agents in the simulation, and our event -generator explicitly waited until ``t=10``. - -Network agents -~~~~~~~~~~~~~~ - -In our disinformation scenario, we will model our agents as a FSM with -two states: ``neutral`` (default) and ``infected``. - -Here’s the code: - -.. code:: ipython3 - - class NewsSpread(soil.Agent): - has_tv = False - infected_by_friends = False - - @soil.state(default=True) - def neutral(self): - if self.infected_by_friends: - return self.infected - if self.has_tv: - if self.prob(self.model.prob_tv_spread): - return self.infected - - @soil.state - def infected(self): - for neighbor in self.iter_neighbors(state_id=self.neutral.id): - if self.prob(self.model.prob_neighbor_spread): - neighbor.infected_by_friends = True - -We can check that our states are well defined, here: - -.. code:: ipython3 - - NewsSpread.states() - - - - -.. parsed-literal:: - - ['dead', 'neutral', 'infected'] - - - -Environment (Model) -~~~~~~~~~~~~~~~~~~~ - -Let’s modify our simple simulation. We will add a network of agents of -type NewsSpread. - -Only one agent (0) will have a TV (in blue). - -.. code:: ipython3 - - def generate_simple(): - G = nx.Graph() - G.add_edge(0, 1) - G.add_edge(0, 2) - G.add_edge(2, 3) - G.add_node(4) - return G - - G = generate_simple() - pos = nx.spring_layout(G) - nx.draw_networkx(G, pos, node_color='red') - nx.draw_networkx(G, pos, nodelist=[0], node_color='blue') - - - -.. image:: output_30_0.png - - -.. code:: ipython3 - - class NewsEnv(soil.NetworkEnvironment): - - prob_tv_spread = 0 - prob_neighbor_spread = 0.1 - event_time = 10 - tv_factor = 0.5 - neighbor_factor = 0.9 - - - def init(self): - self.add_agent(EventGenerator) - self.G = generate_simple() - self.populate_network(NewsSpread) - self.agent(node_id=0).has_tv = True - self.add_model_reporter('prob_tv_spread') - self.add_model_reporter('prob_neighbor_spread') - -.. code:: ipython3 - - it = NewsEnv.run(max_time=20) - it[0].model_df() - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, description='NewsEnv', max=1, style=ProgressStyle(description_width='initi… - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=1), HTML(value=''))) - - -.. parsed-literal:: - - - - - - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
stepagent_countprob_tv_spreadprob_neighbor_spread
time
0060.00.100000
1160.00.100000
2260.00.100000
3360.00.100000
4460.00.100000
5560.00.100000
6660.00.100000
7760.00.100000
8860.00.100000
9960.00.100000
101060.00.100000
111160.50.200000
121260.00.180000
131360.00.162000
141460.00.145800
151560.00.131220
161660.00.118098
171760.00.106288
181860.00.095659
191960.00.086093
202060.00.077484
-

20 rows × 2504 columns

-
- - - -In this case, notice that the inclusion of other agents (which run every -step) means that the simulation did not skip to ``t=10``. - -Now, let’s look at the state of our agents in every step: - -.. code:: ipython3 - - soil.analysis.plot(it[0]) - - - -.. image:: output_34_0.png - - -Running in more scenarios -------------------------- - -In real life, you probably want to run several simulations, varying some -of the parameters so that you can compare and answer your research -questions. - -For instance: - -- Does the outcome depend on the structure of our network? We will use - different generation algorithms to compare them (Barabasi-Albert and - Erdos-Renyi) -- How does neighbor spreading probability affect my simulation? We will - try probability values in the range of [0, 0.4], in intervals of 0.1. - -.. code:: ipython3 - - class NewsEnvComplete(soil.Environment): - prob_tv = 0.05 - prob_tv_spread = 0 - prob_neighbor_spread = 0 - event_time = 10 - tv_factor = 0 - neighbor_factor = 0.5 - generator = "erdos_renyi_graph" - n = 100 - - def init(self): - self.add_agent(EventGenerator) - if not self.G: - opts = {"n": self.n} - if self.generator == "erdos_renyi_graph": - opts["p"] = 0.5 - elif self.generator == "barabasi_albert_graph": - opts["m"] = 4 - self.create_network(generator=self.generator, **opts) - - self.populate_network([NewsSpread, - NewsSpread.w(has_tv=True)], - [1-self.prob_tv, self.prob_tv]) - self.add_model_reporter('prob_tv_spread') - self.add_model_reporter('prob_neighbor_spread') - self.add_agent_reporter('state_id') - -Since we do not care about previous results, we will -set\ ``overwrite=True``. - -.. code:: ipython3 - - s = soil.Simulation(model=NewsEnvComplete, iterations=5, max_time=30, dump=True, overwrite=True) - N = 100 - probabilities = [0, 0.25, 0.5, 0.75, 1.0] - generators = ["erdos_renyi_graph", "barabasi_albert_graph"] - - - it = s.run(name=f"newspread", matrix=dict(n=[N], generator=generators, prob_neighbor_spread=probabilities)) - - -.. parsed-literal:: - - [INFO ][17:29:24] Output directory: /mnt/data/home/j/git/lab.gsi/soil/soil/examples/tutorial/soil_output - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, description='newspread', max=10, style=ProgressStyle(description_width='in… - - -.. parsed-literal:: - - n = 100 - generator = erdos_renyi_graph - prob_neighbor_spread = 0 - - -.. image:: output_58_3.png - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - -.. image:: output_58_4.png - -.. parsed-literal:: - - n = 100 - generator = erdos_renyi_graph - prob_neighbor_spread = 0.25 - -.. code:: ipython3 - - analysis.plot_all('soil_output/Spread_erdos*', analysis.get_count, 'state_id'); - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - -.. image:: output_60_0.png - -.. parsed-literal:: - - n = 100 - generator = erdos_renyi_graph - prob_neighbor_spread = 0.5 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - n = 100 - generator = erdos_renyi_graph - prob_neighbor_spread = 0.75 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - -The previous cells were using the ``count_value`` function for -aggregation. There’s another function to plot numeral values: - -.. parsed-literal:: - - n = 100 - generator = erdos_renyi_graph - prob_neighbor_spread = 1.0 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - n = 100 - generator = barabasi_albert_graph - prob_neighbor_spread = 0 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - n = 100 - generator = barabasi_albert_graph - prob_neighbor_spread = 0.25 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - n = 100 - generator = barabasi_albert_graph - prob_neighbor_spread = 0.5 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - n = 100 - generator = barabasi_albert_graph - prob_neighbor_spread = 0.75 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - n = 100 - generator = barabasi_albert_graph - prob_neighbor_spread = 1.0 - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, max=5), HTML(value=''))) - - -.. parsed-literal:: - - - - -.. code:: ipython3 - - assert len(it) == len(probabilities) * len(generators) * s.iterations - -The results are conveniently stored in sqlite (history of agent and -environment state) and the configuration is saved in a YAML file. - -You can also export the results to GEXF format (dynamic network) and CSV -using .\ ``run(dump=['gexf', 'csv'])`` or the command line flags -``--graph --csv``. - -.. code:: ipython3 - - !tree soil_output - !du -xh soil_output/* - - -.. parsed-literal:: - - soil_output - └── newspread - ├── newspread_1681989837.124865.dumped.yml - ├── newspread_1681990513.1584163.dumped.yml - ├── newspread_1681990524.5204282.dumped.yml - ├── newspread_1681990796.858183.dumped.yml - ├── newspread_1682002299.544348.dumped.yml - ├── newspread_1682003721.597205.dumped.yml - ├── newspread_1682003784.1948986.dumped.yml - ├── newspread_1682003812.4626257.dumped.yml - ├── newspread_1682004020.182087.dumped.yml - ├── newspread_1682004044.6837814.dumped.yml - ├── newspread_1682004398.267355.dumped.yml - ├── newspread_1682004564.1052232.dumped.yml - └── newspread.sqlite - - 1 directory, 13 files - 21M soil_output/newspread - - -Analysing the results -~~~~~~~~~~~~~~~~~~~~~ - -Loading data -^^^^^^^^^^^^ - -Once the simulations are over, we can use soil to analyse the results. - -There are two main ways: directly using the iterations returned by the -``run`` method, or loading up data from the results database. This is -particularly useful to store data between sessions, and to accumulate -results over multiple runs. - -The mainThe main method to load data from the database is ``read_sql``, -which can be used in two ways: - -- ``analysis.read_sql()`` to load all the results from a - sqlite database . e.g. \ ``read_sql('my_simulation/file.db.sqlite')`` -- ``analysis.read_sql(name=)`` will look for the - default path for a simulation named ```` - -The result in both cases is a named tuple with four dataframes: - -- ``configuration``, which contains configuration parameters per - simulation -- ``parameters``, which shows the parameters used **in every - iteration** of every simulation -- ``env``, with the data collected from the model in each iteration (as - specified in ``model_reporters``) -- ``agents``, like ``env``, but for ``agent_reporters`` - -Let’s see it in action by loading the stored results into a pandas -dataframe: - -.. code:: ipython3 - - res = soil.read_sql(name="newspread", include_agents=True) - -Plotting data -~~~~~~~~~~~~~ - -Once we have loaded the results from the file, we can use them just like -any other dataframe. - -Here is an example of plotting the ratio of infected users in each of -our simulations: - -.. code:: ipython3 - - for (g, group) in res.env.dropna().groupby("params_id"): - params = res.parameters.query(f'params_id == "{g}"').iloc[0] - title = f"{params.generator.rstrip('_graph')} {params.prob_neighbor_spread}" - prob = group.groupby(by=["step"]).prob_neighbor_spread.mean() - line = "-" - if "barabasi" in params.generator: - line = "--" - prob.rename(title).fillna(0).plot(linestyle=line) - plt.title("Mean probability for each configuration") - plt.legend(); - - - -.. image:: output_49_0.png - - -.. code:: ipython3 - - for (g, group) in res.agents.dropna().groupby("params_id"): - params = res.parameters.query(f'params_id == "{g}"').iloc[0] - title = f"{params.generator.rstrip('_graph')} {params.prob_neighbor_spread}" - counts = group.groupby(by=["step", "state_id"]).value_counts().unstack() - line = "-" - if "barabasi" in params.generator: - line = "--" - (counts.infected/counts.sum(axis=1)).rename(title).fillna(0).plot(linestyle=line) - plt.legend() - plt.xlim([9, None]); - plt.title("Ratio of infected users"); - - - -.. image:: output_50_0.png - - -Data format ------------ - -Parameters -~~~~~~~~~~ - -The ``parameters`` dataframe has three keys: - -- The identifier of the simulation. This will be shared by all - iterations launched in the same run -- The identifier of the parameters used in the simulation. This will be - shared by all iterations that have the exact same set of parameters. -- The identifier of the iteration. Each row should have a different - iteration identifier - -There will be a column per each parameter passed to the environment. In -this case, that’s three: **generator**, **n** and -**prob_neighbor_spread**. - -.. code:: ipython3 - - res.parameters.head() - - - - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
keygeneratornprob_neighbor_spread
iteration_idparams_idsimulation_id
039063f8newspread_1682002299.544348erdos_renyi_graph1001.0
5db645dnewspread_1682002299.544348barabasi_albert_graph1000.0
8f26adbnewspread_1682002299.544348barabasi_albert_graph1000.5
cb3dbcanewspread_1682002299.544348erdos_renyi_graph1000.5
d1fe9c1newspread_1682002299.544348barabasi_albert_graph1001.0
-
- - - -Configuration -~~~~~~~~~~~~~ - -This dataset is indexed by the identifier of the simulation, and there -will be a column per each attribute of the simulation. For instance, -there is one for the number of processes used, another one for the path -where the results were stored, etc. - -.. code:: ipython3 - - res.config.head() - - - - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
indexversionsource_filenamedescriptiongroupbackupoverwritedry_rundump...num_processesexportersmodel_reportersagent_reporterstablesoutdirexporter_paramslevelskip_testdebug
simulation_id
newspread_1682002299.54434802NonenewspreadNoneFalseTrueFalseTrue...1[<class 'soil.exporters.default'>]{}{}{}/mnt/data/home/j/git/lab.gsi/soil/soil/example...{}20FalseFalse
-

1 rows × 29 columns

-
- - - -Model reporters -~~~~~~~~~~~~~~~ - -The ``env`` dataframe includes the data collected from the model. The -keys in this case are the same as ``parameters``, and an additional one: -**step**. - -.. code:: ipython3 - - res.env.head() - - - -.. image:: output_81_0.png - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
agent_counttimeprob_tv_spreadprob_neighbor_spread
simulation_idparams_iditeration_idstep
newspread_1682002299.544348fcfc9550010100.00.0
110110.00.0
210120.00.0
310130.00.0
410140.00.0
-
- - - -Agent reporters -~~~~~~~~~~~~~~~ - -This dataframe reflects the data collected for all the agents in the -simulation, in every step where data collection was invoked. - -The key in this dataframe is similar to the one in the ``parameters`` -dataframe, but there will be two more keys: the ``step`` and the -``agent_id``. There will be a column per each agent reporter added to -the model. In our case, there is only one: ``state_id``. - - - - res.agents.head() - - - - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
state_id
simulation_idparams_iditeration_idstepagent_id
newspread_1682002299.544348fcfc955000None
1neutral
2neutral
3neutral
4neutral
-
- - diff --git a/examples/tutorial/soil_tutorial.html b/docs/tutorial/soil_tutorial.html similarity index 100% rename from examples/tutorial/soil_tutorial.html rename to docs/tutorial/soil_tutorial.html diff --git a/examples/tutorial/soil_tutorial.ipynb b/docs/tutorial/soil_tutorial.ipynb similarity index 62% rename from examples/tutorial/soil_tutorial.ipynb rename to docs/tutorial/soil_tutorial.ipynb index a83073d..7d301e3 100644 --- a/examples/tutorial/soil_tutorial.ipynb +++ b/docs/tutorial/soil_tutorial.ipynb @@ -38,13 +38,19 @@ }, "source": [ "This notebook is an introduction to the soil agent-based social network simulation framework.\n", - "In particular, we will focus on a specific use case: studying the propagation of news in a social network.\n", + "It will focus on a specific use case: studying the propagation of disinformation through TV and social networks.\n", + "\n", "\n", "The steps we will follow are:\n", "\n", - "* Modelling the behavior of agents\n", + "* Cover some basics about simulations in Soil (environments, agents, etc.)\n", + "* Simulate a basic scenario with a single agent\n", + "* Add more complexity to our scenario\n", "* Running the simulation using different configurations\n", - "* Analysing the results of each simulation" + "* Analysing the results of each simulation\n", + "\n", + "The simulations in this tutorial will be kept simple, for the sake of clarity.\n", + "However, they provide all the building blocks necessary to model, run and analyse more complex scenarios." ] }, { @@ -74,11 +80,8 @@ }, "outputs": [], "source": [ - "import soil\n", + "from soil import *\n", "import networkx as nx\n", - " \n", - "%load_ext autoreload\n", - "%autoreload 2\n", "\n", "import matplotlib.pyplot as plt" ] @@ -104,11 +107,11 @@ "hidePrompt": false }, "source": [ - "There are three main elements in a soil simulation:\n", + "There are two main elements in a soil simulation:\n", " \n", - "* The environment or model. It assigns agents to nodes in the network, and stores the environment parameters (shared state for all agents).\n", - "* The network topology. A simulation may use an existing NetworkX topology, or generate one on the fly.\n", - "* Agents. There are several types of agents, depending on their behavior and their capabilities. Some examples of built-in types of agents are:\n", + "* The **environment** or model. It assigns agents to nodes in the network, and stores the environment parameters (shared state for all agents).\n", + " - `soil.NetworkEnvironment` models also contain a network topology (accessible through through `self.G`). A simulation may use an existing NetworkX topology, or generate one on the fly. The `NetworkEnvironment` class is parameterized, which makes it easy to initialize environments with a variety of network topologies. **In this tutorial, we will manually add a network to each environment**.\n", + "* One or more **agents**. Agents are programmed with their individual behaviors, and they can communicate with the environment and with other agents. There are several types of agents, depending on their behavior and their capabilities. Some examples of built-in types of agents are:\n", " - Network agents, which are linked to a node in the topology. They have additional methods to access their neighbors.\n", " - FSM (Finite state machine) agents. Their behavior is defined in terms of states, and an agent will move from one state to another.\n", " - Evented agents, an actor-based model of agents, which can communicate with one another through message passing.\n", @@ -354,12 +357,12 @@ "source": [ "import logging\n", "\n", - "class EventGenerator(soil.BaseAgent):\n", + "class EventGenerator(BaseAgent):\n", " level = logging.INFO\n", " \n", " def step(self):\n", " # Do nothing until the time of the event\n", - " yield soil.When(self.model.event_time)\n", + " yield When(self.model.event_time)\n", " self.info(\"TV event happened\")\n", " self.model.prob_tv_spread = 0.5\n", " self.model.prob_neighbor_spread *= 2\n", @@ -397,8 +400,10 @@ }, "outputs": [], "source": [ - "class NewsEnv(soil.NetworkEnvironment):\n", + "class NewsEnvSimple(NetworkEnvironment):\n", " \n", + " # Here we set the default parameters for our model\n", + " # We will be able to override them on a per-simulation basis\n", " prob_tv_spread = 0.1\n", " prob_neighbor_spread = 0.1\n", " event_time = 10\n", @@ -418,7 +423,7 @@ "hidePrompt": false }, "source": [ - "Once the environment has been defined, we can run a simulation " + "Once the environment has been defined, we can quickly run our simulation through the `run` method on NewsEnv:" ] }, { @@ -432,12 +437,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9fdc8c75869c4af092737891dab50a30", + "model_id": "3839c24e051d4e82a16aaabcd10eff82", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "NewsEnv: 0%| | 0/1 [00:00" ] @@ -724,7 +729,7 @@ }, "outputs": [], "source": [ - "class NewsEnv(soil.NetworkEnvironment):\n", + "class NewsEnvNetwork(Environment):\n", " \n", " prob_tv_spread = 0\n", " prob_neighbor_spread = 0.1\n", @@ -744,7 +749,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 9, "metadata": { "hideCode": false, "hidePrompt": false @@ -753,12 +758,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6b8425e6b67143ac83c5613346abef3c", + "model_id": "0c1604da16344da89d7d13279ba768eb", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "NewsEnv: 0%| | 0/1 [00:00" ] @@ -1635,9 +1650,7 @@ { "cell_type": "code", "execution_count": 18, - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -1679,35 +1692,35 @@ " \n", " 0\n", " 39063f8\n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " erdos_renyi_graph\n", " 100\n", " 1.0\n", " \n", " \n", " 8f26adb\n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " barabasi_albert_graph\n", " 100\n", " 0.5\n", " \n", " \n", " 92fdcb9\n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " erdos_renyi_graph\n", " 100\n", " 0.25\n", " \n", " \n", " cb3dbca\n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " erdos_renyi_graph\n", " 100\n", " 0.5\n", " \n", " \n", " d1fe9c1\n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " barabasi_albert_graph\n", " 100\n", " 1.0\n", @@ -1719,19 +1732,19 @@ "text/plain": [ "key generator \\\n", "iteration_id params_id simulation_id \n", - "0 39063f8 newspread_1682352039.0612123 erdos_renyi_graph \n", - " 8f26adb newspread_1682352039.0612123 barabasi_albert_graph \n", - " 92fdcb9 newspread_1682352039.0612123 erdos_renyi_graph \n", - " cb3dbca newspread_1682352039.0612123 erdos_renyi_graph \n", - " d1fe9c1 newspread_1682352039.0612123 barabasi_albert_graph \n", + "0 39063f8 newspread_1682354533.7559075 erdos_renyi_graph \n", + " 8f26adb newspread_1682354533.7559075 barabasi_albert_graph \n", + " 92fdcb9 newspread_1682354533.7559075 erdos_renyi_graph \n", + " cb3dbca newspread_1682354533.7559075 erdos_renyi_graph \n", + " d1fe9c1 newspread_1682354533.7559075 barabasi_albert_graph \n", "\n", "key n prob_neighbor_spread \n", "iteration_id params_id simulation_id \n", - "0 39063f8 newspread_1682352039.0612123 100 1.0 \n", - " 8f26adb newspread_1682352039.0612123 100 0.5 \n", - " 92fdcb9 newspread_1682352039.0612123 100 0.25 \n", - " cb3dbca newspread_1682352039.0612123 100 0.5 \n", - " d1fe9c1 newspread_1682352039.0612123 100 1.0 " + "0 39063f8 newspread_1682354533.7559075 100 1.0 \n", + " 8f26adb newspread_1682354533.7559075 100 0.5 \n", + " 92fdcb9 newspread_1682354533.7559075 100 0.25 \n", + " cb3dbca newspread_1682354533.7559075 100 0.5 \n", + " d1fe9c1 newspread_1682354533.7559075 100 1.0 " ] }, "execution_count": 18, @@ -1828,7 +1841,7 @@ " \n", " \n", " \n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " 0\n", " 2\n", " None\n", @@ -1845,7 +1858,7 @@ " {}\n", " {}\n", " {}\n", - " /mnt/data/home/j/git/lab.gsi/soil/soil/example...\n", + " /mnt/data/home/j/git/lab.gsi/soil/soil/docs/tu...\n", " {}\n", " 20\n", " False\n", @@ -1859,31 +1872,31 @@ "text/plain": [ " index version source_file name \\\n", "simulation_id \n", - "newspread_1682352039.0612123 0 2 None newspread \n", + "newspread_1682354533.7559075 0 2 None newspread \n", "\n", " description group backup overwrite dry_run dump \\\n", "simulation_id \n", - "newspread_1682352039.0612123 None False True False True \n", + "newspread_1682354533.7559075 None False True False True \n", "\n", " ... num_processes \\\n", "simulation_id ... \n", - "newspread_1682352039.0612123 ... 1 \n", + "newspread_1682354533.7559075 ... 1 \n", "\n", " exporters \\\n", "simulation_id \n", - "newspread_1682352039.0612123 [] \n", + "newspread_1682354533.7559075 [] \n", "\n", " model_reporters agent_reporters tables \\\n", "simulation_id \n", - "newspread_1682352039.0612123 {} {} {} \n", + "newspread_1682354533.7559075 {} {} {} \n", "\n", " outdir \\\n", "simulation_id \n", - "newspread_1682352039.0612123 /mnt/data/home/j/git/lab.gsi/soil/soil/example... \n", + "newspread_1682354533.7559075 /mnt/data/home/j/git/lab.gsi/soil/soil/docs/tu... \n", "\n", " exporter_params level skip_test debug \n", "simulation_id \n", - "newspread_1682352039.0612123 {} 20 False False \n", + "newspread_1682354533.7559075 {} 20 False False \n", "\n", "[1 rows x 29 columns]" ] @@ -1954,7 +1967,7 @@ " \n", " \n", " \n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " ff1d24a\n", " 0\n", " 0\n", @@ -1998,7 +2011,7 @@ "text/plain": [ " agent_count time \\\n", "simulation_id params_id iteration_id step \n", - "newspread_1682352039.0612123 ff1d24a 0 0 101 0 \n", + "newspread_1682354533.7559075 ff1d24a 0 0 101 0 \n", " 1 101 1 \n", " 2 101 2 \n", " 3 101 3 \n", @@ -2006,7 +2019,7 @@ "\n", " prob_tv_spread \\\n", "simulation_id params_id iteration_id step \n", - "newspread_1682352039.0612123 ff1d24a 0 0 0.0 \n", + "newspread_1682354533.7559075 ff1d24a 0 0 0.0 \n", " 1 0.0 \n", " 2 0.0 \n", " 3 0.0 \n", @@ -2014,7 +2027,7 @@ "\n", " prob_neighbor_spread \n", "simulation_id params_id iteration_id step \n", - "newspread_1682352039.0612123 ff1d24a 0 0 0.0 \n", + "newspread_1682354533.7559075 ff1d24a 0 0 0.0 \n", " 1 0.0 \n", " 2 0.0 \n", " 3 0.0 \n", @@ -2085,7 +2098,7 @@ " \n", " \n", " \n", - " newspread_1682352039.0612123\n", + " newspread_1682354533.7559075\n", " ff1d24a\n", " 0\n", " 0\n", @@ -2115,7 +2128,7 @@ "text/plain": [ " state_id\n", "simulation_id params_id iteration_id step agent_id \n", - "newspread_1682352039.0612123 ff1d24a 0 0 0 None\n", + "newspread_1682354533.7559075 ff1d24a 0 0 0 None\n", " 1 neutral\n", " 2 neutral\n", " 3 neutral\n", diff --git a/setup.py b/setup.py index a64aa41..59d12e2 100644 --- a/setup.py +++ b/setup.py @@ -17,9 +17,9 @@ def parse_requirements(filename): install_reqs = parse_requirements("requirements.txt") test_reqs = parse_requirements("test-requirements.txt") extras_require={ - 'mesa': ['mesa>=0.8.9'], 'geo': ['scipy>=1.3'], - 'web': ['tornado'] + 'web': ['tornado'], + 'ipython': ['ipython==8.12', 'nbformat==5.8'], } extras_require['all'] = [dep for package in extras_require.values() for dep in package] diff --git a/soil/environment.py b/soil/environment.py index 63055d4..1d48e2b 100644 --- a/soil/environment.py +++ b/soil/environment.py @@ -216,10 +216,11 @@ class BaseEnvironment(Model): @classmethod def run(cls, *, + name=None, iterations=1, num_processes=1, **kwargs): from .simulation import Simulation - return Simulation(name=cls.__name__, + return Simulation(name=name or cls.__name__, model=cls, iterations=iterations, num_processes=num_processes, **kwargs).run() diff --git a/test-requirements.txt b/test-requirements.txt index d95c1b1..b1c0d7b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,3 +2,5 @@ pytest pytest-profiling scipy>=1.3 tornado +nbconvert==7.3.1 +nbformat==5.8.0 diff --git a/tests/test_ipython.py b/tests/test_ipython.py index ff94b0e..a765468 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -7,7 +7,7 @@ ROOT = os.path.abspath(os.path.dirname(__file__)) class TestNotebooks(TestCase): def test_tutorial(self): - notebook = os.path.join(ROOT, "../examples/tutorial/soil_tutorial.ipynb") + notebook = os.path.join(ROOT, "../docs/tutorial/soil_tutorial.ipynb") with open(notebook) as f: nb = nbformat.read(f, as_version=4) ep = ExecutePreprocessor(timeout=60000, kernel_name='python3')