369 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| 
 | |
| 
 | |
| 
 |