Source code for flatsurf.geometry.hyperbolic

r"""
Two dimensional hyperbolic geometry.

.. jupyter-execute::
    :hide-code:

    # Allow jupyter-execute blocks in this module to contain doctests
    import jupyter_doctest_tweaks

EXAMPLES::

    sage: from flatsurf import HyperbolicPlane

    sage: H = HyperbolicPlane()

Points in the hyperbolic plane can be specified directly with coordinates
in the upper (complex) half plane::

    sage: H(1 + I)
    1 + I

The hyperbolic plane is defined over a fixed base ring; the rationals if no
base has been specified explicitly::

    sage: H(sqrt(2) + I)
    Traceback (most recent call last):
    ...
    TypeError: unable to convert sqrt(2) to a rational

We can use a bigger field instead::

    sage: H_algebraic = HyperbolicPlane(AA)
    sage: H_algebraic(sqrt(2) + I)
    1.414213562373095? + 1.000000000000000?*I

Given two points in the hyperbolic plane, we can form the geodesic they lay on::

    sage: a = H(I)
    sage: b = H(2*I)
    sage: ab = H.geodesic(a, b)
    sage: ab
    {-x = 0}

Note that such a geodesic is oriented. The orientation is such that when we
replace the ``=`` in the above representation with a ``≥``, we obtain the half
space on its left::

    sage: H.geodesic(a, b).left_half_space()
    {x ≤ 0}

We can pass explicitly to the unoriented geodesic. Note that the oriented and
the unoriented version of a geodesic are not considered equal::

    sage: ab.unoriented()
    {x = 0}
    sage: ab == ab.unoriented()
    False
    sage: ab.is_subset(ab.unoriented())
    True
    sage: ab.unoriented().is_subset(ab)
    True

A vertical can also be specified directly::

    sage: H.vertical(0)
    {-x = 0}

We can also create ideal, i.e., infinite, points in the hyperbolic plane and
construct the geodesic that connects them::

    sage: H(1)
    1

    sage: H(oo)


    sage: H.geodesic(1, oo)
    {-x + 1 = 0}

The geodesic that is given by a half circle in the upper half plane can be
created directly by providing its midpoint and the square of its radius::

    sage: H.half_circle(0, 1)
    {(x^2 + y^2) - 1 = 0}

Geodesics can be intersected::

    sage: H.half_circle(0, 1).intersection(H.vertical(0))
    I

    sage: H.half_circle(0, 1).intersection(H.half_circle(0, 2))
    {}

The intersection of two geodesics might be an ideal point::

    sage: H.vertical(-1).intersection(H.vertical(1))


General convex subsets of the hyperbolic plane can be constructed by
intersecting half spaces; this way we can construct (possibly unbounded) convex
polygons::

    sage: P = H.intersection(
    ....:   H.vertical(-1).right_half_space(),
    ....:   H.vertical(1).left_half_space(),
    ....:   H.half_circle(0, 2).left_half_space())

    sage: P
    {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 2 ≥ 0}

We can also intersect objects that are not half spaces::

    sage: P.intersection(H.vertical(0))
    {x = 0} ∩ {(x^2 + y^2) - 2 ≥ 0}

.. WARNING::

    Our implementation was not conceived with inexact rings in mind. Due to
    popular demand, we do allow inexact base rings but many operations have
    not been tuned for numerical stability, yet.

    To make our implementation work for a variety of (inexact) base rings, we
    delegate some numerically critical bits to a separate "geometry" class that
    can be specified when creating a hyperbolic plane. For exact base rings,
    this defaults to the :class:`HyperbolicExactGeometry` which uses exact text
    book algorithms.

    Over inexact rings, we implement a :class:`HyperbolicEpsilonGeometry` which
    considers two numbers two be equal if they differ only by a small
    (relative) error. This should work reasonably well for inexact base rings
    that have no denormalized numbers, i.e., it will work well for ``RR`` and
    general ``RealField``.

    sage: HyperbolicPlane(RR)
    Hyperbolic Plane over Real Field with 53 bits of precision

    There is currently no implementation that works well with ``RDF``. It
    should be easy to adapt :class:`HyperbolicEpsilonGeometry` for that purpose
    to take into account denormalized numbers::

        sage: HyperbolicPlane(RDF)
        Traceback (most recent call last):
        ...
        ValueError: geometry must be specified for HyperbolicPlane over inexact rings

    There is currently no implementation that works for ball arithmetic::

        sage: HyperbolicPlane(RBF)
        Traceback (most recent call last):
        ...
        ValueError: geometry must be specified for HyperbolicPlane over inexact rings

.. NOTE::

    This module implements different kinds of convex subsets as different
    classes. The alternative would have been to represent all subsets as
    collections of (in)equalities in some hyperbolic model. There is for
    example a :class:`HyperbolicUnorientedSegment` and a
    :class:`HyperbolicConvexPolygon` even though the former could in principle
    be expressed as the latter. The advantage of this approach is that we can
    provide a more natural user interface, e.g., a segment has a single
    underlying geodesic whereas the corresponding convex polygon would have
    four (or three.) Similarly, an oriented geodesic (which cannot really be
    expressed as a convex polygon due to the orientation) has a left and a
    right associated half spaces.

    Sometimes it can, however, be beneficial to treat each subset as a convex
    polygon. In such a case, one can explicitly create polygons from subsets by
    intersecting their :meth:`HyperbolicConvexSet.half_spaces`::

        sage: g = H.vertical(0)
        sage: P = H.polygon(g.half_spaces(), check=False, assume_minimal=True)
        sage: P
        {x ≤ 0} ∩ {x ≥ 0}

    Note that such an object might not be fully functional since some methods
    may assume that the object is an actual polygon::

        sage: P.dimension()
        2

    Similarly, a geodesic can be treated as a segment without endpoints::

        sage: H.segment(g, start=None, end=None, check=False, assume_normalized=True)
        {-x = 0}

.. NOTE::

    This implementation is an alternative to the one that comes with SageMath.
    The one in SageMath has a number of issues, see e.g.
    https://trac.sagemath.org/ticket/32400. The implementation here tries very
    hard to perform all operations over the same base ring, have the best
    complexities possible, keep all objects in the same (Klein) model, is not
    using any symbolic expressions, and tries to produce better plots.

"""

# ****************************************************************************
#  This file is part of sage-flatsurf.
#
#        Copyright (C) 2022-2023 Julian Rüth
#                           2022 Sam Freedman
#                           2022 Vincent Delecroix
#
#  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.sage_object import SageObject
from sage.structure.parent import Parent
from sage.structure.element import Element
from sage.structure.unique_representation import UniqueRepresentation
from sage.misc.cachefunc import cached_method


[docs] class HyperbolicPlane(Parent, UniqueRepresentation): r""" The hyperbolic plane. All objects in the plane must be specified over the given base ring. Note that, in some representations, objects might appear to live in a larger ring. E.g., when specifying a line by giving a center and the square of its radius in the half plane model, then the ideal endpoints of this line might have coordinates in the ring after adjoining a square root. The implemented elements of the plane are convex subsets such as (finite and infinite) points, geodesics, closed half planes, and closed convex polygons. ALGORITHM: We usually display objects as if they were defined in the upper half plane model. However, internally, we store most objects in a representation in the Klein model. In that model it tends to be easier to perform computations without having to extend the base ring and we can also rely on standard algorithms for geometry in the Euclidean plane. For the Klein model, we use a unit disk centered at (0, 0). The map from the upper half plane sends the imaginary unit `i` to the center at the origin, and sends 0 to (0, -1), 1 to (1, 0), -1 to (-1, 0) and infinity to (0, 1). The Möbius transformation .. MATH:: z \mapsto \frac{z-i}{1 - iz} maps from the half plane model to the Poincaré disk model. We then post-compose this with the map that goes from the Poincaré disk model to the Klein model, which in polar coordinates sends .. MATH:: (\phi, r)\mapsto \left(\phi, \frac{2r}{1 + r^2}\right). When we write out the full map explicitly in Euclidean coordinates, we get .. MATH:: (x, y) \mapsto \frac{1}{1 + x^2 + y^2}\left(2x, -1 + x^2 + y^2\right) and .. MATH:: (x, y) \mapsto \frac{1}{1 - y}\left(x, \sqrt{1 - x^2 - y^2}\right), for its inverse. A geodesic in the upper half plane is given by an equation of the form .. MATH:: a(x^2 + y^2) + bx + c = 0 which converts to an equation in the Klein model as .. MATH:: (a + c) + bx + (a - c)y = 0. Conversely, a geodesic's equation in the Klein model .. MATH:: a + bx + cy = 0 corresponds to the equation .. MATH:: (a + c)(x^2 + y^2) + 2bx + (a - c) = 0 in the upper half plane model. Note that the intersection of two geodesics defined by coefficients in a field `K` in the Klein model has coordinates in `K` in the Klein model. The corresponding statement is not true for the upper half plane model. INPUT: - ``base_ring`` -- a base ring for the coefficients defining the equations of geodesics in the plane; defaults to the rational field if not specified. - ``geometry`` -- an implementation of the geometric primitives specified by :class:`HyperbolicExactGeometry`. If the ``base_ring`` is exact, this defaults to :class:`HyperbolicExactGeometry` over that base ring. If the base ring is ``RR`` or ``RealField``, this defaults to the :class:`HyperbolicEpsilonGeometry` over that ring. For other rings, a geometry must be explicitly provided. - ``category`` -- the category for this object; if not specified, defaults to sets. Note that we do not use metric spaces here since the elements are convex subsets of the hyperbolic plane and not just points and do therefore not satisfy the assumptions of a metric space. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane() Hyperbolic Plane over Rational Field :: sage: HyperbolicPlane(AA) Hyperbolic Plane over Algebraic Real Field :: sage: HyperbolicPlane(RR) Hyperbolic Plane over Real Field with 53 bits of precision """
[docs] @staticmethod def __classcall__(cls, base_ring=None, geometry=None, category=None): r""" Create the hyperbolic plane with normalized arguments to make it a unique SageMath parent. TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicExactGeometry sage: HyperbolicPlane() is HyperbolicPlane(QQ) True sage: HyperbolicPlane() is HyperbolicPlane(geometry=HyperbolicExactGeometry(QQ)) True """ from sage.all import QQ base_ring = base_ring or QQ if geometry is None: from sage.all import RR if base_ring is RR: geometry = HyperbolicExactGeometry(QQ).change_ring(base_ring) elif base_ring.is_exact(): geometry = HyperbolicExactGeometry(base_ring) else: raise ValueError( "geometry must be specified for HyperbolicPlane over inexact rings" ) from sage.categories.all import Sets category = category or Sets() return super().__classcall__( cls, base_ring=base_ring, geometry=geometry, category=category )
[docs] def __init__(self, base_ring, geometry, category): r""" Create the hyperbolic plane over ``base_ring``. TESTS:: sage: from flatsurf import HyperbolicPlane sage: TestSuite(HyperbolicPlane(QQ)).run() sage: TestSuite(HyperbolicPlane(AA)).run() # long time (.5s) sage: TestSuite(HyperbolicPlane(RR)).run() """ from sage.all import RR if geometry.base_ring() is not base_ring: raise ValueError( f"geometry base ring must be base ring of hyperbolic plane but {geometry.base_ring()} is not {base_ring}" ) if not RR.has_coerce_map_from(geometry.base_ring()): # We should check that the coercion is an embedding but this is not possible currently. raise ValueError("base ring must embed into the reals") super().__init__(category=category) self._base_ring = geometry.base_ring() self.geometry = geometry
def _coerce_map_from_(self, other): r""" Return a coercion map from ``other`` to this hyperbolic plane. EXAMPLES: Coercions between base rings induce coercion between hyperbolic planes:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: HyperbolicPlane(AA).has_coerce_map_from(H) True Base ring elements coerce as points on the real line:: sage: H.has_coerce_map_from(QQ) True sage: H.has_coerce_map_from(ZZ) True Complex numbers do not coerce into the hyperbolic plane since that coercion would not be total:: sage: H.has_coerce_map_from(CC) False sage: H.has_coerce_map_from(I.parent()) False sage: HyperbolicPlane(RR).has_coerce_map_from(CC) False """ if self.base_ring().has_coerce_map_from(other): return True if isinstance(other, HyperbolicPlane): return self.base_ring().has_coerce_map_from(other.base_ring()) return False
[docs] def __contains__(self, x): r""" Return whether the hyperbolic plane contains ``x``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We mostly rely on the standard implementation in SageMath, i.e., the interplay between the operator ``==`` and the coercion model:: sage: 0 in H True However, we override that logic for complex numbers. There cannot be a coercion from the complex number to the hyperbolic plane since that map would not be total but we want the following to work:: sage: I in H True We do not support such containment checks when conversion of the element could lead to a loss of precision:: sage: CC(I) in H False .. NOTE:: There is currently no way to check whether a point is in the interior of a set. .. SEEALSO:: :meth:`HyperbolicConvexSet.__contains__` to check containment of a point in subsets of the hyperbolic plane. """ from sage.categories.all import NumberFields import sage.structure.element from sage.structure.parent import Parent from sage.all import SR # pylint does not see the Cython parent() so we disable the import check. # pylint: disable=c-extension-no-member parent = sage.structure.element.parent(x) # pylint: enable=c-extension-no-member # Note that in old versions of SageMath (9.1 e.g.), I is not a number field element but a symbolic ring element. # The "parent is SR" part can probably removed at some point. if isinstance(parent, Parent) and parent in NumberFields() or parent is SR: if ( x.real() in self.base_ring() and x.imag() in self.base_ring() and x.imag() >= 0 ): return True return super().__contains__(x)
[docs] def change_ring(self, ring, geometry=None): r""" Return the hyperbolic plane over a different base ``ring``. INPUT: - ``ring`` -- a ring or ``None``; if ``None``, uses the current :meth:`~HyperbolicPlane.base_ring`. - ``geometry`` -- a geometry or ``None``; if ``None``, tries to convert the existing geometry to ``ring``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane(QQ).change_ring(AA) is HyperbolicPlane(AA) True When changing to the ring ``RR`` and no geometry has been specified explicitly, the :class:`HyperbolicExactGeometry` changes to the :class:`HyperbolicEpsilonGeometry`, see :meth:`HyperbolicExactGeometry.change_ring`:: sage: HyperbolicPlane(QQ).change_ring(RR) is HyperbolicPlane(RR) True In the opposite direction, the geometry cannot be determined automatically:: sage: HyperbolicPlane(RR).change_ring(QQ) Traceback (most recent call last): ... ValueError: cannot change_ring() to an exact ring So the geometry has to be specified explicitly:: sage: from flatsurf.geometry.hyperbolic import HyperbolicExactGeometry sage: HyperbolicPlane(RR).change_ring(QQ, geometry=HyperbolicExactGeometry(QQ)) is HyperbolicPlane(QQ) True .. SEEALSO:: :meth:`HyperbolicConvexSet.change_ring` or more generally :meth:`HyperbolicConvexSet.change` to change the ring and geometry a set is defined over. """ if ring is None and geometry is None: return self if ring is None: ring = self.base_ring() if geometry is None: geometry = self.geometry.change_ring(ring) return HyperbolicPlane(ring, geometry)
def _an_element_(self): r""" Return an element of the hyperbolic plane (mostly for testing.) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane().an_element() 0 """ return self.real(0)
[docs] def some_subsets(self): r""" Return some subsets of the hyperbolic plane for testing. Some of the returned sets are elements of the hyperbolic plane (i.e., points) some are parents themselves, e.g., polygons. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane().some_elements() [∞, 0, 1, -1, ...] """ from sage.all import ZZ elements = self.some_elements() elements += [ self.empty_set(), # Oriented Geodesics self.vertical(1), self.half_circle(0, 1), self.half_circle(1, 3), # Unoriented Geodesics self.vertical(-1).unoriented(), self.half_circle(-1, 2).unoriented(), # Half spaces self.vertical(0).left_half_space(), self.half_circle(0, 2).left_half_space(), ] # The intersection algorithm is only implemented over exact rings. elements += [ # An unbounded polygon self.vertical(1) .left_half_space() .intersection(self.vertical(-1).right_half_space()), # An unbounded polygon which is bounded in the Euclidean plane self.vertical(1) .left_half_space() .intersection(self.vertical(-1).right_half_space()) .intersection(self.geodesic(0, 1).left_half_space()) .intersection(self.geodesic(0, -1).right_half_space()), # A bounded polygon self.geodesic(-ZZ(1) / 3, 2) .left_half_space() .intersection(self.geodesic(ZZ(1) / 3, -2).right_half_space()) .intersection(self.geodesic(-ZZ(2) / 3, 3).right_half_space()) .intersection(self.geodesic(ZZ(2) / 3, -3).left_half_space()), # An unbounded oriented segment self.vertical(0).intersection(self.geodesic(-1, 1).left_half_space()), # A bounded oriented segment self.vertical(0) .intersection(self.geodesic(-2, 2).right_half_space()) .intersection(self.geodesic(-ZZ(1) / 2, ZZ(1) / 2).left_half_space()), # An unbounded unoriented segment self.vertical(0) .intersection(self.geodesic(-1, 1).left_half_space()) .unoriented(), # A bounded unoriented segment self.vertical(0) .intersection(self.geodesic(-2, 2).right_half_space()) .intersection(self.geodesic(-ZZ(1) / 2, ZZ(1) / 2).left_half_space()) .unoriented(), ] return elements
[docs] def some_elements(self): r""" Return some representative elements, i.e., points of the hyperbolic plane for testing. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane().some_elements() [∞, 0, 1, -1, ...] """ return [ self.infinity(), self.real(0), self.real(1), self.real(-1), self.geodesic(0, 2).start(), self.half_circle(0, 2).start(), ]
def _test_some_subsets(self, tester=None, **options): r""" Run test suite on some representative convex subsets. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane()._test_some_subsets() """ is_sub_testsuite = tester is not None tester = self._tester(tester=tester, **options) for x in self.some_elements(): tester.info(f"\n Running the test suite of {x}") from sage.all import TestSuite TestSuite(x).run( verbose=tester._verbose, prefix=tester._prefix + " ", raise_on_failure=is_sub_testsuite, ) tester.info(tester._prefix + " ", newline=False)
[docs] def random_element(self, kind=None): r""" Return a random convex subset of this hyperbolic plane. INPUT: - ``kind`` -- one of ``"empty_set"```, ``"point"```, ``"oriented geodesic"```, ``"unoriented geodesic"```, ``"half_space"```, ``"oriented segment"``, ``"unoriented segment"``, ``"polygon"``; the kind of set to produce. If not specified, the kind of set is chosen randomly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Make the following randomized tests reproducible:: sage: set_random_seed(0) :: sage: H.random_element() {} Specific types of random subsets can be requested:: sage: H.random_element("point") -1/2 + 1/95*I sage: H.random_element("oriented geodesic") {-12*(x^2 + y^2) + 1144*x + 1159 = 0} sage: H.random_element("unoriented geodesic") {648*(x^2 + y^2) + 1654*x + 85 = 0} sage: H.random_element("half_space") {3*(x^2 + y^2) - 5*x - 1 ≥ 0} sage: H.random_element("oriented segment") {-3*(x^2 + y^2) + x + 3 = 0} ∩ {9*(x^2 + y^2) - 114*x + 28 ≥ 0} ∩ {(x^2 + y^2) + 12*x - 1 ≥ 0} sage: H.random_element("unoriented segment") {16*(x^2 + y^2) - x - 16 = 0} ∩ {(x^2 + y^2) + 64*x - 1 ≥ 0} ∩ {496*(x^2 + y^2) - 1056*x + 529 ≥ 0} sage: H.random_element("polygon") {56766100*(x^2 + y^2) - 244977117*x + 57459343 ≥ 0} ∩ {822002048*(x^2 + y^2) - 3988505279*x + 2596487836 ≥ 0} ∩ {464*(x^2 + y^2) + 9760*x + 11359 ≥ 0} ∩ {4*(x^2 + y^2) + 45*x + 49 ≥ 0} .. SEEALSO:: :meth:`some_elements` for a curated list of representative subsets. """ kinds = { "empty_set": HyperbolicEmptySet, "point": HyperbolicPointFromCoordinates, "oriented geodesic": HyperbolicOrientedGeodesic, "unoriented geodesic": HyperbolicUnorientedGeodesic, "half_space": HyperbolicHalfSpace, "oriented segment": HyperbolicOrientedSegment, "unoriented segment": HyperbolicUnorientedSegment, "polygon": HyperbolicConvexPolygon, } if kind is None: from sage.all import randint kind = list(kinds.keys())[randint(0, len(kinds) - 1)] if kind not in kinds: raise ValueError(f"kind must be one of {kinds}") return kinds[kind].random_set(self)
[docs] def __call__(self, x): r""" Return ``x`` as an element of the hyperbolic plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(1) 1 We need to override this method. The normal code path in SageMath requires the argument to be an Element but facade sets are not elements:: sage: v = H.vertical(0) sage: Parent.__call__(H, v) Traceback (most recent call last): ... TypeError: Cannot convert HyperbolicOrientedGeodesic_with_category_with_category to sage.structure.element.Element sage: H(v) {-x = 0} """ if isinstance(x, HyperbolicConvexFacade): return self._element_constructor_(x) return super().__call__(x)
def _element_constructor_(self, x): r""" Return ``x`` as an element of the hyperbolic plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(H.an_element()) in H True Base ring elements can be converted to ideal points:: sage: H(1) 1 The point at infinity in the half plane model can be written directly:: sage: H(oo) Complex numbers in the upper half plane can be converted to points in the hyperbolic plane:: sage: H(I) I Elements can be converted between hyperbolic planes with compatible base rings:: sage: HyperbolicPlane(AA)(H(1)) 1 TESTS:: sage: H(-I) Traceback (most recent call last): ... ValueError: point (0, -1) not in the upper half plane """ from sage.all import parent parent = parent(x) if parent is self: return x from sage.all import Infinity if x is Infinity: return self.infinity() if isinstance(x, HyperbolicConvexSet): return x.change(ring=self.base_ring(), geometry=self.geometry) if x in self.base_ring(): return self.real(x) from sage.categories.all import NumberFields if parent in NumberFields(): K = parent from sage.all import I if I not in K: raise NotImplementedError( "cannot create a hyperbolic point from an element in a number field that does not contain the imaginary unit" ) return self.point(x.real(), x.imag(), model="half_plane") from sage.all import SR if parent is SR: return self.point(x.real(), x.imag(), model="half_plane") from sage.categories.all import Rings if parent in Rings(): raise ValueError( f"cannot convert this element in {parent} to the hyperbolic plane over {self.base_ring()}" ) raise NotImplementedError( "cannot create a subset of the hyperbolic plane from this element yet." )
[docs] def base_ring(self): r""" Return the base ring over which objects in the plane are defined. More specifically, all geodesics must have an equation `a + bx + cy = 0` in the Klein model with coefficients in this ring, and all points must have coordinates in this ring when written in the Klein model, or be the end point of a geodesic. All other objects are built from these primitives. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane().base_ring() Rational Field .. SEEALSO:: :meth:`HyperbolicConvexSet.change_ring` to change the ring a set is defined over """ return self._base_ring
[docs] def is_exact(self): r""" Return whether hyperbolic subsets have exact coordinates. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.is_exact() True sage: H = HyperbolicPlane(RR) sage: H.is_exact() False """ return self.base_ring().is_exact()
[docs] def infinity(self): r""" Return the point at infinity in the upper half plane model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.infinity() sage: p sage: p == H(oo) True sage: p.is_ideal() True .. SEEALSO:: :meth:`point` to create points in general. """ return self.projective(1, 0)
[docs] def real(self, r): r""" Return the ideal point ``r`` on the real axis in the upper half plane model. INPUT: - ``r`` -- an element of the :meth:`base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.real(-2) sage: p -2 sage: p == H(-2) True sage: p.is_ideal() True .. SEEALSO:: :meth:`point` to create points in general. """ return self.projective(r, 1)
[docs] def projective(self, p, q): r""" Return the ideal point with projective coordinates ``[p: q]`` in the upper half plane model. INPUT: - ``p`` -- an element of the :meth:`base_ring`. - ``q`` -- an element of the :meth:`base_ring`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.projective(0, 1) 0 sage: H.projective(-1, 0) sage: H.projective(0, 0) Traceback (most recent call last): ... ValueError: one of p and q must not be zero .. SEEALSO:: :meth:`point` to create points in general. """ p = self.base_ring()(p) q = self.base_ring()(q) return self.geometry.projective(p, q, self.point)
[docs] def start(self, geodesic, check=True): r""" Return the ideal starting point of ``geodesic``. INPUT: - ``geodesic`` -- an oriented geodesic - ``check`` -- whether to verify that ``geodesic`` is valid .. NOTE:: This method exists to keep all the methods that actually create hyperbolic sets on the lowest level in the :class:`HyperbolicPlane`. It is otherwise identical to :meth:`HyperbolicOrientedGeodesic.start`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.start(H.vertical(0)) sage: p 0 sage: H.vertical(0).start() == p True Points created this way might have coordinates that cannot be represented in the base ring:: sage: p = H.half_circle(0, 2).start() sage: p.coordinates(model="klein") Traceback (most recent call last): ... ValueError: square root of 32 ... sage: p.coordinates(model="half_plane") Traceback (most recent call last): ... ValueError: square root of 32 ... """ geodesic = self(geodesic) if not isinstance(geodesic, HyperbolicOrientedGeodesic): raise TypeError("geodesic must be an oriented geodesic") if check and geodesic.is_ultra_ideal(): raise ValueError("geodesic does not intersect the Klein disk") return self.__make_element_class__(HyperbolicPointFromGeodesic)(self, geodesic)
[docs] def point(self, x, y, model, check=True): r""" Return the point with coordinates (x, y) in the given model. When ``model`` is ``"half_plane"``, return the point `x + iy` in the upper half plane. When ``model`` is ``"klein"``, return the point (x, y) in the Klein model. INPUT: - ``x`` -- an element of the :meth:`base_ring` - ``y`` -- an element of the :meth:`base_ring` - ``model`` -- one of ``"half_plane"`` or ``"klein"`` - ``check`` -- whether to validate the inputs (default: ``True``); set this to ``False``, to create an ultra-ideal point, i.e., a point outside the unit circle in the Klein model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.point(0, 1, model="half_plane") I sage: H.point(1, 2, model="half_plane") 1 + 2*I sage: H.point(0, 1, model="klein") An ultra-ideal point:: sage: H.point(2, 3, model="klein") Traceback (most recent call last): ... ValueError: point (2, 3) is not in the unit disk in the Klein model sage: H.point(2, 3, model="klein", check=False) (2, 3) .. SEEALSO:: :meth:`HyperbolicOrientedGeodesic.start` and :meth:`HyperbolicOrientedGeodesic.end` to generate points that do not have coordinates over the base ring. :meth:`infinity`, :meth:`real`, and :meth:`projective` as shortcuts to generate ideal points. """ x = self.base_ring()(x) y = self.base_ring()(y) if model == "klein": point = self.__make_element_class__(HyperbolicPointFromCoordinates)( self, x, y ) elif model == "half_plane": if self.geometry.classify_point(x, y, model="half_plane") < 0: raise ValueError(f"point {x, y} not in the upper half plane") denominator = 1 + x * x + y * y return self.point( x=2 * x / denominator, y=(-1 + x * x + y * y) / denominator, model="klein", check=check, ) else: raise NotImplementedError("unsupported model") if check: point._check() return point
[docs] def half_circle(self, center, radius_squared): r""" Return the geodesic centered around the real ``center`` and with ``radius_squared`` in the upper half plane model. The geodesic is oriented such that the point at infinity is to its left. Use the ``-`` operator to pass to the geodesic with opposite orientation. INPUT: - ``center`` -- an element of the :meth:`base_ring`, the center of the half circle on the real axis - ``radius_squared`` -- a positive element of the :meth:`base_ring`, the square of the radius of the half circle EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.half_circle(0, 1) {(x^2 + y^2) - 1 = 0} sage: H.half_circle(1, 3) {(x^2 + y^2) - 2*x - 2 = 0} sage: H.half_circle(1/3, 1/2) {18*(x^2 + y^2) - 12*x - 7 = 0} TESTS:: sage: H.half_circle(0, 0) Traceback (most recent call last): ... ValueError: radius must be positive sage: H.half_circle(0, -1) Traceback (most recent call last): ... ValueError: radius must be positive sage: H.half_circle(oo, 1) Traceback (most recent call last): ... TypeError: unable to convert +Infinity to a rational .. SEEALSO:: :meth:`vertical` to get an oriented vertical in the half plane model and :meth:`geodesic` for the general interface, producing a geodesic from an equation. """ center = self.base_ring()(center) radius_squared = self.base_ring()(radius_squared) return self.geometry.half_circle(center, radius_squared, self.geodesic)
[docs] def vertical(self, real): r""" Return the vertical geodesic at the ``real`` ideal point in the upper half plane model. The geodesic is oriented such that it goes from ``real`` to the point at infinity. Use the ``-`` operator to pass to the geodesic with opposite orientation. Use :meth:`HyperbolicConvexSet.unoriented` to get the unoriented vertical. INPUT: - ``real`` -- an element of the :meth:`base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0) {-x = 0} sage: H.vertical(1) {-x + 1 = 0} sage: H.vertical(-1) {-x - 1 = 0} We can also create an unoriented geodesic:: sage: v = H.vertical(0) sage: v.unoriented() == v False .. SEEALSO:: :meth:`half_circle` to get an oriented geodesic that is not a vertical and :meth:`geodesic` for the general interface, producing a geodesic from an equation. """ real = self.base_ring()(real) return self.geometry.vertical(real, self.geodesic)
[docs] def geodesic(self, a, b, c=None, model=None, oriented=True, check=True): r""" Return a geodesic in the hyperbolic plane. If only ``a`` and ``b`` are given, return the geodesic going through the points ``a`` and then ``b``. If ``c`` is specified and ``model`` is ``"half_plane"``, return the geodesic given by the half circle .. MATH:: a(x^2 + y^2) + bx + c = 0 oriented such that the half plane .. MATH:: a(x^2 + y^2) + bx + c \ge 0 is to its left. If ``c`` is specified and ``model`` is ``"klein"``, return the geodesic given by the chord with the equation .. MATH:: a + bx + cy = 0 oriented such that the half plane .. MATH:: a + bx + cy \ge 0 is to its left. INPUT: - ``a`` -- a point in the hyperbolic plane or an element of the :meth:`base_ring` - ``b`` -- a point in the hyperbolic plane or an element of the :meth:`base_ring` - ``c`` -- ``None`` or an element of the :meth:`base_ring` (default: ``None``) - ``model`` -- ``None``, ``"half_plane"``, or ``"klein"`` (default: ``None``); when ``a``, ``b`` and ``c`` are elements of the :meth:`base_ring`, in which model they should be interpreted. - ``oriented`` -- whether the returned geodesic is oriented (default: ``True``) - ``check`` -- whether to verify that the arguments define a geodesic in the hyperbolic plane (default: ``True``) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geodesic(-1, 1) {(x^2 + y^2) - 1 = 0} sage: H.geodesic(0, I) {-x = 0} sage: H.geodesic(-1, I + 1) {2*(x^2 + y^2) - x - 3 = 0} sage: H.geodesic(2, -1, -3, model="half_plane") {2*(x^2 + y^2) - x - 3 = 0} sage: H.geodesic(-1, -1, 5, model="klein") {2*(x^2 + y^2) - x - 3 = 0} Geodesics cannot be defined from points whose coordinates are over a quadratic field extension:: sage: H.geodesic(H.half_circle(0, 2).start(), H.half_circle(1, 2).end()) Traceback (most recent call last): ... ValueError: square root of 32 not in Rational Field Except for some special cases:: sage: H.geodesic(H.half_circle(0, 2).start(), H.half_circle(0, 2).end()) {(x^2 + y^2) - 2 = 0} Disabling the ``check``, lets us define geodesics in the Klein model that lie outside the unit circle:: sage: H.geodesic(2, 1, 0, model="klein") Traceback (most recent call last): ... ValueError: ... sage: geodesic = H.geodesic(2, 1, 0, model="klein", check=False) sage: geodesic {2 + x = 0} sage: geodesic.start() Traceback (most recent call last): ... ValueError: geodesic does not intersect the Klein disk sage: geodesic.end() Traceback (most recent call last): ... ValueError: geodesic does not intersect the Klein disk TESTS:: sage: H.geodesic(0, 0) Traceback (most recent call last): ... ValueError: points specifying a geodesic must be distinct ..SEEALSO:: :meth:`half_circle` and :meth:`vertical` """ if c is None: a = self(a) b = self(b) if a == b: raise ValueError("points specifying a geodesic must be distinct") if ( isinstance(a, HyperbolicPointFromGeodesic) and isinstance(b, HyperbolicPointFromGeodesic) and a._geodesic == -b._geodesic ): return a._geodesic ax, ay = a.coordinates(model="klein") bx, by = b.coordinates(model="klein") C = bx - ax B = ay - by A = -(B * ax + C * ay) return self.geodesic(A, B, C, model="klein", oriented=oriented, check=check) if model is None: raise ValueError( "a model must be specified when specifying a geodesic with coefficients" ) if model == "half_plane": # Convert to the Klein model. return self.geodesic( a + c, b, a - c, model="klein", oriented=oriented, check=check ) if model == "klein": a = self.base_ring()(a) b = self.base_ring()(b) c = self.base_ring()(c) geodesic = self.__make_element_class__( HyperbolicOrientedGeodesic if oriented else HyperbolicUnorientedGeodesic )(self, a, b, c) if check: geodesic = geodesic._normalize() geodesic._check() return geodesic raise NotImplementedError( "cannot create geodesic from coefficients in this model" )
[docs] def half_space(self, a, b, c, model, check=True): r""" Return a closed half space from its equation in ``model``. If ``model`` is ``"half_plane"``, return the half space .. MATH:: a(x^2 + y^2) + bx + c \ge 0 in the upper half plane. If ``model`` is ``"klein"``, return the half space .. MATH:: a + bx + cy \ge 0 in the Klein model. INPUT: - ``a`` -- an element of the :meth:`base_ring` - ``b`` -- an element of the :meth:`base_ring` - ``c`` -- an element of the :meth:`base_ring` - ``model`` -- one of ``"half_plane"``` or ``"klein"`` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.half_space(0, -1, 0, model="half_plane") {x ≤ 0} It is often easier to construct a half space as the space bounded by a geodesic:: sage: H.vertical(0).left_half_space() {x ≤ 0} The half space y ≥ 0 given by its equation in the Klein model:: sage: H.half_space(0, 0, 1, model="klein") {(x^2 + y^2) - 1 ≥ 0} ..SEEALSO:: :meth:`HperbolicGeodesic.left_half_space` :meth:`HperbolicGeodesic.right_half_space` """ geodesic = self.geodesic(a, b, c, model=model, check=check) return self.__make_element_class__(HyperbolicHalfSpace)(self, geodesic)
[docs] def segment( self, geodesic, start=None, end=None, oriented=None, check=True, assume_normalized=False, ): r""" Return the segment on the ``geodesic`` bounded by ``start`` and ``end``. INPUT: - ``geodesic`` -- a :meth:`geodesic` in this space. - ``start`` -- ``None`` or a :meth:`point` on the ``geodesic``, e.g., obtained from the :meth:`HyperbolicGeodesic._intersection` of ``geodesic`` with another geodesic. If ``None``, the segment starts at the infinite :meth:`HyperbolicOrientedGeodesic.start` point of the geodesic. - ``end`` -- ``None`` or a :meth:`point` on the ``geodesic``, as for ``start``; must be later on ``geodesic`` than ``start`` if the geodesic is oriented. - ``oriented`` -- whether to produce an oriented segment or an unoriented segment. The default (``None``) is to produce an oriented segment iff ``geodesic`` is oriented or both ``start`` and ``end`` are provided so the orientation can be deduced from their order. - ``check`` -- boolean (default: ``True``), whether validation is performed on the arguments. - ``assume_normalized`` -- boolean (default: ``False``), if not set, the returned segment is normalized, i.e., if it is actually a geodesic, a :class:`HyperbolicGeodesic` is returned, if it is actually a point, a :class:`HyperbolicPoint` is returned. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() When neither ``start`` nor ``end`` are given, a geodesic is returned:: sage: H.segment(H.vertical(0), start=None, end=None) {-x = 0} When only one endpoint is provided, the segment is infinite on one end:: sage: H.segment(H.vertical(0), start=I, end=None) {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} When both endpoints are provided, a proper closed segment is returned:: sage: H.segment(H.vertical(0), start=I, end=2*I) {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} However, ideal endpoints on the geodesic are ignored:: sage: H.segment(H.vertical(0), start=0, end=oo) {-x = 0} A segment can be reduced to a single point:: sage: H.segment(H.vertical(0), start=I, end=I) I The endpoints must have coordinates over the base ring:: sage: H.segment(H.half_circle(0, 2), H.half_circle(0, 2).start(), H.half_circle(0, 2).end()) Traceback (most recent call last): ... ValueError: square root of 32 ... The produced segment is oriented if the ``geodesic`` is oriented:: sage: H.segment(H.vertical(0)).is_oriented() True sage: H.segment(H.vertical(0).unoriented()).is_oriented() False The segment is oriented if both ``start`` and ``end`` are provided:: sage: H.segment(H.vertical(0).unoriented(), start=0, end=oo).is_oriented() True sage: H.segment(H.vertical(0).unoriented(), start=2*I, end=I).is_oriented() True sage: H.segment(H.vertical(0), start=I) != H.segment(H.vertical(0), end=I) True TESTS: When only a ``start`` point is provided, we cannot deduce the orientation of the geodesic:: sage: H.segment(H.vertical(0).unoriented(), start=0, oriented=False) Traceback (most recent call last): ... ValueError: cannot deduce segment from single endpoint on an unoriented geodesic sage: H.segment(H.vertical(0).unoriented(), start=0, oriented=True) Traceback (most recent call last): ... ValueError: cannot deduce segment from single endpoint on an unoriented geodesic sage: H.segment(H.vertical(0).unoriented(), start=I, oriented=True) Traceback (most recent call last): ... ValueError: cannot deduce segment from single endpoint on an unoriented geodesic When only an ``end`` point is provided, we cannot deduce the orientation of the geodesic:: sage: H.segment(H.vertical(0).unoriented(), end=0, oriented=False) Traceback (most recent call last): ... ValueError: cannot deduce segment from single endpoint on an unoriented geodesic sage: H.segment(H.vertical(0).unoriented(), end=0, oriented=True) Traceback (most recent call last): ... ValueError: cannot deduce segment from single endpoint on an unoriented geodesic sage: H.segment(H.vertical(0).unoriented(), end=I, oriented=True) Traceback (most recent call last): ... ValueError: cannot deduce segment from single endpoint on an unoriented geodesic When ``start`` and ``end`` are given, they must be ordered correctly:: sage: H.segment(H.vertical(0), start=0, end=oo) {-x = 0} sage: H.segment(H.vertical(0).unoriented(), start=oo, end=0) {x = 0} sage: H.segment(H.vertical(0), start=oo, end=0) Traceback (most recent call last): ... ValueError: end point of segment must not be before start point on the underlying geodesic sage: H.segment(H.vertical(0), start=I, end=2*I) {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} sage: H.segment(H.vertical(0).unoriented(), start=2*I, end=I) {x = 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} sage: H.segment(H.vertical(0), start=2*I, end=I) Traceback (most recent call last): ... ValueError: end point of segment must not be before start point on the underlying geodesic .. SEEALSO:: :meth:`HyperbolicPoint.segment` """ geodesic = self(geodesic) if not isinstance(geodesic, HyperbolicGeodesic): raise TypeError("geodesic must be a geodesic") if start is not None: start = self(start) if not isinstance(start, HyperbolicPoint): raise TypeError("start must be a point") if end is not None: end = self(end) if not isinstance(end, HyperbolicPoint): raise TypeError("end must be a point") if oriented is None: oriented = geodesic.is_oriented() or (start is not None and end is not None) if not geodesic.is_oriented(): geodesic = geodesic.change(oriented=True) if start is None and end is None: # any orientation of the geodesic will do pass elif start is None or end is None or start == end: raise ValueError( "cannot deduce segment from single endpoint on an unoriented geodesic" ) elif geodesic.parametrize( start, model="euclidean", check=False ) > geodesic.parametrize(end, model="euclidean", check=False): geodesic = -geodesic segment = self.__make_element_class__( HyperbolicOrientedSegment if oriented else HyperbolicUnorientedSegment )(self, geodesic, start, end) if check: segment._check(require_normalized=False) if not assume_normalized: segment = segment._normalize() if check: segment._check(require_normalized=True) return segment
[docs] def polygon( self, half_spaces, check=True, assume_sorted=False, assume_minimal=False, marked_vertices=(), ): r""" Return the convex polygon obtained by intersecting ``half_spaces``. INPUT: - ``half_spaces`` -- a non-empty iterable of :class:`HyperbolicHalfSpace`\ s of this hyperbolic plane. - ``check`` -- boolean (default: ``True``), whether the arguments are validated. - ``assume_sorted`` -- boolean (default: ``False``), whether to assume that the ``half_spaces`` are already sorted with respect to :meth:`HyperbolicHalfSpaces._lt_`. When set, we omit sorting the half spaces explicitly, which is asymptotically the most exponsive part of the process of creating a polygon. - ``assume_minimal`` -- boolean (default: ``False``), whether to assume that the ``half_spaces`` provide a minimal representation of the polygon, i.e., removing any of them describes a different polygon. When set, we omit searching for a minimal subset of half spaces to describe the polygon. - ``marked_vertices`` -- an iterable of vertices (default: an empty tuple), the vertices are included in the :meth:`HyperbolicConvexPolygon.vertices` even if they are not in the set of minimal vertices describing this polygon. ALGORITHM: See :meth:`intersection` for algorithmic details. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A finite convex polygon:: sage: H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 2).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ]) {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 2 ≥ 0} Redundant half spaces are removed from the final representation:: sage: H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 2).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: H.half_circle(0, 6).right_half_space(), ....: ]) {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 2 ≥ 0} The vertices of the polygon can be at ideal points; this polygon has vertices at -1 and 1:: sage: H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ]) {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} Any set of half spaces defines a polygon, even if the edges do not even meet at ideal points:: sage: H.polygon([ ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 2).right_half_space(), ....: ]) {(x^2 + y^2) - 2 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} However, when the resulting set is point, the result is not represented as a polygon anymore:: sage: H.polygon([ ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space(), ....: ]) We can force the creation of this set as a polygon which might be beneficial in some algorithmic applications:: sage: H.polygon([ ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space(), ....: ], check=False, assume_minimal=True) {x + 1 ≤ 0} ∩ {x - 1 ≥ 0} Note that forcing this mode does not remove redundant half spaces from the representation; we usually assume that the representation is minimal, so such a polygon might not behave correctly:: sage: H.polygon([ ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space(), ....: H.vertical(2).right_half_space(), ....: ], check=False, assume_minimal=True) {x + 1 ≤ 0} ∩ {x - 1 ≥ 0} ∩ {x - 2 ≥ 0} We could manually pass to a minimal representation by rewriting the point as half spaces again:: sage: minimal = H.polygon([ ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space(), ....: H.vertical(2).right_half_space(), ....: ]) sage: H.polygon(minimal.half_spaces(), check=False, assume_minimal=True) {x ≤ 0} ∩ {x - 1 ≥ 0} Note that this chose half spaces not in the original set; you might also want to have a look at :meth:`HyperbolicConvexPolygon._normalize` for some ideas how to manually reduce the half spaces that are used in a polygon. Note that the same applies if the intersection of half spaces is empty or just a single half space:: sage: empty = H.polygon([ ....: H.half_circle(0, 1).right_half_space(), ....: H.half_circle(0, 2).left_half_space(), ....: ]) sage: type(empty) <class 'flatsurf.geometry.hyperbolic.HyperbolicEmptySet_with_category_with_category'> :: sage: half_space = H.polygon([ ....: H.half_circle(0, 1).right_half_space(), ....: ]) sage: type(half_space) <class 'flatsurf.geometry.hyperbolic.HyperbolicHalfSpace_with_category_with_category'> If we add a marked point to such a half space, the underlying type is a polygon again:: sage: half_space = H.polygon([ ....: H.half_circle(0, 1).right_half_space(), ....: ], marked_vertices=[I]) sage: half_space {(x^2 + y^2) - 1 ≤ 0} ∪ {I} sage: type(half_space) <class 'flatsurf.geometry.hyperbolic.HyperbolicConvexPolygon_with_category_with_category'> Marked points that coincide with vertices are ignored:: sage: half_space = H.polygon([ ....: H.half_circle(0, 1).right_half_space(), ....: ], marked_vertices=[-1]) sage: half_space {(x^2 + y^2) - 1 ≤ 0} sage: type(half_space) <class 'flatsurf.geometry.hyperbolic.HyperbolicHalfSpace_with_category_with_category'> Marked points must be on an edge of the polygon:: sage: H.polygon([ ....: H.half_circle(0, 1).right_half_space(), ....: ], marked_vertices=[-2]) Traceback (most recent call last): ... ValueError: marked vertex must be on an edge of the polygon sage: H.polygon([ ....: H.half_circle(0, 1).right_half_space(), ....: ], marked_vertices=[2*I]) Traceback (most recent call last): ... ValueError: marked vertex must be on an edge of the polygon The intersection of the half spaces is computed in time quasi-linear in the number of half spaces. The limiting factor is sorting the half spaces by :meth:`HyperbolicHalfSpaces._lt_`. If we know that the half spaces are already sorted like that, we can make the process run in linear time by setting ``assume_sorted``. sage: H.polygon(H.infinity().half_spaces(), assume_sorted=True) .. SEEALSO:: :meth:`intersection` to intersect arbitrary convex sets :meth:`convex_hull` to define a polygon by taking the convex hull of a union of convex sets """ if not marked_vertices: marked_vertices = [] half_spaces = [self(half_space) for half_space in half_spaces] marked_vertices = [self(vertex) for vertex in marked_vertices] half_spaces = HyperbolicHalfSpaces(half_spaces, assume_sorted=assume_sorted) polygon = self.__make_element_class__(HyperbolicConvexPolygon)( self, half_spaces, marked_vertices ) if check: polygon._check(require_normalized=False) if check or not assume_minimal: polygon = polygon._normalize(marked_vertices=bool(marked_vertices)) if check: polygon._check() return polygon
[docs] def convex_hull(self, *subsets, marked_vertices=False): r""" Return the convex hull of the ``subsets``. INPUT: - ``subsets`` -- a sequence of subsets of this hyperbolic space. - ``marked_vertices`` -- a boolean (default: ``False``), whether to keep redundant vertices on the boundary. ALGORITHM: We use the standard Graham scan algorithm which runs in O(nlogn), see :meth:`HyperbolicHalfSpaces.convex_hull`. However, to get the unbounded bits of the convex hull right, we use a somewhat naive O(n²) algorithm which could probably be improved easily. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A polygon can also be created as the convex hull of its vertices:: sage: H.convex_hull(I - 1, I + 1, 2*I - 1, 2*I + 1) {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 5 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 2 ≥ 0} The vertices can also be infinite:: sage: H.convex_hull(-1, 1, 2*I) {(x^2 + y^2) + 3*x - 4 ≤ 0} ∩ {(x^2 + y^2) - 3*x - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} Redundant vertices are removed. However, they can be kept by setting ``marked_vertices``:: sage: H.convex_hull(-1, 1, I, 2*I) {(x^2 + y^2) + 3*x - 4 ≤ 0} ∩ {(x^2 + y^2) - 3*x - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} sage: polygon = H.convex_hull(-1, 1, I, 2*I, marked_vertices=True) sage: polygon {(x^2 + y^2) + 3*x - 4 ≤ 0} ∩ {(x^2 + y^2) - 3*x - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∪ {I} sage: polygon.vertices() {-1, I, 1, 2*I} The convex hull of a half space and a point:: sage: H.convex_hull(H.half_circle(0, 1).right_half_space(), 0) {(x^2 + y^2) - 1 ≤ 0} To keep the additional vertices, again ``marked_vertices`` must be set:: sage: H.convex_hull(H.half_circle(0, 1).left_half_space(), I, marked_vertices=True) {(x^2 + y^2) - 1 ≥ 0} ∪ {I} sage: H.convex_hull(H.vertical(0).left_half_space(), 0, I, oo, marked_vertices=True) {x ≤ 0} ∪ {I} sage: H.convex_hull(H.vertical(0).right_half_space(), I, marked_vertices=True) {x ≥ 0} ∪ {I} Note that this cannot be used to produce marked points on a geodesic:: sage: H.convex_hull(-1, I, 1) {(x^2 + y^2) - 1 = 0} sage: H.convex_hull(-1, I, 1, marked_vertices=True) Traceback (most recent call last): ... NotImplementedError: cannot add marked vertices to low dimensional objects Note that this cannot be used to produce marked points on a segment:: sage: H.convex_hull(I, 2*I, 3*I) {x = 0} sage: H.convex_hull(I, 2*I, 3*I, marked_vertices=True) Traceback (most recent call last): ... NotImplementedError: cannot add marked vertices to low dimensional objects The convex point of two polygons which contain infinitely many ideal points:: sage: H.convex_hull( ....: H.polygon([H.geodesic(-1, -1/4).left_half_space(), H.geodesic(0, -2).left_half_space()]), ....: H.polygon([H.geodesic(4, 2).left_half_space(), H.geodesic(4, oo).left_half_space()]), ....: H.polygon([H.geodesic(-1/2, 1/2).left_half_space(), H.geodesic(2, -2).left_half_space()]) ....: ) {2*(x^2 + y^2) - x ≥ 0} ∩ {(x^2 + y^2) - 2*x - 8 ≤ 0} ∩ {8*(x^2 + y^2) + 6*x + 1 ≥ 0} TESTS: A trivial case that did not work initially: sage: H.convex_hull(H(0), H(1), H(oo), H(I), H(I + 1)) {(x^2 + y^2) - x ≥ 0} ∩ {x - 1 ≤ 0} ∩ {x ≥ 0} .. SEEALSO:: :meth:`HyperbolicHalfSpaces.convex_hull` for the underlying implementation :meth:`intersection` to compute the intersection of convex sets """ subsets = [self(subset) for subset in subsets] vertices = sum([list(subset.vertices()) for subset in subsets], []) polygon = self.polygon(HyperbolicHalfSpaces.convex_hull(vertices)) half_spaces = [] for subset in subsets: if subset.dimension() == 2: if isinstance(subset, HyperbolicHalfSpace): half_spaces.append(subset) elif isinstance(subset, HyperbolicConvexPolygon): # An infinity polygon is more than just the convex hull of # its vertices, it may also contain entire half spaces, # namely those that are between vertices that are not # connected by an edge. edges = subset.edges() for a, b in subset.vertices().pairs(): if a.segment(b) not in edges: half_spaces.append(self.geodesic(a, b).right_half_space()) else: raise NotImplementedError( "cannot form convex hull of this kind of set yet" ) edges = [] for edge in polygon.half_spaces(): for half_space in half_spaces: if (-edge).is_subset(half_space): break else: edges.append(edge) if marked_vertices: marked_vertices = [ vertex for vertex in vertices if any(vertex in half_space.boundary() for half_space in edges) ] polygon = self.polygon(edges, marked_vertices=marked_vertices) assert all( subset.is_subset(polygon) for subset in subsets ), "convex hull does not contain all the sets it is supposed to be the convex hull of" return polygon
[docs] def intersection(self, *subsets): r""" Return the intersection of convex ``subsets``. ALGORITHM: We compute the intersection of the :meth:`HyperbolicConvexSet.half_spaces` that make up the ``subsets``. That intersection can be computed in the Klein model where we can essentially reduce this problem to the intersection of half spaces in the Euclidean plane. The Euclidean intersection problem can be solved in time linear in the number of half spaces assuming that the half spaces are already sorted in a certain way. In particular, this is the case if there is only a constant number of ``subsets``. Otherwise, the algorithm is quasi-linear in the number of half spaces due to the added complexity of sorting. See :meth:`HyperbolicConvexPolygon._normalize` for more algorithmic details. INPUT: - ``subsets`` -- a non-empty sequence of subsets of this hyperbolic space. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.intersection(H.vertical(0).left_half_space()) {x ≤ 0} sage: H.intersection(H.vertical(0).left_half_space(), H.vertical(0).right_half_space()) {x = 0} We cannot form the intersection of no spaces yet:: sage: H.intersection() Traceback (most recent call last): ... NotImplementedError: the full hyperbolic space cannot be created as an intersection .. SEEALSO:: :meth:`HyperbolicPlane.polygon` for a specialized version for the intersection of half spaces :meth:`HyperbolicPlane.convex_hull` to compute the convex hull of subspaces """ subsets = [self(subset) for subset in subsets] if len(subsets) == 0: raise NotImplementedError( "the full hyperbolic space cannot be created as an intersection" ) if len(subsets) == 1: return subsets[0].unoriented() half_spaces = sum( [subset.half_spaces() for subset in subsets], HyperbolicHalfSpaces([]) ) return self.polygon( half_spaces, assume_sorted=True, assume_minimal=False, check=False ).unoriented()
[docs] def empty_set(self): r""" Return an empty subset of this space. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane().empty_set() {} """ return self.__make_element_class__(HyperbolicEmptySet)(self)
[docs] def isometry( self, preimage, image, model="half_plane", on_right=False, normalized=False ): r""" Return an isometry that maps ``preimage`` to ``image``. INPUT: - ``preimage`` -- a convex set in the hyperbolic plane or a list of such convex sets. - ``image`` -- a convex set in the hyperbolic plane or a list of such convex sets. - ``model`` -- one of ``"half_plane"`` and ``"klein"``, the model in which this isometry applies. - ``on_right`` -- a boolean (default: ``False``); whether the returned isometry maps ``preimage`` to ``image`` when multiplied from the right; otherwise from the left. - ``normalized`` -- a boolean (default: ``False``); whether the returned matrix has determinant ±1. OUTPUT: If ``model`` is ``"half_plane"``, returns a 2×2 matrix over the :meth:`base_ring`, if ``model`` is ``"klein"``, returns a 3×3 matrix over the base ring. See :meth:`HyperbolicConvexSet.apply_isometry` for meaning of this matrix. ALGORITHM: We compute an isometry with a very inefficient Gröbner basis approach. Essentially, we try to extract three points from the ``preimage`` with their prescribed images in ``image``, see :meth:`_isometry_conditions` and determine the unique isometry mapping the points by solving the corresponding polynomial system, see :meth:`_isometry_from_equations` for the hacky Gröbner basis bit. There are a lot of problems with this approach (apart from it being extremely slow.) Usually, we do not have access to meaningful points (the ideal end points of a geodesic do not typically live in the :meth:`base_ring`) so we have to map around geodesics instead. However, given two pairs of geodesics, there is in general not a unique isometry mapping one pair to the other, since there might be one isometry with positive and one with negative determinant with this property. This adds to the inefficiency because we have to try for both determinants and then check which isometry maps the actual objects in question correctly. Similarly, for more complex objects such as polygons, we do not know a-priori which edge of the preimage polygon can be mapped to which edge of the image polygon, so we have to try for all rotations of both polygons. Finally, the approach cannot work in general for certain undeterdetermined systems. We do not know how to determine an isometry that maps one geodesic to another geodesic. We can of course try to write down an isometry that maps a geodesic to the vertical at 0 but in general no such isometry is defined over the base ring. We can make the system determined by requiring that the midpoints of the geodesics map to each other but (apart from the midpoint not having coordinates in the base ring) that isometry might not be defined over the base ring even though there exists some isometry that maps the geodesics over the base ring. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() An isometry mapping one point to another; naturally this is not the unique isometry with this property:: sage: m = H.isometry(0, 1) sage: H(0).apply_isometry(m) 1 sage: m [1 1] [0 1] :: sage: H.isometry(I, I+1) [1 1] [0 1] sage: H.isometry(0, oo) [ 0 -1] [-1 0] An isometry is uniquely determined by its image on three points:: sage: H.isometry([0, 1, oo], [1, oo, 0]) [ 0 1] [-1 1] It might be impossible to find an isometry with the prescribed mapping:: sage: H.isometry(0, I) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other sage: H.isometry([0, 1, oo, I], [0, 1, oo, I + 1]) # long time (.4s) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other We can determine isometries by mapping more complex objects than points, e.g., geodesics:: sage: H.isometry(H.geodesic(-1, 1), H.geodesic(1, -1)) [ 0 1] [-1 0] We can also determine an isometry mapping polygons:: sage: P = H.polygon([H.vertical(1).left_half_space(), H.vertical(-1).right_half_space(), H.geodesic(-1, 1).left_half_space()]) sage: Q = H.polygon([H.geodesic(-1, 0).left_half_space(), H.geodesic(0, 1).left_half_space(), H.geodesic(1, -1).left_half_space()]) sage: m = H.isometry(P, Q) sage: P.apply_isometry(m) == Q True When determining an isometry of polygons, marked vertices are mapped to marked vertices:: sage: P = H.polygon(P.half_spaces(), marked_vertices=[1 + I]) sage: Q = H.polygon(P.half_spaces(), marked_vertices=[]) sage: H.isometry(P, Q) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other sage: Q = H.polygon(P.half_spaces(), marked_vertices=[1 + 2*I]) sage: H.isometry(P, Q) # long time (1s) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other sage: Q = H.polygon(P.half_spaces(), marked_vertices=[-1 + I]) sage: H.isometry(P, Q) # long time (1s) [ 1 0] [ 0 -1] We can explicitly ask for an isometry in the Klein model, given by a 3×3 matrix:: sage: H.isometry(P, Q, model="klein") # long time (1s) [-1 0 0] [ 0 1 0] [ 0 0 1] The isometries are not returned as matrices of unit determinant since such an isometry might not exist without extending the base ring, we can, however, ask for an isometry of determinant ±1:: sage: H.isometry(I, 2*I, normalized=True) Traceback (most recent call last): ... ValueError: not a perfect 2nd power sage: H.change_ring(AA).isometry(I, 2*I, normalized=True) [ 1.414213562373095? 0] [ 0 0.7071067811865475?] sage: _.det() 1.000000000000000? We can also explicitly ask for the isometry for the right action:: sage: isometry = H.isometry(H.vertical(0), H.vertical(1), on_right=True) sage: isometry [ 1 -1] [ 0 1] sage: H.vertical(0).apply_isometry(isometry) {-x - 1 = 0} sage: H.vertical(0).apply_isometry(isometry, on_right=True) {-x + 1 = 0} TESTS: Points forming an ideal triangle with three ideal vertices:: sage: preimage = H(-1), H(0), H(1) sage: image = H(0), H(1), H(2) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Points forming an ideal triangle with two ideal vertices:: sage: preimage = H(-1), H(2*I), H(1) sage: image = H(0), H(1 + 2*I), H(2) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Points forming an ideal triangle with one ideal vertex:: sage: preimage = H(I - 1), H(I + 1), H(oo) sage: image = H(I), H(I + 2), H(oo) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Points forming a finite triangle:: sage: preimage = H(I), H(2*I), H(2*I + 1) sage: image = H(I + 1), H(2*I + 1), H(2*I + 2) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Points forming a degenerate ideal triangle:: sage: preimage = H(-1), H(I), H(1) sage: image = H(0), H(I + 1), H(2) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Another degenerate ideal triangle:: sage: preimage = H(0), H(I), H(oo) sage: image = H(1), H(I + 1), H(oo) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Points forming a finite degenerate triangle:: sage: preimage = H(I), H(2*I), H(3*I) sage: image = H(I + 1), H(2*I + 1), H(3*I + 1) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] Impossible pairs of points (ideal and non-ideal) are detected:: sage: preimage = H(-1), H(I), H(1) sage: image = H(0), H(1), H(2) sage: H.isometry(preimage, image) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other sage: preimage = H(0), H(1), H(2) sage: image = H(-1), H(I), H(1) sage: H.isometry(preimage, image) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other A case with d=0:: sage: preimage = (H.geodesic(0, 1), H.geodesic(1, oo)) sage: image = (H.geodesic(1, oo), H.geodesic(oo, 0)) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 1 0] A case with no solution, such an isometry would map four ideal points in an impossible way:: sage: preimage = (H.geodesic(0, 1), H.geodesic(2, 3)) sage: image = (H.geodesic(0, 1), H.geodesic(3, 4)) sage: H.isometry(preimage, image, on_right=True) Traceback (most recent call last): ... ValueError: no isometry can map these objects to each other An isometry that swaps end points but maps the corresponding oriented geodesics to themselves:: sage: preimage = (-1, 1, -2, 2) sage: image = (1, -1, 2, -2) sage: H.isometry(preimage, image, on_right=True) [-1 0] [ 0 1] An example with negative determinant:: sage: preimage = (I - 1, I + 1, I - 2, I + 2) sage: image = (I + 1, I - 1, I + 2, I - 2) sage: H.isometry(preimage, image, on_right=True) [-1 0] [ 0 1] A case that could initially not be solved over the rationals:: sage: H.isometry((58*I, I + 1), (116*I - 1, 2*I + 1)) [ 2 -1] [ 0 1] A case that caused problems at some point:: sage: isometry = matrix([[1, 0], [0, -1/2]]) sage: x = H(I/2 - 1) sage: y = H(I/3 - 1) sage: z = H(5/19 * I - 1/3) sage: H.isometry((x, y, z), (x.apply_isometry(isometry), y.apply_isometry(isometry), z.apply_isometry(isometry))) # long time (.3s) [-2 0] [ 0 1] :: sage: preimage = (I - 1, I + 1, I + 1, oo) sage: image = (I, I + 2, I + 2, oo) sage: H.isometry(preimage, image, on_right=True) [ 1 -1] [ 0 1] :: sage: preimage = (H.geodesic(I - 1, I + 1), H.geodesic(I - 2, I + 1)) sage: image = (H.geodesic(I + 1, I - 1), H.geodesic(I + 1, I - 2)) sage: H.isometry(preimage, image, on_right=True) [-1 2] [-1 1] :: sage: isometry = matrix([[2, 0], [1, 2]]) sage: x = H(0) sage: y = H(I/2 - 1) sage: z = H(0) sage: H.isometry((x, y, z), (x.apply_isometry(isometry), y.apply_isometry(isometry), z.apply_isometry(isometry))) [ 1 0] [1/2 1] :: sage: H.isometry(I, 2*I) [ 1 0] [ 0 1/2] An underdetermined case:: sage: P = H.geodesic(126, 4447, 6387, model="half_plane").right_half_space() sage: isometry = matrix([[-1, 2], [2, -1/2]]) sage: Q = P.apply_isometry(isometry) sage: H.isometry(P, Q) [ 126/8579 -4321/8579] [ 0 1] Here, there is also an isometry of negative determinant that maps some of the half spaces correctly:: sage: P = H.polygon([ ....: H.geodesic(1, -5, 5, model="half_plane").left_half_space(), ....: H.geodesic(247, -957, -5156, model="half_plane").right_half_space(), ....: H.geodesic(1, -120, -137, model="half_plane").right_half_space()]) sage: isometry = matrix([[0, -2], [1, 2]]) sage: Q = P.apply_isometry(isometry) sage: H.isometry(P, Q) # long time (.4s) [ 0 -1] [1/2 1] An isometry mapping unoriented segments, though not the most apparent one:: sage: H.isometry(H(I).segment(2*I).unoriented(), H(2*I).segment(I).unoriented()) [ 0 -1] [1/2 0] .. SEEALSO:: :meth:`HyperbolicConvexSet.apply_isometry` to apply the returned isometry to a convex set. """ if normalized: isometry = self.isometry( preimage=preimage, image=image, model=model, on_right=on_right, normalized=False, ) det = abs(isometry.det()) λ = det.nth_root(isometry.nrows()) return ~λ * isometry if model == "klein": isometry = self.isometry( preimage=preimage, image=image, model="half_plane", on_right=on_right, normalized=normalized, ) return self._isometry_gl2_to_sim12(isometry) elif model != "half_plane": raise NotImplementedError("unsupported model") if not on_right: isometry = self.isometry( preimage=preimage, image=image, model=model, on_right=True, normalized=normalized, ) if model == "half_plane": from sage.all import matrix # Pick a nice representative of the inverse matrix. isometry = matrix( [ [isometry[1][1], -isometry[0][1]], [-isometry[1][0], isometry[0][0]], ] ) else: isometry = ~isometry return isometry # Normalize the arguments so that they are a list of convex sets. from collections.abc import Iterable if not isinstance(preimage, Iterable): preimage = [preimage] if not isinstance(image, Iterable): image = [image] preimage = [self(x) for x in preimage] image = [self(y) for y in image] if len(preimage) != len(image): raise ValueError( "preimage and image must be the same size to determine an isometry between them" ) for x, y in zip(preimage, image): if x.dimension() != y.dimension(): raise ValueError( "preimage and image must be of the same dimensions to determine an isometry between them" ) if x.is_oriented() != y.is_oriented(): raise ValueError( "preimage and image must be oriented or unoriented consistently" ) # Drop empty sets from the preimage and image to make our lives easier preimage = [x for x in preimage if x.dimension() >= 0] image = [y for y in image if y.dimension() >= 0] # An isometry is uniquely determined by mapping three points. # In principle, we now just need to find three points in preimage, find # their corresponding images, construct the isometry, and then check # that it maps everything correctly. # However, there are objects that do not really define a mapping of # points. For example, when presented with an unoriented geodesic, we # can map its endpoints two possible ways (apart from that, an isometry # can swap the end points of a geodesic but map an oriented geodesic to # itself at the same time.) Similarly, when mapping a polygon, we can # permute the edges cyclically. # We need a mild form of backtracking to collect all possible triples # that define the isometry. isometry = self._isometry_from_pairs(list(zip(preimage, image))) if isometry is None: raise ValueError("no isometry can map these objects to each other") return isometry
[docs] def _isometry_gl2_to_sim12(self, isometry): r""" Return a lift of the ``isometry`` to a 3×3 matrix in the similitude group `\mathrm{Sim}(1, 2)` describing an isometry in hyperboloid model. This is a helper method for :meth:`isometry` and :meth:`HyperbolicConvexSet.apply_isometry` since isometries in the hyperboloid model can be directly applied to our objects which we represent in the Klein model. INPUT: - ``isometry`` -- a 2×2 matrix with non-zero determinant EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._isometry_gl2_to_sim12(matrix(2, [1,1,0,1])) [ 1 -1 1] [ 1 1/2 1/2] [ 1 -1/2 3/2] sage: H._isometry_gl2_to_sim12(matrix(2, [1,0,1,1])) [ 1 1 1] [ -1 1/2 -1/2] [ 1 1/2 3/2] sage: H._isometry_gl2_to_sim12(matrix(2, [2,0,0,1/2])) [ 1 0 0] [ 0 17/8 15/8] [ 0 15/8 17/8] .. SEEALSO:: :meth:`_isometry_sim12_to_gl2` for an inverse of this construction """ from sage.matrix.constructor import matrix if isometry.dimensions() != (2, 2): raise ValueError( "matrix does not encode an isometry in the half plane model" ) a, b, c, d = isometry.list() return matrix( 3, [ a * d + b * c, a * c - b * d, a * c + b * d, a * b - c * d, (a**2 - b**2 - c**2 + d**2) / 2, (a**2 + b**2 - c**2 - d**2) / 2, a * b + c * d, (a**2 - b**2 + c**2 - d**2) / 2, (a**2 + b**2 + c**2 + d**2) / 2, ], )
[docs] def _isometry_sim12_to_gl2(self, isometry): r""" Return an invertible 2×2 matrix that encodes the same isometry as ``isometry``. INPUT: - ``isometry`` -- a 3×3 matrix in `\mathrm{Sim}(1, 2)` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._isometry_sim12_to_gl2(matrix(3, [1, -1, 1, 1, 1/2, 1/2, 1, -1/2, 3/2])) [1 1] [0 1] sage: H._isometry_sim12_to_gl2(matrix(3, [1, 1, 1, -1, 1/2, -1/2, 1, 1/2, 3/2])) [1 0] [1 1] sage: H._isometry_sim12_to_gl2(matrix(3, [1, 0, 0, 0, 17/8, 15/8, 0, 15/8, 17/8])) [ 2 0] [ 0 1/2] sage: H._isometry_sim12_to_gl2(H._isometry_gl2_to_sim12(matrix([[-1, 0], [0, 1]]))) [ 1 0] [ 0 -1] .. SEEALSO:: :meth:`_isometry_gl2_to_sim12` for an inverse of this construction """ from sage.matrix.constructor import matrix if isometry.dimensions() != (3, 3): raise ValueError("matrix does not encode an isometry in the Klein model") m00, m01, m02, m10, m11, m12, m20, m21, m22 = isometry.list() K = isometry.base_ring() two = K(2) so12_det = isometry.determinant() sl2_det = so12_det.nth_root(3) if so12_det.sign() != sl2_det.sign(): sl2_det *= -1 a2 = (m12 + m22 + m21 + m11) / two b2 = (m12 + m22 - m21 - m11) / two c2 = (m21 + m22 - m12 - m11) / two d2 = (m11 + m22 - m12 - m21) / two ab = (m10 + m20) / two ac = (m01 + m02) / two ad = (m00 + sl2_det) / two bc = (m00 - sl2_det) / two bd = (m02 - m01) / two cd = (m20 - m10) / two # we recover +/- a taking square roots pm_a = a2.sqrt() pm_b = b2.sqrt() pm_c = c2.sqrt() pm_d = d2.sqrt() # We could do something less naive as there is no need to iterate import itertools for sa, sb, sc, sd in itertools.product([1, -1], repeat=4): a = sa * pm_a b = sb * pm_b c = sc * pm_c d = sd * pm_d if ( a * b == ab and a * c == ac and a * d == ad and b * c == bc and b * d == bd and c * d == cd ): return matrix(K, 2, 2, [a, b, c, d]) raise ValueError("no projection to GL(2, R) in the base ring")
def _isometry_from_pairs(self, pairs): r""" Return a right isometry compatible with given (preimage, image) pairs. This is helper method for :meth:`isometry`. ALGORITHM: We extract pairs of primitive elements (points and geodesics) from ``pairs`` that necessarily need to map to each other for the mappings in ``pairs`` to be satisfied. (Based on the idea that there is only one isometry mapping three points in a prescribed way.) For these pairs of primitive elements, we determine the isometries mapping them (there might be more than one when we cannot extract three points) and check whether they map all ``pairs`` correctly. There is a bit of backtracking needed in :meth:`_isometry_conditions` since, e.g., there is no a unique isometry mapping two polygons to each other since we can, e.g., permute the edges of the polygon cyclically. INPUT: - ``pairs`` -- a sequence of pairs of hyperbolic sets EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._isometry_from_pairs([(H(0), H(1)), (H(1), H(2)), (H(2), H(3))]) [ 1 -1] [ 0 1] """ for conditions in self._isometry_conditions([], pairs): for isometry in self._isometry_from_primitives(conditions): if isometry is None: continue if any( preimage.apply_isometry(isometry, on_right=True) != image for (preimage, image) in pairs ): continue return isometry return None def _isometry_from_primitives(self, pairs): r""" Helper method for :meth:`isometry`. Return right isometries as 2x2 matrices that maps the elements of ``pairs`` to each other. INPUT: - ``pairs`` -- a sequence of pairs of geodesics or hyperbolic points OUTPUT: An iterator of matrices, see below. ALGORITHM: If ``pairs`` is a single pair of points, we construct an isometry with :meth:`_isometry_from_single_points`. If ``pairs`` is a single pair of geodesics, we make an attempt to construct an isometry with :meth:`_isometry_from_single_geodesics`. Otherwise, ``pairs`` up to three pairs of points and geodesics. These do not always uniquely define an isometry, e.g., often when presented with two geodesics. Namely, there could be one isometry of positive determinant and one isometry of negative determinant. We return both of them. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() An underdetermined system. Here we are lucky, the midpoint of the geodesics has coordinates in the base ring and the isometry mapping midpoints to each other is defined over the base ring:: sage: preimage = H.geodesic(-126, -4447, -6387, model="half_plane") sage: image = H.geodesic(-8579, -13089, -4510, model="half_plane") sage: list(H._isometry_from_primitives([(preimage, image)])) [ [ 1 4321/8579] [ 0 126/8579] ] Here, a non-trivial isometry maps these oriented geodesics to themselves, i.e., it stabilizes what's to the left of the geodesics:: sage: g = H.geodesic(-1, 1) sage: h = H.geodesic(-2, 2) sage: g.apply_isometry(matrix([[-1, 0], [0, 1]])) == g True sage: h.apply_isometry(matrix([[-1, 0], [0, 1]])) == h True sage: list(H._isometry_from_primitives([(g, g), (h, h)])) [ [1 0] [-1 0] [0 1], [ 0 1] ] Note that that isometry swaps endpoints though:: sage: H(-1).apply_isometry(matrix([[-1, 0], [0, 1]])) 1 """ if len(pairs) == 0: from sage.all import matrix yield matrix(self.base_ring(), [[1, 0], [0, 1]]) return if len(pairs) == 1 and pairs[0][0].dimension() == 0: yield self._isometry_from_single_points(pairs[0][0], pairs[0][1]) return if len(pairs) == 1 and pairs[0][0].dimension() == 1: yield self._isometry_from_single_geodesics(pairs[0][0], pairs[0][1]) return # Create polynomial equations that must be satisfied to map the pairs # to each other. def equations(isometry, λ): equations = [] for i, (preimage, image) in enumerate(pairs): equations.extend(preimage._isometry_equations(isometry, image, λ[i])) return equations # Create a predicate that can be used to check whether the isometry # correctly maps the pairs. def create_filter(det): def filter(isometry): if isometry.det().sign() != det: return False for preimage, image in pairs: if preimage.apply_isometry(isometry, on_right=True) != image: return False return True return filter yield self._isometry_from_equations(equations, create_filter(1)) yield self._isometry_from_equations(equations, create_filter(-1)) def _isometry_untrivialize(self, preimage, image, defining): r""" Helper method for :meth:`isometry`. Return a pair of hyperbolic objects that describe the mapping of ``preimage`` to ``image`` or return ``None`` if that mapping is already captured by the pairs of objects in ``defining``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._isometry_untrivialize(H(I), H(I), []) (I, I) There are many ways to map the geodesic between -1 and 1 to itself. Knowing that we need to fix `I` adds information:: sage: H._isometry_untrivialize(H(I), H(I), [(H.geodesic(-1, 1), H.geodesic(-1, 1))]) (I, I) However, we already know that ``-1`` must go to ``-1``. Well, actually that's not true, we could also be swapping the endpoints otherwise but we want to use this to build three conditions that are independent (this could probably be improved):: sage: H._isometry_untrivialize(H(-1), H(-1), [(H.geodesic(-1, 1), H.geodesic(-1, 1))]) is None True """ existings = [x for (x, y) in defining] if preimage.dimension() == 0: if preimage in existings: return None if preimage.is_ideal(): # Actually, an ideal point is not trivial if the existing # geodesic is unoriented. But it does not take a full # degree of freedom away, so we ignore it here. if any(preimage in existing for existing in existings): return None return (preimage, image) elif preimage.dimension() == 1: if preimage.unoriented() in [ existing.unoriented() for existing in existings ]: # Again, we ignore the distinction between oriented and # unoriented geodesics here. return None if preimage.start() in existings: return self._isometry_untrivialize( preimage.end(), image.end(), defining ) if preimage.end() in existings: return self._isometry_untrivialize( preimage.start(), image.start(), defining ) return (preimage, image) raise NotImplementedError
[docs] def _isometry_conditions(self, defining, remaining): r""" Helper method for :meth:`isometry`. Return sequences (typically triples) of pairs of hyperbolic primitive objects (geodesics and points) that (almost) uniquely define a hyperbolic mapping. Build this sequence by extending ``defining`` with conditions extracted from ``remaining``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Given four points, three points already uniquely determine the isometry:: sage: conditions = H._isometry_conditions(defining=[], remaining=[(H(0), H(0)), (H(1), H(1)), (H(2), H(2)), (H(3), H(3))]) sage: list(conditions) [[(0, 0), (1, 1), (2, 2)]] The data provided by ``remaining`` can contain redundancies:: sage: conditions = H._isometry_conditions(defining=[], remaining=[(H(0), H(0)), (H(1), H(1)), (H(1), H(1)), (H(3), H(3))]) sage: list(conditions) [[(0, 0), (1, 1), (3, 3)]] For more complex objects there might be lots of mappings possible (we could likely have a shorter list of possibilities here):: sage: P = H.polygon([H.vertical(1).left_half_space(), H.vertical(-1).right_half_space(), H.geodesic(-1, 1).left_half_space()], marked_vertices=[I + 1]) sage: Q = H.polygon(P.half_spaces(), marked_vertices=[I - 1]) sage: conditions = H._isometry_conditions([], [(P, Q)]) sage: list(conditions) [[({-x + 1 = 0}, {x + 1 = 0}), ({x + 1 = 0}, {(x^2 + y^2) - 1 = 0})], [({-x + 1 = 0}, {(x^2 + y^2) - 1 = 0}), ({x + 1 = 0}, {-x + 1 = 0})], [({-x + 1 = 0}, {-x + 1 = 0}), ({x + 1 = 0}, {x + 1 = 0})], [({-x + 1 = 0}, {x + 1 = 0}), ({x + 1 = 0}, {-x + 1 = 0})], [({-x + 1 = 0}, {-x + 1 = 0}), ({x + 1 = 0}, {(x^2 + y^2) - 1 = 0})], [({-x + 1 = 0}, {(x^2 + y^2) - 1 = 0}), ({x + 1 = 0}, {x + 1 = 0})]] """ def degree(preimage, image): if preimage.dimension() == 0: return 1 if preimage.dimension() == 1: return 2 assert False degree = sum([degree(preimage, image) for (preimage, image) in defining]) # If we have three pairs of points, determine the unique isometry that maps them to each other. if degree >= 3: yield defining return # There are fewer than three points in "defining". Extend with more points. if remaining: # Extend by turning remaining[0] into a condition x, y = remaining[0] remaining = remaining[1:] # Extend with a pair of points in "remaining[0]" if isinstance(x, HyperbolicPoint): assert y.dimension() == 0 pair = self._isometry_untrivialize(x, y, defining) if pair: defining.append(pair) yield from self._isometry_conditions(defining[:], remaining) # Extend with a pair of geodesics in "remaining[0]" elif isinstance(x, HyperbolicOrientedGeodesic): assert y.dimension() == 1 f = x.geodesic() g = y.geodesic() pair = self._isometry_untrivialize(f, g, defining) if pair: defining.append(pair) yield from self._isometry_conditions( defining[:], remaining + [(x.start(), y.start()), (x.end(), y.end())], ) # Extend with points coming from other hyperbolic objects in "remaining[0]" else: for pairs in x._isometry_conditions(y): yield from self._isometry_conditions(defining[:], pairs + remaining) else: yield defining
def _isometry_from_single_points(self, preimage, image): r""" Helper method for :meth:`isometry`. Return a right isometry that maps the point ``preimage`` to the point ``image`` or ``None`` when no such isometry exists. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._isometry_from_single_points(H(I), H(I)) [1 0] [0 1] sage: H._isometry_from_single_points(H(I), H(2*I)) [1/2 0] [ 0 1] sage: H._isometry_from_single_points(H(0), H(oo)) [0 1] [1 0] sage: H._isometry_from_single_points(H(I), H(1)) is None True """ if preimage.is_ideal() != image.is_ideal(): return None from sage.all import MatrixSpace MS = MatrixSpace(self.base_ring(), 2, 2) isometry = MS(1) if preimage == image: return isometry if preimage.is_ideal(): if preimage == self.infinity(): isometry *= ~MS([[0, 1], [1, 0]]) else: isometry *= ~MS([[1, -preimage.coordinates()[0]], [0, 1]]) if image == self.infinity(): isometry *= ~MS([[0, 1], [1, 0]]) else: isometry *= ~MS([[1, image.coordinates()[0]], [0, 1]]) return isometry else: isometry *= ~MS( [[image.coordinates()[1] / preimage.coordinates()[1], 0], [0, 1]] ) isometry *= ~MS( [ [ 1, image.coordinates()[0] - preimage.apply_isometry( isometry, on_right=True ).coordinates()[0], ], [0, 1], ] ) return isometry def _isometry_from_single_geodesics(self, preimage, image): r""" Helper method for :meth:`isometry`. Return a right isometry that maps the geodesic ``preimage`` to the geodesic ``image`` or ``None`` when no such isometry exists. ALGORITHM: We determine the isometry by forcing the midpoints of the geodesics to be mapped to each other. This might fail because the midpoints of the geodesics are not defined over the :meth:`base_ring`. Also, that isometry might not be defined over the base ring but some other isometry is. In general, this is not a good approach. There might be a much better way to determine such an isometry explicitly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A case where we can actually map things:: sage: H._isometry_from_single_geodesics(H.geodesic(-1, 1), H.geodesic(0, 2)) [ 1 -1] [ 0 1] In many cases, we fail to find the isometry:: sage: g = H.geodesic(1, 2, 3, model="klein") sage: h = g.apply_isometry(matrix([[1, 2], [3, 4]]), on_right=True) sage: H._isometry_from_single_geodesics(g, h) Traceback (most recent call last): ... ValueError: ... """ for isometry in self._isometry_from_primitives( [(preimage, image), (preimage.midpoint(), image.midpoint())] ): if isometry is None: import warnings warnings.warn( "Could not determine an isometry of geodesics over the base ring. There might still be one but the implementation failed to detect it." ) return isometry
[docs] def _isometry_from_equations(self, conditions, filter): r""" Helper method for :meth:`isometry`. Return an isometry that satisfies ``conditions`` and ``filter``. INPUT: - ``conditions`` -- a function that receives a (symbolic) isometry and some (symbolic) variables and creates polynomial equations that a concrete isometry must satisfy. - ``filter`` -- a function that receives a concrete isometry and returns whether it maps objects correctly. ALGORITHM: We guess determine the entries of a 2×2 matrix with entries a, b, c, d by guessing for each term that it is non-zero and then building the symbolic relations that the isometry must satisfy by invoking ``conditions``. These symbolic conditions contain free linear variables coming from the fact that points are encoded projectively and geodesics in the dual. We tune these variables so that all entries of the matrix are in the base ring. (Namely, so that we can take all the square roots that show up.) The whole process is very ad-hoc and very slow since it computes lots of Gröbner bases. It is very likely that the approach is not mathematically sound but it worked for many random inputs that we presented it with. """ from sage.all import PolynomialRing, matrix # Over the reals, the equations are different depending on whether we # send a geodesic to -another geodesic or +another geodesic. We try # both cases to see which one yields a system that we can solve over # the base ring. for sgn in [self.base_ring().one(), -self.base_ring().one()]: # We try to determine the matrix describing the isometry assuming # that "variable" is non-zero. for variable in ["a", "d", "b", "c"]: variables = ["a", "b", "c", "d", "λ1", "λ2"] variables.remove(variable) variables.append(variable) # We use a term order that guarantees that we will see an # equation for "variable" in the Gröbner basis. R = PolynomialRing( self.base_ring(), names=variables, order="degrevlex(5), lex(1)" ) # We are going to run the same procedure twice. Once with λ0 = # ±1 and then with a λ0 tuned to a value so that we can # actually solve for "variable". λ0 = sgn λ1 = R("λ1") λ2 = R("λ2") a = R("a") b = R("b") c = R("c") d = R("d") variable = R(variable) # We keep track of whether we made any assumptions here that # mean that we might be ignoring solutions in this run. equivalence = True # The isometry as a symbolic 2×2 matrix. isometry = matrix([[a, b], [c, d]]) isometry = self._isometry_gl2_to_sim12(isometry) # Build equations for the symbolic variables and make sure that # the resulting variety is zero-dimensional. equations = conditions(isometry, (λ0, λ1, λ2)) for λ in [λ1, λ2]: if all(equation.degree(λ) <= 0 for equation in equations): # Force the unused variable λ to be =0 equations.append(λ) # The system of euations typically has no rational points. # We analyze the Gröbner basis to tune λ0 so that we get # rational points. J = list(R.ideal(equations).groebner_basis()) if J == [1]: # The equations are contradictory. assert equivalence return None # We extract an equation for "variable" from the Gröbner basis. equation = J[-1] assert equation.variables() == ( variable, ), f"expected Gröbner basis algorithm to yield an equation for {variable} but found {equation} instead" equation = equation.polynomial(variable) equation = equation.map_coefficients( self.base_ring(), new_base_ring=self.base_ring() ) variable = equation.parent().gen() # The variable must be zero. We continue with another # (non-zero) variable since we meant to deduce the value of the # scaling factor λ0. if equation == variable: continue # The equation allows the case that the variable is zero. # We ignore this possibility here. If this is needed, then we # will find out in the loop for another variable. while not equation.constant_coefficient(): equivalence = False equation >>= 1 from sage.all import QQbar equation = equation.change_ring(QQbar) # We try to arrange things so that "variable" becomes an element of the base ring. # We look at the minpoly of variable and tune λ0 so that this # becomes a square root of something that is a square. for root in equation.roots(multiplicities=False): minpoly = root.minpoly() if minpoly.degree() == 1: pass elif minpoly.exponents() == [0, 2]: λ0 = -sgn * ~self.base_ring()(minpoly.constant_coefficient()) else: # We cannot change the equations for this root to show # up over the base ring. continue assert ( λ0 in self.base_ring() and λ0 != 0 ), f"did not deduce a non-zero constant for λ0={λ0} from {equation}" # We could now patch the existing Gröbner basis and solve directly, but # we just solve again for the correct value of λ0. equations = conditions(isometry, (λ0, λ1, λ2)) for λ in [λ1, λ2]: if all(equation.degree(λ) <= 0 for equation in equations): # Force the unused variable λ to be =0 equations.append(λ) solutions = R.ideal(equations).variety() assert ( solutions ), "After tuning the constant of the equations describing the isometry, there should be a solution but we did not find any." solutions = [ matrix([[solution[a], solution[b]], [solution[c], solution[d]]]) for solution in solutions ] # Check which solutions do not only satisfy "conditions" # but map the underlying objects correctly, i.e., they also # pass "filter". solutions = [solution for solution in solutions if filter(solution)] if not solutions: continue # Prefer an isometry of determinant 1 and isometries with 1 entries. return max( solutions, key=lambda isometry: ( isometry.det(), isometry[1][1] == 1, isometry[1][0] == 1, ), ) # No luck with this approach. We hope that this means that no such # isometry exists over the base ring but it's not entirely clear # whether that is actually true. return None
def _repr_(self): r""" Return a printable representation of this hyperbolic plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: HyperbolicPlane(AA) Hyperbolic Plane over Algebraic Real Field """ return f"Hyperbolic Plane over {repr(self.base_ring())}"
[docs] class HyperbolicGeometry: r""" Predicates and primitive geometric constructions over a base ``ring``. This class and its subclasses implement the core underlying hyperbolic geometry that depends on the base ring. For example, when deciding whether two points in the hyperbolic plane are equal, we cannot just compare their coordinates if the base ring is inexact. Therefore, that predicate is implemented in this "geometry" class and is implemented differently by :class:`HyperbolicExactGeometry` for exact and :class:`HyperbolicEpsilonGeometry` for inexact rings. INPUT: - ``ring`` -- a ring, the ring in which coordinates in the hyperbolic plane will be represented .. NOTE:: Abstract methods are not marked with `@abstractmethod` since we cannot use the ABCMeta metaclass to enforce their implementation; otherwise, our subclasses could not use the unique representation metaclasses. EXAMPLES: The specific hyperbolic geometry implementation is picked automatically, depending on whether the base ring is exact or not:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry Exact geometry over Rational Field sage: H(0) == H(1/1024) False However, we can explicitly use a different or custom geometry:: sage: from flatsurf.geometry.hyperbolic import HyperbolicEpsilonGeometry sage: H = HyperbolicPlane(QQ, HyperbolicEpsilonGeometry(QQ, 1/1024)) sage: H.geometry Epsilon geometry with ϵ=1/1024 over Rational Field sage: H(0) == H(1/2048) True .. SEEALSO:: :class:`HyperbolicExactGeometry`, :class:`HyperbolicEpsilonGeometry` """
[docs] def __init__(self, ring): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicGeometry sage: H = HyperbolicPlane() sage: isinstance(H.geometry, HyperbolicGeometry) True """ self._ring = ring
[docs] def base_ring(self): r""" Return the ring over which this geometry is implemented. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.base_ring() Rational Field """ return self._ring
def _zero(self, x): r""" Return whether ``x`` should be considered zero in the :meth:`base_ring`. .. NOTE:: This predicate should not be used directly in geometric constructions since it does not specify the context in which this question is asked. This makes it very difficult to override a specific aspect in a custom geometry. Also, this predicate lacks the context of other elements; a proper predicate should also take other elements into account to decide this question relative to the other values. INPUT: - ``x`` -- an element of the :meth:`base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry._zero(1) False sage: H.geometry._zero(1e-9) True """ return self._cmp(x, 0) == 0 def _cmp(self, x, y): r""" Return how ``x`` compares to ``y``. .. NOTE:: This predicate should not be used directly in geometric constructions since it does not specify the context in which this question is asked. This makes it very difficult to override a specific aspect in a custom geometry. INPUT: - ``x`` -- an element of the :meth:`base_ring` - ``y`` -- an element of the :meth:`base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry._cmp(0, 0) 0 sage: H.geometry._cmp(0, 1) -1 sage: H.geometry._cmp(1, 0) 1 :: sage: H = HyperbolicPlane(RR) sage: H.geometry._cmp(0, 0) 0 sage: H.geometry._cmp(0, 1) -1 sage: H.geometry._cmp(1, 0) 1 sage: H.geometry._cmp(1e-10, 0) 0 """ if self._equal(x, y): return 0 if x < y: return -1 assert ( x > y ), "Geometry over this ring must override _cmp since not (x == y) and not (x < y) does not imply x > y" return 1 def _sgn(self, x): r""" Return the sign of ``x``. .. NOTE:: This predicate should not be used directly in geometric constructions since it does not specify the context in which this question is asked. This makes it very difficult to override a specific aspect in a custom geometry. Also, this predicate lacks the context of other elements; a proper predicate should also take other elements into account to decide this question relative to the other values. INPUT: - ``x`` -- an element of the :meth:`base_ring`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry._sgn(1) 1 sage: H.geometry._sgn(-1) -1 sage: H.geometry._sgn(1e-10) 0 """ return self._cmp(x, 0)
[docs] def _equal(self, x, y): r""" Return whether ``x`` and ``y`` should be considered equal in the :meth:`base_ring`. .. NOTE:: This predicate should not be used directly in geometric constructions since it does not specify the context in which this question is asked. This makes it very difficult to override a specific aspect in a custom geometry. INPUT: - ``x`` -- an element of the :meth:`base_ring` - ``y`` -- an element of the :meth:`base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry._equal(0, 1) False sage: H.geometry._equal(0, 1e-10) True """ raise NotImplementedError("this geometry does not implement _equal()")
def _determinant(self, a, b, c, d): r""" Return the determinant of the 2×2 matrix ``[[a, b], [c, d]]`` or ``None`` if the matrix is singular. .. NOTE:: This predicate should not be used directly in geometric constructions since it does not specify the context in which this question is asked. This makes it very difficult to override a specific aspect in a custom geometry. INPUT: - ``a`` -- an element of the :meth:`base_ring` - ``b`` -- an element of the :meth:`base_ring` - ``c`` -- an element of the :meth:`base_ring` - ``d`` -- an element of the :meth:`base_ring` EXAMPLES: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry._determinant(1, 2, 3, 4) -2 sage: H.geometry._determinant(0, 10^-10, 1, 1) -1/10000000000 """ det = a * d - b * c if self._zero(det): return None return det
[docs] def change_ring(self, ring): r""" Return this geometry with the :meth:`base_ring` changed to ``ring``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry Exact geometry over Rational Field sage: H.geometry.change_ring(AA) Exact geometry over Algebraic Real Field """ raise NotImplementedError("this geometry does not implement change_ring()")
[docs] def projective(self, p, q, point): r""" Return the ideal point with projective coordinates ``[p: q]`` in the upper half plane model. INPUT: - ``p`` -- an element of the :meth:`base_ring` - ``q`` -- an element of the :meth:`base_ring` - ``point`` -- the :meth:`HyperbolicPlane.point` to create points EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.projective(1, 0, H.point) sage: H.geometry.projective(0, 1, H.point) 0 """ if self._zero(p) and self._zero(q): raise ValueError("one of p and q must not be zero") if self._zero(q): return point(0, 1, model="klein", check=False) return point(p / q, 0, model="half_plane", check=False)
[docs] def half_circle(self, center, radius_squared, geodesic): r""" Return the geodesic around the real ``center`` and with ``radius_squared`` in the upper half plane. INPUT: - ``center`` -- an element of the :meth:`base_ring`, the center of the half circle on the real axis - ``radius_squared`` -- a positive element of the :meth:`base_ring`, the square of the radius of the half circle - ``geodesic`` -- the :meth:`HyperbolicPlane.geodesic` to create geodesics EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.half_circle(0, 1, H.geodesic) {(x^2 + y^2) - 1 = 0} Unfortunately, this does not work correctly over inexact fields yet:: sage: H = HyperbolicPlane(RR) sage: H.geometry.half_circle(0, 1e-32, H.geodesic) Traceback (most recent call last): ... ValueError: radius must be positive """ if self._sgn(radius_squared) <= 0: raise ValueError("radius must be positive") # Represent this geodesic as a(x^2 + y^2) + b*x + c = 0 a = 1 b = -2 * center c = center * center - radius_squared return geodesic(a, b, c, model="half_plane")
[docs] def vertical(self, real, geodesic): r""" Return the vertical geodesic at the ``real`` ideal point in the upper half plane model. INPUT: - ``real`` -- an element of the :meth:`base_ring` - ``geodesic`` -- the :meth:`HyperbolicPlane.geodesic` to create geodesics EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.vertical(0, H.geodesic) {-x = 0} Unfortunately, this does not allow creation of verticals at large reals over inexact fields yet:: sage: H = HyperbolicPlane(RR) sage: H.geometry.vertical(1e32, H.geodesic) Traceback (most recent call last): ... ValueError: equation ... does not define a chord in the Klein model """ # Convert the equation -x + real = 0 to the Klein model. return geodesic(real, -1, -real, model="klein")
[docs] def classify_point(self, x, y, model): r""" Return whether the point ``(x, y)`` is finite, ideal, or ultra-ideal. INPUT: - ``x`` -- an element of the :meth:`base_ring` - ``y`` -- an element of the :meth:`base_ring` - ``model`` -- a supported model, either ``"half_plane"`` or ``"klein"`` OUTPUT: ``1`` if the point is finite, ``0`` if the point is ideal, ``-1`` if the point is neither of the two. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.classify_point(0, 1, model="half_plane") 1 sage: H.geometry.classify_point(0, 0, model="half_plane") 0 sage: H.geometry.classify_point(0, -1, model="half_plane") -1 Unfortunately, over an inexact field, this detects points close to the real axis as being ultra-ideal:: sage: H = HyperbolicPlane(RR) sage: H.geometry.classify_point(0, -1e32, model="half_plane") -1 """ if model == "half_plane": return self._sgn(y) if model == "klein": raise NotImplementedError("cannot classify points in the Klein model yet") raise NotImplementedError("unsupported model")
[docs] def intersection(self, f, g): r""" Return the point of intersection between the Euclidean lines ``f`` and ``g``. INPUT: - ``f`` -- a triple of elements ``(a, b, c)`` of :meth:`base_ring` encoding the line `a + bx + cy = 0` - ``g`` -- a triple of elements ``(a, b, c)`` of :meth:`base_ring` encoding the line `a + bx + cy = 0` OUTPUT: A pair of elements of :meth:`base_ring`, the coordinates of the point of intersection, or ``None`` if the lines do not intersect. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.intersection((0, 1, 0), (0, 0, 1)) (0, 0) """ (fa, fb, fc) = f (ga, gb, gc) = g det = self._determinant(fb, fc, gb, gc) if det is None: return None x = (-gc * fa + fc * ga) / det y = (gb * fa - fb * ga) / det return (x, y)
[docs] class HyperbolicExactGeometry(UniqueRepresentation, HyperbolicGeometry): r""" Predicates and primitive geometric constructions over an exact base ring. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry Exact geometry over Rational Field TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicExactGeometry sage: isinstance(H.geometry, HyperbolicExactGeometry) True .. SEEALSO:: :class:`HyperbolicEpsilonGeometry` for an implementation over inexact rings """
[docs] def _equal(self, x, y): r""" Return whether the numbers ``x`` and ``y`` should be considered equal in exact geometry. .. NOTE:: This predicate should not be used directly in geometric constructions since it does not specify the context in which this question is asked. This makes it very difficult to override a specific aspect in a custom geometry. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry._equal(0, 1) False sage: H.geometry._equal(0, 1/2**64) False sage: H.geometry._equal(0, 0) True """ return x == y
[docs] def change_ring(self, ring): r""" Return this geometry with the :meth:`~HyperbolicGeometry.base_ring` changed to ``ring``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry.change_ring(QQ) == H.geometry True sage: H.geometry.change_ring(AA) Exact geometry over Algebraic Real Field When presented with the reals, we guess the epsilon for the :class:`HyperbolicEpsilonGeometry` to be consistent with the :class:`HyperbolicGeometry` constructor. (And also, because we use this frequently when plotting.):: sage: H.geometry.change_ring(RR) Epsilon geometry with ϵ=1.00000000000000e-6 over Real Field with 53 bits of precision """ from sage.all import RR if ring is RR: return HyperbolicEpsilonGeometry(ring, 1e-6) if not ring.is_exact(): raise ValueError("cannot change_ring() to an inexact ring") return HyperbolicExactGeometry(ring)
[docs] def __repr__(self): r""" Return a printable representation of this geometry. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geometry Exact geometry over Rational Field """ return f"Exact geometry over {self._ring}"
[docs] class HyperbolicEpsilonGeometry(UniqueRepresentation, HyperbolicGeometry): r""" Predicates and primitive geometric constructions over a base ``ring`` with "precision" ``epsilon``. This is an alternative to :class:`HyperbolicExactGeometry` over inexact rings. The exact meaning of the ``epsilon`` parameter is a bit fuzzy, but the basic idea is that two numbers are considered equal in this geometry if their relative difference is less than ``epsilon``, see :meth:`_equal` for details. INPUT: - ``ring`` -- a ring, the ring in which coordinates in the hyperbolic plane will be represented - ``epsilon`` -- an error bound EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicEpsilonGeometry sage: H = HyperbolicPlane(RR, HyperbolicEpsilonGeometry(RR, 1/1024)) The ``epsilon`` affects the notion of equality in this geometry:: sage: H(0) == H(1/2048) True sage: H(1/2048) == H(2/2048) False This geometry is meant for inexact rings, however, it can also be used in exact rings:: sage: H = HyperbolicPlane(QQ, HyperbolicEpsilonGeometry(QQ, 1/1024)) .. SEEALSO:: :class:`HyperbolicExactGeometry` """
[docs] def __init__(self, ring, epsilon): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicEpsilonGeometry sage: H = HyperbolicPlane(RR) sage: isinstance(H.geometry, HyperbolicEpsilonGeometry) True """ super().__init__(ring) self._epsilon = ring(epsilon)
[docs] def _equal(self, x, y): r""" Return whether ``x`` and ``y`` should be considered equal numbers with respect to an ε error. .. NOTE:: This method has not been tested much. Since this underlies much of the inexact geometry, we should probably do something better here, see e.g., https://floating-point-gui.de/errors/comparison/ EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry._equal(1, 2) False sage: H.geometry._equal(1, 1 + 1e-32) True sage: H.geometry._equal(1e-32, 1e-32 + 1e-33) False sage: H.geometry._equal(1e-32, 1e-32 + 1e-64) True """ if x == 0 or y == 0: return abs(x - y) < self._epsilon return abs(x - y) <= (abs(x) + abs(y)) * self._epsilon
def _determinant(self, a, b, c, d): r""" Return the determinant of the 2×2 matrix ``[[a, b], [c, d]]`` or ``None`` if the matrix is singular. INPUT: - ``a`` -- an element of the :meth:`~HyperbolicGeometry.base_ring` - ``b`` -- an element of the :meth:`~HyperbolicGeometry.base_ring` - ``c`` -- an element of the :meth:`~HyperbolicGeometry.base_ring` - ``d`` -- an element of the :meth:`~HyperbolicGeometry.base_ring` EXAMPLES: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry._determinant(1, 2, 3, 4) -2 sage: H.geometry._determinant(1e-10, 0, 0, 1e-10) 1.00000000000000e-20 Unfortunately, we are not implementing any actual rank detecting algorithm (QR decomposition or such) here. So, we do not detect that this matrik is singular:: sage: H.geometry._determinant(1e-127, 1e-128, 1, 1) 9.00000000000000e-128 """ det = a * d - b * c if det == 0: # Note that we should instead numerically detect the rank here. return None return det
[docs] def projective(self, p, q, point): r""" Return the ideal point with projective coordinates ``[p: q]`` in the upper half plane model. INPUT: - ``p`` -- an element of the :meth:`~HyperbolicGeometry.base_ring` - ``q`` -- an element of the :meth:`~HyperbolicGeometry.base_ring` - ``point`` -- the :meth:`HyperbolicPlane.point` to create points EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) The point ``[p: q]`` is the point at infinity if ``q`` is very small in comparison to ``p``:: sage: H.geometry.projective(1, 0, H.point) sage: H.geometry.projective(1e-8, 1e-16, H.point) sage: H.geometry.projective(1e-8, -1e-16, H.point) Even though ``q`` might be small, ``[p: q]`` is not the point at infinity if both coordinates are of similar size:: sage: H.geometry.projective(1e-16, 1e-16, H.point) 1.00000000000000 sage: H.geometry.projective(-1e-16, 1e-16, H.point) -1.00000000000000 """ if self._zero(p) and self._zero(q): try: pq = p / q except ZeroDivisionError: return point(0, 1, model="klein", check=False) try: qp = q / p except ZeroDivisionError: return point(0, 0, model="half_plane", check=False) if self._zero(qp): return point(0, 1, model="klein", check=False) return point(pq, 0, model="half_plane", check=False) return super().projective(p, q, point)
[docs] def change_ring(self, ring): r""" Return this geometry over ``ring``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry.change_ring(RR) is H.geometry True sage: H.geometry.change_ring(RDF) Epsilon geometry with ϵ=1e-06 over Real Double Field """ if ring.is_exact(): raise ValueError("cannot change_ring() to an exact ring") return HyperbolicEpsilonGeometry(ring, self._epsilon)
[docs] def __repr__(self): r""" Return a printable representation of this geometry. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(RR) sage: H.geometry Epsilon geometry with ϵ=1.00000000000000e-6 over Real Field with 53 bits of precision """ return f"Epsilon geometry with ϵ={self._epsilon} over {self._ring}"
[docs] class HyperbolicConvexSet(SageObject): r""" Base class for convex subsets of :class:`HyperbolicPlane`. .. NOTE:: Concrete subclasses should apply the following rules. There should only be a single type to describe a certain subset: normally, a certain subset, say a point, should only be described by a single class, namely :class:`HyperbolicPoint`. Of course, one could describe a point as a polygon delimited by some edges that all intersect in that single point, such objects should be avoided. Namely, the methods that create a subset, say :meth:`HyperbolicPlane.polygon` take care of this by calling a sets :meth:`HyperbolicConvexSet._normalize` to rewrite a set in its most natural representation. To get the denormalized representation, we can always set `check=False` when creating the object. For this to work, the `__init__` should not take care of any such normalization and accept any input that can possibly be made sense of. Comparison with ``==`` should mean "is essentially indistinguishable from": Implementing == to mean anything else would get us into trouble in the long run. In particular we cannot implement <= to mean "is subset of" since then an oriented and an unoriented geodesic would be `==`. So, objects of a different type should almost never be equal. A notable exception are objects that are indistinguishable to the end user but use different implementations: the starting point of the geodesic going from 0 to infinity, a :class:`HyperbolicPointFromGeodesic`, and the point with coordinates (0, 0) in the upper half plane model, a :class:`HyperbolicPointFromCoordinates`, are equal. Note that we also treat objects as equal that only differ in their exact representation such as the geodesic x = 1 and the geodesic 2x = 2. TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexSet sage: H = HyperbolicPlane() sage: isinstance(H(0), HyperbolicConvexSet) True """
[docs] def half_spaces(self): r""" Return a minimal set of half spaces whose intersection is this convex set. Iteration of the half spaces is in counterclockwise order, i.e., consistent with :meth:`HyperbolicHalfSpaces._lt_`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).left_half_space().half_spaces() {{x ≤ 0},} sage: H.vertical(0).half_spaces() {{x ≤ 0}, {x ≥ 0}} sage: H(0).half_spaces() {{(x^2 + y^2) + x ≤ 0}, {x ≥ 0}} """ raise NotImplementedError(f"{type(self)} does not implement half_spaces()")
def _test_half_spaces(self, **options): r""" Verify that this convex set implements :meth:`half_spaces` correctly. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_half_spaces() """ tester = self._tester(**options) try: half_spaces = self.half_spaces() except ValueError: # We cannot create half spaces defining a point which has no coordinates over the base ring. tester.assertIsInstance(self, HyperbolicPointFromGeodesic) return tester.assertEqual(self.parent().intersection(*half_spaces), self.unoriented()) tester.assertTrue(isinstance(half_spaces, HyperbolicHalfSpaces)) for a, b in zip(list(half_spaces), list(half_spaces)[1:]): tester.assertTrue(HyperbolicHalfSpaces._lt_(a, b)) def _check(self, require_normalized=True): r""" Validate this convex subset. Subclasses run specific checks here that can be disabled when creating objects with ``check=False``. INPUT: - ``require_normalized`` -- a boolean (default: ``True``); whether to include checks that assume that normalization has already happened EXAMPLES: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.point(0, 0, model="klein") sage: P._check() sage: P = H.point(1, 1, model="klein", check=False) sage: P._check() Traceback (most recent call last): ... ValueError: point (1, 1) is not in the unit disk in the Klein model """ pass
[docs] def _normalize(self): r""" Return this set possibly rewritten in a simpler form. This method is only relevant for sets created with ``check=False``. Such sets might have been created in a non-canonical way, e.g., when creating a :class:`HyperbolicOrientedSegment` whose start and end point are ideal, then this is actually a geodesic and it should be described as such. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: segment = H.segment(H.vertical(-1), start=H.infinity(), end=H.infinity(), check=False, assume_normalized=True) sage: segment {-x - 1 = 0} ∩ {x - 1 ≥ 0} ∩ {x - 1 ≤ 0} sage: segment._normalize() """ return self
def _test_normalize(self, **options): r""" Verify that normalization is idempotent. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: segment = H.segment(H.vertical(-1), start=H.infinity(), end=H.infinity(), check=False, assume_normalized=True) sage: segment._test_normalize() """ tester = self._tester(**options) normalization = self._normalize() tester.assertEqual(normalization, normalization._normalize())
[docs] def unoriented(self): r""" Return the non-oriented version of this set. Some sets such as geodesics and segments can have an explicit orientation. This method returns the underlying set without any explicit orientation. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).unoriented() {x = 0} """ return self.change(oriented=False)
def _test_unoriented(self, **options): r""" Verify that :meth:`unoriented` is implemented correctly. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_unoriented() """ tester = self._tester(**options) tester.assertEqual(self.unoriented(), self.unoriented().unoriented())
[docs] def intersection(self, other): r""" Return the intersection with the ``other`` convex set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).intersection(H.vertical(1)) ..SEEALSO:: :meth:`HyperbolicPlane.intersection` """ return self.parent().intersection(self, other)
[docs] def __contains__(self, point): r""" Return whether ``point`` is contained in this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(I) in H.empty_set() False sage: I in H.vertical(0) True sage: 2*I in H.half_circle(0, 1).left_half_space() True sage: I/2 in H.half_circle(0, 1).left_half_space() False .. NOTE:: There is currently no way to check whether a point is in the interior of a set. .. SEEALSO:: :meth:`HyperbolicConvexSet.is_subset` to check containment of arbitrary sets. """ for half_space in self.half_spaces(): if point not in half_space: return False return True
def _test_contains(self, **options): r""" Verify that :meth:`__contains__` is implemented correctly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0)._test_contains() """ tester = self._tester(**options) for vertex in self.vertices(): try: tester.assertIn(vertex, self) # codespell:ignore assertin except ValueError: # Currently, containment can often not be decided when points # do not have coordinates over the base ring. tester.assertIsInstance(vertex, HyperbolicPointFromGeodesic)
[docs] def vertices(self, marked_vertices=True): r""" Return the vertices bounding this hyperbolic set. This returns both finite and ideal vertices. INPUT: - ``marked_vertices`` -- a boolean (default: ``True``) whether to include marked vertices that are not actual cornerns of the convex set. OUTPUT: A set of points, namely :class:`HyperbolicVertices`. Iteration of this set happens incounterclockwise order (as seen from the inside of the convex set.) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() The empty set has no vertices:: sage: H.empty_set().vertices() {} A point is its own vertex:: sage: H(0).vertices() {0,} sage: H(I).vertices() {I,} sage: H(oo).vertices() {∞,} The vertices of a geodesic are its ideal end points:: sage: H.vertical(0).vertices() {0, ∞} The vertices of a half space are the ideal end points of its boundary geodesic:: sage: H.vertical(0).left_half_space().vertices() {0, ∞} The vertices a polygon can be finite and ideal:: sage: P = H.polygon([H.vertical(0).left_half_space(), H.half_circle(0, 1).left_half_space()]) sage: P.vertices() {-1, I, ∞} If a polygon has marked vertices they are included:: sage: P = H.polygon([H.vertical(0).left_half_space(), H.half_circle(0, 1).left_half_space()], marked_vertices=[2*I]) sage: P.vertices() {-1, I, 2*I, ∞} sage: P.vertices(marked_vertices=False) {-1, I, ∞} """ return ( self.parent() .polygon( self.half_spaces(), check=False, assume_sorted=True, assume_minimal=True ) .vertices() )
[docs] def is_finite(self): r""" Return whether all points in this set are finite. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().is_finite() True sage: H.vertical(0).is_finite() False sage: H.vertical(0).left_half_space().is_finite() False sage: H(I).segment(2*I).is_finite() True sage: H(0).segment(I).is_finite() False sage: P = H.polygon([ ....: H.vertical(-1).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ]) sage: P.is_finite() False sage: P = H.polygon([ ....: H.vertical(-1).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ]) sage: P.is_finite() True .. SEEALSO:: :meth:`is_ideal` and :meth:`is_ultra_ideal` for complementary notions """ return all( vertex.is_finite() for vertex in self.vertices(marked_vertices=False) )
[docs] def is_ideal(self): r""" Return whether all points in this set are ideal. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().is_ideal() True sage: H.vertical(0).is_ideal() False sage: H.vertical(0).start().is_ideal() True sage: H(I).is_ideal() False .. SEEALSO:: :meth:`is_finite` and :meth:`is_ultra_ideal` for complementary notions """ if self.dimension() >= 1: return False return all(vertex.is_ideal() for vertex in self.vertices(marked_vertices=False))
[docs] def is_ultra_ideal(self): r""" Return whether all points in this set are ultra-ideal, i.e., the correspond to points outside the Klein disk. Note that it is normally not possible to create ultra ideal sets (except for the actual empty set.) They only exist internally during geometric constructions in the Euclidean plane containing the Klein disk. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().is_ultra_ideal() True sage: H.vertical(0).is_ultra_ideal() False Normally, ultra-ideal objects are not permitted. They can often be created with the ``check=False`` keyword:: sage: H.point(2, 0, check=False, model="klein").is_ultra_ideal() True sage: H.geodesic(2, 0, 1, check=False, model="klein").is_ultra_ideal() True .. SEEALSO:: :meth:`is_finite` and :meth:`is_ultra_ideal` for complementary notions """ if self.is_empty(): return True if any( not vertex.is_ultra_ideal() for vertex in self.vertices(marked_vertices=False) ): return False raise NotImplementedError( f"{type(self)} does not implement is_ultra_ideal() yet" )
def _test_is_finite(self, **options): r""" Verify that :meth;`is_finite`, :meth:`is_ideal`, and :meth:`is_ultra_ideal` are implemented correctly for this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set()._test_is_finite() """ tester = self._tester(**options) finite = self.is_finite() ideal = self.is_ideal() ultra = self.is_ultra_ideal() if self.is_empty(): tester.assertTrue(finite) tester.assertTrue(ideal) tester.assertTrue(ultra) return if finite: tester.assertFalse(ideal) tester.assertFalse(ultra) if ideal: tester.assertFalse(finite) tester.assertFalse(ultra) if ultra: tester.assertFalse(finite) tester.assertFalse(ideal)
[docs] def change_ring(self, ring): r""" Return this set as an element of the hyperbolic plane over ``ring``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H(0) sage: q = p.change_ring(AA) sage: q.parent().base_ring() Algebraic Real Field Changing the base ring can provide coordinates for points:: sage: p = H.half_circle(0, 2).start() sage: p.coordinates() Traceback (most recent call last): ... ValueError: ... sage: q = p.change_ring(AA) sage: q.coordinates() (-1.414213562373095?, 0) Note that changing the ring only works in relatively trivial cases:: sage: q = HyperbolicPlane(AA).point(sqrt(2), 0, model="half_plane") sage: p = q.change_ring(QQ) Traceback (most recent call last): ... ValueError: ... Most other sets also support changing the base ring:: sage: g = H.half_circle(0, 2) sage: g.start().coordinates() Traceback (most recent call last): ... ValueError: ... sage: g.change_ring(AA).start().coordinates() (-1.414213562373095?, 0) .. SEEALSO:: :meth:`change` for a more general interface to changing properties of hyperbolic sets. :meth:`HyperbolicPlane.change_ring` for the hyperbolic plane that the resulting objects lives in. """ return self.change(ring=ring)
def _test_change_ring(self, **options): r""" Verify that this set implements :meth:`change_ring`. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_change_ring() """ tester = self._tester(**options) tester.assertEqual(self, self.change_ring(self.parent().base_ring()))
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this set. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the new set will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the new set. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness) whether the new set will be explicitly oriented. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: geodesic = H.geodesic(0, 1) We can change the base ring over which this set is defined:: sage: geodesic.change(ring=AA) {(x^2 + y^2) - x = 0} We can drop the explicit orientation of a set:: sage: unoriented = geodesic.change(oriented=False) sage: unoriented.is_oriented() False We can also take an unoriented set and pick an orientation:: sage: oriented = geodesic.change(oriented=True) sage: oriented.is_oriented() True .. SEEALSO:: :meth:`is_oriented` for oriented an unoriented sets. """ raise NotImplementedError(f"this {type(self)} does not implement change()")
def _test_change(self, **options): r""" Verify that the full interface of :meth:`change` has been implemented. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_change() """ tester = self._tester(**options) # The ring parameter is supported tester.assertEqual(self, self.change(ring=self.parent().base_ring())) # The geometry parameter is supported tester.assertEqual(self, self.change(geometry=self.parent().geometry)) # The oriented parameter is supported tester.assertEqual( self.change(oriented=False), self.change(oriented=False).change(oriented=False), ) if self != self.change(oriented=False): tester.assertEqual(self, self.change(oriented=True))
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of this subset. Consult the implementation in the subclasses for a list supported keyword arguments, in particular :meth:`HyperbolicConvexPolygon.plot`. INPUT: - ``model`` -- one of ``"half_plane"`` and ``"klein"`` (default: ``"half_plane"``) EXAMPLES: .. jupyter-execute:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).plot() # long time (.5s) ...Graphics object consisting of 1 graphics primitive """ raise NotImplementedError(f"this {type(self)} does not support plotting")
def _test_plot(self, **options): r""" Verify that this set implements :meth:`plot`. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_plot() """ from sage.all import Graphics tester = self._tester(**options) if self != self.parent().infinity(): tester.assertIsInstance(self.plot(), Graphics) tester.assertIsInstance(self.plot(model="half_plane"), Graphics) tester.assertIsInstance(self.plot(model="klein"), Graphics)
[docs] def apply_isometry(self, isometry, model="half_plane", on_right=False): r""" Return the image of this set under the ``isometry``. INPUT: - ``isometry`` -- a 2×2 matrix in `GL(2,\mathbb{R})`, if ``model`` is ``"half_plane"``, or a 3×3 matrix giving a similitude that preserves a quadratic form of type `(1, 2)`, if ``model`` is ``"klein"``. - ``model`` -- a string (default: ``"half_plane"``); either ``"half_plane"`` or ``"klein"`` - ``on_right`` -- a boolean (default: ``False``) whether to apply the right action. ALGORITHM: If ``model`` is ``"half_plane"``, the 2×2 matrix with entries `a, b, c, d` encodes a fractional linear transformation sending .. MATH:: z \mapsto \frac{az + b}{cz + d} if the determinant is positive, and .. MATH:: z \mapsto \frac{a\bar{z} + b}{a\bar{z} + d} if the determinant is negative. Note that these maps are invariant under scaling the matrix with a non-zero real. In any case, we convert the matrix to a corresponding 3×3 matrix, see :meth:`HyperbolicPlane._isometry_gl2_to_sim12` and apply the isometry in the Klein model. To apply an isometry in the Klein model, we lift objects to the hyperboloid model, apply the isometry given by the 3×3 matrix there, and then project to the Klein model again. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() The horizontal translation by 1 in the upper half plane model, a parabolic isometry:: sage: isometry = matrix([[1, 1], [0, 1]]) sage: H.vertical(0).apply_isometry(isometry) {-x + 1 = 0} The same isometry as an isometry of the hyperboloid model:: sage: isometry = matrix([[1, -1, 1], [1, 1/2, 1/2], [1, -1/2, 3/2]]) sage: H.vertical(0).apply_isometry(isometry, model="klein") {-x + 1 = 0} An elliptic isometry:: sage: isometry = matrix([[1, -1], [1, 1]]) sage: H.vertical(0).apply_isometry(isometry) {(x^2 + y^2) - 1 = 0} A hyperbolic isometry:: sage: isometry = matrix([[1, 0], [0, 1/2]]) sage: H.vertical(0).apply_isometry(isometry) {-x = 0} sage: H(I).apply_isometry(isometry) 2*I A reflection:: sage: isometry = matrix([[-1, 0], [0, 1]]) sage: H.vertical(0).apply_isometry(isometry) {x = 0} A glide reflection:: sage: isometry = matrix([[-1, 0], [0, 1/2]]) sage: H.vertical(0).apply_isometry(isometry) {x = 0} sage: H.vertical(1).apply_isometry(isometry) {x + 2 = 0} An isometry of the upper half plane must have non-zero determinant:: sage: isometry = matrix([[1, 0], [1, 0]]) sage: H.vertical(0).apply_isometry(isometry) Traceback (most recent call last): ... ValueError: matrix does not define an isometry An isometry of the Klein model, must preserve a quadratic form of type `(1, 2)`:: sage: isometry = matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) sage: H.vertical(0).apply_isometry(isometry, model="klein") Traceback (most recent call last): ... ValueError: matrix does not define an isometry Isometries can be applied to half spaces:: sage: isometry = matrix([[1, 1], [0, 1]]) sage: H.vertical(0).left_half_space().apply_isometry(isometry) {x - 1 ≤ 0} Isometries can be applied to points:: sage: H(I).apply_isometry(isometry) 1 + I Isometries can be applied to polygons:: sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ], marked_vertices=[I]) sage: P.apply_isometry(isometry) {(x^2 + y^2) - 2*x ≥ 0} ∩ {x - 2 ≤ 0} ∩ {(x^2 + y^2) - 2*x - 3 ≤ 0} ∩ {x ≥ 0} ∪ {1 + I} Isometries can be applied to segments:: sage: segment = H(I).segment(2*I) sage: segment.apply_isometry(isometry) {-x + 1 = 0} ∩ {2*(x^2 + y^2) - 3*x - 1 ≥ 0} ∩ {(x^2 + y^2) - 3*x - 2 ≤ 0} REFERENCES: - Svetlana Katok, "Fuchsian Groups", Chicago University Press, Section 1.3; for the isometries of the upper half plane. - James W. Cannon, William J. Floyd, Richard Kenyon, and Walter R. Parry, "Hyperbolic Geometry", Flavors of Geometry, MSRI Publications, Volume 31, 1997, Section 10; for the isometries as 3×3 matrices. """ if model == "half_plane": isometry = self.parent()._isometry_gl2_to_sim12(isometry) model = "klein" if model == "klein": if isometry.dimensions() != (3, 3): raise ValueError( "isometry in Klein model must be given as a 3×3 matrix" ) isometry = isometry.change_ring(self.parent().base_ring()) # Check that the matrix defines an isometry in the hyperboloid # model, see CFJK "Hyperbolic Geometry" Theorem 10.1 from sage.all import diagonal_matrix D = ( isometry.transpose() * diagonal_matrix(isometry.parent().base_ring(), [1, 1, -1]) * isometry ) # These checks should use a specialized predicate of the geometry # of this space so they are more robust over inexact rings. for i, row in enumerate(D): for j, entry in enumerate(row): if (i == j) == self.parent().geometry._zero(entry): raise ValueError("matrix does not define an isometry") if not self.parent().geometry._equal(D[0, 0], D[1, 1]): raise ValueError("matrix does not define an isometry") if not self.parent().geometry._equal(D[0, 0], -D[2, 2]): raise ValueError("matrix does not define an isometry") return self._apply_isometry_klein(isometry, on_right=on_right) raise NotImplementedError("cannot apply isometries in this model yet")
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the result of applying the ``isometry`` to this hyperbolic set. Helper methed for :meth:`apply_isometry`. Hyperbolic sets implement this method which is not implemented generically. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``); whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: isometry = matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) sage: H.empty_set()._apply_isometry_klein(isometry) {} """ raise NotImplementedError( f"{type(self)} does not implement _apply_isometry_klein() yet" ) def _acted_upon_(self, x, self_on_left): r""" Return the result of acting upon this set with ``x``. INPUT: - ``x`` -- an isometry encoded as a 2×2 matrix, see :meth:`apply_isometry` - ``self_on_left`` -- a boolean; whether this is the right action or the left action EXAMPLES: We apply the Möbius transformation that sends `z` to `(1 + 2z)/(3 + 4z)`:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H(I) sage: m = matrix([[1, 2], [3, 4]]) sage: m * p 11/25 + 2/25*I sage: p * m -7/5 + 1/5*I TESTS:: sage: m0 = matrix(2, [1, 2, 3, 4]) sage: m1 = matrix(2, [1, 1, 0, 1]) sage: p = HyperbolicPlane()(I + 1) sage: assert (m0 * m1) * p == m0 * (m1 * p) sage: assert p * (m0 * m1) == (p * m0) * m1 """ return self.apply_isometry(x, on_right=self_on_left)
[docs] def is_subset(self, other): r""" Return whether this set is a subset of ``other``. INPUT: - ``other`` -- another hyperbolic convex set EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(I).is_subset(H.vertical(0)) True sage: H.vertical(0).is_subset(H(I)) False """ if self.is_empty(): return True other = self.parent()(other) if self.dimension() > other.dimension(): return False if self.dimension() == 0: return self in other # Make sure that we do not get confused by marked vertices self = self.parent().intersection(*self.half_spaces()) return self.intersection(other) == self
def _test_is_subset(self, **options): r""" Verify that :meth:`is_subset` is implemented correctly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_is_subset() """ tester = self._tester(**options) tester.assertTrue(self.is_subset(self)) try: half_spaces = self.half_spaces() except ValueError: # We cannot determine half spaces for points whose coordinates are not over the base ring. tester.assertIsInstance(self, HyperbolicPointFromGeodesic) else: tester.assertTrue(self.is_subset(self.parent().intersection(*half_spaces))) def _an_element_(self): r""" Return a point of this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() The returned element can be a finite point:: sage: H.vertical(0).an_element() I But it can also be an infinite point:: sage: H(0).segment(I).an_element() 0 An exception is raised when there are no elements in this set:: sage: H.empty_set().an_element() Traceback (most recent call last): ... Exception: empty set has no points We get an element for geodesics without end points in the base ring, see :meth:`HyperbolicGeodesic._an_element_`:: sage: H.half_circle(0, 2).an_element() (0, 1/3) """ vertices = self.vertices() if not vertices: raise NotImplementedError( "cannot return points of this set yet because it has no vertices" ) return next(iter(vertices)) @classmethod def _enhance_plot(self, plot, model): r""" Modify the ``plot`` of this set to improve the resulting plot. Currently, this adds the unit circle to plots in the Klein disk model. EXAMPLES: .. jupyter-execute:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H(0) sage: plot = plot([]) sage: type(p)._enhance_plot(plot, model="half_plane") ...Graphics object consisting of 0 graphics primitives .. jupyter-execute:: sage: type(p)._enhance_plot(plot, model="klein") ...Graphics object consisting of 1 graphics primitive """ if model == "klein": from sage.all import circle plot = circle([0, 0], 1, fill=False, color="#d1d1d1", zorder=-1) + plot return plot
[docs] def is_empty(self): r""" Return whether this set is empty. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(0).is_empty() False sage: H.empty_set().is_empty() True """ return self.dimension() < 0
[docs] def __bool__(self): r""" Return whether this set is non-empty. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: bool(H(0)) True sage: bool(H.empty_set()) False .. SEEALSO: :meth:`HyperbolicConvexSet.is_empty` """ return not self.is_empty()
[docs] def dimension(self): r""" Return the dimension of this set. OUTPUT: An integer, one of -1, 0, 1, 2. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We treat the empty set as -1-dimensional:: sage: H.empty_set().dimension() -1 Points are zero-dimensional:: sage: H(0).dimension() 0 Geodesics and segments are one-dimensional:: sage: H.vertical(0).dimension() 1 sage: H(I).segment(2*I).dimension() 1 Polygons and half spaces are two dimensional:: sage: H.random_element("polygon").dimension() 2 sage: H.vertical(0).left_half_space().dimension() 2 """ raise NotImplementedError(f"{type(self)} does not implement dimension() yet")
def _test_dimension(self, **options): r""" Verify that :meth:`dimension` is implemented correctly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set()._test_dimension() """ tester = self._tester(**options) dimension = self.dimension() from sage.all import ZZ tester.assertEqual(dimension.parent(), ZZ) tester.assertIn(dimension, [-1, 0, 1, 2]) # codespell:ignore assertin tester.assertEqual(self.unoriented().dimension(), dimension) tester.assertEqual(self.is_point(), dimension == 0) tester.assertEqual(self.is_empty(), dimension == -1)
[docs] def is_point(self): r""" Return whether this set is a single point. EXAMPLES: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(0).is_point() True sage: H(I).is_point() True sage: H.empty_set().is_point() False sage: H.vertical(0).is_point() False .. SEEALSO:: :meth:`is_ideal` to check whether this is a finite or an infinite point """ return self.dimension() == 0
[docs] def is_oriented(self): r""" Return whether this is a set with an explicit orientation. Some sets come in two flavors. There are oriented geodesics and unoriented geodesics. There are oriented segments and unoriented segments. This method answers whether a set is of the oriented kind if there is a choice. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Normally, geodesics are oriented:: sage: g = H.vertical(0) sage: g.is_oriented() True sage: g.start() 0 sage: g.end() We can ask explicitly for an unoriented version:: sage: h = g.unoriented() sage: h.is_oriented() False sage: h.start() Traceback (most recent call last): ... AttributeError: ... has no attribute 'start'... Segments are oriented:: sage: s = H(I).segment(2*I) sage: s.is_oriented() True sage: s.start() I sage: s.end() 2*I We can ask explicitly for an unoriented segment:: sage: u = s.unoriented() sage: u.is_oriented() False sage: u.start() Traceback (most recent call last): ... AttributeError: ... has no attribute 'start'... Points are not oriented as there is no choice of orientation:: sage: H(0).is_oriented() False Half spaces are not oriented: sage: H.vertical(0).left_half_space().is_oriented() False .. SEEALSO:: :meth:`change` to pick an orientation on an unoriented set :meth:`HyperbolicHalfSpace.__neg__`, :meth:`HyperbolicOrientedGeodesic.__neg__`, :meth:`HyperbolicOrientedSegment.__neg__` i.e., the ``-`` operator, to invert the orientation of a set """ return isinstance(self, HyperbolicOrientedConvexSet)
def _test_is_oriented(self, **options): r""" Verify that this set implements :meth:`is_oriented` correctly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set()._test_is_oriented() """ tester = self._tester(**options) if self.is_oriented(): tester.assertNotEqual(self, -self) # Verify that neg inverts the orientation of the set tester.assertEqual(self, -(-self))
[docs] def edges(self, as_segments=False, marked_vertices=True): r""" Return the :class:`segments <HyperbolicOrientedSegment>` and :class:`geodesics <HyperbolicOrientedGeodesic>` that bound this set. INPUT: - ``as_segments`` -- a boolean (default: ``False``); whether to also return the geodesics as segments with ideal end points. - ``marked_vertices`` -- a boolean (default: ``True``); whether to report segments that start or end at redundant marked vertices or otherwise whether such marked vertices are completely ignored. OUTPUT: A set of segments and geodesics. Iteration through this set is in counterclockwise order with respect to the points of the set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() The edges of a polygon:: sage: P = H.intersection( ....: H.vertical(-1).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space()) sage: P.edges() {{-x + 1 = 0} ∩ {2*(x^2 + y^2) - 3*x - 1 ≥ 0}, {x + 1 = 0} ∩ {2*(x^2 + y^2) + 3*x - 1 ≥ 0}, {(x^2 + y^2) - 2 = 0} ∩ {(x^2 + y^2) + 3*x + 1 ≥ 0} ∩ {(x^2 + y^2) - 3*x + 1 ≥ 0}} The single edge of a half space: sage: H.vertical(0).left_half_space().edges() {{-x = 0},} A geodesic and a segment are bounded by two edges:: sage: H.vertical(0).edges() {{-x = 0}, {x = 0}} sage: H(I).segment(2*I).edges() {{-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0}, {x = 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0}} Lower dimensional objects have no edges: sage: H(I).edges() {} sage: H.empty_set().edges() {} """ edges = [] for v, w in self.vertices().pairs(): edge = v.segment(w) if v.is_ideal() and w.is_ideal(): if edge.right_half_space().is_subset(self): continue if as_segments: edge = self.parent().segment(edge, assume_normalized=True) assert isinstance(edge, HyperbolicSegment) edges.append(edge) return HyperbolicEdges(edges)
def _test_edges(self, **options): r""" Verify that this set implements :meth:`edges` correctly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set()._test_edges() """ tester = self._tester(**options) if not self.parent().base_ring().is_exact(): # Inexact base rings typically do not allow hashing of coordinates # which is needed for the set below. return edges = self.edges() endpoints = set( [edge.start() for edge in edges] + [edge.end() for edge in edges] ) if self.dimension() > 0: vertices = self.vertices() tester.assertEqual(endpoints, vertices)
[docs] def area(self, numerical=True): r""" Return the area of this convex set divided by 2π. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(QQ) sage: p = H.polygon([H.geodesic(0, 1).left_half_space(), ....: H.geodesic(1, Infinity).left_half_space(), ....: H.geodesic(Infinity, 0).left_half_space()]) sage: p.area() 0.5 sage: p.area(numerical=False) 1/2 :: sage: a = H.point(0, 1, model='half_plane') sage: b = H.point(1, 1, model='half_plane') sage: c = H.point(1, 2, model='half_plane') sage: d = H.point(0, 2, model='half_plane') sage: p = H.polygon([H.geodesic(a, b).left_half_space(), ....: H.geodesic(b, c).left_half_space(), ....: H.geodesic(c, d).left_half_space(), ....: H.geodesic(d, a).left_half_space()]) sage: p.area() 0.0696044872730639 Zero and one-dimensional objects have area zero:: sage: p = H(I) sage: p.area() 0.0 sage: p.area(numerical=False) 0 :: sage: g = H.geodesic(0, 1) sage: g.area() 0.0 sage: g.area(numerical=False) 0 A half space has infinite area:: sage: h = g.left_half_space() sage: h.area() inf sage: h.area(numerical=False) +Infinity """ from sage.all import QQ, Infinity if self.dimension() < 2: return 0.0 if numerical else QQ.zero() edges = [e.geodesic() for e in self.edges()] n = len(edges) if len(edges) <= 2 or any( edges[i].intersection(edges[(i + 1) % n]).is_empty() for i in range(n) ): return float("inf") if numerical else Infinity H = self.parent() return QQ((n - 2, 2)) - sum( (edges[(i + 1) % n]).angle(-edges[i], numerical=numerical) for i in range(n) )
[docs] def __hash__(self): r""" Return a hash value for this convex set. Specific sets should override this method. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: hash(H.empty_set()) 0 Note that has values of sets over different base rings might not be consistent:: sage: HyperbolicPlane(ZZ).half_circle(0, 2) == HyperbolicPlane(AA).half_circle(0, 2) True sage: hash(HyperbolicPlane(ZZ).half_circle(0, 2)) == hash(HyperbolicPlane(AA).half_circle(0, 2)) False Sets over inexact base rings are not be hashable (since their hash would not be compatible with the notion of equality):: sage: hash(HyperbolicPlane(RR).vertical(0)) Traceback (most recent call last): ... TypeError: cannot hash geodesic defined over inexact base ring """ raise NotImplementedError(f"the set {self} is not hashable")
def _test_hash(self, **options): r""" Verify that this set implements a good hash function. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.an_element()._test_plot() Nothing is tested for unhashable sets:: sage: H = HyperbolicPlane(RR) sage: H.an_element()._test_hash() """ tester = self._tester(**options) # We refuse to hash inexact elements. if not self.parent().base_ring().is_exact(): if not self.is_empty(): with tester.assertRaises(TypeError): hash(self) return tester.assertEqual(hash(self), hash(self)) # We test that the hash function is good enough to distinguish this set # from some generic sets. While, there will be hash collisions # eventually, they should not show up with such non-random sets for a # good hash function. for subset in self.parent().some_elements(): if subset != self: tester.assertNotEqual(hash(self), hash(subset))
[docs] def _isometry_conditions(self, other): r""" Return an iterable of primitive pairs that must map to each other in an isometry that maps this set to ``other``. Helper method for :meth:`HyperbolicPlane._isometry_conditions`. When determining an isometry that maps sets to each other, we reduce to an isometry that maps points or geodesics to each other. Here, we produce such more primitive objects that map to each other. Sometimes, this mapping is not unique, e.g., when mapping polygons to each other, we may rotate the vertices of the polygon. Therefore, this returns an iterator that produces the possible mappings of primitive objects. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.half_circle(0, 4).left_half_space()], ....: marked_vertices=[4*I]) sage: conditions = P._isometry_conditions(P) sage: list(conditions) [[({x ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {x ≥ 0}), (4*I, 4*I)], [({x ≥ 0}, {x ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), (4*I, 4*I)], [({x ≥ 0}, {x ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), (4*I, 4*I)], [({x ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {x ≥ 0}), (4*I, 4*I)]] """ raise NotImplementedError( f"this {type(self)} does not implement _isometry_conditions yet and cannot be used to compute an isometry" )
[docs] @classmethod def random_set(cls, parent): r""" Return a random convex set. Concrete hyperbolic classes should override this method to provide random sets. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` the set should live in EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Given a hyperbolic object, we can create another one of the same kind:: sage: p = H(I) sage: type(p).random_set(H) # random output 7 :: sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexPolygon sage: P = HyperbolicConvexPolygon.random_set(H) sage: P.dimension() 2 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ raise NotImplementedError(f"{cls} does not support producing random sets yet")
[docs] class HyperbolicOrientedConvexSet(HyperbolicConvexSet): r""" Base class for sets that have an explicit orientation. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicOrientedConvexSet sage: H = HyperbolicPlane() sage: isinstance(H(0), HyperbolicOrientedConvexSet) False sage: isinstance(H.vertical(0), HyperbolicOrientedConvexSet) True sage: isinstance(H.vertical(0).unoriented(), HyperbolicOrientedConvexSet) False .. SEEALSO:: :meth:`HyperbolicConvexSet.is_oriented` """
[docs] class HyperbolicConvexFacade(HyperbolicConvexSet, Parent): r""" A convex subset of the hyperbolic plane that is itself a parent. This is the base class for all hyperbolic convex sets that are not points. This class solves the problem that we want convex sets to be "elements" of the hyperbolic plane but at the same time, we want these sets to live as parents in the category framework of SageMath; so they have be a Parent with hyperbolic points as their Element class. SageMath provides the (not very frequently used and somewhat flaky) facade mechanism for such parents. Such sets being a facade, their points can be both their elements and the elements of the hyperbolic plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0) sage: p = H(0) sage: p in v True sage: p.parent() is H True sage: q = v.an_element() sage: q I sage: q.parent() is H True TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexFacade sage: isinstance(v, HyperbolicConvexFacade) True """
[docs] def __init__(self, parent, category=None): Parent.__init__(self, facade=parent, category=category)
[docs] def parent(self): r""" Return the hyperbolic plane this is a subset of. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0) sage: v.parent() Hyperbolic Plane over Rational Field """ return self.facade_for()[0]
def _element_constructor_(self, x): r""" Return ``x`` as a point of this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0) sage: v(0) 0 sage: v(I) I sage: v(oo) sage: v(2) Traceback (most recent call last): ... ValueError: point not contained in this set sage: 2 in v False """ x = self.parent()(x) if isinstance(x, HyperbolicPoint): if not self.__contains__(x): raise ValueError("point not contained in this set") return x
[docs] def base_ring(self): r""" Return the ring over which points of this set are defined. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0) sage: v.base_ring() Rational Field """ return self.parent().base_ring()
[docs] class HyperbolicHalfSpace(HyperbolicConvexFacade): r""" A closed half space of the hyperbolic plane. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing this half space. - ``geodesic`` -- the :class:`HyperbolicOrientedGeodesic` to whose left this half space lies. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.half_circle(0, 1).left_half_space() {(x^2 + y^2) - 1 ≥ 0} .. SEEALSO:: :meth:`HyperbolicPlane.half_space`, :meth:`HyperbolicOrientedGeodesic.left_half_space`, :meth:`HyperbolicOrientedGeodesic.right_half_space` for the most common ways to create half spaces. """
[docs] def __init__(self, parent, geodesic): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpace sage: H = HyperbolicPlane() sage: h = H.half_circle(0, 1).left_half_space() sage: isinstance(h, HyperbolicHalfSpace) True sage: TestSuite(h).run() """ super().__init__(parent) if not isinstance(geodesic, HyperbolicOrientedGeodesic): raise TypeError("geodesic must be an oriented geodesic") if not geodesic.parent() is parent: raise ValueError("geodesic must be in parent") self._geodesic = geodesic
[docs] def equation(self, model, normalization=None): r""" Return an inequality for this half space as a triple ``a``, ``b``, ``c`` such that: - if ``model`` is ``"half_plane"``, a point `x + iy` of the upper half plane is in the half space if it satisfies `a(x^2 + y^2) + bx + c \ge 0`. - if ``model`` is ``"klein"``, points `(x, y)` in the unit disk satisfy `a + bx + cy \ge 0`. Note that the output is not unique since the coefficients can be scaled by a positive scalar. INPUT: - ``model`` -- either ``"half_plane"`` or ``"klein"`` - ``normalization`` -- how to normalize the coefficients, see :meth:`HyperbolicGeodesic.equation` for details EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpace sage: H = HyperbolicPlane() sage: h = H.half_circle(0, 1).left_half_space() sage: h.equation(model="half_plane") (2, 0, -2) sage: H.half_space(2, 0, -2, model="half_plane") == h True sage: h.equation(model="klein") (0, 0, 2) sage: H.half_space(0, 0, 2, model="klein") == h True .. SEEALSO:: :meth:`HyperbolicPlane.half_space` to build a half space from the coefficients returned by this method. """ return self._geodesic.equation(model=model, normalization=normalization)
def _repr_(self): r""" Return a printable representation of this half space. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: S = H.half_circle(0, 1).right_half_space() sage: S {(x^2 + y^2) - 1 ≤ 0} sage: -S {(x^2 + y^2) - 1 ≥ 0} """ geodesic = repr(self.boundary()) cmp = "≥" if geodesic.startswith("{-"): cmp = "≤" geodesic = repr(-self.boundary()) return geodesic.replace("=", cmp)
[docs] def half_spaces(self): r""" Return the half spaces defining this half space, i.e., this half space itself. Implements :meth:`HyperbolicConvexSet.half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: S = H.vertical(0).left_half_space() sage: [S] == list(S.half_spaces()) True """ return HyperbolicHalfSpaces([self], assume_sorted=True)
[docs] def __neg__(self): r""" Return the closure of the complement of this half space. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: S = H.half_circle(0, 1).left_half_space() sage: S {(x^2 + y^2) - 1 ≥ 0} sage: -S {(x^2 + y^2) - 1 ≤ 0} """ return self._geodesic.right_half_space()
[docs] def boundary(self): r""" Return a geodesic on the boundary of this half space, oriented such that the half space is on its left. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: S = H.vertical(0).left_half_space() sage: S.boundary() {-x = 0} .. SEEALSO:: :meth:`HyperbolicOrientedGeodesic.left_half_space` to recover the half space from the oriented geodesic """ return self._geodesic
[docs] def __contains__(self, point): r""" Return whether ``point`` is contained in this half space. INPUT: - ``point`` -- a :class:`HyperbolicPoint` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: h = H.vertical(0).left_half_space() sage: I in h True sage: I - 1 in h True sage: I + 1 in h False sage: oo in h True We can also check containment of ideal endpoints of geodesics:: sage: g = H.half_circle(0, 2) sage: g.start() in h True sage: g.end() in h False :: sage: g = H.half_circle(-3, 2) sage: g.start() in h True sage: g.end() in h True :: sage: g = H.half_circle(3, 2) sage: g.start() in h False sage: g.end() in h False :: sage: h = H.half_circle(0, 2).left_half_space() sage: g = H.half_circle(0, 5) sage: g.start() in h True sage: g.start() in -h False .. NOTE:: The implementation is currently not very robust over inexact rings. .. SEEALSO:: :meth:`HyperbolicConvexSet.is_subset` to check containment of arbitrary sets. """ point = self.parent()(point) if not isinstance(point, HyperbolicPoint): raise TypeError("point must be a point in the hyperbolic plane") try: x, y = point.coordinates(model="klein") except ValueError: # The point does not have coordinates in the base ring in the Klein model. # It is the starting point of a geodesic. assert point.is_ideal() if not isinstance(point, HyperbolicPointFromGeodesic): raise NotImplementedError( "cannot decide whether this ideal point is contained in the half space yet" ) boundary = self.boundary() intersection = boundary._intersection(point._geodesic) if intersection is None: # The boundary of the half space and the geodesic defining the # point do not intersect in a single point (not even in an # ultra-ideal point,) i.e., they are parallel in the Klein model. return point._geodesic.an_element() in self if not intersection.is_finite(): # The intersection point between the geodesic defining the # point and the half space boundary is ideal or ultra-ideal. # Either the entire geodesic is in the half space or none of # it. return point._geodesic.an_element() in self # The intersection point between the geodesic defining the point # and the half space boundary is finite, i.e., inside the unit # circle in the Klein model. The segment between the starting point # of the geodesic and that intersection point is either completely # inside the half space or completely outside. ccw = boundary.ccw(point._geodesic) assert ccw != 0 return ccw < 0 a, b, c = self.equation(model="klein") # We should use a specialized predicate here to do something more # reasonable for points that are close to the boundary over inexact # rings. return self.parent().geometry._sgn(a + b * x + c * y) >= 0
[docs] def __eq__(self, other): r""" Return whether this set is indistinguishable from ``other``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: h = H.vertical(0).left_half_space() sage: h == H.vertical(0).left_half_space() True sage: h == H.vertical(0).right_half_space() False :: sage: h != H.vertical(0).left_half_space() False sage: h != H.vertical(0).right_half_space() True """ if not isinstance(other, HyperbolicHalfSpace): return False return self._geodesic == other._geodesic
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of this half space in the hyperbolic ``model``. See :meth:`HyperbolicConvexPolygon.plot` for the supported keyword arguments. INPUT: - ``model`` -- one of ``"half_plane"`` and ``"klein"`` (default: ``"half_plane"``) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: G = H.vertical(0).left_half_space().plot() In the half plane model, the half space is rendered as an infinite polygon:: sage: G = H.vertical(0).left_half_space().plot() sage: G[0] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='RAYTO', args=(0, 1)), CartesianPathPlotCommand(code='RAYTO', args=(-1, 0)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000))]) """ return ( self.parent() .polygon([self], check=False, assume_minimal=True) .plot(model=model, **kwds) )
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this half space. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the new half space will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the new half space. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); must be ``None`` or ``False`` since half spaces cannot have an explicit orientation. See :meth:`~HyperbolicConvexSet.is_oriented`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: h = H.vertical(0).left_half_space() We change the base ring over which this space is defined:: sage: h.change(ring=AA) {x ≤ 0} We cannot change the orientedness of a half space: sage: h.change(oriented=True) Traceback (most recent call last): ... NotImplementedError: half spaces cannot have an explicit orientation sage: h.change(oriented=False) {x ≤ 0} """ if ring is not None or geometry is not None: self = self._geodesic.change(ring=ring, geometry=geometry).left_half_space() if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): raise NotImplementedError("half spaces cannot have an explicit orientation") return self
[docs] def dimension(self): r""" Return the dimension of this half space, i.e., 2. This implements :meth:`HyperbolicConvexSet.dimension`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).left_half_space().dimension() 2 """ from sage.all import ZZ return ZZ(2)
[docs] def vertices(self, marked_vertices=True): r""" Return the vertices bounding this half space. INPUT: - ``marked_vertices`` -- a boolean (default: ``True``), ignored since a half space cannot have marked vertices. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() The vertices of a half space are the ideal end points of its boundary geodesic:: sage: H.vertical(0).left_half_space().vertices() {0, ∞} Note that iteration in the set is not consistent with the orientation of the half space (it is chosen such that the subset relation on vertices can be checked quickly):: sage: h = H.vertical(0).left_half_space() sage: list(h.vertices()) [0, ∞] sage: list((-h).vertices()) [0, ∞] Use :meth:`HyperbolicOrientedGeodesic.start` and :meth:`HyperbolicOrientedGeodesic.end` on the :meth:`boundary` to get the end points in an order consistent with the orientation:: sage: g = h.boundary() sage: g.start(), g.end() (0, ∞) sage: g = (-h).boundary() sage: g.start(), g.end() (∞, 0) Vertices can be computed even though they do not have coordinates over the :meth:`HyperbolicPlane.base_ring`:: sage: H.half_circle(0, 2).left_half_space().vertices() {-1.41421356237310, 1.41421356237310} .. SEEALSO:: :meth:`HyperbolicConvexSet.vertices` for more details. """ return self.boundary().vertices()
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the image of this half space under ``isometry``. Helper method for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``) whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: isometry = matrix([[1, -1, 1], [1, 1/2, 1/2], [1, -1/2, 3/2]]) sage: H.vertical(0).left_half_space()._apply_isometry_klein(isometry) {x - 1 ≤ 0} """ return self._geodesic.apply_isometry( isometry, model="klein", on_right=on_right ).left_half_space()
[docs] def __hash__(self): r""" Return a hash value for this half space EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Since half spaces are hashable, they can be put in a hash table, such as a Python ``set``:: sage: S = {H.vertical(0).left_half_space(), H.vertical(0).right_half_space()} sage: len(S) 2 """ # Add the type to the hash value to distinguish the hash value from an # actual geodesic. return hash((type(self), self._geodesic))
[docs] def _isometry_conditions(self, other): r""" Return an iterable of primitive pairs that must map to each other in an isometry that maps this set to ``other``. Helper method for :meth:`HyperbolicPlane._isometry_conditions`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0).left_half_space() sage: w = H.vertical(1).right_half_space() sage: conditions = v._isometry_conditions(w) sage: list(conditions) [[({-x = 0}, {x - 1 = 0})]] .. SEEALSO:: :meth:`HyperbolicConvexSet._isometry_conditions` for a general description. r""" yield [(self.boundary(), other.boundary())]
[docs] @classmethod def random_set(cls, parent): r""" Return a random half space. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the half plane EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpace sage: x = HyperbolicHalfSpace.random_set(H) sage: x.dimension() 2 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ return HyperbolicOrientedGeodesic.random_set(parent).left_half_space()
[docs] class HyperbolicGeodesic(HyperbolicConvexFacade): r""" A geodesic in the hyperbolic plane. This is the abstract base class of :class:`HyperbolicUnorientedGeodesic` and :class:`HyperbolicOrientedGeodesic`. ALGORITHM: Internally, we represent geodesics as a triple `a, b, c` such that they satisfy the equation .. MATH:: a + bx + cy = 0 in the Klein disk model. Note that due to this representation we can always compute intersection points of geodesics but we cannot always get the coordinates of the ideal end points of a geodesic (since we would have to take a square root to solve for the points on the unit circle.) It might be beneficial to store geodesics differently, see https://sagemath.zulipchat.com/#narrow/stream/271193-polygon/topic/hyperbolic.20geometry/near/284722650 for a discussion. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` this geodesic lives in - ``a`` -- an element of :meth:`HyperbolicPlane.base_ring` - ``b`` -- an element of :meth:`HyperbolicPlane.base_ring` - ``c`` -- an element of :meth:`HyperbolicPlane.base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.geodesic(1, 2, 3, model="klein") {2*(x^2 + y^2) + 2*x - 1 = 0} .. SEEALSO:: :meth:`HyperbolicPlane.geodesic` for various ways of constructing geodesics """
[docs] def __init__(self, parent, a, b, c): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicGeodesic sage: H = HyperbolicPlane() sage: geodesic = H.vertical(0) sage: isinstance(geodesic, HyperbolicGeodesic) True sage: TestSuite(geodesic).run() sage: isinstance(geodesic.unoriented(), HyperbolicGeodesic) True sage: TestSuite(geodesic.unoriented()).run() """ super().__init__(parent) if not isinstance(a, Element) or a.parent() is not parent.base_ring(): raise TypeError("a must be an element of the base ring") if not isinstance(b, Element) or b.parent() is not parent.base_ring(): raise TypeError("b must be an element of the base ring") if not isinstance(c, Element) or c.parent() is not parent.base_ring(): raise TypeError("c must be an element of the base ring") self._a = a self._b = b self._c = c
def _check(self, require_normalized=True): r""" Validate the equation defining this geodesic. Implements :meth:`HyperbolicConvexSet._check`. INPUT: - ``require_normalized`` -- a boolean (default: ``True``); ignored EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicGeodesic sage: H = HyperbolicPlane() sage: geodesic = H.vertical(0) sage: geodesic._check() sage: geodesic = H.geodesic(2, 0, 1, model="klein", check=False) sage: geodesic._check() Traceback (most recent call last): ... ValueError: equation 2 + (0)*x + (1)*y = 0 does not define a chord in the Klein model .. SEEALSO:: :meth:`is_ultra_ideal` to check whether a chord is completely outside the Klein disk :meth:`is_ideal` to check whether a chord touches the Klein disk """ if self.is_ultra_ideal() or self.is_ideal(): raise ValueError( f"equation {self._a} + ({self._b})*x + ({self._c})*y = 0 does not define a chord in the Klein model" )
[docs] def is_ideal(self): r""" Return whether all hyperbolic points of this geodesic are ideal, i.e., the defining equation of this geodesic in the Klein model only touches the Klein disk but does not intersect it. Note that it is normally not possible to create ideal geodesics. They only exist internally during constructions in the Euclidean plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).is_ideal() False sage: geodesic = H.geodesic(1, 0, 1, model="klein", check=False) sage: geodesic.is_ideal() True sage: geodesic = H.geodesic(2, 0, 1, model="klein", check=False) sage: geodesic.is_ideal() False .. NOTE:: The implementation of this predicate is not numerically robust over inexact rings. .. SEEALSO:: :meth:`is_ultra_ideal` to detect whether a geodesic does not even touch the Klein disk """ # We should probably use a specialized predicate of the geometry to # make this more robust over inexact rings. return self.parent().geometry._equal( self._b * self._b + self._c * self._c, self._a * self._a )
[docs] def is_ultra_ideal(self): r""" Return whether the line given by the defining equation is completely outside the Klein disk, i.e., all "points" of this geodesic are ultra-ideal. Note that it is normally not possible to create ultra-ideal geodesics. They only exist internally during constructions in the Euclidean plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).is_ultra_ideal() False sage: geodesic = H.geodesic(1, 0, 1, model="klein", check=False) sage: geodesic.is_ultra_ideal() False sage: geodesic = H.geodesic(2, 0, 1, model="klein", check=False) sage: geodesic.is_ultra_ideal() True .. NOTE:: The implementation of this predicate is not numerically robust over inexact rings. .. SEEALSO:: :meth:`is_ideal` to detect whether a geodesic touches the Klein disk """ # We should probably use a specialized predicate of the geometry to # make this more robust over inexact rings. return ( self.parent().geometry._cmp( self._b * self._b + self._c * self._c, self._a * self._a ) < 0 )
def _repr_(self, model=None): r""" Return a printable representation of this geodesic. INPUT: - ``model`` -- ``"half_plane"`` or ``"klein"`` (default: ``None`` to use ``"half_plane"`` if possible); in which model of hyperbolic geometry the equation is realized EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() If the geodesic is oriented, we print a defining equation of the geodesic such that when replacing the ``=`` with a ``≥``, we get an equation for the half space to the left of the geodesic:: sage: H.vertical(1) {-x + 1 = 0} sage: H.vertical(1).left_half_space() {x - 1 ≤ 0} Normally, geodesics are shown with their equation in the upper half plane model. We can also ask for an equation of the chord in the Klein disk:: sage: H.vertical(1)._repr_(model="klein") '{1 + -x - y = 0}' This representation is also chosen automatically for objects that do not have a representation in upper half plane such as ultra-ideal geodesics:: sage: H.geodesic(2, 0, 1, model="klein", check=False) {2 + y = 0} .. SEEALSO:: :meth:`equation` to access the coefficients of the equation defining the geodesic """ if model is None: model = ( "klein" if self.is_ultra_ideal() or self.is_ideal() else "half_plane" ) if model == "half_plane": # Convert to the upper half plane model as a(x^2 + y^2) + bx + c = 0. a, b, c = self.equation(model="half_plane", normalization=["gcd", None]) from sage.all import PolynomialRing R = PolynomialRing(self.parent().base_ring(), names="x") if self.parent().geometry._sgn(a) != 0: return ( f"{{{repr(R([0, a]))[:-1]}(x^2 + y^2){repr(R([c, b, 1]))[3:]} = 0}}" ) else: return f"{{{repr(R([c, b]))} = 0}}" if model == "klein": a, b, c = self.equation(model="klein", normalization=["gcd", None]) from sage.all import PolynomialRing R = PolynomialRing(self.parent().base_ring(), names=["x", "y"]) polynomial_part = R({(1, 0): b, (0, 1): c}) if self.parent().geometry._sgn(a) != 0: return f"{{{repr(a)} + {repr(polynomial_part)} = 0}}" else: return f"{{{repr(polynomial_part)} = 0}}" raise NotImplementedError("printing not supported in this model")
[docs] def equation(self, model, normalization=None): r""" Return an equation for this geodesic as a triple ``a``, ``b``, ``c`` such that: - if ``model`` is ``"half_plane"``, a point `x + iy` of the upper half plane is on the geodesic if it satisfies `a(x^2 + y^2) + bx + c = 0`. - if ``model`` is ``"klein"``, points `(x, y)` in the unit disk satisfy are on the geodesic if `a + bx + cy = 0`. INPUT: - ``model`` -- the model in which this equation holds, either ``"half_plane"`` or ``"klein"`` - ``normalization`` -- how to normalize the coefficients; the default ``None`` is not to normalize at all. Other options are ``gcd``, to divide the coefficients by their greatest common divisor, ``one``, to normalize the first non-zero coefficient to ±1. This can also be a list of such values which are then tried in order and exceptions are silently ignored unless they happen at the last option. If this geodesic :meth;`is_oriented`, then the sign of the coefficients is chosen to encode the orientation of this geodesic. The sign is such that the half plane obtained by replacing ``=`` with ``≥`` in above equationsis on the left of the geodesic. Note that the output might not uniquely describe the geodesic since the coefficients are only unique up to scaling. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0) sage: v.equation(model="half_plane") (0, -2, 0) sage: v.equation(model="half_plane", normalization="gcd") (0, -1, 0) sage: v.equation(model="klein") (0, -1, 0) Sometimes, the desired normalization might not be possible (a more realistic example would be exact-real coefficients):: sage: H = HyperbolicPlane(ZZ) sage: g = H.geodesic(2, 3, -4, model="half_plane") sage: g.equation(model="half_plane", normalization="one") Traceback (most recent call last): ... TypeError: ... In this case, we can ask for the best of several normalization:: sage: g.equation(model="half_plane", normalization=["one", "gcd", None]) (2, 3, -4) For ultra-ideal geodesics, the equation in the half plane model is not very useful:: sage: g = H.geodesic(2, 0, 1, model="klein", check=False) sage: g.equation(model="half_plane") # i.e., 3*(x^2 + y^2) + 1 = 0 (3, 0, 1) .. SEEALSO:: :meth:`HyperbolicPlane.geodesic` to create a geodesic from an equation """ normalization = normalization or [None] if isinstance(normalization, str): normalization = [normalization] from collections.abc import Sequence if not isinstance(normalization, Sequence): normalization = [normalization] normalization = list(normalization) normalization.reverse() a, b, c = self._a, self._b, self._c if model == "klein": a, b, c = a, b, c elif model == "half_plane": a, b, c = a + c, 2 * b, a - c else: raise NotImplementedError("cannot determine equation for this model yet") sgn = self.parent().geometry._sgn sgn = ( -1 if ( sgn(a) < 0 or (sgn(a) == 0 and b < 0) or (sgn(a) == 0 and sgn(b) == 0 and sgn(c) < 0) ) else 1 ) while normalization: strategy = normalization.pop() try: a, b, c = HyperbolicGeodesic._normalize_coefficients( a, b, c, strategy=strategy ) break except Exception: if not normalization: raise if not self.is_oriented(): a *= sgn b *= sgn c *= sgn return a, b, c
@classmethod def _normalize_coefficients(cls, a, b, c, strategy): r""" Return the normalized coefficients for the equation of a geodesic. Helper method for :meth:`equation`. INPUT: - ``a``, ``b``, ``c`` -- the coefficients of the geodesic equation - ``strategy`` -- one of ``"gcd"`` or ``"one"`` EXAMPLES:: sage: from flatsurf.geometry.hyperbolic import HyperbolicGeodesic Normalize the leading coefficient to be ±1:: sage: HyperbolicGeodesic._normalize_coefficients(QQ(6), -QQ(10), -QQ(12), strategy="one") (1, -5/3, -2) sage: HyperbolicGeodesic._normalize_coefficients(-QQ(6), QQ(10), QQ(12), strategy="one") (-1, 5/3, 2) Divide the coefficients by their GCD:: sage: HyperbolicGeodesic._normalize_coefficients(QQ(6), -QQ(10), -QQ(12), strategy="gcd") (3, -5, -6) sage: HyperbolicGeodesic._normalize_coefficients(-QQ(6), QQ(10), QQ(12), strategy="gcd") (-3, 5, 6) """ R = a.parent() if strategy is None: return a, b, c if strategy == "gcd": def gcd(*coefficients): coefficients = [c for c in coefficients if c] if len(coefficients) == 1: return coefficients[0] from sage.all import gcd return gcd(coefficients) d = gcd(a, b, c) if d < 0: d *= -1 return R(a / d), R(b / d), R(c / d) if strategy == "one": if a: d = a elif b: d = b else: assert c d = c if d < 0: d *= -1 return R(a / d), R(b / d), R(c / d) raise ValueError(f"unknown normalization {strategy}")
[docs] def half_spaces(self): r""" Return the two half spaces whose intersection this geodesic is. Implements :meth:`HyperbolicConvexSet.half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).half_spaces() {{x ≤ 0}, {x ≥ 0}} """ self = self.change(oriented=True) return HyperbolicHalfSpaces([self.left_half_space(), self.right_half_space()])
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of this geodesic in the hyperbolic ``model``. See :meth:`HyperbolicSegment.plot` for the supported keyword arguments. INPUT: - ``model`` -- one of ``"half_plane"`` and ``"klein"`` (default: ``"half_plane"``) EXAMPLES: .. jupyter-execute:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).plot() ...Graphics object consisting of 1 graphics primitive """ return ( self.parent() .segment( self.change(oriented=True), start=None, end=None, check=False, assume_normalized=True, ) .plot(model=model, **kwds) )
[docs] def pole(self): r""" Return the pole of this geodesic. ALGORITHM: The pole is the intersection of tangents of the Klein disk at the ideal endpoints of this geodesic, see `Wikipedia <https://en.wikipedia.org/wiki/Beltrami%E2%80%93Klein_model#Compass_and_straightedge_constructions>`. EXAMPLES: The pole of a geodesic is an ultra ideal point:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.vertical(2).pole(); p (1/2, 1) sage: p.is_ultra_ideal() True Computing the pole is only implemented if it is a finite point in the Euclidean plane:: sage: H.half_circle(0, 1).pole() Traceback (most recent call last): ... NotImplementedError: can only compute pole if geodesic is a not a diameter in the Klein model The pole might not be defined without passing to a larger base ring:: sage: H.half_circle(2, 2).pole() Traceback (most recent call last): ... ValueError: square root of 32 not in Rational Field """ if self.is_diameter(): raise NotImplementedError( "can only compute pole if geodesic is a not a diameter in the Klein model" ) def tangent(endpoint): x, y = endpoint.coordinates(model="klein") return self.parent().geodesic( -(x * x + y * y), x, y, model="klein", check=False ) A, B = self.vertices() pole = tangent(A)._intersection(tangent(B)) assert pole is not None, "non-parallel lines must intersect" return pole
[docs] def perpendicular(self, point_or_geodesic=None): r""" Return a geodesic that is perpendicular to this geodesic. If ``point_or_geodesic`` is a point, return a geodesic through that point. If ``point_or_geodesic`` is another geodesic, return a geodesic that is also perpendicular to that geodesic. ALGORITHM: We use the construction as explained on `Wikipedia <https://en.wikipedia.org/wiki/Beltrami%E2%80%93Klein_model#Compass_and_straightedge_constructions>`. INPUT: - ``point_or_geodesic`` -- a point or a geodesic in the hyperbolic plane or ``None`` (the default) EXAMPLES: Without parameters this method returns one of the many perpendicular geodesics:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(2) sage: v.perpendicular() {(x^2 + y^2) - 4*x - 1 = 0} We can request a perpendicular geodesic through a specific point:: sage: v.perpendicular(2 + I) {(x^2 + y^2) - 4*x + 3 = 0} sage: v.perpendicular(I) {(x^2 + y^2) - 4*x - 1 = 0} In some cases, such a geodesic might not exist:: sage: v.perpendicular(oo) Traceback (most recent call last): ... ValueError: ... does not define a chord in the Klein model We can request a geodesic that is also perpendicular to another geodesic:: sage: v.perpendicular(H.half_circle(4, 1)) {(x^2 + y^2) - 4*x + 1 = 0} In some cases, such a geodesic might not exist:: sage: v.perpendicular(H.half_circle(2, 1)) Traceback (most recent call last): ... ValueError: ... does not define a chord in the Klein model TESTS: Verify that this also works for geodesic that have no finite pole:: sage: H.vertical(0).perpendicular() {(x^2 + y^2) - 1 = 0} sage: H.half_circle(0, 1).perpendicular() {x = 0} sage: H.half_circle(0, 1).perpendicular(0) {x = 0} sage: H.half_circle(0, 1).perpendicular(2) {2*(x^2 + y^2) - 5*x + 2 = 0} sage: H.vertical(0).perpendicular(H.half_circle(0, 1)) Traceback (most recent call last): ... ValueError: no geodesic perpendicular to both {-x = 0} and {(x^2 + y^2) - 1 = 0} sage: H.half_circle(0, 1).perpendicular(H.vertical(0)) Traceback (most recent call last): ... ValueError: no geodesic perpendicular to both {(x^2 + y^2) - 1 = 0} and {-x = 0} sage: H.vertical(0).perpendicular(H.vertical(0)) {(x^2 + y^2) - 1 = 0} sage: H.vertical(0).perpendicular(H.half_circle(0, 1)) Traceback (most recent call last): ... ValueError: no geodesic perpendicular to both {-x = 0} and {(x^2 + y^2) - 1 = 0} .. NOTE:: Currently, the orientation of the returned geodesic is somewhat random. It should probably be counterclockwise to this geodesic. """ if point_or_geodesic is None: point_or_geodesic = self.an_element() point_or_geodesic = self.parent()(point_or_geodesic) if isinstance(point_or_geodesic, HyperbolicGeodesic): other = point_or_geodesic if self.unoriented() == other.unoriented(): return self.perpendicular(self.an_element()) if self.is_diameter() and other.is_diameter(): raise ValueError( f"no geodesic perpendicular to both {self} and {other}" ) if other.is_diameter(): return other.perpendicular(self) if self.is_diameter(): # Construct the line a + bx + cy = 0 perpendicular to the # diameter through the pole of the other geodesic. b, c = (-self._c, self._b) x, y = other.pole().coordinates(model="klein") a = -(b * x + c * y) # The line might be not intersect the Klein disk. An error is # raised here in that case. return self.parent().geodesic(a, b, c, model="klein", oriented=False) # In the generic case, the perpendicular goes through both poles. # Throws an error if that line does not define a geodesic because # it's outside of the Klein disk. return self.parent().geodesic(self.pole(), other.pole(), oriented=False) else: point = point_or_geodesic if self.is_diameter(): # Construct the line a + bx + cy = 0 perpendicular to the # diameter through the given point. b, c = (-self._c, self._b) x, y = point.coordinates(model="klein") a = -(b * x + c * y) perpendicular = self.parent().geodesic( a, b, c, model="klein", oriented=False ) else: perpendicular = self.parent().geodesic( self.pole(), point, oriented=False ) assert point in perpendicular return perpendicular
[docs] def midpoint(self): r""" Return the fixed point of the (determinant one) Möbius transformation that interchanges the ideal endpoints of this geodesic. ALGORITHM: For the vertical connecting zero and infinity, the Möbius transformation sending z to `-1/z` has the imaginary unit as its fixed point. For a half circle centered at the origin its point on the imaginary axis must be the fixed point (due to symmetry or a direct computation.) All other geodesics, are just translated versions of these so we can just conjugate with a translation to determine the fixed point, i.e., the fixed point is a translate of one of the above. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: H.vertical(0).midpoint() I sage: H.vertical(1).midpoint() 1 + I sage: H.half_circle(1, 1).midpoint() 1 + I .. SEEALSO:: :meth:`HyperbolicSegment.midpoint` """ a, b = self.vertices() if self.is_vertical(): if a == self.parent().infinity(): a, b = b, a x, _ = a.coordinates(model="half_plane") return self.parent().point(x, 1, model="half_plane") ax, _ = a.coordinates(model="half_plane") bx, _ = b.coordinates(model="half_plane") m = (ax + bx) / 2 r = abs(bx - ax) / 2 return self.parent().point(m, r, model="half_plane")
[docs] def is_diameter(self): r""" Return whether this geodesic is a diameter in the Klein model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).is_diameter() True sage: H.vertical(1).is_diameter() False """ return self.parent().point(0, 0, model="klein") in self
[docs] def is_vertical(self): r""" Return whether this geodesic is a vertical in the upper half plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).is_vertical() True sage: H.half_circle(0, 1).is_vertical() False """ return self.parent().infinity() in self
[docs] def __eq__(self, other): r""" Return whether this geodesic is identical to other up to (orientation preserving) scaling of the defining equation. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0) == H.vertical(0) True We distinguish oriented and unoriented geodesics:: sage: H.vertical(0).unoriented() == H.vertical(0) False sage: H.vertical(0).unoriented() != H.vertical(0) True We distinguish differently oriented geodesics:: sage: H.vertical(0) == -H.vertical(0) False sage: H.vertical(0) != -H.vertical(0) True We do, however, identify geodesics whose defining equations differ by some scaling:: sage: g = H.vertical(0) sage: g.equation(model="half_plane") (0, -2, 0) sage: h = H.geodesic(0, -4, 0, model="half_plane") sage: g.equation(model="half_plane") == h.equation(model="half_plane") False sage: g == h True sage: g != h False .. NOTE:: Over inexact rings, this method is not very reliable. To some extent this is inherent to the problem but also the implementation uses generic predicates instead of relying on a specialized implementation in the :class:`HyperbolicGeometry`. """ if type(self) is not type(other): return False other = self.parent()(other) # See note in the docstring. We should use specialized geometry here. equal = self.parent().geometry._equal sgn = self.parent().geometry._sgn if sgn(self._b): return ( (not self.is_oriented() or sgn(self._b) == sgn(other._b)) and equal(self._a * other._b, other._a * self._b) and equal(self._c * other._b, other._c * self._b) ) else: return ( (not self.is_oriented() or sgn(self._c) == sgn(other._c)) and equal(self._a * other._c, other._a * self._c) and equal(self._b * other._c, other._b * self._c) )
[docs] def __contains__(self, point): r""" Return whether ``point`` lies on this geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() :: sage: g = H.geodesic(20, -479, 858, model="half_plane") sage: g.start() in g True We can often decide containment for points coming from geodesics:: sage: g = H.geodesic(-1, -1/2) sage: h = H.geodesic(1, 1/2) sage: g.start() in g True sage: g.start() in h False sage: g.end() in g True sage: g.end() in h False .. NOTE:: The implementation is currently not very robust over inexact rings. """ point = self.parent()(point) if not isinstance(point, HyperbolicPoint): raise TypeError("point must be a point in the hyperbolic plane") if isinstance(point, HyperbolicPointFromGeodesic): # Short cut the most common case (that intersection cannot handle.) if point._geodesic.unoriented() == self.unoriented(): return True intersection = self._intersection(point._geodesic) if intersection is None: return False if intersection.is_ultra_ideal(): return False return intersection == point x, y = point.coordinates(model="klein") a, b, c = self.equation(model="klein") # We should use a specialized predicate from the geometry class here to # handle points that are close to the geodesic in a more robust way. return self.parent().geometry._zero(a + b * x + c * y)
[docs] def dimension(self): r""" Return the dimension of this set, i.e., 1. This implements :meth:`HyperbolicConvexSet.dimension`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).dimension() 1 """ from sage.all import ZZ return ZZ(1)
[docs] def change(self, *, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this geodesic. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the new geodesic will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the new geodesic. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); whether the new geodesic should be oriented. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(AA) The base ring over which this geodesic is defined can be changed:: sage: H.vertical(1).change_ring(QQ) {-x + 1 = 0} But we cannot change the base ring if the geodesic cannot be expressed in the smaller ring:: sage: H.vertical(AA(2).sqrt()).change(ring=QQ) Traceback (most recent call last): ... ValueError: Cannot coerce irrational Algebraic Real ... to Rational We can forget the orientation of a geodesic:: sage: v = H.vertical(0) sage: v.is_oriented() True sage: v = v.change(oriented=False) sage: v.is_oriented() False We can (somewhat randomly) pick the orientation of a geodesic:: sage: v = v.change(oriented=True) sage: v.is_oriented() True """ if ring is not None or geometry is not None: self = ( self.parent() .change_ring(ring, geometry=geometry) .geodesic( self._a, self._b, self._c, model="klein", check=False, oriented=self.is_oriented(), ) ) if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): self = self.parent().geodesic( self._a, self._b, self._c, model="klein", check=False, oriented=oriented ) return self
[docs] def geodesic(self): r""" Return the geodesic underlying this set, i.e., this geodesic itself. This method exists to unify the interface between segments and geodesics, see :meth:`HyperbolicSegment.geodesic`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).geodesic() == H.vertical(0) True """ return self
[docs] def __hash__(self): r""" Return a hash value for this geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Since oriented geodesics are hashable, they can be put in a hash table, such as a Python ``set``:: sage: S = {H.vertical(0), -H.vertical(0)} sage: len(S) 2 Same for unoriented geodesics:: sage: {H.vertical(0).unoriented(), (-H.vertical(0)).unoriented()} {{x = 0}} Oriented and unoriented geodesics are distinct and so is their hash value (typically):: sage: hash(H.vertical(0)) != hash(H.vertical(0).unoriented()) True We can also mix oriented and unoriented geodesics in hash tables:: sage: S = {H.vertical(0), -H.vertical(0), H.vertical(0).unoriented(), (-H.vertical(0)).unoriented()} sage: len(S) 3 """ if not self.parent().base_ring().is_exact(): raise TypeError("cannot hash geodesic defined over inexact base ring") return hash( (type(self), self.equation(model="klein", normalization=["one", "gcd"])) )
[docs] def _intersection(self, other): r""" Return the intersection of this geodesic and ``other``. Return ``None`` if they do not intersect in a point. INPUT: - ``other`` -- another :class:`HyperbolicGeodesic` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(AA) Geodesics can intersect in a finite point:: sage: H.vertical(0)._intersection(H.half_circle(0, 1)) I Geodesics can intersect in an ideal point:: sage: H.vertical(1)._intersection(H.half_circle(0, 1)) 1 Geodesics might intersect in an ultra ideal point:: sage: H.half_circle(0, 1)._intersection(H.half_circle(1, 8)) (-3, 0) Or they are parallel in the Klein model:: sage: H.half_circle(0, 1)._intersection(H.half_circle(0, 4)) Note that geodesics that overlap do not intersect in a point:: sage: H.vertical(0)._intersection(H.vertical(0)) sage: H.vertical(0)._intersection(-H.vertical(0)) TESTS:: sage: A = -H.vertical(0) sage: B = H.vertical(-1) sage: C = H.vertical(0) sage: A._intersection(B) sage: A._intersection(C) sage: B._intersection(A) sage: B._intersection(C) sage: C._intersection(A) sage: C._intersection(B) .. SEEALSO:: :meth:`HyperbolicConvexSet.intersection` for intersection with more general sets. :meth:`HyperbolicPlane.intersection` for the generic intersection of convex sets. """ if not isinstance(other, HyperbolicGeodesic): return super().intersection(other) xy = self.parent().geometry.intersection( (self._a, self._b, self._c), (other._a, other._b, other._c) ) if xy is None: return None return self.parent().point(*xy, model="klein", check=False)
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the result of applying the ``isometry`` to this geodesic. Helper methed for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``); whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We apply a reflection to a geodesic:: sage: isometry = matrix([[-1, 0, 0], [0, 1, 0], [0, 0, 1]]) sage: g = H.geodesic(-1, 1) sage: g._apply_isometry_klein(isometry) {(x^2 + y^2) - 1 = 0} Note how the reflection swaps the end points of the geodesic:: sage: g.start().apply_isometry(isometry, model="klein") 1 sage: g.end().apply_isometry(isometry, model="klein") -1 However, the isometry maps the oriented geodesic to itself since what's left of the geodesic is not changed:: sage: g._apply_isometry_klein(isometry) == g True An isometry that changes the orientation of the geodesic:: sage: isometry = matrix([[-1, 0, 0], [0, -1, 0], [0, 0, 1]]) sage: g._apply_isometry_klein(isometry) {-(x^2 + y^2) + 1 = 0} For an unoriented geodesic, the geodesic is unchanged though:: sage: g.unoriented()._apply_isometry_klein(isometry) == g.unoriented() True TESTS:: sage: p0 = H(0) sage: p1 = H(1) sage: p2 = H(oo) sage: for (a, b, c, d) in [(2, 1, 1, 1), (1, 1, 0, 1), (1, 0, 1, 1), (2, 0, 0 , 1)]: ....: for on_right in [True, False]: ....: m = matrix(2, [a, b, c, d]) ....: q0 = p0.apply_isometry(m, on_right=on_right) ....: q1 = p1.apply_isometry(m, on_right=on_right) ....: q2 = p2.apply_isometry(m, on_right=on_right) ....: assert H.geodesic(p0, p1).apply_isometry(m, on_right=on_right) == H.geodesic(q0, q1) ....: assert H.geodesic(p1, p0).apply_isometry(m, on_right=on_right) == H.geodesic(q1, q0) ....: assert H.geodesic(p1, p2).apply_isometry(m, on_right=on_right) == H.geodesic(q1, q2) ....: assert H.geodesic(p2, p1).apply_isometry(m, on_right=on_right) == H.geodesic(q2, q1) ....: assert H.geodesic(p2, p0).apply_isometry(m, on_right=on_right) == H.geodesic(q2, q0) ....: assert H.geodesic(p0, p2).apply_isometry(m, on_right=on_right) == H.geodesic(q0, q2) """ from sage.modules.free_module_element import vector if not on_right: isometry = isometry.inverse() b, c, a = ( vector(self.parent().base_ring(), [self._b, self._c, self._a]) * isometry ) return self.parent().geodesic( a, b, c, model="klein", oriented=self.is_oriented() ) def _isometry_equations(self, isometry, image, λ): r""" Return equations that must be satisfied if this set converts to ``image`` under ``isometry`` using ``λ`` as a free variable for the scaling factor. Helper method for :meth:`HyperbolicPlane._isometry_from_primitives`. INPUT: - ``isometry`` -- a 3×3 matrix describing a (right) isometry; typically not over the base ring but in symbolic variables - ``image`` -- a geodesic - ``λ`` -- a symbolic variable EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: R.<a, b, c, d, λ> = QQ[] sage: isometry = H._isometry_gl2_to_sim12(matrix([[a, b], [c, d]])) sage: isometry [ b*c + a*d a*c - b*d a*c + b*d] [ a*b - c*d 1/2*a^2 - 1/2*b^2 - 1/2*c^2 + 1/2*d^2 1/2*a^2 + 1/2*b^2 - 1/2*c^2 - 1/2*d^2] [ a*b + c*d 1/2*a^2 - 1/2*b^2 + 1/2*c^2 - 1/2*d^2 1/2*a^2 + 1/2*b^2 + 1/2*c^2 + 1/2*d^2] sage: H.vertical(0)._isometry_equations(isometry, H.vertical(1), λ) [-b*c - a*d + λ, -a*c + b*d + λ, -a*c - b*d - λ] """ R = λ.parent() a, b, c = self.equation(model="klein") fa, fb, fc = image.equation(model="klein") from sage.all import vector condition = vector((b, c, a)) * isometry - λ * vector(R, (fb, fc, fa)) return condition.list() def _an_element_(self): r""" Return a finite point on this geodesic. ALGORITHM: We take the chord in the Klein model that intersects this geodesic perpendicularly and passes through the origin. The point of intersection must be a finite point. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).an_element() I sage: H.half_circle(0, 2).an_element() (0, 1/3) """ other = self.parent().geodesic(0, -self._c, self._b, model="klein", check=False) cross = other._intersection(self) assert cross return cross
[docs] def vertices(self, marked_vertices=True): r""" Return the ideal end points of this geodesic. INPUT: - ``marked_vertices`` -- a boolean (default: ``True``), ignored since a geodesic cannot have marked vertices. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).vertices() {0, ∞} Note that iteration in the set is not consistent with the orientation of the geodesic (it is chosen such that the subset relation on vertices can be checked quickly):: sage: v = H.vertical(0) sage: list(v.vertices()) [0, ∞] sage: list((-v).vertices()) [0, ∞] Use :meth:`HyperbolicOrientedGeodesic.start` and :meth:`HyperbolicOrientedGeodesic.end` to get the end points in an order that is consistent with orientation:: sage: v.start(), v.end() (0, ∞) sage: (-v).start(), (-v).end() (∞, 0) The vertices can also be determined for an unoriented geodesic:: sage: v.unoriented().vertices() {0, ∞} Vertices can be computed even if they do not have coordinates over the :meth:`HyperbolicPlane.base_ring`:: sage: H.half_circle(0, 2).vertices() {-1.41421356237310, 1.41421356237310} .. NOTE:: The implementation of this method is not robust over inexact rings. .. SEEALSO:: :meth:`HyperbolicConvexSet.vertices` for more details. """ if not self.is_oriented(): self = self.change(oriented=True) return HyperbolicVertices([self.start(), self.end()])
[docs] class HyperbolicUnorientedGeodesic(HyperbolicGeodesic): r""" An unoriented geodesic in the hyperbolic plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicUnorientedGeodesic sage: H = HyperbolicPlane() sage: H.vertical(0).unoriented() {x = 0} TESTS:: sage: v = H.vertical(0).unoriented() sage: isinstance(v, HyperbolicUnorientedGeodesic) True sage: TestSuite(v).run() .. SEEALSO:: :meth:`HyperbolicPlane.geodesic` to create geodesics from points or equations """
[docs] def _isometry_conditions(self, other): r""" Return an iterable of primitive pairs that must map to each other in an isometry that maps this set to ``other``. Helper method for :meth:`HyperbolicPlane._isometry_conditions`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0).unoriented() sage: w = H.vertical(1).unoriented() sage: conditions = v._isometry_conditions(w) sage: list(conditions) [[({-x = 0}, {-x + 1 = 0})], [({-x = 0}, {x - 1 = 0})]] .. SEEALSO:: :meth:`HyperbolicConvexSet._isometry_conditions` for a general description. r""" self = self.change(oriented=True) other = other.change(oriented=True) yield [(self, other)] yield [(self, -other)]
[docs] @classmethod def random_set(cls, parent): r""" Return a random unoriented geodesic. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the geodesic EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicUnorientedGeodesic sage: x = HyperbolicUnorientedGeodesic.random_set(H) sage: x.dimension() 1 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ return HyperbolicOrientedGeodesic.random_set(parent).unoriented()
[docs] class HyperbolicOrientedGeodesic(HyperbolicGeodesic, HyperbolicOrientedConvexSet): r""" An oriented geodesic in the hyperbolic plane. Internally, we represent geodesics as the chords satisfying the equation .. MATH:: a + bx + cy = 0 in the unit disk of the Klein model. The geodesic is oriented such that the half space .. MATH:: a + bx + cy ≥ 0 is on its left. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` this geodesic lives in - ``a`` -- an element of :meth:`HyperbolicPlane.base_ring` - ``b`` -- an element of :meth:`HyperbolicPlane.base_ring` - ``c`` -- an element of :meth:`HyperbolicPlane.base_ring` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0) {-x = 0} sage: H.half_circle(0, 1) {(x^2 + y^2) - 1 = 0} sage: H.geodesic(H(I), 0) {x = 0} TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicOrientedGeodesic sage: g = H.vertical(0) sage: isinstance(g, HyperbolicOrientedGeodesic) True sage: TestSuite(g).run() .. SEEALSO:: :meth:`HyperbolicPlane.geodesic` for the most common ways to construct geodesics. :class:`HyperbolicUnorientedGeodesic` for geodesics without an explicit orientation and :class:`HyperbolicGeodesic` for shared functionality of all geodesics. """
[docs] def __neg__(self): r""" Return this geodesic with its orientation reversed. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: -H.vertical(0) {x = 0} """ return self.parent().geodesic( -self._a, -self._b, -self._c, model="klein", check=False )
[docs] def start(self): r""" Return the ideal starting point of this geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).start() 0 The coordinates of the end points of the half circle of radius `\sqrt{2}` around 0 can not be written down in the rationals:: sage: p = H.half_circle(0, 2).start() sage: p -1.41421356237310 sage: p.coordinates() Traceback (most recent call last): ... ValueError: square root of 32 ... Passing to a bigger field, the coordinates can be represented:: sage: K.<a> = QQ.extension(x^2 - 2, embedding=1.4) sage: H.half_circle(0, 2).change_ring(K).start() -a """ return self.parent().start(self)
[docs] def end(self): r""" Return the ideal end point of this geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).end() The coordinates of the end points of the half circle of radius `\sqrt{2}` around 0 can not be written down in the rationals:: sage: p = H.half_circle(0, 2).end() sage: p 1.41421356237310 sage: p.coordinates() Traceback (most recent call last): ... ValueError: square root of 32 ... Passing to a bigger field, the coordinates can be represented:: sage: K.<a> = QQ.extension(x^2 - 2, embedding=1.4) sage: H.half_circle(0, 2).change_ring(K).end() a """ return (-self).start()
[docs] def left_half_space(self): r""" Return the closed half space to the left of this (oriented) geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(AA) sage: H.vertical(0).left_half_space() {x ≤ 0} .. SEEALSO:: :meth:`right_half_space` for the corresponding half space on the other side :meth:`HyperbolicPlane.half_space` for another method to create half spaces. """ return self.parent().half_space(self._a, self._b, self._c, model="klein")
[docs] def right_half_space(self): r""" Return the closed half space to the right of this (oriented) geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(AA) sage: H.vertical(0).right_half_space() {x ≥ 0} .. SEEALSO:: :meth:`left_half_space` for the corresponding half space on the other side :meth:`HyperbolicPlane.half_space` for another method to create half spaces. """ return (-self).left_half_space()
def _configuration(self, other): r""" Return a classification of the (Euclidean) angle between this geodesic and ``other`` in the Klein model. This is a helper method for :meth:`HyperbolicConvexPolygon._normalize`. INPUT: - ``other`` -- another :class:`HyperbolicOrientedGeodesic` OUTPUT: A string explaining how the two geodesics are oriented. .. NOTE:: This check is not robust over inexact rings and should be improved for that use case. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Two geodesics can be equal:: sage: H.vertical(0)._configuration(H.vertical(0)) 'equal' They can be equal with reversed orientation:: sage: H.vertical(0)._configuration(-H.vertical(0)) 'negative' They can be parallel in the Klein model:: sage: H.vertical(0)._configuration(H.geodesic(1/2, 2)) 'parallel' They can be parallel but with reversed orientation:: sage: H.vertical(0)._configuration(H.geodesic(2, 1/2)) 'anti-parallel' Or they can intersect. We can distinguish the case that ``other`` crosses over from left-to-right or from right-to-left:: sage: H.vertical(0)._configuration(H.geodesic(1/3, 2)) 'concave' sage: H.vertical(0)._configuration(H.geodesic(1/2, 3)) 'convex' .. SEEALSO:: :meth:`~HyperbolicGeodesic._intersection` to compute the (ultra-ideal) intersection of geodesics """ if not isinstance(other, HyperbolicOrientedGeodesic): raise TypeError("other must be an oriented geodesic") intersection = self._intersection(other) if intersection is None: # We should use a specialized method of geometry here to make this # more robust over inexact rings. orientation = self.parent().geometry._sgn( self._b * other._b + self._c * other._c ) assert orientation != 0 if self == other: assert orientation > 0 return "equal" if self == -other: assert orientation < 0 return "negative" if orientation > 0: return "parallel" return "anti-parallel" tangent = (self._c, -self._b) orientation = (-tangent[0] * other._b - tangent[1] * other._c).sign() assert orientation != 0 # Probably convex and concave are not the best terms here. if orientation > 0: return "convex" # We probably would not need to consider the concave case if we always # packed all geodesics into a bounding box that contains the unit disk. return "concave"
[docs] def parametrize(self, point, model, check=True): r""" Return the value of ``point`` in a linear parametrization of this geodesic. INPUT: - ``point`` -- a :class:`HyperbolicPoint` on this geodesic - ``model`` -- a string; currently only ``"euclidean"`` is supported - ``check`` -- a boolean (default: ``True``); whether to ensure that ``point`` is actually a point on the geodesic EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We can parametrize points on a geodesic such that the order of the points corresponds to the parameters we get:: sage: g = H.vertical(0) sage: g.parametrize(0, model="euclidean") -1 sage: g.parametrize(I, model="euclidean") 0 sage: g.parametrize(2*I, model="euclidean") 3/5 sage: g.parametrize(oo, model="euclidean") 1 .. NOTE:: This method is not robust for points over inexact rings and should be improved. .. SEEALSO:: :meth:`unparametrize` for the recovers the point from the parameter """ if check: point = self.parent()(point) if point not in self: raise ValueError("point must be on geodesic to be parametrized") if model == "euclidean": base = self.an_element().coordinates(model="klein") tangent = (self._c, -self._b) # We should use a specialized predicate here to make this work # better over inexact rings. coordinate = 0 if not self.parent().geometry._zero(tangent[0]) else 1 return ( point.coordinates(model="klein")[coordinate] - base[coordinate] ) / tangent[coordinate] raise NotImplementedError("cannot parametrize a geodesic over this model yet")
[docs] def unparametrize(self, λ, model, check=True): r""" Return the point parametrized by ``λ`` on this geodesic. INPUT: - ``λ`` -- an element of :meth:`HyperbolicPlane.base_ring` - ``model`` -- a string; currently only ``"euclidean"`` is supported - ``check`` -- a boolean (default: ``True``); whether to ensure that the returned point is not ultra ideal EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() This method is the inverse of :meth;`parametrize`:: sage: g = H.vertical(0) sage: g.unparametrize(g.parametrize(0, model="euclidean"), model="euclidean") 0 sage: g.unparametrize(g.parametrize(I, model="euclidean"), model="euclidean") I sage: g.unparametrize(g.parametrize(2*I, model="euclidean"), model="euclidean") 2*I sage: g.unparametrize(g.parametrize(oo, model="euclidean"), model="euclidean") """ if model == "euclidean": base = self.an_element().coordinates(model="klein") tangent = (self._c, -self._b) λ = self.parent().base_ring()(λ) return self.parent().point( x=base[0] + λ * tangent[0], y=base[1] + λ * tangent[1], model="klein", check=check, ) raise NotImplementedError("cannot parametrize a geodesic over this model yet")
[docs] @classmethod def random_set(cls, parent): r""" Return a random oriented geodesic. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the geodesic EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicOrientedGeodesic sage: x = HyperbolicOrientedGeodesic.random_set(H) sage: x.dimension() 1 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ a = HyperbolicPointFromCoordinates.random_set(parent) b = a while b == a: b = HyperbolicPointFromCoordinates.random_set(parent) return parent.geodesic(a, b)
[docs] def ccw(self, other, euclidean=False): r""" Return +1, -1, or zero if the turn from this geodesic to ``other`` is counterclockwise, clockwise, or if the geodesics are parallel at their point of intersection, respectively. If this geodesic and ``other`` do not intersect, a ``ValueError`` is raised. INPUT: - ``other`` -- another geodesic intersecting this geodesic - ``euclidean`` -- a boolean (default: ``False``); if ``True``, the geodesics are treated as lines in the Euclidean plane coming from their representations in the Klein model, i.e., geodesics intersecting at a non-finite point are reporting their Euclidean ccw. Otherwise, geodesics intersecting at an ideal point have a ``ccw`` of zero, and geodesics intersecting at an ultra-ideal point, are producing a ``ValueError``. EXAMPLES: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: v = H.vertical(0) sage: g = H.half_circle(0, 2) sage: v.ccw(g) -1 sage: v.ccw(-g) 1 Two verticals meet intersect with an angle 0 at infinity:: sage: w = H.vertical(1) sage: v.ccw(w) 0 However, we can also get the way the chords are turning in the Klein model:: sage: v.ccw(w, euclidean=True) 1 Geodesics that do not intersect produce an error:: sage: g = H.half_circle(2, 1) sage: v.ccw(g) Traceback (most recent call last): ... ValueError: geodesics do not intersect However, these geodesics do intersect at an ultra-ideal point and we can get their orientation at that point:: sage: v.ccw(g, euclidean=True) 1 """ if not isinstance(other, HyperbolicOrientedGeodesic): raise NotImplementedError("can only compute ccw between oriented geodesics") other = self.parent()(other) intersection = self._intersection(other) if intersection is None: if self.unoriented() == other.unoriented(): return 0 raise ValueError("geodesics do not intersect") if intersection.is_ideal(): if not euclidean: return 0 if intersection.is_ultra_ideal(): if not euclidean: raise ValueError("geodesics do not intersect") sgn = self.parent().geometry._sgn from flatsurf.geometry.euclidean import ccw return sgn(ccw((self._c, -self._b), (other._c, -other._b)))
def _test_ccw(self, **options): r""" Verify that :meth:`ccw` has been implemented correctly EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0)._test_ccw() """ tester = self._tester(**options) for euclidean in [False, True]: tester.assertEqual(self.ccw(self, euclidean=euclidean), 0) tester.assertEqual(self.ccw(-self, euclidean=euclidean), 0) for other in self.parent().some_subsets(): if other.dimension() != 1: continue other = other.geodesic().change(oriented=True) intersection = self._intersection(other) if intersection is None: continue if intersection.is_ultra_ideal() and not euclidean: continue tester.assertEqual( self.ccw(other, euclidean=euclidean), -other.ccw(self, euclidean=euclidean), ) tester.assertEqual( self.ccw(-other, euclidean=euclidean), other.ccw(self, euclidean=euclidean), )
[docs] def angle(self, other, numerical=True): r""" Compute the angle between this geodesic and ``other`` divided by 2π, i.e., as a number in `[0, 1)`. If this geodesic and ``other`` do not intersect, a ``ValueError`` is raised. INPUT: - ``other`` -- a geodesic intersecting this geodesic in a finite or ideal point. - ``numerical`` -- a boolean (default: ``True``); whether a numerical approximation of the angle is returned or whether we attempt to render it as a rational number. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane(QQ) sage: g0 = H.geodesic(-1, 1) sage: g1 = H.geodesic(0, 2) sage: g2 = H.geodesic(1, 2) sage: g0.angle(g1, numerical=False) 1/6 sage: assert g0.angle(g1, numerical=False) == (-g0).angle(-g1, numerical=False) sage: assert g1.angle(g2, numerical=False) == (-g2).angle(-g1, numerical=False) sage: assert g0.angle(g1, numerical=False) + g1.angle(-g0, numerical=False) == 1/2 sage: assert g1.angle(g2, numerical=False) + g1.angle(-g2, numerical=False) == 1/2 :: sage: H.geodesic(0, 1).angle(H.geodesic(1, Infinity)) 0.5 sage: H.geodesic(0, 1).angle(H.geodesic(Infinity, 1)) 0.0 :: sage: m = matrix(2, [2, 1, 1, 1]) sage: g0.apply_isometry(m).angle(g1.apply_isometry(m), numerical=False) 1/6 :: sage: a = H.point(0, 1, model='half_plane') sage: b = H.point(1, 1, model='half_plane') sage: c = H.point(1, 2, model='half_plane') sage: H.geodesic(a, b).angle(H.geodesic(b, c)) 0.32379180882521663 sage: H.geodesic(b, a).angle(H.geodesic(c, b)) 0.32379180882521663 sage: H.geodesic(a, b).angle(H.geodesic(b, c)) + H.geodesic(b, c).angle(H.geodesic(b, a)) 0.5 :: sage: g3 = H.geodesic(2, 3) sage: g0.angle(g3) Traceback (most recent call last): ... ValueError: geodesics do not intersect """ if not isinstance(other, HyperbolicOrientedGeodesic): raise NotImplementedError( "can only compute angle between oriented geodesics" ) other = self.parent()(other) from sage.all import QQ ring = float if numerical else QQ ccw = self.ccw(other) if ccw == 0: if self.start() == other.end() or self.end() == other.start(): return ring(0.5) assert ( self.start() == other.start() or self.end() == other.end() ), "geodesics whose enclosed angle is zero, must have one ideal endpoint in common" return ring(0) a1 = self._a b1 = self._b c1 = self._c n1_squared = b1 * b1 + c1 * c1 - a1 * a1 a2 = other._a b2 = other._b c2 = other._c n2_squared = b2 * b2 + c2 * c2 - a2 * a2 import math n12 = math.sqrt(abs(n1_squared * n2_squared)) cos_angle = (b1 * b2 + c1 * c2 - a1 * a2) / n12 from flatsurf.geometry.euclidean import acos angle = acos(cos_angle, numerical=numerical) return angle if ccw > 0 else 1 - angle
def _test_angle(self, **options): r""" Verify that :meth:`angle` has been implemented correctly. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0)._test_angle() """ tester = self._tester(**options) tester.assertEqual(self.angle(self), 0) tester.assertEqual(self.angle(-self), 0.5) for other in self.parent().some_subsets(): if other.dimension() != 1: continue other = other.geodesic().change(oriented=True) intersection = self._intersection(other) if intersection is None: continue tester.assertAlmostEqual(self.angle(other), (1 - other.angle(self)) % 1) tester.assertAlmostEqual(self.angle(-other), (0.5 - other.angle(self)) % 1)
[docs] class HyperbolicPoint(HyperbolicConvexSet, Element): r""" A (possibly infinite or even ultra-ideal) point in the :class:`HyperbolicPlane`. This is the abstract base class providing shared functionality for :class:`HyperbolicPointFromCoordinates` and :class:`HyperbolicPointFromGeodesic`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A point with coordinates in the upper half plane:: sage: p = H(0) The same point, created as an endpoint of a geodesic:: sage: p = H.vertical(0).start() Another point on the same geodesic, a finite point:: sage: p = H(I) TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicPoint sage: isinstance(p, HyperbolicPoint) True sage: TestSuite(p).run() .. SEEALSO:: :meth:`HyperbolicPlane.point` for ways to create points """ def _check(self, require_normalized=True): r""" Verify that this is a (possibly infinite) point in the hyperbolic plane. Implements :meth:`HyperbolicConvexSet._check`. INPUT: - ``require_normalized`` -- a boolean (default: ``True``); ignored EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicGeodesic sage: H = HyperbolicPlane() sage: p = H(0) sage: p._check() sage: p = H.point(2, 0, model="klein", check=False) sage: p._check() Traceback (most recent call last): ... ValueError: point (2, 0) is not in the unit disk in the Klein model .. SEEALSO:: :meth:`is_ultra_ideal` to see whether a point is outside of the Klein disk """ if self.is_ultra_ideal(): raise ValueError( f"point {self.coordinates(model='klein')} is not in the unit disk in the Klein model" )
[docs] def is_ideal(self): r""" Return whether this point is at infinity. This implements :meth:`HyperbolicConvexSet.is_ideal`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicGeodesic sage: H = HyperbolicPlane() sage: H(0).is_ideal() True sage: H(I).is_ideal() False .. NOTE:: This check is not very robust over inexact rings and should be improved. .. SEEALSO:: :meth:`is_ultra_ideal`, :meth:`is_finite` """ x, y = self.coordinates(model="klein") # We should use a specialized predicate from the geometry here to be # more robust over inexact rings. return self.parent().geometry._cmp(x * x + y * y, 1) == 0
[docs] def is_ultra_ideal(self): r""" Return whether this point is ultra-ideal, i.e., outside of the Klein disk. This implements :meth;`HyperbolicConvexSet.is_ultra_ideal`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicGeodesic sage: H = HyperbolicPlane() sage: H(0).is_ultra_ideal() False sage: H(I).is_ultra_ideal() False sage: H.point(2, 0, model="klein", check=False).is_ultra_ideal() True .. NOTE:: This check is not very robust over inexact rings and should be improved. .. SEEALSO:: :meth:`is_ideal`, :meth:`is_finite` """ x, y = self.coordinates(model="klein") # We should use a specialized predicate from the geometry here to be # more robust over inexact rings. return self.parent().geometry._cmp(x * x + y * y, 1) > 0
[docs] def is_finite(self): r""" Return whether this point is finite. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(0).is_finite() False sage: H(I).is_finite() True sage: H.half_circle(0, 2).start().is_finite() False .. NOTE:: Currently, the implementation is not robust over inexact rings. .. SEEALSO:: :meth:`is_ideal`, :meth:`is_ultra_ideal` """ x, y = self.coordinates(model="klein") # We should use specialized predicate from the geometry implementation # here to make this more robust over inexact rings. return self.parent().geometry._cmp(x * x + y * y, 1) < 0
[docs] def half_spaces(self): r""" Return a set of half spaces whose intersection is this point. Implements :meth:`HyperbolicConvexSet.half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(I).half_spaces() {{(x^2 + y^2) + 2*x - 1 ≤ 0}, {x ≥ 0}, {(x^2 + y^2) - 1 ≥ 0}} sage: H(I + 1).half_spaces() {{x - 1 ≤ 0}, {(x^2 + y^2) - 3*x + 1 ≤ 0}, {(x^2 + y^2) - 2 ≥ 0}} sage: H.infinity().half_spaces() {{x ≤ 0}, {x - 1 ≥ 0}} sage: H(0).half_spaces() {{(x^2 + y^2) + x ≤ 0}, {x ≥ 0}} sage: H(-1).half_spaces() {{x + 1 ≤ 0}, {(x^2 + y^2) - 1 ≤ 0}} sage: H(1).half_spaces() {{(x^2 + y^2) - x ≤ 0}, {(x^2 + y^2) - 1 ≥ 0}} sage: H(2).half_spaces() {{2*(x^2 + y^2) - 3*x - 2 ≥ 0}, {3*(x^2 + y^2) - 7*x + 2 ≤ 0}} sage: H(-2).half_spaces() {{(x^2 + y^2) - x - 6 ≥ 0}, {2*(x^2 + y^2) + 3*x - 2 ≤ 0}} sage: H(1/2).half_spaces() {{6*(x^2 + y^2) - x - 1 ≤ 0}, {2*(x^2 + y^2) + 3*x - 2 ≥ 0}} sage: H(-1/2).half_spaces() {{2*(x^2 + y^2) + 7*x + 3 ≤ 0}, {2*(x^2 + y^2) - 3*x - 2 ≤ 0}} For ideal endpoints of geodesics that do not have coordinates over the base ring, we cannot produce defining half spaces since these would require equations over a quadratic extension as well:: sage: H.half_circle(0, 2).start().half_spaces() Traceback (most recent call last): ... ValueError: square root of 32 ... :: sage: H = HyperbolicPlane(RR) sage: H(I).half_spaces() {{(x^2 + y^2) + 2.00000000000000*x - 1.00000000000000 ≤ 0}, {x ≥ 0}, {(x^2 + y^2) - 1.00000000000000 ≥ 0}} sage: H(0).half_spaces() {{(x^2 + y^2) + x ≤ 0}, {x ≥ 0}} .. SEEALSO:: :meth:`HyperbolicPlane.intersection` and :meth:`HyperbolicPlane.polygon` to intersect half spaces """ x0, y0 = self.coordinates(model="klein") if self.is_finite(): return HyperbolicHalfSpaces( # x ≥ x0 [ self.parent().half_space(-x0, 1, 0, model="klein"), # y ≥ y0 self.parent().half_space(-y0, 0, 1, model="klein"), # x + y ≤ x0 + y0 self.parent().half_space(x0 + y0, -1, -1, model="klein"), ] ) else: return HyperbolicHalfSpaces( # left of the line from (0, 0) to this point [ self.parent().half_space(0, -y0, x0, model="klein"), # right of a line to this point with a starting point right of (0, 0) self.parent().half_space( -x0 * x0 - y0 * y0, y0 + x0, y0 - x0, model="klein" ), ] )
[docs] def coordinates(self, model="half_plane", ring=None): r""" Return coordinates of this point in ``ring``. If ``model`` is ``"half_plane"``, return coordinates in the upper half plane model. If ``model`` is ``"klein"``, return Euclidean coordinates in the Klein model. INPUT: - ``model`` -- either ``"half_plane"`` or ``"klein"`` (default: ``"half_plane"``) - ``ring`` -- a ring, ``"maybe"``, or ``None`` (default: ``None``); in which ring the coordinates should be returned. If ``None``, coordinates are returned in the :meth:`HyperbolicPlane.base_ring`. If ``"maybe"``, same as ``None`` but instead of throwing an exception if the coordinates do not exist in the base ring, ``None`` is returned. .. NOTE:: It would be good to add a ``"extension"`` mode here to automatically take a ring extension where the coordinates live. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(I).coordinates() (0, 1) sage: H(0).coordinates() (0, 0) The point at infinity has no coordinates in the upper half plane model:: sage: H(oo).coordinates() Traceback (most recent call last): ... ValueError: point has no coordinates in the upper half plane Some points have coordinates in the Klein model but not in the upper half plane model:: sage: p = H.point(1/2, 0, model="klein") sage: p.coordinates(model="klein") (1/2, 0) sage: p.coordinates() Traceback (most recent call last): ... ValueError: square root of 3/4 not in Rational Field sage: K.<a> = NumberField(x^2 - 3/4) sage: p.coordinates(ring=K) (1/2, a) Some points have no coordinates in either model unless we pass to a field extension:: sage: p = H.half_circle(0, 2).start() sage: p.coordinates(model="half_plane") Traceback (most recent call last): ... ValueError: ... sage: p.coordinates(model="klein") Traceback (most recent call last): ... ValueError: ... sage: K.<a> = QuadraticField(2) sage: p.coordinates(ring=K) (-a, 0) sage: p.coordinates(ring=K, model="klein") (-2/3*a, 1/3) """ coordinates = self._coordinates_klein(ring=ring) if coordinates is None: assert ring == "maybe" return coordinates if model == "klein": pass elif model == "half_plane": x, y = coordinates if self == self.parent().infinity() or self.is_ultra_ideal(): raise ValueError("point has no coordinates in the upper half plane") denominator = 1 - y if not self.is_finite(): return (x / denominator, self.parent().base_ring().zero()) square = 1 - x * x - y * y try: sqrt = square.sqrt() if sqrt not in x.parent(): raise ValueError(f"square root of {square} not in {x.parent()}") except ValueError: if ring == "maybe": return None raise coordinates = x / denominator, sqrt / denominator else: raise NotImplementedError("cannot determine coordinates in this model yet") assert coordinates is not None return coordinates
[docs] def real(self): r""" Return the real part of this point in the upper half plane model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H(I + 2) sage: p.real() 2 .. SEEALSO:: :meth:`imag` """ return self.coordinates(model="half_plane")[0]
[docs] def imag(self): r""" Return the imaginary part of this point in the upper half plane model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H(I + 2) sage: p.imag() 1 .. SEEALSO:: :meth:`real` """ return self.coordinates(model="half_plane")[1]
[docs] def segment(self, end): r""" Return the oriented segment from this point to ``end``. INPUT: - ``end`` -- another :class:`HyperbolicPoint` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(0).segment(I) {-x = 0} ∩ {(x^2 + y^2) - 1 ≤ 0} A geodesic is returned when both endpoints are ideal points:: sage: H(0).segment(1) == H.geodesic(0, 1) True .. SEEALSO:: :meth:`HyperbolicPlane.segment` for other ways to construct segments """ end = self.parent()(end) geodesic = self.parent().geodesic(self, end) if self.is_ideal() and end.is_ideal(): return geodesic return self.parent().segment( geodesic, start=self if self.is_finite() else None, end=end if end.is_finite() else None, assume_normalized=True, )
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of this subset. See :meth:`HyperbolicConvexPolygon.plot` for the supported keyword arguments. INPUT: - ``model`` -- one of ``"half_plane"`` and ``"klein"`` (default: ``"half_plane"``) EXAMPLES: .. jupyter-execute:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(I).plot() ...Graphics object consisting of 1 graphics primitive """ coordinates = self.coordinates(model=model, ring="maybe") if not coordinates: from sage.all import RR coordinates = self.coordinates(model=model, ring=RR) from sage.all import point # We need to wrap the coordinates into a list so they are not # interpreted as a list of complex numbers. plot = point([coordinates], **kwds) return self._enhance_plot(plot, model=model)
[docs] def dimension(self): r""" Return the dimension of this point, i.e., 0. This implements :meth:`HyperbolicConvexSet.dimension`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(0).dimension() 0 """ from sage.all import ZZ return ZZ.zero()
[docs] def vertices(self, marked_vertices=True): r""" Return the vertices of this point, i.e., this point. INPUT: - ``marked_vertices`` -- a boolean (default: ``True``), ignored since a point cannot have marked vertices. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(oo).vertices() {∞,} Vertices can be computed even if they do not have coordinates over the :meth:`HyperbolicPlane.base_ring`:: sage: H.half_circle(0, 2).start().vertices() {-1.41421356237310,} .. SEEALSO:: :meth:`HyperbolicConvexSet.vertices` for more details. """ return HyperbolicVertices([self])
[docs] def __hash__(self): r""" Return a hash value for this point. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Since points are hashable, they can be put in a hash table, such as a Python ``set``:: sage: S = {H(I), H(0)} sage: len(S) 2 :: sage: {H.half_circle(0, 1).start(), H.half_circle(-2, 1).end()} {-1} Also, endpoints of geodesics that have no coordinates in the base ring can be hashed, see :meth:`HyperbolicPointFromGeodesic.__hash__`:: sage: S = {H.half_circle(0, 2).start()} """ if not self.parent().base_ring().is_exact(): raise TypeError("cannot hash a point defined over inexact base ring") return hash(self.coordinates(ring=self.parent().base_ring(), model="klein"))
def _isometry_equations(self, isometry, image, λ): r""" Return equations that must be satisfied if this set converts to ``image`` under ``isometry`` using ``λ`` as a free variable for the scaling factor. Helper method for :meth:`HyperbolicPlane._isometry_from_primitives`. INPUT: - ``isometry`` -- a 3×3 matrix describing a (right) isometry; typically not over the base ring but in symbolic variables - ``image`` -- a point - ``λ`` -- a symbolic variable EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: R.<a, b, c, d, λ> = QQ[] sage: isometry = H._isometry_gl2_to_sim12(matrix([[a, b], [c, d]])) sage: isometry [ b*c + a*d a*c - b*d a*c + b*d] [ a*b - c*d 1/2*a^2 - 1/2*b^2 - 1/2*c^2 + 1/2*d^2 1/2*a^2 + 1/2*b^2 - 1/2*c^2 - 1/2*d^2] [ a*b + c*d 1/2*a^2 - 1/2*b^2 + 1/2*c^2 - 1/2*d^2 1/2*a^2 + 1/2*b^2 + 1/2*c^2 + 1/2*d^2] sage: H(I)._isometry_equations(isometry, H(2*I), λ) [-8/5*a*c - 2/5*b*d, -4/5*a^2 - 1/5*b^2 + 4/5*c^2 + 1/5*d^2, -4/5*a^2 - 1/5*b^2 - 4/5*c^2 - 1/5*d^2 + λ] """ R = λ.parent() x, y, z = (*self.coordinates(model="klein"), R.one()) fx, fy, fz = (*image.coordinates(model="klein"), R.one()) from sage.all import vector equations = λ * vector((x, y, z)) - isometry * vector(R, (fx, fy, fz)) return equations.list()
[docs] def __contains__(self, point): r""" Return whether the set comprised of this point contains ``point``, i.e., whether the points are equal. This implements :meth:`HyperbolicConvexSet.__contains__` without relying on :meth:`HyperbolicConvexSet.half_spaces` which can not be computed for points without coordinates in the :meth:`HyperbolicPlane.base_ring`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H(0) sage: q = H.half_circle(0, 2).start() sage: p in p True sage: p in q False sage: q in p False sage: q in q True """ return self == point
[docs] class HyperbolicPointFromCoordinates(HyperbolicPoint): r""" A :class:`HyperbolicPoint` with explicit coordinates in the Klein model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicPointFromCoordinates sage: H = HyperbolicPlane() sage: H.point(0, 0, model="klein") I Points with coordinates in the half plane model are also stored as points in the Klein model:: sage: p = H.point(0, 0, model="half_plane") sage: isinstance(p, HyperbolicPointFromCoordinates) True INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing this point - ``x`` -- an element of :meth:`HyperbolicPlane.base_ring` - ``y`` -- an element of :meth:`HyperbolicPlane.base_ring` .. SEEALSO:: Use :meth;`HyperbolicPlane.point` to create points from coordinates """
[docs] def __init__(self, parent, x, y): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.point(0, 0, model="klein") sage: TestSuite(p).run() """ super().__init__(parent) if x.parent() is not parent.base_ring(): raise TypeError("x must be an element of the base ring") if y.parent() is not parent.base_ring(): raise TypeError("y must be an element of the base ring") self._coordinates = (x, y)
def _coordinates_klein(self, ring): r""" Return the coordinates of this point in the Klein model. This is a helper method for :meth:`HyperbolicPoint.coordinates`. INPUT: - ``ring`` -- see :meth:`HyperbolicPoint.coordinates` for this parameter EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.point(0, 0, model="klein") sage: p._coordinates_klein(ring=None) (0, 0) """ x, y = self._coordinates from sage.categories.all import Rings if ring is None or ring == "maybe": pass elif ring in Rings(): x, y = ring(x), ring(y) else: raise NotImplementedError("cannot produce coordinates for this ring yet") return x, y def _richcmp_(self, other, op): r""" Return how this point compares to ``other`` with respect to the ``op`` operator. This is only implemented for the operators ``==`` and ``!=``. It returns whether two points are the same. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.infinity() == H.projective(1, 0) True TESTS: We can compare points even though their coordinates are only defined over a quadratic extension:: sage: H.half_circle(0, 2).start() == H.half_circle(0, 2).start() True sage: H.half_circle(0, 2).start() == H.half_circle(0, 2).end() False sage: H.half_circle(0, 2).start() == H(0) False sage: H.half_circle(0, 1).end() == H(1) True .. NOTE:: Over inexact rings, this method is not very reliable. To some extent this is inherent to the problem but also the implementation uses generic predicates instead of relying on a specialized implementation in the :class:`HyperbolicGeometry`. .. SEEALSO:: :meth:`HyperbolicConvexSet.__contains__` to check for containment of a point in a set """ from sage.structure.richcmp import op_EQ, op_NE if op == op_NE: return not self._richcmp_(other, op_EQ) if op == op_EQ: if not isinstance(other, HyperbolicPoint): return False if isinstance(other, HyperbolicPointFromGeodesic): return other == self # See note in the docstring. We should use specialized geometry # here to compare the coordinates simultaneously. return all( self.parent().geometry._equal(a, b) for (a, b) in zip( self.coordinates(model="klein"), other.coordinates(model="klein") ) ) return super()._richcmp_(other, op) def _repr_(self): r""" Return a printable representation of this point. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We represent points in the upper half plane model if possible:: sage: H.point(0, 0, model="klein") I For some points this is not possible without extending the coordinate ring. Then we show their coordinates in the Klein model:: sage: H.point(1/2, 0, model="klein") (1/2, 0) """ if self == self.parent().infinity(): return "∞" if self.is_ultra_ideal(): return repr(self.coordinates(model="klein")) coordinates = self.coordinates(model="half_plane", ring="maybe") if coordinates is None: return repr(self.coordinates(model="klein")) from sage.all import PowerSeriesRing # We represent x + y*I in R[[I]] so we do not have to reimplement printing ourselves. return repr( PowerSeriesRing(self.parent().base_ring(), names="I")(list(coordinates)) )
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this point. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the point will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the point. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); must be ``None`` or ``False`` since points cannot have an orientation. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We change the base ring over which the point is defined:: sage: p = H(0) sage: p.change(ring=AA) 0 We cannot make a point oriented:: sage: p.change(oriented=True) Traceback (most recent call last): ... NotImplementedError: cannot make a point oriented sage: p.change(oriented=False) == p True """ def point(parent): return parent.point(*self._coordinates, model="klein", check=False) if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): raise NotImplementedError("cannot make a point oriented") if ring is not None or geometry is not None: self = ( self.parent() .change_ring(ring, geometry=geometry) .point(*self._coordinates, model="klein", check=False) ) return self
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the result of applying the ``isometry`` to this hyperbolic point. Helper method for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``) whether to return the result of the right action. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: for (a, b, c, d) in [(2, 1, 1, 1), (1, 1, 0, 1), (1, 0, 1, 1), (2, 0, 0 , 1)]: ....: m = matrix(2, [a, b, c, d]) ....: assert H(0).apply_isometry(m) == H(b / d if d else oo) ....: assert H(1).apply_isometry(m) == H((a + b) / (c + d) if c+d else oo) ....: assert H(oo).apply_isometry(m) == H(a / c if c else oo) """ from sage.modules.free_module_element import vector x, y = self.coordinates(model="klein") if on_right: isometry = isometry.inverse() x, y, z = isometry * vector(self.parent().base_ring(), [x, y, 1]) return self.parent().point(x / z, y / z, model="klein")
[docs] @classmethod def random_set(cls, parent): r""" Return a random hyperbolic point. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the point EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicPointFromCoordinates sage: x = HyperbolicPointFromCoordinates.random_set(H) sage: x.dimension() 0 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ return parent.point( parent.base_ring().random_element(), parent.base_ring().random_element().abs(), model="half_plane", check=False, )
[docs] class HyperbolicPointFromGeodesic(HyperbolicPoint): r""" An ideal :class:`HyperbolicPoint`, the end point of a geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).start() 0 This class is necessary because not all points have coordinates in the :meth:`HyperbolicPlane.base_ring`:: sage: p = H.half_circle(0, 2).start() sage: p.coordinates(model="klein") Traceback (most recent call last): ... ValueError: ... sage: p.coordinates(model="half_plane") Traceback (most recent call last): ... ValueError: ... INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing this point - ``geodesic`` -- to :class:`HyperbolicOrientedGeodesic` whose :meth:`HyperbolicOrientedGeodesic.start` this point is .. SEEALSO:: Use :meth:`HyperbolicOrientedGeodesic.start` and :meth:`HyperbolicOrientedGeodesic.end` to create endpoints of geodesics """
[docs] def __init__(self, parent, geodesic): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicPointFromGeodesic sage: H = HyperbolicPlane() sage: p = H.half_circle(0, 2).start() sage: isinstance(p, HyperbolicPointFromGeodesic) True sage: TestSuite(p).run() """ super().__init__(parent) if not isinstance(geodesic, HyperbolicOrientedGeodesic): raise TypeError("x must be an oriented geodesic") self._geodesic = geodesic
[docs] def is_ideal(self): r""" Return whether this is an infinite point, i.e., return ``True`` since this is an endpoint of a geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).start().is_ideal() True """ return True
[docs] def is_ultra_ideal(self): r""" Return whether this is an ultra ideal point, i.e., return ``False`` since this end point of a geodesic is not outside of the unit disk in the Klein model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).start().is_ultra_ideal() False """ return False
[docs] def is_finite(self): r""" Return whether this is a finite point, i.e., return ``False`` since this end point of a geodesic is infinite. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.half_circle(0, 2).start().is_finite() False """ return False
@cached_method def _coordinates_klein(self, ring): r""" Return the coordinates of this point in the Klein model. This is a helper method for :meth:`HyperbolicPoint.coordinates`. INPUT: - ``ring`` -- see :meth:`HyperbolicPoint.coordinates` for this parameter EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: p = H.point(0, 0, model="klein") sage: p._coordinates_klein(ring=None) (0, 0) Since the coordinates are given by intersection a line with the unit circle, they might only exist over a quadratic extension:: sage: p = H.half_circle(0, 2).start() sage: p._coordinates_klein(ring=None) Traceback (most recent call last): ... ValueError: ... sage: K.<a> = QuadraticField(2) sage: p._coordinates_klein(ring=K) (-2/3*a, 1/3) .. NOTE:: The implementation of this predicate is not numerically robust over inexact rings and should be improved. """ a, b, c = self._geodesic.equation(model="half_plane") # We should probably use a specialized predicate of the geometry to make this # more robust over inexact rings. if self.parent().geometry._zero(a): point = None # We should probably use a specialized predicate of the geometry to make this # more robust over inexact rings. if self.parent().geometry._sgn(b) > 0: point = self.parent().point(0, 1, model="klein", check=False) else: point = self.parent().point(-c / b, 0, model="half_plane", check=False) return point.coordinates(model="klein", ring=ring) else: discriminant = b * b - 4 * a * c if ring is None or ring == "maybe": sqrt_ring = self.parent().base_ring() else: sqrt_ring = ring discriminant = sqrt_ring(discriminant) try: sqrt = discriminant.sqrt() if sqrt not in sqrt_ring: raise ValueError( f"square root of {discriminant} not in {sqrt_ring}" ) except ValueError: if ring == "maybe": return None raise endpoints = ((-b - sqrt) / (2 * a), (-b + sqrt) / (2 * a)) return ( self.parent() .change_ring(sqrt_ring) .point( # We should probably use a specialized predicate of the geometry to make this # more robust over inexact rings. (min if self.parent().geometry._sgn(a) > 0 else max)(endpoints), 0, model="half_plane", check=False, ) .coordinates(model="klein", ring=ring) ) def _richcmp_(self, other, op): r""" Return how this point compares to ``other`` with respect to the ``op`` operator. This is only implemented for the operators ``==`` and ``!=``. It returns whether two points are the same. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.infinity() == H.projective(1, 0) True TESTS: We can compare points even though their coordinates are only defined over a quadratic extension:: sage: H.half_circle(0, 2).start() == H.half_circle(0, 2).start() True sage: H.half_circle(0, 2).start() == H.half_circle(0, 2).end() False sage: H.half_circle(0, 2).start() == H(0) False sage: H.half_circle(0, 1).end() == H(1) True .. NOTE:: Over inexact rings, this method is not very reliable. To some extent this is inherent to the problem but also the implementation uses generic predicates instead of relying on a specialized implementation in the :class:`HyperbolicGeometry`. For points that are not defined by coordinates but merely as the starting points of a hyperbolic geodesic, this is probably not implemented to the full extent possible. Instead, this will often throw a ``ValueError`` even though we could say something about the equality of the points. """ from sage.structure.richcmp import op_EQ, op_NE if op == op_NE: return not self._richcmp_(other, op_EQ) if op == op_EQ: if not isinstance(other, HyperbolicPoint): return False if isinstance(other, HyperbolicPointFromGeodesic): if self._geodesic == other._geodesic: return True if self._geodesic == -other._geodesic: return False # This is probably too complicated. If we can compute the # intersection, then it should have coordinates in the base # ring. intersection = self._geodesic._intersection(other._geodesic) return self == intersection and other == intersection if not other.is_ideal(): return False # We should probably be a little bit more careful over inexact # rings here. return ( other in self._geodesic and self._geodesic.parametrize(other, model="euclidean") < 0 ) return super()._richcmp_(other, op) def _repr_(self): """ Return a printable representation of this point. EXAMPLES: We try to represent this point in the upper half plane:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).start() 0 When this is not possible, we show approximate coordinates:: sage: H.half_circle(0, 2).start() -1.41421356237310 TESTS:: sage: geodesic = H.geodesic(733833/5522174119, -242010/5522174119, -105111/788882017, model="klein") sage: geodesic.start() 3.03625883227966 """ if self == self.parent().infinity(): return "∞" if not self.is_ultra_ideal(): coordinates = self.coordinates(model="half_plane", ring="maybe") if coordinates is not None: return repr( self.parent().point(*coordinates, model="half_plane", check=False) ) from sage.all import RR return repr(self.change_ring(RR))
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this point. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the point will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the point. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); must be ``None`` or ``False`` since points cannot have an orientation. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We change the base ring over which the point is defined:: sage: p = H.half_circle(0, 2).start() sage: p.change(ring=AA) -1.414213562373095? We cannot make a point oriented:: sage: p.change(oriented=True) Traceback (most recent call last): ... NotImplementedError: cannot change orientation of a point sage: p.change(oriented=False) == p True """ if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): raise NotImplementedError("cannot change orientation of a point") if ring is not None or geometry is not None: self = self._geodesic.change(ring=ring, geometry=geometry).start() return self
[docs] def __hash__(self): r""" Return a hash value for this point. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Points that are given as the endpoints of a geodesic may or may not have coordinates over the base ring:: sage: H.half_circle(0, 1).start().coordinates(model="klein") (-1, 0) sage: H.half_circle(0, 2).start().coordinates(model="klein") Traceback (most recent call last): ... ValueError: square root of 32 not in Rational Field While they always have coordinates in a quadratic extension, the hash of the coordinates in the extension might not be consistent with has values in the base ring, so we cannot simply hash the coordinates over some field extension:: sage: hash(QQ(1/2)) == hash(AA(1/2)) False To obtain consistent hash values for sets over the same base ring, at least if that base ring is a field, we observe that a point whose coordinates are not in the base ring cannot be the starting point of two different geodesics with an equation in the base ring. Indeed, for it otherwise had coordinates in the base ring as it were the intersection of these two geodesics and whence a solution to a linear equation with coefficients in the base ring. So, for points that have no coordinates in the base ring, we can hash the equation of the oriented geodesic to obtain a hash value:: sage: hash(H.half_circle(0, 2).start()) != hash(H.half_circle(0, 2).end()) True """ if self.coordinates(model="klein", ring="maybe") is not None: return super().__hash__() from sage.categories.all import Fields if self.parent().base_ring() not in Fields(): raise NotImplementedError( "cannot hash point defined by a geodesic over a non-field yet" ) return hash((type(self), self._geodesic))
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the result of applying the ``isometry`` to this hyperbolic point. Helper method for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``) whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: point = H.half_circle(0, 2).start() sage: point -1.41421356237310 We apply an isometry of positive determinant:: sage: isometry = matrix([[1, -1, 1], [1, 1/2, 1/2], [1, -1/2, 3/2]]) sage: point._apply_isometry_klein(isometry) -0.414213562373095 We apply an isometry of negative determinant:: sage: isometry = matrix([[-1, 0, 0], [0, 1, 0], [0, 0, 1]]) sage: point._apply_isometry_klein(isometry) 1.41421356237310 """ image = self._geodesic.apply_isometry( isometry, model="klein", on_right=on_right ) if isometry.det().sign() == -1: # An isometry of negative determinant swaps the end points of the # geodesic image = -image return image.start()
[docs] class HyperbolicConvexPolygon(HyperbolicConvexFacade): r""" A (possibly unbounded) closed polygon in the :class:`HyperbolicPlane`, i.e., the intersection of a finite number of :class:`half spaces <HyperbolicHalfSpace>`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` of which this is a subset - ``half_spaces`` -- the :class:`HyperbolicHalfSpace` of which this is an intersection - ``vertices`` -- marked vertices that should additionally be kept track of EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space()]) .. SEEALSO:: Use :meth:`HyperbolicPlane.polygon` and :meth:`HyperbolicPlane.intersection` to create polygons in the hyperbolic plane. """
[docs] def __init__(self, parent, half_spaces, vertices, category=None): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexPolygon sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space()]) sage: isinstance(P, HyperbolicConvexPolygon) True sage: TestSuite(P).run() """ if category is None: from flatsurf.geometry.categories import HyperbolicPolygons category = HyperbolicPolygons(parent.base_ring()).Convex().Simple() super().__init__(parent, category=category) if not isinstance(half_spaces, HyperbolicHalfSpaces): raise TypeError("half_spaces must be HyperbolicHalfSpaces") self._half_spaces = half_spaces self._marked_vertices = tuple(vertices)
def _check(self, require_normalized=True): r""" Verify that the marked vertices of this polygon are actually on the edges of the polygon. This implements :meth:`HyperbolicConvexSet._check`. INPUT: - ``require_normalized`` -- a boolean (default: ``True``); whether to assume that normalization has already happened, i.e., marked vertices that are actual vertices have already been removed EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexPolygon sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space()], ....: marked_vertices=[4], check=False) sage: P._check() Traceback (most recent call last): ... ValueError: marked vertex must be on an edge of the polygon sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space()], ....: marked_vertices=[oo], assume_minimal=True, check=False) sage: P._check() Traceback (most recent call last): ... ValueError: marked vertex must not be a non-marked vertex of the polygon """ for vertex in self._marked_vertices: if not any(vertex in edge for edge in self.edges(marked_vertices=False)): raise ValueError("marked vertex must be on an edge of the polygon") if require_normalized: if any( vertex in self.vertices(marked_vertices=False) for vertex in self._marked_vertices ): raise ValueError( "marked vertex must not be a non-marked vertex of the polygon" )
[docs] def _normalize(self, marked_vertices=False): r""" Return a convex set describing the intersection of the half spaces underlying this polygon. This implements :meth:`HyperbolicConvexSet._normalize`. The half spaces are assumed to be already sorted respecting :meth:`HyperbolicHalfSpaces._lt_`. ALGORITHM: We compute the intersection of the half spaces in the Klein model in several steps: * Drop trivially redundant half spaces, e.g., repeated ones. * Handle the case that the intersection is empty or a single point, see :meth:`_normalize_euclidean_boundary`. * Compute the intersection of the corresponding half spaces in the Euclidean plane, see :meth:`_normalize_drop_euclidean_redundant`. * Remove redundant half spaces that make no contribution for the unit disk of the Klein model, see :meth:`_normalize_drop_unit_disk_redundant`. * Determine of which nature (point, segment, line, polygon) the intersection of half spaces is and return the resulting set. INPUT: - ``marked_vertices`` -- a boolean (default: ``False``); whether to keep marked vertices when normalizing .. NOTE:: Over inexact rings, this is probably mostly useless. TESTS:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A helper to create non-normalized polygons for testing:: sage: polygon = lambda *half_spaces: H.polygon(half_spaces, check=False, assume_sorted=False, assume_minimal=True) An instance that caused problems at some point:: sage: P = polygon( ....: H.geodesic(7, -4, -3, model="half_plane").left_half_space(), ....: H.geodesic(1, -1, 0, model="half_plane").left_half_space(), ....: H.vertical(1/2).right_half_space(), ....: H.vertical(1).right_half_space(), ....: H.vertical(1).right_half_space(), ....: H.geodesic(1, 4, -5, model="half_plane").left_half_space(), ....: H.geodesic(50, 57, -43, model="half_plane").left_half_space(), ....: H.geodesic(3, 2, -3, model="half_plane").left_half_space() ....: ) sage: P._normalize() {x - 1 ≥ 0} """ marked_vertices = self._marked_vertices if marked_vertices else [] self = self._normalize_drop_trivially_redundant() if not self._half_spaces: raise NotImplementedError("cannot model intersection of no half spaces yet") # Find a segment on the boundary of the intersection. boundary = self._normalize_euclidean_boundary() if not isinstance(boundary, HyperbolicHalfSpace): # When there was no such segment, i.e., the intersection is empty # or just a point, we are done. return boundary # Compute a minimal subset of the half spaces that defines the intersection in the Euclidean plane. self = self._normalize_drop_euclidean_redundant(boundary) # Remove half spaces that make no contribution when restricting to the unit disk of the Klein model. self = self._normalize_drop_unit_disk_redundant() marked_vertices = [ vertex for vertex in marked_vertices if vertex not in self.vertices() ] if marked_vertices: if self.dimension() < 2: raise NotImplementedError( "cannot add marked vertices to low dimensional objects" ) self = self.parent().polygon( self.half_spaces(), check=False, assume_sorted=True, assume_minimal=True, marked_vertices=marked_vertices, ) self = self._normalize_drop_marked_vertices() return self
def _normalize_drop_trivially_redundant(self): r""" Return a sublist of the ``half_spaces`` defining this polygon without changing their intersection by removing some trivially redundant half spaces. The ``half_spaces`` are assumed to be sorted consistent with :meth:`HyperbolicHalfSpaces._lt_`. This is a helper method for :meth:`_normalize`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A helper to create non-normalized polygons for testing:: sage: polygon = lambda *half_spaces: H.polygon(half_spaces, check=False, assume_sorted=False, assume_minimal=True) Repeated half spaces are removed:: sage: polygon(H.vertical(0).left_half_space(), H.vertical(0).left_half_space())._normalize_drop_trivially_redundant() {x ≤ 0} Inclusions of half spaces are simplified:: sage: polygon(H.vertical(0).left_half_space(), H.geodesic(1/2, 2).left_half_space())._normalize_drop_trivially_redundant() {x ≤ 0} But only if the inclusion is already present when extending the half space from the Klein model to the entire Euclidean plane:: sage: polygon(H.vertical(0).left_half_space(), H.vertical(1).left_half_space())._normalize_drop_trivially_redundant() {x ≤ 0} ∩ {x - 1 ≤ 0} TESTS: The intersection of two half circles centered at 0:: sage: polygon(*(H.half_circle(0, 1).half_spaces() + H.half_circle(0, 2).half_spaces()))._normalize_drop_trivially_redundant() {(x^2 + y^2) - 1 ≤ 0} ∩ {(x^2 + y^2) - 2 ≥ 0} """ reduced = [] for half_space in self._half_spaces: if reduced: a, b, c = half_space.equation(model="klein") A, B, C = reduced[-1].equation(model="klein") equal = self.parent().geometry._equal sgn = self.parent().geometry._sgn if equal(c * B, C * b) and sgn(b) == sgn(B) and sgn(c) == sgn(C): # The half spaces are parallel in the Euclidean plane. Since we # assume spaces to be sorted by inclusion, we can drop this # space. continue reduced.append(half_space) return self.parent().polygon( reduced, check=False, assume_sorted=True, assume_minimal=True, marked_vertices=False, )
[docs] def _normalize_drop_euclidean_redundant(self, boundary): r""" Return a minimal sublist of the ``half_spaces`` defining this polygon that describe their intersection as half spaces of the Euclidean plane. Consider the half spaces in the Klein model. Ignoring the unit disk, they also describe half spaces in the Euclidean plane. The half space ``boundary`` must be one of the ``half_spaces`` that defines a boundary edge of the intersection polygon in the Euclidean plane. This is a helper method for :meth:`_normalize`. ALGORITHM: We use an approach similar to gift-wrapping (but from the inside) to remove redundant half spaces from the input list. We start from the ``boundary`` which is one of the minimal half spaces and extend to the full intersection by walking the sorted half spaces. Since we visit each half space once, this reduction runs in linear time in the number of half spaces. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A helper to create non-normalized polygons for testing:: sage: polygon = lambda *half_spaces: H.polygon(half_spaces, check=False, assume_sorted=False, assume_minimal=True) An intersection which is a single point on the boundary of the unit disk:: sage: polygon(*H.infinity().half_spaces())._normalize_drop_euclidean_redundant( ....: boundary=H.vertical(1).right_half_space()) {x ≤ 0} ∩ {x - 1 ≥ 0} An intersection which is a segment outside of the unit disk:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.half_space(-2, -2, 1, model="klein"), ....: H.half_space(17/8, 2, -1, model="klein"), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(0).left_half_space()) {(x^2 + y^2) + 4*x + 3 ≤ 0} ∩ {x ≤ 0} ∩ {9*(x^2 + y^2) + 32*x + 25 ≥ 0} ∩ {x ≥ 0} An intersection which is a polygon outside of the unit disk:: sage: polygon( ....: H.half_space(0, 1, 0, model="klein"), ....: H.half_space(1, -2, 0, model="klein"), ....: H.half_space(-2, -2, 1, model="klein"), ....: H.half_space(17/8, 2, -1, model="klein"), ....: )._normalize_drop_euclidean_redundant(boundary=H.half_space(17/8, 2, -1, model="klein")) {(x^2 + y^2) + 4*x + 3 ≤ 0} ∩ {(x^2 + y^2) - 4*x + 1 ≥ 0} ∩ {9*(x^2 + y^2) + 32*x + 25 ≥ 0} ∩ {x ≥ 0} An intersection which is an (unbounded) polygon touching the unit disk:: sage: polygon( ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(1).right_half_space()) {x + 1 ≤ 0} ∩ {x - 1 ≥ 0} An intersection which is a segment touching the unit disk:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.vertical(-1).left_half_space(), ....: H.geodesic(-1, -2).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(0).left_half_space()) {x + 1 ≤ 0} ∩ {x ≤ 0} ∩ {(x^2 + y^2) + 3*x + 2 ≥ 0} ∩ {x ≥ 0} An intersection which is a polygon inside the unit disk:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.geodesic(0, -1).right_half_space(), ....: H.geodesic(0, 1).left_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.geodesic(0, 1).left_half_space()) {(x^2 + y^2) - x ≥ 0} ∩ {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) + x ≥ 0} A polygon which has no vertices inside the unit disk but intersects the unit disk:: sage: polygon( ....: H.geodesic(2, 3).left_half_space(), ....: H.geodesic(-3, -2).left_half_space(), ....: H.geodesic(-1/2, -1/3).left_half_space(), ....: H.geodesic(1/3, 1/2).left_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.geodesic(1/3, 1/2).left_half_space()) {6*(x^2 + y^2) - 5*x + 1 ≥ 0} ∩ {(x^2 + y^2) - 5*x + 6 ≥ 0} ∩ {(x^2 + y^2) + 5*x + 6 ≥ 0} ∩ {6*(x^2 + y^2) + 5*x + 1 ≥ 0} A single half plane:: sage: polygon( ....: H.vertical(0).left_half_space() ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(0).left_half_space()) {x ≤ 0} A pair of anti-parallel half planes:: sage: polygon( ....: H.geodesic(1/2, 2).left_half_space(), ....: H.geodesic(-1/2, -2).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.geodesic(-1/2, -2).right_half_space()) {2*(x^2 + y^2) - 5*x + 2 ≥ 0} ∩ {2*(x^2 + y^2) + 5*x + 2 ≥ 0} A pair of anti-parallel half planes in the upper half plane:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(1).left_half_space()) {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(-1).right_half_space()) {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} A segment in the unit disk with several superfluous half planes at infinity:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.vertical(1/2).left_half_space(), ....: H.vertical(1/3).left_half_space(), ....: H.vertical(1/4).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.vertical(-1/2).right_half_space(), ....: H.vertical(-1/3).right_half_space(), ....: H.vertical(-1/4).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(0).left_half_space()) {x ≤ 0} ∩ {4*x + 1 ≥ 0} ∩ {x ≥ 0} A polygon in the unit disk with several superfluous half planes:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.geodesic(0, 1).left_half_space(), ....: H.geodesic(0, -1).right_half_space(), ....: H.vertical(2).left_half_space(), ....: H.vertical(-2).right_half_space(), ....: H.geodesic(0, 1/2).left_half_space(), ....: H.geodesic(0, -1/2).right_half_space(), ....: H.vertical(3).left_half_space(), ....: H.vertical(-3).right_half_space(), ....: H.geodesic(0, 1/3).left_half_space(), ....: H.geodesic(0, -1/3).right_half_space(), ....: )._normalize_drop_euclidean_redundant(boundary=H.vertical(1).left_half_space()) {(x^2 + y^2) - x ≥ 0} ∩ {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) + x ≥ 0} TESTS: An example that caused trouble at some point:: sage: P = polygon( ....: H.geodesic(7, -4, -3, model="half_plane").left_half_space(), ....: H.geodesic(1, -1, 0, model="half_plane").left_half_space(), ....: H.vertical(1/2).right_half_space(), ....: H.vertical(1).right_half_space(), ....: H.geodesic(1, 4, -5, model="half_plane").left_half_space(), ....: H.geodesic(50, 57, -43, model="half_plane").left_half_space(), ....: H.geodesic(3, 2, -3, model="half_plane").left_half_space() ....: ) sage: P._normalize_drop_euclidean_redundant(boundary=P._normalize_euclidean_boundary()) {(x^2 + y^2) - x ≥ 0} ∩ {2*x - 1 ≥ 0} ∩ {x - 1 ≥ 0} .. NOTE:: There are some additional assumptions on the input than what is stated here. Please refer to the implementation. """ half_spaces = list(self._half_spaces) half_spaces = ( half_spaces[half_spaces.index(boundary) :] + half_spaces[: half_spaces.index(boundary)] ) half_spaces.reverse() required_half_spaces = [half_spaces.pop()] while half_spaces: A = required_half_spaces[-1] B = half_spaces.pop() C = half_spaces[-1] if half_spaces else required_half_spaces[0] # Determine whether B is redundant, i.e., whether the intersection # A, B, C and A, C are the same. # Since we know that A is required and the space non-empty, the # question here is whether C blocks the line of sight from A to B. # We distinguish cases, depending of the nature of the intersection of A and B. AB = A.boundary()._configuration(B.boundary()) BC = B.boundary()._configuration(C.boundary()) AC = A.boundary()._configuration(C.boundary()) required = False if AB == "convex": if BC == "concave": assert AC in ["equal", "concave"] required = True elif BC == "convex": BC = B.boundary()._intersection(C.boundary()) required = AC == "negative" or (BC in A and BC not in A.boundary()) elif BC == "negative": required = True elif BC == "anti-parallel": required = True else: raise NotImplementedError( f"B and C are in unsupported configuration: {BC}" ) elif AB == "negative": required = True elif AB == "anti-parallel": required = True elif AB == "concave": required = True else: raise NotImplementedError( f"A and B are in unsupported configuration: {AB}" ) if required: required_half_spaces.append(B) elif len(required_half_spaces) > 1: half_spaces.append(required_half_spaces.pop()) required_half_spaces = HyperbolicHalfSpaces( required_half_spaces, assume_sorted="rotated" ) return self.parent().polygon( required_half_spaces, check=False, assume_sorted=True, assume_minimal=True, marked_vertices=False, )
[docs] def _normalize_drop_unit_disk_redundant(self): r""" Return the intersection of the Euclidean ``half_spaces`` defining this polygon with the unit disk. The ``half_spaces`` must be minimal to describe their intersection in the Euclidean plane. If that intersection does not intersect the unit disk, then return the :meth:`HyperbolicPlane.empty_set`. Otherwise, return a minimal sublist of ``half_spaces`` that describes the intersection inside the unit disk. This is a helper method for :meth:`_normalize`. ALGORITHM: When passing to the Klein model, i.e., intersecting the polygon with the unit disk, some of the edges of the (possibly unbounded) polygon described by the ``half_spaces`` are unnecessary because they are not intersecting the unit disk. If none of the edges intersect the unit disk, then the polygon has empty intersection with the unit disk. Otherwise, we can drop the half spaces describing the edges that do not intersect the unit disk. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A helper to create non-normalized polygons for testing:: sage: polygon = lambda *half_spaces: H.polygon(half_spaces, check=False, assume_sorted=False, assume_minimal=True) An intersection which is a single point on the boundary of the unit disk:: sage: polygon(*H.infinity().half_spaces())._normalize_drop_unit_disk_redundant() An intersection which is a segment outside of the unit disk:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.half_space(-2, -2, 1, model="klein"), ....: H.half_space(17/8, 2, -1, model="klein"), ....: )._normalize_drop_unit_disk_redundant() {} An intersection which is a polygon outside of the unit disk:: sage: polygon( ....: H.half_space(0, 1, 0, model="klein"), ....: H.half_space(1, -2, 0, model="klein"), ....: H.half_space(-2, -2, 1, model="klein"), ....: H.half_space(17/8, 2, -1, model="klein"), ....: )._normalize_drop_unit_disk_redundant() {} An intersection which is an (unbounded) polygon touching the unit disk:: sage: polygon( ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space())._normalize_drop_unit_disk_redundant() An intersection which is a segment touching the unit disk:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.vertical(-1).left_half_space(), ....: H.geodesic(-1, -2).right_half_space())._normalize_drop_unit_disk_redundant() An intersection which is a polygon inside the unit disk:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.geodesic(0, -1).right_half_space(), ....: H.geodesic(0, 1).left_half_space())._normalize_drop_unit_disk_redundant() {(x^2 + y^2) - x ≥ 0} ∩ {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) + x ≥ 0} A polygon which has no vertices inside the unit disk but intersects the unit disk:: sage: polygon( ....: H.geodesic(2, 3).left_half_space(), ....: H.geodesic(-3, -2).left_half_space(), ....: H.geodesic(-1/2, -1/3).left_half_space(), ....: H.geodesic(1/3, 1/2).left_half_space())._normalize_drop_unit_disk_redundant() {6*(x^2 + y^2) - 5*x + 1 ≥ 0} ∩ {(x^2 + y^2) - 5*x + 6 ≥ 0} ∩ {(x^2 + y^2) + 5*x + 6 ≥ 0} ∩ {6*(x^2 + y^2) + 5*x + 1 ≥ 0} A single half plane:: sage: polygon(H.vertical(0).left_half_space())._normalize_drop_unit_disk_redundant() {x ≤ 0} A pair of anti-parallel half planes:: sage: polygon( ....: H.geodesic(1/2, 2).left_half_space(), ....: H.geodesic(-1/2, -2).right_half_space())._normalize_drop_unit_disk_redundant() {2*(x^2 + y^2) - 5*x + 2 ≥ 0} ∩ {2*(x^2 + y^2) + 5*x + 2 ≥ 0} A segment in the unit disk with a superfluous half plane at infinity:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space())._normalize_drop_unit_disk_redundant() {x = 0} A polygon in the unit disk with several superfluous half planes:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.geodesic(0, 1).left_half_space(), ....: H.geodesic(0, -1).right_half_space(), ....: H.vertical(2).left_half_space(), ....: H.geodesic(0, 1/2).left_half_space())._normalize_drop_unit_disk_redundant() {(x^2 + y^2) - x ≥ 0} ∩ {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) + x ≥ 0} A segment touching the inside of the unit disk:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space(), ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: )._normalize_drop_unit_disk_redundant() {x = 0} ∩ {(x^2 + y^2) - 2 ≥ 0} An unbounded polygon touching the unit disk from the inside:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: )._normalize_drop_unit_disk_redundant() {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} A segment inside the unit disk:: sage: polygon( ....: H.vertical(0).right_half_space(), ....: H.vertical(0).left_half_space(), ....: H.geodesic(-2, 2).right_half_space(), ....: H.geodesic(-1/2, 1/2).left_half_space(), ....: )._normalize_drop_unit_disk_redundant() {x = 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {4*(x^2 + y^2) - 1 ≥ 0} .. NOTE:: There are some additional assumptions on the input than what is stated here. Please refer to the implementation. """ required_half_spaces = [] maybe_empty = True maybe_point = True maybe_segment = True for A, B, C in self._half_spaces.triples(repeat=True): AB = ( None if A.boundary()._configuration(B.boundary()) == "concave" else A.boundary()._intersection(B.boundary()) ) BC = ( None if B.boundary()._configuration(C.boundary()) == "concave" else B.boundary()._intersection(C.boundary()) ) segment = self.parent().segment(B.boundary(), AB, BC, check=False) if isinstance(segment, HyperbolicEmptySet): pass elif isinstance(segment, HyperbolicPoint): maybe_empty = False if not maybe_point: continue if maybe_point is True: maybe_point = segment elif maybe_point != segment: # Unsurprisingly, pylint gets confused by maybe_point being # both a boolean and a point at times. The code should # probably be cleaned up. But here, it must be a point so # the call is save. # pylint: disable=no-member assert not maybe_point.is_finite() # pylint: enable=no-member assert not segment.is_finite() maybe_point = False else: maybe_empty = False maybe_point = False if maybe_segment is True: maybe_segment = segment elif maybe_segment == -segment: # For the intersection to be only segment, we must see the # segment twice, once from both sides. return segment else: maybe_segment = False required_half_spaces.append(B) if maybe_empty: return self.parent().empty_set() if maybe_point: return maybe_point if len(required_half_spaces) == 0: raise NotImplementedError( "there is no convex set to represent the full space yet" ) if len(required_half_spaces) == 1: return required_half_spaces[0] return self.parent().polygon( required_half_spaces, check=False, assume_sorted=True, assume_minimal=True, marked_vertices=False, )
[docs] def _normalize_euclidean_boundary(self): r""" Return a half space whose (Euclidean) boundary intersects the boundary of the intersection of the ``half_spaces`` defining this polygon in more than a point. Consider the half spaces in the Klein model. Ignoring the unit disk, they also describe half spaces in the Euclidean plane. If their intersection contains a segment it must be on the boundary of one of the ``half_spaces`` which is returned by this method. If this is not the case, and the intersection is empty in the hyperbolic plane, return the :meth:`HyperbolicPlane.empty_set`. Otherwise, if the intersection is a point in the hyperbolic plane, return that point. The ``half_spaces`` must already be sorted with respect to :meth:`HyperbolicHalfSpaces._lt_`. This is a helper method for :meth:`_normalize`. ALGORITHM: We initially ignore the hyperbolic structure and just consider the half spaces of the Klein model as Euclidean half spaces. We use a relatively standard randomized optimization approach to find a point in the intersection: we randomly shuffle the half spaces and then optimize a segment on some boundary of the half spaces. The randomization makes this a linear time algorithm, see e.g., https://www2.cs.arizona.edu/classes/cs437/fall07/Lecture4.prn.pdf If the only segment we can construct is a point, then the intersection is a single point in the Euclidean plane. The intersection in the hyperbolic plane might be a single point or empty. If not even a point exists, the intersection is empty in the Euclidean plane and therefore empty in the hyperbolic plane. Note that the segment returned might not be within the unit disk. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A helper to create non-normalized polygons for testing:: sage: polygon = lambda *half_spaces: H.polygon(half_spaces, check=False, assume_sorted=False, assume_minimal=True) Make the following randomized tests reproducible:: sage: set_random_seed(0) An intersection that is already empty in the Euclidean plane:: sage: polygon( ....: H.geodesic(2, 1/2).left_half_space(), ....: H.geodesic(-1/2, -2).left_half_space() ....: )._normalize_euclidean_boundary() {} An intersection which in the Euclidean plane is a single point but outside the unit disk:: sage: polygon( ....: H.half_space(0, 1, 0, model="klein"), ....: H.half_space(0, -1, 0, model="klein"), ....: H.half_space(2, 2, -1, model="klein"), ....: H.half_space(-2, -2, 1, model="klein"), ....: )._normalize_euclidean_boundary() {} An intersection which is a single point inside the unit disk:: sage: polygon(*H(I).half_spaces())._normalize_euclidean_boundary() I An intersection which is a single point on the boundary of the unit disk:: sage: polygon(*H.infinity().half_spaces())._normalize_euclidean_boundary() {x - 1 ≥ 0} An intersection which is a segment outside of the unit disk:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.half_space(-2, -2, 1, model="klein"), ....: H.half_space(17/8, 2, -1, model="klein"), ....: )._normalize_euclidean_boundary() {x ≤ 0} An intersection which is a polygon outside of the unit disk:: sage: polygon( ....: H.half_space(0, 1, 0, model="klein"), ....: H.half_space(1, -2, 0, model="klein"), ....: H.half_space(-2, -2, 1, model="klein"), ....: H.half_space(17/8, 2, -1, model="klein"), ....: )._normalize_euclidean_boundary() {9*(x^2 + y^2) + 32*x + 25 ≥ 0} An intersection which is an (unbounded) polygon touching the unit disk:: sage: polygon( ....: H.vertical(-1).left_half_space(), ....: H.vertical(1).right_half_space(), ....: )._normalize_euclidean_boundary() {x - 1 ≥ 0} An intersection which is a segment touching the unit disk:: sage: polygon( ....: H.vertical(0).left_half_space(), ....: H.vertical(0).right_half_space(), ....: H.vertical(-1).left_half_space(), ....: H.geodesic(-1, -2).right_half_space(), ....: )._normalize_euclidean_boundary() {x ≥ 0} An intersection which is a polygon inside the unit disk:: sage: polygon( ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.geodesic(0, -1).right_half_space(), ....: H.geodesic(0, 1).left_half_space(), ....: )._normalize_euclidean_boundary() {(x^2 + y^2) - x ≥ 0} A polygon which has no vertices inside the unit disk but intersects the unit disk:: sage: polygon( ....: H.geodesic(2, 3).left_half_space(), ....: H.geodesic(-3, -2).left_half_space(), ....: H.geodesic(-1/2, -1/3).left_half_space(), ....: H.geodesic(1/3, 1/2).left_half_space(), ....: )._normalize_euclidean_boundary() {6*(x^2 + y^2) - 5*x + 1 ≥ 0} A single half plane:: sage: polygon( ....: H.vertical(0).left_half_space() ....: )._normalize_euclidean_boundary() {x ≤ 0} A pair of anti-parallel half planes:: sage: polygon( ....: H.geodesic(1/2, 2).left_half_space(), ....: H.geodesic(-1/2, -2).right_half_space(), ....: )._normalize_euclidean_boundary() {2*(x^2 + y^2) - 5*x + 2 ≥ 0} TESTS: A case that caused problems at some point:: sage: set_random_seed(1) sage: polygon( ....: H.geodesic(300, 3389, -1166, model="half_plane").right_half_space(), ....: H.geodesic(5, -24, -5, model="half_plane").left_half_space(), ....: H.geodesic(182, -1135, 522, model="half_plane").left_half_space(), ....: )._normalize_euclidean_boundary() {5*(x^2 + y^2) - 24*x - 5 ≥ 0} """ if len(self._half_spaces) == 0: raise ValueError("list of half spaces must not be empty") if len(self._half_spaces) == 1: return next(iter(self._half_spaces)) # Randomly shuffle the half spaces so the loop below runs in expected linear time. from sage.all import shuffle random_half_spaces = list(self._half_spaces) shuffle(random_half_spaces) # Move from the random starting point to a point that is contained in all half spaces. point = random_half_spaces[0].boundary().an_element() for half_space in random_half_spaces: if point in half_space: continue else: # The point is not in this half space. Find a point on the # boundary of half_space that is contained in all the half # spaces we have seen so far. boundary = half_space.boundary() # We parametrize the boundary points of half space, i.e., the # points that satisfy a + bx + cy = 0 by picking a base point B # and then writing points as (x, y) = B + λ(c, -b). # Each half space constrains the possible values of λ, starting # from (-∞,∞) to a smaller closed interval. from sage.all import RealSet, oo # Note that RealSet.real_line() would require SageMath 9.4 interval = RealSet(-oo, oo) for constraining in random_half_spaces: if constraining is half_space: break intersection = boundary._intersection(constraining.boundary()) if intersection is None: # constraining is anti-parallel to half_space if ( boundary.unparametrize(0, model="euclidean", check=False) not in constraining ): return self.parent().empty_set() # The intersection is non-empty, so this adds no further constraints. continue λ = boundary.parametrize( intersection, model="euclidean", check=False ) # RealSet in SageMath does not like number fields. We move # everything through AA (which might not always work) to # work around this problem. if λ.parent().is_exact(): from sage.all import AA = AA(λ) else: = λ # Determine whether this half space constrains to (-∞, λ] or [λ, ∞). if ( boundary.unparametrize(λ + 1, model="euclidean", check=False) in constraining ): constraint = RealSet.unbounded_above_closed() else: constraint = RealSet.unbounded_below_closed() interval = interval.intersection(constraint) if interval.is_empty(): # The constraints leave no possibility for λ. return self.parent().empty_set() # Construct a point from any of the λ in interval. λ = interval.an_element() point = boundary.unparametrize(λ, model="euclidean", check=False) return self._normalize_extend_to_euclidean_boundary(point)
def _normalize_extend_to_euclidean_boundary(self, point): r""" Extend ``point`` to a (Euclidean) half space which intersects the intersection of the ``half_spaces`` defining this polygon in more than one point. This is a helper method for :meth:`_normalize_euclidean_boundary`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A helper to create non-normalized polygons for testing:: sage: polygon = lambda *half_spaces: H.polygon(half_spaces, check=False, assume_sorted=False, assume_minimal=True) We extend from a single point to half space:: sage: P = polygon(*H.infinity().half_spaces()) sage: P._normalize_extend_to_euclidean_boundary(H.infinity()) {x ≤ 0} """ half_spaces = [ half_space for half_space in self._half_spaces if point in half_space.boundary() ] if len(half_spaces) == 0: raise ValueError("point must be on the boundary of a defining half space") if len(half_spaces) < 3: return half_spaces[0] for i, half_space in enumerate(half_spaces): following = half_spaces[(i + 1) % len(half_spaces)] configuration = half_space.boundary()._configuration(following.boundary()) if configuration == "convex": continue if configuration == "negative": return half_space if configuration == "concave": return half_space raise NotImplementedError( f"cannot extend point to segment when half spaces are in configuration {configuration}" ) if point.is_ultra_ideal(): # There is no actual intersection in the hyperbolic plane. return self.parent().empty_set() return point def _normalize_drop_marked_vertices(self): r""" Return a copy of this polygon with marked vertices removed that are already vertices of the polygon anyway. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space()], ....: marked_vertices=[oo], assume_minimal=True, check=False) sage: P {x - 1 ≤ 0} ∩ {x ≥ 0} ∩ {(x^2 + y^2) - 2 ≥ 0} ∪ {∞} sage: P._normalize_drop_marked_vertices() {x - 1 ≤ 0} ∩ {x ≥ 0} ∩ {(x^2 + y^2) - 2 ≥ 0} """ vertices = [ vertex for vertex in self._marked_vertices if vertex not in self.vertices(marked_vertices=False) ] return self.parent().polygon( self._half_spaces, check=False, assume_sorted=True, assume_minimal=True, marked_vertices=vertices, )
[docs] def dimension(self): r""" Return the dimension of this polygon, i.e., 2. This implements :meth:`HyperbolicConvexSet.dimension`. Note that this also returns 2 if the actual dimension of the polygon is smaller. This is, however, only possible for polygons created with :meth:`HyperbolicPlane.polygon` setting ``check=False``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(-1).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ]) sage: P.dimension() 2 """ from sage.all import ZZ return ZZ(2)
[docs] @cached_method def edges(self, as_segments=False, marked_vertices=True): r""" Return the :class:`segments <HyperbolicOrientedSegment>` and :class:`geodesics <HyperbolicOrientedGeodesic>` defining this polygon. This implements :meth:`HyperbolicConvexSet.edges` for polygons. INPUT: - ``as_segments`` -- a boolean (default: ``False``); whether to also return the geodesics as segments with ideal end points. - ``marked_vertices`` -- a boolean (default: ``True``); if set, edges with end points at a marked vertex are reported, otherwise, marked vertices are completely ignored. OUTPUT: A set of segments and geodesics. Iteration through this set is in counterclockwise order with respect to the points of the set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() The edges of a polygon:: sage: P = H.intersection( ....: H.vertical(-1).right_half_space(), ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space()) sage: P.edges() {{-x + 1 = 0} ∩ {2*(x^2 + y^2) - 5*x - 3 ≤ 0}, {-(x^2 + y^2) + 4 = 0} ∩ {(x^2 + y^2) - 5*x + 1 ≥ 0} ∩ {(x^2 + y^2) + 5*x + 1 ≥ 0}, {x + 1 = 0} ∩ {2*(x^2 + y^2) + 5*x - 3 ≤ 0}, {(x^2 + y^2) - 1 = 0}} sage: [type(e) for e in P.edges()] [<class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>, <class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>, <class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>, <class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedGeodesic_with_category_with_category'>] sage: [type(e) for e in P.edges(as_segments=True)] [<class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>, <class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>, <class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>, <class 'flatsurf.geometry.hyperbolic.HyperbolicOrientedSegment_with_category_with_category'>] The edges of a polygon with marked vertices:: sage: P = H.convex_hull(-1, 1, I, 2*I, marked_vertices=True) sage: P.edges() {{-(x^2 + y^2) - 3*x + 4 = 0} ∩ {3*(x^2 + y^2) - 25*x - 12 ≤ 0}, {-(x^2 + y^2) + 3*x + 4 = 0} ∩ {3*(x^2 + y^2) + 25*x - 12 ≤ 0}, {(x^2 + y^2) - 1 = 0} ∩ {x ≤ 0}, {(x^2 + y^2) - 1 = 0} ∩ {x ≥ 0}} sage: P.edges(marked_vertices=False) {{-(x^2 + y^2) - 3*x + 4 = 0} ∩ {3*(x^2 + y^2) - 25*x - 12 ≤ 0}, {-(x^2 + y^2) + 3*x + 4 = 0} ∩ {3*(x^2 + y^2) + 25*x - 12 ≤ 0}, {(x^2 + y^2) - 1 = 0}} """ edges = [] boundaries = [half_space.boundary() for half_space in self._half_spaces] if len(boundaries) <= 1: return boundaries for i, B in enumerate(boundaries): A = boundaries[i - 1] C = boundaries[(i + 1) % len(boundaries)] AB = A._configuration(B) BC = B._configuration(C) start = None end = None if AB == "convex": start = A._intersection(B) if not start.is_finite(): start = None elif AB == "concave": pass elif AB == "negative": start = None elif AB == "anti-parallel": start = None else: raise NotImplementedError( f"cannot determine edges when boundaries are in configuration {AB}" ) if BC == "convex": end = B._intersection(C) if not end.is_finite(): end = None elif BC == "concave": pass elif BC == "negative": end = None elif BC == "anti-parallel": end = None else: raise NotImplementedError( f"cannot determine edges when boundaries are in configuration {BC}" ) edges.append( self.parent().segment( B, start=start, end=end, assume_normalized=as_segments, check=False ) ) if marked_vertices and self._marked_vertices: edges_without_marked_vertices = edges edges = [] for edge in edges_without_marked_vertices: vertices_on_edge = [ vertex for vertex in self._marked_vertices if vertex in edge ] def key(vertex, geodesic=edge.geodesic()): return geodesic.parametrize(vertex, model="euclidean") vertices_on_edge.sort(key=key) vertices_on_edge.append(edge.end()) start = edge.start() for vertex in vertices_on_edge: edges.append( self.parent().segment( edge.geodesic(), start=start, end=vertex, assume_normalized=as_segments, check=False, ) ) start = vertex return HyperbolicEdges(edges)
[docs] def vertices(self, marked_vertices=True): r""" Return the vertices of this polygon, i.e., the (possibly ideal) end points of the :meth:`edges`. INPUT: -- ``marked_vertices`` -- a boolean (default: ``True``) whether to include marked vertices in the output OUTPUT: Returns a set of points. Iteration over this set is in counterclockwise order. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A finite polygon with a marked vertex:: sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ], marked_vertices=[I]) sage: P.vertices() {-1, I, 1, (2/5, 3/5), (-2/5, 3/5)} sage: P.vertices(marked_vertices=False) {-1, 1, (2/5, 3/5), (-2/5, 3/5)} Currently, vertices cannot be computed if some of them have coordinates which do not live over the :meth:`HyperbolicPlane.base_ring`; see :class:`HyperbolicVertices`:: sage: P = H.polygon([ ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 2).right_half_space(), ....: ]) sage: P.vertices() Traceback (most recent call last): ... ValueError: ... .. SEEALSO:: :meth:`HyperbolicConvexSet.vertices` for more details. """ vertices = [] edges = self.edges(marked_vertices=False) end = edges[-1].end() for i, edge in enumerate(edges): start = edge.start() if start != end: vertices.append(start) end = edge.end() vertices.append(end) if marked_vertices: vertices.extend(self._marked_vertices) return HyperbolicVertices(vertices)
[docs] def half_spaces(self): r""" Return a minimal set of half spaces whose intersection this polygon is. This implements :meth:`HyperbolicConvexSet.half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Marked vertices are not encoded in the half spaces in any way:: sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ], marked_vertices=[I + 1]) sage: P {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∪ {1 + I} sage: H.polygon(P.half_spaces()) {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} """ return self._half_spaces
def _repr_(self): r""" Return a printable representation of this polygon. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: ], marked_vertices=[I + 1]) sage: P {x - 1 ≤ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∪ {1 + I} """ half_spaces = " ∩ ".join([repr(half_space) for half_space in self._half_spaces]) vertices = ", ".join([repr(vertex) for vertex in self._marked_vertices]) if vertices: return f"{half_spaces}{{{vertices}}}" return half_spaces
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of this polygon in the hyperbolic ``model``. INPUT: - ``model`` -- one of ``"half_plane"`` and ``"klein"`` - ``color`` -- a string (default: ``"#efffff"``); the fill color of polygons - ``edgecolor`` -- a string (default: ``"blue"``); the color of geodesics and segments See :func:`flatsurf.graphical.hyperbolic.hyperbolic_path` for additional keyword arguments to customize the plot. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() A finite triangle:: sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.geodesic(I, I + 1).left_half_space(), ....: H.geodesic(I + 1, 2*I).left_half_space() ....: ]) sage: P {(x^2 + y^2) - x - 1 ≥ 0} ∩ {(x^2 + y^2) + 2*x - 4 ≤ 0} ∩ {x ≥ 0} In the upper half plane model, this plots as a polygon bounded by a segment and two arcs:: sage: P.plot("half_plane")[0] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, 1.00000000000000)), CartesianPathPlotCommand(code='RARCTO', args=((1.00000000000000, 1.00000000000000), (0.500000000000000, 0))), CartesianPathPlotCommand(code='ARCTO', args=((0.000000000000000, 2.00000000000000), (-1.00000000000000, 0))), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 1.00000000000000))]) Technically, the plot consists of two parts, a filled layer with transparent stroke and a transparent layer with solid stroke. The latter shows the (finite) edges of the polygon:: sage: P.plot("half_plane")[1] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, 1.00000000000000)), CartesianPathPlotCommand(code='RARCTO', args=((1.00000000000000, 1.00000000000000), (0.500000000000000, 0))), CartesianPathPlotCommand(code='ARCTO', args=((0.000000000000000, 2.00000000000000), (-1.00000000000000, 0))), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 1.00000000000000))]) In the Klein disk model, this plots as two identical Euclidean triangles (one for the background, one for the edges,) with an added circle representing the ideal points in that model:: sage: P.plot("klein")[0] Circle defined by (0.0,0.0) with r=1.0 sage: P.plot("klein")[1] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.666666666666667, 0.333333333333333)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.600000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000))]) sage: P.plot("klein")[2] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.666666666666667, 0.333333333333333)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.600000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000))]) An ideal triangle plots the same way in the Klein model but now has two rays in the upper half plane model:: sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.geodesic(0, 1).left_half_space(), ....: H.vertical(1).left_half_space() ....: ]) sage: P {(x^2 + y^2) - x ≥ 0} ∩ {x - 1 ≤ 0} ∩ {x ≥ 0} sage: P.plot("half_plane")[0] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='RARCTO', args=((1.00000000000000, 0.000000000000000), (0.500000000000000, 0))), CartesianPathPlotCommand(code='RAYTO', args=(0, 1)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000))]) sage: P.plot("klein")[1] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(0.000000000000000, -1.00000000000000)), CartesianPathPlotCommand(code='LINETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 1.00000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, -1.00000000000000))]) A polygon can contain infinitely many ideal points as is the case in this intersection of two half spaces:: sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.vertical(1).left_half_space() ....: ]) sage: P {x - 1 ≤ 0} ∩ {x ≥ 0} sage: P.plot("half_plane")[0] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='RAYTO', args=(0, 1)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(1.00000000000000, 0.000000000000000))]) The last part, the line connecting 0 and 1, is missing from the stroke plot since we only stroke finite edges:: sage: P.plot("half_plane")[1] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='RAYTO', args=(0, 1)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000))]) Similarly in the Klein model picture, the arc of infinite points is only part of the fill, not of the stroke:: sage: P.plot("klein")[1] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 1.00000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, -1.00000000000000)), CartesianPathPlotCommand(code='ARCTO', args=((1.00000000000000, 0.000000000000000), (0, 0)))]) sage: P.plot("klein")[2] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 1.00000000000000)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, -1.00000000000000))]) If the polygon contains unbounded set of reals, we get a horizontal ray in the half plane picture:: sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.half_circle(2, 1).left_half_space(), ....: ]) sage: P {(x^2 + y^2) - 4*x + 3 ≥ 0} ∩ {x ≥ 0} sage: P.plot("half_plane")[0] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='RARCTO', args=((3.00000000000000, 0.000000000000000), (2.00000000000000, 0))), CartesianPathPlotCommand(code='RAYTO', args=(1, 0)), CartesianPathPlotCommand(code='RAYTO', args=(0, 1)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='LINETO', args=(1.00000000000000, 0.000000000000000))]) sage: P.plot("half_plane")[1] CartesianPathPlot([CartesianPathPlotCommand(code='MOVETO', args=(1.00000000000000, 0.000000000000000)), CartesianPathPlotCommand(code='RARCTO', args=((3.00000000000000, 0.000000000000000), (2.00000000000000, 0))), CartesianPathPlotCommand(code='MOVETOINFINITY', args=(0, 1)), CartesianPathPlotCommand(code='LINETO', args=(0.000000000000000, 0.000000000000000))]) """ kwds.setdefault("color", "#efffff") kwds.setdefault("edgecolor", "blue") if len(self._half_spaces) == 0: raise NotImplementedError("cannot plot full space") edges = self.edges(as_segments=True, marked_vertices=False) pos = edges[0].start() from flatsurf.graphical.hyperbolic import HyperbolicPathPlotCommand commands = [HyperbolicPathPlotCommand("MOVETO", pos)] for edge in edges: if edge.start() != pos: commands.append(HyperbolicPathPlotCommand("MOVETO", edge.start())) commands.append(HyperbolicPathPlotCommand("LINETO", edge.end())) pos = edge.end() if pos != edges[0].start(): commands.append(HyperbolicPathPlotCommand("MOVETO", edges[0].start())) from flatsurf.graphical.hyperbolic import hyperbolic_path plot = hyperbolic_path(commands, model=model, **kwds) return self._enhance_plot(plot, model=model)
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this polygon. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the polygon will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the polygon. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); must be ``None`` or ``False`` since polygons cannot have an explicit orientation. See :meth:`~HyperbolicConvexSet.is_oriented`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We change the ring over which a polygon is defined:: sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space(), ....: H.half_circle(0, 1).left_half_space()], ....: marked_vertices=[I]) sage: P.change(ring=AA) {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∪ {I} We cannot give a polygon an explicit orientation:: sage: P.change(oriented=False) == P True sage: P.change(oriented=True) Traceback (most recent call last): ... NotImplementedError: polygons cannot have an explicit orientation """ if ring is not None or geometry is not None: self = ( self.parent() .change_ring(ring, geometry=geometry) .polygon( [ half_space.change(ring=ring, geometry=geometry) for half_space in self._half_spaces ], check=False, assume_sorted=True, assume_minimal=True, marked_vertices=[ vertex.change(ring=ring, geometry=geometry) for vertex in self._marked_vertices ], ) ) if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): raise NotImplementedError("polygons cannot have an explicit orientation") return self
[docs] def __eq__(self, other): r""" Return whether this polygon is indistinguishable from ``other``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([H.vertical(1).left_half_space(), H.vertical(-1).right_half_space()]) sage: P == P True Marked vertices are taken into account to determine equality:: sage: Q = H.polygon([H.vertical(1).left_half_space(), H.vertical(-1).right_half_space()], marked_vertices=[I + 1]) sage: Q == Q True sage: P == Q False """ if not isinstance(other, HyperbolicConvexPolygon): return False return ( self._half_spaces == other._half_spaces and self._marked_vertices == other._marked_vertices )
[docs] def __hash__(self): r""" Return a hash value for this polygon. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Since polygons are hashable, they can be put in a hash table, such as a Python ``set``:: sage: S = {H.polygon([H.vertical(1).left_half_space(), H.vertical(-1).right_half_space()])} """ return hash((self._half_spaces, self._marked_vertices))
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the image of this polygon under ``isometry``. Helper method for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``) whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: isometry = matrix([[1, -1, 1], [1, 1/2, 1/2], [1, -1/2, 3/2]]) sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.half_circle(0, 4).left_half_space()], ....: marked_vertices=[4*I]) sage: P._apply_isometry_klein(isometry) {(x^2 + y^2) - 2*x - 3 ≥ 0} ∩ {x - 1 ≥ 0} ∪ {1 + 4*I} """ half_spaces = [ h.apply_isometry(isometry, model="klein", on_right=on_right) for h in self._half_spaces ] marked_vertices = [ p.apply_isometry(isometry, model="klein", on_right=on_right) for p in self._marked_vertices ] return self.parent().polygon( half_spaces=half_spaces, check=False, assume_minimal=True, marked_vertices=marked_vertices, )
[docs] def _isometry_conditions(self, other): r""" Return an iterable of primitive pairs that must map to each other in an isometry that maps this set to ``other``. Helper method for :meth:`HyperbolicPlane._isometry_conditions`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).right_half_space(), ....: H.half_circle(0, 4).left_half_space()], ....: marked_vertices=[4*I]) sage: conditions = P._isometry_conditions(P) sage: list(conditions) [[({x ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {x ≥ 0}), (4*I, 4*I)], [({x ≥ 0}, {x ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), (4*I, 4*I)], [({x ≥ 0}, {x ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), (4*I, 4*I)], [({x ≥ 0}, {(x^2 + y^2) - 4 ≥ 0}), ({(x^2 + y^2) - 4 ≥ 0}, {x ≥ 0}), (4*I, 4*I)]] .. SEEALSO:: :meth:`HyperbolicConvexSet._isometry_conditions` for a general description. r""" # We are likely returning too many conditions here. # In particular there are many more ways to determine that no isometry # can possibly exist here. for reverse in [False, True]: preimage = list(self.half_spaces()) image = list(other.half_spaces()) if len(preimage) != len(image): continue if reverse: image.reverse() for i in range(len(image)): image = image[1:] + image[:1] if self._marked_vertices: preimage_vertices = list(self._marked_vertices) image_vertices = list(other._marked_vertices) if len(preimage_vertices) != len(image_vertices): continue for j in range(len(image_vertices)): image_vertices = image_vertices[1:] + image_vertices[:1] yield list( zip(preimage + preimage_vertices, image + image_vertices) ) else: yield list(zip(preimage, image))
[docs] @classmethod def random_set(cls, parent): r""" Return a random hyperbolic convex polygon. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the polygon EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexPolygon sage: x = HyperbolicConvexPolygon.random_set(H) sage: x.dimension() 2 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ while True: from sage.all import ZZ interior_points = [] count = ZZ.random_element().abs() + 3 while len(interior_points) < count: p = HyperbolicPointFromCoordinates.random_set(parent) if p.is_ideal(): continue interior_points.append(p) half_spaces = [] while len(half_spaces) < len(interior_points): half_space = HyperbolicHalfSpace.random_set(parent) for p in interior_points: if p in half_space: continue a, b, c = half_space.equation(model="klein") x, y = p.coordinates(model="klein") a = -(b * x + c * y) half_space = parent.half_space(a, b, c, model="klein") assert p in half_space half_spaces.append(half_space) polygon = parent.polygon(half_spaces) if isinstance(polygon, HyperbolicConvexPolygon): return polygon
[docs] def is_degenerate(self): r""" Return whether this is considered to be a degenerate polygon. EXAMPLES: We consider polygons of area zero as degenerate:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(0).left_half_space(), ....: H.half_circle(0, 1).left_half_space(), ....: H.half_circle(0, 2).right_half_space(), ....: H.vertical(0).right_half_space() ....: ], check=False, assume_minimal=True) sage: P.is_degenerate() True We also consider polygons with marked points as degenerate:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.half_circle(0, 2).left_half_space(), ....: H.half_circle(0, 4).right_half_space(), ....: H.vertical(-1).right_half_space() ....: ], marked_vertices=[2*I]) sage: P.is_degenerate() True sage: H.polygon(P.half_spaces()).is_degenerate() False Finally, we consider polygons with ideal points as degenerate:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: P = H.polygon([ ....: H.vertical(1).left_half_space(), ....: H.vertical(-1).right_half_space() ....: ]) sage: P.is_degenerate() True .. NOTE:: This is not a terribly meaningful notion. This exists mostly because degenerate polygons have a more obvious meaning in Euclidean geometry where this check is used when rendering a polygon as a string. """ if self.parent().polygon(self.half_spaces()) != self: return True return not self.is_finite()
[docs] class HyperbolicSegment(HyperbolicConvexFacade): r""" A segment (possibly infinite) in the hyperbolic plane. This is an abstract base class of :class:`HyperbolicOrientedSegment` and :class:`HyperbolicUnorientedSegment`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing this segment - ``geodesic`` -- the :class:`HyperbolicGeodesic` of which this segment is a subset - ``start`` -- a :class:`HyperbolicPoint` or ``None`` (default: ``None``); the finite endpoint of the segment. If ``None``, then the segment extends all the way to the ideal starting point of the geodesic. - ``end`` -- a :class:`HyperbolicPoint` or ``None`` (default: ``None``); the finite endpoint of the segment. If ``None``, then the segment extends all the way to the ideal end point of the geodesic. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.segment(H.vertical(0), start=I) {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} sage: H(I).segment(oo) {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} .. SEEALSO:: Use :meth:`HyperbolicPlane.segment` or :meth:`HyperbolicPoint.segment` to create segments. """
[docs] def __init__(self, parent, geodesic, start=None, end=None): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: segment = H.segment(H.vertical(0), start=I) sage: isinstance(segment, HyperbolicSegment) True sage: TestSuite(segment).run() sage: segment = segment.unoriented() sage: isinstance(segment, HyperbolicSegment) True sage: TestSuite(segment).run() """ super().__init__(parent) if not isinstance(geodesic, HyperbolicOrientedGeodesic): raise TypeError("geodesic must be an oriented hyperbolic geodesic") if start is not None and not isinstance(start, HyperbolicPoint): raise TypeError("start must be a hyperbolic point") if end is not None and not isinstance(end, HyperbolicPoint): raise TypeError("end must be a hyperbolic point") self._geodesic = geodesic self._start = start self._end = end
def _check(self, require_normalized=True): r""" Verify that this is a valid segment. This implements :meth:`HyperbolicConvexSet._check`. INPUT: - ``require_normalized`` -- a boolean (default: ``True``); whether to assume that normalization has already happened, i.e., segments with no finite end points have been rewritten into geodesics. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicConvexPolygon sage: H = HyperbolicPlane() The end points of the segment must be on the defining geodesic:: sage: s = H.segment(H.vertical(0), start=1, check=False, assume_normalized=True) sage: s._check() Traceback (most recent call last): ... ValueError: start point must be on the geodesic sage: s = H.segment(H.vertical(0), end=1, check=False, assume_normalized=True) sage: s._check() Traceback (most recent call last): ... ValueError: end point must be on the geodesic The end points must be ordered correctly:: sage: s = H.segment(H.vertical(0), start=2*I, end=I, check=False, assume_normalized=True) sage: s._check() Traceback (most recent call last): ... ValueError: end point of segment must not be before start point on the underlying geodesic The end points must be distinct:: sage: s = H.segment(H.vertical(0), start=I, end=I, check=False, assume_normalized=True) sage: s._check(require_normalized=False) sage: s = H.segment(H.vertical(0), start=I, end=I, check=False, assume_normalized=True) sage: s._check(require_normalized=True) Traceback (most recent call last): ... ValueError: end point of segment must be after start point on the underlying geodesic """ if self._start is not None: if self._start not in self._geodesic: raise ValueError("start point must be on the geodesic") if self._end is not None: if self._end not in self._geodesic: raise ValueError("end point must be on the geodesic") start = ( None if self._start is None else self._geodesic.parametrize(self._start, model="euclidean", check=False) ) end = ( None if self._end is None else self._geodesic.parametrize(self._end, model="euclidean", check=False) ) if start is not None and end is not None: if end < start: raise ValueError( "end point of segment must not be before start point on the underlying geodesic" ) if require_normalized and end <= start: raise ValueError( "end point of segment must be after start point on the underlying geodesic" )
[docs] def _normalize(self): r""" Return this set possibly rewritten in a simpler form. This implements :meth:`HyperbolicConvexSet._normalize`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We define a helper method for easier testing:: sage: segment = lambda *args, **kwds: H.segment(*args, **kwds, check=False, assume_normalized=True) A segment that consists of an ideal point, is just that point:: sage: segment(H.vertical(-1), start=H.infinity(), end=H.infinity())._normalize() sage: segment(H.vertical(0), start=H.infinity(), end=None)._normalize() sage: segment(-H.vertical(0), start=None, end=H.infinity())._normalize() A segment that has two ideal end points is a geodesic:: sage: segment(H.vertical(0), start=None, end=H.infinity())._normalize() {-x = 0} sage: segment(-H.vertical(0), start=H.infinity(), end=None)._normalize() {x = 0} Segments that remain segments in normalization:: sage: segment(H.vertical(0), start=I, end=H.infinity())._normalize() {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} sage: segment(-H.vertical(0), start=H.infinity(), end=I)._normalize() {x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} .. NOTE:: This method is not numerically robust and should be improve over inexact rings. """ if self._geodesic.is_ultra_ideal(): return self.parent().empty_set() start = self._start end = self._end def λ(point): return self._geodesic.parametrize(point, model="euclidean", check=False) if start is not None and end is not None: if λ(start) > λ(end): raise ValueError( "end point of segment must be after start point on the underlying geodesic" ) if start is not None: if not start.is_finite(): # We should use a specialized predicate of geometry to make # this more robust over inexact rings. if self.parent().geometry._sgn(λ(start)) > 0: return ( self.parent().empty_set() if start.is_ultra_ideal() else start ) start = None if end is not None: if not end.is_finite(): # We should use a specialized predicate of geometry to make # this more robust over inexact rings. if self.parent().geometry._sgn(λ(end)) < 0: return self.parent().empty_set() if end.is_ultra_ideal() else end end = None if start is None and end is None: segment = self._geodesic if not self.is_oriented(): segment = segment.unoriented() return segment assert (start is None or not start.is_ultra_ideal()) and ( end is None or not end.is_ultra_ideal() ) if start == end: return start return self.parent().segment( self._geodesic, start=start, end=end, check=False, assume_normalized=True, oriented=self.is_oriented(), )
def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the image of this segment under ``isometry``. Helper method for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``) whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We apply an isometry to an oriented segment:: sage: isometry = matrix([[1, -1, 1], [1, 1/2, 1/2], [1, -1/2, 3/2]]) sage: segment = H(I).segment(2*I) sage: segment._apply_isometry_klein(isometry) {-x + 1 = 0} ∩ {2*(x^2 + y^2) - 3*x - 1 ≥ 0} ∩ {(x^2 + y^2) - 3*x - 2 ≤ 0} We apply an isometry of negative determinant to an oriented segment:: sage: isometry = matrix([[-1, 0, 0], [0, 1, 0], [0, 0, 1]]) sage: segment._apply_isometry_klein(isometry) == segment True sage: segment.start().apply_isometry(isometry, model="klein") == segment.start() True sage: segment.end().apply_isometry(isometry, model="klein") == segment.end() True Note that this behavior is different from how the start and end point of a geodesic behave under such an isometry:: sage: segment.geodesic().apply_isometry(isometry, model="klein") == segment.geodesic() False sage: segment.geodesic().apply_isometry(isometry, model="klein").start() == segment.geodesic().end() True sage: segment.geodesic().apply_isometry(isometry, model="klein").end() == segment.geodesic().start() True We can also apply an isometry to an unoriented geodesic:: sage: segment.unoriented()._apply_isometry_klein(isometry) == segment.unoriented() True """ geodesic = self.geodesic()._apply_isometry_klein(isometry, on_right=on_right) if isometry.det().sign() == -1 and geodesic.is_oriented(): geodesic = -geodesic start = ( self._start.apply_isometry(isometry, model="klein", on_right=on_right) if self._start is not None else None ) end = ( self._end.apply_isometry(isometry, model="klein", on_right=on_right) if self._end is not None else None ) return self.parent().segment( geodesic, start=start, end=end, oriented=self.is_oriented() ) def _endpoint_half_spaces(self): r""" Return the half spaces that stop the segment at its endpoints. A helper method for printing and :meth:`half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: s = H.segment(H.half_circle(0, 1), end=I) sage: list(s._endpoint_half_spaces()) [{x ≤ 0}] """ a, b, c = self._geodesic.equation(model="klein") if self._start is not None: x, y = self._start.coordinates(model="klein") yield self.parent().half_space( -c * x + b * y, c, -b, model="klein", check=False ) if self._end is not None: x, y = self._end.coordinates(model="klein") yield self.parent().half_space( c * x - b * y, -c, b, model="klein", check=False ) def _repr_(self): r""" Return a printable representation of this segment. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: H.segment(H.half_circle(0, 1), end=I) {(x^2 + y^2) - 1 = 0} ∩ {x ≤ 0} """ bounds = [repr(self._geodesic)] bounds.extend(repr(half_space) for half_space in self._endpoint_half_spaces()) return " ∩ ".join(bounds)
[docs] def half_spaces(self): r""" Return a minimal set of half spaces whose intersection is this segment. This implements :meth:`HyperbolicConvexSet.half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: segment = H.segment(H.half_circle(0, 1), end=I) sage: segment.half_spaces() {{x ≤ 0}, {(x^2 + y^2) - 1 ≤ 0}, {(x^2 + y^2) - 1 ≥ 0}} """ return self._geodesic.half_spaces() + HyperbolicHalfSpaces( self._endpoint_half_spaces() )
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of this segment. INPUT: - ``model`` -- one of ``"half_plane"`` or ``"klein"`` (default: ``"half_plane"``); in which model to produce the plot See :func:`flatsurf.graphical.hyperbolic.hyperbolic_path` for additional keyword arguments to customize the plot. EXAMPLES: .. jupyter-execute:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: segment = H.segment(H.half_circle(0, 1), end=I) sage: segment.plot() # long time (.25s) ...Graphics object consisting of 1 graphics primitive """ self = self.change(oriented=True) from sage.all import RR kwds["fill"] = False self = self.change_ring(RR) from flatsurf.graphical.hyperbolic import ( hyperbolic_path, HyperbolicPathPlotCommand, ) plot = hyperbolic_path( [ HyperbolicPathPlotCommand("MOVETO", self.start()), HyperbolicPathPlotCommand("LINETO", self.end()), ], model=model, **kwds, ) return self._enhance_plot(plot, model=model)
[docs] def __eq__(self, other): r""" Return whether this segment is indistinguishable from ``other`` (except for scaling in the defining geodesic's equation.) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() Oriented segments are equal if they have the same start and end points:: sage: H(I).segment(2*I) == H(2*I).segment(I) False For an unoriented segment the endpoints must be the same but order does not matter:: sage: H(I).segment(2*I).unoriented() == H(2*I).segment(I).unoriented() True """ if type(self) is not type(other): return False return ( self.geodesic() == other.geodesic() and self.vertices() == other.vertices() )
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a modified copy of this segment. - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`~HyperbolicPlane.base_ring`); the ring over which the new half space will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the new half space. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); must be ``None`` or ``False`` since half spaces cannot have an explicit orientation. See :meth:`~HyperbolicConvexSet.is_oriented`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() We change the ring over which the segment is defined:: sage: s = H(I).segment(2*I) sage: s.change(ring=AA) {-x = 0} ∩ {3/5*(x^2 + y^2) - 3/5 ≥ 0} ∩ {6/25*(x^2 + y^2) - 24/25 ≤ 0} We make the segment unoriented:: sage: s.change(oriented=False).is_oriented() False We pick a (somewhat) random orientation of an unoriented segment:: sage: s.unoriented().change(oriented=True).is_oriented() True """ if ring is not None or geometry is not None: start = ( self._start.change(ring=ring, geometry=geometry) if self._start is not None else None ) end = ( self._end.change(ring=ring, geometry=geometry) if self._end is not None else None ) self = ( self.parent() .change_ring(ring=ring, geometry=geometry) .segment( self._geodesic.change(ring=ring, geometry=geometry), start=start, end=end, check=False, assume_normalized=True, oriented=self.is_oriented(), ) ) if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): self = self.parent().segment( self._geodesic, start=self._start, end=self._end, check=False, assume_normalized=True, oriented=oriented, ) return self
[docs] def geodesic(self): r""" Return the geodesic on which this segment lies. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) sage: s.geodesic() {-x = 0} Since the segment is oriented, the geodesic is also oriented:: sage: s.is_oriented() True sage: s.geodesic().is_oriented() True sage: s.unoriented().geodesic().is_oriented() False .. SEEALSO:: geodesics also implement this method so that segments and geodesics can be treated uniformly, see :meth:`HyperbolicGeodesic.geodesic` """ geodesic = self._geodesic if not self.is_oriented(): geodesic = geodesic.unoriented() return geodesic
[docs] def vertices(self, marked_vertices=True): r""" Return the end poinst of this segment. INPUT: - ``marked_vertices`` -- a boolean (default: ``True``), ignored since a segment cannot have marked vertices. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) sage: s.vertices() {I, 2*I} Note that iteration in the set is not consistent with the orientation of the segment (it is chosen such that the subset relation on vertices can be checked quickly):: sage: (-s).vertices() {I, 2*I} Use :meth:`~HyperbolicOrientedSegment.start` and :meth:`~HyperbolicOrientedSegment.end` to get the vertices in an order that is consistent with the orientation:: sage: s.start(), s.end() (I, 2*I) sage: (-s).start(), (-s).end() (2*I, I) Both finite and ideal end points of the segment are returned:: sage: s = H(-1).segment(I) sage: s.vertices() {-1, I} .. SEEALSO:: :meth:`HyperbolicConvexSet.vertices` for more details. """ self = self.change(oriented=True) return HyperbolicVertices([self.start(), self.end()])
[docs] def dimension(self): r""" Return the dimension of this segment, i.e., 1. This implements :meth:`HyperbolicConvexSet.dimension`. Note that this also returns 1 if the actual dimension of the segment is smaller. This is, however, only possible for segments created with :meth:`HyperbolicPlane.segment` setting ``check=False``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H(I).segment(2*I).dimension() 1 """ from sage.all import ZZ return ZZ(1)
[docs] def midpoint(self): r""" Return the midpoint of this segment. ALGORITHM: We use the construction as explained on `Wikipedia <https://en.wikipedia.org/wiki/Beltrami%E2%80%93Klein_model#Compass_and_straightedge_constructions>`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: s = H(I).segment(4*I) sage: s.midpoint() 2*I :: sage: K.<a> = NumberField(x^2 - 2, embedding=1.414) sage: H = HyperbolicPlane(K) sage: s = H(I - 1).segment(I + 1) sage: s.midpoint() a*I .. SEEALSO:: :meth:`HyperbolicSegment.perpendicular` for the perpendicular bisector """ start, end = self.vertices() if start == end: return start if not start.is_finite() and not end.is_finite(): return self.geodesic().midpoint() if not start.is_finite() or not end.is_finite(): raise NotImplementedError( f"cannot compute midpoint of unbounded segment {self}" ) for p in self.geodesic().perpendicular(start).vertices(): for q in self.geodesic().perpendicular(end).vertices(): line = self.parent().geodesic(p, q) intersection = self.intersection(line) if intersection: return intersection # One of the two lines start at any p must intersect the segment # already. No need to check the other p. assert ( False ), f"segment {self} must have a midpoint but the straightedge and compass construction did not yield any"
[docs] def perpendicular(self, point=None): r""" Return the geodesic through ``point`` that is perpendicular to this segment. If no point is given, return the perpendicular bisector of this segment. ALGORITHM: See :meth:`HyperbolicGeodesic.perpendicular`. INPUT: - ``point`` -- a point on this segment or ``None`` (the default) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicSegment sage: H = HyperbolicPlane() sage: s = H(I).segment(4*I) sage: s.perpendicular() {(x^2 + y^2) - 4 = 0} sage: s.perpendicular(I) {(x^2 + y^2) - 1 = 0} :: sage: K.<a> = NumberField(x^2 - 2, embedding=1.414) sage: H = HyperbolicPlane(K) sage: s = H(I - 1).segment(I + 1) sage: s.perpendicular() {x = 0} sage: s.perpendicular(I - 1) {4/3*(x^2 + y^2) + 16/3*x + 8/3 = 0} """ if point is None: point = self.midpoint() point = self.parent()(point) if point not in self: raise ValueError( f"point must be in the segment but {point} is not in {self}" ) return self.geodesic().perpendicular(point)
[docs] class HyperbolicUnorientedSegment(HyperbolicSegment): r""" An unoriented (possibly infinity) segment in the hyperbolic plane. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: segment = H.segment(H.vertical(0), start=I).unoriented() TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicUnorientedSegment sage: isinstance(segment, HyperbolicUnorientedSegment) True .. SEEALSO:: Use :meth:`HyperbolicPlane.segment` or :meth:`~HyperbolicConvexSet.unoriented` to construct unoriented segments. """
[docs] def __hash__(self): r""" Return a hash value for this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) Since an oriented segment is hashable, it can be put in a hash table, such as a Python ``set``:: sage: {s.unoriented(), (-s).unoriented()} {{-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0}} """ return hash((frozenset([self._start, self._end]), self.geodesic()))
[docs] def _isometry_conditions(self, other): r""" Return an iterable of primitive pairs that must map to each other in an isometry that maps this set to ``other``. Helper method for :meth:`HyperbolicPlane._isometry_conditions`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I).unoriented() sage: conditions = s._isometry_conditions(s) sage: list(conditions) [[({-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0}, {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0})], [({-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0}, {x = 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0})]] .. SEEALSO:: :meth:`HyperbolicConvexSet._isometry_conditions` for a general description. r""" self = self.change(oriented=True) other = other.change(oriented=True) yield [(self, other)] yield [(self, -other)]
[docs] @classmethod def random_set(cls, parent): r""" Return a random unoriented segment. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the segment EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicUnorientedSegment sage: x = HyperbolicUnorientedSegment.random_set(H) sage: x.dimension() 1 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ return HyperbolicOrientedSegment.random_set(parent).unoriented()
[docs] class HyperbolicOrientedSegment(HyperbolicSegment, HyperbolicOrientedConvexSet): r""" An oriented (possibly infinite) segment in the hyperbolic plane such as a boundary edge of a :class:`HyperbolicConvexPolygon`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicOrientedSegment sage: isinstance(s, HyperbolicOrientedSegment) True .. SEEALSO:: Use :meth:`HyperbolicPlane.segment` or :meth:`HyperbolicPoint.segment` to construct oriented segments. """
[docs] def __neg__(self): r""" Return this segment with its orientation reversed. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) sage: s {-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0} sage: -s {x = 0} ∩ {(x^2 + y^2) - 4 ≤ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} """ return self.parent().segment( -self._geodesic, self._end, self._start, check=False, assume_normalized=True )
[docs] def __hash__(self): r""" Return a hash value for this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) Since this set is hashable, it can be put in a hash table, such as a Python ``set``:: sage: {s} {{-x = 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∩ {(x^2 + y^2) - 4 ≤ 0}} """ return hash((self._start, self._end, self.geodesic()))
[docs] def _isometry_conditions(self, other): r""" Return an iterable of primitive pairs that must map to each other in an isometry that maps this set to ``other``. Helper method for :meth:`HyperbolicPlane._isometry_conditions`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) sage: conditions = s._isometry_conditions(s) sage: list(conditions) [[(I, I), (2*I, 2*I)]] .. SEEALSO:: :meth:`HyperbolicConvexSet._isometry_conditions` for a general description. r""" yield [(self.start(), other.start()), (self.end(), other.end())]
[docs] def start(self): r""" Return the start point of this segment. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) sage: s.start() I sage: s.start().is_finite() True The start point can also be an ideal point:: sage: s = H(0).segment(2*I) sage: s.start() 0 sage: s.start().is_ideal() True .. SEEALSO:: :meth:`end` """ if self._start is not None: return self._start return self._geodesic.start()
[docs] def end(self): r""" Return the end point of this segment. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: s = H(I).segment(2*I) sage: s.end() 2*I sage: s.end().is_finite() True The end point can also be an ideal point:: sage: s = H(I).segment(oo) sage: s.end() sage: s.end().is_ideal() True .. SEEALSO:: :meth:`start` """ if self._end is not None: return self._end return self._geodesic.end()
[docs] @classmethod def random_set(cls, parent): r""" Return a random oriented segment. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` containing the segment EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicOrientedSegment sage: x = HyperbolicOrientedSegment.random_set(H) sage: x.dimension() 1 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ a = HyperbolicPointFromCoordinates.random_set(parent) b = HyperbolicPointFromCoordinates.random_set(parent) while a == b or (not a.is_finite() and not b.is_finite()): b = HyperbolicPointFromCoordinates.random_set(parent) return parent.segment(parent.geodesic(a, b), start=a, end=b)
[docs] class HyperbolicEmptySet(HyperbolicConvexFacade): r""" The empty subset of the hyperbolic plane. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` this is the empty set of EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set() {} TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicEmptySet sage: ø = H.empty_set() sage: isinstance(ø, HyperbolicEmptySet) True sage: TestSuite(ø).run() .. SEEALSO:: Use :meth:`HyperbolicPlane.empty_set` to construct the empty set. """
[docs] def __eq__(self, other): r""" Return whether this empty set is indistinguishable from ``other``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set() == H.empty_set() True sage: H.empty_set() == HyperbolicPlane(AA).empty_set() False """ return isinstance(other, HyperbolicEmptySet) and self.parent() == other.parent()
[docs] def some_elements(self): r""" Return some representative points of this set for testing. EXAMPLES: Since this set is empty, there are no points to return:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().some_elements() [] """ return []
def _test_an_element(self, **options): r""" Do not run tests on an element of this empty set (disabling the generic tests run by all parents otherwise.) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._test_an_element() """ def _test_elements(self, **options): r""" Do not run any tests on the elements of this empty set (disabling the generic tests run by all parents otherwise.) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H._test_elements() """ def _repr_(self): r""" Return a printable representation of the empty set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set() {} """ return "{}" def _apply_isometry_klein(self, isometry, on_right=False): r""" Return the result of applying the ``isometry`` to the empty set, i.e., the empty set. Helper method for :meth:`HyperbolicConvexSet.apply_isometry`. INPUT: - ``isometry`` -- a 3×3 matrix over the :meth:`~HyperbolicPlane.base_ring` describing an isometry in the hyperboloid model. - ``on_right`` -- a boolean (default: ``False``) whether to return the result of the right action. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: isometry = matrix([[1, -1, 1], [1, 1/2, 1/2], [1, -1/2, 3/2]]) sage: H.empty_set()._apply_isometry_klein(isometry) == H.empty_set() True """ return self
[docs] def plot(self, model="half_plane", **kwds): r""" Return a plot of the empty set. INPUT: - ``model`` -- one of ``"half_plane"`` and ``"klein"`` (default: ``"half_plane"``) Any keyword arguments are ignored. EXAMPLES: .. jupyter-execute:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().plot() ...Graphics object consisting of 0 graphics primitives """ from sage.all import Graphics return self._enhance_plot(Graphics(), model=model)
[docs] def dimension(self): r""" Return the dimension of this set; returns -1 for the empty set. This implements :meth:`HyperbolicConvexSet.dimension`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().dimension() -1 """ from sage.all import ZZ return ZZ(-1)
[docs] def half_spaces(self): r""" Return a minimal set of half spaces whose intersection is empty. This implements :meth:`HyperbolicConvexSet.half_spaces`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().half_spaces() {{(x^2 + y^2) + 4*x + 3 ≤ 0}, {(x^2 + y^2) - 4*x + 3 ≤ 0}} """ return HyperbolicHalfSpaces( [ self.parent().half_circle(-2, 1).right_half_space(), self.parent().half_circle(2, 1).right_half_space(), ] )
[docs] def change(self, ring=None, geometry=None, oriented=None): r""" Return a copy of the empty set. INPUT: - ``ring`` -- a ring (default: ``None`` to keep the current :meth:`HyperbolicPlane.base_ring`); the ring over which the empty set will be defined. - ``geometry`` -- a :class:`HyperbolicGeometry` (default: ``None`` to keep the current geometry); the geometry that will be used for the empty set. - ``oriented`` -- a boolean (default: ``None`` to keep the current orientedness); must be ``None`` or ``False`` since the empty set cannot have an explicit orientation. EXAMPLES: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().change(ring=AA) == HyperbolicPlane(AA).empty_set() True sage: H.empty_set().change(oriented=True) Traceback (most recent call last): ... NotImplementedError: cannot change orientation of empty set """ if ring is not None: self = self.parent().change_ring(ring, geometry=geometry).empty_set() if oriented is None: oriented = self.is_oriented() if oriented != self.is_oriented(): raise NotImplementedError("cannot change orientation of empty set") return self
[docs] def __hash__(self): r""" Return a hash value for this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() Since this set is hashable, it can be put in a hash table, such as a Python ``set``:: sage: {H.empty_set()} {{}} """ return 0
[docs] def vertices(self, marked_vertices=True): r""" Return the vertices of this empty, i.e., an empty set of points. INPUT: - ``marked_vertices`` -- a boolean (default: ``True``), ignored EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().vertices() {} """ return HyperbolicVertices([])
def _an_element_(self): """ Return a point in this set, i.e., raise an exception since there are no points. See :meth:`HyperbolicConvexSet._an_element_` for more interesting examples of this method. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.empty_set().an_element() Traceback (most recent call last): ... Exception: empty set has no points """ raise Exception("empty set has no points")
[docs] @classmethod def random_set(cls, parent): r""" Return a random empty set, i.e., the empty set. This implements :meth:`HyperbolicConvexSet.random_set`. INPUT: - ``parent`` -- the :class:`HyperbolicPlane` this is the empty set of. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: from flatsurf.geometry.hyperbolic import HyperbolicEmptySet sage: x = HyperbolicEmptySet.random_set(H) sage: x.dimension() -1 .. SEEALSO:: :meth:`HyperbolicPlane.random_element` """ return parent.empty_set()
[docs] class OrderedSet(collections.abc.Set): r""" A set of objects sorted by :meth:`OrderedSet._lt_`. This is used to efficiently represent :meth:`HyperbolicConvexSet.half_spaces`, :meth:`HyperbolicConvexSet.vertices`, and :meth:`HyperbolicConvexSet.edges`. In particular, it allows us to create and merge such sets in linear time. This is an abstract base class for specialized sets such as :class:`HyperbolicHalfSpaces`, :class:`HyperbolicVertices`, and :class:`HyperbolicEdges`. INPUT: - ``entries`` -- an iterable, the elements of this set - ``assume_sorted`` -- a boolean or ``"rotated"`` (default: ``True``); whether to assume that the ``entries`` are already sorted with respect to :meth:`_lt_`. If ``"rotated"``, we assume that the entries are sorted modulo a cyclic permutation. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: segment = H(I).segment(2*I) sage: segment.vertices() {I, 2*I} """
[docs] def __init__(self, entries, assume_sorted=None): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import OrderedSet sage: H = HyperbolicPlane() sage: vertices = H(I).segment(2*I).vertices() sage: isinstance(vertices, OrderedSet) True """ if assume_sorted is None: assume_sorted = isinstance(entries, OrderedSet) if assume_sorted == "rotated": min = 0 for i, entry in enumerate(entries): if self._lt_(entry, entries[min]): min = i entries = entries[min:] + entries[:min] assume_sorted = True if not assume_sorted: entries = self._merge(*[[entry] for entry in entries]) self._entries = tuple(entries)
[docs] def _lt_(self, lhs, rhs): r""" Return whether ``lhs`` should come before ``rhs`` in this set. Subclasses must implement this method. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import OrderedSet sage: H = HyperbolicPlane() sage: vertices = H(I).segment(2*I).vertices() sage: vertices._lt_(vertices[0], vertices[1]) True """ raise NotImplementedError
[docs] def _merge(self, *sets): r""" Return the merge of sorted lists of ``sets`` using merge sort. Note that this set itself is not part of the merge. Naturally, when there are a lot of small sets, such a merge sort takes quasi-linear time. However, when there are only a few sets, this runs in linear time. INPUT: - ``sets`` -- iterables that are sorted with respect to :meth:`_lt_`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: H = HyperbolicPlane() sage: HyperbolicHalfSpaces([])._merge() [] sage: HyperbolicHalfSpaces([])._merge([H.vertical(0).left_half_space()], [H.vertical(0).left_half_space()]) [{x ≤ 0}] sage: HyperbolicHalfSpaces([])._merge(*[[half_space] for half_space in H.real(0).half_spaces()]) [{(x^2 + y^2) + x ≤ 0}, {x ≥ 0}] sage: HyperbolicHalfSpaces([])._merge(list(H.real(0).half_spaces()), list(H.real(0).half_spaces())) [{(x^2 + y^2) + x ≤ 0}, {x ≥ 0}] sage: HyperbolicHalfSpaces([])._merge(*[[half_space] for half_space in list(H.real(0).half_spaces()) * 2]) [{(x^2 + y^2) + x ≤ 0}, {x ≥ 0}] """ # A standard merge-sort implementation. count = len(sets) if count == 0: return [] if count == 1: return sets[0] # The non-trivial base case. if count == 2: A = sets[0] B = sets[1] merged = [] while A and B: if self._lt_(A[-1], B[-1]): merged.append(B.pop()) elif self._lt_(B[-1], A[-1]): merged.append(A.pop()) else: # Drop duplicate from set A.pop() merged.reverse() return A + B + merged # Divide & Conquer recursively. return self._merge( *(self._merge(*sets[: count // 2]), self._merge(*sets[count // 2 :])) )
[docs] def __eq__(self, other): r""" Return whether this set is equal to ``other``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).vertices() == (-H.vertical(0)).vertices() True """ if type(other) is not type(self): other = type(self)(other) return self._entries == other._entries
[docs] def __ne__(self, other): r""" Return whether this set is not equal to ``other``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).vertices() != H.vertical(1).vertices() True """ return not (self == other)
[docs] def __hash__(self): r""" Return a hash value for this set that is consistent with :meth:`__eq__` and :meth:`__ne__`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: hash(H.vertical(0).vertices()) != hash(H.vertical(1).vertices()) True """ return hash(tuple(self._entries))
[docs] def __add__(self, other): r""" Return the :meth:`_merge` of this set and ``other``. INPUT: - ``other`` -- another set of the same kind EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.vertical(0).half_spaces() + H.vertical(1).half_spaces() {{x ≤ 0}, {x - 1 ≤ 0}, {x ≥ 0}, {x - 1 ≥ 0}} """ if type(self) is not type(other): raise TypeError("both sets must be of the same type") entries = self._merge(list(self._entries), list(other._entries)) return type(self)(entries, assume_sorted=True)
[docs] def __repr__(self): r""" Return a printable representation of this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: H.half_circle(0, 1).vertices() {-1, 1} """ return "{" + repr(self._entries)[1:-1] + "}"
[docs] def __iter__(self): r""" Return an iterator of this set. Iteration happens in sorted order, consistent with :meth:`_lt_`. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: vertices = H.half_circle(0, 1).vertices() sage: list(vertices) [-1, 1] """ return iter(self._entries)
[docs] def __len__(self): r""" Return the cardinality of this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: vertices = H.half_circle(0, 1).vertices() sage: len(vertices) 2 """ return len(self._entries)
[docs] def pairs(self, repeat=False): r""" Return an iterable that iterates over all consecutive pairs of elements in this set; including the pair formed by the last element and the first element. INPUT: - ``repeat`` -- a boolean (default: ``False``); whether to produce pair consisting of the first element twice if there is only one element EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: vertices = H.half_circle(0, 1).vertices() sage: list(vertices.pairs()) [(1, -1), (-1, 1)] .. SEEALSO:: :meth:`triples` """ if len(self._entries) <= 1 and not repeat: return for i in range(len(self._entries)): yield self._entries[i - 1], self._entries[i]
[docs] def triples(self, repeat=False): r""" Return an iterable that iterates over all consecutive triples of elements in this set; including the triples formed by wrapping around the end of the set. INPUT: - ``repeat`` -- a boolean (default: ``False``); whether to produce triples by wrapping around even if there are fewer than three elements EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() There must be at least three elements to form any triples:: sage: vertices = H.half_circle(0, 1).vertices() sage: list(vertices.triples()) [] sage: half_spaces = H(I).segment(2*I).half_spaces() sage: list(half_spaces.triples()) [({(x^2 + y^2) - 1 ≥ 0}, {x ≤ 0}, {(x^2 + y^2) - 4 ≤ 0}), ({x ≤ 0}, {(x^2 + y^2) - 4 ≤ 0}, {x ≥ 0}), ({(x^2 + y^2) - 4 ≤ 0}, {x ≥ 0}, {(x^2 + y^2) - 1 ≥ 0}), ({x ≥ 0}, {(x^2 + y^2) - 1 ≥ 0}, {x ≤ 0})] However, we can force triples to be produced by wrapping around with ``repeat``:: sage: vertices = H.half_circle(0, 1).vertices() sage: list(vertices.triples(repeat=True)) [(1, -1, 1), (-1, 1, -1)] .. SEEALSO:: :meth:`pairs` """ if len(self._entries) <= 2 and not repeat: return for i in range(len(self._entries)): yield self._entries[i - 1], self._entries[i], self._entries[ (i + 1) % len(self._entries) ]
[docs] def __getitem__(self, *args, **kwargs): r""" Return items from this set by index. INPUT: Any arguments that can be used to access a tuple are accepted. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: vertices = H.half_circle(0, 1).vertices() sage: vertices[0] -1 """ return self._entries.__getitem__(*args, **kwargs)
[docs] def __contains__(self, x): r""" Return whether this set contains ``x``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: vertices = H.half_circle(0, 1).vertices() sage: H(0) in vertices False sage: H(1) in vertices True .. NOTE:: Presently, this method is not used. It only exists to satisfy the conditions of the Python abc for sets. It could be implemented more efficiently. """ return x in self._entries
[docs] class HyperbolicVertices(OrderedSet): r""" A set of vertices on the boundary of a convex set in the hyperbolic plane, sorted in counterclockwise order. INPUT: - ``vertices`` -- an iterable of :class:`HyperbolicPoint`, the vertices of this set - ``assume_sorted`` -- a boolean or ``"rotated"`` (default: ``True``); whether to assume that the ``vertices`` are already sorted with respect to :meth:`_lt_`. If ``"rotated"``, we assume that the vertices are sorted modulo a cyclic permutation. ALGORITHM: We keep vertices sorted in counterclockwise order relative to a fixed reference vertex (the leftmost and bottommost in the Klein model.) EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: V = H.vertical(0).vertices() sage: V {0, ∞} Note that in this example, ``0`` is chosen as the reference point:: sage: V._start 0 TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicVertices sage: isinstance(V, HyperbolicVertices) True .. SEEALSO:: :meth:`HyperbolicConvexSet.vertices` to obtain such a set """
[docs] def __init__(self, vertices, assume_sorted=None): r""" TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicVertices sage: H = HyperbolicPlane() sage: V = H.vertical(0).vertices() sage: isinstance(V, HyperbolicVertices) True """ vertices = list(vertices) if len(vertices) != 0: if assume_sorted or len(vertices) == 1: self._start = vertices[0] else: # We sort vertices in counterclockwise order. We need to fix a starting # point consistently, namely we choose the leftmost point in the Klein # model and if there are ties the one with minimal y coordinate. # That way we can then order vertices by their slopes with the starting # point to get a counterclockwise walk. # A very common special case is that we are presented with two # end points of a geodesic. This is a hack but getting the # consistent ordering to work with a mix of points without # using their coordinates is a lot of work. if ( len(vertices) == 2 and isinstance(vertices[0], HyperbolicPointFromGeodesic) and isinstance(vertices[1], HyperbolicPointFromGeodesic) and vertices[0]._geodesic == -vertices[1]._geodesic ): geodesic = vertices[0]._geodesic # Note that we should use more robust predicates from the "geometry" to # make this work more reliably over inexact rings. if geodesic.parent().geometry._zero(geodesic._c): # This is a vertical in the Klein model. Both end points have the # same x coordinate. if geodesic._b > 0: # This vertical is oriented downwards, so the end point has the # minimal y coordinate. vertices.reverse() elif geodesic._c < 0: # This geodesic points right-to-left in the Klein model. The # end point has minimal x coordinate. vertices.reverse() self._start = vertices[0] assume_sorted = True else: self._start = min( vertices, key=lambda vertex: vertex.coordinates(model="klein") ) # _lt_ needs to know the global structure of the convex hull of the vertices. # The base class constructor will replace _entries with a sorted version of _entries. self._entries = tuple(vertices) super().__init__(vertices, assume_sorted=assume_sorted)
[docs] def _merge(self, *sets): r""" Return the merge of sorted lists of ``sets``. Note that this set itself is not part of the merge (but its reference point is used.) INPUT: - ``sets`` -- iterables that are sorted with respect to :meth:`_lt_`. .. WARNING:: For this to work correctly, the result of the merge must eventually have the reference point of this set as its reference point. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: H = HyperbolicPlane() sage: V = H.vertical(0).vertices() sage: V._merge([H(1)], [H(0)], [H(oo)]) [0, 1, ∞] """ return super()._merge(*sets)
def _slope(self, vertex): r""" Return the slope of ``vertex`` with respect to the chosen reference vertex of this set as a tuple (Δy, Δx). EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: H = HyperbolicPlane() sage: V = H.vertical(0).vertices() We compute the Euclidean slope from 0 to 1 in the Klein model:: sage: V._slope(H(1)) (1, 1) """ sx, sy = self._start.coordinates(model="klein") x, y = vertex.coordinates(model="klein") return (y - sy, x - sx)
[docs] def _lt_(self, lhs, rhs): r""" Return whether ``lhs`` should come before ``rhs`` in this set. INPUT: - ``lhs`` -- a :class:`HyperbolicPoint` - ``rhs`` -- a :class:`HyperbolicPoint` EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: H = HyperbolicPlane() sage: V = H.vertical(0).vertices() We find that we go counterclockwise from 1 to ∞ when seen from 0 in the Klein model:: sage: V._lt_(H(oo), H(1)) False """ if lhs == self._start: return True if rhs == self._start: return False if lhs == rhs: return False dy_lhs, dx_lhs = self._slope(lhs) dy_rhs, dx_rhs = self._slope(rhs) assert ( dx_lhs >= 0 and dx_rhs >= 0 ), "all points must be to the right of the starting point due to chosen normalization" if dy_lhs * dx_rhs < dy_rhs * dx_lhs: return True if dy_lhs * dx_rhs > dy_rhs * dx_lhs: return False # The points (start, lhs, rhs) are collinear. # In general we cannot decide their order with only looking at start, # lhs, and rhs. We need to understand where the rest of the convex hull # lives. assert ( lhs in self._entries and rhs in self._entries ), "cannot compare vertices that are not defining for the convex hull" # If there is any vertex with a bigger slope, then this line is at the # start of the walk in counterclockwise order. for vertex in self._entries: dy, dx = self._slope(vertex) if dy * dx_rhs > dy_rhs * dx: return dx_lhs < dx_rhs elif dy * dx_rhs < dy_rhs * dx: return dx_lhs > dx_rhs raise ValueError( "cannot decide counterclockwise ordering of exactly three collinear points" )
[docs] class HyperbolicHalfSpaces(OrderedSet): r""" A set of half spaces in the hyperbolic plane ordered counterclockwise. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: half_spaces = H.vertical(0).half_spaces() sage: half_spaces {{x ≤ 0}, {x ≥ 0}} TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: isinstance(half_spaces, HyperbolicHalfSpaces) True .. SEEALSO:: :meth:`HyperbolicConvexSet.half_spaces` to obtain such a set """
[docs] @classmethod def _lt_(cls, lhs, rhs): r""" Return whether the half space ``lhs`` is smaller than ``rhs`` in a cyclic ordering of normal vectors, i.e., order half spaces by whether their normal points to the left/right, the slope of the geodesic, and finally by containment. This ordering is such that :meth:`HyperbolicPlane.intersection` can be computed in linear time for two hyperbolic convex sets. INPUT: - ``lhs`` -- a :class:`HyperbolicHalfSpace` - ``rhs`` -- a :class:`HyperbolicHalfSpace` .. NOTE:: The implementation is not very robust over inexact rings and should be improved for that use case. TESTS:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: H = HyperbolicPlane() A half space is equal to itself:: sage: HyperbolicHalfSpaces._lt_(H.vertical(0).left_half_space(), H.vertical(0).left_half_space()) False A half space whose normal in the Klein model points to the left is smaller than one whose normal points to the right:: sage: HyperbolicHalfSpaces._lt_(H.vertical(1).left_half_space(), H.half_circle(0, 1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.vertical(0).left_half_space(), -H.vertical(0).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(-H.half_circle(-1, 1).left_half_space(), -H.vertical(1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(-H.half_circle(-1, 1).left_half_space(), -H.vertical(1/2).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.vertical(1).left_half_space(), H.half_circle(-1, 1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.vertical(1/2).left_half_space(), H.half_circle(-1, 1).left_half_space()) True Half spaces are ordered by the slope of their normal in the Klein model:: sage: HyperbolicHalfSpaces._lt_(H.vertical(-1).left_half_space(), H.vertical(1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(-H.half_circle(-1, 1).left_half_space(), H.vertical(1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.half_circle(-1, 1).left_half_space(), -H.vertical(1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.vertical(0).left_half_space(), H.vertical(1).left_half_space()) True Parallel half spaces in the Klein model are ordered by inclusion:: sage: HyperbolicHalfSpaces._lt_(-H.half_circle(-1, 1).left_half_space(), H.vertical(1/2).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(-H.vertical(1/2).left_half_space(), H.half_circle(-1, 1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.half_circle(0, 2).left_half_space(), H.half_circle(0, 1).left_half_space()) True sage: HyperbolicHalfSpaces._lt_(H.half_circle(0, 1).right_half_space(), H.half_circle(0, 2).right_half_space()) True Verify that comparisons are projective:: sage: HyperbolicHalfSpaces._lt_(H.geodesic(5, -5, -1, model="half_plane").left_half_space(), H.geodesic(5/13, -5/13, -1/13, model="half_plane").left_half_space()) False sage: HyperbolicHalfSpaces._lt_(H.geodesic(5/13, -5/13, -1/13, model="half_plane").left_half_space(), H.geodesic(5, -5, -1, model="half_plane").left_half_space()) False """ a, b, c = lhs.equation(model="klein") aa, bb, cc = rhs.equation(model="klein") def normal_points_left(b, c): return b < 0 or (b == 0 and c < 0) if normal_points_left(b, c) != normal_points_left(bb, cc): # The normal vectors of the half spaces in the Klein model are in # different half planes, one is pointing left, one is pointing # right. return normal_points_left(b, c) # The normal vectors of the half spaces in the Klein model are in the # same half plane, so we order them by slope. if b * bb == 0: if b == bb: # The normals are vertical and in the same half plane, so # they must be equal. We will order the half spaces by # inclusion later. cmp = 0 else: # Exactly one of the normals is vertical; we order half spaces # such that that one is bigger. return bb == 0 else: # Order by the slope of the normal. cmp = (b * bb).sign() * (c * bb - cc * b).sign() if cmp == 0: # The half spaces are parallel in the Klein model. We order them by # inclusion, i.e., by the offset in direction of the normal. if c * cc: cmp = c.sign() * (a * cc - aa * c).sign() else: assert b * bb cmp = b.sign() * (a * bb - aa * b).sign() return cmp < 0
[docs] @staticmethod def convex_hull(vertices): r""" Return the convex hull of ``vertices`` as a ordered set of half spaces. INPUT: - ``vertices`` -- a sequence of :class:`HyperbolicPoint` ALGORITHM: We use the classical Euclidean Graham scan algorithm in the Klein model. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: from flatsurf.geometry.hyperbolic import HyperbolicHalfSpaces sage: H = HyperbolicPlane() sage: HyperbolicHalfSpaces.convex_hull([H(0), H(1), H(oo)]) {{(x^2 + y^2) - x ≥ 0}, {x - 1 ≤ 0}, {x ≥ 0}} sage: HyperbolicHalfSpaces.convex_hull([H(0), H(1), H(I), H(oo)]) {{(x^2 + y^2) - x ≥ 0}, {x - 1 ≤ 0}, {x ≥ 0}} sage: HyperbolicHalfSpaces.convex_hull([H(0), H(1), H(I), H(I + 1), H(oo)]) {{(x^2 + y^2) - x ≥ 0}, {x - 1 ≤ 0}, {x ≥ 0}} sage: HyperbolicHalfSpaces.convex_hull([H(1/2), H(-1/2), H(1), H(I), H(I + 1), H(oo)]) {{2*(x^2 + y^2) - 3*x + 1 ≥ 0}, {x - 1 ≤ 0}, {2*x + 1 ≥ 0}, {4*(x^2 + y^2) - 1 ≥ 0}} .. SEEALSO:: :meth:`HyperbolicPlane.convex_hull` """ if not vertices: # We cannot return empty_set() because we do not know the HyperbolicPlane this lives in. raise NotImplementedError( "cannot compute convex hull of empty set of vertices" ) H = vertices[0].parent() vertices = set(vertices) if len(vertices) == 1: return next(iter(vertices)).half_spaces() vertices = [vertex.coordinates(model="klein") for vertex in vertices] reference = min(vertices) class Slope: def __init__(self, xy): self.dx = xy[0] - reference[0] self.dy = xy[1] - reference[1] def __eq__(self, other): if self.dx == 0 and self.dy == 0: return other.dx == 0 and other.dy == 0 # Return whether the two points have the same slope relative to "reference" return self.dy * other.dx == other.dy * self.dx def __lt__(self, other): # Return whether the self has smaller slope relative to "reference" or if self.dy * other.dx < other.dy * self.dx: return True if self.dy * other.dx > other.dy * self.dx: return False # if slopes are the same, sort by distance if self.dx**2 + self.dy**2 < other.dx**2 + other.dy**2: return True return False vertices.sort(key=Slope) assert vertices[0] == reference # Drop collinear points filtered = [] for i, vertex in enumerate(vertices): if i + 1 == len(vertices): filtered.append(vertex) continue slope = Slope(vertex) next_slope = Slope(vertices[i + 1]) if slope == next_slope: continue filtered.append(vertex) continue hull = [] def ccw(A, B, C): r""" Return whether the vectors A->B, B->C describe a counter-clockwise turn. """ return (B[0] - A[0]) * (C[1] - B[1]) > (C[0] - B[0]) * (B[1] - A[1]) for vertex in filtered: while len(hull) >= 2 and not ccw(hull[-2], hull[-1], vertex): hull.pop() hull.append(vertex) assert hull[0] == reference hull = [H.point(*xy, model="klein") for xy in hull] half_spaces = [] for i in range(len(hull)): half_spaces.append(H.geodesic(hull[i - 1], hull[i]).left_half_space()) return HyperbolicHalfSpaces(half_spaces)
[docs] class HyperbolicEdges(OrderedSet): r""" A set of hyperbolic segments and geodesics ordered counterclockwise. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: edges = H.vertical(0).edges() sage: edges {{-x = 0}, {x = 0}} TESTS:: sage: from flatsurf.geometry.hyperbolic import HyperbolicEdges sage: isinstance(edges, HyperbolicEdges) True .. SEEALSO:: :meth:`HyperbolicConvexSet.edges` to obtain such a set """
[docs] @classmethod def _lt_(cls, lhs, rhs): r""" Return whether ``lhs`` should come before ``rhs`` in the ordering of this set. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() sage: edges = H.vertical(0).edges() sage: edges._lt_(edges[0], edges[1]) True Segments on the same edge are ordered correctly:: sage: segments = [ ....: H(0).segment(H(I)), ....: H(I).segment(H(2*I)), ....: H(2*I).segment(H(oo)) ....: ] sage: edges._lt_(segments[0], segments[1]) True sage: edges._lt_(segments[0], segments[2]) True sage: edges._lt_(segments[1], segments[2]) True sage: edges._lt_(segments[2], segments[1]) False sage: edges._lt_(segments[2], segments[0]) False sage: edges._lt_(segments[1], segments[0]) False """ lhs_geodesic = lhs if isinstance(lhs, HyperbolicOrientedSegment): lhs_geodesic = lhs.geodesic() rhs_geodesic = rhs if isinstance(rhs, HyperbolicOrientedSegment): rhs_geodesic = rhs.geodesic() if HyperbolicHalfSpaces._lt_( lhs_geodesic.left_half_space(), rhs_geodesic.left_half_space() ): return True if lhs_geodesic != rhs_geodesic: return False if lhs == rhs: return False # The geodesics containing the edges are the same but they are not the # same segments. We compare the finite points on the segment to decide # which edge comes first in counterclockwise order. geodesic = lhs_geodesic if lhs.start().is_ideal(): if rhs.start().is_ideal(): assert ( not lhs.end().is_ideal() and not rhs.end().is_ideal() ), "edges in a set of HyperbolicEdges must be sortable" assert ( lhs.end() != rhs.end() ), "edges were found to be different as segments but they are actually the same" return geodesic.parametrize( lhs.end(), model="euclidean" ) < geodesic.parametrize(rhs.end(), model="euclidean") return True if rhs.start().is_ideal(): return False return geodesic.parametrize( lhs.start(), model="euclidean" ) < geodesic.parametrize(rhs.start(), model="euclidean")