2025-01-22 16:18:30 +01:00

369 lines
13 KiB
Python

from .pack_algo import PackingAlgorithm
from .geometry import Rectangle
import itertools
import operator
class Guillotine(PackingAlgorithm):
"""Implementation of several variants of Guillotine packing algorithm
For a more detailed explanation of the algorithm used, see:
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
"""
def __init__(self, width, height, rot=True, merge=True, *args, **kwargs):
"""
Arguments:
width (int, float):
height (int, float):
merge (bool): Optional keyword argument
"""
self._merge = merge
super(Guillotine, self).__init__(width, height, rot, *args, **kwargs)
def _add_section(self, section):
"""Adds a new section to the free section list, but before that and if
section merge is enabled, tries to join the rectangle with all existing
sections, if successful the resulting section is again merged with the
remaining sections until the operation fails. The result is then
appended to the list.
Arguments:
section (Rectangle): New free section.
"""
section.rid = 0
plen = 0
while self._merge and self._sections and plen != len(self._sections):
plen = len(self._sections)
self._sections = [s for s in self._sections if not section.join(s)]
self._sections.append(section)
def _split_horizontal(self, section, width, height):
"""For an horizontal split the rectangle is placed in the lower
left corner of the section (section's xy coordinates), the top
most side of the rectangle and its horizontal continuation,
marks the line of division for the split.
+-----------------+
| |
| |
| |
| |
+-------+---------+
|#######| |
|#######| |
|#######| |
+-------+---------+
If the rectangle width is equal to the the section width, only one
section is created over the rectangle. If the rectangle height is
equal to the section height, only one section to the right of the
rectangle is created. If both width and height are equal, no sections
are created.
"""
# First remove the section we are splitting so it doesn't
# interfere when later we try to merge the resulting split
# rectangles, with the rest of free sections.
#self._sections.remove(section)
# Creates two new empty sections, and returns the new rectangle.
if height < section.height:
self._add_section(Rectangle(section.x, section.y+height,
section.width, section.height-height))
if width < section.width:
self._add_section(Rectangle(section.x+width, section.y,
section.width-width, height))
def _split_vertical(self, section, width, height):
"""For a vertical split the rectangle is placed in the lower
left corner of the section (section's xy coordinates), the
right most side of the rectangle and its vertical continuation,
marks the line of division for the split.
+-------+---------+
| | |
| | |
| | |
| | |
+-------+ |
|#######| |
|#######| |
|#######| |
+-------+---------+
If the rectangle width is equal to the the section width, only one
section is created over the rectangle. If the rectangle height is
equal to the section height, only one section to the right of the
rectangle is created. If both width and height are equal, no sections
are created.
"""
# When a section is split, depending on the rectangle size
# two, one, or no new sections will be created.
if height < section.height:
self._add_section(Rectangle(section.x, section.y+height,
width, section.height-height))
if width < section.width:
self._add_section(Rectangle(section.x+width, section.y,
section.width-width, section.height))
def _split(self, section, width, height):
"""
Selects the best split for a section, given a rectangle of dimmensions
width and height, then calls _split_vertical or _split_horizontal,
to do the dirty work.
Arguments:
section (Rectangle): Section to split
width (int, float): Rectangle width
height (int, float): Rectangle height
"""
raise NotImplementedError
def _section_fitness(self, section, width, height):
"""The subclass for each one of the Guillotine selection methods,
BAF, BLSF.... will override this method, this is here only
to asure a valid value return if the worst happens.
"""
raise NotImplementedError
def _select_fittest_section(self, w, h):
"""Calls _section_fitness for each of the sections in free section
list. Returns the section with the minimal fitness value, all the rest
is boilerplate to make the fitness comparison, to rotatate the rectangles,
and to take into account when _section_fitness returns None because
the rectangle couldn't be placed.
Arguments:
w (int, float): Rectangle width
h (int, float): Rectangle height
Returns:
(section, was_rotated): Returns the tuple
section (Rectangle): Section with best fitness
was_rotated (bool): The rectangle was rotated
"""
fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections
if self._section_fitness(s, w, h) is not None)
fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections
if self._section_fitness(s, h, w) is not None)
if not self.rot:
fitr = []
fit = itertools.chain(fitn, fitr)
try:
_, sec, rot = min(fit, key=operator.itemgetter(0))
except ValueError:
return None, None
return sec, rot
def add_rect(self, width, height, rid=None):
"""
Add rectangle of widthxheight dimensions.
Arguments:
width (int, float): Rectangle width
height (int, float): Rectangle height
rid: Optional rectangle user id
Returns:
Rectangle: Rectangle with placemente coordinates
None: If the rectangle couldn be placed.
"""
assert(width > 0 and height >0)
# Obtain the best section to place the rectangle.
section, rotated = self._select_fittest_section(width, height)
if not section:
return None
if rotated:
width, height = height, width
# Remove section, split and store results
self._sections.remove(section)
self._split(section, width, height)
# Store rectangle in the selected position
rect = Rectangle(section.x, section.y, width, height, rid)
self.rectangles.append(rect)
return rect
def fitness(self, width, height):
"""
In guillotine algorithm case, returns the min of the fitness of all
free sections, for the given dimension, both normal and rotated
(if rotation enabled.)
"""
assert(width > 0 and height > 0)
# Get best fitness section.
section, rotated = self._select_fittest_section(width, height)
if not section:
return None
# Return fitness of returned section, with correct dimmensions if the
# the rectangle was rotated.
if rotated:
return self._section_fitness(section, height, width)
else:
return self._section_fitness(section, width, height)
def reset(self):
super(Guillotine, self).reset()
self._sections = []
self._add_section(Rectangle(0, 0, self.width, self.height))
class GuillotineBaf(Guillotine):
"""Implements Best Area Fit (BAF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return section.area()-width*height
class GuillotineBlsf(Guillotine):
"""Implements Best Long Side Fit (BLSF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return max(section.width-width, section.height-height)
class GuillotineBssf(Guillotine):
"""Implements Best Short Side Fit (BSSF) section selection criteria for
Guillotine algorithm.
"""
def _section_fitness(self, section, width, height):
if width > section.width or height > section.height:
return None
return min(section.width-width, section.height-height)
class GuillotineSas(Guillotine):
"""Implements Short Axis Split (SAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if section.width < section.height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineLas(Guillotine):
"""Implements Long Axis Split (LAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if section.width >= section.height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineSlas(Guillotine):
"""Implements Short Leftover Axis Split (SLAS) selection rule for
Guillotine algorithm.
"""
def _split(self, section, width, height):
if section.width-width < section.height-height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineLlas(Guillotine):
"""Implements Long Leftover Axis Split (LLAS) selection rule for
Guillotine algorithm.
"""
def _split(self, section, width, height):
if section.width-width >= section.height-height:
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineMaxas(Guillotine):
"""Implements Max Area Axis Split (MAXAS) selection rule for Guillotine
algorithm. Maximize the larger area == minimize the smaller area.
Tries to make the rectangles more even-sized.
"""
def _split(self, section, width, height):
if width*(section.height-height) <= height*(section.width-width):
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
class GuillotineMinas(Guillotine):
"""Implements Min Area Axis Split (MINAS) selection rule for Guillotine
algorithm.
"""
def _split(self, section, width, height):
if width*(section.height-height) >= height*(section.width-width):
return self._split_horizontal(section, width, height)
else:
return self._split_vertical(section, width, height)
# Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one
# Axis split, and one selection criteria.
class GuillotineBssfSas(GuillotineBssf, GuillotineSas):
pass
class GuillotineBssfLas(GuillotineBssf, GuillotineLas):
pass
class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas):
pass
class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas):
pass
class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas):
pass
class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas):
pass
class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas):
pass
class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas):
pass
class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas):
pass
class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas):
pass
class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas):
pass
class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas):
pass
class GuillotineBafSas(GuillotineBaf, GuillotineSas):
pass
class GuillotineBafLas(GuillotineBaf, GuillotineLas):
pass
class GuillotineBafSlas(GuillotineBaf, GuillotineSlas):
pass
class GuillotineBafLlas(GuillotineBaf, GuillotineLlas):
pass
class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas):
pass
class GuillotineBafMinas(GuillotineBaf, GuillotineMinas):
pass