r"""
Generic mutable and immutable surfaces
This module provides base classes and implementations of surfaces. Most
surfaces in sage-flatsurf inherit from some of the classes in this module.
The most important class in this module is
:class:`MutableOrientedSimilaritySurface` which allows you to create a surface
by gluing polygons with similarities.
EXAMPLES:
We build a translation surface by gluing two hexagons, labeled 0 and 1::
sage: from flatsurf import MutableOrientedSimilaritySurface, polygons
sage: S = MutableOrientedSimilaritySurface(QuadraticField(3))
sage: S.add_polygon(polygons.regular_ngon(6))
0
sage: S.add_polygon(polygons.regular_ngon(6))
1
sage: S.glue((0, 0), (1, 3))
sage: S.glue((0, 1), (1, 4))
sage: S.glue((0, 2), (1, 5))
sage: S.glue((0, 3), (1, 0))
sage: S.glue((0, 4), (1, 1))
sage: S.glue((0, 5), (1, 2))
sage: S
Translation Surface built from 2 regular hexagons
We signal that the construction is complete. This refines the category of the
surface and makes more functionality available::
sage: S.set_immutable()
sage: S
Translation Surface in H_2(1^2) built from 2 regular hexagons
"""
# ********************************************************************
# This file is part of sage-flatsurf.
#
# Copyright (C) 2016-2020 Vincent Delecroix
# 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/>.
# ********************************************************************
import collections.abc
from sage.structure.parent import Parent
from sage.misc.cachefunc import cached_method
from flatsurf.geometry.surface_objects import SurfacePoint
[docs]class Surface_base(Parent):
r"""
A base class for all surfaces in sage-flatsurf.
This class patches bits of the category framework in SageMath that assume
that all parent structures are immutable.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: from flatsurf.geometry.surface import Surface_base
sage: isinstance(S, Surface_base)
True
"""
def _refine_category_(self, category):
r"""
Refine the category of this surface to a subcategory ``category``.
We need to override this method from ``Parent`` since we need to
disable a hashing check that is otherwise enabled when doctesting.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.category()
Category of finite type oriented similarity surfaces
sage: S._refine_category_(S.refined_category())
sage: S.category()
Category of connected without boundary finite type translation surfaces
"""
from sage.structure.debug_options import debug
old_refine_category_hash_check = debug.refine_category_hash_check
debug.refine_category_hash_check = False
try:
super()._refine_category_(category)
finally:
debug.refine_category_hash_check = old_refine_category_hash_check
# The (cached) an_element is going to fail its test suite because it has the wrong category now.
# Make sure that we create a new copy of an_element when requested.
self._cache_an_element = None
[docs] def an_element(self):
r"""
Return a point of this surface.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: S.an_element()
Point (1/2, 1/2) of polygon 0
"""
# Do not use the builtin caching of an_element if this surface is mutable.
if self.is_mutable():
return self._an_element_()
return super().an_element()
[docs]class MutablePolygonalSurface(Surface_base):
r"""
A base class for mutable surfaces that are built by gluing polygons.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf.geometry.surface import MutablePolygonalSurface
sage: isinstance(S, MutablePolygonalSurface)
True
"""
[docs] def __init__(self, base, category=None):
from sage.all import ZZ
self._next_label = ZZ(0)
self._roots = ()
self._polygons = {}
self._mutable = True
super().__init__(base, category=category)
def _test_refined_category(self, **options):
r"""
Test that this surface has been refined to its best possible
subcategory (that can be computed cheaply.)
We override this method here to disable this check for mutable
surfaces. Mutable surfaces have not been refined yet since changes in
the surface might require a widening of the category which is not
possible.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S._test_refined_category()
sage: S.set_immutable()
sage: S._test_refined_category()
"""
if self._mutable:
return
super()._test_refined_category(**options)
[docs] def add_polygon(self, polygon, *, label=None):
r"""
Add an unglued polygon to this surface and return its label.
INPUT:
- ``polygon`` -- a simple Euclidean polygon
- ``label`` -- a hashable identifier or ``None`` (default: ``None``);
if ``None`` an integer identifier is automatically selected
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square(), label=0)
Traceback (most recent call last):
...
ValueError: polygon label already present in this surface
sage: S.add_polygon(polygons.square(), label='X')
'X'
"""
if not self._mutable:
raise Exception("cannot modify an immutable surface")
if label is None:
while self._next_label in self._polygons:
self._next_label += 1
label = self._next_label
if label in self._polygons:
raise ValueError("polygon label already present in this surface")
self._polygons[label] = polygon.change_ring(self.base_ring())
return label
[docs] def add_polygons(self, polygons):
r"""
Add several polygons with automatically assigned labels at once.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygons([polygons.square(), polygons.square()])
doctest:warning
...
UserWarning: add_polygons() has been deprecated and will be removed in a future version of sage-flatsurf; use labels = [add_polygon(p) for p in polygons] instead
[0, 1]
"""
import warnings
warnings.warn(
"add_polygons() has been deprecated and will be removed in a future version of sage-flatsurf; use labels = [add_polygon(p) for p in polygons] instead"
)
return [self.add_polygon(p) for p in polygons]
[docs] def set_default_graphical_surface(self, graphical_surface):
r"""
EXAMPLES:
This has been disabled because it tends to break caching::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.set_default_graphical_surface(S.graphical_surface())
Traceback (most recent call last):
...
NotImplementedError: set_default_graphical_surface() has been removed from this version of sage-flatsurf. If you want to change the default plotting of a surface, create a subclass and override graphical_surface() instead
"""
raise NotImplementedError(
"set_default_graphical_surface() has been removed from this version of sage-flatsurf. If you want to change the default plotting of a surface, create a subclass and override graphical_surface() instead"
)
[docs] def remove_polygon(self, label):
r"""
Remove the polygon with label ``label`` from this surface (and all data
associated to it.)
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.remove_polygon(0)
sage: S.add_polygon(polygons.square())
0
"""
if not self._mutable:
raise Exception("cannot modify an immutable surface")
self._polygons.pop(label)
self._roots = tuple(root for root in self._roots if root != label)
[docs] def roots(self):
r"""
Return a label for each connected component on this surface.
This implements :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.roots()
()
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.roots()
(0,)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
1
sage: S.roots()
(0, 1)
sage: S.glue((0, 0), (1, 0))
sage: S.roots()
(0,)
.. SEEALSO::
:meth:`components`
"""
return LabeledView(
self, RootedComponents_MutablePolygonalSurface(self).keys(), finite=True
)
[docs] def components(self):
r"""
Return the connected components as the sequence of their respective
polygon labels.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.components()
()
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.components()
((0,),)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
1
sage: S.components()
((0,), (1,))
sage: S.glue((0, 0), (1, 0))
sage: S.components()
((0, 1),)
"""
return LabeledView(
self, RootedComponents_MutablePolygonalSurface(self).values(), finite=True
)
[docs] def polygon(self, label):
r"""
Return the polygon with label ``label`` in this surface.
This implements
:meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.polygon(0)
Traceback (most recent call last):
...
KeyError: 0
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.polygon(0)
Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])
"""
return self._polygons[label]
[docs] def set_immutable(self):
r"""
Make this surface immutable.
Any mutation attempts from now on will be an error.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.glue((0, 0), (0, 2))
sage: S.glue((0, 1), (0, 3))
Note that declaring a surface immutable refines its category and
thereby unlocks more methods that are available to such a surface::
sage: S.category()
Category of finite type oriented similarity surfaces
sage: old_methods = set(method for method in dir(S) if not method.startswith('_'))
sage: S.set_immutable()
sage: S.category()
Category of connected without boundary finite type translation surfaces
sage: new_methods = set(method for method in dir(S) if not method.startswith('_'))
sage: new_methods - old_methods
{'apply_matrix',
'area',
'canonicalize',
'canonicalize_mapping',
'erase_marked_points',
'holonomy_field',
'is_veering_triangulated',
'j_invariant',
'l_infinity_delaunay_triangulation',
'minimal_translation_cover',
'normalized_coordinates',
'rel_deformation',
'stratum',
'veering_triangulation'}
An immutable surface cannot be mutated anymore::
sage: S.remove_polygon(0)
Traceback (most recent call last):
...
Exception: cannot modify an immutable surface
However, the category of an immutable might be further refined as
(expensive to determine) features of the surface are deduced.
"""
if self._mutable:
self.set_roots(self.roots())
self._mutable = False
self._refine_category_(self.refined_category())
[docs] def is_mutable(self):
r"""
Return whether this surface can be modified.
This implements
:meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.is_mutable()
True
sage: S.set_immutable()
sage: S.is_mutable()
False
"""
return self._mutable
[docs] def __eq__(self, other):
r"""
Return whether this surface is indistinguishable from ``other``.
See
:meth:`~.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.ParentMethods._test_eq_surface`
for details on this notion of equality.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: T = MutableOrientedSimilaritySurface(QQ)
sage: S == T
True
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S == T
False
TESTS::
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: T = MutableOrientedSimilaritySurface(QQ)
sage: S != T
False
sage: S.add_polygon(polygons.square())
0
sage: S != T
True
"""
if not isinstance(other, MutablePolygonalSurface):
return False
if self.base() != other.base():
return False
if self._polygons != other._polygons:
return False
# Note that the order of the root labels matters since it changes the order of iteration in labels()
if self._roots != other._roots:
return False
if self._mutable != other._mutable:
return False
return True
[docs] def __hash__(self):
r"""
Return a hash value for this surface that is compatible with
:meth:`__eq__`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: T = MutableOrientedSimilaritySurface(QQ)
sage: hash(S) == hash(T)
Traceback (most recent call last):
...
TypeError: cannot hash a mutable surface
sage: S.set_immutable()
sage: T.set_immutable()
sage: hash(S) == hash(T)
True
"""
if self._mutable:
raise TypeError("cannot hash a mutable surface")
return hash((tuple(self.labels()), tuple(self.polygons()), self._roots))
def _repr_(self):
r"""
Return a printable representation of this surface.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S
Empty Surface
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S
Translation Surface with boundary built from a square
"""
if not self.is_finite_type():
return "Surface built from infinitely many polygons"
if len(self.labels()) == 0:
return "Empty Surface"
return f"{self._describe_surface()} built from {self._describe_polygons()}"
def _describe_surface(self):
r"""
Return a string describing this kind of surface.
This is a helper method for :meth:`_repr_`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S._describe_surface()
'Translation Surface'
"""
return "Surface"
def _describe_polygons(self):
r"""
Return a string describing the nature of the polygons that make up this surface.
This is a helper method for :meth:`_repr_`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S._describe_polygons()
''
"""
polygons = [
(-len(p.erase_marked_vertices().vertices()), p.describe_polygon())
for p in self.polygons()
]
polygons.sort()
polygons = [description for (edges, description) in polygons]
if not polygons:
return ""
collated = []
while polygons:
count = 1
polygon = polygons.pop()
while polygons and polygons[-1] == polygon:
count += 1
polygons.pop()
if count == 1:
collated.append(f"{polygon[0]} {polygon[1]}")
else:
collated.append(f"{count} {polygon[2]}")
description = collated.pop()
if collated:
description = ", ".join(collated) + " and " + description
return description
[docs] def set_root(self, root):
r"""
Set ``root`` as the label at which exploration of a connected component
starts.
This method can be called for connected and disconnected surfaces. In
either case, it establishes ``root`` as the new label from which
enumeration of the connected component containing it starts. If another
label for this component had been set earlier, it is replaced.
.. NOTE::
After roots have been declared explicitly, gluing operations come
at an additional cost since the root labels have to be updated
sometimes. It is therefore good practice to declare the root labels
after all the gluings have been established when creating a
surface.
INPUT:
- ``root`` -- a polygon label in this surface
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.set_root(0)
Traceback (most recent call last):
...
ValueError: root must be a label in the surface
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: S.set_root(0)
sage: S.set_root(1)
sage: S.roots()
(0, 1)
Note that the roots get updated automatically when merging components::
sage: S.glue((0, 0), (1, 0))
sage: S.roots()
(0,)
The purpose of changing the root label is to modify the order of
exploration, e.g., in :meth:`labels`::
sage: S.labels()
(0, 1)
sage: S.set_root(1)
sage: S.labels()
(1, 0)
.. SEEALSO::
:meth:`set_roots` to replace all the root labels
"""
if not self._mutable:
raise Exception("cannot modify an immutable surface")
root = [label for label in self.labels() if label == root]
if not root:
raise ValueError("root must be a label in the surface")
assert len(root) == 1
root = root[0]
component = [component for component in self.components() if root in component]
assert len(component) == 1
component = component[0]
self._roots = tuple(r for r in self._roots if r not in component) + (root,)
[docs] def set_roots(self, roots):
r"""
Declare that the labels in ``roots`` are the labels from which their
corresponding connected components should be enumerated.
There must be at most one label for each connected component in
``roots``. Components that have no label set explicitly will have their
label chosen automatically.
INPUT:
- ``roots`` -- a sequence of polygon labels in this surface
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: S.add_polygon(polygons.square())
2
sage: S.glue((0, 0), (1, 0))
sage: S.set_roots([1])
sage: S.roots()
(1, 2)
Setting the roots of connected components affects their enumeration in :meth:`labels`::
sage: S.labels()
(1, 0, 2)
sage: S.set_roots([0, 2])
sage: S.labels()
(0, 1, 2)
There must be at most one root per component::
sage: S.set_roots([0, 1, 2])
Traceback (most recent call last):
...
ValueError: there must be at most one root label for each connected component
"""
if not self._mutable:
raise Exception("cannot modify an immutable surface")
roots = [[label for label in self.labels() if label == root] for root in roots]
if any(len(root) == 0 for root in roots):
raise ValueError("roots must be existing labels in the surface")
assert all(len(root) == 1 for root in roots)
roots = tuple(root[0] for root in roots)
for component in self.components():
if len([root for root in roots if root in component]) > 1:
raise ValueError(
"there must be at most one root label for each connected component"
)
self._roots = tuple(roots)
[docs] def change_base_label(self, label):
r"""
This is a deprecated alias for :meth:`set_root`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.change_base_label(0)
doctest:warning
...
UserWarning: change_base_label() has been deprecated and will be removed in a future version of sage-flatsurf; use set_root() instead
"""
import warnings
warnings.warn(
"change_base_label() has been deprecated and will be removed in a future version of sage-flatsurf; use set_root() instead"
)
self.set_root(label)
[docs] @cached_method
def labels(self):
r"""
Return the polygon labels in this surface.
This replaces the generic
:meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`
method with a more efficient implementation.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.labels()
()
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.labels()
(0,)
.. SEEALSO::
:meth:`polygon` to translate polygon labels to the corresponding polygons
:meth:`polygons` for the corresponding sequence of polygons
"""
return LabelsFromView(self, self._polygons.keys(), finite=True)
[docs] @cached_method
def polygons(self):
r"""
Return the polygons that make up this surface.
The order the polygons are returned is guaranteed to be compatible with
the order of the labels in :meth:`~.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`.
This replaces the generic
:meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygons`
with a more efficient implementation.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.polygons()
()
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: S.add_polygon(polygons.square())
2
sage: S.polygons()
(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]), Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]), Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]))
.. SEEALSO::
:meth:`polygon` to get a single polygon
"""
return Polygons_MutableOrientedSimilaritySurface(self)
[docs]class OrientedSimilaritySurface(Surface_base):
r"""
Base class for surfaces built from Euclidean polygons that are glued with
orientation preserving similarities.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf.geometry.surface import OrientedSimilaritySurface
sage: isinstance(S, OrientedSimilaritySurface)
True
"""
Element = SurfacePoint
[docs] def __init__(self, base, category=None):
from sage.categories.all import Rings
if base not in Rings():
raise TypeError("base ring must be a ring")
from flatsurf.geometry.categories import SimilaritySurfaces
if category is None:
category = SimilaritySurfaces().Oriented()
category &= SimilaritySurfaces().Oriented()
super().__init__(base, category=category)
def _describe_surface(self):
if not self.is_finite_type():
return "Surface built from infinitely many polygons"
if not self.is_connected():
# Many checks do not work yet if a surface is not connected, so we stop here.
return "Disconnected Surface"
if self.is_translation_surface(positive=True):
description = "Translation Surface"
elif self.is_translation_surface(positive=False):
description = "Half-Translation Surface"
elif self.is_dilation_surface(positive=True):
description = "Positive Dilation Surface"
elif self.is_dilation_surface(positive=False):
description = "Dilation Surface"
elif self.is_cone_surface():
description = "Cone Surface"
if self.is_rational_surface():
description = f"Rational {description}"
else:
description = "Surface"
if hasattr(self, "stratum"):
try:
description += f" in {self.stratum()}"
except NotImplementedError:
# Computation of the stratum might fail due to #227.
pass
elif self.genus is not NotImplemented:
description = f"Genus {self.genus()} {description}"
if self.is_with_boundary():
description += " with boundary"
return description
[docs]class MutableOrientedSimilaritySurface_base(OrientedSimilaritySurface):
r"""
Base class for surface built from Euclidean polyogns glued by orientation
preserving similarities.
This provides the features of :class:`MutableOrientedSimilaritySurface`
without making a choice about how data is stored internally; it is a
generic base class for other implementations of mutable surfaces.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf.geometry.surface import MutableOrientedSimilaritySurface_base
sage: isinstance(S, MutableOrientedSimilaritySurface_base)
True
"""
[docs] def triangle_flip(self, l1, e1, in_place=False, test=False, direction=None):
r"""
Overrides
:meth:`.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.triangle_flip`
to provide in-place flipping of triangles.
See that method for details.
"""
if not in_place:
return super().triangle_flip(
l1=l1, e1=e1, in_place=in_place, test=test, direction=direction
)
s = self
p1 = s.polygon(l1)
if not len(p1.vertices()) == 3:
raise ValueError("The polygon with the provided label is not a triangle.")
l2, e2 = s.opposite_edge(l1, e1)
sim = s.edge_transformation(l2, e2)
p2 = s.polygon(l2)
if not len(p2.vertices()) == 3:
raise ValueError(
"The polygon opposite the provided edge is not a triangle."
)
from flatsurf import Polygon
p2 = Polygon(vertices=[sim(v) for v in p2.vertices()], base_ring=p1.base_ring())
if direction is None:
direction = (s.base_ring() ** 2)((0, 1))
# Get vertices corresponding to separatices in the provided direction.
v1 = p1.find_separatrix(direction=direction)[0]
v2 = p2.find_separatrix(direction=direction)[0]
# Our quadrilateral has vertices labeled:
# * 0=p1.vertex(e1+1)=p2.vertex(e2)
# * 1=p1.vertex(e1+2)
# * 2=p1.vertex(e1)=p2.vertex(e2+1)
# * 3=p2.vertex(e2+2)
# Record the corresponding vertices of this quadrilateral.
q1 = (3 + v1 - e1 - 1) % 3
q2 = (2 + (3 + v2 - e2 - 1) % 3) % 4
new_diagonal = p2.vertex((e2 + 2) % 3) - p1.vertex((e1 + 2) % 3)
# This list will store the new triangles which are being glued in.
# (Unfortunately, they may not be cyclically labeled in the correct way.)
new_triangle = []
try:
new_triangle.append(
Polygon(
edges=[
p1.edge((e1 + 2) % 3),
p2.edge((e2 + 1) % 3),
-new_diagonal,
],
base_ring=p1.base_ring(),
)
)
new_triangle.append(
Polygon(
edges=[
p2.edge((e2 + 2) % 3),
p1.edge((e1 + 1) % 3),
new_diagonal,
],
base_ring=p1.base_ring(),
)
)
# The above triangles would be glued along edge 2 to form the diagonal of the quadrilateral being removed.
except ValueError:
raise ValueError(
"Gluing triangles along this edge yields a non-convex quadrilateral."
)
# Find the separatrices of the two new triangles, and in particular which way they point.
new_sep = []
new_sep.append(new_triangle[0].find_separatrix(direction=direction)[0])
new_sep.append(new_triangle[1].find_separatrix(direction=direction)[0])
# The quadrilateral vertices corresponding to these separatrices are
# new_sep[0]+1 and (new_sep[1]+3)%4 respectively.
# i=0 if the new_triangle[0] should be labeled l1 and new_triangle[1] should be labeled l2.
# i=1 indicates the opposite labeling.
if new_sep[0] + 1 == q1:
assert (new_sep[1] + 3) % 4 == q2
i = 0
else:
assert (new_sep[1] + 3) % 4 == q1
assert new_sep[0] + 1 == q2
i = 1
# These quantities represent the cyclic relabeling of triangles needed.
cycle1 = (new_sep[i] - v1 + 3) % 3
cycle2 = (new_sep[1 - i] - v2 + 3) % 3
# This will be the new triangle with label l1:
tri1 = Polygon(
edges=[
new_triangle[i].edge(cycle1),
new_triangle[i].edge((cycle1 + 1) % 3),
new_triangle[i].edge((cycle1 + 2) % 3),
],
base_ring=p1.base_ring(),
)
# This will be the new triangle with label l2:
tri2 = Polygon(
edges=[
new_triangle[1 - i].edge(cycle2),
new_triangle[1 - i].edge((cycle2 + 1) % 3),
new_triangle[1 - i].edge((cycle2 + 2) % 3),
],
base_ring=p1.base_ring(),
)
# In the above, edge 2-cycle1 of tri1 would be glued to edge 2-cycle2 of tri2
diagonal_glue_e1 = 2 - cycle1
diagonal_glue_e2 = 2 - cycle2
assert p1.find_separatrix(direction=direction) == tri1.find_separatrix(
direction=direction
)
assert p2.find_separatrix(direction=direction) == tri2.find_separatrix(
direction=direction
)
# Two opposite edges will not change their labels (label,edge) under our regluing operation.
# The other two opposite ones will change and in fact they change labels.
# The following finds them (there are two cases).
# At the end of the if statement, the following will be true:
# * new_glue_e1 and new_glue_e2 will be the edges of the new triangle with label l1 and l2 which need regluing.
# * old_e1 and old_e2 will be the corresponding edges of the old triangles.
# (Note that labels are swapped between the pair. The appending 1 or 2 refers to the label used for the triangle.)
if p1.edge(v1) == tri1.edge(v1):
# We don't have to worry about changing gluings on edge v1 of the triangles with label l1
# We do have to worry about the following edge:
new_glue_e1 = (
3 - diagonal_glue_e1 - v1
) # returns the edge which is neither diagonal_glue_e1 nor v1.
# This corresponded to the following old edge:
old_e1 = 3 - e1 - v1 # Again this finds the edge which is neither e1 nor v1
else:
temp = (v1 + 2) % 3
assert p1.edge(temp) == tri1.edge(temp)
# We don't have to worry about changing gluings on edge (v1+2)%3 of the triangles with label l1
# We do have to worry about the following edge:
new_glue_e1 = (
3 - diagonal_glue_e1 - temp
) # returns the edge which is neither diagonal_glue_e1 nor temp.
# This corresponded to the following old edge:
old_e1 = (
3 - e1 - temp
) # Again this finds the edge which is neither e1 nor temp
if p2.edge(v2) == tri2.edge(v2):
# We don't have to worry about changing gluings on edge v2 of the triangles with label l2
# We do have to worry about the following edge:
new_glue_e2 = (
3 - diagonal_glue_e2 - v2
) # returns the edge which is neither diagonal_glue_e2 nor v2.
# This corresponded to the following old edge:
old_e2 = 3 - e2 - v2 # Again this finds the edge which is neither e2 nor v2
else:
temp = (v2 + 2) % 3
assert p2.edge(temp) == tri2.edge(temp)
# We don't have to worry about changing gluings on edge (v2+2)%3 of the triangles with label l2
# We do have to worry about the following edge:
new_glue_e2 = (
3 - diagonal_glue_e2 - temp
) # returns the edge which is neither diagonal_glue_e2 nor temp.
# This corresponded to the following old edge:
old_e2 = (
3 - e2 - temp
) # Again this finds the edge which is neither e2 nor temp
# remember the old gluings.
old_opposite1 = s.opposite_edge(l1, old_e1)
old_opposite2 = s.opposite_edge(l2, old_e2)
us = s
# Replace the triangles.
us.replace_polygon(l1, tri1)
us.replace_polygon(l2, tri2)
# Glue along the new diagonal of the quadrilateral
us.glue((l1, diagonal_glue_e1), (l2, diagonal_glue_e2))
# Now we deal with that pair of opposite edges of the quadrilateral that need regluing.
# There are some special cases:
if old_opposite1 == (l2, old_e2):
# These opposite edges were glued to each other.
# Do the same in the new surface:
us.glue((l1, new_glue_e1), (l2, new_glue_e2))
else:
if old_opposite1 == (l1, old_e1):
# That edge was "self-glued".
us.glue((l2, new_glue_e2), (l2, new_glue_e2))
else:
# The edge (l1,old_e1) was glued in a standard way.
# That edge now corresponds to (l2,new_glue_e2):
us.glue((l2, new_glue_e2), (old_opposite1[0], old_opposite1[1]))
if old_opposite2 == (l2, old_e2):
# That edge was "self-glued".
us.glue((l1, new_glue_e1), (l1, new_glue_e1))
else:
# The edge (l2,old_e2) was glued in a standard way.
# That edge now corresponds to (l1,new_glue_e1):
us.glue((l1, new_glue_e1), (old_opposite2[0], old_opposite2[1]))
return s
[docs] def standardize_polygons(self, in_place=False):
r"""
Replace each polygon with a new polygon which differs by
translation and reindexing. The new polygon will have the property
that vertex zero is the origin, and all vertices lie either in the
upper half plane, or on the x-axis with non-negative x-coordinate.
This is done to the current surface if in_place=True. A mutable
copy is created and returned if in_place=False (as default).
This overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.Oriented.ParentMethods.standardize_polygons`
to provide in-place standardizing of surfaces.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon
sage: p = Polygon(vertices = ([(1,1),(2,1),(2,2),(1,2)]))
sage: s = MutableOrientedSimilaritySurface(QQ)
sage: s.add_polygon(p)
0
sage: s.glue((0, 0), (0, 2))
sage: s.glue((0, 1), (0, 3))
sage: s.set_root(0)
sage: s.set_immutable()
sage: s.standardize_polygons().polygon(0)
Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])
"""
if not in_place:
S = MutableOrientedSimilaritySurface.from_surface(self)
S.standardize_polygons(in_place=True)
return S
cv = {} # dictionary for non-zero canonical vertices
for label, polygon in zip(self.labels(), self.polygons()):
best = 0
best_pt = polygon.vertex(best)
for v in range(1, len(polygon.vertices())):
pt = polygon.vertex(v)
if (pt[1] < best_pt[1]) or (pt[1] == best_pt[1] and pt[0] < best_pt[0]):
best = v
best_pt = pt
# We replace the polygon if the best vertex is not the zero vertex, or
# if the coordinates of the best vertex differs from the origin.
if not (best == 0 and best_pt.is_zero()):
cv[label] = best
for label, v in cv.items():
self.set_vertex_zero(label, v, in_place=True)
return self
[docs]class MutableOrientedSimilaritySurface(
MutableOrientedSimilaritySurface_base, MutablePolygonalSurface
):
r"""
A surface built from Euclidean polyogns glued by orientation preserving
similarities.
This is the main tool to create new surfaces of finite type in
sage-flatsurf.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.glue((0, 0), (0, 2))
sage: S.glue((0, 1), (0, 3))
sage: S.set_immutable()
sage: S
Translation Surface in H_1(0) built from a square
TESTS::
sage: TestSuite(S).run()
"""
[docs] def __init__(self, base, category=None):
self._gluings = {}
from flatsurf.geometry.categories import SimilaritySurfaces
if category is None:
category = SimilaritySurfaces().Oriented().FiniteType()
category &= SimilaritySurfaces().Oriented().FiniteType()
super().__init__(base, category=category)
[docs] @classmethod
def from_surface(cls, surface, category=None):
r"""
Return a mutable copy of ``surface``.
EXAMPLES::
sage: from flatsurf import translation_surfaces, MutableOrientedSimilaritySurface, polygons
sage: T = translation_surfaces.square_torus()
We build a surface that is made from two tori:
sage: S = MutableOrientedSimilaritySurface.from_surface(T)
sage: S.add_polygon(polygons.square())
1
sage: S.glue((1, 0), (1, 2))
sage: S.glue((1, 1), (1, 3))
sage: S.set_immutable()
sage: S
Disconnected Surface built from 2 squares
"""
if not surface.is_finite_type():
raise TypeError
self = MutableOrientedSimilaritySurface(surface.base_ring(), category=category)
for label in surface.labels():
self.add_polygon(surface.polygon(label), label=label)
for label in surface.labels():
for edge in range(len(surface.polygon(label).vertices())):
cross = surface.opposite_edge(label, edge)
if cross:
self.glue((label, edge), cross)
if isinstance(surface, MutablePolygonalSurface):
# Only copy explicitly set roots over
self._roots = surface._roots
else:
self.set_roots(surface.roots())
return self
[docs] def add_polygon(self, polygon, *, label=None):
# Overrides add_polygon from MutablePolygonalSurface
label = super().add_polygon(polygon, label=label)
assert label not in self._gluings
self._gluings[label] = [None] * len(polygon.vertices())
if self._roots:
self._roots = self._roots + (label,)
return label
[docs] def remove_polygon(self, label):
# Overrides remove_polygon from MutablePolygonalSurface
self._unglue_polygon(label)
self._gluings.pop(label)
super().remove_polygon(label)
[docs] def glue(self, x, y):
r"""
Glue ``x`` and ``y`` with an (orientation preserving) similarity.
INPUT:
- ``x`` -- a pair consisting of a polygon label and an edge index for
that polygon
- ``y`` -- a pair consisting of a polygon label and an edge index for
that polygon
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface, polygons
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(polygons.square())
0
Glue two opposite sides of the square to each other::
sage: S.glue((0, 1), (0, 3))
Glue the other sides of the square to themselves::
sage: S.glue((0, 0), (0, 0))
sage: S.glue((0, 2), (0, 2))
Note that existing gluings are removed when gluing already glued
sides::
sage: S.glue((0, 0), (0, 2))
sage: S.set_immutable()
sage: S
Translation Surface in H_1(0) built from a square
"""
if not self._mutable:
raise Exception(
"cannot modify immutable surface; create a copy with MutableOrientedSimilaritySurface.from_surface()"
)
if x[0] not in self._polygons:
raise ValueError
if y[0] not in self._polygons:
raise ValueError
self.unglue(*x)
self.unglue(*y)
if self._roots:
component = set(self.component(x[0]))
if y[0] not in component:
# Gluing will join two connected components.
cross_component = set(self.component(y[0]))
for root in reversed(self._roots):
if root in component or root in cross_component:
self._roots = tuple([r for r in self._roots if r != root])
break
else:
assert False, "did not find any root to eliminate"
self._gluings[x[0]][x[1]] = y
self._gluings[y[0]][y[1]] = x
[docs] def unglue(self, label, edge):
r"""
Unglue the side ``edge`` of the polygon ``label`` if it is glued.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface, translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = MutableOrientedSimilaritySurface.from_surface(T)
sage: S.unglue(0, 0)
sage: S.gluings()
(((0, 1), (0, 3)), ((0, 3), (0, 1)))
sage: S.set_immutable()
sage: S
Translation Surface with boundary built from a square
"""
if not self._mutable:
raise Exception(
"cannot modify immutable surface; create a copy with MutableOrientedSimilaritySurface.from_surface()"
)
cross = self._gluings[label][edge]
if cross is not None:
self._gluings[cross[0]][cross[1]] = None
self._gluings[label][edge] = None
if cross is not None and self._roots:
component = set(self.component(label))
if cross[0] not in component:
# Ungluing created a new connected component.
cross_component = set(self.component(cross[0]))
assert label not in cross_component
for root in self._roots:
if root in component:
self._roots = self._roots + (
LabeledView(
surface=self, view=cross_component, finite=True
).min(),
)
break
if root in cross_component:
self._roots = self._roots + (
LabeledView(
surface=self, view=component, finite=True
).min(),
)
break
else:
assert False, "did not find any root to split"
def _unglue_polygon(self, label):
r"""
Remove all gluigns from polygon ``label``.
This is a helper method to completely unglue a polygon before removing
or replacing it.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface, translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = MutableOrientedSimilaritySurface.from_surface(T)
sage: S._unglue_polygon(0)
sage: S.gluings()
()
"""
for edge, cross in enumerate(self._gluings[label]):
if cross is None:
continue
cross_label, cross_edge = cross
self._gluings[cross_label][cross_edge] = None
self._gluings[label] = [None] * len(self.polygon(label).vertices())
[docs] def set_edge_pairing(self, label0, edge0, label1, edge1):
r"""
TESTS::
sage: from flatsurf import polygons, MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(polygons.square())
0
sage: S.set_edge_pairing(0, 0, 0, 2)
doctest:warning
...
UserWarning: set_edge_pairing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead
sage: S.set_edge_pairing(0, 1, 0, 3)
sage: S.gluings()
(((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1)))
"""
import warnings
warnings.warn(
"set_edge_pairing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead"
)
return self.glue((label0, edge0), (label1, edge1))
[docs] def change_edge_gluing(self, label0, edge0, label1, edge1):
r"""
TESTS::
sage: from flatsurf import polygons, MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(polygons.square())
0
sage: S.change_edge_gluing(0, 0, 0, 2)
doctest:warning
...
UserWarning: change_edge_gluing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead
sage: S.change_edge_gluing(0, 1, 0, 3)
sage: S.gluings()
(((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1)))
"""
import warnings
warnings.warn(
"change_edge_gluing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead"
)
return self.glue((label0, edge0), (label1, edge1))
[docs] def change_polygon_gluings(self, label, gluings):
r"""
TESTS::
sage: from flatsurf import polygons, MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(polygons.square())
0
sage: S.change_polygon_gluings(0, [(0, 2), (0, 3), (0, 0), (0, 1)])
doctest:warning
...
UserWarning: change_polygon_gluings() has been deprecated and will be removed in a future version of sage-flatsurf; use glue() in a loop instead
sage: S.gluings()
(((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1)))
"""
import warnings
warnings.warn(
"change_polygon_gluings() has been deprecated and will be removed in a future version of sage-flatsurf; use glue() in a loop instead"
)
for edge0, cross in enumerate(gluings):
if cross is None:
self.unglue(label, edge0)
else:
self.glue((label, edge0), cross)
[docs] def change_polygon(self, label, polygon, gluing_list=None):
r"""
TESTS::
sage: from flatsurf import MutableOrientedSimilaritySurface, translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = MutableOrientedSimilaritySurface.from_surface(T)
sage: S.change_polygon(0, 2 * S.polygon(0))
doctest:warning
...
UserWarning: change_polygon() has been deprecated and will be removed in a future version of sage-flatsurf; use replace_polygon() or remove_polygon() and add_polygon() instead
sage: S.polygon(0)
Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)])
"""
import warnings
warnings.warn(
"change_polygon() has been deprecated and will be removed in a future version of sage-flatsurf; use replace_polygon() or remove_polygon() and add_polygon() instead"
)
if not self._mutable:
raise Exception(
"cannot modify immutable surface; create a copy with MutableOrientedSimilaritySurface.from_surface()"
)
# Note that this obscure feature. If the number of edges is unchanged, we keep the gluings, otherwise we trash them all.
if len(polygon.vertices()) != len(self.polygon(label).vertices()):
self._unglue_polygon(label)
self._gluings[label] = [None] * len(polygon.vertices())
self._polygons[label] = polygon
if gluing_list is not None:
for i, cross in enumerate(gluing_list):
self.glue((label, i), cross)
[docs] def replace_polygon(self, label, polygon):
r"""
Replace the polygon ``label`` with ``polygon`` while keeping its
gluings intact.
INPUT:
- ``label`` -- an element of :meth:`~.MutablePolygonalSurface.labels`
- ``polygon`` -- a Euclidean polygon
EXAMPLES::
sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]))
0
sage: S.glue((0, 0), (0, 2))
sage: S.glue((0, 1), (0, 3))
sage: S.replace_polygon(0, Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)]))
The replacement of a polygon must have the same number of sides::
sage: S.replace_polygon(0, Polygon(vertices=[(0, 0), (2, 0), (2, 2)]))
Traceback (most recent call last):
...
ValueError: polygon must be a quadrilateral
To replace the polygon without keeping its glueings, remove the polygon
first and then add a new one::
sage: S.remove_polygon(0)
sage: S.add_polygon(Polygon(vertices=[(0, 0), (2, 0), (2, 2)]), label=0)
0
"""
old = self.polygon(label)
if len(old.vertices()) != len(polygon.vertices()):
from flatsurf.geometry.categories.polygons import Polygons
article, singular, plural = Polygons._describe_polygon(len(old.vertices()))
raise ValueError(f"polygon must be {article} {singular}")
self._polygons[label] = polygon
[docs] def opposite_edge(self, label, edge=None):
r"""
Return the edge that ``edge`` of ``label`` is glued to or ``None`` if this edge is unglued.
This implements
:meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`.
INPUT:
- ``label`` -- one of the labels included in :meth:`~.MutablePolygonalSurface.labels`
- ``edge`` -- a non-negative integer to specify an edge (the edges
of a polygon are numbered starting from zero.)
EXAMPLES::
sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]))
0
sage: S.glue((0, 0), (0, 1))
sage: S.glue((0, 2), (0, 2))
sage: S.opposite_edge(0, 0)
(0, 1)
sage: S.opposite_edge(0, 1)
(0, 0)
sage: S.opposite_edge(0, 2)
(0, 2)
sage: S.opposite_edge(0, 3)
sage: S.opposite_edge((0, 0))
doctest:warning
...
UserWarning: calling opposite_edge() with a single argument has been deprecated and will be removed in a future version of sage-flatsurf; use opposite_edge(label, edge) instead
(0, 1)
"""
if edge is None:
import warnings
warnings.warn(
"calling opposite_edge() with a single argument has been deprecated and will be removed in a future version of sage-flatsurf; use opposite_edge(label, edge) instead"
)
label, edge = label
return self._gluings[label][edge]
[docs] def set_vertex_zero(self, label, v, in_place=False):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.set_vertex_zero`
to make it possible to set the zero vertex in-place.
"""
if not in_place:
return super().set_vertex_zero(label, v, in_place=in_place)
us = self
if not us.is_mutable():
raise ValueError(
"set_vertex_zero can only be done in_place for a mutable surface."
)
p = us.polygon(label)
n = len(p.vertices())
if not (0 <= v < n):
raise ValueError
glue = []
from flatsurf import Polygon
pp = Polygon(
edges=[p.edge((i + v) % n) for i in range(n)], base_ring=us.base_ring()
)
for i in range(n):
e = (v + i) % n
ll, ee = us.opposite_edge(label, e)
if ll == label:
ee = (ee + n - v) % n
glue.append((ll, ee))
us.remove_polygon(label)
us.add_polygon(pp, label=label)
for e, cross in enumerate(glue):
us.glue((label, e), cross)
return self
[docs] def relabel(self, relabeling_map, in_place=False):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.relabel`
to allow relabeling in-place.
"""
if not in_place:
return super().relabel(relabeling_map=relabeling_map, in_place=in_place)
us = self
if not isinstance(relabeling_map, dict):
raise NotImplementedError(
"Currently relabeling is only implemented via a dictionary."
)
domain = set()
codomain = set()
data = {}
for l1, l2 in relabeling_map.items():
p = us.polygon(l1)
glue = []
for e in range(len(p.vertices())):
ll, ee = us.opposite_edge(l1, e)
try:
lll = relabeling_map[ll]
except KeyError:
lll = ll
glue.append((lll, ee))
data[l2] = (p, glue)
domain.add(l1)
codomain.add(l2)
if len(domain) != len(codomain):
raise ValueError(
"The relabeling_map must be injective. Received " + str(relabeling_map)
)
changed_labels = domain.intersection(codomain)
added_labels = codomain.difference(domain)
removed_labels = domain.difference(codomain)
# Pass to add_polygons
roots = list(us.roots())
relabel_errors = {}
for l2 in added_labels:
p, glue = data[l2]
l3 = us.add_polygon(p, label=l2)
if not l2 == l3:
# This means the label l2 could not be added for some reason.
# Perhaps the implementation does not support this type of label.
# Or perhaps there is already a polygon with this label.
relabel_errors[l2] = l3
# Pass to change polygons
for l2 in changed_labels:
p, glue = data[l2]
us.remove_polygon(l2)
us.add_polygon(p, label=l2)
us.replace_polygon(l2, p)
# Deal with the component roots
roots = [relabeling_map.get(label, label) for label in roots]
roots = [relabel_errors.get(label, label) for label in roots]
# Pass to remove polygons:
for l1 in removed_labels:
us.remove_polygon(l1)
# Pass to update the edge gluings
if len(relabel_errors) == 0:
# No problems. Update the gluings.
for l2 in codomain:
p, glue = data[l2]
for e, cross in enumerate(glue):
us.glue((l2, e), cross)
else:
# Use the gluings provided by relabel_errors when necessary
for l2 in codomain:
p, glue = data[l2]
for e in range(len(p.vertices())):
ll, ee = glue[e]
try:
# First try the error dictionary
us.glue((l2, e), (relabel_errors[ll], ee))
except KeyError:
us.glue((l2, e), (ll, ee))
us.set_roots(roots)
return self, len(relabel_errors) == 0
[docs] def join_polygons(self, p1, e1, test=False, in_place=False):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.join_polygons`
to allow joining in-place.
"""
if test:
in_place = False
if not in_place:
return super().join_polygon(p1, e1, test=test, in_place=in_place)
poly1 = self.polygon(p1)
p2, e2 = self.opposite_edge(p1, e1)
poly2 = self.polygon(p2)
if p1 == p2:
raise ValueError("Can't glue polygon to itself.")
t = self.edge_transformation(p2, e2)
dt = t.derivative()
es = []
edge_map = {} # Store the pairs for the old edges.
for i in range(e1):
edge_map[len(es)] = (p1, i)
es.append(poly1.edge(i))
ne = len(poly2.vertices())
for i in range(1, ne):
ee = (e2 + i) % ne
edge_map[len(es)] = (p2, ee)
es.append(dt * poly2.edge(ee))
for i in range(e1 + 1, len(poly1.vertices())):
edge_map[len(es)] = (p1, i)
es.append(poly1.edge(i))
from flatsurf import Polygon
new_polygon = Polygon(edges=es, base_ring=self.base_ring())
# Do the gluing.
ss = self
s = ss
inv_edge_map = {}
for key, value in edge_map.items():
inv_edge_map[value] = (p1, key)
glue_list = []
for i in range(len(es)):
p3, e3 = edge_map[i]
p4, e4 = self.opposite_edge(p3, e3)
if p4 == p1 or p4 == p2:
glue_list.append(inv_edge_map[(p4, e4)])
else:
glue_list.append((p4, e4))
if p2 in s.roots():
s.set_roots(p1 if label == p2 else label for label in s.roots())
s.remove_polygon(p2)
s.remove_polygon(p1)
s.add_polygon(new_polygon, label=p1)
for e, cross in enumerate(glue_list):
s.glue((p1, e), cross)
return s
[docs] def subdivide_polygon(self, p, v1, v2, test=False, new_label=None):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.subdivide_polygon`
to allow subdividing in-place.
"""
if test:
return super().subdivide_polygon(
p=p, v1=v1, v2=v2, test=test, new_label=new_label
)
poly = self.polygon(p)
ne = len(poly.vertices())
if v1 < 0 or v2 < 0 or v1 >= ne or v2 >= ne:
raise ValueError("Provided vertices out of bounds.")
if abs(v1 - v2) <= 1 or abs(v1 - v2) >= ne - 1:
raise ValueError("Provided diagonal is not actually a diagonal.")
if v2 < v1:
v2 = v2 + ne
newedges1 = [poly.vertex(v2) - poly.vertex(v1)]
for i in range(v2, v1 + ne):
newedges1.append(poly.edge(i))
from flatsurf import Polygon
newpoly1 = Polygon(edges=newedges1, base_ring=self.base_ring())
newedges2 = [poly.vertex(v1) - poly.vertex(v2)]
for i in range(v1, v2):
newedges2.append(poly.edge(i))
newpoly2 = Polygon(edges=newedges2, base_ring=self.base_ring())
# Store the old gluings
old_gluings = {(p, i): self.opposite_edge(p, i) for i in range(ne)}
# Update the polygon with label p, add a new polygon.
self.remove_polygon(p)
self.add_polygon(newpoly1, label=p)
if new_label is None:
new_label = self.add_polygon(newpoly2)
else:
new_label = self.add_polygon(newpoly2, label=new_label)
# This gluing is the diagonal we used.
self.glue((p, 0), (new_label, 0))
# Setup conversion from old to new labels.
old_to_new_labels = {}
for i in range(v1, v2):
old_to_new_labels[(p, i % ne)] = (new_label, i - v1 + 1)
for i in range(v2, ne + v1):
old_to_new_labels[(p, i % ne)] = (p, i - v2 + 1)
for e in range(1, len(newpoly1.vertices())):
pair = old_gluings[(p, (v2 + e - 1) % ne)]
if pair in old_to_new_labels:
pair = old_to_new_labels[pair]
self.glue((p, e), (pair[0], pair[1]))
for e in range(1, len(newpoly2.vertices())):
pair = old_gluings[(p, (v1 + e - 1) % ne)]
if pair in old_to_new_labels:
pair = old_to_new_labels[pair]
self.glue((new_label, e), (pair[0], pair[1]))
[docs] def reposition_polygons(self, in_place=False, relabel=None):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.Oriented.ParentMethods.reposition_polygons`
to allow normalizing in-place.
"""
if not in_place:
return super().reposition_polygons(in_place=in_place, relabel=relabel)
if relabel is not None:
if relabel:
raise NotImplementedError(
"the relabel keyword has been removed from reposition_polygon; use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead"
)
else:
import warnings
warnings.warn(
"the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to reposition_polygons()"
)
s = self
labels = list(s.labels())
from flatsurf.geometry.similarity import SimilarityGroup
S = SimilarityGroup(self.base_ring())
identity = S.one()
it = iter(labels)
label = next(it)
changes = {label: identity}
for label in it:
polygon = self.polygon(label)
adjacencies = {
edge: self.opposite_edge(label, edge)[0]
for edge in range(len(polygon.vertices()))
}
edge = min(
adjacencies,
# pylint: disable-next=cell-var-from-loop
key=lambda edge: labels.index(adjacencies[edge]),
)
label2, edge2 = s.opposite_edge(label, edge)
changes[label] = changes[label2] * s.edge_transformation(label, edge)
it = iter(labels)
# Skip the base label:
label = next(it)
for label in it:
p = s.polygon(label)
p = changes[label].derivative() * p
s.replace_polygon(label, p)
return s
[docs] def triangulate(self, in_place=False, label=None, relabel=None):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.triangulate`
to allow triangulating in-place.
.. TODO::
The code here is not using
:meth:`~.categories.euclidean_polygons.EuclideanPolygons.Simple.ParentMethods.triangulation`.
It should probably be rewritten to share the same logic.
TESTS:
Verify that the monotile can be triangulated::
sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface
sage: K = QuadraticField(3)
sage: a = K.gen()
sage: # build the vectors
sage: l = [(1, 0), (1, 2), (a, 11), (a, 1), (1, 4), (1, 6), (a, 3),
....: (a, 5), (1, 8), (1, 6), (a, 9), (a, 7), (1, 10), (1, 0)]
sage: vecs = []
sage: for m, e in l:
....: v = vector(K, [m * cos(2*pi*e/12), m * sin(2*pi*e/12)])
....: vecs.append(v)
sage: p = Polygon(edges=vecs)
sage: from collections import defaultdict
sage: d = defaultdict(list)
sage: for i, e in enumerate(p.edges()):
....: e.set_immutable()
....: d[e].append(i)
sage: Sbase = MutableOrientedSimilaritySurface(K)
sage: _ = Sbase.add_polygon(p)
sage: for v in list(d):
....: if v in d:
....: indices = d[v]
....: v_op = -v
....: v_op.set_immutable()
....: opposite_indices = d[v_op]
....: assert len(indices) == len(opposite_indices), (len(indices), len(opposite_indices))
....: if len(indices) == 1:
....: del d[v]
....: del d[v_op]
....: Sbase.glue((0, indices[0]), (0, opposite_indices[0]))
sage: (i0, j0), (i1, j1) = d.values()
sage: S1 = MutableOrientedSimilaritySurface.from_surface(Sbase)
sage: S1.glue((0, i0), (0, i1))
sage: S1.glue((0, j0), (0, j1))
sage: S1.set_immutable()
sage: S1.triangulate()
Translation Surface in H_3(4, 0) built from 5 isosceles triangles, 6 triangles and a right triangle
"""
if relabel is not None:
import warnings
warnings.warn(
"the relabel keyword argument of triangulate() is ignored, it has been deprecated and will be removed in a future version of sage-flatsurf"
)
if not in_place:
return super().triangulate(in_place=in_place, label=label)
if label is None:
# We triangulate the whole surface
# Store the current labels.
labels = list(self.labels())
s = self
# Subdivide each polygon in turn.
for label in labels:
s = s.triangulate(in_place=True, label=label)
return s
poly = self.polygon(label)
n = len(poly.vertices())
if n > 3:
s = self
else:
# This polygon is already a triangle.
return self
from flatsurf.geometry.euclidean import ccw
for i in range(n - 3):
poly = s.polygon(label)
n = len(poly.vertices())
for i in range(n):
e1 = poly.edge(i)
e2 = poly.edge((i + 1) % n)
if ccw(e1, e2) > 0:
# This is in case the polygon is a triangle with subdivided edge.
e3 = poly.edge((i + 2) % n)
if ccw(e1 + e2, e3) != 0:
s.subdivide_polygon(label, i, (i + 2) % n)
break
return s
[docs] def delaunay_single_flip(self):
r"""
Perform a single in place flip of a triangulated mutable surface
in-place.
"""
lc = self._label_comparator()
for (l1, e1), (l2, e2) in self.gluings():
if (
lc.lt(l1, l2) or (l1 == l2 and e1 <= e2)
) and self._delaunay_edge_needs_flip(l1, e1):
self.triangle_flip(l1, e1, in_place=True)
return True
return False
[docs] def delaunay_triangulation(
self,
triangulated=False,
in_place=False,
direction=None,
relabel=None,
):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.delaunay_triangulation`
to allow triangulating in-place.
"""
if not in_place:
return super().delaunay_triangulation(
triangulated=triangulated,
in_place=in_place,
direction=direction,
relabel=relabel,
)
if relabel is not None:
if relabel:
raise NotImplementedError(
"the relabel keyword has been removed from delaunay_triangulation(); use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead"
)
else:
import warnings
warnings.warn(
"the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to delaunay_triangulation()"
)
if triangulated:
s = self
else:
s = self
self.triangulate(in_place=True)
if direction is None:
direction = (self.base_ring() ** 2)((0, 1))
if direction.is_zero():
raise ValueError
from collections import deque
unchecked_labels = deque(s.labels())
checked_labels = set()
while unchecked_labels:
label = unchecked_labels.popleft()
flipped = False
for edge in range(3):
if s._delaunay_edge_needs_flip(label, edge):
# Record the current opposite edge:
label2, edge2 = s.opposite_edge(label, edge)
# Perform the flip.
s.triangle_flip(label, edge, in_place=True, direction=direction)
# Move the opposite polygon to the list of labels we need to check.
if label2 != label:
try:
checked_labels.remove(label2)
unchecked_labels.append(label2)
except KeyError:
# Occurs if label2 is not in checked_labels
pass
flipped = True
break
if flipped:
unchecked_labels.append(label)
else:
checked_labels.add(label)
return s
[docs] def delaunay_decomposition(
self,
triangulated=False,
delaunay_triangulated=False,
in_place=False,
direction=None,
relabel=None,
):
r"""
Overrides
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.delaunay_decomposition`
to allow normalizing in-place.
"""
if not in_place:
return super().delaunay_decomposition(
triangulated=triangulated,
delaunay_triangulated=delaunay_triangulated,
in_place=in_place,
direction=direction,
relabel=relabel,
)
if relabel is not None:
if relabel:
raise NotImplementedError(
"the relabel keyword has been removed from delaunay_decomposition(); use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead"
)
else:
import warnings
warnings.warn(
"the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to delaunay_decomposition()"
)
s = self
if not delaunay_triangulated:
s = s.delaunay_triangulation(
triangulated=triangulated,
in_place=True,
direction=direction,
relabel=relabel,
)
while True:
for (l1, e1), (l2, e2) in s.gluings():
if s._delaunay_edge_needs_join(l1, e1):
s.join_polygons(l1, e1, in_place=True)
break
else:
return s
[docs] def cmp(self, s2, limit=None):
r"""
Compare two surfaces. This is an ordering returning -1, 0, or 1.
The surfaces will be considered equal if and only if there is a translation automorphism
respecting the polygons and the root labels.
If the two surfaces are infinite, we just examine the first limit polygons.
"""
if self.is_finite_type():
if s2.is_finite_type():
if limit is not None:
raise ValueError("limit only enabled for finite surfaces")
sign = len(self.polygons()) - len(s2.polygons())
if sign > 0:
return 1
if sign < 0:
return -1
lw1 = self.labels()
labels1 = list(lw1)
lw2 = s2.labels()
labels2 = list(lw2)
for l1, l2 in zip(lw1, lw2):
ret = self.polygon(l1).cmp(self.polygon(l2))
if ret != 0:
return ret
for e in range(len(self.polygon(l1).vertices())):
ll1, e1 = self.opposite_edge(l1, e)
ll2, e2 = s2.opposite_edge(l2, e)
num1 = labels1.index(ll1)
num2 = labels2.index(ll2)
ret = (num1 > num2) - (num1 < num2)
if ret:
return ret
ret = (e1 > e2) - (e1 < e2)
if ret:
return ret
return 0
else:
# s1 is finite but s2 is infinite.
return -1
else:
if s2.is_finite_type():
# s1 is infinite but s2 is finite.
return 1
else:
if limit is None:
raise NotImplementedError
# both surfaces are infinite.
from itertools import islice
lw1 = self.labels()
labels1 = list(islice(lw1, limit))
lw2 = s2.labels()
labels2 = list(islice(lw2, limit))
count = 0
for l1, l2 in zip(lw1, lw2):
ret = self.polygon(l1).cmp(s2.polygon(l2))
if ret != 0:
return ret
for e in range(len(self.polygon(l1).vertices())):
ll1, ee1 = self.opposite_edge(l1, e)
ll2, ee2 = s2.opposite_edge(l2, e)
num1 = labels1.index(ll1)
num2 = labels2.index(ll2)
ret = (num1 > num2) - (num1 < num2)
if ret:
return ret
ret = (ee1 > ee2) - (ee1 < ee2)
if ret:
return ret
if count >= limit:
break
count += 1
return 0
[docs] def __eq__(self, other):
r"""
Return whether this surface is indistinguishable from ``other``.
See
:meth:`~.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.ParentMethods._test_eq_surface`
for details on this notion of equality.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: T = MutableOrientedSimilaritySurface(AA)
sage: S == T
False
sage: S == S
True
"""
if not isinstance(other, MutableOrientedSimilaritySurface):
return False
if not super().__eq__(other):
return False
if self._gluings != other._gluings:
return False
return True
[docs] def __hash__(self):
r"""
Return a hash value for this surface that is compatible with
:meth:`__eq__`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: T = MutableOrientedSimilaritySurface(QQ)
sage: hash(S) == hash(T)
Traceback (most recent call last):
...
TypeError: cannot hash a mutable surface
sage: S.set_immutable()
sage: T.set_immutable()
sage: hash(S) == hash(T)
True
"""
if self._mutable:
raise TypeError("cannot hash a mutable surface")
return hash((super().__hash__(), tuple(self.gluings())))
[docs]class BaseRingChangedSurface(OrientedSimilaritySurface):
r"""
Changes the ring over which a surface is defined.
EXAMPLES:
This class is used in the implementation of
:meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.change_ring`::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = T.change_ring(AA)
sage: from flatsurf.geometry.surface import BaseRingChangedSurface
sage: isinstance(S, BaseRingChangedSurface)
True
sage: TestSuite(S).run()
"""
[docs] def __init__(self, surface, ring, category=None):
if surface.is_mutable():
raise NotImplementedError("surface must be immutable")
self._reference = surface
super().__init__(ring, category=category or surface.category())
[docs] def is_mutable(self):
r"""
Return whether this surface can be modified, i.e., return ``False``.
This implements
:meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = T.change_ring(AA)
sage: S.is_mutable()
False
"""
return False
[docs] def labels(self):
r"""
Return the labels of the polygons of this surface.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = T.change_ring(AA)
sage: S.labels()
(0,)
"""
return self._reference.labels()
[docs] def roots(self):
r"""
Return a label for each connected component on this surface.
This implements :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = T.change_ring(AA)
sage: S.roots()
(0,)
"""
return self._reference.roots()
[docs] def polygon(self, label):
r"""
Return the polygon with ``label``.
This implements
:meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = T.change_ring(AA)
sage: p = S.polygon(0)
sage: p.base_ring()
Algebraic Real Field
"""
return self._reference.polygon(label).change_ring(self.base_ring())
[docs] def opposite_edge(self, label, edge):
r"""
Return the edge that ``edge`` of ``label`` is glued to or ``None`` if this edge is unglued.
This implements
:meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: S = T.change_ring(AA)
sage: S.opposite_edge(0, 0)
(0, 2)
"""
return self._reference.opposite_edge(label, edge)
[docs] def __eq__(self, other):
r"""
Return whether this surface is indistinguishable from ``other``.
See
:meth:`~.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.ParentMethods._test_eq_surface`
for details on this notion of equality.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: T.change_ring(AA) == T.change_ring(AA)
True
"""
if not isinstance(other, BaseRingChangedSurface):
return False
return self._reference == other._reference and self.base() == other.base()
[docs] def __hash__(self):
r"""
Return a hash value for this surface that is compatible with
:meth:`__eq__`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: T = translation_surfaces.square_torus()
sage: hash(T.change_ring(AA)) == hash(T.change_ring(AA))
True
"""
return hash((self._reference, self.base()))
[docs]class RootedComponents_MutablePolygonalSurface(collections.abc.Mapping):
r"""
Connected components of a :class:`MutablePolygonalSurface`.
The components are represented as a mapping that maps the root labels to
the labels of the corresponding component.
This is a helper method for :meth:`MutablePolygonalSurface.components` and
:meth:`MutablePolygonalSurface.roots`.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface
sage: components = RootedComponents_MutablePolygonalSurface(S)
"""
[docs] def __init__(self, surface):
self._surface = surface
[docs] def __getitem__(self, root):
r"""
Return the labels of the connected component rooted at the label
``root``.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: S.glue((0, 0), (1, 0))
sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface
sage: components = RootedComponents_MutablePolygonalSurface(S)
sage: components[0]
(0, 1)
"""
return self._surface.component(root)
[docs] def __iter__(self):
r"""
Iterate over the keys of this mapping, i.e., the root labels of the
connected components.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: S.glue((0, 0), (1, 0))
sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface
sage: components = RootedComponents_MutablePolygonalSurface(S)
sage: list(components)
[0]
"""
# Shortcut enumeration if this is known to be a connected surface.
connected = "Connected" in self._surface.category().axioms()
for root in self._surface._roots:
yield root
if connected:
return
labels = set(self._surface._polygons)
for root in self._surface._roots:
for label in self._surface.component(root):
labels.remove(label)
while labels:
root = LabeledView(surface=self._surface, view=labels, finite=True).min()
yield root
if connected:
return
for label in self._surface.component(root):
labels.remove(label)
[docs] def __len__(self):
r"""
Return the number of connected components of this surface.
EXAMPLES::
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square())
0
sage: S.add_polygon(polygons.square())
1
sage: S.glue((0, 0), (1, 0))
sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface
sage: components = RootedComponents_MutablePolygonalSurface(S)
sage: len(components)
1
"""
components = 0
for root in self:
components += 1
return components
[docs]class LabeledCollection:
r"""
Abstract base class for collection of labels as returned by ``labels()``
methods of surfaces.
This also serves as a base clas for things such as ``polygons()`` that are
tied to labels.
INPUT:
- ``surface`` -- a polygonal surface, the labels are taken from that
surface; subclasses might change this to only represent a subset of the
labels of this surface
- ``finite`` -- a boolean or ``None`` (default: ``None``); whether this is
a finite set; if ``None``, it is not known whether the set is finite
(some operations might not be supported in that case or not terminate if
the set is actually infinite.)
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: labels = S.labels()
sage: from flatsurf.geometry.surface import LabeledCollection
sage: isinstance(labels, LabeledCollection)
True
"""
[docs] def __init__(self, surface, finite=None):
if finite is None and surface.is_finite_type():
finite = True
self._surface = surface
self._finite = finite
[docs] def __repr__(self):
r"""
Return a printable representation of this set.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: S.labels()
(0,)
sage: S = translation_surfaces.infinite_staircase()
sage: S.labels()
(0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, …)
"""
from itertools import islice
items = list(islice(self, 17))
if self._finite is True or len(items) < 17:
return repr(tuple(self))
return f"({', '.join(str(x) for x in islice(self, 16))}, …)"
[docs] def __len__(self):
r"""
Return the size of this set.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: len(S.labels())
1
Python does not allow ``__len__`` to return anything but an integer, so
we cannot return infinity::
sage: S = translation_surfaces.infinite_staircase()
sage: len(S.labels())
Traceback (most recent call last):
...
NotImplementedError: len() of an infinite set
"""
if self._finite is False:
raise TypeError("infinite set has no integer length")
length = 0
for x in self: # pylint: disable=not-an-iterable
length += 1
return length
[docs] def __contains__(self, x):
r"""
Return whether ``x`` is contained in this set.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: labels = S.labels()
sage: 0 in labels
True
sage: 1 in labels
False
"""
for item in self: # pylint: disable=not-an-iterable
if x == item:
return True
return False
[docs]class LabeledView(LabeledCollection):
r"""
A set of labels (or something resembling labels such as ``polygons()``)
backed by another collection ``view``.
INPUT:
- ``surface`` -- a polygonal surface, the labels in ``view`` are labels of
that surface
- ``view`` -- a collection that all queries are going to be redirected to.
Note that ``labels()`` guarantees that iteration over labels happens in a
breadth-first-search so iteration over ``view`` must follow that same
order. However, subclasses can remove this requirement by overriding
:meth:`__iter__`.
- ``finite`` -- a boolean or ``None`` (default: ``None``); whether this is
a finite set; if ``None``, it is not known whether the set is finite
(some operations might not be supported in that case or not terminate if
the set is actually infinite.)
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.t_fractal()
sage: labels = S.labels()
sage: from flatsurf.geometry.surface import LabeledView
sage: isinstance(labels, LabeledView)
True
"""
[docs] def __init__(self, surface, view, finite=None):
super().__init__(surface, finite=finite)
self._view = view
[docs] def __iter__(self):
return iter(self._view)
[docs] def __contains__(self, x):
return x in self._view
[docs] def __len__(self):
return len(self._view)
[docs] def min(self):
r"""
Return a minimal item in this set.
If the items can be compared, this is just the actual ``min`` of the
items.
Otherwise, we take the one with minimal ``repr``.
.. NOTE::
If the items cannot be compared, and there are clashes in the
``repr``, this method will fail.
Also, if comparison of items is not consistent, then this can
produce somewhat random output.
Finally, note with this approach the min of a set is not the always
the min of the mins of a each partition of that set.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.t_fractal()
sage: S.labels().min()
Traceback (most recent call last):
...
NotImplementedError: cannot determine minimum of an infinite set
::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1,2)
sage: C.labels().min()
0
::
sage: labels = list(C.labels())[:3]
sage: from flatsurf.geometry.surface import LabeledView
sage: LabeledView(C, labels).min()
0
"""
if self._finite is False:
raise NotImplementedError("cannot determine minimum of an infinite set")
try:
return min(self)
except TypeError:
reprs = {repr(item): item for item in self}
if len(reprs) != len(self):
raise TypeError(
"cannot determine minimum of tset without ordering and with non-unique repr()"
)
return reprs[min(reprs)]
[docs]class ComponentLabels(LabeledCollection):
r"""
The labels of a connected component.
INPUT:
- ``surface`` -- a polygonal surface
- ``root`` -- a label of the connected component from which enumeration of
the component starts.
- ``finite`` -- a boolean or ``None`` (default: ``None``); whether this is
a finite component; if ``None``, it is not known whether the component is
finite (some operations might not be supported in that case or not
terminate if the component is actually infinite.)
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.t_fractal()
sage: component = S.component(0)
sage: from flatsurf.geometry.surface import ComponentLabels
sage: isinstance(component, ComponentLabels)
True
"""
[docs] def __init__(self, surface, root, finite=None):
super().__init__(surface, finite=finite)
self._root = root
[docs] def __iter__(self):
r"""
Return an iterator of this component that enumerates labels starting
from the root label in a breadth-first-search.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: component = C.component(0)
sage: list(component)
[0, 1, 3, 2]
"""
from collections import deque
seen = set()
pending = deque([self._root])
while pending:
label = pending.popleft()
if label in seen:
continue
seen.add(label)
yield label
for e in range(len(self._surface.polygon(label).vertices())):
cross = self._surface.opposite_edge(label, e)
if cross is not None:
pending.append(cross[0])
[docs]class Labels(LabeledCollection, collections.abc.Set):
r"""
The labels of a surface.
.. NOTE::
This is a generic implementation that represents the set of labels of a
surface in a breadth-first iteration starting from the root labels of the
connected components.
This implementation makes no assumption on the surface and can be very
slow to answer, e.g., containment or compute the number of labels in
the surface (because it needs to iterate over the entire surface.)
When possible, a faster implementation should be used such as
:class:`LabelsFromView`.
EXAMPLES::
sage: from flatsurf import polygons, similarity_surfaces
sage: T = polygons.triangle(1, 2, 5)
sage: S = similarity_surfaces.billiard(T)
sage: S = S.minimal_cover("translation")
sage: labels = S.labels()
sage: labels
((0, 1, 0), (1, 1, 0), (1, 0, -1), (1, 1/2*c0, 1/2*c0), (0, 1/2*c0, -1/2*c0), (0, 0, 1), (0, -1/2*c0, -1/2*c0), (0, 0, -1), (0, -1/2*c0, 1/2*c0), (0, 1/2*c0, 1/2*c0),
(1, 1/2*c0, -1/2*c0), (1, -1/2*c0, -1/2*c0), (1, 0, 1), (1, -1/2*c0, 1/2*c0), (1, -1, 0), (0, -1, 0))
TESTS::
sage: from flatsurf.geometry.surface import Labels
sage: type(labels) == Labels
True
"""
[docs] def __iter__(self):
for component in self._surface.components():
yield from component
[docs]class LabelsFromView(Labels, LabeledView):
r"""
The labels of a surface backed by another set that can quickly compute the
length of the labels and decide containment in the set.
.. NOTE::
Iteration of the view collection does not have to be in breadth-first
search order in the surface since this class is picking up the generic
:meth:`Labels.__iter__`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: labels = C.labels()
sage: from flatsurf.geometry.surface import LabelsFromView
sage: type(labels) == LabelsFromView
True
"""
[docs]class Polygons(LabeledCollection, collections.abc.Collection):
r"""
The collection of polygons of a surface.
The polygons are returned in the same order as labels of the surface are
returned by :class:`.Labels`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: polygons = C.polygons()
sage: from flatsurf.geometry.surface import Polygons
sage: isinstance(polygons, Polygons)
True
"""
[docs] def __iter__(self):
r"""
Iterate over the polygons in the same order as ``labels()`` does.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: labels = C.labels()
sage: polygons = C.polygons()
sage: for entry in zip(labels, polygons):
....: print(entry)
(0, Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]))
(1, Polygon(vertices=[(1, 0), (1, -2), (3/2, -5/2), (2, -2), (2, 0), (2, 1), (2, 3), (3/2, 7/2), (1, 3), (1, 1)]))
(3, Polygon(vertices=[(3, 0), (7/2, -1/2), (11/2, -1/2), (6, 0), (6, 1), (11/2, 3/2), (7/2, 3/2), (3, 1)]))
(2, Polygon(vertices=[(2, 0), (3, 0), (3, 1), (2, 1)]))
"""
for label in self._surface.labels():
yield self._surface.polygon(label)
[docs] def __len__(self):
r"""
Return the number of polygons in this surface.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: polygons = C.polygons()
sage: len(polygons)
4
"""
return len(self._surface.labels())
[docs]class Polygons_MutableOrientedSimilaritySurface(Polygons):
r"""
The collection of polygons of a :class:`MutableOrientedSimilaritySurface`.
This is a faster version of :class:`Polygons`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: polygons = C.polygons()
sage: from flatsurf.geometry.surface import Polygons_MutableOrientedSimilaritySurface
sage: isinstance(polygons, Polygons_MutableOrientedSimilaritySurface)
True
"""
[docs] def __init__(self, surface):
# This hack makes __len__ 20% faster (it saves one attribute lookup.)
self._polygons = surface._polygons
super().__init__(surface)
[docs] def __len__(self):
return len(self._polygons)
[docs]class Edges(LabeledCollection, collections.abc.Set):
r"""
The set of edges of a surface.
The set of edges contains of pairs (label, index) with the labels of the
polygons and the actual edges indexed from 0 in the second component.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: C = translation_surfaces.cathedral(1, 2)
sage: edges = C.edges()
sage: edges
((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (2, 0), (2, 1), (2, 2), (2, 3))
TESTS::
sage: from flatsurf.geometry.surface import Edges
sage: isinstance(edges, Edges)
True
"""
[docs] def __iter__(self):
for label, polygon in zip(self._surface.labels(), self._surface.polygons()):
for edge in range(len(polygon.vertices())):
yield (label, edge)
[docs] def __contains__(self, x):
label, edge = x
if label not in self._surface.labels():
return False
polygon = self._surface.polygon(label)
return 0 <= len(polygon.vertices()) < edge
[docs]class Gluings(LabeledCollection, collections.abc.Set):
r"""
The set of gluings of the surface.
Each gluing consists of two pairs (label, index) that describe the edges
being glued.
Note that each gluing (that is not a self-gluing) is reported twice.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: gluings = S.gluings()
sage: gluings
(((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1)))
TESTS::
sage: from flatsurf.geometry.surface import Gluings
sage: isinstance(gluings, Gluings)
True
"""
[docs] def __iter__(self):
for label, edge in self._surface.edges():
cross = self._surface.opposite_edge(label, edge)
if cross is None:
continue
yield (label, edge), cross
[docs] def __contains__(self, x):
x, y = x
if x not in self._surface.edges():
return False
cross = self._surface.opposite_edge(*x)
if cross is None:
return False
return y == cross
# Import deprecated symbols so imports using flatsurf.geometry.surface do not break.
from flatsurf.geometry.surface_legacy import ( # noqa, we import at the bottom of the file to break a circular import # pylint: disable=wrong-import-position
Surface,
Surface_list,
Surface_dict,
surface_list_from_polygons_and_gluings,
)