resourcessimpyanyof

Simpy resource never available after first request-release


I want to simulate a factory where a list of operations needs to be executed. There are two machines that can execute the steps, but the execution times of each of them is different.

My problem is that machine_a is only used once, and then it never becomes available again, even though I can see there are no users.

The code includes all data, so you can easily run it.

import simpy
import numpy as np
import pandas as pd

data = {
    'operation_id': ['part_A_1', 'part_A_1', 'part_A_2', 'part_A_2', 'part_A_3', 'part_A_3', 
                      'part_B_1', 'part_B_1', 'part_B_2', 'part_B_2', 'part_C_1', 'part_C_1', 
                      'part_D_1', 'part_D_1', 'part_D_2', 'part_D_2'],
    'workstation_id': ['machine_a', 'machine_b', 'machine_a', 'machine_b', 'machine_a', 'machine_b', 
                       'machine_a', 'machine_b', 'machine_a', 'machine_b', 'machine_a', 'machine_b', 
                       'machine_a', 'machine_b', 'machine_a', 'machine_b'],
    'processing_time': [1.0, 1.3, 0.8, 1.1, 1.0, 1.2, 1.3, 1.0, 1.1, 3.0, 3.3, 1.0, 5.1, 9.5, 10.3, 5.0]
}

def process_order(env, workstations, operations_schedule):

  while len(operations_schedule) > 0:

    # Parts will be processed in order
    component_to_process = operations_schedule[0]

    # Request all machines
    requests = {name:resource.request() for name, resource in workstations.items()}
 
    # Proceed when any of them is available
    accepted_requests = yield env.any_of(requests.values())
  
    # get the request dictionary containing only accepted requests
    available_resources_names = [name for name, request in requests.items() if request in accepted_requests]
    available_resources = {name:request for name, request in requests.items()if request in accepted_requests}

    # For this case, we always select the first machine in the request list:
    selected_machine = available_resources_names[0]

    # if more than one machine is available, seize only the first one in the list and release the rest
    if len(available_resources) > 1:

      # Release all resources after the first one
      for i in range(1, len(available_resources)):
        request_name = available_resources_names[i] # this contains the name of the ith requested machine
        request_object = available_resources[request_name]
        workstations[request_name].release(request_object)

    # Now that we know the workstation, we can calculate the processing time with random sampling
    print(f"Component {component_to_process} starts processing at {env.now:.2f} at {selected_machine}")

    processing_time = processing_times[(processing_times['operation_id'] == component_to_process) & \
                                              (processing_times['workstation_id'] == selected_machine)]\
                                                ['processing_time'].item()

    # launch machinning process
    env.process(machine_process(env = env,
                                machine = selected_machine,
                                processing_time = processing_time,
                                component_to_process=component_to_process,
                                request_to_release = available_resources[selected_machine]))

    operations_schedule.pop(0)


# Process a single part
def machine_process(env, machine, processing_time, component_to_process, request_to_release):

  # we wait for machine_a to be available
  yield env.timeout(processing_time)
  print(f"{component_to_process} finised at time {env.now:.2f} at {machine}")

  # release the machine once it's processed
  workstations[machine].release(request_to_release)


processing_times = pd.DataFrame(data)
operations_schedule = processing_times['operation_id'].drop_duplicates().tolist()
env = simpy.Environment()
workstations = {'machine_a': simpy.Resource(env, capacity=1), 'machine_b': simpy.Resource(env, capacity=1)}
env.process(process_order(env = env, workstations = workstations, operations_schedule = operations_schedule))
env.run()

This is the output I get:

Component part_A_1 starts processing at 0.00 at machine_a
Component part_A_2 starts processing at 0.00 at machine_b
part_A_1 finised at time 1.00 at machine_a
part_A_2 finised at time 1.10 at machine_b
Component part_A_3 starts processing at 1.10 at machine_b
part_A_3 finised at time 2.30 at machine_b
Component part_B_1 starts processing at 2.30 at machine_b
part_B_1 finised at time 3.30 at machine_b
Component part_B_2 starts processing at 3.30 at machine_b
part_B_2 finised at time 6.30 at machine_b
Component part_C_1 starts processing at 6.30 at machine_b
part_C_1 finised at time 7.30 at machine_b
Component part_D_1 starts processing at 7.30 at machine_b
part_D_1 finised at time 16.80 at machine_b
Component part_D_2 starts processing at 16.80 at machine_b
part_D_2 finised at time 21.80 at machine_b

If you see, machine_a becomes available at time 1.00, but nothing else happens until machine_b is available

I checked the queue on the resource, and it seems to be growing on machine_a, releasing the request sometimes has an effect, making the queue shorter, but it does not help.

If I try to force machine_a for a later iteration, the run ends at that point.

I checked the users of machine_a and it's empty after the resource is released.


Solution

  • You got the use case when more then one resource is selected, but when only one requests get selected you still need to cancel the other request or it will eventually seize the resources but then never release it.

    Here is a fix

    import simpy
    import numpy as np
    import pandas as pd
    
    data = {
        'operation_id': ['part_A_1', 'part_A_1', 'part_A_2', 'part_A_2', 'part_A_3', 'part_A_3', 
                          'part_B_1', 'part_B_1', 'part_B_2', 'part_B_2', 'part_C_1', 'part_C_1', 
                          'part_D_1', 'part_D_1', 'part_D_2', 'part_D_2'],
        'workstation_id': ['machine_a', 'machine_b', 'machine_a', 'machine_b', 'machine_a', 'machine_b', 
                           'machine_a', 'machine_b', 'machine_a', 'machine_b', 'machine_a', 'machine_b', 
                           'machine_a', 'machine_b', 'machine_a', 'machine_b'],
        'processing_time': [1.0, 1.3, 0.8, 1.1, 1.0, 1.2, 1.3, 1.0, 1.1, 3.0, 3.3, 1.0, 5.1, 9.5, 10.3, 5.0]
    }
    
    def process_order(env, workstations, operations_schedule):
    
      while len(operations_schedule) > 0:
    
        # Parts will be processed in order
        component_to_process = operations_schedule[0]
    
        # Request all machines
        requests = {name:resource.request() for name, resource in workstations.items()}
     
        # Proceed when any of them is available
        accepted_requests = yield env.any_of(requests.values())
    
        #------------------------------------- fix ----------------------------------
        # need to cancel requests that are still open
        open_requests = [request for name, request in requests.items() if request not in accepted_requests]
        for request in open_requests:
          request.cancel()
      
        # get the request dictionary containing only accepted requests
        available_resources_names = [name for name, request in requests.items() if request in accepted_requests]
        available_resources = {name:request for name, request in requests.items()if request in accepted_requests}
    
        # For this case, we always select the first machine in the request list:
        selected_machine = available_resources_names[0]
    
        # if more than one machine is available, seize only the first one in the list and release the rest
        if len(available_resources) > 1:
    
          # Release all resources after the first one
          for i in range(1, len(available_resources)):
            request_name = available_resources_names[i] # this contains the name of the ith requested machine
            request_object = available_resources[request_name]
            workstations[request_name].release(request_object)
    
        # Now that we know the workstation, we can calculate the processing time with random sampling
        print(f"Component {component_to_process} starts processing at {env.now:.2f} at {selected_machine}")
    
        processing_time = processing_times[(processing_times['operation_id'] == component_to_process) & \
                                                  (processing_times['workstation_id'] == selected_machine)]\
                                                    ['processing_time'].item()
    
        # launch machinning process
        env.process(machine_process(env = env,
                                    machine = selected_machine,
                                    processing_time = processing_time,
                                    component_to_process=component_to_process,
                                    request_to_release = available_resources[selected_machine]))
    
        operations_schedule.pop(0)
    
    
    # Process a single part
    def machine_process(env, machine, processing_time, component_to_process, request_to_release):
    
      # we wait for machine_a to be available
      yield env.timeout(processing_time)
      print(f"{component_to_process} finised at time {env.now:.2f} at {machine}")
    
      # release the machine once it's processed
      workstations[machine].release(request_to_release)
    
    
    processing_times = pd.DataFrame(data)
    operations_schedule = processing_times['operation_id'].drop_duplicates().tolist()
    env = simpy.Environment()
    workstations = {'machine_a': simpy.Resource(env, capacity=1), 'machine_b': simpy.Resource(env, capacity=1)}
    env.process(process_order(env = env, workstations = workstations, operations_schedule = operations_schedule))
    env.run()