Source code for flatsurf.geometry.categories.euclidean_polygons_with_angles

r"""
The category of polygons in the real plane with fixed rational
angles.

This module provides a common structure for all polygons with certain fixed
angles.

See :mod:`flatsurf.geometry.categories` for a general description of the
category framework in sage-flatsurf.

Normally, you won't create this (or any other) category directly. The correct
category is automatically determined for polygons.

EXAMPLES:

The category of rectangles::

    sage: from flatsurf.geometry.categories import EuclideanPolygons
    sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1])

It is often tedious to create this category manually, since you need to
determine a base ring that can describe the coordinates of polygons with such
angles::

    sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1])
    sage: C.slopes()
    Traceback (most recent call last):
    ...
    TypeError: Unable to coerce c to a rational

    sage: C = EuclideanPolygons(AA).WithAngles([1, 1, 1])
    sage: C.slopes()
    [(1, 0), (-0.5773502691896258?, 1), (-0.5773502691896258?, -1)]

Instead, we can use :func:`~.polygon.EuclideanPolygonsWithAngles` to create this category
over a minimal number field::

    sage: from flatsurf import EuclideanPolygonsWithAngles
    sage: C = EuclideanPolygonsWithAngles([1, 1, 1])
    sage: C.slopes()
    [(1, 0), (-c, 3), (-c, -3)]

The category of polygons is automatically determined when using
:func:`~.polygon.Polygon`::

    sage: from flatsurf import Polygon
    sage: p = Polygon(angles=(1, 1, 1))
    sage: p.category()
    Category of convex simple euclidean equilateral triangles over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878?

However, it can be very costly to determine that a polygon is rational and what
its actual angles are (the "equilateral" in the previous example.) Therefore,
the category might get refined once these aspects have been determined::

    sage: p = Polygon(edges=[(1, 0), (0, 1), (-1, 0), (0, -1)])
    sage: p.category()
    Category of convex simple euclidean polygons over Rational Field
    sage: p.is_rational()
    True
    sage: p.category()
    Category of rational convex simple euclidean polygons over Rational Field
    sage: p.angles()
    (1/4, 1/4, 1/4, 1/4)
    sage: p.category()
    Category of convex simple euclidean rectangles over Rational Field

Note that SageMath applies the same strategy when determining whether the
integers modulo N are a field::

    sage: K = Zmod(1361)
    sage: K.category()
    Join of Category of finite commutative rings and Category of subquotients of monoids and Category of quotients of semigroups and Category of finite enumerated sets
    sage: K.is_field()
    True
    sage: K.category()
    Join of Category of finite enumerated fields and Category of subquotients of monoids and Category of quotients of semigroups

"""
# ****************************************************************************
#  This file is part of sage-flatsurf.
#
#        Copyright (C) 2016-2020 Vincent Delecroix
#                      2020-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, cached_function
from sage.categories.category_types import Category_over_base_ring
from sage.categories.category_with_axiom import CategoryWithAxiom_over_base_ring
from flatsurf.geometry.categories.euclidean_polygons import EuclideanPolygons


[docs]class EuclideanPolygonsWithAngles(Category_over_base_ring): r""" The category of euclidean polygons with fixed rational angles. EXAMPLES:: sage: from flatsurf.geometry.categories import EuclideanPolygons sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]) TESTS:: sage: TestSuite(C).run() """ def __init__(self, base_ring, angles): self._angles = angles super().__init__(base_ring)
[docs] def super_categories(self): r""" Return the other categories such polygons are automatically members of, namely, the category of rational euclidean polygons polygons. EXAMPLES:: sage: from flatsurf.geometry.categories import EuclideanPolygons sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]) sage: C.super_categories() [Category of rational euclidean polygons over Rational Field] """ return [EuclideanPolygons(self.base_ring()).Rational()]
@staticmethod def _normalize_angles(angles): r""" Return ``angles`` normalized such that they sum to n/2-1 where ``n`` is the number of angles, i.e., they scale to the angle in an n-gon divided by π. EXAMPLES:: sage: from flatsurf.geometry.categories import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles._normalize_angles([1, 1, 1, 1]) (1/4, 1/4, 1/4, 1/4) sage: EuclideanPolygonsWithAngles._normalize_angles([1, 2, 3, 4]) (1/10, 1/5, 3/10, 2/5) sage: EuclideanPolygonsWithAngles._normalize_angles([1, 2, 3]) (1/12, 1/6, 1/4) """ n = len(angles) if n < 3: raise ValueError("there must be at least three angles") from sage.all import QQ, ZZ angles = [QQ.coerce(a) for a in angles] if any(angle <= 0 for angle in angles): raise ValueError("angles must be positive rationals") # Store each angle as a multiple of 2π, i.e., normalize them such their sum is (n - 2)/2. angles = [a / sum(angles) for a in angles] angles = [a * ZZ(n - 2) / 2 for a in angles] if any(angle <= 0 or angle >= 1 for angle in angles): raise NotImplementedError("each angle must be in (0, 2π)") angles = tuple(angles) return angles @cached_method def _slopes(self): slopes = _slopes(self._angles) # We bring the slopes first into the minimal number field in which they # are defined since otherwise conversion from the cosines ring to the # base_ring might fail. E.g., when the base ring is the exact-reals # over the minimal base ring. minimal_base_ring = _base_ring(self._angles) slopes = [ (minimal_base_ring(slope[0]), minimal_base_ring(slope[1])) for slope in slopes ] return [ (self.base_ring()(slope[0]), self.base_ring()(slope[1])) for slope in slopes ] @cached_method def _cosines_ring(self): slopes = _slopes(self._angles) return slopes[0][0].parent() def _repr_object_names(self): r""" Helper method to create the name of this category. EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles([1/6, 1/6, 1/6]) Category of simple euclidean equilateral triangles over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? sage: EuclideanPolygonsWithAngles([1/4, 1/4, 1/4, 1/4]) Category of simple euclidean rectangles over Rational Field sage: EuclideanPolygonsWithAngles([1/10, 2/10, 3/10, 4/10]) Category of simple euclidean quadrilaterals with angles (1/10, 1/5, 3/10, 2/5) over Number Field in c with defining polynomial x^4 - 5*x^2 + 5 with c = 1.902113032590308? """ names = super()._repr_object_names() equiangular = len(set(self._angles)) == 1 from flatsurf.geometry.categories.polygons import Polygons _, _, polygons = Polygons._describe_polygon( len(self._angles), equiangular=equiangular ) with_angles = "" if equiangular else f" with angles {self.angles(False)}" return names.replace(" with angles", with_angles).replace("polygons", polygons)
[docs] class ParentMethods: r""" Provides methods available to all polygons with known angles. If you want to add functionality to all such polygons, you probably want to put it here. """
[docs] def is_convex(self, strict=False): r""" Return whether this is a convex polygon. INPUT: - ``strict`` -- whether to check for strict convexity, i.e., a polygon with a π angle is not considered convex. EXAMPLES:: sage: from flatsurf import polygons sage: S = polygons.square() sage: S.is_convex() True sage: S.is_convex(strict=True) True """ return self.category().is_convex()
[docs] def angle(self, e, numerical=None, assume_rational=None): r""" Return the angle at the beginning of the start point of the edge ``e``. EXAMPLES:: sage: from flatsurf.geometry.polygon import polygons sage: polygons.square().angle(0) 1/4 sage: polygons.regular_ngon(8).angle(0) 3/8 sage: from flatsurf import Polygon sage: T = Polygon(vertices=[(0,0), (3,1), (1,5)]) sage: [T.angle(i, numerical=True) for i in range(3)] # abs tol 1e-13 [0.16737532973071603, 0.22741638234956674, 0.10520828791971722] sage: sum(T.angle(i, numerical=True) for i in range(3)) # abs tol 1e-13 0.5 """ if assume_rational is not None: import warnings warnings.warn( "assume_rational has been deprecated as a keyword to angle() and will be removed from a future version of sage-flatsurf" ) if numerical is None: numerical = not self.is_rational() if numerical: import warnings warnings.warn( "the behavior of angle() has been changed in recent versions of sage-flatsurf; for non-rational polygons, numerical=True must be set explicitly to get a numerical approximation of the angle" ) angle = self.category().angles()[e] if numerical: from sage.all import RR angle = RR(angle) return angle
[docs] class SubcategoryMethods:
[docs] def is_convex(self, strict=False): r""" Return whether the polygons in this category are convex. INPUT: - ``strict`` -- a boolean (default: ``False``); whether only to consider polygons convex if all angles are <π. EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles(1, 2, 5).is_convex() True sage: EuclideanPolygonsWithAngles(2, 2, 3, 13).is_convex() False :: sage: E = EuclideanPolygonsWithAngles([1, 1, 1, 1, 2]) sage: E.angles() (1/4, 1/4, 1/4, 1/4, 1/2) sage: E.is_convex(strict=False) True sage: E.is_convex(strict=True) False """ if strict: return all(2 * a < 1 for a in self.angles()) return all(2 * a <= 1 for a in self.angles())
[docs] def convexity(self): r""" EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles(1, 2, 5).convexity() doctest:warning ... UserWarning: convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex() instead True sage: EuclideanPolygonsWithAngles(2, 2, 3, 13).convexity() False """ import warnings warnings.warn( "convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex() instead" ) return self.is_convex()
[docs] def strict_convexity(self): r""" EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: E = EuclideanPolygonsWithAngles([1, 1, 1, 1, 2]) sage: E.angles() (1/4, 1/4, 1/4, 1/4, 1/2) sage: E.convexity() True sage: E.strict_convexity() doctest:warning ... UserWarning: strict_convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex(strict=True) instead False """ import warnings warnings.warn( "strict_convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex(strict=True) instead" ) return self.is_convex(strict=True)
[docs] def angles(self, integral=False): r""" Return the interior angles of this polygon as multiples of 2π. INPUT: - ``integral`` -- a boolean (default: ``False``); whether to return the angles not as multiples of 2π but rescaled so that they have no denominators. EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: E = EuclideanPolygonsWithAngles(1, 1, 1, 2, 6) sage: E.angles() (3/22, 3/22, 3/22, 3/11, 9/11) When ``integral`` is set, the output is scaled to eliminate denominators:: sage: E.angles(integral=True) (1, 1, 1, 2, 6) """ angles = self.__angles() if integral: from sage.all import lcm, ZZ, gcd C = lcm([a.denominator() for a in self.angles()]) / gcd( [a.numerator() for a in self.angles()] ) angles = tuple(ZZ(C * a) for a in angles) return angles
@cached_method def __angles(self): r""" Helper method for :meth:`angles` to lookup the stored angles if this is a subcategory of :class:`EuclideanPolygonsWithAngles`. """ if isinstance(self, EuclideanPolygonsWithAngles): return self._angles for category in self.all_super_categories(): if isinstance(category, EuclideanPolygonsWithAngles): return category._angles assert ( False ), "EuclideanPolygonsWithAngles should be a supercategory of this category"
[docs] def slopes(self, e0=(1, 0)): r""" Return the slopes of the edges as a list of vectors. INPUT: - ``e0`` -- the first slope returned (default: ``(1, 0)``) EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles(1, 2, 1, 2).slopes() [(1, 0), (c, 3), (-1, 0), (-c, -3)] """ V = self.base_ring() ** 2 slopes = self.__slopes() n = len(slopes) cosines = [x[0] for x in slopes] sines = [x[1] for x in slopes] e = V(e0) edges = [e] for i in range(n - 1): e = ( -cosines[i + 1] * e[0] - sines[i + 1] * e[1], sines[i + 1] * e[0] - cosines[i + 1] * e[1], ) from flatsurf.geometry.euclidean import projectivization e = projectivization(*e) edges.append(V(e)) return edges
@cached_method def __slopes(self): r""" Helper method for :meth:`slopes` to lookup the stored slopes if this is a subcategory of :class:`EuclideanPolygonsWithAngles`. """ if isinstance(self, EuclideanPolygonsWithAngles): return self._slopes() for category in self.all_super_categories(): if isinstance(category, EuclideanPolygonsWithAngles): return category._slopes() assert ( False ), "EuclideanPolygonsWithAngles should be a supercategory of this category" # TODO: rather than lengths, it would be more convenient to have access # to the tangent space (that is the space of possible holonomies). However, # since it is not defined over the real numbers, there are several possible ways # to handle the data. # TODO: here we ignored the direction SO(2) which provides additional symmetry # in the tangent space
[docs] @cached_method def lengths_polytope(self): r""" Return the polytope parametrizing the admissible vectors data. This polytope parametrizes the tangent space to the set of these equiangular polygons. Be careful that even though the lengths are admissible, they may not define a polygon without intersection. EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles(1, 2, 1, 2).lengths_polytope() A 2-dimensional polyhedron in (Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878?)^4 defined as the convex hull of 1 vertex and 2 rays """ n = len(self.angles()) slopes = self.slopes() eqns = [[0] + [s[0] for s in slopes], [0] + [s[1] for s in slopes]] ieqs = [] for i in range(n): ieq = [0] * (n + 1) ieq[i + 1] = 1 ieqs.append(ieq) from sage.geometry.polyhedron.constructor import Polyhedron return Polyhedron(eqns=eqns, ieqs=ieqs, base_ring=self.base_ring())
[docs] def an_element(self): r""" Return a polygon in this category. Since currently polygons must not be self-intersecting, the construction used might fail. EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles(4, 3, 4, 4, 3, 4).an_element() Polygon(vertices=[(0, 0), (1/22*c + 1, 0), (9*c^9 + 1/2*c^8 - 88*c^7 - 9/2*c^6 + 297*c^5 + 27/2*c^4 - 396*c^3 - 15*c^2 + 3631/22*c + 11/2, 1/2*c + 11), (16*c^9 + c^8 - 154*c^7 - 9*c^6 + 506*c^5 + 27*c^4 - 638*c^3 - 30*c^2 + 4841/22*c + 9, c + 22), (16*c^9 + c^8 - 154*c^7 - 9*c^6 + 506*c^5 + 27*c^4 - 638*c^3 - 30*c^2 + 220*c + 8, c + 22), (7*c^9 + 1/2*c^8 - 66*c^7 - 9/2*c^6 + 209*c^5 + 27/2*c^4 - 242*c^3 - 15*c^2 + 55*c + 7/2, 1/2*c + 11)]) """ from flatsurf import Polygon p = Polygon(angles=self.angles()) if p not in self: # pylint: disable=unsupported-membership-test raise NotImplementedError( "cannot create an element in this category yet" ) return p
[docs] def random_element(self, ring=None, **kwds): r""" Return a random polygon in this category. EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: EuclideanPolygonsWithAngles(1, 1, 1, 2, 5).random_element() Polygon(vertices=[(0, 0), ...]) sage: EuclideanPolygonsWithAngles(1,1,1,15,15,15).random_element() Polygon(vertices=[(0, 0), ...]) sage: EuclideanPolygonsWithAngles(1,15,1,15,1,15).random_element() Polygon(vertices=[(0, 0), ...]) """ if ring is None: from sage.all import QQ ring = QQ rays = [r.vector() for r in self.lengths_polytope().rays()] def random_element(): while True: coeffs = [] while len(coeffs) < len(rays): x = ring.random_element(**kwds) while x < 0: x = ring.random_element(**kwds) coeffs.append(x) sol = sum(c * r for c, r in zip(coeffs, rays)) if all(x > 0 for x in sol): return coeffs, sol while True: coeffs, lengths = random_element() edges = [ length * slope for (length, slope) in zip(lengths, self.slopes()) ] from flatsurf import Polygon p = Polygon(edges=edges, check=False) from flatsurf.geometry.categories import EuclideanPolygons if not EuclideanPolygons.ParentMethods.is_simple(p): continue p = Polygon(edges=edges, angles=self.angles(), check=False) break if p not in self: # pylint: disable=unsupported-membership-test raise NotImplementedError( "cannot create a random element in this category yet" ) return p
[docs] def billiard_unfolding_angles(self, cover_type="translation"): r""" Return the angles of the unfolding rational, half-translation or translation surface. INPUT: - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, ``"half-translation"`` or ``"translation"`` EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: E = EuclideanPolygonsWithAngles(1, 2, 5) sage: E.billiard_unfolding_angles(cover_type="rational") {1/8: 1, 1/4: 1, 5/8: 1} sage: (1/8 - 1) + (1/4 - 1) + (5/8 - 1) # Euler characteristic (of the sphere) -2 sage: E.billiard_unfolding_angles(cover_type="half-translation") {1/2: 3, 5/2: 1} sage: E.billiard_unfolding_angles(cover_type="translation") {1: 3, 5: 1} sage: E = EuclideanPolygonsWithAngles(1, 3, 1, 7) sage: E.billiard_unfolding_angles(cover_type="rational") {1/6: 2, 1/2: 1, 7/6: 1} sage: 2 * (1/6 - 1) + (1/2 - 1) + (7/6 - 1) # Euler characteristic -2 sage: E.billiard_unfolding_angles(cover_type="half-translation") {1/2: 5, 7/2: 1} sage: E.billiard_unfolding_angles(cover_type="translation") {1: 5, 7: 1} sage: E = EuclideanPolygonsWithAngles(1, 3, 5, 7) sage: E.billiard_unfolding_angles(cover_type="rational") {1/8: 1, 3/8: 1, 5/8: 1, 7/8: 1} sage: (1/8 - 1) + (3/8 - 1) + (5/8 - 1) + (7/8 - 1) # Euler characteristic -2 sage: E.billiard_unfolding_angles(cover_type="half-translation") {1/2: 1, 3/2: 1, 5/2: 1, 7/2: 1} sage: E.billiard_unfolding_angles(cover_type="translation") {1: 1, 3: 1, 5: 1, 7: 1} sage: E = EuclideanPolygonsWithAngles(1, 2, 8) sage: E.billiard_unfolding_angles(cover_type="rational") {1/11: 1, 2/11: 1, 8/11: 1} sage: (1/11 - 1) + (2/11 - 1) + (8/11 - 1) # Euler characteristic -2 sage: E.billiard_unfolding_angles(cover_type="half-translation") {1: 1, 2: 1, 8: 1} sage: E.billiard_unfolding_angles(cover_type="translation") {1: 1, 2: 1, 8: 1} """ rat_angles = {} for a in self.angles(): if 2 * a in rat_angles: rat_angles[2 * a] += 1 else: rat_angles[2 * a] = 1 if cover_type == "rational": return rat_angles from sage.all import lcm N = lcm([x.denominator() for x in rat_angles]) if N % 2: N *= 2 cov_angles = {} for x, mult in rat_angles.items(): y = x.numerator() d = x.denominator() if d % 2: d *= 2 else: y = y / 2 assert N % d == 0 if y in cov_angles: cov_angles[y] += mult * N // d else: cov_angles[y] = mult * N // d if cover_type == "translation" and any( y.denominator() == 2 for y in cov_angles ): covcov_angles = {} for y, mult in cov_angles.items(): yy = y.numerator() if yy not in covcov_angles: covcov_angles[yy] = 0 covcov_angles[yy] += 2 // y.denominator() * mult return covcov_angles elif cover_type == "half-translation" or cover_type == "translation": return cov_angles else: raise ValueError("unknown 'cover_type' {!r}".format(cover_type))
[docs] def billiard_unfolding_stratum( self, cover_type="translation", marked_points=False ): r""" Return the stratum of quadratic or Abelian differential obtained by unfolding a billiard in a polygon of this equiangular family. INPUT: - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, ``"half-translation"`` or ``"translation"`` - ``marked_poins`` (optional, default ``False``) - whether the stratum should have regular marked points EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles, similarity_surfaces sage: E = EuclideanPolygonsWithAngles(1, 2, 5) sage: E.billiard_unfolding_stratum("half-translation") Q_1(3, -1^3) sage: E.billiard_unfolding_stratum("translation") H_3(4) sage: E.billiard_unfolding_stratum("half-translation", True) Q_1(3, -1^3) sage: E.billiard_unfolding_stratum("translation", True) H_3(4, 0^3) sage: E = EuclideanPolygonsWithAngles(1, 3, 1, 7) sage: E.billiard_unfolding_stratum("half-translation") Q_1(5, -1^5) sage: E.billiard_unfolding_stratum("translation") H_4(6) sage: E.billiard_unfolding_stratum("half-translation", True) Q_1(5, -1^5) sage: E.billiard_unfolding_stratum("translation", True) H_4(6, 0^5) sage: P = E.an_element() sage: S = similarity_surfaces.billiard(P) sage: S.minimal_cover("half-translation").stratum() Q_1(5, -1^5) sage: S.minimal_cover("translation").stratum() H_4(6, 0^5) sage: E = EuclideanPolygonsWithAngles(1, 3, 5, 7) sage: E.billiard_unfolding_stratum("half-translation") Q_3(5, 3, 1, -1) sage: E.billiard_unfolding_stratum("translation") H_7(6, 4, 2) sage: P = E.an_element() sage: S = similarity_surfaces.billiard(P) sage: S.minimal_cover("half-translation").stratum() Q_3(5, 3, 1, -1) sage: S.minimal_cover("translation").stratum() H_7(6, 4, 2, 0) sage: E = EuclideanPolygonsWithAngles(1, 2, 8) sage: E.billiard_unfolding_stratum("half-translation") H_5(7, 1) sage: E.billiard_unfolding_stratum("translation") H_5(7, 1) sage: E.billiard_unfolding_stratum("half-translation", True) H_5(7, 1, 0) sage: E.billiard_unfolding_stratum("translation", True) H_5(7, 1, 0) sage: E = EuclideanPolygonsWithAngles(9, 6, 3, 2) sage: p = E.an_element() sage: B = similarity_surfaces.billiard(p) sage: B.minimal_cover("half-translation").stratum() Q_4(7, 4, 1, 0) sage: E.billiard_unfolding_stratum("half-translation", True) Q_4(7, 4, 1, 0) sage: B.minimal_cover("translation").stratum() H_8(8, 2^3, 0^2) sage: E.billiard_unfolding_stratum("translation", True) H_8(8, 2^3, 0^2) """ from sage.all import ZZ angles = self.billiard_unfolding_angles(cover_type) if all(a.is_integer() for a in angles): from surface_dynamics import AbelianStratum if not marked_points and len(angles) == 1 and 1 in angles: return AbelianStratum([0]) else: return AbelianStratum( { ZZ(a - 1): mult for a, mult in angles.items() if marked_points or a != 1 } ) else: from surface_dynamics import QuadraticStratum return QuadraticStratum( { ZZ(2 * (a - 1)): mult for a, mult in angles.items() if marked_points or a != 1 } )
[docs] def billiard_unfolding_stratum_dimension( self, cover_type="translation", marked_points=False ): r""" Return the dimension of the stratum of quadratic or Abelian differential obtained by unfolding a billiard in a polygon of this equiangular family. INPUT: - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, ``"half-translation"`` or ``"translation"`` - ``marked_poins`` (optional, default ``False``) - whether the stratum should have marked regular points EXAMPLES:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: E = EuclideanPolygonsWithAngles(1, 1, 1) sage: E.billiard_unfolding_stratum_dimension("half-translation") 2 sage: E.billiard_unfolding_stratum_dimension("translation") 2 sage: E.billiard_unfolding_stratum_dimension("half-translation", True) 4 sage: E.billiard_unfolding_stratum_dimension("translation", True) 4 sage: E = EuclideanPolygonsWithAngles(1, 2, 5) sage: E.billiard_unfolding_stratum_dimension("half-translation") 4 sage: E.billiard_unfolding_stratum("half-translation").dimension() 4 sage: E.billiard_unfolding_stratum_dimension(cover_type="translation") 6 sage: E.billiard_unfolding_stratum("translation").dimension() 6 sage: E.billiard_unfolding_stratum_dimension("translation", True) 9 sage: E.billiard_unfolding_stratum("translation", True).dimension() 9 sage: E = EuclideanPolygonsWithAngles(1, 3, 5) sage: E.billiard_unfolding_stratum_dimension("half-translation") 6 sage: E.billiard_unfolding_stratum("half-translation").dimension() 6 sage: E.billiard_unfolding_stratum_dimension("translation") 6 sage: E.billiard_unfolding_stratum("translation").dimension() 6 sage: E = EuclideanPolygonsWithAngles(1, 3, 1, 7) sage: E.billiard_unfolding_stratum_dimension("half-translation") 6 sage: E = EuclideanPolygonsWithAngles(1, 3, 5, 7) sage: E.billiard_unfolding_stratum_dimension("half-translation") 8 sage: E = EuclideanPolygonsWithAngles(1, 2, 8) sage: E.billiard_unfolding_stratum_dimension() 11 sage: E.billiard_unfolding_stratum().dimension() 11 sage: E.billiard_unfolding_stratum_dimension(marked_points=True) 12 sage: E.billiard_unfolding_stratum(marked_points=True).dimension() 12 """ from sage.all import ZZ if cover_type == "rational": raise NotImplementedError if cover_type != "translation" and cover_type != "half-translation": raise ValueError angles = self.billiard_unfolding_angles(cover_type) if not marked_points: if 1 in angles: del angles[1] if not angles: angles[ZZ.one()] = ZZ.one() abelian = all(a.is_integer() for a in angles) s = sum(angles.values()) chi = sum(mult * (a - 1) for a, mult in angles.items()) assert chi.denominator() == 1 chi = ZZ(chi) assert chi % 2 == 0 g = chi // 2 + 1 return 2 * g + s - 1 if abelian else 2 * g + s - 2
[docs] class Simple(CategoryWithAxiom_over_base_ring): r""" The subcategory of simple Euclidean polygons with prescribed angles. EXAMPLES:: sage: from flatsurf.geometry.categories import EuclideanPolygons sage: EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]).Simple() Category of simple euclidean rectangles over Rational Field """ # Due to some limitations in SageMath this does not work. Apparently, # extra_super_categories cannot depend on parameters of the category. # With this code, some polygons are randomly declared as convex. # Unfortunately, this means that the category prints as "convex simple # rectangles" and not just "rectangles". # def extra_super_categories(self): # r""" # Return the categories that simple polygons with prescribed angles # are additionally contained in; namely, in some cases the category # of convex polygons. # EXAMPLES:: # sage: from flatsurf.geometry.categories import EuclideanPolygons # sage: C = EuclideanPolygons(QQ).Simple().WithAngles([1, 1, 1, 1]) # sage: "Convex" in C.axioms() # True # sage: C = EuclideanPolygons(QQ).Simple().WithAngles([2, 2, 1, 6, 1]) # sage: "Convex" in C.axioms() # False # """ # # We cannot call is_convex() yet because the SubcategoryMethods # # have not been established yet. # if self._base_category.is_convex(): # return (self._base_category.Convex(),) # return () def __call__(self, *lengths, normalized=False, base_ring=None): r""" Return a polygon with these angles from ``lengths``. TESTS:: sage: from flatsurf import EuclideanPolygonsWithAngles sage: P = EuclideanPolygonsWithAngles(1, 2, 1, 2) sage: L = P.lengths_polytope() sage: r0, r1 = [r.vector() for r in L.rays()] sage: lengths = r0 + r1 sage: P(*lengths[:-2]) doctest:warning ... UserWarning: calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead. To make the resulting polygon non-normalized, i.e., the lengths are not actual edge lengths but the multiple of slope vectors, use Polygon(edges=[length * slope for (length, slope) in zip(lengths, EuclideanPolygonsWithAngles(angles).slopes())]). Polygon(vertices=[(0, 0), (1, 0), (c + 1, 3), (c, 3)]) sage: from flatsurf import Polygon, EuclideanPolygonsWithAngles sage: P = EuclideanPolygonsWithAngles([1, 2, 1, 2]) sage: Polygon(angles=[1, 2, 1, 2], lengths=lengths[:-2]) Polygon(vertices=[(0, 0), (1, 0), (3/2, 1/2*c), (1/2, 1/2*c)]) sage: Polygon(angles=[1, 2, 1, 2], edges=[length * slope for (length, slope) in zip(lengths[:-2], P.slopes())]) Polygon(vertices=[(0, 0), (1, 0), (c + 1, 3), (c, 3)]) sage: P = EuclideanPolygonsWithAngles(2, 2, 3, 13) sage: r0, r1 = [r.vector() for r in P.lengths_polytope().rays()] sage: P(r0 + r1) Polygon(vertices=[(0, 0), (20, 0), (5, -15*c^3 + 60*c), (5, -5*c^3 + 20*c)]) sage: P = EuclideanPolygonsWithAngles([2, 2, 3, 13]) sage: Polygon(angles=[2, 2, 3, 13], lengths=r0 + r1) Traceback (most recent call last): ... ValueError: polygon not closed sage: Polygon(angles=[2, 2, 3, 13], edges=[length * slope for (length, slope) in zip(r0 + r1, P.slopes())]) Polygon(vertices=[(0, 0), (20, 0), (5, -15*c^3 + 60*c), (5, -5*c^3 + 20*c)]) """ # __call__() cannot be properly inherited in subcategories since it # cannot be in SubcategoryMethods; that's why we want to get rid of it. import warnings warning = "calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead." if not normalized: warning += ( " To make the resulting polygon non-normalized, i.e., the lengths are not actual edge lengths but the multiple of slope vectors, use " "Polygon(edges=[length * slope for (length, slope) in zip(lengths, EuclideanPolygonsWithAngles(angles).slopes())])." ) warnings.warn(warning) from sage.structure.element import Vector if len(lengths) == 1 and isinstance(lengths[0], (tuple, list, Vector)): lengths = lengths[0] n = len(self.angles()) if len(lengths) != n - 2 and len(lengths) != n: raise ValueError( "must provide %d or %d lengths but provided %d" % (n - 2, n, len(lengths)) ) V = self.base_ring() ** 2 slopes = self.slopes() if normalized: cosines_ring = ( self._without_axiom("Simple") ._without_axiom("Convex") ._cosines_ring() ) V = V.change_ring(cosines_ring) for i, s in enumerate(slopes): x, y = map(cosines_ring, s) norm2 = (x**2 + y**2).sqrt() slopes[i] = V((x / norm2, y / norm2)) if base_ring is None: from sage.all import Sequence base_ring = Sequence(lengths).universe() from sage.categories.pushout import pushout if normalized: base_ring = pushout(base_ring, cosines_ring) else: base_ring = pushout(base_ring, self.base_ring()) v = V((0, 0)) vertices = [v] from sage.all import vector, matrix if len(lengths) == n - 2: for i in range(n - 2): v += lengths[i] * slopes[i] vertices.append(v) s, t = ( vector(vertices[0] - vertices[n - 2]) * matrix([slopes[-1], slopes[n - 2]]).inverse() ) assert ( vertices[0] - s * slopes[-1] == vertices[n - 2] + t * slopes[n - 2] ) if s <= 0 or t <= 0: raise ValueError( "the provided lengths do not give rise to a polygon" ) vertices.append(vertices[0] - s * slopes[-1]) elif len(lengths) == n: for i in range(n): v += lengths[i] * slopes[i] vertices.append(v) if not vertices[-1].is_zero(): raise ValueError( "the provided lengths do not give rise to a polygon" ) vertices.pop(-1) category = self.change_ring(base_ring) if self.is_convex(): category = category.Convex() from flatsurf.geometry.polygon import Polygon return Polygon(base_ring=base_ring, vertices=vertices, category=category)
@cached_function def _slopes(angles): r""" Return the slopes of the sides of a polygon with ``angles`` in a (possibly non-minimal) number field. .. NOTE:: This function gets called a lot from the other functions here. We could refactor things and make sure that the function is only invoked once. However, it seems easier to just cache its output. Also, this speeds up the relative common case when multiple polygons with the same angles are created. (If lots of polygons with different angles get created then we pay this cache with RAM of course but in our experiments it has not been an issue.) .. NOTE:: SageMath 9.1 does not support cached functions that are staticmethods. Therefore we define this function at the module level. EXAMPLES:: sage: from flatsurf.geometry.categories.euclidean_polygons_with_angles import _slopes sage: _slopes((1/6, 1/6, 1/6)) [(c, 3), (c, 3), (c, 3)] sage: _slopes((1/4, 1/4, 1/4, 1/4)) [(0, 1), (0, 1), (0, 1), (0, 1)] sage: _slopes((1/10, 2/10, 3/10, 4/10)) [(c^3, 5), (3*c^3 - 10*c, 5), (-3*c^3 + 10*c, 5), (-c^3, 5)] """ from sage.all import QQ, RIF, lcm, AA, NumberField from flatsurf.geometry.subfield import chebyshev_T, cos_minpoly # We determine the number field that contains the slopes of the sides, # i.e., the cosines and sines of the inner angles of the polygon. # Let us first write all angles as multiples of 2π/N with the smallest # possible common N. N = lcm(a.denominator() for a in angles) # The field containing the cosine and sine of 2π/N might be too small # to write down all the slopes when N is not divisible by 4. if N == 1: raise ValueError("there cannot be a polygon with all angles multiples of 2π") if N == 2: pass elif N % 4: while N % 4: N *= 2 angles = [QQ(a * N) for a in angles] if N == 2: base_ring = QQ c = QQ.zero() else: # Construct the minimal polynomial f(x) of c = 2 cos(2π / N) f = cos_minpoly(N // 2) emb = AA.polynomial_root(f, 2 * (2 * RIF.pi() / N).cos()) base_ring = NumberField(f, "c", embedding=emb) c = base_ring.gen() # Construct the cosine and sine of each angle as an element of our number field. def cosine(a): return chebyshev_T(abs(a), c) / 2 def sine(a): # Use sin(x) = cos(π/2 - x) return cosine(N // 4 - a) slopes = [(cosine(a), sine(a)) for a in angles] assert all((x**2 + y**2).is_one() for x, y in slopes) from flatsurf.geometry.euclidean import projectivization return [projectivization(x, y) for x, y in slopes] @cached_function def _base_ring(angles): r""" Return a minimal number field containing all the :meth:`_slopes` of a polygon with ``angles``. .. NOTE:: Internally, this uses :func:`~flatsurf.geometry.subfield.subfield_from_element` which is very slow. We therefore cache the result currently. .. NOTE:: SageMath 9.1 does not support cached functions that are staticmethods. Therefore we define this function at the module level. EXAMPLES:: sage: from flatsurf.geometry.categories.euclidean_polygons_with_angles import _base_ring sage: _base_ring((1/6, 1/6, 1/6)) Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? sage: _base_ring((1/4, 1/4, 1/4, 1/4)) Rational Field sage: _base_ring((1/10, 2/10, 3/10, 4/10)) Number Field in c with defining polynomial x^4 - 5*x^2 + 5 with c = 1.902113032590308? """ slopes = _slopes(angles) base_ring = slopes[0][0].parent() # It might be the case that the slopes generate a smaller field. For # now we use an ugly workaround via subfield_from_elements. old_slopes = [] for v in slopes: old_slopes.extend(v) from flatsurf.geometry.subfield import subfield_from_elements L, _, _ = subfield_from_elements(base_ring, old_slopes) return L