1 | initial version |

Here is the same code as posted on MO. However, here i is the place to say some words about the sage part of the implementation.

```
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)
c1, c2, c3 = c(a1, a2, a3), c(a2, a3, a1), c(a3, a1, a2) # algebraic in the given example
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
# 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)
```

It was possible to perform "exact arithmetics". Sage comes with some models of the `HyperbolicPlane`

- and here they are:

```
sage: HP = HyperbolicPlane()
sage: HP.HM()
Hyperbolic plane in the Hyperboloid Model
sage: HP.Hyperboloid()
Hyperbolic plane in the Hyperboloid Model
sage: # same
sage: HP.KM()
Hyperbolic plane in the Klein Disk Model
sage: HP.KleinDisk()
Hyperbolic plane in the Klein Disk Model
sage: # same
sage: HP.PoincareDisk()
Hyperbolic plane in the Poincare Disk Model
sage: HP.PD()
Hyperbolic plane in the Poincare Disk Model
sage: # same
sage: HP.UHP()
Hyperbolic plane in the Upper Half Plane Model
sage: HP.realizations()
[Hyperbolic plane in the Upper Half Plane Model,
Hyperbolic plane in the Hyperboloid Model,
Hyperbolic plane in the Klein Disk Model,
Hyperbolic plane in the Poincare Disk Model]
```

In the implementation, we start with the given angles, use the hyperbolic laws of sine and cosine, see also the short pdf, solve a system in the `HM`

, then move it in the world of the upper half plane $\Bbb H$ by simple coercion, and there is for $\Bbb H=$`UHP`

a coercion from `HM`

:

```
UHP = HyperbolicPlane().UHP() # UHP is the upper half plane IH
HM = HyperbolicPlane().HM() # HM is the hyperboloid model
```

and now:

```
sage: UHP.has_coerce_map_from(HM)
True
```

After solving the system, your idea to use this framework, we obtain the vectors `V1, V2, V3`

, each with three coordinates, that should be considered now in `HM`

. It turns out that we have the right order of components.

For instance the solution `S2`

(instead of `V2`

) is:

```
sage: S2
(0, sqrt(2)*sqrt(sqrt(2) + 1), sqrt(2) + 1)
sage: s(S2, S2)
-(sqrt(2) + 1)^2 + 2*sqrt(2) + 2
sage: _.simplify_full()
-1
```

And the line is accepted:

```
sage: HM(S2)
Point in HM (0, sqrt(2)*sqrt(sqrt(2) + 1), sqrt(2) + 1)
```

so the bigger coordinate is at the last place. The point `HM(S2)`

becomes now `M2`

. Similarly, we have after solving the system and mapping the vectors to `HM`

also the other points `M1, M3`

. Sage gives us the chance to compute the distance between these points in the given model:

```
sage: # HM.dist(M1, M2) # arccosh( a long expression )
sage: HM.dist(M1, M2).n()
1.52857091948100
sage: arccosh(c3).n()
1.52857091948100
```

So the distance corresponds to the one stated in the linked pdf, second cosine rule in hyperbolic geometry. The distance between each two points is the same. For instance, we have symbolically the `True`

in the lines:

```
sage: bool( HM.dist(M1, M2) == HM.dist(M2, M3) )
True
sage: bool( HM.dist(M2, M3) == HM.dist(M3, M1) )
True
```

From here, we can pass to the upper half-plane $\Bbb H=$`UHP`

, and the points are also exact, to work with them as complex number we take the "coordinates", and we can convert them now to numbers in some exact field we prefer, for instance in $\bar{\Bbb Q}$ (or with some work in some cyclotomic field). Here are some test lines, that illustrate the structure, using

```
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()
x1, x2, x3 = QQbar(Q1), QQbar(Q2), QQbar(Q3)
```

we have:

```
sage: UHP.dist(H1, H2).n()
1.52857091948100
sage: arccosh(c3).n() # same distance in HM and UHP
1.52857091948100
sage: x1, x2, x3
(I, 4.611581789308715?*I, -1.805790894654358? + 1.162196641748775?*I)
sage: x1.minpoly(), x2.minpoly(), x3.minpoly()
(x^2 + 1, x^8 + 20*x^6 - 26*x^4 + 20*x^2 + 1, x^8 - 4*x^6 + 22*x^4 - 4*x^2 + 1)
sage: x2.minpoly().galois_group()
Transitive group number 4 of degree 8
sage: x2.minpoly().galois_group().structure_description()
'D4'
sage: x3.minpoly().galois_group()
Transitive group number 4 of degree 8
sage: x3.minpoly().galois_group().structure_description()
'D4'
```

This may seem less important for drawing one triangle, but may become important when trying to draw a full tesselation starting from one triangle and applying MÃ¶bius transformations, details in a deep level can be made visible (also in an animation with zoom-in features)...