Search code examples
pythonsimpy

Simpy: simultaneously release resource on allocation of new resource


I have simplified a version of a problem I am attempting to model where I am using Simpy to describe the movement of travellers along a path.

The path is represented by a collection of Node() objects where each node contains a Simpy.Resource. Each node is connected to the next node in the path by the connected_to attribute. In the example code I have created a list of 10 nodes where each node in the list is connected to the preceding node in the list.

When a traveller (represented by an Occupier() object) is instantiated it is allocated the resource of a node. The traveller then moves along the nodes, only taking a step if the next node is available. My aim is for the traveller to simultaneously be allocated its destination node and release the node where it was previously located.

import simpy


class Node(object):
    def __init__(self, env):
        self.env = env
        self.resource = simpy.Resource(self.env)
        self.up_connection = None
        self.travel_delay = 5


class Occupier(object):
    def __init__(self, env):
        self.env = env
        self.location = None
        self.destination = None
        self.requests = []

    def travel(self, instantiation_loc):
        self.requests.append(instantiation_loc.resource.request())
        yield(self.requests[-1])

        self.location = instantiation_loc
        self.destination = instantiation_loc.up_connection
        yield self.env.timeout(self.location.travel_delay)
        node_occupancy(nodes)

        while self.destination.up_connection != None:
            self.requests.append(self.destination.resource.request())
            yield self.requests[-1]

            self.location.resource.release(self.requests[0])
            self.requests.pop(0)
            self.location = self.destination
            self.destination = self.location.up_connection

            yield self.env.timeout(self.location.travel_delay)
            node_occupancy(nodes)


def node_occupancy(nodes):
    print([node.resource.count for node in nodes])       


env = simpy.Environment()

nodes = [Node(env) for i in range(10)]
for i in range(len(nodes) - 1):
    nodes[i].up_connection = nodes[i + 1]

env.process(Occupier(env).travel(nodes[0]))

env.run()

If I run the above code with one traveller it seems to work fine giving the following output:

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0]

However, if instantiate a second traveller you can see that there are points in time where one traveller is occupying two resources when it should only occupy one:

env.process(Occupier(env).travel(nodes[3]))
env.process(Occupier(env).travel(nodes[0]))

corresponding output:

[1, 0, 0, 1, 0, 0, 0, 0, 0, 0]
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0]
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0]
[0, 1, 0, 0, 1, 1, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 1, 0, 0, 0, 0]
[0, 0, 1, 0, 0, 1, 1, 0, 0, 0]
[0, 0, 0, 1, 0, 0, 1, 0, 0, 0]
[0, 0, 0, 1, 0, 0, 1, 1, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 1, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 1, 1, 0]
[0, 0, 0, 0, 0, 1, 0, 0, 1, 0]
[0, 0, 0, 0, 0, 1, 0, 0, 1, 0]
[0, 0, 0, 0, 0, 0, 1, 0, 1, 0]
[0, 0, 0, 0, 0, 0, 0, 1, 1, 0]

It is important for my simulation that a traveller only ever occupies one resource as the attributes of the nodes are amended frequently based on this.

Is there any way to prevent this behaviour where a traveller never occupies more than one resource? i.e. a resource is simultaneously released when the traveller is allocated a new resource


Solution

  • In fact, your model is running correctly.

    Try to add to the function node_ocupacy, the current execution time and some markers to identify the current stage of the simulation:

    def node_occupancy(nodes, node, case):
        print(env.now, node, case, [node.resource.count for node in nodes])  
    

    Also, I made some changes just to see a better simulation log:

    def travel(self, instantiation_loc, loc):
        self.requests.append(instantiation_loc.resource.request())
        yield(self.requests[-1])
    
        self.location = instantiation_loc
        self.destination = instantiation_loc.up_connection
        yield self.env.timeout(self.location.travel_delay)
        node_occupancy(nodes, loc, 1)
    
        while self.destination.up_connection != None:
            self.requests.append(self.destination.resource.request())
            node_occupancy(nodes, loc, 2)
    
            yield self.requests[-1]
            node_occupancy(nodes, loc, 3)
            self.location.resource.release(self.requests[0])
            node_occupancy(nodes, loc, 4)
            self.requests.pop(0)
            self.location = self.destination
            self.destination = self.location.up_connection
    
            yield self.env.timeout(self.location.travel_delay)
            node_occupancy(nodes, loc, 5)
    

    Now, run the simulation with a marker for the current node:

    env.process(Occupier(env).travel(nodes[3], 3))
    env.process(Occupier(env).travel(nodes[0], 0))
    

    Look at the results and you will notice that events (request/release) occurs at the same time and the simultaneous resource time occupation is always = 0 (i.e.: the time between stages '3' and '4', for the same entity will be always 0):

    5 3 1 [1, 0, 0, 1, 0, 0, 0, 0, 0, 0]
    5 3 2 [1, 0, 0, 1, 1, 0, 0, 0, 0, 0]
    5 0 1 [1, 0, 0, 1, 1, 0, 0, 0, 0, 0]
    5 0 2 [1, 1, 0, 1, 1, 0, 0, 0, 0, 0]
    5 3 3 [1, 1, 0, 1, 1, 0, 0, 0, 0, 0]
    5 3 4 [1, 1, 0, 0, 1, 0, 0, 0, 0, 0]
    5 0 3 [1, 1, 0, 0, 1, 0, 0, 0, 0, 0]
    5 0 4 [0, 1, 0, 0, 1, 0, 0, 0, 0, 0]