1
0
mirror of https://github.com/gsi-upm/soil synced 2025-08-24 03:52:20 +00:00
This commit is contained in:
J. Fernando Sánchez
2023-05-12 14:09:00 +02:00
parent f49be3af68
commit 3041156f19
27 changed files with 887 additions and 953 deletions

View File

@@ -43,7 +43,7 @@ class Journey:
driver: Optional[Driver] = None
class City(EventedEnvironment):
class City(Environment):
"""
An environment with a grid where drivers and passengers will be placed.
@@ -85,11 +85,12 @@ class Driver(Evented, FSM):
journey = None
earnings = 0
def on_receive(self, msg, sender):
"""This is not a state. It will run (and block) every time process_messages is invoked"""
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
msg.driver = self
self.journey = msg
# TODO: remove
# def on_receive(self, msg, sender):
# """This is not a state. It will run (and block) every time process_messages is invoked"""
# if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
# msg.driver = self
# self.journey = msg
def check_passengers(self):
"""If there are no more passengers, stop forever"""
@@ -104,7 +105,7 @@ class Driver(Evented, FSM):
if not self.check_passengers():
return self.die("No passengers left")
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):
target = self.random.choice(
self.model.grid.get_neighborhood(self.pos, moore=False)
@@ -113,7 +114,7 @@ class Driver(Evented, FSM):
if not self.check_passengers():
return self.die("No passengers left")
# 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
try:
@@ -167,12 +168,13 @@ class Driver(Evented, FSM):
class Passenger(Evented, FSM):
pos = None
def on_receive(self, msg, sender):
"""This is not a state. It will be run synchronously every time `process_messages` is run"""
# TODO: Remove
# 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):
self.journey = msg
return msg
# if isinstance(msg, Journey):
# self.journey = msg
# return msg
@default_state
@state
@@ -192,17 +194,34 @@ class Passenger(Evented, FSM):
timeout = 60
expiration = self.now + timeout
self.info(f"Asking for journey at: { self.pos }")
self.model.broadcast(journey, ttl=timeout, sender=self, agent_class=Driver)
self.broadcast(journey, ttl=timeout, agent_class=Driver)
while not self.journey:
self.debug(f"Waiting for responses at: { self.pos }")
try:
# This will call process_messages behind the scenes, and the agent's status will be updated
# If you want to avoid that, you can call it with: check=False
await self.received(expiration=expiration, delay=10)
offers = await self.received(expiration=expiration, delay=10)
accepted = None
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:
self.info(f"Still no response. Waiting at: { self.pos }")
self.model.broadcast(
journey, ttl=timeout, sender=self, agent_class=Driver
self.broadcast(
journey, ttl=timeout, agent_class=Driver
)
expiration = self.now + timeout
self.info(f"Got a response! Waiting for driver")

View File

@@ -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 *
import logging

View File

@@ -1,7 +1,7 @@
There are two similar implementations of this simulation.
- `basic`. Using simple primites
- `improved`. Using more advanced features such as the `time` module to avoid unnecessary computations (i.e., skip steps), and generator functions.
- `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.
For example, to enable the CSV exporter and the Summary exporter, while setting `max_time` to `100` and `seed` to `CustomSeed`:

View File

@@ -1,22 +1,36 @@
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation
from enum import Enum
from collections import Counter
import logging
from soil import Evented, FSM, state, default_state, BaseAgent, NetworkAgent, Environment, parameters, report, TimedOut
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):
"""Initialize the environment with the new versions of the agents"""
a1 = self.add_node(Male)
a2 = self.add_node(Female)
a1.add_edge(a2)
self.add_agent(RandomAccident)
@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
life_expectancy = 300
@@ -31,42 +45,40 @@ class Rabbit(FSM, NetworkAgent):
@default_state
@state
def newborn(self):
self.info("I am a newborn.")
self.debug("I am a newborn.")
self.birth = self.now
self.offspring = 0
return self.youngling.delay(self.sexual_maturity - self.age)
return self.youngling
@state
def youngling(self):
if self.age >= self.sexual_maturity:
self.info(f"I am fertile! My age is {self.age}")
return self.fertile
async def youngling(self):
self.debug("I am a youngling.")
await self.delay(self.sexual_maturity - self.age)
assert self.age >= self.sexual_maturity
self.debug(f"I am fertile! My age is {self.age}")
return self.fertile
@state
def fertile(self):
raise Exception("Each subclass should define its fertile state")
@state
def dead(self):
self.die()
class Male(Rabbit):
max_females = 5
mating_prob = 0.001
mating_prob = 0.005
@state
def fertile(self):
if self.age > self.life_expectancy:
return self.dead
return self.die()
# Males try to mate
for f in self.model.agents(
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
):
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
self.debug(f"FOUND A FEMALE: {repr(f)}. Mating with 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
@@ -75,78 +87,91 @@ class Female(Rabbit):
conception = None
@state
def fertile(self):
async def fertile(self):
# Just wait for a Male
if self.age > self.life_expectancy:
return self.dead
if self.conception is not None:
return self.pregnant
@property
def pregnancy(self):
if self.conception is None:
return None
return self.now - self.conception
def impregnate(self, male):
self.info(f"impregnated by {repr(male)}")
self.mate = male
self.conception = self.now
self.number_of_babies = int(8 + 4 * self.random.random())
try:
timeout = self.life_expectancy - self.age
while timeout > 0:
mates = await self.received(timeout=timeout)
# assert self.age <= self.life_expectancy
for mate in mates:
try:
male = self.model.agents[mate.payload]
except ValueError:
continue
self.debug(f"impregnated by {repr(male)}")
self.mate = male
self.number_of_babies = int(8 + 4 * self.random.random())
self.conception = self.now
return self.pregnant
except TimedOut:
pass
return self.die()
@state
def pregnant(self):
async def pregnant(self):
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:
self.info("Dying before giving birth")
self.debug("Dying before giving birth")
return self.die()
if self.pregnancy >= self.gestation:
self.info("Having {} babies".format(self.number_of_babies))
for i in range(self.number_of_babies):
state = {}
agent_class = self.random.choice([Male, Female])
child = self.model.add_node(agent_class=agent_class, **state)
child.add_edge(self)
if self.mate:
child.add_edge(self.mate)
self.mate.offspring += 1
else:
self.debug("The father has passed away")
# assert self.now - self.conception >= self.gestation
if not self.alive:
return self.die()
self.offspring += 1
self.mate = None
return self.fertile
self.debug("Having {} babies".format(self.number_of_babies))
for i in range(self.number_of_babies):
state = {}
agent_class = self.random.choice([Male, Female])
child = self.model.add_node(agent_class=agent_class, **state)
child.add_edge(self)
try:
child.add_edge(self.mate)
self.model.agents[self.mate].offspring += 1
except ValueError:
self.debug("The father has passed away")
self.offspring += 1
self.mate = None
self.conception = None
return self.fertile
def die(self):
if self.pregnancy is not None:
self.info("A mother has died carrying a baby!!")
if self.conception is not None:
self.debug("A mother has died carrying a baby!!")
return super().die()
class RandomAccident(BaseAgent):
# Default value, but the value from the environment takes precedence
prob_death = 1e-3
def step(self):
rabbits_alive = self.model.G.number_of_nodes()
if not rabbits_alive:
return self.die()
alive = self.get_agents(agent_class=Rabbit, alive=True)
prob_death = self.model.get("prob_death", 1e-100) * math.floor(
math.log10(max(1, rabbits_alive))
)
if not 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))
for i in self.iter_agents(agent_class=Rabbit):
for i in alive:
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
self.debug("I killed a rabbit: {}".format(i.unique_id))
num_alive -= 1
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)
if __name__ == "__main__":
sim.run()
RabbitsImprovedEnv.run(max_time=1000, seed="MySeed", iterations=1)

View File

@@ -1,11 +1,9 @@
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, Simulation, report, parameters as params
from collections import Counter
import logging
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment, report, parameters as params
import math
class RabbitEnv(Environment):
prob_death: params.probability = 1e-100
prob_death: params.probability = 1e-3
def init(self):
a1 = self.add_node(Male)
@@ -37,7 +35,7 @@ class Rabbit(NetworkAgent, FSM):
@default_state
@state
def newborn(self):
self.info("I am a newborn.")
self.debug("I am a newborn.")
self.age = 0
self.offspring = 0
return self.youngling
@@ -46,7 +44,7 @@ class Rabbit(NetworkAgent, FSM):
def youngling(self):
self.age += 1
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
@state
@@ -60,7 +58,7 @@ class Rabbit(NetworkAgent, FSM):
class Male(Rabbit):
max_females = 5
mating_prob = 0.001
mating_prob = 0.005
@state
def fertile(self):
@@ -70,9 +68,8 @@ class Male(Rabbit):
return self.dead
# Males try to mate
for f in self.model.agents(
agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
):
for f in self.model.agents.filter(
agent_class=Female, state_id=Female.fertile.id).limit(self.max_females):
self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
if self.prob(self["mating_prob"]):
f.impregnate(self)
@@ -93,14 +90,14 @@ class Female(Rabbit):
return self.pregnant
def impregnate(self, male):
self.info(f"impregnated by {repr(male)}")
self.debug(f"impregnated by {repr(male)}")
self.mate = male
self.pregnancy = 0
self.number_of_babies = int(8 + 4 * self.random.random())
@state
def pregnant(self):
self.info("I am pregnant")
self.debug("I am pregnant")
self.age += 1
if self.age >= self.life_expectancy:
@@ -110,7 +107,7 @@ class Female(Rabbit):
self.pregnancy += 1
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):
state = {}
agent_class = self.random.choice([Male, Female])
@@ -129,33 +126,30 @@ class Female(Rabbit):
def die(self):
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()
class RandomAccident(BaseAgent):
prob_death = None
def step(self):
rabbits_alive = self.model.G.number_of_nodes()
alive = self.get_agents(agent_class=Rabbit, alive=True)
if not rabbits_alive:
return self.die()
if not alive:
return self.die("No more rabbits to kill")
prob_death = self.model.prob_death * math.floor(
math.log10(max(1, rabbits_alive))
)
num_alive = len(alive)
prob_death = min(1, self.prob_death * num_alive/10)
self.debug("Killing some rabbits with prob={}!".format(prob_death))
for i in self.get_agents(agent_class=Rabbit):
if i.state_id == i.dead.id:
continue
if self.prob(prob_death):
self.info("I killed a rabbit: {}".format(i.id))
rabbits_alive -= 1
self.debug("I killed a rabbit: {}".format(i.unique_id))
num_alive -= 1
i.die()
self.debug("Rabbits alive: {}".format(rabbits_alive))
self.debug("Rabbits alive: {}".format(num_alive))
sim = Simulation(model=RabbitEnv, max_time=100, seed="MySeed", iterations=1)
if __name__ == "__main__":
sim.run()
RabbitEnv.run(max_time=1000, seed="MySeed", iterations=1)

View File

@@ -1,5 +1,5 @@
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 import Environment, Simulation
from soil.parameters import *