1
0
mirror of https://github.com/gsi-upm/soil synced 2025-08-23 19:52:19 +00:00

WIP: all tests pass

Documentation needs some improvement

The API has been simplified to only allow for ONE topology per
NetworkEnvironment.
This covers the main use case, and simplifies the code.
This commit is contained in:
J. Fernando Sánchez
2022-10-16 17:54:03 +02:00
parent cd62c23cb9
commit d9947c2c52
34 changed files with 693 additions and 736 deletions

View File

@@ -10,19 +10,14 @@ seed: "CompleteSeed!"
model_class: Environment
model_params:
am_i_complete: true
topologies:
default:
params:
generator: complete_graph
n: 10
another_graph:
params:
generator: complete_graph
n: 2
topology:
params:
generator: complete_graph
n: 12
environment:
agents:
agent_class: CounterModel
topology: default
topology: true
state:
times: 1
# In this group we are not specifying any topology
@@ -30,25 +25,23 @@ model_params:
- name: 'Environment Agent 1'
agent_class: BaseAgent
group: environment
topology: null
topology: false
hidden: true
state:
times: 10
- agent_class: CounterModel
id: 0
group: other_counters
topology: another_graph
group: fixed_counters
state:
times: 1
total: 0
- agent_class: CounterModel
topology: another_graph
group: other_counters
group: fixed_counters
id: 1
distribution:
- agent_class: CounterModel
weight: 1
group: general_counters
group: distro_counters
state:
times: 3
- agent_class: AggregatedCounter

View File

@@ -1,63 +0,0 @@
---
version: '2'
id: simple
group: tests
dir_path: "/tmp/"
num_trials: 3
max_steps: 100
interval: 1
seed: "CompleteSeed!"
model_class: "soil.Environment"
model_params:
topologies:
default:
params:
generator: complete_graph
n: 10
another_graph:
params:
generator: complete_graph
n: 2
agents:
# The values here will be used as default values for any agent
agent_class: CounterModel
topology: default
state:
times: 1
# This specifies a distribution of agents, each with a `weight` or an explicit number of agents
distribution:
- agent_class: CounterModel
weight: 1
# This is inherited from the default settings
#topology: default
state:
times: 3
- agent_class: AggregatedCounter
topology: default
weight: 0.2
fixed:
- name: 'Environment Agent 1'
# All the other agents will assigned to the 'default' group
group: environment
# Do not count this agent towards total limits
hidden: true
agent_class: soil.BaseAgent
topology: null
state:
times: 10
- agent_class: CounterModel
topology: another_graph
id: 0
state:
times: 1
total: 0
- agent_class: CounterModel
topology: another_graph
id: 1
override:
# 2 agents that match this filter will be updated to match the state {times: 5}
- filter:
agent_class: AggregatedCounter
n: 2
state:
times: 5

View File

@@ -15,6 +15,7 @@ class Fibonacci(FSM):
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
@@ -23,9 +24,8 @@ class Odds(FSM):
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_class': Fibonacci},
{'ids': [1], 'agent_class': Odds}],

View File

@@ -8,17 +8,12 @@ interval: 1
seed: '1'
model_class: social_wealth.MoneyEnv
model_params:
topologies:
default:
params:
generator: social_wealth.graph_generator
n: 5
generator: social_wealth.graph_generator
agents:
topology: true
distribution:
- agent_class: social_wealth.SocialMoneyAgent
topology: default
weight: 1
mesa_agent_class: social_wealth.MoneyAgent
N: 10
width: 50
height: 50

View File

@@ -2,6 +2,7 @@ from mesa.visualization.ModularVisualization import ModularServer
from soil.visualization import UserSettableParameter
from mesa.visualization.modules import ChartModule, NetworkModule, CanvasGrid
from social_wealth import MoneyEnv, graph_generator, SocialMoneyAgent
import networkx as nx
class MyNetwork(NetworkModule):
@@ -13,15 +14,16 @@ def network_portrayal(env):
# The model ensures there is 0 or 1 agent per node
portrayal = dict()
wealths = {node_id: data['agent'].wealth for (node_id, data) in env.G.nodes(data=True)}
portrayal["nodes"] = [
{
"id": agent_id,
"size": env.get_agent(agent_id).wealth,
# "color": "#CC0000" if not agents or agents[0].wealth == 0 else "#007959",
"color": "#CC0000",
"label": f"{agent_id}: {env.get_agent(agent_id).wealth}",
}
for (agent_id) in env.G.nodes
"id": node_id,
"size": 2*(wealth+1),
"color": "#CC0000" if wealth == 0 else "#007959",
# "color": "#CC0000",
"label": f"{node_id}: {wealth}",
} for (node_id, wealth) in wealths.items()
]
portrayal["edges"] = [
@@ -29,7 +31,6 @@ def network_portrayal(env):
for edge_id, (source, target) in enumerate(env.G.edges)
]
return portrayal
@@ -55,7 +56,7 @@ def gridPortrayal(agent):
}
grid = MyNetwork(network_portrayal, 500, 500, library="sigma")
grid = MyNetwork(network_portrayal, 500, 500)
chart = ChartModule(
[{"Label": "Gini", "Color": "Black"}], data_collector_name="datacollector"
)
@@ -70,7 +71,6 @@ model_params = {
1,
description="Choose how many agents to include in the model",
),
"network_agents": [{"agent_class": SocialMoneyAgent}],
"height": UserSettableParameter(
"slider",
"height",
@@ -89,12 +89,15 @@ model_params = {
1,
description="Grid width",
),
"network_params": {
'generator': graph_generator
},
"agent_class": UserSettableParameter('choice', 'Agent class', value='MoneyAgent',
choices=['MoneyAgent', 'SocialMoneyAgent']),
"generator": graph_generator,
}
canvas_element = CanvasGrid(gridPortrayal, model_params["width"].value, model_params["height"].value, 500, 500)
canvas_element = CanvasGrid(gridPortrayal,
model_params["width"].value,
model_params["height"].value, 500, 500)
server = ModularServer(

View File

@@ -10,7 +10,7 @@ from mesa.batchrunner import BatchRunner
import networkx as nx
from soil import NetworkAgent, Environment
from soil import NetworkAgent, Environment, serialization
def compute_gini(model):
agent_wealths = [agent.wealth for agent in model.agents]
@@ -19,15 +19,16 @@ def compute_gini(model):
B = sum( xi * (N-i) for i,xi in enumerate(x) ) / (N*sum(x))
return (1 + (1/N) - 2*B)
class MoneyAgent(MesaAgent):
"""
A MESA agent with fixed initial wealth.
It will only share wealth with neighbors based on grid proximity
"""
def __init__(self, unique_id, model):
def __init__(self, unique_id, model, wealth=1):
super().__init__(unique_id=unique_id, model=model)
self.wealth = 1
self.wealth = wealth
def move(self):
possible_steps = self.model.grid.get_neighborhood(
@@ -45,7 +46,7 @@ class MoneyAgent(MesaAgent):
self.wealth -= 1
def step(self):
self.info("Crying wolf", self.pos)
print("Crying wolf", self.pos)
self.move()
if self.wealth > 0:
self.give_money()
@@ -58,8 +59,8 @@ class SocialMoneyAgent(NetworkAgent, MoneyAgent):
cellmates = set(self.model.grid.get_cell_list_contents([self.pos]))
friends = set(self.get_neighboring_agents())
self.info("Trying to give money")
self.debug("Cellmates: ", cellmates)
self.debug("Friends: ", friends)
self.info("Cellmates: ", cellmates)
self.info("Friends: ", friends)
nearby_friends = list(cellmates & friends)
@@ -68,14 +69,29 @@ class SocialMoneyAgent(NetworkAgent, MoneyAgent):
other.wealth += 1
self.wealth -= 1
def graph_generator(n=5):
G = nx.Graph()
for ix in range(n):
G.add_edge(0, ix)
return G
class MoneyEnv(Environment):
"""A model with some number of agents."""
def __init__(self, width, height, *args, topologies, **kwargs):
def __init__(self, width, height, N, generator=graph_generator,
agent_class=SocialMoneyAgent,
topology=None, **kwargs):
super().__init__(*args, topologies=topologies, **kwargs)
generator = serialization.deserialize(generator)
agent_class = serialization.deserialize(agent_class, globs=globals())
topology = generator(n=N)
super().__init__(topology=topology,
N=N,
**kwargs)
self.grid = MultiGrid(width, height, False)
self.populate_network(agent_class=agent_class)
# Create agents
for agent in self.agents:
x = self.random.randrange(self.grid.width)
@@ -87,17 +103,9 @@ class MoneyEnv(Environment):
agent_reporters={"Wealth": "wealth"})
def graph_generator(n=5):
G = nx.Graph()
for ix in range(n):
G.add_edge(0, ix)
return G
if __name__ == '__main__':
G = graph_generator()
fixed_params = {"topology": G,
fixed_params = {"generator": nx.complete_graph,
"width": 10,
"network_agents": [{"agent_class": SocialMoneyAgent,
'weight': 1}],
@@ -116,4 +124,3 @@ if __name__ == '__main__':
run_data = batch_run.get_model_vars_dataframe()
run_data.head()
print(run_data.Gini)

View File

@@ -126,7 +126,7 @@ class Patron(FSM, NetworkAgent):
success depend on both agents' openness.
'''
if force or self['openness'] > self.random.random():
self.model.add_edge(self, other_agent)
self.add_edge(self, other_agent)
self.info('Made some friend {}'.format(other_agent))
return True
return False

View File

@@ -57,7 +57,7 @@ class Male(RabbitModel):
class Female(RabbitModel):
gestation = 100
gestation = 30
@state
def fertile(self):
@@ -72,10 +72,10 @@ class Female(RabbitModel):
self.pregnancy = -1
self.set_state(self.pregnant, when=self.now)
self.number_of_babies = int(8+4*self.random.random())
self.debug('I am pregnant')
@state
def pregnant(self):
self.debug('I am pregnant')
self.age += 1
self.pregnancy += 1
@@ -88,7 +88,6 @@ class Female(RabbitModel):
state = {}
agent_class = self.random.choice([Male, Female])
child = self.model.add_node(agent_class=agent_class,
topology=self.topology,
**state)
child.add_edge(self)
try:
@@ -113,7 +112,7 @@ class RandomAccident(BaseAgent):
level = logging.INFO
def step(self):
rabbits_alive = self.model.topology.number_of_nodes()
rabbits_alive = self.model.G.number_of_nodes()
if not rabbits_alive:
return self.die()
@@ -121,10 +120,15 @@ class RandomAccident(BaseAgent):
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=RabbitModel):
if i.state.id == i.dead.id:
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.set_state(i.dead)
self.debug('Rabbits alive: {}'.format(rabbits_alive))
if __name__ == '__main__':
from soil import easy
sim = easy('rabbits.yml')
sim.run()

View File

@@ -10,18 +10,16 @@ max_time: 100
model_class: soil.environment.Environment
model_params:
agents:
topology: default
topology: true
agent_class: rabbit_agents.RabbitModel
distribution:
- agent_class: rabbit_agents.Male
topology: default
weight: 1
- agent_class: rabbit_agents.Female
topology: default
weight: 1
fixed:
- agent_class: rabbit_agents.RandomAccident
topology: null
topology: false
hidden: true
state:
group: environment
@@ -29,13 +27,12 @@ model_params:
group: network
mating_prob: 0.1
prob_death: 0.001
topologies:
default:
topology:
directed: true
links: []
nodes:
- id: 1
- id: 0
topology:
fixed:
directed: true
links: []
nodes:
- id: 1
- id: 0
extra:
visualization_params: {}

View File

@@ -10,18 +10,16 @@ max_time: 100
model_class: soil.environment.Environment
model_params:
agents:
topology: default
topology: true
agent_class: rabbit_agents.RabbitModel
distribution:
- agent_class: rabbit_agents.Male
topology: default
weight: 1
- agent_class: rabbit_agents.Female
topology: default
weight: 1
fixed:
- agent_class: rabbit_agents.RandomAccident
topology: null
topology: false
hidden: true
state:
group: environment
@@ -29,13 +27,12 @@ model_params:
group: network
mating_prob: 0.1
prob_death: 0.001
topologies:
default:
topology:
directed: true
links: []
nodes:
- id: 1
- id: 0
topology:
fixed:
directed: true
links: []
nodes:
- id: 1
- id: 0
extra:
visualization_params: {}

View File

@@ -4,7 +4,6 @@ Example of a fully programmatic simulation, without definition files.
'''
from soil import Simulation, agents
from soil.time import Delta
import logging
@@ -40,5 +39,4 @@ s = Simulation(name='Programmatic',
dry_run=True)
logging.basicConfig(level=logging.INFO)
envs = s.run()