Source code for flatsurf.geometry.categories.translation_surfaces
r"""
The category of translation surfaces.
This module provides shared functionality for all surfaces in sage-flatsurf
that are built from Euclidean polygons whose glued edges can be transformed
into each other with translations.
See :mod:`flatsurf.geometry.categories` for a general description of the
category framework in sage-flatsurf.
Normally, you won't create this (or any other) category directly. The correct
category is automatically determined for immutable surfaces.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: C = S.category()
sage: from flatsurf.geometry.categories import TranslationSurfaces
sage: C.is_subcategory(TranslationSurfaces())
True
"""
# ####################################################################
# This file is part of sage-flatsurf.
#
# Copyright (C) 2013-2019 Vincent Delecroix
# 2013-2019 W. Patrick Hooper
# 2023 Julian Rüth
#
# sage-flatsurf is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# sage-flatsurf is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with sage-flatsurf. If not, see <https://www.gnu.org/licenses/>.
# ####################################################################
from flatsurf.geometry.categories.surface_category import SurfaceCategoryWithAxiom
from flatsurf.geometry.categories.half_translation_surfaces import (
HalfTranslationSurfaces,
)
[docs]class TranslationSurfaces(SurfaceCategoryWithAxiom):
r"""
The category of surfaces built by gluing (Euclidean) polygons with
translations.
EXAMPLES::
sage: from flatsurf.geometry.categories import TranslationSurfaces
sage: TranslationSurfaces()
Category of translation surfaces
"""
# The category of translation surfaces is identical to the category of
# half-translation surfaces with the positive axiom.
_base_category_class_and_axiom = (HalfTranslationSurfaces, "Positive")
[docs] def extra_super_categories(self):
r"""
Return the other categories that a translation surface is automatically
a member of (apart from being a positive half-translation surface, its
orientation is compatible with the orientation of the polygons in the
real plane, so it's "oriented.")
EXAMPLES::
sage: from flatsurf.geometry.categories import TranslationSurfaces
sage: C = TranslationSurfaces()
sage: C.extra_super_categories()
(Category of oriented polygonal surfaces,)
"""
from flatsurf.geometry.categories.polygonal_surfaces import PolygonalSurfaces
return (PolygonalSurfaces().Oriented(),)
[docs] class ParentMethods:
r"""
Provides methods available to all translation surfaces in
sage-flatsurf.
If you want to add functionality for such surfaces you most likely want
to put it here.
"""
[docs] def is_translation_surface(self, positive=True):
r"""
Return whether this surface is a translation surface, i.e., return
``True``.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: S.is_translation_surface(positive=True)
True
sage: S.is_translation_surface(positive=False)
True
"""
return True
@staticmethod
def _is_translation_surface(surface, positive=True, limit=None):
r"""
Return whether ``surface`` is a translation surface by checking how its
polygons are glued.
This is a helper method for
:meth:`flatsurf.geometry.categories.similarity_surfaces.ParentMethods.is_translation_surface.
INPUT:
- ``surface`` -- an oriented similarity surface
- ``positive`` -- a boolean (default: ``True``); whether the
transformation must be a translation or is allowed to be a
half-translation, i.e., a translation followed by a reflection in
a point (equivalently, a rotation by π.)
- ``limit`` -- an integer or ``None`` (default: ``None``); if set, only
the first ``limit`` polygons are checked
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.infinite_staircase()
sage: from flatsurf.geometry.categories import TranslationSurfaces
sage: TranslationSurfaces.ParentMethods._is_translation_surface(S, limit=8)
True
::
sage: from flatsurf import Polygon, similarity_surfaces
sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)])
sage: S = similarity_surfaces.self_glued_polygon(P)
sage: TranslationSurfaces.ParentMethods._is_translation_surface(S)
False
sage: TranslationSurfaces.ParentMethods._is_translation_surface(S, positive=False)
True
"""
if "Oriented" not in surface.category().axioms():
raise NotImplementedError(
"cannot decide whether a non-oriented surface is a translation surface yet"
)
labels = surface.labels()
if limit is not None:
from itertools import islice
labels = islice(labels, limit)
checked = set()
for label in labels:
for edge in range(len(surface.polygon(label).vertices())):
cross = surface.opposite_edge(label, edge)
if cross is None:
continue
if cross in checked:
continue
checked.add((label, edge))
# We do not call self.edge_matrix() since the surface might
# have overridden this (just returning the identity matrix e.g.)
# and we want to deduce the matrix from the attached polygon
# edges instead.
from flatsurf.geometry.categories import SimilaritySurfaces
matrix = SimilaritySurfaces.Oriented.ParentMethods.edge_matrix.f( # pylint: disable=no-member
surface, label, edge
)
if not matrix.is_diagonal():
return False
if matrix[0][0] == 1 and matrix[1][1] == 1:
continue
if matrix[0][0] == -1 and matrix[1][1] == -1:
if not positive:
continue
return False
return True
[docs] def minimal_translation_cover(self):
r"""
Return the minimal cover of this surface that makes this surface a
translation surface, i.e., return this surface itself.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: S.minimal_translation_cover() is S
True
"""
return self
[docs] def edge_matrix(self, p, e=None):
r"""
Returns the 2x2 matrix representing a similarity which when
applied to the polygon with label `p` makes it so the edge `e`
can be glued to its opposite edge by translation.
Since this is a translation surface, this is just the identity.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: S.edge_matrix(0, 0)
[1 0]
[0 1]
"""
if e is None:
import warnings
warnings.warn(
"passing only a single tuple argument to edge_matrix() has been deprecated and will be deprecated in a future version of sage-flatsurf; pass the label and edge index as separate arguments instead"
)
p, e = p
if e < 0 or e >= len(self.polygon(p).vertices()):
raise ValueError("invalid edge index for this polygon")
from sage.all import identity_matrix
return identity_matrix(self.base_ring(), 2)
[docs] def canonicalize_mapping(self):
r"""
Return a SurfaceMapping canonicalizing this translation surface.
"""
from flatsurf.geometry.mappings import (
canonicalize_translation_surface_mapping,
)
return canonicalize_translation_surface_mapping(self)
[docs] def rel_deformation(self, deformation, local=False, limit=100):
r"""
Perform a rel deformation of the surface and return the result.
This algorithm currently assumes that all polygons affected by this deformation are
triangles. That should be fixable in the future.
INPUT:
- ``deformation`` (dictionary) - A dictionary mapping singularities of
the surface to deformation vectors (in some 2-dimensional vector
space). The rel deformation being done will move the singularities
(relative to each other) linearly to the provided vector for each
vertex. If a singularity is not included in the dictionary then the
vector will be treated as zero.
- ``local`` - (boolean) - If true, the algorithm attempts to deform all
the triangles making up the surface without destroying any of them.
So, the area of the triangle must be positive along the full interval
of time of the deformation. If false, then the deformation must have
a particular form: all vectors for the deformation must be parallel.
In this case we achieve the deformation with the help of the SL(2,R)
action and Delaunay triangulations.
- ``limit`` (integer) - Restricts the length of the size of SL(2,R)
deformations considered. The algorithm should be roughly worst time
linear in limit.
.. TODO::
- Support arbitrary rel deformations.
- Remove the requirement that triangles be used.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: s = translation_surfaces.arnoux_yoccoz(4)
sage: field = s.base_ring()
sage: a = field.gen()
sage: V = VectorSpace(field,2)
sage: deformation1 = {s.singularity(0,0):V((1,0))}
doctest:warning
...
UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead.
sage: s1 = s.rel_deformation(deformation1).canonicalize() # long time (.8s)
sage: deformation2 = {s.singularity(0,0):V((a,0))} # long time (see above)
sage: s2 = s.rel_deformation(deformation2).canonicalize() # long time (.6s)
sage: m = Matrix([[a,0],[0,~a]])
sage: s2.cmp((m*s1).canonicalize()) # long time (see above)
0
"""
s = self
# Find a common field
field = s.base_ring()
for singularity, v in deformation.items():
if v.parent().base_field() != field:
from sage.structure.element import get_coercion_model
cm = get_coercion_model()
field = cm.common_parent(field, v.parent().base_field())
from sage.modules.free_module import VectorSpace
vector_space = VectorSpace(field, 2)
from collections import defaultdict
vertex_deformation = defaultdict(
vector_space.zero
) # dictionary associating the vertices.
deformed_labels = set() # list of polygon labels being deformed.
for singularity, vect in deformation.items():
for label, coordinates in singularity.representatives():
v = self.polygon(label).get_point_position(coordinates).get_vertex()
vertex_deformation[(label, v)] = vect
deformed_labels.add(label)
assert len(s.polygon(label).vertices()) == 3
from flatsurf.geometry.euclidean import ccw
if local:
from flatsurf.geometry.surface import MutableOrientedSimilaritySurface
ss = MutableOrientedSimilaritySurface.from_surface(s)
ss.set_immutable()
ss = MutableOrientedSimilaritySurface.from_surface(
ss.change_ring(field)
)
us = ss
for label in deformed_labels:
polygon = s.polygon(label)
a0 = vector_space(polygon.vertex(1))
b0 = vector_space(polygon.vertex(2))
v0 = vector_space(vertex_deformation[(label, 0)])
v1 = vector_space(vertex_deformation[(label, 1)])
v2 = vector_space(vertex_deformation[(label, 2)])
a1 = v1 - v0
b1 = v2 - v0
# We deform by changing the triangle so that its vertices 1 and 2 have the form
# a0+t*a1 and b0+t*b1
# respectively. We are deforming from t=0 to t=1.
# We worry that the triangle degenerates along the way.
# The area of the deforming triangle has the form
# A0 + A1*t + A2*t^2.
A0 = ccw(a0, b0)
A1 = ccw(a0, b1) + ccw(a1, b0)
A2 = ccw(a1, b1)
if A2:
# Critical point of area function
c = A1 / (-2 * A2)
if field.zero() < c and c < 1:
if A0 + A1 * c + A2 * c**2 <= 0:
raise ValueError(
f"Triangle with label {label} degenerates at critical point before endpoint"
)
if A0 + A1 + A2 <= field.zero():
raise ValueError(
f"Triangle with label {label} degenerates at or before endpoint"
)
# Triangle does not degenerate.
from flatsurf import Polygon
us.replace_polygon(
label,
Polygon(
vertices=[vector_space.zero(), a0 + a1, b0 + b1],
base_ring=field,
),
)
ss.set_immutable()
return ss
else: # Non local deformation
# We can only do this deformation if all the rel vector are parallel.
# Check for this.
nonzero = None
for singularity, vect in deformation.items():
vvect = vector_space(vect)
if vvect != vector_space.zero():
if nonzero is None:
nonzero = vvect
else:
assert (
ccw(nonzero, vvect) == 0
), "In non-local deformation all deformation vectos must be parallel"
assert nonzero is not None, "Deformation appears to be trivial."
from sage.matrix.constructor import Matrix
m = Matrix([[nonzero[0], -nonzero[1]], [nonzero[1], nonzero[0]]])
mi = ~m
g = Matrix([[1, 0], [0, 2]], ring=field)
prod = m * g * mi
ss = None
k = 0
while True:
if ss is None:
from flatsurf.geometry.surface import (
MutableOrientedSimilaritySurface,
)
ss = MutableOrientedSimilaritySurface.from_surface(
s.change_ring(field),
category=TranslationSurfaces(),
)
else:
# In place matrix deformation
ss.apply_matrix(prod)
ss.delaunay_triangulation(direction=nonzero, in_place=True)
deformation2 = {}
for singularity, vect in deformation.items():
found_start = None
for label, coordinates in singularity.representatives():
v = (
s.polygon(label)
.get_point_position(coordinates)
.get_vertex()
)
if (
ccw(s.polygon(label).edge(v), nonzero) >= 0
and ccw(nonzero, -s.polygon(label).edge((v + 2) % 3))
> 0
):
found_start = (label, v)
found = None
for vv in range(3):
if (
ccw(ss.polygon(label).edge(vv), nonzero) >= 0
and ccw(
nonzero,
-ss.polygon(label).edge((vv + 2) % 3),
)
> 0
):
found = vv
deformation2[
ss.point(
label, ss.polygon(label).vertex(vv)
)
] = vect
break
assert found is not None
break
assert found_start is not None
try:
sss = ss.rel_deformation(deformation2, local=True)
except ValueError:
k += 1
if limit is not None and k >= limit:
raise Exception("exceeded limit iterations")
continue
sss = sss.apply_matrix(mi * g ** (-k) * m, in_place=False)
return sss.delaunay_triangulation(direction=nonzero)
[docs] def j_invariant(self):
r"""
Return the Kenyon-Smillie J-invariant of this translation surface.
It is assumed that the coordinates are defined over a number field.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: O = translation_surfaces.regular_octagon()
sage: O.j_invariant()
(
[2 2]
(0), (0), [2 1]
)
"""
it = iter(self.labels())
lab = next(it)
P = self.polygon(lab)
Jxx, Jyy, Jxy = P.j_invariant()
for lab in it:
xx, yy, xy = self.polygon(lab).j_invariant()
Jxx += xx
Jyy += yy
Jxy += xy
return (Jxx, Jyy, Jxy)
[docs] def erase_marked_points(self):
r"""
Return an isometric or similar surface with a minimal number of regular
vertices of angle 2π.
EXAMPLES::
sage: import flatsurf
sage: G = SymmetricGroup(4)
sage: S = flatsurf.translation_surfaces.origami(G('(1,2,3,4)'), G('(1,4,2,3)'))
sage: S.stratum()
H_2(2, 0)
sage: S.erase_marked_points().stratum() # optional: pyflatsurf # long time (1s) # random output due to matplotlib warnings with some combinations of setuptools and matplotlib
H_2(2)
sage: for (a,b,c) in [(1,4,11), (1,4,15), (3,4,13)]: # long time (10s), optional: pyflatsurf
....: T = flatsurf.polygons.triangle(a,b,c)
....: S = flatsurf.similarity_surfaces.billiard(T)
....: S = S.minimal_cover("translation")
....: print(S.erase_marked_points().stratum())
H_6(10)
H_6(2^5)
H_8(12, 2)
If the surface had no marked points then it is returned unchanged by this
function::
sage: O = flatsurf.translation_surfaces.regular_octagon()
sage: O.erase_marked_points() is O
True
TESTS:
Verify that https://github.com/flatsurf/flatsurf/issues/263 has been resolved::
sage: from flatsurf import Polygon, similarity_surfaces
sage: P = Polygon(angles=(10, 8, 3, 1, 1, 1), lengths=(1, 1, 2, 4))
sage: B = similarity_surfaces.billiard(P)
sage: S = B.minimal_cover(cover_type="translation")
sage: S = S.erase_marked_points() # long time (3s), optional: pyflatsurf
::
sage: from flatsurf import Polygon, similarity_surfaces
sage: P = Polygon(angles=(10, 7, 2, 2, 2, 1), lengths=(1, 1, 2, 3))
sage: B = similarity_surfaces.billiard(P)
sage: S_mp = B.minimal_cover(cover_type="translation")
sage: S = S_mp.erase_marked_points() # long time (3s), optional: pyflatsurf
"""
if all(a != 1 for a in self.angles()):
# no 2π angle
return self
from flatsurf.geometry.pyflatsurf_conversion import (
from_pyflatsurf,
to_pyflatsurf,
)
S = to_pyflatsurf(self)
S.delaunay()
S = S.eliminateMarkedPoints().surface()
S.delaunay()
return from_pyflatsurf(S)
def _test_translation_surface(self, **options):
r"""
Verify that this is a translation surface.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: S._test_translation_surface()
"""
tester = self._tester(**options)
limit = None
if not self.is_finite_type():
limit = 32
tester.assertTrue(
TranslationSurfaces.ParentMethods._is_translation_surface(
self, limit=limit
)
)
[docs] class FiniteType(SurfaceCategoryWithAxiom):
r"""
The category of translation surfaces built from finitely many polygons.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: s = translation_surfaces.octagon_and_squares()
sage: s.category()
Category of connected without boundary finite type translation surfaces
"""
[docs] class WithoutBoundary(SurfaceCategoryWithAxiom):
r"""
The category of translation surfaces without boundary built from
finitely many polygons.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: s = translation_surfaces.octagon_and_squares()
sage: s.category()
Category of connected without boundary finite type translation surfaces
"""
[docs] class ParentMethods:
r"""
Provides methods available to all translation surfaces that are
built from finitely many polygons.
If you want to add functionality for such surfaces you most likely
want to put it here.
"""
[docs] def stratum(self):
r"""
Return the stratum this surface belongs to.
This uses the package ``surface-dynamics``
(see http://www.labri.fr/perso/vdelecro/flatsurf_sage.html)
EXAMPLES::
sage: import flatsurf.geometry.similarity_surface_generators as sfg
sage: sfg.translation_surfaces.octagon_and_squares().stratum()
H_3(4)
"""
from surface_dynamics import AbelianStratum
from sage.rings.integer_ring import ZZ
return AbelianStratum([ZZ(a - 1) for a in self.angles()])
[docs] def canonicalize(self, in_place=None):
r"""
Return a canonical version of this translation surface.
EXAMPLES:
We will check if an element lies in the Veech group::
sage: from flatsurf import translation_surfaces
sage: s = translation_surfaces.octagon_and_squares()
sage: s
Translation Surface in H_3(4) built from 2 squares and a regular octagon
sage: from flatsurf.geometry.categories import TranslationSurfaces
sage: s in TranslationSurfaces()
True
sage: a = s.base_ring().gen()
sage: mat = Matrix([[1,2+a],[0,1]])
sage: s1 = s.canonicalize()
sage: s1.set_immutable()
sage: s2 = (mat*s).canonicalize()
sage: s2.set_immutable()
sage: s1.cmp(s2) == 0
True
sage: hash(s1) == hash(s2)
True
"""
if in_place is not None:
if in_place:
raise NotImplementedError(
"calling canonicalize(in_place=True) is not supported anymore"
)
import warnings
warnings.warn(
"the in_place keyword of canonicalize() has been deprecated and will be removed in a future version of sage-flatsurf"
)
s = self.delaunay_decomposition().standardize_polygons()
from flatsurf.geometry.surface import (
MutableOrientedSimilaritySurface,
)
s = MutableOrientedSimilaritySurface.from_surface(s)
from flatsurf.geometry.surface import (
MutableOrientedSimilaritySurface,
)
ss = MutableOrientedSimilaritySurface.from_surface(s)
for label in ss.labels():
ss.set_roots([label])
if ss.cmp(s) > 0:
s.set_roots([label])
# We have chosen the root label such that this surface is minimal.
# Now we relabel all the polygons so that they are natural
# numbers in the order of the walk on the surface.
labels = {label: i for (i, label) in enumerate(s.labels())}
s.relabel(labels, in_place=True)
s.set_immutable()
return s