├── py3dbp ├── __init__.py ├── constants.py ├── auxiliary_methods.py └── main.py ├── .gitignore ├── erick_dube_507-034.pdf ├── setup.py ├── LICENSE ├── example.py └── README.md /py3dbp/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import Packer, Bin, Item 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | *.egg-info 5 | .pypirc 6 | .vscode -------------------------------------------------------------------------------- /erick_dube_507-034.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enzoruiz/3dbinpacking/HEAD/erick_dube_507-034.pdf -------------------------------------------------------------------------------- /py3dbp/constants.py: -------------------------------------------------------------------------------- 1 | class RotationType: 2 | RT_WHD = 0 3 | RT_HWD = 1 4 | RT_HDW = 2 5 | RT_DHW = 3 6 | RT_DWH = 4 7 | RT_WDH = 5 8 | 9 | ALL = [RT_WHD, RT_HWD, RT_HDW, RT_DHW, RT_DWH, RT_WDH] 10 | 11 | 12 | class Axis: 13 | WIDTH = 0 14 | HEIGHT = 1 15 | DEPTH = 2 16 | 17 | ALL = [WIDTH, HEIGHT, DEPTH] 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='py3dbp', 8 | version='1.1.2', 9 | author="Juan Cho", 10 | author_email="juanperez@gmail.com", 11 | description="3D Bin Packing", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/enzoruiz/3dbinpacking", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /py3dbp/auxiliary_methods.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from .constants import Axis 3 | 4 | 5 | def rect_intersect(item1, item2, x, y): 6 | d1 = item1.get_dimension() 7 | d2 = item2.get_dimension() 8 | 9 | cx1 = item1.position[x] + d1[x]/2 10 | cy1 = item1.position[y] + d1[y]/2 11 | cx2 = item2.position[x] + d2[x]/2 12 | cy2 = item2.position[y] + d2[y]/2 13 | 14 | ix = max(cx1, cx2) - min(cx1, cx2) 15 | iy = max(cy1, cy2) - min(cy1, cy2) 16 | 17 | return ix < (d1[x]+d2[x])/2 and iy < (d1[y]+d2[y])/2 18 | 19 | 20 | def intersect(item1, item2): 21 | return ( 22 | rect_intersect(item1, item2, Axis.WIDTH, Axis.HEIGHT) and 23 | rect_intersect(item1, item2, Axis.HEIGHT, Axis.DEPTH) and 24 | rect_intersect(item1, item2, Axis.WIDTH, Axis.DEPTH) 25 | ) 26 | 27 | 28 | def get_limit_number_of_decimals(number_of_decimals): 29 | return Decimal('1.{}'.format('0' * number_of_decimals)) 30 | 31 | 32 | def set_to_decimal(value, number_of_decimals): 33 | number_of_decimals = get_limit_number_of_decimals(number_of_decimals) 34 | 35 | return Decimal(value).quantize(number_of_decimals) 36 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from py3dbp import Packer, Bin, Item 2 | 3 | packer = Packer() 4 | 5 | packer.add_bin(Bin('small-envelope', 11.5, 6.125, 0.25, 10)) 6 | packer.add_bin(Bin('large-envelope', 15.0, 12.0, 0.75, 15)) 7 | packer.add_bin(Bin('small-box', 8.625, 5.375, 1.625, 70.0)) 8 | packer.add_bin(Bin('medium-box', 11.0, 8.5, 5.5, 70.0)) 9 | packer.add_bin(Bin('medium-2-box', 13.625, 11.875, 3.375, 70.0)) 10 | packer.add_bin(Bin('large-box', 12.0, 12.0, 5.5, 70.0)) 11 | packer.add_bin(Bin('large-2-box', 23.6875, 11.75, 3.0, 70.0)) 12 | 13 | packer.add_item(Item('50g [powder 1]', 3.9370, 1.9685, 1.9685, 1)) 14 | packer.add_item(Item('50g [powder 2]', 3.9370, 1.9685, 1.9685, 2)) 15 | packer.add_item(Item('50g [powder 3]', 3.9370, 1.9685, 1.9685, 3)) 16 | packer.add_item(Item('250g [powder 4]', 7.8740, 3.9370, 1.9685, 4)) 17 | packer.add_item(Item('250g [powder 5]', 7.8740, 3.9370, 1.9685, 5)) 18 | packer.add_item(Item('250g [powder 6]', 7.8740, 3.9370, 1.9685, 6)) 19 | packer.add_item(Item('250g [powder 7]', 7.8740, 3.9370, 1.9685, 7)) 20 | packer.add_item(Item('250g [powder 8]', 7.8740, 3.9370, 1.9685, 8)) 21 | packer.add_item(Item('250g [powder 9]', 7.8740, 3.9370, 1.9685, 9)) 22 | 23 | packer.pack() 24 | 25 | for b in packer.bins: 26 | print(":::::::::::", b.string()) 27 | 28 | print("FITTED ITEMS:") 29 | for item in b.items: 30 | print("====> ", item.string()) 31 | 32 | print("UNFITTED ITEMS:") 33 | for item in b.unfitted_items: 34 | print("====> ", item.string()) 35 | 36 | print("***************************************************") 37 | print("***************************************************") 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 3D Bin Packing 2 | ==== 3 | 4 | 3D Bin Packing implementation based on [this paper](erick_dube_507-034.pdf). The code is based on [gedex](https://github.com/gedex/bp3d) implementation in Go. 5 | 6 | ## Features 7 | 1. Sorting Bins and Items: 8 | ```[bigger_first=False/True]``` By default all the bins and items are sorted from the smallest to the biggest, also it can be vice versa, to make the packing in such ordering. 9 | 2. Item Distribution: 10 | - ```[distribute_items=True]``` From a list of bins and items, put the items in the bins that at least one item be in one bin that can be fitted. That is, distribute all the items in all the bins so that they can be contained. 11 | - ```[distribute_items=False]``` From a list of bins and items, try to put all the items in each bin and in the end it show per bin all the items that was fitted and the items that was not. 12 | 3. Number of decimals: 13 | ```[number_of_decimals=X]``` Define the limits of decimals of the inputs and the outputs. By default is 3. 14 | 15 | ## Install 16 | 17 | ``` 18 | pip install py3dbp 19 | ``` 20 | 21 | ## Basic Explanation 22 | 23 | Bin and Items have the same creation params: 24 | ``` 25 | my_bin = Bin(name, width, height, depth, max_weight) 26 | my_item = Item(name, width, height, depth, weight) 27 | ``` 28 | Packer have three main functions: 29 | ``` 30 | packer = Packer() # PACKER DEFINITION 31 | 32 | packer.add_bin(my_bin) # ADDING BINS TO PACKER 33 | packer.add_item(my_item) # ADDING ITEMS TO PACKER 34 | 35 | packer.pack() # PACKING - by default (bigger_first=False, distribute_items=False, number_of_decimals=3) 36 | ``` 37 | 38 | After packing: 39 | ``` 40 | packer.bins # GET ALL BINS OF PACKER 41 | my_bin.items # GET ALL FITTED ITEMS IN EACH BIN 42 | my_bin.unfitted_items # GET ALL UNFITTED ITEMS IN EACH BIN 43 | ``` 44 | 45 | 46 | ## Usage 47 | 48 | ``` 49 | from py3dbp import Packer, Bin, Item 50 | 51 | packer = Packer() 52 | 53 | packer.add_bin(Bin('small-envelope', 11.5, 6.125, 0.25, 10)) 54 | packer.add_bin(Bin('large-envelope', 15.0, 12.0, 0.75, 15)) 55 | packer.add_bin(Bin('small-box', 8.625, 5.375, 1.625, 70.0)) 56 | packer.add_bin(Bin('medium-box', 11.0, 8.5, 5.5, 70.0)) 57 | packer.add_bin(Bin('medium-2-box', 13.625, 11.875, 3.375, 70.0)) 58 | packer.add_bin(Bin('large-box', 12.0, 12.0, 5.5, 70.0)) 59 | packer.add_bin(Bin('large-2-box', 23.6875, 11.75, 3.0, 70.0)) 60 | 61 | packer.add_item(Item('50g [powder 1]', 3.9370, 1.9685, 1.9685, 1)) 62 | packer.add_item(Item('50g [powder 2]', 3.9370, 1.9685, 1.9685, 2)) 63 | packer.add_item(Item('50g [powder 3]', 3.9370, 1.9685, 1.9685, 3)) 64 | packer.add_item(Item('250g [powder 4]', 7.8740, 3.9370, 1.9685, 4)) 65 | packer.add_item(Item('250g [powder 5]', 7.8740, 3.9370, 1.9685, 5)) 66 | packer.add_item(Item('250g [powder 6]', 7.8740, 3.9370, 1.9685, 6)) 67 | packer.add_item(Item('250g [powder 7]', 7.8740, 3.9370, 1.9685, 7)) 68 | packer.add_item(Item('250g [powder 8]', 7.8740, 3.9370, 1.9685, 8)) 69 | packer.add_item(Item('250g [powder 9]', 7.8740, 3.9370, 1.9685, 9)) 70 | 71 | packer.pack() 72 | 73 | for b in packer.bins: 74 | print(":::::::::::", b.string()) 75 | 76 | print("FITTED ITEMS:") 77 | for item in b.items: 78 | print("====> ", item.string()) 79 | 80 | print("UNFITTED ITEMS:") 81 | for item in b.unfitted_items: 82 | print("====> ", item.string()) 83 | 84 | print("***************************************************") 85 | print("***************************************************") 86 | 87 | ``` 88 | 89 | ## Latest Stable Version 90 | py3dbp==1.1.2 91 | 92 | ## Versioning 93 | - **1.x** 94 | - Two ways to distribute items (all items in all bins - all items in each bin). 95 | - Get per bin the fitted and unfitted items. 96 | - Set the limit of decimals of inputs and outputs. 97 | - **0.x** 98 | - Try to put all items in the first bin that can fit at least one. 99 | 100 | ## Credit 101 | 102 | * https://github.com/bom-d-van/binpacking 103 | * https://github.com/gedex/bp3d 104 | * [Optimizing three-dimensional bin packing through simulation](erick_dube_507-034.pdf) 105 | 106 | ## License 107 | 108 | [MIT](./LICENSE) -------------------------------------------------------------------------------- /py3dbp/main.py: -------------------------------------------------------------------------------- 1 | from .constants import RotationType, Axis 2 | from .auxiliary_methods import intersect, set_to_decimal 3 | 4 | DEFAULT_NUMBER_OF_DECIMALS = 3 5 | START_POSITION = [0, 0, 0] 6 | 7 | 8 | class Item: 9 | def __init__(self, name, width, height, depth, weight): 10 | self.name = name 11 | self.width = width 12 | self.height = height 13 | self.depth = depth 14 | self.weight = weight 15 | self.rotation_type = 0 16 | self.position = START_POSITION 17 | self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS 18 | 19 | def format_numbers(self, number_of_decimals): 20 | self.width = set_to_decimal(self.width, number_of_decimals) 21 | self.height = set_to_decimal(self.height, number_of_decimals) 22 | self.depth = set_to_decimal(self.depth, number_of_decimals) 23 | self.weight = set_to_decimal(self.weight, number_of_decimals) 24 | self.number_of_decimals = number_of_decimals 25 | 26 | def string(self): 27 | return "%s(%sx%sx%s, weight: %s) pos(%s) rt(%s) vol(%s)" % ( 28 | self.name, self.width, self.height, self.depth, self.weight, 29 | self.position, self.rotation_type, self.get_volume() 30 | ) 31 | 32 | def get_volume(self): 33 | return set_to_decimal( 34 | self.width * self.height * self.depth, self.number_of_decimals 35 | ) 36 | 37 | def get_dimension(self): 38 | if self.rotation_type == RotationType.RT_WHD: 39 | dimension = [self.width, self.height, self.depth] 40 | elif self.rotation_type == RotationType.RT_HWD: 41 | dimension = [self.height, self.width, self.depth] 42 | elif self.rotation_type == RotationType.RT_HDW: 43 | dimension = [self.height, self.depth, self.width] 44 | elif self.rotation_type == RotationType.RT_DHW: 45 | dimension = [self.depth, self.height, self.width] 46 | elif self.rotation_type == RotationType.RT_DWH: 47 | dimension = [self.depth, self.width, self.height] 48 | elif self.rotation_type == RotationType.RT_WDH: 49 | dimension = [self.width, self.depth, self.height] 50 | else: 51 | dimension = [] 52 | 53 | return dimension 54 | 55 | 56 | class Bin: 57 | def __init__(self, name, width, height, depth, max_weight): 58 | self.name = name 59 | self.width = width 60 | self.height = height 61 | self.depth = depth 62 | self.max_weight = max_weight 63 | self.items = [] 64 | self.unfitted_items = [] 65 | self.number_of_decimals = DEFAULT_NUMBER_OF_DECIMALS 66 | 67 | def format_numbers(self, number_of_decimals): 68 | self.width = set_to_decimal(self.width, number_of_decimals) 69 | self.height = set_to_decimal(self.height, number_of_decimals) 70 | self.depth = set_to_decimal(self.depth, number_of_decimals) 71 | self.max_weight = set_to_decimal(self.max_weight, number_of_decimals) 72 | self.number_of_decimals = number_of_decimals 73 | 74 | def string(self): 75 | return "%s(%sx%sx%s, max_weight:%s) vol(%s)" % ( 76 | self.name, self.width, self.height, self.depth, self.max_weight, 77 | self.get_volume() 78 | ) 79 | 80 | def get_volume(self): 81 | return set_to_decimal( 82 | self.width * self.height * self.depth, self.number_of_decimals 83 | ) 84 | 85 | def get_total_weight(self): 86 | total_weight = 0 87 | 88 | for item in self.items: 89 | total_weight += item.weight 90 | 91 | return set_to_decimal(total_weight, self.number_of_decimals) 92 | 93 | def put_item(self, item, pivot): 94 | fit = False 95 | valid_item_position = item.position 96 | item.position = pivot 97 | 98 | for i in range(0, len(RotationType.ALL)): 99 | item.rotation_type = i 100 | dimension = item.get_dimension() 101 | if ( 102 | self.width < pivot[0] + dimension[0] or 103 | self.height < pivot[1] + dimension[1] or 104 | self.depth < pivot[2] + dimension[2] 105 | ): 106 | continue 107 | 108 | fit = True 109 | 110 | for current_item_in_bin in self.items: 111 | if intersect(current_item_in_bin, item): 112 | fit = False 113 | break 114 | 115 | if fit: 116 | if self.get_total_weight() + item.weight > self.max_weight: 117 | fit = False 118 | return fit 119 | 120 | self.items.append(item) 121 | 122 | if not fit: 123 | item.position = valid_item_position 124 | 125 | return fit 126 | 127 | if not fit: 128 | item.position = valid_item_position 129 | 130 | return fit 131 | 132 | 133 | class Packer: 134 | def __init__(self): 135 | self.bins = [] 136 | self.items = [] 137 | self.unfit_items = [] 138 | self.total_items = 0 139 | 140 | def add_bin(self, bin): 141 | return self.bins.append(bin) 142 | 143 | def add_item(self, item): 144 | self.total_items = len(self.items) + 1 145 | 146 | return self.items.append(item) 147 | 148 | def pack_to_bin(self, bin, item): 149 | fitted = False 150 | 151 | if not bin.items: 152 | response = bin.put_item(item, START_POSITION) 153 | 154 | if not response: 155 | bin.unfitted_items.append(item) 156 | 157 | return 158 | 159 | for axis in range(0, 3): 160 | items_in_bin = bin.items 161 | 162 | for ib in items_in_bin: 163 | pivot = [0, 0, 0] 164 | w, h, d = ib.get_dimension() 165 | if axis == Axis.WIDTH: 166 | pivot = [ 167 | ib.position[0] + w, 168 | ib.position[1], 169 | ib.position[2] 170 | ] 171 | elif axis == Axis.HEIGHT: 172 | pivot = [ 173 | ib.position[0], 174 | ib.position[1] + h, 175 | ib.position[2] 176 | ] 177 | elif axis == Axis.DEPTH: 178 | pivot = [ 179 | ib.position[0], 180 | ib.position[1], 181 | ib.position[2] + d 182 | ] 183 | 184 | if bin.put_item(item, pivot): 185 | fitted = True 186 | break 187 | if fitted: 188 | break 189 | 190 | if not fitted: 191 | bin.unfitted_items.append(item) 192 | 193 | def pack( 194 | self, bigger_first=False, distribute_items=False, 195 | number_of_decimals=DEFAULT_NUMBER_OF_DECIMALS 196 | ): 197 | for bin in self.bins: 198 | bin.format_numbers(number_of_decimals) 199 | 200 | for item in self.items: 201 | item.format_numbers(number_of_decimals) 202 | 203 | self.bins.sort( 204 | key=lambda bin: bin.get_volume(), reverse=bigger_first 205 | ) 206 | self.items.sort( 207 | key=lambda item: item.get_volume(), reverse=bigger_first 208 | ) 209 | 210 | for bin in self.bins: 211 | for item in self.items: 212 | self.pack_to_bin(bin, item) 213 | 214 | if distribute_items: 215 | for item in bin.items: 216 | self.items.remove(item) 217 | --------------------------------------------------------------------------------