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.
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)}')