def test_pack_boxes_big_die_and_several_decks_of_cards(self): deck = ItemTuple('deck', [2, 8, 12], 0) die = ItemTuple('die', [8, 8, 8], 0) item_info = [deck, deck, deck, deck, die] box_dims = [8, 8, 12] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(len(packed_items), 2) self.assertEqual(packed_items, [[deck] * 4, [die]])
def test_pack_boxes_three_item_one_box(self): ''' test odd sized items will be rotated to fit ''' item1 = ItemTuple('Item1', [13, 13, 31], 0) item2 = ItemTuple('Item2', [8, 13, 31], 0) item3 = ItemTuple('Item3', [5, 13, 31], 0) item_info = [item1, item2, item3] box_dims = [13, 26, 31] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item1, item2, item3]]))
def test_pack_boxes_odd_sizes(self): ''' test odd sized items will be rotated to fit ''' item1 = ItemTuple('Item1', [3, 8, 10], 0) item2 = ItemTuple('Item2', [1, 2, 5], 0) item3 = ItemTuple('Item3', [1, 2, 2], 0) item_info = [item1, item2, item2, item3] box_dims = [10, 20, 20] packed_items = pack_boxes(box_dims, item_info) self.assertEqual([[item1, item2, item2, item3]], packed_items)
def test_pack_boxes_odd_sizes_again(self): ''' test items with different dimensions will be rotated to fit into one box ''' item1 = ItemTuple('Item1', [1, 18, 19], 0) item2 = ItemTuple('Item2', [17, 18, 18], 0) item3 = ItemTuple('Item3', [1, 17, 18], 0) item_info = [item1, item2, item3] box_dims = [18, 18, 19] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item1, item2, item3]]))
def test_pack_boxes_flat_box(self): item = ItemTuple('MFuelMock', [1.25, 7, 10], 0) item_info = [item] * 3 box_dims = [3.5, 9.5, 12.5] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(2, len(packed_items)) self.assertEqual(2, len(packed_items[0]))
def how_many_items_fit(item_info, box_info, max_packed=None): ''' returns the number of of items of a certain size can fit in a box, as well as the remaining volume assumes item and box dimensions are on in the same units Args: item_info (Dict[{ width: float height: float length: float weight: float }]) box_info (Dict[{ width: float height: float length: float weight: float }]) max_packed (int) Returns: Dict[{ total_packed: int remaining_volume: float }] ''' item_dims = sorted( [item_info['width'], item_info['height'], item_info['length']]) box_dims = sorted( [box_info['width'], box_info['height'], box_info['length']]) remaining_dimensions = [box_dims] remaining_volume = volume(box_dims) item = ItemTuple(None, item_dims, item_info.get('weight', 0)) # a list of lists. each nested list is representative of a package items_packed = [[]] while remaining_dimensions != []: for block in remaining_dimensions: # items_to_pack is of length 4 at every loop because # insert_items_into_dimensions will pack up to 3 items at any given # time and then check that there are more items to pack before # continuing items_to_pack = [item, item, item, item] remaining_dimensions, items_packed = insert_items_into_dimensions( remaining_dimensions, items_to_pack, items_packed) # items_to_pack updates, insert items into dimensions may pack more # than one item and therefore we find the difference between the # length of the remaining items to pack and the original (4) remaining_volume -= volume(item_dims) * (4 - len(items_to_pack)) if (max_packed is not None and len(items_packed[0]) == int(max_packed)): # set remaining dimensions to empty to break from the while loop remaining_dimensions = [] break return { 'total_packed': len(items_packed[0]), 'remaining_volume': remaining_volume }
def test_setup_packages(self): item = ItemTuple('Item1', [1, 2, 3], 0) normal_box = self.make_generic_box('NORM') packed_boxes = {normal_box: [[item]]} box_dictionary = setup_packages(packed_boxes) expected_return = Packaging(box=normal_box, items_per_box=[[item]], last_parcel=None) self.assertEqual(expected_return, box_dictionary)
def test_pack_boxes_two_item_two_box(self): ''' test two items of same size as box will go into two boxes ''' item = ItemTuple('Item1', [13, 13, 31], 0) item_info = [item, item] box_dims = [13, 13, 31] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item], [item]]))
def test_pack_boxes_one_item(self): ''' test exact fit one item ''' item1 = ItemTuple('Item1', [13, 13, 31], 0) item_info = [item1] box_dims = [13, 13, 31] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item1]]))
def test_pack_boxes_one_overflow(self): ''' tests when there should be one overflow box ''' item = ItemTuple('Item1', [1, 1, 1], 0) item_info = [item] * 28 box_dims = [3, 3, 3] packed_items = pack_boxes(box_dims, item_info) self.assertEqual([[item] * 27, [item]], packed_items)
def test_pack_boxes_two_item_exact(self): ''' test items will go exactly into box ''' item1 = ItemTuple('Item1', [13, 13, 31], 0) item_info = [item1, item1] box_dims = [13, 26, 31] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item1, item1]]))
def test_pack_boxes_tight_fit_many_oblong_inexact(self): ''' tests that the algorithm remains at least as accurate as it already is if it were perfect, the first box would have 48 in it ''' item = ItemTuple('Item1', [1, 2, 3], 0) item_info = [item] * 49 box_dims = [4, 8, 9] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(2, len(packed_items)) self.assertGreaterEqual(44, len(packed_items[0]))
def test_pack_boxes_100_items(self): ''' test many items into one box with inexact fit ''' item = ItemTuple('Item1', [5, 5, 5], 0) item_info = [item] * 100 box_dims = [51, 51, 6] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(len(packed_items), 1)
def test_pack_boxes_tight_fit_many_oblong(self): ''' tests a tight fit for non-cubic items ''' item = ItemTuple('Item1', [1, 2, 3], 0) item_info = [item] * 107 box_dims = [8, 9, 9] packed_items = pack_boxes(box_dims, item_info) expected_return = [[item] * 106, [item]] self.assertEqual(2, len(packed_items)) self.assertEqual(expected_return, packed_items)
def test_pack_boxes_dim_over_2(self): ''' test that when length of item <= length of box / 2 it packs along longer edge ''' item = ItemTuple('Item1', [3, 4, 5], 0) item_info = [item] * 4 box_dims = [6, 8, 10] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item, item, item, item]]))
def test_pack_3_boxes(self): ''' test that multiple parcels will be selected ''' item = ItemTuple('Item1', [4, 4, 12], 0) item_info = [item] * 3 # item_info = [['Item1', [2, 2, 12]]] * 3 # no error # item_info = [['Item1', [4, 4, 12]]] * 2 # no error box_dims = [4, 4, 12] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item], [item], [item]]))
def test_slightly_larger_box(self): ''' test inexact dimensions ''' # Fails due to recursion limits reached item = ItemTuple('Item1', [4, 4, 12], 0) item_info = [item] * 2 box_dims = [5, 8, 12] # box_dims = [4, 8, 12] # passes packed_items = pack_boxes(box_dims, item_info) self.assertEqual(packed_items, ([[item, item]]))
def test_setup_packages_three_flat_rates(self): ''' assert that when there are two flat rates that require a different number of boxes, it selects the one that requires the fewest ''' item = ItemTuple('Item1', [1, 2, 3], 0) normal_box = self.make_generic_box('NORM') packed_boxes = {normal_box: [[item, item]]} box_dictionary = setup_packages(packed_boxes) expected_return = Packaging(box=normal_box, items_per_box=[[item, item]], last_parcel=None) self.assertEqual(expected_return, box_dictionary)
def test_setup_packages_smaller_volume(self): ''' asserts the the dictionary uses the smallest box with the fewest parcels ''' item = ItemTuple('Item1', [1, 2, 3], 0) normal_box = self.make_generic_box('NORM') bigger_box = self.make_generic_box('Big', volume=2000) packed_boxes = {normal_box: [[item, item]], bigger_box: [[item, item]]} box_dictionary = setup_packages(packed_boxes) expected_return = Packaging(box=normal_box, items_per_box=[[item, item]], last_parcel=None) self.assertEqual(expected_return, box_dictionary)
def test_pack_boxes_100_items_2_boxes(self): ''' test many items separated into 2 boxes with exact fit ''' item = ItemTuple('Item1', [5, 5, 5], 0) item_info = [item] * 100 box_dims = [10, 25, 25] packed_items = pack_boxes(box_dims, item_info) self.assertEqual(len(packed_items), 2) self.assertEqual(len(packed_items[0]), 50) self.assertEqual(len(packed_items[1]), 50)
def compare_pyshipping_with_shotput(): from random import randint from pyshipping import binpack_simple as binpack from pyshipping.package import Package from time import time items = [] py_items = [] box_dims = sorted( [randint(100, 200), randint(100, 200), randint(100, 200)]) num_items = 500 for _ in xrange(num_items): item_dims = sorted( [randint(20, 100), randint(20, 100), randint(20, 100)]) items.append(ItemTuple(str(volume(item_dims)), item_dims, 0)) py_items.append(Package((item_dims[0], item_dims[1], item_dims[2]), 0)) start = time() items_packed = pack_boxes(box_dims, items) end = time() shotput = { 'num_parcels': len(items_packed), 'items_per_parcel': [len(parcel) for parcel in items_packed], 'time': end - start } py_box = Package((box_dims[0], box_dims[1], box_dims[2]), 0) start = time() py_items_packed = binpack.packit(py_box, py_items) end = time() pyshipping = { 'num_parcels': len(py_items_packed[0]), 'items_per_parcel': [len(parcel) for parcel in py_items_packed[0]], 'time': end - start } if len(items_packed) > len(py_items_packed[0]): best_results = 'pyshipping' elif len(items_packed) < len(py_items_packed[0]): best_results = 'shotput' else: best_results = 'tie' return { 'shotput': shotput, 'pyshipping': pyshipping, 'best_results': best_results }
def test_setup_packages_fewest_parcels(self): ''' asserts the the dictionary uses the smallest box with the fewest parcels and if flat rate has more parcels, don't even return it. ''' item = ItemTuple('Item1', [1, 2, 3], 0) normal_box = self.make_generic_box('NORM') smaller_box = self.make_generic_box('Small', volume=500) packed_boxes = { normal_box: [[item, item]], smaller_box: [[item], [item]] } box_dictionary = setup_packages(packed_boxes) expected_return = Packaging(box=normal_box, items_per_box=[[item, item]], last_parcel=None) self.assertEqual(expected_return, box_dictionary)
def test_setup_packages_complex(self): ''' asserts that in complex situations, it chooses the smallest, fewest parcels, cheapest box. ''' item = ItemTuple('Item1', [1, 2, 3], 0) normal_box = self.make_generic_box('NORM') smaller_box = self.make_generic_box('Small', volume=500) bigger_box = self.make_generic_box('Big', volume=2000) packed_boxes = { normal_box: [[item, item]], bigger_box: [[item, item]], smaller_box: [[item], [item]] } expected_return = Packaging(box=normal_box, items_per_box=[[item, item]], last_parcel=None) box_dictionary = setup_packages(packed_boxes) self.assertEqual(expected_return, box_dictionary)
def api_packing_algorithm(boxes_info, items_info, options): ''' non-database calling method which allows checking multiple boxes for packing efficiency Args: session (sqlalchemy.orm.session.Session) boxes_info (List[Dict( weight: float height: float length: float width: float dimension_units: ('inches', 'centimeters', 'feet', 'meters') weight_units: ('grams', 'pounds', 'kilograms', 'onces') name: String )]) items_info (List[Dict( weight: float height: float length: float width: float dimension_units: ('inches', 'centimeters', 'feet', 'meters') weight_units: ('grams', 'pounds', 'kilograms', 'onces') product_name: String )]) options (Dict( max_weight: float )) Returns: Dict[ 'package_contents': List[Dict[ items_packed: Dict[item, quantity] total_weight: float 'best_box': Dict[ weight: float height: float length: float width: float dimension_units: ('inches', 'centimeters', 'feet', 'meters') weight_units: ('grams', 'pounds', 'kilograms', 'onces') name: String ] ] ] ''' boxes = [] items = [] if len(set(box['name'] for box in boxes_info)) < len(boxes_info): # non-unique names for the boxes have been used. raise BoxError('Please use unique boxes with unique names') min_box_dimensions = [0, 0, 0] for item in items_info: dimensions = sorted([ float(item['width']), float(item['height']), float(item['length']) ]) weight_units = item['weight_units'] item_weight = float(item['weight']) items += ([ItemTuple(item['product_name'], dimensions, item_weight)] * item['quantity']) min_box_dimensions = [ max(a, b) for a, b in zip(dimensions, min_box_dimensions) ] if options is not None: max_weight = int(options.get('max_weight', 31710)) else: max_weight = 31710 for box in boxes_info: dimension_units = box.get('dimension_units', units.CENTIMETERS) dimensions = sorted([box['width'], box['length'], box['height']]) if does_it_fit(min_box_dimensions, dimensions): box_weight = float(box['weight']) boxes.append({ 'box': Box(name=box['name'], weight=box_weight, length=dimensions[0], width=dimensions[1], height=dimensions[2]), 'dimensions': dimensions }) if len(boxes) == 0: raise BoxError('Some of your products are too big for your boxes. ' 'Please provide larger boxes.') # sort boxes by volume boxes = sorted(boxes, key=lambda box: volume(box['dimensions'])) # send everything through the packing algorithm package_info = packing_algorithm(items, boxes, max_weight) package_contents_dict = [ get_item_dictionary_from_list(parcel) for parcel in package_info.items_per_box ] package_contents = [] best_box = [ box for box in boxes_info if box['name'] == package_info.box.name ][0] if package_info.last_parcel is not None: last_parcel = [ box for box in boxes_info if box['name'] == package_info.last_parcel.name ][0] else: last_parcel = None for i, parcel in enumerate(package_contents_dict): if i == len(package_contents_dict) - 1 and last_parcel is not None: selected_box = last_parcel total_weight = package_info.last_parcel.weight else: selected_box = best_box total_weight = package_info.box.weight items_packed = {} for item, info in parcel.items(): items_packed[item] = info['quantity'] total_weight += info['quantity'] * info['item'].weight package_contents.append({ 'packed_products': items_packed, 'total_weight': total_weight, 'box': selected_box }) return {'packages': package_contents}
def pre_pack_boxes(box_info, items_info, options): ''' returns the packed items of one specific box based on item_info the item info input does not require a db call Args boxes_info (Dict[ weight: float height: float length: float width: float dimension_units: ('inches', 'centimeters', 'feet', 'meters') weight_units: ('grams', 'pounds', 'kilograms', 'onces') name: String ]) products_info (List[Dict[ weight: float height: float length: float width: float dimension_units: ('inches', 'centimeters', 'feet', 'meters') weight_units: ('grams', 'pounds', 'kilograms', 'onces') product_name: String ]) options (Dict[ max_weight: float ]) Returns List[Dict[{ packed_products: Dict[item, qty], total_weight: float }]] ''' dimension_units = box_info['dimension_units'] box_dims = sorted( [box_info['width'], box_info['length'], box_info['height']]) items_to_pack = [] weight_units = box_info['weight_units'] box_weight = box_info['weight'] total_weight = box_weight max_weight = options.get('max_weight', 31710) # given max weight or 70lbs for item in items_info: dimension_units = item['dimension_units'] weight_units = item['weight_units'] sorted_dims = sorted([item['height'], item['length'], item['width']]) if not does_it_fit(sorted_dims, box_dims): raise BoxError('Some of your items are too big for the box you\'ve' ' selected. Please select a bigger box or contact' ' [email protected].') item['weight'] = item['weight'] items_to_pack += [ ItemTuple(item['product_name'], sorted_dims, int(item['weight'])) ] * int(item['quantity']) total_weight += item['weight'] * int(item['quantity']) items_to_pack = sorted(items_to_pack, key=lambda item: item.dimensions[2], reverse=True) box_dims = sorted(box_dims) items_packed = pack_boxes(box_dims, items_to_pack) if math.ceil(float(total_weight) / max_weight) > len(items_packed): additional_box = [] for items in items_packed: while weight_of_box_contents(items) + box_weight > max_weight: if (weight_of_box_contents(additional_box) + items[-1].weight <= max_weight): additional_box.append(items.pop()) else: items_packed.append(list(additional_box)) additional_box = [items.pop()] items_packed.append(additional_box) parcel_shipments = [] for items in items_packed: item_qty = Counter() parcel_weight = box_weight for item in items: item_qty[item.item_number] += 1 parcel_weight += item.weight parcel_shipments.append({ 'packed_products': dict(item_qty), 'total_weight': parcel_weight }) return parcel_shipments