Search code examples
pythonsimulationsimpy

Requesting resources when monitoring the resource usage


I try to monitor my resources and as explained in the documentation here

I was able to achieve that as you can see in the code below. My issue is when I have multiple instance of a resource, I cannot figure out how to call an available resource, or wait until one becomes available if they all are busy when requested.

Here is a simple model where an item goes through two tasks sequentially and both tasks are done by the same resource that I have ten instances of. I add the resource instance to a dictionary along with a label showing whether the recorded time was done in the first or second task


import simpy
import random


def arrival(env, worker, arrival_time, task1_mean, task1_std, task2_mean, task2_std):
    
    id = 0
    
    while True:
        
        w = first_task(env, worker, task1_mean, task1_std, task2_mean, task2_std, id)
        
        env.process(w)
        
        yield env.timeout(random.expovariate(1/arrival_time))
        
        id += 1


def first_task(env, worker, task1_mean, task1_std, task2_mean, task2_std, id):


    task1_queue = env.now
    with worker[id % len(10)]['time'].request() as req:
        
        task1_start = env.now
        worker[id % len(10)]['task'].append('Task 1')
        
        yield req
        
        yield env.timeout(abs(random.normalvariate(task1_mean, task1_std)))
        task1_end = env.now
    
    w = first_task(env, worker, task2_mean, task2_std, id)
    
    env.process(w)
    

def second_task(env, worker, task2_mean, task2_std, id):

    task2_queue = env.now
    with worker[id % len(10)]['time'].request() as req:
        
        worker[id % len(10)]['task'].append('Task 2')
        task2_start = env.now
        yield req
        
        yield env.timeout(abs(random.normalvariate(task2_mean, task2_std)))
        task2_end = env.now
    

class TimeMonitoredResource(simpy.Resource):
    
  def __init__(self,*args,**kwargs):
      
    super().__init__(*args,**kwargs)
    self.start_time = None
    self.end_time = None
    self.working_time = []
    self.starting_time = []
    self.releasing_time = []

  def request(self,*args,**kwargs):
      
    self.start_time = self._env.now

    return super().request(*args,**kwargs)
  
  def release(self,*args,**kwargs):
    self.end_time = self._env.now
    self.working_time.append(self.end_time - self.start_time)
    self.starting_time.append(self.start_time)
    self.releasing_time.append(self.end_time)

    return super().release(*args,**kwargs)

def calculate_metrics(op):
    total_operation_time = []
    total_starting_time = []
    total_releasing_time = []
    
    total_operation_time = op.working_time
    total_starting_time = op.starting_time
    total_releasing_time = op.releasing_time
    
    return total_starting_time, total_releasing_time, total_operation_time


env = simpy.Environment()

worker = {}
for i in range(10):
    worker[i] = {'time': TimeMonitoredResource(env, 1), 'task': []}
          
task1_mean, task1_std, task2_mean, task2_std = 6, 2, 8, 2
arrival_time = 2

env.process(arrival(env, worker, arrival_time, task1_mean, task1_std, task2_mean, task2_std))

env.run(1200)


worker_op_start = {}
worker_op_release = {}
worker_op_total = {}
worker_op_task = {}


for i, op in worker.items():
    start, release, total = calculate_metrics(op['time'])
    worker_op_start[i] = start
    worker_op_release[i] = release
    worker_op_release[i] = total
    worker_op_task[i] = op['task']

As you can see in this code I used the item id to call one of the ten resources using id % len(10) as a trick to call a specific resource and make the code work. What I want to be able to achieve is calling any of the ten resources to do a task. If all ten are not available, then the process wait until one becomes available.

I tried to use a loop and iterate over the resources and find an available one as follows:


for _, resource in worker.items():
    if resource['time'].count == 0:  
        with resource['time'].request() as req:
            resource['task'].append('Task 1')
            yield env.timeout(abs(random.normalvariate(task1_mean, task1_std)))
            yield req
            break  

Unfortunately, this retuned nothing and all the recorded times were zeros. I hope someone can help with this. Also I want to record the time for each item by id. The question is, is the places where I recorcd those times using env.now() correct or not? As you can see I have task1_queue, task1_start, etc. I just want to make sure I'm recording these times in the right place.

Thank you all. This community has been really helpful in my SimPy journey.


Solution

  • So the problem with collecting resource metrics at the resource level with a simpy.Resource is simpy.Resource does not have resouce objects. It only has a count of available / deployed resources. I created a resource object to collect the metrics and used a simpy.Store to manage the requests. I wrapped the simpyStore in a class that updates the resource objects as they are requested and released. The issue with a simpy store is that it does not have a context manager to automatically return the resources to the simpy.Store like a simpy.Resouce pool does. So I created a special simpy Event by adding a context manager which wraps the simpy.Store get(), and adds a release call on the "with" exit. Did not do a lot of testing, but it works in the happy path. So your getting two things in this example. a simpy.Store with a context manager the returns resources on exit of the "with", and resources that collect usage metrics at the resource level. This was a bit more trick then I thought it would be.

    """
    Demo of resources where each resource collects usage data
    
    Composes a resource pool that uses a simpy.Store to hold
    resouces objects.  The pool updates the resouce stats as
    they are requested and released
    
    Creatd a special event to act as a context manager for
    resource requests
    
    programmer: Michael R. Gibbs
    """
    
    import simpy
    import random
    
    class ResourceEvent(simpy.Event):
        """
        A event for rquesting a resouce.
        Is also a context manager so
        'with' syntax can be use to 
        atomatic release the resource
    
        event returns a resouce when it succeeds
        """
    
        def __init__(self, env, request_process, release_process):
            """
            starts the process to get a resouce
            saves processes
    
            parameters
            ----------
            request_process: process 
                a process to get the resource
            release_process: process
                a process to release a resouce
    
            """
    
            super().__init__(env)
            
            self.env = env
            self.request_process = request_process
            self.release_process = release_process
    
            env.process(self._echo())
    
        def _echo(self):
            """
                process to get a resouce
    
                triggers this event when process gets the resouce
                returning the resource
            """
            self.request_event = self.env.process(self.request_process())
    
            resource = yield self.request_event
    
            # save resouce so it can be released later
            self.resource = resource
    
            self.succeed(resource)
    
        def __enter__(self):
            """
            Context enter, returns self
            """
            return self
        
        def __exit__(self, *arg, **karg):
            """
            Context exit
    
            if the resource request was successful, then
                return the resouce
            else
                cancel the pending resouce request
            """
            if self.request_event.triggered:
                self.release_process(self.resource)
            else:
                self.request_event.cancel()
    
        
    class MyResourcePool():
        """
        Wraps a simpy.Store to behaive more like a simpy.Resouce
    
        Uses resouces objects that collect usage stats
        """
    
        def __init__(self, env, recourse_cnt):
            """
            creates a simpy.Store and load it with the desirec
            number of resouces
    
            parameter
            ---------
            env: simpy.Environment
    
            resourse_cnt: int
                number of resouces in the pool
            """
    
            self.env = env
            self._pool = simpy.Store(env)
            self.resources = [] # master list to simplify resouce interation
    
            for i in range(recourse_cnt):
                resource = self.MyResource(i+1)
    
                self.resources.append(resource)
                self._pool.put(resource)
    
        def _request_event(self):
            """
            helper method to get a resouce
            and to update metrics for the resouce
            """
    
            print(f'{self.env.now} requesting a resouce')
    
            request_start = self.env.now
    
            # using a "with" insures the get gets
            # cancled if this process gets canceled
            with self._pool.get() as req:
                resource = yield req
    
            request_time = self.env.now - request_start
            resource.req_wait_time += request_time
    
            idle = self.env.now - resource._relase_time
            resource.idle_time += idle
    
            # save dispatch time to calc the busy/work time later
            resource._dispatch_time = self.env.now
    
            resource.busy = True
    
            print(f'{self.env.now} resouce {resource.id} has been deployed')
    
            return resource
    
    
        def request(self):
            """
            Creates a event that returns a resouce when a resouce 
            becomes available
    
            event is also a context manager so it can be uses 
            in a 'with' syntax
            """
    
            req = ResourceEvent(self.env, self._request_event, self.release)
    
            return req
            
            
    
        def release(self, resource):
            """
            Release a resouce and updates it stats
            """
    
            print(f'{self.env.now} resouce {resource.id} has been released')
    
            if resource in self.resources:
                work = self.env.now - resource._dispatch_time
                resource.busy_time += work
    
                resource._relase_time = self.env.now
                resource.busy = False
    
                self._pool.put(resource)
    
            else:
                raise Exception("Trying to release a object that does not belong to pool")
    
    
        class MyResource():
            """
            Resouce class for collecting usage stats
            """
    
            def __init__(self, id):
                """
                initalize collectors and trackers
                """
    
                self.id = id
    
                self.busy = False
    
                self.req_wait_time = 0
                self.busy_time = 0
                self.idle_time = 0
    
                self._dispatch_time = 0
                self._relase_time = 0
    
    def use_resouce(env, pool):
        """
        test grabing a resource
        """
    
        with myPool.request() as req:
        
            resource = yield req
            yield env.timeout(random.triangular(1,3,10))
    
    def test(env, myPool):
    
        # doing a test without the with syntax
        req = myPool.request()
    
        resource = yield req
    
        yield env.timeout(5)
    
        myPool.release(resource)
    
        # do real stress test
    
        for _ in range(20):
            env.process(use_resouce(env, myPool))
            yield env.timeout(random.triangular(1,1,4))
        
    
    
    env = simpy.Environment()
    myPool = MyResourcePool(env, 3)
    
    env.process(test(env, myPool))
    
    env.run(100)
    
    print()
    print('done')
    for r in myPool.resources:
        print(f'id: {r.id}, busy: {r.busy_time}, idle: {r.idle_time}, usage: {r.busy_time / (r.busy_time + r.idle_time)}')