diff --git a/containers.py b/containers.py new file mode 100644 index 0000000..be093dc --- /dev/null +++ b/containers.py @@ -0,0 +1,168 @@ +import matplotlib.pyplot as plt + + +class Container: + def __init__(self, length, width, height, capacity): + self.length = length # Länge in m + self.width = width # Breite in m + self.height = height # Höhe in m + self.capacity = capacity # Gewichtskapazität in t + self.items = [] + self.fitting = [] + + def volume(self): + return self.length * self.width * self.height + + def used_volume(self): + if len(self.items) == 0: + raise Exception("Container empty, can't calculate used volume!") + + return sum(item[0].volume() * item[1] for item in self.items) + + def utilization_volume(self): + return self.used_volume() / self.volume() * 100 + + def used_weight(self): + if len(self.items) == 0: + raise Exception("Container empty, can't calculate load weight!") + + return sum(item[0].weight * item[1] for item in self.items) + + def utilization_weight(self): + return self.used_weight() / self.capacity * 100 + + def try_fit(self, items) -> bool: + self.items = items + if self.used_weight() > self.capacity: + print("Load too heavy!") + return False + if self.used_volume() > self.volume(): + print("Load too large!") + return False + + fitting = [] + + # Packstücke, Packordnung: Y, Z, X (Breite -> Höhe -> Länge) + x_offset = 0 + y_offset = 0 + z_offset = 0 + for i, item_ in enumerate(items): + item, count = item_ + + for _ in range(count): + fitting += [ + ( + x_offset, + y_offset, + z_offset, + item.length, + item.width, + item.height, + COLORS[i], + ) + ] + + if y_offset + item.width < self.width: + # Selbe "Zeile" + y_offset += item.width + elif z_offset + item.height < self.height: + y_offset = 0 + # Neue "Ebene" (Höhe) + z_offset += item.height + elif x_offset + item.length < self.length: + y_offset = 0 + z_offset = 0 + # Neue "Scheibe" (Länge) + x_offset += item.length + else: + return False + + if x_offset + item.length < self.length: + x_offset += item.length + else: + return False + y_offset = 0 + z_offset = 0 + + self.fitting = fitting + return True + + def visualize(self): + if len(self.fitting) == 0: + raise Exception("Container not fitted, can't visualize!") + + fig = plt.figure() + ax = fig.add_subplot(111, projection="3d") + + # Container + ax.bar3d(0, 0, 0, self.length, self.width, self.height, alpha=0.1, color="gray") + + # TODO: Don't add individual packages, combine them into blocks (perf) + # Packages + for x, y, z, l, w, h, color in self.fitting: + ax.bar3d(x, y, z, l, w, h, color=color) + + ax.set_xlim([0, self.length]) + ax.set_ylim([0, self.width]) + ax.set_zlim([0, self.height]) + ax.set_xlabel("Länge (cm)") + ax.set_ylabel("Breite (cm)") + ax.set_zlabel("Höhe (cm)") + plt.title(f"Container-Auslastung: {self.utilization_volume():.2f}%") + plt.autoscale() + plt.show() + + +class Item: + def __init__(self, length, width, height, weight): + self.length = length # Länge in cm + self.width = width # Breite in cm + self.height = height # Höhe in cm + self.weight = weight # Gewicht in kg + + def volume(self): + return self.length * self.width * self.height + + +# Length (cm), Width (cm), Height (cm), Capacity (kg) +CONTAINERS = { + "20'": Container(590, 235, 239, 21670), + "40'": Container(1203, 240, 239, 25680), + "40'HQ": Container(1203, 235, 269, 26480), +} + +COLORS = ["red", "blue", "green", "yellow", "purple"] + + +def main(): + print("Container-Auslastungsrechner") + items = [] + + while True: + print("\nNeues Packstück hinzufügen:") + length = float(input("Länge (cm): ")) + width = float(input("Breite (cm): ")) + height = float(input("Höhe (cm): ")) + weight = float(input("Gewicht (kg): ")) + count = int(input("Anzahl der Packstücke: ")) + + item = Item(length, width, height, weight) + items += [(item, count)] + + more_items = input("Weitere Packstücke hinzufügen? (ja/nein): ").strip().lower() + if more_items != "ja": + break + + for name, container in CONTAINERS.items(): + if container.try_fit(items): + print( + f"\nBenötigter Container: {name}, Gesamtauslastung (Volumen): {container.utilization_volume():.2f}%, Ladungsgewicht: {container.used_weight()}kg, Gesamtauslastung (Gewicht): {container.utilization_weight():.2f}%" + ) + container.visualize() + return + + print("Couldn't find fitting container!") + + +if __name__ == "__main__": + main() diff --git a/containersV2.py b/containersV2.py new file mode 100644 index 0000000..58b751e --- /dev/null +++ b/containersV2.py @@ -0,0 +1,138 @@ +from typing import Dict, List, Tuple +import ezdxf +from ezdxf import colors +from ezdxf.addons import binpacking as bp +from ezdxf.addons.drawing import RenderContext, Frontend, matplotlib +from ezdxf.addons.drawing.matplotlib import MatplotlibBackend +import matplotlib.pyplot as plt + +# Length (cm), Width (cm), Height (cm), Capacity (kg) +CONTAINERS: List[Tuple[str, float, float, float, float]] = [ + ("20'", 590, 235, 239, 21670), + ("40'", 1203, 240, 239, 25680), + ("40'HQ", 1203, 235, 269, 26480), +] + +COLORS: List[str] = ["red", "blue", "green", "yellow", "purple"] + + +def float_input(msg: str) -> float: + read_str: str = input(f"{msg}: ") + read_float: float + + try: + read_float = float(read_str) + except: + return float_input(msg) + + return read_float + + +def int_input(msg: str) -> int: + read_str: str = input(f"{msg}: ") + read_int: int + + try: + read_int = int(read_str) + except: + return int_input(msg) + + return read_int + + +def yes_no_input(msg: str) -> bool: + read_str: str = input(f"{msg} (j/n): ").strip().lower() + + if read_str == "j": + return True + elif read_str == "n": + return False + else: + return yes_no_input(msg) + + +def build_packer( + items: List[Tuple[Tuple[float, float, float, float], int]] +) -> bp.Packer: + packer: bp.Packer = bp.Packer() + + for i, ((length, width, height, weight), count) in enumerate(items): + for ii in range(count): + packer.add_item(f"Item {i}_{ii}", width, height, length, weight) + + return packer + + +def make_doc(): + doc = ezdxf.new() + doc.layers.add("FRAME", color=colors.YELLOW) + doc.layers.add("ITEMS") + doc.layers.add("TEXT") + return doc + + +def main() -> None: + print("Container-Auslastungsrechner") + items: List[Tuple[Tuple[float, float, float, float], int]] = [] + + more_items: bool = True + while more_items: + print("\nNeues Packstück hinzufügen:") + length = float_input("- Länge (cm)") + width = float_input("- Breite (cm)") + height = float_input("- Höhe (cm)") + weight = float_input("- Gewicht (kg)") + count = int_input("- Packstückzahl") + + items += [((length, width, height, weight), count)] + + more_items = yes_no_input("Weitere Packstücke hinzufügen?") + + for name, length, width, height, capacity in CONTAINERS: + print( + f"Teste {name:>5} Container ({length / 100:>5.2f}m x {width / 100:>5.2f}m x {height / 100:>5.2f}m mit {capacity / 1000:>5.2f}t)..." + ) + + packer: bp.Packer = build_packer(items) + packer.add_bin(name, width, height, length, capacity) + packer.pack(bp.PickStrategy.BIGGER_FIRST) + + if len(packer.unfitted_items) > 0: + print(f"{name} Container ist zu klein!") + break + + bins: List[bp.Bin] = [] + bins.extend(packer.bins) + + doc = make_doc() + bp.export_dxf(doc.modelspace(), bins, offset=(0, 20, 0)) + # doc.saveas("packing.dxf") + print( + f"{name} Container passt: Zu {packer.get_fill_ratio() * 100:.2f}% gefüllt ({packer.get_total_weight():.2f}kg)" + ) + + fig = plt.figure() + ax = fig.add_subplot(111, projection="3d") + + # Container bounding box + ax.bar3d(0, 0, 0, length, width, height, alpha=0.1, color="gray") + + # Package bounding boxes + for it in packer.bins[0].items: + x, y, z = it.position + w, h, l = it.width, it.height, it.depth + ax.bar3d(x, y, z, l, w, h, color="red") + + plt.title( + f"{name} Container mit {packer.get_fill_ratio() * 100:.2f}% Auslastung" + ) + plt.autoscale() + plt.show() + + return + + print("Konnte keinen passenden Container ermitteln!") + + +if __name__ == "__main__": + main()