Merge branch 'mesa'
@ -1,241 +0,0 @@
|
||||
Configuring a simulation
|
||||
------------------------
|
||||
|
||||
There are two ways to configure a simulation: programmatically and with a configuration file.
|
||||
In both cases, the parameters used are the same.
|
||||
The advantage of a configuration file is that it is a clean declarative description, and it makes it easier to reproduce.
|
||||
|
||||
Simulation configuration files can be formatted in ``json`` or ``yaml`` and they define all the parameters of a simulation.
|
||||
Here's an example (``example.yml``).
|
||||
|
||||
.. literalinclude:: example.yml
|
||||
:language: yaml
|
||||
|
||||
|
||||
This example configuration will run three trials (``num_trials``) of a simulation containing a randomly generated network (``network_params``).
|
||||
The 100 nodes in the network will be SISaModel agents (``network_agents.agent_type``), which is an agent behavior that is included in Soil.
|
||||
10% of the agents (``weight=1``) will start in the content state, 10% in the discontent state, and the remaining 80% (``weight=8``) in the neutral state.
|
||||
All agents will have access to the environment (``environment_params``), which only contains one variable, ``prob_infected``.
|
||||
The state of the agents will be updated every 2 seconds (``interval``).
|
||||
|
||||
Now run the simulation with the command line tool:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
soil example.yml
|
||||
|
||||
Once the simulation finishes, its results will be stored in a folder named ``MyExampleSimulation``.
|
||||
Three types of objects are saved by default: a pickle of the simulation; a ``YAML`` representation of the simulation (which can be used to re-launch it); and for every trial, a ``sqlite`` file with the content of the state of every network node and the environment parameters at every step of the simulation.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
soil_output
|
||||
└── MyExampleSimulation
|
||||
├── MyExampleSimulation.dumped.yml
|
||||
├── MyExampleSimulation.simulation.pickle
|
||||
├── MyExampleSimulation_trial_0.db.sqlite
|
||||
├── MyExampleSimulation_trial_1.db.sqlite
|
||||
└── MyExampleSimulation_trial_2.db.sqlite
|
||||
|
||||
|
||||
You may also ask soil to export the states in a ``csv`` file, and the network in gephi format (``gexf``).
|
||||
|
||||
Network
|
||||
=======
|
||||
|
||||
The network topology for the simulation can be loaded from an existing network file or generated with one of the random network generation methods from networkx.
|
||||
|
||||
Loading a network
|
||||
#################
|
||||
|
||||
To load an existing network, specify its path in the configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
network_params:
|
||||
path: /tmp/mynetwork.gexf
|
||||
|
||||
Soil will try to guess what networkx method to use to read the file based on its extension.
|
||||
However, we only test using ``gexf`` files.
|
||||
|
||||
For simple networks, you may also include them in the configuration itself using , using the ``topology`` parameter like so:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
topology:
|
||||
nodes:
|
||||
- id: First
|
||||
- id: Second
|
||||
links:
|
||||
- source: First
|
||||
target: Second
|
||||
|
||||
|
||||
Generating a random network
|
||||
###########################
|
||||
|
||||
To generate a random network using one of networkx's built-in methods, specify the `graph generation algorithm <https://networkx.github.io/documentation/development/reference/generators.html>`_ and other parameters.
|
||||
For example, the following configuration is equivalent to :code:`nx.complete_graph(n=100)`:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 100
|
||||
|
||||
Environment
|
||||
============
|
||||
The environment is the place where the shared state of the simulation is stored.
|
||||
For instance, the probability of disease outbreak.
|
||||
The configuration file may specify the initial value of the environment parameters:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
environment_params:
|
||||
daily_probability_of_earthquake: 0.001
|
||||
number_of_earthquakes: 0
|
||||
|
||||
All agents have access to the environment parameters.
|
||||
|
||||
In some scenarios, it is useful to have a custom environment, to provide additional methods or to control the way agents update environment state.
|
||||
For example, if our agents play the lottery, the environment could provide a method to decide whether the agent wins, instead of leaving it to the agent.
|
||||
|
||||
|
||||
Agents
|
||||
======
|
||||
Agents are a way of modelling behavior.
|
||||
Agents can be characterized with two variables: agent type (``agent_type``) and state.
|
||||
Only one agent is executed at a time (generally, every ``interval`` seconds), and it has access to its state and the environment parameters.
|
||||
Through the environment, it can access the network topology and the state of other agents.
|
||||
|
||||
There are three three types of agents according to how they are added to the simulation: network agents and environment agent.
|
||||
|
||||
Network Agents
|
||||
##############
|
||||
Network agents are attached to a node in the topology.
|
||||
The configuration file allows you to specify how agents will be mapped to topology nodes.
|
||||
|
||||
The simplest way is to specify a single type of agent.
|
||||
Hence, every node in the network will be associated to an agent of that type.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_type: SISaModel
|
||||
|
||||
It is also possible to add more than one type of agent to the simulation, and to control the ratio of each type (using the ``weight`` property).
|
||||
For instance, with following configuration, it is five times more likely for a node to be assigned a CounterModel type than a SISaModel type.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
- agent_type: CounterModel
|
||||
weight: 5
|
||||
|
||||
The third option is to specify the type of agent on the node itself, e.g.:
|
||||
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
topology:
|
||||
nodes:
|
||||
- id: first
|
||||
agent_type: BaseAgent
|
||||
states:
|
||||
first:
|
||||
agent_type: SISaModel
|
||||
|
||||
|
||||
This would also work with a randomly generated network:
|
||||
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network:
|
||||
generator: complete
|
||||
n: 5
|
||||
agent_type: BaseAgent
|
||||
states:
|
||||
- agent_type: SISaModel
|
||||
|
||||
|
||||
|
||||
In addition to agent type, you may add a custom initial state to the distribution.
|
||||
This is very useful to add the same agent type with different states.
|
||||
e.g., to populate the network with SISaModel, roughly 10% of them with a discontent state:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 9
|
||||
state:
|
||||
id: neutral
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
|
||||
Lastly, the configuration may include initial state for one or more nodes.
|
||||
For instance, to add a state for the two nodes in this configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_type: SISaModel
|
||||
network:
|
||||
generator: complete_graph
|
||||
n: 2
|
||||
states:
|
||||
- id: content
|
||||
- id: discontent
|
||||
|
||||
|
||||
Or to add state only to specific nodes (by ``id``).
|
||||
For example, to apply special skills to Linux Torvalds in a simulation:
|
||||
|
||||
.. literalinclude:: ../examples/torvalds.yml
|
||||
:language: yaml
|
||||
|
||||
|
||||
Environment Agents
|
||||
##################
|
||||
In addition to network agents, more agents can be added to the simulation.
|
||||
These agents are programmed in much the same way as network agents, the only difference is that they will not be assigned to network nodes.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
environment_agents:
|
||||
- agent_type: MyAgent
|
||||
state:
|
||||
mood: happy
|
||||
- agent_type: DummyAgent
|
||||
|
||||
|
||||
You may use environment agents to model events that a normal agent cannot control, such as natural disasters or chance.
|
||||
They are also useful to add behavior that has little to do with the network and the interactions within that network.
|
||||
|
||||
Templating
|
||||
==========
|
||||
|
||||
Sometimes, it is useful to parameterize a simulation and run it over a range of values in order to compare each run and measure the effect of those parameters in the simulation.
|
||||
For instance, you may want to run a simulation with different agent distributions.
|
||||
|
||||
This can be done in Soil using **templates**.
|
||||
A template is a configuration where some of the values are specified with a variable.
|
||||
e.g., ``weight: "{{ var1 }}"`` instead of ``weight: 1``.
|
||||
There are two types of variables, depending on how their values are decided:
|
||||
|
||||
* Fixed. A list of values is provided, and a new simulation is run for each possible value. If more than a variable is given, a new simulation will be run per combination of values.
|
||||
* Bounded/Sampled. The bounds of the variable are provided, along with a sampler method, which will be used to compute all the configuration combinations.
|
||||
|
||||
When fixed and bounded variables are mixed, Soil generates a new configuration per combination of fixed values and bounded values.
|
||||
|
||||
Here is an example with a single fixed variable and two bounded variable:
|
||||
|
||||
.. literalinclude:: ../examples/template.yml
|
||||
:language: yaml
|
@ -0,0 +1,22 @@
|
||||
Mesa compatibility
|
||||
------------------
|
||||
|
||||
Soil is in the process of becoming fully compatible with MESA.
|
||||
The idea is to provide a set of modular classes and functions that extend the functionality of mesa, whilst staying compatible.
|
||||
In the end, it should be possible to add regular mesa agents to a soil simulation, or use a soil agent within a mesa simulation/model.
|
||||
|
||||
This is a non-exhaustive list of tasks to achieve compatibility:
|
||||
|
||||
- [ ] Integrate `soil.Simulation` with mesa's runners:
|
||||
- [ ] `soil.Simulation` could mimic/become a `mesa.batchrunner`
|
||||
- [ ] Integrate `soil.Environment` with `mesa.Model`:
|
||||
- [x] `Soil.Environment` inherits from `mesa.Model`
|
||||
- [x] `Soil.Environment` includes a Mesa-like Scheduler (see the `soil.time` module.
|
||||
- [ ] Allow for `mesa.Model` to be used in a simulation.
|
||||
- [ ] Integrate `soil.Agent` with `mesa.Agent`:
|
||||
- [x] Rename agent.id to unique_id?
|
||||
- [x] mesa agents can be used in soil simulations (see `examples/mesa`)
|
||||
- [ ] Provide examples
|
||||
- [ ] Using mesa modules in a soil simulation
|
||||
- [ ] Using soil modules in a mesa simulation
|
||||
- [ ] Document the new APIs and usage
|
@ -0,0 +1,35 @@
|
||||
What are the main changes in version 1.0?
|
||||
#########################################
|
||||
|
||||
Version 1.0 is a major rewrite of the Soil system, focused on simplifying the API, aligning it with Mesa, and making it easier to use.
|
||||
Unfortunately, this comes at the cost of backwards compatibility.
|
||||
|
||||
We drew several lessons from the previous version of Soil, and tried to address them in this version.
|
||||
Mainly:
|
||||
|
||||
- The split between simulation configuration and simulation code was overly complicated for most use cases. As a result, most users ended up reusing configuration.
|
||||
- Storing **all** the simulation data in a database is costly and unnecessary for most use cases. For most use cases, only a handful of variables need to be stored. This fits nicely with Mesa's data collection system.
|
||||
- The API was too complex, and it was difficult to understand how to use it.
|
||||
- Most parts of the API were not aligned with Mesa, which made it difficult to use Mesa's features or to integrate Soil modules with Mesa code, especially for newcomers.
|
||||
- Many parts of the API were tightly coupled, which made it difficult to find bugs, test the system and add new features.
|
||||
|
||||
The 0.30 rewrite should provide a middle ground between Soil's opinionated approach and Mesa's flexibility.
|
||||
The new Soil is less configuration-centric.
|
||||
It aims to provide more modular and convenient functions, most of which can be used in vanilla Mesa.
|
||||
|
||||
How are agents assigned to nodes in the network
|
||||
###############################################
|
||||
|
||||
The constructor of the `NetworkAgent` class has two arguments: `node_id` and `topology`.
|
||||
If `topology` is not provided, it will default to `self.model.topology`.
|
||||
This assignment might err if the model does not have a `topology` attribute, but most Soil environments derive from `NetworkEnvironment`, so they include a topology by default.
|
||||
If `node_id` is not provided, a random node will be selected from the topology, until a node with no agent is found.
|
||||
Then, the `node_id` of that node is assigned to the agent.
|
||||
If no node with no agent is found, a new node is automatically added to the topology.
|
||||
|
||||
|
||||
Can Soil environments include more than one network / topology?
|
||||
###############################################################
|
||||
|
||||
Yes, but each network has to be included manually.
|
||||
Somewhere between 0.20 and 0.30 we included the ability to include multiple networks, but it was deemed too complex and was removed.
|
Before Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 23 KiB |
@ -1,30 +0,0 @@
|
||||
---
|
||||
name: quickstart
|
||||
num_trials: 1
|
||||
max_time: 1000
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
state:
|
||||
id: neutral
|
||||
weight: 1
|
||||
- agent_type: SISaModel
|
||||
state:
|
||||
id: content
|
||||
weight: 2
|
||||
network_params:
|
||||
n: 100
|
||||
k: 5
|
||||
p: 0.2
|
||||
generator: newman_watts_strogatz_graph
|
||||
environment_params:
|
||||
neutral_discontent_spon_prob: 0.05
|
||||
neutral_discontent_infected_prob: 0.1
|
||||
neutral_content_spon_prob: 0.2
|
||||
neutral_content_infected_prob: 0.4
|
||||
discontent_neutral: 0.2
|
||||
discontent_content: 0.05
|
||||
content_discontent: 0.05
|
||||
variance_d_c: 0.05
|
||||
variance_c_d: 0.1
|
||||
content_neutral: 0.1
|
||||
standard_variance: 0.1
|
@ -1 +1 @@
|
||||
ipython==7.31.1
|
||||
ipython>=7.31.1
|
||||
|
@ -0,0 +1,12 @@
|
||||
### MESA
|
||||
|
||||
Starting with version 0.3, Soil has been redesigned to complement Mesa, while remaining compatible with it.
|
||||
That means that every component in Soil (i.e., Models, Environments, etc.) can be mixed with existing mesa components.
|
||||
In fact, there are examples that show how that integration may be used, in the `examples/mesa` folder in the repository.
|
||||
|
||||
Here are some reasons to use Soil instead of plain mesa:
|
||||
|
||||
- Less boilerplate for common scenarios (by some definitions of common)
|
||||
- Functions to automatically populate a topology with an agent distribution (i.e., different ratios of agent class and state)
|
||||
- The `soil.Simulation` class allows you to run multiple instances of the same experiment (i.e., multiple trials with the same parameters but a different randomness seed)
|
||||
- Reporting functions that aggregate multiple
|
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: simple
|
||||
group: tests
|
||||
dir_path: "/tmp/"
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: "CompleteSeed!"
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: 1
|
||||
state:
|
||||
state_id: 0
|
||||
- agent_type: AggregatedCounter
|
||||
weight: 0.2
|
||||
environment_agents: []
|
||||
environment_class: Environment
|
||||
environment_params:
|
||||
am_i_complete: true
|
||||
default_state:
|
||||
incidents: 0
|
||||
states:
|
||||
- name: 'The first node'
|
||||
- name: 'The second node'
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
name: custom-generator
|
||||
description: Using a custom generator for the network
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
network_params:
|
||||
generator: mymodule.mygenerator
|
||||
# These are custom parameters
|
||||
n: 10
|
||||
n_edges: 5
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: 1
|
||||
state:
|
||||
state_id: 0
|
@ -0,0 +1,39 @@
|
||||
from networkx import Graph
|
||||
import random
|
||||
import networkx as nx
|
||||
from soil import Simulation, Environment, CounterModel, parameters
|
||||
|
||||
|
||||
def mygenerator(n=5, n_edges=5):
|
||||
"""
|
||||
Just a simple generator that creates a network with n nodes and
|
||||
n_edges edges. Edges are assigned randomly, only avoiding self loops.
|
||||
"""
|
||||
G = nx.Graph()
|
||||
|
||||
for i in range(n):
|
||||
G.add_node(i)
|
||||
|
||||
for i in range(n_edges):
|
||||
nodes = list(G.nodes)
|
||||
n_in = random.choice(nodes)
|
||||
nodes.remove(n_in) # Avoid loops
|
||||
n_out = random.choice(nodes)
|
||||
G.add_edge(n_in, n_out)
|
||||
return G
|
||||
|
||||
|
||||
class GeneratorEnv(Environment):
|
||||
"""Using a custom generator for the network"""
|
||||
|
||||
generator: parameters.function = staticmethod(mygenerator)
|
||||
|
||||
def init(self):
|
||||
self.create_network(generator=self.generator, n=10, n_edges=5)
|
||||
self.add_agents(CounterModel)
|
||||
|
||||
|
||||
sim = Simulation(model=GeneratorEnv, max_steps=10, interval=1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
sim.run(dump=False)
|
@ -1,27 +0,0 @@
|
||||
from networkx import Graph
|
||||
import networkx as nx
|
||||
from random import choice
|
||||
|
||||
def mygenerator(n=5, n_edges=5):
|
||||
'''
|
||||
Just a simple generator that creates a network with n nodes and
|
||||
n_edges edges. Edges are assigned randomly, only avoiding self loops.
|
||||
'''
|
||||
G = nx.Graph()
|
||||
|
||||
for i in range(n):
|
||||
G.add_node(i)
|
||||
|
||||
for i in range(n_edges):
|
||||
nodes = list(G.nodes)
|
||||
n_in = choice(nodes)
|
||||
nodes.remove(n_in) # Avoid loops
|
||||
n_out = choice(nodes)
|
||||
G.add_edge(n_in, n_out)
|
||||
return G
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
|
||||
|
||||
class Fibonacci(FSM):
|
||||
'''Agent that only executes in t_steps that are Fibonacci numbers'''
|
||||
|
||||
defaults = {
|
||||
'prev': 1
|
||||
}
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def counting(self):
|
||||
self.log('Stopping at {}'.format(self.now))
|
||||
prev, self['prev'] = self['prev'], max([self.now, self['prev']])
|
||||
return None, self.env.timeout(prev)
|
||||
|
||||
class Odds(FSM):
|
||||
'''Agent that only executes in odd t_steps'''
|
||||
@default_state
|
||||
@state
|
||||
def odds(self):
|
||||
self.log('Stopping at {}'.format(self.now))
|
||||
return None, self.env.timeout(1+self.now%2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
from soil import Simulation
|
||||
s = Simulation(network_agents=[{'ids': [0], 'agent_type': Fibonacci},
|
||||
{'ids': [1], 'agent_type': Odds}],
|
||||
network_params={"generator": "complete_graph", "n": 2},
|
||||
max_time=100,
|
||||
)
|
||||
s.run(dry_run=True)
|
@ -0,0 +1,41 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
from soil.time import Delta
|
||||
|
||||
|
||||
class Fibonacci(FSM):
|
||||
"""Agent that only executes in t_steps that are Fibonacci numbers"""
|
||||
prev = 1
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def counting(self):
|
||||
self.log("Stopping at {}".format(self.now))
|
||||
prev, self["prev"] = self["prev"], max([self.now, self["prev"]])
|
||||
return None, Delta(prev)
|
||||
|
||||
|
||||
class Odds(FSM):
|
||||
"""Agent that only executes in odd t_steps"""
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def odds(self):
|
||||
self.log("Stopping at {}".format(self.now))
|
||||
return None, Delta(1 + self.now % 2)
|
||||
|
||||
|
||||
from soil import Environment, Simulation
|
||||
from networkx import complete_graph
|
||||
|
||||
|
||||
class TimeoutsEnv(Environment):
|
||||
def init(self):
|
||||
self.create_network(generator=complete_graph, n=2)
|
||||
self.add_agent(agent_class=Fibonacci, node_id=0)
|
||||
self.add_agent(agent_class=Odds, node_id=1)
|
||||
|
||||
|
||||
sim = Simulation(model=TimeoutsEnv, max_steps=10, interval=1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run(dump=False)
|
@ -0,0 +1,9 @@
|
||||
This example can be run like with command-line options, like this:
|
||||
|
||||
```bash
|
||||
python cars.py --level DEBUG -e summary --csv
|
||||
#or
|
||||
soil cars.py -e summary
|
||||
```
|
||||
|
||||
This will set the `CSV` (save the agent and model data to a CSV) and `summary` (print the a summary of the data to stdout) exporters, and set the log level to DEBUG.
|
@ -0,0 +1,231 @@
|
||||
"""
|
||||
This is an example of a simplified city, where there are Passengers and Drivers that can take those passengers
|
||||
from their location to their desired location.
|
||||
|
||||
An example scenario could play like the following:
|
||||
|
||||
- Drivers start in the `wandering` state, where they wander around the city until they have been assigned a journey
|
||||
- Passenger(1) tells every driver that it wants to request a Journey.
|
||||
- Each driver receives the request.
|
||||
If Driver(2) is interested in providing the Journey, it asks Passenger(1) to confirm that it accepts Driver(2)'s request
|
||||
- When Passenger(1) accepts the request, two things happen:
|
||||
- Passenger(1) changes its state to `driving_home`
|
||||
- Driver(2) starts moving towards the origin of the Journey
|
||||
- Once Driver(2) reaches the origin, it starts moving itself and Passenger(1) to the destination of the Journey
|
||||
- When Driver(2) reaches the destination (carrying Passenger(1) along):
|
||||
- Driver(2) starts wondering again
|
||||
- Passenger(1) dies, and is removed from the simulation
|
||||
- If there are no more passengers available in the simulation, Drivers die
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
from soil import *
|
||||
from soil import events
|
||||
from mesa.space import MultiGrid
|
||||
|
||||
|
||||
# More complex scenarios may use more than one type of message between objects.
|
||||
# A common pattern is to use `enum.Enum` to represent state changes in a request.
|
||||
@dataclass
|
||||
class Journey:
|
||||
"""
|
||||
This represents a request for a journey. Passengers and drivers exchange this object.
|
||||
|
||||
A journey may have a driver assigned or not. If the driver has not been assigned, this
|
||||
object is considered a "request for a journey".
|
||||
"""
|
||||
|
||||
origin: (int, int)
|
||||
destination: (int, int)
|
||||
tip: float
|
||||
|
||||
passenger: Passenger
|
||||
driver: Optional[Driver] = None
|
||||
|
||||
|
||||
class City(EventedEnvironment):
|
||||
"""
|
||||
An environment with a grid where drivers and passengers will be placed.
|
||||
|
||||
The number of drivers and riders is configurable through its parameters:
|
||||
|
||||
:param str n_cars: The total number of drivers to add
|
||||
:param str n_passengers: The number of passengers in the simulation
|
||||
:param list agents: Specific agents to use in the simulation. It overrides the `n_passengers`
|
||||
and `n_cars` params.
|
||||
:param int height: Height of the internal grid
|
||||
:param int width: Width of the internal grid
|
||||
"""
|
||||
n_cars = 1
|
||||
n_passengers = 10
|
||||
height = 100
|
||||
width = 100
|
||||
|
||||
def init(self):
|
||||
self.grid = MultiGrid(width=self.width, height=self.height, torus=False)
|
||||
if not self.agents:
|
||||
self.add_agents(Driver, k=self.n_cars)
|
||||
self.add_agents(Passenger, k=self.n_passengers)
|
||||
|
||||
for agent in self.agents:
|
||||
self.grid.place_agent(agent, (0, 0))
|
||||
self.grid.move_to_empty(agent)
|
||||
|
||||
self.total_earnings = 0
|
||||
self.add_model_reporter("total_earnings")
|
||||
|
||||
@report
|
||||
@property
|
||||
def number_passengers(self):
|
||||
return self.count_agents(agent_class=Passenger)
|
||||
|
||||
|
||||
class Driver(Evented, FSM):
|
||||
pos = None
|
||||
journey = None
|
||||
earnings = 0
|
||||
|
||||
def on_receive(self, msg, sender):
|
||||
"""This is not a state. It will run (and block) every time check_messages is invoked"""
|
||||
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
|
||||
msg.driver = self
|
||||
self.journey = msg
|
||||
|
||||
def check_passengers(self):
|
||||
"""If there are no more passengers, stop forever"""
|
||||
c = self.count_agents(agent_class=Passenger)
|
||||
self.debug(f"Passengers left {c}")
|
||||
if not c:
|
||||
self.die("No more passengers")
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def wandering(self):
|
||||
"""Move around the city until a journey is accepted"""
|
||||
target = None
|
||||
self.check_passengers()
|
||||
self.journey = None
|
||||
while self.journey is None: # No potential journeys detected (see on_receive)
|
||||
if target is None or not self.move_towards(target):
|
||||
target = self.random.choice(
|
||||
self.model.grid.get_neighborhood(self.pos, moore=False)
|
||||
)
|
||||
|
||||
self.check_passengers()
|
||||
# This will call on_receive behind the scenes, and the agent's status will be updated
|
||||
self.check_messages()
|
||||
yield Delta(30) # Wait at least 30 seconds before checking again
|
||||
|
||||
try:
|
||||
# Re-send the journey to the passenger, to confirm that we have been selected
|
||||
self.journey = yield self.journey.passenger.ask(self.journey, timeout=60)
|
||||
except events.TimedOut:
|
||||
# No journey has been accepted. Try again
|
||||
self.journey = None
|
||||
return
|
||||
|
||||
return self.driving
|
||||
|
||||
@state
|
||||
def driving(self):
|
||||
"""The journey has been accepted. Pick them up and take them to their destination"""
|
||||
self.info(f"Driving towards Passenger {self.journey.passenger.unique_id}")
|
||||
while self.move_towards(self.journey.origin):
|
||||
yield
|
||||
self.info(f"Driving {self.journey.passenger.unique_id} from {self.journey.origin} to {self.journey.destination}")
|
||||
while self.move_towards(self.journey.destination, with_passenger=True):
|
||||
yield
|
||||
self.info("Arrived at destination")
|
||||
self.earnings += self.journey.tip
|
||||
self.model.total_earnings += self.journey.tip
|
||||
self.check_passengers()
|
||||
return self.wandering
|
||||
|
||||
def move_towards(self, target, with_passenger=False):
|
||||
"""Move one cell at a time towards a target"""
|
||||
self.debug(f"Moving { self.pos } -> { target }")
|
||||
if target[0] == self.pos[0] and target[1] == self.pos[1]:
|
||||
return False
|
||||
|
||||
next_pos = [self.pos[0], self.pos[1]]
|
||||
for idx in [0, 1]:
|
||||
if self.pos[idx] < target[idx]:
|
||||
next_pos[idx] += 1
|
||||
break
|
||||
if self.pos[idx] > target[idx]:
|
||||
next_pos[idx] -= 1
|
||||
break
|
||||
self.model.grid.move_agent(self, tuple(next_pos))
|
||||
if with_passenger:
|
||||
self.journey.passenger.pos = (
|
||||
self.pos
|
||||
) # This could be communicated through messages
|
||||
return True
|
||||
|
||||
|
||||
class Passenger(Evented, FSM):
|
||||
pos = None
|
||||
|
||||
def on_receive(self, msg, sender):
|
||||
"""This is not a state. It will be run synchronously every time `check_messages` is run"""
|
||||
|
||||
if isinstance(msg, Journey):
|
||||
self.journey = msg
|
||||
return msg
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def asking(self):
|
||||
destination = (
|
||||
self.random.randint(0, self.model.grid.height-1),
|
||||
self.random.randint(0, self.model.grid.width-1),
|
||||
)
|
||||
self.journey = None
|
||||
journey = Journey(
|
||||
origin=self.pos,
|
||||
destination=destination,
|
||||
tip=self.random.randint(10, 100),
|
||||
passenger=self,
|
||||
)
|
||||
|
||||
timeout = 60
|
||||
expiration = self.now + timeout
|
||||
self.info(f"Asking for journey at: { self.pos }")
|
||||
self.model.broadcast(journey, ttl=timeout, sender=self, agent_class=Driver)
|
||||
while not self.journey:
|
||||
self.debug(f"Waiting for responses at: { self.pos }")
|
||||
try:
|
||||
# This will call check_messages behind the scenes, and the agent's status will be updated
|
||||
# If you want to avoid that, you can call it with: check=False
|
||||
yield self.received(expiration=expiration)
|
||||
except events.TimedOut:
|
||||
self.info(f"Still no response. Waiting at: { self.pos }")
|
||||
self.model.broadcast(
|
||||
journey, ttl=timeout, sender=self, agent_class=Driver
|
||||
)
|
||||
expiration = self.now + timeout
|
||||
self.info(f"Got a response! Waiting for driver")
|
||||
return self.driving_home
|
||||
|
||||
@state
|
||||
def driving_home(self):
|
||||
while (
|
||||
self.pos[0] != self.journey.destination[0]
|
||||
or self.pos[1] != self.journey.destination[1]
|
||||
):
|
||||
try:
|
||||
yield self.received(timeout=60)
|
||||
except events.TimedOut:
|
||||
pass
|
||||
|
||||
self.die("Got home safe!")
|
||||
|
||||
|
||||
simulation = Simulation(name="RideHailing",
|
||||
model=City,
|
||||
seed="carsSeed",
|
||||
max_time=1000,
|
||||
parameters=dict(n_passengers=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
easy(simulation)
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
name: mesa_sim
|
||||
group: tests
|
||||
dir_path: "/tmp"
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: '1'
|
||||
network_params:
|
||||
generator: social_wealth.graph_generator
|
||||
n: 5
|
||||
network_agents:
|
||||
- agent_type: social_wealth.SocialMoneyAgent
|
||||
weight: 1
|
||||
environment_class: social_wealth.MoneyEnv
|
||||
environment_params:
|
||||
num_mesa_agents: 5
|
||||
mesa_agent_type: social_wealth.MoneyAgent
|
||||
N: 10
|
||||
width: 50
|
||||
height: 50
|
@ -0,0 +1,7 @@
|
||||
from soil import Simulation
|
||||
from social_wealth import MoneyEnv, graph_generator
|
||||
|
||||
sim = Simulation(name="mesa_sim", dump=False, max_steps=10, interval=2, model=MoneyEnv, parameters=dict(generator=graph_generator, N=10, width=50, height=50))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run()
|
@ -1,138 +0,0 @@
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
interval: 1
|
||||
max_time: 300
|
||||
name: Sim_all_dumb
|
||||
network_agents:
|
||||
- agent_type: DumbViewer
|
||||
state:
|
||||
has_tv: false
|
||||
weight: 1
|
||||
- agent_type: DumbViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 500
|
||||
m: 5
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
interval: 1
|
||||
max_time: 300
|
||||
name: Sim_half_herd
|
||||
network_agents:
|
||||
- agent_type: DumbViewer
|
||||
state:
|
||||
has_tv: false
|
||||
weight: 1
|
||||
- agent_type: DumbViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
- agent_type: HerdViewer
|
||||
state:
|
||||
has_tv: false
|
||||
weight: 1
|
||||
- agent_type: HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 500
|
||||
m: 5
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
interval: 1
|
||||
max_time: 300
|
||||
name: Sim_all_herd
|
||||
network_agents:
|
||||
- agent_type: HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
- agent_type: HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 500
|
||||
m: 5
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
prob_neighbor_cure: 0.1
|
||||
interval: 1
|
||||
max_time: 300
|
||||
name: Sim_wise_herd
|
||||
network_agents:
|
||||
- agent_type: HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
- agent_type: WiseViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 500
|
||||
m: 5
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
prob_neighbor_cure: 0.1
|
||||
interval: 1
|
||||
max_time: 300
|
||||
name: Sim_all_wise
|
||||
network_agents:
|
||||
- agent_type: WiseViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
- agent_type: WiseViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 500
|
||||
m: 5
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 500
|
||||
m: 5
|
||||
num_trials: 50
|
@ -1,86 +0,0 @@
|
||||
from soil.agents import FSM, state, default_state, prob
|
||||
import logging
|
||||
|
||||
|
||||
class DumbViewer(FSM):
|
||||
'''
|
||||
A viewer that gets infected via TV (if it has one) and tries to infect
|
||||
its neighbors once it's infected.
|
||||
'''
|
||||
defaults = {
|
||||
'prob_neighbor_spread': 0.5,
|
||||
'prob_tv_spread': 0.1,
|
||||
}
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def neutral(self):
|
||||
if self['has_tv']:
|
||||
if prob(self.env['prob_tv_spread']):
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
for neighbor in self.get_neighboring_agents(state_id=self.neutral.id):
|
||||
if prob(self.env['prob_neighbor_spread']):
|
||||
neighbor.infect()
|
||||
|
||||
def infect(self):
|
||||
'''
|
||||
This is not a state. It is a function that other agents can use to try to
|
||||
infect this agent. DumbViewer always gets infected, but other agents like
|
||||
HerdViewer might not become infected right away
|
||||
'''
|
||||
|
||||
self.set_state(self.infected)
|
||||
|
||||
|
||||
class HerdViewer(DumbViewer):
|
||||
'''
|
||||
A viewer whose probability of infection depends on the state of its neighbors.
|
||||
'''
|
||||
|
||||
def infect(self):
|
||||
'''Notice again that this is NOT a state. See DumbViewer.infect for reference'''
|
||||
infected = self.count_neighboring_agents(state_id=self.infected.id)
|
||||
total = self.count_neighboring_agents()
|
||||
prob_infect = self.env['prob_neighbor_spread'] * infected/total
|
||||
self.debug('prob_infect', prob_infect)
|
||||
if prob(prob_infect):
|
||||
self.set_state(self.infected)
|
||||
|
||||
|
||||
class WiseViewer(HerdViewer):
|
||||
'''
|
||||
A viewer that can change its mind.
|
||||
'''
|
||||
|
||||
defaults = {
|
||||
'prob_neighbor_spread': 0.5,
|
||||
'prob_neighbor_cure': 0.25,
|
||||
'prob_tv_spread': 0.1,
|
||||
}
|
||||
|
||||
@state
|
||||
def cured(self):
|
||||
prob_cure = self.env['prob_neighbor_cure']
|
||||
for neighbor in self.get_neighboring_agents(state_id=self.infected.id):
|
||||
if prob(prob_cure):
|
||||
try:
|
||||
neighbor.cure()
|
||||
except AttributeError:
|
||||
self.debug('Viewer {} cannot be cured'.format(neighbor.id))
|
||||
|
||||
def cure(self):
|
||||
self.set_state(self.cured.id)
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
cured = max(self.count_neighboring_agents(self.cured.id),
|
||||
1.0)
|
||||
infected = max(self.count_neighboring_agents(self.infected.id),
|
||||
1.0)
|
||||
prob_cure = self.env['prob_neighbor_cure'] * (cured/infected)
|
||||
if prob(prob_cure):
|
||||
return self.cured
|
||||
return self.set_state(super().infected)
|
@ -0,0 +1,134 @@
|
||||
from soil.agents import FSM, NetworkAgent, state, default_state, prob
|
||||
from soil.parameters import *
|
||||
import logging
|
||||
|
||||
from soil.environment import Environment
|
||||
|
||||
|
||||
class DumbViewer(FSM, NetworkAgent):
|
||||
"""
|
||||
A viewer that gets infected via TV (if it has one) and tries to infect
|
||||
its neighbors once it's infected.
|
||||
"""
|
||||
|
||||
has_been_infected: bool = False
|
||||
has_tv: bool = False
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def neutral(self):
|
||||
if self.has_tv:
|
||||
if self.prob(self.get("prob_tv_spread")):
|
||||
return self.infected
|
||||
if self.has_been_infected:
|
||||
return self.infected
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
for neighbor in self.get_neighbors(state_id=self.neutral.id):
|
||||
if self.prob(self.get("prob_neighbor_spread")):
|
||||
neighbor.infect()
|
||||
|
||||
def infect(self):
|
||||
"""
|
||||
This is not a state. It is a function that other agents can use to try to
|
||||
infect this agent. DumbViewer always gets infected, but other agents like
|
||||
HerdViewer might not become infected right away
|
||||
"""
|
||||
self.has_been_infected = True
|
||||
|
||||
|
||||
class HerdViewer(DumbViewer):
|
||||
"""
|
||||
A viewer whose probability of infection depends on the state of its neighbors.
|
||||
"""
|
||||
|
||||
def infect(self):
|
||||
"""Notice again that this is NOT a state. See DumbViewer.infect for reference"""
|
||||
infected = self.count_neighbors(state_id=self.infected.id)
|
||||
total = self.count_neighbors()
|
||||
prob_infect = self.get("prob_neighbor_spread") * infected / total
|
||||
self.debug("prob_infect", prob_infect)
|
||||
if self.prob(prob_infect):
|
||||
self.has_been_infected = True
|
||||
|
||||
|
||||
class WiseViewer(HerdViewer):
|
||||
"""
|
||||
A viewer that can change its mind.
|
||||
"""
|
||||
|
||||
@state
|
||||
def cured(self):
|
||||
prob_cure = self.get("prob_neighbor_cure")
|
||||
for neighbor in self.get_neighbors(state_id=self.infected.id):
|
||||
if self.prob(prob_cure):
|
||||
try:
|
||||
neighbor.cure()
|
||||
except AttributeError:
|
||||
self.debug("Viewer {} cannot be cured".format(neighbor.id))
|
||||
|
||||
def cure(self):
|
||||
self.has_been_cured = True
|
||||
|
||||
@state
|
||||
def infected(self):
|
||||
if self.has_been_cured:
|
||||
return self.cured
|
||||
cured = max(self.count_neighbors(self.cured.id), 1.0)
|
||||
infected = max(self.count_neighbors(self.infected.id), 1.0)
|
||||
prob_cure = self.get("prob_neighbor_cure") * (cured / infected)
|
||||
if self.prob(prob_cure):
|
||||
return self.cured
|
||||
|
||||
|
||||
class NewsSpread(Environment):
|
||||
ratio_dumb: probability = 1,
|
||||
ratio_herd: probability = 0,
|
||||
ratio_wise: probability = 0,
|
||||
prob_tv_spread: probability = 0.1,
|
||||
prob_neighbor_spread: probability = 0.1,
|
||||
prob_neighbor_cure: probability = 0.05,
|
||||
|
||||
def init(self):
|
||||
self.populate_network([DumbViewer, HerdViewer, WiseViewer],
|
||||
[self.ratio_dumb, self.ratio_herd, self.ratio_wise])
|
||||
|
||||
|
||||
from itertools import product
|
||||
from soil import Simulation
|
||||
|
||||
|
||||
# We want to investigate the effect of different agent distributions on the spread of news.
|
||||
# To do that, we will run different simulations, with a varying ratio of DumbViewers, HerdViewers, and WiseViewers
|
||||
# Because the effect of these agents might also depend on the network structure, we will run our simulations on two different networks:
|
||||
# one with a small-world structure and one with a connected structure.
|
||||
|
||||
counter = 0
|
||||
for [r1, r2] in product([0, 0.5, 1.0], repeat=2):
|
||||
for (generator, netparams) in {
|
||||
"barabasi_albert_graph": {"m": 5},
|
||||
"erdos_renyi_graph": {"p": 0.1},
|
||||
}.items():
|
||||
print(r1, r2, 1-r1-r2, generator)
|
||||
# Create new simulation
|
||||
netparams["n"] = 500
|
||||
Simulation(
|
||||
name='newspread_sim',
|
||||
model=NewsSpread,
|
||||
parameters=dict(
|
||||
ratio_dumb=r1,
|
||||
ratio_herd=r2,
|
||||
ratio_wise=1-r1-r2,
|
||||
network_generator=generator,
|
||||
network_params=netparams,
|
||||
prob_neighbor_spread=0,
|
||||
),
|
||||
iterations=5,
|
||||
max_steps=300,
|
||||
dump=False,
|
||||
).run()
|
||||
counter += 1
|
||||
# Run all the necessary instances
|
||||
|
||||
print(f"A total of {counter} simulations were run.")
|
@ -1,40 +0,0 @@
|
||||
'''
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
'''
|
||||
from soil import Simulation, agents
|
||||
from networkx import Graph
|
||||
import logging
|
||||
|
||||
|
||||
def mygenerator():
|
||||
# Add only a node
|
||||
G = Graph()
|
||||
G.add_node(1)
|
||||
return G
|
||||
|
||||
|
||||
class MyAgent(agents.FSM):
|
||||
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def neutral(self):
|
||||
self.debug('I am running')
|
||||
if agents.prob(0.2):
|
||||
self.info('This runs 2/10 times on average')
|
||||
|
||||
|
||||
s = Simulation(name='Programmatic',
|
||||
network_params={'generator': mygenerator},
|
||||
num_trials=1,
|
||||
max_time=100,
|
||||
agent_type=MyAgent,
|
||||
dry_run=True)
|
||||
|
||||
|
||||
# By default, logging will only print WARNING logs (and above).
|
||||
# You need to choose a lower logging level to get INFO/DEBUG traces
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
envs = s.run()
|
||||
|
||||
# Uncomment this to output the simulation to a YAML file
|
||||
# s.dump_yaml('simulation.yaml')
|
@ -0,0 +1,53 @@
|
||||
"""
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
"""
|
||||
from soil import Simulation, Environment, agents
|
||||
from networkx import Graph
|
||||
import logging
|
||||
|
||||
|
||||
def mygenerator():
|
||||
# Add only a node
|
||||
G = Graph()
|
||||
G.add_node(1)
|
||||
G.add_node(2)
|
||||
return G
|
||||
|
||||
|
||||
class MyAgent(agents.NetworkAgent, agents.FSM):
|
||||
times_run = 0
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def neutral(self):
|
||||
self.debug("I am running")
|
||||
if self.prob(0.2):
|
||||
self.times_run += 1
|
||||
self.info("This runs 2/10 times on average")
|
||||
|
||||
|
||||
class ProgrammaticEnv(Environment):
|
||||
|
||||
def init(self):
|
||||
self.create_network(generator=mygenerator)
|
||||
assert len(self.G)
|
||||
self.populate_network(agent_class=MyAgent)
|
||||
self.add_agent_reporter('times_run')
|
||||
|
||||
|
||||
simulation = Simulation(
|
||||
name="Programmatic",
|
||||
model=ProgrammaticEnv,
|
||||
seed='Program',
|
||||
iterations=1,
|
||||
max_time=100,
|
||||
dump=False,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# By default, logging will only print WARNING logs (and above).
|
||||
# You need to choose a lower logging level to get INFO/DEBUG traces
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
envs = simulation.run()
|
||||
|
||||
for agent in envs[0].agents:
|
||||
print(agent.times_run)
|
@ -1,175 +0,0 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
from soil import Environment
|
||||
from random import random, shuffle
|
||||
from itertools import islice
|
||||
import logging
|
||||
|
||||
|
||||
class CityPubs(Environment):
|
||||
'''Environment with Pubs'''
|
||||
level = logging.INFO
|
||||
|
||||
def __init__(self, *args, number_of_pubs=3, pub_capacity=10, **kwargs):
|
||||
super(CityPubs, self).__init__(*args, **kwargs)
|
||||
pubs = {}
|
||||
for i in range(number_of_pubs):
|
||||
newpub = {
|
||||
'name': 'The awesome pub #{}'.format(i),
|
||||
'open': True,
|
||||
'capacity': pub_capacity,
|
||||
'occupancy': 0,
|
||||
}
|
||||
pubs[newpub['name']] = newpub
|
||||
self['pubs'] = pubs
|
||||
|
||||
def enter(self, pub_id, *nodes):
|
||||
'''Agents will try to enter. The pub checks if it is possible'''
|
||||
try:
|
||||
pub = self['pubs'][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError('Pub {} is not available'.format(pub_id))
|
||||
if not pub['open'] or (pub['capacity'] < (len(nodes) + pub['occupancy'])):
|
||||
return False
|
||||
pub['occupancy'] += len(nodes)
|
||||
for node in nodes:
|
||||
node['pub'] = pub_id
|
||||
return True
|
||||
|
||||
def available_pubs(self):
|
||||
for pub in self['pubs'].values():
|
||||
if pub['open'] and (pub['occupancy'] < pub['capacity']):
|
||||
yield pub['name']
|
||||
|
||||
def exit(self, pub_id, *node_ids):
|
||||
'''Agents will notify the pub they want to leave'''
|
||||
try:
|
||||
pub = self['pubs'][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError('Pub {} is not available'.format(pub_id))
|
||||
for node_id in node_ids:
|
||||
node = self.get_agent(node_id)
|
||||
if pub_id == node['pub']:
|
||||
del node['pub']
|
||||
pub['occupancy'] -= 1
|
||||
|
||||
|
||||
class Patron(FSM):
|
||||
'''Agent that looks for friends to drink with. It will do three things:
|
||||
1) Look for other patrons to drink with
|
||||
2) Look for a bar where the agent and other agents in the same group can get in.
|
||||
3) While in the bar, patrons only drink, until they get drunk and taken home.
|
||||
'''
|
||||
level = logging.DEBUG
|
||||
|
||||
defaults = {
|
||||
'pub': None,
|
||||
'drunk': False,
|
||||
'pints': 0,
|
||||
'max_pints': 3,
|
||||
}
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def looking_for_friends(self):
|
||||
'''Look for friends to drink with'''
|
||||
self.info('I am looking for friends')
|
||||
available_friends = list(self.get_agents(drunk=False,
|
||||
pub=None,
|
||||
state_id=self.looking_for_friends.id))
|
||||
if not available_friends:
|
||||
self.info('Life sucks and I\'m alone!')
|
||||
return self.at_home
|
||||
befriended = self.try_friends(available_friends)
|
||||
if befriended:
|
||||
return self.looking_for_pub
|
||||
|
||||
@state
|
||||
def looking_for_pub(self):
|
||||
'''Look for a pub that accepts me and my friends'''
|
||||
if self['pub'] != None:
|
||||
return self.sober_in_pub
|
||||
self.debug('I am looking for a pub')
|
||||
group = list(self.get_neighboring_agents())
|
||||
for pub in self.env.available_pubs():
|
||||
self.debug('We\'re trying to get into {}: total: {}'.format(pub, len(group)))
|
||||
if self.env.enter(pub, self, *group):
|
||||
self.info('We\'re all {} getting in {}!'.format(len(group), pub))
|
||||
return self.sober_in_pub
|
||||
|
||||
@state
|
||||
def sober_in_pub(self):
|
||||
'''Drink up.'''
|
||||
self.drink()
|
||||
if self['pints'] > self['max_pints']:
|
||||
return self.drunk_in_pub
|
||||
|
||||
@state
|
||||
def drunk_in_pub(self):
|
||||
'''I'm out. Take me home!'''
|
||||
self.info('I\'m so drunk. Take me home!')
|
||||
self['drunk'] = True
|
||||
pass # out drunk
|
||||
|
||||
@state
|
||||
def at_home(self):
|
||||
'''The end'''
|
||||
others = self.get_agents(state_id=Patron.at_home.id, limit_neighbors=True)
|
||||
self.debug('I\'m home. Just like {} of my friends'.format(len(others)))
|
||||
|
||||
def drink(self):
|
||||
self['pints'] += 1
|
||||
self.debug('Cheers to that')
|
||||
|
||||
def kick_out(self):
|
||||
self.set_state(self.at_home)
|
||||
|
||||
def befriend(self, other_agent, force=False):
|
||||
'''
|
||||
Try to become friends with another agent. The chances of
|
||||
success depend on both agents' openness.
|
||||
'''
|
||||
if force or self['openness'] > random():
|
||||
self.env.add_edge(self, other_agent)
|
||||
self.info('Made some friend {}'.format(other_agent))
|
||||
return True
|
||||
return False
|
||||
|
||||
def try_friends(self, others):
|
||||
''' Look for random agents around me and try to befriend them'''
|
||||
befriended = False
|
||||
k = int(10*self['openness'])
|
||||
shuffle(others)
|
||||
for friend in islice(others, k): # random.choice >= 3.7
|
||||
if friend == self:
|
||||
continue
|
||||
if friend.befriend(self):
|
||||
self.befriend(friend, force=True)
|
||||
self.debug('Hooray! new friend: {}'.format(friend.id))
|
||||
befriended = True
|
||||
else:
|
||||
self.debug('{} does not want to be friends'.format(friend.id))
|
||||
return befriended
|
||||
|
||||
|
||||
class Police(FSM):
|
||||
'''Simple agent to take drunk people out of pubs.'''
|
||||
level = logging.INFO
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def patrol(self):
|
||||
drunksters = list(self.get_agents(drunk=True,
|
||||
state_id=Patron.drunk_in_pub.id))
|
||||
for drunk in drunksters:
|
||||
self.info('Kicking out the trash: {}'.format(drunk.id))
|
||||
drunk.kick_out()
|
||||
else:
|
||||
self.info('No trash to take out. Too bad.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from soil import simulation
|
||||
simulation.run_from_config('pubcrawl.yml',
|
||||
dry_run=True,
|
||||
dump=None,
|
||||
parallel=False)
|
@ -1,26 +0,0 @@
|
||||
---
|
||||
name: pubcrawl
|
||||
num_trials: 3
|
||||
max_time: 10
|
||||
dump: false
|
||||
network_params:
|
||||
# Generate 100 empty nodes. They will be assigned a network agent
|
||||
generator: empty_graph
|
||||
n: 30
|
||||
network_agents:
|
||||
- agent_type: pubcrawl.Patron
|
||||
description: Extroverted patron
|
||||
state:
|
||||
openness: 1.0
|
||||
weight: 9
|
||||
- agent_type: pubcrawl.Patron
|
||||
description: Introverted patron
|
||||
state:
|
||||
openness: 0.1
|
||||
weight: 1
|
||||
environment_agents:
|
||||
- agent_type: pubcrawl.Police
|
||||
environment_class: pubcrawl.CityPubs
|
||||
environment_params:
|
||||
altercations: 0
|
||||
number_of_pubs: 3
|
@ -0,0 +1,195 @@
|
||||
from soil.agents import FSM, NetworkAgent, state, default_state
|
||||
from soil import Environment, Simulation, parameters
|
||||
from itertools import islice
|
||||
import networkx as nx
|
||||
import logging
|
||||
|
||||
|
||||
class CityPubs(Environment):
|
||||
"""Environment with Pubs"""
|
||||
|
||||
level = logging.INFO
|
||||
number_of_pubs: parameters.Integer = 3
|
||||
ratio_extroverted: parameters.probability = 0.1
|
||||
pub_capacity: parameters.Integer = 10
|
||||
|
||||
def init(self):
|
||||
self.pubs = {}
|
||||
for i in range(self.number_of_pubs):
|
||||
newpub = {
|
||||
"name": "The awesome pub #{}".format(i),
|
||||
"open": True,
|
||||
"capacity": self.pub_capacity,
|
||||
"occupancy": 0,
|
||||
}
|
||||
self.pubs[newpub["name"]] = newpub
|
||||
self.add_agent(agent_class=Police)
|
||||
self.populate_network([Patron.w(openness=0.1), Patron.w(openness=1)],
|
||||
[self.ratio_extroverted, 1-self.ratio_extroverted])
|
||||
assert all(["agent" in node and isinstance(node["agent"], Patron) for (_, node) in self.G.nodes(data=True)])
|
||||
|
||||
def enter(self, pub_id, *nodes):
|
||||
"""Agents will try to enter. The pub checks if it is possible"""
|
||||
try:
|
||||
pub = self["pubs"][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError("Pub {} is not available".format(pub_id))
|
||||
if not pub["open"] or (pub["capacity"] < (len(nodes) + pub["occupancy"])):
|
||||
return False
|
||||
pub["occupancy"] += len(nodes)
|
||||
for node in nodes:
|
||||
node["pub"] = pub_id
|
||||
return True
|
||||
|
||||
def available_pubs(self):
|
||||
for pub in self["pubs"].values():
|
||||
if pub["open"] and (pub["occupancy"] < pub["capacity"]):
|
||||
yield pub["name"]
|
||||
|
||||
def exit(self, pub_id, *node_ids):
|
||||
"""Agents will notify the pub they want to leave"""
|
||||
try:
|
||||
pub = self["pubs"][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError("Pub {} is not available".format(pub_id))
|
||||
for node_id in node_ids:
|
||||
node = self.get_agent(node_id)
|
||||
if pub_id == node["pub"]:
|
||||
del node["pub"]
|
||||
pub["occupancy"] -= 1
|
||||
|
||||
|
||||
class Patron(FSM, NetworkAgent):
|
||||
"""Agent that looks for friends to drink with. It will do three things:
|
||||
1) Look for other patrons to drink with
|
||||
2) Look for a bar where the agent and other agents in the same group can get in.
|
||||
3) While in the bar, patrons only drink, until they get drunk and taken home.
|
||||
"""
|
||||
|
||||
level = logging.DEBUG
|
||||
|
||||
pub = None
|
||||
drunk = False
|
||||
pints = 0
|
||||
max_pints = 3
|
||||
kicked_out = False
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def looking_for_friends(self):
|
||||
"""Look for friends to drink with"""
|
||||
self.info("I am looking for friends")
|
||||
available_friends = list(
|
||||
self.get_agents(drunk=False, pub=None, state_id=self.looking_for_friends.id)
|
||||
)
|
||||
if not available_friends:
|
||||
self.info("Life sucks and I'm alone!")
|
||||
return self.at_home
|
||||
befriended = self.try_friends(available_friends)
|
||||
if befriended:
|
||||
return self.looking_for_pub
|
||||
|
||||
@state
|
||||
def looking_for_pub(self):
|
||||
"""Look for a pub that accepts me and my friends"""
|
||||
if self["pub"] != None:
|
||||
return self.sober_in_pub
|
||||
self.debug("I am looking for a pub")
|
||||
group = list(self.get_neighbors())
|
||||
for pub in self.model.available_pubs():
|
||||
self.debug("We're trying to get into {}: total: {}".format(pub, len(group)))
|
||||
if self.model.enter(pub, self, *group):
|
||||
self.info("We're all {} getting in {}!".format(len(group), pub))
|
||||
return self.sober_in_pub
|
||||
|
||||
@state
|
||||
def sober_in_pub(self):
|
||||
"""Drink up."""
|
||||
self.drink()
|
||||
if self["pints"] > self["max_pints"]:
|
||||
return self.drunk_in_pub
|
||||
|
||||
@state
|
||||
def drunk_in_pub(self):
|
||||
"""I'm out. Take me home!"""
|
||||
self.info("I'm so drunk. Take me home!")
|
||||
self["drunk"] = True
|
||||
if self.kicked_out:
|
||||
return self.at_home
|
||||
pass # out drun
|
||||
|
||||
@state
|
||||
def at_home(self):
|
||||
"""The end"""
|
||||
others = self.get_agents(state_id=Patron.at_home.id, limit_neighbors=True)
|
||||
self.debug("I'm home. Just like {} of my friends".format(len(others)))
|
||||
|
||||
def drink(self):
|
||||
self["pints"] += 1
|
||||
self.debug("Cheers to that")
|
||||
|
||||
def kick_out(self):
|
||||
self.kicked_out = True
|
||||
|
||||
def befriend(self, other_agent, force=False):
|
||||
"""
|
||||
Try to become friends with another agent. The chances of
|
||||
success depend on both agents' openness.
|
||||
"""
|
||||
if force or self["openness"] > self.random.random():
|
||||
self.add_edge(self, other_agent)
|
||||
self.info("Made some friend {}".format(other_agent))
|
||||
return True
|
||||
return False
|
||||
|
||||
def try_friends(self, others):
|
||||
"""Look for random agents around me and try to befriend them"""
|
||||
befriended = False
|
||||
k = int(10 * self["openness"])
|
||||
self.random.shuffle(others)
|
||||
for friend in islice(others, k): # random.choice >= 3.7
|
||||
if friend == self:
|
||||
continue
|
||||
if friend.befriend(self):
|
||||
self.befriend(friend, force=True)
|
||||
self.debug("Hooray! new friend: {}".format(friend.unique_id))
|
||||
befriended = True
|
||||
else:
|
||||
self.debug("{} does not want to be friends".format(friend.unique_id))
|
||||
return befriended
|
||||
|
||||
|
||||
class Police(FSM):
|
||||
"""Simple agent to take drunk people out of pubs."""
|
||||
|
||||
level = logging.INFO
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def patrol(self):
|
||||
drunksters = list(self.get_agents(drunk=True, state_id=Patron.drunk_in_pub.id))
|
||||
for drunk in drunksters:
|
||||
self.info("Kicking out the trash: {}".format(drunk.unique_id))
|
||||
drunk.kick_out()
|
||||
else:
|
||||
self.info("No trash to take out. Too bad.")
|
||||
|
||||
|
||||
sim = Simulation(
|
||||
model=CityPubs,
|
||||
name="pubcrawl",
|
||||
iterations=3,
|
||||
max_steps=10,
|
||||
dump=False,
|
||||
parameters=dict(
|
||||
network_generator=nx.empty_graph,
|
||||
network_params={"n": 30},
|
||||
model=CityPubs,
|
||||
altercations=0,
|
||||
number_of_pubs=3,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run(parallel=False)
|
@ -0,0 +1,14 @@
|
||||
There are two similar implementations of this simulation.
|
||||
|
||||
- `basic`. Using simple primites
|
||||
- `improved`. Using more advanced features such as the `time` module to avoid unnecessary computations (i.e., skip steps), and generator functions.
|
||||
|
||||
The examples can be run directly in the terminal, and they accept command like arguments.
|
||||
For example, to enable the CSV exporter and the Summary exporter, while setting `max_time` to `100` and `seed` to `CustomSeed`:
|
||||
|
||||
```
|
||||
python rabbit_agents.py --set max_time=100 --csv -e summary --set 'seed="CustomSeed"'
|
||||
```
|
||||
|
||||
To learn more about how this functionality works, check out the `soil.easy` function.
|
||||
|
@ -1,135 +0,0 @@
|
||||
from soil.agents import FSM, state, default_state, BaseAgent, NetworkAgent
|
||||
from enum import Enum
|
||||
from random import random, choice
|
||||
import logging
|
||||
import math
|
||||
|
||||
|
||||
class Genders(Enum):
|
||||
male = 'male'
|
||||
female = 'female'
|
||||
|
||||
|
||||
class RabbitModel(FSM):
|
||||
|
||||
defaults = {
|
||||
'age': 0,
|
||||
'gender': Genders.male.value,
|
||||
'mating_prob': 0.001,
|
||||
'offspring': 0,
|
||||
}
|
||||
|
||||
sexual_maturity = 3 #4*30
|
||||
life_expectancy = 365 * 3
|
||||
gestation = 33
|
||||
pregnancy = -1
|
||||
max_females = 5
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def newborn(self):
|
||||
self.debug(f'I am a newborn at age {self["age"]}')
|
||||
self['age'] += 1
|
||||
|
||||
if self['age'] >= self.sexual_maturity:
|
||||
self.debug('I am fertile!')
|
||||
return self.fertile
|
||||
@state
|
||||
def fertile(self):
|
||||
raise Exception("Each subclass should define its fertile state")
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
self.info('Agent {} is dying'.format(self.id))
|
||||
self.die()
|
||||
|
||||
|
||||
class Male(RabbitModel):
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
self['age'] += 1
|
||||
if self['age'] > self.life_expectancy:
|
||||
return self.dead
|
||||
|
||||
if self['gender'] == Genders.female.value:
|
||||
return
|
||||
|
||||
# Males try to mate
|
||||
for f in self.get_agents(state_id=Female.fertile.id,
|
||||
agent_type=Female,
|
||||
limit_neighbors=False,
|
||||
limit=self.max_females):
|
||||
r = random()
|
||||
if r < self['mating_prob']:
|
||||
self.impregnate(f)
|
||||
break # Take a break
|
||||
def impregnate(self, whom):
|
||||
whom['pregnancy'] = 0
|
||||
whom['mate'] = self.id
|
||||
whom.set_state(whom.pregnant)
|
||||
self.debug('{} impregnating: {}. {}'.format(self.id, whom.id, whom.state))
|
||||
|
||||
class Female(RabbitModel):
|
||||
@state
|
||||
def fertile(self):
|
||||
# Just wait for a Male
|
||||
pass
|
||||
|
||||
@state
|
||||
def pregnant(self):
|
||||
self['age'] += 1
|
||||
if self['age'] > self.life_expectancy:
|
||||
return self.dead
|
||||
|
||||
self['pregnancy'] += 1
|
||||
self.debug('Pregnancy: {}'.format(self['pregnancy']))
|
||||
if self['pregnancy'] >= self.gestation:
|
||||
number_of_babies = int(8+4*random())
|
||||
self.info('Having {} babies'.format(number_of_babies))
|
||||
for i in range(number_of_babies):
|
||||
state = {}
|
||||
state['gender'] = choice(list(Genders)).value
|
||||
child = self.env.add_node(self.__class__, state)
|
||||
self.env.add_edge(self.id, child.id)
|
||||
self.env.add_edge(self['mate'], child.id)
|
||||
# self.add_edge()
|
||||
self.debug('A BABY IS COMING TO LIFE')
|
||||
self.env['rabbits_alive'] = self.env.get('rabbits_alive', self.topology.number_of_nodes())+1
|
||||
self.debug('Rabbits alive: {}'.format(self.env['rabbits_alive']))
|
||||
self['offspring'] += 1
|
||||
self.env.get_agent(self['mate'])['offspring'] += 1
|
||||
del self['mate']
|
||||
self['pregnancy'] = -1
|
||||
return self.fertile
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
super().dead()
|
||||
if 'pregnancy' in self and self['pregnancy'] > -1:
|
||||
self.info('A mother has died carrying a baby!!')
|
||||
|
||||
|
||||
class RandomAccident(NetworkAgent):
|
||||
|
||||
level = logging.DEBUG
|
||||
|
||||
def step(self):
|
||||
rabbits_total = self.topology.number_of_nodes()
|
||||
if 'rabbits_alive' not in self.env:
|
||||
self.env['rabbits_alive'] = 0
|
||||
rabbits_alive = self.env.get('rabbits_alive', rabbits_total)
|
||||
prob_death = self.env.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive)))
|
||||
self.debug('Killing some rabbits with prob={}!'.format(prob_death))
|
||||
for i in self.env.network_agents:
|
||||
if i.state['id'] == i.dead.id:
|
||||
continue
|
||||
r = random()
|
||||
if r < prob_death:
|
||||
self.debug('I killed a rabbit: {}'.format(i.id))
|
||||
rabbits_alive = self.env['rabbits_alive'] = rabbits_alive -1
|
||||
self.log('Rabbits alive: {}'.format(self.env['rabbits_alive']))
|
||||
i.set_state(i.dead)
|
||||
self.log('Rabbits alive: {}/{}'.format(rabbits_alive, rabbits_total))
|
||||
if self.count_agents(state_id=RabbitModel.dead.id) == self.topology.number_of_nodes():
|
||||
self.die()
|
@ -0,0 +1,153 @@
|
||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
|
||||
from soil.time import Delta
|
||||
from enum import Enum
|
||||
from collections import Counter
|
||||
import logging
|
||||
import math
|
||||
|
||||
from rabbits_basic_sim import RabbitEnv
|
||||
|
||||
|
||||
class RabbitsImprovedEnv(RabbitEnv):
|
||||
def init(self):
|
||||
"""Initialize the environment with the new versions of the agents"""
|
||||
a1 = self.add_node(Male)
|
||||
a2 = self.add_node(Female)
|
||||
a1.add_edge(a2)
|
||||
self.add_agent(RandomAccident)
|
||||
|
||||
|
||||
class Rabbit(FSM, NetworkAgent):
|
||||
|
||||
sexual_maturity = 30
|
||||
life_expectancy = 300
|
||||
birth = None
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
if self.birth is None:
|
||||
return None
|
||||
return self.now - self.birth
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def newborn(self):
|
||||
self.info("I am a newborn.")
|
||||
self.birth = self.now
|
||||
self.offspring = 0
|
||||
return self.youngling, Delta(self.sexual_maturity - self.age)
|
||||
|
||||
@state
|
||||
def youngling(self):
|
||||
if self.age >= self.sexual_maturity:
|
||||
self.info(f"I am fertile! My age is {self.age}")
|
||||
return self.fertile
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
raise Exception("Each subclass should define its fertile state")
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
self.die()
|
||||
|
||||
|
||||
class Male(Rabbit):
|
||||
max_females = 5
|
||||
mating_prob = 0.001
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
if self.age > self.life_expectancy:
|
||||
return self.dead
|
||||
|
||||
# Males try to mate
|
||||
for f in self.model.agents(
|
||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||
):
|
||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||
if self.prob(self["mating_prob"]):
|
||||
f.impregnate(self)
|
||||
break # Do not try to impregnate other females
|
||||
|
||||
|
||||
class Female(Rabbit):
|
||||
gestation = 10
|
||||
conception = None
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
# Just wait for a Male
|
||||
if self.age > self.life_expectancy:
|
||||
return self.dead
|
||||
if self.conception is not None:
|
||||
return self.pregnant
|
||||
|
||||
@property
|
||||
def pregnancy(self):
|
||||
if self.conception is None:
|
||||
return None
|
||||
return self.now - self.conception
|
||||
|
||||
def impregnate(self, male):
|
||||
self.info(f"impregnated by {repr(male)}")
|
||||
self.mate = male
|
||||
self.conception = self.now
|
||||
self.number_of_babies = int(8 + 4 * self.random.random())
|
||||
|
||||
@state
|
||||
def pregnant(self):
|
||||
self.debug("I am pregnant")
|
||||
|
||||
if self.age > self.life_expectancy:
|
||||
self.info("Dying before giving birth")
|
||||
return self.die()
|
||||
|
||||
if self.pregnancy >= self.gestation:
|
||||
self.info("Having {} babies".format(self.number_of_babies))
|
||||
for i in range(self.number_of_babies):
|
||||
state = {}
|
||||
agent_class = self.random.choice([Male, Female])
|
||||
child = self.model.add_node(agent_class=agent_class, **state)
|
||||
child.add_edge(self)
|
||||
if self.mate:
|
||||
child.add_edge(self.mate)
|
||||
self.mate.offspring += 1
|
||||
else:
|
||||
self.debug("The father has passed away")
|
||||
|
||||
self.offspring += 1
|
||||
self.mate = None
|
||||
return self.fertile
|
||||
|
||||
def die(self):
|
||||
if self.pregnancy is not None:
|
||||
self.info("A mother has died carrying a baby!!")
|
||||
return super().die()
|
||||
|
||||
|
||||
class RandomAccident(BaseAgent):
|
||||
def step(self):
|
||||
rabbits_alive = self.model.G.number_of_nodes()
|
||||
|
||||
if not rabbits_alive:
|
||||
return self.die()
|
||||
|
||||
prob_death = self.model.get("prob_death", 1e-100) * math.floor(
|
||||
math.log10(max(1, rabbits_alive))
|
||||
)
|
||||
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
||||
for i in self.iter_agents(agent_class=Rabbit):
|
||||
if i.state_id == i.dead.id:
|
||||
continue
|
||||
if self.prob(prob_death):
|
||||
self.info("I killed a rabbit: {}".format(i.id))
|
||||
rabbits_alive -= 1
|
||||
i.die()
|
||||
self.debug("Rabbits alive: {}".format(rabbits_alive))
|
||||
|
||||
|
||||
sim = Simulation(model=RabbitsImprovedEnv, max_time=100, seed="MySeed", iterations=1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run()
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
load_module: rabbit_agents
|
||||
name: rabbits_example
|
||||
max_time: 1000
|
||||
interval: 1
|
||||
seed: MySeed
|
||||
agent_type: rabbit_agents.RabbitModel
|
||||
environment_agents:
|
||||
- agent_type: rabbit_agents.RandomAccident
|
||||
environment_params:
|
||||
prob_death: 0.001
|
||||
default_state:
|
||||
mating_prob: 0.1
|
||||
topology:
|
||||
nodes:
|
||||
- id: 1
|
||||
agent_type: rabbit_agents.Male
|
||||
- id: 0
|
||||
agent_type: rabbit_agents.Female
|
||||
directed: true
|
||||
links: []
|
@ -0,0 +1,161 @@
|
||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation, report, parameters as params
|
||||
from collections import Counter
|
||||
import logging
|
||||
import math
|
||||
|
||||
|
||||
class RabbitEnv(Environment):
|
||||
prob_death: params.probability = 1e-100
|
||||
|
||||
def init(self):
|
||||
a1 = self.add_node(Male)
|
||||
a2 = self.add_node(Female)
|
||||
a1.add_edge(a2)
|
||||
self.add_agent(RandomAccident)
|
||||
|
||||
@report
|
||||
@property
|
||||
def num_rabbits(self):
|
||||
return self.count_agents(agent_class=Rabbit)
|
||||
|
||||
@report
|
||||
@property
|
||||
def num_males(self):
|
||||
return self.count_agents(agent_class=Male)
|
||||
|
||||
@report
|
||||
@property
|
||||
def num_females(self):
|
||||
return self.count_agents(agent_class=Female)
|
||||
|
||||
|
||||
class Rabbit(NetworkAgent, FSM):
|
||||
|
||||
sexual_maturity = 30
|
||||
life_expectancy = 300
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def newborn(self):
|
||||
self.info("I am a newborn.")
|
||||
self.age = 0
|
||||
self.offspring = 0
|
||||
return self.youngling
|
||||
|
||||
@state
|
||||
def youngling(self):
|
||||
self.age += 1
|
||||
if self.age >= self.sexual_maturity:
|
||||
self.info(f"I am fertile! My age is {self.age}")
|
||||
return self.fertile
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
raise Exception("Each subclass should define its fertile state")
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
self.die()
|
||||
|
||||
|
||||
class Male(Rabbit):
|
||||
max_females = 5
|
||||
mating_prob = 0.001
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
self.age += 1
|
||||
|
||||
if self.age > self.life_expectancy:
|
||||
return self.dead
|
||||
|
||||
# Males try to mate
|
||||
for f in self.model.agents(
|
||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||
):
|
||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||
if self.prob(self["mating_prob"]):
|
||||
f.impregnate(self)
|
||||
break # Take a break
|
||||
|
||||
|
||||
class Female(Rabbit):
|
||||
gestation = 10
|
||||
pregnancy = -1
|
||||
|
||||
@state
|
||||
def fertile(self):
|
||||
# Just wait for a Male
|
||||
self.age += 1
|
||||
if self.age > self.life_expectancy:
|
||||
return self.dead
|
||||
if self.pregnancy >= 0:
|
||||
return self.pregnant
|
||||
|
||||
def impregnate(self, male):
|
||||
self.info(f"impregnated by {repr(male)}")
|
||||
self.mate = male
|
||||
self.pregnancy = 0
|
||||
self.number_of_babies = int(8 + 4 * self.random.random())
|
||||
|
||||
@state
|
||||
def pregnant(self):
|
||||
self.info("I am pregnant")
|
||||
self.age += 1
|
||||
|
||||
if self.age >= self.life_expectancy:
|
||||
return self.die()
|
||||
|
||||
if self.pregnancy < self.gestation:
|
||||
self.pregnancy += 1
|
||||
return
|
||||
|
||||
self.info("Having {} babies".format(self.number_of_babies))
|
||||
for i in range(self.number_of_babies):
|
||||
state = {}
|
||||
agent_class = self.random.choice([Male, Female])
|
||||
child = self.model.add_node(agent_class=agent_class, **state)
|
||||
child.add_edge(self)
|
||||
try:
|
||||
child.add_edge(self.mate)
|
||||
self.model.agents[self.mate].offspring += 1
|
||||
except ValueError:
|
||||
self.debug("The father has passed away")
|
||||
|
||||
self.offspring += 1
|
||||
self.mate = None
|
||||
self.pregnancy = -1
|
||||
return self.fertile
|
||||
|
||||
def die(self):
|
||||
if "pregnancy" in self and self["pregnancy"] > -1:
|
||||
self.info("A mother has died carrying a baby!!")
|
||||
return super().die()
|
||||
|
||||
|
||||
class RandomAccident(BaseAgent):
|
||||
def step(self):
|
||||
rabbits_alive = self.model.G.number_of_nodes()
|
||||
|
||||
if not rabbits_alive:
|
||||
return self.die()
|
||||
|
||||
prob_death = self.model.prob_death * math.floor(
|
||||
math.log10(max(1, rabbits_alive))
|
||||
)
|
||||
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
||||
for i in self.get_agents(agent_class=Rabbit):
|
||||
if i.state_id == i.dead.id:
|
||||
continue
|
||||
if self.prob(prob_death):
|
||||
self.info("I killed a rabbit: {}".format(i.id))
|
||||
rabbits_alive -= 1
|
||||
i.die()
|
||||
self.debug("Rabbits alive: {}".format(rabbits_alive))
|
||||
|
||||
|
||||
|
||||
sim = Simulation(model=RabbitEnv, max_time=100, seed="MySeed", iterations=1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sim.run()
|
@ -1,45 +0,0 @@
|
||||
'''
|
||||
Example of setting a
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
'''
|
||||
from soil import Simulation, agents
|
||||
from soil.time import Delta
|
||||
from random import expovariate
|
||||
import logging
|
||||
|
||||
|
||||
|
||||
class MyAgent(agents.FSM):
|
||||
'''
|
||||
An agent that first does a ping
|
||||
'''
|
||||
|
||||
defaults = {'pong_counts': 2}
|
||||
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def ping(self):
|
||||
self.info('Ping')
|
||||
return self.pong, Delta(expovariate(1/16))
|
||||
|
||||
@agents.state
|
||||
def pong(self):
|
||||
self.info('Pong')
|
||||
self.pong_counts -= 1
|
||||
self.info(str(self.pong_counts))
|
||||
if self.pong_counts < 1:
|
||||
return self.die()
|
||||
return None, Delta(expovariate(1/16))
|
||||
|
||||
|
||||
s = Simulation(name='Programmatic',
|
||||
network_agents=[{'agent_type': MyAgent, 'id': 0}],
|
||||
topology={'nodes': [{'id': 0}], 'links': []},
|
||||
num_trials=1,
|
||||
max_time=100,
|
||||
agent_type=MyAgent,
|
||||
dry_run=True)
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
envs = s.run()
|
@ -0,0 +1,47 @@
|
||||
"""
|
||||
Example of setting a
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
"""
|
||||
from soil import Simulation, agents, Environment
|
||||
from soil.time import Delta
|
||||
|
||||
|
||||
class MyAgent(agents.FSM):
|
||||
"""
|
||||
An agent that first does a ping
|
||||
"""
|
||||
|
||||
defaults = {"pong_counts": 2}
|
||||
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def ping(self):
|
||||
self.info("Ping")
|
||||
return self.pong, Delta(self.random.expovariate(1 / 16))
|
||||
|
||||
@agents.state
|
||||
def pong(self):
|
||||
self.info("Pong")
|
||||
self.pong_counts -= 1
|
||||
self.info(str(self.pong_counts))
|
||||
if self.pong_counts < 1:
|
||||
return self.die()
|
||||
return None, Delta(self.random.expovariate(1 / 16))
|
||||
|
||||
|
||||
class RandomEnv(Environment):
|
||||
|
||||
def init(self):
|
||||
self.add_agent(agent_class=MyAgent)
|
||||
|
||||
|
||||
s = Simulation(
|
||||
name="Programmatic",
|
||||
model=RandomEnv,
|
||||
iterations=1,
|
||||
max_time=100,
|
||||
dump=False,
|
||||
)
|
||||
|
||||
|
||||
envs = s.run()
|
@ -1,30 +0,0 @@
|
||||
---
|
||||
sampler:
|
||||
method: "SALib.sample.morris.sample"
|
||||
N: 10
|
||||
template:
|
||||
group: simple
|
||||
num_trials: 1
|
||||
interval: 1
|
||||
max_time: 2
|
||||
seed: "CompleteSeed!"
|
||||
dump: false
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: "{{ x1 }}"
|
||||
state:
|
||||
state_id: 0
|
||||
- agent_type: AggregatedCounter
|
||||
weight: "{{ 1 - x1 }}"
|
||||
environment_params:
|
||||
name: "{{ x3 }}"
|
||||
skip_test: true
|
||||
vars:
|
||||
bounds:
|
||||
x1: [0, 1]
|
||||
x2: [1, 2]
|
||||
fixed:
|
||||
x3: ["a", "b", "c"]
|
@ -1,208 +0,0 @@
|
||||
import random
|
||||
import networkx as nx
|
||||
from soil.agents import Geo, NetworkAgent, FSM, state, default_state
|
||||
from soil import Environment
|
||||
|
||||
|
||||
class TerroristSpreadModel(FSM, Geo):
|
||||
"""
|
||||
Settings:
|
||||
information_spread_intensity
|
||||
|
||||
terrorist_additional_influence
|
||||
|
||||
min_vulnerability (optional else zero)
|
||||
|
||||
max_vulnerability
|
||||
|
||||
prob_interaction
|
||||
"""
|
||||
|
||||
def __init__(self, model=None, unique_id=0, state=()):
|
||||
super().__init__(model=model, unique_id=unique_id, state=state)
|
||||
|
||||
self.information_spread_intensity = model.environment_params['information_spread_intensity']
|
||||
self.terrorist_additional_influence = model.environment_params['terrorist_additional_influence']
|
||||
self.prob_interaction = model.environment_params['prob_interaction']
|
||||
|
||||
if self['id'] == self.civilian.id: # Civilian
|
||||
self.mean_belief = random.uniform(0.00, 0.5)
|
||||
elif self['id'] == self.terrorist.id: # Terrorist
|
||||
self.mean_belief = random.uniform(0.8, 1.00)
|
||||
elif self['id'] == self.leader.id: # Leader
|
||||
self.mean_belief = 1.00
|
||||
else:
|
||||
raise Exception('Invalid state id: {}'.format(self['id']))
|
||||
|
||||
if 'min_vulnerability' in model.environment_params:
|
||||
self.vulnerability = random.uniform( model.environment_params['min_vulnerability'], model.environment_params['max_vulnerability'] )
|
||||
else :
|
||||
self.vulnerability = random.uniform( 0, model.environment_params['max_vulnerability'] )
|
||||
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
neighbours = list(self.get_neighboring_agents(agent_type=TerroristSpreadModel))
|
||||
if len(neighbours) > 0:
|
||||
# Only interact with some of the neighbors
|
||||
interactions = list(n for n in neighbours if random.random() <= self.prob_interaction)
|
||||
influence = sum( self.degree(i) for i in interactions )
|
||||
mean_belief = sum( i.mean_belief * self.degree(i) / influence for i in interactions )
|
||||
mean_belief = mean_belief * self.information_spread_intensity + self.mean_belief * ( 1 - self.information_spread_intensity )
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability )
|
||||
|
||||
if self.mean_belief >= 0.8:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]):
|
||||
if self.betweenness(neighbour) > self.betweenness(self):
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
neighbours = self.get_agents(state_id=[self.terrorist.id, self.leader.id],
|
||||
agent_type=TerroristSpreadModel,
|
||||
limit_neighbors=True)
|
||||
if len(neighbours) > 0:
|
||||
influence = sum( self.degree(n) for n in neighbours )
|
||||
mean_belief = sum( n.mean_belief * self.degree(n) / influence for n in neighbours )
|
||||
mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability )
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
|
||||
# Check if there are any leaders in the group
|
||||
leaders = list(filter(lambda x: x.state.id == self.leader.id, neighbours))
|
||||
if not leaders:
|
||||
# Check if this is the potential leader
|
||||
# Stop once it's found. Otherwise, set self as leader
|
||||
for neighbour in neighbours:
|
||||
if self.betweenness(self) < self.betweenness(neighbour):
|
||||
return
|
||||
return self.leader
|
||||
|
||||
|
||||
class TrainingAreaModel(FSM, Geo):
|
||||
"""
|
||||
Settings:
|
||||
training_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
def __init__(self, model=None, unique_id=0, state=()):
|
||||
super().__init__(model=model, unique_id=unique_id, state=state)
|
||||
self.training_influence = model.environment_params['training_influence']
|
||||
if 'min_vulnerability' in model.environment_params:
|
||||
self.min_vulnerability = model.environment_params['min_vulnerability']
|
||||
else: self.min_vulnerability = 0
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_neighboring_agents(agent_type=TerroristSpreadModel):
|
||||
if neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence )
|
||||
|
||||
|
||||
class HavenModel(FSM, Geo):
|
||||
"""
|
||||
Settings:
|
||||
haven_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
max_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
def __init__(self, model=None, unique_id=0, state=()):
|
||||
super().__init__(model=model, unique_id=unique_id, state=state)
|
||||
self.haven_influence = model.environment_params['haven_influence']
|
||||
if 'min_vulnerability' in model.environment_params:
|
||||
self.min_vulnerability = model.environment_params['min_vulnerability']
|
||||
else: self.min_vulnerability = 0
|
||||
self.max_vulnerability = model.environment_params['max_vulnerability']
|
||||
|
||||
def get_occupants(self, **kwargs):
|
||||
return self.get_neighboring_agents(agent_type=TerroristSpreadModel, **kwargs)
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
civilians = self.get_occupants(state_id=self.civilian.id)
|
||||
if not civilians:
|
||||
return self.terrorist
|
||||
|
||||
for neighbour in self.get_occupants():
|
||||
if neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence )
|
||||
return self.civilian
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_occupants():
|
||||
if neighbour.vulnerability < self.max_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence )
|
||||
return self.terrorist
|
||||
|
||||
|
||||
class TerroristNetworkModel(TerroristSpreadModel):
|
||||
"""
|
||||
Settings:
|
||||
sphere_influence
|
||||
|
||||
vision_range
|
||||
|
||||
weight_social_distance
|
||||
|
||||
weight_link_distance
|
||||
"""
|
||||
|
||||
def __init__(self, model=None, unique_id=0, state=()):
|
||||
super().__init__(model=model, unique_id=unique_id, state=state)
|
||||
|
||||
self.vision_range = model.environment_params['vision_range']
|
||||
self.sphere_influence = model.environment_params['sphere_influence']
|
||||
self.weight_social_distance = model.environment_params['weight_social_distance']
|
||||
self.weight_link_distance = model.environment_params['weight_link_distance']
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
self.update_relationships()
|
||||
return super().terrorist()
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.update_relationships()
|
||||
return super().leader()
|
||||
|
||||
def update_relationships(self):
|
||||
if self.count_neighboring_agents(state_id=self.civilian.id) == 0:
|
||||
close_ups = set(self.geo_search(radius=self.vision_range, agent_type=TerroristNetworkModel))
|
||||
step_neighbours = set(self.ego_search(self.sphere_influence, agent_type=TerroristNetworkModel, center=False))
|
||||
neighbours = set(agent.id for agent in self.get_neighboring_agents(agent_type=TerroristNetworkModel))
|
||||
search = (close_ups | step_neighbours) - neighbours
|
||||
for agent in self.get_agents(search):
|
||||
social_distance = 1 / self.shortest_path_length(agent.id)
|
||||
spatial_proximity = ( 1 - self.get_distance(agent.id) )
|
||||
prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity
|
||||
if agent['id'] == agent.civilian.id and random.random() < prob_new_interaction:
|
||||
self.add_edge(agent)
|
||||
break
|
||||
|
||||
def get_distance(self, target):
|
||||
source_x, source_y = nx.get_node_attributes(self.topology, 'pos')[self.id]
|
||||
target_x, target_y = nx.get_node_attributes(self.topology, 'pos')[target]
|
||||
dx = abs( source_x - target_x )
|
||||
dy = abs( source_y - target_y )
|
||||
return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 )
|
||||
|
||||
def shortest_path_length(self, target):
|
||||
try:
|
||||
return nx.shortest_path_length(self.topology, self.id, target)
|
||||
except nx.NetworkXNoPath:
|
||||
return float('inf')
|
@ -1,63 +0,0 @@
|
||||
name: TerroristNetworkModel_sim
|
||||
load_module: TerroristNetworkModel
|
||||
max_time: 150
|
||||
num_trials: 1
|
||||
network_params:
|
||||
generator: random_geometric_graph
|
||||
radius: 0.2
|
||||
# generator: geographical_threshold_graph
|
||||
# theta: 20
|
||||
n: 100
|
||||
network_agents:
|
||||
- agent_type: TerroristNetworkModel
|
||||
weight: 0.8
|
||||
state:
|
||||
id: civilian # Civilians
|
||||
- agent_type: TerroristNetworkModel
|
||||
weight: 0.1
|
||||
state:
|
||||
id: leader # Leaders
|
||||
- agent_type: TrainingAreaModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: terrorist # Terrorism
|
||||
- agent_type: HavenModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: civilian # Civilian
|
||||
|
||||
environment_params:
|
||||
# TerroristSpreadModel
|
||||
information_spread_intensity: 0.7
|
||||
terrorist_additional_influence: 0.035
|
||||
max_vulnerability: 0.7
|
||||
prob_interaction: 0.5
|
||||
|
||||
# TrainingAreaModel and HavenModel
|
||||
training_influence: 0.20
|
||||
haven_influence: 0.20
|
||||
|
||||
# TerroristNetworkModel
|
||||
vision_range: 0.30
|
||||
sphere_influence: 2
|
||||
weight_social_distance: 0.035
|
||||
weight_link_distance: 0.035
|
||||
|
||||
visualization_params:
|
||||
# Icons downloaded from https://www.iconfinder.com/
|
||||
shape_property: agent
|
||||
shapes:
|
||||
TrainingAreaModel: target
|
||||
HavenModel: home
|
||||
TerroristNetworkModel: person
|
||||
colors:
|
||||
- attr_id: civilian
|
||||
color: '#40de40'
|
||||
- attr_id: terrorist
|
||||
color: red
|
||||
- attr_id: leader
|
||||
color: '#c16a6a'
|
||||
background_image: 'map_4800x2860.jpg'
|
||||
background_opacity: '0.9'
|
||||
background_filter_color: 'blue'
|
||||
skip_test: true # This simulation takes too long for automated tests.
|
@ -0,0 +1,341 @@
|
||||
import networkx as nx
|
||||
from soil.agents import Geo, NetworkAgent, FSM, custom, state, default_state
|
||||
from soil import Environment, Simulation
|
||||
from soil.parameters import *
|
||||
from soil.utils import int_seed
|
||||
|
||||
|
||||
class TerroristEnvironment(Environment):
|
||||
n: Integer = 100
|
||||
radius: Float = 0.2
|
||||
|
||||
information_spread_intensity: probability = 0.7
|
||||
terrorist_additional_influence: probability = 0.03
|
||||
terrorist_additional_influence: probability = 0.035
|
||||
max_vulnerability: probability = 0.7
|
||||
prob_interaction: probability = 0.5
|
||||
|
||||
# TrainingAreaModel and HavenModel
|
||||
training_influence: probability = 0.20
|
||||
haven_influence: probability = 0.20
|
||||
|
||||
# TerroristNetworkModel
|
||||
vision_range: Float = 0.30
|
||||
sphere_influence: Integer = 2
|
||||
weight_social_distance: Float = 0.035
|
||||
weight_link_distance: Float = 0.035
|
||||
|
||||
ratio_civil: probability = 0.8
|
||||
ratio_leader: probability = 0.1
|
||||
ratio_training: probability = 0.05
|
||||
ratio_haven: probability = 0.05
|
||||
|
||||
def init(self):
|
||||
self.create_network(generator=self.generator, n=self.n, radius=self.radius)
|
||||
self.populate_network([
|
||||
TerroristNetworkModel.w(state_id='civilian'),
|
||||
TerroristNetworkModel.w(state_id='leader'),
|
||||
TrainingAreaModel,
|
||||
HavenModel
|
||||
], [self.ratio_civil, self.ratio_leader, self.ratio_training, self.ratio_haven])
|
||||
|
||||
def generator(self, *args, **kwargs):
|
||||
return nx.random_geometric_graph(*args, **kwargs, seed=int_seed(self._seed))
|
||||
|
||||
class TerroristSpreadModel(FSM, Geo):
|
||||
"""
|
||||
Settings:
|
||||
information_spread_intensity
|
||||
|
||||
terrorist_additional_influence
|
||||
|
||||
min_vulnerability (optional else zero)
|
||||
|
||||
max_vulnerability
|
||||
"""
|
||||
|
||||
information_spread_intensity = 0.1
|
||||
terrorist_additional_influence = 0.1
|
||||
min_vulnerability = 0
|
||||
max_vulnerability = 1
|
||||
|
||||
def init(self):
|
||||
if self.state_id == self.civilian.id: # Civilian
|
||||
self.mean_belief = self.model.random.uniform(0.00, 0.5)
|
||||
elif self.state_id == self.terrorist.id: # Terrorist
|
||||
self.mean_belief = self.random.uniform(0.8, 1.00)
|
||||
elif self.state_id == self.leader.id: # Leader
|
||||
self.mean_belief = 1.00
|
||||
else:
|
||||
raise Exception("Invalid state id: {}".format(self["id"]))
|
||||
|
||||
self.vulnerability = self.random.uniform(
|
||||
self.get("min_vulnerability", 0), self.get("max_vulnerability", 1)
|
||||
)
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def civilian(self):
|
||||
neighbours = list(self.get_neighbors(agent_class=TerroristSpreadModel))
|
||||
if len(neighbours) > 0:
|
||||
# Only interact with some of the neighbors
|
||||
interactions = list(
|
||||
n for n in neighbours if self.random.random() <= self.model.prob_interaction
|
||||
)
|
||||
influence = sum(self.degree(i) for i in interactions)
|
||||
mean_belief = sum(
|
||||
i.mean_belief * self.degree(i) / influence for i in interactions
|
||||
)
|
||||
mean_belief = (
|
||||
mean_belief * self.information_spread_intensity
|
||||
+ self.mean_belief * (1 - self.information_spread_intensity)
|
||||
)
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.mean_belief * (
|
||||
1 - self.vulnerability
|
||||
)
|
||||
|
||||
if self.mean_belief >= 0.8:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.mean_belief = self.mean_belief ** (1 - self.terrorist_additional_influence)
|
||||
for neighbour in self.get_neighbors(
|
||||
state_id=[self.terrorist.id, self.leader.id]
|
||||
):
|
||||
if self.betweenness(neighbour) > self.betweenness(self):
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
neighbours = self.get_agents(
|
||||
state_id=[self.terrorist.id, self.leader.id],
|
||||
agent_class=TerroristSpreadModel,
|
||||
limit_neighbors=True,
|
||||
)
|
||||
if len(neighbours) > 0:
|
||||
influence = sum(self.degree(n) for n in neighbours)
|
||||
mean_belief = sum(
|
||||
n.mean_belief * self.degree(n) / influence for n in neighbours
|
||||
)
|
||||
mean_belief = mean_belief * self.vulnerability + self.mean_belief * (
|
||||
1 - self.vulnerability
|
||||
)
|
||||
self.mean_belief = self.mean_belief ** (
|
||||
1 - self.terrorist_additional_influence
|
||||
)
|
||||
|
||||
# Check if there are any leaders in the group
|
||||
leaders = list(filter(lambda x: x.state_id == self.leader.id, neighbours))
|
||||
if not leaders:
|
||||
# Check if this is the potential leader
|
||||
# Stop once it's found. Otherwise, set self as leader
|
||||
for neighbour in neighbours:
|
||||
if self.betweenness(self) < self.betweenness(neighbour):
|
||||
return
|
||||
return self.leader
|
||||
|
||||
def ego_search(self, steps=1, center=False, agent=None, **kwargs):
|
||||
"""Get a list of nodes in the ego network of *node* of radius *steps*"""
|
||||
node = agent.node_id if agent else self.node_id
|
||||
G = self.subgraph(**kwargs)
|
||||
return nx.ego_graph(G, node, center=center, radius=steps).nodes()
|
||||
|
||||
def degree(self, agent, force=False):
|
||||
if (
|
||||
force
|
||||
or (not hasattr(self.model, "_degree"))
|
||||
or getattr(self.model, "_last_step", 0) < self.now
|
||||
):
|
||||
self.model._degree = nx.degree_centrality(self.G)
|
||||
self.model._last_step = self.now
|
||||
return self.model._degree[agent.node_id]
|
||||
|
||||
def betweenness(self, agent, force=False):
|
||||
if (
|
||||
force
|
||||
or (not hasattr(self.model, "_betweenness"))
|
||||
or getattr(self.model, "_last_step", 0) < self.now
|
||||
):
|
||||
self.model._betweenness = nx.betweenness_centrality(self.G)
|
||||
self.model._last_step = self.now
|
||||
return self.model._betweenness[agent.node_id]
|
||||
|
||||
|
||||
class TrainingAreaModel(FSM, Geo):
|
||||
"""
|
||||
Settings:
|
||||
training_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
training_influence = 0.1
|
||||
min_vulnerability = 0
|
||||
|
||||
def init(self):
|
||||
self.mean_believe = 1
|
||||
self.vulnerability = 0
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_neighbors(agent_class=TerroristSpreadModel):
|
||||
if neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** (
|
||||
1 - self.training_influence
|
||||
)
|
||||
|
||||
|
||||
class HavenModel(FSM, Geo):
|
||||
"""
|
||||
Settings:
|
||||
haven_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
max_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
min_vulnerability = 0
|
||||
haven_influence = 0.1
|
||||
max_vulnerability = 0.5
|
||||
|
||||
def init(self):
|
||||
self.mean_believe = 0
|
||||
self.vulnerability = 0
|
||||
|
||||
def get_occupants(self, **kwargs):
|
||||
return self.get_neighbors(agent_class=TerroristSpreadModel,
|
||||
**kwargs)
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def civilian(self):
|
||||
civilians = self.get_occupants(state_id=self.civilian.id)
|
||||
if not civilians:
|
||||
return self.terrorist
|
||||
|
||||
for neighbour in self.get_occupants():
|
||||
if neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability * (
|
||||
1 - self.haven_influence
|
||||
)
|
||||
return self.civilian
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_occupants():
|
||||
if neighbour.vulnerability < self.max_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** (
|
||||
1 - self.haven_influence
|
||||
)
|
||||
return self.terrorist
|
||||
|
||||
|
||||
class TerroristNetworkModel(TerroristSpreadModel):
|
||||
"""
|
||||
Settings:
|
||||
sphere_influence
|
||||
|
||||
vision_range
|
||||
|
||||
weight_social_distance
|
||||
|
||||
weight_link_distance
|
||||
"""
|
||||
|
||||
sphere_influence: float = 1
|
||||
vision_range: float = 1
|
||||
weight_social_distance: float = 0.5
|
||||
weight_link_distance: float = 0.2
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
self.update_relationships()
|
||||
return super().terrorist()
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.update_relationships()
|
||||
return super().leader()
|
||||
|
||||
def update_relationships(self):
|
||||
if self.count_neighbors(state_id=self.civilian.id) == 0:
|
||||
close_ups = set(
|
||||
self.geo_search(
|
||||
radius=self.vision_range, agent_class=TerroristNetworkModel
|
||||
)
|
||||
)
|
||||
step_neighbours = set(
|
||||
self.ego_search(
|
||||
self.sphere_influence,
|
||||
agent_class=TerroristNetworkModel,
|
||||
center=False,
|
||||
)
|
||||
)
|
||||
neighbours = set(
|
||||
agent.unique_id
|
||||
for agent in self.get_neighbors(agent_class=TerroristNetworkModel)
|
||||
)
|
||||
search = (close_ups | step_neighbours) - neighbours
|
||||
for agent in self.get_agents(search):
|
||||
social_distance = 1 / self.shortest_path_length(agent.unique_id)
|
||||
spatial_proximity = 1 - self.get_distance(agent.unique_id)
|
||||
prob_new_interaction = (
|
||||
self.weight_social_distance * social_distance
|
||||
+ self.weight_link_distance * spatial_proximity
|
||||
)
|
||||
if (
|
||||
agent.state_id == "civilian"
|
||||
and self.random.random() < prob_new_interaction
|
||||
):
|
||||
self.add_edge(agent)
|
||||
break
|
||||
|
||||
def get_distance(self, target):
|
||||
source_x, source_y = nx.get_node_attributes(self.G, "pos")[self.unique_id]
|
||||
target_x, target_y = nx.get_node_attributes(self.G, "pos")[target]
|
||||
dx = abs(source_x - target_x)
|
||||
dy = abs(source_y - target_y)
|
||||
return (dx**2 + dy**2) ** (1 / 2)
|
||||
|
||||
def shortest_path_length(self, target):
|
||||
try:
|
||||
return nx.shortest_path_length(self.G, self.unique_id, target)
|
||||
except nx.NetworkXNoPath:
|
||||
return float("inf")
|
||||
|
||||
|
||||
sim = Simulation(
|
||||
model=TerroristEnvironment,
|
||||
iterations=1,
|
||||
name="TerroristNetworkModel_sim",
|
||||
max_steps=150,
|
||||
seed="default2",
|
||||
skip_test=False,
|
||||
dump=False,
|
||||
)
|
||||
|
||||
# TODO: integrate visualization
|
||||
# visualization_params:
|
||||
# # Icons downloaded from https://www.iconfinder.com/
|
||||
# shape_property: agent
|
||||
# shapes:
|
||||
# TrainingAreaModel: target
|
||||
# HavenModel: home
|
||||
# TerroristNetworkModel: person
|
||||
# colors:
|
||||
# - attr_id: civilian
|
||||
# color: '#40de40'
|
||||
# - attr_id: terrorist
|
||||
# color: red
|
||||
# - attr_id: leader
|
||||
# color: '#c16a6a'
|
||||
# background_image: 'map_4800x2860.jpg'
|
||||
# background_opacity: '0.9'
|
||||
# background_filter_color: 'blue'
|
@ -1,14 +0,0 @@
|
||||
---
|
||||
name: torvalds_example
|
||||
max_time: 10
|
||||
interval: 2
|
||||
agent_type: CounterModel
|
||||
default_state:
|
||||
skill_level: 'beginner'
|
||||
network_params:
|
||||
path: 'torvalds.edgelist'
|
||||
states:
|
||||
Torvalds:
|
||||
skill_level: 'God'
|
||||
balkian:
|
||||
skill_level: 'developer'
|
@ -0,0 +1,25 @@
|
||||
from soil import Environment, Simulation, CounterModel, report
|
||||
|
||||
|
||||
# Get directory path for current file
|
||||
import os, sys, inspect
|
||||
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
|
||||
class TorvaldsEnv(Environment):
|
||||
|
||||
def init(self):
|
||||
self.create_network(path=os.path.join(currentdir, 'torvalds.edgelist'))
|
||||
self.populate_network(CounterModel, skill_level='beginner')
|
||||
self.agent(node_id="Torvalds").skill_level = 'God'
|
||||
self.agent(node_id="balkian").skill_level = 'developer'
|
||||
self.add_agent_reporter("times")
|
||||
|
||||
@report
|
||||
def god_developers(self):
|
||||
return self.count_agents(skill_level='God')
|
||||
|
||||
|
||||
sim = Simulation(name='torvalds_example',
|
||||
max_steps=10,
|
||||
interval=2,
|
||||
model=TorvaldsEnv)
|
@ -1,3 +1,7 @@
|
||||
[metadata]
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
[tool:pytest]
|
||||
|
@ -1 +1 @@
|
||||
0.20.8
|
||||
1.0.0rc2
|
||||
|
@ -1,4 +1,9 @@
|
||||
from . import main
|
||||
from . import main as init_main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
def main():
|
||||
init_main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_main()
|
||||
|
@ -1,95 +0,0 @@
|
||||
import random
|
||||
from . import FSM, state, default_state
|
||||
|
||||
|
||||
class BigMarketModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
Names:
|
||||
enterprises [Array]
|
||||
|
||||
tweet_probability_enterprises [Array]
|
||||
Users:
|
||||
tweet_probability_users
|
||||
|
||||
tweet_relevant_probability
|
||||
|
||||
tweet_probability_about [Array]
|
||||
|
||||
sentiment_about [Array]
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.enterprises = self.env.environment_params['enterprises']
|
||||
self.type = ""
|
||||
|
||||
if self.id < len(self.enterprises): # Enterprises
|
||||
self.set_state(self.enterprise.id)
|
||||
self.type = "Enterprise"
|
||||
self.tweet_probability = environment.environment_params['tweet_probability_enterprises'][self.id]
|
||||
else: # normal users
|
||||
self.type = "User"
|
||||
self.set_state(self.user.id)
|
||||
self.tweet_probability = environment.environment_params['tweet_probability_users']
|
||||
self.tweet_relevant_probability = environment.environment_params['tweet_relevant_probability']
|
||||
self.tweet_probability_about = environment.environment_params['tweet_probability_about'] # List
|
||||
self.sentiment_about = environment.environment_params['sentiment_about'] # List
|
||||
|
||||
@state
|
||||
def enterprise(self):
|
||||
|
||||
if random.random() < self.tweet_probability: # Tweets
|
||||
aware_neighbors = self.get_neighboring_agents(state_id=self.number_of_enterprises) # Nodes neighbour users
|
||||
for x in aware_neighbors:
|
||||
if random.uniform(0,10) < 5:
|
||||
x.sentiment_about[self.id] += 0.1 # Increments for enterprise
|
||||
else:
|
||||
x.sentiment_about[self.id] -= 0.1 # Decrements for enterprise
|
||||
|
||||
# Establecemos limites
|
||||
if x.sentiment_about[self.id] > 1:
|
||||
x.sentiment_about[self.id] = 1
|
||||
if x.sentiment_about[self.id]< -1:
|
||||
x.sentiment_about[self.id] = -1
|
||||
|
||||
x.attrs['sentiment_enterprise_%s'% self.enterprises[self.id]] = x.sentiment_about[self.id]
|
||||
|
||||
@state
|
||||
def user(self):
|
||||
if random.random() < self.tweet_probability: # Tweets
|
||||
if random.random() < self.tweet_relevant_probability: # Tweets something relevant
|
||||
# Tweet probability per enterprise
|
||||
for i in range(len(self.enterprises)):
|
||||
random_num = random.random()
|
||||
if random_num < self.tweet_probability_about[i]:
|
||||
# The condition is fulfilled, sentiments are evaluated towards that enterprise
|
||||
if self.sentiment_about[i] < 0:
|
||||
# NEGATIVO
|
||||
self.userTweets("negative",i)
|
||||
elif self.sentiment_about[i] == 0:
|
||||
# NEUTRO
|
||||
pass
|
||||
else:
|
||||
# POSITIVO
|
||||
self.userTweets("positive",i)
|
||||
for i in range(len(self.enterprises)): # So that it never is set to 0 if there are not changes (logs)
|
||||
self.attrs['sentiment_enterprise_%s'% self.enterprises[i]] = self.sentiment_about[i]
|
||||
|
||||
def userTweets(self, sentiment,enterprise):
|
||||
aware_neighbors = self.get_neighboring_agents(state_id=self.number_of_enterprises) # Nodes neighbours users
|
||||
for x in aware_neighbors:
|
||||
if sentiment == "positive":
|
||||
x.sentiment_about[enterprise] +=0.003
|
||||
elif sentiment == "negative":
|
||||
x.sentiment_about[enterprise] -=0.003
|
||||
else:
|
||||
pass
|
||||
|
||||
# Establecemos limites
|
||||
if x.sentiment_about[enterprise] > 1:
|
||||
x.sentiment_about[enterprise] = 1
|
||||
if x.sentiment_about[enterprise] < -1:
|
||||
x.sentiment_about[enterprise] = -1
|
||||
|
||||
x.attrs['sentiment_enterprise_%s'% self.enterprises[enterprise]] = x.sentiment_about[enterprise]
|
@ -1,21 +1,21 @@
|
||||
from scipy.spatial import cKDTree as KDTree
|
||||
import networkx as nx
|
||||
from . import NetworkAgent, as_node
|
||||
from . import NetworkAgent
|
||||
|
||||
|
||||
class Geo(NetworkAgent):
|
||||
'''In this type of network, nodes have a "pos" attribute.'''
|
||||
"""In this type of network, nodes have a "pos" attribute."""
|
||||
|
||||
def geo_search(self, radius, node=None, center=False, **kwargs):
|
||||
'''Get a list of nodes whose coordinates are closer than *radius* to *node*.'''
|
||||
node = as_node(node if node is not None else self)
|
||||
def geo_search(self, radius, center=False, **kwargs):
|
||||
"""Get a list of nodes whose coordinates are closer than *radius* to *node*."""
|
||||
node = self.node_id
|
||||
|
||||
G = self.subgraph(**kwargs)
|
||||
|
||||
pos = nx.get_node_attributes(G, 'pos')
|
||||
pos = nx.get_node_attributes(G, "pos")
|
||||
if not pos:
|
||||
return []
|
||||
nodes, coords = list(zip(*pos.items()))
|
||||
kdtree = KDTree(coords) # Cannot provide generator.
|
||||
indices = kdtree.query_ball_point(pos[node], radius)
|
||||
return [nodes[i] for i in indices if center or (nodes[i] != node)]
|
||||
|
||||
return [nodes[i] for i in indices if center or (nodes[i] != node)]
|
@ -1,242 +0,0 @@
|
||||
import random
|
||||
import numpy as np
|
||||
from . import BaseAgent
|
||||
|
||||
|
||||
class SpreadModelM2(BaseAgent):
|
||||
"""
|
||||
Settings:
|
||||
prob_neutral_making_denier
|
||||
|
||||
prob_infect
|
||||
|
||||
prob_cured_healing_infected
|
||||
|
||||
prob_cured_vaccinate_neutral
|
||||
|
||||
prob_vaccinated_healing_infected
|
||||
|
||||
prob_vaccinated_vaccinate_neutral
|
||||
|
||||
prob_generate_anti_rumor
|
||||
"""
|
||||
|
||||
def __init__(self, model=None, unique_id=0, state=()):
|
||||
super().__init__(model=environment, unique_id=unique_id, state=state)
|
||||
|
||||
self.prob_neutral_making_denier = np.random.normal(environment.environment_params['prob_neutral_making_denier'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
self.prob_infect = np.random.normal(environment.environment_params['prob_infect'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
self.prob_cured_healing_infected = np.random.normal(environment.environment_params['prob_cured_healing_infected'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.prob_cured_vaccinate_neutral = np.random.normal(environment.environment_params['prob_cured_vaccinate_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
self.prob_vaccinated_healing_infected = np.random.normal(environment.environment_params['prob_vaccinated_healing_infected'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.prob_vaccinated_vaccinate_neutral = np.random.normal(environment.environment_params['prob_vaccinated_vaccinate_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.prob_generate_anti_rumor = np.random.normal(environment.environment_params['prob_generate_anti_rumor'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
def step(self):
|
||||
|
||||
if self.state['id'] == 0: # Neutral
|
||||
self.neutral_behaviour()
|
||||
elif self.state['id'] == 1: # Infected
|
||||
self.infected_behaviour()
|
||||
elif self.state['id'] == 2: # Cured
|
||||
self.cured_behaviour()
|
||||
elif self.state['id'] == 3: # Vaccinated
|
||||
self.vaccinated_behaviour()
|
||||
|
||||
def neutral_behaviour(self):
|
||||
|
||||
# Infected
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if len(infected_neighbors) > 0:
|
||||
if random.random() < self.prob_neutral_making_denier:
|
||||
self.state['id'] = 3 # Vaccinated making denier
|
||||
|
||||
def infected_behaviour(self):
|
||||
|
||||
# Neutral
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_infect:
|
||||
neighbor.state['id'] = 1 # Infected
|
||||
|
||||
def cured_behaviour(self):
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
def vaccinated_behaviour(self):
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Generate anti-rumor
|
||||
infected_neighbors_2 = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors_2:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
class ControlModelM2(BaseAgent):
|
||||
"""
|
||||
Settings:
|
||||
prob_neutral_making_denier
|
||||
|
||||
prob_infect
|
||||
|
||||
prob_cured_healing_infected
|
||||
|
||||
prob_cured_vaccinate_neutral
|
||||
|
||||
prob_vaccinated_healing_infected
|
||||
|
||||
prob_vaccinated_vaccinate_neutral
|
||||
|
||||
prob_generate_anti_rumor
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, model=None, unique_id=0, state=()):
|
||||
super().__init__(model=environment, unique_id=unique_id, state=state)
|
||||
|
||||
self.prob_neutral_making_denier = np.random.normal(environment.environment_params['prob_neutral_making_denier'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
self.prob_infect = np.random.normal(environment.environment_params['prob_infect'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
self.prob_cured_healing_infected = np.random.normal(environment.environment_params['prob_cured_healing_infected'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.prob_cured_vaccinate_neutral = np.random.normal(environment.environment_params['prob_cured_vaccinate_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
self.prob_vaccinated_healing_infected = np.random.normal(environment.environment_params['prob_vaccinated_healing_infected'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.prob_vaccinated_vaccinate_neutral = np.random.normal(environment.environment_params['prob_vaccinated_vaccinate_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.prob_generate_anti_rumor = np.random.normal(environment.environment_params['prob_generate_anti_rumor'],
|
||||
environment.environment_params['standard_variance'])
|
||||
|
||||
def step(self):
|
||||
|
||||
if self.state['id'] == 0: # Neutral
|
||||
self.neutral_behaviour()
|
||||
elif self.state['id'] == 1: # Infected
|
||||
self.infected_behaviour()
|
||||
elif self.state['id'] == 2: # Cured
|
||||
self.cured_behaviour()
|
||||
elif self.state['id'] == 3: # Vaccinated
|
||||
self.vaccinated_behaviour()
|
||||
elif self.state['id'] == 4: # Beacon-off
|
||||
self.beacon_off_behaviour()
|
||||
elif self.state['id'] == 5: # Beacon-on
|
||||
self.beacon_on_behaviour()
|
||||
|
||||
def neutral_behaviour(self):
|
||||
self.state['visible'] = False
|
||||
|
||||
# Infected
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if len(infected_neighbors) > 0:
|
||||
if random.random() < self.prob_neutral_making_denier:
|
||||
self.state['id'] = 3 # Vaccinated making denier
|
||||
|
||||
def infected_behaviour(self):
|
||||
|
||||
# Neutral
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_infect:
|
||||
neighbor.state['id'] = 1 # Infected
|
||||
self.state['visible'] = False
|
||||
|
||||
def cured_behaviour(self):
|
||||
|
||||
self.state['visible'] = True
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
def vaccinated_behaviour(self):
|
||||
self.state['visible'] = True
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Generate anti-rumor
|
||||
infected_neighbors_2 = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors_2:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
def beacon_off_behaviour(self):
|
||||
self.state['visible'] = False
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if len(infected_neighbors) > 0:
|
||||
self.state['id'] == 5 # Beacon on
|
||||
|
||||
def beacon_on_behaviour(self):
|
||||
self.state['visible'] = False
|
||||
# Cure (M2 feature added)
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
neutral_neighbors_infected = neighbor.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors_infected:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
infected_neighbors_infected = neighbor.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors_infected:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
@ -1,93 +1,110 @@
|
||||
import random
|
||||
import numpy as np
|
||||
from . import FSM, state
|
||||
from hashlib import sha512
|
||||
from . import Agent, state, default_state
|
||||
|
||||
|
||||
class SISaModel(FSM):
|
||||
class SISaModel(Agent):
|
||||
"""
|
||||
Settings:
|
||||
neutral_discontent_spon_prob
|
||||
|
||||
|
||||
neutral_discontent_infected_prob
|
||||
|
||||
|
||||
neutral_content_spon_prob
|
||||
|
||||
|
||||
neutral_content_infected_prob
|
||||
|
||||
|
||||
discontent_neutral
|
||||
|
||||
|
||||
discontent_content
|
||||
|
||||
|
||||
variance_d_c
|
||||
|
||||
|
||||
content_discontent
|
||||
|
||||
|
||||
variance_c_d
|
||||
|
||||
|
||||
content_neutral
|
||||
|
||||
|
||||
standard_variance
|
||||
"""
|
||||
|
||||
def __init__(self, environment, unique_id=0, state=()):
|
||||
super().__init__(model=environment, unique_id=unique_id, state=state)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
seed = self.model._seed
|
||||
if isinstance(seed, (str, bytes, bytearray)):
|
||||
if isinstance(seed, str):
|
||||
seed = seed.encode()
|
||||
seed = int.from_bytes(seed + sha512(seed).digest(), 'big')
|
||||
|
||||
random = np.random.default_rng(seed=seed)
|
||||
|
||||
self.neutral_discontent_spon_prob = np.random.normal(self.env['neutral_discontent_spon_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_discontent_infected_prob = np.random.normal(self.env['neutral_discontent_infected_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_content_spon_prob = np.random.normal(self.env['neutral_content_spon_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_content_infected_prob = np.random.normal(self.env['neutral_content_infected_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_discontent_spon_prob = random.normal(
|
||||
self.model.neutral_discontent_spon_prob, self.model.standard_variance
|
||||
)
|
||||
self.neutral_discontent_infected_prob = random.normal(
|
||||
self.model.neutral_discontent_infected_prob, self.model.standard_variance
|
||||
)
|
||||
self.neutral_content_spon_prob = random.normal(
|
||||
self.model.neutral_content_spon_prob, self.model.standard_variance
|
||||
)
|
||||
self.neutral_content_infected_prob = random.normal(
|
||||
self.model.neutral_content_infected_prob, self.model.standard_variance
|
||||
)
|
||||
|
||||
self.discontent_neutral = np.random.normal(self.env['discontent_neutral'],
|
||||
self.env['standard_variance'])
|
||||
self.discontent_content = np.random.normal(self.env['discontent_content'],
|
||||
self.env['variance_d_c'])
|
||||
self.discontent_neutral = random.normal(
|
||||
self.model.discontent_neutral, self.model.standard_variance
|
||||
)
|
||||
self.discontent_content = random.normal(
|
||||
self.model.discontent_content, self.model.variance_d_c
|
||||
)
|
||||
|
||||
self.content_discontent = np.random.normal(self.env['content_discontent'],
|
||||
self.env['variance_c_d'])
|
||||
self.content_neutral = np.random.normal(self.env['content_neutral'],
|
||||
self.env['standard_variance'])
|
||||
self.content_discontent = random.normal(
|
||||
self.model.content_discontent, self.model.variance_c_d
|
||||
)
|
||||
self.content_neutral = random.normal(
|
||||
self.model.discontent_neutral, self.model.standard_variance
|
||||
)
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def neutral(self):
|
||||
# Spontaneous effects
|
||||
if random.random() < self.neutral_discontent_spon_prob:
|
||||
if self.prob(self.neutral_discontent_spon_prob):
|
||||
return self.discontent
|
||||
if random.random() < self.neutral_content_spon_prob:
|
||||
if self.prob(self.neutral_content_spon_prob):
|
||||
return self.content
|
||||
|
||||
# Infected
|
||||
discontent_neighbors = self.count_neighboring_agents(state_id=self.discontent)
|
||||
if random.random() < discontent_neighbors * self.neutral_discontent_infected_prob:
|
||||
discontent_neighbors = self.count_neighbors(state_id=self.discontent)
|
||||
if self.prob(discontent_neighbors * self.neutral_discontent_infected_prob):
|
||||
return self.discontent
|
||||
content_neighbors = self.count_neighboring_agents(state_id=self.content.id)
|
||||
if random.random() < content_neighbors * self.neutral_content_infected_prob:
|
||||
content_neighbors = self.count_neighbors(state_id=self.content.id)
|
||||
if self.prob(content_neighbors * self.neutral_content_infected_prob):
|
||||
return self.content
|
||||
return self.neutral
|
||||
|
||||
@state
|
||||
def discontent(self):
|
||||
# Healing
|
||||
if random.random() < self.discontent_neutral:
|
||||
if self.prob(self.discontent_neutral):
|
||||
return self.neutral
|
||||
|
||||
# Superinfected
|
||||
content_neighbors = self.count_neighboring_agents(state_id=self.content.id)
|
||||
if random.random() < content_neighbors * self.discontent_content:
|
||||
content_neighbors = self.count_neighbors(state_id=self.content.id)
|
||||
if self.prob(content_neighbors * self.discontent_content):
|
||||
return self.content
|
||||
return self.discontent
|
||||
|
||||
@state
|
||||
def content(self):
|
||||
# Healing
|
||||
if random.random() < self.content_neutral:
|
||||
if self.prob(self.content_neutral):
|
||||
return self.neutral
|
||||
|
||||
# Superinfected
|
||||
discontent_neighbors = self.count_neighboring_agents(state_id=self.discontent.id)
|
||||
if random.random() < discontent_neighbors * self.content_discontent:
|
||||
discontent_neighbors = self.count_neighbors(state_id=self.discontent.id)
|
||||
if self.prob(discontent_neighbors * self.content_discontent):
|
||||
self.discontent
|
||||
return self.content
|
||||
|
@ -1,102 +0,0 @@
|
||||
import random
|
||||
from . import BaseAgent
|
||||
|
||||
|
||||
class SentimentCorrelationModel(BaseAgent):
|
||||
"""
|
||||
Settings:
|
||||
outside_effects_prob
|
||||
|
||||
anger_prob
|
||||
|
||||
joy_prob
|
||||
|
||||
sadness_prob
|
||||
|
||||
disgust_prob
|
||||
"""
|
||||
|
||||
def __init__(self, environment, unique_id=0, state=()):
|
||||
super().__init__(model=environment, unique_id=unique_id, state=state)
|
||||
self.outside_effects_prob = environment.environment_params['outside_effects_prob']
|
||||
self.anger_prob = environment.environment_params['anger_prob']
|
||||
self.joy_prob = environment.environment_params['joy_prob']
|
||||
self.sadness_prob = environment.environment_params['sadness_prob']
|
||||
self.disgust_prob = environment.environment_params['disgust_prob']
|
||||
self.state['time_awareness'] = []
|
||||
for i in range(4): # In this model we have 4 sentiments
|
||||
self.state['time_awareness'].append(0) # 0-> Anger, 1-> joy, 2->sadness, 3 -> disgust
|
||||
self.state['sentimentCorrelation'] = 0
|
||||
|
||||
def step(self):
|
||||
self.behaviour()
|
||||
|
||||
def behaviour(self):
|
||||
|
||||
angry_neighbors_1_time_step = []
|
||||
joyful_neighbors_1_time_step = []
|
||||
sad_neighbors_1_time_step = []
|
||||
disgusted_neighbors_1_time_step = []
|
||||
|
||||
angry_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for x in angry_neighbors:
|
||||
if x.state['time_awareness'][0] > (self.env.now-500):
|
||||
angry_neighbors_1_time_step.append(x)
|
||||
num_neighbors_angry = len(angry_neighbors_1_time_step)
|
||||
|
||||
joyful_neighbors = self.get_neighboring_agents(state_id=2)
|
||||
for x in joyful_neighbors:
|
||||
if x.state['time_awareness'][1] > (self.env.now-500):
|
||||
joyful_neighbors_1_time_step.append(x)
|
||||
num_neighbors_joyful = len(joyful_neighbors_1_time_step)
|
||||
|
||||
sad_neighbors = self.get_neighboring_agents(state_id=3)
|
||||
for x in sad_neighbors:
|
||||
if x.state['time_awareness'][2] > (self.env.now-500):
|
||||
sad_neighbors_1_time_step.append(x)
|
||||
num_neighbors_sad = len(sad_neighbors_1_time_step)
|
||||
|
||||
disgusted_neighbors = self.get_neighboring_agents(state_id=4)
|
||||
for x in disgusted_neighbors:
|
||||
if x.state['time_awareness'][3] > (self.env.now-500):
|
||||
disgusted_neighbors_1_time_step.append(x)
|
||||
num_neighbors_disgusted = len(disgusted_neighbors_1_time_step)
|
||||
|
||||
anger_prob = self.anger_prob+(len(angry_neighbors_1_time_step)*self.anger_prob)
|
||||
joy_prob = self.joy_prob+(len(joyful_neighbors_1_time_step)*self.joy_prob)
|
||||
sadness_prob = self.sadness_prob+(len(sad_neighbors_1_time_step)*self.sadness_prob)
|
||||
disgust_prob = self.disgust_prob+(len(disgusted_neighbors_1_time_step)*self.disgust_prob)
|
||||
outside_effects_prob = self.outside_effects_prob
|
||||
|
||||
num = random.random()
|
||||
|
||||
if num<outside_effects_prob:
|
||||
self.state['id'] = random.randint(1, 4)
|
||||
|
||||
self.state['sentimentCorrelation'] = self.state['id'] # It is stored when it has been infected for the dynamic network
|
||||
self.state['time_awareness'][self.state['id']-1] = self.env.now
|
||||
self.state['sentiment'] = self.state['id']
|
||||
|
||||
|
||||
if(num<anger_prob):
|
||||
|
||||
self.state['id'] = 1
|
||||
self.state['sentimentCorrelation'] = 1
|
||||
self.state['time_awareness'][self.state['id']-1] = self.env.now
|
||||
elif (num<joy_prob+anger_prob and num>anger_prob):
|
||||
|
||||
self.state['id'] = 2
|
||||
self.state['sentimentCorrelation'] = 2
|
||||
self.state['time_awareness'][self.state['id']-1] = self.env.now
|
||||
elif (num<sadness_prob+anger_prob+joy_prob and num>joy_prob+anger_prob):
|
||||
|
||||
self.state['id'] = 3
|
||||
self.state['sentimentCorrelation'] = 3
|
||||
self.state['time_awareness'][self.state['id']-1] = self.env.now
|
||||
elif (num<disgust_prob+sadness_prob+anger_prob+joy_prob and num>sadness_prob+anger_prob+joy_prob):
|
||||
|
||||
self.state['id'] = 4
|
||||
self.state['sentimentCorrelation'] = 4
|
||||
self.state['time_awareness'][self.state['id']-1] = self.env.now
|
||||
|
||||
self.state['sentiment'] = self.state['id']
|
@ -0,0 +1,77 @@
|
||||
from . import BaseAgent
|
||||
from ..events import Message, Tell, Ask, TimedOut
|
||||
from ..time import BaseCond
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
|
||||
|
||||
class ReceivedOrTimeout(BaseCond):
|
||||
def __init__(
|
||||
self, agent, expiration=None, timeout=None, check=True, ignore=False, **kwargs
|
||||
):
|
||||
if expiration is None:
|
||||
if timeout is not None:
|
||||
expiration = agent.now + timeout
|
||||
self.expiration = expiration
|
||||
self.ignore = ignore
|
||||
self.check = check
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def expired(self, time):
|
||||
return self.expiration and self.expiration < time
|
||||
|
||||
def ready(self, agent, time):
|
||||
return len(agent._inbox) or self.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if not self.ignore and self.expired(agent.now):
|
||||
raise TimedOut("No messages received")
|
||||
if self.check:
|
||||
agent.check_messages()
|
||||
return None
|
||||
|
||||
def schedule_next(self, time, delta, first=False):
|
||||
if self._delta is not None:
|
||||
delta = self._delta
|
||||
return (time + delta, self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReceivedOrTimeout(expires={self.expiration})"
|
||||
|
||||
|
||||
class EventedAgent(BaseAgent):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._inbox = deque()
|
||||
self._processed = 0
|
||||
|
||||
def on_receive(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def received(self, *args, **kwargs):
|
||||
return ReceivedOrTimeout(self, *args, **kwargs)
|
||||
|
||||
def tell(self, msg, sender=None):
|
||||
self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender))
|
||||
|
||||
def ask(self, msg, timeout=None, **kwargs):
|
||||
ask = Ask(timestamp=self.now, payload=msg, sender=self)
|
||||
self._inbox.append(ask)
|
||||
expiration = float("inf") if timeout is None else self.now + timeout
|
||||
return ask.replied(expiration=expiration, **kwargs)
|
||||
|
||||
def check_messages(self):
|
||||
changed = False
|
||||
while self._inbox:
|
||||
msg = self._inbox.popleft()
|
||||
self._processed += 1
|
||||
if msg.expired(self.now):
|
||||
continue
|
||||
changed = True
|
||||
reply = self.on_receive(msg.payload, sender=msg.sender)
|
||||
if isinstance(msg, Ask):
|
||||
msg.reply = reply
|
||||
return changed
|
||||
|
||||
|
||||
Evented = EventedAgent
|
@ -0,0 +1,148 @@
|
||||
from . import MetaAgent, BaseAgent
|
||||
from ..time import Delta
|
||||
|
||||
from functools import partial, wraps
|
||||
import inspect
|
||||
|
||||
|
||||
def state(name=None, default=False):
|
||||
def decorator(func, name=None):
|
||||
"""
|
||||
A state function should return either a state id, or a tuple (state_id, when)
|
||||
The default value for state_id is the current state id.
|
||||
The default value for when is the interval defined in the environment.
|
||||
"""
|
||||
if inspect.isgeneratorfunction(func):
|
||||
orig_func = func
|
||||
|
||||
@wraps(func)
|
||||
def func(self):
|
||||
while True:
|
||||
if not self._coroutine:
|
||||
self._coroutine = orig_func(self)
|
||||
|
||||
try:
|
||||
if self._last_except:
|
||||
n = self._coroutine.throw(self._last_except)
|
||||
else:
|
||||
n = self._coroutine.send(self._last_return)
|
||||
if n:
|
||||
return None, n
|
||||
return n
|
||||
except StopIteration as ex:
|
||||
self._coroutine = None
|
||||
next_state = ex.value
|
||||
if next_state is not None:
|
||||
self._set_state(next_state)
|
||||
return next_state
|
||||
finally:
|
||||
self._last_return = None
|
||||
self._last_except = None
|
||||
|
||||
func.id = name or func.__name__
|
||||
func.is_default = default
|
||||
return func
|
||||
|
||||
if callable(name):
|
||||
return decorator(name)
|
||||
else:
|
||||
return partial(decorator, name=name)
|
||||
|
||||
|
||||
def default_state(func):
|
||||
func.is_default = True
|
||||
return func
|
||||
|
||||
|
||||
class MetaFSM(MetaAgent):
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
states = {}
|
||||
# Re-use states from inherited classes
|
||||
default_state = None
|
||||
for i in bases:
|
||||
if isinstance(i, MetaFSM):
|
||||
for state_id, state in i._states.items():
|
||||
if state.is_default:
|
||||
default_state = state
|
||||
states[state_id] = state
|
||||
|
||||
# Add new states
|
||||
for attr, func in namespace.items():
|
||||
if hasattr(func, "id"):
|
||||
if func.is_default:
|
||||
default_state = func
|
||||
states[func.id] = func
|
||||
|
||||
namespace.update(
|
||||
{
|
||||
"_default_state": default_state,
|
||||
"_states": states,
|
||||
}
|
||||
)
|
||||
|
||||
return super(MetaFSM, mcls).__new__(
|
||||
mcls=mcls, name=name, bases=bases, namespace=namespace
|
||||
)
|
||||
|
||||
|
||||
class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
def __init__(self, init=True, **kwargs):
|
||||
super().__init__(**kwargs, init=False)
|
||||
if not hasattr(self, "state_id"):
|
||||
if not self._default_state:
|
||||
raise ValueError(
|
||||
"No default state specified for {}".format(self.unique_id)
|
||||
)
|
||||
self.state_id = self._default_state.id
|
||||
|
||||
self._coroutine = None
|
||||
self.default_interval = Delta(self.model.interval)
|
||||
self._set_state(self.state_id)
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
@classmethod
|
||||
def states(cls):
|
||||
return list(cls._states.keys())
|
||||
|
||||
def step(self):
|
||||
self.debug(f"Agent {self.unique_id} @ state {self.state_id}")
|
||||
|
||||
self._check_alive()
|
||||
next_state = self._states[self.state_id](self)
|
||||
|
||||
when = None
|
||||
try:
|
||||
next_state, *when = next_state
|
||||
if not when:
|
||||
when = None
|
||||
elif len(when) == 1:
|
||||
when = when[0]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Too many values returned. Only state (and time) allowed"
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if next_state is not None:
|
||||
self._set_state(next_state)
|
||||
|
||||
return when or self.default_interval
|
||||
|
||||
def _set_state(self, state, when=None):
|
||||
if hasattr(state, "id"):
|
||||
state = state.id
|
||||
if state not in self._states:
|
||||
raise ValueError("{} is not a valid state".format(state))
|
||||
self.state_id = state
|
||||
if when is not None:
|
||||
self.model.schedule.add(self, when=when)
|
||||
return state
|
||||
|
||||
def die(self, *args, **kwargs):
|
||||
return self.dead, super().die(*args, **kwargs)
|
||||
|
||||
@state
|
||||
def dead(self):
|
||||
return self.die()
|
@ -0,0 +1,100 @@
|
||||
from . import BaseAgent
|
||||
|
||||
|
||||
class NetworkAgent(BaseAgent):
|
||||
def __init__(self, *args, topology=None, init=True, node_id=None, **kwargs):
|
||||
super().__init__(*args, init=False, **kwargs)
|
||||
|
||||
self.G = topology or self.model.G
|
||||
assert self.G
|
||||
if node_id is None:
|
||||
nodes = self.random.choices(list(self.G.nodes), k=len(self.G))
|
||||
for n_id in nodes:
|
||||
if "agent" not in self.G.nodes[n_id] or self.G.nodes[n_id]["agent"] is None:
|
||||
node_id = n_id
|
||||
break
|
||||
else:
|
||||
node_id = len(self.G)
|
||||
self.info(f"All nodes ({len(self.G)}) have an agent assigned, adding a new node to the graph for agent {self.unique_id}")
|
||||
self.G.add_node(node_id)
|
||||
assert node_id is not None
|
||||
self.G.nodes[node_id]["agent"] = self
|
||||
self.node_id = node_id
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
def count_neighbors(self, state_id=None, **kwargs):
|
||||
return len(self.get_neighbors(state_id=state_id, **kwargs))
|
||||
if init:
|
||||
self.init()
|
||||
|
||||
def iter_neighbors(self, **kwargs):
|
||||
return self.iter_agents(limit_neighbors=True, **kwargs)
|
||||
|
||||
def get_neighbors(self, **kwargs):
|
||||
return list(self.iter_neighbors(**kwargs))
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
return self.G.nodes[self.node_id]
|
||||
|
||||
def iter_agents(self, unique_id=None, *, limit_neighbors=False, **kwargs):
|
||||
unique_ids = None
|
||||
if unique_ids is not None:
|
||||
try:
|
||||
unique_ids = set(unique_id)
|
||||
except TypeError:
|
||||
unique_ids = set([unique_id])
|
||||
|
||||
if limit_neighbors:
|
||||
neighbor_ids = set()
|
||||
for node_id in self.G.neighbors(self.node_id):
|
||||
agent = self.G.nodes[node_id].get("agent")
|
||||
if agent is not None:
|
||||
neighbor_ids.add(agent.unique_id)
|
||||
if unique_ids:
|
||||
unique_ids = unique_ids & neighbor_ids
|
||||
else:
|
||||
unique_ids = neighbor_ids
|
||||
if not unique_ids:
|
||||
return
|
||||
unique_ids = list(unique_ids)
|
||||
yield from super().iter_agents(unique_id=unique_ids, **kwargs)
|
||||
|
||||
def subgraph(self, center=True, **kwargs):
|
||||
include = [self] if center else []
|
||||
G = self.G.subgraph(
|
||||
n.node_id for n in list(self.get_agents(**kwargs) + include)
|
||||
)
|
||||
return G
|
||||
|
||||
def remove_node(self):
|
||||
self.debug(f"Removing node for {self.unique_id}: {self.node_id}")
|
||||
self.G.remove_node(self.node_id)
|
||||
self.node_id = None
|
||||
|
||||
def add_edge(self, other, edge_attr_dict=None, *edge_attrs):
|
||||
if self.node_id not in self.G.nodes(data=False):
|
||||
raise ValueError(
|
||||
"{} not in list of existing agents in the network".format(
|
||||
self.unique_id
|
||||
)
|
||||
)
|
||||
if other.node_id not in self.G.nodes(data=False):
|
||||
raise ValueError(
|
||||
"{} not in list of existing agents in the network".format(other)
|
||||
)
|
||||
|
||||
self.G.add_edge(
|
||||
self.node_id, other.node_id, edge_attr_dict=edge_attr_dict, *edge_attrs
|
||||
)
|
||||
|
||||
def die(self, remove=True):
|
||||
if not self.alive:
|
||||
return None
|
||||
if remove:
|
||||
self.remove_node()
|
||||
return super().die()
|
||||
|
||||
|
||||
NetAgent = NetworkAgent
|
@ -1,206 +1,49 @@
|
||||
import os
|
||||
import sqlalchemy
|
||||
import pandas as pd
|
||||
|
||||
import glob
|
||||
import yaml
|
||||
from os.path import join
|
||||
|
||||
from . import serialization
|
||||
from tsih import History
|
||||
|
||||
|
||||
def read_data(*args, group=False, **kwargs):
|
||||
iterable = _read_data(*args, **kwargs)
|
||||
if group:
|
||||
return group_trials(iterable)
|
||||
from collections import namedtuple
|
||||
|
||||
def plot(env, agent_df=None, model_df=None, steps=False, ignore=["agent_count", ]):
|
||||
"""Plot the model dataframe and agent dataframe together."""
|
||||
if agent_df is None:
|
||||
agent_df = env.agent_df()
|
||||
if model_df is None:
|
||||
model_df = env.model_df()
|
||||
ignore = list(ignore)
|
||||
if not steps:
|
||||
ignore.append("step")
|
||||
else:
|
||||
return list(iterable)
|
||||
|
||||
|
||||
def _read_data(pattern, *args, from_csv=False, process_args=None, **kwargs):
|
||||
if not process_args:
|
||||
process_args = {}
|
||||
for folder in glob.glob(pattern):
|
||||
config_file = glob.glob(join(folder, '*.yml'))[0]
|
||||
config = yaml.load(open(config_file), Loader=yaml.SafeLoader)
|
||||
df = None
|
||||
if from_csv:
|
||||
for trial_data in sorted(glob.glob(join(folder,
|
||||
'*.environment.csv'))):
|
||||
df = read_csv(trial_data, **kwargs)
|
||||
yield config_file, df, config
|
||||
else:
|
||||
for trial_data in sorted(glob.glob(join(folder, '*.sqlite'))):
|
||||
df = read_sql(trial_data, **kwargs)
|
||||
yield config_file, df, config
|
||||
|
||||
|
||||
def read_sql(db, *args, **kwargs):
|
||||
h = History(db_path=db, backup=False, readonly=True)
|
||||
df = h.read_sql(*args, **kwargs)
|
||||
return df
|
||||
|
||||
|
||||
def read_csv(filename, keys=None, convert_types=False, **kwargs):
|
||||
'''
|
||||
Read a CSV in canonical form: ::
|
||||
|
||||
<agent_id, t_step, key, value, value_type>
|
||||
|
||||
'''
|
||||
df = pd.read_csv(filename)
|
||||
if convert_types:
|
||||
df = convert_types_slow(df)
|
||||
if keys:
|
||||
df = df[df['key'].isin(keys)]
|
||||
df = process_one(df)
|
||||
return df
|
||||
|
||||
|
||||
def convert_row(row):
|
||||
row['value'] = serialization.deserialize(row['value_type'], row['value'])
|
||||
return row
|
||||
|
||||
|
||||
def convert_types_slow(df):
|
||||
'''
|
||||
Go over every column in a dataframe and convert it to the type determined by the `get_types`
|
||||
function.
|
||||
|
||||
This is a slow operation.
|
||||
'''
|
||||
dtypes = get_types(df)
|
||||
for k, v in dtypes.items():
|
||||
t = df[df['key']==k]
|
||||
t['value'] = t['value'].astype(v)
|
||||
df = df.apply(convert_row, axis=1)
|
||||
return df
|
||||
|
||||
|
||||
def split_processed(df):
|
||||
env = df.loc[:, df.columns.get_level_values(1).isin(['env', 'stats'])]
|
||||
agents = df.loc[:, ~df.columns.get_level_values(1).isin(['env', 'stats'])]
|
||||
return env, agents
|
||||
|
||||
|
||||
def split_df(df):
|
||||
'''
|
||||
Split a dataframe in two dataframes: one with the history of agents,
|
||||
and one with the environment history
|
||||
'''
|
||||
envmask = (df['agent_id'] == 'env')
|
||||
n_env = envmask.sum()
|
||||
if n_env == len(df):
|
||||
return df, None
|
||||
elif n_env == 0:
|
||||
return None, df
|
||||
agents, env = [x for _, x in df.groupby(envmask)]
|
||||
return env, agents
|
||||
|
||||
|
||||
def process(df, **kwargs):
|
||||
'''
|
||||
Process a dataframe in canonical form ``(t_step, agent_id, key, value, value_type)`` into
|
||||
two dataframes with a column per key: one with the history of the agents, and one for the
|
||||
history of the environment.
|
||||
'''
|
||||
env, agents = split_df(df)
|
||||
return process_one(env, **kwargs), process_one(agents, **kwargs)
|
||||
|
||||
|
||||
def get_types(df):
|
||||
'''
|
||||
Get the value type for every key stored in a raw history dataframe.
|
||||
'''
|
||||
dtypes = df.groupby(by=['key'])['value_type'].unique()
|
||||
return {k:v[0] for k,v in dtypes.items()}
|
||||
|
||||
|
||||
def process_one(df, *keys, columns=['key', 'agent_id'], values='value',
|
||||
fill=True, index=['t_step',],
|
||||
aggfunc='first', **kwargs):
|
||||
'''
|
||||
Process a dataframe in canonical form ``(t_step, agent_id, key, value, value_type)`` into
|
||||
a dataframe with a column per key
|
||||
'''
|
||||
if df is None:
|
||||
return df
|
||||
if keys:
|
||||
df = df[df['key'].isin(keys)]
|
||||
|
||||
df = df.pivot_table(values=values, index=index, columns=columns,
|
||||
aggfunc=aggfunc, **kwargs)
|
||||
if fill:
|
||||
df = fillna(df)
|
||||
return df
|
||||
|
||||
|
||||
def get_count(df, *keys):
|
||||
'''
|
||||
For every t_step and key, get the value count.
|
||||
|
||||
The result is a dataframe with `t_step` as index, an a multiindex column based on `key` and the values found for each `key`.
|
||||
'''
|
||||
if keys:
|
||||
df = df[list(keys)]
|
||||
df.columns = df.columns.remove_unused_levels()
|
||||
counts = pd.DataFrame()
|
||||
for key in df.columns.levels[0]:
|
||||
g = df[[key]].apply(pd.Series.value_counts, axis=1).fillna(0)
|
||||
for value, series in g.items():
|
||||
counts[key, value] = series
|
||||
counts.columns = pd.MultiIndex.from_tuples(counts.columns)
|
||||
return counts
|
||||
|
||||
|
||||
def get_majority(df, *keys):
|
||||
'''
|
||||
For every t_step and key, get the value of the majority of agents
|
||||
|
||||
The result is a dataframe with `t_step` as index, and columns based on `key`.
|
||||
'''
|
||||
df = get_count(df, *keys)
|
||||
return df.stack(level=0).idxmax(axis=1).unstack()
|
||||
|
||||
|
||||
def get_value(df, *keys, aggfunc='sum'):
|
||||
'''
|
||||
For every t_step and key, get the value of *numeric columns*, aggregated using a specific function.
|
||||
'''
|
||||
if keys:
|
||||
df = df[list(keys)]
|
||||
df.columns = df.columns.remove_unused_levels()
|
||||
df = df.select_dtypes('number')
|
||||
return df.groupby(level='key', axis=1).agg(aggfunc)
|
||||
|
||||
|
||||
def plot_all(*args, plot_args={}, **kwargs):
|
||||
'''
|
||||
Read all the trial data and plot the result of applying a function on them.
|
||||
'''
|
||||
dfs = do_all(*args, **kwargs)
|
||||
ps = []
|
||||
for line in dfs:
|
||||
f, df, config = line
|
||||
if len(df) < 1:
|
||||
continue
|
||||
df.plot(title=config['name'], **plot_args)
|
||||
ps.append(df)
|
||||
return ps
|
||||
|
||||
def do_all(pattern, func, *keys, include_env=False, **kwargs):
|
||||
for config_file, df, config in read_data(pattern, keys=keys):
|
||||
if len(df) < 1:
|
||||
continue
|
||||
p = func(df, *keys, **kwargs)
|
||||
yield config_file, p, config
|
||||
|
||||
|
||||
def group_trials(trials, aggfunc=['mean', 'min', 'max', 'std']):
|
||||
trials = list(trials)
|
||||
trials = list(map(lambda x: x[1] if isinstance(x, tuple) else x, trials))
|
||||
return pd.concat(trials).groupby(level=0).agg(aggfunc).reorder_levels([2, 0,1] ,axis=1)
|
||||
|
||||
|
||||
def fillna(df):
|
||||
new_df = df.ffill(axis=0)
|
||||
return new_df
|
||||
ignore.append("time")
|
||||
|
||||
ax = model_df.drop(ignore, axis='columns').plot();
|
||||
if not agent_df.empty:
|
||||
agent_df.unstack().apply(lambda x: x.value_counts(),
|
||||
axis=1).fillna(0).plot(ax=ax, secondary_y=True);
|
||||
|
||||
Results = namedtuple("Results", ["config", "parameters", "env", "agents"])
|
||||
#TODO implement reading from CSV and SQLITE
|
||||
def read_sql(fpath=None, name=None, include_agents=False):
|
||||
if not (fpath is None) ^ (name is None):
|
||||
raise ValueError("Specify either a path or a simulation name")
|
||||
if name:
|
||||
fpath = os.path.join("soil_output", name, f"{name}.sqlite")
|
||||
fpath = os.path.abspath(fpath)
|
||||
# TODO: improve url parsing. This is a hacky way to check we weren't given a URL
|
||||
if "://" not in fpath:
|
||||
fpath = f"sqlite:///{fpath}"
|
||||
engine = sqlalchemy.create_engine(fpath)
|
||||
with engine.connect() as conn:
|
||||
env = pd.read_sql_table("env", con=conn,
|
||||
index_col="step").reset_index().set_index([
|
||||
"simulation_id", "params_id",
|
||||
"iteration_id", "step"
|
||||
])
|
||||
agents = pd.read_sql_table("agents", con=conn, index_col=["simulation_id", "params_id", "iteration_id", "step", "agent_id"])
|
||||
config = pd.read_sql_table("configuration", con=conn, index_col="simulation_id")
|
||||
parameters = pd.read_sql_table("parameters", con=conn, index_col=["iteration_id", "params_id", "simulation_id"])
|
||||
try:
|
||||
parameters = parameters.pivot(columns="key", values="value")
|
||||
except Exception as e:
|
||||
print(f"warning: coult not pivot parameters: {e}")
|
||||
|
||||
return Results(config, parameters, env, agents)
|
||||
|
@ -0,0 +1,2 @@
|
||||
def load_config(cfg):
|
||||
return cfg
|
@ -1,26 +1,19 @@
|
||||
from mesa import DataCollector as MDC
|
||||
|
||||
class SoilDataCollector(MDC):
|
||||
|
||||
|
||||
def __init__(self, environment, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Populate model and env reporters so they have a key per
|
||||
# So they can be shown in the web interface
|
||||
self.environment = environment
|
||||
|
||||
|
||||
@property
|
||||
def model_vars(self):
|
||||
pass
|
||||
|
||||
@model_vars.setter
|
||||
def model_vars(self, value):
|
||||
pass
|
||||
|
||||
@property
|
||||
def agent_reporters(self):
|
||||
self.model._history._
|
||||
|
||||
pass
|
||||
|
||||
class SoilCollector(MDC):
|
||||
def __init__(self, model_reporters=None, agent_reporters=None, tables=None, **kwargs):
|
||||
model_reporters = model_reporters or {}
|
||||
agent_reporters = agent_reporters or {}
|
||||
tables = tables or {}
|
||||
if 'agent_count' not in model_reporters:
|
||||
model_reporters['agent_count'] = lambda m: m.schedule.get_agent_count()
|
||||
if 'time' not in model_reporters:
|
||||
model_reporters['time'] = lambda m: m.now
|
||||
# if 'state_id' not in agent_reporters:
|
||||
# agent_reporters['state_id'] = lambda agent: getattr(agent, 'state_id', None)
|
||||
|
||||
super().__init__(model_reporters=model_reporters,
|
||||
agent_reporters=agent_reporters,
|
||||
tables=tables,
|
||||
**kwargs)
|
||||
|
@ -0,0 +1,243 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pdb
|
||||
import sys
|
||||
import os
|
||||
|
||||
from textwrap import indent
|
||||
from functools import wraps
|
||||
|
||||
from .agents import FSM, MetaFSM
|
||||
from mesa import Model, Agent
|
||||
|
||||
|
||||
def wrapcmd(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, arg: str, temporary=False):
|
||||
sys.settrace(self.trace_dispatch)
|
||||
|
||||
lastself = self
|
||||
known = globals()
|
||||
known.update(self.curframe.f_globals)
|
||||
known.update(self.curframe.f_locals)
|
||||
known["attrs"] = arg.strip().split()
|
||||
|
||||
this = known.get("self", None)
|
||||
|
||||
if isinstance(this, Model):
|
||||
known["model"] = this
|
||||
elif isinstance(this, Agent):
|
||||
known["agent"] = this
|
||||
known["model"] = this.model
|
||||
|
||||
known["self"] = lastself
|
||||
return exec(func.__code__, known, known)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Debug(pdb.Pdb):
|
||||
def __init__(self, *args, skip_soil=False, **kwargs):
|
||||
skip = kwargs.get("skip", [])
|
||||
if skip_soil:
|
||||
skip.append("soil")
|
||||
skip.append("contextlib")
|
||||
skip.append("soil.*")
|
||||
skip.append("mesa.*")
|
||||
super(Debug, self).__init__(*args, skip=skip, **kwargs)
|
||||
self.prompt = "[soil-pdb] "
|
||||
|
||||
@staticmethod
|
||||
def _soil_agents(model, attrs=None, pretty=True, **kwargs):
|
||||
for agent in model.agents(**kwargs):
|
||||
d = agent
|
||||
print(" - " + indent(agent.to_str(keys=attrs, pretty=pretty), " "))
|
||||
|
||||
@wrapcmd
|
||||
def do_soil_agents():
|
||||
return Debug._soil_agents(model, attrs=attrs or None)
|
||||
|
||||
do_sa = do_soil_agents
|
||||
|
||||
@wrapcmd
|
||||
def do_soil_list():
|
||||
return Debug._soil_agents(model, attrs=["state_id"], pretty=False)
|
||||
|
||||
do_sl = do_soil_list
|
||||
|
||||
def do_continue_state(self, arg):
|
||||
"""Continue until next time this state is reached"""
|
||||
self.do_break_state(arg, temporary=True)
|
||||
return self.do_continue("")
|
||||
|
||||
do_cs = do_continue_state
|
||||
|
||||
@wrapcmd
|
||||
def do_soil_agent():
|
||||
if not agent:
|
||||
print("No agent available")
|
||||
return
|
||||
|
||||
keys = None
|
||||
if attrs:
|
||||
keys = []
|
||||
for k in attrs:
|
||||
for key in agent.keys():
|
||||
if key.startswith(k):
|
||||
keys.append(key)
|
||||
|
||||
print(agent.to_str(pretty=True, keys=keys))
|
||||
|
||||
do_aa = do_soil_agent
|
||||
|
||||
def do_break_step(self, arg: str):
|
||||
"""
|
||||
Break before the next step.
|
||||
"""
|
||||
try:
|
||||
known = globals()
|
||||
known.update(self.curframe.f_globals)
|
||||
known.update(self.curframe.f_locals)
|
||||
func = getattr(known["model"], "step")
|
||||
except AttributeError as ex:
|
||||
self.error(f"The model does not have a step function: {ex}")
|
||||
return
|
||||
if hasattr(func, "__func__"):
|
||||
func = func.__func__
|
||||
|
||||
code = func.__code__
|
||||
# use co_name to identify the bkpt (function names
|
||||
# could be aliased, but co_name is invariant)
|
||||
funcname = code.co_name
|
||||
lineno = code.co_firstlineno
|
||||
filename = code.co_filename
|
||||
|
||||
# Check for reasonable breakpoint
|
||||
line = self.checkline(filename, lineno)
|
||||
if not line:
|
||||
raise ValueError("no line found")
|
||||
# now set the break point
|
||||
|
||||
existing = self.get_breaks(filename, line)
|
||||
if existing:
|
||||
self.message("Breakpoint already exists at %s:%d" % (filename, line))
|
||||
return
|
||||
cond = f"self.schedule.steps > {model.schedule.steps}"
|
||||
err = self.set_break(filename, line, True, cond, funcname)
|
||||
if err:
|
||||
self.error(err)
|
||||
else:
|
||||
bp = self.get_breaks(filename, line)[-1]
|
||||
self.message("Breakpoint %d at %s:%d" % (bp.number, bp.file, bp.line))
|
||||
return self.do_continue("")
|
||||
|
||||
do_bstep = do_break_step
|
||||
|
||||
def do_break_state(self, arg: str, instances=None, temporary=False):
|
||||
"""
|
||||
Break before a specified state is stepped into.
|
||||
"""
|
||||
|
||||
klass = None
|
||||
state = arg
|
||||
if not state:
|
||||
self.error("Specify at least a state name")
|
||||
return
|
||||
|
||||
state, *tokens = state.lstrip().split()
|
||||
if tokens:
|
||||
instances = list(eval(token) for token in tokens)
|
||||
|
||||
colon = state.find(":")
|
||||
|
||||
if colon > 0:
|
||||
klass = state[:colon].rstrip()
|
||||
state = state[colon + 1 :].strip()
|
||||
|
||||
print(klass, state, tokens)
|
||||
klass = eval(klass, self.curframe.f_globals, self.curframe_locals)
|
||||
|
||||
if klass:
|
||||
klasses = [klass]
|
||||
else:
|
||||
klasses = [
|
||||
k
|
||||
for k in self.curframe.f_globals.values()
|
||||
if isinstance(k, type) and issubclass(k, FSM)
|
||||
]
|
||||
|
||||
if not klasses:
|
||||
self.error("No agent classes found")
|
||||
|
||||
for klass in klasses:
|
||||
try:
|
||||
func = getattr(klass, state)
|
||||
except AttributeError:
|
||||
self.error(f"State {state} not found in class {klass}")
|
||||
continue
|
||||
if hasattr(func, "__func__"):
|
||||
func = func.__func__
|
||||
|
||||
code = func.__code__
|
||||
# use co_name to identify the bkpt (function names
|
||||
# could be aliased, but co_name is invariant)
|
||||
funcname = code.co_name
|
||||
lineno = code.co_firstlineno
|
||||
filename = code.co_filename
|
||||
|
||||
# Check for reasonable breakpoint
|
||||
line = self.checkline(filename, lineno)
|
||||
if not line:
|
||||
raise ValueError("no line found")
|
||||
# now set the break point
|
||||
cond = None
|
||||
if instances:
|
||||
cond = f"self.unique_id in { repr(instances) }"
|
||||
|
||||
existing = self.get_breaks(filename, line)
|
||||
if existing:
|
||||
self.message("Breakpoint already exists at %s:%d" % (filename, line))
|
||||
continue
|
||||
err = self.set_break(filename, line, temporary, cond, funcname)
|
||||
if err:
|
||||
self.error(err)
|
||||
else:
|
||||
bp = self.get_breaks(filename, line)[-1]
|
||||
self.message("Breakpoint %d at %s:%d" % (bp.number, bp.file, bp.line))
|
||||
|
||||
do_bs = do_break_state
|
||||
|
||||
def do_break_state_self(self, arg: str, temporary=False):
|
||||
"""
|
||||
Break before a specified state is stepped into, for the current agent
|
||||
"""
|
||||
agent = self.curframe.f_locals.get("self")
|
||||
if not agent:
|
||||
self.error("No current agent.")
|
||||
self.error("Try this again when the debugger is stopped inside an agent")
|
||||
return
|
||||
|
||||
arg = f"{agent.__class__.__name__}:{ arg } {agent.unique_id}"
|
||||
return self.do_break_state(arg)
|
||||
|
||||
do_bss = do_break_state_self
|
||||
|
||||
|
||||
debugger = None
|
||||
|
||||
|
||||
def set_trace(frame=None, **kwargs):
|
||||
global debugger
|
||||
if debugger is None:
|
||||
debugger = Debug(**kwargs)
|
||||
frame = frame or sys._getframe().f_back
|
||||
debugger.set_trace(frame)
|
||||
|
||||
|
||||
def post_mortem(traceback=None, **kwargs):
|
||||
global debugger
|
||||
if debugger is None:
|
||||
debugger = Debug(**kwargs)
|
||||
t = sys.exc_info()[2]
|
||||
debugger.reset()
|
||||
debugger.interaction(None, t)
|
@ -0,0 +1,6 @@
|
||||
def report(f: property):
|
||||
if isinstance(f, property):
|
||||
setattr(f.fget, "add_to_report", True)
|
||||
else:
|
||||
setattr(f, "add_to_report", True)
|
||||
return f
|
@ -0,0 +1,56 @@
|
||||
from .time import BaseCond
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class Event:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
payload: Any
|
||||
sender: Any = None
|
||||
expiration: float = None
|
||||
timestamp: float = None
|
||||
id: int = field(default_factory=uuid4)
|
||||
|
||||
def expired(self, when):
|
||||
return self.expiration is not None and self.expiration < when
|
||||
|
||||
|
||||
class Reply(Message):
|
||||
source: Message
|
||||
|
||||
|
||||
class ReplyCond(BaseCond):
|
||||
def __init__(self, ask, *args, **kwargs):
|
||||
self._ask = ask
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self, agent, time):
|
||||
return self._ask.reply is not None or self._ask.expired(time)
|
||||
|
||||
def return_value(self, agent):
|
||||
if self._ask.expired(agent.now):
|
||||
raise TimedOut()
|
||||
return self._ask.reply
|
||||
|
||||
def __repr__(self):
|
||||
return f"ReplyCond({self._ask.id})"
|
||||
|
||||
|
||||
class Ask(Message):
|
||||
reply: Message = None
|
||||
|
||||
def replied(self, expiration=None):
|
||||
return ReplyCond(self)
|
||||
|
||||
|
||||
class Tell(Message):
|
||||
pass
|
||||
|
||||
|
||||
class TimedOut(Exception):
|
||||
pass
|
@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from . import config, serialization, basestring
|
||||
|
||||
|
||||
def from_topology(topology, dir_path: str = None):
|
||||
if topology is None:
|
||||
return nx.Graph()
|
||||
if isinstance(topology, nx.Graph):
|
||||
return topology
|
||||
|
||||
# If it's a dict, assume it's a node-link graph
|
||||
if isinstance(topology, dict):
|
||||
try:
|
||||
return nx.json_graph.node_link_graph(topology)
|
||||
except Exception as ex:
|
||||
raise ValueError("Unknown topology format")
|
||||
|
||||
# Otherwise, treat like a path
|
||||
path = topology
|
||||
if dir_path and not os.path.isabs(path):
|
||||
path = os.path.join(dir_path, path)
|
||||
extension = os.path.splitext(path)[1][1:]
|
||||
kwargs = {}
|
||||
if extension == "gexf":
|
||||
kwargs["version"] = "1.2draft"
|
||||
kwargs["node_type"] = int
|
||||
try:
|
||||
method = getattr(nx.readwrite, "read_" + extension)
|
||||
except AttributeError:
|
||||
raise AttributeError("Unknown format")
|
||||
return method(path, **kwargs)
|
||||
|
||||
|
||||
def from_params(generator, dir_path: str = None, **params):
|
||||
|
||||
if dir_path not in sys.path:
|
||||
sys.path.append(dir_path)
|
||||
|
||||
method = serialization.deserializer(
|
||||
generator,
|
||||
known_modules=[
|
||||
"networkx.generators",
|
||||
],
|
||||
)
|
||||
return method(**params)
|
||||
|
||||
|
||||
def find_unassigned(G, shuffle=False, random=random):
|
||||
"""
|
||||
Link an agent to a node in a topology.
|
||||
|
||||
If node_id is None, a node without an agent_id will be found.
|
||||
"""
|
||||
candidates = list(G.nodes(data=True))
|
||||
if shuffle:
|
||||
random.shuffle(candidates)
|
||||
for next_id, data in candidates:
|
||||
if "agent" not in data:
|
||||
return next_id
|
||||
return None
|
||||
|
||||
|
||||
def dump_gexf(G, f):
|
||||
for node in G.nodes():
|
||||
if "pos" in G.nodes[node]:
|
||||
G.nodes[node]["viz"] = {
|
||||
"position": {
|
||||
"x": G.nodes[node]["pos"][0],
|
||||
"y": G.nodes[node]["pos"][1],
|
||||
"z": 0.0,
|
||||
}
|
||||
}
|
||||
del G.nodes[node]["pos"]
|
||||
|
||||
nx.write_gexf(G, f, version="1.2draft")
|
@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Annotated
|
||||
import annotated_types
|
||||
from typing import *
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
class Parameter:
|
||||
pass
|
||||
|
||||
|
||||
def floatrange(
|
||||
*,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
multiple_of: Optional[float] = None,
|
||||
) -> type[float]:
|
||||
return Annotated[
|
||||
float,
|
||||
annotated_types.Interval(gt=gt, ge=ge, lt=lt, le=le),
|
||||
annotated_types.MultipleOf(multiple_of) if multiple_of is not None else None,
|
||||
]
|
||||
|
||||
function = Annotated[Callable, Parameter]
|
||||
Integer = Annotated[int, Parameter]
|
||||
Float = Annotated[float, Parameter]
|
||||
|
||||
|
||||
probability = floatrange(ge=0, le=1)
|
@ -1,355 +1,395 @@
|
||||
import os
|
||||
import importlib
|
||||
from time import time as current_time, strftime
|
||||
import sys
|
||||
import yaml
|
||||
import traceback
|
||||
import hashlib
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import networkx as nx
|
||||
|
||||
from time import strftime
|
||||
from networkx.readwrite import json_graph
|
||||
from multiprocessing import Pool
|
||||
from functools import partial
|
||||
from tsih import History
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
import pickle
|
||||
from textwrap import dedent
|
||||
|
||||
from . import serialization, utils, basestring, agents
|
||||
from .environment import Environment
|
||||
from .utils import logger
|
||||
from .exporters import default
|
||||
from .stats import defaultStats
|
||||
from dataclasses import dataclass, field, asdict, replace
|
||||
from typing import Any, Dict, Union, Optional, List
|
||||
|
||||
|
||||
#TODO: change documentation for simulation
|
||||
from functools import partial
|
||||
from contextlib import contextmanager
|
||||
from itertools import product
|
||||
import json
|
||||
|
||||
|
||||
from . import serialization, exporters, utils, basestring, agents
|
||||
from .environment import Environment
|
||||
from .utils import logger, run_and_return_exceptions
|
||||
from .debugging import set_trace
|
||||
|
||||
_AVOID_RUNNING = False
|
||||
_QUEUED = []
|
||||
|
||||
@contextmanager
|
||||
def do_not_run():
|
||||
global _AVOID_RUNNING
|
||||
_AVOID_RUNNING = True
|
||||
try:
|
||||
logger.debug("NOT RUNNING")
|
||||
yield
|
||||
finally:
|
||||
logger.debug("RUNNING AGAIN")
|
||||
_AVOID_RUNNING = False
|
||||
|
||||
|
||||
def _iter_queued():
|
||||
while _QUEUED:
|
||||
(cls, params) = _QUEUED.pop(0)
|
||||
yield replace(cls, parameters=params)
|
||||
|
||||
|
||||
# TODO: change documentation for simulation
|
||||
# TODO: rename iterations to iterations
|
||||
# TODO: make parameters a dict of iterable/any
|
||||
@dataclass
|
||||
class Simulation:
|
||||
"""
|
||||
Similar to nsim.NetworkSimulation with three main differences:
|
||||
1) agent type can be specified by name or by class.
|
||||
2) instead of just one type, a network agents distribution can be used.
|
||||
The distribution specifies the weight (or probability) of each
|
||||
agent type in the topology. This is an example distribution: ::
|
||||
|
||||
[
|
||||
{'agent_type': 'agent_type_1',
|
||||
'weight': 0.2,
|
||||
'state': {
|
||||
'id': 0
|
||||
}
|
||||
},
|
||||
{'agent_type': 'agent_type_2',
|
||||
'weight': 0.8,
|
||||
'state': {
|
||||
'id': 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
In this example, 20% of the nodes will be marked as type
|
||||
'agent_type_1'.
|
||||
3) if no initial state is given, each node's state will be set
|
||||
to `{'id': 0}`.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
name : str, optional
|
||||
name of the Simulation
|
||||
group : str, optional
|
||||
a group name can be used to link simulations
|
||||
topology : networkx.Graph instance, optional
|
||||
network_params : dict
|
||||
parameters used to create a topology with networkx, if no topology is given
|
||||
network_agents : dict
|
||||
definition of agents to populate the topology with
|
||||
agent_type : NetworkAgent subclass, optional
|
||||
Default type of NetworkAgent to use for nodes not specified in network_agents
|
||||
states : list, optional
|
||||
List of initial states corresponding to the nodes in the topology. Basic form is a list of integers
|
||||
whose value indicates the state
|
||||
dir_path: str, optional
|
||||
Directory path to load simulation assets (files, modules...)
|
||||
seed : str, optional
|
||||
Seed to use for the random generator
|
||||
num_trials : int, optional
|
||||
Number of independent simulation runs
|
||||
max_time : int, optional
|
||||
Time how long the simulation should run
|
||||
environment_params : dict, optional
|
||||
Dictionary of globally-shared environmental parameters
|
||||
environment_agents: dict, optional
|
||||
Similar to network_agents. Distribution of Agents that control the environment
|
||||
environment_class: soil.environment.Environment subclass, optional
|
||||
Class for the environment. It defailts to soil.environment.Environment
|
||||
load_module : str, module name, deprecated
|
||||
If specified, soil will load the content of this module under 'soil.agents.custom'
|
||||
A simulation is a collection of agents and a model. It is responsible for running the model and agents, and collecting data from them.
|
||||
|
||||
Args:
|
||||
version: The version of the simulation. This is used to determine how to load the simulation.
|
||||
name: The name of the simulation.
|
||||
description: A description of the simulation.
|
||||
group: The group that the simulation belongs to.
|
||||
model: The model to use for the simulation. This can be a string or a class.
|
||||
parameters: The parameters to pass to the model.
|
||||
matrix: A matrix of values for each parameter.
|
||||
seed: The seed to use for the simulation.
|
||||
dir_path: The directory path to use for the simulation.
|
||||
max_time: The maximum time to run the simulation.
|
||||
max_steps: The maximum number of steps to run the simulation.
|
||||
interval: The interval to use for the simulation.
|
||||
iterations: The number of iterations (times) to run the simulation.
|
||||
num_processes: The number of processes to use for the simulation. If greater than one, simulations will be performed in parallel. This may make debugging and error handling difficult.
|
||||
tables: The tables to use in the simulation datacollector
|
||||
agent_reporters: The agent reporters to use in the datacollector
|
||||
model_reporters: The model reporters to use in the datacollector
|
||||
dry_run: Whether or not to run the simulation. If True, the simulation will not be run.
|
||||
backup: Whether or not to backup the simulation. If True, the simulation files will be backed up to a different directory.
|
||||
overwrite: Whether or not to replace existing simulation data.
|
||||
source_file: Python file to use to find additional classes.
|
||||
"""
|
||||
|
||||
version: str = "2"
|
||||
source_file: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = ""
|
||||
group: str = None
|
||||
backup: bool = False
|
||||
overwrite: bool = False
|
||||
dry_run: bool = False
|
||||
dump: bool = False
|
||||
model: Union[str, type] = "soil.Environment"
|
||||
parameters: dict = field(default_factory=dict)
|
||||
matrix: dict = field(default_factory=dict)
|
||||
seed: str = "default"
|
||||
dir_path: str = field(default_factory=lambda: os.getcwd())
|
||||
max_time: float = None
|
||||
max_steps: int = None
|
||||
interval: int = 1
|
||||
iterations: int = 1
|
||||
num_processes: Optional[int] = 1
|
||||
exporters: Optional[List[str]] = field(default_factory=lambda: [exporters.default])
|
||||
model_reporters: Optional[Dict[str, Any]] = field(default_factory=dict)
|
||||
agent_reporters: Optional[Dict[str, Any]] = field(default_factory=dict)
|
||||
tables: Optional[Dict[str, Any]] = field(default_factory=dict)
|
||||
outdir: str = field(default_factory=lambda: os.path.join(os.getcwd(), "soil_output"))
|
||||
# outdir: Optional[str] = None
|
||||
exporter_params: Optional[Dict[str, Any]] = field(default_factory=dict)
|
||||
level: int = logging.INFO
|
||||
skip_test: Optional[bool] = False
|
||||
debug: Optional[bool] = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.name is None:
|
||||
if isinstance(self.model, str):
|
||||
self.name = self.model
|
||||
else:
|
||||
self.name = self.model.__name__
|
||||
self.logger = logger.getChild(self.name)
|
||||
self.logger.setLevel(self.level)
|
||||
|
||||
if self.source_file:
|
||||
source_file = self.source_file
|
||||
if not os.path.isabs(source_file):
|
||||
source_file = os.path.abspath(os.path.join(self.dir_path, source_file))
|
||||
serialization.add_source_file(source_file)
|
||||
self.source_file = source_file
|
||||
|
||||
if isinstance(self.model, str):
|
||||
self.model = serialization.deserialize(self.model)
|
||||
|
||||
def deserialize_reporters(reporters):
|
||||
for (k, v) in reporters.items():
|
||||
if isinstance(v, str) and v.startswith("py:"):
|
||||
reporters[k] = serialization.deserialize(v.split(":", 1)[1])
|
||||
return reporters
|
||||
|
||||
self.agent_reporters = deserialize_reporters(self.agent_reporters)
|
||||
self.model_reporters = deserialize_reporters(self.model_reporters)
|
||||
self.tables = deserialize_reporters(self.tables)
|
||||
if self.source_file:
|
||||
serialization.remove_source_file(self.source_file)
|
||||
self.id = f"{self.name}_{current_time()}"
|
||||
|
||||
def run(self, **kwargs):
|
||||
"""Run the simulation and return the list of resulting environments"""
|
||||
if kwargs:
|
||||
return replace(self, **kwargs).run()
|
||||
|
||||
self.logger.debug(
|
||||
dedent(
|
||||
"""
|
||||
Simulation:
|
||||
---
|
||||
"""
|
||||
)
|
||||
+ self.to_yaml()
|
||||
)
|
||||
param_combinations = self._collect_params(**kwargs)
|
||||
if _AVOID_RUNNING:
|
||||
_QUEUED.extend((self, param) for param in param_combinations)
|
||||
return []
|
||||
|
||||
self.logger.debug("Using exporters: %s", self.exporters or [])
|
||||
|
||||
exporters = serialization.deserialize_all(
|
||||
self.exporters,
|
||||
simulation=self,
|
||||
known_modules=[
|
||||
"soil.exporters",
|
||||
],
|
||||
dump=self.dump and not self.dry_run,
|
||||
outdir=self.outdir,
|
||||
**self.exporter_params,
|
||||
)
|
||||
|
||||
results = []
|
||||
for exporter in exporters:
|
||||
exporter.sim_start()
|
||||
|
||||
for params in tqdm(param_combinations, desc=self.name, unit="configuration"):
|
||||
for (k, v) in params.items():
|
||||
tqdm.write(f"{k} = {v}")
|
||||
sha = hashlib.sha256()
|
||||
sha.update(repr(sorted(params.items())).encode())
|
||||
params_id = sha.hexdigest()[:7]
|
||||
for env in self._run_iters_for_params(params):
|
||||
for exporter in exporters:
|
||||
exporter.iteration_end(env, params, params_id)
|
||||
results.append(env)
|
||||
|
||||
for exporter in exporters:
|
||||
exporter.sim_end()
|
||||
|
||||
"""
|
||||
return results
|
||||
|
||||
def __init__(self, name=None, group=None, topology=None, network_params=None,
|
||||
network_agents=None, agent_type=None, states=None,
|
||||
default_state=None, interval=1, num_trials=1,
|
||||
max_time=100, load_module=None, seed=None,
|
||||
dir_path=None, environment_agents=None,
|
||||
environment_params=None, environment_class=None,
|
||||
**kwargs):
|
||||
|
||||
self.load_module = load_module
|
||||
self.network_params = network_params
|
||||
self.name = name or 'Unnamed'
|
||||
self.seed = str(seed or name)
|
||||
self._id = '{}_{}'.format(self.name, strftime("%Y-%m-%d_%H.%M.%S"))
|
||||
self.group = group or ''
|
||||
self.num_trials = num_trials
|
||||
self.max_time = max_time
|
||||
self.default_state = default_state or {}
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
self.interval = interval
|
||||
|
||||
sys.path += list(x for x in [os.getcwd(), self.dir_path] if x not in sys.path)
|
||||
|
||||
if topology is None:
|
||||
topology = serialization.load_network(network_params,
|
||||
dir_path=self.dir_path)
|
||||
elif isinstance(topology, basestring) or isinstance(topology, dict):
|
||||
topology = json_graph.node_link_graph(topology)
|
||||
self.topology = nx.Graph(topology)
|
||||
|
||||
|
||||
self.environment_params = environment_params or {}
|
||||
self.environment_class = serialization.deserialize(environment_class,
|
||||
known_modules=['soil.environment', ]) or Environment
|
||||
|
||||
environment_agents = environment_agents or []
|
||||
self.environment_agents = agents._convert_agent_types(environment_agents,
|
||||
known_modules=[self.load_module])
|
||||
|
||||
distro = agents.calculate_distribution(network_agents,
|
||||
agent_type)
|
||||
self.network_agents = agents._convert_agent_types(distro,
|
||||
known_modules=[self.load_module])
|
||||
|
||||
self.states = agents._validate_states(states,
|
||||
self.topology)
|
||||
|
||||
self._history = History(name=self.name,
|
||||
backup=False)
|
||||
|
||||
def run_simulation(self, *args, **kwargs):
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
'''Run the simulation and return the list of resulting environments'''
|
||||
return list(self.run_gen(*args, **kwargs))
|
||||
|
||||
def _run_sync_or_async(self, parallel=False, **kwargs):
|
||||
if parallel and not os.environ.get('SENPY_DEBUG', None):
|
||||
p = Pool()
|
||||
func = partial(self.run_trial_exceptions, **kwargs)
|
||||
for i in p.imap_unordered(func, range(self.num_trials)):
|
||||
if isinstance(i, Exception):
|
||||
logger.error('Trial failed:\n\t%s', i.message)
|
||||
continue
|
||||
yield i
|
||||
else:
|
||||
for i in range(self.num_trials):
|
||||
yield self.run_trial(trial_id=i,
|
||||
**kwargs)
|
||||
|
||||
def run_gen(self, parallel=False, dry_run=False,
|
||||
exporters=[default, ], stats=[], outdir=None, exporter_params={},
|
||||
stats_params={}, log_level=None,
|
||||
**kwargs):
|
||||
'''Run the simulation and yield the resulting environments.'''
|
||||
if log_level:
|
||||
logger.setLevel(log_level)
|
||||
logger.info('Using exporters: %s', exporters or [])
|
||||
logger.info('Output directory: %s', outdir)
|
||||
exporters = serialization.deserialize_all(exporters,
|
||||
simulation=self,
|
||||
known_modules=['soil.exporters',],
|
||||
dry_run=dry_run,
|
||||
outdir=outdir,
|
||||
**exporter_params)
|
||||
stats = serialization.deserialize_all(simulation=self,
|
||||
names=stats,
|
||||
known_modules=['soil.stats',],
|
||||
**stats_params)
|
||||
|
||||
with utils.timer('simulation {}'.format(self.name)):
|
||||
for stat in stats:
|
||||
stat.start()
|
||||
|
||||
for exporter in exporters:
|
||||
exporter.start()
|
||||
for env in self._run_sync_or_async(parallel=parallel,
|
||||
log_level=log_level,
|
||||
**kwargs):
|
||||
|
||||
collected = list(stat.trial(env) for stat in stats)
|
||||
|
||||
saved = self.save_stats(collected, t_step=env.now, trial_id=env.name)
|
||||
def _collect_params(self):
|
||||
|
||||
for exporter in exporters:
|
||||
exporter.trial(env, saved)
|
||||
|
||||
yield env
|
||||
|
||||
|
||||
collected = list(stat.end() for stat in stats)
|
||||
saved = self.save_stats(collected)
|
||||
|
||||
for exporter in exporters:
|
||||
exporter.end(saved)
|
||||
|
||||
|
||||
def save_stats(self, collection, **kwargs):
|
||||
stats = dict(kwargs)
|
||||
for stat in collection:
|
||||
stats.update(stat)
|
||||
self._history.save_stats(utils.flatten_dict(stats))
|
||||
return stats
|
||||
|
||||
def get_stats(self, **kwargs):
|
||||
return self._history.get_stats(**kwargs)
|
||||
|
||||
def log_stats(self, stats):
|
||||
logger.info('Stats: \n{}'.format(yaml.dump(stats, default_flow_style=False)))
|
||||
|
||||
|
||||
def get_env(self, trial_id=0, **kwargs):
|
||||
'''Create an environment for a trial of the simulation'''
|
||||
opts = self.environment_params.copy()
|
||||
opts.update({
|
||||
'name': '{}_trial_{}'.format(self.name, trial_id),
|
||||
'topology': self.topology.copy(),
|
||||
'network_params': self.network_params,
|
||||
'seed': '{}_trial_{}'.format(self.seed, trial_id),
|
||||
'initial_time': 0,
|
||||
'interval': self.interval,
|
||||
'network_agents': self.network_agents,
|
||||
'initial_time': 0,
|
||||
'states': self.states,
|
||||
'dir_path': self.dir_path,
|
||||
'default_state': self.default_state,
|
||||
'environment_agents': self.environment_agents,
|
||||
})
|
||||
opts.update(kwargs)
|
||||
env = self.environment_class(**opts)
|
||||
return env
|
||||
|
||||
def run_trial(self, trial_id=0, until=None, log_level=logging.INFO, **opts):
|
||||
parameters = []
|
||||
if self.parameters:
|
||||
parameters.append(self.parameters)
|
||||
if self.matrix:
|
||||
assert isinstance(self.matrix, dict)
|
||||
for values in product(*(self.matrix.values())):
|
||||
parameters.append(dict(zip(self.matrix.keys(), values)))
|
||||
|
||||
if not parameters:
|
||||
parameters = [{}]
|
||||
if self.dump:
|
||||
self.logger.info("Output directory: %s", self.outdir)
|
||||
|
||||
return parameters
|
||||
|
||||
def _run_iters_for_params(
|
||||
self,
|
||||
params
|
||||
):
|
||||
"""Run the simulation and yield the resulting environments."""
|
||||
|
||||
try:
|
||||
if self.source_file:
|
||||
serialization.add_source_file(self.source_file)
|
||||
|
||||
with utils.timer(f"running for config {params}"):
|
||||
if self.dry_run:
|
||||
def func(*args, **kwargs):
|
||||
return None
|
||||
else:
|
||||
func = self._run_model
|
||||
|
||||
for env in tqdm(utils.run_parallel(
|
||||
func=func,
|
||||
iterable=range(self.iterations),
|
||||
**params,
|
||||
), total=self.iterations, leave=False):
|
||||
if env is None and self.dry_run:
|
||||
continue
|
||||
|
||||
yield env
|
||||
finally:
|
||||
if self.source_file:
|
||||
serialization.remove_source_file(self.source_file)
|
||||
|
||||
def _get_env(self, iteration_id, params):
|
||||
"""Create an environment for a iteration of the simulation"""
|
||||
|
||||
iteration_id = str(iteration_id)
|
||||
|
||||
agent_reporters = self.agent_reporters
|
||||
agent_reporters.update(params.pop("agent_reporters", {}))
|
||||
model_reporters = self.model_reporters
|
||||
model_reporters.update(params.pop("model_reporters", {}))
|
||||
|
||||
return self.model(
|
||||
id=iteration_id,
|
||||
seed=f"{self.seed}_iteration_{iteration_id}",
|
||||
dir_path=self.dir_path,
|
||||
interval=self.interval,
|
||||
logger=self.logger.getChild(iteration_id),
|
||||
agent_reporters=agent_reporters,
|
||||
model_reporters=model_reporters,
|
||||
tables=self.tables,
|
||||
**params,
|
||||
)
|
||||
|
||||
def _run_model(self, iteration_id, **params):
|
||||
"""
|
||||
Run a single trial of the simulation
|
||||
Run a single iteration of the simulation
|
||||
|
||||
"""
|
||||
if log_level:
|
||||
logger.setLevel(log_level)
|
||||
# Set-up trial environment and graph
|
||||
until = until or self.max_time
|
||||
env = self.get_env(trial_id=trial_id, **opts)
|
||||
# Set up agents on nodes
|
||||
with utils.timer('Simulation {} trial {}'.format(self.name, trial_id)):
|
||||
env.run(until)
|
||||
return env
|
||||
|
||||
def run_trial_exceptions(self, *args, **kwargs):
|
||||
'''
|
||||
A wrapper for run_trial that catches exceptions and returns them.
|
||||
It is meant for async simulations
|
||||
'''
|
||||
try:
|
||||
return self.run_trial(*args, **kwargs)
|
||||
except Exception as ex:
|
||||
if ex.__cause__ is not None:
|
||||
ex = ex.__cause__
|
||||
ex.message = ''.join(traceback.format_exception(type(ex), ex, ex.__traceback__)[:])
|
||||
return ex
|
||||
# Set-up iteration environment and graph
|
||||
model = self._get_env(iteration_id, params)
|
||||
with utils.timer("Simulation {} iteration {}".format(self.name, iteration_id)):
|
||||
|
||||
max_time = self.max_time
|
||||
max_steps = self.max_steps
|
||||
|
||||
if (max_time is not None) and (max_steps is not None):
|
||||
is_done = lambda model: (not model.running) or (model.schedule.time >= max_time) or (model.schedule.steps >= max_steps)
|
||||
elif max_time is not None:
|
||||
is_done = lambda model: (not model.running) or (model.schedule.time >= max_time)
|
||||
elif max_steps is not None:
|
||||
is_done = lambda model: (not model.running) or (model.schedule.steps >= max_steps)
|
||||
else:
|
||||
is_done = lambda model: not model.running
|
||||
|
||||
if not model.schedule.agents:
|
||||
raise Exception("No agents in model. This is probably a bug. Make sure that the model has agents scheduled after its initialization.")
|
||||
|
||||
newline = "\n"
|
||||
self.logger.debug(
|
||||
dedent(
|
||||
f"""
|
||||
Model stats:
|
||||
Agent count: { model.schedule.get_agent_count() }):
|
||||
Topology size: { len(model.G) if hasattr(model, "G") else 0 }
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
if self.debug:
|
||||
set_trace()
|
||||
|
||||
while not is_done(model):
|
||||
self.logger.debug(
|
||||
f'Simulation time {model.schedule.time}/{max_time}.'
|
||||
)
|
||||
model.step()
|
||||
|
||||
return model
|
||||
|
||||
def to_dict(self):
|
||||
return self.__getstate__()
|
||||
d = asdict(self)
|
||||
return serialization.serialize_dict(d)
|
||||
|
||||
def to_yaml(self):
|
||||
return yaml.dump(self.to_dict())
|
||||
|
||||
|
||||
def dump_yaml(self, f=None, outdir=None):
|
||||
if not f and not outdir:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir, '{}.dumped.yml'.format(self.name))
|
||||
|
||||
with utils.open_or_reuse(f, 'w') as f:
|
||||
f.write(self.to_yaml())
|
||||
|
||||
def dump_pickle(self, f=None, outdir=None):
|
||||
if not outdir and not f:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir,
|
||||
'{}.simulation.pickle'.format(self.name))
|
||||
with utils.open_or_reuse(f, 'wb') as f:
|
||||
pickle.dump(self, f)
|
||||
|
||||
def dump_sqlite(self, f):
|
||||
return self._history.dump(f)
|
||||
|
||||
def __getstate__(self):
|
||||
state={}
|
||||
for k, v in self.__dict__.items():
|
||||
if k[0] != '_':
|
||||
state[k] = v
|
||||
state['topology'] = json_graph.node_link_data(self.topology)
|
||||
state['network_agents'] = agents.serialize_definition(self.network_agents,
|
||||
known_modules = [])
|
||||
state['environment_agents'] = agents.serialize_definition(self.environment_agents,
|
||||
known_modules = [])
|
||||
state['environment_class'] = serialization.serialize(self.environment_class,
|
||||
known_modules=['soil.environment'])[1] # func, name
|
||||
if state['load_module'] is None:
|
||||
del state['load_module']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__ = state
|
||||
self.load_module = getattr(self, 'load_module', None)
|
||||
if self.dir_path not in sys.path:
|
||||
sys.path += [self.dir_path, os.getcwd()]
|
||||
self.topology = json_graph.node_link_graph(state['topology'])
|
||||
self.network_agents = agents.calculate_distribution(agents._convert_agent_types(self.network_agents))
|
||||
self.environment_agents = agents._convert_agent_types(self.environment_agents,
|
||||
known_modules=[self.load_module])
|
||||
self.environment_class = serialization.deserialize(self.environment_class,
|
||||
known_modules=[self.load_module, 'soil.environment', ]) # func, name
|
||||
|
||||
|
||||
def all_from_config(config):
|
||||
configs = list(serialization.load_config(config))
|
||||
for config, _ in configs:
|
||||
sim = Simulation(**config)
|
||||
yield sim
|
||||
def iter_from_file(*files, **kwargs):
|
||||
for f in files:
|
||||
try:
|
||||
yield from iter_from_py(f, **kwargs)
|
||||
except ValueError as ex:
|
||||
yield from iter_from_config(f, **kwargs)
|
||||
|
||||
|
||||
def from_file(*args, **kwargs):
|
||||
return list(iter_from_file(*args, **kwargs))
|
||||
|
||||
|
||||
def iter_from_config(*cfgs, **kwargs):
|
||||
for config in cfgs:
|
||||
configs = list(serialization.load_config(config))
|
||||
for config, path in configs:
|
||||
d = dict(config)
|
||||
d.update(kwargs)
|
||||
if "dir_path" not in d:
|
||||
d["dir_path"] = os.path.dirname(path)
|
||||
yield Simulation(**d)
|
||||
|
||||
|
||||
def from_config(conf_or_path):
|
||||
config = list(serialization.load_config(conf_or_path))
|
||||
if len(config) > 1:
|
||||
raise AttributeError('Provide only one configuration')
|
||||
config = config[0][0]
|
||||
sim = Simulation(**config)
|
||||
return sim
|
||||
|
||||
|
||||
def run_from_config(*configs, **kwargs):
|
||||
for config_def in configs:
|
||||
# logger.info("Found {} config(s)".format(len(ls)))
|
||||
for config, path in serialization.load_config(config_def):
|
||||
name = config.get('name', 'unnamed')
|
||||
logger.info("Using config(s): {name}".format(name=name))
|
||||
|
||||
dir_path = config.pop('dir_path', os.path.dirname(path))
|
||||
sim = Simulation(dir_path=dir_path,
|
||||
**config)
|
||||
sim.run_simulation(**kwargs)
|
||||
lst = list(iter_from_config(conf_or_path))
|
||||
if len(lst) > 1:
|
||||
raise AttributeError("Provide only one configuration")
|
||||
return lst[0]
|
||||
|
||||
|
||||
def iter_from_py(pyfile, module_name='imported_file', **kwargs):
|
||||
"""Try to load every Simulation instance in a given Python file"""
|
||||
import importlib
|
||||
added = False
|
||||
sims = []
|
||||
assert not _AVOID_RUNNING
|
||||
with do_not_run():
|
||||
assert _AVOID_RUNNING
|
||||
spec = importlib.util.spec_from_file_location(module_name, pyfile)
|
||||
folder = os.path.dirname(pyfile)
|
||||
if folder not in sys.path:
|
||||
added = True
|
||||
sys.path.append(folder)
|
||||
if not spec:
|
||||
raise ValueError(f"{pyfile} does not seem to be a Python module")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
for (_name, sim) in inspect.getmembers(module, lambda x: isinstance(x, Simulation)):
|
||||
sims.append(sim)
|
||||
for sim in _iter_queued():
|
||||
sims.append(sim)
|
||||
if not sims:
|
||||
for (_name, sim) in inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, Simulation)):
|
||||
sims.append(sim(**kwargs))
|
||||
del sys.modules[module_name]
|
||||
assert not _AVOID_RUNNING
|
||||
if not sims:
|
||||
raise AttributeError(f"No valid configurations found in {pyfile}")
|
||||
if added:
|
||||
sys.path.remove(folder)
|
||||
for sim in sims:
|
||||
yield replace(sim, **kwargs)
|
||||
|
||||
|
||||
def from_py(pyfile):
|
||||
return next(iter_from_py(pyfile))
|
||||
|
||||
|
||||
def run_from_file(*files, **kwargs):
|
||||
for sim in iter_from_file(*files):
|
||||
logger.info(f"Using config(s): {sim.name}")
|
||||
sim.run_simulation(**kwargs)
|
||||
|
||||
def run(env, iterations=1, num_processes=1, dump=False, name="test", **kwargs):
|
||||
return Simulation(model=env, iterations=iterations, name=name, dump=dump, num_processes=num_processes, **kwargs).run()
|
@ -1,106 +0,0 @@
|
||||
import pandas as pd
|
||||
|
||||
from collections import Counter
|
||||
|
||||
class Stats:
|
||||
'''
|
||||
Interface for all stats. It is not necessary, but it is useful
|
||||
if you don't plan to implement all the methods.
|
||||
'''
|
||||
|
||||
def __init__(self, simulation):
|
||||
self.simulation = simulation
|
||||
|
||||
def start(self):
|
||||
'''Method to call when the simulation starts'''
|
||||
pass
|
||||
|
||||
def end(self):
|
||||
'''Method to call when the simulation ends'''
|
||||
return {}
|
||||
|
||||
def trial(self, env):
|
||||
'''Method to call when a trial ends'''
|
||||
return {}
|
||||
|
||||
|
||||
class distribution(Stats):
|
||||
'''
|
||||
Calculate the distribution of agent states at the end of each trial,
|
||||
the mean value, and its deviation.
|
||||
'''
|
||||
|
||||
def start(self):
|
||||
self.means = []
|
||||
self.counts = []
|
||||
|
||||
def trial(self, env):
|
||||
df = env[None, None, None].df()
|
||||
df = df.drop('SEED', axis=1)
|
||||
ix = df.index[-1]
|
||||
attrs = df.columns.get_level_values(0)
|
||||
vc = {}
|
||||
stats = {
|
||||
'mean': {},
|
||||
'count': {},
|
||||
}
|
||||
for a in attrs:
|
||||
t = df.loc[(ix, a)]
|
||||
try:
|
||||
stats['mean'][a] = t.mean()
|
||||
self.means.append(('mean', a, t.mean()))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
for name, count in t.value_counts().items():
|
||||
if a not in stats['count']:
|
||||
stats['count'][a] = {}
|
||||
stats['count'][a][name] = count
|
||||
self.counts.append(('count', a, name, count))
|
||||
|
||||
return stats
|
||||
|
||||
def end(self):
|
||||
dfm = pd.DataFrame(self.means, columns=['metric', 'key', 'value'])
|
||||
dfc = pd.DataFrame(self.counts, columns=['metric', 'key', 'value', 'count'])
|
||||
|
||||
count = {}
|
||||
mean = {}
|
||||
|
||||
if self.means:
|
||||
res = dfm.drop('metric', axis=1).groupby(by=['key']).agg(['mean', 'std', 'count', 'median', 'max', 'min'])
|
||||
mean = res['value'].to_dict()
|
||||
if self.counts:
|
||||
res = dfc.drop('metric', axis=1).groupby(by=['key', 'value']).agg(['mean', 'std', 'count', 'median', 'max', 'min'])
|
||||
for k,v in res['count'].to_dict().items():
|
||||
if k not in count:
|
||||
count[k] = {}
|
||||
for tup, times in v.items():
|
||||
subkey, subcount = tup
|
||||
if subkey not in count[k]:
|
||||
count[k][subkey] = {}
|
||||
count[k][subkey][subcount] = times
|
||||
|
||||
|
||||
return {'count': count, 'mean': mean}
|
||||
|
||||
|
||||
class defaultStats(Stats):
|
||||
|
||||
def trial(self, env):
|
||||
c = Counter()
|
||||
c.update(a.__class__.__name__ for a in env.network_agents)
|
||||
|
||||
c2 = Counter()
|
||||
c2.update(a['id'] for a in env.network_agents)
|
||||
|
||||
return {
|
||||
'network ': {
|
||||
'n_nodes': env.G.number_of_nodes(),
|
||||
'n_edges': env.G.number_of_edges(),
|
||||
},
|
||||
'agents': {
|
||||
'model_count': dict(c),
|
||||
'state_count': dict(c2),
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
from mesa.visualization.UserParam import UserSettableParameter
|
||||
|
||||
class UserSettableParameter(UserSettableParameter):
|
||||
def __str__(self):
|
||||
return self.value
|
Before Width: | Height: | Size: 1.1 MiB |