1
0
mirror of https://github.com/gsi-upm/soil synced 2024-11-14 23:42:29 +00:00
soil/examples/cars/cars_sim.py

252 lines
9.0 KiB
Python
Raw Normal View History

"""
This is an example of a simplified city, where there are Passengers and Drivers that can take those passengers
from their location to their desired location.
An example scenario could play like the following:
- Drivers start in the `wandering` state, where they wander around the city until they have been assigned a journey
- Passenger(1) tells every driver that it wants to request a Journey.
- Each driver receives the request.
If Driver(2) is interested in providing the Journey, it asks Passenger(1) to confirm that it accepts Driver(2)'s request
- When Passenger(1) accepts the request, two things happen:
- Passenger(1) changes its state to `driving_home`
- Driver(2) starts moving towards the origin of the Journey
- Once Driver(2) reaches the origin, it starts moving itself and Passenger(1) to the destination of the Journey
- When Driver(2) reaches the destination (carrying Passenger(1) along):
- Driver(2) starts wondering again
- Passenger(1) dies, and is removed from the simulation
- If there are no more passengers available in the simulation, Drivers die
"""
2022-10-18 11:11:01 +00:00
from __future__ import annotations
from typing import Optional
2022-10-18 11:11:01 +00:00
from soil import *
from soil import events
from mesa.space import MultiGrid
# More complex scenarios may use more than one type of message between objects.
# A common pattern is to use `enum.Enum` to represent state changes in a request.
2022-10-18 11:11:01 +00:00
@dataclass
class Journey:
"""
2022-10-18 15:03:40 +00:00
This represents a request for a journey. Passengers and drivers exchange this object.
A journey may have a driver assigned or not. If the driver has not been assigned, this
object is considered a "request for a journey".
"""
2022-10-18 15:03:40 +00:00
2022-10-18 11:11:01 +00:00
origin: (int, int)
destination: (int, int)
tip: float
passenger: Passenger
driver: Optional[Driver] = None
2022-10-18 11:11:01 +00:00
2023-05-12 12:09:00 +00:00
class City(Environment):
"""
An environment with a grid where drivers and passengers will be placed.
The number of drivers and riders is configurable through its parameters:
:param str n_cars: The total number of drivers to add
:param str n_passengers: The number of passengers in the simulation
:param list agents: Specific agents to use in the simulation. It overrides the `n_passengers`
and `n_cars` params.
:param int height: Height of the internal grid
:param int width: Width of the internal grid
"""
n_cars = 1
n_passengers = 10
height = 100
width = 100
def init(self):
self.grid = MultiGrid(width=self.width, height=self.height, torus=False)
if not self.agents:
self.add_agents(Driver, k=self.n_cars)
self.add_agents(Passenger, k=self.n_passengers)
2022-10-18 15:03:40 +00:00
2022-10-18 11:11:01 +00:00
for agent in self.agents:
self.grid.place_agent(agent, (0, 0))
self.grid.move_to_empty(agent)
self.total_earnings = 0
self.add_model_reporter("total_earnings")
2022-10-18 11:11:01 +00:00
@report
@property
def number_passengers(self):
return self.count_agents(agent_class=Passenger)
2022-10-18 11:11:01 +00:00
class Driver(Evented, FSM):
pos = None
journey = None
earnings = 0
2023-05-12 12:09:00 +00:00
# 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
2022-10-18 11:11:01 +00:00
def check_passengers(self):
2022-10-18 15:03:40 +00:00
"""If there are no more passengers, stop forever"""
c = self.count_agents(agent_class=Passenger)
2023-04-20 15:56:44 +00:00
self.debug(f"Passengers left {c}")
2023-05-03 10:14:49 +00:00
return c
2023-05-03 10:14:49 +00:00
@state(default=True)
async def wandering(self):
2022-10-18 15:03:40 +00:00
"""Move around the city until a journey is accepted"""
2022-10-18 11:11:01 +00:00
target = None
2023-05-03 10:14:49 +00:00
if not self.check_passengers():
return self.die("No passengers left")
2022-10-18 11:11:01 +00:00
self.journey = None
2023-05-12 12:09:00 +00:00
while self.journey is None: # No potential journeys detected
2022-10-18 11:11:01 +00:00
if target is None or not self.move_towards(target):
2022-10-18 15:03:40 +00:00
target = self.random.choice(
self.model.grid.get_neighborhood(self.pos, moore=False)
)
2023-05-03 10:14:49 +00:00
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
2023-05-12 12:09:00 +00:00
2023-05-03 10:14:49 +00:00
await self.delay(30) # Wait at least 30 seconds before checking again
2022-10-18 11:11:01 +00:00
try:
# Re-send the journey to the passenger, to confirm that we have been selected
2023-05-03 10:14:49 +00:00
self.journey = await self.journey.passenger.ask(self.journey, timeout=60, delay=5)
2022-10-18 11:11:01 +00:00
except events.TimedOut:
# No journey has been accepted. Try again
2022-10-18 11:11:01 +00:00
self.journey = None
return
return self.driving
2022-10-18 11:11:01 +00:00
@state
2023-05-03 10:14:49 +00:00
async def driving(self):
2022-10-18 15:03:40 +00:00
"""The journey has been accepted. Pick them up and take them to their destination"""
2023-04-20 15:56:44 +00:00
self.info(f"Driving towards Passenger {self.journey.passenger.unique_id}")
2022-10-18 11:11:01 +00:00
while self.move_towards(self.journey.origin):
2023-05-03 10:14:49 +00:00
await self.delay()
2023-04-20 15:56:44 +00:00
self.info(f"Driving {self.journey.passenger.unique_id} from {self.journey.origin} to {self.journey.destination}")
2022-10-18 11:11:01 +00:00
while self.move_towards(self.journey.destination, with_passenger=True):
2023-05-03 10:14:49 +00:00
await self.delay()
2023-04-20 15:56:44 +00:00
self.info("Arrived at destination")
self.earnings += self.journey.tip
self.model.total_earnings += self.journey.tip
2023-05-03 10:14:49 +00:00
if not self.check_passengers():
return self.die("No passengers left")
2022-10-18 11:11:01 +00:00
return self.wandering
def move_towards(self, target, with_passenger=False):
2022-10-18 15:03:40 +00:00
"""Move one cell at a time towards a target"""
2023-04-20 15:56:44 +00:00
self.debug(f"Moving { self.pos } -> { target }")
2022-10-18 11:11:01 +00:00
if target[0] == self.pos[0] and target[1] == self.pos[1]:
return False
next_pos = [self.pos[0], self.pos[1]]
for idx in [0, 1]:
if self.pos[idx] < target[idx]:
next_pos[idx] += 1
break
if self.pos[idx] > target[idx]:
next_pos[idx] -= 1
break
self.model.grid.move_agent(self, tuple(next_pos))
if with_passenger:
2022-10-18 15:03:40 +00:00
self.journey.passenger.pos = (
self.pos
) # This could be communicated through messages
2022-10-18 11:11:01 +00:00
return True
2022-10-18 15:03:40 +00:00
2022-10-18 11:11:01 +00:00
class Passenger(Evented, FSM):
pos = None
2023-05-12 12:09:00 +00:00
# TODO: Remove
# def on_receive(self, msg, sender):
# """This is not a state. It will be run synchronously every time `process_messages` is run"""
2023-05-12 12:09:00 +00:00
# if isinstance(msg, Journey):
# self.journey = msg
# return msg
2022-10-18 11:11:01 +00:00
@default_state
@state
2023-05-03 10:14:49 +00:00
async def asking(self):
2022-10-18 15:03:40 +00:00
destination = (
2023-04-20 15:56:44 +00:00
self.random.randint(0, self.model.grid.height-1),
self.random.randint(0, self.model.grid.width-1),
2022-10-18 15:03:40 +00:00
)
2022-10-18 11:11:01 +00:00
self.journey = None
2022-10-18 15:03:40 +00:00
journey = Journey(
origin=self.pos,
destination=destination,
tip=self.random.randint(10, 100),
passenger=self,
)
2022-10-18 11:11:01 +00:00
timeout = 60
expiration = self.now + timeout
2023-04-20 15:56:44 +00:00
self.info(f"Asking for journey at: { self.pos }")
2023-05-12 12:09:00 +00:00
self.broadcast(journey, ttl=timeout, agent_class=Driver)
2022-10-18 11:11:01 +00:00
while not self.journey:
2023-04-20 15:56:44 +00:00
self.debug(f"Waiting for responses at: { self.pos }")
2022-10-18 11:11:01 +00:00
try:
2023-05-12 12:09:00 +00:00
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)
2022-10-18 11:11:01 +00:00
except events.TimedOut:
2023-04-20 15:56:44 +00:00
self.info(f"Still no response. Waiting at: { self.pos }")
2023-05-12 12:09:00 +00:00
self.broadcast(
journey, ttl=timeout, agent_class=Driver
2022-10-18 15:03:40 +00:00
)
2022-10-18 11:11:01 +00:00
expiration = self.now + timeout
2023-04-20 15:56:44 +00:00
self.info(f"Got a response! Waiting for driver")
2022-10-18 11:11:01 +00:00
return self.driving_home
@state
2023-05-03 10:14:49 +00:00
async def driving_home(self):
2022-10-18 15:03:40 +00:00
while (
self.pos[0] != self.journey.destination[0]
or self.pos[1] != self.journey.destination[1]
):
try:
2023-05-03 10:14:49 +00:00
await self.received(timeout=60)
except events.TimedOut:
pass
self.die("Got home safe!")
2022-10-18 11:11:01 +00:00
simulation = Simulation(name="RideHailing",
model=City,
seed="carsSeed",
max_time=1000,
2023-04-20 15:56:44 +00:00
parameters=dict(n_passengers=2))
2022-10-18 11:11:01 +00:00
if __name__ == "__main__":
2023-05-03 10:14:49 +00:00
easy(simulation)