4

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.

Joost Loth
  • 43
  • 2

2 Answers2

4

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 packanges
    """

    # tranks the next id to be assiend 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 packanges
    """

    # tranks the next id to be assiend 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 ridres we need
        self.target_cnt = start_riders

        # tracks the number of riders we have
        self.curr_cnt = start_riders

        # the store idel 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 ridder 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 idel 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 th 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}')
Michael
  • 1,671
  • 2
  • 4
  • 8
  • Thanks for your response! A store seems to be the better solution. Unfortunately, I was not able to incorporate it properly within my model. Is there any chance you could me provide with a simple example e.g. - Orders arrive at one unit per minute - The riders_pool is modelled as a store, items are the riders class -8 riders are available in the beginning - the delivery process gets 1 rider from the store and returns it after a random time between 4 and 5 minutes - after 1 hour 3 riders are added, after 2 hours they are distracted again I would really appreciate your help! Thank you! – Joost Loth May 31 '22 at 19:45
  • added a example to answer – Michael Jun 02 '22 at 00:55
  • Thanks a lot! The example was really helpful! – Joost Loth Jun 02 '22 at 16:33
0

I like the answer provided, but there are some - such as myself - that like to use the resources more or less as they are written. As a result, I wrote a subclass of simpy.Resource which simply consumes the resource for a duration. I think that this is more appropriate to model a person who is out sick or something along those lines... it isn't as if the person no longer exists, but is simply on a "higher priority" task. This approach also makes monitoring easier to insert as you can - within the method - start counting sick time, number of times diverted, etc. depending on the context of the model. It may even be useful to put a reason parameter into the reduce_capacity method for the purposes of monitoring different categories of unavailability.

One would have to change the method somewhat for PriorityResource and PreemptiveResource, but the basic methodology works well without changing the basic functionality of the Resource as it is written or having to change to using the Store.

from simpy import Resource

class CustomResource(Resource):
    def reduce_capacity(duration: float | int, qty: int = 1):
        """
        Temporarily reduce the capacity of `qty` resources for `duration`
        """
        def reduce(d):
            with self.request() as req:
                yield req
                yield self._env.timeout(d)

        for _ in range(qty):
            self._env.process(reduce_capacity(duration))

    
slightlynybbled
  • 2,408
  • 2
  • 20
  • 38