Search code examples
pythonsimulationsimpyevent-simulation

Simpy: Items in a Store disappear while modelling a CarFleet with a SimpyStore and conditional events


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:

  • the more days are being simulated, the more likely this behaviour will show up.
  • if all trips per day are being generated at the same time, instead of following an occurrence-probability, the problem never occurs.

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))


Solution

  • 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))