Search code examples
pythonnetwork-programmingsimulationcommunicationsimpy

Simpy - How to let item wait for some arbitrary time before get method called?


I want to simulate a kind of communication process where the source constantly generates a message and send it to sink, while the message has to cover some distance before arrive at the destination. However, the distance may change over time, so the duration in the cable of each message is not the same. My first approach is to use env.timeout(t_i), where t_i is a time of ith message in the cable. My code is looking like this,

import simpy
import random

class network:

    def __init__(self) -> None:
        self.env = simpy.Environment()
        self.cable = simpy.Store(self.env)

    def generateMessage(self):
        
        i = 0
        while True:

            message = (f'message: {i}', f'travel time {0.1 + round(random.random(), 3)}', (round(random.random(), 3)))
            i += 1
            print(f'gen at: {self.env.now} {message}')
            yield self.env.timeout(0.1)
            self.cable.put(message)

    def receieveMessage(self):

        while True:

            # wait for message to travel in the cable for 'sometime' before arrive
            
            message = yield self.cable.get()
            yield self.env.timeout(message[2]) #<- This is not the right approach

            print(f'receieve at {self.env.now}, message: {message[:2]}')


    def run(self):

        self.env.process(self.generateMessage())
        self.env.process(self.receieveMessage())

        self.env.run(until=1)

q = network()
q.run()

Where the output is

gen at: 0 ('message: 0', 'travel time 0.22', 0.22)
gen at: 0.1 ('message: 1', 'travel time 0.71', 0.71)
gen at: 0.2 ('message: 2', 'travel time 0.377', 0.377)
gen at: 0.30000000000000004 ('message: 3', 'travel time 1.056', 1.056)
receieve at 0.32, message: ('message: 0', 'travel time 0.22')
gen at: 0.4 ('message: 4', 'travel time 0.138', 0.138)
gen at: 0.5 ('message: 5', 'travel time 0.45299999999999996', 0.45299999999999996)
gen at: 0.6 ('message: 6', 'travel time 1.024', 1.024)
gen at: 0.7 ('message: 7', 'travel time 0.695', 0.695)
gen at: 0.7999999999999999 ('message: 8', 'travel time 0.503', 0.503)
gen at: 0.8999999999999999 ('message: 9', 'travel time 0.702', 0.702)
gen at: 0.9999999999999999 ('message: 10', 'travel time 0.75', 0.75)

This is not what I want, because in this approach, each loop of receieveMessage method wait for the travel_time before next iteration instead of a message delay for travel_time before get by receieveMessage method.

The desired output should be something like this,

gen at: 0 ('message: 0', 'travel time 0.207')
gen at: 0.1 ('message: 1', 'travel time 0.735')
receieve at 0.207, message: ('message: 0', 'travel time 0.207')
gen at: 0.2 ('message: 2', 'travel time 0.498')
gen at: 0.3 ('message: 3', 'travel time 0.492')
gen at: 0.4 ('message: 4', 'travel time 0.864')
gen at: 0.5 ('message: 5', 'travel time 0.241')
gen at: 0.6 ('message: 6', 'travel time 0.505')
receieve at 0.698, message: ('message: 2', 'travel time 0.498')
gen at: 0.7 ('message: 7', 'travel time 0.76')
receieve at 0.741, message: ('message: 5', 'travel time 0.241')
receieve at 0.792, message: ('message: 3', 'travel time 0.492')
gen at: 0.8 ('message: 8', 'travel time 0.815')
receieve at 0.835, message: ('message: 1', 'travel time 0.735')
gen at: 0.9 ('message: 9', 'travel time 0.104')
gen at: 1 ('message: 10', 'travel time 0.524')

# Message of generated message which should not print out be cause the util arg.
receieve at 1.004, message: ('message: 9', 'travel time 0.104')
receieve at 1.105, message: ('message: 6', 'travel time 0.505')
receieve at 1.264, message: ('message: 4', 'travel time 0.864')
receieve at 1.46, message: ('message: 7', 'travel time 0.76')
receieve at 1.524, message: ('message: 10', 'travel time 0.524')
receieve at 1.615, message: ('message: 8', 'travel time 0.815')


How should I achieve this? I'm thinking of introducing another process for each message but that seems like it can only be an idea.


Solution

  • I think simpy is more useful if you can send more then one message at a time. I also think you are on the right track when you suggested creating another process.

    I separated the generator from the send process so the messages can compete for a common resource (the channel) as a independent processes. Up the channels to see how messages send time stamps overlap

    """
    quick sim to send messages over a network chanel
    
    programmer: Michael R. Gibbs
    """
    
    import simpy
    import random
    
    class Network():
    
        def __init__(self, env, num_of_chanels):
            """
            The num_of_chanels limits how many messages
            can be "sending" at one time.
            If all the chanels are busy, then new messages queue up
            for next available chanel
            """
            self.env = env
            self.num_of_chanels = num_of_chanels
    
            self.chanels = simpy.Resource(env, num_of_chanels)
    
        def send_mess(self, mess):
            """
            sim process for sending a message
            All the send mess process queue up for a channel resource
            Then with the channel 'sends' the messaage
            The release the channel for the next senm mess process in the queue
    
            mess is a tuple of (id, message text, send time)
            """
    
            print(f'{self.env.now}: - message: {mess[0]} being queued')
    
            with self.chanels.request() as req:
                yield req
    
                print(f'{self.env.now}: - message: {mess[0]} being sent')
    
                yield self.env.timeout(mess[2])
    
                print(f'{self.env.now}: - message: {mess[0]} has been sent {mess[1]}')
    
    
    def gen_messages(env, network):
        """
        Generates a series of messages with a random send time
    
        """
    
        id = 1
    
        while True:
            # change the randint params to stress the queue more
            yield env.timeout(random.randint(1,3))
    
            # (id, mess text, send time)
            mess = (id, 'mess ' + str(id), random.randint(1,4))
            id += 1
    
            # no yield here, just drop and go
            env.process(network.send_mess(mess))
    
    # create and start sim
    env = simpy.Environment()
    network = Network(env,1) # start with just one chanel
    
    env.process(gen_messages(env, network))
    env.run(50)