The core aim of the analyzed algorithm is to enhance operational efficiency and reduce the costs related to picking drawers/pallets from the warehouse. This approach involves assessing the layout of materials within the warehouse and, when determining the allocation of different products within the same drawer/pallet, identifying the optimal combination of products to be stored together. In the context of the "ReDiT" project, the traditional arrangement logic based on product rotation index is expanded by attempting to consolidate products frequently purchased together into the same container. This aspect constitutes an innovation in the reference scenario, as it represents an improvement on current systems.
The practical implementation of this algorithm faces challenges related to data availability and requires the development of a Machine Learning methodology. This methodology must balance customer preferences with the product turnover index, ensuring an efficient system that can improve warehouse management in the omnichannel context proposed by the "ReDiT project." The experiment conducted with this algorithm aims to empirically evaluate the application of the "ReDiT solution," thereby contributing to the consolidation of existing scientific literature and offering practical insights into the efficacy of the proposed solution within a real-world business setting.
Allocation phase
After order generation, allocation and picking strategies and constraints are defined. For the allocation phase, the following rules are established:
Two methodologies will be discussed, with the first one involving random allocation, whilst the second one involves optimized allocation. An initial assumption is that the warehouse starts empty, and that all types of product have to be allocated with defined, random quantities as shown in
Table 1.
Before proceeding with the definition of the two methodologies, the constraints to be respected, which are used in both cases, are defined:
tray_capacity: 30 (maximum capacity of a single tray)
products_per_type: 10 (number of products per type in a single tray)
types_per_tray: 3 (number of product types in a single tray)
num_trays: 40 (number of trays)
The random product allocation logic is outlined in
Figure 5.
For each product type, in the products_to_allocate dictionary, a loop is triggered. Different variables are initialized at the beginning of the loop, such as the total number of units to be allocated for the current product type. Subsequently, the loop enters a while loop, which will continue as long as units of this product type are allocated. Within this while loop, another for loop iterates through the available trays (1 through num_trays). The algorithm calculates the number of units to be allocated for this product type in each tray. This calculation is based on several conditions, including the maximum number of units allowed for that product type, the remaining units to be allocated, and the tray’s capacity. If it is possible to allocate units within that tray, the program proceeds to distribute the units of that product type in that tray. After the allocation of products, the program updates the relevant variables. This involves reducing the total number of units to be allocated and keeping track of the last tray allocated for the following calculation. The while loop continues until all units for this product type have been allocated or until no more units can be allocated in the trays. After all units for this product type have been allocated, the loop repeats for the following product type in the products_to_allocate dictionary. In summary, the loop for each product type ensures that all product types are allocated in the trays according to the conditions specified in the algorithm.
The allocation generated by the algorythm is shown in
Figure 6
In the case of optimized allocation, subsequent to parsing the constants and quantities of products slated for allocation, a dataset comprising past orders is analyzed to discern product correlations. Thus, the algorithm takes into account the occurrence count between pairs of products within orders. The algorithm selects the tray with the fewest products allocated and space left, starting with the first product to be allocated. It then calculates the number of units to be allocated based on various criteria, such as the maximum capacity of the tray and the remaining amount of product to be allocated. If the defined conditions are met, it assigns the units to the selected tray and updates the remaining quantity accordingly. If the tray can still allocate additional products, the algorithm assigns related products based on the correlations retrieved from the file. The same criteria used for allocating the primary product are applied to the correlated products. The loop continues until all units of the selected product type have been allocated, and then it moves on to the next product type. The final configuration of products in the trays is printed at the end of the algorithm. The algorithm is schematized in
Figure 7.
The logic of the implemented code is as follows:
Initialization of Trays: An array of strings representing the trays is created using list comprehension. Each element has the form "Tray[i]", where i is the tray index. This initialization is based on the variable num_trays.
A dictionary named allocated_products is created with keys representing the trays and initially empty values (empty lists) representing the products assigned to each tray.
A while loop is used to assign products to trays until there are still products to allocate. The tray chosen for allocation is determined using the min function, which selects the tray with the fewest products already assigned.
The remaining space in the tray (space_left) is calculated, which is the maximum capacity of the tray minus the length of the list of products already assigned to the tray. The number of products to allocate (to_allocate) is then calculated as the minimum between the maximum quantity of products per type, the remaining quantity of that type of product, and the remaining space in the tray. If to_allocate is equal to the maximum quantity of products per type, the allocation is made, and the remaining quantity of that type of product is updated.
If there are correlated products defined in the correlations variable, a similar check is made to allocate correlated products if there is available space in the tray.
The final arrangement of products in the trays is printed, showing the products assigned to each tray.
A dictionary named initial_configuration is created to represent the initial configuration by copying the allocated_products dictionary.
In practice, this phase involves the creation of a list of orders and a correlations dictionary that tracks correlations between products based on their frequency of being purchased together. It utilizes an "orders" list to store all orders as lists of products and employs a "correlations" dictionary where the keys are main products, and the values are "Counter" objects. For each line in the file, all products in the order are added to the "orders" list. Then, all possible combinations of products within each order are examined using "combinations." For each combined pair of products, the correlation count is incremented in their respective Counter objects. For example, if the order contains "A, B, C, D," the combinations (A, B), (A, C), (A, D), (B, C), (B, D), and (C, D) are computed. For each product pair within the order, the count is incremented in their respective Counter objects based on the correlation principle. For instance, if the pair (A, B) is found in an order, the count for both (A, B) and (B, A) is increased by 1 in their respective Counter objects. This keeps track of the fact that products A and B have been ordered together in an order. At the end of this process, the correlations dictionary will contain all correlations between pairs of products along with the count of how many times they have been ordered together. This is useful for identifying products that are often correlated and can be allocated together to optimize the picking process or other order management activities.
The final configuration generated in the case of an optimized solution is depicted in
Figure 8, where products are arranged based on the defined correlations.
The assumption is that products commonly bought together are:
commonly\_bought\_together = [
[’A’, ’F’, ’C’],
[’D’, ’E’, ’B’],
[’G’, ’H’],
[’I’, ’J’],
[’K’, ’L’, ’M’],
[’B’, ’G’, ’H’],
[’E’, ’I’, ’M’]
]
Thus, products A, F, and C are placed together in trays 1 through 3; similarly, products D, E, and B are allocated in the same trays, and so forth.
Figure 9 depicts the final configuration, highlighting correlated products. This configuration reveals that nearly all products are paired with related items.
After defining the allocation of products in both the random and optimized solutions, the allocation times were calculated by means of an algorythm with the following logic:
Definition of he function "calculate_tray_time": this function computes the total movement time between trays, considering the round-trip time as well. If the previous tray_index is None, it indicates the first tray, and the round-trip time is calculated solely based on the tray_index. If the previous tray_index is not None, it calculates the movement time between the two trays and adds the round-trip time.
Use of defaultdict for allocation_times: defaultdict is employed to manage the allocation_times dictionary, which automatically initializes values to zero for new keys.
Iteration over products in unique_products_list: Iteration is based on the sorted list unique_products_list, which contains all the products present in the trays.
Calculation of allocation times for each product: The code iterates through the trays, and for each product, it calculates the allocation time using the calculate_tray_time function and adds the cobot time (cobot_time). Cobot_time is the time to perform the product allocation task, assumed to be carried out by a cobot. The total time for each product is then added to the allocation_times dictionary.
The variables considered in the code are "allocation_times," which is a defaultdict with values initialized to zero. It is used to track the allocation time for each product type; "unique_products" is a set used to collect all product types present in the trays. The set "unique_products" is converted into a sorted list "unique_products_list" to facilitate accessing products in order. The defined function "calculate_tray_time" takes as input tray_index (the index of the current tray) and tray_index_previous (the index of the previous tray). If tray_index_previous is None, the code returns the round-trip time for the first tray, calculated as the product of the tray index by 5. Otherwise, the code calculates the total movement time between the two trays, considering also the round-trip time. The one-way time is set to 5 if there is movement between trays, otherwise it is set to 0. For each product type, the code iterates through the trays in initial_configuration to determine if the product is present in each tray. If the product is present in a tray, the code calculates the allocation time using the calculate_tray_time function and adds the cobot time (cobot_time). The variable tray_index_previous is updated each time the tray changes. The allocation_times dictionary is updated with the allocation time for each product type, and the variable total_time with the total allocation time for all products, printing the final results.
As described above, two different configurations are generated by applying these algorithms to a randomly generated dataset.
Figure 10 shows an example of the tray 1 configuration. On the left side, the "Random Allocation" solution displays products placed sequentially in Tray 1. On the right side, the "Optimized Allocation" solution showcases a configuration where products A, C, and F are grouped in Tray 1. This optimized arrangement leverages correlations between frequently purchased products, enhancing efficiency and order fulfillment.
Picking Phase
In a vertical carousel warehouse, the picking process starts with reading the order. This step involves scanning the products specified in the order and identifying all the required trays. Subsequently, it checks the presence of the products in these trays and determines the optimal tray through an optimization function. After defining the allocation of products and calculating the time required for all products to be arranged in the warehouse, the code for product retrieval was defined. The picking algorithm remains consistent in both the configurations shown in
Figure 10, with the difference being the placement of products within the warehouse.
Figure 11 shows the flowchart of the picking algorithm.
To define the logic of the picking algorythm, the first step is to enstablish essential variables that encompass product details, tray assignments, and corresponding quantities. Thus, three maps are initialized whith the following variables:
all_products: a set of all products present in the trays.
product_to_trays: a dictionary mapping each product to the trays where it is present.
tray_to_quantities: a dictionary mapping each tray to a Counter object that tracks the quantities of each product in the tray.
Subsequently, orders are read from an external file, and the loop that iterates over each order is started. A list of trays is created for each product in the order. A Counter object (tray_ranking) is also created to keep track of how many times each tray appears. Subsequently, the function that calculates the distance between trays is defined, using the tray indices. Two variables representing the current and next trays are initialized. current_tray is modified during the execution of the algorithm. The available trays for the product are obtained, sorted based on tray ranking and distance from the current tray. The best tray is selected through the get_optimal_tray function, and the value of current_tray is updated. The quantity of the product in the tray is reduced, and the association is removed if the quantity is less than or equal to zero. The get_optimal_tray(product) function is responsible for finding the optimal tray for a given product. The available trays for the current product are obtained using the product_to_trays dictionary. If the product is not present in the warehouse, available_trays will be an empty list.
The list of available trays (available_trays) is sorted based on two key criteria:
tray_ranking: this sorts the trays in descending order according to their tray ranking (tray_ranking). Essentially, trays with higher rankings, which have been frequently selected in the past, are positioned at the top of the list.
tray_distance: in cases where there is a tie in the ranking, trays are organized based on the distance between the current tray (current_tray_force_low) and the tray under consideration (x). This preference is designed to select the tray that is closest to the current tray.
The first tray from the sorted list (ranked_trays) is assigned. If the list is empty (no available trays), optimal is set to None. Finally, the optimal tray (optimal) is assigned to the global variable current_tray. This will be the tray used as "current" for the next allocation. If no tray is available, current_tray is set to None. Thus, once the optimal tray has been determined, it moves toward the picking point (where, in a real scenario, an operator retrieves the required products). If the tray is emptied of all products, it is removed from the list of available trays. A subsequent check is performed to verify if all the products specified in the order have been picked. If additional products are to be collected, the same picking logic is applied iteratively. On the other hand, if all products in the order have been successfully picked, the process proceeds to the following order. If a product is not available, a message is printed.
After defining the logic for product retrieval, the retrieval time is calculated. The first step of the algorythm is the initialization of the necessary variables:
total_time: A variable to track the total time spent in order picking.
current_tray: The current tray from which the picking sequence starts.
visited_trays: A set to keep track of the trays already visited during picking.
sequence: A list to record the picking sequence, containing tuples with information about the tray and associated products.
Next, a loop is defined that continues as long as there are trays (tray_to_products) from which to pick products. The next tray (next_tray) from which picking initiates is selected. The choice is based on the distance between the current tray and the remaining trays. Subsequently, the time to move between the current tray and the next tray (time_to_move) and the time to pick the products from the next tray (time_to_pick) are calculated. The total_time variable is then updated with the time taken to move and the time taken to pick, current_tray is updated with the next tray and visited_trays is updated with the next visited tray. The next tray is removed from tray_to_products since it has been visited. Then, the total picking time is printed for the current order, signaling whether all products have been successfully retrieved. The total picking time for the current order is added to the total picking time across all orders, which is then finally printed.