diff --git a/posts/pentagons/1/goldberg/common.py b/posts/pentagons/1/goldberg/common.py index 00fff63..1245de3 100644 --- a/posts/pentagons/1/goldberg/common.py +++ b/posts/pentagons/1/goldberg/common.py @@ -1,4 +1,3 @@ -from ast import TypeAlias import itertools from dataclasses import dataclass, field from typing import Iterable, Literal @@ -6,9 +5,7 @@ import re import sympy -from goldberg.operators import GoldbergOperator - -t_param = sympy.symbols("T") +from goldberg.operators import GoldbergOperator, Polyhedron GoldbergClass = Literal["I", "II", "III"] @@ -20,19 +17,13 @@ def group_dict(xs: Iterable, key=None): return {i: list(j) for i, j in itertools.groupby(sorted(xs, key=key), key=key)} -@dataclass -class PolyData: - hexagon_count: int - vertex_count: int - edge_count: int - - @classmethod - def from_hexagons(cls, hexagon_count: int): - return cls( - hexagon_count=hexagon_count, - vertex_count=2 * hexagon_count + 20, - edge_count=3 * hexagon_count + 30, - ) +def hexagons_to_polyhedron(hexagon_count: int): # | sympy.Expr): + """General Goldberg solution""" + return Polyhedron( + face_count=hexagon_count + 12, + vertex_count=2 * hexagon_count + 20, + edge_count=3 * hexagon_count + 30, + ) @dataclass @@ -69,7 +60,7 @@ def display_conway(conway_notation: str, seed: str | None = None): # If a seed is given, split at the equals and apply to both sides if seed is not None: conway_notation = " = ".join( - operation + "D" + operation + seed for operation in conway_notation.split(" = ") ) diff --git a/posts/pentagons/1/goldberg/dodecahedral.py b/posts/pentagons/1/goldberg/dodecahedral.py index 7405858..dd1bb02 100644 --- a/posts/pentagons/1/goldberg/dodecahedral.py +++ b/posts/pentagons/1/goldberg/dodecahedral.py @@ -21,9 +21,9 @@ dodecahedral_goldberg_recipes: dict[tuple[int, int], str] = { # Assert all the above recipes are correct verify_recipes( { - param: goldberg_parameters_to_operations[param] + "D" - for param in dodecahedral_recipes.keys() + param: " = ".join([op + "D" for op in goldberg_parameters_to_operations[param].split(" = ")]) + for param in dodecahedral_goldberg_recipes.keys() }, - dodecahedral_recipes, + dodecahedral_goldberg_recipes, "Dodecahedral recipe does not match!", ) diff --git a/posts/pentagons/1/goldberg/operators.py b/posts/pentagons/1/goldberg/operators.py index 52490a3..f093532 100644 --- a/posts/pentagons/1/goldberg/operators.py +++ b/posts/pentagons/1/goldberg/operators.py @@ -98,10 +98,15 @@ def generalize_recipe(recipe: str | list[str], depth: int = 3) -> set[str]: ret = set( [ + # Need to run this twice to account for adjacent u's re.sub( "u(\\D)", "u2\\1", - re.sub("([KC]\\d*)", "", rec), + re.sub( + "u(\\D)", + "u2\\1", + re.sub("([KC]\\d*)", "", rec), + ) ).replace("dd", "") for rec in recipe ] diff --git a/posts/pentagons/1/goldberg/tetrahedral.py b/posts/pentagons/1/goldberg/tetrahedral.py new file mode 100644 index 0000000..5fa3dda --- /dev/null +++ b/posts/pentagons/1/goldberg/tetrahedral.py @@ -0,0 +1,199 @@ +from dataclasses import dataclass + +from goldberg.common import GoldbergOperator +from goldberg.operators import ( + goldberg_operators_to_parameters, + goldberg_parameters_to_operations, + verify_recipes, +) + + +tetrahedral_goldberg_recipes = { + # Class I + (1, 0): "T", + (2, 0): "K300cT", + (3, 0): "K300tkT", + (4, 0): "K300duC30uT", + (5, 0): "C300du5T", + (6, 0): "K300duK300dtkT", + # Class II + (1, 1): "tT", + (2, 2): "K300duK30dtT", + (3, 3): "K300tktT", + (4, 4): "duK300dK300dudtT", + # Class III + (2, 1): "K300wT", + (3, 1): "*", + (3, 2): "*", + (4, 1): "K300wK30tT", + (4, 2): "K300wK30cT", +} + + +# Assert all the above recipes are correct +verify_recipes( + { + param: " = ".join( + [op + "T" for op in goldberg_parameters_to_operations[param].split(" = ")] + ) + for param in tetrahedral_goldberg_recipes.keys() + }, + tetrahedral_goldberg_recipes, + "Tetrahedral recipe does not match!", +) + + +@dataclass +class HexagonPath: + """A path along hexagons on a Goldberg polyhedron.""" + + a: int + b: int + + def __hash__(self): + return hash((self.a, self.b)) + + def __str__(self): + return str(f"{(self.a, self.b)}") + + +_P = HexagonPath + + +@dataclass +class WhirledPath: + """ + A path on a tetrahedral solution where leaving an initial pentagon from + the edge not touching another pentagon enters a terminal pentagon through one which is. + + In other words, the initial and terminal pentagons are in some sense "rotated" + from one another. + + Note that one whirled path signifies two possible paths: (a, b) and (a + b, 0) + """ + + a: int + b: int + + def __hash__(self): + return hash((self.a, self.b)) + + def __str__(self): + return str(f"w{(self.a, self.b)}") + + +_WP = WhirledPath + + +@dataclass +class TetrahedralAntitruncation: + conway: str # Conway polyhedron notation for constructing the polygon + conway_recipe: str # The recipe for constructing the polyhedron in a viewer, equivalent to `conway` but stabler + intercluster_paths: list[ + HexagonPath | WhirledPath + ] # Path connecting pentagons of two different clusters + cluster_path: HexagonPath | WhirledPath = _P( + 1, 0 + ) # Path between pentagons in a cluster + + +_TA = TetrahedralAntitruncation + +tetrahedral_goldberg_antitruncations: dict[ + tuple[int, int], TetrahedralAntitruncation +] = { + # Class I + (1, 0): _TA("dT", "dT", []), + (2, 0): _TA("C", "C", []), + (3, 0): _TA("t6kT", "K300t6kT", [_P(1, 0)]), + (4, 0): _TA("t6juT", "C300t6juT", [_P(2, 0)]), + (5, 0): _TA("**", "**", [_P(3, 0)]), + (6, 0): _TA("t6kcT", "K300t6kcT", [_P(4, 0)]), + # Class II + (1, 1): _TA("T", "T", []), + (2, 2): _TA("t6uT", "K300t6uT", [_P(1, 1)]), + (3, 3): _TA("t6ktT", "t6ktT", [_P(2, 2)]), + (4, 4): _TA("t6uuT", "K300t6uK30dK30duT", [_P(3, 3)]), + # Class III + (2, 1): _TA("D", "D", [_P(1, 0)]), + (3, 1): _TA("*", "*", []), + (3, 2): _TA("*", "*", []), + (4, 1): _TA("t6gtT", "K300t6gK30tT", [_WP(2, 1)]), + (4, 2): _TA("t6guT = t6gcT", "K300t6gK30cT", [_P(3, 1)]), +} + +# Ensure the correctness of all tetrahedral antitruncation recipes +verify_recipes( + { + parameter: data.conway + for parameter, data in tetrahedral_goldberg_antitruncations.items() + }, + { + parameter: data.conway_recipe + for parameter, data in tetrahedral_goldberg_antitruncations.items() + }, + "Tetrahedral antitruncation recipe does not match!", +) + + +double_goldberg_operators: list[GoldbergOperator] = ["c", "dk", "tk", "w"] + +double_goldberg_paths: dict[ + GoldbergOperator, dict[HexagonPath | WhirledPath, list[HexagonPath | WhirledPath]] +] = { + "c": { + _P(1, 0): [_P(2, 0)], + _P(1, 1): [_P(2, 2)], + _WP(2, 1): [_WP(4, 2)], # paths get doubled + }, + "dk": { + _P(1, 0): [_P(1, 1)], + _P(1, 1): [_P(3, 0)], + _WP(2, 1): [ + _P(4, 1), + _P(3, 3), + ], # Whirled path splits into Class II and Class III-types + }, + "tk": { + _P(1, 0): [_P(3, 0)], + _P(1, 1): [_P(3, 3)], + _WP(2, 1): [_WP(6, 3)], # paths get tripled + }, + "w": { + _P(1, 0): [_P(2, 1)], + _P(1, 1): [_P(4, 1)], + _WP(2, 1): [_P(5, 3), _P(6, 3)], # Whirled path splits into two Class III-types + }, +} + + +def _apply_double( + operation: GoldbergOperator, +) -> dict[tuple[int, int], TetrahedralAntitruncation]: + """Build data for a double-Goldberg tetrahedral solution""" + return { + entry: TetrahedralAntitruncation( + conway=" = ".join( + [operation + i for i in antitruncation.conway.split(" = ")] + ), + conway_recipe=( + "*" + if antitruncation.conway_recipe.find("*") == 0 + else operation + antitruncation.conway_recipe + ), # TODO: might need to commute whirl with the last regularization operator + intercluster_paths=double_goldberg_paths[operation][ + antitruncation.intercluster_paths[0] + ], + cluster_path=_P(*goldberg_operators_to_parameters[operation]), + ) + for entry in [(3, 0), (2, 2), (4, 1)] + for antitruncation in (tetrahedral_goldberg_antitruncations[entry],) + } + + +double_goldberg_antitruncations: dict[ + GoldbergOperator, dict[tuple[int, int], TetrahedralAntitruncation] +] = { + operator: _apply_double(operator) + for operator in double_goldberg_operators # for some reason, using a literal list causes type errors +} diff --git a/posts/pentagons/1/index.qmd b/posts/pentagons/1/index.qmd index 107543c..1025e55 100644 --- a/posts/pentagons/1/index.qmd +++ b/posts/pentagons/1/index.qmd @@ -113,7 +113,7 @@ In this form, the simplest cases of the problem become obvious: - $n = 5$, and there are only pentagons - $V(6 - 5) + F_5(2(5) - 10) = V = 4(5) = 20$ - - In this case, all occurrences of $F_n$ above turns out to be 0, + - In this case, all occurrences of $F_n$ above turn out to be 0, since all of the pentagons are counted by $F_5$ - $n = 6$, and the non-pentagons are hexagons - $V(6 - 6) + F_5(2(6) - 10) = 2F_5 = 4(6) = 24$ @@ -569,8 +569,8 @@ rows: list[DodecahedralGoldbergSolution] = [ edge_count=str(polyhedron.edge_count), vertex_count=str(polyhedron.vertex_count), conway=( - "*" - if recipe == "*" + recipe + if recipe.find("*") == 0 else link_polyhedronisme( display_conway(goldberg_parameters_to_operations[parameter], "D"), recipe, @@ -600,10 +600,10 @@ headers = DodecahedralGoldbergSolution( Markdown(tabulate( { - field: [name] + (remove_repeats if field == "klass" else lambda x: x)( - [getattr(item, field) for item in rows] - ) - for field, name in asdict(headers).items() + field: [name] + (remove_repeats if field == "klass" else lambda x: x)( + [getattr(item, field) for item in rows] + ) + for field, name in asdict(headers).items() }, headers="firstrow", numalign="left", diff --git a/posts/pentagons/2/goldberg b/posts/pentagons/2/goldberg new file mode 120000 index 0000000..8089557 --- /dev/null +++ b/posts/pentagons/2/goldberg @@ -0,0 +1 @@ +../1/goldberg \ No newline at end of file diff --git a/posts/pentagons/2/index.qmd b/posts/pentagons/2/index.qmd index 89ab12e..19a4557 100644 --- a/posts/pentagons/2/index.qmd +++ b/posts/pentagons/2/index.qmd @@ -12,22 +12,6 @@ categories: - geometry - combinatorics - symmetry -# Get rid of the figure label -crossref: - fig-title: "" - fig-labels: " " - tbl-title: "" - tbl-labels: " " - title-delim: "" - custom: - - key: fig - kind: float - reference-prefix: Figure - space-before-numbering: false - - key: tbl - kind: float - reference-prefix: Table - space-before-numbering: false --- ```{python} -#| echo: false - -from dataclasses import dataclass, field -import itertools -from typing import Literal +from dataclasses import dataclass, asdict from IPython.display import Markdown import matplotlib.pyplot as plt -import numpy as np from tabulate import tabulate -import goldberg_triangles - -polyhedronisme = lambda x, y: f"[{x}](https://levskaya.github.io/polyhedronisme/?recipe={y})" - -@dataclass -class PolyData: - hexagon_count: int - vertex_count: int - edge_count: int - - @classmethod - def from_hexagons(cls, hexagon_count: int): - return cls( - hexagon_count=hexagon_count, - vertex_count=2*hexagon_count + 20, - edge_count=3*hexagon_count + 30, - ) - - -@dataclass -class GoldbergData: - parameter: tuple[int, int] - conway: str - - parameter_link: str | None = None - conway_recipe: str | None = None - - extra: list = field(default_factory=list) +from goldberg.common import ( + GoldbergData, + GoldbergOperator, + hexagons_to_polyhedron, + group_dict, + link_polyhedronisme, + parameter_to_class, + display_conway, + remove_repeats, +) +from goldberg.operators import ( + goldberg_parameters_to_operations, + goldberg_operators_to_parameters, + loeschian_norm, +) +from goldberg.tetrahedral import ( + tetrahedral_goldberg_recipes, + tetrahedral_goldberg_antitruncations, + double_goldberg_antitruncations +) +import goldberg.triangles as goldberg_triangles ``` This is the second part in an investigation into answering the following question: @@ -114,7 +86,7 @@ Recall the triangular plane and hexagonal paths from the previous part (presented again here for convenience): [^1]: -
+ ::: {} The equation we used to obtain the "12 pentagons" figure also applies to the tetrahedron, with some adjustments: @@ -139,12 +111,10 @@ Recall the triangular plane and hexagonal paths from the previous part From this, it is discerned that there are 4 vertices ($n = 3$) or 4 trianglar faces ($n = 6$), with the rest of the faces being hexagons. -
+ ::: ::: {.row width="100%"} ```{python} -#| echo: false - goldberg_triangles.draw_sector((4, 2), path=True) plt.show() ``` @@ -220,141 +190,82 @@ This is because the antitruncations correspond to one of the three Platonic soli With these exceptional cases taken care of, the table of solution polyhedra of this form is as follows: ```{python} -#| echo: false #| label: tbl-antitruncation #| classes: plain #| tbl-cap: \\\* higher-order whirl needed
\\\*\\\* Unknown. Possibly nonexistent under standard operators +count_dodecahedral = lambda a, b: 10*loeschian_norm(a, b) - 10 +count_tetrahedral = lambda a, b: 2*loeschian_norm(a, b) - 14 -norm = lambda a, b: a**2 + a*b + b**2 -count_dodecahedral = lambda a, b: 10*norm(a, b) - 10 -count_tetrahedral = lambda a, b: 2*norm(a, b) - 14 +@dataclass +class TetrahedralSolution: + klass: str + parameter: str + conway_trunc: str + conway: str + hexagon_count: str + vertex_count: str + edge_count: str + paths: str -tetrahedral_goldberg_classes = { - "I": [ - GoldbergData( - parameter=(1, 0), - extra=[polyhedronisme("T", "T")], - conway="dT", - conway_recipe="dT", - ), - GoldbergData( - parameter=(2, 0), - extra=[polyhedronisme("cT", "K300cT")], - conway="C", - conway_recipe="C", - ), - GoldbergData( - parameter=(3, 0), - extra=[polyhedronisme("du3T = tkT", "K300tkT")], - conway="t6kT", - conway_recipe="K300t6kT", - ), - GoldbergData( - parameter=(4, 0), - extra=[polyhedronisme("duuT = ccT", "K300duC30uT")], - conway="t6juT", - conway_recipe="C300t6juT", - ), - GoldbergData( - parameter=(5, 0), - extra=[polyhedronisme("du5T", "K300duC30uT")], - conway="**", - ), - GoldbergData( - parameter=(6, 0), - extra=[polyhedronisme("duu3T = ctkT", "K300duK300dtkT")], - conway="K300", - conway_recipe="K300t6kcT", - ), - ], - "II": [ - GoldbergData( - parameter=(1, 1), - extra=[polyhedronisme("tT", "tT")], - conway="T", - conway_recipe="T", - ), - GoldbergData( - parameter=(2, 2), - extra=[polyhedronisme("ctT", "K300duK30dtT")], - conway="t6uT", - conway_recipe="K300t6uT", - ), - GoldbergData( - parameter=(3, 3), - extra=[polyhedronisme("tktT", "K300tktT")], - conway="t6ktT", - conway_recipe="K30t6ktT", - ), - GoldbergData( - parameter=(4, 4), - extra=[polyhedronisme("duuT = cctT", "K300duC30uT")], - conway="t6uuT", - conway_recipe="K300t6uK30dK30duT", - ), - ], - "III": [ - GoldbergData( - parameter=(2, 1), - extra=[polyhedronisme("wT", "K300wT")], - conway="D", - conway_recipe="D", - ), - GoldbergData( - parameter=(3, 1), - extra=["*"], - conway="*", - ), - GoldbergData( - parameter=(3, 2), - extra=["*"], - conway="*", - ), - GoldbergData( - parameter=(4, 1), - extra=[polyhedronisme("wtT", "K300wK30tT")], - conway="t6gtT", - conway_recipe="K300t6gK30tT", - ), - GoldbergData( - parameter=(4, 2), - extra=[polyhedronisme("wcT", "K300wK30cT")], - conway="t6guT = t6gcT", - conway_recipe="K300t6gK30cT", - ), - ], -} +tet_anti_rows = [ + TetrahedralSolution( + klass=parameter_to_class(parameter), + parameter=str(parameter), + conway_trunc=( + truncated_recipe + if truncated_recipe.find("*") == 0 + else link_polyhedronisme( + display_conway(goldberg_parameters_to_operations[parameter], "T"), + truncated_recipe, + ) + ), + hexagon_count=str(polyhedron.face_count - 12), + vertex_count=str(polyhedron.vertex_count), + edge_count=str(polyhedron.edge_count), + conway=( + antitruncation.conway + if antitruncation.conway.find("*") == 0 + else link_polyhedronisme( + display_conway(antitruncation.conway), + antitruncation.conway_recipe, + ) + ), + paths=( + "" + if not antitruncation.intercluster_paths + else f"${', '.join( + map(str, antitruncation.intercluster_paths) + )}$" + ) + ) + for parameter, antitruncation in tetrahedral_goldberg_antitruncations.items() + for truncated_recipe in (tetrahedral_goldberg_recipes[parameter].split(" = ")[-1],) + for polyhedron in (hexagons_to_polyhedron(count_tetrahedral(*parameter)),) +] -def poly_from_tetrahedral_goldberg_parameter(parameter: tuple[int, int]): - a, b = parameter - hexagon_count = count_tetrahedral(a, b) - return PolyData.from_hexagons(hexagon_count) - - -def tetrahedral_goldberg_table_row(data: GoldbergData) -> list[str]: - poly_data = poly_from_tetrahedral_goldberg_parameter(data.parameter) - return [ - str(data.parameter), - *map(str, data.extra), - str(poly_data.hexagon_count), - str(poly_data.vertex_count), - str(poly_data.edge_count), - polyhedronisme(data.conway, data.conway_recipe) - if data.conway_recipe is not None - else str(data.conway) - ] +tet_anti_headers = TetrahedralSolution( + klass="Class", + parameter="Parameter", + conway_trunc="Conway (Trunc)", + hexagon_count="$F_6$", + edge_count="E", + vertex_count="V", + conway="Conway", + paths="Paths", +) Markdown(tabulate( - [ - ([class_ if i == 0 else ""]) + tetrahedral_goldberg_table_row(item) - for class_, items in tetrahedral_goldberg_classes.items() - for i, item in enumerate(items) - ], - headers=["Class", "Parameter", "Conway (Trunc)", "$F_6$", "V", "E", "Conway"], - numalign="left", + { + field: [name] + (remove_repeats if field == "klass" else lambda x: x)( + [getattr(item, field) for item in tet_anti_rows] + ) + for field, name in asdict(tet_anti_headers).items() + }, + headers="firstrow", + numalign="left", )) ``` @@ -367,6 +278,16 @@ Note also that the links in the above table may have recipes do not match their Rather, the recipe is equivalent to the simpler string in the table to ensure the viewer can stably generate the output shape. +By leaving a pentagon from an edge which does not share a vertex with another pentagon, we can follow + a path on the hexagons to another pentagon. +In the Class I case, this crosses two fewer edges than the parameter, + since the edges of the triangle from the Goldberg operation have been capped. +This is also true in the Class II case, but both terms of the parameter are decreased by 1. +Class III is mostly the same as Class II, except when one of the parameters is 1. +In this case, the path is "whirled" -- it approaches the terminal pentagon from + an edge touching another pentagon. +Also, for a whirled path *w*(*a*, *b*), both paths (*a*, *b*) and (*a + b*, 0) have this property. + The smallest "real" tetrahedral solution has 4 hexagonal faces. This is the [(order-6) truncated triakis tetrahedron](https://en.wikipedia.org/wiki/Truncated_triakis_tetrahedron), @@ -405,7 +326,7 @@ In particular, $\|m + mu\|$ is always congruent to 0 (mod 3) But -2 is congruent to 1, so equality is impossible. [^2]: -
+ ::: {} Consider the equation $a^2 + ab + b^2 = 2$. Since 0 and 1 are the only squares mod 3, we can build a table out of each possibility: @@ -417,49 +338,49 @@ But -2 is congruent to 1, so equality is impossible. | 1 | 1 | $ab \equiv 0$, but neither of *a* or *b* is a multiple of 3 | No | On the other hand, if $b \equiv 0$, then the norm is $a^2$, which is either 0 or 1. -
+ ::: Only tetrahedral Class III collisions exist, and they must be congruent to the pairs (1, 2), (2, 1), (2, 2), (3, 3), (3, 4), or (4, 3) (mod 5). Some of these can be found in the table below. ```{python} -#| echo: false #| classes: plain +#| tbl-cap: "Red paths denote paths between pentagons in the same \"cluster\".\ +#|
Blue paths are those between clusters, as from the earlier table." -grouped = lambda x, f: {i: list(j) for i, j in itertools.groupby(sorted(x, key=f), key=f)} - -dodecahedrals = grouped( - [ - ((a, b), count_dodecahedral(a, b)) - for a in range(8) - for b in range(5) - if a > b - ], - lambda x: x[1] -) - -tetrahedrals = grouped( - [ - ((m, n), count_tetrahedral(m, n)) - for m in range(18) - for n in range(9) - if m > n - ], - lambda x: x[1] -) - -Markdown(tabulate( - [ +# Groupings of dodecahedral and tetrahedral solutions by hexagon counts +dodecahedrals = group_dict( [ - "
".join([str(j) for j, _ in dodecahedrals[i]]), - "
".join([str(j) for j, _ in tetrahedrals[i]]), - i, - ] - for i in sorted(dodecahedrals.keys()) if tetrahedrals.get(i) is not None - ], - headers=["Dodecahedral Parameter", "Tetrahedral Parameter", "$F_6$"], - numalign="left", + ((a, b), count_dodecahedral(a, b)) + for a in range(8) + for b in range(5) + if a > b + ], + lambda x: x[1] +) +tetrahedrals = group_dict( + [ + ((m, n), count_tetrahedral(m, n)) + for m in range(18) + for n in range(9) + if m > n + ], + lambda x: x[1] +) + +# Table of collisions between both groupings +Markdown(tabulate( + [ + [ + "
".join([str(j) for j, _ in dodecahedrals[i]]), + "
".join([str(j) for j, _ in tetrahedrals[i]]), + i, + ] + for i in sorted(dodecahedrals.keys()) if tetrahedrals.get(i) is not None + ], + headers=["Dodecahedral Parameter", "Tetrahedral Parameter", "$F_6$"], + numalign="left", )) ``` @@ -613,172 +534,86 @@ Every one of these solution figures, both constructed and otherwise, Some of the easily-constructible solutions based on Goldberg polyhedra are accumulated in the table below. ```{python} -#| echo: false #| label: tbl-double-goldberg +#| tbl-colwidths: [20, 15, 10, 10, 10, 35] #| classes: plain -double_goldberg: dict[Literal["c", "dk", "tk", "w"], list[GoldbergData]] = { - "c": [ - GoldbergData( - parameter=(3, 0), - conway=("ct6kT"), - conway_recipe=("K300dudt6kT"), - extra=[(3, 0), (2, 2), (4, 0)], - ), - GoldbergData( - parameter=(2, 2), - conway=("ct6uT"), - conway_recipe=("K300duK30dt6uT"), - extra=[(2, 2), (2, 0), (4, 2)], - ), - GoldbergData( - parameter=(4, 1), - conway=("ct6gtT"), - conway_recipe=("K300duK50dt6gtT"), - extra=[(4, 2), (2, 0), (6, 0)], - ), - ], - "dk": [ - GoldbergData( - parameter=(3, 0), - conway=("dkt6kT"), - conway_recipe=("K300dkt6kT"), - extra=[(1, 1), (2, 2)], - ), - GoldbergData( - parameter=(2, 2), - conway=("dkt6uT"), - conway_recipe=("K300dkK30t6uT"), - extra=[(1, 1), (3, 0), (3, 3)], - ), - GoldbergData( - parameter=(4, 1), - conway=("dkt6gtT"), - conway_recipe=("K300dkK50t6gtT"), - extra=[(1, 1), (3, 3), (4, 1)], - ), - ], - "tk": [ - GoldbergData( - parameter=(3, 0), - conway=("tkt6kT"), - conway_recipe=("K300tkt6kT"), - extra=[(3, 0), (3, 3)], - ), - GoldbergData( - parameter=(2, 2), - conway=("tkt6uT"), - conway_recipe=("K300tkK30t6uT"), - extra=[(3, 0), (3, 3), (6, 0)], - ), - GoldbergData( - parameter=(4, 1), - conway=("tkt6gtT"), - conway_recipe=("K300tkK50t6gtT"), - extra=[(3, 0), (6, 3), (9, 0)], - ), - ], - "w": [ - GoldbergData( - parameter=(3, 0), - conway=("wt6kT"), - conway_recipe=("K300wK30t6kT"), - extra=[(2, 1), (4, 1)], - ), - GoldbergData( - parameter=(2, 2), - conway=("wt6uT"), - conway_recipe=("K300wK100t6uT"), - extra=[(2, 1), (3, 0), (4, 1)], - ), - GoldbergData( - parameter=(4, 1), - conway=("wt6gtT"), - conway_recipe=("K300wK50t6gtT"), - extra=[(2, 1), (5, 3), (6, 3)], - ), - ], -} -# TODO: add assertions around recipe/conway from earlier table +@dataclass +class DoubleTetrahedralSolution: + parameter: str + conway: str + hexagon_count: str + vertex_count: str + edge_count: str + paths: str -# matrices applied over column vectors of the form [v, e, f] -goldberg_operators = { - "dk": np.array([ - [0, 2, 0], - [0, 3, 0], - [1, 0, 1], - ]), - "c": np.array([ - [1, 2, 0], - [0, 4, 0], - [0, 1, 1], - ]), - "w": np.array([ - [1, 4, 0], - [0, 7, 0], - [0, 2, 1], - ]), -} -goldberg_operators["tk"] = goldberg_operators["dk"] @ goldberg_operators["dk"] -def apply_goldberg_operator( - poly: PolyData, - operator: Literal["c", "dk", "tk", "w"], - other_face_count: int = 0, -) -> PolyData: - v, e, f = goldberg_operators[operator] @ np.array([ - poly.vertex_count, - poly.edge_count, - poly.hexagon_count + other_face_count, - ]).T - return PolyData( - vertex_count=v, - edge_count=e, - hexagon_count=f - other_face_count, - ) +double_tet_anti_rows = [ + DoubleTetrahedralSolution( + parameter=f"{operator}{parameter}", + hexagon_count=str(polyhedron.face_count - 12), + vertex_count=str(polyhedron.vertex_count), + edge_count=str(polyhedron.edge_count), + conway=( + double_goldberg.conway + if double_goldberg.conway.find("*") == 0 + else link_polyhedronisme( + display_conway(double_goldberg.conway), + double_goldberg.conway_recipe, + ) + ), + paths=( + f"$\\textcolor{{red}}{{{ + double_goldberg.cluster_path + }}}, \\textcolor{{blue}}{{{', '.join( + map(str, double_goldberg.intercluster_paths) + )}}}$" + ) + ) + for operator, double_goldbergs in double_goldberg_antitruncations.items() + for parameter, double_goldberg in double_goldbergs.items() + for polyhedron in ( + hexagons_to_polyhedron( + loeschian_norm(*goldberg_operators_to_parameters[operator]) + * (count_tetrahedral(*parameter) + 10) - 10 + ), + ) +] -# XXX: This fails to construct correct data when data.parameter gives a negative number of hexagons! -def tetrahedral_double_goldberg_table_row( - data: GoldbergData, - operator: Literal["c", "dk", "tk", "w"], -) -> list[str]: - poly_data = poly_from_tetrahedral_goldberg_parameter(data.parameter) - doubled = apply_goldberg_operator(poly_data, operator, other_face_count=12) - return [ - operator + str(data.parameter), - polyhedronisme(data.conway, data.conway_recipe) - if data.conway_recipe is not None - else str(data.conway), - ", ".join(map(str, data.extra)), - str(doubled.hexagon_count), - str(doubled.vertex_count), - str(doubled.edge_count), - ] + +double_tet_anti_headers = DoubleTetrahedralSolution( + parameter="(Tetrahedral Goldberg) Parameter", + conway="Conway", + hexagon_count="$F_6$", + edge_count="E", + vertex_count="V", + paths="Paths", +) Markdown(tabulate( - [ - tetrahedral_double_goldberg_table_row(data, operator) - for operator, datas in double_goldberg.items() - for data in datas - ], - headers=["(Tetrahedral Goldberg) Parameter", "Conway", "Paths Between Pairs", "$F_6$", "V", "E"], - numalign="left", + { + field: [name] + [getattr(item, field) for item in double_tet_anti_rows] + for field, name in asdict(double_tet_anti_headers).items() + }, + headers="firstrow", + numalign="left", )) ``` -Note that higher-order chamfers (dual, subdivide, dual) and higher-order whirls are possible. - The same operators can also be applied to the edge-based cases, but since I have not computationally generated these, I will not attempt to tabulate them. Doing so would require that I: -- Formalize the construction of the graph, - preferably by writing a program which can identify identical vertices. +1. Formalize the construction of the graph, + preferably by writing a program which can identify identical vertices. + - I did this step manually to sketch the figures -- Find good planar and spherical embeddings of the graph. -- (Optionally,) import and display the result in a polyhedron viewer, - preferably one which can apply Conway notation to an arbitrary figure + +2. Find good planar and spherical embeddings of the graph. + +3. (Optionally,) import and display the result in a polyhedron viewer, + preferably one which can apply Conway notation to an arbitrary figure Closing @@ -796,9 +631,9 @@ In fact, I made an error while constructing one of the edge-based solutions: ![ ](./edge_whirl_net_bad.png){.image-wide} -The face in the center is actually a horizontal reflection of how it should be. +The face in the center has been replaced with its chiral opposite. You can check this by trying to consistently follow the path (2, 1) -- - along the center face, you need follow (1, 2) instead. + along the center face, you need to follow (1, 2) instead. As you may be able to guess, not even specifying tetrahedral symmetry is enough to completely classify every solution.