Notes from the Warwick EPSRC Symposium on “Computation in geometric topology”

Veech group elements (affine symmetries)

Veech’s double n-gon surfaces:

from flatsurf import translation_surfaces

s = translation_surfaces.veech_double_n_gon(5).canonicalize()
s.plot()
../_images/5fa069f6160c81c48a4469cc372273de4abc996e5881c4d68e6afca54f5fddc9.png
p = s.polygon(0)
modulus = (p.vertex(3)[1] - p.vertex(2)[1]) / (p.vertex(2)[0] - p.vertex(4)[0])
AA(modulus)
0.3632712640026804?
m = matrix(s.base_ring(), [[1, 2], [0, 1]])
show(matrix(AA, m))
ss = m * s
ss.plot()
\(\displaystyle \left(\begin{array}{rr} 1 & 2 \\ 0 & 1 \end{array}\right)\)
../_images/0a29edbcad5d52af2abf91231a46ce97fd6cf0a3c6ab332614abe29c764e1261.png
ss = ss.delaunay_decomposition()
ss.plot()
/usr/share/miniconda3/envs/test/lib/python3.11/site-packages/flatsurf/geometry/categories/similarity_surfaces.py:2877: UserWarning: delaunay_decomposition() has been deprecated and will be removed in a future version of sage-flatsurf; use delaunay_decompose().codomain() instead
  warnings.warn(
../_images/7ea3b1b98a9c8d9227f71be9de3c166936e5d528c9f7e665f0432d940a15fba2.png

The following checks that the matrix m stabilizes s; actually, it does not, see #230:

ss.canonicalize() == s
False

Geodesics

s = translation_surfaces.veech_double_n_gon(5)

The tangent bundle of the surface:

TB = s.tangent_bundle()

Define a tangent vector in polygon \(0\) starting at \((\frac{1}{2}, 0)\) and pointed in some direction:

direction = s.polygon(0).vertex(2) + 3 * s.polygon(0).vertex(3)
v = TB(0, (1 / 2, 0), direction)

Convert the vector to a straight-line trajectory.

traj = v.straight_line_trajectory()
s.plot() + traj.plot()
../_images/21537f2914e7e6c956c2da9f1171bb2024a71db07992d4680ab88f8953b11d1d.png
traj.flow(1000)
print(traj.is_closed())
print(traj.combinatorial_length())
s.plot() + traj.plot()
True
40
../_images/5078b23762480015444d1adb3f4c3674059a5379b01f4adabbe07bb38c8acd46.png

Cone surfaces from polyhedra

Polyhedra are built into Sage and you can use them to build a translation surface. In this demo we only use a built-in function for a Platonic Solid.

from flatsurf.geometry.polyhedra import platonic_dodecahedron

polyhedron, s, mapping = platonic_dodecahedron()
polyhedron.plot(frame=False)
s.plot(polygon_labels=False, edge_labels=False)
../_images/e151d8a9a79e88a689b5446415132113147f24d7a58afa97d8f2c71ca7556238.png
TB = s.tangent_bundle()
direction = s.polygon(0).vertex(2) + 2 * s.polygon(0).vertex(3)
v = TB(0, (1 / 2, 0), direction)
traj = v.straight_line_trajectory()
traj.flow(100)
print(traj.is_closed())
print(traj.combinatorial_length())
True
90
s.plot() + traj.plot()
../_images/9db6311c8d0b7fd26c3bfa7a51e6306750d61aa9726bbb253f9bd335207b75f7.png
G = polyhedron.plot(frame=False, point=False, line=False, wireframe=None)
G += line3d(mapping(traj), radius=0.02, frame=False)
G
TB = s.tangent_bundle()
direction = s.polygon(0).vertex(2) + 3 * s.polygon(0).vertex(3)
v = TB(0, (1 / 2, 0), direction)
traj = v.straight_line_trajectory()
traj.flow(1000)
print(traj.is_closed())
print(traj.combinatorial_length())
True
320
show(s.plot() + traj.plot())
../_images/6d3e8d30cd4279682997c41bb5abd682748f99397a4079ba4e49f5b768c307fa.png
p = polyhedron.plot(frame=False, point=False, line=False, wireframe=None)
p += line3d(mapping(traj), radius=0.02, frame=False)
p.show(viewer="tachyon", frame=False)
../_images/88cfccdf1ef75e72fa1261994cb5cdd2d7e093082df9778b2890d5ce53b508bb.png

Relative period deformations

s = translation_surfaces.veech_2n_gon(5)
s.plot(edge_labels=False, polygon_labels=False)
../_images/95a6b83b1ac417152c38043277eb2298d638deec33c083ed2a9cf714ce32a96c.png

Currently we have to triangulate to do a rel deformation.

s = s.triangulate().codomain().relabel()

A singularity is an equivalence class of vertices of polygons.

sing = s.point(0, 0)
sing
Vertex 0 of polygon 0

We can now deform by moving one singularity relative to the others. Here is a small deformation in the slope one direction.

ss = s.rel_deformation({sing: vector(s.base_ring(), (1 / 20, 1 / 20))})
ss.plot()
../_images/b1262e54fdcb3a90767841389c708139162a6648cffb18e5cbcd19e9dcfec088.png

A larger deformation:

ss = s.rel_deformation({sing: vector(s.base_ring(), (100, 100))})
ss.plot()
../_images/565ae8b907464259738f4c9a369325f38d517e77b000b62e8bfbd36ddde85138.png

The Necker Cube Surface

I’m demonstrating a result (in progress) of Pavel Javornik, an undergraduate at City College of New York.

from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectory


class SurfaceToSpaceMapping(SageObject):
    def __init__(self, similarity_surface, transformation):
        self._s = similarity_surface
        from types import FunctionType

        if isinstance(transformation, FunctionType):
            self.transformation = transformation

    def transformation(self, label):
        r"""
        Return a pair (m, t) where m is a 3x2 matrix and t is a vector with 3 entries.

        The associated transformation from the polygon with the given label
        is v mapsto m*v + t where v is a point in the polygon.
        """
        return self._t[label]

    def image_polygon(self, label):
        r"""
        Return a 2-dimensional polyhedron in 3-space representing
        the image of the polygon with the given label.
        """
        p = self._s.polygon(label)
        m, t = self.transformation(label)
        vertices = [m * v + t for v in p.vertices()]
        return Polyhedron(vertices=vertices)

    def plot(
        self,
        labels,
        point=False,
        line=False,
        polygon=None,
        wireframe=None,
        frame=False,
        label_to_color=None,
    ):
        r"""
        Return a 3d plot of the polygonal images in 3-space
        corresponding to the collection of labels.

        The other parameters are passed to a Polyhedron.plot method
        and affect the rendering.
        """
        it = iter(labels)
        label = next(it)
        if label_to_color is None:
            p = self.image_polygon(label).plot(
                point=point,
                line=line,
                polygon=polygon,
                wireframe=wireframe,
                frame=frame,
                color="pink",
            )
        else:
            p = self.image_polygon(label).plot(
                point=point,
                line=line,
                polygon=polygon,
                wireframe=wireframe,
                frame=frame,
                color=label_to_color(label),
            )
        for label in it:
            if label_to_color is None:
                p += self.image_polygon(label).plot(
                    point=point,
                    line=line,
                    polygon=polygon,
                    wireframe=wireframe,
                    frame=frame,
                    color="pink",
                )
            else:
                p += self.image_polygon(label).plot(
                    point=point,
                    line=line,
                    polygon=polygon,
                    wireframe=wireframe,
                    frame=frame,
                    color=label_to_color(label),
                )
        from sage.modules.free_module_element import vector

        p.frame_aspect_ratio(
            tuple(vector(p.bounding_box()[1]) - vector(p.bounding_box()[0]))
        )
        return p

    def __call__(self, o):
        r"""
        This method is used to convert from an object on the surface to an object in space.

        Currently works with

        - ``StraightLineTrajectory`` -- returns the corresponding list of points in space
        - ``SegmentInPolygon`` -- returns the corresponding pair of points in space
        - ``SimilaritySurfaceTangentVector`` -- returns a pair of points corresponding
          to the image point and image of the tangent vector.
        """
        if isinstance(o, StraightLineTrajectory):
            points = []
            it = iter(o.segments())
            s = next(it)
            label = s.polygon_label()
            m, t = self.transformation(label)
            points.append(t + m * s.start().point())
            points.append(t + m * s.end().point())
            for s in it:
                label = s.polygon_label()
                m, t = self.transformation(label)
                points.append(t + m * s.end().point())
            return points
        if isinstance(o, SegmentInPolygon):
            # Return the pair of images of the endpoints.
            label = o.polygon_label()
            m, t = self.transformation(label)
            return (t + m * o.start().point(), t + m * o.end().point())
        if isinstance(o, SimilaritySurfaceTangentVector):
            # Map to a pair of vectors consisting of the image
            # of the basepoint and the image of the vector.
            label = o.polygon_label()
            m, t = self.transformation(label)
            point = o.point()
            vector = o.vector()
            return (t + m * point, m * vector)
        raise ValueError("Failed to recognize type of passed object")
from flatsurf.geometry.surface import OrientedSimilaritySurface
from flatsurf.geometry.categories import ConeSurfaces
from flatsurf.geometry.polygon import Polygon


class CubeSurf(OrientedSimilaritySurface):
    def __init__(self, F):
        self._faceA = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], base_ring=F)
        self._faceB = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], base_ring=F)
        self._faceC = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], base_ring=F)
        super().__init__(
            F,
            category=ConeSurfaces()
            .Rational()
            .InfiniteType()
            .WithoutBoundary()
            .Connected(),
        )

    def is_mutable(self):
        return False

    def is_compact(self):
        return False

    def roots(self):
        return ((0, 0, IntegerModRing(3)(0)),)

    def is_translation_surface(self, positive=True):
        return False

    def is_dilation_surface(self, positive=False):
        return False

    def __eq__(self, other):
        if not isinstance(other, CubeSurf):
            return False

        return self.base_ring() is other.base_ring()

    def __hash__(self):
        return hash(self.base_ring())

    def polygon(self, label):
        x, y, l = label
        if l == 0:
            return self._faceA
        if l == 1:
            return self._faceB
        if l == 2:
            return self._faceC

    def opposite_edge(self, label, edge):
        x, y, l = label
        # l(0) = A, l(1) = B, l(2) = C
        if l == 0:
            if edge == 0:
                return ((x, y - 1, l + 2), 2)
            if edge == 1:
                return ((x, y, l + 1), 3)
            if edge == 2:
                return ((x, y, l + 2), 0)
            if edge == 3:
                return ((x - 1, y, l + 1), 1)
        if l == 1:
            if edge == 0:
                return ((x + 1, y - 1, l + 1), 3)
            if edge == 1:
                return ((x + 1, y, l + 2), 3)
            if edge == 2:
                return ((x, y, l + 1), 1)
            if edge == 3:
                return ((x, y, l + 2), 1)
        if l == 2:
            if edge == 0:
                return ((x, y, l + 1), 2)
            if edge == 1:
                return ((x, y, l + 2), 2)
            if edge == 2:
                return ((x, y + 1, l + 1), 0)
            if edge == 3:
                return ((x - 1, y + 1, l + 2), 0)
s = CubeSurf(QQ)
TestSuite(s).run()
MM = matrix(QQ, [[0, 1, 0], [-1, 0, 0], [0, 0, 1]])


def transformation(label):
    M = MatrixSpace(QQ, 3, 2)
    V = VectorSpace(QQ, 3)
    x, y, l = label
    if l == 0:
        return MM * M([[1, 0], [0, 1], [0, 0]]), MM * V([x, y, -x - y])
    elif l == 1:
        return MM * M([[0, 0], [0, 1], [-1, 0]]), MM * V([x + 1, y, -x - y])
    else:  # l == 2
        return MM * M([[1, 0], [0, 0], [0, -1]]), MM * V([x, y + 1, -x - y])


m = SurfaceToSpaceMapping(s, transformation)


def label_to_color(label):
    if label[2] == 0:
        return "pink"
    if label[2] == 1:
        return "yellow"
    if label[2] == 2:
        return "beige"
from itertools import islice

m.plot(set(islice(s.labels(), 30)), label_to_color=label_to_color)

Theorem (Pavel Javornik). A trajectory of rational slope (measured on one of the squares interpreted to have horizontal and vertical sides) on the Necker Cube Surface closes up if and only if the slope can be expressed as the ratio of two odd integers.

B = s.tangent_bundle()

The following builds a trajectory starting in the base polygon at the point \((\frac{1}{4}, \frac{1}{4})\) and traveling in a direction of slope one.

v = B(s.root(), (1 / 4, 1 / 4), (-1, 1))
traj = v.straight_line_trajectory()
traj.flow(100)
if traj.is_closed():
    print("The trajectory closed up.")
labels = [seg.polygon_label() for seg in traj.segments()]
m.plot(labels, label_to_color=label_to_color) + line3d(m(traj), radius=0.02)
The trajectory closed up.

A trajectory of slope \(5/4\).

v = B(s.root(), (1 / 3, 1 / 4), (4, 5))
traj = v.straight_line_trajectory()
traj.flow(50)
labels = [seg.polygon_label() for seg in traj.segments()]
p = m.plot(labels, label_to_color=label_to_color) + line3d(
    m(traj), radius=0.04, label_to_color=label_to_color
)
p.frame_aspect_ratio(tuple(vector(p.bounding_box()[1]) - vector(p.bounding_box()[0])))
p

A trajectory of slope \(11/9\)

v = B(s.root(), (1 / 3, 1 / 4), (9, 11))
traj = v.straight_line_trajectory()
traj.flow(1000)
while not traj.is_closed():
    traj.flow(1000)
labels = [seg.polygon_label() for seg in traj.segments()]
p = m.plot(labels, label_to_color=label_to_color)
p += line3d(m(traj), radius=0.04)
p