refactor pentagons.2
This commit is contained in:
parent
118b1a2477
commit
6caf4640ac
@ -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(" = ")
|
||||
)
|
||||
|
||||
|
||||
@ -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!",
|
||||
)
|
||||
|
||||
@ -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
|
||||
]
|
||||
|
||||
199
posts/pentagons/1/goldberg/tetrahedral.py
Normal file
199
posts/pentagons/1/goldberg/tetrahedral.py
Normal 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
|
||||
}
|
||||
@ -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
1
posts/pentagons/2/goldberg
Symbolic link
@ -0,0 +1 @@
|
||||
../1/goldberg
|
||||
@ -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:
|
||||
|
||||
{.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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user