I am modelling the usage of a car fleet. (sourcecode attached below) The car fleet is represented by a SimpyStore with elements {0}...{9} Cars can be "requested" by daily occurring "trips", if successful cars are being rented by a trip and then (after a delay) returned to the SimpyStore.
As there can be more requests per day than cars available, trip-patience is also being modelled using a conditional event. If a trip has to wait longer than its patience, it fails.
Now to the strange part (See the excerpt of the console log below): After a Trip (e.g. 9) returns its car and in the following timesteps a new trip (e.g. 11) requests this car, it is not longer available in the store though it has been successfully put back by the previous trip (9). (In this example there is just one car)
On day 4 at time 114 the trip 9 returned the car {0}
Contents of store after the trip 9 returns its car: [{0}]
On day 4 at Time 114 the Trip 8 failed because it waited 1
On day 5 at Time 134 the Trip 11 needs a Car
Available Cars in the Store: [] at Time 134
On day 5 at Time 135 the Trip 11 failed because it waited 1
This leads then to the failure of all subsequent trips as the car {0} has "disappeared"
I made the following observations:
I assume a timing issue connected to the conditional event. I was not able to fully understand the problem with the code. Is there anyone who has an idea?
###############################################################################
# imports
###############################################################################
from random import choices
import simpy
import numpy as np
from numpy.random import default_rng
# generate numpy random object
rng = default_rng()
###############################################################################
# Global definitions for probability and simulation time
###############################################################################
mu_1, sigma_1 = 8, 2 # mean and standard deviation "morning departure"
mu_2, sigma_2 = 17, 2 # mean and standard deviation "evening departure"
def global_departure_pdf(x):
# pdf from sum of two weighted gaussian pdfs
y = 0.6 * (1 / (sigma_1 * np.sqrt(2 * np.pi)) * np.exp(- (x - mu_1) ** 2 / (2 * sigma_1 ** 2))) + \
0.4 * (1 / (sigma_2 * np.sqrt(2 * np.pi)) * np.exp(- (x - mu_2) ** 2 / (2 * sigma_2 ** 2)))
return y
# values and weights for random choices to generate samples the global_departure_pdf
dep_time_values = np.linspace(0, 23, num=24)
dep_time_weights = global_departure_pdf(dep_time_values)
# Available Cars in the Fleet
capacity = 1
# Daily trip demand, is the ex ante generated number of trips per day
daily_trip_demand = 2
# simulation duration
simulated_days = 20
# patience of a trip to wait for a car
patience = 1
# total count for all uscase appearances during simulation
total_count = 0
failed_count = 0
###############################################################################
# SimPy generator function: generate the individual processes aka trips for
# each day and for all inter day occurrences
###############################################################################
def trip_gen(env):
# start the process generator with one instance of the process function called "job()"
day = 0
for d in range(simulated_days):
for t in range(daily_trip_demand):
# make global count globally accessible
global total_count
env.process(trip(env, day, total_count))
total_count += 1
day += 1
# make sure that a day has 24h
yield env.timeout(24)
####################################################################################
# main process function: get a car, use it and return it OR Fail because of patience
####################################################################################
def trip(env, day, total_count):
# draw and yield a trip start time from all hours of a day, but weighted with probability function
start_time = choices(dep_time_values, dep_time_weights)
yield env.timeout(int(start_time[0]))
print('On day {} at Time {} the Trip {} needs a Car'.format(day, env.now, total_count))
print('Available Cars in the Store: {} at Time {}'.format(CarFleet.items, env.now))
arrival = env.now
with CarFleet.get() as req:
# Wait for the car or the trip fails
results = yield req | env.timeout(patience)
wait = env.now - arrival
if req in results:
# We got the car in time
departure_timestep = env.now
print('On day {} at Time {} the Trip {} got the Car {}'.format(day, departure_timestep, total_count, results[req]))
print('')
# this represents the driving time
yield env.timeout(8)
# return the car to the store after usage
CarFleet.put(results[req])
return_timestep = env.now
print('On day {} at time {} the trip {} returned the car {}'.format(day, return_timestep, total_count, results[req]))
print('Contents of store after the trip {} returns its car: {}'.format(total_count, CarFleet.items))
print('')
else:
# We quit
fail_timestep = env.now
global failed_count
failed_count += 1
print('On day {} at Time {} the Trip {} failed because it waited {}'.format(day, fail_timestep, total_count, wait))
###############################################################################
# Simpy simulation setup
###############################################################################
# define an environment where the processes live in
env = simpy.Environment()
# instantiate CarFleet Store
CarFleet = simpy.Store(env, capacity=capacity)
# fill the store with elements = Cars
for i in range(capacity):
CarFleet.put({i})
i += 1
# call the function that generates the individual rental processes
env.process(trip_gen(env))
# start the simulation
env.run()
print('Total Count: {}'.format(total_count))
print('Failed Count: {}'.format(failed_count))
print('All Cars should be back by the end of sim: {}'.format(CarFleet.items))
There is a special case where you get to this else, and the req still grabs a car that you will need to return, but not always.
else:
# We quit
fail_timestep = env.now
global failed_count
failed_count += 1
print('On day {} at Time {} the Trip {} failed because it waited {}'.format(day, fail_timestep, total_count, wait))
There is a special case where in on one tick, your timeout fires, which fires your results = yield req | env.timeout(patience)
resulting in the results having only the timeout event in it, then you req fires which grabs a car, but your timeout else still gets called. Since you wrote you resource grab with a context manager, you do not need to do a cancel on the req, but you do still need to check in your timeout else to see if a car has been been seized and return it. I think this slight mod to your code fixes the problem
###############################################################################
# imports
###############################################################################
from random import choices
import simpy
import numpy as np
from numpy.random import default_rng
# generate numpy random object
rng = default_rng()
###############################################################################
# Global definitions for probability and simulation time
###############################################################################
mu_1, sigma_1 = 8, 2 # mean and standard deviation "morning departure"
mu_2, sigma_2 = 17, 2 # mean and standard deviation "evening departure"
def global_departure_pdf(x):
# pdf from sum of two weighted gaussian pdfs
y = 0.6 * (1 / (sigma_1 * np.sqrt(2 * np.pi)) * np.exp(- (x - mu_1) ** 2 / (2 * sigma_1 ** 2))) + \
0.4 * (1 / (sigma_2 * np.sqrt(2 * np.pi)) * np.exp(- (x - mu_2) ** 2 / (2 * sigma_2 ** 2)))
return y
# values and weights for random choices to generate samples the global_departure_pdf
dep_time_values = np.linspace(0, 23, num=24)
dep_time_weights = global_departure_pdf(dep_time_values)
# Available Cars in the Fleet
capacity = 1
# Daily trip demand, is the ex ante generated number of trips per day
daily_trip_demand = 2
# simulation duration
simulated_days = 20
# patience of a trip to wait for a car
patience = 1
# total count for all uscase appearances during simulation
total_count = 0
failed_count = 0
###############################################################################
# SimPy generator function: generate the individual processes aka trips for
# each day and for all inter day occurrences
###############################################################################
def trip_gen(env):
# start the process generator with one instance of the process function called "job()"
day = 0
for d in range(simulated_days):
for t in range(daily_trip_demand):
# make global count globally accessible
global total_count
env.process(trip(env, day, total_count))
total_count += 1
day += 1
# make sure that a day has 24h
yield env.timeout(24)
####################################################################################
# main process function: get a car, use it and return it OR Fail because of patience
####################################################################################
def trip(env, day, total_count):
# draw and yield a trip start time from all hours of a day, but weighted with probability function
start_time = choices(dep_time_values, dep_time_weights)
yield env.timeout(int(start_time[0]))
print('On day {} at Time {} the Trip {} needs a Car'.format(day, env.now, total_count))
print('Available Cars in the Store: {} at Time {}'.format(CarFleet.items, env.now))
arrival = env.now
with CarFleet.get() as req:
# Wait for the car or the trip fails
results = yield req | env.timeout(patience)
wait = env.now - arrival
if req in results:
# We got the car in time
departure_timestep = env.now
print('On day {} at Time {} the Trip {} got the Car {}'.format(day, departure_timestep, total_count, results[req]))
print('')
# this represents the driving time
yield env.timeout(8)
# return the car to the store after usage
CarFleet.put(results[req])
return_timestep = env.now
print('On day {} at time {} the trip {} returned the car {}'.format(day, return_timestep, total_count, results[req]))
print('Contents of store after the trip {} returns its car: {}'.format(total_count, CarFleet.items))
print('')
else:
# We quit
fail_timestep = env.now
global failed_count
failed_count += 1
if req.triggered:
print('--- request triggered after time out ----')
car = yield req
CarFleet.put(car)
print('On day {} at Time {} the Trip {} failed because it waited {}'.format(day, fail_timestep, total_count, wait))
###############################################################################
# Simpy simulation setup
###############################################################################
# define an environment where the processes live in
env = simpy.Environment()
# instantiate CarFleet Store
CarFleet = simpy.Store(env, capacity=capacity)
# fill the store with elements = Cars
for i in range(capacity):
CarFleet.put({i})
i += 1
# call the function that generates the individual rental processes
env.process(trip_gen(env))
# start the simulation
env.run()
print('Total Count: {}'.format(total_count))
print('Failed Count: {}'.format(failed_count))
print('All Cars should be back by the end of sim: {}'.format(CarFleet.items))