Search code examples
pythonsimulationsimpy

Reserve a resource for a group of activities


I have been learning SimPy and I've asked a few questions here and I appreciate the help I got. I'm revisiting the order fulfillment model I've posted about before here

I have boxes that have items for order to be fulfilled. The data is in a dataframe where each row is an item. Each box contains several items for different orders. When a box comes in, it's scanned, then all the items inside the box are scanned and put on a conveyor belt and they are being directed to carts where each order has its own designated carts. There are a number of operators that scan boxes and items and put them on the belt. The columns of the dataframe are:

item_id, order_id, box_id, Scanned. There are other columns but these are the important ones. Scanned indicates whether the box has been scanned or not

enter image description here

Let me first show you the code i'm working on.

env = simpy.Environment()
scan_op = simpy.Resource(env, capacity=5)  # operators who scanned boxes and items
cart_op = simpy.Resource(env, capacity=3)  # operators who process items and empty the carts
belt = simpy.Container(env, init=0)         # conveyor belt

carts = {}  
for order in order_list:  # order_list is a list of tuples that have (order_id, order quantity)
   carts[order[0]]= simpy.Container(env, capacity=order[1], init=0) 
def arrival_activity(df, env, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold):
    index = 0
    
    while index != len(df):
        
        box_process = box_activities(df, env, index, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold):
        
        yield(env.process(box_process ))
                
        index += 1

Here we iterate over all the rows of the dataframe (items) by index and run this process

def box_activities(df, env, index, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold):
    
    df.loc[index,'box_scan_queue']= env.now
    if not df.loc[index,'Scanned']:
        
        with scan_op.request() as req:
            df.loc[index,'box_scan_start']= env.now
                      
            yield req
            box_scan_time = np.random.triangular(min_box, mode_box, max_box)
            yield env.timeout(box_scan_time)
            
            df.loc[df['box_id'] == df.loc[index,'box_id'], 'Scanned'] = True                
            df.loc[df['box_id'] == df.loc[index,'box_id'], 'box_scan_done'] = env.now
    
    
    item_process = item_activity(df, env, index, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold)
    
    
    env.process(item_process)

Here we check if the box has been scanned or not. If not we scan it. Then we start a process to scan the items by their index in the dataframe

def item_activity(df, env, index, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold)):
    
    
    df.loc[index,'item_scan_queue']= env.now
    if df.loc[index,'Scanned']:
                
        with scan_op.request() as req:

            df.loc[index,'item_scan_start']= env.now

            yield req
            item_scan_time =np.random.triangular(min_item, mode_item, max_item)
            yield env.timeout(item_scan_time)
            df.loc[index,'item_scan_done']= env.now
        
        convey = belt_activity(df, env, index, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold) 
            
        yield(env.process(convey))

Here we scan each item and start a process for the conveyor belt and later a process for the carts but that's not important here for now.

In this code what happens is, as we iterate over the rows (items), if a box is not scanned then one of the scan operators scans it. Then, the scan operators start scanning the items. So the operators work together at one box and scan all the items until a box arrives with "Scanned" set to False. In that case one operator scan the box and again all operators start scanning the items.

What I want to try now is having one scan operator scanning a box and all the items in that box. So several boxes to be worked on at the same time instead of one. I tried to group the dataframe by box_id and pass each group to a process as follows:

def arrival_activity(df, env, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold):
    
    box_df = df.groupby('box_id')
    for _, box in box_df:

        box_process = box_activities(df, env, box, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold):
        
        yield(env.process(box_process))

As you can see, here I group the dataframe by box_id and pass that box to the process. Now we run the process for the box and the items inside it.

def box_activities(df, env, box, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold):

    with scan_op.request() as req:
    
        for index, _ in box.iterrows():

            if not df.loc[index,'Scanned']:

               box_scan_time = np.random.triangular(min_box, mode_box, max_box)
               yield env.timeout(box_scan_time)
            
               df.loc[df['box_id'] == df.loc[index,'box_id'], 'Scanned'] = True                
               df.loc[df['box_id'] == df.loc[index,'box_id'], 'box_scan_done'] = env.now
               ic_scan_time = np.random.triangular(min_ic, mode_ic, max_ic)
                
            item_scan_time =np.random.triangular(min_item, mode_item, max_item)
            yield env.timeout(item_scan_time)
            df.loc[index,'item_scan_done']= env.now
            
            convey = belt_activity(df, env, index, scan_op, cart_op, belt, carts,\
                      min_box, max_box, mode_box, min_item, max_item, mode_item,\
                      min_cart, max_cart, mode_cart, mean_belt, std_belt, emptying_threshold) 
                
            env.process(convey)
            
            yield req   

Here we request a scan operator. We then iterate over the rows of the box (items) and scan the box and the items and call the next process (conveyor).

This does not give me what I want. The results show that the boxes are scanned sequentially. Meaning one box after and other rather than multiple operators work different boxes at the same time. Not sure if it a flaw in the logic or it's a problem of where the times are recorded when I use env.now

Also speaking of env.now. In the original mode, when I record box_scan_done and item_scan_done they seem correct, but scan_queue and scan_start for both box and item are not correct especially the start one when I record it right after I request the resource.


Solution

  • what happens if you drop the yield in

    yield(env.process(box_process))
    

    with the yield, the code stops and waits for the procedure to finish. Without the yield the code starts the procedure and moves on to the next line of code.