Search code examples
pythonsimpy

Simpy resource capacity management


I'm developing a discrete even simulator for a very long, multiply re-entrant manufacturing process (parts go through a number of tools in a certain sequence, and often come back to the same tool multiple times). The overall flow of the program is working nicely and give the expected queue depths, congestion points etc, under various build plan scenarios. The challenge I have not yet found a way to resolve is that of a tool with capacity >1 which may run ,multiple parts at a time but it must start them all at the same time (i.e., they may run in a chamber or a bath for example that cannot be opened/accessed when one part is running to add another).

Thus, I'm looking for a way to implement a tool with, say capacity=4, so that at the start of a run if there are 4 or more items in its queue, it'll load 4 of them and run them, but if there is only one part at time zero, that one part runs and anything that comes into the queue while that one part is running has to wait until the run is over.

My code is rather too long and complicated to illustrate the problem, well but the problem is well-described by the famous simpy "fueling station" example (code below). The behavior of that I'm trying to get rid of is expressed in this code. That is, the fuel station has capacity = 2, a car comes up and takes one of the slots, then some time later another car arrives and takes the remaining slot. That's great for gas pumps but I'm trying to block later users from gaining access once a run is launched.

I could envision giving the tool a property like self.status and set status to 'busy' or some such thing when the tool is in use, or perhaps use self.res.users in some way to do that but I wonder if there is some more natively simpy way of getting my tools to behave in the desired way.

Thanks!

import itertools
import random

import simpy


RANDOM_SEED = 42
GAS_STATION_SIZE = 200     # liters
THRESHOLD = 10             # Threshold for calling the tank truck (in %)
FUEL_TANK_SIZE = 50        # liters
FUEL_TANK_LEVEL = [5, 25]  # Min/max levels of fuel tanks (in liters)
REFUELING_SPEED = 2        # liters / second
TANK_TRUCK_TIME = 300      # Seconds it takes the tank truck to arrive
T_INTER = [30, 300]        # Create a car every [min, max] seconds
SIM_TIME = 1000            # Simulation time in seconds


def car(name, env, gas_station, fuel_pump):
    """A car arrives at the gas station for refueling.

    It requests one of the gas station's fuel pumps and tries to get the
    desired amount of gas from it. If the stations reservoir is
    depleted, the car has to wait for the tank truck to arrive.

    """
    fuel_tank_level = random.randint(*FUEL_TANK_LEVEL)
    print('%s arriving at gas station at %.1f' % (name, env.now))
    with gas_station.request() as req:
        start = env.now
        # Request one of the gas pumps
        yield req

        # Get the required amount of fuel
        liters_required = FUEL_TANK_SIZE - fuel_tank_level
        yield fuel_pump.get(liters_required)

        # The "actual" refueling process takes some time
        yield env.timeout(liters_required / REFUELING_SPEED)

        print('%s finished refueling in %.1f seconds.' % (name,
                                                          env.now - start))


def gas_station_control(env, fuel_pump):
    """Periodically check the level of the *fuel_pump* and call the tank
    truck if the level falls below a threshold."""
    while True:
        if fuel_pump.level / fuel_pump.capacity * 100 < THRESHOLD:
            # We need to call the tank truck now!
            print('Calling tank truck at %d' % env.now)
            # Wait for the tank truck to arrive and refuel the station
            yield env.process(tank_truck(env, fuel_pump))

        yield env.timeout(10)  # Check every 10 seconds


def tank_truck(env, fuel_pump):
    """Arrives at the gas station after a certain delay and refuels it."""
    yield env.timeout(TANK_TRUCK_TIME)
    print('Tank truck arriving at time %d' % env.now)
    amount = fuel_pump.capacity - fuel_pump.level
    print('Tank truck refuelling %.1f liters.' % amount)
    yield fuel_pump.put(amount)


def car_generator(env, gas_station, fuel_pump):
    """Generate new cars that arrive at the gas station."""
    for i in itertools.count():
        yield env.timeout(random.randint(*T_INTER))
        env.process(car('Car %d' % i, env, gas_station, fuel_pump))


# Setup and start the simulation
print('Gas Station refuelling')
random.seed(RANDOM_SEED)

# Create environment and start processes
env = simpy.Environment()
gas_station = simpy.Resource(env, 2)
fuel_pump = simpy.Container(env, GAS_STATION_SIZE, init=GAS_STATION_SIZE)
env.process(gas_station_control(env, fuel_pump))
env.process(car_generator(env, gas_station, fuel_pump))

# Execute!
env.run(until=SIM_TIME)

Solution

  • So it seems to me each car grabs a pump, but what you really want is a pump to grab up to two cars at a time. So you need a process that does the batching. Here is a quick and dirty batch processor. I use a store for the input queue, and when every my yield get() gets a first entity, I check the queue for up to capacity more entities to process, What this yield really does is cause my process to wait when the queue is empty

    """
        A simple example of a process that process entities in batches
    
        Programmer: Michael R. Gibbs
    """
    
    import simpy
    import random
    
    def batch_process(env, ent_q, next_q, max_cap):
        """
            grabs up to max_cap of entites and processes them
        """
    
        while True:
    
            # use a yield to wait when queue is empty 
            ent_1 = yield ent_q.get()
    
            # grabs up to capacity -1 more
            p_batch = [ent_1] + ent_q.items[:(max_cap - 1)]
            ent_q.items = ent_q.items[(max_cap - 1):]
    
            print(f'{env.now:.2f} grabbed {len(p_batch)} entites leaving {len(ent_q.items)} left in queue')
    
            # do the processing
            yield env.timeout(10)
    
            # send entitles to next stop
            next_q.items = next_q.items + p_batch
    
    def gen_ents(env, q):
        """
            Generate the arrival of entities
    
            arrival profile starts slow
            has a peek
            then nothing which drains the queue
            and puts the process in a wait mode
            another peek
    
        """
        for _ in range(10):
            yield env.timeout(random.uniform(1,6))
            q.put(object())
        for _ in range(15):
            yield env.timeout(random.uniform(1,3))
            q.put(object())
        yield env.timeout(50)
        for _ in range(15):
            yield env.timeout(random.uniform(1,3))
            q.put(object())
        for _ in range(10):
            yield env.timeout(random.uniform(1,7))
            q.put(object())
    
    
    # boot up model
    random.seed(100)
    env = simpy.Environment()
    
    process_q = simpy.Store(env)
    exit_q = simpy.Store(env)
    
    env.process(gen_ents(env, process_q))
    env.process(batch_process(env, process_q, exit_q, 4))
    
    env.run(1000)
    print(f'Simulation has finish a time {env.now}')