1
$\begingroup$

I'm working on visualizing hyperbolic triangles given angles following a previous discussion on MathOverflow. The algorithm, as outlined in this MathOverflow answer, involves computing the side lengths of the triangle using the second hyperbolic law of cosines and then using these lengths to find the coordinates of the vertices on the hyperboloid model.

My Implementation

a1, a2, a3 = pi / 4, pi / 4, pi / 4
c1 = (cos(a1) + cos(a2) * cos(a3)) / sin(a2) * sin(a3)
c2 = (cos(a2) + cos(a3) * cos(a1)) / sin(a3) * sin(a1)
c3 = (cos(a3) + cos(a1) * cos(a2)) / sin(a1) * sin(a2)
a, b, p, q, r = var("a b p q r")

""" a, b, p, q, r are used in the following coordinates in Hyperboloid model
p1 = (0.0, 0.0, 1.0)
p2 = (0.0, a, b)
p3 = (p, q, r)
"""

eq = list()
eq += [b == c3]
eq += [r == c2]
eq += [a == sqrt((b ** 2) - 1)]
eq += [-c1 == a * q - b * r]
eq += [(p ** 2) == (r ** 2) - 1 - (q ** 2)]
solns = solve(eq, a, b, p, q, r, solution_dict=True)
solns = [[s[a].n(8), s[b].n(8), s[p].n(8), s[q].n(8), s[r].n(8)] for s in solns]
print(solns)
a, b, p, q, r = solns[0]  # pick the first solution
print(a, b, p, q, r)

# Pre-computed Constant for a, b, p, q, r for a1, a2, a3 = pi / 4, pi / 4, pi / 4
# a, b, p, q, r = 0.68, 1.2, -0.57, 0.37, 1.2

# Map the points in Hyperboloid to UHP
p1 = (0.1, 0.1)  # (0.0, 0.0)
p2 = (0.0 / 1 + b, a / 1 + b)
p3 = (p / 1 + r, q / 1 + r)
print(p1, p2, p3)

p1, p2, p3 = CC(p1), CC(p2), CC(p3)

p = hyperbolic_polygon(pts=[p1, p2, p3], model="UHP", fill=True, alpha=0.3)

UHP = HyperbolicPlane().UHP()
p1 = UHP.get_point(p1)
p2 = UHP.get_point(p2)
p3 = UHP.get_point(p3)
l1 = UHP.get_geodesic(p1, p2)
l2 = UHP.get_geodesic(p2, p3)
l3 = UHP.get_geodesic(p1, p3)
print(l1, l2, l3)
print(l1.angle(l2) * 180, l2.angle(l3) * 180, l3.angle(l1) * 180)

g = Graphics()
g += p.plot()
g.show(axes=True)

Question

The above code executes the algorithm and prints out the angles of the computed triangles (see e.g. here). However, when specifying angles of $\pi / 4$ for all three angles, the computed angles are 31.4577306707415, 45.6137310195116, and 1.36504071705956, which are incorrect. Can anyone spot the error in my implementation or suggest a correction to achieve the correct angles?

$\endgroup$
2
  • $\begingroup$ What happens if you put patentheses around the $1+b$ and $1+r$ denominators? It looks like they would be needed. $\endgroup$ Mar 22 at 15:24
  • $\begingroup$ We have now a closed topic, however the linked MO post has a formula, which crashes when implemented, it bring for instance a point of the hyperboloid model of the hyperbolic plane into the cusp zero, this should not happen. If adding visual content to a publication or project on modular forms or tesselations may be seen as part of research, then this question illustrates the problems one has when making things explicit. It gives also a starting point for having computations done in $\Bbb H$ in a structural manner. $\endgroup$
    – dan_fulea
    Mar 28 at 2:01

1 Answer 1

1
$\begingroup$

The following code works for me:

UHP = HyperbolicPlane().UHP()    # UHP is the upper half plane IH
HM  = HyperbolicPlane().HM()     # HM  is the hyperboloid model
a1, a2, a3 = pi/4, pi/4, pi/4    # given angles, we draw a hyperbolic triangle with these angles

def c(a1, a2, a3):
    return (cos(a1) + cos(a2)*cos(a3)) / sin(a2) / sin(a3)    # !!! sin(a3) is in denominator !!!

c1, c2, c3 = c(a1, a2, a3), c(a2, a3, a1), c(a3, a1, a2)    # algebraic in the given example
# sides are CC(arccosh(c1)), CC(arccosh(c2)), CC(arccosh(c3))

def s(v, w):
    """v, w are vectors with three entries, we return the Minkowski product with signature ++-"""
    return v*diagonal_matrix([1, 1, -1])*w    # no need of a transposition

# a, b, p, q, r are used in the following coordinates in Hyperboloid model
# as a parametrization of points
myvars = var("a b p q r");
a, b, p, q, r = myvars

V1 = vector([0, 0, 1])
V2 = vector([0, a, b])
V3 = vector([p, q, r])

sols = solve([ s(V2, V2) == -1, s(V3, V3) == -1,
               s(V1, V2) == -c3, s(V2, V3) == -c1, s(V3, V1) == -c2 ]
             , myvars, solution_dict=True)
sols = [sol for sol in sols if sol[a] > 0 and sol[q] > 0]    # so V2, V3 maps to IH
sol  = sols[0]                                               # first solution

a0, b0, p0, q0, r0 = [sol[v].simplify_full() for v in myvars]

S1, S2, S3 = vector([0, 0, 1]), vector([0, a0, b0]), vector([p0, q0, r0])
M1, M2, M3 = HM.get_point(S1), HM.get_point(S2), HM.get_point(S3)
H1, H2, H3 = UHP(M1), UHP(M2), UHP(M3)    # using the coercion from HM to UHP
Q1, Q2, Q3 = H1.coordinates(), H2.coordinates(), H3.coordinates()

p = hyperbolic_polygon(pts=[Q1, Q2, Q3], model="UHP", fill=True, alpha=0.3)

g = Graphics()
g += p.plot()
g.show(axes=True, aspect_ratio=1)

Then the picture is as follows, and when i would have to guess which fraction(s) of $\pi$ the angles may be, i'd pick the quarter:

mo question 467456 picture

sage comes with more models of the hyperbolic plane, UHP is one (OP), but the hyperboloid model HM is also present. And we have a coercion from one world to the other one:

sage: UHP.has_coerce_map_from(HM)
True

So at this point, the geometry works for us, we can simply solve the system and coerce from the HM-points directly to the UHP-points. The point $M_1=(0,0,1)$ goes by the way to the point $H_1=i\in\Bbb H$ (and not to the cusp zero, it is hard to go there in time.) All computed values are exact, the complex numbers corresponding to $H_1,H_2,H_3$ are $Q_1,Q_2,Q_3$, and for them we have for instance their minimal polynomial over $\Bbb Q$:

sage: Q1
I
sage: Q1.minpoly()
x^2 + 1

sage: Q2
(-I*sqrt(2) - 2*I)/((sqrt(2)*sqrt(sqrt(2) + 1) - 1)*(sqrt(2) + 1) + sqrt(2)*sqrt(sqrt(2) + 1) - 2*sqrt(2) - 3)
sage: Q2.minpoly()
x^8 + 20*x^6 - 26*x^4 + 20*x^2 + 1

sage: Q3
((sqrt(2) + 1)^(3/2) - I*sqrt(2) + sqrt(sqrt(2) + 1) - 2*I)/((sqrt(2) + 1)*(sqrt(sqrt(2) + 1) - 1) - 2*sqrt(2) + sqrt(sqrt(2) + 1) - 3)
sage: Q3.minpoly()
x^8 - 4*x^6 + 22*x^4 - 4*x^2 + 1


LATER EDIT: The above code is limited by solve to the cases when solutions can be obtained in this manner. The solve way was taken to respect the OP idea to get the points, still remaining in a standard, elementary situation. When angles like $\pi/5$, $\pi/5$, $\pi/5$ are taken instead, then solutions are no longer explicit, so the solve direction fails. So the above code is adapted to use an ideal in the polynomial ring in the same unknowns, the equations are easily transposed as generators of the ideal, and then the set of solutions is the "variety" associated to the ideal. From them, we pick also a first one that maps to $\Bbb H$. In order to be able to work algebraically, exact computations, $n_1,n_2,n_3$ should be below integers (or suitable rational numbers, numerators) not too big. The slightly changed code is as follows:

UHP = HyperbolicPlane().UHP()       # UHP is the upper half plane IH
HM  = HyperbolicPlane().HM()        # HM  is the hyperboloid model
n1, n2, n3 = 5, 5, 4
a1, a2, a3 = pi/n1, pi/n2, pi/n3    # given angles, we draw a hyperbolic triangle with these angles
N = 2*lcm([n1, n2, n3])
F.<u> = CyclotomicField(N)

def c(a1, a2, a3):
    return (cos(a1) + cos(a2)*cos(a3)) / sin(a2) / sin(a3)    # !!! sin(a3) is in denominator !!!

c1, c2, c3 = c(a1, a2, a3), c(a2, a3, a1), c(a3, a1, a2)    # algebraic in the given example
# sides are CC(arccosh(c1)), CC(arccosh(c2)), CC(arccosh(c3))

def toUHP(M):
    """M is a point in the HM model, we construct an ad-hoc version in UHP
    """
    a, b, c = M.coordinates()
    a, b, c = QQbar(a), QQbar(b), QQbar(c)
    return UHP(-(a + i)*(c + 1)/((b - 1)*c - a^2 - b^2 + b - 1))

def s(v, w):
    """v, w are vectors with three entries, we return the Minkowski product with signature ++-"""
    return v*diagonal_matrix([1, 1, -1])*w    # no need of a transposition

# a, b, p, q, r are used in the following coordinates in Hyperboloid model
# as a parametrization of points
R.<a,b,p,q,r> = PolynomialRing(F)

V1 = vector([0, 0, 1])
V2 = vector([0, a, b])
V3 = vector([p, q, r])

J = R.ideal([s(V2, V2) +  1, s(V3, V3) +  1,
             s(V1, V2) + c3, s(V2, V3) + c1, s(V3, V1) + c2])
V = J.variety(ring=QQbar)
sols = [sol for sol in V if sol[a] > 0 and sol[q] > 0]    # so V2, V3 maps to IH
sol  = sols[0]                                            # first solution

a0, b0, p0, q0, r0 = [sol[v].real() for v in R.gens()]

S1, S2, S3 = vector([0, 0, 1]), vector([0, a0, b0]), vector([p0, q0, r0])
M1, M2, M3 = HM.get_point(S1), HM.get_point(S2), HM.get_point(S3)
H1, H2, H3 = toUHP(M1), toUHP(M2), toUHP(M3)    # using the ad-hoc coercion from HM to UHP
Q1, Q2, Q3 = H1.coordinates(), H2.coordinates(), H3.coordinates()

p = hyperbolic_polygon(pts=[Q1, Q2, Q3], model="UHP", fill=True, alpha=0.3)

g = Graphics()
g += p.plot()
g.show(axes=True, aspect_ratio=1)

mse 467545


Let us check the angles:

sage: H1, H2, H3
(Point in UHP I,
 Point in UHP 7.753213798134539?*I,
 Point in UHP -2.453248512528677? + 1.317112574501655?*I)
sage: H12 = UHP.get_geodesic(H1, H2)
sage: H23 = UHP.get_geodesic(H2, H3)
sage: H31 = UHP.get_geodesic(H3, H1)
sage: H12.angle(H23)
arccos(0.8090169943749474?)
sage: cos(H12.angle(H23)) == cos(a2)
0.8090169943749474? == 1/4*sqrt(5) + 1/4
sage: bool(cos(H12.angle(H23)) == cos(a2))
True
sage: bool(cos(H23.angle(H31)) == cos(a3))
True
sage: bool(cos(H31.angle(H12)) == cos(a1))
True
$\endgroup$
2

Not the answer you're looking for? Browse other questions tagged or ask your own question.