1

I asked a different question about this simpy simulation model before, and Michael answered it and helped me tremendously. Now I have a new issue with this model, and I hope you guys can help me out. You can find that question here

Let me first describe the model with some new modifications I have made since the last question.

I have orders to process and fulfill. These orders are in a dataframe. Each row of the dataframe is an item of those orders. The columns of the dataframe are "order_id, item_id, box_id, scanned, order_quantity".

each order consists of multiple items. The items are located in different boxes. When processing the orders, all the boxes that have all the items for all the orders are ready. There are some operators that scan the box first, then scan each item in that box after that. So, each box is scanned only once. After the items are scanned, they are put on a conveyor belt. At the end of the conveyor there are there are carts. For now, these are as many carts as orders. Hence, the items of each order go to the cart designated for that order. When some percentage of the total number of items of an order are already in the order cart- say 90% of the total number of items, some operators start processing the items and remove them from the carts until all the items are processed and emptied. For instance, if an order has 10 items, once there are 5 of these items in that order's cart, the operators start emptying that cart as the other five items are on the way to the cart. They keep emptying until all 10 items are processed.

Now let's look at a sample dataframe and orders.

enter image description here

Here we have 18 items that belong to 4 orders and these items are located in 3 boxes. The column scanned indicate if the box has been scanned or not.

For the simulation, I iterate over the rows(items) one by one, and check if the box of that order has been scanned or not, then scan it and scan the item after that. I have a simpy container for the conveyor belt that all items are added to. Finally, the items go to the carts that they belong to and the carts are simpy containers as well.


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.boxainer(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.boxainer(env, capacity=order[1], init=0) 

Now for how the system is models

def activity(df,env,scan_op,cart_op,belt,carts,\
             mean_box, std_box,mean_item, std_item,\
                 mean_cart, std_cart,\
                 belt_dist,belt_speed, per):
    
    for i in range(len(df)):  # df is the dataframe described above
    
 
        # Here we check if the box has been scanned or not. If not, then it is scanned and all the 
          rows that have that box in the box_id column are changed to True 

        if not df.loc[i,'Scanned']: 
                    
            with scan_op.request() as req:
            
                yield req

                box_scan_time = abs(random.normalvariate(mean_box, std_box))

                yield env.timeout(box_scan_time)
                
                
                df.loc[df['box_id'] == df.loc[i,'box_id'], 'Scanned'] = True
          
                
    
                       
        # Now the item is scanned
        with scan_op.request() as req:
        
            yield req

            item_scan_time =abs(random.normalvariate(mean_item, std_item))
        
            yield env.timeout(item_scan_time)
            
        # Now we add the item to the conveyor belt and remove it after some time 
        yield belt.put(1)
        yield env.timeout(belt_dist/belt_speed)
        yield belt.get(1)
        

        # Now we add the item to the cart of the order the item belongs to 

        yield carts[df.loc[i,'order_id']].put(1)

        print(f"item {df.loc[i,'item_id']} of order {df.loc[i,'order_id']} has entered the cart "
        

        with cart_op.request() as req:
            
             # Check of the desired fraction of the items are in the cart. let's use per = 0.9
            if carts[df.loc[i,'order_id']].level >= df.loc[i,'quantity']*per: 
            
                yield req 
                
                cart_time =abs(random.normalvariate(mean_cart, std_cart))
                
                yield env.timeout(cart_time)
                
                print(f"item {df.loc[i,'items_id']} of {df.loc[i,'order_id']} has left the cart")


            
mean_item, std_item, mean_cart, std_cart, mean_box, std_box = 3, 1.5, 2, 1, 10, 2
per, belt_speed,belt_dist = 0.9, 5,10      

env.process(activity(df,env,scan_op,cart_op,belt,carts,\
             mean_box, std_box,mean_item, std_item,\
                 mean_cart, std_cart,\
                 belt_dist,belt_speed, per))
   
env.run()



As you can see I don't remove the items from the cart to cart using get() because if 90% of the items are in the cart then the operators start processing, and if I remove items that condition might hold next time. So once that percentage is reached, they start emptying as more item arrive. I guess I could use get() and remove them, but use different condition- like a bool variable for each cart, that turns True when 90% of the items reach the cart but honestly I tried to do that with now success. So my first question is how can I do that?

Now, the most important question is how to let the model run until all items are cleared from that cart. When I run this model with the provided data, all 18 items reach their correspondent carts, but only 4 of them get removed.

here is the output I get and it seems like only the item that arrives and meet the percentage condition gets removed and the process doesn't go further

item 76464851 of order 34 has entered the cart 
item 76464842 of order 34 has entered the cart 
item 76464841 of order 34 has entered the cart 
item 76464841 of 34 has left the cart 
item 81333558 of order 133 has entered the cart 
item 81333557 of order 133 has entered the cart 
item 81333556 of order 133 has entered the cart 
item 81333556 of 133 has left the cart 
item 81333554 of order 956 has entered the cart 
item 81333553 of order 956 has entered the cart 
item 81333552 of order 956 has entered the cart 
item 81333551 of order 956 has entered the cart 
item 77026821 of order 956 has entered the cart 
item 77026822 of order 956 has entered the cart 
item 77026823 of order 956 has entered the cart 
item 77026823 of 956 has left the cart 
item 77026824 of order 420 has entered the cart 
item 77026825 of order 420 has entered the cart 
item 77026826 of order 420 has entered the cart 
item 77026828 of order 420 has entered the cart 
item 77026829 of order 420 has entered the cart 
item 77026829 of 420 has left the cart 

One thing I tried is making that as a process as follows. This code goes after the items are added to the cart with put()

 print(f"item {df.loc[i,'item_id']} of order {df.loc[i,'order_id']} has entered the cart ")

    if carts[df.loc[i,'order_id']].level >= df.loc[i,'quantity']*per:
        
        p = emptying(env,df,i,mean_cart, std_cart) 
        
        env.process(p)
def emptying(env,df, i, mean_cart, std_cart):
    
    while True:
    
        with cart_op.request() as req:
            
            yield req
            
            cart_time =abs(random.normalvariate(mean_cart, std_cart))
            
            yield env.timeout(cart_time)
            
            print(f"item {df.loc[i,'items_id']} of {df.loc[i,'order_id']} has left the cart")

This process still result in the same output as the original script.

Your help is hightly appreciated as I'm struggling with this simulation and I can't think of any new ideas to fix it after having been trying for the past week

3 Answers3

1

simulations can be stopped with events This example just uses one event, but you can also events.AnyOf to stop the sim on one of many events

This sim stops when n-1 of n processes finish.

"""
Quick example on how to use a event to stop a simulation

Programer: Michael R. Gibbs

"""

import simpy
import random

def someProcess(env, id):
    """
    Simple process with a random duration
    """

    yield env.timeout(random.uniform(1,10))
    print(f'{env.now} process {id} has finish')

    global process_cnt, sim_end_event
    process_cnt -= 1

    if process_cnt <= 0:
        sim_end_event.succeed()

process_cnt = 4
env = simpy.Environment()

sim_end_event = env.event()

# start a bunch of processes
# since their process time is random, 
# so will the order the processes finish
#
# start one extra process that will not
# finish

for i in range(process_cnt + 1):
    env.process(someProcess(env, i+1))

env.run(sim_end_event)
print(f'{env.now} sim has finished')
Michael
  • 1,671
  • 2
  • 4
  • 8
  • Thank you Michael for your feedback. I fixed the issue of not emptying all carts in my new answer to the post. I added a process to do that. But as you can see in my answer, I'm facing a new issue. I add items to the containers one at the time using put(1). How can I add the actual item id to the cart and let simpy interpret it as one unit. if I add and id like 76464851 it interpret it as 76464851 items added, and when I cast it as string, it fail to do the comparison because in container.level it cant compare string to numbers – Erin Walter Feb 26 '23 at 20:26
  • can you use a store instead of a container? – Michael Feb 26 '23 at 23:52
  • I looked into it but how can I keep track of the items that are removed using get() since it takes no argument? Let's make it simple and use stores for the conveyor belt. I did this : belt = simpy.Store(env). then when the items are added to the conveyor yield belt.put(df.loc[i,'SortationLinkId']) yield env.timeout(belt_dist/belt_speed) yield belt.get() Now how can I keep track of the items that are removed? – Erin Walter Feb 27 '23 at 03:17
0

I was able to fix the issue by doing the following.

After adding item to the cart, I check the condition of emptying then run a separate process for emptying

First I create a new dictionary for the status of the cart

carts = {}
carts_status= {}
for order in order_list:
    carts [order[0]]= simpy.Container(env, capacity=order[1], init=0)
    carts_status[order[0]] = False

Now every time an item is added to a cart, I check the order percentage condition and only use that to change the status of the cart and I also run an emptying process.

if carts[df.loc[i,'order_id']].level >= df.loc[i,'quantity']*per: 

   carts_status[df.loc[i,'order_id']] = True 
proc = emptying(env,df,i,mean_cart, std_cart,\
          carts[df.loc[i,'order_id']], carts_status[df.loc[i,'order_id']]) 
env.process(proc)


def emptying(env,df, i, mean_cart, std_cart, cart, cart_status):

  if cart_status:

    while cart.level != 0:
            
        with cart_op.request() as req:
            
            yield req
            
            cart_time =abs(random.normalvariate(mean_chute, std_chute))
            
            yield env.timeout(cart_time)
            yield cart.get(1)
            
            print(f"item {df.loc[i,'item_id']} of {df.loc[i,'order_id']} has left the cart")

This now remove all the items from the carts. In this datafra,me example, 18 items are added to their correspondent carts and 18 items are removed from the carts and the level of all the carts at the end is 0.

There is still one issue. Currently, When adding and removing items to the cart (simpy container) I add them as one unit, i.e. cart.put(1) and cart.get(1). When I pass the item id which is an eight digit number, simpy thinks I'm adding that many items at once. If I cast that number as a string, simpy don't understand that as one unit and fails to compare the level to the percentage because it tries to compare string to integer. So the question is how can I add the order id- as a number or string, to the container and still be interpreted as one unit?

0

This models a picker getting a box, scanning the box, scanning each item in the box and putting the item on a conveyor belt. The conveyor belt is constrained so there is at least a minimum amount of time between each item being put on the conveyor belt, which limits the conveyor belt's throughput. The conveyor belt loads carts with items. One cart per order, and the belt is smart enough to to route the item to the right cart. If enough items are put into a cart, a packer is requested to start packing. If the packer empties the cart before the last remaining items arrive the packer will wait for each remaining item until packing of the order is completed.

I use relationships between Items, Orders, carts, and boxes classes. A item knows what order it belongs to, what box it starts in, and what cart it is going to.

"""
Quick sim where a picker scans a box
whose items are put onto a conveyer
which feeds into carts
which get packed when the carts have x% of a order
Each cart has one order
"""

import simpy
import pandas as df

class Order():
    """
    quick data class to track the processing of a order
    """

    def __init__(self):
        self.order_id = -1
        self.order_items = []

        # makes sure packing only starts once
        self.packing_started = False 

class Box():
    """
    quick data class listing the items in a box
    """

    def __init__(self):
        self.box_id = -1
        self.items = []

class Cart():
    """
    quick data class listing the items in a cart
    one cart per id, so uses order_id for cart id
    """

    def __init__(self, env):
        self.order = None

        # need a store here incase
        # the packer needs to wait
        # for a item to arrive
        self.items = simpy.Store(env)

class Item():
    """
    quick data class maping a item to its order and box

    also maps where items go from box to cart
    """

    def __init__(self):
        self.item_id = -1
        self.order = None
        self.box = None
        self.cart = None

def pickBox(env, box, conveyer_q1, conveyer_q2, picker_pool, packer_pool):
    """
        Starts the processing of a box
        where a box waits for a picker
        gets scanned,
        gets all its items put onto a conveyer
    """

    # wait for a picker
    with picker_pool.request() as req:
        yield req

        yield env.timeout(2) # box scan time
        print(f'{env.now} box {box.box_id} has been scaned')

        # load itemon onto conveyer
        for  item in box.items:

            yield env.timeout(1) # item scan time
            print(f'{env.now} item {item.item_id} has been scanned from box {box.box_id}')

            # wait turn to put on convayer
            yield env.process(load_conveyer(item, conveyer_q1, conveyer_q2, packer_pool))

def q_boxes(env, boxes, conveyer_q1, conveyer_q2, picker_pool, packer_pool):
    """
    queues up the boxes for picking by picker

    Each thread will queue up when they make a
    request for a picker
    """
    for box in boxes:
        env.process(pickBox(env, box, conveyer_q1, conveyer_q2, picker_pool, packer_pool))

def load_conveyer(item, conveyer_q1, conveyer_q2, packer_pool):
    """
    q1 acts as a blocker while
    q2 gets processed 
    """
    yield conveyer_q1.put(item)
    yield conveyer_q2.put(item)

def check_conveyer(env, item, conveyer_q1, conveyer_q2, packer_pool):
    """
    Limits how fast the convery can be loaded,
    keeps the pickers from putting items on the 
    conveyer at the same time

    starts when q2 gets a item
    but keeps blocking with q1
    until the load time has passed

    q1 is what blocks other processes from putting stuff on the conveyer
    """

    while True:

        # wait for something to be put on the conveyer
        item = yield conveyer_q2.get()

        # block others from puting another item on the
        # conveyer too soon
        yield env.timeout(1) # min loading speed for conveyor

        yield conveyer_q1.get()

        # start it on its way
        env.process(load_cart(env, item, packer_pool))



def load_cart(env, item, packer_pool):
    """
    models the conveyer travel time for a item
    and starts the packing if enough items for 
    a cart has arrived at the cart
    """

    # convayer travel time
    yield env.timeout(5)

    item.cart.items.put(item)

    print(f'{env.now} itme {item.item_id} is in cart')
    
    pack_percent = len(item.cart.items.items) / len(item.order.order_items)
    
    if (not item.order.packing_started) and (pack_percent > .9):
        # over 90% and has not started packing
        print(f'{env.now} starting to pack order {item.order.order_id}')
        item.order.packing_started = True
        env.process(pack_items(env, item.cart, packer_pool))

def pack_items(env, cart, packer_pool):
    """
    Packs the items in a cart
    """

    pack_cnt = 0

    # get a packer
    with packer_pool.request() as req:
        yield req

        while pack_cnt < len(cart.order.order_items):
            # still has stuff to pack

            # yield incase the final items have not arrived yet
            item = yield cart.items.get()

            yield env.timeout(1) # packing time

            pack_cnt +=1
            print(f'{env.now} item {item.item_id} for order {item.order.order_id} has been packed')

    print(f'{env.now} order {cart.order.order_id} has finish packing')

processing_data = df.DataFrame(
    [
        # order id, item id, box id
        [1, 1, 1],
        [1, 2, 1],
        [1, 3, 1],
        [2, 4, 2],
        [2, 5, 2],
        [2, 6, 2],
        [3, 7, 2],
        [3, 8, 2],
        [3, 9, 2],
        [3, 10, 2],
        [3, 11, 3],
        [3, 12, 3],
        [3, 13, 3],
        [5, 14, 3],
        [4, 15, 3],
        [4, 16, 3],
        [4, 17, 3],
        [4, 18, 3],
    ],
    columns=['order_id', 'item_id', 'box_id']
)

env = simpy.Environment()

picker_pool = simpy.Resource(env, capacity=5)
packer_pool = simpy.Resource(env, capacity=3)

# used to limit how fast things can be put on belt
# and that only one item at a time is put on the conveyer
conveyer_q1 = simpy.Store(env, capacity=1) # blocks
conveyer_q2 = simpy.Store(env, capacity=1) # flags

# make wharehouse
order_map = {}
cart_map = {}
box_map = {}

for index, row in processing_data.iterrows():
    order = order_map.get(row['order_id'])
    if order is None:
        order=Order()
        order.order_id = row['order_id']
        order_map[row['order_id']] = order

    cart = cart_map.get(row['order_id'])
    if cart == None:
        cart = Cart(env)
        cart.order = order
        cart_map[row['order_id']] = cart

    box = box_map.get(row['box_id'])
    if box is None:
        box = Box()
        box.box_id = row['box_id']
        box_map[row['box_id']] = box

    item = Item()
    item.item_id = row['item_id']

    # map the item to its order and box
    item.order = order
    item.box = box
    item.cart = cart

    # add item to order's order list
    order.order_items.append(item)

    # add item to box's box list
    box.items.append(item)

# limits loading spead of conveyer
env.process(check_conveyer(env, item, conveyer_q1, conveyer_q2, packer_pool))

# use processes to queue up each box to be picked
q_boxes(env, box_map.values(), conveyer_q1, conveyer_q2, picker_pool, packer_pool)

env.run(1000)
Michael
  • 1,671
  • 2
  • 4
  • 8