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