Right-Angled Hyperbolic Heptagons
Moduli and an explicit realization, built by subdividing into a pentagon and a hexagon.
This note continues the series of computing moduli spaces of convex right angled hyperbolic polygons to those with seven sides. There’s one labeled below for reference, followed by the main theorem:
The moduli space of right angled heptagons is homeomorphic to . One choice of coordinates involves a pair of adjacent side lengths, and the lengths of the two sides separated from these by an edge (sides in the diagram). In these coordinates the moduli space is represented as
That is, the moduli space is determined by selecting a point in the moduli of right angled pentagons and attaching two arbitrary positive parameters. We then make this theorem effective, and determine the lengths of the remaining three sides in terms of these moduli:
Given a point the remaining lengths are as follows (using the auxiliary implicitly determined by )
Finally, like we’ve done for pentagons and hexagons we provide an explicit realization in the upper half plane:
Up to isometry, every right angled hyperbolic heptagon with sides is bounded by the following seven geodesics, each given by its pair of endpoints in :
where the auxiliary quantities are
Proof by Subdivision
Begin by drawing the common perpendicular to the sides and , and label it . This divides the heptagon into a pentagon and hexagon, and since (by definition) our dividing line is perpendicular to both, the new shapes are right angled. Thus we can use our understanding of pentagons and hexagons to inductively build an understanding of heptagons.
The pentagon is determined by any pair of adjacent sides, so we may choose as they are full sides of the heptagon itself. In particular, these sides determine , which is shared by the right angled hexagon on the other side. Since such hexagons are uniquely determined by alternating triples of sides, the sides together with the already-determined fix all other lengths.
Now, all side lengths of the heptagon are fixed, and so it is determined up to isometry (as one can see by starting at a single point and creating edges one by one - there are no choices remaining be made at any step; so the construction is unique.) In our construction the adjacent lengths must construct a pentagon, and by the moduli space of pentagons we know Any arbitrary lengths determine a unique right angled hexagon, so there are no additional constraints on completing the proof of the theorem.
Getting Explicit
We now try to make this theorem effective: and actually come up with an explicit description of the remaining three side lengths in terms of . The argument will follow our subdivision construction above, but take more care to actually track the lengths of things. As a first step, we know by the trigonometry of pentagons that determine via
Knowing , its opposing side in the hexagon is calculable, using what we know of such hexagons (the hexagon law of cosines):
In the calculation of the remaining sides, we thus take as known, and freely use it in our expressions, along with . (Though we did not here, we will also do the same with ). The calculations of and are symmetric, so we proceed to just do , and at the end give the analogous formula for .
Label the two sub-segments of as , and the sub segments of by . Then is directly determined by the trigonometry of hyperbolic pentagons:
We determine from the fact that it lies in the upper hexagon, using the hyperbolic law of sines for right angled hexagons:
While rather messy, we now have a direct formula for :
Following the same for yields
Realization
We follow a very similar strategy here as for hexagons and pentagons, so describe the process more briefly. Up to isometries, we choose one vertex to be at , and align the sides meeting there to be segments of the vertical geodesic and unit circle:
The side of length meets the vertical side at a right angle - thus is represented by a circle of radius :
The geodesics also have easily calculable endpoints: is the image of the vertical geodesic under translation by distance along the unit circle, and is translation of the vertical geodesic along geodesic by distance . Exactly as in the hexagon case, this implies:
This leaves only two sides whose geodesics need to be computed: and .
Beginning with , we recall that we have already analyzed the common perpendicular of and , and we can leverage this knowledge to express as the result of translating the vertical geodesic along this perpendicular by distance . We know the hyperbolic distance at which this perpendicular intersects the vertical: it’s ‘’ units above the vertex at . Thus, we can compute the endpoints of much as we did for .
Where is determined by the heptagon moduli with the formula derived in the effective argument above: . Finally we turn to the side , where we attempt to use the same trick! The difference here is we do not already know the common perpendicular to the vertical side and , so we must compute it. Call it , and label the distance from the vertex to ‘s intersection with the vertical .
Then if we know and we are done: our required translation of the vertical geodesic has endpoints
To determine these two final missing quantities, note that determines a right angled pentagon
and the trigonometry of pentagons gives us what we need:
The same heptagon in the disk
The half-plane realization works, but it is untidy in two ways: the seven sides come out at wildly different scales, and getting the last two needed auxiliary quantities — , , , — that are not side lengths at all. Carrying everything to the Poincaré disk by the Cayley transform
removes both complaints.
In the disk a geodesic is an arc of a Euclidean circle meeting the boundary at right angles. Such a circle with center and radius has
so its center alone determines it, and two of them are perpendicular exactly when . Our vertex at went to the middle of the disk, so and pass through the center: they are diameters, and we take them along the two axes.
From there each side is fixed by its neighbors. is perpendicular to the diameter , so its center sits on that axis, a distance set by ; likewise on the other axis. For and , one coordinate comes from a dot product with the neighbor just placed and the other from the side separating it from a diameter.
Naming each geodesic by the center of the Euclidean circle carrying it,
and , being perpendicular to both and , is the solution of the two linear equations
That last line is the real gain. In the half plane the side was the awkward one: it needed its own common perpendicular , its own offset , and a fresh pentagon to compute them. Here it is a solve, and , , and have all disappeared — every center above is written in the side lengths alone.
In Code
The pentagon and hexagon notes each ended by turning a realization into a shader: a geodesic is the center of a circle, reflecting in it is inversion, and every pixel folds itself back into the fundamental polygon, its tile colored by how many reflections that took.
None of that machinery cares how many sides there are. The heptagon needs only its own list of walls.
Two of the seven sides pass through the center of the disk and are the axes. Four more are read straight off the theorem above. The last, , is the only one that takes any work — and in these coordinates being perpendicular to both its neighbors is two linear equations, so it is a solve:
float det = C[2].x * C[3].y - C[2].y * C[3].x;
C[4] = vec2(C[3].y - C[2].y, C[2].x - C[3].x) / det; // gamma_e
It is worth seeing what became of the auxiliaries. The half-plane realization needed four quantities that are not side lengths — , , and — and and existed only to place that one awkward side. In the disk, and survive because they are genuinely how is built, while and are gone entirely.
Here is the whole thing. Swap heptagonSides for the pentagon’s or the
hexagon’s, change the three array bounds, and the same program draws those
instead.
// A right-angled hyperbolic heptagon and the tiling it generates, in the
// Poincaré disk. Everything but heptagonSides is explained in the pentagon note.
//
// To run on Shadertoy, add these at the top; on the page they are uniforms —
// the four moduli from the sliders, the colors from the site palette.
//
// const float b = 2.0, c = 1.9, g = 1.45, e = 1.2;
// const vec3 uBackground = vec3(0.96, 0.95, 0.92);
// const vec3 uInk = vec3(0.15, 0.15, 0.16);
// const vec3 uBlue = vec3(0.20, 0.45, 0.70);
// const vec3 uGold = vec3(0.80, 0.62, 0.18);
// A geodesic is the center of its circle: orthogonality forces |C|^2 = 1 + r^2.
float radius2(vec2 C){ return dot(C, C) - 1.0; }
vec2 reflectIn(vec2 p, vec2 C){
vec2 d = p - C;
return C + radius2(C) * d / dot(d, d);
}
bool inside(vec2 p, vec2 C){
vec2 d = p - C;
return dot(d, d) < radius2(C);
}
// sinh of the distance to a geodesic, and to a diameter with unit normal n
float wallDist(vec2 p, vec2 C){
float r2 = radius2(C);
vec2 d = p - C;
return abs(dot(d, d) - r2) / (sqrt(r2) * (1.0 - dot(p, p)));
}
float axisDist(vec2 p, vec2 n){
return 2.0 * abs(dot(p, n)) / (1.0 - dot(p, p));
}
// gamma_a and gamma_b are the two axes; these are the other five sides.
// The subdivision supplies X and f; gamma_e is the one geodesic perpendicular
// to both of its neighbors, which here is a 2x2 solve.
void heptagonSides(out vec2 C[5]){
float X = acosh(sinh(b) * sinh(c));
float f = acosh((cosh(e) * cosh(g) + cosh(X)) / (sinh(e) * sinh(g)));
float k = asinh(cosh(c) / sinh(X));
float h = asinh(sinh(e) * sinh(f) / sinh(X));
float a = h + k;
C[0] = vec2(1.0 / tanh(b), 0.0); // gamma_c
C[1] = vec2(0.0, 1.0 / tanh(a)); // gamma_g
C[2] = vec2(tanh(b), 1.0 / (tanh(c) * cosh(b))); // gamma_d
C[3] = vec2(1.0 / (tanh(g) * cosh(a)), tanh(a)); // gamma_f
float det = C[2].x * C[3].y - C[2].y * C[3].x;
C[4] = vec2(C[3].y - C[2].y, C[2].x - C[3].x) / det; // gamma_e
}
// Walk a point back into the heptagon, counting reflections. The domain is the
// positive quadrant, outside all five circles.
int fold(inout vec2 p, vec2 C[5]){
int word = 0;
for (int step = 0; step < 48; step++){
bool moved = false;
if (p.x < 0.0){ p.x = -p.x; word++; moved = true; }
if (p.y < 0.0){ p.y = -p.y; word++; moved = true; }
for (int k = 0; k < 5; k++){
if (inside(p, C[k])){ p = reflectIn(p, C[k]); word++; moved = true; }
}
if (!moved) break;
}
return word;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
vec2 p = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y) * 1.04;
if (dot(p, p) >= 1.0){ fragColor = vec4(uBackground, 1.0); return; }
// b and c must build a pentagon before anything else exists
if (sinh(b) * sinh(c) <= 1.0){ fragColor = vec4(uBackground, 1.0); return; }
vec2 C[5];
heptagonSides(C);
int word = fold(p, C);
float s = min(axisDist(p, vec2(1.0, 0.0)), axisDist(p, vec2(0.0, 1.0)));
for (int k = 0; k < 5; k++) s = min(s, wallDist(p, C[k]));
vec3 col = mix(uBackground, (word % 2 == 0) ? uBlue : uGold, 0.30);
if (word == 0) col = mix(uBackground, uGold, 0.60);
col = mix(uInk, col, smoothstep(0.0, 2.0 * fwidth(s) + 0.012, s));
fragColor = vec4(col, 1.0);
}