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.
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).
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())