├── .gitignore ├── README.md ├── ants.py ├── geometry.py ├── graph.py ├── hull.py ├── life.py ├── lindenmayer.py ├── penrose_tsp.py ├── quadtree.py ├── run_all.py ├── shortpath.py ├── test_tsp.py ├── triangulation.py ├── tsplib.py ├── uberplot.py ├── utils.py └── voronoi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.mat 3 | *.points 4 | *.segments 5 | *.png 6 | *.svg 7 | *.triangles 8 | *.graph 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Übergeekism 2 | =========== 3 | 4 | This is an attempt at using as many as possible **cool** computer science stuff 5 | to produce a single image. 6 | 7 | Algorithms may not be implemented in the most efficient manner, as the aim is to 8 | have elegant and simple code for educational purpose. 9 | 10 | Until now, the following algorithms/data structure/concepts are used: 11 | - the (logo) turtle, 12 | - Lindenmayer systems, 13 | - Penrose tiling, 14 | - travelling salesman problem, 15 | - ant colony algorithm, 16 | - A\* shortest path, 17 | - circumcircle of a triangle, 18 | - Delaunay triangulation, 19 | - Bowyer-Watson algorithm, 20 | - lines and segments intersections, 21 | - graph (adjacency list, adjacency matrix), 22 | - hash table. 23 | 24 | And the following ones are implemented but not used: 25 | - convex hull, 26 | - Chan's algorithm, 27 | 28 | The current code is written in Python. 29 | 30 | 31 | Penrose tiling drawing 32 | ---------------------- 33 | 34 | The main shape visible on the image is a Penrose tiling (type P3), which is a 35 | **non-periodic tiling** with an absurd level of coolness. 36 | 37 | The edges are **recursively** built with a **Lindenmayer system**. Yes, it is 38 | capable of building a Penrose tiling if you know which grammar to use. Yes, this 39 | is insanely cool. 40 | 41 | The Lindenamyer system works by drawing edges one after another, we thus use a 42 | (LOGO) **turtle** to draw them. 43 | 44 | Because the L-system grammar is not very efficient to build the tiling, we 45 | insert edges in a data structure that contains an unordered collection of unique 46 | element: a **hash table**. 47 | 48 | 49 | Travelling Salesman Problem 50 | --------------------------- 51 | 52 | The Penrose tiling segments defines a **graph**, which connects a set of 53 | vertices with a set of edges. We can consider the vertices as cities and edges 54 | as roads between them. 55 | 56 | Now we want to find the shortest possible route that visits each city exactly 57 | once and returns to the origin city. This is the **Travelling Salesman 58 | Problem**. We use an **Ant Colony Optimization** algorithm to (try) to solve it. 59 | 60 | Because each city is not connected to every other cities, we need to find the 61 | shortest path between two cities. This is done with the help of the famously 62 | cool **A-star** algorithm. 63 | 64 | The ant colony algorithm output a path that connect every cities, which is drawn 65 | on the image, but it also stores a so-called pheromones matrix, which can be 66 | drawn as edges with variable transparency/width. 67 | 68 | 69 | Penrose tiling 70 | -------------- 71 | 72 | Because the L-system draws the Penrose tiling segments by segments, we need to 73 | compute how each segment is related to the diamonds to rebuild the tiling 74 | corresponding to all those edges. 75 | 76 | Fortunately, computing a **Delaunay triangulation** of the Penrose vertices 77 | brings back a triangular subgraph of the Penrose graph (how cool is that!?) and 78 | stores plain shapes (triangles) instead of unordered segments. This is done 79 | thanks to the **Bowyer-Watson algorithm**. 80 | 81 | But this triangulation contains edges that link the set of exterior vertices, 82 | which are not in the Penrose tiling. This is solved by ~~computing the **convex 83 | hull**, with the **Chan's algorithm**, and removing the triangles that contains 84 | those edges from the triangulation~~ removing obtuse triangles. 85 | 86 | 87 | Penrose graph 88 | ------------- 89 | 90 | We now want to connect each diamond to its neighbour, so as to build the Penrose 91 | graph itself. One way to do that is to compute the **Voronoï diagram** of the 92 | previous Delaunay triangulation. But we really want the Voronoï diagram of the 93 | Penrose tiling (with diamonds), not its triangulation (with, well, triangles). 94 | 95 | We thus need to merge each edge of the Voronoï graph that do not cross a segment 96 | of the Penrose tiling into a single node, while preserving its neighbours. We 97 | thus need to compute **segments intersection** (which does not seems so cool but 98 | really is) and find a way to reduce the graph. 99 | 100 | 101 | TODO 102 | ---- 103 | 104 | More coolness: 105 | - **quad trees** may be useful somewhere to query neighbors points?, 106 | - Draw the neighborhood with **splines** across the center of diamonds 107 | segments, 108 | - Run a **cellular automata** on this Penrose tiling, 109 | - Draw a **planner** on it. 110 | 111 | Maybe even more coolness? 112 | - percolation theory? 113 | 114 | Improvements: 115 | - Use a triangular matrix for pheromones in ACO. 116 | 117 | -------------------------------------------------------------------------------- /ants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import math 5 | import random 6 | from collections import Counter 7 | import shortpath 8 | from utils import tour 9 | from utils import LOG,LOGN 10 | 11 | def euclidian_distance( ci, cj, graph = None): 12 | return math.sqrt( float(ci[0] - cj[0])**2 + float(ci[1] - cj[1])**2 ) 13 | 14 | def graph_distance( ci, cj, graph ): 15 | p,c = shortpath.astar( graph, ci, cj ) 16 | return c 17 | 18 | 19 | def cost( permutation, cost_func, cities ): 20 | dist = 0 21 | for ci,cj in tour(permutation): 22 | dist += cost_func( ci, cj, cities ) 23 | return dist 24 | 25 | 26 | def look( cities, last, exclude, pheromones, w_heuristic, w_history, cost_func = graph_distance ): 27 | choices = [] 28 | # gather informations about possible moves 29 | for current in cities: 30 | if current in exclude: 31 | # This is faster than "if current not in exclude" 32 | continue 33 | c = {"city" : current} 34 | c["history"] = pheromones[last][current] ** w_history 35 | c["distance"] = cost_func( last, current, cities ) 36 | c["heuristic"] = (1.0 / c["distance"]) ** w_heuristic 37 | c["proba"] = c["history"] * c["heuristic"] 38 | choices.append(c) 39 | return choices 40 | 41 | 42 | def proba_choose( choices ): 43 | s = float(sum( c["proba"] for c in choices )) 44 | if s == 0.0: 45 | return random.choice(choices)["city"] 46 | 47 | v = random.random() 48 | for i,c in enumerate(choices): 49 | v -= c["proba"] / s 50 | if v <= 0.0: 51 | return c["city"] 52 | 53 | return c[-1]["city"] 54 | 55 | 56 | def greedy_choose( choices ): 57 | c = max( choices, key = lambda c : c["proba"] ) 58 | return c["city"] 59 | 60 | 61 | def walk( cities, pheromones, w_heuristic, w_history, c_greedy, cost_func = graph_distance ): 62 | assert( len(cities) > 0 ) 63 | # permutations are indices 64 | # randomly draw the first city index 65 | permutation = [ random.choice( cities.keys() ) ] 66 | # then choose the next ones to build the permutation 67 | while len(permutation) < len(cities): 68 | choices = look( cities, permutation[-1], permutation, pheromones, w_heuristic, w_history, cost_func ) 69 | do_greedy = ( random.random() <= c_greedy ) 70 | if do_greedy: 71 | next_city = greedy_choose( choices ) 72 | else: 73 | next_city = proba_choose( choices ) 74 | permutation.append( next_city ) 75 | 76 | # assert no duplicates 77 | assert( max(Counter(permutation).values()) == 1 ) 78 | return permutation 79 | 80 | 81 | def initialize_pheromones_whole( cities, init_value ): 82 | rows = {} 83 | for i in cities: 84 | cols = {} 85 | for j in cities: 86 | cols[j] = init_value 87 | rows[i] = cols 88 | return rows 89 | 90 | 91 | def update_global_whole( pheromones, candidate, graph, decay ): 92 | for i,j in tour(candidate["permutation"]): 93 | value = ((1.0 - decay) * pheromones[i][j]) + (decay * (1.0/candidate["cost"])) 94 | pheromones[i][j] = value 95 | pheromones[j][i] = value 96 | 97 | 98 | def update_local_whole( pheromones, candidate, graph, w_pheromone, init_pheromone ): 99 | for i,j in tour(candidate["permutation"]): 100 | value = ((1.0 - w_pheromone) * pheromones[i][j]) + (w_pheromone * init_pheromone) 101 | pheromones[i][j] = value 102 | pheromones[j][i] = value 103 | 104 | 105 | def initialize_pheromones_neighbors( cities, init_value ): 106 | rows = {} 107 | for i in cities: 108 | cols = {} 109 | for j in cities: 110 | # set an init value for neighbors only 111 | if j in cities[i]: 112 | cols[j] = init_value 113 | else: # else, there should be no edge 114 | cols[j] = 0 115 | rows[i] = cols 116 | return rows 117 | 118 | 119 | def update_global_neighbors( pheromones, candidate, graph, decay ): 120 | for ci,cj in tour(candidate["permutation"]): 121 | # subpath between ci and cj 122 | p,c = path.astar( graph, ci, cj ) 123 | # deposit pheromones on each edges of the subpath 124 | for i,j in zip(p,p[1:]): 125 | value = ((1.0 - decay) * pheromones[i][j]) + (decay * (1.0/candidate["cost"])) 126 | pheromones[i][j] = value 127 | pheromones[j][i] = value 128 | 129 | 130 | def update_local_neighbors( pheromones, candidate, graph, w_pheromone, init_pheromone ): 131 | for ci,cj in tour(candidate["permutation"]): 132 | p,c = path.astar( graph, ci, cj ) 133 | for i,j in zip(p,p[1:]): 134 | value = ((1.0 - w_pheromone) * pheromones[i][j]) + (w_pheromone * init_pheromone) 135 | pheromones[i][j] = value 136 | pheromones[j][i] = value 137 | 138 | 139 | def search( cities, max_iterations, nb_ants, decay, w_heuristic, w_pheromone, w_history, c_greedy, cost_func = graph_distance ): 140 | # like random.shuffle(cities) but on a copy 141 | best = { "permutation" : sorted( cities, key=lambda i: random.random()) } 142 | best["cost"] = cost( best["permutation"], cost_func, cities ) 143 | 144 | init_pheromone = 1.0 / float(len(cities)) * best["cost"] 145 | pheromones = initialize_pheromones_whole( cities, init_pheromone ) 146 | 147 | for i in range(max_iterations): 148 | LOG( i ) 149 | solutions = [] 150 | for j in range(nb_ants): 151 | LOG( "." ) 152 | candidate = {} 153 | candidate["permutation"] = walk( cities, pheromones, w_heuristic, w_history, c_greedy, cost_func ) 154 | candidate["cost"] = cost( candidate["permutation"], cost_func, cities ) 155 | if candidate["cost"] < best["cost"]: 156 | best = candidate 157 | update_local_whole( pheromones, candidate, cities, w_pheromone, init_pheromone ) 158 | update_global_whole( pheromones, best, cities, decay ) 159 | LOGN( best["cost"] ) 160 | 161 | return best,pheromones 162 | 163 | 164 | if __name__ == "__main__": 165 | max_it = 40 166 | num_ants = 10 167 | decay = 0.1 168 | w_heur = 2.5 169 | w_local_phero = 0.1 170 | c_greed = 0.9 171 | w_history = 1.0 172 | 173 | print """Graph TSP: 174 | -1 0 2 : x 175 | 1 o o-----o 176 | | | | 177 | 0 o--o-----o 178 | | | 179 | | | 180 | -2 o--o-----o 181 | : 182 | y 183 | """ 184 | G = { 185 | ( 0, 0) : [(-1, 0),( 0, 1),( 2, 0),( 0,-2)], 186 | ( 0, 1) : [( 0, 0),( 2, 1)], 187 | ( 0,-2) : [( 0, 0),( 2,-2),(-1,-2)], 188 | (-1, 0) : [(-1, 1),( 0, 0)], 189 | (-1, 1) : [(-1, 0)], 190 | (-1,-2) : [( 0,-2)], 191 | ( 2, 0) : [( 2, 1),( 2,-2),( 0, 0)], 192 | ( 2, 1) : [( 0, 1),( 2, 0)], 193 | ( 2,-2) : [( 2, 0),( 0,-2)], 194 | } 195 | 196 | best,phero = search( G, max_it, num_ants, decay, w_heur, w_local_phero, w_history, c_greed, cost_func = graph_distance ) 197 | print best["cost"], best["permutation"] 198 | -------------------------------------------------------------------------------- /geometry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | import math 5 | 6 | epsilon = 1e-6 7 | # epsilon = 0 8 | 9 | def x( point ): 10 | return point[0] 11 | 12 | def y( point ): 13 | return point[1] 14 | 15 | 16 | def mid( xy, pa, pb ): 17 | return ( xy(pa) + xy(pb) ) / 2.0 18 | 19 | def middle( pa, pb ): 20 | return mid(x,pa,pb),mid(y,pa,pb) 21 | 22 | 23 | def euclidian_distance( ci, cj, graph = None): 24 | return math.sqrt( float( x(ci) - x(cj) )**2 + float( y(ci) - y(cj) )**2 ) 25 | 26 | 27 | def linear_equation( p0, p1 ): 28 | """Return the linear equation coefficients of a line given by two points. 29 | Use the general form: c=a*x+b*y """ 30 | assert( len(p0) == 2 ) 31 | assert( len(p1) == 2 ) 32 | 33 | a = y(p0) - y(p1) 34 | b = x(p1) - x(p0) 35 | c = x(p0) * y(p1) - x(p1) * y(p0) 36 | return a, b, -c 37 | 38 | 39 | def is_null( x, e = epsilon ): 40 | return -e <= x <= e 41 | 42 | 43 | def is_vertical( leq ): 44 | a,b,c = leq 45 | return is_null(b) 46 | 47 | 48 | def is_point( segment ): 49 | """Return True if the given segment is degenerated (i.e. is a single point).""" 50 | return segment[0] == segment[1] 51 | 52 | 53 | def collinear( p, q, r, e = epsilon ): 54 | """Returns True if the 3 given points are collinear. 55 | 56 | Note: there is a lot of algorithm to test collinearity, the most known involving linear algebra. 57 | This one has been found in Jonathan Shewchuk's "Lecture Notes on Geometric Robustness". 58 | It is maybe the most elegant one: just arithmetic on x and y, without ifs, sqrt or risk of divide-by-zero error. 59 | """ 60 | # Without epsilon comparison, this would ends as: 61 | # return (x(p)-x(r)) * (y(q)-y(r)) == (x(q)-x(r)) * (y(p)-y(r)) 62 | return abs((x(p)-x(r)) * (y(q)-y(r)) - (x(q)-x(r)) * (y(p)-y(r))) <= e 63 | 64 | 65 | def line_intersection( seg0, seg1 ): 66 | """Return the coordinates of the intersection point of two lines given by pairs of points, or None.""" 67 | 68 | # Degenerated segments 69 | def on_line(p,seg): 70 | if collinear(p,*seg): 71 | return p 72 | else: 73 | return None 74 | 75 | # Segments degenerated as a single points, 76 | if seg0[0] == seg0[1] == seg1[0] == seg1[1]: 77 | return seg0[0] 78 | # as two different points, 79 | elif is_point(seg0) and is_point(seg1) and seg0 != seg1: 80 | return None 81 | # as a point and a line. 82 | elif is_point(seg0) and not is_point(seg1): 83 | return on_line(seg0[0],seg1) 84 | elif is_point(seg1) and not is_point(seg0): 85 | return on_line(seg1[0],seg0) 86 | 87 | 88 | leq0 = linear_equation(*seg0) 89 | leq1 = linear_equation(*seg1) 90 | 91 | # Collinear lines. 92 | if leq0 == leq1: 93 | return None 94 | 95 | # Vertical line 96 | def on_vertical( seg, leq ): 97 | a,b,c = leq 98 | assert( not is_null(b) ) 99 | assert( is_null( x(seg[0])-x(seg[1]) ) ) 100 | px = x(seg[0]) 101 | # Remember that the leq form is c=a*x+b*y, thus y=(c-ax)/b 102 | py = (c-a*px)/b 103 | return px,py 104 | 105 | if is_vertical(leq0) and not is_vertical(leq1): 106 | return on_vertical( seg0, leq1 ) 107 | elif is_vertical(leq1) and not is_vertical(leq0): 108 | return on_vertical( seg1, leq0 ) 109 | elif is_vertical(leq0) and is_vertical(leq1): 110 | return None 111 | 112 | # Generic case. 113 | a0,b0,c0 = leq0 114 | a1,b1,c1 = leq1 115 | 116 | d = a0 * b1 - b0 * a1 117 | dx = c0 * b1 - b0 * c1 118 | dy = a0 * c1 - c0 * a1 119 | if not is_null(d): 120 | px = dx / d 121 | py = dy / d 122 | return px,py 123 | else: 124 | # Parallel lines 125 | return None 126 | 127 | 128 | def box( points ): 129 | """Return the min and max points of the bounding box enclosing the given set of points.""" 130 | minp = min( [ x(p) for p in points ] ), min( [ y(p) for p in points ] ) 131 | maxp = max( [ x(p) for p in points ] ), max( [ y(p) for p in points ] ) 132 | return minp,maxp 133 | 134 | 135 | def in_box( point, box, exclude_edges = False ): 136 | """Return True if the given point is located within the given box.""" 137 | pmin,pmax = box 138 | if exclude_edges: 139 | return x(pmin)-epsilon < x(point) < x(pmax)+epsilon and y(pmin)-epsilon < y(point) < y(pmax)+epsilon 140 | else: 141 | return x(pmin)-epsilon <= x(point) <= x(pmax)+epsilon and y(pmin)-epsilon <= y(point) <= y(pmax)+epsilon 142 | 143 | 144 | def segment_intersection( seg0, seg1 ): 145 | """Return the coordinates of the intersection point of two segments, or None.""" 146 | assert( len(seg0) == 2 ) 147 | assert( len(seg1) == 2 ) 148 | 149 | p = line_intersection(seg0,seg1) 150 | 151 | if p is None: 152 | return None 153 | else: 154 | if in_box(p,box(seg0)) and in_box(p,box(seg1)): 155 | return p 156 | else: 157 | return None 158 | 159 | 160 | if __name__ == "__main__": 161 | import sys 162 | import random 163 | import utils 164 | import uberplot 165 | import matplotlib.pyplot as plot 166 | 167 | # intersections demo 168 | if len(sys.argv) > 1: 169 | scale = 100 170 | nb = int(sys.argv[1]) 171 | points = [ (scale*random.random(),scale*random.random()) for i in range(nb)] 172 | else: 173 | points = [ 174 | (10,0), 175 | (-190,0), 176 | (10,200), 177 | (110,100), 178 | (110,-100), 179 | (-90,100), 180 | (-90,-100), 181 | ] 182 | 183 | segments = [] 184 | for p0 in points: 185 | for p1 in points: 186 | if p0 != p1: 187 | segments.append( (p0,p1) ) 188 | 189 | seg_inter = [] 190 | line_inter = [] 191 | for s0 in segments: 192 | for s1 in segments: 193 | if s0 != s1: 194 | s = segment_intersection( s0, s1 ) 195 | if s is not None: 196 | seg_inter.append(s) 197 | l = line_intersection( s0, s1 ) 198 | if l is not None: 199 | line_inter.append(l) 200 | 201 | 202 | fig = plot.figure() 203 | 204 | ax = fig.add_subplot(121) 205 | uberplot.plot_segments( ax, segments, linewidth=0.5, edgecolor = "blue" ) 206 | uberplot.scatter_points( ax, points, edgecolor="blue", facecolor="blue", s=120, alpha=1, linewidth=1 ) 207 | uberplot.scatter_points( ax, line_inter, edgecolor="none", facecolor="green", s=60, alpha=0.5 ) 208 | uberplot.scatter_points( ax, seg_inter, edgecolor="none", facecolor="red", s=60, alpha=0.5 ) 209 | ax.set_aspect('equal') 210 | 211 | 212 | # collinear test demo 213 | if len(sys.argv) > 1: 214 | scale = 100 215 | nb = int(sys.argv[1]) 216 | triangles = [] 217 | for t in range(nb): 218 | triangles.append( [ [scale*random.random(),scale*random.random()] for i in range(3)] ) 219 | # forced collinear 220 | t = [ [scale*random.random(),scale*random.random()] for i in range(2)] 221 | triangles.append( [t[0],t[1],t[0]] ) 222 | 223 | else: 224 | triangles = [ 225 | [(-60.45085, -24.898983), (-68.54102, -30.776835), (-58.54102, -30.776835)], 226 | [(-68.54102, -0.0), (-65.45085, -9.510565), (-58.54102, -0.0)], 227 | [(-65.45085, 9.510565), (-68.54102, -0.0), (-58.54102, -0.0)], 228 | [(-68.54102, 30.776835), (-60.45085, 24.898983), (-58.54102, 30.776835)], 229 | [(-58.54102, -0.0), (-65.45085, -9.510565), (-55.45085, -9.510565)], 230 | [(-65.45085, -9.510565), (-57.36068, -15.388418), (-55.45085, -9.510565)], 231 | [(-65.45085, 9.510565), (-58.54102, -0.0), (-55.45085, 9.510565)], 232 | [(-65.45085, 9.510565), (-65.45085, 9.510565), (-55.45085, 9.510565)], # is collinear 233 | ] 234 | 235 | ax = fig.add_subplot(122) 236 | 237 | for triangle in triangles: 238 | status="green" 239 | if collinear(*triangle): 240 | status="red" 241 | uberplot.plot_segments( ax, utils.tour(triangle), edgecolor = status ) 242 | 243 | # ax.set_aspect('equal') 244 | ax.axis('auto') 245 | 246 | 247 | plot.show() 248 | 249 | -------------------------------------------------------------------------------- /graph.py: -------------------------------------------------------------------------------- 1 | 2 | from geometry import x,y 3 | 4 | def graph_of( segments ): 5 | graph = {} 6 | for start,end in segments: 7 | graph[start] = graph.get( start, [] ) 8 | graph[start].append( end ) 9 | graph[end] = graph.get( end, [] ) 10 | graph[end].append( start ) 11 | return graph 12 | 13 | 14 | def edges_of( graph ): 15 | edges = set() 16 | for k in graph: 17 | for n in graph[k]: 18 | if k != n and (k,n) not in edges and (n,k) not in edges: 19 | edges.add( (k,n) ) 20 | return list(edges) 21 | 22 | 23 | def nodes_of( graph ): 24 | return graph.keys() 25 | 26 | 27 | def load( stream ): 28 | graph = {} 29 | for line in stream: 30 | if line.strip()[0] != "#": 31 | skey,svals = line.split(":") 32 | key = tuple((float(i) for i in skey.split(','))) 33 | graph[key] = [] 34 | for sp in svals.split(): 35 | p = tuple(float(i) for i in sp.split(",")) 36 | assert(len(p)==2) 37 | graph[key].append( p ) 38 | return graph 39 | 40 | 41 | def write( graph, stream ): 42 | for k in graph: 43 | stream.write( "%f,%f:" % (x(k),y(k)) ) 44 | for p in graph[k]: 45 | stream.write( "%f,%f " % (x(p),y(p)) ) 46 | stream.write("\n") 47 | 48 | -------------------------------------------------------------------------------- /hull.py: -------------------------------------------------------------------------------- 1 | 2 | import operator 3 | from utils import LOG,LOGN 4 | from geometry import x,y,euclidian_distance 5 | 6 | # Based on the excellent article by Tom Switzer 7 | # http://tomswitzer.net/2010/12/2d-convex-hulls-chans-algorithm/ 8 | 9 | TURN_RIGHT, TURN_NONE, TURN_LEFT = (-1, 0, 1) 10 | 11 | def turn(p, q, r): 12 | """Returns -1, 0, 1 if the sequence of points (p,q,r) forms a right, straight, or left turn.""" 13 | qr = ( x(q) - x(p) ) * ( y(r) - y(p) ) 14 | rq = ( x(r) - x(p) ) * ( y(q) - y(p) ) 15 | # cmp(x,y) returns -1 if xy 16 | return cmp( qr - rq, 0) 17 | 18 | 19 | def keep_left(hull, point): 20 | """ Remove the last elements of the given hull that does not form a left turn with the given point.""" 21 | 22 | while len(hull) > 1 and turn( hull[-2], hull[-1], point ) != TURN_LEFT: 23 | hull.pop() 24 | 25 | # if the hull is empty or does not contains the point point 26 | if len(hull) == 0 or hull[-1] != point: 27 | # add at least the asked point 28 | hull.append(point) 29 | 30 | return hull 31 | 32 | 33 | def graham_scan(points): 34 | """Returns points on convex hull of an array of points in counter clockwise order.""" 35 | 36 | # Sort from the furthest left point. 37 | spots = sorted(points) 38 | 39 | # Browse the hull turning left from the furthest left point, 40 | # this is thus the lower part of the convex hull. 41 | lower_hull = reduce(keep_left, spots, []) 42 | 43 | # Going from the furthest right point, we get the upper hull. 44 | upper_hull = reduce(keep_left, reversed(spots), []) 45 | 46 | # Merge the lower and the upper hull, 47 | # omitting the extreme points that are in both hulls parts. 48 | for p in upper_hull[1:-1]: 49 | lower_hull.append( p ) 50 | 51 | return lower_hull 52 | 53 | 54 | def right_tangent(hull, p): 55 | """Return the index of the point in hull that the right tangent line from p to hull touches.""" 56 | 57 | def at( index, h = hull ): 58 | """Secured index accessor to the hull: 59 | if the given index is greater than the container length, then start from the beginning.""" 60 | return index % len(h) 61 | 62 | # Consider the turn formed by a window sliding 63 | # from the extremes points toward the center of the hull. 64 | # 65 | # >> sliding window << 66 | # ___________________________ 67 | # / \ 68 | # hull = [ . . .(. . . . . . . . . . . . . . .). . . ] 69 | # ^ ^ ^ ^ ^ 70 | # | | | | | 71 | # ileft | | icenter+1 iright 72 | # | icenter 73 | # icenter-1 74 | 75 | # Remember that the points are ordered in the hull. 76 | ileft, iright = 0, len(hull) 77 | # With the last point. 78 | l_prev = turn( p, hull[0], hull[-1] ) 79 | # With the second point (if the hull contains only one point, then ileft stays at zero). 80 | l_next = turn( p, hull[0], hull[ at(ileft+1) ] ) 81 | 82 | # While the right tangent is not found, 83 | # and the sliding window is not null. 84 | while ileft < iright: 85 | # Index of the point in the middle of the window. 86 | icenter = (ileft + iright) / 2 87 | # Consider the turn formed by the given point, the center point and... 88 | # ... the point before the center, 89 | c_prev = turn( p, hull[icenter], hull[ at(icenter-1) ] ) 90 | # ... the point after the center, 91 | c_next = turn( p, hull[icenter], hull[ at(icenter+1) ] ) 92 | # ... the point on the left of the window. 93 | c_side = turn( p, hull[ileft] , hull[icenter] ) 94 | 95 | # The right tangent is the middle of the window iff 96 | # the turns formed with points around the center are to the LEFT (or straight) 97 | # (i.e. are not to the right). 98 | if c_prev != TURN_RIGHT and c_next != TURN_RIGHT: 99 | return icenter 100 | 101 | # If the tangent touches the left point in the window. 102 | elif c_side == TURN_LEFT and ( l_next == TURN_RIGHT or l_prev == l_next ) \ 103 | or c_side == TURN_RIGHT and c_prev == TURN_RIGHT: 104 | # Do not consider points at the RIGHT of the center. 105 | iright = icenter 106 | 107 | # If the tangent touches the right point in the window, 108 | # but this is not the last possible tangent. 109 | elif icenter+1 < iright: 110 | # Do not consider points at the LEFT of the center. 111 | ileft = icenter+1 112 | # Switch sides: if the turn toward the point before the center 113 | # was to the right, search to the left and conversely. 114 | l_prev = -1 * c_next 115 | # Update the turn to the next left point. 116 | l_next = turn( p, hull[ileft], hull[ at(ileft+1) ] ) 117 | 118 | # There is no more possible tangent, the hull contains a straight segment. 119 | else: 120 | return ileft 121 | 122 | return ileft 123 | 124 | 125 | def min_hull_pt_pair(hulls): 126 | """Returns the (hull, point) index pair that is minimal.""" 127 | # Find an extreme point and the hull chunk to which it is related. 128 | min_h_i, min_p_i = 0, 0 129 | for i,hull in enumerate(hulls): 130 | # Find the index j of the minimal point in the ith hull 131 | # itemgetter(1) will return the point, because enumerate produces (index,point) pairs 132 | # and thus (index,point)[1] == point 133 | j,pt = min(enumerate(hull), key=operator.itemgetter(1)) 134 | # Minimize across the hulls 135 | if pt < hull[min_p_i]: 136 | min_h_i, min_p_i = i, j 137 | # Return the index of the hull which holds the minimal point and the index of the point itself. 138 | return (min_h_i, min_p_i) 139 | 140 | 141 | def next_hull_pt_pair( hulls, (hi,pi) ): 142 | """ Returns the (hull, point) index pair of the next point in the convex hull.""" 143 | 144 | # The current point itself 145 | base_pt = hulls[hi][pi] 146 | 147 | # Indices of the next (hull,point) pair in hulls 148 | next_hullpt = ( hi, (pi+1)%len(hulls[hi]) ) 149 | 150 | # Now search for a "next" point after the base point 151 | # that forms a right turn along with its tangent point. 152 | 153 | # Loop over the indices of hulls, 154 | # but do not consider the current index. 155 | for nhi in (i for i in range(len(hulls)) if i != hi): 156 | # Index of the right tangent point of the base point 157 | rti = right_tangent( hulls[nhi], base_pt ) 158 | 159 | # The other points themselves 160 | tangent_pt = hulls[nhi][rti] 161 | next_pt = hulls[ next_hullpt[0] ][ next_hullpt[1] ] 162 | 163 | # How is the turn formed by the three points? 164 | t = turn( base_pt, next_pt, tangent_pt ) 165 | 166 | # If it forms a right turn, it is on the convex hull. 167 | # If it forms a left turn, it is not and we continue the loop. 168 | # In the (rare) case the points are aligned, we consider the next point 169 | # only if it is closer to the base point than the tangent point. 170 | if t == TURN_RIGHT \ 171 | or ( t == TURN_NONE and euclidian_distance(base_pt, next_pt) < euclidian_distance(base_pt, tangent_pt) ): 172 | # save the indices of the hull and point 173 | next_hullpt = (nhi, rti) 174 | 175 | return next_hullpt 176 | 177 | 178 | def convex_hull(points): 179 | """Returns the points on the convex hull of points in CCW order.""" 180 | 181 | # Increasing guesses for the hull size. 182 | for guess in ( 2**(2**t) for t in range(len(points)) ): 183 | LOG( "Guess",guess) 184 | hulls = [] 185 | for i in range(0, len(points), guess): 186 | # LOG(".") 187 | # Split the points into chunks of (roughly) the guess. 188 | chunk = points[i:i + guess] 189 | # Find the corresponding convex hull of these chunks. 190 | hulls.append( graham_scan(chunk) ) 191 | 192 | # Find the extreme point and initialize the list of (hull,point) with it. 193 | hullpt_pairs = [min_hull_pt_pair(hulls)] 194 | 195 | # Ensure we stop after no more than "guess" iterations. 196 | for __ in range(guess): 197 | LOG("*") 198 | pair = next_hull_pt_pair(hulls, hullpt_pairs[-1]) 199 | if pair == hullpt_pairs[0]: 200 | # Return the points in sequence 201 | LOGN("o") 202 | return [hulls[h][i] for h,i in hullpt_pairs] 203 | hullpt_pairs.append(pair) 204 | LOGN("x") 205 | 206 | 207 | if __name__ == "__main__": 208 | import sys 209 | import random 210 | import utils 211 | import uberplot 212 | import matplotlib.pyplot as plot 213 | 214 | if len(sys.argv) > 1: 215 | scale = 100 216 | nb = int(sys.argv[1]) 217 | points = [ (scale*random.random(),scale*random.random()) for i in range(nb)] 218 | else: 219 | points = [ 220 | (0,40), 221 | (100,60), 222 | (40,0), 223 | (50,100), 224 | (90,10), 225 | (50,50), 226 | ] 227 | 228 | fig = plot.figure() 229 | 230 | hull = convex_hull( points ) 231 | edges = list(utils.tour(hull)) 232 | 233 | ax = fig.add_subplot(111) 234 | ax.set_aspect('equal') 235 | ax.scatter( [i[0] for i in points],[i[1] for i in points], facecolor="red") 236 | uberplot.plot_segments( ax, edges, edgecolor = "blue" ) 237 | plot.show() 238 | 239 | -------------------------------------------------------------------------------- /life.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from itertools import permutations 3 | import random 4 | 5 | 6 | def count( node, states, board, graph ): 7 | """Count the number of neighbours in each given states, in a single pass.""" 8 | nb = {s:0 for s in states} 9 | for neighbor in graph[node]: 10 | for state in states: 11 | if board[neighbor] == state: 12 | nb[state] += 1 13 | return nb 14 | 15 | 16 | class Rule: 17 | """The template to create a rule for a game of life. 18 | 19 | A rule is just a set of states and a function to compute the state of a given node, 20 | given the current board states and a neighborhood represented by an adjacency graph.""" 21 | 22 | class State: 23 | default = 0 24 | 25 | # Available states, the first one should be the default "empty" (or "dead") one. 26 | states = [State.default] 27 | 28 | def __call__(self, node, board, graph ): 29 | raise NotImplemented 30 | 31 | 32 | class Conway(Rule): 33 | """The original rules for Conway's game of life on square grid.""" 34 | 35 | class State: 36 | dead = 0 37 | live = 1 38 | 39 | states = [State.dead, State.live] 40 | 41 | def __call__(self, node, board, graph ): 42 | # "a" is just a shortcut. 43 | a = self.State() 44 | next = a.dead 45 | 46 | nb = count( node, [a.live], board, graph ) 47 | if board[node] is a.dead: 48 | if nb[a.live] == 3: # reproduction 49 | next = a.live 50 | else: 51 | assert(board[node] is a.live) 52 | 53 | if nb[a.live] < 2: # under-population 54 | next = a.dead 55 | elif nb[a.live] > 3: # over-population 56 | next = a.dead 57 | else: 58 | assert( 2 <= nb[a.live] <= 3 ) 59 | next = a.live 60 | 61 | return next 62 | 63 | 64 | class Goucher(Rule): 65 | """This is the Goucher 4-states rule. 66 | It permits gliders on Penrose tiling. 67 | From: Adam P. Goucher, "Gliders in cellular automata on Penrose tilings", J. Cellular Automata, 2012""" 68 | 69 | class State: # Should be an Enum in py3k 70 | ground = 0 71 | head = 1 72 | tail = 2 73 | wing = 3 74 | 75 | states = [ State.ground, State.head, State.tail, State.wing ] 76 | 77 | def __call__(self, node, current, graph ): 78 | """Summarized as: 79 | ------------------------------------------------------ 80 | | Current state | Neighbour condition | Next state | 81 | ------------------------------------------------------ 82 | | 0 | n1>=1 | n2>=1 | * | 3 | 83 | | 0 | n1>=1 | * | n3>=2 | 3 | 84 | | 1 | * | * | n3>=1 | 2 | 85 | | 1 | * | * | * | 1 | 86 | | 2 | * | * | * | 3 | 87 | | * | * | * | * | 0 | 88 | ------------------------------------------------------ 89 | """ 90 | # "a" is just a shortcut. 91 | a = self.State() 92 | 93 | # Default state, if nothing matches. 94 | next = a.ground 95 | 96 | if current[node] is a.ground: 97 | # Count the number of neighbors of each state in one pass. 98 | stated = [a.head,a.tail,a.wing] 99 | nb = count( node, stated, current, graph ) 100 | # This is the max size of the neighborhood on a rhomb Penrose tiling (P2) 101 | assert( all(nb[s] <= 11 for s in stated) ) 102 | 103 | if nb[a.head] >= 1 and nb[a.tail] >= 1: 104 | next = a.wing 105 | elif nb[a.head] >= 1 and nb[a.wing] >= 3: 106 | next = a.wing 107 | 108 | elif current[node] is a.head: 109 | # It is of no use to compute the number of heads and tails if the current state is not ground. 110 | nb = count( node, [a.wing], current, graph ) 111 | assert( all(nb[s] <= 11 for s in [a.wing]) ) 112 | 113 | if nb[a.wing] >= 1: 114 | next = a.tail 115 | else: 116 | next = a.head 117 | 118 | elif current[node] is a.tail: 119 | next = a.wing 120 | 121 | # Default to ground, as stated above. 122 | # else: 123 | # next = a.ground 124 | 125 | return next 126 | 127 | 128 | def make_board( graph, state = lambda x: 0 ): 129 | """Create a new board board, filled with the results of the calls to the given state function. 130 | The given graph should be an iterable with all the nodes. 131 | The given state function should take a node and return a state. 132 | The default state function returns zero. 133 | """ 134 | board = {} 135 | for node in graph: 136 | board[node] = state(node) 137 | return board 138 | 139 | 140 | def step( current, graph, rule ): 141 | """Compute one generation of the board. 142 | i.e. apply the given rule function on each node of the given graph board. 143 | The given current board should associate a state to a node. 144 | The given graph should associate each node with its neighbors. 145 | The given rule is a function that takes a node, the current board and the graph and return the next state of the node.""" 146 | 147 | # Defaults to the first state of the rule. 148 | next = make_board(graph, lambda x : rule.states[0]) 149 | 150 | for node in graph: 151 | next[node] = rule( node, current, graph ) 152 | 153 | return next 154 | 155 | 156 | def play( board, graph, rule, nb_gen ): 157 | for i in range(nb_gen): 158 | board = step( board, graph, rule ) 159 | 160 | 161 | if __name__ == "__main__": 162 | import sys 163 | 164 | # Simple demo on a square grid torus. 165 | graph = {} 166 | size = 5 167 | if len(sys.argv) >= 2: 168 | size = int(sys.argv[1]) 169 | 170 | for i in range(size): 171 | for j in range(size): 172 | 173 | # All Moore neighborhood around a coordinate. 174 | neighborhood = set(permutations( [0]+[-1,1]*2, 2)) # FIXME ugly 175 | assert( len(neighborhood) == 8 ) 176 | 177 | graph[(i,j)] = [] 178 | for di,dj in neighborhood: 179 | # Use modulo to avoid limits and create a torus. 180 | graph[ (i,j) ].append(( (i+di)%size, (j+dj)%size )) 181 | 182 | rule = Conway() 183 | # Fill a board with random states. 184 | board = make_board( graph, lambda x : random.choice(rule.states) ) 185 | 186 | # Play and print. 187 | for i in range(size): 188 | print i 189 | for i in range(size): 190 | for j in range(size): 191 | print board[(i,j)], 192 | print "" 193 | board = step(board,graph,rule) 194 | 195 | -------------------------------------------------------------------------------- /lindenmayer.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | class IndexedGenerator(object): 4 | """Add a way to get a generator item by its index""" 5 | def __init__(self, generator): 6 | self.generator = generator 7 | self.cache = [] 8 | 9 | def __getitem__(self, index): 10 | for i in xrange( index - len(self.cache) + 1 ): 11 | self.cache.append( self.generator.next() ) 12 | return self.cache[index] 13 | 14 | 15 | class LindenmayerSystem(IndexedGenerator): 16 | """Base virtual class for a Lindenmayer system""" 17 | def __init__(self, axiom, rules, angle, heading=0): 18 | self.angle = angle 19 | self.heading = heading 20 | self.states = deque() 21 | self.actions = { 22 | 'F': self.forward, 23 | '+': self.right, 24 | '-': self.left, 25 | '[': self.save, 26 | ']': self.restore, 27 | } 28 | super(LindenmayerSystem, self).__init__(self.lindenmayer(axiom, rules)) 29 | 30 | def lindenmayer(self, axiom, rules): 31 | rules = rules.items() 32 | 33 | # def inside the lindenmayer function, so as to use "axiom" at instanciation 34 | def apply(axiom, (symbol, replacement)): 35 | return axiom.replace(symbol, replacement.lower()) 36 | 37 | while True: 38 | yield axiom 39 | axiom = reduce(apply, rules, axiom).upper() 40 | 41 | def forward(self): 42 | raise NotImplementedError 43 | 44 | def right(self): 45 | raise NotImplementedError 46 | 47 | def left(self): 48 | raise NotImplementedError 49 | 50 | def save(self): 51 | raise NotImplementedError 52 | 53 | def restore(self): 54 | raise NotImplementedError 55 | 56 | 57 | class TurtleLSystem(LindenmayerSystem): 58 | """Draw a L-System using the Turtle module""" 59 | def __init__(self, turtle, axiom, rules, angle, heading=0, size=1): 60 | self.turtle = turtle 61 | self.size = size 62 | super(TurtleLSystem, self).__init__( axiom, rules, angle, heading ) 63 | 64 | def draw(self, depth): 65 | self.turtle.setheading(self.heading) 66 | 67 | for char in self[depth]: 68 | if char in self.actions: 69 | self.actions[char]() 70 | 71 | def forward(self): 72 | self.turtle.forward(self.size) 73 | 74 | def left(self): 75 | self.turtle.left(self.angle) 76 | 77 | def right(self): 78 | self.turtle.right(self.angle) 79 | 80 | def save(self): 81 | x = self.turtle.xcor() 82 | y = self.turtle.ycor() 83 | h = self.turtle.heading() 84 | self.states.append( (x, y, h) ) 85 | 86 | def restore(self): 87 | self.turtle.up() 88 | x, y, h = self.states.pop() 89 | self.turtle.setx(x) 90 | self.turtle.sety(y) 91 | self.turtle.setheading(h) 92 | self.turtle.down() 93 | 94 | 95 | class DumpTurtleLSystem(TurtleLSystem): 96 | """Keep the set of uniques L-System segments drawn by the Turtle""" 97 | def __init__(self, turtle, axiom, rules, angle, heading=0, size=1, rounding=10): 98 | # using a set avoid duplicate segments 99 | self.segments = set() 100 | # nb of significant digits for rounding 101 | self.rounding=10 102 | super(DumpTurtleLSystem, self).__init__( turtle, axiom, rules, angle, heading, size ) 103 | 104 | def forward(self): 105 | """Store segment coordinates and do a forward movement""" 106 | # without rounding, there may be the same node with different coordinates, 107 | # because of error propagation 108 | x1 = round( self.turtle.xcor(), self.rounding ) 109 | y1 = round( self.turtle.ycor(), self.rounding ) 110 | start = ( x1, y1 ) 111 | super(DumpTurtleLSystem, self).forward() 112 | x2 = round( self.turtle.xcor(), self.rounding ) 113 | y2 = round( self.turtle.ycor(), self.rounding ) 114 | end = ( x2, y2 ) 115 | self.segments.add( (start,end) ) 116 | 117 | def draw(self, depth): 118 | """Call the draw function, then clean the data""" 119 | super(DumpTurtleLSystem, self).draw(depth) 120 | self.clean() 121 | 122 | def clean(self): 123 | """Remove segments that have duplicated clones in the reverse direction 124 | (the segments is a set, that guarantees that no other clone exists)""" 125 | for segment in self.segments: 126 | for start,end in segment: 127 | # FIXME surely faster to catch the exception than to do two search? 128 | if (end,start) in self.segments: 129 | self.segments.remove( (end,start) ) 130 | 131 | def __str__(self): 132 | dump = "" 133 | for segment in self.segments: 134 | for coords in segment: 135 | for x in coords: 136 | dump += str(x)+" " 137 | dump += "\n" 138 | return dump 139 | 140 | 141 | 142 | if __name__=="__main__": 143 | import sys 144 | 145 | depth = 1 146 | if len(sys.argv) > 1: 147 | depth = int( sys.argv[1] ) 148 | 149 | segment_size = 10 150 | float_rounding = 10 151 | 152 | import turtle 153 | ttl = turtle.Turtle() 154 | ttl.speed('fastest') 155 | penrose = DumpTurtleLSystem(ttl, 156 | axiom="[X]++[X]++[X]++[X]++[X]", 157 | rules={ 158 | 'F': "", 159 | 'W': "YF++ZF----XF[-YF----WF]++", 160 | 'X': "+YF--ZF[---WF--XF]+", 161 | 'Y': "-WF++XF[+++YF++ZF]-", 162 | 'Z': "--YF++++WF[+ZF++++XF]--XF" 163 | }, 164 | angle=36, heading=0, size=segment_size, rounding=float_rounding ) 165 | 166 | penrose.draw( depth ) 167 | print penrose 168 | 169 | plot_segments( penrose.segments ) 170 | 171 | -------------------------------------------------------------------------------- /penrose_tsp.py: -------------------------------------------------------------------------------- 1 | import lindenmayer 2 | import tsplib 3 | import sys 4 | 5 | depth = 1 6 | if len(sys.argv) > 1: 7 | depth = int( sys.argv[1] ) 8 | 9 | segment_size = 10 10 | float_rounding = 10 11 | 12 | import turtle 13 | ttl = turtle.Turtle() 14 | ttl.speed('fastest') 15 | penrose = lindenmayer.DumpTurtleLSystem(ttl, 16 | axiom="[X]++[X]++[X]++[X]++[X]", 17 | rules={ 18 | 'F': "", 19 | 'W': "YF++ZF----XF[-YF----WF]++", 20 | 'X': "+YF--ZF[---WF--XF]+", 21 | 'Y': "-WF++XF[+++YF++ZF]-", 22 | 'Z': "--YF++++WF[+ZF++++XF]--XF" 23 | }, 24 | angle=36, heading=0, size=segment_size, rounding=float_rounding ) 25 | 26 | penrose.draw( depth ) 27 | #print penrose 28 | 29 | #plot_segments( penrose.segments ) 30 | 31 | 32 | tsplib.write_segments( penrose.segments, segment_size, depth, float_rounding, fd=sys.stdout ) 33 | 34 | -------------------------------------------------------------------------------- /quadtree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import utils 4 | import geometry 5 | from geometry import x,y 6 | 7 | # import enum 8 | 9 | def as_box( quadrant ): 10 | """"Convert a quadrant of the form: ((x_min,y_min),width) to a box: ((x_min,y_min),(x_max,y_max)).""" 11 | width = quadrant[1] 12 | minp = quadrant[0] 13 | maxp = tuple(xy+width for xy in minp) 14 | assert( x(minp) <= x(maxp) and y(minp) <= y(maxp) ) 15 | return (minp,maxp) 16 | 17 | 18 | def as_rect( quadrant ): 19 | """"Convert a quadrant of the form: ((x_min,y_min),width) to a rectangle: ((x0,y0),(x1,y1),(x2,y2),(x3,y3)).""" 20 | qx,qy = quadrant[0] 21 | w = quadrant[1] 22 | return [(qx,qy),(qx+w,qy),(qx+w,qy+w),(qx,qy+w)] 23 | 24 | 25 | class QuadTree(object): 26 | 27 | def __init__( self, points = [] ): 28 | """Build a quadtree on the given set of points. 29 | 30 | Points must be an iterable containing 2-tuples of the form: (x,y)""" 31 | 32 | # Initialize the root quadrant as the box around the points 33 | self.root, self.quadrants = self.init( points = points ) 34 | 35 | # Each leaf of the quadtree may contains one resident point. 36 | self.residents = { self.root: None } 37 | 38 | # Each node of the quadtree may contains four children. 39 | self.children = { self.root: [] } 40 | 41 | # Status of quadrants 42 | # class Status(enum.Enum): 43 | class Status: 44 | Leaf = 1 45 | Node = 2 46 | Empty = 3 47 | Out = 4 48 | self.Status = Status() 49 | 50 | # Choose one of the two available functions for walking the tree: 51 | # self.walk = self.recursive_walk 52 | self.walk = self.iterative_walk 53 | 54 | # Generate the complete tree. 55 | self.build( points ) 56 | 57 | 58 | def init( self, quadrant = None, box = None, points = None ): 59 | """Initialize the root quadrant with the given quadrant, the given box or the given set of points.""" 60 | 61 | if len([k for k in (box,points,quadrant) if k]) > 1: 62 | raise BaseException("ERROR: you should specify a box, a quadrant or points") 63 | 64 | # Initialize the root quadrant as the given box 65 | if box: 66 | minp,maxp = box 67 | width = max( x(maxp)-x(minp), y(maxp)-y(minp) ) 68 | 69 | # Initialize the root quadrant as the box around the points 70 | elif points: 71 | minp,maxp = geometry.box( points ) 72 | width = max( x(maxp)-x(minp), y(maxp)-y(minp) ) 73 | 74 | # Initialize the root quadrant as the given origin point and width 75 | elif quadrant: 76 | minp = quadrant[0] 77 | width = quadrant[1] 78 | 79 | assert( x(minp) <= x(minp)+width and y(minp) <= y(minp)+width ) 80 | 81 | # There is always the root quadrant in the list of available ones. 82 | root = (minp,width) 83 | quadrants = [ root ] 84 | 85 | return root,quadrants 86 | 87 | 88 | def status( self, point, quadrant ): 89 | """Return Status.Empty if the given point can be appended in the given quadrant.""" 90 | 91 | assert(point is not None) 92 | assert(len(point) == 2) 93 | assert(quadrant is not None) 94 | assert(len(quadrant) == 2) 95 | 96 | box = as_box( quadrant ) 97 | 98 | # if the point lies inside the given quadrant 99 | if geometry.in_box( point, box): 100 | if self.residents[quadrant]: 101 | # external: a quadrant that already contains a point 102 | assert( not self.children[quadrant] ) 103 | return self.Status.Leaf 104 | elif self.children[quadrant]: 105 | # internal: a quadrant that contains other quadrants 106 | return self.Status.Node 107 | else: 108 | # empty: there is not point yet in this quadrant 109 | return self.Status.Empty 110 | else: 111 | # point is out of the quadrant 112 | return self.Status.Out 113 | 114 | 115 | def split( self, quadrant ): 116 | """Split an existing quadrant in four children quadrants. 117 | 118 | Move the existing resident to the correct child.""" 119 | 120 | # We cannot split a quadrant if it already have sub-quadrants 121 | if quadrant != self.root: 122 | assert( not self.children[quadrant] ) 123 | 124 | qx, qy = quadrant[0] 125 | w = quadrant[1] / 2 126 | 127 | # For each four children quadrant's origins 128 | self.children[quadrant] = [] 129 | for origin in ( (qx,qy), (qx,qy+w), (qx+w,qy+w), (qx+w,qy) ): 130 | # Create a child quadrant of half its width 131 | q = (origin, w) 132 | self.quadrants.append(q) 133 | # Default resident to None, because we will test for this key later on. 134 | self.residents[q] = None 135 | 136 | # Add this new child to the current parent. 137 | self.children[quadrant].append(q) 138 | # This new quadrant has no child. 139 | self.children[q] = [] 140 | 141 | # Move the resident to the related children node 142 | p = self.residents[quadrant] 143 | if p is not None: 144 | # Find the suitable children quadrant 145 | for child in self.children[quadrant]: 146 | if self.status(p,child) == self.Status.Empty: 147 | self.residents[child] = p 148 | break 149 | # Forget we had resident here 150 | # Do not pop the key, because we have tests on it elsewhere 151 | self.residents[quadrant] = None 152 | 153 | 154 | def append( self, point, quadrant = None ): 155 | """Try to inset the given point in the existing quadtree, under the given quadrant. 156 | 157 | The default quadrant is the root one. 158 | Returns True if the point was appended, False if it is impossible to append it.""" 159 | 160 | # Default to the root quadrant 161 | if not quadrant: 162 | quadrant = self.root 163 | assert(quadrant in self.quadrants) 164 | 165 | # The point should not be out of the root quadrant 166 | assert( self.status(point,self.root) != self.Status.Out ) 167 | 168 | # FIXME use a recursive walk and prune branches with the Out status. 169 | for q in self.walk(quadrant): 170 | status = self.status( point, q ) 171 | if status == self.Status.Leaf: 172 | # Create sub-quadrants 173 | self.split(q) 174 | # Try to attach the point in children quadrants, recursively 175 | for child in self.children[q]: 176 | if self.append( point, child ): 177 | return True 178 | elif status == self.Status.Empty: 179 | # add the point as an resident of the quadrant q 180 | self.residents[q] = point 181 | return True 182 | return False 183 | 184 | 185 | def build( self, points ): 186 | """Append all the given points in the quadtree.""" 187 | for p in points: 188 | self.append(p) 189 | assert( len(points) == len(self) ) 190 | 191 | 192 | def iterative_walk( self, at_quad = None ): 193 | 194 | # Default to the root quadrant 195 | if not at_quad: 196 | at_quad = self.root 197 | 198 | # First, consider the root quadrant 199 | yield at_quad 200 | 201 | # then children of the root quadrant 202 | quads = list(self.children[at_quad]) 203 | for child in quads: 204 | yield child 205 | # then children of the current child 206 | quads.extend( self.children[child] ) 207 | 208 | 209 | def recursive_walk( self, at_quad = None ): 210 | 211 | # Default to the root quadrant 212 | if not at_quad: 213 | at_quad = self.root 214 | 215 | yield at_quad 216 | for child in self.children[at_quad]: 217 | for q in self.recursive_walk(child): 218 | yield q 219 | 220 | 221 | def repr( self, quadrant = None, depth = 0 ): 222 | """Return a string representing the quadtree in a JSON-like format.""" 223 | 224 | # Default to the root quadrant 225 | if not quadrant: 226 | quadrant = self.root 227 | 228 | head = " "*depth 229 | r = head+"{" 230 | quadrep = '"origin" : %s, "width" : %f' % quadrant 231 | if self.residents[quadrant]: # external 232 | r += ' "resident" : %s, \t%s },\n' % (self.residents[quadrant],quadrep) 233 | elif self.children[quadrant]: # internal 234 | r += ' "children_ids" : %s, \t%s, "children" : [\n' % (self.children[quadrant],quadrep) 235 | for child in self.children[quadrant]: 236 | r += self.repr(child, depth+1) 237 | r+="%s]},\n" % head 238 | else: # empty 239 | r += ' "resident" : (), \t\t\t%s},\n' % (quadrep) 240 | return r 241 | 242 | 243 | def points( self ): 244 | """Return the set of points attached to the quadtree. 245 | 246 | In a random order.""" 247 | return [p for p in self.residents.values() if p is not None] 248 | 249 | 250 | def covers( self, this, that ): 251 | """Return true if the given quadrants does intersects each other.""" 252 | 253 | # Convert quadrants ((x,y),w) as box ((a,b),(c,d)). 254 | this_box = as_box(this) 255 | that_box = as_box(that) 256 | 257 | # Convert boxes as list of edges. 258 | this_segments = tuple(utils.tour(as_rect(this))) 259 | that_segments = tuple(utils.tour(as_rect(that))) 260 | 261 | # If at least one of the segment of "this" intersects with "that". 262 | intersects = any( geometry.segment_intersection(s0,s1) for s0 in this_segments for s1 in that_segments ) 263 | 264 | # Transform nested list of segments in flat list of points without any duplicates. 265 | this_points = as_rect(this) 266 | that_points = as_rect(that) 267 | 268 | # If all the points of "this" are inside "that". 269 | # Note: what we would want to test here is if ALL the points are comprised, 270 | # as the case where at least one is already tested by the "intersects" stage. 271 | # But we use an "any" anyway, because it is sufficient in this case and 272 | # that testing all the points takes more time. 273 | this_in = any( geometry.in_box(p,this_box) for p in that_points ) 274 | that_in = any( geometry.in_box(p,that_box) for p in this_points ) 275 | 276 | return intersects or this_in or that_in 277 | 278 | 279 | def query( self, query_quad, at_quad = None ): 280 | """Return all the points (currently attached to the quad tree) that are located within the query_quad quadrant.""" 281 | if not at_quad: 282 | at_quad = self.root 283 | 284 | query_box = as_box(query_quad) 285 | 286 | # If we ask for a quadrant that intersects with the current one. 287 | if self.covers( query_quad, at_quad ): 288 | # If the current quadrant contains sub-quadrants. 289 | if len(self.children[at_quad]) > 0: 290 | # Then go explore them. 291 | points = [] 292 | for quad in self.children[at_quad]: 293 | points += self.query(query_quad,quad) 294 | return points 295 | else: 296 | # Else, just return the point within the current quadrant. 297 | resident = self.residents[at_quad] 298 | if resident: 299 | if geometry.in_box(resident,query_box): 300 | # In a list, because we will concatenate. 301 | return [resident] 302 | # If there is no intersection, there is no points. 303 | return [] 304 | 305 | 306 | # Pythonesque API: 307 | 308 | def __getitem__( self, quadrant ): 309 | """Return all the points that are located within the given quadrant. 310 | 311 | Ex.: points = quad[quad.root] # get all the points""" 312 | return self.query(quadrant,self.root) 313 | 314 | 315 | def __iter__(self): 316 | """Iterate over the attached points.""" 317 | return iter(self.points()) 318 | 319 | 320 | def __call__(self, points): 321 | """Append all the given points in the quadtree.""" 322 | self.build(points) 323 | 324 | 325 | def __len__(self): 326 | """Return the number of points attached to the quad tree.""" 327 | return len(self.points()) 328 | 329 | 330 | def __repr__(self): 331 | """Return a string representing the quadtree in a JSON-like format.""" 332 | return self.repr() 333 | 334 | 335 | if __name__ == "__main__": 336 | 337 | import sys 338 | import math 339 | import random 340 | 341 | import utils 342 | import uberplot 343 | import matplotlib.pyplot as plot 344 | 345 | if len(sys.argv) == 2: 346 | seed = sys.argv[1] 347 | else: 348 | seed = None 349 | 350 | random.seed(seed) 351 | 352 | n=200 353 | points = [ ( round(random.uniform(-n,n),2),round(random.uniform(-n,n),2) ) for i in range(n) ] 354 | 355 | quad = QuadTree( points ) 356 | # print(quad) 357 | # sys.stderr.write( "%i points in the quadtree / %i points\n" % (len(quad), len(points)) ) 358 | 359 | 360 | fig = plot.figure() 361 | 362 | ax = fig.add_subplot(111) 363 | ax.set_aspect('equal') 364 | 365 | # Plot the whole quad tree and its points. 366 | # Iterating over the quadtree will generate points, thus list(quad) is equivalent to quad.points() 367 | uberplot.scatter_points( ax, list(quad), facecolor="green", edgecolor="None") 368 | 369 | for q in quad.quadrants: 370 | edges = list( utils.tour(as_rect(q)) ) 371 | uberplot.plot_segments( ax, edges, edgecolor = "blue", alpha = 0.1, linewidth = 2 ) 372 | 373 | # Plot a random query on the quad tree. 374 | # Remember a quadrant is ( (orig_y,orig_y), width ) 375 | minp = ( round(random.uniform(-n,n),2), round(random.uniform(-n,n),2) ) 376 | rand_quad = ( minp, round(random.uniform(0,n),2) ) 377 | # Asking for a quadrant will query the quad tree and return the corresponding points. 378 | uberplot.scatter_points( ax, quad[rand_quad], facecolor="None", edgecolor="red", alpha=0.5, linewidth = 2 ) 379 | edges = list( utils.tour(as_rect(rand_quad)) ) 380 | uberplot.plot_segments( ax, edges, edgecolor = "red", alpha = 0.5, linewidth = 2 ) 381 | 382 | plot.show() 383 | 384 | assert(len(points) == len(quad)) 385 | 386 | -------------------------------------------------------------------------------- /run_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #encoding: utf-8 3 | 4 | import os 5 | import sys 6 | import turtle 7 | import argparse 8 | import matplotlib.pyplot as plot 9 | from itertools import ifilterfalse as filter_if_not 10 | 11 | import ants 12 | import utils 13 | from utils import LOG,LOGN 14 | from geometry import x,y 15 | import hull 16 | import uberplot 17 | import shortpath 18 | import lindenmayer 19 | import geometry 20 | import triangulation 21 | import voronoi 22 | import graph 23 | 24 | parser = argparse.ArgumentParser() 25 | 26 | parser.add_argument('-p', "--penrose", help="Do not compute the Penrose tiling but load it from a file", 27 | default=None, action='store', type=str, metavar="SEGMENTS") 28 | parser.add_argument( '-d', '--depth', help="Recursive depth of the Lindenmayer computations = size of the Penrose tiling", 29 | default=1, type=int, metavar="DEPTH") 30 | 31 | parser.add_argument('-t', "--notsp", help="Do not compute the TSP", 32 | default=False, action='store_true') 33 | parser.add_argument('-r', "--tour", help="Load several TSP tour from a file", 34 | default=[None], action='store', type=str, nargs="*", metavar="POINTS") 35 | parser.add_argument('-m', "--pheromones", help="Load a pheromones matrix from a file", 36 | default=None, action='store', type=str, metavar="MATRIX") 37 | 38 | parser.add_argument('-g', "--triangulation", help="Do not compute the Delaunay triangulation but load it from a file", 39 | default=None, action='store', type=str, metavar="SEGMENTS") 40 | parser.add_argument('-v', "--voronoi", help="Do not compute the Voronoï diagram but load it from a file", 41 | default=None, action='store', type=str, metavar="POINTS") 42 | 43 | parser.add_argument('-C', "--cache", help="Try to load available precomputed files instead of computing from scratch", 44 | default=False, action='store_true') 45 | 46 | parser.add_argument('-P', "--noplot-penrose", help="Do not plot the Penrose tiling", default=False, action='store_true') 47 | parser.add_argument('-T', "--noplot-tour", help="Do not plot the TSP tours", default=False, action='store_true') 48 | parser.add_argument('-M', "--noplot-pheromones", help="Do not plot the pheromones matrix", default=False, action='store_true') 49 | parser.add_argument('-G', "--noplot-triangulation", help="Do not plot the triangulation", default=False, action='store_true') 50 | parser.add_argument('-V', "--noplot-voronoi", help="Do not plot the Voronoï diagram", default=False, action='store_true') 51 | 52 | ask_for = parser.parse_args() 53 | 54 | def set_cache( filename, asked = None ): 55 | fname = filename % ask_for.depth 56 | if os.path.isfile(fname): 57 | return fname 58 | else: 59 | return asked 60 | 61 | if ask_for.cache: 62 | ask_for.penrose = set_cache( "d%i_penrose.segments", ask_for.penrose ) 63 | ask_for.triangulation = set_cache("d%i_triangulation.triangles", ask_for.triangulation ) 64 | ask_for.tour = [set_cache("d%i_tour.points", ask_for.tour )] 65 | if ask_for.tour != [None]: 66 | ask_for.notsp = True 67 | else: 68 | ask_for.notsp = False 69 | ask_for.pheromones = set_cache("d%i_pheromones.mat", ask_for.pheromones ) 70 | ask_for.voronoi = set_cache("d%i_voronoi.graph", ask_for.voronoi ) 71 | 72 | 73 | error_codes = {"NOTSP":100} 74 | 75 | depth = ask_for.depth 76 | LOGN( "depth",depth ) 77 | 78 | ######################################################################## 79 | # PENROSE TILING 80 | ######################################################################## 81 | 82 | penrose_segments = set() 83 | 84 | if ask_for.penrose: 85 | LOGN( "Load the penrose tiling" ) 86 | with open(ask_for.penrose) as fd: 87 | penrose_segments = utils.load_segments(fd) 88 | 89 | else: 90 | LOGN( "Draw the penrose tiling" ) 91 | 92 | 93 | segment_size = 10 94 | float_rounding = 10 95 | 96 | ttl = turtle.Turtle() 97 | ttl.speed('fastest') 98 | penrose = lindenmayer.DumpTurtleLSystem(ttl, 99 | axiom="[X]++[X]++[X]++[X]++[X]", 100 | rules={ 101 | 'F': "", 102 | 'W': "YF++ZF----XF[-YF----WF]++", 103 | 'X': "+YF--ZF[---WF--XF]+", 104 | 'Y': "-WF++XF[+++YF++ZF]-", 105 | 'Z': "--YF++++WF[+ZF++++XF]--XF" 106 | }, 107 | angle=36, heading=0, size=segment_size, rounding=float_rounding ) 108 | 109 | # actually do something 110 | penrose.draw( depth ) 111 | 112 | # save this intermediate step 113 | penrose_segments = penrose.segments 114 | LOGN( "\tsegments",len(penrose_segments) ) 115 | with open("d%i_penrose.segments" % depth, "w") as fd: 116 | utils.write_segments( penrose_segments, fd ) 117 | 118 | 119 | 120 | ######################################################################## 121 | # TSP 122 | ######################################################################## 123 | 124 | trajs = [] 125 | 126 | if ask_for.tour != [None]: 127 | for tour in ask_for.tour: 128 | with open(tour) as fd: 129 | trajs.append( utils.load_points(fd) ) 130 | 131 | if ask_for.notsp: 132 | if ask_for.tour == [None] or not ask_for.pheromones: 133 | LOGN( "If you do not want to solve the TSP, you must provide a solution tour (--tour) and a pheromones matrix (--pheromones)" ) 134 | sys.exit(error_codes["NO-TSP"]) 135 | 136 | if ask_for.pheromones: 137 | with open(ask_for.pheromones) as fd: 138 | phero = utils.load_matrix(fd) 139 | 140 | else: 141 | LOGN( "Solve the TSP with an Ant Colony Algorithm" ) 142 | 143 | LOGN( "\tConvert the segment list into an adjacency list graph" ) 144 | G = graph.graph_of( penrose_segments ) 145 | 146 | LOGN( "\tCompute a tour" ) 147 | # max_it = 10 148 | max_it = 2 149 | # num_ants = 10 #* depth 150 | num_ants = 2 #* depth 151 | decay = 0.1 152 | w_heur = 2.5 153 | w_local_phero = 0.1 154 | c_greed = 0.9 155 | w_history = 1.0 156 | 157 | best,phero = ants.search( G, max_it, num_ants, decay, w_heur, w_local_phero, w_history, c_greed, cost_func = ants.graph_distance ) 158 | 159 | LOGN( "\tTransform the resulting nodes permutation into a path on the graph" ) 160 | # by finding the shortest path between two cities. 161 | traj = [] 162 | for start,end in utils.tour(best["permutation"]): 163 | p,c = shortpath.astar( G, start, end ) 164 | traj += p 165 | trajs.append(traj) 166 | 167 | with open("d%i_tour.points" % depth, "w") as fd: 168 | utils.write_points( traj, fd ) 169 | 170 | with open("d%i_pheromones.mat" % depth, "w") as fd: 171 | utils.write_matrix( phero, fd ) 172 | 173 | 174 | ######################################################################## 175 | # TRIANGULATION 176 | ######################################################################## 177 | 178 | triangulated = [] 179 | 180 | if ask_for.triangulation: 181 | with open(ask_for.triangulation) as fd: 182 | triangulated = triangulation.load(fd) 183 | 184 | else: 185 | LOGN( "Compute the triangulation of the penrose vertices" ) 186 | points = utils.vertices_of(penrose_segments) 187 | triangles = triangulation.delaunay_bowyer_watson( points, do_plot = False ) 188 | 189 | LOGN( "\tRemove triangles that are not sub-parts of the Penrose tiling" ) 190 | 191 | 192 | # Filter (i.e. keep) triangles that are strictly acute, 193 | def strictly_acute(triangle): 194 | # By excluding edges, we also ensure that no triangle can be collinear nor rectangle, 195 | return triangulation.is_acute( triangle, exclude_edges = True ) 196 | triangulated_acute = list(filter( strictly_acute, triangles )) 197 | # A more consise but less readable one-liner would be: 198 | # triangulated = list(filter( lambda t: triangulation.is_acute( t, exclude_edges = True ), triangles )) 199 | 200 | # def not_collinear(triangle): 201 | # return not geometry.collinear(*triangle) 202 | # triangulated = list(filter( not_collinear, triangulated_acute )) 203 | triangulated = triangulated_acute 204 | 205 | LOGN( "\t\tRemoved", len(triangles)-len(triangulated), "triangles from", len(triangles)) 206 | 207 | with open("d%i_triangulation.triangles" % depth, "w") as fd: 208 | triangulation.write( triangulated, fd ) 209 | 210 | triangulation_edges = triangulation.edges_of( triangulated ) 211 | 212 | 213 | ######################################################################## 214 | # VORONOÏ 215 | ######################################################################## 216 | 217 | voronoi_graph = {} 218 | 219 | if ask_for.voronoi: 220 | with open(ask_for.voronoi) as fd: 221 | voronoi_graph = graph.load( fd ) 222 | 223 | else: 224 | LOGN( "Compute the Voronoï diagram of the triangulation" ) 225 | # Changing the neighborhood to be on vertices instead of edges will not compute the true Voronoï dual graph, 226 | # but we want this graph to represent the relations on vertices of the tiles. 227 | voronoi_tri_graph = voronoi.dual(triangulated, neighborhood = voronoi.vertices_neighbours) 228 | # voronoi_tri_edges = graph.edges_of(voronoi_tri_graph) 229 | # voronoi_tri_centers = graph.nodes_of(voronoi_tri_graph) 230 | 231 | LOGN("\tMerge nodes that are both located within a single diamond" ) 232 | LOG("\t\tMerge",len(voronoi_tri_graph),"nodes") 233 | with open("d%i_voronoi_dual.graph" % depth, "w") as fd: 234 | graph.write( voronoi_tri_graph, fd ) 235 | voronoi_graph = voronoi.merge_enclosed( voronoi_tri_graph, penrose_segments ) 236 | LOGN("as",len(voronoi_graph),"enclosed nodes") 237 | 238 | with open("d%i_voronoi.graph" % depth, "w") as fd: 239 | graph.write( voronoi_graph, fd ) 240 | 241 | 242 | voronoi_edges = graph.edges_of( voronoi_graph ) 243 | voronoi_centers = graph.nodes_of( voronoi_graph ) 244 | 245 | 246 | ######################################################################## 247 | # PLOT 248 | ######################################################################## 249 | 250 | dpi = 600 251 | 252 | contrast = [ 253 | ("pheromones",{"edgecolor":"blue"}), # do not specify linewidth and alpha 254 | ("tour",{"edgecolor":"red","alpha":0.9, "linewidth":3}), 255 | ("penrose",{"edgecolor":"black", "alpha":0.9, "linewidth":0.9}), 256 | ("triangulation",{"edgecolor":"green","alpha":0.2,"linewidth":1}), 257 | ("voronoi_edges",{"edgecolor":"magenta","alpha":1,"linewidth":1}), 258 | ("voronoi_nodes",{"edgecolor":"magenta","facecolor":"white","alpha":1,"linewidth":1,"s":200}), 259 | ] 260 | 261 | clean = [ 262 | ("triangulation",{"edgecolor":"lightgreen","alpha":1,"linewidth":0.5}), 263 | ("pheromones",{"edgecolor":"#ffcc00"}), 264 | ("tour",{"edgecolor":"#7777dd","alpha":0.7, "linewidth":2.5}), 265 | ("penrose",{"edgecolor":"black", "alpha":1, "linewidth":0.3}), 266 | ("voronoi_edges",{"edgecolor":"blue","alpha":0.4,"linewidth":1}), 267 | ("voronoi_nodes",{"edgecolor":"blue","facecolor":"white","alpha":0.4,"linewidth":1.5,"s":250}), 268 | ] 269 | 270 | chosen_theme = clean 271 | 272 | # Rebuild the theme from the chosen one and add zorder using the order given in the theme. 273 | theme = {} 274 | for i in range(len(chosen_theme)): 275 | k = chosen_theme[i][0] 276 | theme[k] = chosen_theme[i][1] 277 | theme[k]["zorder"] = i 278 | 279 | 280 | LOGN( "Plot the image" ) 281 | fig = plot.figure() 282 | ax = fig.add_subplot(111) 283 | 284 | if not ask_for.noplot_pheromones: 285 | LOGN( "\tpheromones",len(phero),"nodes" )#,"x",len(phero[traj[0]]) ) 286 | maxph=0 287 | for i in phero: 288 | maxph = max( maxph, max(phero[i].values())) 289 | 290 | # ant colony 291 | # pheromones 292 | for i in phero: 293 | for j in phero[i]: 294 | if i == j: 295 | continue 296 | nph = phero[i][j]/maxph 297 | seg = [(i,j)] 298 | # LOGN( nph,seg ) 299 | uberplot.plot_segments( ax, seg, alpha=0.01*nph, linewidth=1*nph, **theme["pheromones"] ) 300 | # uberplot.scatter_segments( ax, seg, color="red", alpha=0.5, linewidth=nph ) 301 | 302 | if not ask_for.noplot_tour: 303 | for traj in trajs: 304 | LOGN( "\ttraj",len(traj),"points" ) 305 | # best tour 306 | uberplot.plot_segments( ax, utils.tour(traj), **theme["tour"]) 307 | 308 | if not ask_for.noplot_penrose: 309 | LOGN( "\ttiling",len(penrose_segments),"segments" ) 310 | uberplot.plot_segments( ax, penrose_segments, **theme["penrose"]) 311 | 312 | if not ask_for.noplot_triangulation: 313 | LOGN( "\ttriangulation",len(triangulation_edges),"edges" ) 314 | uberplot.plot_segments( ax, triangulation_edges, **theme["triangulation"]) 315 | 316 | if not ask_for.noplot_voronoi: 317 | LOGN( "\tVoronoï",len(voronoi_edges),"edges") 318 | # uberplot.plot_segments( ax, voronoi_tri_edges, edgecolor="red", alpha=1, linewidth=1 ) 319 | # uberplot.scatter_points( ax, voronoi_tri_centers, edgecolor="red", facecolor="white", s=200, alpha=1, zorder=10 ) 320 | uberplot.plot_segments( ax, voronoi_edges, **theme["voronoi_edges"] ) 321 | uberplot.scatter_points( ax, voronoi_centers, **theme["voronoi_nodes"] ) 322 | 323 | ax.set_aspect('equal') 324 | 325 | 326 | # transparent background in SVG 327 | fig.patch.set_visible(False) 328 | ax.axis('off') 329 | plot.savefig("ubergeekism_d%i.svg" % depth, dpi=dpi) 330 | 331 | fig.patch.set_visible(True) 332 | fig.patch.set_facecolor('white') 333 | plot.savefig("ubergeekism_d%i.png" % depth, dpi=dpi) 334 | 335 | plot.show() 336 | 337 | -------------------------------------------------------------------------------- /shortpath.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | from geometry import x,y,euclidian_distance 4 | 5 | 6 | def astar(graph, start, goal, cost = euclidian_distance, heuristic = euclidian_distance): 7 | opened = set() 8 | closed = set() 9 | m_heur = {} 10 | m_parent = {} 11 | m_cost = {} # absolute path costs 12 | 13 | def path_from(node): 14 | def parents(node): 15 | while node: 16 | yield node 17 | node = m_parent.get(node, None) 18 | path = [p for p in parents(node)] 19 | cost = m_cost[goal] 20 | path.reverse() 21 | return path,cost 22 | 23 | opened.add(start) 24 | m_cost[start] = 0 25 | while opened: 26 | # sort opened nodes based on the heuristic and consider the first one 27 | current = sorted(opened, key=lambda n : m_heur.get( n, heuristic(n,goal) ) )[0] 28 | if current == goal: 29 | # FIXME add backtrack validation 30 | return path_from(current) 31 | 32 | closed.add(current) 33 | opened.remove(current) 34 | 35 | for neighbor in graph[current]: 36 | if neighbor in closed: 37 | continue 38 | 39 | elif neighbor in opened: 40 | next_cost = m_cost[current] + cost(current,neighbor) 41 | if next_cost < m_cost[neighbor]: 42 | m_cost[neighbor] = next_cost 43 | m_parent[neighbor] = current 44 | else: 45 | m_cost[neighbor] = m_cost[current] + cost(current,neighbor) 46 | m_heur[neighbor] = heuristic( neighbor, goal ) 47 | m_parent[neighbor] = current 48 | opened.add(neighbor) 49 | return [] 50 | 51 | 52 | if __name__ == "__main__": 53 | print """Graph: 54 | -1 0 2 : x 55 | 1 o o-----o 56 | | | | 57 | 0 o--o-----o 58 | | | 59 | | | 60 | -2 o--o-----o 61 | : 62 | y 63 | """ 64 | G = { 65 | ( 0, 0) : [(-1, 0),( 0, 1),( 2, 0),( 0,-2)], 66 | ( 0, 1) : [( 0, 0),( 2, 1)], 67 | ( 0,-2) : [( 0, 0),( 2,-2),(-1,-2)], 68 | (-1, 0) : [(-1, 1),( 0, 0)], 69 | (-1, 1) : [(-1, 0)], 70 | (-1,-2) : [( 0,-2)], 71 | ( 2, 0) : [( 2, 1),( 2,-2),( 0, 0)], 72 | ( 2, 1) : [( 0, 1),( 2, 0)], 73 | ( 2,-2) : [( 2, 0),( 0,-2)], 74 | } 75 | 76 | print "Path from (-1,1) to (-1,-2):" 77 | path,cost = astar( G, (-1,1), (-1,-2) ) 78 | print "Cost:",cost 79 | print"Path:",path 80 | 81 | -------------------------------------------------------------------------------- /test_tsp.py: -------------------------------------------------------------------------------- 1 | 2 | from tsplib import * 3 | 4 | 5 | segments = [ 6 | ( (0,0),(0,2) ), 7 | ( (0,2),(2,2) ), 8 | ( (2,2),(2,0) ), 9 | ( (2,0),(0,0) ) 10 | ] 11 | 12 | filename = "test.tsp" 13 | with open(filename,"w") as fd: 14 | write_segments( segments, fd=fd, size=1, depth=0, rounding=10 ) 15 | write_segments( segments, fd=sys.stdout, size=1, depth=0, rounding=10 ) 16 | 17 | with open(filename,"r") as fd: 18 | nodes = read_nodes( fd ) 19 | 20 | print "Nodes: id (x, y)" 21 | for idx,node in nodes.items(): 22 | print idx,node 23 | 24 | with open(filename,"r") as fd: 25 | vertices = read_vertices( fd ) 26 | 27 | print "Segments: (x1,y1) (x2,y2)" 28 | segments = [] 29 | for i1,i2 in vertices: 30 | print nodes[i1],nodes[i2] 31 | segments.append( (nodes[i1],nodes[i2]) ) 32 | 33 | plot_segments( segments ) 34 | 35 | 36 | -------------------------------------------------------------------------------- /triangulation.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import math 4 | from itertools import ifilterfalse as filter_if_not 5 | 6 | from utils import tour,LOG,LOGN 7 | from geometry import mid,middle,x,y 8 | 9 | # Based on http://paulbourke.net/papers/triangulate/ 10 | # Efficient Triangulation Algorithm Suitable for Terrain Modelling 11 | # An Algorithm for Interpolating Irregularly-Spaced Data 12 | # with Applications in Terrain Modelling 13 | # Written by Paul Bourke 14 | # Presented at Pan Pacific Computer Conference, Beijing, China. 15 | # January 1989 16 | 17 | def mtan( pa, pb ): 18 | return -1 * ( x(pa) - x(pb) ) / ( y(pa) - y(pb) ) 19 | 20 | 21 | class CoincidentPointsError(Exception): 22 | """Coincident points""" 23 | pass 24 | 25 | def circumcircle( triangle, epsilon = sys.float_info.epsilon ): 26 | """Compute the circumscribed circle of a triangle and 27 | Return a 2-tuple: ( (center_x, center_y), radius )""" 28 | 29 | assert( len(triangle) == 3 ) 30 | p0,p1,p2 = triangle 31 | assert( len(p0) == 2 ) 32 | assert( len(p1) == 2 ) 33 | assert( len(p2) == 2 ) 34 | 35 | dy01 = abs( y(p0) - y(p1) ) 36 | dy12 = abs( y(p1) - y(p2) ) 37 | 38 | if dy01 < epsilon and dy12 < epsilon: 39 | # coincident points 40 | raise CoincidentPointsError 41 | 42 | elif dy01 < epsilon: 43 | m12 = mtan( p2,p1 ) 44 | mx12,my12 = middle( p1, p2 ) 45 | cx = mid( x, p1, p0 ) 46 | cy = m12 * (cx - mx12) + my12 47 | 48 | elif dy12 < epsilon: 49 | m01 = mtan( p1, p0 ) 50 | mx01,my01 = middle( p0, p1 ) 51 | cx = mid( x, p2, p1 ) 52 | cy = m01 * ( cx - mx01 ) + my01 53 | 54 | else: 55 | m01 = mtan( p1, p0 ) 56 | m12 = mtan( p2, p1 ) 57 | mx01,my01 = middle( p0, p1 ) 58 | mx12,my12 = middle( p1, p2 ) 59 | cx = ( m01 * mx01 - m12 * mx12 + my12 - my01 ) / ( m01 - m12 ) 60 | if dy01 > dy12: 61 | cy = m01 * ( cx - mx01 ) + my01 62 | else: 63 | cy = m12 * ( cx - mx12 ) + my12 64 | 65 | dx1 = x(p1) - cx 66 | dy1 = y(p1) - cy 67 | r = math.sqrt(dx1**2 + dy1**2) 68 | 69 | return (cx,cy),r 70 | 71 | 72 | def in_circle( p, center, radius, epsilon = sys.float_info.epsilon ): 73 | """Return True if the given point p is in the given circle""" 74 | 75 | assert( len(p) == 2 ) 76 | cx,cy = center 77 | 78 | dxp = x(p) - cx 79 | dyp = y(p) - cy 80 | dr = math.sqrt(dxp**2 + dyp**2) 81 | 82 | if (dr - radius) <= epsilon: 83 | return True 84 | else: 85 | return False 86 | 87 | 88 | def in_circumcircle( p, triangle, epsilon = sys.float_info.epsilon ): 89 | """Return True if the given point p is in the circumscribe circle of the given triangle""" 90 | 91 | assert( len(p) == 2 ) 92 | (cx,cy),r = circumcircle( triangle, epsilon ) 93 | return in_circle( p, (cx,cy), r, epsilon ) 94 | 95 | 96 | def in_triangle( p0, triangle, exclude_edges = False ): 97 | """Return True if the given point lies inside the given triangle""" 98 | 99 | p1,p2,p3 = triangle 100 | 101 | # Compute the barycentric coordinates 102 | alpha = ( (y(p2) - y(p3)) * (x(p0) - x(p3)) + (x(p3) - x(p2)) * (y(p0) - y(p3)) ) \ 103 | / ( (y(p2) - y(p3)) * (x(p1) - x(p3)) + (x(p3) - x(p2)) * (y(p1) - y(p3)) ) 104 | beta = ( (y(p3) - y(p1)) * (x(p0) - x(p3)) + (x(p1) - x(p3)) * (y(p0) - y(p3)) ) \ 105 | / ( (y(p2) - y(p3)) * (x(p1) - x(p3)) + (x(p3) - x(p2)) * (y(p1) - y(p3)) ) 106 | gamma = 1.0 - alpha - beta 107 | 108 | if exclude_edges: 109 | # If all of alpha, beta, and gamma are strictly in ]0,1[, 110 | # then the point p0 strictly lies within the triangle. 111 | return all( 0 < x < 1 for x in (alpha, beta, gamma) ) 112 | else: 113 | # If the inequality is not strict, then the point may lies on an edge. 114 | return all( 0 <= x <= 1 for x in (alpha, beta, gamma) ) 115 | 116 | 117 | def is_acute(triangle, exclude_edges = False ): 118 | """Return True if the center of the circumcircle of the given triangle lies inside the triangle. 119 | That is if the triangle is acute.""" 120 | return in_triangle( circumcircle(triangle)[0], triangle, exclude_edges ) 121 | 122 | 123 | def bounds( vertices ): 124 | """Return the iso-axis rectangle enclosing the given points""" 125 | # find vertices set bounds 126 | xmin = x(vertices[0]) 127 | ymin = y(vertices[0]) 128 | xmax = xmin 129 | ymax = ymin 130 | 131 | # we do not use min(vertices,key=x) because it would iterate 4 times over the list, instead of just one 132 | for v in vertices: 133 | xmin = min(x(v),xmin) 134 | xmax = max(x(v),xmax) 135 | ymin = min(y(v),ymin) 136 | ymax = max(y(v),ymax) 137 | return (xmin,ymin),(xmax,ymax) 138 | 139 | 140 | def supertriangle( vertices, delta = 0.1 ): 141 | """Return a super-triangle that encloses all given points. 142 | The super-triangle has its base at the bottom and encloses the bounding box at a distance given by: 143 | delta*max(width,height) 144 | """ 145 | 146 | # Iso-rectangle bounding box. 147 | (xmin,ymin),(xmax,ymax) = bounds( vertices ) 148 | 149 | dx = xmax - xmin 150 | dy = ymax - ymin 151 | dmax = max( dx, dy ) 152 | xmid = (xmax + xmin) / 2.0 153 | 154 | supertri = ( ( xmin-dy-dmax*delta, ymin-dmax*delta ), 155 | ( xmax+dy+dmax*delta, ymin-dmax*delta ), 156 | ( xmid , ymax+(xmax-xmid)+dmax*delta ) ) 157 | 158 | return supertri 159 | 160 | 161 | def delaunay_bowyer_watson( points, supertri = None, superdelta = 0.1, epsilon = sys.float_info.epsilon, 162 | do_plot = None, plot_filename = "Bowyer-Watson_%i.png" ): 163 | """Return the Delaunay triangulation of the given points 164 | 165 | epsilon: used for floating point comparisons, two points are considered equals if their distance is < epsilon. 166 | do_plot: if not None, plot intermediate steps on this matplotlib object and save them as images named: plot_filename % i 167 | """ 168 | 169 | if do_plot and len(points) > 10: 170 | print "WARNING it is a bad idea to plot each steps of a triangulation of many points" 171 | 172 | # Sort points first on the x-axis, then on the y-axis. 173 | vertices = sorted( points ) 174 | 175 | # LOGN( "super-triangle",supertri ) 176 | if not supertri: 177 | supertri = supertriangle( vertices, superdelta ) 178 | 179 | # It is the first triangle of the list. 180 | triangles = [ supertri ] 181 | 182 | completed = { supertri: False } 183 | 184 | # The predicate returns true if at least one of the vertices 185 | # is also found in the supertriangle. 186 | def match_supertriangle( tri ): 187 | if tri[0] in supertri or \ 188 | tri[1] in supertri or \ 189 | tri[2] in supertri: 190 | return True 191 | 192 | # Returns the base of each plots, with points, current triangulation, super-triangle and bounding box. 193 | def plot_base(ax,vi = len(vertices), vertex = None): 194 | ax.set_aspect('equal') 195 | # regular points 196 | scatter_x = [ p[0] for p in vertices[:vi]] 197 | scatter_y = [ p[1] for p in vertices[:vi]] 198 | ax.scatter( scatter_x,scatter_y, s=30, marker='o', facecolor="black") 199 | # super-triangle vertices 200 | scatter_x = [ p[0] for p in list(supertri)] 201 | scatter_y = [ p[1] for p in list(supertri)] 202 | ax.scatter( scatter_x,scatter_y, s=30, marker='o', facecolor="lightgrey", edgecolor="lightgrey") 203 | # current vertex 204 | if vertex: 205 | ax.scatter( vertex[0],vertex[1], s=30, marker='o', facecolor="red", edgecolor="red") 206 | # current triangulation 207 | uberplot.plot_segments( ax, edges_of(triangles), edgecolor = "blue", alpha=0.5, linestyle='solid' ) 208 | # bounding box 209 | (xmin,ymin),(xmax,ymax) = bounds(vertices) 210 | uberplot.plot_segments( ax, tour([(xmin,ymin),(xmin,ymax),(xmax,ymax),(xmax,ymin)]), edgecolor = "magenta", alpha=0.2, linestyle='dotted' ) 211 | 212 | 213 | # Insert vertices one by one. 214 | LOG("Insert vertices: ") 215 | if do_plot: 216 | it=0 217 | for vi,vertex in enumerate(vertices): 218 | # LOGN( "\tvertex",vertex ) 219 | assert( len(vertex) == 2 ) 220 | 221 | if do_plot: 222 | ax = do_plot.add_subplot(111) 223 | plot_base(ax,vi,vertex) 224 | 225 | # All the triangles whose circumcircle encloses the point to be added are identified, 226 | # the outside edges of those triangles form an enclosing polygon. 227 | 228 | # Forget previous candidate polygon's edges. 229 | enclosing = [] 230 | 231 | removed = [] 232 | for triangle in triangles: 233 | # LOGN( "\t\ttriangle",triangle ) 234 | assert( len(triangle) == 3 ) 235 | 236 | # Do not consider triangles already tested. 237 | # If completed has a key, test it, else return False. 238 | if completed.get( triangle, False ): 239 | # LOGN( "\t\t\tAlready completed" ) 240 | # if do_plot: 241 | # uberplot.plot_segments( ax, tour(list(triangle)), edgecolor = "magenta", alpha=1, lw=1, linestyle='dotted' ) 242 | continue 243 | 244 | # LOGN( "\t\t\tCircumcircle" ) 245 | assert( triangle[0] != triangle[1] and triangle[1] != triangle [2] and triangle[2] != triangle[0] ) 246 | center,radius = circumcircle( triangle, epsilon ) 247 | 248 | # If it match Delaunay's conditions. 249 | if x(center) < x(vertex) and math.sqrt((x(vertex)-x(center))**2) > radius: 250 | # LOGN( "\t\t\tMatch Delaunay, mark as completed" ) 251 | completed[triangle] = True 252 | 253 | # If the current vertex is inside the circumscribe circle of the current triangle, 254 | # add the current triangle's edges to the candidate polygon. 255 | if in_circle( vertex, center, radius, epsilon ): 256 | # LOGN( "\t\t\tIn circumcircle, add to enclosing polygon",triangle ) 257 | if do_plot: 258 | circ = plot.Circle(center, radius, facecolor='yellow', edgecolor="orange", alpha=0.2, clip_on=False) 259 | ax.add_patch(circ) 260 | 261 | for p0,p1 in tour(list(triangle)): 262 | # Then add this edge to the polygon enclosing the vertex, 263 | enclosing.append( (p0,p1) ) 264 | # and remove the corresponding triangle from the current triangulation. 265 | removed.append( triangle ) 266 | completed.pop(triangle,None) 267 | 268 | elif do_plot: 269 | circ = plot.Circle(center, radius, facecolor='lightgrey', edgecolor="grey", alpha=0.2, clip_on=False) 270 | ax.add_patch(circ) 271 | 272 | # end for triangle in triangles 273 | 274 | # The triangles in the enclosing polygon are deleted and 275 | # new triangles are formed between the point to be added and 276 | # each outside edge of the enclosing polygon. 277 | 278 | # Actually remove triangles. 279 | for triangle in removed: 280 | triangles.remove(triangle) 281 | 282 | 283 | # Remove duplicated edges. 284 | # This leaves the edges of the enclosing polygon only, 285 | # because enclosing edges are only in a single triangle, 286 | # but edges inside the polygon are at least in two triangles. 287 | hull = [] 288 | for i,(p0,p1) in enumerate(enclosing): 289 | # Clockwise edges can only be in the remaining part of the list. 290 | # Search for counter-clockwise edges as well. 291 | if (p0,p1) not in enclosing[i+1:] and (p1,p0) not in enclosing: 292 | hull.append((p0,p1)) 293 | elif do_plot: 294 | uberplot.plot_segments( ax, [(p0,p1)], edgecolor = "white", alpha=1, lw=1, linestyle='dotted' ) 295 | 296 | 297 | 298 | if do_plot: 299 | uberplot.plot_segments( ax, hull, edgecolor = "red", alpha=1, lw=1, linestyle='solid' ) 300 | 301 | 302 | # Create new triangles using the current vertex and the enclosing hull. 303 | # LOGN( "\t\tCreate new triangles" ) 304 | for p0,p1 in hull: 305 | assert( p0 != p1 ) 306 | triangle = tuple([p0,p1,vertex]) 307 | # LOGN("\t\t\tNew triangle",triangle) 308 | triangles.append( triangle ) 309 | completed[triangle] = False 310 | 311 | if do_plot: 312 | uberplot.plot_segments( ax, [(p0,vertex),(p1,vertex)], edgecolor = "green", alpha=1, linestyle='solid' ) 313 | 314 | if do_plot: 315 | plot.savefig( plot_filename % it, dpi=150) 316 | plot.clf() 317 | 318 | it+=1 319 | LOG(".") 320 | 321 | # end for vertex in vertices 322 | LOGN(" done") 323 | 324 | 325 | # Remove triangles that have at least one of the supertriangle vertices. 326 | # LOGN( "\tRemove super-triangles" ) 327 | 328 | # Filter out elements for which the predicate is False, 329 | # here: *keep* elements that *do not* have a common vertex. 330 | # The filter is a generator, so we must make a list with it to actually get the data. 331 | triangulation = list(filter_if_not( match_supertriangle, triangles )) 332 | 333 | if do_plot: 334 | ax = do_plot.add_subplot(111) 335 | plot_base(ax) 336 | uberplot.plot_segments( ax, edges_of(triangles), edgecolor = "red", alpha=0.5, linestyle='solid' ) 337 | uberplot.plot_segments( ax, edges_of(triangulation), edgecolor = "blue", alpha=1, linestyle='solid' ) 338 | plot.savefig( plot_filename % it, dpi=150) 339 | plot.clf() 340 | 341 | return triangulation 342 | 343 | 344 | def edges_of( triangulation ): 345 | """Return a list containing the edges of the given list of 3-tuples of points""" 346 | edges = [] 347 | for t in triangulation: 348 | for e in tour(list(t)): 349 | edges.append( e ) 350 | return edges 351 | 352 | 353 | def load( stream ): 354 | triangles = [] 355 | for line in stream: 356 | if line.strip()[0] != "#": 357 | tri = line.strip().split() 358 | assert(len(tri)==3) 359 | triangle = [] 360 | for p in tri: 361 | point = tuple(float(i) for i in p.split(",")) 362 | assert(len(point)==2) 363 | triangle.append( point ) 364 | triangles.append( triangle ) 365 | return triangles 366 | 367 | 368 | def write( triangles, stream ): 369 | for tri in triangles: 370 | assert(len(tri)==3) 371 | p,q,r = tri 372 | stream.write("%f,%f %f,%f %f,%f\n" % ( x(p),y(p), x(q),y(q), x(r),y(r) ) ) 373 | 374 | 375 | if __name__ == "__main__": 376 | import random 377 | import utils 378 | import uberplot 379 | import matplotlib.pyplot as plot 380 | 381 | if len(sys.argv) > 1: 382 | scale = 100 383 | nb = int(sys.argv[1]) 384 | points = [ (scale*random.random(),scale*random.random()) for i in range(nb)] 385 | else: 386 | points = [ 387 | (0,40), 388 | (100,60), 389 | (40,0), 390 | (50,100), 391 | (90,10), 392 | # (50,50), 393 | ] 394 | 395 | fig = plot.figure() 396 | 397 | triangles = delaunay_bowyer_watson( points, do_plot = fig ) 398 | 399 | edges = edges_of( triangles ) 400 | 401 | ax = fig.add_subplot(111) 402 | ax.set_aspect('equal') 403 | uberplot.scatter_segments( ax, edges, facecolor = "red" ) 404 | uberplot.plot_segments( ax, edges, edgecolor = "blue" ) 405 | plot.show() 406 | 407 | -------------------------------------------------------------------------------- /tsplib.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import scipy 3 | 4 | def segments_to_nodes( segments ): 5 | """construct a {coords:id} dictionary and returns (nodes,nb)""" 6 | nodes = {} 7 | nb = 0 8 | for segment in segments: 9 | for coords in segment: 10 | if not nodes.has_key(coords): 11 | nodes[coords] = nb 12 | nb += 1 13 | return nodes,nb 14 | 15 | 16 | def write_nodes_simple( nodes, fd = sys.stdout ): 17 | """write only the coordinates""" 18 | fd.write("%i\n" % len(nodes)) 19 | nodes_index = nodes.keys() 20 | nodes_index.sort() 21 | for i in nodes_index: 22 | x,y = nodes[i] 23 | fd.write( "%f %f\n" % (x,y)) 24 | 25 | 26 | 27 | def write_segments( segments, size, depth, rounding, fd = sys.stdout, 28 | node_coord_section=False, 29 | edge_data_section=False, 30 | edge_weight_section=True, 31 | display_data_section=True ): 32 | 33 | nodes,nb = segments_to_nodes(segments) 34 | 35 | fd.write( "NAME : penrose3_%i\n" % depth) 36 | fd.write("COMMENT : Rhombus Penrose tiling (type P3) as generated by a L-system, at depth %i\n" % depth) 37 | fd.write("TYPE : TSP\n") 38 | fd.write("DIMENSION : %i\n" % nb ) 39 | if edge_weight_section: 40 | fd.write("EDGE_WEIGHT_TYPE : EXPLICIT\n") 41 | fd.write("EDGE_WEIGHT_FORMAT : FULL_MATRIX\n") 42 | if edge_data_section: 43 | fd.write("EDGE_DATA_FORMAT : ADJ_LIST\n") # via the weight matrix? 44 | if node_coord_section: 45 | fd.write("NODE_COORD_TYPE : TWOD_COORDS\n") # do not work with concord 46 | if display_data_section: 47 | fd.write("DISPLAY_DATA_TYPE : TWOD_DISPLAY\n") 48 | 49 | if node_coord_section: 50 | fd.write("NODE_COORD_SECTION\n") 51 | fmt = "%"+str(len(str(nb)))+"i %"+str(rounding)+"f %"+str(rounding)+"f\n" 52 | for x,y in nodes: 53 | fd.write(fmt % (nodes[(x,y)],x,y)) 54 | 55 | if edge_data_section: 56 | fd.write("EDGE_DATA_SECTION\n") 57 | for segment in segments: 58 | start,end = segment 59 | fd.write( str(nodes[start])+" "+str(nodes[end])+"\n" ) 60 | 61 | if edge_weight_section: 62 | fd.write("EDGE_WEIGHT_SECTION\n") 63 | # fill the weights matrix with size where necessary 64 | weights = scipy.zeros((nb,nb), type(size)) 65 | for segment in segments: 66 | start,end = segment 67 | weights[nodes[start],nodes[end]] = size 68 | weights[nodes[end],nodes[start]] = size 69 | #fd.write(nodes[start],nodes[end] 70 | 71 | fmt = "%"+str(len(str(size)))+"i " 72 | for i in xrange(weights.shape[0]): 73 | # full matrix 74 | for j in xrange(weights.shape[1]): 75 | fd.write(fmt % weights[i,j]) 76 | fd.write('\n') 77 | 78 | if display_data_section: 79 | fd.write("DISPLAY_DATA_SECTION\n") 80 | fmt = "%"+str(len(str(nb)))+"i %"+str(rounding)+"f %"+str(rounding)+"f\n" 81 | sorted_nodes = [0] * len(nodes) 82 | for x,y in nodes: 83 | sorted_nodes[nodes[(x,y)]] = (x,y) 84 | for i in xrange(len(sorted_nodes)): 85 | x,y = sorted_nodes[i] 86 | fd.write(fmt % (i,x,y)) 87 | 88 | 89 | fd.write("EOF\n") 90 | 91 | 92 | def read_tour_index( fd ): 93 | tour = [] 94 | nb = int(fd.readline().strip()) 95 | for line in fd: 96 | tour += line.split() 97 | 98 | return map(int, tour) 99 | 100 | def read_tour_EO( fd ): 101 | line = fd.readline() 102 | sol = line.split() 103 | tour = sol[2:] # 0=fitness, 1=size, 2+=tour 104 | 105 | return map(int, tour) 106 | 107 | 108 | 109 | def read_nodes( fd ): 110 | """Parse a .tsp file and returns a dictionary of nodes, of the form {id:(x,y)}""" 111 | nodes = {} 112 | data_section = False 113 | for line in fd: 114 | if line.strip() == "DISPLAY_DATA_SECTION": 115 | data_section = True 116 | continue 117 | 118 | data = line.strip().split() 119 | if len(data) != 3 or ( len(data) > 1 and data[0].isalpha() ): 120 | data_section = False 121 | 122 | if data_section == True: 123 | nodes[ int(data[0]) ] = ( float(data[1]),float(data[2]) ) 124 | 125 | return nodes 126 | 127 | 128 | def read_vertices( fd ): 129 | vertices = set() 130 | 131 | data_section = False 132 | i=-1 133 | for line in fd: 134 | if line.strip() == "EDGE_WEIGHT_SECTION": 135 | data_section = True 136 | i=0 137 | continue 138 | 139 | data = line.strip().split() 140 | if len(data)==0 or ( len(data) >= 1 and data[0][0].isalpha() ): 141 | data_section = False 142 | 143 | if data_section == True: 144 | for j in xrange(len(data)): 145 | if float(data[j]) != 0 and (j,i) not in vertices: 146 | vertices.add( (i,j) ) 147 | i += 1 148 | 149 | return vertices 150 | 151 | 152 | def plot_segments( segments ): 153 | 154 | import matplotlib.pyplot as plot 155 | from matplotlib.path import Path 156 | import matplotlib.patches as patches 157 | 158 | fig = plot.figure() 159 | ax = fig.add_subplot(111) 160 | 161 | for segment in segments: 162 | start,end = segment 163 | verts = [start,end,(0,0)] 164 | codes = [Path.MOVETO,Path.LINETO,Path.STOP] 165 | path = Path(verts, codes) 166 | patch = patches.PathPatch(path, edgecolor='green', lw=1) 167 | ax.add_patch(patch) 168 | 169 | ax.set_xlim(-50,50) 170 | ax.set_ylim(-50,50) 171 | 172 | plot.show() 173 | 174 | 175 | def plot_segments_tour( segments_1, segments_2 ): 176 | 177 | import matplotlib.pyplot as plot 178 | from matplotlib.path import Path 179 | import matplotlib.patches as patches 180 | 181 | fig = plot.figure() 182 | ax = fig.add_subplot(111) 183 | 184 | for segment in segments_1: 185 | start,end = segment 186 | verts = [start,end,(0,0)] 187 | codes = [Path.MOVETO,Path.LINETO,Path.STOP] 188 | path = Path(verts, codes) 189 | patch = patches.PathPatch(path, edgecolor='blue', lw=1) 190 | ax.add_patch(patch) 191 | 192 | for segment in segments_2: 193 | start,end = segment 194 | verts = [start,end,(0,0)] 195 | codes = [Path.MOVETO,Path.LINETO,Path.STOP] 196 | path = Path(verts, codes) 197 | patch = patches.PathPatch(path, edgecolor='red', lw=2) 198 | ax.add_patch(patch) 199 | 200 | 201 | ax.set_xlim(-50,50) 202 | ax.set_ylim(-50,50) 203 | 204 | plot.show() 205 | 206 | 207 | 208 | 209 | if __name__=="__main__": 210 | import sys 211 | 212 | finstance = sys.argv[1] 213 | ftour = sys.argv[2] 214 | 215 | print "Read nodes", 216 | sys.stdout.flush() 217 | with open(finstance,"r") as fd: 218 | nodes = read_nodes( fd ) 219 | print len(nodes) 220 | 221 | with open(finstance+".nodes","w") as fd: 222 | write_nodes_simple(nodes,fd) 223 | 224 | print "Read vertices", 225 | sys.stdout.flush() 226 | with open(finstance,"r") as fd: 227 | vertices = read_vertices( fd ) 228 | print len(vertices) 229 | 230 | print "Build segments", 231 | sys.stdout.flush() 232 | segments = [] 233 | for i1,i2 in vertices: 234 | #print nodes[i1],nodes[i2] 235 | segments.append( (nodes[i1],nodes[i2]) ) 236 | print len(segments) 237 | 238 | # print "Plot segments" 239 | # plot_segments( segments ) 240 | 241 | 242 | print "Read tour", 243 | sys.stdout.flush() 244 | with open(ftour,"r") as fd: 245 | #tour = read_tour_index( fd ) 246 | tour = read_tour_EO( fd ) 247 | print len(tour) 248 | 249 | #print tour 250 | 251 | print "Build tour segments", 252 | sys.stdout.flush() 253 | tour_segments = [] 254 | for i in xrange(0,len(tour)-1): 255 | tour_segments.append( ( nodes[tour[i]],nodes[tour[i+1]] ) ) 256 | #print tour_segments[-1] 257 | 258 | tour_segments.append( ( nodes[tour[len(tour)]], nodes[tour[0]] ) ) 259 | #print tour_segments[-1] 260 | print len(tour_segments) 261 | 262 | print "Plot tour segments" 263 | plot_segments_tour( segments, tour_segments ) 264 | 265 | -------------------------------------------------------------------------------- /uberplot.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import argparse 4 | 5 | import matplotlib.pyplot as plot 6 | from matplotlib.path import Path 7 | import matplotlib.patches as patches 8 | 9 | import utils 10 | 11 | def parse_segments( filename ): 12 | segments = [] 13 | with open( filename ) as fd: 14 | for line in fd: 15 | edge = [ float(i) for i in line.split() ] 16 | if len(edge) == 4: 17 | start = (edge[0],edge[1]) 18 | end = (edge[2],edge[3]) 19 | segments.append( (start,end) ) 20 | return segments 21 | 22 | 23 | def plot_segments( ax, segments, **kwargs ): 24 | for start,end in segments: 25 | verts = [start,end,(0,0)] 26 | codes = [Path.MOVETO,Path.LINETO,Path.STOP] 27 | path = Path(verts, codes) 28 | patch = patches.PathPatch(path, facecolor='none', **kwargs ) 29 | ax.add_patch(patch) 30 | 31 | 32 | def scatter_segments( ax, segments, **kwargs ): 33 | xy = [ ((i[0],j[0]),(i[1],j[1])) for (i,j) in segments] 34 | x = [i[0] for i in xy] 35 | y = [i[1] for i in xy] 36 | ax.scatter( x,y, s=20, marker='o', **kwargs) 37 | 38 | 39 | def scatter_points( ax, points, **kwargs ): 40 | x = [i[0] for i in points] 41 | y = [i[1] for i in points] 42 | # ax.scatter( x,y, s=20, marker='o', **kwargs) 43 | ax.scatter( x,y, marker='o', **kwargs) 44 | 45 | 46 | if __name__=="__main__": 47 | 48 | parser = argparse.ArgumentParser() 49 | 50 | parser.add_argument('-s', "--segments", default=[None], action='store', type=str, nargs='*') 51 | 52 | args = parser.parse_args() 53 | 54 | fig = plot.figure() 55 | ax = fig.add_subplot(111) 56 | 57 | if args.segments != [None]: 58 | for filename in args.segments: 59 | seg = parse_segments(filename) 60 | scatter_segments( ax, seg, edgecolor="red" ) 61 | plot_segments( ax, seg, edgecolor="blue" ) 62 | 63 | plot.show() 64 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import math 4 | from geometry import x,y 5 | 6 | def LOG( *args ): 7 | """Print something on stderr and flush""" 8 | for msg in args: 9 | sys.stderr.write( str(msg) ) 10 | sys.stderr.write(" ") 11 | sys.stderr.flush() 12 | 13 | 14 | def LOGN( *args ): 15 | """Print something on stdeer, with a trailing new line, and flush""" 16 | LOG( *args ) 17 | LOG("\n") 18 | 19 | 20 | def load_points( stream ): 21 | points = [] 22 | for line in stream: 23 | if line.strip()[0] != "#": 24 | p = tuple([float(i) for i in line.split(",")]) 25 | assert(len(p)==2) 26 | points.append( p ) 27 | return points 28 | 29 | 30 | def write_points( points, stream ): 31 | for p in points: 32 | stream.write( "%f,%f\n" % ( x(p),y(p) ) ) 33 | 34 | 35 | def load_segments( stream ): 36 | segments = [] 37 | for line in stream: 38 | if line.strip()[0] != "#": 39 | seg = line.strip().split() 40 | assert(len(seg)==2) 41 | edge = [] 42 | for p in seg: 43 | point = tuple([float(i) for i in p.split(",")]) 44 | assert(len(point)==2) 45 | edge.append( point ) 46 | segments.append( edge ) 47 | return segments 48 | 49 | 50 | def write_segments( segments, stream ): 51 | for seg in segments: 52 | for p in seg: 53 | stream.write( "%f,%f " % ( x(p),y(p) ) ) 54 | stream.write( "\n" ) 55 | 56 | 57 | def load_matrix( stream ): 58 | matrix = {} 59 | for line in stream: 60 | if line.strip()[0] != "#": 61 | skey,svals = line.split(":") 62 | key = tuple((float(i) for i in skey.split(','))) 63 | col = {} 64 | for stri in svals.split(): 65 | sk,sv = stri.split("=") 66 | value = float(sv) 67 | k = tuple((float(i) for i in sk.split(","))) 68 | col[k] = value 69 | matrix[key] = col 70 | assert(len(matrix) == len(matrix[key])) 71 | return matrix 72 | 73 | 74 | def write_matrix( mat, stream): 75 | for row in mat: 76 | key = "%f,%f:" % row 77 | line = key 78 | for k in mat[row]: 79 | val = mat[row][k] 80 | line += "%f,%f=%f " % (k[0],k[1],val) 81 | stream.write( line + "\n" ) 82 | 83 | 84 | def vertices_of( segments ): 85 | vertices = set() 86 | for start,end in segments: 87 | vertices.add(start) 88 | vertices.add(end) 89 | return vertices 90 | 91 | 92 | def tour(lst): 93 | # consecutive pairs in lst + last-to-first element 94 | for a,b in zip(lst, lst[1:] + [lst[0]]): 95 | yield (a,b) 96 | 97 | 98 | -------------------------------------------------------------------------------- /voronoi.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/env python 2 | #encoding: utf-8 3 | 4 | from utils import tour,LOG,LOGN,x,y 5 | import triangulation 6 | import geometry 7 | import graph 8 | 9 | def nodes( triangles ): 10 | """Compute the locations of the centers of all the circumscribed circles of the given triangles""" 11 | for triangle in triangles: 12 | (cx,cy),r = triangulation.circumcircle(triangle) 13 | yield (cx,cy) 14 | 15 | 16 | def edge_in( edge, edges ): 17 | """Return True if the given edge (or its symetric) is in the given polygon.""" 18 | n,m = edge 19 | assert( len(n) == 2 ) 20 | assert( len(m) == 2 ) 21 | return (n,m) in edges or (m,n) in edges 22 | 23 | 24 | def edges_neighbours( candidate, polygons ): 25 | """Returns the set of candidates in polygons that have an edge in common with the given candidate.""" 26 | for polygon in polygons: 27 | if polygon == candidate: 28 | continue 29 | 30 | # Convert list of points to list of edges, because we want to find EDGE neighbours. 31 | edges_poly = list(tour(list(polygon))) 32 | assert( len(list(edges_poly)) > 0 ) 33 | edges_can = list(tour(list(candidate))) 34 | assert( len(list(edges_can)) > 0 ) 35 | 36 | # If at least one of the edge that are in availables polygons are also in the given candidate. 37 | # Beware the symetric edges. 38 | for edge in edges_poly: 39 | # We use yield within the loop, because we want to test all the candidate edges and 40 | # return all the matching ones. 41 | if edge_in( edge, edges_can ): 42 | yield polygon 43 | break 44 | 45 | def vertices_neighbours( candidate, polygons ): 46 | """Returns the set of candidates in polygon that have a vertex in common with the given candidate.""" 47 | for polygon in polygons: 48 | if polygon == candidate: 49 | continue 50 | 51 | for vertex in polygon: 52 | # We use yield within the loop, because we want to test all the candidate vertex and 53 | # return all the matching ones. 54 | if vertex in candidate: 55 | yield polygon 56 | break 57 | 58 | 59 | 60 | def dual( triangles, neighborhood = edges_neighbours ): 61 | """Compute the dual Voronoï graph of a triangulation.""" 62 | graph = {} 63 | 64 | def add_edge( current, neighbor ): 65 | if current in graph: 66 | if neighbor not in graph[current]: 67 | graph[current].append( neighbor ) 68 | else: 69 | graph[current] = [ neighbor ] 70 | 71 | for triangle in triangles: 72 | assert( len(triangle) == 3 ) 73 | assert( not geometry.collinear(*triangle) ) 74 | assert( triangulation.is_acute(triangle) ) 75 | # Consider the center of the circumcircle as the node for this triangle. 76 | current_node = triangulation.circumcircle(triangle)[0] 77 | assert( len(current_node) == 2 ) 78 | 79 | for neighbor_triangle in neighborhood( triangle, triangles ): 80 | assert( len(triangle) == 3 ) 81 | assert( not geometry.collinear(*neighbor_triangle) ) 82 | assert( triangulation.is_acute(neighbor_triangle) ) 83 | # Consider the neighbor's center as nodes. 84 | neighbor_node = triangulation.circumcircle(neighbor_triangle)[0] 85 | assert( len(neighbor_node) == 2 ) 86 | 87 | # Add edges between the current triangle's node and thoses of its neighbors. 88 | add_edge( current_node, neighbor_node ) 89 | add_edge( neighbor_node, current_node ) 90 | 91 | return graph 92 | 93 | 94 | def merge_nodes( graph, n0, n1, n2 ): 95 | """Merge n0 and n1 nodes as n2 within the given graph.""" 96 | 97 | # Assert that the old nodes are in the graph 98 | # and that they are linked by an edge. 99 | assert( n0 in graph ) 100 | assert( n1 in graph[n0] ) 101 | assert( n1 in graph ) 102 | assert( n0 in graph[n1] ) 103 | assert( n0 != n1 ) 104 | 105 | # Remove and save the neigbhours of the old nodes. 106 | n0_ngb = graph.pop(n0) 107 | n1_ngb = graph.pop(n1) 108 | # Insert the new node along with the old neighbours. 109 | # We use a set to ensure that there is no duplicated nodes. 110 | neighbours = list(set(n0_ngb + n1_ngb)) 111 | # Filter out duplicate of the considered nodes. 112 | # Because the new node cannot be linked to the old nodes, nor to itself. 113 | graph[n2] = filter( lambda n: n not in (n0,n1,n2), neighbours ) 114 | 115 | for node in graph: 116 | # Replace occurences of old nodes as neighbours by the new node. 117 | while n0 in graph[node]: 118 | graph[node].remove(n0) 119 | graph[node].append(n2) 120 | while n1 in graph[node]: 121 | graph[node].remove(n1) 122 | graph[node].append(n2) 123 | 124 | # assert that any neighbour is also a node of the graph. 125 | for node in graph: 126 | for neighbour in graph[node]: 127 | assert( neighbour in graph ) 128 | assert( neighbour != node ) 129 | 130 | return graph 131 | 132 | 133 | def merge_enclosed( graph, segments ): 134 | """Merge nodes of the given graph that are on edges that do not intersects with the given segments.""" 135 | i=0 136 | while i < len(graph.keys()): 137 | node = graph.keys()[i] 138 | j=0 139 | altered = False 140 | while j < len(graph[node]): 141 | neighbour = graph[node][j] 142 | assert( neighbour in graph ) 143 | edge = (node,neighbour) 144 | 145 | if not any( geometry.segment_intersection(edge,seg) for seg in segments ): 146 | graph = merge_nodes( graph, edge[0], edge[1], geometry.middle(*edge) ) 147 | altered = True 148 | LOG(".") 149 | break 150 | else: 151 | j+=1 152 | continue 153 | 154 | if altered: 155 | i = 0 156 | else: 157 | i+=1 158 | 159 | return graph 160 | 161 | 162 | if __name__ == "__main__": 163 | import sys 164 | import random 165 | import utils 166 | import uberplot 167 | import triangulation 168 | import matplotlib.pyplot as plot 169 | 170 | if len(sys.argv) > 1: 171 | scale = 100 172 | nb = int(sys.argv[1]) 173 | points = [ (scale*random.random(),scale*random.random()) for i in range(nb)] 174 | else: 175 | points = [ 176 | (0,40), 177 | (100,60), 178 | (40,0), 179 | (50,100), 180 | (90,10), 181 | # (50,50), 182 | ] 183 | 184 | fig = plot.figure() 185 | 186 | triangles = triangulation.delaunay_bowyer_watson( points ) 187 | delaunay_edges = triangulation.edges_of( triangles ) 188 | 189 | voronoi_graph = dual( triangles ) 190 | voronoi_edges = graph.edges_of( voronoi_graph ) 191 | print voronoi_edges 192 | 193 | ax = fig.add_subplot(111) 194 | ax.set_aspect('equal') 195 | uberplot.scatter_segments( ax, delaunay_edges, facecolor = "blue" ) 196 | uberplot.plot_segments( ax, delaunay_edges, edgecolor = "blue" ) 197 | uberplot.scatter_segments( ax, voronoi_edges, facecolor = "red" ) 198 | uberplot.plot_segments( ax, voronoi_edges, edgecolor = "red" ) 199 | plot.show() 200 | 201 | --------------------------------------------------------------------------------