mirror of
https://github.com/gsi-upm/soil
synced 2024-12-22 16:28:11 +00:00
WIP
This commit is contained in:
parent
f49be3af68
commit
3041156f19
File diff suppressed because one or more lines are too long
@ -43,7 +43,7 @@ class Journey:
|
|||||||
driver: Optional[Driver] = None
|
driver: Optional[Driver] = None
|
||||||
|
|
||||||
|
|
||||||
class City(EventedEnvironment):
|
class City(Environment):
|
||||||
"""
|
"""
|
||||||
An environment with a grid where drivers and passengers will be placed.
|
An environment with a grid where drivers and passengers will be placed.
|
||||||
|
|
||||||
@ -85,11 +85,12 @@ class Driver(Evented, FSM):
|
|||||||
journey = None
|
journey = None
|
||||||
earnings = 0
|
earnings = 0
|
||||||
|
|
||||||
def on_receive(self, msg, sender):
|
# TODO: remove
|
||||||
"""This is not a state. It will run (and block) every time process_messages is invoked"""
|
# def on_receive(self, msg, sender):
|
||||||
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
|
# """This is not a state. It will run (and block) every time process_messages is invoked"""
|
||||||
msg.driver = self
|
# if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
|
||||||
self.journey = msg
|
# msg.driver = self
|
||||||
|
# self.journey = msg
|
||||||
|
|
||||||
def check_passengers(self):
|
def check_passengers(self):
|
||||||
"""If there are no more passengers, stop forever"""
|
"""If there are no more passengers, stop forever"""
|
||||||
@ -104,7 +105,7 @@ class Driver(Evented, FSM):
|
|||||||
if not self.check_passengers():
|
if not self.check_passengers():
|
||||||
return self.die("No passengers left")
|
return self.die("No passengers left")
|
||||||
self.journey = None
|
self.journey = None
|
||||||
while self.journey is None: # No potential journeys detected (see on_receive)
|
while self.journey is None: # No potential journeys detected
|
||||||
if target is None or not self.move_towards(target):
|
if target is None or not self.move_towards(target):
|
||||||
target = self.random.choice(
|
target = self.random.choice(
|
||||||
self.model.grid.get_neighborhood(self.pos, moore=False)
|
self.model.grid.get_neighborhood(self.pos, moore=False)
|
||||||
@ -113,7 +114,7 @@ class Driver(Evented, FSM):
|
|||||||
if not self.check_passengers():
|
if not self.check_passengers():
|
||||||
return self.die("No passengers left")
|
return self.die("No passengers left")
|
||||||
# This will call on_receive behind the scenes, and the agent's status will be updated
|
# This will call on_receive behind the scenes, and the agent's status will be updated
|
||||||
self.process_messages()
|
|
||||||
await self.delay(30) # Wait at least 30 seconds before checking again
|
await self.delay(30) # Wait at least 30 seconds before checking again
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -167,12 +168,13 @@ class Driver(Evented, FSM):
|
|||||||
class Passenger(Evented, FSM):
|
class Passenger(Evented, FSM):
|
||||||
pos = None
|
pos = None
|
||||||
|
|
||||||
def on_receive(self, msg, sender):
|
# TODO: Remove
|
||||||
"""This is not a state. It will be run synchronously every time `process_messages` is run"""
|
# def on_receive(self, msg, sender):
|
||||||
|
# """This is not a state. It will be run synchronously every time `process_messages` is run"""
|
||||||
|
|
||||||
if isinstance(msg, Journey):
|
# if isinstance(msg, Journey):
|
||||||
self.journey = msg
|
# self.journey = msg
|
||||||
return msg
|
# return msg
|
||||||
|
|
||||||
@default_state
|
@default_state
|
||||||
@state
|
@state
|
||||||
@ -192,17 +194,34 @@ class Passenger(Evented, FSM):
|
|||||||
timeout = 60
|
timeout = 60
|
||||||
expiration = self.now + timeout
|
expiration = self.now + timeout
|
||||||
self.info(f"Asking for journey at: { self.pos }")
|
self.info(f"Asking for journey at: { self.pos }")
|
||||||
self.model.broadcast(journey, ttl=timeout, sender=self, agent_class=Driver)
|
self.broadcast(journey, ttl=timeout, agent_class=Driver)
|
||||||
while not self.journey:
|
while not self.journey:
|
||||||
self.debug(f"Waiting for responses at: { self.pos }")
|
self.debug(f"Waiting for responses at: { self.pos }")
|
||||||
try:
|
try:
|
||||||
# This will call process_messages behind the scenes, and the agent's status will be updated
|
offers = await self.received(expiration=expiration, delay=10)
|
||||||
# If you want to avoid that, you can call it with: check=False
|
accepted = None
|
||||||
await self.received(expiration=expiration, delay=10)
|
for event in offers:
|
||||||
|
offer = event.payload
|
||||||
|
if isinstance(offer, Journey):
|
||||||
|
self.journey = offer
|
||||||
|
assert isinstance(event.sender, Driver)
|
||||||
|
try:
|
||||||
|
answer = await event.sender.ask(True, sender=self, timeout=60, delay=5)
|
||||||
|
if answer:
|
||||||
|
accepted = offer
|
||||||
|
self.journey = offer
|
||||||
|
break
|
||||||
|
except events.TimedOut:
|
||||||
|
pass
|
||||||
|
if accepted:
|
||||||
|
for event in offers:
|
||||||
|
if event.payload != accepted:
|
||||||
|
event.sender.tell(False, timeout=60, delay=5)
|
||||||
|
|
||||||
except events.TimedOut:
|
except events.TimedOut:
|
||||||
self.info(f"Still no response. Waiting at: { self.pos }")
|
self.info(f"Still no response. Waiting at: { self.pos }")
|
||||||
self.model.broadcast(
|
self.broadcast(
|
||||||
journey, ttl=timeout, sender=self, agent_class=Driver
|
journey, ttl=timeout, agent_class=Driver
|
||||||
)
|
)
|
||||||
expiration = self.now + timeout
|
expiration = self.now + timeout
|
||||||
self.info(f"Got a response! Waiting for driver")
|
self.info(f"Got a response! Waiting for driver")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from soil.agents import FSM, NetworkAgent, state, default_state, prob
|
from soil.agents import FSM, NetworkAgent, state, default_state
|
||||||
from soil.parameters import *
|
from soil.parameters import *
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
There are two similar implementations of this simulation.
|
There are two similar implementations of this simulation.
|
||||||
|
|
||||||
- `basic`. Using simple primites
|
- `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.
|
- `improved`. Using more advanced features such as the delays to avoid unnecessary computations (i.e., skip steps).
|
||||||
|
|
||||||
The examples can be run directly in the terminal, and they accept command like arguments.
|
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`:
|
For example, to enable the CSV exporter and the Summary exporter, while setting `max_time` to `100` and `seed` to `CustomSeed`:
|
||||||
|
@ -1,22 +1,36 @@
|
|||||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
|
from soil import Evented, FSM, state, default_state, BaseAgent, NetworkAgent, Environment, parameters, report, TimedOut
|
||||||
from enum import Enum
|
|
||||||
from collections import Counter
|
|
||||||
import logging
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from rabbits_basic_sim import RabbitEnv
|
from soilent import Scheduler
|
||||||
|
|
||||||
|
|
||||||
class RabbitsImprovedEnv(RabbitEnv):
|
class RabbitsImprovedEnv(Environment):
|
||||||
|
prob_death: parameters.probability = 1e-3
|
||||||
|
schedule_class = Scheduler
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
"""Initialize the environment with the new versions of the agents"""
|
|
||||||
a1 = self.add_node(Male)
|
a1 = self.add_node(Male)
|
||||||
a2 = self.add_node(Female)
|
a2 = self.add_node(Female)
|
||||||
a1.add_edge(a2)
|
a1.add_edge(a2)
|
||||||
self.add_agent(RandomAccident)
|
self.add_agent(RandomAccident)
|
||||||
|
|
||||||
|
@report
|
||||||
|
@property
|
||||||
|
def num_rabbits(self):
|
||||||
|
return self.count_agents(agent_class=Rabbit)
|
||||||
|
|
||||||
class Rabbit(FSM, NetworkAgent):
|
@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(Evented, FSM, NetworkAgent):
|
||||||
|
|
||||||
sexual_maturity = 30
|
sexual_maturity = 30
|
||||||
life_expectancy = 300
|
life_expectancy = 300
|
||||||
@ -31,42 +45,40 @@ class Rabbit(FSM, NetworkAgent):
|
|||||||
@default_state
|
@default_state
|
||||||
@state
|
@state
|
||||||
def newborn(self):
|
def newborn(self):
|
||||||
self.info("I am a newborn.")
|
self.debug("I am a newborn.")
|
||||||
self.birth = self.now
|
self.birth = self.now
|
||||||
self.offspring = 0
|
self.offspring = 0
|
||||||
return self.youngling.delay(self.sexual_maturity - self.age)
|
return self.youngling
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def youngling(self):
|
async def youngling(self):
|
||||||
if self.age >= self.sexual_maturity:
|
self.debug("I am a youngling.")
|
||||||
self.info(f"I am fertile! My age is {self.age}")
|
await self.delay(self.sexual_maturity - self.age)
|
||||||
return self.fertile
|
assert self.age >= self.sexual_maturity
|
||||||
|
self.debug(f"I am fertile! My age is {self.age}")
|
||||||
|
return self.fertile
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def fertile(self):
|
def fertile(self):
|
||||||
raise Exception("Each subclass should define its fertile state")
|
raise Exception("Each subclass should define its fertile state")
|
||||||
|
|
||||||
@state
|
|
||||||
def dead(self):
|
|
||||||
self.die()
|
|
||||||
|
|
||||||
|
|
||||||
class Male(Rabbit):
|
class Male(Rabbit):
|
||||||
max_females = 5
|
max_females = 5
|
||||||
mating_prob = 0.001
|
mating_prob = 0.005
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def fertile(self):
|
def fertile(self):
|
||||||
if self.age > self.life_expectancy:
|
if self.age > self.life_expectancy:
|
||||||
return self.dead
|
return self.die()
|
||||||
|
|
||||||
# Males try to mate
|
# Males try to mate
|
||||||
for f in self.model.agents(
|
for f in self.model.agents(
|
||||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
||||||
):
|
):
|
||||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
self.debug(f"FOUND A FEMALE: {repr(f)}. Mating with prob {self.mating_prob}")
|
||||||
if self.prob(self["mating_prob"]):
|
if self.prob(self["mating_prob"]):
|
||||||
f.impregnate(self)
|
f.tell(self.unique_id, sender=self, timeout=1)
|
||||||
break # Do not try to impregnate other females
|
break # Do not try to impregnate other females
|
||||||
|
|
||||||
|
|
||||||
@ -75,78 +87,91 @@ class Female(Rabbit):
|
|||||||
conception = None
|
conception = None
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def fertile(self):
|
async def fertile(self):
|
||||||
# Just wait for a Male
|
# Just wait for a Male
|
||||||
if self.age > self.life_expectancy:
|
try:
|
||||||
return self.dead
|
timeout = self.life_expectancy - self.age
|
||||||
if self.conception is not None:
|
while timeout > 0:
|
||||||
return self.pregnant
|
mates = await self.received(timeout=timeout)
|
||||||
|
# assert self.age <= self.life_expectancy
|
||||||
@property
|
for mate in mates:
|
||||||
def pregnancy(self):
|
try:
|
||||||
if self.conception is None:
|
male = self.model.agents[mate.payload]
|
||||||
return None
|
except ValueError:
|
||||||
return self.now - self.conception
|
continue
|
||||||
|
self.debug(f"impregnated by {repr(male)}")
|
||||||
def impregnate(self, male):
|
self.mate = male
|
||||||
self.info(f"impregnated by {repr(male)}")
|
self.number_of_babies = int(8 + 4 * self.random.random())
|
||||||
self.mate = male
|
self.conception = self.now
|
||||||
self.conception = self.now
|
return self.pregnant
|
||||||
self.number_of_babies = int(8 + 4 * self.random.random())
|
except TimedOut:
|
||||||
|
pass
|
||||||
|
return self.die()
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def pregnant(self):
|
async def pregnant(self):
|
||||||
self.debug("I am pregnant")
|
self.debug("I am pregnant")
|
||||||
|
# assert self.mate is not None
|
||||||
|
|
||||||
|
when = min(self.gestation, self.life_expectancy - self.age)
|
||||||
|
if when < 0:
|
||||||
|
return self.die()
|
||||||
|
await self.delay(when)
|
||||||
|
|
||||||
if self.age > self.life_expectancy:
|
if self.age > self.life_expectancy:
|
||||||
self.info("Dying before giving birth")
|
self.debug("Dying before giving birth")
|
||||||
return self.die()
|
return self.die()
|
||||||
|
|
||||||
if self.pregnancy >= self.gestation:
|
# assert self.now - self.conception >= self.gestation
|
||||||
self.info("Having {} babies".format(self.number_of_babies))
|
if not self.alive:
|
||||||
for i in range(self.number_of_babies):
|
return self.die()
|
||||||
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.debug("Having {} babies".format(self.number_of_babies))
|
||||||
self.mate = None
|
for i in range(self.number_of_babies):
|
||||||
return self.fertile
|
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.conception = None
|
||||||
|
return self.fertile
|
||||||
|
|
||||||
def die(self):
|
def die(self):
|
||||||
if self.pregnancy is not None:
|
if self.conception is not None:
|
||||||
self.info("A mother has died carrying a baby!!")
|
self.debug("A mother has died carrying a baby!!")
|
||||||
return super().die()
|
return super().die()
|
||||||
|
|
||||||
|
|
||||||
class RandomAccident(BaseAgent):
|
class RandomAccident(BaseAgent):
|
||||||
|
# Default value, but the value from the environment takes precedence
|
||||||
|
prob_death = 1e-3
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
rabbits_alive = self.model.G.number_of_nodes()
|
|
||||||
|
|
||||||
if not rabbits_alive:
|
alive = self.get_agents(agent_class=Rabbit, alive=True)
|
||||||
return self.die()
|
|
||||||
|
|
||||||
prob_death = self.model.get("prob_death", 1e-100) * math.floor(
|
if not alive:
|
||||||
math.log10(max(1, rabbits_alive))
|
return self.die("No more rabbits to kill")
|
||||||
)
|
|
||||||
|
num_alive = len(alive)
|
||||||
|
prob_death = min(1, self.prob_death * num_alive/10)
|
||||||
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
||||||
for i in self.iter_agents(agent_class=Rabbit):
|
|
||||||
|
for i in alive:
|
||||||
if i.state_id == i.dead.id:
|
if i.state_id == i.dead.id:
|
||||||
continue
|
continue
|
||||||
if self.prob(prob_death):
|
if self.prob(prob_death):
|
||||||
self.info("I killed a rabbit: {}".format(i.id))
|
self.debug("I killed a rabbit: {}".format(i.unique_id))
|
||||||
rabbits_alive -= 1
|
num_alive -= 1
|
||||||
i.die()
|
i.die()
|
||||||
self.debug("Rabbits alive: {}".format(rabbits_alive))
|
self.debug("Rabbits alive: {}".format(num_alive))
|
||||||
|
|
||||||
|
|
||||||
sim = Simulation(model=RabbitsImprovedEnv, max_time=100, seed="MySeed", iterations=1)
|
RabbitsImprovedEnv.run(max_time=1000, seed="MySeed", iterations=1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sim.run()
|
|
@ -1,11 +1,9 @@
|
|||||||
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation, report, parameters as params
|
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, report, parameters as params
|
||||||
from collections import Counter
|
|
||||||
import logging
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
class RabbitEnv(Environment):
|
class RabbitEnv(Environment):
|
||||||
prob_death: params.probability = 1e-100
|
prob_death: params.probability = 1e-3
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
a1 = self.add_node(Male)
|
a1 = self.add_node(Male)
|
||||||
@ -37,7 +35,7 @@ class Rabbit(NetworkAgent, FSM):
|
|||||||
@default_state
|
@default_state
|
||||||
@state
|
@state
|
||||||
def newborn(self):
|
def newborn(self):
|
||||||
self.info("I am a newborn.")
|
self.debug("I am a newborn.")
|
||||||
self.age = 0
|
self.age = 0
|
||||||
self.offspring = 0
|
self.offspring = 0
|
||||||
return self.youngling
|
return self.youngling
|
||||||
@ -46,7 +44,7 @@ class Rabbit(NetworkAgent, FSM):
|
|||||||
def youngling(self):
|
def youngling(self):
|
||||||
self.age += 1
|
self.age += 1
|
||||||
if self.age >= self.sexual_maturity:
|
if self.age >= self.sexual_maturity:
|
||||||
self.info(f"I am fertile! My age is {self.age}")
|
self.debug(f"I am fertile! My age is {self.age}")
|
||||||
return self.fertile
|
return self.fertile
|
||||||
|
|
||||||
@state
|
@state
|
||||||
@ -60,7 +58,7 @@ class Rabbit(NetworkAgent, FSM):
|
|||||||
|
|
||||||
class Male(Rabbit):
|
class Male(Rabbit):
|
||||||
max_females = 5
|
max_females = 5
|
||||||
mating_prob = 0.001
|
mating_prob = 0.005
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def fertile(self):
|
def fertile(self):
|
||||||
@ -70,9 +68,8 @@ class Male(Rabbit):
|
|||||||
return self.dead
|
return self.dead
|
||||||
|
|
||||||
# Males try to mate
|
# Males try to mate
|
||||||
for f in self.model.agents(
|
for f in self.model.agents.filter(
|
||||||
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
|
agent_class=Female, state_id=Female.fertile.id).limit(self.max_females):
|
||||||
):
|
|
||||||
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
|
||||||
if self.prob(self["mating_prob"]):
|
if self.prob(self["mating_prob"]):
|
||||||
f.impregnate(self)
|
f.impregnate(self)
|
||||||
@ -93,14 +90,14 @@ class Female(Rabbit):
|
|||||||
return self.pregnant
|
return self.pregnant
|
||||||
|
|
||||||
def impregnate(self, male):
|
def impregnate(self, male):
|
||||||
self.info(f"impregnated by {repr(male)}")
|
self.debug(f"impregnated by {repr(male)}")
|
||||||
self.mate = male
|
self.mate = male
|
||||||
self.pregnancy = 0
|
self.pregnancy = 0
|
||||||
self.number_of_babies = int(8 + 4 * self.random.random())
|
self.number_of_babies = int(8 + 4 * self.random.random())
|
||||||
|
|
||||||
@state
|
@state
|
||||||
def pregnant(self):
|
def pregnant(self):
|
||||||
self.info("I am pregnant")
|
self.debug("I am pregnant")
|
||||||
self.age += 1
|
self.age += 1
|
||||||
|
|
||||||
if self.age >= self.life_expectancy:
|
if self.age >= self.life_expectancy:
|
||||||
@ -110,7 +107,7 @@ class Female(Rabbit):
|
|||||||
self.pregnancy += 1
|
self.pregnancy += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
self.info("Having {} babies".format(self.number_of_babies))
|
self.debug("Having {} babies".format(self.number_of_babies))
|
||||||
for i in range(self.number_of_babies):
|
for i in range(self.number_of_babies):
|
||||||
state = {}
|
state = {}
|
||||||
agent_class = self.random.choice([Male, Female])
|
agent_class = self.random.choice([Male, Female])
|
||||||
@ -129,33 +126,30 @@ class Female(Rabbit):
|
|||||||
|
|
||||||
def die(self):
|
def die(self):
|
||||||
if "pregnancy" in self and self["pregnancy"] > -1:
|
if "pregnancy" in self and self["pregnancy"] > -1:
|
||||||
self.info("A mother has died carrying a baby!!")
|
self.debug("A mother has died carrying a baby!!")
|
||||||
return super().die()
|
return super().die()
|
||||||
|
|
||||||
|
|
||||||
class RandomAccident(BaseAgent):
|
class RandomAccident(BaseAgent):
|
||||||
|
prob_death = None
|
||||||
def step(self):
|
def step(self):
|
||||||
rabbits_alive = self.model.G.number_of_nodes()
|
alive = self.get_agents(agent_class=Rabbit, alive=True)
|
||||||
|
|
||||||
if not rabbits_alive:
|
if not alive:
|
||||||
return self.die()
|
return self.die("No more rabbits to kill")
|
||||||
|
|
||||||
prob_death = self.model.prob_death * math.floor(
|
num_alive = len(alive)
|
||||||
math.log10(max(1, rabbits_alive))
|
prob_death = min(1, self.prob_death * num_alive/10)
|
||||||
)
|
|
||||||
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
self.debug("Killing some rabbits with prob={}!".format(prob_death))
|
||||||
|
|
||||||
for i in self.get_agents(agent_class=Rabbit):
|
for i in self.get_agents(agent_class=Rabbit):
|
||||||
if i.state_id == i.dead.id:
|
if i.state_id == i.dead.id:
|
||||||
continue
|
continue
|
||||||
if self.prob(prob_death):
|
if self.prob(prob_death):
|
||||||
self.info("I killed a rabbit: {}".format(i.id))
|
self.debug("I killed a rabbit: {}".format(i.unique_id))
|
||||||
rabbits_alive -= 1
|
num_alive -= 1
|
||||||
i.die()
|
i.die()
|
||||||
self.debug("Rabbits alive: {}".format(rabbits_alive))
|
self.debug("Rabbits alive: {}".format(num_alive))
|
||||||
|
|
||||||
|
|
||||||
|
RabbitEnv.run(max_time=1000, seed="MySeed", iterations=1)
|
||||||
sim = Simulation(model=RabbitEnv, max_time=100, seed="MySeed", iterations=1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sim.run()
|
|
@ -1,5 +1,5 @@
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
from soil.agents import NetworkAgent, FSM, custom, state, default_state
|
from soil.agents import FSM, state, default_state
|
||||||
from soil.agents.geo import Geo
|
from soil.agents.geo import Geo
|
||||||
from soil import Environment, Simulation
|
from soil import Environment, Simulation
|
||||||
from soil.parameters import *
|
from soil.parameters import *
|
||||||
|
@ -1 +1 @@
|
|||||||
1.0.0rc3
|
1.0.0rc6
|
||||||
|
@ -19,7 +19,7 @@ from pathlib import Path
|
|||||||
from .agents import *
|
from .agents import *
|
||||||
from . import agents
|
from . import agents
|
||||||
from .simulation import *
|
from .simulation import *
|
||||||
from .environment import Environment, EventedEnvironment
|
from .environment import Environment
|
||||||
from .datacollection import SoilCollector
|
from .datacollection import SoilCollector
|
||||||
from . import serialization
|
from . import serialization
|
||||||
from .utils import logger
|
from .utils import logger
|
||||||
@ -117,13 +117,13 @@ def main(
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--max_time",
|
"--max_time",
|
||||||
default="-1",
|
default="",
|
||||||
help="Set maximum time for the simulation to run. ",
|
help="Set maximum time for the simulation to run. ",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--max_steps",
|
"--max_steps",
|
||||||
default="-1",
|
default="",
|
||||||
help="Set maximum number of steps for the simulation to run.",
|
help="Set maximum number of steps for the simulation to run.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -249,9 +249,12 @@ def main(
|
|||||||
if args.only_convert:
|
if args.only_convert:
|
||||||
print(sim.to_yaml())
|
print(sim.to_yaml())
|
||||||
continue
|
continue
|
||||||
max_time = float(args.max_time) if args.max_time != "-1" else None
|
d = {}
|
||||||
max_steps = float(args.max_steps) if args.max_steps != "-1" else None
|
if args.max_time:
|
||||||
res.append(sim.run(max_time=max_time, max_steps=max_steps))
|
d["max_time"] = float(args.max_time)
|
||||||
|
if args.max_steps:
|
||||||
|
d["max_steps"] = int(args.max_steps)
|
||||||
|
res.append(sim.run(**d))
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if args.pdb:
|
if args.pdb:
|
||||||
|
@ -1,107 +1,23 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict, defaultdict
|
from collections.abc import MutableMapping
|
||||||
from collections.abc import MutableMapping, Mapping, Set
|
from copy import deepcopy
|
||||||
from abc import ABCMeta
|
|
||||||
from copy import deepcopy, copy
|
|
||||||
from functools import partial, wraps
|
|
||||||
from itertools import islice, chain
|
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import networkx as nx
|
|
||||||
import warnings
|
import warnings
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from typing import Any
|
from mesa import Agent as MesaAgent
|
||||||
|
|
||||||
from mesa import Agent as MesaAgent, Model
|
from .. import utils, time
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from .. import serialization, network, utils, time, config
|
from .meta import MetaAgent
|
||||||
|
|
||||||
|
|
||||||
IGNORED_FIELDS = ("model", "logger")
|
IGNORED_FIELDS = ("model", "logger")
|
||||||
|
|
||||||
|
|
||||||
def decorate_generator_step(func, name):
|
|
||||||
@wraps(func)
|
|
||||||
def decorated(self):
|
|
||||||
while True:
|
|
||||||
if self._coroutine is None:
|
|
||||||
self._coroutine = func(self)
|
|
||||||
try:
|
|
||||||
if self._last_except:
|
|
||||||
val = self._coroutine.throw(self._last_except)
|
|
||||||
else:
|
|
||||||
val = self._coroutine.send(self._last_return)
|
|
||||||
except StopIteration as ex:
|
|
||||||
self._coroutine = None
|
|
||||||
val = ex.value
|
|
||||||
finally:
|
|
||||||
self._last_return = None
|
|
||||||
self._last_except = None
|
|
||||||
return float(val) if val is not None else val
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
def decorate_normal_func(func, name):
|
|
||||||
@wraps(func)
|
|
||||||
def decorated(self):
|
|
||||||
val = func(self)
|
|
||||||
return float(val) if val is not None else val
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
class MetaAgent(ABCMeta):
|
|
||||||
def __new__(mcls, name, bases, namespace):
|
|
||||||
defaults = {}
|
|
||||||
|
|
||||||
# Re-use defaults from inherited classes
|
|
||||||
for i in bases:
|
|
||||||
if isinstance(i, MetaAgent):
|
|
||||||
defaults.update(i._defaults)
|
|
||||||
|
|
||||||
new_nmspc = {
|
|
||||||
"_defaults": defaults,
|
|
||||||
}
|
|
||||||
|
|
||||||
for attr, func in namespace.items():
|
|
||||||
if attr == "step":
|
|
||||||
if inspect.isgeneratorfunction(func) or inspect.iscoroutinefunction(func):
|
|
||||||
func = decorate_generator_step(func, attr)
|
|
||||||
new_nmspc.update({
|
|
||||||
"_last_return": None,
|
|
||||||
"_last_except": None,
|
|
||||||
"_coroutine": None,
|
|
||||||
})
|
|
||||||
elif inspect.isasyncgenfunction(func):
|
|
||||||
raise ValueError("Illegal step function: {}. It probably mixes both async/await and yield".format(func))
|
|
||||||
elif inspect.isfunction(func):
|
|
||||||
func = decorate_normal_func(func, attr)
|
|
||||||
else:
|
|
||||||
raise ValueError("Illegal step function: {}".format(func))
|
|
||||||
new_nmspc[attr] = func
|
|
||||||
elif (
|
|
||||||
isinstance(func, types.FunctionType)
|
|
||||||
or isinstance(func, property)
|
|
||||||
or isinstance(func, classmethod)
|
|
||||||
or attr[0] == "_"
|
|
||||||
):
|
|
||||||
new_nmspc[attr] = func
|
|
||||||
elif attr == "defaults":
|
|
||||||
defaults.update(func)
|
|
||||||
elif inspect.isfunction(func):
|
|
||||||
new_nmspc[attr] = func
|
|
||||||
else:
|
|
||||||
defaults[attr] = copy(func)
|
|
||||||
|
|
||||||
|
|
||||||
# Add attributes for their use in the decorated functions
|
|
||||||
return super().__new__(mcls, name, bases, new_nmspc)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
||||||
"""
|
"""
|
||||||
A special type of Mesa Agent that:
|
A special type of Mesa Agent that:
|
||||||
@ -154,11 +70,11 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
return hash(self.unique_id)
|
return hash(self.unique_id)
|
||||||
|
|
||||||
def prob(self, probability):
|
def prob(self, probability):
|
||||||
return prob(probability, self.model.random)
|
return utils.prob(probability, self.model.random)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def w(cls, **kwargs):
|
def w(cls, **kwargs):
|
||||||
return custom(cls, **kwargs)
|
return utils.custom(cls, **kwargs)
|
||||||
|
|
||||||
# TODO: refactor to clean up mesa compatibility
|
# TODO: refactor to clean up mesa compatibility
|
||||||
@property
|
@property
|
||||||
@ -168,21 +84,6 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
print(msg, file=sys.stderr)
|
print(msg, file=sys.stderr)
|
||||||
return self.unique_id
|
return self.unique_id
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, model, attrs, warn_extra=True):
|
|
||||||
ignored = {}
|
|
||||||
args = {}
|
|
||||||
for k, v in attrs.items():
|
|
||||||
if k in inspect.signature(cls).parameters:
|
|
||||||
args[k] = v
|
|
||||||
else:
|
|
||||||
ignored[k] = v
|
|
||||||
if ignored and warn_extra:
|
|
||||||
utils.logger.info(
|
|
||||||
f"Ignoring the following arguments for agent class { agent_class.__name__ }: { ignored }"
|
|
||||||
)
|
|
||||||
return cls(model=model, **args)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
try:
|
try:
|
||||||
return getattr(self, key)
|
return getattr(self, key)
|
||||||
@ -302,399 +203,23 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
|
|||||||
return time.Delay(delay)
|
return time.Delay(delay)
|
||||||
|
|
||||||
|
|
||||||
def prob(prob, random):
|
class Noop(BaseAgent):
|
||||||
"""
|
def step(self):
|
||||||
A true/False uniform distribution with a given probability.
|
return
|
||||||
To be used like this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
if prob(0.3):
|
|
||||||
do_something()
|
|
||||||
|
|
||||||
"""
|
|
||||||
r = random.random()
|
|
||||||
return r < prob
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_distribution(network_agents=None, agent_class=None):
|
|
||||||
"""
|
|
||||||
Calculate the threshold values (thresholds for a uniform distribution)
|
|
||||||
of an agent distribution given the weights of each agent type.
|
|
||||||
|
|
||||||
The input has this form: ::
|
|
||||||
|
|
||||||
[
|
|
||||||
{'agent_class': 'agent_class_1',
|
|
||||||
'weight': 0.2,
|
|
||||||
'state': {
|
|
||||||
'id': 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{'agent_class': 'agent_class_2',
|
|
||||||
'weight': 0.8,
|
|
||||||
'state': {
|
|
||||||
'id': 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
In this example, 20% of the nodes will be marked as type
|
|
||||||
'agent_class_1'.
|
|
||||||
"""
|
|
||||||
if network_agents:
|
|
||||||
network_agents = [
|
|
||||||
deepcopy(agent) for agent in network_agents if not hasattr(agent, "id")
|
|
||||||
]
|
|
||||||
elif agent_class:
|
|
||||||
network_agents = [{"agent_class": agent_class}]
|
|
||||||
else:
|
|
||||||
raise ValueError("Specify a distribution or a default agent type")
|
|
||||||
|
|
||||||
# Fix missing weights and incompatible types
|
|
||||||
for x in network_agents:
|
|
||||||
x["weight"] = float(x.get("weight", 1))
|
|
||||||
|
|
||||||
# Calculate the thresholds
|
|
||||||
total = sum(x["weight"] for x in network_agents)
|
|
||||||
acc = 0
|
|
||||||
for v in network_agents:
|
|
||||||
if "ids" in v:
|
|
||||||
continue
|
|
||||||
upper = acc + (v["weight"] / total)
|
|
||||||
v["threshold"] = [acc, upper]
|
|
||||||
acc = upper
|
|
||||||
return network_agents
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_type(agent_class, known_modules=[], **kwargs):
|
|
||||||
if isinstance(agent_class, str):
|
|
||||||
return agent_class
|
|
||||||
known_modules += ["soil.agents"]
|
|
||||||
return serialization.serialize(agent_class, known_modules=known_modules, **kwargs)[
|
|
||||||
1
|
|
||||||
] # Get the name of the class
|
|
||||||
|
|
||||||
|
|
||||||
def _deserialize_type(agent_class, known_modules=[]):
|
|
||||||
if not isinstance(agent_class, str):
|
|
||||||
return agent_class
|
|
||||||
known = known_modules + ["soil.agents", "soil.agents.custom"]
|
|
||||||
agent_class = serialization.deserializer(agent_class, known_modules=known)
|
|
||||||
return agent_class
|
|
||||||
|
|
||||||
|
|
||||||
class AgentView(Mapping, Set):
|
|
||||||
"""A lazy-loaded list of agents."""
|
|
||||||
|
|
||||||
__slots__ = ("_agents",)
|
|
||||||
|
|
||||||
def __init__(self, agents):
|
|
||||||
self._agents = agents
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return {"_agents": self._agents}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
self._agents = state["_agents"]
|
|
||||||
|
|
||||||
# Mapping methods
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._agents)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
yield from self._agents.values()
|
|
||||||
|
|
||||||
def __getitem__(self, agent_id):
|
|
||||||
if isinstance(agent_id, slice):
|
|
||||||
raise ValueError(f"Slicing is not supported")
|
|
||||||
if agent_id in self._agents:
|
|
||||||
return self._agents[agent_id]
|
|
||||||
raise ValueError(f"Agent {agent_id} not found")
|
|
||||||
|
|
||||||
def filter(self, *args, **kwargs):
|
|
||||||
yield from filter_agents(self._agents, *args, **kwargs)
|
|
||||||
|
|
||||||
def one(self, *args, **kwargs):
|
|
||||||
return next(filter_agents(self._agents, *args, **kwargs))
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
return list(self.filter(*args, **kwargs))
|
|
||||||
|
|
||||||
def __contains__(self, agent_id):
|
|
||||||
return agent_id in self._agents
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(list(unique_id for unique_id in self.keys()))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__class__.__name__}({self})"
|
|
||||||
|
|
||||||
|
|
||||||
def filter_agents(
|
|
||||||
agents: dict,
|
|
||||||
*id_args,
|
|
||||||
unique_id=None,
|
|
||||||
state_id=None,
|
|
||||||
agent_class=None,
|
|
||||||
ignore=None,
|
|
||||||
state=None,
|
|
||||||
limit=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Filter agents given as a dict, by the criteria given as arguments (e.g., certain type or state id).
|
|
||||||
"""
|
|
||||||
assert isinstance(agents, dict)
|
|
||||||
|
|
||||||
ids = []
|
|
||||||
|
|
||||||
if unique_id is not None:
|
|
||||||
if isinstance(unique_id, list):
|
|
||||||
ids += unique_id
|
|
||||||
else:
|
|
||||||
ids.append(unique_id)
|
|
||||||
|
|
||||||
if id_args:
|
|
||||||
ids += id_args
|
|
||||||
|
|
||||||
if ids:
|
|
||||||
f = (agents[aid] for aid in ids if aid in agents)
|
|
||||||
else:
|
|
||||||
f = agents.values()
|
|
||||||
|
|
||||||
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
|
||||||
state_id = tuple([state_id])
|
|
||||||
|
|
||||||
if agent_class is not None:
|
|
||||||
agent_class = _deserialize_type(agent_class)
|
|
||||||
try:
|
|
||||||
agent_class = tuple(agent_class)
|
|
||||||
except TypeError:
|
|
||||||
agent_class = tuple([agent_class])
|
|
||||||
|
|
||||||
if ignore:
|
|
||||||
f = filter(lambda x: x not in ignore, f)
|
|
||||||
|
|
||||||
if state_id is not None:
|
|
||||||
f = filter(lambda agent: agent.get("state_id", None) in state_id, f)
|
|
||||||
|
|
||||||
if agent_class is not None:
|
|
||||||
f = filter(lambda agent: isinstance(agent, agent_class), f)
|
|
||||||
|
|
||||||
state = state or dict()
|
|
||||||
state.update(kwargs)
|
|
||||||
|
|
||||||
for k, vs in state.items():
|
|
||||||
if not isinstance(vs, list):
|
|
||||||
vs = [vs]
|
|
||||||
f = filter(lambda agent: any(getattr(agent, k, None) == v for v in vs), f)
|
|
||||||
|
|
||||||
if limit is not None:
|
|
||||||
f = islice(f, limit)
|
|
||||||
|
|
||||||
yield from f
|
|
||||||
|
|
||||||
|
|
||||||
def from_config(
|
|
||||||
cfg: config.AgentConfig, random, topology: nx.Graph = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
This function turns an agentconfig into a list of individual "agent specifications", which are just a dictionary
|
|
||||||
with the parameters that the environment will use to construct each agent.
|
|
||||||
|
|
||||||
This function does NOT return a list of agents, mostly because some attributes to the agent are not known at the
|
|
||||||
time of calling this function, such as `unique_id`.
|
|
||||||
"""
|
|
||||||
default = cfg or config.AgentConfig()
|
|
||||||
if not isinstance(cfg, config.AgentConfig):
|
|
||||||
cfg = config.AgentConfig(**cfg)
|
|
||||||
|
|
||||||
agents = []
|
|
||||||
|
|
||||||
assigned_total = 0
|
|
||||||
assigned_network = 0
|
|
||||||
|
|
||||||
if cfg.fixed is not None:
|
|
||||||
agents, assigned_total, assigned_network = _from_fixed(
|
|
||||||
cfg.fixed, topology=cfg.topology, default=cfg
|
|
||||||
)
|
|
||||||
|
|
||||||
n = cfg.n
|
|
||||||
|
|
||||||
if cfg.distribution:
|
|
||||||
topo_size = len(topology) if topology else 0
|
|
||||||
|
|
||||||
networked = []
|
|
||||||
total = []
|
|
||||||
|
|
||||||
for d in cfg.distribution:
|
|
||||||
if d.strategy == config.Strategy.topology:
|
|
||||||
topo = d.topology if ("topology" in d.__fields_set__) else cfg.topology
|
|
||||||
if not topo:
|
|
||||||
raise ValueError(
|
|
||||||
'The "topology" strategy only works if the topology parameter is set to True'
|
|
||||||
)
|
|
||||||
if not topo_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Topology does not have enough free nodes to assign one to the agent"
|
|
||||||
)
|
|
||||||
|
|
||||||
networked.append(d)
|
|
||||||
|
|
||||||
if d.strategy == config.Strategy.total:
|
|
||||||
if not cfg.n:
|
|
||||||
raise ValueError(
|
|
||||||
'Cannot use the "total" strategy without providing the total number of agents'
|
|
||||||
)
|
|
||||||
total.append(d)
|
|
||||||
|
|
||||||
if networked:
|
|
||||||
new_agents = _from_distro(
|
|
||||||
networked,
|
|
||||||
n=topo_size - assigned_network,
|
|
||||||
topology=topo,
|
|
||||||
default=cfg,
|
|
||||||
random=random,
|
|
||||||
)
|
|
||||||
assigned_total += len(new_agents)
|
|
||||||
assigned_network += len(new_agents)
|
|
||||||
agents += new_agents
|
|
||||||
|
|
||||||
if total:
|
|
||||||
remaining = n - assigned_total
|
|
||||||
agents += _from_distro(total, n=remaining, default=cfg, random=random)
|
|
||||||
|
|
||||||
if assigned_network < topo_size:
|
|
||||||
utils.logger.warn(
|
|
||||||
f"The total number of agents does not match the total number of nodes in "
|
|
||||||
"every topology. This may be due to a definition error: assigned: "
|
|
||||||
f"{ assigned } total size: { topo_size }"
|
|
||||||
)
|
|
||||||
|
|
||||||
return agents
|
|
||||||
|
|
||||||
|
|
||||||
def _from_fixed(
|
|
||||||
lst: List[config.FixedAgentConfig],
|
|
||||||
topology: bool,
|
|
||||||
default: config.SingleAgentConfig,
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
agents = []
|
|
||||||
|
|
||||||
counts_total = 0
|
|
||||||
counts_network = 0
|
|
||||||
|
|
||||||
for fixed in lst:
|
|
||||||
agent = {}
|
|
||||||
if default:
|
|
||||||
agent = default.state.copy()
|
|
||||||
agent.update(fixed.state)
|
|
||||||
cls = serialization.deserialize(
|
|
||||||
fixed.agent_class or (default and default.agent_class)
|
|
||||||
)
|
|
||||||
agent["agent_class"] = cls
|
|
||||||
topo = (
|
|
||||||
fixed.topology
|
|
||||||
if ("topology" in fixed.__fields_set__)
|
|
||||||
else topology or default.topology
|
|
||||||
)
|
|
||||||
|
|
||||||
if topo:
|
|
||||||
agent["topology"] = True
|
|
||||||
counts_network += 1
|
|
||||||
if not fixed.hidden:
|
|
||||||
counts_total += 1
|
|
||||||
agents.append(agent)
|
|
||||||
|
|
||||||
return agents, counts_total, counts_network
|
|
||||||
|
|
||||||
|
|
||||||
def _from_distro(
|
|
||||||
distro: List[config.AgentDistro],
|
|
||||||
n: int,
|
|
||||||
default: config.SingleAgentConfig,
|
|
||||||
random,
|
|
||||||
topology: str = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
|
|
||||||
agents = []
|
|
||||||
|
|
||||||
if n is None:
|
|
||||||
if any(lambda dist: dist.n is None, distro):
|
|
||||||
raise ValueError(
|
|
||||||
"You must provide a total number of agents, or the number of each type"
|
|
||||||
)
|
|
||||||
n = sum(dist.n for dist in distro)
|
|
||||||
|
|
||||||
weights = list(dist.weight if dist.weight is not None else 1 for dist in distro)
|
|
||||||
minw = min(weights)
|
|
||||||
norm = list(weight / minw for weight in weights)
|
|
||||||
total = sum(norm)
|
|
||||||
chunk = n // total
|
|
||||||
|
|
||||||
# random.choices would be enough to get a weighted distribution. But it can vary a lot for smaller k
|
|
||||||
# So instead we calculate our own distribution to make sure the actual ratios are close to what we would expect
|
|
||||||
|
|
||||||
# Calculate how many times each has to appear
|
|
||||||
indices = list(
|
|
||||||
chain.from_iterable([idx] * int(n * chunk) for (idx, n) in enumerate(norm))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Complete with random agents following the original weight distribution
|
|
||||||
if len(indices) < n:
|
|
||||||
indices += random.choices(
|
|
||||||
list(range(len(distro))),
|
|
||||||
weights=[d.weight for d in distro],
|
|
||||||
k=n - len(indices),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Deserialize classes for efficiency
|
|
||||||
classes = list(
|
|
||||||
serialization.deserialize(i.agent_class or default.agent_class) for i in distro
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add them in random order
|
|
||||||
random.shuffle(indices)
|
|
||||||
|
|
||||||
for idx in indices:
|
|
||||||
d = distro[idx]
|
|
||||||
agent = d.state.copy()
|
|
||||||
cls = classes[idx]
|
|
||||||
agent["agent_class"] = cls
|
|
||||||
if default:
|
|
||||||
agent.update(default.state)
|
|
||||||
topology = (
|
|
||||||
d.topology
|
|
||||||
if ("topology" in d.__fields_set__)
|
|
||||||
else topology or default.topology
|
|
||||||
)
|
|
||||||
if topology:
|
|
||||||
agent["topology"] = topology
|
|
||||||
agents.append(agent)
|
|
||||||
|
|
||||||
return agents
|
|
||||||
|
|
||||||
|
|
||||||
from .network_agents import *
|
from .network_agents import *
|
||||||
from .fsm import *
|
from .fsm import *
|
||||||
from .evented import *
|
from .evented import *
|
||||||
from typing import Optional
|
from .view import *
|
||||||
|
|
||||||
|
|
||||||
class Agent(NetworkAgent, FSM, EventedAgent):
|
class Agent(FSM, EventedAgent, NetworkAgent):
|
||||||
"""Default agent class, has both network and event capabilities"""
|
"""Default agent class, has network, FSM and event capabilities"""
|
||||||
|
|
||||||
|
|
||||||
from ..environment import NetworkEnvironment
|
|
||||||
|
|
||||||
|
|
||||||
|
# Additional types of agents
|
||||||
from .BassModel import *
|
from .BassModel import *
|
||||||
from .IndependentCascadeModel import *
|
from .IndependentCascadeModel import *
|
||||||
from .SISaModel import *
|
from .SISaModel import *
|
||||||
from .CounterModel import *
|
from .CounterModel import *
|
||||||
|
|
||||||
|
|
||||||
def custom(cls, **kwargs):
|
|
||||||
"""Create a new class from a template class and keyword arguments"""
|
|
||||||
return type(cls.__name__, (cls,), kwargs)
|
|
@ -1,58 +1,34 @@
|
|||||||
from . import BaseAgent
|
from . import BaseAgent
|
||||||
from ..events import Message, Tell, Ask, TimedOut
|
from ..events import Message, Tell, Ask, TimedOut
|
||||||
|
from .. import environment, events
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from types import coroutine
|
from types import coroutine
|
||||||
|
|
||||||
|
# from soilent import Scheduler
|
||||||
|
|
||||||
|
|
||||||
class EventedAgent(BaseAgent):
|
class EventedAgent(BaseAgent):
|
||||||
|
# scheduler_class = Scheduler
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._inbox = deque()
|
assert isinstance(self.model, environment.EventedEnvironment), "EventedAgent requires an EventedEnvironment"
|
||||||
self._processed = 0
|
self.model.register(self)
|
||||||
|
|
||||||
def on_receive(self, *args, **kwargs):
|
def received(self, **kwargs):
|
||||||
pass
|
return self.model.received(self, **kwargs)
|
||||||
|
|
||||||
@coroutine
|
def tell(self, msg, **kwargs):
|
||||||
def received(self, expiration=None, timeout=60, delay=1, process=True):
|
return self.model.tell(msg, recipient=self, **kwargs)
|
||||||
if not expiration:
|
|
||||||
expiration = self.now + timeout
|
|
||||||
while self.now < expiration:
|
|
||||||
if self._inbox:
|
|
||||||
msgs = self._inbox
|
|
||||||
if process:
|
|
||||||
self.process_messages()
|
|
||||||
return msgs
|
|
||||||
yield self.delay(delay)
|
|
||||||
raise TimedOut("No message received")
|
|
||||||
|
|
||||||
def tell(self, msg, sender=None):
|
def broadcast(self, msg, **kwargs):
|
||||||
self._inbox.append(Tell(timestamp=self.now, payload=msg, sender=sender))
|
return self.model.broadcast(msg, sender=self, **kwargs)
|
||||||
|
|
||||||
@coroutine
|
def ask(self, msg, **kwargs):
|
||||||
def ask(self, msg, expiration=None, timeout=None, delay=1):
|
return self.model.ask(msg, recipient=self, **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
|
|
||||||
while self.now < expiration:
|
|
||||||
if ask.reply:
|
|
||||||
return ask.reply
|
|
||||||
yield self.delay(delay)
|
|
||||||
raise TimedOut("No reply received")
|
|
||||||
|
|
||||||
def process_messages(self):
|
def process_messages(self):
|
||||||
valid = list()
|
return self.model.process_messages(self.model.inbox_for(self))
|
||||||
for msg in self._inbox:
|
|
||||||
self._processed += 1
|
|
||||||
if msg.expired(self.now):
|
|
||||||
continue
|
|
||||||
valid.append(msg)
|
|
||||||
reply = self.on_receive(msg.payload, sender=msg.sender)
|
|
||||||
if isinstance(msg, Ask):
|
|
||||||
msg.reply = reply
|
|
||||||
self._inbox.clear()
|
|
||||||
return valid
|
|
||||||
|
|
||||||
|
|
||||||
Evented = EventedAgent
|
Evented = EventedAgent
|
||||||
|
@ -140,11 +140,19 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
|||||||
self._set_state(value)
|
self._set_state(value)
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
|
if self._state is None:
|
||||||
|
if len(self._states) == 1:
|
||||||
|
raise Exception("Agent class has no valid states: {}. Make sure to define states or define a custom step function".format(self.__class__.__name__))
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid state (None) for agent {}".format(self))
|
||||||
|
|
||||||
self._check_alive()
|
self._check_alive()
|
||||||
next_state = yield from self._state.step(self)
|
next_state = yield from self._state.step(self)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
next_state, when = next_state
|
next_state, when = next_state
|
||||||
|
self._set_state(next_state)
|
||||||
|
return when
|
||||||
except (TypeError, ValueError) as ex:
|
except (TypeError, ValueError) as ex:
|
||||||
try:
|
try:
|
||||||
self._set_state(next_state)
|
self._set_state(next_state)
|
||||||
@ -152,9 +160,6 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return next_state
|
return next_state
|
||||||
|
|
||||||
self._set_state(next_state)
|
|
||||||
return when
|
|
||||||
|
|
||||||
def _set_state(self, state):
|
def _set_state(self, state):
|
||||||
if state is None:
|
if state is None:
|
||||||
return
|
return
|
||||||
|
87
soil/agents/meta.py
Normal file
87
soil/agents/meta.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from abc import ABCMeta
|
||||||
|
from copy import copy
|
||||||
|
from functools import wraps
|
||||||
|
from .. import time
|
||||||
|
|
||||||
|
import types
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
def decorate_generator_step(func, name):
|
||||||
|
@wraps(func)
|
||||||
|
def decorated(self):
|
||||||
|
if not self.alive:
|
||||||
|
return time.INFINITY
|
||||||
|
|
||||||
|
if self._coroutine is None:
|
||||||
|
self._coroutine = func(self)
|
||||||
|
try:
|
||||||
|
if self._last_except:
|
||||||
|
val = self._coroutine.throw(self._last_except)
|
||||||
|
else:
|
||||||
|
val = self._coroutine.send(self._last_return)
|
||||||
|
except StopIteration as ex:
|
||||||
|
self._coroutine = None
|
||||||
|
val = ex.value
|
||||||
|
finally:
|
||||||
|
self._last_return = None
|
||||||
|
self._last_except = None
|
||||||
|
return float(val) if val is not None else val
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def decorate_normal_step(func, name):
|
||||||
|
@wraps(func)
|
||||||
|
def decorated(self):
|
||||||
|
# if not self.alive:
|
||||||
|
# return time.INFINITY
|
||||||
|
val = func(self)
|
||||||
|
return float(val) if val is not None else val
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
class MetaAgent(ABCMeta):
|
||||||
|
def __new__(mcls, name, bases, namespace):
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
|
# Re-use defaults from inherited classes
|
||||||
|
for i in bases:
|
||||||
|
if isinstance(i, MetaAgent):
|
||||||
|
defaults.update(i._defaults)
|
||||||
|
|
||||||
|
new_nmspc = {
|
||||||
|
"_defaults": defaults,
|
||||||
|
}
|
||||||
|
|
||||||
|
for attr, func in namespace.items():
|
||||||
|
if attr == "step":
|
||||||
|
if inspect.isgeneratorfunction(func) or inspect.iscoroutinefunction(func):
|
||||||
|
func = decorate_generator_step(func, attr)
|
||||||
|
new_nmspc.update({
|
||||||
|
"_last_return": None,
|
||||||
|
"_last_except": None,
|
||||||
|
"_coroutine": None,
|
||||||
|
})
|
||||||
|
elif inspect.isasyncgenfunction(func):
|
||||||
|
raise ValueError("Illegal step function: {}. It probably mixes both async/await and yield".format(func))
|
||||||
|
elif inspect.isfunction(func):
|
||||||
|
func = decorate_normal_step(func, attr)
|
||||||
|
else:
|
||||||
|
raise ValueError("Illegal step function: {}".format(func))
|
||||||
|
new_nmspc[attr] = func
|
||||||
|
elif (
|
||||||
|
isinstance(func, types.FunctionType)
|
||||||
|
or isinstance(func, property)
|
||||||
|
or isinstance(func, classmethod)
|
||||||
|
or attr[0] == "_"
|
||||||
|
):
|
||||||
|
new_nmspc[attr] = func
|
||||||
|
elif attr == "defaults":
|
||||||
|
defaults.update(func)
|
||||||
|
elif inspect.isfunction(func):
|
||||||
|
new_nmspc[attr] = func
|
||||||
|
else:
|
||||||
|
defaults[attr] = copy(func)
|
||||||
|
|
||||||
|
|
||||||
|
# Add attributes for their use in the decorated functions
|
||||||
|
return super().__new__(mcls, name, bases, new_nmspc)
|
@ -1,9 +1,11 @@
|
|||||||
from . import BaseAgent
|
from . import BaseAgent
|
||||||
|
from .. import environment
|
||||||
|
|
||||||
|
|
||||||
class NetworkAgent(BaseAgent):
|
class NetworkAgent(BaseAgent):
|
||||||
def __init__(self, *args, topology=None, init=True, node_id=None, **kwargs):
|
def __init__(self, *args, topology=None, init=True, node_id=None, **kwargs):
|
||||||
super().__init__(*args, init=False, **kwargs)
|
super().__init__(*args, init=False, **kwargs)
|
||||||
|
assert isinstance(self.model, environment.NetworkEnvironment), "NetworkAgent requires a NetworkEnvironment"
|
||||||
|
|
||||||
self.G = topology or self.model.G
|
self.G = topology or self.model.G
|
||||||
assert self.G is not None, "Network agents should have a network"
|
assert self.G is not None, "Network agents should have a network"
|
||||||
@ -16,7 +18,7 @@ class NetworkAgent(BaseAgent):
|
|||||||
else:
|
else:
|
||||||
node_id = len(self.G)
|
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.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)
|
self.G.add_node(node_id, find_unassigned=True)
|
||||||
assert node_id is not None
|
assert node_id is not None
|
||||||
self.G.nodes[node_id]["agent"] = self
|
self.G.nodes[node_id]["agent"] = self
|
||||||
self.node_id = node_id
|
self.node_id = node_id
|
||||||
|
136
soil/agents/view.py
Normal file
136
soil/agents/view.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
from collections.abc import Mapping, Set
|
||||||
|
from itertools import islice
|
||||||
|
|
||||||
|
|
||||||
|
class AgentView(Mapping, Set):
|
||||||
|
"""A lazy-loaded list of agents."""
|
||||||
|
|
||||||
|
__slots__ = ("_agents", "agents_by_type")
|
||||||
|
|
||||||
|
def __init__(self, agents, agents_by_type):
|
||||||
|
self._agents = agents
|
||||||
|
self.agents_by_type = agents_by_type
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return {"_agents": self._agents}
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
self._agents = state["_agents"]
|
||||||
|
|
||||||
|
# Mapping methods
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._agents)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self._agents.values()
|
||||||
|
|
||||||
|
def __getitem__(self, agent_id):
|
||||||
|
if isinstance(agent_id, slice):
|
||||||
|
raise ValueError(f"Slicing is not supported")
|
||||||
|
if agent_id in self._agents:
|
||||||
|
return self._agents[agent_id]
|
||||||
|
raise ValueError(f"Agent {agent_id} not found")
|
||||||
|
|
||||||
|
def filter(self, agent_class=None, include_subclasses=True, **kwargs):
|
||||||
|
if agent_class and self.agents_by_type:
|
||||||
|
if not include_subclasses:
|
||||||
|
return filter_agents(self.agents_by_type[agent_class],
|
||||||
|
**kwargs)
|
||||||
|
else:
|
||||||
|
d = {}
|
||||||
|
for k, v in self.agents_by_type.items():
|
||||||
|
if (k == agent_class) or issubclass(k, agent_class):
|
||||||
|
d.update(v)
|
||||||
|
return filter_agents(d, **kwargs)
|
||||||
|
return filter_agents(self._agents, agent_class=agent_class, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def one(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return next(self.filter(*args, **kwargs))
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return list(self.filter(*args, **kwargs))
|
||||||
|
|
||||||
|
def __contains__(self, agent_id):
|
||||||
|
return agent_id in self._agents
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(list(unique_id for unique_id in self.keys()))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self})"
|
||||||
|
|
||||||
|
|
||||||
|
def filter_agents(
|
||||||
|
agents: dict,
|
||||||
|
*id_args,
|
||||||
|
unique_id=None,
|
||||||
|
state_id=None,
|
||||||
|
agent_class=None,
|
||||||
|
ignore=None,
|
||||||
|
state=None,
|
||||||
|
limit=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Filter agents given as a dict, by the criteria given as arguments (e.g., certain type or state id).
|
||||||
|
"""
|
||||||
|
assert isinstance(agents, dict)
|
||||||
|
|
||||||
|
ids = []
|
||||||
|
|
||||||
|
if unique_id is not None:
|
||||||
|
if isinstance(unique_id, list):
|
||||||
|
ids += unique_id
|
||||||
|
else:
|
||||||
|
ids.append(unique_id)
|
||||||
|
|
||||||
|
if id_args:
|
||||||
|
ids += id_args
|
||||||
|
|
||||||
|
if ids:
|
||||||
|
f = list(agents[aid] for aid in ids if aid in agents)
|
||||||
|
else:
|
||||||
|
f = agents.values()
|
||||||
|
|
||||||
|
if state_id is not None and not isinstance(state_id, (tuple, list)):
|
||||||
|
state_id = tuple([state_id])
|
||||||
|
|
||||||
|
if ignore:
|
||||||
|
f = filter(lambda x: x not in ignore, f)
|
||||||
|
|
||||||
|
if state_id is not None:
|
||||||
|
f = filter(lambda agent: agent.get("state_id", None) in state_id, f)
|
||||||
|
|
||||||
|
if agent_class is not None:
|
||||||
|
f = filter(lambda agent: isinstance(agent, agent_class), f)
|
||||||
|
|
||||||
|
state = state or dict()
|
||||||
|
state.update(kwargs)
|
||||||
|
|
||||||
|
for k, vs in state.items():
|
||||||
|
if not isinstance(vs, list):
|
||||||
|
vs = [vs]
|
||||||
|
f = filter(lambda agent: any(getattr(agent, k, None) == v for v in vs), f)
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
|
f = islice(f, limit)
|
||||||
|
|
||||||
|
return AgentResult(f)
|
||||||
|
|
||||||
|
class AgentResult:
|
||||||
|
def __init__(self, iterator):
|
||||||
|
self.iterator = iterator
|
||||||
|
|
||||||
|
def limit(self, limit):
|
||||||
|
self.iterator = islice(self.iterator, limit)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.iterator)
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return next(self.iterator)
|
@ -43,15 +43,14 @@ def read_sql(fpath=None, name=None, include_agents=False):
|
|||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
env = pd.read_sql_table("env", con=conn,
|
env = pd.read_sql_table("env", con=conn,
|
||||||
index_col="step").reset_index().set_index([
|
index_col="step").reset_index().set_index([
|
||||||
"simulation_id", "params_id",
|
"params_id", "iteration_id", "step"
|
||||||
"iteration_id", "step"
|
|
||||||
])
|
])
|
||||||
agents = pd.read_sql_table("agents", con=conn, index_col=["simulation_id", "params_id", "iteration_id", "step", "agent_id"])
|
agents = pd.read_sql_table("agents", con=conn, index_col=["params_id", "iteration_id", "step", "agent_id"])
|
||||||
config = pd.read_sql_table("configuration", con=conn, index_col="simulation_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"])
|
parameters = pd.read_sql_table("parameters", con=conn, index_col=["simulation_id", "params_id", "iteration_id"])
|
||||||
try:
|
# try:
|
||||||
parameters = parameters.pivot(columns="key", values="value")
|
# parameters = parameters.pivot(columns="key", values="value")
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
print(f"warning: coult not pivot parameters: {e}")
|
# print(f"warning: coult not pivot parameters: {e}")
|
||||||
|
|
||||||
return Results(config, parameters, env, agents)
|
return Results(config, parameters, env, agents)
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sys
|
||||||
import math
|
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, Optional, Union, List, Type
|
from typing import Any, Callable, Dict, Optional, Union, List, Type
|
||||||
from collections import namedtuple
|
from types import coroutine
|
||||||
from time import time as current_time
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
|
||||||
from mesa import Model, Agent
|
|
||||||
|
from mesa import Model
|
||||||
|
|
||||||
from . import agents as agentmod, datacollection, utils, time, network, events
|
from . import agents as agentmod, datacollection, utils, time, network, events
|
||||||
|
|
||||||
@ -114,14 +110,14 @@ class BaseEnvironment(Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def agents(self):
|
def agents(self):
|
||||||
return agentmod.AgentView(self.schedule._agents)
|
return agentmod.AgentView(self.schedule._agents, getattr(self.schedule, "agents_by_type", None))
|
||||||
|
|
||||||
def agent(self, *args, **kwargs):
|
def agent(self, *args, **kwargs):
|
||||||
return agentmod.AgentView(self.schedule._agents).one(*args, **kwargs)
|
return agentmod.AgentView(self.schedule._agents, self.schedule.agents_by_type).one(*args, **kwargs)
|
||||||
|
|
||||||
def count_agents(self, *args, **kwargs):
|
def count_agents(self, *args, **kwargs):
|
||||||
return sum(1 for i in self.agents(*args, **kwargs))
|
return sum(1 for i in self.agents(*args, **kwargs))
|
||||||
|
|
||||||
def agent_df(self, steps=False):
|
def agent_df(self, steps=False):
|
||||||
df = self.datacollector.get_agent_vars_dataframe()
|
df = self.datacollector.get_agent_vars_dataframe()
|
||||||
if steps:
|
if steps:
|
||||||
@ -205,11 +201,18 @@ class BaseEnvironment(Model):
|
|||||||
func = name
|
func = name
|
||||||
self.datacollector._new_model_reporter(name, func)
|
self.datacollector._new_model_reporter(name, func)
|
||||||
|
|
||||||
def add_agent_reporter(self, name, reporter=None, agent_type=None):
|
def add_agent_reporter(self, name, reporter=None, agent_class=None, *, agent_type=None):
|
||||||
if not agent_type and not reporter:
|
if agent_type:
|
||||||
|
print("agent_type is deprecated, use agent_class instead", file=sys.stderr)
|
||||||
|
agent_class = agent_type or agent_class
|
||||||
|
if not reporter and not agent_class:
|
||||||
reporter = name
|
reporter = name
|
||||||
elif agent_type:
|
if agent_class:
|
||||||
reporter = lambda a: reporter(a) if isinstance(a, agent_type) else None
|
if reporter:
|
||||||
|
_orig = reporter
|
||||||
|
else:
|
||||||
|
_orig = lambda a: getattr(a, name)
|
||||||
|
reporter = lambda a: (_orig(a) if isinstance(a, agent_class) else None)
|
||||||
self.datacollector._new_agent_reporter(name, reporter)
|
self.datacollector._new_agent_reporter(name, reporter)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -331,15 +334,16 @@ class NetworkEnvironment(BaseEnvironment):
|
|||||||
if getattr(agent, "alive", True):
|
if getattr(agent, "alive", True):
|
||||||
yield agent
|
yield agent
|
||||||
|
|
||||||
def add_node(self, agent_class, unique_id=None, node_id=None, **kwargs):
|
def add_node(self, agent_class, unique_id=None, node_id=None, find_unassigned=False, **kwargs):
|
||||||
if unique_id is None:
|
if unique_id is None:
|
||||||
unique_id = self.next_id()
|
unique_id = self.next_id()
|
||||||
if node_id is None:
|
if node_id is None:
|
||||||
node_id = network.find_unassigned(
|
if find_unassigned:
|
||||||
G=self.G, shuffle=True, random=self.random
|
node_id = network.find_unassigned(
|
||||||
)
|
G=self.G, shuffle=True, random=self.random
|
||||||
|
)
|
||||||
if node_id is None:
|
if node_id is None:
|
||||||
node_id = f"node_for_{unique_id}"
|
node_id = f"Node_for_agent_{unique_id}"
|
||||||
|
|
||||||
if node_id not in self.G.nodes:
|
if node_id not in self.G.nodes:
|
||||||
self.G.add_node(node_id)
|
self.G.add_node(node_id)
|
||||||
@ -409,27 +413,84 @@ class NetworkEnvironment(BaseEnvironment):
|
|||||||
|
|
||||||
|
|
||||||
class EventedEnvironment(BaseEnvironment):
|
class EventedEnvironment(BaseEnvironment):
|
||||||
def broadcast(self, msg, sender=None, expiration=None, ttl=None, **kwargs):
|
|
||||||
for agent in self.agents(**kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if agent == sender:
|
self._inbox = dict()
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def register(self, agent):
|
||||||
|
self._inbox[agent.unique_id] = []
|
||||||
|
|
||||||
|
def inbox_for(self, agent):
|
||||||
|
try:
|
||||||
|
return self._inbox[agent.unique_id]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(f"Trying to access inbox for unregistered agent: {agent} (class: {type(agent)}). "
|
||||||
|
"Make sure your agent is of type EventedAgent and it is registered with the environment.")
|
||||||
|
|
||||||
|
@coroutine
|
||||||
|
def received(self, agent, expiration=None, timeout=60, delay=1):
|
||||||
|
if not expiration:
|
||||||
|
expiration = self.now + timeout
|
||||||
|
inbox = self.inbox_for(agent)
|
||||||
|
if inbox:
|
||||||
|
return self.process_messages(inbox)
|
||||||
|
while self.now < expiration:
|
||||||
|
# TODO: this wakes the agent up at every step. It would be better to wait until timeout (or inf)
|
||||||
|
# and if a message is received before that, reschedule the agent when
|
||||||
|
if inbox:
|
||||||
|
return self.process_messages(inbox)
|
||||||
|
yield time.Delay(delay)
|
||||||
|
raise events.TimedOut("No message received")
|
||||||
|
|
||||||
|
def tell(self, msg, sender, recipient, expiration=None, timeout=None, **kwargs):
|
||||||
|
if expiration is None:
|
||||||
|
expiration = float("inf") if timeout is None else self.now + timeout
|
||||||
|
self.inbox_for(recipient).append(
|
||||||
|
events.Tell(timestamp=self.now,
|
||||||
|
payload=msg,
|
||||||
|
sender=sender,
|
||||||
|
expiration=expiration,
|
||||||
|
**kwargs))
|
||||||
|
|
||||||
|
def broadcast(self, msg, sender, ttl=None, expiration=None, agent_class=None):
|
||||||
|
expiration = expiration if ttl is None else self.now + ttl
|
||||||
|
# This only works for Soil environments. Mesa agents do not have an `agents` method
|
||||||
|
sender_id = sender.unique_id
|
||||||
|
for (agent_id, inbox) in self._inbox.items():
|
||||||
|
if agent_id == sender_id:
|
||||||
continue
|
continue
|
||||||
self.logger.debug(f"Telling {repr(agent)}: {msg} ttl={ttl}")
|
if agent_class and not isinstance(self.agents(unique_id=agent_id), agent_class):
|
||||||
try:
|
|
||||||
inbox = agent._inbox
|
|
||||||
except AttributeError:
|
|
||||||
self.logger.info(
|
|
||||||
f"Agent {agent.unique_id} cannot receive events because it does not have an inbox"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
# Allow for AttributeError exceptions in this part of the code
|
self.logger.debug(f"Telling {agent_id}: {msg} ttl={ttl}")
|
||||||
inbox.append(
|
inbox.append(
|
||||||
events.Tell(
|
events.Tell(
|
||||||
payload=msg,
|
payload=msg,
|
||||||
sender=sender,
|
sender=sender,
|
||||||
expiration=expiration if ttl is None else self.now + ttl,
|
expiration=expiration,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@coroutine
|
||||||
|
def ask(self, msg, recipient, sender=None, expiration=None, timeout=None, delay=1):
|
||||||
|
ask = events.Ask(timestamp=self.now, payload=msg, sender=sender)
|
||||||
|
self.inbox_for(recipient).append(ask)
|
||||||
|
expiration = float("inf") if timeout is None else self.now + timeout
|
||||||
|
while self.now < expiration:
|
||||||
|
if ask.reply:
|
||||||
|
return ask.reply
|
||||||
|
yield time.Delay(delay)
|
||||||
|
raise events.TimedOut("No reply received")
|
||||||
|
|
||||||
class Environment(NetworkEnvironment, EventedEnvironment):
|
def process_messages(self, inbox):
|
||||||
"""Default environment class, has both network and event capabilities"""
|
valid = list()
|
||||||
|
for msg in inbox:
|
||||||
|
if msg.expired(self.now):
|
||||||
|
continue
|
||||||
|
valid.append(msg)
|
||||||
|
inbox.clear()
|
||||||
|
return valid
|
||||||
|
|
||||||
|
|
||||||
|
class Environment(EventedEnvironment, NetworkEnvironment):
|
||||||
|
pass
|
@ -18,7 +18,6 @@ class Message:
|
|||||||
def expired(self, when):
|
def expired(self, when):
|
||||||
return self.expiration is not None and self.expiration < when
|
return self.expiration is not None and self.expiration < when
|
||||||
|
|
||||||
|
|
||||||
class Reply(Message):
|
class Reply(Message):
|
||||||
source: Message
|
source: Message
|
||||||
|
|
||||||
@ -28,7 +27,9 @@ class Ask(Message):
|
|||||||
|
|
||||||
|
|
||||||
class Tell(Message):
|
class Tell(Message):
|
||||||
pass
|
def __post_init__(self):
|
||||||
|
assert self.sender is not None, "Tell requires a sender"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TimedOut(Exception):
|
class TimedOut(Exception):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from time import time as current_time
|
from time import time as current_time
|
||||||
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from textwrap import dedent, indent
|
from textwrap import dedent, indent
|
||||||
|
|
||||||
@ -86,20 +87,21 @@ class Exporter:
|
|||||||
pass
|
pass
|
||||||
return open_or_reuse(f, mode=mode, backup=self.simulation.backup, **kwargs)
|
return open_or_reuse(f, mode=mode, backup=self.simulation.backup, **kwargs)
|
||||||
|
|
||||||
def get_dfs(self, env, **kwargs):
|
def get_dfs(self, env, params_id, **kwargs):
|
||||||
yield from get_dc_dfs(env.datacollector,
|
yield from get_dc_dfs(env.datacollector,
|
||||||
simulation_id=self.simulation.id,
|
params_id,
|
||||||
iteration_id=env.id,
|
iteration_id=env.id,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_dc_dfs(dc, **kwargs):
|
def get_dc_dfs(dc, params_id, **kwargs):
|
||||||
dfs = {}
|
dfs = {}
|
||||||
dfe = dc.get_model_vars_dataframe()
|
dfe = dc.get_model_vars_dataframe()
|
||||||
dfe.index.rename("step", inplace=True)
|
dfe.index.rename("step", inplace=True)
|
||||||
dfs["env"] = dfe
|
dfs["env"] = dfe
|
||||||
|
kwargs["params_id"] = params_id
|
||||||
try:
|
try:
|
||||||
dfa = dc.get_agent_vars_dataframe()
|
dfa = dc.get_agent_vars_dataframe()
|
||||||
dfa.index.rename(["step", "agent_id"], inplace=True)
|
dfa.index.rename(["step", "agent_id"], inplace=True)
|
||||||
dfs["agents"] = dfa
|
dfs["agents"] = dfa
|
||||||
except UserWarning:
|
except UserWarning:
|
||||||
@ -108,9 +110,13 @@ def get_dc_dfs(dc, **kwargs):
|
|||||||
dfs[table_name] = dc.get_table_dataframe(table_name)
|
dfs[table_name] = dc.get_table_dataframe(table_name)
|
||||||
for (name, df) in dfs.items():
|
for (name, df) in dfs.items():
|
||||||
for (k, v) in kwargs.items():
|
for (k, v) in kwargs.items():
|
||||||
df[k] = v
|
if v:
|
||||||
df.set_index(["simulation_id", "iteration_id"], append=True, inplace=True)
|
df[k] = v
|
||||||
|
else:
|
||||||
|
df[k] = pd.Series(dtype="object")
|
||||||
|
df.reset_index(inplace=True)
|
||||||
|
df.set_index(["params_id", "iteration_id"], inplace=True)
|
||||||
|
|
||||||
yield from dfs.items()
|
yield from dfs.items()
|
||||||
|
|
||||||
|
|
||||||
@ -129,17 +135,21 @@ class SQLite(Exporter):
|
|||||||
logger.info("Dumping results to %s", self.dbpath)
|
logger.info("Dumping results to %s", self.dbpath)
|
||||||
if self.simulation.backup:
|
if self.simulation.backup:
|
||||||
try_backup(self.dbpath, remove=True)
|
try_backup(self.dbpath, remove=True)
|
||||||
|
|
||||||
if self.simulation.overwrite:
|
if self.simulation.overwrite:
|
||||||
if os.path.exists(self.dbpath):
|
if os.path.exists(self.dbpath):
|
||||||
os.remove(self.dbpath)
|
os.remove(self.dbpath)
|
||||||
|
|
||||||
|
outdir = os.path.dirname(self.dbpath)
|
||||||
|
if outdir and not os.path.exists(outdir):
|
||||||
|
os.makedirs(outdir)
|
||||||
|
|
||||||
self.engine = create_engine(f"sqlite:///{self.dbpath}", echo=False)
|
self.engine = create_engine(f"sqlite:///{self.dbpath}", echo=False)
|
||||||
|
|
||||||
sim_dict = {k: serialize(v)[0] for (k,v) in self.simulation.to_dict().items()}
|
sim_dict = {k: serialize(v)[0] for (k,v) in self.simulation.to_dict().items()}
|
||||||
sim_dict["simulation_id"] = self.simulation.id
|
sim_dict["simulation_id"] = self.simulation.id
|
||||||
df = pd.DataFrame([sim_dict])
|
df = pd.DataFrame([sim_dict])
|
||||||
df.to_sql("configuration", con=self.engine, if_exists="append")
|
df.reset_index().to_sql("configuration", con=self.engine, if_exists="append", index=False)
|
||||||
|
|
||||||
def iteration_end(self, env, params, params_id, *args, **kwargs):
|
def iteration_end(self, env, params, params_id, *args, **kwargs):
|
||||||
if not self.dump:
|
if not self.dump:
|
||||||
@ -149,15 +159,28 @@ class SQLite(Exporter):
|
|||||||
with timer(
|
with timer(
|
||||||
"Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
"Dumping simulation {} iteration {}".format(self.simulation.name, env.id)
|
||||||
):
|
):
|
||||||
|
d = {"simulation_id": self.simulation.id,
|
||||||
pd.DataFrame([{"simulation_id": self.simulation.id,
|
|
||||||
"params_id": params_id,
|
"params_id": params_id,
|
||||||
"iteration_id": env.id,
|
"iteration_id": env.id,
|
||||||
"key": k,
|
}
|
||||||
"value": serialize(v)[0]} for (k,v) in params.items()]).to_sql("parameters", con=self.engine, if_exists="append")
|
for (k,v) in params.items():
|
||||||
|
d[k] = serialize(v)[0]
|
||||||
|
|
||||||
|
pd.DataFrame([d]).reset_index().to_sql("parameters",
|
||||||
|
con=self.engine,
|
||||||
|
if_exists="append",
|
||||||
|
index=False)
|
||||||
|
pd.DataFrame([{
|
||||||
|
"simulation_id": self.simulation.id,
|
||||||
|
"params_id": params_id,
|
||||||
|
"iteration_id": env.id,
|
||||||
|
}]).reset_index().to_sql("iterations",
|
||||||
|
con=self.engine,
|
||||||
|
if_exists="append",
|
||||||
|
index=False)
|
||||||
|
|
||||||
for (t, df) in self.get_dfs(env, params_id=params_id):
|
for (t, df) in self.get_dfs(env, params_id=params_id):
|
||||||
df.to_sql(t, con=self.engine, if_exists="append")
|
df.reset_index().to_sql(t, con=self.engine, if_exists="append", index=False)
|
||||||
|
|
||||||
class csv(Exporter):
|
class csv(Exporter):
|
||||||
"""Export the state of each environment (and its agents) a CSV file for the simulation"""
|
"""Export the state of each environment (and its agents) a CSV file for the simulation"""
|
||||||
@ -226,9 +249,9 @@ class graphdrawing(Exporter):
|
|||||||
class summary(Exporter):
|
class summary(Exporter):
|
||||||
"""Print a summary of each iteration to sys.stdout"""
|
"""Print a summary of each iteration to sys.stdout"""
|
||||||
|
|
||||||
def iteration_end(self, env, *args, **kwargs):
|
def iteration_end(self, env, params_id, *args, **kwargs):
|
||||||
msg = ""
|
msg = ""
|
||||||
for (t, df) in self.get_dfs(env):
|
for (t, df) in self.get_dfs(env, params_id):
|
||||||
if not len(df):
|
if not len(df):
|
||||||
continue
|
continue
|
||||||
tabs = "\t" * 2
|
tabs = "\t" * 2
|
||||||
@ -262,21 +285,5 @@ class YAML(Exporter):
|
|||||||
logger.info(f"Dumping simulation configuration to {self.outdir}")
|
logger.info(f"Dumping simulation configuration to {self.outdir}")
|
||||||
f.write(self.simulation.to_yaml())
|
f.write(self.simulation.to_yaml())
|
||||||
|
|
||||||
class default(Exporter):
|
|
||||||
"""Default exporter. Writes sqlite results, as well as the simulation YAML"""
|
|
||||||
|
|
||||||
def __init__(self, *args, exporter_cls=[], **kwargs):
|
default = SQLite
|
||||||
exporter_cls = exporter_cls or [YAML, SQLite]
|
|
||||||
self.inner = [cls(*args, **kwargs) for cls in exporter_cls]
|
|
||||||
|
|
||||||
def sim_start(self, *args, **kwargs):
|
|
||||||
for exporter in self.inner:
|
|
||||||
exporter.sim_start(*args, **kwargs)
|
|
||||||
|
|
||||||
def sim_end(self, *args, **kwargs):
|
|
||||||
for exporter in self.inner:
|
|
||||||
exporter.sim_end(*args, **kwargs)
|
|
||||||
|
|
||||||
def iteration_end(self, *args, **kwargs):
|
|
||||||
for exporter in self.inner:
|
|
||||||
exporter.iteration_end(*args, **kwargs)
|
|
90
soil/time.py
90
soil/time.py
@ -1,7 +1,7 @@
|
|||||||
from mesa.time import BaseScheduler
|
from mesa.time import BaseScheduler
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from heapq import heappush, heappop, heapreplace
|
from heapq import heappush, heappop, heapreplace
|
||||||
from collections import deque
|
from collections import deque, defaultdict
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -27,6 +27,12 @@ class Delay:
|
|||||||
def __await__(self):
|
def __await__(self):
|
||||||
return (yield self.delta)
|
return (yield self.delta)
|
||||||
|
|
||||||
|
class When:
|
||||||
|
def __init__(self, when):
|
||||||
|
raise Exception("The use of When is deprecated. Use the `Agent.at` and `Agent.delay` methods instead")
|
||||||
|
class Delta:
|
||||||
|
def __init__(self, delta):
|
||||||
|
raise Exception("The use of Delay is deprecated. Use the `Agent.at` and `Agent.delay` methods instead")
|
||||||
|
|
||||||
class DeadAgent(Exception):
|
class DeadAgent(Exception):
|
||||||
pass
|
pass
|
||||||
@ -46,27 +52,39 @@ class PQueueActivation(BaseScheduler):
|
|||||||
self._shuffle = shuffle
|
self._shuffle = shuffle
|
||||||
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||||
self.next_time = self.time
|
self.next_time = self.time
|
||||||
|
self.agents_by_type = defaultdict(dict)
|
||||||
|
|
||||||
def add(self, agent: MesaAgent, when=None):
|
def add(self, agent: MesaAgent, when=None):
|
||||||
if when is None:
|
if when is None:
|
||||||
when = self.time
|
when = self.time
|
||||||
else:
|
else:
|
||||||
when = float(when)
|
when = float(when)
|
||||||
|
agent_class = type(agent)
|
||||||
self._schedule(agent, None, when)
|
self.agents_by_type[agent_class][agent.unique_id] = agent
|
||||||
super().add(agent)
|
super().add(agent)
|
||||||
|
self.add_callback(agent.step, when)
|
||||||
def _schedule(self, agent, when=None, replace=False):
|
|
||||||
|
def add_callback(self, callback, when=None, replace=False):
|
||||||
if when is None:
|
if when is None:
|
||||||
when = self.time
|
when = self.time
|
||||||
|
else:
|
||||||
|
when = float(when)
|
||||||
if self._shuffle:
|
if self._shuffle:
|
||||||
key = (when, self.model.random.random())
|
key = (when, self.model.random.random())
|
||||||
else:
|
else:
|
||||||
key = (when, agent.unique_id)
|
key = when
|
||||||
if replace:
|
if replace:
|
||||||
heapreplace(self._queue, (key, agent))
|
heapreplace(self._queue, (key, callback))
|
||||||
else:
|
else:
|
||||||
heappush(self._queue, (key, agent))
|
heappush(self._queue, (key, callback))
|
||||||
|
|
||||||
|
def remove(self, agent):
|
||||||
|
del self._agents[agent.unique_id]
|
||||||
|
del self._agents[type(agent)][agent.unique_id]
|
||||||
|
for i, (key, callback) in enumerate(self._queue):
|
||||||
|
if callback == agent.step:
|
||||||
|
del self._queue[i]
|
||||||
|
break
|
||||||
|
|
||||||
def step(self) -> None:
|
def step(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -87,18 +105,14 @@ class PQueueActivation(BaseScheduler):
|
|||||||
next_time = when
|
next_time = when
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
when = agent.step() or 1
|
||||||
when = agent.step() or 1
|
|
||||||
when += now
|
|
||||||
except DeadAgent:
|
|
||||||
heappop(self._queue)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if when == INFINITY:
|
if when == INFINITY:
|
||||||
heappop(self._queue)
|
heappop(self._queue)
|
||||||
continue
|
continue
|
||||||
|
when += now
|
||||||
|
|
||||||
self._schedule(agent, when, replace=True)
|
self.add_callback(agent, when, replace=True)
|
||||||
|
|
||||||
self.steps += 1
|
self.steps += 1
|
||||||
|
|
||||||
@ -117,26 +131,42 @@ class TimedActivation(BaseScheduler):
|
|||||||
self._shuffle = shuffle
|
self._shuffle = shuffle
|
||||||
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
self.logger = getattr(self.model, "logger", logger).getChild(f"time_{ self.model }")
|
||||||
self.next_time = self.time
|
self.next_time = self.time
|
||||||
|
self.agents_by_type = defaultdict(dict)
|
||||||
|
|
||||||
def add(self, agent: MesaAgent, when=None):
|
def add(self, agent: MesaAgent, when=None):
|
||||||
|
self.add_callback(agent.step, when)
|
||||||
|
agent_class = type(agent)
|
||||||
|
self.agents_by_type[agent_class][agent.unique_id] = agent
|
||||||
|
super().add(agent)
|
||||||
|
|
||||||
|
def _find_loc(self, when=None):
|
||||||
if when is None:
|
if when is None:
|
||||||
when = self.time
|
when = self.time
|
||||||
else:
|
else:
|
||||||
when = float(when)
|
when = float(when)
|
||||||
self._schedule(agent, None, when)
|
|
||||||
super().add(agent)
|
|
||||||
|
|
||||||
def _schedule(self, agent, when=None, replace=False):
|
|
||||||
when = when or self.time
|
when = when or self.time
|
||||||
pos = len(self._queue)
|
pos = len(self._queue)
|
||||||
for (ix, l) in enumerate(self._queue):
|
for (ix, l) in enumerate(self._queue):
|
||||||
if l[0] == when:
|
if l[0] == when:
|
||||||
l[1].append(agent)
|
return l[1]
|
||||||
return
|
|
||||||
if l[0] > when:
|
if l[0] > when:
|
||||||
pos = ix
|
pos = ix
|
||||||
break
|
break
|
||||||
self._queue.insert(pos, (when, [agent]))
|
lst = []
|
||||||
|
self._queue.insert(pos, (when, lst))
|
||||||
|
return lst
|
||||||
|
|
||||||
|
def add_callback(self, func, when=None, replace=False):
|
||||||
|
lst = self._find_loc(when)
|
||||||
|
lst.append(func)
|
||||||
|
|
||||||
|
def add_bulk(self, funcs, when=None):
|
||||||
|
lst = self._find_loc(when)
|
||||||
|
lst.extend(funcs)
|
||||||
|
|
||||||
|
def remove(self, agent):
|
||||||
|
del self._agents[agent.unique_id]
|
||||||
|
del self.agents_by_type[type(agent)][agent.unique_id]
|
||||||
|
|
||||||
def step(self) -> None:
|
def step(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -157,20 +187,22 @@ class TimedActivation(BaseScheduler):
|
|||||||
bucket = self._queue.popleft()[1]
|
bucket = self._queue.popleft()[1]
|
||||||
if self._shuffle:
|
if self._shuffle:
|
||||||
self.model.random.shuffle(bucket)
|
self.model.random.shuffle(bucket)
|
||||||
for agent in bucket:
|
next_batch = defaultdict(list)
|
||||||
try:
|
for func in bucket:
|
||||||
when = agent.step() or 1
|
when = func() or 1
|
||||||
when += now
|
|
||||||
except DeadAgent:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if when != INFINITY:
|
if when != INFINITY:
|
||||||
self._schedule(agent, when, replace=True)
|
when += now
|
||||||
|
next_batch[when].append(func)
|
||||||
|
|
||||||
|
for (when, bucket) in next_batch.items():
|
||||||
|
self.add_bulk(bucket, when)
|
||||||
|
|
||||||
self.steps += 1
|
self.steps += 1
|
||||||
if self._queue:
|
if self._queue:
|
||||||
self.time = self._queue[0][0]
|
self.time = self._queue[0][0]
|
||||||
else:
|
else:
|
||||||
|
self.model.running = False
|
||||||
self.time = INFINITY
|
self.time = INFINITY
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,4 +157,24 @@ def run_parallel(func, iterable, num_processes=1, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def int_seed(seed: str):
|
def int_seed(seed: str):
|
||||||
return int.from_bytes(seed.encode(), "little")
|
return int.from_bytes(seed.encode(), "little")
|
||||||
|
|
||||||
|
|
||||||
|
def prob(prob, random):
|
||||||
|
"""
|
||||||
|
A true/False uniform distribution with a given probability.
|
||||||
|
To be used like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if prob(0.3):
|
||||||
|
do_something()
|
||||||
|
|
||||||
|
"""
|
||||||
|
r = random.random()
|
||||||
|
return r < prob
|
||||||
|
|
||||||
|
|
||||||
|
def custom(cls, **kwargs):
|
||||||
|
"""Create a new class from a template class and keyword arguments"""
|
||||||
|
return type(cls.__name__, (cls,), kwargs)
|
@ -20,13 +20,14 @@ class TestAgents(TestCase):
|
|||||||
assert ret == stime.INFINITY
|
assert ret == stime.INFINITY
|
||||||
|
|
||||||
def test_die_raises_exception(self):
|
def test_die_raises_exception(self):
|
||||||
"""A dead agent should raise an exception if it is stepped after death"""
|
"""A dead agent should continue returning INFINITY after death"""
|
||||||
d = Dead(unique_id=0, model=environment.Environment())
|
d = Dead(unique_id=0, model=environment.Environment())
|
||||||
assert d.alive
|
assert d.alive
|
||||||
d.step()
|
d.step()
|
||||||
assert not d.alive
|
assert not d.alive
|
||||||
with pytest.raises(stime.DeadAgent):
|
when = d.step()
|
||||||
d.step()
|
assert not d.alive
|
||||||
|
assert when == stime.INFINITY
|
||||||
|
|
||||||
def test_agent_generator(self):
|
def test_agent_generator(self):
|
||||||
"""
|
"""
|
||||||
@ -79,30 +80,28 @@ class TestAgents(TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
class BCast(agents.Evented):
|
class BCast(agents.Evented):
|
||||||
pings_received = 0
|
pings_received = []
|
||||||
|
|
||||||
def step(self):
|
async def step(self):
|
||||||
print(self.model.broadcast)
|
self.broadcast("PING")
|
||||||
try:
|
print("PING sent")
|
||||||
self.model.broadcast("PING")
|
|
||||||
except Exception as ex:
|
|
||||||
print(ex)
|
|
||||||
while True:
|
while True:
|
||||||
self.process_messages()
|
msgs = await self.received()
|
||||||
yield
|
self.pings_received += msgs
|
||||||
|
|
||||||
def on_receive(self, msg, sender=None):
|
e = environment.Environment()
|
||||||
self.pings_received += 1
|
|
||||||
|
|
||||||
e = environment.EventedEnvironment()
|
num_agents = 10
|
||||||
|
for i in range(num_agents):
|
||||||
for i in range(10):
|
|
||||||
e.add_agent(agent_class=BCast)
|
e.add_agent(agent_class=BCast)
|
||||||
e.step()
|
e.step()
|
||||||
pings_received = lambda: [a.pings_received for a in e.agents]
|
# Agents are executed in order, so the first agent should have not received any messages
|
||||||
assert sorted(pings_received()) == list(range(1, 11))
|
pings_received = lambda: [len(a.pings_received) for a in e.agents]
|
||||||
|
assert sorted(pings_received()) == list(range(0, num_agents))
|
||||||
e.step()
|
e.step()
|
||||||
assert all(x == 10 for x in pings_received())
|
# After the second step, every agent should have received a broadcast from every other agent
|
||||||
|
received = pings_received()
|
||||||
|
assert all(x == (num_agents - 1) for x in received)
|
||||||
|
|
||||||
def test_ask_messages(self):
|
def test_ask_messages(self):
|
||||||
"""
|
"""
|
||||||
@ -140,17 +139,16 @@ class TestAgents(TestCase):
|
|||||||
print("NOT sending ping")
|
print("NOT sending ping")
|
||||||
print("Checking msgs")
|
print("Checking msgs")
|
||||||
# Do not block if we have already received a PING
|
# Do not block if we have already received a PING
|
||||||
if not self.process_messages():
|
msgs = yield from self.received()
|
||||||
yield from self.received()
|
for ping in msgs:
|
||||||
print("done")
|
if ping.payload == "PING":
|
||||||
|
ping.reply = "PONG"
|
||||||
|
pongs.append(self.now)
|
||||||
|
else:
|
||||||
|
raise Exception("This should never happen")
|
||||||
|
|
||||||
def on_receive(self, msg, sender=None):
|
|
||||||
if msg == "PING":
|
|
||||||
pongs.append(self.now)
|
|
||||||
return "PONG"
|
|
||||||
raise Exception("This should never happen")
|
|
||||||
|
|
||||||
e = environment.EventedEnvironment(schedule_class=stime.OrderedTimedActivation)
|
e = environment.Environment(schedule_class=stime.OrderedTimedActivation)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
e.add_agent(agent_class=Ping)
|
e.add_agent(agent_class=Ping)
|
||||||
assert e.now == 0
|
assert e.now == 0
|
||||||
@ -372,4 +370,24 @@ class TestAgents(TestCase):
|
|||||||
assert a.my_state == 5
|
assert a.my_state == 5
|
||||||
model.step()
|
model.step()
|
||||||
assert a.now == 17
|
assert a.now == 17
|
||||||
assert a.my_state == 5
|
assert a.my_state == 5
|
||||||
|
|
||||||
|
def test_send_nonevent(self):
|
||||||
|
'''
|
||||||
|
Sending a non-event should raise an error.
|
||||||
|
'''
|
||||||
|
model = environment.Environment()
|
||||||
|
a = model.add_agent(agents.Noop)
|
||||||
|
class TestAgent(agents.Agent):
|
||||||
|
@agents.state(default=True)
|
||||||
|
def one(self):
|
||||||
|
try:
|
||||||
|
a.tell(b, 1)
|
||||||
|
raise AssertionError('Should have raised an error.')
|
||||||
|
except AttributeError:
|
||||||
|
self.model.tell(1, sender=self, recipient=a)
|
||||||
|
|
||||||
|
model.add_agent(TestAgent)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
model.step()
|
@ -81,7 +81,8 @@ class Exporters(TestCase):
|
|||||||
model=ConstantEnv,
|
model=ConstantEnv,
|
||||||
name="exporter_sim",
|
name="exporter_sim",
|
||||||
exporters=[
|
exporters=[
|
||||||
exporters.default,
|
exporters.YAML,
|
||||||
|
exporters.SQLite,
|
||||||
exporters.csv,
|
exporters.csv,
|
||||||
],
|
],
|
||||||
exporter_params={"copy_to": output},
|
exporter_params={"copy_to": output},
|
||||||
|
@ -135,9 +135,9 @@ class TestMain(TestCase):
|
|||||||
|
|
||||||
def test_serialize_agent_class(self):
|
def test_serialize_agent_class(self):
|
||||||
"""A class from soil.agents should be serialized without the module part"""
|
"""A class from soil.agents should be serialized without the module part"""
|
||||||
ser = agents._serialize_type(CustomAgent)
|
ser = serialization.serialize(CustomAgent, known_modules=["soil.agents"])[1]
|
||||||
assert ser == "test_main.CustomAgent"
|
assert ser == "test_main.CustomAgent"
|
||||||
ser = agents._serialize_type(agents.BaseAgent)
|
ser = serialization.serialize(agents.BaseAgent, known_modules=["soil.agents"])[1]
|
||||||
assert ser == "BaseAgent"
|
assert ser == "BaseAgent"
|
||||||
pickle.dumps(ser)
|
pickle.dumps(ser)
|
||||||
|
|
||||||
@ -227,3 +227,29 @@ class TestMain(TestCase):
|
|||||||
for i in a:
|
for i in a:
|
||||||
for j in b:
|
for j in b:
|
||||||
assert {"a": i, "b": j} in configs
|
assert {"a": i, "b": j} in configs
|
||||||
|
|
||||||
|
def test_agent_reporters(self):
|
||||||
|
"""An environment should be able to set its own reporters"""
|
||||||
|
class Noop2(agents.Noop):
|
||||||
|
pass
|
||||||
|
|
||||||
|
e = Environment()
|
||||||
|
e.add_agent(agents.Noop)
|
||||||
|
e.add_agent(Noop2)
|
||||||
|
e.add_agent_reporter("now")
|
||||||
|
e.add_agent_reporter("base", lambda a: "base", agent_class=agents.Noop)
|
||||||
|
e.add_agent_reporter("subclass", lambda a:"subclass", agent_class=Noop2)
|
||||||
|
e.step()
|
||||||
|
|
||||||
|
# Step 0 is not present because we added the reporters
|
||||||
|
# after initialization.
|
||||||
|
df = e.agent_df()
|
||||||
|
assert "now" in df.columns
|
||||||
|
assert "base" in df.columns
|
||||||
|
assert "subclass" in df.columns
|
||||||
|
assert df["now"][(0,0)] == 1
|
||||||
|
assert df["now"][(0,1)] == 1
|
||||||
|
assert df["base"][(0,0)] == "base"
|
||||||
|
assert df["base"][(0,1)] == "base"
|
||||||
|
assert df["subclass"][(0,0)] is None
|
||||||
|
assert df["subclass"][(0,1)] == "subclass"
|
Loading…
Reference in New Issue
Block a user