r"""
Geometric objects on surfaces.
This includes singularities, saddle connections and cylinders.
.. jupyter-execute::
:hide-code:
# Allow jupyter-execute blocks in this module to contain doctests
import jupyter_doctest_tweaks
"""
# ****************************************************************************
# This file is part of sage-flatsurf.
#
# Copyright (C) 2017-2020 W. Patrick Hooper
# 2017-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/>.
# ****************************************************************************
from sage.misc.cachefunc import cached_method
from sage.modules.free_module_element import vector
from sage.plot.graphics import Graphics
from sage.plot.polygon import polygon2d
from sage.rings.qqbar import AA
from sage.rings.infinity import Infinity
from sage.structure.sage_object import SageObject
from sage.structure.element import Element
from flatsurf.geometry.similarity import SimilarityGroup
[docs]
def Singularity(similarity_surface, label, v, limit=None):
r"""
Return the point of ``similarity_surface`` at the ``v``-th vertex of the
polygon ``label``.
If the surface is infinite, the ``limit`` can be set. In this case the
construction of the singularity is successful if the sequence of vertices
hit by passing through edges closes up in ``limit`` or less steps.
EXAMPLES::
sage: from flatsurf.geometry.similarity_surface_generators import TranslationSurfaceGenerators
sage: s=TranslationSurfaceGenerators.veech_2n_gon(5)
sage: from flatsurf.geometry.surface_objects import Singularity
sage: sing=Singularity(s, 0, 1)
doctest:warning
...
UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead.
sage: print(sing)
Vertex 1 of polygon 0
sage: TestSuite(sing).run()
"""
import warnings
warnings.warn(
"Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead."
)
return similarity_surface.point(
label, similarity_surface.polygon(label).vertex(v), limit=limit
)
[docs]
class SurfacePoint(Element):
r"""
A point on ``surface``.
INPUT:
- ``surface`` -- a similarity surface
- ``label`` -- a polygon label for the polygon with respect to which the
``point`` coordinates can be made sense of
- ``point`` -- coordinates of a point in the polygon ``label`` or the index
of the vertex of the polygon with ``label``
- ``ring`` -- a SageMath ring or ``None`` (default: ``None``); the
coordinate ring for ``point``
- ``limit`` -- an integer or ``None`` (default: ``None`` for an unlimited
number of steps); if this is a singularity of the surface, then this
limits the number of edges that are crossed to determine all the edges
adjacent to that singularity. An error is raised if the limit is
insufficient.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
A point can have a single representation with coordinates when it is in
interior of a polygon::
sage: S.point(1, (1/2, 1/2))
Point (1/2, 1/2) of polygon 1
A point can have two representations when it is in interior of an edge::
sage: p = S.point(1, (1/2, 0))
sage: q = S.point(2, (1/2, 1))
sage: p == q
True
sage: p.coordinates(2)
((1/2, 1),)
A point can have even more representations when it is a vertex::
sage: S.point(1, (0, 0))
Vertex 0 of polygon 1
TESTS:
Verify that #275 has been resolved, i.e., points on the boundary can be
created::
sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: S.add_polygon(Polygon(vertices=[(0,0), (-1, -1), (1,0)]))
0
sage: S.add_polygon(Polygon(vertices=[(0,0), (0, 1), (-1,-1)]))
1
sage: S.glue((0, 0), (1, 2))
sage: S.set_immutable()
sage: S
Translation Surface with boundary built from 2 triangles
sage: S(0, (0, 0))
Vertex 0 of polygon 0
sage: S(0, (1/2, 0))
Point (1/2, 0) of polygon 0
sage: S(0, (1, 0))
Vertex 2 of polygon 0
"""
def __init__(self, surface, label, point, ring=None, limit=None):
if limit is not None:
import warnings
warnings.warn(
"limit has been deprecated as a keyword argument when creating points and will be removed without replacement in a future version of sage-flatsurf"
)
self._surface = surface
if ring is None:
ring = surface.base_ring()
if ring is not surface.base_ring():
import warnings
warnings.warn(
"the ring parameter is deprecated and will be removed in a future version of sage-flatsurf; define the surface over a larger ring instead so that this points' coordinates live in the base ring"
)
polygon = surface.polygon(label)
from sage.all import ZZ
if point in ZZ:
point = surface.polygon(label).vertex(point)
point = (ring**2)(point)
point.set_immutable()
position = polygon.get_point_position(point)
if not position.is_inside():
raise NotImplementedError(
"point must be positioned within the polygon with the given label"
)
if position.is_in_interior():
self._representatives = {(label, point)}
elif position.is_in_edge_interior():
self._representatives = {(label, point)}
opposite_edge = surface.opposite_edge(label, position.get_edge())
if opposite_edge is not None:
cross_label, cross_edge = opposite_edge
cross_point = surface.edge_transformation(label, position.get_edge())(
point
)
cross_point.set_immutable()
self._representatives.add((cross_label, cross_point))
elif position.is_vertex():
self._representatives = set()
source_edge = position.get_vertex()
def collect_representatives(label, source_edge, direction, limit):
def rotate(label, source_edge, direction):
if direction == -1:
source_edge = (source_edge - 1) % len(
surface.polygon(label).vertices()
)
opposite_edge = surface.opposite_edge(label, source_edge)
if opposite_edge is None:
return None
label, source_edge = opposite_edge
if direction == 1:
source_edge = (source_edge + 1) % len(
surface.polygon(label).vertices()
)
return label, source_edge
while True:
self._representatives.add((label, source_edge))
# Rotate to the next edge that is leaving at the vertex
rotated = rotate(label, source_edge, direction)
if rotated is None:
# Surface is disconnected
return
label, source_edge = rotated
if limit is not None:
limit -= 1
if limit < 0:
raise ValueError(
"number of edges at singularity exceeds limit"
)
if (label, source_edge) in self._representatives:
break
# Collect respresentatives of edge walking clockwise and counterclockwise
collect_representatives(label, source_edge, 1, limit)
collect_representatives(label, source_edge, -1, limit)
self._representatives = {
(label, surface.polygon(label).vertex(vertex))
for (label, vertex) in self._representatives
}
else:
raise NotImplementedError
self._representatives = frozenset(self._representatives)
super().__init__(surface)
[docs]
def surface(self):
r"""
Return the surface containing this point.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (1/2, 1/2))
sage: p.surface() is S
True
"""
return self._surface
[docs]
def is_vertex(self):
r"""
Return whether this point is a singularity of the surface.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: p.is_vertex()
True
"""
label, coordinates = self.representative()
position = self.surface().polygon(label).get_point_position(coordinates)
return position.is_vertex()
[docs]
def one_vertex(self):
r"""
Return a pair (l, v) from the equivalence class of this singularity.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: p.one_vertex() # random output: depends on the Python version
doctest:warning
...
UserWarning: one_vertex() is deprecated and will be removed in a future version of sage-flatsurf; use (label, coordinates) = point.representative(); vertex = surface.polygon(label).get_point_position(coordinates).get_vertex() instead
(2, 1)
"""
import warnings
warnings.warn(
"one_vertex() is deprecated and will be removed in a future version of sage-flatsurf; use (label, coordinates) = point.representative(); vertex = surface.polygon(label).get_point_position(coordinates).get_vertex() instead"
)
label, coordinates = self.representative()
vertex = (
self.surface().polygon(label).get_point_position(coordinates).get_vertex()
)
return label, vertex
[docs]
def representatives(self):
r"""
Return the representatives of this point as pairs of polygon labels and
coordinates.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: p.representatives()
frozenset({(1, (0, 0)), (1, (1, 1)), (2, (0, 1)), (2, (1, 0))})
"""
return self._representatives
[docs]
def representative(self):
r"""
Return a representative of this point, i.e., the first of
:meth:`representatives`.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: p.representative() # random output: depends on the Python version
(2, (1, 0))
"""
return next(iter(self.representatives()))
[docs]
def vertex_set(self):
r"""
Return the list of pairs (l, v) in the equivalence class of this singularity.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: list(p.vertex_set()) # random output: ordering depends on the Python version
doctest:warning
...
UserWarning: vertex_set() is deprecated and will be removed in a future version of sage-flatsurf; use representatives() and then vertex = surface.polygon(label).get_point_position(coordinates).get_vertex() instead
[(2, 1), (1, 2), (1, 0), (2, 3)]
"""
import warnings
warnings.warn(
"vertex_set() is deprecated and will be removed in a future version of sage-flatsurf; use representatives() and then vertex = surface.polygon(label).get_point_position(coordinates).get_vertex() instead"
)
return [
(
label,
self.surface()
.polygon(label)
.get_point_position(coordinates)
.get_vertex(),
)
for label, coordinates in self.representatives()
]
[docs]
def contains_vertex(self, label, v=None):
r"""
Checks if the pair ``(label, v)`` is in the equivalence class returning
true or false. If ``v`` is ``None``, the both the pair ``(label, v)``
is passed as a single parameter in ``label``.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: p.contains_vertex((1, 0))
doctest:warning
...
UserWarning: contains_vertex() is deprecated and will be removed in a future version of sage-flatsurf; use the == operator instead
doctest:warning
...
UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead.
True
sage: p.contains_vertex(label=1, v=0)
True
"""
import warnings
warnings.warn(
"contains_vertex() is deprecated and will be removed in a future version of sage-flatsurf; use the == operator instead"
)
if v is None:
label, v = label
return Singularity(self.surface(), label, v) == self
[docs]
def num_coordinates(self):
r"""
Return the number of different coordinate representations of the point.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: permutation = SymmetricGroup(2)('(1, 2)')
sage: S = translation_surfaces.origami(permutation, permutation)
sage: p = S.point(1, (0, 0))
sage: p.num_coordinates()
doctest:warning
...
UserWarning: num_coordinates() is deprecated and will be removed in a future version of sage-flatsurf; use len(representatives()) instead.
4
"""
import warnings
warnings.warn(
"num_coordinates() is deprecated and will be removed in a future version of sage-flatsurf; use len(representatives()) instead."
)
return len(self._representatives)
[docs]
def labels(self):
r"""
Return the labels of polygons containing the point.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.mcmullen_L(1, 1, 1, 1)
For a point in the interior of polygon, there is exactly one label::
sage: p = S.point(0, (1/2, 1/2))
sage: p.labels()
{0}
For a point in the interior of an edge of a polygon, there can be up to
two labels::
sage: p = S.point(0, (0, 1/2))
sage: p.labels()
{0, 2}
For a point at a vertex, there can be more labels::
sage: p = S.point(0, (0, 0))
sage: p.labels()
{0, 1, 2}
"""
return {label for (label, _) in self._representatives}
[docs]
def coordinates(self, label):
r"""
Return coordinates for the point in the in the polygon ``label``.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.mcmullen_L(1, 1, 1, 1)
sage: p = S.point(0, (0, 0))
sage: p.coordinates(0) # random output: order depends on the Python version
((0, 0), (1, 0), (0, 1), (1, 1))
"""
return tuple(
coordinates for (l, coordinates) in self._representatives if l == label
)
[docs]
def graphical_surface_point(self, graphical_surface=None):
r"""
Return a
:class:`flatsurf.graphical.surface_point.GraphicalSurfacePoint` to
represent this point graphically.
EXAMPLES::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: p = S.point(0, (1/2, 1/2))
sage: G = p.graphical_surface_point()
"""
from flatsurf.graphical.surface_point import GraphicalSurfacePoint
return GraphicalSurfacePoint(self, graphical_surface=graphical_surface)
[docs]
def plot(self, *args, **kwargs):
r"""
Return a plot of this point.
EXAMPLES:
.. jupyter-execute::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: p = S.point(0, (0, 0))
sage: p.plot()
...Graphics object consisting of 1 graphics primitive
.. jupyter-execute::
sage: p = S.point(0, (0, 25/12))
sage: p.plot()
...Graphics object consisting of 1 graphics primitive
"""
graphical_surface = None
if args:
graphical_surface = args[0]
args = args[1:]
return self.graphical_surface_point(graphical_surface=graphical_surface).plot(
*args, **kwargs
)
def __repr__(self):
r"""
Return a printable representation of this point.
TESTS::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: S.point(0, (1/2, 1/2))
Point (1/2, 1/2) of polygon 0
"""
def render(label, coordinates):
if self.is_vertex():
vertex = (
self.surface()
.polygon(label)
.get_point_position(coordinates)
.get_vertex()
)
return "Vertex {} of polygon {}".format(vertex, label)
return "Point {} of polygon {}".format(coordinates, label)
# We pick a specific representative to make our lives easier when doctesting
return min(
render(label, coordinates)
for (label, coordinates) in self.representatives()
)
def __eq__(self, other):
r"""
Return whether this point is indistinguishable from ``other``.
EXAMPLES::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: p = S.point(0, (1/2, 1/2))
sage: p == p
True
sage: q = S.point(0, (1/2, 1/3))
sage: p == q
False
TESTS:
Verify that points can be compared to non-points so they can be put into sets and dicts with other objects::
sage: p == 42
False
"""
if self is other:
return True
if not isinstance(other, SurfacePoint):
return False
if not self._surface == other._surface:
return False
return self._representatives == other._representatives
def _test_category(self, **options):
r"""
Check that this point inherits from the element class of its surface's
category.
Overridden to disable these tests when this is a point of a mutable
surface since the category might then change as the surface becomes
immutable.
EXAMPLES::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: p = S.point(0, (1/2, 1/2))
sage: p._test_category()
"""
if self.surface().is_mutable():
return
super()._test_category(**options)
def __hash__(self):
r"""
Return a hash value of this point.
EXAMPLES::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: p = S.point(0, (0, 0))
sage: q = S.point(4, (0, 0))
sage: hash(p) == hash(q)
True
"""
return hash(self._representatives)
def __ne__(self, other):
r"""
Return whether this point is distinguishable from ``other``.
EXAMPLES::
sage: from flatsurf import half_translation_surfaces
sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4])
sage: p = S.point(0, (1/2, 1/2))
sage: p != p
False
sage: q = S.point(0, (1/2, 1/3))
sage: p != q
True
"""
return not (self == other)
[docs]
class SaddleConnection(SageObject):
r"""
Represents a saddle connection on a SimilaritySurface.
"""
def __init__(
self,
surface,
start_data,
direction,
end_data=None,
end_direction=None,
holonomy=None,
end_holonomy=None,
check=True,
limit=1000,
):
r"""
Construct a saddle connection on a SimilaritySurface.
The only necessary parameters are the surface, start_data, and direction
(to start). If there is missing data that can not be inferred from the surface
type, then a straight-line trajectory will be computed to confirm that this is
indeed a saddle connection. The trajectory will pass through at most limit
polygons before we give up.
Details of the parameters are provided below.
Parameters
----------
surface : a SimilaritySurface
which will contain the saddle connection being constructed.
start_data : a pair
consisting of the label of the polygon where the saddle connection starts
and the starting vertex.
direction : 2-dimensional vector with entries in the base_ring of the surface
representing the direction the saddle connection is moving in (in the
coordinates of the initial polygon).
end_data : a pair
consisting of the label of the polygon where the saddle connection terminates
and the terminating vertex.
end_direction : 2-dimensional vector with entries in the base_ring of the surface
representing the direction to move backward from the end point (in the
coordinates of the terminal polygon). If the surface is a DilationSurface
or better this will be the negation of the direction vector. If the surface
is a HalfDilation surface or better, then this will be either the direction
vector or its negation. In either case the value can be inferred from the
end_data.
holonomy : 2-dimensional vector with entries in the base_ring of the surface
the holonomy of the saddle connection measured from the start. To compute this
you develop the saddle connection into the plane starting from the starting
polygon.
end_holonomy : 2-dimensional vector with entries in the base_ring of the surface
the holonomy of the saddle connection measured from the end (with the opposite
orientation). To compute this you develop the saddle connection into the plane
starting from the terminating polygon. For a translation surface, this will be
the negation of holonomy, and for a HalfTranslation surface it will be either
equal to holonomy or equal to its negation. In both these cases the end_holonomy
can be inferred and does not need to be passed to the constructor.
check : boolean
If all data above is provided or can be inferred, then when check=False this
geometric data is not verified. With check=True the data is always verified
by straight-line flow. Erroroneous data will result in a ValueError being thrown.
Defaults to true.
limit :
The combinatorial limit (in terms of number of polygons crossed) to flow forward
to check the saddle connection geometry.
"""
from flatsurf.geometry.categories import SimilaritySurfaces
if surface not in SimilaritySurfaces():
raise TypeError
self._surface = surface
# Sanitize the direction vector:
V = self._surface.base_ring().fraction_field() ** 2
self._direction = V(direction)
if self._direction == V.zero():
raise ValueError("Direction must be nonzero.")
# To canonicalize the direction vector we ensure its endpoint lies in the boundary of the unit square.
xabs = self._direction[0].abs()
yabs = self._direction[1].abs()
if xabs > yabs:
self._direction = self._direction / xabs
else:
self._direction = self._direction / yabs
# Fix end_direction if not standard.
if end_direction is not None:
xabs = end_direction[0].abs()
yabs = end_direction[1].abs()
if xabs > yabs:
end_direction = end_direction / xabs
else:
end_direction = end_direction / yabs
self._surfacetart_data = tuple(start_data)
if end_direction is None:
from flatsurf.geometry.categories import DilationSurfaces
# Attempt to infer the end_direction.
if self._surface in DilationSurfaces().Positive():
end_direction = -self._direction
elif self._surface in DilationSurfaces() and end_data is not None:
p = self._surface.polygon(end_data[0])
from flatsurf.geometry.euclidean import ccw
if (
ccw(p.edge(end_data[1]), self._direction) >= 0
and ccw(
p.edge(
(len(p.vertices()) + end_data[1] - 1) % len(p.vertices())
),
self._direction,
)
> 0
):
end_direction = self._direction
else:
end_direction = -self._direction
if end_holonomy is None and holonomy is not None:
# Attempt to infer the end_holonomy:
from flatsurf.geometry.categories import (
HalfTranslationSurfaces,
TranslationSurfaces,
)
if self._surface in TranslationSurfaces():
end_holonomy = -holonomy
if self._surface in HalfTranslationSurfaces():
if direction == end_direction:
end_holonomy = holonomy
else:
end_holonomy = -holonomy
if (
end_data is None
or end_direction is None
or holonomy is None
or end_holonomy is None
or check
):
v = self.start_tangent_vector()
traj = v.straight_line_trajectory()
traj.flow(limit)
if not traj.is_saddle_connection():
raise ValueError(
"Did not obtain saddle connection by flowing forward. Limit="
+ str(limit)
)
tv = traj.terminal_tangent_vector()
self._end_data = (tv.polygon_label(), tv.vertex())
if end_data is not None:
if end_data != self._end_data:
raise ValueError(
"Provided or inferred end_data="
+ str(end_data)
+ " does not match actual end_data="
+ str(self._end_data)
)
self._end_direction = tv.vector()
# Canonicalize again.
xabs = self._end_direction[0].abs()
yabs = self._end_direction[1].abs()
if xabs > yabs:
self._end_direction = self._end_direction / xabs
else:
self._end_direction = self._end_direction / yabs
if end_direction is not None:
if end_direction != self._end_direction:
raise ValueError(
"Provided or inferred end_direction="
+ str(end_direction)
+ " does not match actual end_direction="
+ str(self._end_direction)
)
if traj.segments()[0].is_edge():
# Special case (The below method causes error if the trajectory is just an edge.)
self._holonomy = self._surface.polygon(start_data[0]).edge(
start_data[1]
)
self._end_holonomy = self._surface.polygon(self._end_data[0]).edge(
self._end_data[1]
)
else:
from .similarity import SimilarityGroup
sim = SimilarityGroup(self._surface.base_ring()).one()
itersegs = iter(traj.segments())
next(itersegs)
for seg in itersegs:
sim = sim * self._surface.edge_transformation(
seg.start().polygon_label(), seg.start().position().get_edge()
)
self._holonomy = (
sim(traj.segments()[-1].end().point())
- traj.initial_tangent_vector().point()
)
self._end_holonomy = -((~sim.derivative()) * self._holonomy)
if holonomy is not None:
if holonomy != self._holonomy:
print("Combinatorial length: " + str(traj.combinatorial_length()))
print("Start: " + str(traj.initial_tangent_vector().point()))
print("End: " + str(traj.terminal_tangent_vector().point()))
print("Start data:" + str(start_data))
print("End data:" + str(end_data))
raise ValueError(
"Provided holonomy "
+ str(holonomy)
+ " does not match computed holonomy of "
+ str(self._holonomy)
)
if end_holonomy is not None:
if end_holonomy != self._end_holonomy:
raise ValueError(
"Provided or inferred end_holonomy "
+ str(end_holonomy)
+ " does not match computed end_holonomy of "
+ str(self._end_holonomy)
)
else:
self._end_data = tuple(end_data)
self._end_direction = end_direction
self._holonomy = holonomy
self._end_holonomy = end_holonomy
# Make vectors immutable
self._direction.set_immutable()
self._end_direction.set_immutable()
self._holonomy.set_immutable()
self._end_holonomy.set_immutable()
[docs]
def surface(self):
return self._surface
[docs]
def direction(self):
r"""
Returns a vector parallel to the saddle connection pointing from the start point.
The will be normalized so that its $l_\infty$ norm is 1.
"""
return self._direction
[docs]
def end_direction(self):
r"""
Returns a vector parallel to the saddle connection pointing from the end point.
The will be normalized so that its `l_\infty` norm is 1.
"""
return self._end_direction
[docs]
def start_data(self):
r"""
Return the pair (l, v) representing the label and vertex of the corresponding polygon
where the saddle connection originates.
"""
return self._surfacetart_data
[docs]
def end_data(self):
r"""
Return the pair (l, v) representing the label and vertex of the corresponding polygon
where the saddle connection terminates.
"""
return self._end_data
[docs]
def holonomy(self):
r"""
Return the holonomy vector of the saddle connection (measured from the start).
In a SimilaritySurface this notion corresponds to developing the saddle connection into the plane
using the initial chart coming from the initial polygon.
"""
return self._holonomy
[docs]
def length(self):
r"""
In a cone surface, return the length of this saddle connection. Since
this may not lie in the field of definition of the surface, it is
returned as an element of the Algebraic Real Field.
"""
from flatsurf.geometry.categories import ConeSurfaces
if self._surface not in ConeSurfaces():
raise NotImplementedError(
"length of a saddle connection only makes sense for cone surfaces"
)
return vector(AA, self._holonomy).norm()
[docs]
def end_holonomy(self):
r"""
Return the holonomy vector of the saddle connection (measured from the end).
In a SimilaritySurface this notion corresponds to developing the saddle connection into the plane
using the initial chart coming from the initial polygon.
"""
return self._end_holonomy
[docs]
def start_tangent_vector(self):
r"""
Return a tangent vector to the saddle connection based at its start.
"""
return self._surface.tangent_vector(
self._surfacetart_data[0],
self._surface.polygon(self._surfacetart_data[0]).vertex(
self._surfacetart_data[1]
),
self._direction,
)
[docs]
@cached_method(key=lambda self, limit, cache: None)
def trajectory(self, limit=1000, cache=None):
r"""
Return a straight line trajectory representing this saddle connection.
Fails if the trajectory passes through more than limit polygons.
"""
if cache is not None:
import warnings
warnings.warn(
"The cache keyword argument of trajectory() is ignored. Trajectories are always cached."
)
v = self.start_tangent_vector()
traj = v.straight_line_trajectory()
traj.flow(limit)
if not traj.is_saddle_connection():
raise ValueError(
"Did not obtain saddle connection by flowing forward. Limit="
+ str(limit)
)
return traj
[docs]
def plot(self, *args, **options):
r"""
Equivalent to ``.trajectory().plot(*args, **options)``
"""
return self.trajectory().plot(*args, **options)
[docs]
def end_tangent_vector(self):
r"""
Return a tangent vector to the saddle connection based at its start.
"""
return self._surface.tangent_vector(
self._end_data[0],
self._surface.polygon(self._end_data[0]).vertex(self._end_data[1]),
self._end_direction,
)
[docs]
def invert(self):
r"""
Return this saddle connection but with opposite orientation.
"""
return SaddleConnection(
self._surface,
self._end_data,
self._end_direction,
self._surfacetart_data,
self._direction,
self._end_holonomy,
self._holonomy,
check=False,
)
[docs]
def intersections(self, traj, count_singularities=False, include_segments=False):
r"""
See documentation of :meth:`~.straight_line_trajectory.AbstractStraightLineTrajectory.intersections`
"""
return self.trajectory().intersections(
traj, count_singularities, include_segments
)
[docs]
def intersects(self, traj, count_singularities=False):
r"""
See documentation of :meth:`~.straight_line_trajectory.AbstractStraightLineTrajectory.intersects`
"""
return self.trajectory().intersects(
traj, count_singularities=count_singularities
)
def __eq__(self, other):
r"""
Return whether this saddle connection is indistinguishable from
``other``.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: S = translation_surfaces.square_torus()
sage: connections = S.saddle_connections(13)
sage: connections[0] == connections[0]
True
sage: connections[0] == connections[1]
False
TESTS:
Verify that saddle connections can be compared to arbitrary objects (so
they can be put into dicts with other objects)::
sage: connections[0] == 42
False
::
sage: len(connections)
32
sage: len(set(connections))
32
"""
if self is other:
return True
if not isinstance(other, SaddleConnection):
return False
if not self._surface == other._surface:
return False
if not self._direction == other._direction:
return False
if not self._surfacetart_data == other._surfacetart_data:
return False
# Initial data should determine the saddle connection:
return True
def __ne__(self, other):
return not self == other
def __hash__(self):
return 41 * hash(self._direction) - 97 * hash(self._surfacetart_data)
def _test_geometry(self, **options):
# Test that this saddle connection actually exists on the surface.
SaddleConnection(
self._surface,
self._surfacetart_data,
self._direction,
self._end_data,
self._end_direction,
self._holonomy,
self._end_holonomy,
check=True,
)
def __repr__(self):
return "Saddle connection in direction {} with start data {} and end data {}".format(
self._direction, self._surfacetart_data, self._end_data
)
def _test_inverse(self, **options):
# Test that inverting works properly.
SaddleConnection(
self._surface,
self._end_data,
self._end_direction,
self._surfacetart_data,
self._direction,
self._end_holonomy,
self._holonomy,
check=True,
)
def _homology_(self, H):
r"""
Return this saddle connection as a chain of edges in the homology group ``H``.
EXAMPLES::
sage: from flatsurf import *
sage: S = translation_surfaces.mcmullen_L(1,1,1,1)
sage: H = S.homology()
sage: holonomy = lambda x: sum(coeff * S.polygon(label).edge(e) for (label, e), coeff in dict(x._chain).items())
sage: for sc in S.saddle_connections(5):
....: h = H(sc)
....: hol1 = holonomy(h)
....: hol2 = sc.holonomy()
....: assert hol1 == hol2, (sc, h, hol1, hol2)
"""
from .homology import SimplicialHomologyGroup
if not isinstance(H, SimplicialHomologyGroup):
raise TypeError("H must be a SimplicialHomologyGroup")
surface = self._surface
if H._surface != surface:
raise ValueError("homology and surface do not match")
traj = self.start_tangent_vector().straight_line_trajectory()
traj.flow(Infinity)
segments = traj.segments()
if len(segments) == 1 and segments[0].is_edge():
# NOTE: in the special case the saddle connection is an edge,
# the behavior of the corresponding segment is not appropriate
# to the generic code afterwards. Namely, the start and the end
# belongs to two different polygons. See
# https://github.com/flatsurf/sage-flatsurf/issues/309
label = segments[0].polygon_label()
e = segments[0].edge()
return H((label, e))
h = H.zero()
for s in segments:
label = s.polygon_label()
n = len(surface.polygon(label).vertices())
start = s.start()
if start.position().is_in_edge_interior():
# pick the next vertex clockwise
i = (start.position().get_edge() + 1) % n
else:
assert start.position().is_vertex()
i = start.vertex()
end = s.end()
if end.position().is_in_edge_interior():
# pick the previous vertex clockwise
j = end.position().get_edge()
else:
assert end.position().is_vertex()
j = end.vertex()
if i < j:
h += sum(H((label, e)) for e in range(i, j))
else:
h += sum(H((label, e)) for e in range(i, n))
h += sum(H((label, e)) for e in range(j))
return h
[docs]
class Cylinder(SageObject):
r"""
Represents a cylinder in a SimilaritySurface. A cylinder for these purposes is a
topological annulus in a surface bounded by a finite collection of saddle connections
meeting at 180 degree angles.
To Do
-----
* Support cylinders whose monodromy is a dilation.
EXAMPLES::
sage: from flatsurf import translation_surfaces
sage: s = translation_surfaces.octagon_and_squares()
sage: from flatsurf.geometry.surface_objects import Cylinder
sage: cyl = Cylinder(s, 0, [2, 3, 3, 3, 2, 0, 1, 3, 2, 0])
sage: cyl.initial_label()
0
sage: cyl.edges()
(2, 3, 3, 3, 2, 0, 1, 3, 2, 0)
sage: # a = sqrt(2) below.
sage: cyl.area()
2*a + 4
sage: cyl.circumference().minpoly()
x^4 - 680*x^2 + 400
sage: cyl.holonomy()
(8*a + 12, 4*a + 6)
"""
def __init__(self, s, label0, edges):
r"""
Construct a cylinder on the surface `s` from an initial label and a
sequence of edges crossed.
Parameters
----------
s: A SimilaritySurface
the surface containing the cylinder
label0: An initial label
representing a polygon the cylinder passes through.
edges: a list
giving the sequence of edges the cylinder crosses until it closes.
"""
self._surface = s
self._label0 = label0
self._edges = tuple(edges)
ss = s.minimal_cover(cover_type="planar")
SG = SimilarityGroup(s.base_ring())
labels = [(label0, SG.one())] # labels of polygons on the cover ss.
for e in edges:
labels.append(ss.opposite_edge(labels[-1], e)[0])
if labels[0][0] != labels[-1][0]:
raise ValueError("Combinatorial path does not close.")
trans = labels[-1][1]
if not trans.is_translation():
raise NotImplementedError(
"Only cylinders with translational monodromy are currently supported"
)
m = trans.matrix()
v = vector(s.base_ring(), (m[0][2], m[1][2])) # translation vector
from flatsurf.geometry.euclidean import ccw
p = ss.polygon(labels[0])
e = edges[0]
min_y = ccw(v, p.vertex(e))
max_y = ccw(v, p.vertex((e + 1) % len(p.vertices())))
if min_y >= max_y:
raise ValueError("Combinatorial data does not represent a cylinder")
# Stores the vertices where saddle connections starts:
min_list = [0]
max_list = [0]
for i in range(1, len(edges)):
e = edges[i]
p = ss.polygon(labels[i])
y = ccw(v, p.vertex(e))
if y == min_y:
min_list.append(i)
elif y > min_y:
min_list = [i]
min_y = y
if min_y >= max_y:
raise ValueError("Combinatorial data does not represent a cylinder")
y = ccw(v, p.vertex((e + 1) % len(p.vertices())))
if y == max_y:
max_list.append(i)
elif y < max_y:
max_list = [i]
max_y = y
if min_y >= max_y:
raise ValueError("Combinatorial data does not represent a cylinder")
# Extract the saddle connections on the right side:
from flatsurf.geometry.surface_objects import SaddleConnection
sc_set_right = set()
vertices = []
for i in min_list:
label = labels[i]
p = ss.polygon(label)
vertices.append((i, p.vertex(edges[i])))
i, vert_i = vertices[-1]
vert_i = vert_i - v
j, vert_j = vertices[0]
if vert_i != vert_j:
li = labels[i]
li = (li[0], SG(-v) * li[1])
lio = ss.opposite_edge(li, edges[i])
lj = labels[j]
sc = SaddleConnection(
s,
(lio[0][0], (lio[1] + 1) % len(ss.polygon(lio[0]).vertices())),
(~lio[0][1])(vert_j) - (~lio[0][1])(vert_i),
)
sc_set_right.add(sc)
i = j
vert_i = vert_j
for j, vert_j in vertices[1:]:
if vert_i != vert_j:
li = labels[i]
li = (li[0], SG(-v) * li[1])
lio = ss.opposite_edge(li, edges[i])
lj = labels[j]
sc = SaddleConnection(
s,
(lio[0][0], (lio[1] + 1) % len(ss.polygon(lio[0]).vertices())),
(~lio[0][1])(vert_j) - (~lio[0][1])(vert_i),
limit=j - i,
)
sc_set_right.add(sc)
i = j
vert_i = vert_j
# Extract the saddle connections on the left side:
sc_set_left = set()
vertices = []
for i in max_list:
label = labels[i]
p = ss.polygon(label)
vertices.append((i, p.vertex((edges[i] + 1) % len(p.vertices()))))
i, vert_i = vertices[-1]
vert_i = vert_i - v
j, vert_j = vertices[0]
if vert_i != vert_j:
li = labels[i]
li = (li[0], SG(-v) * li[1])
lio = ss.opposite_edge(li, edges[i])
lj = labels[j]
sc = SaddleConnection(
s,
(lj[0], (edges[j] + 1) % len(ss.polygon(lj).vertices())),
(~lj[1])(vert_i) - (~lj[1])(vert_j),
)
sc_set_left.add(sc)
i = j
vert_i = vert_j
for j, vert_j in vertices[1:]:
if vert_i != vert_j:
li = labels[i]
lio = ss.opposite_edge(li, edges[i])
lj = labels[j]
sc = SaddleConnection(
s,
(lj[0], (edges[j] + 1) % len(ss.polygon(lj).vertices())),
(~lj[1])(vert_i) - (~lj[1])(vert_j),
)
sc_set_left.add(sc)
i = j
vert_i = vert_j
self._boundary1 = frozenset(sc_set_right)
self._boundary2 = frozenset(sc_set_left)
self._boundary = frozenset(self._boundary1.union(self._boundary2))
edge_intersections = []
i = min_list[0]
label = labels[i]
p = ss.polygon(label)
right_point = p.vertex(edges[i]) # point on the right boundary
i = max_list[0]
label = labels[i]
p = ss.polygon(label)
left_point = p.vertex((edges[i] + 1) % len(p.vertices()))
from flatsurf.geometry.euclidean import solve
for i in range(len(edges)):
label = labels[i]
p = ss.polygon(label)
e = edges[i]
v1 = p.vertex(e)
v2 = p.vertex((e + 1) % len(p.vertices()))
a, b = solve(left_point, v, v1, v2 - v1)
w1 = (~(label[1]))(v1 + b * (v2 - v1))
a, b = solve(right_point, v, v1, v2 - v1)
w2 = (~(label[1]))(v1 + b * (v2 - v1))
edge_intersections.append((w1, w2))
polygons = []
pair1 = edge_intersections[-1]
l1 = labels[-2][0]
e1 = edges[-1]
for i in range(len(edges)):
l2 = labels[i][0]
pair2 = edge_intersections[i]
e2 = edges[i]
trans = s.edge_transformation(l1, e1)
pair1p = (trans(pair1[0]), trans(pair1[1]))
polygon_verts = [pair1p[0], pair1p[1]]
if pair2[1] != pair1p[1]:
polygon_verts.append(pair2[1])
if pair2[0] != pair1p[0]:
polygon_verts.append(pair2[0])
from flatsurf import Polygon
polygons.append(
(l2, Polygon(vertices=polygon_verts, base_ring=s.base_ring()))
)
l1 = l2
pair1 = pair2
e1 = e2
self._polygons = tuple(polygons)
[docs]
def surface(self):
return self._surface
[docs]
def initial_label(self):
r"""
Return one label on the surface that the cylinder passes through.
"""
return self._label0
[docs]
def edges(self):
r"""
Return a tuple of edge numbers representing the edges crossed
when the cylinder leaves the polygon with `initial_label` until
it returns by closing.
"""
return self._edges
[docs]
def boundary(self):
r"""
Return the set of saddle connections in the boundary, oriented so that
the surface is on the left.
"""
return self._boundary
[docs]
def polygons(self):
r"""
Return a list of pairs each consisting of a label and a polygon.
Each polygon represents a sub-polygon of the polygon on the surface
with the given label. The union of these sub-polygons form the
cylinder. The subpolygons are listed in cyclic order.
"""
return self._polygons
[docs]
@cached_method
def area(self):
r"""
Return the area of this cylinder if it is contained in a ConeSurface.
"""
from flatsurf.geometry.categories import ConeSurfaces
if self._surface not in ConeSurfaces():
raise NotImplementedError("area only makes sense for cone surfaces")
area = 0
for label, p in self.polygons():
area += p.area()
return area
[docs]
def plot(self, **options):
r"""
Plot this cylinder in coordinates used by a graphical surface. This
plots this cylinder as a union of subpolygons. Only the intersections
with polygons visible in the graphical surface are shown.
Parameters other than `graphical_surface` are passed to `polygon2d`
which is called to render the polygons.
Parameters
----------
graphical_surface : a GraphicalSurface
If not provided or `None`, the plot method uses the default graphical
surface for the surface.
"""
if "graphical_surface" in options and options["graphical_surface"] is not None:
gs = options["graphical_surface"]
if gs.get_surface() != self._surface:
raise ValueError("graphical surface for the wrong surface")
del options["graphical_surface"]
else:
gs = self._surface.graphical_surface()
plt = Graphics()
for label, p in self.polygons():
if gs.is_visible(label):
gp = gs.graphical_polygon(label)
t = gp.transformation()
pp = t(p)
poly = polygon2d(pp.vertices(), **options)
plt += poly.plot()
return plt
[docs]
@cached_method
def labels(self):
r"""
Return the set of labels that this cylinder passes through.
"""
polygons = self.polygons()
return frozenset([label for label, p in polygons])
[docs]
def boundary_components(self):
r"""
Return a set of two elements: the set of saddle connections on
the right and left sides. Saddle connections are oriented so that
the surface is on the left.
"""
return frozenset([self._boundary1, self._boundary2])
[docs]
def next(self, sc):
r"""
Return the next saddle connection as you move around the cylinder boundary
moving from sc in the direction of its orientation.
"""
if sc not in self._boundary:
raise ValueError
v = sc.end_tangent_vector()
v = v.clockwise_to(-v.vector())
from flatsurf.geometry.euclidean import is_parallel
for sc2 in self._boundary:
if sc2.start_data() == (
v.polygon_label(),
v.vertex(),
) and is_parallel(sc2.direction(), v.vector()):
return sc2
raise ValueError("Failed to find next saddle connection in boundary set.")
[docs]
def previous(self, sc):
r"""
Return the previous saddle connection as you move around the cylinder boundary
moving from sc in the direction opposite its orientation.
"""
if sc not in self._boundary:
raise ValueError
v = sc.start_tangent_vector()
v = v.counterclockwise_to(-v.vector())
from flatsurf.geometry.euclidean import is_parallel
for sc2 in self._boundary:
if sc2.end_data() == (v.polygon_label(), v.vertex()) and is_parallel(
sc2.end_direction(), v.vector()
):
return sc2
raise ValueError("Failed to find previous saddle connection in boundary set.")
[docs]
@cached_method
def holonomy(self):
r"""
In a translation surface, return one of the two holonomy vectors of the cylinder,
which differ by a sign.
"""
from flatsurf.geometry.categories import TranslationSurfaces
if self._surface not in TranslationSurfaces():
raise NotImplementedError(
"holonomy currently only computable for translation surfaces"
)
V = self._surface.base_ring() ** 2
total = V.zero()
for sc in self._boundary1:
total += sc.holonomy()
# Debugging:
total2 = V.zero()
for sc in self._boundary2:
total2 += sc.holonomy()
assert (
total + total2 == V.zero()
), "Holonomy of the two boundary components should sum to zero."
return total
[docs]
@cached_method
def circumference(self):
r"""
In a cone surface, return the circumference, i.e., the length
of a geodesic loop running around the cylinder. Since this may
not lie in the field of definition of the surface, it is returned
as an element of the Algebraic Real Field.
"""
from flatsurf.geometry.categories import ConeSurfaces
if self._surface not in ConeSurfaces():
raise NotImplementedError(
"circumference only makes sense for cone surfaces"
)
total = 0
for sc in self._boundary1:
total += sc.length()
return total