diff --git a/posts/pentagons/1/triangles.py b/posts/pentagons/1/triangles.py index 78cbbbc..31f40ca 100644 --- a/posts/pentagons/1/triangles.py +++ b/posts/pentagons/1/triangles.py @@ -1,389 +1,367 @@ +import itertools +from dataclasses import dataclass +from typing import Iterable + import numpy as np +from numpy.typing import NDArray import matplotlib.pyplot as plt root3 = 0.5 - (3**0.5) / 2j -triangle_coords = lambda i: (i[0] + i[1] * root3.real, i[1] * root3.imag) +embed_triangular = lambda i: (i[0] + i[1] * root3.real, i[1] * root3.imag) -def hex_positions(n): +def plot_triangular(plottype="plot"): + """ + Plot an iterable of coordinates in a triangular grid using the member `plottype` of `pyplot`. + Coordinates are in the form `(x, y)`. + Extra arguments are passed to the plotting method. + """ + + def ret(coords: Iterable[tuple[int, int] | NDArray], *args, **kwargs): + getattr(plt, plottype)( + *list(zip(*map(embed_triangular, coords))), *args, **kwargs + ) + + return ret + + +def hex_positions(n: int, offset: int = 0) -> list[tuple[int, int]]: + """ + List of coordinates which, in a triangular lattice, form the vertices of hexagons. + + The points (1, 1), (3, 0), and (0, 3) are all at the center of hexagons, + so linear combinations of these are NOT part of the grid. + """ coordinates = [ - (i, j) for i in range(n) for j in range(n) if i + j < n and (i - j) % 3 + (i + offset, j + offset) + for i in range(n) + for j in range(n) + if i + j < n and (i - j) % 3 != 0 ] - drawing = [(i + j * root3.real, j * root3.imag) for (i, j) in coordinates] - return list(zip(*drawing)) + return coordinates -def hex_lines(n): +def draw_hex_lines(n: int, offset: int = 0): # vertices of hexagons in rightmost third of the the upper half plane - coordinates = [ - (i, j) for i in range(n) for j in range(n) if i + j < n and (i - j) % 3 - ] + hex_coordinates = hex_positions(n, offset) # find connections between a vertex and its (up to 3) neighbors lines = [ - filter( - lambda x: x[1] in coordinates, - [[(i, j), (i + 1, j)], [(i, j), (i, j + 1)], [(i, j), (i + 1, j - 1)]], + list( + itertools.chain.from_iterable( + zip( + # the point being considered + itertools.repeat((i, j)), + # its neighbors which exist in the grid + filter( + lambda x: x in hex_coordinates, + [ + (i + 1, j), + (i, j + 1), + (i + 1, j - 1), + ], + ), + ) + ) ) - for (i, j) in coordinates + for i, j in hex_coordinates ] for line in lines: - for unfiltered in line: - plt.plot(*list(zip(*map(triangle_coords, unfiltered))), "ko-") - # flat_drawing = [map(triangle_coords, unzip) for line in lines for unzip in line] - # return list(zip(*flat_drawing)) - # for i in flat_drawing: - # plt.plot(*list(zip(*i)), "k") + # draw lines between connected points + plot_triangular("plot")(line, "ko-") -def triangular_grid(n, show_hexes=False): - coordinates = [(i, j) for i in range(n) for j in range(n) if i + j < n] - drawing = [(i + j * root3.real, j * root3.imag) for (i, j) in coordinates] - # plt.plot(*list(zip(*drawing)), "o") - plt.triplot(*list(zip(*drawing)), "-", color="0.7", lw=0.75) +def draw_triangular_grid(n: int, offset: int = 0, show_hexes: bool = False): + plot_triangular("triplot")( + [(i + offset, j + offset) for i in range(n) for j in range(n) if i + j < n], + "-", + color="0.7", + lw=0.75, + ) if show_hexes: - # plt.plot(*hex_positions(n), "ok") - hex_lines(n) + draw_hex_lines(n, offset=offset) plt.gca().axis("off") - plt.xlim((-2, (1920 / 1080) * plt.ylim()[1])) -""" -def color_at_hex_coordinates(coord): - draw = None - if min(coord) == 0: #class 1 - #(0,1) = (1,0) => (2,1), (1,2), (2, 2) - #(0,2) = (2,0) => (3,2), (2,3), (3, 3) - big = max(coord) - draw = [(big, big+1), (big+1, big), (big+1, big+1)] - #plt.fill([big, big+1, big+1], [big, big, big+1],"r") - elif (coord[0] == coord[1]): #class 2 - #(1,1) => (2,0) |+| (1,1) - #(2,2) => (3,1) |+| (1,1) - #(n,n) => (n+1,n-1) |+| (1,1) - big = coord[0] - draw = [(big+1, big-1), (big+1, big), (big+2, big-1)] - #plt.fill([big+1, big+2, big+1], [big-1, big-1, big],"r") - else: - #choose one enantiomer - #(2,1) => [(3, 1), (3, 2), (4, 1)] - #(3,1) => [(4, 2), (3, 2), (4, 1)] - #(a,b) => [(a+1, b), (a+1, b+1), (a+2, b)] - a,b = coord - draw = [(a+1, b), (a+1, b+1), (a+2, b)] +class Triangle: + def __init__( + self, + left: tuple[int, int] | NDArray, + right: tuple[int, int] | NDArray, + top: tuple[int, int] | NDArray, + ): + self.left = np.array(left) + self.right = np.array(right) + self.top = np.array(top) - plt.fill(*list(zip(*map(triangle_coords, draw))), "r") -""" + def __iter__(self): + yield self.left + yield self.right + yield self.top + yield self.left -def identify_triangles(coord, offset=0, point=True, fill_all=False, path=False): - # (a,b) specifies coordinates to the center of a hexagon - # (a,a) is the first hexagon for turning - # (2b, -b) is the length of the path after the 60 degree turn, meaning - # (a+2b, a-b) is the center of the hexagon - a, b = coord - if a != b and min(coord) > 0 and offset == 0: - offset = a - b - # origin - plt.fill( - *list( - zip( - *map( - triangle_coords, - [(offset, offset), (offset + 1, offset), (offset, offset + 1)], - ) - ) - ), - "r", +def draw_class_i(a: int, fill_all: bool = False): + """ + Plot a Class I Goldberg-Coxeter sector with parameter (a, 0) + """ + origin = Triangle( + left=(0, 0), + right=(1, 0), + top=(0, 1), + ) + plot_triangular("fill")(origin, "r") + + terminal = Triangle( + right=(a, a), + left=(a - 1, a), + top=(a, a - 1), # actually the bottom + ) + plot_triangular("fill")(terminal, "b") + + bottom_sector = Triangle( + left=origin.left, + right=(a, 0), + top=(0, a), + ) + top_sector = Triangle( + left=(0, a), + right=terminal.right, + top=(a, 0), ) - # control point - if point: - plt.plot(*triangle_coords((a + offset, b + offset)), "bo") + # outline sector + plot_triangular("plot")( + [ + origin.top, + origin.right, + bottom_sector.right, + terminal.top, + terminal.left, + bottom_sector.top, + origin.top, + ], + color="0.25", + lw=2, + ) - c, d = (a - offset + 2 * (b + offset), a - b + offset) - if min(coord) == 0: - # upside-down triangle - triangle = [(c, d), (c - 1, d), (c, d - 1)] - # terminal - plt.fill(*list(zip(*map(triangle_coords, triangle))), "b") + if fill_all: + plot_triangular("fill")(bottom_sector, "r") + plot_triangular("fill")(top_sector, "b") - if fill_all: - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset, offset), - (offset + a, offset), - (offset, offset + a), - ], - ) - ) - ), - "r", - ) - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset, offset + a), - (offset + a, offset), - (offset + a, offset + a), - ], - ) - ) - ), - "b", - ) - else: - triangle = [(c, d), (c - 1, d), (c - 1, d + 1)] - # terminal - plt.fill(*list(zip(*map(triangle_coords, triangle))), "b") - # alternate terminal - c, d = (b - a + offset, b - offset + 2 * (a + offset)) - triangle2 = [(c, d), (c, d - 1), (c + 1, d - 1)] - plt.fill(*list(zip(*map(triangle_coords, triangle2))), "g") - # lines connecting triangles - plt.plot( - *list( - zip( - *map( - triangle_coords, - [ - (offset + 1, offset), - triangle[1], - triangle[2], - triangle2[2], - triangle2[1], - (offset, offset + 1), - (offset + 1, offset), - ], - ) - ) - ), - color="0.25", - lw=2, - ) +def draw_trapezoids(a: int): + """Draw trapezoidal regions in a Class II Goldberg-Coxeter sector""" + plot_triangular("fill")( + [(0, 0), (0, a), (a, a), (2 * a, 0)], + "r", + ) + plot_triangular("fill")( + [(0, a), (a, a), (a, 2 * a), (0, 3 * a)], + "g", + ) + plot_triangular("fill")( + [(a, a), (a, 2 * a), (3 * a, 0), (2 * a, 0)], + "b", + ) - if fill_all and a == b: - # trapezoids - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset, offset), - (offset, offset + a), - (offset + a, offset + a), - (offset + 2 * a, offset), - ], - ) - ) - ), - "r", - ) - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset, offset + a), - (offset + a, offset + a), - (offset + a, offset + 2 * a), - (offset, offset + 3 * a), - ], - ) - ) - ), - "g", - ) - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset + a, offset + a), - (offset + a, offset + 2 * a), - (offset + 3 * a, offset), - (offset + 2 * a, offset), - ], - ) - ) - ), - "b", - ) - elif fill_all == 1: - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset + 1, offset), - triangle[1], - triangle[2], - triangle2[2], - triangle2[1], - (offset, offset + 1), - (offset + 1, offset), - ], - ) - ) - ), - color="0.4", - ) +def draw_classes_ii_iii(parameter: tuple[int, int], fill_all: int): + """ + Plot a Class II or III Goldberg-Coxeter sector with parameter (a, b) - elif fill_all == 2: - # dark gray interior - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset + 1, offset), - triangle[1], - triangle[2], - triangle2[2], - triangle2[1], - (offset, offset + 1), - (offset + 1, offset), - ], - ) - ) - ), + Parameters where the second term is larger are NOT plotted correctly by fill_all. + """ + # (a, b) specifies coordinates to the center of a hexagon + a, b = parameter + # (a, a) is the hexagon we turn at, + # (2b, -b) is the length of the path after the 60 degree turn, so + # (a+2b, a-b) is the center of the hexagon + w = a + 2 * b - 1 + l = a - b + + # origin + origin = Triangle( + left=(0, 0), + right=(1, 0), + top=(0, 1), + ) + plot_triangular("fill")(origin, "r") + + # terminal triangle #1 + terminal1 = Triangle(right=(w + 1, l), left=(w, l), top=(w, l + 1)) + plot_triangular("fill")(terminal1, "b") + + # terminal triangle #2 + terminal2 = Triangle(top=(-l, w + l + 1), left=(-l, w + l), right=(-l + 1, w + l)) + plot_triangular("fill")(terminal2, "g") + + # lines connecting triangles + plot_triangular("plot")( + [ + origin.top, + origin.right, + terminal1.left, + terminal1.top, + terminal2.right, + terminal2.left, + origin.top, + ], + color="0.25", + lw=2, + ) + + if fill_all: + if a == b: + draw_trapezoids(a) + else: + plot_triangular("fill")( + [ + origin.top, + origin.right, + terminal1.left, + terminal1.top, + terminal2.right, + terminal2.left, + origin.top, + ], color="0.4", ) # lower parallelogram - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset + 1, offset), - (offset + 1, offset + offset), - triangle[1], - (lambda x, y: (x, y - offset))(*triangle[1]), - (offset + 1, offset), - ], - ) - ) - ), + plot_triangular("fill")( + [ + origin.right, + (1, l), + terminal1.left, + terminal1.left + (0, -l), + origin.right, + ], color="xkcd:light red", ) # upper right parallelogram - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - triangle[2], - (lambda x, y: (x - offset, y))(*triangle[2]), - triangle2[2], - (lambda x, y: (x + offset, y))(*triangle2[2]), - triangle[2], - ], - ) - ) - ), + plot_triangular("fill")( + [ + terminal1.top, + terminal1.top + (-l, 0), + terminal2.right, + terminal2.right + (l, 0), + terminal1.top, + ], color="xkcd:light blue", ) # upper left parallelogram - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - triangle2[1], - (lambda x, y: (x + offset, y - offset))(*triangle2[1]), - (offset, offset + 1), - (0, offset + 1 + offset), - triangle2[1], - ], - ) - ) - ), + plot_triangular("fill")( + [ + terminal2.left, + terminal2.left + (l, -l), + origin.top, + origin.top + (-l, l), + terminal2.left, + ], color="xkcd:light green", ) # center triangle - plt.fill( - *list( - zip( - *map( - triangle_coords, - [ - (offset, offset + offset), - (lambda x, y: (x - offset + 1, y - 1))(*triangle[2]), - (lambda x, y: (x + offset, y - offset + 1))( - *triangle2[1] - ), - (offset, offset + offset), - ], - ) - ) - ), + plot_triangular("fill")( + [ + (0, a - b), + (3 * b, a - b), + (0, a + 2 * b), + (0, a - b), + ], color="0.8", ) + +def draw_path(a: int, b: int): + plot_triangular("plot")( + [(i, i) for i in range(a + 1)], + marker="o", + color="xkcd:light red", + mfc="k", + lw=3, + ) + plt.text( + *embed_triangular((a / 2 - 1, a / 2 + 1.5)), + f"a = {a}", + bbox={ + "facecolor": "xkcd:light red", + "pad": 6, + }, + ) + + plot_triangular("plot")( + [(a + 2 * i, a - i) for i in range(b + 1)], + marker="o", + color="xkcd:light blue", + mfc="k", + lw=3, + ) + plt.text( + *embed_triangular((a + b, a - b / 2 + 0.75)), + f"b = {b}", + bbox={ + "facecolor": "xkcd:light blue", + "pad": 6, + }, + ) + + +def recenter_axes(a: int, b: int): + minx, maxx = plt.xlim() + miny, maxy = plt.ylim() + xspan = maxx - minx + yspan = maxy - miny + + extent_x = a + 2*b + (a - b) * root3.real # rightward extent due to blue triangle + if a > b and min(a, b) != 0: + extent_x += -(a-b) + (1 + a-b) * root3.real # leftward extent due to green parallelogram + + extent_y = (2 * max(a, b) + min(a, b)) * root3.imag + + if min(a, b) == 0 or (extent_x * yspan / xspan) > extent_y: + span_diff = extent_x * yspan / xspan - extent_y + plt.xlim(-0.1, extent_x + 0.1) + plt.ylim(span_diff / 2, (extent_x + 0.2) * yspan / xspan + span_diff / 2) + else: + span_diff = extent_y * xspan / yspan - extent_x + # the figure extends below the y axis if a < b + if a >= b: + plt.ylim(-0.1, extent_y + 0.1) + else: + plt.ylim( + -extent_y / (2 * a + 1) - 0.1, extent_y * (1 - (1 / (2 * a + 1))) + 0.1 + ) + + plt.xlim(-span_diff / 2, (extent_y + 0.2) * xspan / yspan - span_diff / 2) + + +def draw_sector(parameter: tuple[int, int], fill_all: bool = False, path: bool = False): + """ + Draw a sector from the Goldberg-Coxeter construction with a certain `parameter`. + `offset` will translate the figure to the point `(offset, offset)` in a triangular plane. + `fill_all` controls whether the sector should be filled. + `path` controls whether to label the path described by `parameter` + """ + draw_triangular_grid(50, offset=-10, show_hexes=True) + # (a, b) specifies coordinates to the center of a hexagon + a, b = parameter + + if min(parameter) == 0: + draw_class_i(max(parameter), fill_all) + else: + draw_classes_ii_iii(parameter, fill_all) + if path: - plt.plot( - *list( - zip( - *map( - lambda x: triangle_coords((offset + x, offset + x)), - range(a + 1), - ) - ) - ), - marker="o", - color="xkcd:light red", - mfc="k", - lw=3, - ) - plt.text( - *triangle_coords((offset + a / 2 - 1, offset + a / 2 + 1.5)), - f"a = {a}", - bbox={"facecolor": "xkcd:light red", "pad": 6}, - ) + draw_path(*parameter) - plt.plot( - *list( - zip( - *map( - lambda x: triangle_coords((offset + a + 2 * x, offset + a - x)), - range(b + 1), - ) - ) - ), - marker="o", - color="xkcd:light blue", - mfc="k", - lw=3, - ) - plt.text( - *triangle_coords((offset + a + b, offset + a - b / 2 + 0.75)), - f"b = {b}", - bbox={"facecolor": "xkcd:light blue", "pad": 6}, - ) + recenter_axes(*parameter) -if __name__ == "__main__": - triangular_grid(24, True) - # identify_triangles((4,2), point=False, path=True) - identify_triangles((4, 2), offset=5, point=False, path=True) - plt.show() +# if __name__ == "__main__": +# draw_triangular_grid(24, True) +# # draw_sector((4,2), point=False, path=True) +# draw_sector((4, 2), offset=5, point=False, path=True) +# plt.show()