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.

Answer

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)