Gas Station¶
This example expands upon SimPy’s Gas Station Refueling example, demonstrating various desmod features.
Note
Desmod’s goal is to support large-scale modeling. Thus this example is somewhat larger-scale than the SimPy model it expands upon.
"""Model refueling at several gas stations.
Each gas station has several fuel pumps and a single, shared reservoir. Each
arrving car pumps gas from the reservoir via a fuel pump.
As the gas station's reservoir empties, a request is made to a tanker truck
company to send a truck to refill the reservoir. The tanker company maintains a
fleet of tanker trucks.
This example demonstrates core desmod concepts including:
- Modeling using Component subclasses
- The "batteries-included" simulation environment
- Centralized configuration
- Logging
"""
from itertools import count, cycle
from simpy import Resource
from desmod.component import Component
from desmod.dot import generate_dot
from desmod.pool import Pool
from desmod.queue import Queue
from desmod.simulation import simulate
class Top(Component):
"""Every model has a single top-level Component.
For this gas station model, the top level components are gas stations and a
tanker truck company.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# The simulation configuration is available everywhere via the
# simulation environment.
num_gas_stations = self.env.config.get('gas_station.count', 1)
# Instantiate GasStation components. An index is passed so that each
# child gas station gets a unique name.
self.gas_stations = [GasStation(self, index=i) for i in range(num_gas_stations)]
# There is just one tanker company.
self.tanker_company = TankerCompany(self)
def connect_children(self):
# This function is called during the elaboration phase, i.e. after all
# of the components have been instantiated, but before the simulation
# phase.
for gas_station in self.gas_stations:
# Each GasStation instance gets a reference to (is connected to)
# the tanker_company instance. This demonstrates the most
# abbreviated way to call connect().
self.connect(gas_station, 'tanker_company')
def elab_hook(self):
generate_dot(self)
class TankerCompany(Component):
"""The tanker company owns and dispatches its fleet of tanker trunks."""
# This base_name is used to build names and scopes of component instances.
base_name = 'tankerco'
def __init__(self, *args, **kwargs):
# Many Component subclasses can simply forward *args and **kwargs to
# the superclass initializer; although Component subclasses may also
# have custom positional and keyword arguments.
super().__init__(*args, **kwargs)
num_tankers = self.env.config.get('tanker.count', 1)
# Instantiate the fleet of tanker trucks.
trucks = [TankerTruck(self, index=i) for i in range(num_tankers)]
# Trucks are dispatched in a simple round-robin fashion.
self.trucks_round_robin = cycle(trucks)
def request_truck(self, gas_station, done_event):
"""Called by gas stations to request a truck to refill its reservior.
Returns an event that the gas station must yield for.
"""
truck = next(self.trucks_round_robin)
# Each component has debug(), info(), warn(), and error() log methods.
# Log lines are automatically annotated with the simulation time and
# the scope of the component doing the logging.
self.info(f'dispatching {truck.name} to {gas_station.name}')
return truck.dispatch(gas_station, done_event)
class TankerTruck(Component):
"""Tanker trucks carry fuel to gas stations.
Each tanker truck has a queue of gas stations it must visit. When the
truck's tank becomes empty, it must go refill itself.
"""
base_name = 'truck'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pump_rate = self.env.config.get('tanker.pump_rate', 10)
self.avg_travel = self.env.config.get('tanker.travel_time', 600)
tank_capacity = self.env.config.get('tanker.capacity', 200)
self.tank = Pool(self.env, tank_capacity)
# This auto_probe() call uses the self.tank Pool get/put hooks so that
# whenever it's level changes, the new level is noted in the log.
self.auto_probe('tank', log={})
# The parent TankerCompany enqueues instructions to this queue.
self._instructions = Queue(self.env)
# Declare a persistant process to be started at simulation-time.
self.add_process(self._dispatch_loop)
def dispatch(self, gas_station, done_event):
"""Append dispatch instructions to the truck's queue."""
return self._instructions.put((gas_station, done_event))
def _dispatch_loop(self):
"""This is the tanker truck's main behavior. Travel, pump, refill..."""
while True:
if not self.tank.level:
self.info('going for refill')
# Desmod simulation environments come equipped with a
# random.Random() instance seeded based on the 'sim.seed'
# configuration key.
travel_time = self.env.rand.expovariate(1 / self.avg_travel)
yield self.env.timeout(travel_time)
self.info('refilling')
pump_time = self.tank.capacity / self.pump_rate
yield self.env.timeout(pump_time)
yield self.tank.put(self.tank.capacity)
self.info(f'refilled {self.tank.capacity}L in {pump_time:.0f}s')
gas_station, done_event = yield self._instructions.get()
self.info(f'traveling to {gas_station.name}')
travel_time = self.env.rand.expovariate(1 / self.avg_travel)
yield self.env.timeout(travel_time)
self.info(f'arrived at {gas_station.name}')
while self.tank.level and (
gas_station.reservoir.level < gas_station.reservoir.capacity
):
yield self.env.timeout(1 / self.pump_rate)
yield gas_station.reservoir.put(1)
yield self.tank.get(1)
self.info('done pumping')
done_event.succeed()
class GasStation(Component):
"""A gas station has a fuel reservoir shared among several fuel pumps.
The gas station has a traffic generator process that causes cars to arrive
to fill up their tanks.
As the cars fill up, the reservoir's level goes down. When the level goes
below a critical threshold, the gas station makes a request to the tanker
company for a tanker truck to refill the reservoir.
"""
base_name = 'station'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
config = self.env.config
self.add_connections('tanker_company')
self.arrival_interval = config.get('gas_station.arrival_interval', 60)
station_capacity = config.get('gas_station.capacity', 200)
self.reservoir = Pool(
self.env, capacity=station_capacity, init=station_capacity
)
self.auto_probe('reservoir', log={})
threshold_pct = config.get('gas_station.threshold_pct', 10)
self.reservoir_low_water = threshold_pct * station_capacity / 100
self.pump_rate = config.get('gas_station.pump_rate', 2)
num_pumps = config.get('gas_station.pumps', 2)
self.fuel_pumps = Resource(self.env, capacity=num_pumps)
self.auto_probe('fuel_pumps', log={})
self.car_capacity = config.get('car.capacity', 50)
self.car_level_range = config.get('car.level', [5, 25])
# A gas station has two persistent processes. One to monitor the
# reservoir level and one that models the arrival of cars at the
# station. Desmod starts these processes before simulation phase.
self.add_processes(self._monitor_reservoir, self._traffic_generator)
@property
def reservoir_pct(self):
return self.reservoir.level / self.reservoir.capacity * 100
def _monitor_reservoir(self):
"""Periodically monitor reservoir level.
The a request is made to the tanker company when the reservoir falls
below a critical threshold.
"""
while True:
yield self.reservoir.when_at_most(self.reservoir_low_water)
done_event = self.env.event()
yield self.tanker_company.request_truck(self, done_event)
yield done_event
def _traffic_generator(self):
"""Model the sporadic arrival of cars to the gas station."""
for i in count():
interval = self.env.rand.expovariate(1 / self.arrival_interval)
yield self.env.timeout(interval)
self.env.process(self._car(i))
def _car(self, i):
"""Model a car transacting fuel."""
with self.fuel_pumps.request() as pump_req:
self.info(f'car{i} awaiting pump')
yield pump_req
self.info(f'car{i} at pump')
car_level = self.env.rand.randint(*self.car_level_range)
amount = self.car_capacity - car_level
t0 = self.env.now
for _ in range(amount):
yield self.reservoir.get(1)
yield self.env.timeout(1 / self.pump_rate)
pump_time = self.env.now - t0
self.info(f'car{i} pumped {amount}L in {pump_time:.0f}s')
# Desmod uses a plain dictionary to represent the simulation configuration.
# The various 'sim.xxx' keys are reserved for desmod while the remainder are
# application-specific.
config = {
'car.capacity': 50,
'car.level': [5, 25],
'gas_station.capacity': 200,
'gas_station.count': 3,
'gas_station.pump_rate': 2,
'gas_station.pumps': 2,
'gas_station.arrival_interval': 60,
'sim.dot.enable': True,
'sim.dot.colorscheme': 'blues5',
'sim.duration': '500 s',
'sim.log.enable': True,
'sim.log.file': 'sim.log',
'sim.log.format': '{level:7} {ts:.3f} {ts_unit}: {scope:<16}:',
'sim.log.level': 'INFO',
'sim.result.file': 'results.yaml',
'sim.seed': 42,
'sim.timescale': 's',
'sim.workspace': 'workspace',
'tanker.capacity': 200,
'tanker.count': 2,
'tanker.pump_rate': 10,
'tanker.travel_time': 100,
}
if __name__ == '__main__':
# Desmod takes responsibility for instantiating and elaborating the model,
# thus we only need to pass the configuration dict and the top-level
# Component class (Top) to simulate().
simulate(config, Top)
The model hierarchy is captured during elaboration as a DOT graph. See the
desmod.dot
documentation for more detail on DOT output.
The simulation log, sim.log
, shows what happened during the
simulation:
INFO 0.000 s: tankerco.truck0 : going for refill
INFO 0.000 s: tankerco.truck1 : going for refill
INFO 1.520 s: station1 : car0 awaiting pump
INFO 1.520 s: station1 : car0 at pump
INFO 15.520 s: station1 : car0 pumped 28L in 14s
INFO 19.297 s: station2 : car0 awaiting pump
INFO 19.297 s: station2 : car0 at pump
INFO 24.755 s: station2 : car1 awaiting pump
INFO 24.755 s: station2 : car1 at pump
INFO 25.259 s: tankerco.truck0 : refilling
INFO 26.693 s: station2 : car2 awaiting pump
INFO 35.297 s: station2 : car0 pumped 32L in 16s
INFO 35.297 s: station2 : car2 at pump
INFO 41.496 s: station2 : car3 awaiting pump
INFO 45.259 s: tankerco.truck0 : refilled 200L in 20s
INFO 46.255 s: station2 : car1 pumped 43L in 22s
INFO 46.255 s: station2 : car3 at pump
INFO 49.797 s: station2 : car2 pumped 29L in 14s
INFO 60.255 s: station2 : car3 pumped 28L in 14s
INFO 61.204 s: station0 : car0 awaiting pump
INFO 61.204 s: station0 : car0 at pump
INFO 69.270 s: station1 : car1 awaiting pump
INFO 69.270 s: station1 : car1 at pump
INFO 73.704 s: station0 : car0 pumped 25L in 13s
INFO 74.505 s: station0 : car1 awaiting pump
INFO 74.505 s: station0 : car1 at pump
INFO 85.270 s: station1 : car1 pumped 32L in 16s
INFO 88.005 s: station0 : car1 pumped 27L in 14s
INFO 89.447 s: station0 : car2 awaiting pump
INFO 89.447 s: station0 : car2 at pump
INFO 96.777 s: station2 : car4 awaiting pump
INFO 96.777 s: station2 : car4 at pump
INFO 109.006 s: station0 : car3 awaiting pump
INFO 109.006 s: station0 : car3 at pump
INFO 111.947 s: station0 : car2 pumped 45L in 22s
INFO 116.777 s: station2 : car4 pumped 40L in 20s
INFO 126.506 s: station0 : car3 pumped 35L in 18s
INFO 133.359 s: tankerco.truck1 : refilling
INFO 141.774 s: station1 : car2 awaiting pump
INFO 141.774 s: station1 : car2 at pump
INFO 153.359 s: tankerco.truck1 : refilled 200L in 20s
INFO 161.274 s: station1 : car2 pumped 39L in 20s
INFO 161.307 s: station1 : car3 awaiting pump
INFO 161.307 s: station1 : car3 at pump
INFO 178.807 s: station1 : car3 pumped 35L in 18s
INFO 180.874 s: station0 : car4 awaiting pump
INFO 180.874 s: station0 : car4 at pump
INFO 182.106 s: station2 : car5 awaiting pump
INFO 182.106 s: station2 : car5 at pump
INFO 185.606 s: tankerco : dispatching truck0 to station2
INFO 185.606 s: tankerco.truck0 : traveling to station2
INFO 187.343 s: station0 : car5 awaiting pump
INFO 187.343 s: station0 : car5 at pump
INFO 188.209 s: station2 : car6 awaiting pump
INFO 188.209 s: station2 : car6 at pump
INFO 195.843 s: tankerco : dispatching truck1 to station0
INFO 195.843 s: tankerco.truck1 : traveling to station0
INFO 197.374 s: station0 : car4 pumped 33L in 16s
INFO 202.843 s: station0 : car5 pumped 31L in 16s
INFO 204.051 s: tankerco.truck1 : arrived at station0
INFO 223.651 s: tankerco.truck1 : done pumping
INFO 234.311 s: station2 : car7 awaiting pump
INFO 255.130 s: station2 : car8 awaiting pump
INFO 278.171 s: tankerco.truck0 : arrived at station2
INFO 285.271 s: station2 : car5 pumped 34L in 103s
INFO 285.271 s: station2 : car7 at pump
INFO 286.087 s: station0 : car6 awaiting pump
INFO 286.087 s: station0 : car6 at pump
INFO 290.871 s: station2 : car6 pumped 33L in 103s
INFO 290.871 s: station2 : car8 at pump
INFO 298.171 s: tankerco.truck0 : done pumping
INFO 298.171 s: tankerco.truck0 : going for refill
INFO 302.271 s: station2 : car7 pumped 34L in 17s
INFO 307.587 s: station0 : car6 pumped 43L in 22s
INFO 312.871 s: station2 : car8 pumped 44L in 22s
INFO 314.565 s: station2 : car9 awaiting pump
INFO 314.565 s: station2 : car9 at pump
INFO 336.065 s: station2 : car9 pumped 43L in 22s
INFO 337.760 s: station0 : car7 awaiting pump
INFO 337.760 s: station0 : car7 at pump
INFO 350.399 s: station1 : car4 awaiting pump
INFO 350.399 s: station1 : car4 at pump
INFO 358.760 s: station0 : car7 pumped 42L in 21s
INFO 365.899 s: station1 : car4 pumped 31L in 16s
INFO 379.093 s: station1 : car5 awaiting pump
INFO 379.093 s: station1 : car5 at pump
INFO 386.093 s: tankerco : dispatching truck0 to station1
INFO 396.093 s: station1 : car5 pumped 34L in 17s
INFO 403.551 s: station2 : car10 awaiting pump
INFO 403.551 s: station2 : car10 at pump
INFO 406.424 s: tankerco.truck0 : refilling
INFO 413.051 s: tankerco : dispatching truck1 to station2
INFO 413.051 s: tankerco.truck1 : traveling to station2
INFO 414.202 s: station2 : car11 awaiting pump
INFO 414.202 s: station2 : car11 at pump
INFO 426.424 s: tankerco.truck0 : refilled 200L in 20s
INFO 426.424 s: tankerco.truck0 : traveling to station1
INFO 432.837 s: station2 : car12 awaiting pump
INFO 433.832 s: tankerco.truck0 : arrived at station1
INFO 436.561 s: tankerco.truck1 : arrived at station2
INFO 436.961 s: tankerco.truck1 : done pumping
INFO 436.961 s: tankerco.truck1 : going for refill
INFO 436.961 s: tankerco : dispatching truck0 to station2
INFO 439.677 s: station1 : car6 awaiting pump
INFO 439.677 s: station1 : car6 at pump
INFO 453.753 s: station0 : car8 awaiting pump
INFO 453.753 s: station0 : car8 at pump
INFO 453.832 s: tankerco.truck0 : done pumping
INFO 453.832 s: tankerco.truck0 : going for refill
INFO 455.177 s: station1 : car6 pumped 31L in 16s
INFO 456.524 s: station1 : car7 awaiting pump
INFO 456.524 s: station1 : car7 at pump
INFO 466.253 s: station0 : car8 pumped 25L in 12s
INFO 471.402 s: station1 : car8 awaiting pump
INFO 471.402 s: station1 : car8 at pump
INFO 474.024 s: station1 : car7 pumped 35L in 18s
INFO 482.382 s: station0 : car9 awaiting pump
INFO 482.382 s: station0 : car9 at pump
INFO 493.305 s: station2 : car13 awaiting pump
INFO 493.402 s: station1 : car8 pumped 44L in 22s
INFO 497.990 s: station0 : car10 awaiting pump
INFO 497.990 s: station0 : car10 at pump
This example does not make heavy use of desmod’s result-gathering
capability, but we can nonetheless see the minimal results.yaml
file
generated from the simulation:
config:
car.capacity: 50
car.level: [5, 25]
gas_station.arrival_interval: 60
gas_station.capacity: 200
gas_station.count: 3
gas_station.pump_rate: 2
gas_station.pumps: 2
meta.sim.workspace: workspace
sim.config.file: null
sim.db.enable: false
sim.db.persist: true
sim.dot.all.file: all.dot
sim.dot.colorscheme: blues5
sim.dot.conn.file: conn.dot
sim.dot.enable: true
sim.dot.hier.file: hier.dot
sim.duration: 500 s
sim.log.buffering: -1
sim.log.enable: true
sim.log.exclude_pat: []
sim.log.file: sim.log
sim.log.format: '{level:7} {ts:.3f} {ts_unit}: {scope:<16}:'
sim.log.include_pat: [.*]
sim.log.level: INFO
sim.log.persist: true
sim.progress.enable: false
sim.progress.max_width: null
sim.progress.update_period: 1 s
sim.result.file: results.yaml
sim.seed: 42
sim.timescale: s
sim.vcd.enable: false
sim.vcd.persist: true
sim.workspace: workspace
sim.workspace.overwrite: false
tanker.capacity: 200
tanker.count: 2
tanker.pump_rate: 10
tanker.travel_time: 100
sim.exception: null
sim.now: 500.0
sim.runtime: 0.039155590000000004
sim.time: 500