diff --git a/2020/sketch_2020_08_13a/arcs.py b/2020/sketch_2020_08_13a/arcs.py new file mode 100644 index 00000000..68ef4f26 --- /dev/null +++ b/2020/sketch_2020_08_13a/arcs.py @@ -0,0 +1,106 @@ +# -*- 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): + if r2 is None: + r2 = r1 + d = dist(p1x, p1y, p2x, p2y) + if d > 0: + with pushMatrix(): + translate(p1x, p1y) + angle = atan2(p1x - p2x, p2y - p1y) + rotate(angle + HALF_PI) + ri = r1 - r2 + beta = asin(ri / d) + HALF_PI + x1 = cos(beta) * r1 + y1 = sin(beta) * r1 + x2 = cos(beta) * r2 + y2 = sin(beta) * r2 + with pushStyle(): + noStroke() + beginShape() + vertex(-x1, -y1) + vertex(d - x2, -y2) + vertex(d, 0) + vertex(d - x2, +y2) + vertex(-x1, +y1) + vertex(0, 0) + endShape(CLOSE) + line(-x1, -y1, d - x2, -y2) + line(-x1, +y1, d - x2, +y2) + arc(0, 0, r1 * 2, r1 * 2, + -beta - PI, beta - PI) + arc(d, 0, r2 * 2, r2 * 2, + beta - PI, PI - beta) + else: + ellipse(p1x, p1y, r1 * 2, r1 * 2) + ellipse(p2y, p2x, r2 * 2, r2 * 2) diff --git a/2020/sketch_2020_08_13a/graph.py b/2020/sketch_2020_08_13a/graph.py new file mode 100644 index 00000000..412493fa --- /dev/null +++ b/2020/sketch_2020_08_13a/graph.py @@ -0,0 +1,302 @@ +#*- 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] + dist[next] = dist[at]+[next] # less efficient but nicer output + q.append(next) + return dist.get(end) + + + def get_random_vertex(self): + return choice(self.vertices()) + + @staticmethod + def random_graph(names, connect_rate=.9, allow_loops=True, 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(): + break + return graph diff --git a/2020/sketch_2020_08_13a/grid.py b/2020/sketch_2020_08_13a/grid.py new file mode 100644 index 00000000..e08d33aa --- /dev/null +++ b/2020/sketch_2020_08_13a/grid.py @@ -0,0 +1,77 @@ +#*- coding: utf-8 -*- + +from __future__ import division, print_function +from random import sample, choice + + +def setup_grid(graph, width, height, margin=None): + global w, h + 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)) + v_list = reversed(sorted(graph.vertices(), key=graph.vertex_degree)) + # v_list = sorted(graph.vertices(), key=graph.vertex_degree) + grid = {v: p for v, p in zip(v_list, points)} + for k in grid.keys(): + grid[k][2] = (w / 10) * graph.vertex_degree(k) + return grid + +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 grid_swap(graph, grid, display_text, num=2): + 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) + display_text.append(t) + print("\n" + t, end="") + for k in new_grid.keys(): + new_grid[k][2] = (w / 10) * graph.vertex_degree(k) + return new_grid + else: + fail += 1 + print(".", end='') + return grid + +def v_dist(a, b): + xa, ya, _ = a + xb, yb, _ = b + return dist(xa, ya, xb, yb) diff --git a/2020/sketch_2020_08_13a/sketch_2020_08_13a.pyde b/2020/sketch_2020_08_13a/sketch_2020_08_13a.pyde new file mode 100644 index 00000000..d06616be --- /dev/null +++ b/2020/sketch_2020_08_13a/sketch_2020_08_13a.pyde @@ -0,0 +1,164 @@ +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_count = 0 +gx, gy = 0, 100 +viz_stat = False + +def setup(): + size(400, 400) + colorMode(HSB) + textAlign(CENTER, CENTER) + f = createFont("Source Code Pro Bold", 12) + textFont(f) + setup_graph() + +def setup_graph(): + # create a random graph and a dict of grid postions for its vertices + global graph, grid, m, d, display_text + graph = Graph.random_graph(range(49), allow_loops=False, connected=True) + grid = setup_grid(graph, width=width, height=width, margin=10) + # display setup + display_text = [""] + m = edge_distances(graph, grid) # "metric", sum of edge distances + d = createGraphics(width, 100) # canvas for data display + d.beginDraw() + d.background(150) + d.endDraw() + print(graph) + # setup walker + global sel_v, path_walker, t_walker + sel_v = graph.get_random_vertex() + path_walker = [] + t_walker = 0 + +def draw(): + background(200) + noFill() + 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] + noStroke() + fill(((za + zb) / 2) * 12, 255, 255, 128) + var_bar(xa, ya, xb, yb, za, zb) + + for v in grid.keys(): + x, y, z = grid[v] + fill(64) + circle(x, y, 10) + if keyPressed: + fill(0) + text("{}".format(v).upper(), x - 15, y - 5) + + walker() + this.surface.setResizable(False) + if viz_stat: + image(d, 0, height - 100) + fill(0) + textAlign(LEFT) + text(format(gy / 100, ".2%"), width - 100, height - 80) + text(format(m, ".0f"), width - 100, height - 60) + fill(255) + text('\n'.join(display_text[-2:]), 20, height - 40) + +def display(): + d.beginDraw() + d.stroke(0) + d.strokeWeight(1) + d.line(gx, 100, gx, 100 - gy) + d.noStroke() + d.endDraw() + +def walker(): + global t_walker, path_walker, sel_v + if path_walker and t_walker < 1: + path_vectors = [PVector(*grid[pv]) for pv in path_walker] + p = lerpVectors(t_walker, path_vectors) + noFill() + stroke(255) + circle(p.x, p.y, p.z) + t_walker += .03 / len(path_walker) + else: + path_walker = [] + noStroke() + fill(255) + x, y, z = grid[sel_v] + circle(x, y, 10) + +def lerpVectors(amt, vecs): + """ from Jeremy Douglass """ + amt = constrain(amt, 0, 1) # let's play safe + if len(vecs) == 1: + return vecs[0] + cunit = 1.0 / (len(vecs) - 1) + return PVector.lerp(vecs[floor(amt / cunit)], + vecs[ceil(amt / cunit)], + amt % cunit / cunit) + +def keyTyped(): + global gx, gy, viz_stat + if key == 'r': + setup_graph() + background(200) + gx, gy = 0, 100 + elif key == 'v': + viz_stat = not viz_stat + this.surface.setResizable(True) + if viz_stat: + this.surface.setSize(400, 500) + else: + this.surface.setSize(400, 400) + + else: + thread("swapping") + +def swapping(): + global grid, thread_count, gx, gy, m, t + if str(key) not in 'sc23456789': + return + thread_count += 1 + this_thread, this_key = thread_count, str(key) + m = edge_distances(graph, grid) + t = "Starting thread:{} key:{}".format(this_thread, key) + display_text.append(t) + print("\n" + t, end="") + len_graph = len(graph) + for _ in range(len_graph): + if this_key == 's': + grid = grid_swap(graph, grid, display_text, num=len_graph) + if this_key in '234556789': + grid = grid_swap(graph, grid, display_text, num=int(this_key)) + n = edge_distances(graph, grid) + gx += 1 + if n < m: + gy -= gy * (m - n) / m + m = n + display() + if key == 'k': + break + t = "Ending thread: {}".format(this_thread) + display_text.append(t) + print("\n" + t, end="") + + +def mousePressed(): + global path_walker, t_walker, sel_v + for v in graph.vertices(): + x, y, _ = grid[v] + if v != sel_v and dist(x, y, mouseX, mouseY) < 10: + path = graph.find_shortest_path(sel_v, v) + if path: + path_walker = path + t_walker = 0 + sel_v = v + +# TODO IDEAS: +# Show what "nearby" sample means +# Find distance outliers and try to shuffle them closer +# Run many times without visualisation on Python 3 to get some huge samples