def remove(self, superitem): """ Return a new layer without the given superitem """ new_spool = superitems.SuperitemPool( superitems=[s for s in self.superitems_pool if s != superitem]) new_scoords = [ c for i, c in enumerate(self.superitems_coords) if i != self.superitems_pool.get_index(superitem) ] return Layer(new_spool, new_scoords, self.pallet_dims)
def _get_new_layer(to_place): """ Place the maximum amount of items that can fit in a new layer, starting from the given pool """ assert len( to_place) > 0, "The number of superitems to place must be > 0" spool = superitems.SuperitemPool(superitems=to_place) layer = maxrects.maxrects_single_layer_online( spool, self.pallet_dims) return layer
def not_covered_superitems(self): """ Return a list of superitems which are not present in any layer """ covered_spool = superitems.SuperitemPool(superitems=None) for l in self.layers: covered_spool.extend(l.superitems_pool) return [ s for s in self.superitems_pool if covered_spool.get_index(s) is None ]
def _add_single_layers(self): """ Add one layer for each superitem that only contains that superitem """ for superitem in self.superitems_pool: self.add( Layer( superitems.SuperitemPool([superitem]), [utils.Coordinate(x=0, y=0)], self.pallet_dims, ))
def maxrects_single_layer_offline(superitems_pool, pallet_dims, superitems_in_layer=None): """ Given a superitems pool and the maximum dimensions to pack them into, try to fit each superitem in a single layer (if not possible, return an error) """ logger.debug("MR-SL-Offline starting") # Set all superitems in layer if superitems_in_layer is None: superitems_in_layer = np.arange(len(superitems_pool)) logger.debug( f"MR-SL-Offline {superitems_in_layer}/{len(superitems_pool)} superitems to place" ) # Iterate over each placement strategy ws, ds, _ = superitems_pool.get_superitems_dims() for strategy in MAXRECTS_PACKING_STRATEGIES: # Create the maxrects packing algorithm packer = newPacker( mode=PackingMode.Offline, bin_algo=PackingBin.Global, pack_algo=strategy, sort_algo=SORT_AREA, rotation=False, ) # Add one bin representing one layer packer.add_bin(pallet_dims.width, pallet_dims.depth, count=1) # Add superitems to be packed for i in superitems_in_layer: packer.add_rect(ws[i], ds[i], rid=i) # Start the packing procedure packer.pack() # Feasible packing with a single layer if len(packer) == 1 and len(packer[0]) == len(superitems_in_layer): spool = superitems.SuperitemPool( superitems=[superitems_pool[s.rid] for s in packer[0]]) layer = layers.Layer( spool, [utils.Coordinate(s.x, s.y) for s in packer[0]], pallet_dims) logger.debug( f"MR-SL-Offline generated a new layer with {len(layer)} superitems " f"and {layer.get_density(two_dims=False)} 3D density") return layer return None
def build_layer_from_model_output(superitems_pool, superitems_in_layer, solution, pallet_dims): """ Return a single layer from the given model solution (either baseline or column generation). The 'solution' parameter should be a dictionary of the form { 'c_{s}_x': ..., 'c_{s}_y: ..., ... } """ spool, scoords = [], [] for s in superitems_in_layer: spool += [superitems_pool[s]] scoords += [Coordinate(x=solution[f"c_{s}_x"], y=solution[f"c_{s}_y"])] spool = superitems.SuperitemPool(superitems=spool) return layers.Layer(spool, scoords, pallet_dims)
def get_height_groups(superitems_pool, pallet_dims, height_tol=0, density_tol=0.5): """ Divide the whole pool of superitems into groups having either the exact same height or an height within the given tolerance """ assert height_tol >= 0 and density_tol >= 0.0, "Tolerance parameters must be non-negative" # Get unique heights unique_heights = sorted(set(s.height for s in superitems_pool)) height_sets = { h: {k for k in unique_heights[i:] if k - h <= height_tol} for i, h in enumerate(unique_heights) } for (i, hi), (j, hj) in zip( list(height_sets.items())[:-1], list(height_sets.items())[1:]): if hj.issubset(hi): unique_heights.remove(j) # Generate one group of superitems for each similar height groups = [] for height in unique_heights: spool = [ s for s in superitems_pool if s.height >= height and s.height <= height + height_tol ] spool = superitems.SuperitemPool(superitems=spool) if (sum(s.volume for s in spool) >= density_tol * spool.get_max_height() * pallet_dims.width * pallet_dims.depth): groups += [spool] return groups
def maxrects_single_layer_online(superitems_pool, pallet_dims, superitems_duals=None): """ Given a superitems pool and the maximum dimensions to pack them into, try to fit the greatest number of superitems in a single layer following the given order """ logger.debug("MR-SL-Online starting") # If no duals are given use superitems' heights as a fallback ws, ds, hs = superitems_pool.get_superitems_dims() if superitems_duals is None: superitems_duals = np.array(hs) # Sort rectangles by duals indexes = utils.argsort(list(zip(superitems_duals, hs)), reverse=True) logger.debug( f"MR-SL-Online {sum(superitems_duals[i] > 0 for i in indexes)} non-zero duals to place" ) # Iterate over each placement strategy generated_layers, num_duals = [], [] for strategy in MAXRECTS_PACKING_STRATEGIES: # Create the maxrects packing algorithm packer = newPacker( mode=PackingMode.Online, pack_algo=strategy, rotation=False, ) # Add one bin representing one layer packer.add_bin(pallet_dims.width, pallet_dims.depth, count=1) # Online packing procedure n_packed, non_zero_packed, layer_height = 0, 0, 0 for i in indexes: if superitems_duals[i] > 0 or hs[i] <= layer_height: packer.add_rect(ws[i], ds[i], i) if len(packer[0]) > n_packed: n_packed = len(packer[0]) if superitems_duals[i] > 0: non_zero_packed += 1 if hs[i] > layer_height: layer_height = hs[i] num_duals += [non_zero_packed] # Build layer after packing spool, coords = [], [] for s in packer[0]: spool += [superitems_pool[s.rid]] coords += [utils.Coordinate(s.x, s.y)] layer = layers.Layer(superitems.SuperitemPool(spool), coords, pallet_dims) generated_layers += [layer] # Find the best layer by taking into account the number of # placed superitems with non-zero duals and density layer_indexes = utils.argsort( [(duals, layer.get_density(two_dims=False)) for duals, layer in zip(num_duals, generated_layers)], reverse=True, ) layer = generated_layers[layer_indexes[0]] logger.debug( f"MR-SL-Online generated a new layer with {len(layer)} superitems " f"(of which {num_duals[layer_indexes[0]]} with non-zero dual) " f"and {layer.get_density(two_dims=False)} 3D density") return layer
def maxrects_multiple_layers(superitems_pool, pallet_dims, add_single=True): """ Given a superitems pool and the maximum dimensions to pack them into, return a layer pool with warm start placements """ logger.debug("MR-ML-Offline starting") logger.debug( f"MR-ML-Offline {'used' if add_single else 'not_used'} as warm_start") logger.debug(f"MR-ML-Offline {len(superitems_pool)} superitems to place") # Return a layer with a single item if only one is present in the superitems pool if len(superitems_pool) == 1: layer_pool = layers.LayerPool(superitems_pool, pallet_dims, add_single=True) uncovered = 0 else: generated_pools = [] for strategy in MAXRECTS_PACKING_STRATEGIES: # Build initial layer pool layer_pool = layers.LayerPool(superitems_pool, pallet_dims, add_single=add_single) # Create the maxrects packing algorithm packer = newPacker( mode=PackingMode.Offline, bin_algo=PackingBin.Global, pack_algo=strategy, sort_algo=SORT_AREA, rotation=False, ) # Add an infinite number of layers (no upper bound) packer.add_bin(pallet_dims.width, pallet_dims.depth, count=float("inf")) # Add superitems to be packed ws, ds, _ = superitems_pool.get_superitems_dims() for i, (w, d) in enumerate(zip(ws, ds)): packer.add_rect(w, d, rid=i) # Start the packing procedure packer.pack() # Build a layer pool for layer in packer: spool, scoords = [], [] for superitem in layer: spool += [superitems_pool[superitem.rid]] scoords += [utils.Coordinate(superitem.x, superitem.y)] spool = superitems.SuperitemPool(superitems=spool) layer_pool.add(layers.Layer(spool, scoords, pallet_dims)) layer_pool.sort_by_densities(two_dims=False) # Add the layer pool to the list of generated pools generated_pools += [layer_pool] # Find the best layer pool by considering the number of placed superitems, # the number of generated layers and the density of each layer dense uncovered = [ len(pool.not_covered_superitems()) for pool in generated_pools ] n_layers = [len(pool) for pool in generated_pools] densities = [ pool[0].get_density(two_dims=False) for pool in generated_pools ] pool_indexes = utils.argsort(list(zip(uncovered, n_layers, densities)), reverse=True) layer_pool = generated_pools[pool_indexes[0]] uncovered = uncovered[pool_indexes[0]] logger.debug( f"MR-ML-Offline generated {len(layer_pool)} layers with 3D densities {layer_pool.get_densities(two_dims=False)}" ) logger.debug( f"MR-ML-Offline placed {len(superitems_pool) - uncovered}/{len(superitems_pool)} superitems" ) return layer_pool
def _place_not_covered(self, singles_removed=None, area_tol=1.0): """ Place the remaining items (not superitems) either on top of existing bins or in a whole new bin, if they do not fit """ def _get_unplaceable_items(superitems_list, max_spare_height): """ Return items that must be placed in a new bin """ index = len(superitems_list) for i, s in enumerate(superitems_list): if s.height > max_spare_height: index = i break return superitems_list[:index], superitems_list[index:] def _get_placeable_items(superitems_list, working_bin): """ Return items that can be placed in a new layer in the given bin """ to_place = [] for s in superitems_list: last_layer_area = working_bin.layer_pool[-1].area max_area = np.clip(area_tol * last_layer_area, 0, self.pallet_dims.area) area = sum(s.area for s in to_place) if area < max_area and s.height < working_bin.remaining_height: to_place += [s] else: break return to_place def _get_new_layer(to_place): """ Place the maximum amount of items that can fit in a new layer, starting from the given pool """ assert len( to_place) > 0, "The number of superitems to place must be > 0" spool = superitems.SuperitemPool(superitems=to_place) layer = maxrects.maxrects_single_layer_online( spool, self.pallet_dims) return layer def _place_new_layers(superitems_list, remaining_heights): """ Try to place items in the bin with the least spare height and fallback to the other open bins, if the layer doesn't fit """ sorted_indices = utils.argsort(remaining_heights) working_index = 0 while len(superitems_list) > 0 and working_index < len(self.bins): working_bin = self.bins[sorted_indices[working_index]] to_place = _get_placeable_items(superitems_list, working_bin) if len(to_place) > 0: layer = _get_new_layer(to_place) self.layer_pool.add(layer) working_bin.add(layer) for s in layer.superitems_pool: superitems_list.remove(s) else: working_index = working_index + 1 return superitems_list # Get single superitems that are not yet covered # (assuming that the superitems pool in the layer pool contains all single superitems) superitems_list = self.layer_pool.not_covered_single_superitems( singles_removed=singles_removed) # Sort superitems by ascending height superitems_list = [ superitems_list[i] for i in utils.argsort([s.height for s in superitems_list]) ] # Get placeable and unplaceable items remaining_heights = self.get_remaining_heights() max_remaining_height = 0 if len(remaining_heights) == 0 else max( remaining_heights) superitems_list, remaining_items = _get_unplaceable_items( superitems_list, max_remaining_height) superitems_list = _place_new_layers(superitems_list, remaining_heights) # Place unplaceable items in a new bin remaining_items += superitems_list if len(remaining_items) > 0: spool = superitems.SuperitemPool(superitems=remaining_items) lpool = maxrects.maxrects_multiple_layers(spool, self.pallet_dims, add_single=False) self.layer_pool.extend(lpool) self.bins += self._build(lpool)
def main( order, procedure="cg", max_iters=1, superitems_horizontal=True, superitems_horizontal_type="two-width", superitems_max_vstacked=4, density_tol=0.5, filtering_two_dims=False, filtering_max_coverage_all=3, filtering_max_coverage_single=3, tlim=None, enable_solver_output=False, height_tol=0, cg_use_height_groups=True, cg_mr_warm_start=True, cg_max_iters=100, cg_max_stag_iters=20, cg_sp_mr=False, cg_sp_np_type="mip", cg_sp_p_type="cp", cg_return_only_last=False, ): """ External interface to all the implemented solutions to solve 3D-BPP """ assert max_iters > 0, "The number of maximum iteration must be > 0" assert procedure in ("mr", "bl", "cg"), "Unsupported procedure" logger.info(f"{procedure.upper()} procedure starting") # Create the final superitems pool and a copy of the order final_layer_pool = layers.LayerPool(superitems.SuperitemPool(), config.PALLET_DIMS) working_order = order.copy() # Iterate the specified number of times in order to reduce # the number of uncovered items at each iteration not_covered, all_singles_removed = [], [] for iter in range(max_iters): logger.info(f"{procedure.upper()} iteration {iter + 1}/{max_iters}") # Create the superitems pool and call the baseline procedure superitems_list, singles_removed = superitems.SuperitemPool.gen_superitems( order=working_order, pallet_dims=config.PALLET_DIMS, max_vstacked=superitems_max_vstacked, horizontal=superitems_horizontal, horizontal_type=superitems_horizontal_type, ) superitems_pool = superitems.SuperitemPool(superitems=superitems_list) all_singles_removed += singles_removed # Call the right packing procedure if procedure == "bl": layer_pool = baseline.baseline(superitems_pool, config.PALLET_DIMS, tlim=tlim) elif procedure == "mr": layer_pool = maxrects_warm_start(superitems_pool, height_tol=height_tol, density_tol=density_tol, add_single=False) elif procedure == "cg": layer_pool = cg( superitems_pool, height_tol=height_tol, density_tol=density_tol, use_height_groups=cg_use_height_groups, mr_warm_start=cg_mr_warm_start, max_iters=cg_max_iters, max_stag_iters=cg_max_stag_iters, tlim=tlim, sp_mr=cg_sp_mr, sp_np_type=cg_sp_np_type, sp_p_type=cg_sp_p_type, return_only_last=cg_return_only_last, enable_solver_output=enable_solver_output, ) # Filter layers based on the given parameters layer_pool = layer_pool.filter_layers( min_density=density_tol, two_dims=filtering_two_dims, max_coverage_all=filtering_max_coverage_all, max_coverage_single=filtering_max_coverage_single, ) # Add only the filtered layers final_layer_pool.extend(layer_pool) # Compute the number of uncovered Items prev_not_covered = len(not_covered) item_coverage = final_layer_pool.item_coverage() not_covered = [k for k, v in item_coverage.items() if not v] logger.info( f"Items not covered: {len(not_covered)}/{len(item_coverage)}") if len(not_covered) == prev_not_covered: logger.info( "Stop iterating, no improvement from the previous iteration") break # Compute a new order composed of only not covered items working_order = order.iloc[not_covered].copy() # Build a pool of bins from the layer pool and compact # all layers in each bin to avoid having "flying" products bin_pool = bins.BinPool(final_layer_pool, config.PALLET_DIMS, singles_removed=set(all_singles_removed)) return bins.CompactBinPool(bin_pool)