Skip to content

Worker Assignment

In many production lines, manual tasks can be completed more efficiently when multiple workers collaborate. This example seeks to identify the optimal distribution of a limited workforce across various stations, each with different processing times. In addition, it is assumed that the processing time at each station depends on the number of workers assigned. In our experiments, adding one more worker reduces the processing time by approximately 74%.

What is optimized?

The stations are linked by buffers, and the overall production speed is determined by the slowest station. Therefore, the goal is to allocate workers in such a way that the maximum processing time across all stations is minimized. This allocation process is constrained by factors such as delays caused by the movement of workers between stations. If workers are assigned in a way that maximizes the production time of the slowest station, the expected reward is also maximized.

Optimization using Lineflow

We can get the best results by using the actor-critic method A2C with an averaged reward of 278, surpassing all other methods and roughly reaching the reward optimum of 287. Worker Assignment rl

Verification of the optimization

We compare the results given by LineFlow with the theoretical optimal solution in the following paragraph. This problem can be framed as an optimization challenge, where the objective is to minimize the maximum processing time by determining the optimal worker distribution across stations. This requires solving a max-min problem, which is computationally complex due to its inherent difficulty and the need for integer partitioning. Additionally, finding the best distribution in a dynamic environment is even more challenging, as it involves accurately estimating the parameters for each station. To check the worker distribution calculated by LineFlow, all possible worker allocations are listed to evaluate them empirically. As mentioned above, the line can reach a reward of 287 with the optimal worker allocation. For a detailed calculation of the worker assignment, see (Link to Lineflow paper).

Worker Assignment optimum

Code

import numpy as np
from lineflow.simulation import (
    Line,
    Sink,
    Source,
    Assembly,
    Magazine,
    Switch,
    Process,
    WorkerPool,
)


def make_random_agent(n_assemblies):
    n_workers = n_assemblies * 3

    def shuffle_workers(state, env):
        """
        Shuffles every few seconds the workers
        """
        worker_names = [a.name for a in state.get_actions()["Pool"]]

        assignments = np.random.randint(n_assemblies, size=n_workers)

        return {
            'Pool': dict(zip(worker_names, assignments))
        }
    return shuffle_workers


class WorkerAssignment(Line):
    '''
    Assembly line with two assembly stations served by a component source
    '''
    def __init__(self, n_assemblies=8, n_carriers=20, with_rework=False, *args, **kwargs):
        self.with_rework = with_rework
        self.n_carriers = n_carriers
        self.n_assemblies = n_assemblies

        super().__init__(*args, **kwargs)

    def build(self):

        magazine = Magazine(
            'Setup',
            unlimited_carriers=False,
            carriers_in_magazine=self.n_carriers,
            position=(50, 100),
            carrier_capacity=self.n_assemblies,
            actionable_magazine=False,
        )

        pool = WorkerPool(name='Pool', n_workers=3*self.n_assemblies)

        sink = Sink(
            'EOL',
            position=(self.n_assemblies*100-50, 100),
            processing_time=4
        )

        sink.connect_to_output(magazine, capacity=6)

        # Create assemblies
        assemblies = []
        for i in range(self.n_assemblies):
            a = Assembly(
                f'A{i}',
                position=((i+1)*100-50, 300),
                processing_time=16+4*i,
                worker_pool=pool,
            )

            s = Source(
                f'SA{i}',
                position=((i+1)*100-50, 450),
                processing_time=2,
                unlimited_carriers=True,
                carrier_capacity=1,
                actionable_waiting_time=False,
            )

            a.connect_to_component_input(s, capacity=2, transition_time=4)
            assemblies.append(a)
        # connect assemblies
        magazine.connect_to_output(assemblies[0], capacity=4, transition_time=10)
        for a_prior, a_after in zip(assemblies[:-1], assemblies[1:]):
            a_prior.connect_to_output(a_after, capacity=2, transition_time=10)

        if self.with_rework:

            rework_switch = Switch("ReworkStart", alternate=True, position=(750, 300))
            rework_switch.connect_to_input(assemblies[-1], capacity=2, transition_time=10)

            distribute_switch = Switch("Distribute", alternate=True, position=(900, 400))
            distribute_switch.connect_to_input(rework_switch)

            collect_switch = Switch("Collect", alternate=True, position=(900, 100))

            for i in range(3):
                p = Process(f'R{i+1}', position=(850+i*50, 250))
                p.connect_to_input(distribute_switch, capacity=2, transition_time=2)
                p.connect_to_output(collect_switch, capacity=2, transition_time=2)

            rework_end_switch = Switch("ReworkEnd", alternate=True, position=(750, 200))
            rework_end_switch.connect_to_input(rework_switch, capacity=2, transition_time=2)
            rework_end_switch.connect_to_input(collect_switch, capacity=2, transition_time=2)
            rework_end_switch.connect_to_output(sink, capacity=2, transition_time=2)
        else:
            assemblies[-1].connect_to_output(sink, capacity=4)


if __name__ == '__main__':
    line = WorkerAssignment(with_rework=True, realtime=False, n_assemblies=7, step_size=2)

    agent = make_random_agent(7)
    line.run(simulation_end=1000, agent=agent, visualize=True, capture_screen=True)
    print(line.get_n_parts_produced())