diff --git a/LICENSE b/LICENSE index d3dcf03..39a03e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Hsu Ruei-Chang (jerry800416) +Copyright (c) 2024 Riccardo Ravello Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Profiling.bat b/Profiling.bat new file mode 100644 index 0000000..27d0df6 --- /dev/null +++ b/Profiling.bat @@ -0,0 +1,2 @@ +cd C:\Users\Riccardo Ravello\OneDrive - CentraleSupelec\Bureau\3D-bin-packing>snakeviz profilazione.prof +snakeviz profilazione.prof \ No newline at end of file diff --git a/Script.py b/Script.py new file mode 100644 index 0000000..e20d2e9 --- /dev/null +++ b/Script.py @@ -0,0 +1,594 @@ +import random +import time +import cProfile +import pstats +import logging +import sys +from py3dbp.main import Packer, Bin, Item, Painter, set_external_logger # Import from external module + + +def setup_logger(name, level=logging.DEBUG): + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + + logger = logging.getLogger(name) + logger.setLevel(level) + logger.addHandler(handler) + + return logger + +logger = setup_logger(__name__) + +logger.info('Logger successfully configured in the main module.') + +# Pass the logger to the external module +set_external_logger(logger) + +# Create an instance of the profiler +# profiler = cProfile.Profile() +# Enable the profiler, execute the function, and disable the profiler +# profiler.enable() + +pallet_types = { + "Heavy": { + "Quarter Pallet": {"Max Length": 120, "Max Width": 100, "Max Height": 100, "Max Weight": 300, "Priority_pallet_choice": 2, "Color": "red"}, + "Half Pallet": {"Max Length": 120, "Max Width": 100, "Max Height": 150, "Max Weight": 600, "Priority_pallet_choice": 5, "Color": "orange"}, + "Full Pallet": {"Max Length": 120, "Max Width": 100, "Max Height": 240, "Max Weight": 1200, "Priority_pallet_choice": 7, "Color": "yellow"}, + }, + "Light": { + "Light Pallet": {"Max Length": 120, "Max Width": 100, "Max Height": 240, "Max Weight": 750, "Priority_pallet_choice": 6, "Color": "green"}, + "Extra Light Pallet": {"Max Length": 120, "Max Width": 100, "Max Height": 150, "Max Weight": 450, "Priority_pallet_choice": 3, "Color": "olive"}, + "Ultra Light Pallet": {"Max Length": 120, "Max Width": 100, "Max Height": 240, "Max Weight": 350, "Priority_pallet_choice": 4, "Color": "blue"}, + "Mini Quarter": {"Max Length": 120, "Max Width": 100, "Max Height": 60, "Max Weight": 150, "Priority_pallet_choice": 1, "Color": "pink"}, + }, + "Oversized": { + "Custom Pallet": {"Max Length": 1300, "Max Width": 240, "Max Height": 240, "Max Weight": 22000, "Priority_pallet_choice": 8, "Color": "brown"}, + } +} + +pallet_dimensions = { + "EUR": {"Length": 120, "Width": 80, "Height": 15, "Weight": 25}, + "ISO": {"Length": 120, "Width": 100, "Height": 15, "Weight": 28}, + "120x120": {"Length": 120, "Width": 120, "Height": 15, "Weight": 30}, +} + +all_subtypes = [] +for type_, subtype in pallet_types.items(): + for subtype_name, dimensions in subtype.items(): + all_subtypes.append((type_, subtype_name, dimensions)) + +sorted_all_types = sorted(all_subtypes, key=lambda x: x[2]["Priority_pallet_choice"], reverse=True) + +min_possible_height = sorted(pallet_dimensions.values(), key=lambda x: x["Height"])[0]["Height"] +# print(min_possible_height) +min_possible_weight = sorted(pallet_dimensions.values(), key=lambda x: x["Weight"])[0]["Weight"] +# print(min_possible_weight) + +class Pallet: + def __init__(self, type_): + self.type = type_ + self.length = pallet_dimensions[type_]["Length"] + self.width = pallet_dimensions[type_]["Width"] + self.height = pallet_dimensions[type_]["Height"] + self.weight = pallet_dimensions[type_]["Weight"] + + +class Package: + def __init__(self, length=120, width=80, height=240, weight=1200, pallet=None, name="ERROR: No Name"): + self.pallet = pallet + # print(height, {self.pallet.height if self.pallet is not None else 0}) + self.height = height - (self.pallet.height if self.pallet is not None else 0) + self.weight = weight - (self.pallet.weight if self.pallet is not None else 0) + self.length, self.width = max(length, width), min(length, width) + self.volume = self.length * self.width * self.height + self.density = self.weight / self.volume # kg/cm^3 + self.name = name + self.assign_pallet_to_package() + + def assign_pallet_to_package(self): + if self.pallet is None: + # print(f"Package dimensions: Length={self.length}, Width={self.width}, Height={self.height}, Weight={self.weight}") + # Reorder pallets by width in ascending order + pallets = sorted(pallet_dimensions, key=lambda x: pallet_dimensions[x]["Width"], reverse=True) + # print(pallets) + pallets_class = [Pallet(pallet) for pallet in pallets] + + # Find the pallet with minimum width sufficient for the package (starting from the largest) + for pallet in pallets_class: + if pallet.width >= self.width: + self.pallet = pallet + + # If no pallet is sufficient, assign the largest pallet + if self.pallet is None: + self.pallet = pallets_class[-1] + + # print(f"Package {self.name} with width {self.width} assigned to pallet {self.pallet.type}.") + + # print(f"Package dimensions: Length={self.length}, Width={self.width}, Height={self.height}, Weight={self.weight}") + # Ensure package dimensions + + +class PalletStack: + def __init__(self, package, bay=None, assigned_truck=None, priority=5, stackable=True, stack_index=1): + self.pallet = package.pallet + self.stack_index = stack_index + self.stackable = stackable + self.length = max(package.length, self.pallet.length) + self.width = max(package.width, self.pallet.width) + self.height = package.height + self.pallet.height + self.weight = package.weight + self.pallet.weight + self.volume = self.length * self.width * self.height + self.density = self.weight / self.volume # kg/cm^3 + self.name = package.name + self.position = None # Added to track the position of the pallet stack in the truck + self.subtype = None + self.shape = "cube" + self.priority = priority + self.color = None + self.assigned_truck = assigned_truck + self.bay = bay + self.load_capacity = self.calculate_dynamic_stackability() # Call the method to calculate the dynamic load-bearing capacity + self.assign_subtype_to_pallet_stack() # Call the method to assign the pallet stack type + self.version_item = Item(item_id=self.name, item_name=self.subtype, typeof=self.shape, WHD=(self.width, self.length, self.height), weight=self.weight, priority_level=self.priority, loadbear=self.load_capacity, updown=False, color=self.color, assigned_bin=(self.assigned_truck.version_bin if self.assigned_truck is not None else None)) + + def update_version_item(self): + self.version_item = Item(item_id=self.name, item_name=self.subtype, typeof=self.shape, WHD=(self.width, self.length, self.height), weight=self.weight, priority_level=self.priority, loadbear=self.load_capacity, updown=False, color=self.color, assigned_bin=(self.assigned_truck.version_bin if self.assigned_truck is not None else None)) + + def calculate_dynamic_stackability(self): + if self.stackable: + # Calculate the load-bearing capacity based on weight and stackability index + return self.weight * self.stack_index + else: + return 0 + + def assign_subtype_to_pallet_stack(self): + if self.width > self.length: + self.length, self.width = self.width, self.length + print(f"Pallet stack {self.name} with width greater than length. Swapping dimensions.") + + sorted_all_types_copy = sorted_all_types[:] + for type_, subtype, dimensions in sorted_all_types_copy: + if ( + self.length <= dimensions["Max Length"] + and self.width <= dimensions["Max Width"] + and self.height <= dimensions["Max Height"] + and self.weight <= dimensions["Max Weight"] + ): + self.subtype = subtype # Assign the pallet stack type to the attribute + self.color = dimensions["Color"] # Assign the color to the attribute + # print(f"Assigned type: {type_}, Assigned color: {self.color}") + self.update_version_item() + break + # print(f"Pallet stack {self.name} assigned as {self.subtype}") + # print(f"Pallet stack dimensions: Length={self.length}, Width={self.width}, Height={self.height}, Weight={self.weight}") + + +class Truck: + def __init__(self, length=1300, width=240, height=240, max_weight=24000, license_plate="AA000AA", load_method="Side", movable_existing_pallets=True, bay=None): + self.length = length # in cm + self.width = width # in cm + self.height = height # in cm + self.max_weight = max_weight # in kg + self.loaded_pallets = [] + self.volume = self.length * self.width * self.height + self.current_weight = 0 + self.current_volume = 0 + self.license_plate = license_plate + self.load_method = load_method + self.bay = bay + self.existing_pallets = [] + self.movable_existing_pallets = movable_existing_pallets + self.version_bin = Bin(bin_id=self.license_plate, WHD=((self.width, self.length, self.height)), max_weight=self.max_weight, corner=0, put_type=0) + self.bay.packer.addBin(self.version_bin) + + def consider_existing_pallets(self, initial_pallets): + global packages # Ensure 'packages' is accessible + + for pallet in self.existing_pallets: + # print(pallet.name) + pallet.priority = 1 + # position=pallet.position POSITION NOT YET MODIFIABLE TO BE IMPLEMENTED + + while not self.check_truck_compatibility(pallet): + print(f"ERROR: The existing pallet {pallet.name} is not compatible with the truck {self.license_plate} on which it is loaded.") + print(f"Pallet dimensions are: Length={pallet.length}, Width={pallet.width}, Height={pallet.height}, Weight={pallet.weight}, Pallet Type={pallet.pallet.type}.") + print(f"Truck dimensions are: Length={self.length}, Width={self.width}, Height={self.height}, Weight={self.max_weight}.") + print(f"Please re-enter the dimensions of pallet {pallet.name}.") + length_pallet = int(get_value("Length (if empty 120): ", 120, lambda x: x.isdigit())) + width_pallet = int(get_value("Width (if empty 80): ", 80, lambda x: x.isdigit())) + height_pallet = int(get_value("Height (if empty 240): ", 240, lambda x: x.isdigit())) + weight_pallet = int(get_value("Weight (if empty 1200): ", 1200, lambda x: x.isdigit())) + pallet_type = get_value("Pallet type (EUR/ISO/120x120) (if empty automatic): ", None, lambda x: x in ["EUR", "ISO", "120x120", ""]) + pallet_included = get_value("Is pallet included in measurements? (yes/no) (if empty yes): ", "yes", lambda x: x.lower() in ["yes", "no", ""]).lower() + for package in packages: + if package.name == pallet.name: + package.length = length_pallet + package.width = width_pallet + package.pallet = Pallet(pallet_type) if pallet_type else None + package.height = height_pallet - ((package.pallet.height if package.pallet is not None else min_possible_height) if pallet_included == "yes" else 0) + package.weight = weight_pallet - ((package.pallet.weight if package.pallet is not None else min_possible_weight) if pallet_included == "yes" else 0) + package.volume = package.length * package.width * package.height + package.density = package.weight / package.volume # kg/cm^3 + package.assign_pallet_to_package() + pallet.pallet = package.pallet + pallet.length = max(package.length, package.pallet.length) + pallet.width = max(package.width, package.pallet.width) + pallet.height = package.height + pallet.pallet.height + pallet.weight = package.weight + pallet.pallet.weight + pallet.volume = pallet.length * pallet.width * pallet.height + pallet.density = pallet.weight / pallet.volume # kg/cm^3 + pallet.load_capacity = pallet.calculate_dynamic_stackability() + + pallet.assign_subtype_to_pallet_stack() + + # Attempt to load the pallet + if pallet not in self.loaded_pallets: + if pallet.version_item not in self.bay.packer.items: + self.bay.packer.addItem(pallet.version_item) + print(f"Existing pallet {pallet.name} added to the packer of bay {pallet.assigned_truck.bay.number}. Confirmed.") + if pallet in initial_pallets: + initial_pallets.remove(pallet) + else: + print(f"ERROR: The pallet {pallet.name} is already present in both truck {self.license_plate} and the packer of bay {self.bay.number}.") + else: + print(f"ERROR: The pallet {pallet.name} is already present among the loaded pallets of truck {self.license_plate}.") + + def check_truck_compatibility(self, pallet): + if pallet.length <= self.length and \ + pallet.width <= self.width and \ + pallet.height <= self.height and \ + pallet.weight <= self.max_weight: + return True + else: + # print(f"Concern: The pallet {pallet.name} cannot be loaded on truck {self.license_plate}.") + return False + + +class Bay: + def __init__(self, number, length, width, height): + self.number = number + self.length = length + self.width = width + self.height = height + self.pallets_in_bay = [] + self.volume = self.length * self.width * self.height + self.current_volume = sum([b.volume for b in self.pallets_in_bay]) + self.packer = Packer(self.number) + + def add_pallet_to_bay(self, pallet_to_add, initial_pallets): + if self.check_compatibility_with_bay(pallet_to_add): + if pallet_to_add not in self.pallets_in_bay: + self.pallets_in_bay.append(pallet_to_add) + if pallet_to_add in initial_pallets: + initial_pallets.remove(pallet_to_add) + else: + print(f"ERROR: The pallet {pallet_to_add.name} is already present in bay {self.number}.") + else: + print(f"The pallet {pallet_to_add.name} cannot be loaded into bay {self.number}. Pallet discarded. Length {pallet_to_add.length} instead of {self.length}, width {pallet_to_add.width} instead of {self.width}, height {pallet_to_add.height} instead of {self.height}") + pallet_to_add.bay = None + + def check_compatibility_with_bay(self, pallet): + if pallet.length <= self.length and \ + pallet.width <= self.width and \ + pallet.height <= self.height: + return True + else: + print(f"ERROR: The pallet {pallet.name} cannot be loaded into bay {self.number}.") + return False + + def load_onto_trucks(self, initial_pallets, trucks_in_this_bay, stability_param=0.9): + # Add new pallets to the bay packer + for truck in trucks_in_this_bay: + truck.consider_existing_pallets(initial_pallets) + print(f"Truck {truck.license_plate} with {len(truck.existing_pallets)} existing pallets + {len(truck.loaded_pallets)} already loaded.") + for pallet in self.pallets_in_bay: + # Attempt to load the pallet + if truck.check_truck_compatibility(pallet): + if pallet.name not in [item.item_id for item in self.packer.items]: + self.packer.addItem(pallet.version_item) + # print(f"Pallet {pallet.name} added to the packer of bay {self.number}.") + # else: + # print(f"ERROR: The pallet {pallet.name} is already present in the packer of bay {self.number}.") + else: + print(f"Pallet {pallet.name} not compatible with truck {truck.license_plate} so left in bay") + + print(f"List of items in the packer of bay {self.number} before packing:") + for item in self.packer.items: + print(f"Item {item.item_id} added to packer of bay {self.number}") + + # Execute packing only for pallets without assigned position + self.packer.pack( + bigger_first=True, # bigger item first. + fix_point=True, # fix item floating problem. + # binding=None, # make a set of items. + distribute_items=True, # If multiple bins, to distribute or not. + check_stable=True, # check stability on item. + support_surface_ratio=stability_param, # set support surface ratio. + number_of_decimals=0 + ) + + # Assign pallets to trucks + for truck in trucks_in_this_bay: + for bin in self.packer.bins: + if bin.bin_id == truck.license_plate: + list_copy = (self.pallets_in_bay + truck.existing_pallets)[:] + for pallet in list_copy: + for item in bin.items: + if item.item_id == pallet.name: + pallet.position = item.position # Update the position of the pallet + pallet.assigned_truck = truck # Update the assigned truck of the pallet + if pallet not in truck.loaded_pallets: + truck.loaded_pallets.append(pallet) # Add the pallet to the list of pallets loaded on the truck + # print(f"Pallet {pallet.name} loaded on truck {truck.license_plate} at position x: {pallet.position[0]}, y: {pallet.position[1]}, z: {pallet.position[2]}") + pallet.bay = None # Remove the bay from the pallet + truck.current_weight += item.weight # Update the current weight of the truck + truck.current_volume += item.getVolume() # Update the current volume of the truck + if pallet in initial_pallets: + initial_pallets.remove(pallet) # Remove pallet from initial_pallets as it is placed on a truck + if pallet in self.pallets_in_bay: + self.pallets_in_bay.remove(pallet) # Remove the pallet from the bay (if it was in this list) + if pallet in truck.existing_pallets: + truck.existing_pallets.remove(pallet) + + +def distribute_pallets_to_bays(pallets_to_distribute): + pallets_to_distribute_copy = pallets_to_distribute[:] + for pallet in pallets_to_distribute_copy: + if pallet.bay: + pallet.bay.add_pallet_to_bay(pallet, pallets_to_distribute) + if pallet.assigned_truck is not None and not pallet.assigned_truck.version_bin.items: + pallet.priority = 2 + # If it has an assigned bay and also an assigned truck, the truck will load it during packing + else: + if pallet.assigned_truck is None: + print(f"Concern: The pallet {pallet.name} is not assigned to any truck and to any bay.") + + +def get_value(message, default_value, validation): + while True: + input_value = input(message) + if input_value == "": + return default_value + if validation(input_value): + return input_value + else: + print("Invalid value. Please try again.") + + +# Initialize bays +bays = [] +num_bays = 2 # random.randint(2, 3) +for i in range(num_bays): + bay = Bay(number=i, length=1300, width=240, height=480) + bays.append(bay) + +# Initialize trucks +trucks = [] +num_trucks = random.randint(7, 8) +for _ in range(num_trucks): + truck = Truck(license_plate=f"AA{_}", bay=random.choice(bays)) + trucks.append(truck) + +# Create packages +packages = [] +for _ in range(50): + length = random.randint(20, 150) + width = random.randint(20, 150) + height = random.randint(30, 245) + volume = length * width * height + weight = int(volume * random.uniform(0.0001, 0.0005)) + pallet = Pallet(random.choice(["EUR", "ISO", "120x120"])) + name = f"{_}" + package = Package(length=length, + width=width, + height=height, + weight=weight, + pallet=pallet, + name=name) + packages.append(package) + +# Transform packages into pallet stacks +initial_pallet_stacks = [] +for package in packages: + bay = random.choice(bays) + # stackable = random.choice([True, False]) + priority = random.randint(3, 5) + pallet_stack = PalletStack(package=package, bay=bay, priority=priority) # stackable=stackable + stack_index = random.random() # random between zero and 1 + if pallet_stack.subtype is not None: # Check if a valid type was assigned + initial_pallet_stacks.append(pallet_stack) + else: + initial_pallet_stacks.append(pallet_stack) # For testing, but to be removed later + # print(f"Pallet stack discarded (theoretically, not now in test): Pallet stack {pallet_stack.name} with Length={pallet_stack.length}, Width={pallet_stack.width}, Height={pallet_stack.height}, Weight={pallet_stack.weight} cannot be assigned to any truck.") + +# Assign some of the newly created pallet stacks to some trucks as if they had already been loaded onto them +i = 0 # Initialize i +for truck in trucks: + i += 3 + for pallet_stack in initial_pallet_stacks[i-4:i-1]: + pallet_stack.assigned_truck = truck + pallet_stack.bay = None + truck.existing_pallets.append(pallet_stack) + # pallet_stack.position = (0, 0, 0) + # print(f"Pallet stack {pallet_stack.name} assigned to truck {truck.license_plate}.") + +# Distribute the pallet stacks into bays +distribute_pallets_to_bays(pallets_to_distribute=initial_pallet_stacks) + +parametro_stabilità = 0.9 + +for bay in bays: + trucks_in_this_bay = [truck for truck in trucks if truck.bay == bay] + print(f"Trucks for bay {bay.number}: {[truck.license_plate for truck in trucks_in_this_bay]}") + # Load the packages onto trucks + bay.load_onto_trucks(initial_pallets=initial_pallet_stacks, trucks_in_this_bay=trucks_in_this_bay, stability_param=parametro_stabilità) + + # Print the loaded pallet stacks + print(f"**Pallet stacks loaded from bay {bay.number}**") + for truck in trucks_in_this_bay: + print(f"- On truck {truck.license_plate}:") + for truck_bin in bay.packer.bins: + if truck_bin.bin_id == truck.license_plate: + # print(truck.loaded_pallets) + for pallet_stack in truck.loaded_pallets: + print(f"Pallet stack {pallet_stack.name} loaded on truck {truck.license_plate} at position x: {pallet_stack.position[0]}, y: {pallet_stack.position[1]}, z: {pallet_stack.position[2]}") + + print("Remaining in bay:") + for pallet_stack in bay.pallets_in_bay: + i = 0 + for truck in trucks_in_this_bay: + i += 1 + if truck.check_truck_compatibility(pallet_stack): + print(f"Pallet stack {pallet_stack.name} remained in bay because trucks are full. Acceptable dimensions.") + break + if i == len(trucks_in_this_bay): + print(f"Pallet stack {pallet_stack.name} remained in bay but NOT TRANSPORTABLE. Dimensions: Length {pallet_stack.length}, Width {pallet_stack.width}, Height {pallet_stack.height}") + + for pallet_stack in initial_pallet_stacks: + if pallet_stack.bay == bay: + i = 0 + for truck in trucks_in_this_bay: + i += 1 + if truck.check_truck_compatibility(pallet_stack): + print(f"ERROR: Pallet stack {pallet_stack.name} assigned to this bay ({pallet_stack.bay.number}) but never entered bay. Transportable by at least one truck.") + break + if i == len(trucks_in_this_bay): + print(f"Pallet stack {pallet_stack.name} assigned to this bay ({pallet_stack.bay.number}) but never entered bay. NOT TRANSPORTABLE. Dimensions: Length {pallet_stack.length}, Width {pallet_stack.width}, Height {pallet_stack.height}") + + print("***************************************************") + for truck_bin in bay.packer.bins: + print("**", truck_bin.string(), "**") + print("***************************************************") + print("FITTED ITEMS:") + print("***************************************************") + volume = truck_bin.width * truck_bin.height * truck_bin.depth + volume_fitted_items = 0 # Total volume of fitted items + for item in truck_bin.items: + volume_fitted_items += int(item.width) * int(item.height) * int(item.depth) + + print('space utilization : {}%'.format(round(volume_fitted_items / int(volume) * 100, 2))) + print('residual volume : ', int(volume) - volume_fitted_items) + print("gravity distribution : ", truck_bin.gravity) + print("***************************************************") + + # Draw results + painter = Painter(truck_bin) + fig = painter.plotBoxAndItems( + title=truck_bin.bin_id, + alpha=0.6, # Transparency + write_name=True, + fontsize=10, + alpha_proportional=True, + top_face_proportional=True + ) + + print("***************************************************") + print(f"UNFITTED ITEMS (with acceptable dimensions) in bay {bay.number}:") + unfitted_names = '' + volume_unfitted_items = 0 # Total volume of unfitted items + for item in bay.packer.unfit_items: + print("***************************************************") + print("Pallet stack number : ", item.item_id) + print('type : ', item.item_name) + print("color : ", item.color) + print("Width*Length*Height : ", str(item.width) + ' * ' + str(item.height) + ' * ' + str(item.depth)) + print("volume : ", int(item.width) * int(item.height) * int(item.depth)) + print("weight : ", int(item.weight)) + volume_unfitted_items += int(item.width) * int(item.height) * int(item.depth) + unfitted_names += '{},'.format(item.item_id) + print("***************************************************") + print("***************************************************") + # print(f'PALLET STACKS with acceptable dimensions NOT LOADED in BAY {bay.number}: ', unfitted_names) + print(f'VOLUME of pallet stacks with acceptable dimensions NOT LOADED in BAY {bay.number}: ', volume_unfitted_items) + +print("***************************************************") +print("***************************************************") +print("***************************************************") +print("***************************************************") +print("***************************************************") +print("***************************************************") +print("***************************************************") + +# Creation of additional packages for packing 2 +additional_packages = [] +for _ in range(51, 60): + length = random.randint(20, 150) + width = random.randint(20, 150) + height = random.randint(30, 245) + volume = length * width * height + weight = int(volume * random.uniform(0.0001, 0.0005)) + pallet = Pallet(random.choice(["EUR", "ISO", "120x120"])) + name = f"{_}" + package = Package(length=length, + width=width, + height=height, + weight=weight, + pallet=pallet, + name=name) + additional_packages.append(package) + print(f"Name of newly created additional package: {package.name}") + +# Transform additional packages into additional pallet stacks +initial_pallet_stacks_v2 = [] +for package in additional_packages: + priority = random.randint(3, 5) + pallet_stack = PalletStack(package=package, priority=priority, bay=trucks[0].bay) + stack_index = random.random() # random between zero and 1 + initial_pallet_stacks_v2.append(pallet_stack) + +# Assign the newly created pallet stacks to truck 0 +for pallet_stack in initial_pallet_stacks_v2: + pallet_stack.assigned_truck = trucks[0] + +distribute_pallets_to_bays(pallets_to_distribute=initial_pallet_stacks_v2) +trucks_in_this_bay_v2 = [truck for truck in trucks if truck.bay == trucks[0].bay] +trucks[0].bay.load_onto_trucks(initial_pallets=initial_pallet_stacks_v2, trucks_in_this_bay=trucks_in_this_bay_v2, stability_param=parametro_stabilità) + +# Print the loaded pallet stacks on the truck +print("***************************************************") +truck_bin = trucks[0].version_bin +bay = trucks[0].bay +print("**", truck_bin.string(), "**") +print("***************************************************") +print("FITTED ITEMS:") +print("***************************************************") +volume = truck_bin.width * truck_bin.height * truck_bin.depth +volume_fitted_items = 0 # Total volume of fitted items +for item in truck_bin.items: + volume_fitted_items += int(item.width) * int(item.height) * int(item.depth) + +print('space utilization : {}%'.format(round(volume_fitted_items / int(volume) * 100, 2))) +print('residual volume : ', int(volume) - volume_fitted_items) +print("gravity distribution : ", truck_bin.gravity) +print("***************************************************") + +# Draw results +painter = Painter(truck_bin) +fig = painter.plotBoxAndItems( + title=truck_bin.bin_id, + alpha=0.6, # Transparency + write_name=True, + fontsize=10, + alpha_proportional=True, + top_face_proportional=True +) + +print("***************************************************") +print(f"UNFITTED ITEMS (with acceptable dimensions) in bay {bay.number}:") +unfitted_names = '' +volume_unfitted_items = 0 # Total volume of unfitted items +for item in bay.packer.unfit_items: + print("***************************************************") + print("Pallet stack number : ", item.item_id) + print('type : ', item.item_name) + print("color : ", item.color) + print("Width*Length*Height : ", str(item.width) + ' * ' + str(item.height) + ' * ' + str(item.depth)) + print("volume : ", int(item.width) * int(item.height) * int(item.depth)) + print("weight : ", int(item.weight)) + volume_unfitted_items += int(item.width) * int(item.height) * int(item.depth) + unfitted_names += '{},'.format(item.item_id) + print("***************************************************") +print("***************************************************") +print(f'VOLUME of pallet stacks with acceptable dimensions NOT LOADED in BAY {bay.number}: ', volume_unfitted_items) diff --git a/example0.py b/example0.py index f6e5b9f..13b6a98 100644 --- a/example0.py +++ b/example0.py @@ -14,7 +14,7 @@ # Evergreen Real Container (20ft Steel Dry Cargo Container) # Unit cm/kg box = Bin( - partno='example0', + bin_id='example0', WHD=(589.8,243.8,259.1), max_weight=28080, corner=15, @@ -27,12 +27,11 @@ # 64 pcs per case , 82 * 46 * 170 (85.12) for i in range(5): packer.addItem(Item( - partno='Dyson DC34 Animal{}'.format(str(i+1)), - name='Dyson', + item_id='Dyson DC34 Animal', typeof='cube', WHD=(170, 82, 46), weight=85.12, - level=1, + priority_level=1, loadbear=100, updown=True, color='#FF0000') @@ -42,12 +41,11 @@ # 1 pcs per case, 85 * 60 *60 (10) for i in range(10): packer.addItem(Item( - partno='wash{}'.format(str(i+1)), - name='wash', + item_id='washing machine', typeof='cube', WHD=(85, 60, 60), weight=10, - level=1, + priority_level=1, loadbear=100, updown=True, color='#FFFF37' @@ -57,12 +55,11 @@ # one per box, 60 * 80 * 200 (80) for i in range(5): packer.addItem(Item( - partno='Cabinet{}'.format(str(i+1)), - name='cabint', + item_id='Cabinet', typeof='cube', WHD=(60, 80, 200), weight=80, - level=1, + priority_level=1, loadbear=100, updown=True, color='#842B00') @@ -72,12 +69,11 @@ # one per box , 70 * 100 * 30 (20) for i in range(10): packer.addItem(Item( - partno='Server{}'.format(str(i+1)), - name='server', + item_id='Server', typeof='cube', WHD=(70, 100, 30), weight=20, - level=1, + priority_level=1, loadbear=100, updown=True, color='#0000E3') @@ -88,7 +84,7 @@ packer.pack( bigger_first=True, distribute_items=False, - fix_point=False, # Try switching fix_point=True/False to compare the results + fix_point=True, # Try switching fix_point=True/False to compare the results check_stable=False, support_surface_ratio=0.75, number_of_decimals=0 @@ -107,8 +103,8 @@ # ''' for item in box.items: - print("partno : ",item.partno) - print("type : ",item.name) + print("item_id : ",item.item_id) + print("type : ",item.item_name) print("color : ",item.color) print("position : ",item.position) print("rotation type : ",item.rotation_type) @@ -121,14 +117,14 @@ # ''' print("UNFITTED ITEMS:") for item in box.unfitted_items: - print("partno : ",item.partno) - print("type : ",item.name) + print("item_id : ",item.item_id) + print("type : ",item.item_name) print("color : ",item.color) print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) print("volume : ",float(item.width) * float(item.height) * float(item.depth)) print("weight : ",float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) @@ -143,9 +139,8 @@ # draw results painter = Painter(box) fig = painter.plotBoxAndItems( - title=box.partno, + title=box.bin_id, alpha=0.2, - write_num=True, + write_name=True, fontsize=10 - ) -fig.show() \ No newline at end of file + ) \ No newline at end of file diff --git a/example1.py b/example1.py index 85be897..d6bc3a1 100644 --- a/example1.py +++ b/example1.py @@ -9,24 +9,24 @@ ''' # init packing function -packer = Packer() -# init bin -box = Bin('example1', (5.6875, 10.75, 15.0), 70.0,0,0) +packer = Packer(packer_id='Example Packer') +# init bin +box = Bin(WHD=(5.6875, 8, 10.0), max_weight=700.0, bin_id='example1') packer.addBin(box) -# add item -packer.addItem(Item('50g [powder 1]', 'test','cube',(2, 2, 4), 1,1,100,True,'red')) -packer.addItem(Item('50g [powder 2]', 'test','cube',(2, 2, 4), 2,1,100,True,'blue')) -packer.addItem(Item('50g [powder 3]', 'test','cube',(2, 2, 4), 3,1,100,True,'gray')) -packer.addItem(Item('50g [powder 4]', 'test','cube',(2, 2, 4), 3,1,100,True,'orange')) -packer.addItem(Item('50g [powder 5]', 'test','cylinder',(2, 2, 4), 3,1,100,True,'lawngreen')) -packer.addItem(Item('50g [powder 6]', 'test','cylinder',(2, 2, 4), 3,1,100,True,'purple')) -packer.addItem(Item('50g [powder 7]', 'test','cylinder',(1, 1, 5), 3,1,100,True,'yellow')) -packer.addItem(Item('250g [powder 8]', 'test','cylinder',(4, 4, 2), 4,1,100,True,'pink')) -packer.addItem(Item('250g [powder 9]', 'test','cylinder',(4, 4, 2), 5,1,100,True,'brown')) -packer.addItem(Item('250g [powder 10]', 'test','cube',(4, 4, 2), 6,1,100,True,'cyan')) -packer.addItem(Item('250g [powder 11]', 'test','cylinder',(4, 4, 2), 7,1,100,True,'olive')) -packer.addItem(Item('250g [powder 12]', 'test','cylinder',(4, 4, 2), 8,1,100,True,'darkgreen')) -packer.addItem(Item('250g [powder 13]', 'test','cube',(4, 4, 2), 9,1,100,True,'orange')) +# add items +packer.addItem(Item(WHD=(2, 2, 4), weight=10, priority_level=1, updown=True, color='red', loadbear=7.5, item_id='10kg/7.5kg/Prio1', item_name='test', typeof='cube')) +packer.addItem(Item(WHD=(2, 2, 4), weight=8, priority_level=1, updown=True, color='blue', loadbear=7.5, item_id='8kg/7.5kg/Prio1', item_name='test', typeof='cube')) +packer.addItem(Item(WHD=(2, 2, 4), weight=8, priority_level=2, updown=True, color='gray', loadbear=4, item_id='8kg/4kg/Prio2', item_name='test', typeof='cube')) +packer.addItem(Item(WHD=(2, 2, 3), weight=3.5, priority_level=1, updown=True, color='orange', loadbear=2, item_id='3.5kg/2kg/Prio1', item_name='test', typeof='cube')) +packer.addItem(Item(WHD=(3, 2, 4), weight=9, priority_level=1, updown=True, color='lawngreen', loadbear=8, item_id='9kg/8kg/Prio1', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(3, 2, 4), weight=8, priority_level=2, updown=True, color='purple', loadbear=8, item_id='8kg/8kg/Prio2', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(3, 1, 5), weight=9, priority_level=1, updown=True, color='yellow', loadbear=8, item_id='9kg/8kg/Prio1', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(4, 4, 2), weight=3, priority_level=1, updown=True, color='pink', loadbear=2, item_id='3kg/2kg/Prio1', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(4, 4, 2), weight=3, priority_level=1, updown=True, color='brown', loadbear=2.5, item_id='3kg/2.5kg/Prio1', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(4, 4, 2), weight=11, priority_level=1, updown=True, color='cyan', loadbear=10, item_id='11kg/10kg/Prio1', item_name='test', typeof='cube')) +packer.addItem(Item(WHD=(2, 2, 2), weight=1.5, priority_level=1, updown=True, color='olive', loadbear=1.5, item_id='1.5kg/1.5kg/Prio1', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(2, 2, 1), weight=2, priority_level=1, updown=True, color='darkgreen', loadbear=2, item_id='2kg/2kg/Prio1', item_name='test', typeof='cylinder')) +packer.addItem(Item(WHD=(5, 2, 2), weight=2.5, priority_level=1, updown=True, color='orange', loadbear=1, item_id='2.5kg/1kg/Prio1', item_name='test', typeof='cube')) # calculate packing packer.pack( @@ -48,41 +48,40 @@ volume_f = 0 unfitted_name = '' for item in b.items: - print("partno : ",item.partno) - print("color : ",item.color) - print("position : ",item.position) - print("rotation type : ",item.rotation_type) - print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) - print("volume : ",float(item.width) * float(item.height) * float(item.depth)) - print("weight : ",float(item.weight)) + print("item_id : ", item.item_id) + print("color : ", item.color) + print("position : ", item.position) + print("rotation type : ", item.rotation_type) + print("W*H*D : ", str(item.width) + '*' + str(item.height) + '*' + str(item.depth)) + print("volume : ", float(item.width) * float(item.height) * float(item.depth)) + print("weight : ", float(item.weight)) volume_t += float(item.width) * float(item.height) * float(item.depth) print("***************************************************") print("***************************************************") print("UNFITTED ITEMS:") for item in b.unfitted_items: - print("partno : ",item.partno) - print("color : ",item.color) - print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) - print("volume : ",float(item.width) * float(item.height) * float(item.depth)) - print("weight : ",float(item.weight)) + print("item_id : ", item.item_id) + print("color : ", item.color) + print("W*H*D : ", str(item.width) + '*' + str(item.height) + '*' + str(item.depth)) + print("volume : ", float(item.width) * float(item.height) * float(item.depth)) + print("weight : ", float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") -print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) -print('residual volumn : ', float(volume) - volume_t ) -print('unpack item : ',unfitted_name) -print('unpack item volumn : ',volume_f) -print("gravity distribution : ",b.gravity) +print('space utilization : {}%'.format(round(volume_t / float(volume) * 100, 2))) +print('residual volume : ', float(volume) - volume_t) +print('unpack item : ', unfitted_name) +print('unpack item volume : ', volume_f) +print("gravity distribution : ", b.gravity) stop = time.time() -print('used time : ',stop - start) +print('used time : ', stop - start) # draw results painter = Painter(b) fig = painter.plotBoxAndItems( - title=b.partno, + title=b.bin_id, alpha=0.2, - write_num=False, - fontsize=5 + write_name=True, + fontsize=10 ) -fig.show() \ No newline at end of file diff --git a/example2.py b/example2.py index d98e5cb..cc1c1de 100644 --- a/example2.py +++ b/example2.py @@ -9,41 +9,41 @@ ''' # init packing function -packer = Packer() +packer = Packer(packer_id = 'Example Packer') # init bin -box = Bin('example2',(30, 10, 15), 99,0,1) +box = Bin((30, 10, 15), 99, bin_id='example2', corner=0, put_type=1) packer.addBin(box) # add item -packer.addItem(Item('test1', 'test','cube',(9, 8, 7), 1, 1, 100, True,'red')) -packer.addItem(Item('test2', 'test','cube',(4, 25, 1), 1, 1, 100, True,'blue')) -packer.addItem(Item('test3', 'test','cube',(2, 13, 5), 1, 1, 100, True,'gray')) -packer.addItem(Item('test4', 'test','cube',(7, 5, 4), 1, 1, 100, True,'orange')) -packer.addItem(Item('test5', 'test','cube',(10, 5, 2), 1, 1, 100, True,'lawngreen')) -packer.addItem(Item('test6', 'test','cube',(6, 5, 2), 1, 1, 100, True,'purple')) -packer.addItem(Item('test7', 'test','cube',(5, 2, 9), 1, 1, 100, True,'yellow')) -packer.addItem(Item('test8', 'test','cube',(10, 8, 5), 1, 1, 100, True,'pink')) -packer.addItem(Item('test9', 'test','cube',(1, 3, 5), 1, 1, 100, True,'brown')) -packer.addItem(Item('test10', 'test','cube',(8, 4, 7), 1, 1, 100, True,'cyan')) -packer.addItem(Item('test11', 'test','cube',(2, 5, 3), 1, 1, 100, True,'olive')) -packer.addItem(Item('test12', 'test','cube',(1, 9, 2), 1, 1, 100, True,'darkgreen')) -packer.addItem(Item('test13', 'test','cube',(7, 5, 4), 1, 1, 100, True,'orange')) -packer.addItem(Item('test14', 'test','cube',(10, 2, 1), 1, 1, 100, True,'lawngreen')) -packer.addItem(Item('test15', 'test','cube',(3, 2, 4), 1, 1, 100, True,'purple')) -packer.addItem(Item('test16', 'test','cube',(5, 7, 8), 1, 1, 100, True,'yellow')) -packer.addItem(Item('test17', 'test','cube',(4, 8, 3), 1, 1, 100, True,'white')) -packer.addItem(Item('test18', 'test','cube',(2, 11, 5), 1, 1, 100, True,'brown')) -packer.addItem(Item('test19', 'test','cube',(8, 3, 5), 1, 1, 100, True,'cyan')) -packer.addItem(Item('test20', 'test','cube',(7, 4, 5), 1, 1, 100, True,'olive')) -packer.addItem(Item('test21', 'test','cube',(2, 4, 11), 1, 1, 100, True,'darkgreen')) -packer.addItem(Item('test22', 'test','cube',(1, 3, 4), 1, 1, 100, True,'orange')) -packer.addItem(Item('test23', 'test','cube',(10, 5, 2), 1, 1, 100, True,'lawngreen')) -packer.addItem(Item('test24', 'test','cube',(7, 4, 5), 1, 1, 100, True,'purple')) -packer.addItem(Item('test25', 'test','cube',(2, 10, 3), 1, 1, 100, True,'yellow')) -packer.addItem(Item('test26', 'test','cube',(3, 8, 1), 1, 1, 100, True,'pink')) -packer.addItem(Item('test27', 'test','cube',(7, 2, 5), 1, 1, 100, True,'brown')) -packer.addItem(Item('test28', 'test','cube',(8, 9, 5), 1, 1, 100, True,'cyan')) -packer.addItem(Item('test29', 'test','cube',(4, 5, 10), 1, 1, 100, True,'olive')) -packer.addItem(Item('test30', 'test','cube',(10, 10, 2), 1, 1, 100, True,'darkgreen')) +packer.addItem(Item((9, 8, 7), 1, 1, True, 'red', 100, 'test1', 'test', 'cube')) +packer.addItem(Item((4, 25, 1), 1, 1, True, 'blue', 100, 'test2', 'test', 'cube')) +packer.addItem(Item((2, 13, 5), 1, 1, True, 'gray', 100, 'test3', 'test', 'cube')) +packer.addItem(Item((7, 5, 4), 1, 1, True, 'orange', 100, 'test4', 'test', 'cube')) +packer.addItem(Item((10, 5, 2), 1, 1, True, 'lawngreen', 100, 'test5', 'test', 'cube')) +packer.addItem(Item((6, 5, 2), 1, 1, True, 'purple', 100, 'test6', 'test', 'cube')) +packer.addItem(Item((5, 2, 9), 1, 1, True, 'yellow', 100, 'test7', 'test', 'cube')) +packer.addItem(Item((10, 8, 5), 1, 1, True, 'pink', 100, 'test8', 'test', 'cube')) +packer.addItem(Item((1, 3, 5), 1, 1, True, 'brown', 100, 'test9', 'test', 'cube')) +packer.addItem(Item((8, 4, 7), 1, 1, True, 'cyan', 100, 'test10', 'test', 'cube')) +packer.addItem(Item((2, 5, 3), 1, 1, True, 'olive', 100, 'test11', 'test', 'cube')) +packer.addItem(Item((1, 9, 2), 1, 1, True, 'darkgreen', 100, 'test12', 'test', 'cube')) +packer.addItem(Item((7, 5, 4), 1, 1, True, 'orange', 100, 'test13', 'test', 'cube')) +packer.addItem(Item((10, 2, 1), 1, 1, True, 'lawngreen', 100, 'test14', 'test', 'cube')) +packer.addItem(Item((3, 2, 4), 1, 1, True, 'purple', 100, 'test15', 'test', 'cube')) +packer.addItem(Item((5, 7, 8), 1, 1, True, 'yellow', 100, 'test16', 'test', 'cube')) +packer.addItem(Item((4, 8, 3), 1, 1, True, 'white', 100, 'test17', 'test', 'cube')) +packer.addItem(Item((2, 11, 5), 1, 1, True, 'brown', 100, 'test18', 'test', 'cube')) +packer.addItem(Item((8, 3, 5), 1, 1, True, 'cyan', 100, 'test19', 'test', 'cube')) +packer.addItem(Item((7, 4, 5), 1, 1, True, 'olive', 100, 'test20', 'test', 'cube')) +packer.addItem(Item((2, 4, 11), 1, 1, True, 'darkgreen', 100, 'test21', 'test', 'cube')) +packer.addItem(Item((1, 3, 4), 1, 1, True, 'orange', 100, 'test22', 'test', 'cube')) +packer.addItem(Item((10, 5, 2), 1, 1, True, 'lawngreen', 100, 'test23', 'test', 'cube')) +packer.addItem(Item((7, 4, 5), 1, 1, True, 'purple', 100, 'test24', 'test', 'cube')) +packer.addItem(Item((2, 10, 3), 1, 1, True, 'yellow', 100, 'test25', 'test', 'cube')) +packer.addItem(Item((3, 8, 1), 1, 1, True, 'pink', 100, 'test26', 'test', 'cube')) +packer.addItem(Item((7, 2, 5), 1, 1, True, 'brown', 100, 'test27', 'test', 'cube')) +packer.addItem(Item((8, 9, 5), 1, 1, True, 'cyan', 100, 'test28', 'test', 'cube')) +packer.addItem(Item((4, 5, 10), 1, 1, True, 'olive', 100, 'test29', 'test', 'cube')) +packer.addItem(Item((10, 10, 2), 1, 1, True, 'darkgreen', 100, 'test30', 'test', 'cube')) # calculate packing packer.pack( @@ -65,7 +65,7 @@ volume_f = 0 unfitted_name = '' for item in b.items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("position : ",item.position) print("rotation type : ",item.rotation_type) @@ -77,13 +77,13 @@ print("***************************************************") print("UNFITTED ITEMS:") for item in b.unfitted_items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) print("volume : ",float(item.width) * float(item.height) * float(item.depth)) print("weight : ",float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) @@ -97,9 +97,8 @@ # draw results painter = Painter(b) fig = painter.plotBoxAndItems( - title=b.partno, + title=b.bin_id, alpha=0.8, - write_num=False, + write_name=False, fontsize=10 -) -fig.show() \ No newline at end of file +) \ No newline at end of file diff --git a/example3.py b/example3.py index f45080c..fc8242c 100644 --- a/example3.py +++ b/example3.py @@ -9,18 +9,18 @@ ''' # init packing function -packer = Packer() +packer = Packer(packer_id = 'Example Packer') # init bin -box = Bin('example3', (6, 1, 5), 100,0,put_type=0) +box = Bin(WHD=(6, 1, 5), max_weight=100, bin_id='example3', corner=0, put_type=0) # add item -# Item('item partno', (W,H,D), Weight, Packing Priority level, load bear, Upside down or not , 'item color') +# Item('item item_id', (W,H,D), Weight, Packing Priority priority_level, load bear, Upside down or not , 'item color') packer.addBin(box) # If all item WHD=(2, 1, 3) , item can be fully packed into box, but if choose one item and modify WHD=(3, 1, 2) , item can't be fully packed into box. -packer.addItem(Item(partno='Box-1',name='test',typeof='cube', WHD=(2, 1, 3), weight=1, level=1,loadbear=100, updown=True, color='yellow')) -packer.addItem(Item(partno='Box-2',name='test',typeof='cube', WHD=(3, 1, 2), weight=1, level=1,loadbear=100, updown=True, color='pink')) # Try switching WHD=(3, 1, 2) and (2, 1, 3) to compare the results -packer.addItem(Item(partno='Box-3',name='test',typeof='cube', WHD=(2, 1, 3), weight=1,level= 1,loadbear=100, updown=True, color='brown')) -packer.addItem(Item(partno='Box-4',name='test',typeof='cube', WHD=(2, 1, 3), weight=1, level=1,loadbear=100, updown=True, color='cyan')) -packer.addItem(Item(partno='Box-5',name='test',typeof='cube', WHD=(2, 1, 3), weight=1, level=1,loadbear=100, updown=True, color='olive')) +packer.addItem(Item(item_id='Box-1', typeof='cube', WHD=(2, 1, 3), weight=1, priority_level=1, loadbear=100, updown=True, color='yellow')) +packer.addItem(Item(item_id='Box-2', typeof='cube', WHD=(3, 1, 2), weight=1, priority_level=1, loadbear=100, updown=True, color='pink')) # Try switching WHD=(3, 1, 2) and (2, 1, 3) to compare the results +packer.addItem(Item(item_id='Box-3', typeof='cube', WHD=(2, 1, 3), weight=1, priority_level=1, loadbear=100, updown=True, color='brown')) +packer.addItem(Item(item_id='Box-4', typeof='cube', WHD=(2, 1, 3), weight=1, priority_level=1, loadbear=100, updown=True, color='cyan')) +packer.addItem(Item(item_id='Box-5', typeof='cube', WHD=(2, 1, 3), weight=1, priority_level=1, loadbear=100, updown=True, color='olive')) # calculate packing packer.pack( @@ -42,7 +42,7 @@ volume_f = 0 unfitted_name = '' for item in b.items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("position : ",item.position) print("rotation type : ",item.rotation_type) @@ -54,13 +54,13 @@ print("***************************************************") print("UNFITTED ITEMS:") for item in b.unfitted_items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) print("volume : ",float(item.width) * float(item.height) * float(item.depth)) print("weight : ",float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) @@ -74,9 +74,8 @@ # draw results painter = Painter(b) fig = painter.plotBoxAndItems( - title=b.partno, + title=b.bin_id, alpha=0.8, - write_num=False, + write_name=False, fontsize=10 -) -fig.show() \ No newline at end of file +) \ No newline at end of file diff --git a/example4.py b/example4.py index f0dd535..9562c8f 100644 --- a/example4.py +++ b/example4.py @@ -9,12 +9,12 @@ ''' # init packing function -packer = Packer() +packer = Packer(packer_id = 'Example Packer') # Evergreen Real Container (20ft Steel Dry Cargo Container) # Unit cm/kg box = Bin( - partno='example4', + bin_id='example4', WHD=(589.8,243.8,259.1), max_weight=28080, corner=15, @@ -27,12 +27,11 @@ # 64 pcs per case , 82 * 46 * 170 (85.12) for i in range(15): packer.addItem(Item( - partno='Dyson DC34 Animal{}'.format(str(i+1)), - name='Dyson', + item_id='Dyson DC34 Animal', typeof='cube', WHD=(170, 82, 46), weight=85.12, - level=1, + priority_level=1, loadbear=100, updown=True, color='#FF0000') @@ -42,12 +41,11 @@ # 1 pcs per case, 85 * 60 *60 (10) for i in range(18): packer.addItem(Item( - partno='wash{}'.format(str(i+1)), - name='wash', + item_id='wash', typeof='cube', WHD=(85, 60, 60), weight=10, - level=1, + priority_level=1, loadbear=100, updown=True, color='#FFFF37' @@ -57,12 +55,11 @@ # one per box, 60 * 80 * 200 (80) for i in range(15): packer.addItem(Item( - partno='Cabinet{}'.format(str(i+1)), - name='cabint', + item_id='Cabinet', typeof='cube', WHD=(60, 80, 200), weight=80, - level=1, + priority_level=1, loadbear=100, updown=True, color='#842B00') @@ -72,12 +69,11 @@ # one per box , 70 * 100 * 30 (20) for i in range(42): packer.addItem(Item( - partno='Server{}'.format(str(i+1)), - name='server', + item_id='Server', typeof='cube', WHD=(70, 100, 30), weight=20, - level=1, + priority_level=1, loadbear=100, updown=True, color='#0000E3') @@ -109,8 +105,8 @@ # ''' for item in box.items: - print("partno : ",item.partno) - print("type : ",item.name) + print("item_id : ",item.item_id) + print("type : ",item.item_name) print("color : ",item.color) print("position : ",item.position) print("rotation type : ",item.rotation_type) @@ -123,14 +119,14 @@ # ''' print("UNFITTED ITEMS:") for item in box.unfitted_items: - print("partno : ",item.partno) - print("type : ",item.name) + print("item_id : ",item.item_id) + print("type : ",item.item_name) print("color : ",item.color) print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) print("volume : ",float(item.width) * float(item.height) * float(item.depth)) print("weight : ",float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) @@ -145,9 +141,8 @@ # draw results painter = Painter(box) fig = painter.plotBoxAndItems( - title=box.partno, + title=box.bin_id, alpha=0.2, - write_num=False, + write_name=False, fontsize=6 - ) -fig.show() \ No newline at end of file + ) \ No newline at end of file diff --git a/example5.py b/example5.py index 6c42bed..143b30a 100644 --- a/example5.py +++ b/example5.py @@ -10,15 +10,15 @@ ''' # init packing function -packer = Packer() +packer = Packer( packer_id = 'Example Packer') # init bin -box = Bin('example5', (5, 4, 3), 100,0,0) +box = Bin(WHD=(5, 4, 3), max_weight=100, bin_id='example5', corner=0, put_type=0) # add item -# Item('item partno', (W,H,D), Weight, Packing Priority level, load bear, Upside down or not , 'item color') +# Item('item item_id', (W,H,D), Weight, Packing Priority level, load bear, Upside down or not , 'item color') packer.addBin(box) -packer.addItem(Item(partno='Box-3', name='test', typeof='cube', WHD=(2, 5, 2), weight=1, level=1,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-3', name='test', typeof='cube', WHD=(2, 3, 2), weight=1, level=2,loadbear=100, updown=True, color='pink')) # Try switching WHD=(2, 2, 2) and (2, 3, 2) to compare the results -packer.addItem(Item(partno='Box-4', name='test', typeof='cube', WHD=(5, 4, 1), weight=1,level=3,loadbear=100, updown=True, color='brown')) +packer.addItem(Item(item_id='Box-3', typeof='cube', WHD=(2, 5, 2), weight=1, priority_level=1, loadbear=100, updown=True, color='pink')) +packer.addItem(Item(item_id='Box-3', typeof='cube', WHD=(2, 3, 2), weight=1, priority_level=2, loadbear=100, updown=True, color='pink')) # Try switching WHD=(2, 2, 2) and (2, 3, 2) to compare the results +packer.addItem(Item(item_id='Box-4', typeof='cube', WHD=(5, 4, 1), weight=1, priority_level=3, loadbear=100, updown=True, color='brown')) # calculate packing packer.pack( @@ -43,7 +43,7 @@ volume_f = 0 unfitted_name = '' for item in b.items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("position : ",item.position) print("rotation type : ",item.rotation_type) @@ -55,13 +55,13 @@ print("***************************************************") print("UNFITTED ITEMS:") for item in b.unfitted_items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) print("volume : ",float(item.width) * float(item.height) * float(item.depth)) print("weight : ",float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) @@ -75,10 +75,8 @@ # draw results painter = Painter(b) fig = painter.plotBoxAndItems( - title=b.partno, + title=b.bin_id, alpha=0.8, - write_num=False, + write_name=False, fontsize=10 ) - -fig.show() \ No newline at end of file diff --git a/example6.py b/example6.py index 8b03a99..75bdd90 100644 --- a/example6.py +++ b/example6.py @@ -11,21 +11,21 @@ ''' # init packing function -packer = Packer() +packer = Packer( packer_id = 'Example Packer') # init bin -box = Bin('example6', (5, 4, 7), 100,0,0) +box = Bin(WHD=(5, 4, 7), max_weight=100, bin_id='example6', corner=0, put_type=0) # add item -# Item('item partno', (W,H,D), Weight, Packing Priority level, load bear, Upside down or not , 'item color') +# Item('item item_id', (W,H,D), Weight, Packing Priority level, load bear, Upside down or not , 'item color') packer.addBin(box) -packer.addItem(Item(partno='Box-1', name='test', typeof='cube', WHD=(5, 4, 1), weight=1, level=1,loadbear=100, updown=True, color='yellow')) -packer.addItem(Item(partno='Box-2', name='test', typeof='cube', WHD=(1, 1, 4), weight=1, level=2,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-3', name='test', typeof='cube', WHD=(3, 4, 2), weight=1, level=3,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-4', name='test', typeof='cube', WHD=(1, 1, 4), weight=1, level=4,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-5', name='test', typeof='cube', WHD=(1, 2, 1), weight=1, level=5,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-6', name='test', typeof='cube', WHD=(1, 2, 1), weight=1, level=6,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-7', name='test', typeof='cube', WHD=(1, 1, 4), weight=1, level=7,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-8', name='test', typeof='cube', WHD=(1, 1, 4), weight=1, level=8,loadbear=100, updown=True, color='olive'))# Try switching WHD=(1, 1, 3) and (1, 1, 4) to compare the results -packer.addItem(Item(partno='Box-9', name='test', typeof='cube', WHD=(5, 4, 2), weight=1, level=9,loadbear=100, updown=True, color='brown')) +packer.addItem(Item(item_id='Box-1', typeof='cube', WHD=(5, 4, 1), weight=1, priority_level=1, loadbear=100, updown=True, color='yellow')) +packer.addItem(Item(item_id='Box-2', typeof='cube', WHD=(1, 1, 4), weight=1, priority_level=2, loadbear=100, updown=True, color='olive')) +packer.addItem(Item(item_id='Box-3', typeof='cube', WHD=(3, 4, 2), weight=1, priority_level=3, loadbear=100, updown=True, color='pink')) +packer.addItem(Item(item_id='Box-4', typeof='cube', WHD=(1, 1, 4), weight=1, priority_level=4, loadbear=100, updown=True, color='olive')) +packer.addItem(Item(item_id='Box-5', typeof='cube', WHD=(1, 2, 1), weight=1, priority_level=5, loadbear=100, updown=True, color='pink')) +packer.addItem(Item(item_id='Box-6', typeof='cube', WHD=(1, 2, 1), weight=1, priority_level=6, loadbear=100, updown=True, color='pink')) +packer.addItem(Item(item_id='Box-7', typeof='cube', WHD=(1, 1, 4), weight=1, priority_level=7, loadbear=100, updown=True, color='olive')) +packer.addItem(Item(item_id='Box-8', typeof='cube', WHD=(1, 1, 4), weight=1, priority_level=8, loadbear=100, updown=True, color='olive')) # Try switching WHD=(1, 1, 3) and (1, 1, 4) to compare the results +packer.addItem(Item(item_id='Box-9', typeof='cube', WHD=(5, 4, 2), weight=1, priority_level=9, loadbear=100, updown=True, color='brown')) # calculate packing packer.pack( @@ -50,7 +50,7 @@ volume_f = 0 unfitted_name = '' for item in b.items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("position : ",item.position) print("rotation type : ",item.rotation_type) @@ -62,13 +62,13 @@ print("***************************************************") print("UNFITTED ITEMS:") for item in b.unfitted_items: - print("partno : ",item.partno) + print("item_id : ",item.item_id) print("color : ",item.color) print("W*H*D : ",str(item.width) +'*'+ str(item.height) +'*'+ str(item.depth)) print("volume : ",float(item.width) * float(item.height) * float(item.depth)) print("weight : ",float(item.weight)) volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) + unfitted_name += '{},'.format(item.item_id) print("***************************************************") print("***************************************************") print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) @@ -82,9 +82,8 @@ # draw results painter = Painter(b) fig = painter.plotBoxAndItems( - title=b.partno, + title=b.bin_id, alpha=0.8, - write_num=False, + write_name=False, fontsize=10 -) -fig.show() \ No newline at end of file +) \ No newline at end of file diff --git a/example7.py b/example7.py index 661f4da..604058d 100644 --- a/example7.py +++ b/example7.py @@ -1,111 +1,107 @@ from py3dbp import Packer, Bin, Item, Painter import time + start = time.time() ''' - If you have multiple boxes, you can change distribute_items to achieve different packaging purposes. 1. distribute_items=True , put the items into the box in order, if the box is full, the remaining items will continue to be loaded into the next box until all the boxes are full or all the items are packed. 2. distribute_items=False, compare the packaging of all boxes, that is to say, each box packs all items, not the remaining items. - ''' -# init packing function -packer = Packer() -# init bin -box = Bin('example7-Bin1', (5, 5, 5), 100,0,0) -box2 = Bin('example7-Bin2', (3, 3, 5), 100,0,0) -# add item -# Item('item partno', (W,H,D), Weight, Packing Priority level, load bear, Upside down or not , 'item color') +# Initialize the packing function +packer = Packer(packer_id='Example Packer') + +# Initialize bins +box = Bin((5, 5, 5), 100, bin_id='example7-Bin1', bin_name='example7-Bin1', corner=0, put_type=1) +box2 = Bin((3, 3, 5), 100, bin_id='example7-Bin2', bin_name='example7-Bin2', corner=0, put_type=1) + +# Add bins to the packer packer.addBin(box) packer.addBin(box2) -packer.addItem(Item(partno='Box-1', name='test1', typeof='cube', WHD=(5, 4, 1), weight=1, level=1,loadbear=100, updown=True, color='yellow')) -packer.addItem(Item(partno='Box-2', name='test2', typeof='cube', WHD=(1, 2, 4), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-3', name='test3', typeof='cube', WHD=(1, 2, 3), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-4', name='test4', typeof='cube', WHD=(1, 2, 2), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-5', name='test5', typeof='cube', WHD=(1, 2, 3), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-6', name='test6', typeof='cube', WHD=(1, 2, 4), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-7', name='test7', typeof='cube', WHD=(1, 2, 2), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-8', name='test8', typeof='cube', WHD=(1, 2, 3), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-9', name='test9', typeof='cube', WHD=(1, 2, 4), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-10', name='test10', typeof='cube', WHD=(1, 2, 3), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-11', name='test11', typeof='cube', WHD=(1, 2, 2), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-12', name='test12', typeof='cube', WHD=(5, 4, 1), weight=1, level=1,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-13', name='test13', typeof='cube', WHD=(1, 1, 4), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-14', name='test14', typeof='cube', WHD=(1, 2, 1), weight=1, level=1,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-15', name='test15', typeof='cube', WHD=(1, 2, 1), weight=1, level=1,loadbear=100, updown=True, color='pink')) -packer.addItem(Item(partno='Box-16', name='test16', typeof='cube', WHD=(1, 1, 4), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-17', name='test17', typeof='cube', WHD=(1, 1, 4), weight=1, level=1,loadbear=100, updown=True, color='olive')) -packer.addItem(Item(partno='Box-18', name='test18', typeof='cube', WHD=(5, 4, 2), weight=1, level=1,loadbear=100, updown=True, color='brown')) +# Add items to the packer +items = [ + ('Box-1', (5, 4, 1), 'yellow'), + ('Box-2', (1, 2, 4), 'blue'), + ('Box-3', (1, 2, 3), 'orange'), + ('Box-4', (1, 2, 2), 'grey'), + ('Box-5', (1, 2, 3), 'green'), + ('Box-6', (1, 2, 4), 'red'), + ('Box-7', (1, 2, 2), 'pink'), + ('Box-8', (1, 2, 3), 'olive'), + ('Box-9', (1, 2, 4), 'olive'), + ('Box-10', (1, 2, 3), 'pink'), + ('Box-11', (1, 2, 2), 'olive'), + ('Box-12', (5, 4, 1), 'pink'), + ('Box-13', (1, 1, 4), 'olive'), + ('Box-14', (1, 2, 1), 'pink'), + ('Box-15', (1, 2, 1), 'red'), + ('Box-16', (1, 1, 4), 'blue'), + ('Box-17', (1, 1, 4), 'olive'), + ('Box-18', (5, 4, 2), 'brown'), +] -# calculate packing +for item_id, dimensions, color in items: + packer.addItem(Item( + WHD=dimensions, + weight=1, + priority_level=1, + updown=True, + color=color, + loadbear=100, + item_id=item_id, + item_name=item_id, + typeof='cube' + )) + +# Calculate packing packer.pack( bigger_first=True, - # Change distribute_items=False to compare the packing situation in multiple boxes of different capacities. - distribute_items=False, + distribute_items=False, # Change this to True to compare packing across bins fix_point=True, check_stable=True, support_surface_ratio=0.75, number_of_decimals=0 ) -# put order -packer.putOrder() - -# print result -print("***************************************************") -for idx,b in enumerate(packer.bins) : - print("**", b.string(), "**") +# Print and visualize results +for idx, b in enumerate(packer.bins): + print("***************************************************") + print(f"** {b.string()} **") print("***************************************************") print("FITTED ITEMS:") print("***************************************************") + volume = b.width * b.height * b.depth volume_t = 0 - volume_f = 0 - unfitted_name = '' + + if not b.items: + print(f"Bin {b.item_id} is empty. Skipping plot.") + continue + for item in b.items: - print("partno : ",item.partno) - print("color : ",item.color) - print("position : ",item.position) - print("rotation type : ",item.rotation_type) - print("W*H*D : ",str(item.width) +' * '+ str(item.height) +' * '+ str(item.depth)) - print("volume : ",float(item.width) * float(item.height) * float(item.depth)) - print("weight : ",float(item.weight)) - volume_t += float(item.width) * float(item.height) * float(item.depth) + item_volume = float(item.width) * float(item.height) * float(item.depth) + volume_t += item_volume + print(f"item_id : {item.item_id}") + print(f"color : {item.color}") + print(f"position : {item.position}") + print(f"rotation type : {item.rotation_type}") + print(f"W*H*D : {item.width} * {item.height} * {item.depth}") + print(f"volume : {item_volume}") + print(f"weight : {item.weight}") print("***************************************************") - print('space utilization : {}%'.format(round(volume_t / float(volume) * 100 ,2))) - print('residual volumn : ', float(volume) - volume_t ) - print("gravity distribution : ",b.gravity) + print(f"space utilization : {round(volume_t / float(volume) * 100, 2)}%") + print(f"residual volume : {float(volume) - volume_t}") + print(f"gravity distribution : {b.gravity}") print("***************************************************") - # draw results + + # Generate and show plot for the current bin painter = Painter(b) fig = painter.plotBoxAndItems( - title=b.partno, + title=b.bin_id, alpha=0.8, - write_num=False, + write_name=False, fontsize=10 - ) - -print("***************************************************") -print("UNFITTED ITEMS:") -for item in packer.unfit_items: - print("***************************************************") - print('name : ',item.name) - print("partno : ",item.partno) - print("color : ",item.color) - print("W*H*D : ",str(item.width) +' * '+ str(item.height) +' * '+ str(item.depth)) - print("volume : ",float(item.width) * float(item.height) * float(item.depth)) - print("weight : ",float(item.weight)) - volume_f += float(item.width) * float(item.height) * float(item.depth) - unfitted_name += '{},'.format(item.partno) - print("***************************************************") -print("***************************************************") -print('unpack item : ',unfitted_name) -print('unpack item volumn : ',volume_f) - -stop = time.time() -print('used time : ',stop - start) - -fig.show() \ No newline at end of file + ) \ No newline at end of file diff --git a/example8.py b/example8.py new file mode 100644 index 0000000..519058b --- /dev/null +++ b/example8.py @@ -0,0 +1,52 @@ +from py3dbp import Packer, Bin, Item, Painter + +# Creation of the default packer +packer = Packer(packer_id='packer1') + +# Creation of bin and items without specifying the packer +bin1 = Bin(WHD=(100, 100, 100), bin_id='bin1') +item1 = Item(WHD=(10, 10, 10), item_id='item1') +item2 = Item(WHD=(20, 20, 20), item_id='item2') + +# Adding the bin and items to the packer +packer.addBin(bin1) +packer.addItem(item1) +packer.addItem(item2) + +# Executing the packing +packer.pack() + +# Example with multiple packers (necessary to specify the packer) + +# Creation of the packers +packer1 = Packer(packer_id='packer1') +packer2 = Packer(packer_id='packer2') + +# Creation of bin and items specifying the packer +bin1 = Bin(WHD=(100, 100, 100), bin_id='bin1', packer=packer1) +bin2 = Bin(WHD=(200, 200, 200), bin_id='bin2', packer=packer2) + +item1 = Item(WHD=(10, 10, 10), item_id='item1', packer=packer1) +item2 = Item(WHD=(20, 20, 20), item_id='item2', packer=packer2) + +# Adding the bins and items to their respective packers +packer1.addBin(bin1) +packer1.addItem(item1) + +packer2.addBin(bin2) +packer2.addItem(item2) + +# Executing the packing for each packer +packer1.pack() +packer2.pack() + +# draw results for each packer +for packer in [packer1, packer2]: + for b in packer.bins: + painter = Painter(b) + fig = painter.plotBoxAndItems( + title=b.bin_id, + alpha=0.8, + write_name=False, + fontsize=10 + ) \ No newline at end of file diff --git a/py3dbp/auxiliary_methods.py b/py3dbp/auxiliary_methods.py index 65e7134..ef21006 100644 --- a/py3dbp/auxiliary_methods.py +++ b/py3dbp/auxiliary_methods.py @@ -1,21 +1,19 @@ from decimal import Decimal from .constants import Axis - def rectIntersect(item1, item2, x, y): d1 = item1.getDimension() d2 = item2.getDimension() - cx1 = item1.position[x] + d1[x]/2 - cy1 = item1.position[y] + d1[y]/2 - cx2 = item2.position[x] + d2[x]/2 - cy2 = item2.position[y] + d2[y]/2 + cx1 = item1.position[x] + d1[x] / Decimal('2') + cy1 = item1.position[y] + d1[y] / Decimal('2') + cx2 = item2.position[x] + d2[x] / Decimal('2') + cy2 = item2.position[y] + d2[y] / Decimal('2') ix = max(cx1, cx2) - min(cx1, cx2) iy = max(cy1, cy2) - min(cy1, cy2) - return ix < (d1[x]+d2[x])/2 and iy < (d1[y]+d2[y])/2 - + return ix < (d1[x] + d2[x]) / Decimal('2') and iy < (d1[y] + d2[y]) / Decimal('2') def intersect(item1, item2): return ( @@ -24,12 +22,11 @@ def intersect(item1, item2): rectIntersect(item1, item2, Axis.WIDTH, Axis.DEPTH) ) - def getLimitNumberOfDecimals(number_of_decimals): - return Decimal('1.{}'.format('0' * number_of_decimals)) - + return Decimal('1.' + '0' * number_of_decimals) def set2Decimal(value, number_of_decimals=0): number_of_decimals = getLimitNumberOfDecimals(number_of_decimals) - - return Decimal(value).quantize(number_of_decimals) + if not isinstance(value, Decimal): + value = Decimal(str(value)) + return value.quantize(number_of_decimals) diff --git a/py3dbp/main.py b/py3dbp/main.py index b3c228e..5a5e296 100644 --- a/py3dbp/main.py +++ b/py3dbp/main.py @@ -1,157 +1,178 @@ from .constants import RotationType, Axis from .auxiliary_methods import intersect, set2Decimal +from decimal import Decimal, getcontext import numpy as np -# required to plot a representation of Bin and contained items -from matplotlib.patches import Rectangle,Circle -import matplotlib.pyplot as plt -import mpl_toolkits.mplot3d.art3d as art3d -from collections import Counter import copy -DEFAULT_NUMBER_OF_DECIMALS = 0 -START_POSITION = [0, 0, 0] +import plotly.graph_objects as go +# Set global context for decimal precision +getcontext().prec = 28 # Adjust as needed +# Global variable for the external logger +external_logger = None -class Item: +def set_external_logger(logger): + global external_logger + external_logger = logger + if external_logger: + external_logger.info('Logger correctly configured in the external module.') + +DEFAULT_NUMBER_OF_DECIMALS = 0 +START_POSITION = [Decimal('0'), Decimal('0'), Decimal('0')] + +avg_density_coefficient = Decimal('0.00026') # kg/cm³ - def __init__(self, partno,name,typeof, WHD, weight, level, loadbear, updown, color): - ''' ''' - self.partno = partno - self.name = name +class Item: + def __init__(self, WHD, weight=None, priority_level=100, updown=False, color="red", loadbear=None, item_id=None, item_name=None, typeof='cube', assigned_bin=None, packer=None): + self.packer = packer if packer is not None else Packer.get_default_packer() + if typeof not in ['cube', 'cylinder']: + raise ValueError(f"Invalid item type: {typeof}. Must be 'cube' or 'cylinder'.") + self.item_id = self.generate_unique_id(item_id) + self.item_name = item_name if item_name else item_id self.typeof = typeof - self.width = WHD[0] - self.height = WHD[1] - self.depth = WHD[2] - self.weight = weight - # Packing Priority level ,choose 1-3 - self.level = level - # loadbear - self.loadbear = loadbear - # Upside down? True or False + self.width = Decimal(str(WHD[0])) + self.height = Decimal(str(WHD[1])) + self.depth = Decimal(str(WHD[2])) + self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS + self.weight = Decimal(str(weight)) if weight else self.getVolume() * avg_density_coefficient + self.priority_level = priority_level + self.loadbear = Decimal(str(loadbear)) if loadbear else self.weight # Load bearing capacity self.updown = updown if typeof == 'cube' else False - # Draw item color self.color = color self.rotation_type = 0 - self.position = START_POSITION - self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS - + self.position = START_POSITION.copy() + self.assigned_bin = assigned_bin + + def generate_unique_id(self, base_id): + ''' Generate a unique ID if the base ID already exists ''' + existing_items_ids = self.packer.existing_items_ids + if base_id and base_id not in existing_items_ids: + existing_items_ids.add(base_id) + return base_id + + counter = 1 + new_id = f"{base_id}{counter}" + while new_id in existing_items_ids: + counter += 1 + new_id = f"{base_id}{counter}" + + existing_items_ids.add(new_id) + if external_logger: + external_logger.warning(f'ID conflict for item {base_id}. Assigned new id: {new_id}') + return new_id def formatNumbers(self, number_of_decimals): - ''' ''' self.width = set2Decimal(self.width, number_of_decimals) self.height = set2Decimal(self.height, number_of_decimals) self.depth = set2Decimal(self.depth, number_of_decimals) self.weight = set2Decimal(self.weight, number_of_decimals) + self.loadbear = set2Decimal(self.loadbear, number_of_decimals) self.number_of_decimals = number_of_decimals - def string(self): - ''' ''' return "%s(%sx%sx%s, weight: %s) pos(%s) rt(%s) vol(%s)" % ( - self.partno, self.width, self.height, self.depth, self.weight, + self.item_id, self.width, self.height, self.depth, self.weight, self.position, self.rotation_type, self.getVolume() ) - def getVolume(self): - ''' ''' - return set2Decimal(self.width * self.height * self.depth, self.number_of_decimals) - + volume = self.width * self.height * self.depth + return set2Decimal(volume, self.number_of_decimals) def getMaxArea(self): - ''' ''' - a = sorted([self.width,self.height,self.depth],reverse=True) if self.updown == True else [self.width,self.height,self.depth] - - return set2Decimal(a[0] * a[1] , self.number_of_decimals) - + dimensions = [self.width, self.height, self.depth] + if self.updown: + dimensions.sort(reverse=True) + area = dimensions[0] * dimensions[1] + return set2Decimal(area, self.number_of_decimals) def getDimension(self): - ''' rotation type ''' - if self.rotation_type == RotationType.RT_WHD: - dimension = [self.width, self.height, self.depth] - elif self.rotation_type == RotationType.RT_HWD: - dimension = [self.height, self.width, self.depth] - elif self.rotation_type == RotationType.RT_HDW: - dimension = [self.height, self.depth, self.width] - elif self.rotation_type == RotationType.RT_DHW: - dimension = [self.depth, self.height, self.width] - elif self.rotation_type == RotationType.RT_DWH: - dimension = [self.depth, self.width, self.height] - elif self.rotation_type == RotationType.RT_WDH: - dimension = [self.width, self.depth, self.height] - else: - dimension = [] - - return dimension - - + ''' Rotation type ''' + rotation_dict = { + RotationType.RT_WHD: [self.width, self.height, self.depth], + RotationType.RT_HWD: [self.height, self.width, self.depth], + RotationType.RT_HDW: [self.height, self.depth, self.width], + RotationType.RT_DHW: [self.depth, self.height, self.width], + RotationType.RT_DWH: [self.depth, self.width, self.height], + RotationType.RT_WDH: [self.width, self.depth, self.height], + } + return rotation_dict.get(self.rotation_type, []) class Bin: - - def __init__(self, partno, WHD, max_weight,corner=0,put_type=1): - ''' ''' - self.partno = partno - self.width = WHD[0] - self.height = WHD[1] - self.depth = WHD[2] - self.max_weight = max_weight - self.corner = corner + def __init__(self, WHD, max_weight=10000000000000, bin_id=None, bin_name=None, corner=0, put_type=1, packer=None): + self.packer = packer if packer is not None else Packer.get_default_packer() + self.bin_id = self.generate_unique_id(bin_id) + self.width = Decimal(str(WHD[0])) + self.height = Decimal(str(WHD[1])) + self.depth = Decimal(str(WHD[2])) + self.max_weight = Decimal(str(max_weight)) + self.corner = Decimal(str(corner)) self.items = [] - self.fit_items = np.array([[0,WHD[0],0,WHD[1],0,0]]) + self.fit_items = np.array([[Decimal('0'), self.width, Decimal('0'), self.height, Decimal('0'), Decimal('0')]], dtype=object) self.unfitted_items = [] self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS - self.fix_point = False + self.fix_point = True self.check_stable = False - self.support_surface_ratio = 0 + self.support_surface_ratio = Decimal('0') self.put_type = put_type # used to put gravity distribution self.gravity = [] - + self.bin_name = bin_name if bin_name else bin_id + + def generate_unique_id(self, base_id): + ''' Generate a unique id if the base id already exists ''' + existing_bins_ids = self.packer.existing_bins_ids + if base_id and base_id not in existing_bins_ids: + existing_bins_ids.add(base_id) + return base_id + + counter = 1 + new_id = f"{base_id}{counter}" + while new_id in existing_bins_ids: + counter += 1 + new_id = f"{base_id}{counter}" + + existing_bins_ids.add(new_id) + if external_logger: + external_logger.warning(f'ID conflict for bin {base_id}. Assigned new id: {new_id}') + return new_id def formatNumbers(self, number_of_decimals): - ''' ''' self.width = set2Decimal(self.width, number_of_decimals) self.height = set2Decimal(self.height, number_of_decimals) self.depth = set2Decimal(self.depth, number_of_decimals) self.max_weight = set2Decimal(self.max_weight, number_of_decimals) + self.corner = set2Decimal(self.corner, number_of_decimals) self.number_of_decimals = number_of_decimals - def string(self): - ''' ''' return "%s(%sx%sx%s, max_weight:%s) vol(%s)" % ( - self.partno, self.width, self.height, self.depth, self.max_weight, + self.bin_id, self.width, self.height, self.depth, self.max_weight, self.getVolume() ) - def getVolume(self): - ''' ''' - return set2Decimal( - self.width * self.height * self.depth, self.number_of_decimals - ) - + volume = self.width * self.height * self.depth + return set2Decimal(volume, self.number_of_decimals) def getTotalWeight(self): - ''' ''' - total_weight = 0 + total_weight = Decimal('0') for item in self.items: total_weight += item.weight return set2Decimal(total_weight, self.number_of_decimals) - - def putItem(self, item, pivot,axis=None): - ''' put item in bin ''' + def putItem(self, item, pivot, axis=None): + ''' Put item in bin ''' fit = False - valid_item_position = item.position - item.position = pivot - rotate = RotationType.ALL if item.updown == True else RotationType.Notupdown + valid_item_position = item.position.copy() + item.position = pivot.copy() + rotate = RotationType.ALL if item.updown else RotationType.Notupdown for i in range(0, len(rotate)): item.rotation_type = i dimension = item.getDimension() - # rotatate + # Check if item exceeds bin boundaries after rotation if ( self.width < pivot[0] + dimension[0] or self.height < pivot[1] + dimension[1] or @@ -161,246 +182,367 @@ def putItem(self, item, pivot,axis=None): fit = True + # Check for intersection with items already in the bin for current_item_in_bin in self.items: if intersect(current_item_in_bin, item): fit = False break if fit: - # cal total weight + # Check if total weight exceeds bin's maximum weight if self.getTotalWeight() + item.weight > self.max_weight: fit = False return fit - - # fix point float prob - if self.fix_point == True : - - [w,h,d] = dimension - [x,y,z] = [float(pivot[0]),float(pivot[1]),float(pivot[2])] - - for i in range(3): + + # Fix positioning issues + if self.fix_point: + [w, h, d] = dimension + [x, y, z] = pivot.copy() + + for _ in range(3): # fix height - y = self.checkHeight([x,x+float(w),y,y+float(h),z,z+float(d)]) + y = self.checkHeight([x, x + w, y, y + h, z, z + d]) # fix width - x = self.checkWidth([x,x+float(w),y,y+float(h),z,z+float(d)]) + x = self.checkWidth([x, x + w, y, y + h, z, z + d]) # fix depth - z = self.checkDepth([x,x+float(w),y,y+float(h),z,z+float(d)]) + z = self.checkDepth([x, x + w, y, y + h, z, z + d]) - # check stability on item - # rule : + # Check stability of the item + # Rule: # 1. Define a support ratio, if the ratio below the support surface does not exceed this ratio, compare the second rule. # 2. If there is no support under any vertices of the bottom of the item, then fit = False. - if self.check_stable == True : - # Cal the surface area of ​​item. - item_area_lower = int(dimension[0] * dimension[1]) - # Cal the surface area of ​​the underlying support. - support_area_upper = 0 + if self.check_stable: + # Calculate the surface area of item's bottom + item_area_lower = dimension[0] * dimension[1] + # Calculate the surface area of the underlying support + support_area_upper = Decimal('0') for i in self.fit_items: # Verify that the lower support surface area is greater than the upper support surface area * support_surface_ratio. - if z == i[5] : - area = len(set([ j for j in range(int(x),int(x+int(w)))]) & set([ j for j in range(int(i[0]),int(i[1]))])) * \ - len(set([ j for j in range(int(y),int(y+int(h)))]) & set([ j for j in range(int(i[2]),int(i[3]))])) + if z == i[5]: + x_overlap = max(Decimal('0'), min(x + w, i[1]) - max(x, i[0])) + y_overlap = max(Decimal('0'), min(y + h, i[3]) - max(y, i[2])) + area = x_overlap * y_overlap support_area_upper += area - # If not , get four vertices of the bottom of the item. - if support_area_upper / item_area_lower < self.support_surface_ratio : - four_vertices = [[x,y],[x+float(w),y],[x,y+float(h)],[x+float(w),y+float(h)]] - # If any vertices is not supported, fit = False. - c = [False,False,False,False] + # If not, get four vertices of the bottom of the item + if support_area_upper / item_area_lower < self.support_surface_ratio: + four_vertices = [ + (x, y), + (x + w, y), + (x, y + h), + (x + w, y + h) + ] + # If any vertices are not supported, fit = False + c = [False, False, False, False] for i in self.fit_items: - if z == i[5] : - for jdx,j in enumerate(four_vertices) : - if (i[0] <= j[0] <= i[1]) and (i[2] <= j[1] <= i[3]) : - c[jdx] = True - if False in c : + if z == i[5]: + for idx, vertex in enumerate(four_vertices): + if (i[0] <= vertex[0] <= i[1]) and (i[2] <= vertex[1] <= i[3]): + c[idx] = True + if False in c: item.position = valid_item_position fit = False return fit - - self.fit_items = np.append(self.fit_items,np.array([[x,x+float(w),y,y+float(h),z,z+float(d)]]),axis=0) - item.position = [set2Decimal(x),set2Decimal(y),set2Decimal(z)] - if fit : + # Load-bearing capacity check + bottom_y = y # Bottom y-coordinate of the item being placed + can_support = True + tolerance = Decimal('1e-6') # Tolerance for decimal comparison + + for supporting_item in self.items: + supporting_item_top_y = supporting_item.position[1] + supporting_item.getDimension()[1] + + if abs(supporting_item_top_y - bottom_y) < tolerance: + # Check overlap in x and z axes + item_x_range = (x, x + w) + item_z_range = (z, z + d) + + supporting_x = supporting_item.position[0] + supporting_z = supporting_item.position[2] + supporting_w, _, supporting_d = supporting_item.getDimension() + supporting_x_range = (supporting_x, supporting_x + supporting_w) + supporting_z_range = (supporting_z, supporting_z + supporting_d) + + x_overlap = max(Decimal('0'), min(item_x_range[1], supporting_x_range[1]) - max(item_x_range[0], supporting_x_range[0])) + z_overlap = max(Decimal('0'), min(item_z_range[1], supporting_z_range[1]) - max(item_z_range[0], supporting_z_range[0])) + + if x_overlap > 0 and z_overlap > 0: + # Check cumulative load-bearing capacity + cumulative_weight = self.get_cumulative_weight_above(supporting_item, item) + if supporting_item.loadbear == 0 or supporting_item.loadbear < cumulative_weight: + can_support = False + break + + if not can_support: + fit = False + item.position = valid_item_position + if external_logger: + external_logger.info(f"Item {item.item_id} cannot be placed on top of items that cannot bear its cumulative load") + continue # Try next rotation or pivot + + # Record the item's position in the bin + new_fit_item = np.array([[x, x + w, y, y + h, z, z + d]], dtype=object) + self.fit_items = np.append(self.fit_items, new_fit_item, axis=0) + item.position = [set2Decimal(coord, self.number_of_decimals) for coord in [x, y, z]] + + if fit: self.items.append(copy.deepcopy(item)) - else : + else: item.position = valid_item_position return fit - else : - item.position = valid_item_position - - return fit - - - def checkDepth(self,unfix_point): - ''' fix item position z ''' - z_ = [[0,0],[float(self.depth),float(self.depth)]] + def is_directly_supported_by(self, item_above, item_below): + ''' Check if item_above is directly supported by item_below ''' + tolerance = Decimal('1e-6') + item_below_top_y = item_below.position[1] + item_below.getDimension()[1] + item_above_bottom_y = item_above.position[1] + + if abs(item_below_top_y - item_above_bottom_y) < tolerance: + # Check overlap in x and z axes + item_above_x_range = (item_above.position[0], item_above.position[0] + item_above.getDimension()[0]) + item_above_z_range = (item_above.position[2], item_above.position[2] + item_above.getDimension()[2]) + + item_below_x_range = (item_below.position[0], item_below.position[0] + item_below.getDimension()[0]) + item_below_z_range = (item_below.position[2], item_below.position[2] + item_below.getDimension()[2]) + + x_overlap = max(Decimal('0'), min(item_above_x_range[1], item_below_x_range[1]) - max(item_above_x_range[0], item_below_x_range[0])) + z_overlap = max(Decimal('0'), min(item_above_z_range[1], item_below_z_range[1]) - max(item_above_z_range[0], item_below_z_range[0])) + + if x_overlap > 0 and z_overlap > 0: + return True + return False + + def get_cumulative_weight_above(self, supporting_item, current_item): + ''' Calculate the cumulative weight of all items above supporting_item, including current_item ''' + cumulative_weight = Decimal('0') + items_to_check = [supporting_item] + visited = set() + while items_to_check: + item = items_to_check.pop() + if item in visited: + continue + visited.add(item) + for item_above in self.items + [current_item]: + if item_above == supporting_item or item_above in visited: + continue + if self.is_directly_supported_by(item_above, item): + cumulative_weight += item_above.weight + items_to_check.append(item_above) + return cumulative_weight + + def checkDepth(self, unfix_point): + ''' Fix item position z ''' + z_intervals = [[Decimal('0'), Decimal('0')], [self.depth, self.depth]] for j in self.fit_items: - # creat x set - x_bottom = set([i for i in range(int(j[0]),int(j[1]))]) - x_top = set([i for i in range(int(unfix_point[0]),int(unfix_point[1]))]) - # creat y set - y_bottom = set([i for i in range(int(j[2]),int(j[3]))]) - y_top = set([i for i in range(int(unfix_point[2]),int(unfix_point[3]))]) - # find intersection on x set and y set. - if len(x_bottom & x_top) != 0 and len(y_bottom & y_top) != 0 : - z_.append([float(j[4]),float(j[5])]) + # x intervals + x_bottom = (j[0], j[1]) + x_top = (unfix_point[0], unfix_point[1]) + # y intervals + y_bottom = (j[2], j[3]) + y_top = (unfix_point[2], unfix_point[3]) + # Check for overlap + x_overlap = max(Decimal('0'), min(x_bottom[1], x_top[1]) - max(x_bottom[0], x_top[0])) + y_overlap = max(Decimal('0'), min(y_bottom[1], y_top[1]) - max(y_bottom[0], y_top[0])) + if x_overlap > 0 and y_overlap > 0: + z_intervals.append([j[4], j[5]]) top_depth = unfix_point[5] - unfix_point[4] - # find diff set on z_. - z_ = sorted(z_, key = lambda z_ : z_[1]) - for j in range(len(z_)-1): - if z_[j+1][0] -z_[j][1] >= top_depth: - return z_[j][1] + z_intervals = sorted(z_intervals, key=lambda z_: z_[1]) + for j in range(len(z_intervals) - 1): + if z_intervals[j + 1][0] - z_intervals[j][1] >= top_depth: + return z_intervals[j][1] return unfix_point[4] - - def checkWidth(self,unfix_point): - ''' fix item position x ''' - x_ = [[0,0],[float(self.width),float(self.width)]] + def checkWidth(self, unfix_point): + ''' Fix item position x ''' + x_intervals = [[Decimal('0'), Decimal('0')], [self.width, self.width]] for j in self.fit_items: - # creat z set - z_bottom = set([i for i in range(int(j[4]),int(j[5]))]) - z_top = set([i for i in range(int(unfix_point[4]),int(unfix_point[5]))]) - # creat y set - y_bottom = set([i for i in range(int(j[2]),int(j[3]))]) - y_top = set([i for i in range(int(unfix_point[2]),int(unfix_point[3]))]) - # find intersection on z set and y set. - if len(z_bottom & z_top) != 0 and len(y_bottom & y_top) != 0 : - x_.append([float(j[0]),float(j[1])]) + # z intervals + z_bottom = (j[4], j[5]) + z_top = (unfix_point[4], unfix_point[5]) + # y intervals + y_bottom = (j[2], j[3]) + y_top = (unfix_point[2], unfix_point[3]) + # Check for overlap + z_overlap = max(Decimal('0'), min(z_bottom[1], z_top[1]) - max(z_bottom[0], z_top[0])) + y_overlap = max(Decimal('0'), min(y_bottom[1], y_top[1]) - max(y_bottom[0], y_top[0])) + if z_overlap > 0 and y_overlap > 0: + x_intervals.append([j[0], j[1]]) top_width = unfix_point[1] - unfix_point[0] - # find diff set on x_bottom and x_top. - x_ = sorted(x_,key = lambda x_ : x_[1]) - for j in range(len(x_)-1): - if x_[j+1][0] -x_[j][1] >= top_width: - return x_[j][1] + x_intervals = sorted(x_intervals, key=lambda x_: x_[1]) + for j in range(len(x_intervals) - 1): + if x_intervals[j + 1][0] - x_intervals[j][1] >= top_width: + return x_intervals[j][1] return unfix_point[0] - - def checkHeight(self,unfix_point): - '''fix item position y ''' - y_ = [[0,0],[float(self.height),float(self.height)]] + def checkHeight(self, unfix_point): + ''' Fix item position y ''' + y_intervals = [[Decimal('0'), Decimal('0')], [self.height, self.height]] for j in self.fit_items: - # creat x set - x_bottom = set([i for i in range(int(j[0]),int(j[1]))]) - x_top = set([i for i in range(int(unfix_point[0]),int(unfix_point[1]))]) - # creat z set - z_bottom = set([i for i in range(int(j[4]),int(j[5]))]) - z_top = set([i for i in range(int(unfix_point[4]),int(unfix_point[5]))]) - # find intersection on x set and z set. - if len(x_bottom & x_top) != 0 and len(z_bottom & z_top) != 0 : - y_.append([float(j[2]),float(j[3])]) + # x intervals + x_bottom = (j[0], j[1]) + x_top = (unfix_point[0], unfix_point[1]) + # z intervals + z_bottom = (j[4], j[5]) + z_top = (unfix_point[4], unfix_point[5]) + # Check for overlap + x_overlap = max(Decimal('0'), min(x_bottom[1], x_top[1]) - max(x_bottom[0], x_top[0])) + z_overlap = max(Decimal('0'), min(z_bottom[1], z_top[1]) - max(z_bottom[0], z_top[0])) + if x_overlap > 0 and z_overlap > 0: + y_intervals.append([j[2], j[3]]) top_height = unfix_point[3] - unfix_point[2] - # find diff set on y_bottom and y_top. - y_ = sorted(y_,key = lambda y_ : y_[1]) - for j in range(len(y_)-1): - if y_[j+1][0] -y_[j][1] >= top_height: - return y_[j][1] - + y_intervals = sorted(y_intervals, key=lambda y_: y_[1]) + for j in range(len(y_intervals) - 1): + if y_intervals[j + 1][0] - y_intervals[j][1] >= top_height: + return y_intervals[j][1] return unfix_point[2] - def addCorner(self): - '''add container coner ''' - if self.corner != 0 : - corner = set2Decimal(self.corner) + ''' Add container corner ''' + if self.corner != 0: corner_list = [] for i in range(8): a = Item( - partno='corner{}'.format(i), - name='corner', + item_id='corner{}'.format(i), + item_name='corner', typeof='cube', - WHD=(corner,corner,corner), - weight=0, - level=0, - loadbear=0, - updown=True, + WHD=(self.corner, self.corner, self.corner), + weight=0, + priority_level=0, + loadbear=0, + updown=True, color='#000000') corner_list.append(a) return corner_list - - def putCorner(self,info,item): - '''put coner in bin ''' - fit = False - x = set2Decimal(self.width - self.corner) - y = set2Decimal(self.height - self.corner) - z = set2Decimal(self.depth - self.corner) - pos = [[0,0,0],[0,0,z],[0,y,z],[0,y,0],[x,y,0],[x,0,0],[x,0,z],[x,y,z]] + def putCorner(self, info, item): + ''' Put corner in bin ''' + x = set2Decimal(self.width - self.corner, self.number_of_decimals) + y = set2Decimal(self.height - self.corner, self.number_of_decimals) + z = set2Decimal(self.depth - self.corner, self.number_of_decimals) + pos = [ + [Decimal('0'), Decimal('0'), Decimal('0')], + [Decimal('0'), Decimal('0'), z], + [Decimal('0'), y, z], + [Decimal('0'), y, Decimal('0')], + [x, y, Decimal('0')], + [x, Decimal('0'), Decimal('0')], + [x, Decimal('0'), z], + [x, y, z] + ] item.position = pos[info] self.items.append(item) - corner = [float(item.position[0]),float(item.position[0])+float(self.corner),float(item.position[1]),float(item.position[1])+float(self.corner),float(item.position[2]),float(item.position[2])+float(self.corner)] - - self.fit_items = np.append(self.fit_items,np.array([corner]),axis=0) - return + corner = [item.position[0], item.position[0] + self.corner, + item.position[1], item.position[1] + self.corner, + item.position[2], item.position[2] + self.corner] + corner = np.array([corner], dtype=object) + self.fit_items = np.append(self.fit_items, corner, axis=0) def clearBin(self): - ''' clear item which in bin ''' + ''' Clear items in bin ''' self.items = [] - self.fit_items = np.array([[0,self.width,0,self.height,0,0]]) - return - + self.fit_items = np.array([[Decimal('0'), self.width, Decimal('0'), self.height, Decimal('0'), Decimal('0')]], dtype=object) class Packer: + _default_packer = None - def __init__(self): - ''' ''' + def __init__(self, packer_id=None, packer_name=None): + self.existing_items_ids = set() + self.existing_bins_ids = set() + self.existing_packers_ids = set() self.bins = [] self.items = [] self.unfit_items = [] self.total_items = 0 self.binding = [] - # self.apex = [] + self.packer_id = self._generate_unique_id(packer_id) + self.packer_name = packer_name if packer_name else packer_id + if Packer._default_packer is None: + Packer._default_packer = self + if external_logger: + external_logger.info(f'Added packer: {self.packer_id}') + + @staticmethod + def get_default_packer(): + if Packer._default_packer is not None: + return Packer._default_packer + else: + raise ValueError("No default packer available. Create an instance of Packer before creating Item or Bin.") + def _generate_unique_id(self, base_id): + ''' Generate a unique id if the base id already exists ''' + if base_id and base_id not in self.existing_packers_ids: + self.existing_packers_ids.add(base_id) + return base_id - def addBin(self, bin): - ''' ''' - return self.bins.append(bin) + counter = 1 + new_id = f"{base_id}{counter}" + while new_id in self.existing_packers_ids: + counter += 1 + new_id = f"{base_id}{counter}" + self.existing_packers_ids.add(new_id) + if external_logger: + external_logger.warning(f'ID conflict for packer {base_id}. Assigned new id: {new_id}') - def addItem(self, item): - ''' ''' - self.total_items = len(self.items) + 1 + return new_id - return self.items.append(item) + def addBin(self, bin): + if external_logger: + external_logger.info(f'Bin added: {bin.bin_id} to packer {self.packer_id}') + self.bins.append(bin) + def addItem(self, item): + self.total_items = len(self.items) + 1 + if external_logger: + external_logger.info(f'Item added: {item.item_id} to packer: {self.packer_id}') + self.items.append(item) - def pack2Bin(self, bin, item,fix_point,check_stable,support_surface_ratio): - ''' pack item to bin ''' + def pack2Bin(self, bin, item, fix_point, check_stable, support_surface_ratio): + ''' Pack item into bin ''' fitted = False bin.fix_point = fix_point bin.check_stable = check_stable - bin.support_surface_ratio = support_surface_ratio + bin.support_surface_ratio = Decimal(str(support_surface_ratio)) + if external_logger: + external_logger.info(f"Attempting to pack item {item.item_id} into bin {bin.bin_id}") - # first put item on (0,0,0) , if corner exist ,first add corner in box. + if item.assigned_bin and item.assigned_bin.bin_id != bin.bin_id: + return False # Skip packing if item is assigned to a different bin + + # First put item at (0, 0, 0), if corner exists, first add corner in box if bin.corner != 0 and not bin.items: corner_lst = bin.addCorner() - for i in range(len(corner_lst)) : - bin.putCorner(i,corner_lst[i]) + for i in range(len(corner_lst)): + bin.putCorner(i, corner_lst[i]) elif not bin.items: response = bin.putItem(item, item.position) if not response: bin.unfitted_items.append(item) - return + if external_logger: + external_logger.info(f'Item: {item.item_id} does not fit in bin: {bin.bin_id}') + return False + else: + return True for axis in range(0, 3): items_in_bin = bin.items for ib in items_in_bin: - pivot = [0, 0, 0] + pivot = [Decimal('0'), Decimal('0'), Decimal('0')] w, h, d = ib.getDimension() if axis == Axis.WIDTH: - pivot = [ib.position[0] + w,ib.position[1],ib.position[2]] + pivot = [ib.position[0] + w, ib.position[1], ib.position[2]] elif axis == Axis.HEIGHT: - pivot = [ib.position[0],ib.position[1] + h,ib.position[2]] + pivot = [ib.position[0], ib.position[1] + h, ib.position[2]] elif axis == Axis.DEPTH: - pivot = [ib.position[0],ib.position[1],ib.position[2] + d] - + pivot = [ib.position[0], ib.position[1], ib.position[2] + d] + if bin.putItem(item, pivot, axis): fitted = True break @@ -408,325 +550,752 @@ def pack2Bin(self, bin, item,fix_point,check_stable,support_surface_ratio): break if not fitted: bin.unfitted_items.append(item) - - - def sortBinding(self,bin): - ''' sorted by binding ''' - b,front,back = [],[],[] - for i in range(len(self.binding)): - b.append([]) - for item in self.items: - if item.name in self.binding[i]: - b[i].append(item) - elif item.name not in self.binding: - if len(b[0]) == 0 and item not in front: - front.append(item) - elif item not in back and item not in front: - back.append(item) - - min_c = min([len(i) for i in b]) - - sort_bind =[] - for i in range(min_c): - for j in range(len(b)): - sort_bind.append(b[j][i]) - - for i in b: - for j in i: - if j not in sort_bind: - self.unfit_items.append(j) - - self.items = front + sort_bind + back - return - + if external_logger: + external_logger.info(f"Item {item.item_id} does not fit in bin {bin.bin_id}") + return fitted + + def sortBinding(self): + ''' Process binding groups according to the new specifications ''' + # Step 1: Merge groups with common items + merged_bindings = self.mergeBindingGroups() + + # Step 2: For each merged group, check assigned bins and set minimum priority + for group in merged_bindings: + assigned_bins = set() + priorities = [] + for item_id in group: + item = next((i for i in self.items if i.item_id == item_id), None) + if item: + if item.assigned_bin: + assigned_bins.add(item.assigned_bin) + priorities.append(item.priority_level) + else: + if external_logger: + external_logger.warning(f"Item {item_id} specified in binding but not found in items.") + # Check for conflicting assigned bins + if len(assigned_bins) > 1: + raise ValueError(f"Items in binding group {group} have conflicting assigned bins.") + # Assign the bin to all items in the group if any + assigned_bin = assigned_bins.pop() if assigned_bins else None + for item_id in group: + item = next((i for i in self.items if i.item_id == item_id), None) + if item: + item.assigned_bin = assigned_bin + # Set minimum priority to all items in the group + min_priority = min(priorities) if priorities else None + for item_id in group: + item = next((i for i in self.items if i.item_id == item_id), None) + if item and min_priority is not None: + item.priority_level = min_priority + + # Step 3: Reorder items so that binding groups are together + bound_items = [] + unbound_items = self.items.copy() + for group in merged_bindings: + group_items = [] + for item_id in group: + item = next((i for i in self.items if i.item_id == item_id), None) + if item: + group_items.append(item) + if item in unbound_items: + unbound_items.remove(item) + bound_items.append(group_items) + + # Flatten the list of bound items while maintaining groupings + self.items = [] + for group_items in bound_items: + self.items.extend(group_items) + self.items.extend(unbound_items) + + def mergeBindingGroups(self): + ''' Merge binding groups that have common items ''' + parent = dict() + + def find(item): + while parent[item] != item: + parent[item] = parent[parent[item]] # Path compression + item = parent[item] + return item + + def union(item1, item2): + root1 = find(item1) + root2 = find(item2) + if root1 != root2: + parent[root2] = root1 + + # Initialize parent pointers + all_items_in_bindings = set() + for group in self.binding: + for item_id in group: + parent[item_id] = item_id + all_items_in_bindings.add(item_id) + + # Union items in the same group + for group in self.binding: + for i in range(len(group) - 1): + union(group[i], group[i + 1]) + + # Collect merged groups + groups = dict() + for item_id in all_items_in_bindings: + root = find(item_id) + if root in groups: + groups[root].add(item_id) + else: + groups[root] = {item_id} + + merged_bindings = [list(group) for group in groups.values()] + return merged_bindings def putOrder(self): - '''Arrange the order of items ''' - r = [] + ''' Arrange the order of items ''' for i in self.bins: - # open top container + # Open top container if i.put_type == 2: i.items.sort(key=lambda item: item.position[0], reverse=False) i.items.sort(key=lambda item: item.position[1], reverse=False) i.items.sort(key=lambda item: item.position[2], reverse=False) - # general container + # General container elif i.put_type == 1: i.items.sort(key=lambda item: item.position[1], reverse=False) i.items.sort(key=lambda item: item.position[2], reverse=False) i.items.sort(key=lambda item: item.position[0], reverse=False) - else : + else: pass return + def gravityCenter(self, bin): + '''Deviation Of Cargo gravity distribution''' + w = bin.width + h = bin.height + + # Define areas using decimal.Decimal + half_w = w / 2 + half_h = h / 2 + + areas = [ + {'x_range': (Decimal('0'), half_w), 'y_range': (Decimal('0'), half_h), 'weight': Decimal('0')}, + {'x_range': (half_w, w), 'y_range': (Decimal('0'), half_h), 'weight': Decimal('0')}, + {'x_range': (Decimal('0'), half_w), 'y_range': (half_h, h), 'weight': Decimal('0')}, + {'x_range': (half_w, w), 'y_range': (half_h, h), 'weight': Decimal('0')} + ] + + for item in bin.items: + x_start = item.position[0] + y_start = item.position[1] + dimension = item.getDimension() + x_end = x_start + dimension[0] + y_end = y_start + dimension[1] - def gravityCenter(self,bin): - ''' - Deviation Of Cargo gravity distribution - ''' - w = int(bin.width) - h = int(bin.height) - d = int(bin.depth) - - area1 = [set(range(0,w//2+1)),set(range(0,h//2+1)),0] - area2 = [set(range(w//2+1,w+1)),set(range(0,h//2+1)),0] - area3 = [set(range(0,w//2+1)),set(range(h//2+1,h+1)),0] - area4 = [set(range(w//2+1,w+1)),set(range(h//2+1,h+1)),0] - area = [area1,area2,area3,area4] - - for i in bin.items: - - x_st = int(i.position[0]) - y_st = int(i.position[1]) - if i.rotation_type == 0: - x_ed = int(i.position[0] + i.width) - y_ed = int(i.position[1] + i.height) - elif i.rotation_type == 1: - x_ed = int(i.position[0] + i.height) - y_ed = int(i.position[1] + i.width) - elif i.rotation_type == 2: - x_ed = int(i.position[0] + i.height) - y_ed = int(i.position[1] + i.depth) - elif i.rotation_type == 3: - x_ed = int(i.position[0] + i.depth) - y_ed = int(i.position[1] + i.height) - elif i.rotation_type == 4: - x_ed = int(i.position[0] + i.depth) - y_ed = int(i.position[1] + i.width) - elif i.rotation_type == 5: - x_ed = int(i.position[0] + i.width) - y_ed = int(i.position[1] + i.depth) - - x_set = set(range(x_st,int(x_ed)+1)) - y_set = set(range(y_st,y_ed+1)) - - # cal gravity distribution - for j in range(len(area)): - if x_set.issubset(area[j][0]) and y_set.issubset(area[j][1]) : - area[j][2] += int(i.weight) - break - # include x and !include y - elif x_set.issubset(area[j][0]) == True and y_set.issubset(area[j][1]) == False and len(y_set & area[j][1]) != 0 : - y = len(y_set & area[j][1]) / (y_ed - y_st) * int(i.weight) - area[j][2] += y - if j >= 2 : - area[j-2][2] += (int(i.weight) - x) - else : - area[j+2][2] += (int(i.weight) - y) - break - # include y and !include x - elif x_set.issubset(area[j][0]) == False and y_set.issubset(area[j][1]) == True and len(x_set & area[j][0]) != 0 : - x = len(x_set & area[j][0]) / (x_ed - x_st) * int(i.weight) - area[j][2] += x - if j >= 2 : - area[j-2][2] += (int(i.weight) - x) - else : - area[j+2][2] += (int(i.weight) - x) - break - # !include x and !include y - elif x_set.issubset(area[j][0])== False and y_set.issubset(area[j][1]) == False and len(y_set & area[j][1]) != 0 and len(x_set & area[j][0]) != 0 : - all = (y_ed - y_st) * (x_ed - x_st) - y = len(y_set & area[0][1]) - y_2 = y_ed - y_st - y - x = len(x_set & area[0][0]) - x_2 = x_ed - x_st - x - area[0][2] += x * y / all * int(i.weight) - area[1][2] += x_2 * y / all * int(i.weight) - area[2][2] += x * y_2 / all * int(i.weight) - area[3][2] += x_2 * y_2 / all * int(i.weight) - break - - r = [area[0][2],area[1][2],area[2][2],area[3][2]] - result = [] - for i in r : - result.append(round(i / sum(r) * 100,2)) - return result + item_area = (x_end - x_start) * (y_end - y_start) + item_weight = item.weight + + for area in areas: + x_overlap = max(Decimal('0'), min(area['x_range'][1], x_end) - max(area['x_range'][0], x_start)) + y_overlap = max(Decimal('0'), min(area['y_range'][1], y_end) - max(area['y_range'][0], y_start)) + overlap_area = x_overlap * y_overlap + + if overlap_area > 0: + weight_contribution = (overlap_area / item_area) * item_weight + area['weight'] += weight_contribution + total_weight = sum(area['weight'] for area in areas) + if total_weight == 0: + return [0, 0, 0, 0] # No items in the bin - def pack(self, bigger_first=False,distribute_items=True,fix_point=True,check_stable=True,support_surface_ratio=0.75,binding=[],number_of_decimals=DEFAULT_NUMBER_OF_DECIMALS): - '''pack master func ''' - # set decimals + result = [float((area['weight'] / total_weight) * 100) for area in areas] + return result + + def pack(self, bigger_first=False, distribute_items=True, fix_point=True, check_stable=True, support_surface_ratio=0.75, binding=[], number_of_decimals=DEFAULT_NUMBER_OF_DECIMALS): + '''Pack items into bins with binding considerations.''' + # Set the number of decimals for measurements for bin in self.bins: bin.formatNumbers(number_of_decimals) for item in self.items: item.formatNumbers(number_of_decimals) - # add binding attribute + + # Add the binding attribute self.binding = binding - # Bin : sorted by volumn - self.bins.sort(key=lambda bin: bin.getVolume(), reverse=bigger_first) - # Item : sorted by volumn -> sorted by loadbear -> sorted by level -> binding - self.items.sort(key=lambda item: item.getVolume(), reverse=bigger_first) - # self.items.sort(key=lambda item: item.getMaxArea(), reverse=bigger_first) - self.items.sort(key=lambda item: item.loadbear, reverse=True) - self.items.sort(key=lambda item: item.level, reverse=False) - # sorted by binding + + # Process binding groups if binding != []: - self.sortBinding(bin) - - for idx,bin in enumerate(self.bins): - # pack item to bin - for item in self.items: - self.pack2Bin(bin, item, fix_point, check_stable, support_surface_ratio) - - if binding != []: - # resorted - self.items.sort(key=lambda item: item.getVolume(), reverse=bigger_first) - self.items.sort(key=lambda item: item.loadbear, reverse=True) - self.items.sort(key=lambda item: item.level, reverse=False) - # clear bin - bin.items = [] - bin.unfitted_items = self.unfit_items - bin.fit_items = np.array([[0,bin.width,0,bin.height,0,0]]) - # repacking - for item in self.items: - self.pack2Bin(bin, item,fix_point,check_stable,support_surface_ratio) - - # Deviation Of Cargo Gravity Center - self.bins[idx].gravity = self.gravityCenter(bin) - - if distribute_items : - for bitem in bin.items: - no = bitem.partno - for item in self.items : - if item.partno == no : - self.items.remove(item) + self.sortBinding() + + # Sort bins by volume + self.bins.sort(key=lambda bin: bin.getVolume(), reverse=bigger_first) + + # Sort items by priority_level, loadbear, and volume + self.items.sort(key=lambda item: ( + item.priority_level, # Ascending priority_level + -item.loadbear, # Descending loadbear + -item.getVolume() if bigger_first else item.getVolume() # Volume + )) + + # Prepare a set to track already packed items + packed_items = set() + unfit_binding_groups = [] + + # Initialize the list of unfit items + self.unfit_items = [] + + # Create a dictionary of items for easy lookup + items_dict = {item.item_id: item for item in self.items} + + # Process binding groups + if self.binding != []: + for group in self.binding: + group_items = [items_dict[item_id] for item_id in group if item_id in items_dict] + group_packed = False + + # Try to pack the group into each bin + for bin in self.bins: + # Check if items have assigned bins and if they match the current bin + assigned_bins = set(item.assigned_bin.bin_id for item in group_items if item.assigned_bin) + if assigned_bins and bin.bin_id not in assigned_bins: + continue # Skip bin if it doesn't match assigned bin + + # Set the assigned bin for all items in the group + for item in group_items: + item.assigned_bin = bin + + # Save the current state of the bin for rollback + bin_items_backup = bin.items.copy() + bin_fit_items_backup = bin.fit_items.copy() + bin_unfitted_items_backup = bin.unfitted_items.copy() + + # Try to pack the group + group_fitted = True + for item in group_items: + if not self.pack2Bin(bin, item, fix_point, check_stable, support_surface_ratio): + group_fitted = False break - # put order of items + if group_fitted: + packed_items.update(item.item_id for item in group_items) + group_packed = True + break # Group packed successfully, move to next group + else: + # Restore bin state and try next bin + bin.items = bin_items_backup + bin.fit_items = bin_fit_items_backup + bin.unfitted_items = bin_unfitted_items_backup + + if not group_packed: + # Group couldn't be packed into any bin + unfit_binding_groups.extend(group_items) + if external_logger: + external_logger.info(f"Group {group} could not be packed into any bin") + + # After processing binding groups, pack unbound items + for bin in self.bins: + # Set bin properties + bin.fix_point = fix_point + bin.check_stable = check_stable + bin.support_surface_ratio = Decimal(str(support_surface_ratio)) + + # Get items not yet packed and not in unfit binding groups + items_to_pack = [item for item in self.items if item.item_id not in packed_items and item not in unfit_binding_groups] + + # Sort remaining items + items_to_pack.sort(key=lambda item: ( + item.priority_level, # Ascending priority_level + -item.loadbear, # Descending loadbear + -item.getVolume() if bigger_first else item.getVolume() # Volume + )) + + # Try to pack unbound items + for item in items_to_pack: + if item.assigned_bin and item.assigned_bin.bin_id != bin.bin_id: + if external_logger: + external_logger.info(f'Item {item.item_id} (assigned to bin {item.assigned_bin.bin_id}) does not match bin {bin.bin_id}') + continue # Skip items assigned to another bin + if self.pack2Bin(bin, item, fix_point, check_stable, support_surface_ratio): + packed_items.add(item.item_id) + else: + bin.unfitted_items.append(item) + if external_logger: + external_logger.info(f"Item {item.item_id} does not fit in bin {bin.bin_id}") + + # Calculate the cargo gravity center + bin.gravity = self.gravityCenter(bin) + + # Add unfit binding groups to unfit items + self.unfit_items.extend(unfit_binding_groups) + + # Add any remaining unfit items + remaining_unpacked_items = [item for item in self.items if item.item_id not in packed_items and item not in unfit_binding_groups] + self.unfit_items.extend(remaining_unpacked_items) + + # Remove packed items from self.items if distribute_items is True + if distribute_items: + self.items = [item for item in self.items if item.item_id not in packed_items] + + # Arrange the order of items self.putOrder() - if self.items != []: - self.unfit_items = copy.deepcopy(self.items) - self.items = [] - # for item in self.items.copy(): - # if item in bin.unfitted_items: - # self.items.remove(item) + @staticmethod + def packAllPackers(packers, bigger_first=False, distribute_items=True, fix_point=True, check_stable=True, + support_surface_ratio=0.75, binding=[], number_of_decimals=DEFAULT_NUMBER_OF_DECIMALS): + '''Pack all items in all packers provided.''' + for packer in packers: + packer.pack(bigger_first=bigger_first, + distribute_items=distribute_items, + fix_point=fix_point, + check_stable=check_stable, + support_surface_ratio=support_surface_ratio, + binding=binding, + number_of_decimals=number_of_decimals) + if external_logger: + external_logger.info(f"All packers have been packed.") +class Painter: + def __init__(self, bin): + self.items = bin.items + self.width = float(bin.width) + self.height = float(bin.height) + self.depth = float(bin.depth) -class Painter: + def plotBoxAndItems(self, title="", alpha=0.2, write_name=True, fontsize=10, alpha_proportional=False, top_face_proportional=False, show_edges=True): + """Plot the Bin and the items it contains.""" + fig = go.Figure() + + # Plot bin as wireframe + self._plotBinWireframe(fig, 0.0, 0.0, 0.0, self.width, self.height, self.depth, color='black') + + # Find max weight for proportional alpha + max_weight = max([item.weight for item in self.items]) if len(self.items) > 0 else Decimal('1') - def __init__(self,bins): - ''' ''' - self.items = bins.items - self.width = bins.width - self.height = bins.height - self.depth = bins.depth - - - def _plotCube(self, ax, x, y, z, dx, dy, dz, color='red',mode=2,linewidth=1,text="",fontsize=15,alpha=0.5): - """ Auxiliary function to plot a cube. code taken somewhere from the web. """ - xx = [x, x, x+dx, x+dx, x] - yy = [y, y+dy, y+dy, y, y] - - kwargs = {'alpha': 1, 'color': color,'linewidth':linewidth } - if mode == 1 : - ax.plot3D(xx, yy, [z]*5, **kwargs) - ax.plot3D(xx, yy, [z+dz]*5, **kwargs) - ax.plot3D([x, x], [y, y], [z, z+dz], **kwargs) - ax.plot3D([x, x], [y+dy, y+dy], [z, z+dz], **kwargs) - ax.plot3D([x+dx, x+dx], [y+dy, y+dy], [z, z+dz], **kwargs) - ax.plot3D([x+dx, x+dx], [y, y], [z, z+dz], **kwargs) - else : - p = Rectangle((x,y),dx,dy,fc=color,ec='black',alpha = alpha) - p2 = Rectangle((x,y),dx,dy,fc=color,ec='black',alpha = alpha) - p3 = Rectangle((y,z),dy,dz,fc=color,ec='black',alpha = alpha) - p4 = Rectangle((y,z),dy,dz,fc=color,ec='black',alpha = alpha) - p5 = Rectangle((x,z),dx,dz,fc=color,ec='black',alpha = alpha) - p6 = Rectangle((x,z),dx,dz,fc=color,ec='black',alpha = alpha) - ax.add_patch(p) - ax.add_patch(p2) - ax.add_patch(p3) - ax.add_patch(p4) - ax.add_patch(p5) - ax.add_patch(p6) - - if text != "": - ax.text( (x+ dx/2), (y+ dy/2), (z+ dz/2), str(text),color='black', fontsize=fontsize, ha='center', va='center') - - art3d.pathpatch_2d_to_3d(p, z=z, zdir="z") - art3d.pathpatch_2d_to_3d(p2, z=z+dz, zdir="z") - art3d.pathpatch_2d_to_3d(p3, z=x, zdir="x") - art3d.pathpatch_2d_to_3d(p4, z=x + dx, zdir="x") - art3d.pathpatch_2d_to_3d(p5, z=y, zdir="y") - art3d.pathpatch_2d_to_3d(p6, z=y + dy, zdir="y") - - - def _plotCylinder(self, ax, x, y, z, dx, dy, dz, color='red',mode=2,text="",fontsize=10,alpha=0.2): - """ Auxiliary function to plot a Cylinder """ - # plot the two circles above and below the cylinder - p = Circle((x+dx/2,y+dy/2),radius=dx/2,color=color,alpha=0.5) - p2 = Circle((x+dx/2,y+dy/2),radius=dx/2,color=color,alpha=0.5) - ax.add_patch(p) - ax.add_patch(p2) - art3d.pathpatch_2d_to_3d(p, z=z, zdir="z") - art3d.pathpatch_2d_to_3d(p2, z=z+dz, zdir="z") - # plot a circle in the middle of the cylinder - center_z = np.linspace(0, dz, 10) - theta = np.linspace(0, 2*np.pi, 10) - theta_grid, z_grid=np.meshgrid(theta, center_z) - x_grid = dx / 2 * np.cos(theta_grid) + x + dx / 2 - y_grid = dy / 2 * np.sin(theta_grid) + y + dy / 2 - z_grid = z_grid + z - ax.plot_surface(x_grid, y_grid, z_grid,shade=False,fc=color,alpha=alpha,color=color) - if text != "" : - ax.text( (x+ dx/2), (y+ dy/2), (z+ dz/2), str(text),color='black', fontsize=fontsize, ha='center', va='center') - - def plotBoxAndItems(self,title="",alpha=0.2,write_num=False,fontsize=10): - """ side effective. Plot the Bin and the items it contains. """ - fig = plt.figure() - axGlob = plt.axes(projection='3d') - - # plot bin - self._plotCube(axGlob,0, 0, 0, float(self.width), float(self.height), float(self.depth),color='black',mode=1,linewidth=2,text="") - - counter = 0 - # fit rotation type for item in self.items: - rt = item.rotation_type - x,y,z = item.position - [w,h,d] = item.getDimension() + x, y, z = [float(coord) for coord in item.position] + w, h, d = [float(dim) for dim in item.getDimension()] color = item.color - text= item.partno if write_num else "" + text = item.item_id if write_name and 'corner' not in item.item_name else "" + + # Calculate alpha and top_alpha + if alpha_proportional: + alpha = float(item.weight / max_weight) if item.weight is not None else alpha + else: + alpha = alpha + if top_face_proportional: + top_alpha = 1 - float(item.loadbear / max_weight) + top_alpha = max(0, min(top_alpha, 1)) # Ensure alpha is between 0 and 1 + else: + top_alpha = alpha if item.typeof == 'cube': - # plot item of cube - self._plotCube(axGlob, float(x), float(y), float(z), float(w),float(h),float(d),color=color,mode=2,text=text,fontsize=fontsize,alpha=alpha) + # Plot the cube with optional top face adjustment + self._plotCube(fig, x, y, z, w, h, d, + color=color, opacity=alpha, text=text, fontsize=fontsize, + show_edges=show_edges, item_name=item.item_name, top_alpha=top_alpha, top_face_proportional=top_face_proportional) elif item.typeof == 'cylinder': - # plot item of cylinder - self._plotCylinder(axGlob, float(x), float(y), float(z), float(w),float(h),float(d),color=color,mode=2,text=text,fontsize=fontsize,alpha=alpha) - - counter = counter + 1 - - - plt.title(title) - self.setAxesEqual(axGlob) - return plt - - - def setAxesEqual(self,ax): - '''Make axes of 3D plot have equal scale so that spheres appear as spheres, - cubes as cubes, etc.. This is one possible solution to Matplotlib's - ax.set_aspect('equal') and ax.axis('equal') not working for 3D. - - Input - ax: a matplotlib axis, e.g., as output from plt.gca().''' - x_limits = ax.get_xlim3d() - y_limits = ax.get_ylim3d() - z_limits = ax.get_zlim3d() - - x_range = abs(x_limits[1] - x_limits[0]) - x_middle = np.mean(x_limits) - y_range = abs(y_limits[1] - y_limits[0]) - y_middle = np.mean(y_limits) - z_range = abs(z_limits[1] - z_limits[0]) - z_middle = np.mean(z_limits) - - # The plot bounding box is a sphere in the sense of the infinity - # norm, hence I call half the max range the plot radius. - plot_radius = 0.5 * max([x_range, y_range, z_range]) - - ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) - ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) - ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) + # Plot cylinder if applicable + self._plotCylinder(fig, x, y, z, w, h, d, + color=color, opacity=alpha, text=text, fontsize=fontsize, + show_edges=show_edges, item_name=item.item_name, top_alpha=top_alpha, top_face_proportional=top_face_proportional) + else: + if external_logger: + external_logger.warning(f'Item {item.item_id} has an invalid type: {item.typeof}') + external_logger.warning(f'Item {item.item_id} will not be plotted') + + # Configure plot layout + fig.update_layout( + title=title, + scene=dict( + xaxis_title='X Axis', + yaxis_title='Y Axis', + zaxis_title='Z Axis', + aspectmode='data' + ), + autosize=True, # Ensure it resizes automatically + template='plotly_white' # Optional, improves aesthetics + ) + fig.show(config={"displayModeBar": True, "responsive": True}) + + return fig # Return the generated figure + + @staticmethod + def plotAllBinsSeparately(bins, title_prefix="", alpha=0.2, write_name=True, fontsize=10, + alpha_proportional=False, top_face_proportional=False, show_edges=True): + """Plot all bins separately.""" + for idx, bin in enumerate(bins): + title = f"{title_prefix} Bin {idx + 1}: {bin.bin_id}" + painter = Painter(bin) + painter.plotBoxAndItems( + title=title, + alpha=alpha, + write_name=write_name, + fontsize=fontsize, + alpha_proportional=alpha_proportional, + top_face_proportional=top_face_proportional, + show_edges=show_edges + ) + if external_logger: + external_logger.info(f"All bins have been plotted separately.") + + def _plotBinWireframe(self, fig, x, y, z, dx, dy, dz, color='black'): + """ Auxiliary function to plot a wireframe cube for the bin. """ + # Define the vertices of the cube + vertices = [ + [x, y, z], + [x+dx, y, z], + [x+dx, y+dy, z], + [x, y+dy, z], + [x, y, z+dz], + [x+dx, y, z+dz], + [x+dx, y+dy, z+dz], + [x, y+dy, z+dz] + ] + + # Define the 12 lines (edges) of the cube + edges = [ + [vertices[0], vertices[1], 'Lower north edge'], [vertices[1], vertices[2], 'Lower east edge'], [vertices[2], vertices[3], 'Lower south edge'], [vertices[3], vertices[0], 'Lower west edge'], + [vertices[4], vertices[5], 'Upper north edge'], [vertices[5], vertices[6], 'Upper east edge'], [vertices[6], vertices[7], 'Upper south edge'], [vertices[7], vertices[4], 'Upper west edge'], + [vertices[0], vertices[4], 'North vertical edge'], [vertices[1], vertices[5], 'East vertical edge'], [vertices[2], vertices[6], 'South vertical edge'], [vertices[3], vertices[7], 'West vertical edge'] + ] + + # Add the edges to the plot + for edge in edges: + fig.add_trace(go.Scatter3d( + x=[edge[0][0], edge[1][0]], + y=[edge[0][1], edge[1][1]], + z=[edge[0][2], edge[1][2]], + mode='lines', + line=dict(color=color, width=2), + name=f'Bin - {edge[2]}', + legendgroup='bin', + showlegend=False + )) + + def _plotCube(self, fig, x, y, z, dx, dy, dz, color='red', opacity=0.5, text="", fontsize=10, show_edges=True, item_name="", top_alpha=None, top_face_proportional=False): + """ Auxiliary function to plot a cube with optional top face transparency adjustment. """ + if top_alpha is None: + top_alpha = opacity # Default to opacity if not provided + + # Define the vertices of the cube + vertices = [ + [x, y, z], # 0 + [x+dx, y, z], # 1 + [x+dx, y+dy, z], # 2 + [x, y+dy, z], # 3 + [x, y, z+dz], # 4 + [x+dx, y, z+dz], # 5 + [x+dx, y+dy, z+dz], # 6 + [x, y+dy, z+dz] # 7 + ] + + # Prepare the faces of the cube + if top_face_proportional: + # Exclude the top face + faces = [] + # Bottom face + faces.extend([ + (0, 1, 2), (0, 2, 3) + ]) + # Front face + faces.extend([ + (3, 2, 6), (3, 6, 7) + ]) + # Back face + faces.extend([ + (0, 1, 5), (0, 5, 4) + ]) + # Left face + faces.extend([ + (0, 3, 7), (0, 7, 4) + ]) + # Right face + faces.extend([ + (1, 2, 6), (1, 6, 5) + ]) + else: + # Include all faces + faces = [] + faces.extend([ + (0, 1, 2), (0, 2, 3), # Bottom face + (4, 5, 6), (4, 6, 7), # Top face + (3, 2, 6), (3, 6, 7), # Front face + (0, 1, 5), (0, 5, 4), # Back face + (0, 3, 7), (0, 7, 4), # Left face + (1, 2, 6), (1, 6, 5) # Right face + ]) + + # Create indices for the faces + i = [face[0] for face in faces] + j = [face[1] for face in faces] + k = [face[2] for face in faces] + + # Create a 3D mesh for the cube without the top face if top_face_proportional is True + fig.add_trace(go.Mesh3d( + x=[v[0] for v in vertices], + y=[v[1] for v in vertices], + z=[v[2] for v in vertices], + i=i, + j=j, + k=k, + color=color, + opacity=opacity, + hovertext=text, + hoverinfo='text', + name=item_name, + legendgroup=item_name, + showlegend=True + )) + + # Optionally add the edges of the cube + if show_edges: + edges = [ + [vertices[0], vertices[1]], [vertices[1], vertices[2]], [vertices[2], vertices[3]], [vertices[3], vertices[0]], + [vertices[4], vertices[5]], [vertices[5], vertices[6]], [vertices[6], vertices[7]], [vertices[7], vertices[4]], + [vertices[0], vertices[4]], [vertices[1], vertices[5]], [vertices[2], vertices[6]], [vertices[3], vertices[7]] + ] + for edge in edges: + fig.add_trace(go.Scatter3d( + x=[edge[0][0], edge[1][0]], + y=[edge[0][1], edge[1][1]], + z=[edge[0][2], edge[1][2]], + mode='lines', + line=dict(color='black', width=1), + name=f'{item_name} edge', + legendgroup=item_name, + showlegend=False + )) + + # Add the top face separately if top_face_proportional is True + if top_face_proportional: + fig.add_trace(go.Surface( + x=[[x, x+dx], [x, x+dx]], + y=[[y, y], [y+dy, y+dy]], + z=[[z+dz, z+dz], [z+dz, z+dz]], + opacity=top_alpha, + colorscale=[[0, color], [1, color]], # Single color + showscale=False, # No color scale + hoverinfo='skip', # No hover info for the face + name=f'{item_name} top face', + legendgroup=item_name, + showlegend=False + )) + elif top_alpha != opacity: + # Add the top face with adjusted opacity if needed + fig.add_trace(go.Surface( + x=[[x, x+dx], [x, x+dx]], + y=[[y, y], [y+dy, y+dy]], + z=[[z+dz, z+dz], [z+dz, z+dz]], + opacity=top_alpha, + colorscale=[[0, color], [1, color]], # Single color + showscale=False, # No color scale + hoverinfo='skip', # No hover info for the face + name=f'{item_name} top face', + legendgroup=item_name, + showlegend=False + )) + + # Add optional text label + if text: + # Calculate the center position of the cube + x_center = x + dx / 2 + y_center = y + dy / 2 + z_center = z + dz / 2 + + fig.add_trace(go.Scatter3d( + x=[x_center], + y=[y_center], + z=[z_center], + mode='text', + text=[text], + textfont=dict(size=fontsize, color="Black"), + hoverinfo='skip', + showlegend=False + )) + + def _plotCylinder(self, fig, x, y, z, dx, dy, dz, color='red', opacity=0.5, text="", fontsize=10, show_edges=True, item_name="", top_alpha=None, top_face_proportional=False): + """ Auxiliary function to plot a cylinder using parametric representation. """ + + if top_alpha is None: + top_alpha = opacity # Default to opacity if not provided + + # Radius and height for the cylinder + radius = min(dx, dy) / 2 + height = dz + + # Center of the cylinder + x_center = x + dx / 2 + y_center = y + dy / 2 + + # Parametrize the cylinder surface (without top face if needed) + x_surface, y_surface, z_surface = self.cylinder(radius, height, a=z, include_top=not top_face_proportional) + x_surface += x_center + y_surface += y_center + + # Add cylinder surface + fig.add_trace(go.Surface( + x=x_surface, + y=y_surface, + z=z_surface, + colorscale=[[0, color], [1, color]], + opacity=opacity, + showscale=False, + hoverinfo='skip', + name=item_name, + legendgroup=item_name, + showlegend=True + )) + + # Plot bottom face + xb_low, yb_low, zb_low = self.disk(radius, z) + xb_low += x_center + yb_low += y_center + + fig.add_trace(go.Surface( + x=xb_low, + y=yb_low, + z=zb_low, + colorscale=[[0, color], [1, color]], + opacity=opacity, + showscale=False, + hoverinfo='skip', + name=f'{item_name} bottom face', + legendgroup=item_name, + showlegend=False + )) + + # Add the top face separately if needed + if top_face_proportional: + # Plot top face with adjusted opacity + xb_up, yb_up, zb_up = self.disk(radius, z + dz) + xb_up += x_center + yb_up += y_center + + fig.add_trace(go.Surface( + x=xb_up, + y=yb_up, + z=zb_up, + colorscale=[[0, color], [1, color]], + opacity=top_alpha, + showscale=False, + hoverinfo='skip', + name=f'{item_name} top face', + legendgroup=item_name, + showlegend=False + )) + elif top_alpha != opacity: + # Plot top face with specified top_alpha + xb_up, yb_up, zb_up = self.disk(radius, z + dz) + xb_up += x_center + yb_up += y_center + + fig.add_trace(go.Surface( + x=xb_up, + y=yb_up, + z=zb_up, + colorscale=[[0, color], [1, color]], + opacity=top_alpha, + showscale=False, + hoverinfo='skip', + name=f'{item_name} top face', + legendgroup=item_name, + showlegend=False + )) + else: + # Plot top face as part of the cylinder surface (already plotted) + pass # No action needed + + # Plot edges if requested + if show_edges: + # Boundary circles at top and bottom + xb_low_edge, yb_low_edge, zb_low_edge = self.boundary_circle(radius, z) + xb_up_edge, yb_up_edge, zb_up_edge = self.boundary_circle(radius, z + dz) + xb_low_edge += x_center + yb_low_edge += y_center + xb_up_edge += x_center + yb_up_edge += y_center + + # Bottom edge + fig.add_trace(go.Scatter3d( + x=xb_low_edge, + y=yb_low_edge, + z=zb_low_edge, + mode='lines', + line=dict(color="black", width=1), + opacity=1, + hoverinfo='skip', + name=f'{item_name} bottom edge', + legendgroup=item_name, + showlegend=False + )) + + # Top edge + fig.add_trace(go.Scatter3d( + x=xb_up_edge, + y=yb_up_edge, + z=zb_up_edge, + mode='lines', + line=dict(color="black", width=1), + opacity=1, + hoverinfo='skip', + name=f'{item_name} top edge', + legendgroup=item_name, + showlegend=False + )) + + # Add optional text label + if text: + fig.add_trace(go.Scatter3d( + x=[x_center], + y=[y_center], + z=[z + dz / 2], + mode='text', + text=[text], + textfont=dict(size=fontsize, color="Black"), + hoverinfo='skip', + showlegend=False + )) + + # Modifica della funzione di parametrizzazione del cilindro + @staticmethod + def cylinder(r, h, a=0, nt=100, nv=50, include_top=True): + """ + Parametrize the cylinder of radius r, height h, base at z=a. + If include_top is False, the top surface is not included. + """ + theta = np.linspace(0, 2 * np.pi, nt) + v = np.linspace(a, a + h, nv) + theta_grid, v_grid = np.meshgrid(theta, v) + x = r * np.cos(theta_grid) + y = r * np.sin(theta_grid) + z = v_grid + + if not include_top: + # Exclude the top layer of z values + mask = z < (a + h) + x = np.where(mask, x, np.nan) + y = np.where(mask, y, np.nan) + z = np.where(mask, z, np.nan) + + return x, y, z + + @staticmethod + def boundary_circle(r, h, nt=100): + """ + Parametrize the circle at height h with radius r. + """ + theta = np.linspace(0, 2 * np.pi, nt) + x = r * np.cos(theta) + y = r * np.sin(theta) + z = h * np.ones(theta.shape) + return x, y, z + + @staticmethod + def disk(r, h, nr=50, nt=100): + """ + Generate x, y, z coordinates for a filled disk (circle) at height h. + """ + theta = np.linspace(0, 2*np.pi, nt) + radius = np.linspace(0, r, nr) + radius_grid, theta_grid = np.meshgrid(radius, theta) + x = radius_grid * np.cos(theta_grid) + y = radius_grid * np.sin(theta_grid) + z = h * np.ones_like(x) + return x, y, z \ No newline at end of file diff --git a/py3dbp/main_original.py b/py3dbp/main_original.py new file mode 100644 index 0000000..2618528 --- /dev/null +++ b/py3dbp/main_original.py @@ -0,0 +1,731 @@ +from .constants import RotationType, Axis +from .auxiliary_methods import intersect, set2Decimal +import numpy as np +# required to plot a representation of Bin and contained items +from matplotlib.patches import Rectangle,Circle +import matplotlib.pyplot as plt +import mpl_toolkits.mplot3d.art3d as art3d +from collections import Counter +import copy +DEFAULT_NUMBER_OF_DECIMALS = 0 +START_POSITION = [0, 0, 0] + + + +class Item: + + def __init__(self, partno,name,typeof, WHD, weight, level, loadbear, updown, color): + ''' ''' + self.partno = partno + self.name = name + self.typeof = typeof + self.width = WHD[0] + self.height = WHD[1] + self.depth = WHD[2] + self.weight = weight + # Packing Priority level ,choose 1-3 + self.level = level + # loadbear + self.loadbear = loadbear + # Upside down? True or False + self.updown = updown if typeof == 'cube' else False + # Draw item color + self.color = color + self.rotation_type = 0 + self.position = START_POSITION + self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS + + + def formatNumbers(self, number_of_decimals): + ''' ''' + self.width = set2Decimal(self.width, number_of_decimals) + self.height = set2Decimal(self.height, number_of_decimals) + self.depth = set2Decimal(self.depth, number_of_decimals) + self.weight = set2Decimal(self.weight, number_of_decimals) + self.number_of_decimals = number_of_decimals + + + def string(self): + ''' ''' + return "%s(%sx%sx%s, weight: %s) pos(%s) rt(%s) vol(%s)" % ( + self.partno, self.width, self.height, self.depth, self.weight, + self.position, self.rotation_type, self.getVolume() + ) + + + def getVolume(self): + ''' ''' + return set2Decimal(self.width * self.height * self.depth, self.number_of_decimals) + + + def getMaxArea(self): + ''' ''' + a = sorted([self.width,self.height,self.depth],reverse=True) if self.updown == True else [self.width,self.height,self.depth] + + return set2Decimal(a[0] * a[1] , self.number_of_decimals) + + + def getDimension(self): + ''' rotation type ''' + if self.rotation_type == RotationType.RT_WHD: + dimension = [self.width, self.height, self.depth] + elif self.rotation_type == RotationType.RT_HWD: + dimension = [self.height, self.width, self.depth] + elif self.rotation_type == RotationType.RT_HDW: + dimension = [self.height, self.depth, self.width] + elif self.rotation_type == RotationType.RT_DHW: + dimension = [self.depth, self.height, self.width] + elif self.rotation_type == RotationType.RT_DWH: + dimension = [self.depth, self.width, self.height] + elif self.rotation_type == RotationType.RT_WDH: + dimension = [self.width, self.depth, self.height] + else: + dimension = [] + + return dimension + + + +class Bin: + + def __init__(self, partno, WHD, max_weight,corner=0,put_type=1): + ''' ''' + self.partno = partno + self.width = WHD[0] + self.height = WHD[1] + self.depth = WHD[2] + self.max_weight = max_weight + self.corner = corner + self.items = [] + self.fit_items = np.array([[0,WHD[0],0,WHD[1],0,0]]) + self.unfitted_items = [] + self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS + self.fix_point = False + self.check_stable = False + self.support_surface_ratio = 0 + self.put_type = put_type + # used to put gravity distribution + self.gravity = [] + + + def formatNumbers(self, number_of_decimals): + ''' ''' + self.width = set2Decimal(self.width, number_of_decimals) + self.height = set2Decimal(self.height, number_of_decimals) + self.depth = set2Decimal(self.depth, number_of_decimals) + self.max_weight = set2Decimal(self.max_weight, number_of_decimals) + self.number_of_decimals = number_of_decimals + + + def string(self): + ''' ''' + return "%s(%sx%sx%s, max_weight:%s) vol(%s)" % ( + self.partno, self.width, self.height, self.depth, self.max_weight, + self.getVolume() + ) + + + def getVolume(self): + ''' ''' + return set2Decimal( + self.width * self.height * self.depth, self.number_of_decimals + ) + + + def getTotalWeight(self): + ''' ''' + total_weight = 0 + + for item in self.items: + total_weight += item.weight + + return set2Decimal(total_weight, self.number_of_decimals) + + + def putItem(self, item, pivot,axis=None): + ''' put item in bin ''' + fit = False + valid_item_position = item.position + item.position = pivot + rotate = RotationType.ALL if item.updown == True else RotationType.Notupdown + for i in range(0, len(rotate)): + item.rotation_type = i + dimension = item.getDimension() + # rotatate + if ( + self.width < pivot[0] + dimension[0] or + self.height < pivot[1] + dimension[1] or + self.depth < pivot[2] + dimension[2] + ): + continue + + fit = True + + for current_item_in_bin in self.items: + if intersect(current_item_in_bin, item): + fit = False + break + + if fit: + # cal total weight + if self.getTotalWeight() + item.weight > self.max_weight: + fit = False + return fit + + # fix point float prob + if self.fix_point == True : + + [w,h,d] = dimension + [x,y,z] = [float(pivot[0]),float(pivot[1]),float(pivot[2])] + + for i in range(3): + # fix height + y = self.checkHeight([x,x+float(w),y,y+float(h),z,z+float(d)]) + # fix width + x = self.checkWidth([x,x+float(w),y,y+float(h),z,z+float(d)]) + # fix depth + z = self.checkDepth([x,x+float(w),y,y+float(h),z,z+float(d)]) + + # check stability on item + # rule : + # 1. Define a support ratio, if the ratio below the support surface does not exceed this ratio, compare the second rule. + # 2. If there is no support under any vertices of the bottom of the item, then fit = False. + if self.check_stable == True : + # Cal the surface area of ​​item. + item_area_lower = int(dimension[0] * dimension[1]) + # Cal the surface area of ​​the underlying support. + support_area_upper = 0 + for i in self.fit_items: + # Verify that the lower support surface area is greater than the upper support surface area * support_surface_ratio. + if z == i[5] : + area = len(set([ j for j in range(int(x),int(x+int(w)))]) & set([ j for j in range(int(i[0]),int(i[1]))])) * \ + len(set([ j for j in range(int(y),int(y+int(h)))]) & set([ j for j in range(int(i[2]),int(i[3]))])) + support_area_upper += area + + # If not , get four vertices of the bottom of the item. + if support_area_upper / item_area_lower < self.support_surface_ratio : + four_vertices = [[x,y],[x+float(w),y],[x,y+float(h)],[x+float(w),y+float(h)]] + # If any vertices is not supported, fit = False. + c = [False,False,False,False] + for i in self.fit_items: + if z == i[5] : + for jdx,j in enumerate(four_vertices) : + if (i[0] <= j[0] <= i[1]) and (i[2] <= j[1] <= i[3]) : + c[jdx] = True + if False in c : + item.position = valid_item_position + fit = False + return fit + + self.fit_items = np.append(self.fit_items,np.array([[x,x+float(w),y,y+float(h),z,z+float(d)]]),axis=0) + item.position = [set2Decimal(x),set2Decimal(y),set2Decimal(z)] + + if fit : + self.items.append(copy.deepcopy(item)) + + else : + item.position = valid_item_position + + return fit + + else : + item.position = valid_item_position + + return fit + + + def checkDepth(self,unfix_point): + ''' fix item position z ''' + z_ = [[0,0],[float(self.depth),float(self.depth)]] + for j in self.fit_items: + # creat x set + x_bottom = set([i for i in range(int(j[0]),int(j[1]))]) + x_top = set([i for i in range(int(unfix_point[0]),int(unfix_point[1]))]) + # creat y set + y_bottom = set([i for i in range(int(j[2]),int(j[3]))]) + y_top = set([i for i in range(int(unfix_point[2]),int(unfix_point[3]))]) + # find intersection on x set and y set. + if len(x_bottom & x_top) != 0 and len(y_bottom & y_top) != 0 : + z_.append([float(j[4]),float(j[5])]) + top_depth = unfix_point[5] - unfix_point[4] + # find diff set on z_. + z_ = sorted(z_, key = lambda z_ : z_[1]) + for j in range(len(z_)-1): + if z_[j+1][0] -z_[j][1] >= top_depth: + return z_[j][1] + return unfix_point[4] + + + def checkWidth(self,unfix_point): + ''' fix item position x ''' + x_ = [[0,0],[float(self.width),float(self.width)]] + for j in self.fit_items: + # creat z set + z_bottom = set([i for i in range(int(j[4]),int(j[5]))]) + z_top = set([i for i in range(int(unfix_point[4]),int(unfix_point[5]))]) + # creat y set + y_bottom = set([i for i in range(int(j[2]),int(j[3]))]) + y_top = set([i for i in range(int(unfix_point[2]),int(unfix_point[3]))]) + # find intersection on z set and y set. + if len(z_bottom & z_top) != 0 and len(y_bottom & y_top) != 0 : + x_.append([float(j[0]),float(j[1])]) + top_width = unfix_point[1] - unfix_point[0] + # find diff set on x_bottom and x_top. + x_ = sorted(x_,key = lambda x_ : x_[1]) + for j in range(len(x_)-1): + if x_[j+1][0] -x_[j][1] >= top_width: + return x_[j][1] + return unfix_point[0] + + + def checkHeight(self,unfix_point): + '''fix item position y ''' + y_ = [[0,0],[float(self.height),float(self.height)]] + for j in self.fit_items: + # creat x set + x_bottom = set([i for i in range(int(j[0]),int(j[1]))]) + x_top = set([i for i in range(int(unfix_point[0]),int(unfix_point[1]))]) + # creat z set + z_bottom = set([i for i in range(int(j[4]),int(j[5]))]) + z_top = set([i for i in range(int(unfix_point[4]),int(unfix_point[5]))]) + # find intersection on x set and z set. + if len(x_bottom & x_top) != 0 and len(z_bottom & z_top) != 0 : + y_.append([float(j[2]),float(j[3])]) + top_height = unfix_point[3] - unfix_point[2] + # find diff set on y_bottom and y_top. + y_ = sorted(y_,key = lambda y_ : y_[1]) + for j in range(len(y_)-1): + if y_[j+1][0] -y_[j][1] >= top_height: + return y_[j][1] + + return unfix_point[2] + + + def addCorner(self): + '''add container coner ''' + if self.corner != 0 : + corner = set2Decimal(self.corner) + corner_list = [] + for i in range(8): + a = Item( + partno='corner{}'.format(i), + name='corner', + typeof='cube', + WHD=(corner,corner,corner), + weight=0, + level=0, + loadbear=0, + updown=True, + color='#000000') + + corner_list.append(a) + return corner_list + + + def putCorner(self,info,item): + '''put coner in bin ''' + fit = False + x = set2Decimal(self.width - self.corner) + y = set2Decimal(self.height - self.corner) + z = set2Decimal(self.depth - self.corner) + pos = [[0,0,0],[0,0,z],[0,y,z],[0,y,0],[x,y,0],[x,0,0],[x,0,z],[x,y,z]] + item.position = pos[info] + self.items.append(item) + + corner = [float(item.position[0]),float(item.position[0])+float(self.corner),float(item.position[1]),float(item.position[1])+float(self.corner),float(item.position[2]),float(item.position[2])+float(self.corner)] + + self.fit_items = np.append(self.fit_items,np.array([corner]),axis=0) + return + + + def clearBin(self): + ''' clear item which in bin ''' + self.items = [] + self.fit_items = np.array([[0,self.width,0,self.height,0,0]]) + return + + +class Packer: + + def __init__(self): + ''' ''' + self.bins = [] + self.items = [] + self.unfit_items = [] + self.total_items = 0 + self.binding = [] + # self.apex = [] + + + def addBin(self, bin): + ''' ''' + return self.bins.append(bin) + + + def addItem(self, item): + ''' ''' + self.total_items = len(self.items) + 1 + + return self.items.append(item) + + + def pack2Bin(self, bin, item,fix_point,check_stable,support_surface_ratio): + ''' pack item to bin ''' + fitted = False + bin.fix_point = fix_point + bin.check_stable = check_stable + bin.support_surface_ratio = support_surface_ratio + + # first put item on (0,0,0) , if corner exist ,first add corner in box. + if bin.corner != 0 and not bin.items: + corner_lst = bin.addCorner() + for i in range(len(corner_lst)) : + bin.putCorner(i,corner_lst[i]) + + elif not bin.items: + response = bin.putItem(item, item.position) + + if not response: + bin.unfitted_items.append(item) + return + + for axis in range(0, 3): + items_in_bin = bin.items + for ib in items_in_bin: + pivot = [0, 0, 0] + w, h, d = ib.getDimension() + if axis == Axis.WIDTH: + pivot = [ib.position[0] + w,ib.position[1],ib.position[2]] + elif axis == Axis.HEIGHT: + pivot = [ib.position[0],ib.position[1] + h,ib.position[2]] + elif axis == Axis.DEPTH: + pivot = [ib.position[0],ib.position[1],ib.position[2] + d] + + if bin.putItem(item, pivot, axis): + fitted = True + break + if fitted: + break + if not fitted: + bin.unfitted_items.append(item) + + + def sortBinding(self,bin): + ''' sorted by binding ''' + b,front,back = [],[],[] + for i in range(len(self.binding)): + b.append([]) + for item in self.items: + if item.name in self.binding[i]: + b[i].append(item) + elif item.name not in self.binding: + if len(b[0]) == 0 and item not in front: + front.append(item) + elif item not in back and item not in front: + back.append(item) + + min_c = min([len(i) for i in b]) + + sort_bind =[] + for i in range(min_c): + for j in range(len(b)): + sort_bind.append(b[j][i]) + + for i in b: + for j in i: + if j not in sort_bind: + self.unfit_items.append(j) + + self.items = front + sort_bind + back + return + + + def putOrder(self): + '''Arrange the order of items ''' + r = [] + for i in self.bins: + # open top container + if i.put_type == 2: + i.items.sort(key=lambda item: item.position[0], reverse=False) + i.items.sort(key=lambda item: item.position[1], reverse=False) + i.items.sort(key=lambda item: item.position[2], reverse=False) + # general container + elif i.put_type == 1: + i.items.sort(key=lambda item: item.position[1], reverse=False) + i.items.sort(key=lambda item: item.position[2], reverse=False) + i.items.sort(key=lambda item: item.position[0], reverse=False) + else : + pass + return + + + def gravityCenter(self,bin): + ''' + Deviation Of Cargo gravity distribution + ''' + w = int(bin.width) + h = int(bin.height) + d = int(bin.depth) + + area1 = [set(range(0,w//2+1)),set(range(0,h//2+1)),0] + area2 = [set(range(w//2+1,w+1)),set(range(0,h//2+1)),0] + area3 = [set(range(0,w//2+1)),set(range(h//2+1,h+1)),0] + area4 = [set(range(w//2+1,w+1)),set(range(h//2+1,h+1)),0] + area = [area1,area2,area3,area4] + + for i in bin.items: + + x_st = int(i.position[0]) + y_st = int(i.position[1]) + if i.rotation_type == 0: + x_ed = int(i.position[0] + i.width) + y_ed = int(i.position[1] + i.height) + elif i.rotation_type == 1: + x_ed = int(i.position[0] + i.height) + y_ed = int(i.position[1] + i.width) + elif i.rotation_type == 2: + x_ed = int(i.position[0] + i.height) + y_ed = int(i.position[1] + i.depth) + elif i.rotation_type == 3: + x_ed = int(i.position[0] + i.depth) + y_ed = int(i.position[1] + i.height) + elif i.rotation_type == 4: + x_ed = int(i.position[0] + i.depth) + y_ed = int(i.position[1] + i.width) + elif i.rotation_type == 5: + x_ed = int(i.position[0] + i.width) + y_ed = int(i.position[1] + i.depth) + + x_set = set(range(x_st,int(x_ed)+1)) + y_set = set(range(y_st,y_ed+1)) + + # cal gravity distribution + for j in range(len(area)): + if x_set.issubset(area[j][0]) and y_set.issubset(area[j][1]) : + area[j][2] += int(i.weight) + break + # include x and !include y + elif x_set.issubset(area[j][0]) == True and y_set.issubset(area[j][1]) == False and len(y_set & area[j][1]) != 0 : + y = len(y_set & area[j][1]) / (y_ed - y_st) * int(i.weight) + area[j][2] += y + if j >= 2 : + area[j-2][2] += (int(i.weight) - x) + else : + area[j+2][2] += (int(i.weight) - y) + break + # include y and !include x + elif x_set.issubset(area[j][0]) == False and y_set.issubset(area[j][1]) == True and len(x_set & area[j][0]) != 0 : + x = len(x_set & area[j][0]) / (x_ed - x_st) * int(i.weight) + area[j][2] += x + if j >= 2 : + area[j-2][2] += (int(i.weight) - x) + else : + area[j+2][2] += (int(i.weight) - x) + break + # !include x and !include y + elif x_set.issubset(area[j][0])== False and y_set.issubset(area[j][1]) == False and len(y_set & area[j][1]) != 0 and len(x_set & area[j][0]) != 0 : + all = (y_ed - y_st) * (x_ed - x_st) + y = len(y_set & area[0][1]) + y_2 = y_ed - y_st - y + x = len(x_set & area[0][0]) + x_2 = x_ed - x_st - x + area[0][2] += x * y / all * int(i.weight) + area[1][2] += x_2 * y / all * int(i.weight) + area[2][2] += x * y_2 / all * int(i.weight) + area[3][2] += x_2 * y_2 / all * int(i.weight) + break + + r = [area[0][2],area[1][2],area[2][2],area[3][2]] + result = [] + for i in r : + result.append(round(i / sum(r) * 100,2)) + return result + + + def pack(self, bigger_first=False,distribute_items=True,fix_point=True,check_stable=True,support_surface_ratio=0.75,binding=[],number_of_decimals=DEFAULT_NUMBER_OF_DECIMALS): + '''pack master func ''' + # set decimals + for bin in self.bins: + bin.formatNumbers(number_of_decimals) + + for item in self.items: + item.formatNumbers(number_of_decimals) + # add binding attribute + self.binding = binding + # Bin : sorted by volumn + self.bins.sort(key=lambda bin: bin.getVolume(), reverse=bigger_first) + # Item : sorted by volumn -> sorted by loadbear -> sorted by level -> binding + self.items.sort(key=lambda item: item.getVolume(), reverse=bigger_first) + # self.items.sort(key=lambda item: item.getMaxArea(), reverse=bigger_first) + self.items.sort(key=lambda item: item.loadbear, reverse=True) + self.items.sort(key=lambda item: item.level, reverse=False) + # sorted by binding + if binding != []: + self.sortBinding(bin) + + for idx,bin in enumerate(self.bins): + # pack item to bin + for item in self.items: + self.pack2Bin(bin, item, fix_point, check_stable, support_surface_ratio) + + if binding != []: + # resorted + self.items.sort(key=lambda item: item.getVolume(), reverse=bigger_first) + self.items.sort(key=lambda item: item.loadbear, reverse=True) + self.items.sort(key=lambda item: item.level, reverse=False) + # clear bin + bin.items = [] + bin.unfitted_items = self.unfit_items + bin.fit_items = np.array([[0,bin.width,0,bin.height,0,0]]) + # repacking + for item in self.items: + self.pack2Bin(bin, item,fix_point,check_stable,support_surface_ratio) + + # Deviation Of Cargo Gravity Center + self.bins[idx].gravity = self.gravityCenter(bin) + + if distribute_items : + for bitem in bin.items: + no = bitem.partno + for item in self.items : + if item.partno == no : + self.items.remove(item) + break + + # put order of items + self.putOrder() + + if self.items != []: + self.unfit_items = copy.deepcopy(self.items) + self.items = [] + # for item in self.items.copy(): + # if item in bin.unfitted_items: + # self.items.remove(item) + + + +class Painter: + + def __init__(self,bins): + ''' ''' + self.items = bins.items + self.width = bins.width + self.height = bins.height + self.depth = bins.depth + + + def _plotCube(self, ax, x, y, z, dx, dy, dz, color='red',mode=2,linewidth=1,text="",fontsize=15,alpha=0.5): + """ Auxiliary function to plot a cube. code taken somewhere from the web. """ + xx = [x, x, x+dx, x+dx, x] + yy = [y, y+dy, y+dy, y, y] + + kwargs = {'alpha': 1, 'color': color,'linewidth':linewidth } + if mode == 1 : + ax.plot3D(xx, yy, [z]*5, **kwargs) + ax.plot3D(xx, yy, [z+dz]*5, **kwargs) + ax.plot3D([x, x], [y, y], [z, z+dz], **kwargs) + ax.plot3D([x, x], [y+dy, y+dy], [z, z+dz], **kwargs) + ax.plot3D([x+dx, x+dx], [y+dy, y+dy], [z, z+dz], **kwargs) + ax.plot3D([x+dx, x+dx], [y, y], [z, z+dz], **kwargs) + else : + p = Rectangle((x,y),dx,dy,fc=color,ec='black',alpha = alpha) + p2 = Rectangle((x,y),dx,dy,fc=color,ec='black',alpha = alpha) + p3 = Rectangle((y,z),dy,dz,fc=color,ec='black',alpha = alpha) + p4 = Rectangle((y,z),dy,dz,fc=color,ec='black',alpha = alpha) + p5 = Rectangle((x,z),dx,dz,fc=color,ec='black',alpha = alpha) + p6 = Rectangle((x,z),dx,dz,fc=color,ec='black',alpha = alpha) + ax.add_patch(p) + ax.add_patch(p2) + ax.add_patch(p3) + ax.add_patch(p4) + ax.add_patch(p5) + ax.add_patch(p6) + + if text != "": + ax.text( (x+ dx/2), (y+ dy/2), (z+ dz/2), str(text),color='black', fontsize=fontsize, ha='center', va='center') + + art3d.pathpatch_2d_to_3d(p, z=z, zdir="z") + art3d.pathpatch_2d_to_3d(p2, z=z+dz, zdir="z") + art3d.pathpatch_2d_to_3d(p3, z=x, zdir="x") + art3d.pathpatch_2d_to_3d(p4, z=x + dx, zdir="x") + art3d.pathpatch_2d_to_3d(p5, z=y, zdir="y") + art3d.pathpatch_2d_to_3d(p6, z=y + dy, zdir="y") + + + def _plotCylinder(self, ax, x, y, z, dx, dy, dz, color='red',mode=2,text="",fontsize=10,alpha=0.2): + """ Auxiliary function to plot a Cylinder """ + # plot the two circles above and below the cylinder + p = Circle((x+dx/2,y+dy/2),radius=dx/2,color=color,alpha=0.5) + p2 = Circle((x+dx/2,y+dy/2),radius=dx/2,color=color,alpha=0.5) + ax.add_patch(p) + ax.add_patch(p2) + art3d.pathpatch_2d_to_3d(p, z=z, zdir="z") + art3d.pathpatch_2d_to_3d(p2, z=z+dz, zdir="z") + # plot a circle in the middle of the cylinder + center_z = np.linspace(0, dz, 10) + theta = np.linspace(0, 2*np.pi, 10) + theta_grid, z_grid=np.meshgrid(theta, center_z) + x_grid = dx / 2 * np.cos(theta_grid) + x + dx / 2 + y_grid = dy / 2 * np.sin(theta_grid) + y + dy / 2 + z_grid = z_grid + z + ax.plot_surface(x_grid, y_grid, z_grid,shade=False,fc=color,alpha=alpha,color=color) + if text != "" : + ax.text( (x+ dx/2), (y+ dy/2), (z+ dz/2), str(text),color='black', fontsize=fontsize, ha='center', va='center') + + def plotBoxAndItems(self,title="",alpha=0.2,write_num=False,fontsize=10): + """ side effective. Plot the Bin and the items it contains. """ + fig = plt.figure() + axGlob = plt.axes(projection='3d') + + # plot bin + self._plotCube(axGlob,0, 0, 0, float(self.width), float(self.height), float(self.depth),color='black',mode=1,linewidth=2,text="") + + counter = 0 + # fit rotation type + for item in self.items: + rt = item.rotation_type + x,y,z = item.position + [w,h,d] = item.getDimension() + color = item.color + text= item.partno if write_num else "" + + if item.typeof == 'cube': + # plot item of cube + self._plotCube(axGlob, float(x), float(y), float(z), float(w),float(h),float(d),color=color,mode=2,text=text,fontsize=fontsize,alpha=alpha) + elif item.typeof == 'cylinder': + # plot item of cylinder + self._plotCylinder(axGlob, float(x), float(y), float(z), float(w),float(h),float(d),color=color,mode=2,text=text,fontsize=fontsize,alpha=alpha) + + counter = counter + 1 + + + plt.title(title) + self.setAxesEqual(axGlob) + return plt + + + def setAxesEqual(self,ax): + '''Make axes of 3D plot have equal scale so that spheres appear as spheres, + cubes as cubes, etc.. This is one possible solution to Matplotlib's + ax.set_aspect('equal') and ax.axis('equal') not working for 3D. + + Input + ax: a matplotlib axis, e.g., as output from plt.gca().''' + x_limits = ax.get_xlim3d() + y_limits = ax.get_ylim3d() + z_limits = ax.get_zlim3d() + + x_range = abs(x_limits[1] - x_limits[0]) + x_middle = np.mean(x_limits) + y_range = abs(y_limits[1] - y_limits[0]) + y_middle = np.mean(y_limits) + z_range = abs(z_limits[1] - z_limits[0]) + z_middle = np.mean(z_limits) + + # The plot bounding box is a sphere in the sense of the infinity + # norm, hence I call half the max range the plot radius. + plot_radius = 0.5 * max([x_range, y_range, z_range]) + + ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) + ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) + ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) \ No newline at end of file