refactor pentagons.2

This commit is contained in:
queue-miscreant 2025-03-23 20:25:31 -05:00
parent 118b1a2477
commit 6caf4640ac
7 changed files with 424 additions and 393 deletions

View File

@ -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(" = ")
)

View File

@ -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!",
)

View File

@ -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
]

View File

@ -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
}

View File

@ -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",

1
posts/pentagons/2/goldberg Symbolic link
View File

@ -0,0 +1 @@
../1/goldberg

View File

@ -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
---
<style>
@ -42,45 +26,33 @@ figure > p {
</style>
```{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]:
<div>
::: {}
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.
</div>
:::
::: {.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 <br> \\\*\\\* 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]:
<div>
::: {}
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.
</div>
:::
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\".\
#| <br>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(
[
"<br>".join([str(j) for j, _ in dodecahedrals[i]]),
"<br>".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(
[
[
"<br>".join([str(j) for j, _ in dodecahedrals[i]]),
"<br>".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:
![&#32;](./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.