Search code examples
pythonresourcessimulationsimpy

Change the number of resources during simulation in simpy


I am currently working on a model which simulates a delivery process throughout an operating day in simpy. The deliveries are conducted by riders which are delivering one package at a time. The packages need to be delivered within a certain time window. In addition, the system deals with fluctuating demand throughout the day. Therefore, I would like to adapt the number of employees available to the system matching the fluctuating demand on an hourly basis. I modelled the riders as a resource with a certain capacity. Is there any possibility to adjust the capacity of the resource during a simulation run or are there other ways to model the riders with my system?

I already looked for possible solution within the simpy documentation, examples or other posts. However, I was not successful yet. Thus, for any advice or possible solutions I would be very grateful! Thank you in advance.


Solution

  • Use a store instead of a resource. Resources has a fix number of resources. Stores works a bit like a bit more like a queue backed with a list with a optional max capacity. To reduce then number in the store, just stop putting the object back into the store.

    So is a example I wrapped a store with a class to manage the number of riders.

    """
        Simple demo of a pool of riders delivering packages where the 
        number of riders can change over time
    
        Idel riders are keep in a store that is wrapped in a class
        to manage the number of riders
    
        Programmer Michael R. Gibbs
    """
    
    import simpy
    import random
    
    class Rider():
        """
            quick class to track the riders that deliver packages
        """
    
        # tracks the next id to be assigned to a rider
        next_id = 1
    
        def __init__(self):
    
            self.id = Rider.next_id
            Rider.next_id += 1
    
    class Pack():
        """
            quick class to track the packages
        """
    
        # tracks the next id to be assigned to a pack
        next_id = 1
    
        def __init__(self):
    
            self.id = Pack.next_id
            Pack.next_id += 1
    
    class RiderPool():
        """
            Pool of riders where the number of riders can be changed
        """
    
        def __init__(self, env, start_riders=10):
    
            self.env = env
    
            # tracks the number of riders we need
            self.target_cnt = start_riders
    
            # tracks the number of riders we have
            self.curr_cnt = start_riders
    
            # the store idle riders
            self.riders = simpy.Store(env)
    
            # stores do not start with objects like resource pools do.
            # need to add riders yourself as part of set up
            self.riders.items = [Rider() for _ in range(start_riders)]
     
    
        def add_rider(self):
            """
                Add a rider to the pool
            """
    
            self.target_cnt += 1
    
    
            if self.curr_cnt < self.target_cnt:
                # need to add a rider to the pool to get to the target
                rider = Rider()
                self.riders.put(rider)
                self.curr_cnt += 1
                print(f'{env.now:0.2f} rider {rider.id} added')
    
            else:
                # already have enough riders,
                # must have tried to reduce the rider pool while all riders were busy
                # In effect we are cancelling a previous remove rider call
                print(f'{env.now:0.2f} keeping rider scheduled to be removed instead of adding')
    
        def remove_rider(self):
            """
                Remove a rider from the pool
    
                If all the riders are busy, the actual removal of a rider
                will happen when a that rider finishes it current task and is
                tried to be put/returned back into the pool
            """
    
            self.target_cnt -= 1
    
            if self.curr_cnt > self.target_cnt:
                if len(self.riders.items) > 0:
                    # we have a idle rider that we can remove now
    
                    rider = yield self.riders.get()
                    self.curr_cnt -= 1
                    print(f'{env.now:0.2f} rider {rider.id} removed from store')
    
                else:
                    # wait for a rider to be put back to the pool
                    pass
            
    
        def get(self):
            """
                Get a rider from the pool
    
                returns a get request that can be yield to, not a rider
            """
    
            rider_req = self.riders.get()
    
            return rider_req
    
        def put(self, rider):
            """
                put a rider pack into the pool
            """
    
            if self.curr_cnt <= self.target_cnt:
                # still need the rider
                self.riders.put(rider)
            else:
                # have tool many riders, do not add back to pool
                self.curr_cnt -= 1
                print(f'{env.now:0.2f} rider {rider.id} removed on return to pool')
    
    def gen_packs(env, riders):
        """
            generates the arrival of packages to be delivered by riders
        """
    
        while True:
    
            yield env.timeout(random.randint(1,4))
            pack = Pack()
    
            env.process(ship_pack(env, pack, riders))
    
    def ship_pack(env, pack, riders):
        """
            The process of a rider delivering a packages
        """
    
        print(f'{env.now:0.2f} pack {pack.id} getting rider')
    
        rider = yield riders.get()
    
        print(f'{env.now:0.2f} pack {pack.id} has rider {rider.id}')
    
        # trip time
        yield env.timeout(random.randint(5,22))
    
        riders.put(rider)
    
        print(f'{env.now:0.2f} pack {pack.id} delivered')
    
    def rider_sched(env, riders):
        """
            Changes the number of riders in rider pool over time
        """
    
    
        yield env.timeout(30)
        # time to remove a few riders
    
        print(f'{env.now:0.2f} -- reducing riders')
        print(f'{env.now:0.2f} -- request queue len {len(riders.riders.get_queue)}')
        print(f'{env.now:0.2f} -- rider store len {len(riders.riders.items)}')
    
        for _ in range(5):
            env.process(riders.remove_rider())
    
        yield env.timeout(60)
        # time to add back some riders
    
        print(f'{env.now:0.2f} -- adding riders ')
        for _ in range(2):
            riders.add_rider()
    
    
    # run the model
    env = simpy.Environment()
    riders = RiderPool(env, 10)
    
    env.process(gen_packs(env, riders))
    env.process(rider_sched(env, riders))
    
    env.run(100)
    
    print(f'{env.now:0.2f} -- end rider count {riders.target_cnt}')