Cars take fuel from a station's tank, and a control process calls a tank truck when the tank runs low. This is SimPy's Gas Station Refueling example, written in desflow.
Before You Start
SimPy's gas station has two pumps and a tank of 200 litres. A car arrives every 30 to 300 seconds with 5 to 25 litres left in its 50 litre tank. It waits for a pump, takes the fuel it needs from the station's tank, and refuels at 2 litres a second. A control process checks the tank every 10 seconds. When the tank is below 25 percent, it calls a tank truck, which arrives 300 seconds later and fills the tank. The run lasts 1000 seconds.
The example needs only desflow, which each Quick Start in Run It installs, and no other service. desflow examples get simpy-gas-station-refuel --api yaml downloads it, as Get an Example shows. SimPy Examples says how these pages read SimPy's times and numbers. SimPy's own page for the example is on its documentation site.
Steps
Car Refuels
SimPy's car requests a pump, takes the fuel it needs from the tank, and refuels at 2 litres a second.
defcar(name,env,gas_station,station_tank):"""A car arrives at the gas station for refueling. It requests one of the gas station's fuel pumps and tries to get the desired amount of fuel from it. If the station's fuel tank is depleted, the car has to wait for the tank truck to arrive. """car_tank_level=random.randint(*CAR_TANK_LEVEL)print(f'{env.now:6.1f} s: {name} arrived at gas station')withgas_station.request()asreq:# Request one of the gas pumpsyieldreq# Get the required amount of fuelfuel_required=CAR_TANK_SIZE-car_tank_levelyieldstation_tank.get(fuel_required)# The "actual" refueling process takes some timeyieldenv.timeout(fuel_required/REFUELING_SPEED)print(f'{env.now:6.1f} s: {name} refueled with {fuel_required:.1f}L')
pumps is a resource with two units and tank is a container of 200 litres, full at the start. The car task takes a pump, gets 50 - attr.level litres from the tank, and refuels for (50 - attr.level) / 2 seconds. attr.level is the fuel the car has left.
defgas_station_control(env,station_tank):"""Periodically check the level of the gas station tank and call the tank truck if the level falls below a threshold."""whileTrue:ifstation_tank.level/station_tank.capacity*100<THRESHOLD:# We need to call the tank truck now!print(f'{env.now:6.1f} s: Calling tank truck')# Wait for the tank truck to arrive and refuel the station tankyieldenv.process(tank_truck(env,station_tank))yieldenv.timeout(10)# Check every 10 seconds
The control task checks level.tank / 200 * 100 < 25 in its next rule. When the tank is low it goes to call_truck, and then to await_truck, which waits for the truck task to finish. Otherwise it goes to pause, which waits 10 seconds and returns to control.
@app.task(name="control",next={"when":[{"condition":"level.tank / 200 * 100 < 25","task":"call_truck"}],"default":"pause",},)defcontrol(task_id):"""Checks the level of the tank."""return{"event_type":"checked"}@app.task(name="call_truck",next="await_truck")defcall_truck(task_id):"""Calls the tank truck."""return{"event_type":"truck_called"}@app.task(name="await_truck",wait={"task":"truck"},next="pause")defawait_truck(task_id):"""Waits for the tank truck."""return{"event_type":"truck_returned"}
deftank_truck(env,station_tank):"""Arrives at the gas station after a certain delay and refuels it."""yieldenv.timeout(TANK_TRUCK_TIME)amount=station_tank.capacity-station_tank.levelstation_tank.put(amount)print(f'{env.now:6.1f} s: Tank truck arrived and refuelled station with {amount:.1f}L')
The truck task waits service_time: 300 and then puts 200 - level.tank litres into the container, which fills it.
truck_payload=Template({"event_type":"truck_arrived","litres":{"gen":"expr","expr":"200 - level.tank"},},name="truck",)@app.task(name="truck",service_time=300,put={"container":"tank","amount":"200 - level.tank"},)deftruck(task_id):"""Fills the tank on arrival."""returntruck_payload
defcar_generator(env,gas_station,station_tank):"""Generate new cars that arrive at the gas station."""foriinitertools.count():yieldenv.timeout(random.randint(*T_INTER))env.process(car(f'Car {i}',env,gas_station,station_tank))
The arrival cars draws its gap from 30 to 300 seconds and gives each car an attribute level from 5 to 25 litres. The arrival control starts the control process once at time 0.
# 1. Installpipinstalldesflow
# 2. Download the exampledesflowexamplesgetsimpy-gas-station-refuel--apilow-level
cdsimpy-gas-station-refuel/low-level
# 3. Run itpythongas_station_refuel_example.py
Check the Result
Five cars refuel in the 1000 seconds. The control task finds the tank below 25 percent at 970 seconds and calls the truck. The truck needs 300 seconds, so it would arrive at 1270 seconds, after the run ends, and the records show no truck arrival.
In SimPy's output, below, the truck is called at 460 seconds and arrives at 760 seconds. Here the cars arrive at other times, so the tank is low later. desflow draws each random value from its own stream, named by the place where the value is used, such as GasStation.service.patience. A seed of 42 therefore gives different numbers from SimPy's random.seed(42). The system is the same. The figures are not.
Gas Station refuelling
87.0 s: Car 0 arrived at gas station
105.5 s: Car 0 refueled with 37.0L
129.0 s: Car 1 arrived at gas station
148.0 s: Car 1 refueled with 38.0L
284.0 s: Car 2 arrived at gas station
305.0 s: Car 2 refueled with 42.0L
385.0 s: Car 3 arrived at gas station
398.5 s: Car 3 refueled with 27.0L
459.0 s: Car 4 arrived at gas station
460.0 s: Calling tank truck
481.0 s: Car 4 refueled with 44.0L
705.0 s: Car 5 arrived at gas station
750.0 s: Car 6 arrived at gas station
760.0 s: Tank truck arrived and refuelled station with 188.0L
779.0 s: Car 6 refueled with 38.0L
781.5 s: Car 5 refueled with 43.0L
891.0 s: Car 7 arrived at gas station
904.0 s: Car 7 refueled with 26.0L
The three versions in desflow print the same records, which a test checks by running each version unpaced. Each record has sim_ts, the simulation time, and a key for the entity it belongs to. The statistics and the parameters that a live run can change are in the same stream, which SimPy's file does not have.
# Gas Station Refueling: SimPy's gas_station_refuel.py example, in YAML.## The YAML version of gas_station_refuel_example.py, which --api builder downloads. A# gas station has two pumps and a tank of 200 litres. A car arrives every 30 to 300# seconds with 5 to 25 litres left in its 50 litre tank. It waits for a pump, takes# the fuel it needs from the station's tank and refuels at 2 litres a second. A control# task checks the tank every 10 seconds, and when it is below 25 percent it calls a# tank truck, which arrives 300 seconds later and fills the tank.## Each value is drawn from desflow's random sequence for its place in the file, so the# numbers differ from those SimPy prints.## Needs no containers. Runs unpaced and ends after 1000 simulation seconds.# Run it from its folder with: desflow run gas_station_refuel.yamlsimulation:{sim_id:GasStation, factor:0, random_seed:42}egress:-{type:Console}resources:pumps:{current_cap:2}containers:tank:{current_cap:200, level:200}arrivals:control:{initial:1, spawn:control}cars:gap:{gen:integer, bounds:[30,300]}spawn:carkey:{gen:sequence, format:"Car{}"}attributes:level:{gen:integer, bounds:[5,25]}tasks:car:resource:pumpsget:{container:tank, amount:"50-attr.level"}service_time:"(50-attr.level)/2"payload:event_type:refueledlitres:{gen:expr, expr:"50-attr.level"}control:payload:{event_type:checked}next:when:-{condition:"level.tank/200*100<25", task:call_truck}default:pausecall_truck:payload:{event_type:truck_called}next:await_truckawait_truck:wait:{task:truck}payload:{event_type:truck_returned}next:pausetruck:service_time:300put:{container:tank, amount:"200-level.tank"}payload:event_type:truck_arrivedlitres:{gen:expr, expr:"200-level.tank"}pause:service_time:10payload:{event_type:waited}next:controlrun:{until:1000}
"""Gas Station Refueling: SimPy's gas_station_refuel.py example, builder API.A gas station has two pumps and a tank of 200 litres. A car arrives every 30 to 300seconds with 5 to 25 litres left in its 50 litre tank. It waits for a pump, takes thefuel it needs from the station's tank and refuels at 2 litres a second. A control taskchecks the tank every 10 seconds, and when it is below 25 percent it calls a tanktruck, which arrives 300 seconds later and fills the tank.Each value is drawn from desflow's random sequence for its place in the model, so thenumbers differ from those SimPy prints.Needs no containers. Runs unpaced and ends after 1000 simulation seconds."""importloggingfromdesflowimportConsoleEgress,SimulationContextfromdesflow.valuesimportTemplate,generatordefbuild()->SimulationContext:"""Builds the gas station."""app=SimulationContext(sim_id="GasStation",factor=0,random_seed=42)app.add_egress(ConsoleEgress())app.add_resource("pumps",current_cap=2)app.add_container("tank",current_cap=200,level=200)app.add_initial_arrival("control",1,spawn="control")app.add_arrival("cars",gap=generator({"gen":"integer","bounds":[30,300]}),key=lambdan:"Car {}".format(n-1),attributes={"level":generator({"gen":"integer","bounds":[5,25]})},spawn="car",)car_payload=Template({"event_type":"refueled","litres":{"gen":"expr","expr":"50 - attr.level"},},name="car",)@app.task(name="car",resource_id="pumps",get={"container":"tank","amount":"50 - attr.level"},service_time="(50 - attr.level) / 2",)defcar(task_id):"""Refuels at a pump."""returncar_payload@app.task(name="control",next={"when":[{"condition":"level.tank / 200 * 100 < 25","task":"call_truck"}],"default":"pause",},)defcontrol(task_id):"""Checks the level of the tank."""return{"event_type":"checked"}@app.task(name="call_truck",next="await_truck")defcall_truck(task_id):"""Calls the tank truck."""return{"event_type":"truck_called"}@app.task(name="await_truck",wait={"task":"truck"},next="pause")defawait_truck(task_id):"""Waits for the tank truck."""return{"event_type":"truck_returned"}truck_payload=Template({"event_type":"truck_arrived","litres":{"gen":"expr","expr":"200 - level.tank"},},name="truck",)@app.task(name="truck",service_time=300,put={"container":"tank","amount":"200 - level.tank"},)deftruck(task_id):"""Fills the tank on arrival."""returntruck_payload@app.task(name="pause",service_time=10,next="control")defpause(task_id):"""Waits until the next check."""return{"event_type":"waited"}returnappapp=build()defrun(model:SimulationContext=app)->None:"""Runs 1000 simulation seconds."""model.run(until=1000)if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s%(message)s")run()
The script wires the model by hand on DynamicRealtimeEnvironment, with SimPy's own processes.
"""Gas Station Refueling: SimPy's gas_station_refuel.py example, low-level API.The low-level version of `builder/simpy/gas_station_refuel_example.py`: SimPy's ownprocesses on a `DynamicRealtimeEnvironment`. They publish with `publish_event` therecords that the YAML and builder versions publish: each task's `queued` and`started`, its own events, and the number of the task's instance. A gas station hastwo pumps and a tank of 200 litres. A car arrives every 30 to 300 seconds with 5 to25 litres left in its 50 litre tank. It waits for a pump, takes the fuel it needsfrom the station's tank and refuels at 2 litres a second. A control process checksthe tank every 10 seconds, and when it is below 25 percent it calls a tank truck,which arrives 300 seconds later and fills the tank.Each value is drawn from desflow's `Sampler`, from the random sequence the YAML andbuilder versions use for the same place, so the three versions publish the samerecords.Needs no containers. Runs unpaced and ends after 1000 simulation seconds."""importitertoolsimportloggingimportsimpyfromdesflowimportConsoleEgress,DynamicRealtimeEnvironment,SamplerSTATION_TANK_SIZE=200# litresTHRESHOLD=25# percent of the tank that calls the truckCAR_TANK_SIZE=50# litresREFUELING_SPEED=2# litres a secondTANK_TRUCK_TIME=300# secondsUNTIL=1000defbuild(env:DynamicRealtimeEnvironment)->None:"""Starts the control process and the cars on `env`."""sampler=Sampler(seed=42)gaps=sampler.generator("arrivals.cars.gap")levels=sampler.generator("arrivals.cars.attributes.level")gas_station=simpy.Resource(env,2)station_tank=simpy.Container(env,STATION_TANK_SIZE,init=STATION_TANK_SIZE)numbers={task:itertools.count()fortaskin("car","control","call_truck","await_truck","truck","pause")}defpublish(name,task,number,value):env.publish_event(name,value,task=task,instance=number)defcar(name,car_tank_level):number=next(numbers["car"])withgas_station.request()asreq:publish(name,"car",number,{"status":"queued"})yieldreqfuel_required=CAR_TANK_SIZE-car_tank_levelyieldstation_tank.get(fuel_required)publish(name,"car",number,{"status":"started"})yieldenv.timeout(fuel_required/REFUELING_SPEED)publish(name,"car",number,{"event_type":"refueled","litres":fuel_required})deftank_truck(name):number=next(numbers["truck"])publish(name,"truck",number,{"status":"started"})yieldenv.timeout(TANK_TRUCK_TIME)amount=station_tank.capacity-station_tank.levelyieldstation_tank.put(amount)publish(name,"truck",number,{"event_type":"truck_arrived","litres":amount})defgas_station_control(name):whileTrue:publish(name,"control",next(numbers["control"]),{"event_type":"checked"})ifstation_tank.level/station_tank.capacity*100<THRESHOLD:call=next(numbers["call_truck"])publish(name,"call_truck",call,{"event_type":"truck_called"})waiting=next(numbers["await_truck"])publish(name,"await_truck",waiting,{"status":"queued"})yieldenv.process(tank_truck(name))publish(name,"await_truck",waiting,{"status":"started"})publish(name,"await_truck",waiting,{"event_type":"truck_returned"})pause=next(numbers["pause"])publish(name,"pause",pause,{"status":"started"})yieldenv.timeout(10)publish(name,"pause",pause,{"event_type":"waited"})defcar_generator():foriinitertools.count():yieldenv.timeout(int(gaps.integers(30,301)))env.process(car(f"Car {i}",int(levels.integers(5,26))))env.process(gas_station_control("control-1"))env.process(car_generator())defrun()->None:"""Runs 1000 simulation seconds."""env=DynamicRealtimeEnvironment(factor=0)env.setup_egress([ConsoleEgress()])build(env)try:env.run(until=UNTIL)finally:env.teardown()if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s%(message)s")run()
This is SimPy 4.1.2's gas_station_refuel.py, with SimPy's licence notice added at the top and comment lines that mark the pieces shown above. Run with Python, it prints the output shown under Check the Result.
# SimPy 4.1.2, docs/examples/code/gas_station_refuel.py, under SimPy's licence (LICENSE.rst):## The MIT License (MIT)## Copyright (c) 2013 Ontje Lünsdorf and Stefan Scherfke (also see AUTHORS.txt)## Permission is hereby granted, free of charge, to any person obtaining a copy of# this software and associated documentation files (the "Software"), to deal in# the Software without restriction, including without limitation the rights to# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of# the Software, and to permit persons to whom the Software is furnished to do so,# subject to the following conditions:## The above copyright notice and this permission notice shall be included in all# copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""Gas Station Refueling exampleCovers:- Resources: Resource- Resources: Container- Waiting for other processesScenario: A gas station has a limited number of gas pumps that share a common fuel reservoir. Cars randomly arrive at the gas station, request one of the fuel pumps and start refueling from that reservoir. A gas station control process observes the gas station's fuel level and calls a tank truck for refueling if the station's level drops below a threshold."""importitertoolsimportrandomimportsimpy# fmt: offRANDOM_SEED=42STATION_TANK_SIZE=200# Size of the gas station tank (liters)THRESHOLD=25# Station tank minimum level (% of full)CAR_TANK_SIZE=50# Size of car fuel tanks (liters)CAR_TANK_LEVEL=[5,25]# Min/max levels of car fuel tanks (liters)REFUELING_SPEED=2# Rate of refuelling car fuel tank (liters / second)TANK_TRUCK_TIME=300# Time it takes tank truck to arrive (seconds)T_INTER=[30,300]# Interval between car arrivals [min, max] (seconds)SIM_TIME=1000# Simulation time (seconds)# fmt: ondefcar(name,env,gas_station,station_tank):"""A car arrives at the gas station for refueling. It requests one of the gas station's fuel pumps and tries to get the desired amount of fuel from it. If the station's fuel tank is depleted, the car has to wait for the tank truck to arrive. """car_tank_level=random.randint(*CAR_TANK_LEVEL)print(f'{env.now:6.1f} s: {name} arrived at gas station')withgas_station.request()asreq:# Request one of the gas pumpsyieldreq# Get the required amount of fuelfuel_required=CAR_TANK_SIZE-car_tank_levelyieldstation_tank.get(fuel_required)# The "actual" refueling process takes some timeyieldenv.timeout(fuel_required/REFUELING_SPEED)print(f'{env.now:6.1f} s: {name} refueled with {fuel_required:.1f}L')defgas_station_control(env,station_tank):"""Periodically check the level of the gas station tank and call the tank truck if the level falls below a threshold."""whileTrue:ifstation_tank.level/station_tank.capacity*100<THRESHOLD:# We need to call the tank truck now!print(f'{env.now:6.1f} s: Calling tank truck')# Wait for the tank truck to arrive and refuel the station tankyieldenv.process(tank_truck(env,station_tank))yieldenv.timeout(10)# Check every 10 secondsdeftank_truck(env,station_tank):"""Arrives at the gas station after a certain delay and refuels it."""yieldenv.timeout(TANK_TRUCK_TIME)amount=station_tank.capacity-station_tank.levelstation_tank.put(amount)print(f'{env.now:6.1f} s: Tank truck arrived and refuelled station with {amount:.1f}L')defcar_generator(env,gas_station,station_tank):"""Generate new cars that arrive at the gas station."""foriinitertools.count():yieldenv.timeout(random.randint(*T_INTER))env.process(car(f'Car {i}',env,gas_station,station_tank))# Setup and start the simulationprint('Gas Station refuelling')random.seed(RANDOM_SEED)# Create environment and start processesenv=simpy.Environment()gas_station=simpy.Resource(env,2)station_tank=simpy.Container(env,STATION_TANK_SIZE,init=STATION_TANK_SIZE)env.process(gas_station_control(env,station_tank))env.process(car_generator(env,gas_station,station_tank))# Execute!env.run(until=SIM_TIME)