diff --git a/2020/sketch_2020_08_18a/arcs.py b/2020/sketch_2020_08_18a/arcs.py new file mode 100644 index 00000000..09dbf57b --- /dev/null +++ b/2020/sketch_2020_08_18a/arcs.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +ROTATION = {0: 0, + BOTTOM: 0, + DOWN: 0, + 1: HALF_PI, + LEFT: HALF_PI, + 2: PI, + TOP: PI, + UP: PI, + 3: PI + HALF_PI, + RIGHT: PI + HALF_PI, + BOTTOM + RIGHT: 0, + DOWN + RIGHT: 0, + DOWN + LEFT: HALF_PI, + BOTTOM + LEFT: HALF_PI, + TOP + LEFT: PI, + UP + LEFT: PI, + TOP + RIGHT: PI + HALF_PI, + UP + RIGHT: PI + HALF_PI, + } + +def quarter_circle(x, y, radius, quadrant): + circle_arc(x, y, radius, ROTATION[quadrant], HALF_PI) + +def half_circle(x, y, radius, quadrant): + circle_arc(x, y, radius, ROTATION[quadrant], PI) + +def circle_arc(x, y, radius, start_ang, sweep_ang): + arc(x, y, radius * 2, radius * 2, start_ang, start_ang + sweep_ang) + +def poly_arc(x, y, radius, start_ang, sweep_ang, num_points=2): + angle = sweep_ang / int(num_points) + a = start_ang + with beginShape(): + while a <= start_ang + sweep_ang: + sx = x + cos(a) * radius + sy = y + sin(a) * radius + vertex(sx, sy) + a += angle + +def arc_poly(x, y, d, _, start_ang, end_ang, num_points=5): + sweep_ang = end_ang - start_ang + angle = sweep_ang / int(num_points) + a = start_ang + with beginShape(): + while a <= end_ang: + sx = x + cos(a) * d / 2 + sy = y + sin(a) * d / 2 + vertex(sx, sy) + a += angle + +def bar(x1, y1, x2, y2, thickness=None, shorter=0, ends=(1, 1)): + """ + O código para fazer as barras, dois pares (x, y), + um parâmetro de encurtamento: shorter + """ + L = dist(x1, y1, x2, y2) + if not thickness: + thickness = 10 + with pushMatrix(): + translate(x1, y1) + angle = atan2(x1 - x2, y2 - y1) + rotate(angle) + offset = shorter / 2 + line(thickness / 2, offset, thickness / 2, L - offset) + line(-thickness / 2, offset, -thickness / 2, L - offset) + if ends[0]: + half_circle(0, offset, thickness / 2, UP) + if ends[1]: + half_circle(0, L - offset, thickness / 2, DOWN) + +def var_bar(p1x, p1y, p2x, p2y, r1, r2=None): + """ + Tangent/tangent shape on 2 circles of arbitrary radius + """ + if r2 is None: + r2 = r1 + #line(p1x, p1y, p2x, p2y) + d = dist(p1x, p1y, p2x, p2y) + ri = r1 - r2 + if d > abs(ri): + rid = (r1 - r2) / d + if rid > 1: + rid = 1 + if rid < -1: + rid = -1 + beta = asin(rid) + HALF_PI + with pushMatrix(): + translate(p1x, p1y) + angle = atan2(p1x - p2x, p2y - p1y) + rotate(angle + HALF_PI) + x1 = cos(beta) * r1 + y1 = sin(beta) * r1 + x2 = cos(beta) * r2 + y2 = sin(beta) * r2 + #print((d, beta, ri, x1, y1, x2, y2)) + beginShape() + b_arc(0, 0, r1 * 2, r1 * 2, + -beta - PI, beta - PI, mode=2) + b_arc(d, 0, r2 * 2, r2 * 2, + beta - PI, PI - beta, mode=2) + endShape(CLOSE) + else: + ellipse(p1x, p1y, r1 * 2, r1 * 2) + ellipse(p2x, p2y, r2 * 2, r2 * 2) + +def b_arc(cx, cy, w, h, start_angle, end_angle, mode=0): + """ + A bezier approximation of an arc + using the same signature as the original Processing arc() + mode: 0 "normal" arc, using beginShape() and endShape() + 1 "middle" used in recursive call of smaller arcs + 2 "naked" like normal, but without beginShape() and endShape() + for use inside a larger PShape + """ + theta = end_angle - start_angle + # Compute raw Bezier coordinates. + if mode != 1 or abs(theta) < HALF_PI: + x0 = cos(theta / 2.0) + y0 = sin(theta / 2.0) + x3 = x0 + y3 = 0 - y0 + x1 = (4.0 - x0) / 3.0 + if y0 != 0: + y1 = ((1.0 - x0) * (3.0 - x0)) / (3.0 * y0) # y0 != 0... + else: + y1 = 0 + x2 = x1 + y2 = 0 - y1 + # Compute rotationally-offset Bezier coordinates, using: + # x' = cos(angle) * x - sin(angle) * y + # y' = sin(angle) * x + cos(angle) * y + bezAng = start_angle + theta / 2.0 + cBezAng = cos(bezAng) + sBezAng = sin(bezAng) + rx0 = cBezAng * x0 - sBezAng * y0 + ry0 = sBezAng * x0 + cBezAng * y0 + rx1 = cBezAng * x1 - sBezAng * y1 + ry1 = sBezAng * x1 + cBezAng * y1 + rx2 = cBezAng * x2 - sBezAng * y2 + ry2 = sBezAng * x2 + cBezAng * y2 + rx3 = cBezAng * x3 - sBezAng * y3 + ry3 = sBezAng * x3 + cBezAng * y3 + # Compute scaled and translated Bezier coordinates. + rx, ry = w / 2.0, h / 2.0 + px0 = cx + rx * rx0 + py0 = cy + ry * ry0 + px1 = cx + rx * rx1 + py1 = cy + ry * ry1 + px2 = cx + rx * rx2 + py2 = cy + ry * ry2 + px3 = cx + rx * rx3 + py3 = cy + ry * ry3 + # Debug points... comment this out! + # stroke(0) + # ellipse(px3, py3, 15, 15) + # ellipse(px0, py0, 5, 5) + # Drawing + if mode == 0: # 'normal' arc (not 'middle' nor 'naked') + beginShape() + if mode != 1: # if not 'middle' + vertex(px3, py3) + if abs(theta) < HALF_PI: + bezierVertex(px2, py2, px1, py1, px0, py0) + else: + # to avoid distortion, break into 2 smaller arcs + b_arc(cx, cy, w, h, start_angle, end_angle - theta / 2.0, mode=1) + b_arc(cx, cy, w, h, start_angle + theta / 2.0, end_angle, mode=1) + if mode == 0: # end of a 'normal' arc + endShape() diff --git a/2020/sketch_2020_08_18a/graph.py b/2020/sketch_2020_08_18a/graph.py new file mode 100644 index 00000000..850b005c --- /dev/null +++ b/2020/sketch_2020_08_18a/graph.py @@ -0,0 +1,338 @@ +#*- coding: utf-8 -*- + +""" +A simple Python graph class, demonstrating the essential facts and functionalities of graphs +based on https://www.python-course.eu/graphs_python.php and https://www.python.org/doc/essays/graphs/ +""" + +from random import choice + +class Graph(object): + + def __init__(self, graph_dict=None): + """ + Initialize a graph object with dictionary provided, + if none provided, create an empty one. + """ + if graph_dict is None: + graph_dict = {} + self.__graph_dict = graph_dict + + def __len__(self): + return len(self.__graph_dict) + + def vertices(self): + """Return the vertices of graph.""" + return list(self.__graph_dict.keys()) + + def edges(self): + """Return the edges of graph """ + return self.__generate_edges() + + def add_vertex(self, vertex): + """ + If the vertex "vertex" is not in self.__graph_dict, + add key "vertex" with an empty list as a value, + otherwise, do nothing. + """ + if vertex not in self.__graph_dict: + self.__graph_dict[vertex] = [] + + def add_edge(self, edge): + """ + Assuming that edge is of type set, tuple or list; + add edge between vertices. Can add multiple edges! + """ + edge = set(edge) + vertex1 = edge.pop() + if edge: + # not a loop + vertex2 = edge.pop() + if vertex1 in self.__graph_dict: + self.__graph_dict[vertex1].append(vertex2) + else: + self.__graph_dict[vertex1] = [vertex2] + if vertex2 in self.__graph_dict: + self.__graph_dict[vertex2].append(vertex1) + else: + self.__graph_dict[vertex2] = [vertex1] + else: + # a loop + if vertex1 in self.__graph_dict: + self.__graph_dict[vertex1].append(vertex1) + else: + self.__graph_dict[vertex1] = [vertex1] + + def remove_vertex(self, vert): + del self.__graph_dict[vert] + for k in self.__graph_dict.keys(): + if vert in self.__graph_dict[k]: + self.__graph_dict[k].remove(vert) + + def remove_edge(self, edge): + edge = set(edge) + vertex1 = edge.pop() + if edge: + vertex2 = edge.pop() + self.__graph_dict[vertex1].remove(vertex2) + self.__graph_dict[vertex2].remove(vertex1) + else: + self.__graph_dict[vertex1].remove(vertex1) + + def __generate_edges(self): + """ + Generate the edges, represented as sets with one + (a loop back to the vertex) or two vertices. + """ + edges = [] + for vertex in self.__graph_dict: + for neighbour in self.__graph_dict[vertex]: + if {neighbour, vertex} not in edges: + edges.append({vertex, neighbour}) + return edges + + def __str__(self): + res = "vertices: " + for k in self.__graph_dict: + res += str(k) + " " + res += "\nedges: " + for edge in self.__generate_edges(): + res += str(edge) + " " + return res + + def find_isolated_vertices(self): + """ + Return a list of isolated vertices. + """ + graph = self.__graph_dict + isolated = [] + for vertex in graph: + print(isolated, vertex) + if not graph[vertex]: + isolated += [vertex] + return isolated + + def find_path(self, start_vertex, end_vertex, path=[]): + """ + Find a path from start_vertex to end_vertex in graph. + """ + graph = self.__graph_dict + path = path + [start_vertex] + if start_vertex == end_vertex: + return path + if start_vertex not in graph: + return None + for vertex in graph[start_vertex]: + if vertex not in path: + extended_path = self.find_path(vertex, + end_vertex, + path) + if extended_path: + return extended_path + return None + + def find_all_paths(self, start_vertex, end_vertex, path=[]): + """ + Find all paths from start_vertex to end_vertex. + """ + graph = self.__graph_dict + path = path + [start_vertex] + if start_vertex == end_vertex: + return [path] + if start_vertex not in graph: + return [] + paths = [] + for vertex in graph[start_vertex]: + if vertex not in path: + extended_paths = self.find_all_paths(vertex, + end_vertex, + path) + for p in extended_paths: + paths.append(p) + return paths + + def is_connected(self, + vertices_encountered=None, + start_vertex=None): + """Find if the graph is connected.""" + if vertices_encountered is None: + vertices_encountered = set() + gdict = self.__graph_dict + vertices = list(gdict.keys()) # "list" necessary in Python 3 + if not start_vertex: + # chosse a vertex from graph as a starting point + start_vertex = vertices[0] + vertices_encountered.add(start_vertex) + if len(vertices_encountered) != len(vertices): + for vertex in gdict[start_vertex]: + if vertex not in vertices_encountered: + if self.is_connected(vertices_encountered, vertex): + return True + else: + return True + return False + + def vertex_degree(self, vertex): + """ + Return the number of edges connecting to a vertex (the number of adjacent vertices). + Loops are counted double, i.e. every occurence of vertex in the list of adjacent vertices. + """ + adj_vertices = self.__graph_dict[vertex] + degree = len(adj_vertices) + adj_vertices.count(vertex) + return degree + + def degree_sequence(self): + """Calculates the degree sequence.""" + seq = [] + for vertex in self.__graph_dict: + seq.append(self.vertex_degree(vertex)) + seq.sort(reverse=True) + return tuple(seq) + + @staticmethod + def is_degree_sequence(sequence): + """ + Return True, if the sequence is a degree sequence (non-increasing), + otherwise return False. + """ + return all(x >= y for x, y in zip(sequence, sequence[1:])) + + def delta(self): + """Find minimum degree of vertices.""" + min = 100000000 + for vertex in self.__graph_dict: + vertex_degree = self.vertex_degree(vertex) + if vertex_degree < min: + min = vertex_degree + return min + + def Delta(self): + """Finde maximum degree of vertices.""" + max = 0 + for vertex in self.__graph_dict: + vertex_degree = self.vertex_degree(vertex) + if vertex_degree > max: + max = vertex_degree + return max + + def density(self): + """Calculate the graph density.""" + g = self.__graph_dict + V = len(g.keys()) + E = len(self.edges()) + return 2.0 * E / (V * (V - 1)) + + def diameter(self): + """Calculates the graph diameter.""" + + v = self.vertices() + pairs = [ + (v[i], + v[j]) for i in range( + len(v)) for j in range( + i + 1, + len(v) - 1)] + smallest_paths = [] + for (s, e) in pairs: + paths = self.find_all_paths(s, e) + smallest = sorted(paths, key=len)[0] + smallest_paths.append(smallest) + + smallest_paths.sort(key=len) + + # longest path is at the end of list, + # i.e. diameter corresponds to the length of this path + diameter = len(smallest_paths[-1]) - 1 + return diameter + + @staticmethod + def erdoes_gallai(dsequence): + """ + Check if Erdoes-Gallai inequality condition is fullfilled. + """ + if sum(dsequence) % 2: + # sum of sequence is odd + return False + if Graph.is_degree_sequence(dsequence): + for k in range(1, len(dsequence) + 1): + left = sum(dsequence[:k]) + right = k * (k - 1) + sum([min(x, k) for x in dsequence[k:]]) + if left > right: + return False + else: + # sequence is increasing + return False + return True + + # Code by Eryk Kopczyński + def find_shortest_path(self, start, end): + from collections import deque + graph = self.__graph_dict + dist = {start: [start]} + q = deque((start,)) + while len(q): + at = q.popleft() + for next in graph[at]: + if next not in dist: + # dist[next] = [dist[at], next] + # less efficient but nicer output + dist[next] = dist[at] + [next] + q.append(next) + return dist.get(end) + + ############## + def is_cyclic(self): + """ + Returns true if the graph contains a cycle, else false. + """ + # Mark all the vertices as not visited + visited = [False] * len(self) + # Call helper function to detect cycle in different DFS trees + for i, v in enumerate(visited): + # Don't recur for u if it is already visited + if v == False: + if self.dfs(i, visited, -1): + return True + return False + + def dfs(self, v, visited, parent): + # Mark the current node as visited + visited[v] = True + # Recur for all the vertices adjacent to this vertex + for i in self.__graph_dict[v]: + # If the node is not visited then recurse on it + if visited[i] == False: + if(self.dfs(i, visited, v)): + return True + # If an adjacent vertex is visited and not parent of current + # vertex, then there is a cycle + elif parent != i: + return True + return False + + ############## + + def get_random_vertex(self): + return choice(self.vertices()) + + @staticmethod + def random_graph(names, + connect_rate=.9, + allow_loops=True, + allow_cyclic=False, + connected=False): + vertices = set(names) + while True: + graph = Graph() + for v in vertices: + graph.add_vertex(v) + if random(1) < connect_rate: + if allow_loops: + names = list(vertices) + else: + names = list(vertices - set((v,))) + graph.add_edge({v, choice(names)}) + if not connected or graph.is_connected(): + if allow_cyclic or not graph.is_cyclic(): + break + return graph diff --git a/2020/sketch_2020_08_18a/grid.py b/2020/sketch_2020_08_18a/grid.py new file mode 100644 index 00000000..691b5699 --- /dev/null +++ b/2020/sketch_2020_08_18a/grid.py @@ -0,0 +1,115 @@ +#*- coding: utf-8 -*- + +from __future__ import division, print_function +from random import sample, choice + + +class Grid(): + + def __init__(self, graph, width, height, margin=None, mode=0): + """ + mode 0: heavy center + mode 1: heavy perifery + """ + margin = margin or width / 40 + cols, rows = dim_grid(len(graph)) + w, h = (width - margin * 2) / cols, (height - margin * 2) / rows + points = [] + for i in range(cols * rows): + c = i % cols + r = i // rows + x = margin + w * 0.5 + c * w # - 14 * (r % 2) + 7 + y = margin + h * 0.5 + r * h # - 14 * (c % 2) + 7 + z = 0 + points.append([x, y, z]) + points = sorted( + points, key=lambda p: dist(p[0], p[1], width / 2, height / 2)) + if mode == 0: + v_list = reversed( + sorted(graph.vertices(), key=graph.vertex_degree)) + elif mode == 1: + v_list = sorted(graph.vertices(), key=graph.vertex_degree) + else: + v_list = graph.vertices() + print("random mode") + + Grid.w, Grid.h = w, h + self.graph = graph + self.grid = {v: p for v, p in zip(v_list, points)} + recalculate_sizes_from_v_deg(self.graph, self) + + def __getitem__(self, k): + return self.grid[k] + + def __setitem__(self, k, v): + self.grid[k] = v + + def __len__(self): + return len(self.grid) + + def keys(self): + return self.grid.keys() + + def values(self): + return self.grid.values() + + def __iter__(self): + return iter(self.grid) + + def swap(self, num=2): + grid = self.grid + graph = self.graph + fail = 0 + n = m = edge_distances(graph, grid) + while m <= n and fail < len(graph) ** 2: + new_grid = dict(grid) + if num == 2: + a, b = sample(graph.vertices(), 2) + new_grid[a], new_grid[b] = new_grid[b], new_grid[a] + else: + ks = sample(graph.vertices(), num) + vs = [grid[k] for k in sample(ks, num)] + for k, v in zip(ks, vs): + new_grid[k] = v + n = edge_distances(graph, new_grid) + if m > n: + t = "{:.2%} at: {} tries of {}v shuffle/swap" \ + .format((n - m) / m, fail + 1, num) + print("\n" + t, end="") + recalculate_sizes_from_v_deg(graph, new_grid) + self.grid = new_grid + else: + fail += 1 + print(".", end='') + + + +def dim_grid(n): + a = int(sqrt(n)) + b = n // a + if a * b < n: + b += 1 + print(u'{}: {} × {} ({})'.format(n, a, b, a * b)) + return a, b + +def edge_distances(graph, grid): + total = 0 + for edge in graph.edges(): + if len(edge) == 2: + a, b = edge + d = PVector.dist(PVector(*grid[a]), + PVector(*grid[b])) + total += d + return total + + +def recalculate_sizes_from_v_deg(graph, grid): + u = Grid.w / 10 + for k in grid.keys(): + grid[k][2] = u * graph.vertex_degree(k) + return u + +def v_dist(a, b): + xa, ya, _ = a + xb, yb, _ = b + return dist(xa, ya, xb, yb) diff --git a/2020/sketch_2020_08_18a/sketch_2020_08_18a.png b/2020/sketch_2020_08_18a/sketch_2020_08_18a.png new file mode 100644 index 00000000..70bc90a8 Binary files /dev/null and b/2020/sketch_2020_08_18a/sketch_2020_08_18a.png differ diff --git a/2020/sketch_2020_08_18a/sketch_2020_08_18a.pyde b/2020/sketch_2020_08_18a/sketch_2020_08_18a.pyde new file mode 100644 index 00000000..d83ad0bf --- /dev/null +++ b/2020/sketch_2020_08_18a/sketch_2020_08_18a.pyde @@ -0,0 +1,134 @@ +from __future__ import print_function, division +from random import choice +from graph import Graph +from grid import * # setup_grid, grid_swap, edge_distances +from arcs import var_bar + +thread_running = False +gx, gy = 0, 100 +viz_stat = False +selected_v = None + +def setup(): + size(500, 500) + colorMode(HSB) + textAlign(CENTER, CENTER) + f = createFont("Source Code Pro Bold", 12) + textFont(f) + setup_graph() + +def setup_graph(mode=0): + # create a random graph and a dict of grid postions for its vertices + global graph, grid, m, d + graph = Graph.random_graph(range(64), + connect_rate=.95, + allow_loops=False, + connected=True, + allow_cyclic=False) + grid = Grid(graph, + width=width, + height=width, + margin=10, + mode=mode) + d = recalculate_sizes_from_v_deg(graph, grid) + m = edge_distances(graph, grid) # "metric", sum of edge distances + print("Cyclic: " + str(graph.is_cyclic())) + +def draw(): + background(200) + for e in graph.edges(): + va = e.pop() + xa, ya, za = grid[va] + if len(e) == 1: + vb = e.pop() + xb, yb, zb = grid[vb] + fill(240) + degree = ((za + zb) / 2) / d + strokeWeight(map(degree, 1.5, 5, 5, 1)) + var_bar(xa, ya, xb, yb, za, zb) + + for v in grid.keys(): + x, y, z = grid[v] + if v == selected_v: + fill(0) + elif mouse_near(v): + fill(128) + else: + noFill() + strokeWeight(1) + circle(x, y, 10) + if viz_stat: + fill(255) + text("{}:{}".format(v, graph.vertex_degree(v)).upper(), + x - 15, y - 5) + fill(100) + text(int(m / 200), 20, 20) + + if selected_v: + x, y, _ = grid[selected_v] + stroke(0) + strokeWeight(5) + line(x, y, mouseX, mouseY) + + +def keyTyped(): + global gx, gy, viz_stat + if key == 'r': + setup_graph(mode=0) + gx, gy = 0, 100 + if key == 'e': + setup_graph(mode=1) + gx, gy = 0, 100 + if key == 'R': + setup_graph(mode=2) + gx, gy = 0, 100 + elif key == 'v': + viz_stat = not viz_stat + else: + if not thread_running: + thread("swapping") + +def swapping(): + global grid, thread_running, gx, gy, m + if str(key) not in 'sc23456789': + return + thread_running = True + m = edge_distances(graph, grid) + len_graph = len(graph) + multiple = 1 if mousePressed else 100 + for _ in range(multiple): + if key == 's': + grid.swap(num=len_graph) + if key in '234556789': + grid.swap(num=int(key)) + n = edge_distances(graph, grid) + gx += 1 + if n < m: + gy -= gy * (m - n) / m + m = n + thread_running = False + +def mouse_near(v): + x, y, _ = grid[v] + return dist(x, y, mouseX, mouseY) < 12 + +def mousePressed(): + global selected_v + for v in grid.keys(): + if mouse_near(v): + selected_v = v + return + +def mouseReleased(): + global selected_v, u + if selected_v: + for v in grid.keys(): + if mouse_near(v): + grid[selected_v], grid[v] = grid[v], grid[selected_v] + u = recalculate_sizes_from_v_deg(graph, grid) + selected_v = None + + +# TODO IDEAS: +# Find distance outliers and try to shuffle them closer +# Run many times without visualisation on Python 3 to get some huge samples diff --git a/README.md b/README.md index 4419999a..61f04e24 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ --- +![sketch_2020_08_18a](2020/sketch_2020_08_18a/sketch_2020_08_18a.png) + +[sketch_2020_08_18a](https://github.com/villares/sketch-a-day/tree/master/2020/sketch_2020_08_18a) [[Py.Processing](https://villares.github.io/como-instalar-o-processing-modo-python/index-EN)] + +--- + ![sketch_2020_08_17a](2020/sketch_2020_08_17a/sketch_2020_08_17a.png) [sketch_2020_08_17a](https://github.com/villares/sketch-a-day/tree/master/2020/sketch_2020_08_17a) [[Py.Processing](https://villares.github.io/como-instalar-o-processing-modo-python/index-EN)] diff --git a/admin_scripts/s.py b/admin_scripts/s.py new file mode 100644 index 00000000..42886e34 --- /dev/null +++ b/admin_scripts/s.py @@ -0,0 +1,10 @@ +palavra = "Heineken" +palavra_invertida = "" # começa vazia +for letra in palavra: + # soma letra da palavra uma por uma + # formando string invertido + palavra_invertida = letra + palavra_invertida + +print(palavra_invertida) + +print(reversed(palavra)) \ No newline at end of file