Penlect/rectangle-packer

Possible other goal functions?

redhog opened this issue · 7 comments

Would it be possible to have other goal functions for the algorithm? In particular I'm interested in the rectangle with the smallest area with a certain aspect ratio that can hold the boxes. My use case is for packing windows on a screen (tiling), where any packing will be padded to the aspect ratio of the screen, and therefore often far from optimal. If not, do you know a python library that does this?

Sounds like a useful thing. Unfortunately don't know of any python library that can do that. I will add this to the todo-list of future improvements.

grst commented

I would also be interested in this. In particular, I want to optimally arrange rectangles into a square.

I'm planning to add this feature in July (when I have vacation)

In the latest version, 2.0.0, two keyword arguments have been added: max_width and max_height. Read more about them here: https://rectangle-packer.readthedocs.io/en/latest/rpack.html#rpack.pack

You can use max_width=x and max_height=y where x and y satisfies the desired aspect ratio x/y. Increase x and y in a for-loop until the packing is successful, @redhog. For the square case, you can use y = x @grst.

rbreu commented

Hi! I think there's an error when using max_width / max_height: If the algorithm runs up against these values, the positions it returns have (x,y) switched. My code (trying to get more of a square bounding box):

  positions = None
  print('sizes:', sizes)
  while not positions:
      print('max_width, max_height:', width)
      try:
	  positions = rpack.pack(
	      sizes, max_width=width, max_height=width)
      except rpack.PackingImpossibleError:
	  print('packing impossible, trying again')
	  width = math.ceil(width * 1.2)

  print('positions:', positions)
  print('overlapping:', rpack.overlapping(sizes, positions))

Result:

sizes: [(2736, 3648), (2736, 3648), (2736, 3648), (2736, 3648), (3648, 2736)]
max_width, max_height: 7065
packing impossible, trying again
max_width, max_height: 8478
positions: [(0, 0), (0, 2736), (0, 5472), (3648, 0), (3648, 2736)]
overlapping: (0, 1)

If you switch x and y for each position, the result is fine!

Same set of rectangles but larger max values, same issue:

sizes: [(2736, 3648), (2736, 3648), (3648, 2736), (2736, 3648), (2736, 3648)]
max_width, max_height: 7065
packing impossible, trying again
max_width, max_height: 14130
positions: [(0, 0), (0, 2736), (3648, 0), (0, 5472), (0, 8208)]
overlapping: (0, 1)

And here I let the max values get big enough that the algorithm can put the rectangles in its preferred shape, now x and y are correct:

sizes: [(2736, 3648), (2736, 3648), (3648, 2736), (2736, 3648), (2736, 3648)]
max_width, max_height: 7065
packing impossible, trying again
max_width, max_height: 70650
positions: [(0, 0), (2736, 0), (10944, 0), (5472, 0), (8208, 0)]
overlapping: None

Thank you @rbreu for reporting this bug. I have released a bugfix release, 2.0.1, and deployed new wheels to PyPI.

So here's the function I'm using for the "find the best packing for the given aspect ratio" problem. It just uses binary search to find a good packing. Not sure if that's the most efficient approach but it's working for my purposes. If there's interest I can make a PR into rectangle-packer.


def rpack_from_aspect_ratio(sizes: Sequence[Tuple[int, int]], aspect_ratio = 1., max_iter=8) -> Sequence[Tuple[int, int]]:
    """
    Find box corners of the smallest packing solution that fits in the given aspect ratio.
    This is approximate, and tries up to n_iter iterations.
    """
    upper_bound_width = max(sum(w for w, _ in sizes), sum(h*aspect_ratio for _, h in sizes))
    lower_bound_width = max(max(w for w, _ in sizes), max(h*aspect_ratio for _, h in sizes))
    result = None
    width = upper_bound_width
    for i in range(max_iter):
        height = int(width / aspect_ratio)
        try:
            print(f"Trying packing with {width}x{height}")
            result = rpack.pack(sizes, max_width=width, max_height=height)
        except PackingImpossibleError:
            lower_bound_width = width
        else:
            upper_bound_width = width
        width = int(lower_bound_width + upper_bound_width)//2
    assert result is not None, f"Should have been able to pack with width {upper_bound_width}"
    return result

Passes test:

def test_pack_with_aspect_ratio():
    corners = rpack_from_aspect_ratio(sizes = [(6, 6), (5, 5), (4, 4)], aspect_ratio=1.)
    assert corners == [(0, 0), (6, 0), (6, 5)]
    corners = rpack_from_aspect_ratio(sizes = [(6, 6), (5, 5), (4, 4)], aspect_ratio=10)
    assert corners == [(0, 0), (6, 0), (11, 0)]
    corners = rpack_from_aspect_ratio(sizes = [(6, 6), (5, 5), (4, 4)], aspect_ratio=0.1)
    assert corners == [(0, 0), (0, 6), (0, 11)]