'How would I code a simulation of 2 machines with a buffer using simpy?

I am new to simulation and am going through the simpy documentation. I kind of get the gist of it but cannot really seem to grasp how to translate the production line that I want to simulate.

I am trying to simulate a production line with m number of machines and m-1 number of buffers.

The machines basically work the same:

  • Processes units at speed s per state
  • random distribution of states and time in said state
  • Is there starvation/blockage from the buffers?

The buffers all work the same:

  • Receives units from previous machine
  • Loses units from next machine
  • capacity(t) = capacity(t-1) + (input previous machine) - (output next machine)
  • Capacity(t) <= Max capacity

Now I get that the first steps of simulation are baby steps so I need to make a simple model first. What I am looking to create then using simpy is a 2 machine and 1 buffer system with fixed processing speeds, failure rate and maintenance speeds:

  1. An unlimited capacity of units is delivered to machine 1.
  2. Machine 1 processes at speed s, fails after f minutes and is repaired after r minutes. If buffer is full then Machine 1 stops.
  3. Buffer gets filled by Machine 1 and emptied by Machine 2. There is a max capacity.
  4. Machine 2 processes at min(speed s, capacity buffer) fails after f minutes and is repaired after r minutes. If buffer is empty then Machine 2 stops.
  5. Unlimited buffer capacity after Machine 2.

EDIT: The answer I received from @Michael works very well, I tried playing around with failures and maintenance. The machine seems to fail and repaired, but keeps failing at multiples of the time to failure (which I need to fix). The code I am using is as follows:

# Machine 1
speed_1 = 2          # Avg. processing time of Machine 1 in minutes
# speed_1_stdev = 0.6  # St. dev. of processing time of Machine 1
MTTF_1 = 10          # Mean time to failure Machine 1
# fail_1 = 1/MTTF_1    # Parameter for exp. distribution
repair_1 = 3         # Time it takes to repair Machine 1

# Machine 2
speed_2 = 3          # Processing time of Machine 2 in minutes
# speed_2_stdev = 0.6  # St. dev. of processing time of Machine 2
MTTF_2 = 7           # Mean time to failure Machine 1
# fail_2 = 1/MTTF_2    # Parameter for exp. distribution
repair_2 = 4         # Time it takes to repair Machine 2

# Simulation time
time = 120           # Sim time in minutes

#---------------------------------------------------------------------
# Class setup for a Machine
class Machine(object):
    """
    A machine produces units at a fixed processing speed, 
    takes units from a store before and puts units into a store after.

    Machine has a *name*, a processing speed *speed*, a preceeding buffer *in_q*,
    and a proceeding buffer *out_q*.
    
    Next steps: 
    - Machine produces units at distributed processing speeds.
    - A machine fails at fixed intervals and is repaired at a fixed time.
    - Failure and repair times are distributed.
    """
    def __init__(self, env, name, in_q, out_q, speed, mttf, repair):
        self.env = env
        self.name = name
        self.in_q = in_q
        self.out_q = out_q
        self.speed = speed
        self.mttf = mttf
        self.repair = repair
        self.broken = False

        # Start the producing process
        self.process = env.process(self.produce())
        # Start the failure process
        env.process(self.fail_machine())
    
    def produce(self):
        """
        Produce parts as long as the simulation runs.
        """
        while True:
            part = yield self.in_q.get()
            try:
                # If want to see time {self.env.now:.2f} 
                print(f'{self.name} has got a part')

                yield env.timeout(self.speed)
                if len(self.out_q.items) < self.out_q.capacity:
                    print(f'{self.name} finish a part next buffer has {len(self.out_q.items)} and capacity of {self.out_q.capacity}')
                else:
                    print(f'{self.env.now:.2f}  {self.name} output buffer full!!!')

                yield self.out_q.put(part)
                print(f'{self.name} pushed part to next buffer')
            
            except simpy.Interrupt:
                self.broken = True
                yield self.env.timeout(self.repair)
                print(f'{self.env.now:.2f} {self.name} is in fixed')
                self.broken = False
        
    def fail_machine(self):
        """
        The machine is prone to break down every now and then.
        """
        while True:
            yield self.env.timeout(self.mttf)
            print(f'{self.env.now:.2f} {self.name} is in failure.')
            if not self.broken:
                # Machine only fails if currently working.
                self.process.interrupt(self.mttf)
#---------------------------------------------------------------------
# Generating the arrival of parts in the entry buffer to be used by machine 1
def gen_arrivals(env, entry_buffer):
    """
    Start the process for each part by putting
    the part in the starting buffer
    """
    while True:
        yield env.timeout(random.uniform(0,0.001))
        # print(f'{env.now:.2f} part has arrived')
        part = object() # Too lazy to make a real part class, also isn't necessary

        yield entry_buffer.put(part)

#---------------------------------------------------------------------
# Create environment and start the setup process
env = simpy.Environment()
bufferStart = simpy.Store(env)  # Buffer with unlimited capacity
buffer1 = simpy.Store(env, capacity = 8) # Buffer between machines with limited capacity
bufferEnd = simpy.Store(env)  # Last buffer with unlimited capacity

# The machines __init__ starts the machine process so no env.process() is needed here
machine_1 = Machine(env, 'Machine 1', bufferStart, buffer1, speed_1, MTTF_1, repair_1)
machine_2 = Machine(env, 'Machine 2', buffer1, bufferEnd, speed_2, MTTF_2, repair_2)

env.process(gen_arrivals(env, bufferStart))

# Execute
env.run(until = time)


Solution 1:[1]

Got bored of my work, so I made some small changes to fix your code. I think it even works. One final note, more then one machine can use the same buffer. so a pool of machines can process out of the same one buffer. This is what I do instead of using a simpy.resurce for a resource pool

import simpy
import random

speed_1 = 3          # Avg. processing time of Machine 1 in minutes
speed_2 = 4          # Processing time of Machine 2 in minutes
time = 120           # Sim time in minutes


# Class setup for Machine 1 
# This needs to be done as there are varying processing speeds
class Machine(object):
    """
    A machine produces units at a fixed processing speed, 
    takes units from a store before and puts units into a store after.

    Machine has a *name*
    Next steps: 
    - Machine produces units at distributed processing speeds.
    - A machine fails at fixed intervals and is repaired at a fixed time.
    - Failure and repair times are distributed.
    
    """
    def __init__(self, env, name, in_q, out_q, speed):
        self.env = env
        self.name = name
        self.in_q = in_q
        self.out_q = out_q
        self.speed = speed

        # Start the producing process
        self.process = env.process(self.produce())
    
    def produce(self):
        """
        Produce parts as long as the simulation runs.
        
        """
        while True:
            part = yield self.in_q.get()
            print(f'{self.env.now:.2f} Machine {self.name} has got a part')
            
            yield env.timeout(self.speed)
            print(f'{self.env.now:.2f} Machine {self.name} finish a part next q has {len(self.out_q.items)} and capacit of {self.out_q.capacity}')

            yield self.out_q.put(part)
            print(f'{self.env.now:.2f} Machine {self.name} pushed part to next buffer')

def gen_arrivals(env, entry_buffer):
    """
    start the process for each part by putting
    the part in the starting buffer
    """

    while True:

        yield env.timeout(random.uniform(1,4))
        print(f'{env.now:.2f} part has arrived')
        part = object() # too lazy to make a real part class

        yield entry_buffer.put(part)




# Create environment and start the setup process
env = simpy.Environment()
bufferStart = simpy.Store(env)  # Buffer with unlimited capacity
buffer1 = simpy.Store(env, capacity = 6) # Buffer between machines with limited capacity
bufferEnd = simpy.Store(env)  # Last buffer with unlimited capacity

# the machines __init__ starts the machine process so no env.process() is needed here
machine_1 = Machine(env, 'Machine 1', bufferStart, buffer1, speed_1)
machine_2 = Machine(env, 'Machine 2', buffer1, bufferEnd, speed_2)
#machine_3 = Machine(env, 'Machine 3', buffer1, bufferEnd, speed_2)

env.process(gen_arrivals(env, bufferStart))


# Execute
env.run(until = time)

Solution 2:[2]

I use simmpy.Store for buffers. you can set a max capacity for a store, I think the default is infinite. When you do a yield my_store.put() it will block if the store is at capacity. Also when you do a yield my_store.get(), it will block if the store is empty.

You will need to make a machine class. for the first version I would skip the breakdown. The machine should have a process with a infinite loop that does a get() from its input buffer, delay some time (yield env.timeout()) and then does a put() to put the part into the next machine's input buffer. The first version should look like: a process that generate arriving parts that are put into machine 1 input buffer, a machine 1 that pulls (get()) from its input buffer and pushes (put()) to machine's 2 input buffer, a machine 2 that pulls (get()) from its input buffer and pushes (put()) to a exit buffer buffer. So thats 1 generate arrivals, 2 machines (can be the same class) and 3 simpy.Store s

give it a try. If you still need a example code, I try to to it this weekend. Got real job stuff to do today and next.

good luck

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Michael
Solution 2 Michael