The basics

This package lets you draw hyperbolic geometry using the Poincaré disk model.

The Poincaré disk

The Poincaré disk is a two-dimensional hyperbolic plane. It provides a surface on which you can draw using non-Euclidean geometry, in which most of Euclid's geometric postulates apply, but the final (fifth, "parallel") postulate doesn't. You'll have no trouble finding explanatory material on the internet!

In the Poincaré disk model, the shortest path between two points is drawn as a circular arc called a geodesic. The internal angles of triangles add up to less than 180°. Hyperbolic circles are represented as Euclidean circles contained entirely inside the disk.

Example block output
Note

Famous French mathematician and physicist Henri Poincaré (1854–1912) popularized the hyperbolic disk model in 1905 and it now carries his name. However, this was really a rediscovery of the original work of Eugenio Beltrami some decades earlier.

Overview

Points on the Poincaré disk are represented as complex numbers z, where |z| < 1.

The disk is a unit disk, with (0 + 0im) at the center. The four cardinal points (E, S, W, and N) are:

  • 1.0 + 0.0im
  • 0 + 1.0im
  • -1.0 + 0.0im
  • 0 - 1.0im
using PoincareDiskusing Luxor@drawsvg begin    background("black")    sethue("grey15")    draw_poincare_disk(action = :fill)    p1 = 1.0 + 0.0im  # E    p2 = 0 + 1.0im    # S    p3 = -1.0 + 0.0im # W    p4 = 0 - 1.0im    # N    sethue("cyan")    circle.(complex_to_point.([p1, p2, p3, p4]), 5, :fill)    label.(["E", "S", "W", "N"], [:w, :n, :e, :s], complex_to_point.([p1, p2, p3, p4]))end

In this package, the important functions are:

All other graphics functions are provided by Luxor.jl, and you can find the documentation for that package here.

The Luxor.jl method of supplying an action to a drawing function, such as :stroke or :fill is used here too.

Hyperbolic points

The hyperbolic_point(z) function places a hyperbolic point at complex coordinate z and draws a small circle on the current drawing.

There's a dotradius keyword that determines the radius of the Luxor circle used to mark the position. All other graphic properties are as set in Luxor.jl before you call the function.

The next example draws points on the disk using the form center + radius * θ.

using PoincareDiskusing Luxor@drawsvg begin    sethue("grey40")    draw_poincare_disk(action=:fill)    sethue("gold")    for θ in range(0, 2π, length=35)        za = 0.5 + 0.4 * exp(1im * θ)        hyperbolic_point(za, dotradius = 4)        zb = -0.5 + 0.4 * exp(1im * θ)        hyperbolic_point(zb, dotradius = 4)    endend
Example block output

The cis() function

In Julia you can use the cis() function to generate complex numbers. It provides a more efficient method for exp(im * x).

using PoincareDiskusing Luxor@drawsvg begin    sethue("grey40")    draw_poincare_disk(action=:fill)    sethue("gold")    for θ in range(0, 2π, length=35)        za = 0.5 + 0.4cis(θ)        hyperbolic_point(za, dotradius = 4)        zb = -0.5 + 0.4cis(θ)        hyperbolic_point(zb, dotradius = 4)    endend
Example block output

The Poincaré disk

You can add graphics for the Poincaré disk itself with:

draw_poincare_disk(action=:fill)

The size of the disk (in terms of the Luxor drawing) is determined by the global constant DEFAULT_DISK_RADIUS, which is initially set to 295.0 (so that the Poincare disk fits neatly on the default Luxor drawing size of 600 × 600). The default action is :stroke.

Hyperbolic lines

Use hyperbolic_line(z1, z2) to construct a hyperbolic line between z1 and z2.

These lines are called geodesics: they're arcs of circles that would meet the edge of the unit circle at right angles.

In this example, each hyperbolic line starts near the bottom edge (at z1 = Complex(0, 0.999999)) and draws a hyperbolic line from here to each of the positions around the edge generated by the loop. The Colors.Oklch function generates a pleasing set of shades.

using PoincareDiskusing Luxorusing Colors@drawsvg begin    sethue("grey10")    draw_poincare_disk(action = :fill)    setline(2)    z1 = 0.999999 * exp(π / 2 * im)    for θ in range(0, 2π - 2π / 50, length = 50)        sethue(Oklch(0.6, 0.6, 360rescale(θ, 0, 2π)))        z2 = 0.999999 * exp(θ * im)        hyperbolic_line(z1, z2) # default action is :stroke        circle(complex_to_point(z2), 5, :fill)    endend
Example block output

The complex_to_point(z) function converts from disk coordinate to Luxor drawing coordinates, so we can draw a circular dot using Luxor.circle() to mark the end point.

Hyperbolic circles

The hyperbolic_circle(zc, rho) function constructs a hyperbolic circle of hyperbolic radius rho centered at the disk point zc.

Hyperbolic circles are ordinary Euclidean circles, but the actual center/radius differs from the hyperbolic center/radius.

Notice in the next example that the two hyperbolic circles have the same hyperbolic radius (0.9) but look different sizes, because the centers are different.

using PoincareDiskusing Luxorusing Colors@drawsvg beginsethue("grey15")draw_poincare_disk(action = :fill)setopacity(0.6)sethue("magenta")zc = 0.7cis(π / 2)hyperbolic_circle(zc, 0.9) # default action is :strokesethue("cyan")zc = 0.3cis(π / 2)hyperbolic_circle(zc, 0.9, action = :fill)end

Here's a slightly more interesting example, that explores the idea that hyperbolic circles with the same designated 'radius' (here 0.3) look different depending on their distance from the center of the Poincaré disk.

using PoincareDiskusing Luxorusing Colors@drawsvg begin    sethue("grey15")    draw_poincare_disk(action = :fill)    setopacity(0.6)    for k in range(0.1, 0.9, length = 20)        for angle in [0, π / 2, π, 3π / 2]            sethue(Oklch(0.5, 0.5, 360k))            zc = k * cis(angle)            hyperbolic_circle(zc, 0.3, action = :fillpreserve)            sethue("white")            strokepath()        end    endend
Example block output

Hyperbolic polygons

The hyperbolic_poly() function constructs and draws a hyperbolic polygon. Each side of the polygon will be a geodesic curve. The default action is :stroke.

This function expects an array of complex number coordinates.

In this example, we use a simple function that generates an array of random positions on the disk.

function random_point_in_disk(r_max = 0.5)    θ = 2π * rand()    r = r_max * sqrt(rand())    return r * exp(im * θ)endsethue("grey15")draw_poincare_disk(action = :fill)setopacity(0.7)for i in 1:10    z = [random_point_in_disk(0.9) for _ in 1:3]    randomhue()    hyperbolic_poly(z, action = :fillpreserve)    sethue("white")    strokepath()end
Example block output

In the next example, we generate a polar grid of boxes, and render each one as a hyperbolic polygon.

@drawsvg begin    sethue("grey10")    draw_poincare_disk(action = :fill)    setline(2)    # radius from 0.2 to ~1    rs = range(0.2, 0.99999, length = 16)    # angle from 0 to 2π    θs = range(0, 2π, length = 16)    polargrid = [r * cis(θ) for r in rs, θ in θs]    let        fill = 0        for r in 1:(length(polargrid[1, :]) - 1)            for c in 1:(length(polargrid[:, 1]) - 1)                setgray(fill == 0 ? fill = 1 : fill = 0)                p1, p2, p3, p4 = polargrid[r, c],                    polargrid[r, c + 1],                    polargrid[r + 1, c + 1],                    polargrid[r + 1, c]                hyperbolic_poly([p1, p2, p3, p4], action = :fill)            end        end    endend
Example block output

A number of the drawing functions have the radius= keyword:

You'll need to use this keyword to specify the radius of the Poincaré disk if you're not using the default (295.0). Luxor default drawings are 600 × 600, so a disk with radius 295 fits nicely.

hyperbolic_poly() provides these keywords:

  • radius=DEFAULT_DISK_RADIUS
  • diskcenter=O
  • action=:stroke
  • steps=40

The utility function regular_hyperbolic_poly() generates a regular hyperbolic polygon, inside a hyperbolic circle of a given radius.

sethue("grey10")draw_poincare_disk(action = :fill)setline(1)sethue("white")for i in 0.1:0.1:6    vs = regular_hyperbolic_poly(5, i, rotation = -π/2)    hyperbolic_poly(vs, action=:stroke)end
Example block output