pythonsimpy

Simpy model with closure, entities grabbing resource while closing code running


I have a model where:

My MRE is below (its quite long):

import simpy
import random
import math
import numpy as np

class default_params():
    random.seed(10)
    run_time = 10080
    iterations = 1
    no_resources = 4
    inter_arr = 120
    resource_time = 240
    #shop Opening Hours
    shop_open_time = 8
    shop_stop_accept = 18
    shop_close_time = 20

class spawn_entity:
    def __init__(self, p_id):
        self.id = p_id
        self.arrival_time = np.nan
        self.resource_gained_time = np.nan
        self.leave_time = np.nan

class shop_model:
    def __init__(self, run_number, input_params):
        #start environment, set entity counter to 0 and set run number
        self.env = simpy.Environment()
        self.input_params = input_params
        self.entity_counter = 0
        self.run_number = run_number
        #establish resources
        self.resources = simpy.PriorityResource(self.env,
                                            capacity=input_params.no_resources)
        
    ##############################MODEL TIME###############################
    def model_time(self, time):
        #Function to work out day and hour in the model
        day = math.floor(time / (24*60))
        day_of_week = day % 7
        #If day 0, hour is time / 60, otherwise it is the remainder time once
        #divided by number of days
        hour = math.floor((time % (day*(24*60)) if day != 0 else time) / 60)
        return day, day_of_week, hour
    
    ###########################ARRIVALS##################################
    def arrivals(self):
        yield self.env.timeout(1)
        while True:
            #up entity counter and spawn a new entity
            self.entity_counter += 1
            p = spawn_entity(self.entity_counter)
            #begin entity to shop process
            self.env.process(self.entity_journey(p))
            #randomly sample the time until the next arrival
            sampled_interarrival = round(random.expovariate(1.0
                                        / self.input_params.inter_arr))
            yield self.env.timeout(sampled_interarrival)

    ######################### CLOSE SHOP ################################
    def close_shop(self):
        while True:
            #if first day, close shop until open time, then wait until close
            if self.env.now == 0:
                time_closed = self.input_params.shop_open_time
                time_out = self.input_params.shop_close_time
            else:
                #close for 12 hour overnight and time out process until next day
                time_closed = 12
                time_out = 24

            #Take away all the resources for the close time to simulate the shop
            # being closed.
            print(f'--!!!!!CLOSING SHOP AT {self.env.now} FOR {time_closed * 60} MINS!!!!!--')
            i=0
            for _ in range(self.input_params.no_resources):
                i += 1
                print(f'--claiming resource {i}')
                self.env.process(self.fill_shop(time_closed * 60))
            print(f'--!!!!!SHOP CLOSED!!!!!--')

            #Timout for timeout period until next closure. 
            yield self.env.timeout(time_out * 60)
    
    def fill_shop(self, time_closed):
        #If close time, take away all the resources
        with self.resources.request(priority=-1) as req:
            yield req
            print(f'--resource claimed for close at {self.env.now} for {time_closed} mins')
            yield self.env.timeout(time_closed)

    ######################## ENTITY JOURNEY #############################

    def entity_journey(self, entity):
        #Arrival
        entity.arrival_time = self.env.now
        day, day_of_week, hour = self.model_time(entity.arrival_time)
        print(f'entity {entity.id} starting process at {entity.arrival_time}')

        #If arrival hour between stop accept and close, add an extra wait
        #to ensure they don't sneak in between these times
        if ((hour >= self.input_params.shop_stop_accept) 
            and (hour < self.input_params.shop_close_time)):
            #Time out until shop close, where resources will all be claimed
            #then entity can wait until next open.
            next_close = ((day * 60 * 24)
                           + (self.input_params.shop_close_time * 60))
            time_out = (next_close - entity.arrival_time) + 1
            print(f'entity {entity.id} arrived in queue after stop accepting.  Time out {time_out} mins until close')
            yield self.env.timeout(time_out)
            print(f'entity {entity.id} has waited until close at {self.env.now}')

        #request resource
        i=1
        with self.resources.request(priority=1) as req:
            #Find out current model time
            time = self.env.now
            print(f'entity {entity.id} requesting resource at time {time} - attempt {i}')
            day, day_of_week, hour = self.model_time(time)
            #Work out the minutes until the next shop stop accept time (next day
            #if hour after stop accept hour)
            next_stop_accept_day = (day + 1 if hour
                                    >= self.input_params.shop_stop_accept
                                    else day)
            next_stop_accept = ((next_stop_accept_day * 60 * 24)
                                + (self.input_params.shop_stop_accept * 60))
            time_to_stop_accept = (next_stop_accept - time) + 1

            print(f'entity {entity.id} has {time_to_stop_accept} mins until shop stops accepting')

            #entity either gets resource or timesout if past stop accept time
            yield req | self.env.timeout(time_to_stop_accept)

            #If entity doesn't get a bed the first attempt, keep trying
            #until they do
            while not req.triggered:
                i += 1
                print(f'entity {entity.id} did not get resource as shop stopped accepting at {self.env.now}, entity waiting 2 hours then rejoining queue for attempt {i}')
                #Time out for the time between stop accepting and close, to
                #ensure no entities get a resource during this time.
                yield self.env.timeout(((self.input_params.shop_close_time
                                        - self.input_params.shop_stop_accept)
                                        * 60) + 1)

                print(f'entity {entity.id} requesting resource at time {self.env.now} - attempt {i}')
                #entity tries again to get a resource until the next stop accept
                #time.
                time = self.env.now
                day, day_of_week, hour = self.model_time(time)
                next_stop_accept_day = (day + 1 if hour 
                                        >= self.input_params.shop_stop_accept
                                        else day)
                next_stop_accept = ((next_stop_accept_day * 60 * 24)
                                    + (self.input_params.shop_stop_accept
                                        * 60))
                time_to_stop_accept = (next_stop_accept - time) + 1
                print(f'entity {entity.id} has {time_to_stop_accept} until shop stops accepting')

                #entity either gets resource or timesout if past stop accept time
                yield req | self.env.timeout(time_to_stop_accept)
                
            #Entity got resource, record the time and randomly sample
            #process time
            print(f'entity {entity.id} got resource at {self.env.now} on attempt {i}')
            entity.resource_gained_time = self.env.now
            #Time entity spends is randomly sampled, with them being kicked out
            #1 minute before 8pm.
            day, day_of_week, hour = self.model_time(self.env.now)
            next_close_day = (day + 1 if hour 
                                        >= self.input_params.shop_close_time
                                        else day)
            next_close = ((next_close_day * 60 * 24)
                          + (self.input_params.shop_close_time * 60)) - 1
            time_to_next_close = next_close - self.env.now
            sampled_shop_time = round(random.expovariate(1.0
                                    / self.input_params.resource_time))
            yield self.env.timeout(min(sampled_shop_time, time_to_next_close))

            #record entity leave time
            entity.leave_time = self.env.now

########################RUN#######################
    def run(self):
        self.env.process(self.arrivals())
        self.env.process(self.close_shop())
        self.env.run(until = self.input_params.run_time)

def run_the_model(input_params):
    #run the model for the number of iterations specified
    for run in range(input_params.iterations):
        print(f"Run {run+1} of {input_params.iterations}")
        model = shop_model(run, input_params)
        model.run()

run_the_model(default_params)

Sometimes the close code runs perfectly, and all the resources are claimed at 8pm, which looks like:

--!!!!!CLOSING SHOP AT 4080 FOR 720 MINS!!!!!--
--claiming resource 1
--claiming resource 2
--claiming resource 3
--claiming resource 4
--!!!!!SHOP CLOSED!!!!!--
--resource claimed for close at 4080 for 720 mins
--resource claimed for close at 4080 for 720 mins
--resource claimed for close at 4080 for 720 mins
--resource claimed for close at 4080 for 720 mins

The issue I have is that sometimes when I'm closing the shop, not all resources get claimed at exactly 8pm and some entity activity will cut in before all resources have been claimed, sometimes even claiming a resource for a short time! An example from the above code output is:

--!!!!!CLOSING SHOP AT 2640 FOR 720 MINS!!!!!--
--claiming resource 1
--claiming resource 2
--claiming resource 3
--claiming resource 4
--!!!!!SHOP CLOSED!!!!!--
--resource claimed for close at 2640 for 720 mins
--resource claimed for close at 2640 for 720 mins
entity 21 has waited until close at 2641
entity 21 requesting resource at time 2641 - attempt 1
entity 21 has 1320 mins until shop stops accepting
entity 22 has waited until close at 2641
entity 22 requesting resource at time 2641 - attempt 1
entity 22 has 1320 mins until shop stops accepting
entity 19 requesting resource at time 2642 - attempt 2
entity 19 has 1319 until shop stops accepting
entity 20 requesting resource at time 2642 - attempt 2
entity 20 has 1319 until shop stops accepting
entity 19 got resource at 2642 on attempt 2
entity 20 got resource at 2642 on attempt 2
--resource claimed for close at 2683 for 720 mins
--resource claimed for close at 2703 for 720 mins

Here, 2 of the 4 resources gets closed exactly at 2640mins as expected, but the other 2 don't get claimed until 2683 and 2703. Entities 19 and 20 both manage to claim a resource briefly before they get closed, and leave the model. I don't understanding how they are managing to jump ahead of the close in the queue, as the close is priority -1 compared to their priority of 1. Nor do I understand why they sometimes don't get claimed to so many minutes after the close. Any help greatly appreciated.


Solution

  • Ok, I've banged by head against the keyboard over the last few days trying many different ways to do this, but I finally have something that has the desired behaviour. It uses similar logic to @Michael's answer in that it cancels requests when the shop is not open, and waits until the next opening to ask again, but I am using a list to manually keep track of my waiting list overnight, even if the requests have been cancelled. The key additions/changes are:

    Full code:

    import simpy
    import random
    import math
    import numpy as np
    
    class default_params():
        random.seed(10)
        run_time = 10080
        iterations = 1
        no_resources = 4
        inter_arr = 120
        resource_time = 240
        #shop Opening Hours
        shop_open_time = 8
        shop_stop_accept = 18
        shop_close_time = 20
        shop_day_of_week = [0, 1, 2, 3, 4]
    
    class spawn_entity:
        def __init__(self, p_id):
            self.id = p_id
            self.arrival_time = np.nan
            self.resource_gained_time = np.nan
            self.leave_time = np.nan
    
    class shop_model:
        def __init__(self, run_number, input_params):
            #start environment, set entity counter to 0 and set run number
            self.env = simpy.Environment()
            self.input_params = input_params
            self.entity_counter = 0
            self.run_number = run_number
            #establish resources
            self.resources = simpy.PriorityResource(self.env,
                                                capacity=input_params.no_resources)
            #Create list to manually track the queue
            self.entity_queue = []
            #Start shop accepting condition event
            self.shop_is_open = False
            self.shop_accepting = False
            self.shop_accepting_event = self.env.event()
            
        ##############################MODEL TIME###############################
        def model_time(self, time):
            #Function to work out day and hour in the model
            day = math.floor(time / (24*60))
            day_of_week = day % 7
            #If day 0, hour is time / 60, otherwise it is the remainder time once
            #divided by number of days
            hour = math.floor((time % (day*(24*60)) if day != 0 else time) / 60)
            return day, day_of_week, hour
        
        def shop_open(self, day, day_of_week, hour):
            #Function to work out if shop is open and/or accepting at any time
            shop_accepting = ((day_of_week in self.input_params.shop_day_of_week)
                               and ((hour >= self.input_params.shop_open_time)
                                  and (hour < self.input_params.shop_stop_accept)))
            shop_open = ((day_of_week in self.input_params.shop_day_of_week)
                               and ((hour >= self.input_params.shop_open_time)
                                   and (hour < self.input_params.shop_close_time)))
            return shop_accepting, shop_open
        
        ###########################ARRIVALS##################################
        def arrivals(self):
            yield self.env.timeout(1)
            while True:
                #up entity counter and spawn a new entity
                self.entity_counter += 1
                p = spawn_entity(self.entity_counter)
                #begin entity to shop process
                self.env.process(self.entity_journey(p))
                #randomly sample the time until the next arrival
                sampled_interarrival = round(random.expovariate(1.0
                                            / self.input_params.inter_arr))
                yield self.env.timeout(sampled_interarrival)
    
        ######################### CLOSE SHOP ################################
        def create_shop_accepting_event(self):
            #Event to monitor if shop is open or closed
            if self.shop_accepting:
                if not self.shop_accepting_event.triggered:
                    self.shop_accepting_event.succeed()
            else:
            # If it's already triggered, and we are now closing shop, replace with a new untriggered event
                if self.shop_accepting_event.triggered:
                    self.shop_accepting_event = self.env.event()
    
        def close_shop(self):
            #Manage shop opening and closing schedule
            while True:
                current_time = self.env.now
                day, day_of_week, hour = self.model_time(current_time)
                
                # Check current status
                should_be_accepting, should_be_open = self.shop_open(day, day_of_week, hour)
                
                if should_be_accepting and not self.shop_accepting:
                    # Time to open shop and start accepting customers
                    print(f'!!!!!SHOP START ACCEPTING AT {current_time}!!!!!')
                    self.shop_accepting = True
                    self.shop_is_open = True
                    self.create_shop_accepting_event()
                    
                elif not should_be_accepting and self.shop_accepting:
                    # Time to stop accepting new customers (6pm)
                    print(f'!!!!!SHOP STOP ACCEPTING AT {current_time}!!!!!')
                    self.shop_accepting = False
                    self.create_shop_accepting_event()  # Create new event but don't trigger
                    
                elif not should_be_open and self.shop_is_open:
                    # Time to close completely (8pm) - claim all beds
                    print(f'!!!!!SHOP CLOSING AT {current_time}!!!!!')
                    self.shop_is_open = False
                    self.shop_accepting = False
                    
                    # Calculate how long to stay closed
                    if day_of_week < 4:  # Monday-Thursday, reopen next morning
                        close_duration = (24 - self.input_params.shop_close_time + 
                                        self.input_params.shop_open_time) * 60
                    else:  # Friday, closed for weekend
                        close_duration = (72 - self.input_params.shop_close_time + 
                                        self.input_params.shop_open_time) * 60
                    
                    #time out until next open
                    print(f'!!!!!SHOP CLOSED!!!!!')
                    yield self.env.timeout(close_duration) 
    
                    #shop Re-opening
                    print(f'!!!!!SHOP REOPENING AT {self.env.now}!!!!!')
                    self.shop_is_open = True
                    self.shop_accepting = True
                    self.create_shop_accepting_event()
                
                # Check again in 60 minutes
                yield self.env.timeout(60)
    
        ######################## ENTITY JOURNEY #############################
    
        def entity_journey(self, entity):
            #Arrival
            entity.arrival_time = self.env.now
            day, day_of_week, hour = self.model_time(entity.arrival_time)
            print(f'entity {entity.id} starting process at {entity.arrival_time}')
    
            #Add entity to manually monitored queue
            self.entity_queue.append(entity)
    
            # Keep trying to get a bed until successful
            req_secured = False
            req = None
    
            while not req_secured:
                if not self.shop_accepting:
                     yield self.shop_accepting_event
    
                print(f'entity {entity.id} requesting resource at {self.env.now}')
                # shop is now accepting, make a bed request
                req = self.resources.request()
                #Work out time until the shop stops accepting
                time = self.env.now
                day, day_of_week, hour = self.model_time(time)
                next_stop_accept_day = (day + 1 if hour 
                                        >= self.input_params.shop_stop_accept
                                        else day)
                next_stop_accept = ((next_stop_accept_day * 60 * 24)
                                        + (self.input_params.shop_stop_accept
                                        * 60))
                time_to_stop_accept = max((next_stop_accept - time), 1)
    
                #entity either gets bed or timesout if past stop accept time
                success_or_timeout = yield req | self.env.timeout(time_to_stop_accept)
    
                if req in success_or_timeout:
                    req_secured = True
                    print(f'++ entity {entity.id} got resource at {self.env.now}')
                else:
                    # Timed out, cancel request and wait for next opening
                    if not req.triggered:
                        req.cancel()
                        print(f'entity {entity.id} did not get resource at  {self.env.now} - retrying...')
    
                #Entity got resource, record the time and randomly sample
                #process time
                entity.resource_gained_time = self.env.now
                #Time entity spends is randomly sampled, with them being kicked out
                #1 minute before 8pm.
                day, day_of_week, hour = self.model_time(self.env.now)
                next_close_day = (day + 1 if hour 
                                            >= self.input_params.shop_close_time
                                            else day)
                next_close = ((next_close_day * 60 * 24)
                              + (self.input_params.shop_close_time * 60)) - 1
                time_to_next_close = next_close - self.env.now
                sampled_shop_time = round(random.expovariate(1.0
                                        / self.input_params.resource_time))
                yield self.env.timeout(min(sampled_shop_time, time_to_next_close))
    
                # Release the resource
                self.resources.release(req)
    
                print(f'-- entity {entity.id} released resource at {self.env.now}')
    
                #record entity leave time
                entity.leave_time = self.env.now
    
    ########################RUN#######################
        def run(self):
            self.env.process(self.arrivals())
            self.env.process(self.close_shop())
            self.env.run(until = self.input_params.run_time)
    
    def run_the_model(input_params):
        #run the model for the number of iterations specified
        for run in range(input_params.iterations):
            print(f"Run {run+1} of {input_params.iterations}")
            model = shop_model(run, input_params)
            model.run()
    
    run_the_model(default_params)
    

    The output (with additional print statements) now looks like:

    !!!!!SHOP CLOSING AT 2640!!!!!
    !!!!!SHOP CLOSED!!!!!
    entity 24 starting process at 2641
    entity 25 starting process at 2646
    entity 26 starting process at 2728
    entity 27 starting process at 2932
    entity 28 starting process at 3061
    entity 29 starting process at 3152
    !!!!!SHOP REOPENING AT 3360!!!!!
    entity 21 requesting resource at 3360
    entity 22 requesting resource at 3360
    entity 23 requesting resource at 3360
    entity 19 requesting resource at 3360
    entity 20 requesting resource at 3360
    entity 24 requesting resource at 3360
    entity 25 requesting resource at 3360
    entity 26 requesting resource at 3360
    entity 27 requesting resource at 3360
    entity 28 requesting resource at 3360
    entity 29 requesting resource at 3360
    ++ entity 21 got resource at 3360
    ++ entity 22 got resource at 3360
    ++ entity 23 got resource at 3360
    ++ entity 19 got resource at 3360
    entity 30 starting process at 3384
    entity 30 requesting resource at 3384
    entity 31 starting process at 3398
    entity 31 requesting resource at 3398
    -- entity 21 released resource at 3399
    ++ entity 20 got resource at 3399
    -- entity 20 released resource at 3423
    ++ entity 24 got resource at 3423
    -- entity 24 released resource at 3467
    ++ entity 25 got resource at 3467
    -- entity 23 released resource at 3472
    ++ entity 26 got resource at 3472
    -- entity 22 released resource at 3561
    ++ entity 27 got resource at 3561
    entity 32 starting process at 3577
    entity 32 requesting resource at 3577
    -- entity 19 released resource at 3581
    ++ entity 28 got resource at 3581
    entity 33 starting process at 3641
    entity 33 requesting resource at 3641
    -- entity 28 released resource at 3649
    ++ entity 29 got resource at 3649
    entity 34 starting process at 3680
    entity 34 requesting resource at 3680
    -- entity 27 released resource at 3697
    ++ entity 30 got resource at 3697
    entity 35 starting process at 3704
    entity 35 requesting resource at 3704
    -- entity 30 released resource at 3728
    ++ entity 31 got resource at 3728
    -- entity 31 released resource at 3771
    ++ entity 32 got resource at 3771
    entity 36 starting process at 3777
    entity 36 requesting resource at 3777
    -- entity 25 released resource at 3862
    ++ entity 33 got resource at 3862
    -- entity 29 released resource at 3879
    ++ entity 34 got resource at 3879
    !!!!!SHOP STOP ACCEPTING AT 3960!!!!!
    entity 35 did not get resource at  3960 - retrying...
    entity 36 did not get resource at  3960 - retrying...
    entity 37 starting process at 3983
    entity 38 starting process at 3984
    -- entity 32 released resource at 4022
    -- entity 34 released resource at 4036
    -- entity 35 released resource at 4062
    -- entity 26 released resource at 4079
    -- entity 33 released resource at 4079
    -- entity 36 released resource at 4079
    !!!!!SHOP CLOSING AT 4080!!!!!