diff --git a/mysite/plots/packing_algo.py b/mysite/plots/packing_algo.py index e2710c5981466dfb83c3b247d2ba6f682595b4a1..4ae3f8703d31677c183fa0185f40d15ac447d100 100644 --- a/mysite/plots/packing_algo.py +++ b/mysite/plots/packing_algo.py @@ -1,3 +1,16 @@ +"""This module represents a approximation algorithm for the optimal packing of convex polygons. + +The guideline and heart of this algorithm is the paper: + + Alt H., de Berg M., Knauer C. (2015) + Approximating Minimum-Area Rectangular and Convex Containers for Packing Convex Polygons. + In: Bansal N., Finocchi I. (eds) Algorithms - ESA 2015. Lecture Notes in Computer Science, + vol 9294. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-662-48350-3_3 + + DOI: + https://doi.org/10.1007/978-3-662-48350-3_3 +""" + import copy import numpy import math diff --git a/mysite/plots/polygon_creator.py b/mysite/plots/polygon_creator.py index 00d5518aaa0ba73a8cd667f823328c847a62c60b..2f8a278c25eb6f7194c01639baabb13363e32f99 100644 --- a/mysite/plots/polygon_creator.py +++ b/mysite/plots/polygon_creator.py @@ -1,18 +1,47 @@ -# Zerlegen des Polygons -#import import_ipynb +""" +This module can cut a rectangular with known area into random convex polygons. +These convex polygons can be packed with the approximation algorithm from the module packing_algo.py. +The purpose is to compare the area between the packed container and the optimal rectangular area. -# Voronoi diagramm -import scipy.spatial as spatial -from collections import defaultdict +This module has two different algorithm to cut the rectangular into convex polygons. +""" + +import scipy.spatial as spatial # package with voronoi diagramm import random import numpy -from shapely.geometry import Polygon, MultiPolygon +from shapely.geometry import Polygon from .packing_algo import ConvexPolygon, Point_xy, plot_polygons_in_single_plot def rectangle_cutter(rect_width: float, rect_height: float, cut_count: int, intervals=[0, 0.01, 0.05, 1], weights=[0, 0, 0.5, 1], render=False, plot_width=700, plot_height=700) -> [ConvexPolygon]: + """Cuts the given rectangle into several convex polygons + + Workflow of the algorithm: + 0. Check the convex polygon area and assign it to his interval, every interval has his own weight partner which + represent the chance to cut the convex polygon. + Weight 0.5 means a 50%, weight 1.0 means a 100% chance that the polygon goes threw steps 1-4 which means + it will be cut into two new convex polygons. + 1. Chooses two random viable edges. + 2. Takes a random point from each edge chosen in step 1 + 3. Builds the shortest distance between the points chosen in step 2 + 4. The distance in step 3 is the border which cuts the old convex polygon into two new ones. + 5. Step 0-4 will repeated until the cut_count is 0 or all polygons stopped to get cut because of step 0. + Args: + rect_width (float): the width of the starting rectangular + rect_height (float): the height of the starting rectangular + cut_count (int): the maximal cut count, example cut_count=5 means 2^5 convex polygons as result + which would be the the best case where every convex polygon cut decision was True + intervals ([float]): the area intervals which sort the convex polygons into different weigh groups + weights ([float]): the weights which represent the chance that a polygon will be cut + render (bool): if True the cut steps will also be rendered else not + plot_width (int): the width of the plot objects + plot_height (int): the height of the plot objects + + Returns: + all_polygons ([ConvexPolygon]): all cut convex polygons + """ if len(intervals) != len(weights): raise ValueError("the number of values from weights and intervals need to be the same") rectangle_polygon = ConvexPolygon([(0, 0), (0, rect_height), (rect_width, rect_height), (rect_width, 0), (0, 0)]) @@ -22,17 +51,17 @@ def rectangle_cutter(rect_width: float, rect_height: float, cut_count: int, inte if cut_count < 0: raise ValueError("the cut cut_count need to be positive to cut the polygon") while cut_count > 0 and len(polygons_to_cut) > 0: - cutted_polygons = [] + cut_polygons = [] for polygon in polygons_to_cut: related_area = polygon.area / max_area weight = find_weight(related_area, intervals, weights) rand = random.random() if rand <= weight: - cutted_polygons = cutted_polygons + cut_polygon(polygon) + cut_polygons = cut_polygons + cut_polygon(polygon) else: polygon.plot_fill = True small_area_polygons.append(polygon) - polygons_to_cut = cutted_polygons + polygons_to_cut = cut_polygons cut_count = cut_count - 1 if render: current_polygons = polygons_to_cut + small_area_polygons @@ -47,17 +76,42 @@ def rectangle_cutter(rect_width: float, rect_height: float, cut_count: int, inte return all_polygons -def find_weight(x, intervals, weights): +def find_weight(area: float, intervals: [float], weights: [float]) -> float: + """Finds the cut chance for the input area + + Args: + area (float): the input area + intervals ([float]): the intervals in which the input area can fall + weights ([float]): the cut chances for every interval + + Returns: + weights[index] (float): the cut chance for the input area + """ for index in range(0, len(intervals)): - if x <= intervals[index]: + if area <= intervals[index]: return weights[index] def cut_polygon(polygon: ConvexPolygon) -> [ConvexPolygon]: - polygon = polygon.shell # polyog + """Cuts a ConvexPolygon into two new ConvexPolygons + + Steps: + 1. Chooses two random edges from the input polygon + 2. Takes a random point from each edge chosen in step 1 + 3. Builds the shortest distance between the points chosen in step 2 + 4. The distance in step 3 is the border which cuts the old convex polygon into two new ones. + + Args: + polygon (ConvexPolygon): the convex input polygon which will be cut + + Returns: + [ConvexPolygon(polygon_1), ConvexPolygon(polygon_2)]([ConvexPolygon]): two new ConvexPolygons + """ + polygon = polygon.shell number_vertices = len(polygon) if number_vertices < 3: raise ValueError("Polygon has not enough vertices") + # Step 1 in the cut_polygon description first_edge = numpy.random.randint(1, number_vertices) second_edge = numpy.random.randint(1, number_vertices) while first_edge == second_edge: @@ -68,17 +122,26 @@ def cut_polygon(polygon: ConvexPolygon) -> [ConvexPolygon]: vertex_2_first_edge = polygon[first_edge] vertex_1_second_edge = polygon[second_edge - 1] vertex_2_second_edge = polygon[second_edge] - + # Step 2-4 in the cut_polygon description new_vertex_1 = random_point_between_edge(vertex_1_first_edge, vertex_2_first_edge) new_vertex_2 = random_point_between_edge(vertex_1_second_edge, vertex_2_second_edge) polygon_1 = polygon[:first_edge] + [new_vertex_1] + [new_vertex_2] + polygon[second_edge:] polygon_2 = [new_vertex_1] + polygon[first_edge:second_edge] + [new_vertex_2] + [new_vertex_1] - # s = [1,2,5,6,7] ,s[6:] => []; daher können wir für den spezial Fall second_edge== vertices_number so vorgehen return [ConvexPolygon(polygon_1), ConvexPolygon(polygon_2)] def random_point_between_edge(vertex_1: Point_xy, vertex_2: Point_xy) -> Point_xy: - new_vertex = (0, 0) + """Returns a random point from an edge, the edge is build by the two input points + + Uses the equation of a straight line to return a random point from the edge. + + Args: + vertex_1 (Point_xy): the first input vertex/point + vertex_2 (Point_xy): the second input vertex/point + + Returns: + new_vertex (Point_xy): the random point on the edge + """ if vertex_1[0] == vertex_2[0]: low, high = vertex_1[1], vertex_2[1] if low > high: @@ -92,11 +155,8 @@ def random_point_between_edge(vertex_1: Point_xy, vertex_2: Point_xy) -> Point_x rand_number = numpy.random.uniform(low, high) new_vertex = (rand_number, vertex_1[1]) else: - # y = m*x+n - # n = y-m*x - slope = (vertex_1[1] - vertex_2[1]) / (vertex_1[0] - vertex_2[ - 0]) # m = y2-y1/x2-x1 Formel für die Geradensteigung mithilfe aus zwei verschiedenen Punkten der Geraden - n = vertex_1[1] - (slope * vertex_1[0]) + slope = (vertex_1[1] - vertex_2[1]) / (vertex_1[0] - vertex_2[0]) # m = y2-y1/x2-x1 + n = vertex_1[1] - (slope * vertex_1[0]) # n = y-m*x low, high = vertex_1[1], vertex_2[1] if low > high: low, high = high, low @@ -106,9 +166,36 @@ def random_point_between_edge(vertex_1: Point_xy, vertex_2: Point_xy) -> Point_x return new_vertex -# https://gist.github.com/pv/8036995 -# https://stackoverflow.com/questions/20515554/colorize-voronoi-diagram/20678647#20678647 -def voronoi_polygons_wrapper(rect_width, rect_height, point_count): +def voronoi_polygons_wrapper(rect_width: float, rect_height: float, point_count: int) -> [ConvexPolygon]: + """Cuts the input rectangle into voronoi cell polygons (they are convex) + + Steps: + 1. Creates n random points in the rectangle area, where n = point_count + 2. Adds 4 distant dummy points which are far away from the rectangular to the random created points in steps 1, + the purpose of the dummy points is to be sure that the voronoi diagram is bigger than the rectangular, which + is important for step 4 + 3. creating the voronoi diagram with the scipy.spatial package + 4. Intersect the voronoi regions which are not infinite with the input rectangular these intersections are the + cut convex polygons. The intersection is done with shapely package. + + Help/Idea: + Using the scipy.spatial package for the voronoi diagram: + https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html + + The idea with the dummy elements is from: + https://stackoverflow.com/questions/20515554/colorize-voronoi-diagram + + Using the shapely package for the intersection between the input rectangle and finite voronoi regions: + https://pypi.org/project/Shapely/ + Args: + rect_width (float): the width of the input rectangle + rect_height (float): the height of the input rectangle + point_count (int): the number of random points which will created in the rectangle area, every point create a + convex polygon + + Returns: + polygon_list ([ConvexPolygon]): list of voronoi polygons cut from the input rectangle + """ boundary = numpy.array([[0, 0], [0, rect_height], [rect_width, rect_height], [rect_width, 0], [0, 0]]) boundary_polygon = Polygon(boundary) @@ -121,14 +208,14 @@ def voronoi_polygons_wrapper(rect_width, rect_height, point_count): help_border = [[border_point, border_point], [-border_point, border_point], [border_point, -border_point], [-border_point, -border_point]] points = numpy.append(points, help_border, axis=0) - # compute Voronoi tesselation + # compute voronoi tesselation vor = spatial.Voronoi(points) polygon_list = [] for region in vor.regions: if not -1 in region: polygon = [vor.vertices[i] for i in region] if len(polygon) > 2: - # Clipping polygon + # clipping the voronoi cell an creating a convex polygon poly = Polygon(polygon) clipped_poly = poly.intersection(boundary_polygon) polygon_list.append(ConvexPolygon(clipped_poly.exterior.coords)) @@ -136,6 +223,17 @@ def voronoi_polygons_wrapper(rect_width, rect_height, point_count): def pre_decimal_finder(i: float) -> int: + """Gives the number of pre_decimal places + + This function helps to build the 4 voronoi dummy points which need to be far away from the rectangular which will + cut into voronoi cells. + + Args: + i (float): the input value + + Returns: + counter (int): the number of pre_decimal places + """ if i < 0: i = abs(i) counter = 0