Skip to content

Part Distribution

This example focuses on a specific scheduling problem in manufacturing. Components need to be distributed across multiple parallel processes by a single switch (SwitchD) in an optimal manner. Additionally, another switch (SwitchF) is responsible for collecting the outputs from these processes and sending them to a final destination. These parallel processes differ in terms of the processing times. As a result, they need a different number of parts distributed by the switch in order to work optimally.

Within a given timeframe, the maximum number of parts that a process can produce is determined by ensuring a continuous supply of components without any interruptions. If the processing times of the source and the final destination are negligible compared to those of the processes, the total number of parts produced by the system is simply the sum of the parts produced by each individual process. This provides an estimate of the maximum production capacity of the system.

What is optimized?

We optimize the distribution of the parts by the switch (SwitchD) to the stations, considering the processing times of all processing stations. In addition, collecting the parts that have been processed and can now be pushed to the final station at the end of the line needs to be optimized (SwitchF). A policy implemented at both switches, that assigns components by placing them in the least filled buffer, while prioritizing faster processes, and retrieving them from the most filled buffer seems like the best approach for this problem. We examine the case with k=5 stations. Processing times vary with ∑ik 10+10*i + exp(0.2). The reward can be maximized by optimizing the part distribution of both switches.

Optimization using Lineflow

We validated the effectiveness of Lineflow algorithms by comparing them to the rewards achieved by the greedy approach. Our optimal agent achieved rewards up to 738, as shown in the plot below. Only the policy based methods PPO and TRPO ensure a proper switch distribution. Part distrib empirical n5 reward

Verification of the optimization

As mentioned above, a greedy scheduling policy is implemented at both switches. This policy prioritizes sending components to the least filled buffer and retrieving components from the most filled buffer, ensuring an optimal distribution and therefore the maximum number of parts produced. A comparison of the component distributions between the greedy policy and the optimal distribution for 5 parallel processing cells is given in the following figure. For a detailed calculation and proof of the optimal part distribution, see (Link to Lineflow paper). Part distrib empirical n5

Code

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


def make_fast_only_policy(n_processes):
    def policy(state, env):
        return {
            'SwitchF': {'index_buffer_in': 0, 'index_buffer_out': 0},
            'SwitchD': {'index_buffer_in': 0, 'index_buffer_out': 0},
        }
    return policy


def make_greedy_policy(n_processes):
    def policy(state, env):
        # Fetch from buffer where fill is largest
        fills_prior_process = np.array(
            [state[f'Buffer_SwitchD_to_P{i}']['fill'].value for i in range(n_processes)]
        )
        fills_after_process = np.array(
            [state[f'Buffer_P{i}_to_SwitchF']['fill'].value for i in range(n_processes)]
        )
        return {
            # Fetch where fill is maximal
            'SwitchF': {
                'index_buffer_in': fills_after_process.argmax(),
                },
            # Push where fill is minimal
            'SwitchD': {
                'index_buffer_out': fills_prior_process.argmin()
            },
        }
    return policy


class MultiProcess(Line):
    """
    Assembly line with two sources a switch and a sink
    """

    def __init__(self, alternate=True, n_processes=5, **kwargs):
        self.alternate = alternate
        self.n_processes = n_processes
        super().__init__(**kwargs)

    def build(self):
        source = Source(
            name='Source',
            processing_time=2,
            actionable_magazin=False,
            actionable_waiting_time=False,
            unlimited_carriers=True,
            carrier_capacity=1,
            position=(50, 300),
            processing_std=0,
        )

        switch_d = Switch(
            'SwitchD',
            position=(200, 300),
            alternate=self.alternate,
            processing_time=1,
        )

        switch_f = Switch(
            'SwitchF',
            position=(900, 300),
            alternate=self.alternate,
            processing_time=1,
        )

        processes = []
        for i in range(self.n_processes):
            processes.append(
                Process(
                    name=f'P{i}',
                    processing_time=12+10*i,
                    position=(600, 500-100*i),
                    processing_std=0.1,
                )
            )
        sink = Sink('Sink', position=(1100, 300), processing_time=1, processing_std=0)
        switch_f.connect_to_output(sink, capacity=2, transition_time=5)
        switch_d.connect_to_input(source, capacity=2, transition_time=5)

        for process in processes:
            process.connect_to_input(switch_d, capacity=8, transition_time=7)
            process.connect_to_output(switch_f, capacity=5, transition_time=7)


if __name__ == "__main__":
    n_processes = 5
    line = MultiProcess(n_processes=n_processes, realtime=True, factor=0.1, alternate=False)
    agent = make_greedy_policy(n_processes)
    line.run(simulation_end=3000, agent=agent, visualize=True)

    print('Number of parts produced: ', line.get_n_parts_produced())