diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c9b664af1..ac548f6a5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -68,10 +68,15 @@ jobs: - name: Install dependencies shell: bash -l {0} run: | + mamba env update -n test --quiet -f environment.yml mamba install black=22.12 - name: Run black shell: bash -l {0} - run: black `find -name '*.py'` + run: | + black `find -name '*.py'` + # We run our notebooks through black to make sure everything is nicely formatted in our examples. + # Consequently, we cannot use SageMath syntax in the notebooks. (But presently we do not need it anyway.) + jupytext --sync --pipe black doc/examples/*.md - name: Detect changes shell: bash -l {0} run: git diff --exit-code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1ade004e..f5a265894 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,31 +13,19 @@ jobs: strategy: matrix: include: - - optionals: "sage,flipper" - sagelib: "8.8" - environment: "environment.yml" - python: "3.7.12" - - optionals: "sage,flipper" - sagelib: "8.9" - environment: "environment.yml" - python: "3.7.12" - - optionals: "sage,flipper" - sagelib: "9.1" - environment: "environment.yml" - python: "3.7.12" - optionals: "sage,flipper" sagelib: "9.2" environment: "environment.yml" python: "3.9.15" - - optionals: "sage,flipper,eantic,exactreal" + - optionals: "sage,flipper,eantic" sagelib: "9.3" environment: "environment.yml" python: "3.9.15" - - optionals: "sage,flipper,eantic,exactreal,pyflatsurf" + - optionals: "sage,flipper,eantic,pyflatsurf" sagelib: "9.4" environment: "environment.yml" python: "3.9.15" - - optionals: "sage,flipper,eantic,exactreal,pyflatsurf" + - optionals: "sage,flipper,eantic,pyflatsurf" sagelib: "9.5" environment: "environment.yml" python: "3.9.15" @@ -49,6 +37,11 @@ jobs: sagelib: "9.7" environment: "environment.yml" python: "3.10.8" + # surface-dynamics is not available for SageMath 9.8 on conda-forge, see https://github.com/conda-forge/surface-dynamics-feedstock/pull/17 + # - optionals: "sage,flipper,eantic,exactreal,pyflatsurf" + # sagelib: "9.8" + # environment: "environment.yml" + # python: "3.10.8" - optionals: "sage,flipper,eantic,exactreal,pyflatsurf" environment: "flatsurf.yml" python: "3.10.8" @@ -60,6 +53,7 @@ jobs: - name: Install dependencies shell: bash -l {0} run: | + mamba upgrade -n base --all --yes # mamba 1.4.1 segfaults sometimes, make sure we use a later version mamba install -n test sagelib=${{ matrix.sagelib }} echo "sagelib ==${{ matrix.sagelib }}" >> $CONDA_PREFIX/conda-meta/pinned while read; do diff --git a/.pylintrc b/.pylintrc index 4ae693686..fd58579bf 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,5 +2,8 @@ max-line-length=256 max-module-lines=65536 +[TYPECHECK] +mixin-class-rgx=ParentMethods|SubcategoryMethods|Parent|HyperbolicConvexSet + [MESSAGES CONTROL] -disable=import-outside-toplevel,no-name-in-module,invalid-name,invalid-unary-operand-type,no-value-for-parameter,abstract-method,arguments-differ,arguments-out-of-order,arguments-renamed,attribute-defined-outside-init,bad-classmethod-argument,bad-staticmethod-argument,broad-exception-caught,broad-exception-raised,chained-comparison,consider-merging-isinstance,consider-swap-variables,consider-using-enumerate,consider-using-f-string,consider-using-generator,consider-using-get,consider-using-in,consider-using-set-comprehension,consider-using-with,cyclic-import,dangerous-default-value,duplicate-code,import-self,inconsistent-return-statements,missing-class-docstring,missing-function-docstring,missing-module-docstring,no-else-break,no-else-continue,no-else-raise,no-else-return,non-ascii-name,non-parent-init-called,pointless-exception-statement,protected-access,raise-missing-from,raising-format-tuple,redefined-argument-from-local,redefined-builtin,redefined-outer-name,self-assigning-variable,self-cls-assignment,singleton-comparison,superfluous-parens,super-init-not-called,too-few-public-methods,too-many-arguments,too-many-boolean-expressions,too-many-branches,too-many-instance-attributes,too-many-locals,too-many-nested-blocks,too-many-public-methods,too-many-return-statements,too-many-statements,undefined-loop-variable,unidiomatic-typecheck,unnecessary-comprehension,unnecessary-dunder-call,unnecessary-lambda,unnecessary-pass,unspecified-encoding,unused-argument,unused-import,unused-variable,unused-wildcard-import,use-a-generator,use-dict-literal,useless-object-inheritance,useless-parent-delegation,wildcard-import,wrong-import-order,fixme,unreachable,reimported +disable=import-outside-toplevel,no-name-in-module,invalid-name,invalid-unary-operand-type,no-value-for-parameter,abstract-method,arguments-differ,arguments-out-of-order,arguments-renamed,attribute-defined-outside-init,bad-classmethod-argument,bad-staticmethod-argument,broad-exception-caught,broad-exception-raised,chained-comparison,consider-merging-isinstance,consider-swap-variables,consider-using-enumerate,consider-using-f-string,consider-using-generator,consider-using-get,consider-using-in,consider-using-set-comprehension,consider-using-with,cyclic-import,dangerous-default-value,duplicate-code,import-self,inconsistent-return-statements,missing-class-docstring,missing-function-docstring,missing-module-docstring,no-else-break,no-else-continue,no-else-raise,no-else-return,non-ascii-name,non-parent-init-called,pointless-exception-statement,protected-access,raise-missing-from,raising-format-tuple,redefined-argument-from-local,redefined-builtin,redefined-outer-name,self-assigning-variable,self-cls-assignment,singleton-comparison,superfluous-parens,super-init-not-called,too-few-public-methods,too-many-arguments,too-many-boolean-expressions,too-many-branches,too-many-instance-attributes,too-many-locals,too-many-nested-blocks,too-many-public-methods,too-many-return-statements,too-many-statements,undefined-loop-variable,unidiomatic-typecheck,unnecessary-comprehension,unnecessary-dunder-call,unnecessary-lambda,unnecessary-pass,unspecified-encoding,unused-argument,unused-import,unused-variable,unused-wildcard-import,use-a-generator,use-dict-literal,useless-object-inheritance,useless-parent-delegation,wildcard-import,wrong-import-order,fixme,unreachable,reimported,too-many-ancestors diff --git a/asv.conf.json b/asv.conf.json index 0801d86a2..080b85c58 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -12,11 +12,11 @@ "gap-defaults": [], "matplotlib-base": [], "pip+flipper": [], - "pyeantic": ["1.0.3"], - "pyexactreal": ["2.2.1"], - "pyflatsurf": ["3.9.3"], + "pyeantic": ["1.3.0"], + "pyexactreal": ["3.1.0"], + "pyflatsurf": ["3.13.3"], "python": [], - "sagelib": ["9.4"], + "sagelib": ["10.0"], "scipy": [], "surface-dynamics>=0.4.7": [] }, diff --git a/doc/conf.py b/doc/conf.py index 966a6d8b1..306a67be3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,7 +3,10 @@ # -- General configuration ------------------------------------------------ extensions = [ - "sphinx.ext.autodoc", + # We need to use SageMath's autodoc to render nested classes in categories + # correctly. Otherwise they just render as "alias for" in the + # documentation. + "sage_docbuild.ext.sage_autodoc", "sphinx.ext.todo", "sphinx.ext.mathjax", "sphinx.ext.viewcode", diff --git a/doc/environment.yml b/doc/environment.yml index bf368ad63..e91d85e0b 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -9,7 +9,6 @@ channels: - defaults dependencies: - sphinx - - jupytext - myst-nb - tachyon - furo diff --git a/doc/examples/apisa_wright.md b/doc/examples/apisa_wright.md index 9da9620f8..9a374d9b3 100644 --- a/doc/examples/apisa_wright.md +++ b/doc/examples/apisa_wright.md @@ -5,9 +5,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.0 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.3 + display_name: SageMath 9.7 language: sage name: sagemath --- @@ -39,62 +39,61 @@ We consider the following half-translation surface 2 2 | | +---1---+---1---+ - + It belongs to $Q_3(10, -1^2)$. -```{code-cell} ipython3 +```{code-cell} +from flatsurf import Polygon, MutableOrientedSimilaritySurface + + def apisa_wright_surface(h24, h3, l15, l6, l7, l8): - from flatsurf import ConvexPolygons, HalfTranslationSurface, Surface_list - params = [h24, h3, l15, l6, l7, l8] - K = Sequence(params).universe().fraction_field() + K = Sequence([h24, h3, l15, l6, l7, l8]).universe().fraction_field() + v24 = vector(K, (0, h24)) v3 = vector(K, (0, h3)) v15 = vector(K, (l15, 0)) v6 = vector(K, (l6, 0)) v7 = vector(K, (l7, 0)) v8 = vector(K, (l8, 0)) - C = ConvexPolygons(K) - P0 = C(edges=[v15,v15,v24,-2*v15,-v24]) - P1 = C(edges=[2*v15,v8,v7,v6,v3,-v8,-v7,-v6,-v15,-v15,-v3]) - P2 = C(edges=[v15,v24,-v15,-v24]) - S = Surface_list(base_ring = C.base_ring()) - S.rename("ApisaWrightSurface({})".format(', '.join(map(str, params)))) - S.add_polygons([P0, P1, P2]) - # set_edge_pairing(poly_num1, edge_num1, poly_num2, edge_num2) - S.set_edge_pairing(0, 0, 0, 1) - S.set_edge_pairing(0, 2, 0, 4) - S.set_edge_pairing(0, 3, 1, 0) - S.set_edge_pairing(1, 1, 1, 5) - S.set_edge_pairing(1, 2, 1, 6) - S.set_edge_pairing(1, 3, 1, 7) - S.set_edge_pairing(1, 4, 1, 10) - S.set_edge_pairing(1, 8, 2, 2) - S.set_edge_pairing(1, 9, 2, 0) - S.set_edge_pairing(2, 1, 2, 3) + + S = MutableOrientedSimilaritySurface(K) + + S.add_polygon(Polygon(edges=[v15, v15, v24, -2 * v15, -v24])) + S.add_polygon( + Polygon(edges=[2 * v15, v8, v7, v6, v3, -v8, -v7, -v6, -v15, -v15, -v3]) + ) + S.add_polygon(Polygon(edges=[v15, v24, -v15, -v24], base_ring=K)) + + S.glue((0, 0), (0, 1)) + S.glue((0, 2), (0, 4)) + S.glue((0, 3), (1, 0)) + S.glue((1, 1), (1, 5)) + S.glue((1, 2), (1, 6)) + S.glue((1, 3), (1, 7)) + S.glue((1, 4), (1, 10)) + S.glue((1, 8), (2, 2)) + S.glue((1, 9), (2, 0)) + S.glue((2, 1), (2, 3)) S.set_immutable() - return HalfTranslationSurface(S) + return S ``` We use some simple parameters: -```{code-cell} ipython3 -x = polygen(QQ) -K. = NumberField(x^2 - 2, embedding=AA(2).sqrt()) -S = apisa_wright_surface(1, 1+c, 1, c, 1+c, 2*c-1) +```{code-cell} +K = QuadraticField(2) +a = K.gen() +S = apisa_wright_surface(1, 1 + a, 1, a, 1 + a, 2 * a - 1) S.plot(edge_labels=False) ``` -Now build the canonical double cover and orbit closure: - -```{code-cell} ipython3 -S.stratum() +```{code-cell} +S ``` -```{code-cell} ipython3 -S.stratum().dimension() -``` +Now build the canonical double cover and orbit closure: -```{code-cell} ipython3 +```{code-cell} U = S.minimal_cover("translation") U.stratum() ``` @@ -104,37 +103,39 @@ length 16 looking for cylinders. Each decomposition into cylinders and minimal components provides a new tangent direction in the `GL(2,R)`-orbit closure of the surface via A. Wright's cylinder deformation. -```{code-cell} ipython3 +```{code-cell} from flatsurf import GL2ROrbitClosure # optional: pyflatsurf -U = S.minimal_cover('translation') # optional: pyflatsurf -O = GL2ROrbitClosure(U) # optional: pyflatsurf -O.dimension() # optional: pyflatsurf + +O = GL2ROrbitClosure(U) # optional: pyflatsurf +O.dimension() # optional: pyflatsurf ``` The above dimension is just the current dimension. At initialization it only consists of the `GL(2,R)`-direction. -```{code-cell} ipython3 -old_dim = O.dimension() # optional: pyflatsurf -for i, dec in enumerate(O.decompositions(16, bfs=True)): # optional: pyflatsurf - O.update_tangent_space_from_flow_decomposition(dec) - new_dim = O.dimension() - if old_dim != new_dim: - holonomies = [cyl.circumferenceHolonomy() for cyl in dec.cylinders()] - # .area() as reported by liblatsurf is actually twice the area - areas = [cyl.area()/2 for cyl in dec.cylinders()] - moduli = [(v.x()*v.x() + v.y()*v.y()) / area for v, area in zip(holonomies, areas)] - u = dec.vertical().vertical() - print("saddle connection number", i) - print("holonomy :", u) - print("length :", RDF(u.x()*u.x() + u.y()*u.y()).sqrt()) - print("num cylinders :", len(dec.cylinders())) - print("num minimal comps. :", len(dec.minimalComponents())) - print("current dimension :", new_dim) - print("cyls. holonomies :", holonomies) - print("cyls. moduli :", moduli) - if new_dim == 7: - break - old_dim = new_dim - print('-' * 30) +```{code-cell} +old_dim = O.dimension() # optional: pyflatsurf +for i, dec in enumerate(O.decompositions(16, bfs=True)): # optional: pyflatsurf + O.update_tangent_space_from_flow_decomposition(dec) + new_dim = O.dimension() + if old_dim != new_dim: + holonomies = [cyl.circumferenceHolonomy() for cyl in dec.cylinders()] + # .area() as reported by liblatsurf is actually twice the area + areas = [cyl.area() / 2 for cyl in dec.cylinders()] + moduli = [ + (v.x() * v.x() + v.y() * v.y()) / area for v, area in zip(holonomies, areas) + ] + u = dec.vertical().vertical() + print("saddle connection number", i) + print("holonomy :", u) + print("length :", RDF(u.x() * u.x() + u.y() * u.y()).sqrt()) + print("num cylinders :", len(dec.cylinders())) + print("num minimal comps. :", len(dec.minimalComponents())) + print("current dimension :", new_dim) + print("cyls. holonomies :", holonomies) + print("cyls. moduli :", moduli) + if new_dim == 7: + break + old_dim = new_dim + print("-" * 30) ``` diff --git a/doc/examples/boshernitzan_conjecture.md b/doc/examples/boshernitzan_conjecture.md index 08632f8c8..99359902d 100644 --- a/doc/examples/boshernitzan_conjecture.md +++ b/doc/examples/boshernitzan_conjecture.md @@ -5,9 +5,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.13.8 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.5 + display_name: SageMath 9.7 language: sage name: sagemath --- @@ -51,24 +51,27 @@ Let us verify that the (7, 7, 16) triangle indeed fails the assertion of the con First, we construct a triangle with angles (7, 7, 16). -```{code-cell} ipython3 -from flatsurf import EquiangularPolygons -Δ = EquiangularPolygons(7, 7, 16).an_element() +```{code-cell} +from flatsurf import EuclideanPolygonsWithAngles + +Δ = EuclideanPolygonsWithAngles(7, 7, 16).an_element() Δ ``` We unfold this triangle and obtain a translation surface. -```{code-cell} ipython3 +```{code-cell} from flatsurf import similarity_surfaces + S = similarity_surfaces.billiard(Δ).minimal_cover(cover_type="translation") S.plot(edge_labels=False, polygon_labels=False) ``` We construct the flow decomposition in direction (0, 1), orthogonal to one of the sides of the triangle. -```{code-cell} ipython3 +```{code-cell} from flatsurf import GL2ROrbitClosure + D = GL2ROrbitClosure(S).decomposition(vector(Δ.base_ring(), (0, 1))) D ``` @@ -82,7 +85,7 @@ cylinders = [c for c in D.components() if c.cylinder()] Widget(cylinders) ``` -## (b) Cylinder Periodic Directions of Odd Triangles +## (b) Cylinder Periodic Directions of Odd Triangles Assertion (b) can be phrased as follows: > Let $d$ be odd and let $d=α+β+γ$ be a partition into coprime positive integers. Consider the triangle $\Delta=(α,β,γ)$, i.e., the triangle with angles $(α\pi/d, β\pi/d, γ\pi/d)$, embedded into the complex plane such that one of its sides is horizontal. Let $z\in S^1$ be such that $z^{2d}=-1$. Then the flow in direction $z$ on the unfolding of $\Delta$ completely decomposes into cylinders. @@ -104,17 +107,18 @@ We can use sage-flatsurf to verify that this assertion does indeed hold. Let us We start by constructing the unfolding of that triangle: -```{code-cell} ipython3 -from flatsurf import EquiangularPolygons, similarity_surfaces +```{code-cell} +from flatsurf import EuclideanPolygonsWithAngles, similarity_surfaces -Δ = EquiangularPolygons(1, 1, 10).an_element() +Δ = EuclideanPolygonsWithAngles(1, 1, 10).an_element() S = similarity_surfaces.billiard(Δ).minimal_cover(cover_type="translation") ``` We find that this completely decomposes into cylinders in horizontal direction: -```{code-cell} ipython3 +```{code-cell} from flatsurf import GL2ROrbitClosure + D = GL2ROrbitClosure(S).decomposition(vector(Δ.base_ring(), (0, 1))) D ``` @@ -135,20 +139,20 @@ As indicated in the conjecture, (2, 3, 6) is exceptional, i.e., there is such a #### Computing a Flow Decomposition for the (2, 3, 6) Triangle Again, we can ask sage-flatsurf to compute the flow decomposition of the unfolding of the (2, 3, 6) triangle. It turns out that in vertical direction (1, 0), it does not fully decompose into cylinders: -```{code-cell} ipython3 -from flatsurf import EquiangularPolygons, similarity_surfaces, GL2ROrbitClosure +```{code-cell} +from flatsurf import EuclideanPolygonsWithAngles, similarity_surfaces, GL2ROrbitClosure -Δ = EquiangularPolygons(2, 3, 6).an_element() +Δ = EuclideanPolygonsWithAngles(2, 3, 6).an_element() S = similarity_surfaces.billiard(Δ).minimal_cover(cover_type="translation") D = GL2ROrbitClosure(S).decomposition(vector(Δ.base_ring(), (1, 0))) D ``` -```{code-cell} ipython3 -from flatsurf import polygons, similarity_surfaces, EquiangularPolygons +```{code-cell} +from flatsurf import polygons, similarity_surfaces, EuclideanPolygonsWithAngles from flatsurf import GL2ROrbitClosure -E = EquiangularPolygons(2, 3, 6) +E = EuclideanPolygonsWithAngles(2, 3, 6) T = E.random_element() S = similarity_surfaces.billiard(T) S = S.minimal_cover(cover_type="translation") @@ -171,7 +175,7 @@ Internally, the preceding computation is performed on Interval Exchange Transfor Currently, the only way to work with such low-level objects is by invoking functions in the C++ libraries [libflatsurf](https://github.com/flatsurf/flatsurf) and [libintervalxt](https://github.com/flatsurf/intervalxt) directly. We start by passing from our translation surface to the corresponding surface in libflatsurf. (Note that these operations are not considered part of the stable interface of sage-flatsurf and subject to change.) -```{code-cell} ipython3 +```{code-cell} from flatsurf.geometry.pyflatsurf_conversion import to_pyflatsurf F = to_pyflatsurf(S) @@ -181,19 +185,21 @@ F = to_pyflatsurf(S) We start by retriangulating our surface. Namely, we want to obtain a single *large edge* for the flow direction, i.e., a unique edge that is wider (perpendicular to the flow direction) than all the other edges. -```{code-cell} ipython3 +```{code-cell} import pyflatsurf V = pyflatsurf.flatsurf.Vector[type(F).Coordinate] -horizontal = V(1R, 0R) +horizontal = V(int(1), int(0)) F = pyflatsurf.flatsurf.FlatTriangulationCollapsed[type(F).Coordinate](F, horizontal) -pyflatsurf.flatsurf.IntervalExchangeTransformation[type(F)].makeUniqueLargeEdges(F, horizontal) +pyflatsurf.flatsurf.IntervalExchangeTransformation[type(F)].makeUniqueLargeEdges( + F, horizontal +) ``` Unfortunately, we cannot display a plot of such a surface since it is not a real translation surface anymore. Some of the edges (the ones is direction of the flow) have been collapsed, see [#62](https://github.com/flatsurf/vue-flatsurf/issues/62). -```{code-cell} ipython3 +```{code-cell} large = [e for e in F.edges() if F.vertical().large(e.positive())][0].negative() large ``` @@ -204,10 +210,12 @@ We (unfortunately do not) see in the above plot how the half edges on the left g This defines an Interval Exchange Transformation. -```{code-cell} ipython3 +```{code-cell} import pyintervalxt, pyeantic -iet = pyflatsurf.flatsurf.IntervalExchangeTransformation[type(F)](F, F.vertical().vertical(), large).forget() +iet = pyflatsurf.flatsurf.IntervalExchangeTransformation[type(F)]( + F, F.vertical().vertical(), large +).forget() iet ``` @@ -217,7 +225,7 @@ We now attempt to decompose the Interval Exchange Transformation by performing s Namely, we begin by subtracting `a` at the top from `g` at the bottom. -```{code-cell} ipython3 +```{code-cell} iet.swap() iet.zorichInduction() iet.swap() @@ -226,14 +234,14 @@ iet Next, we subtract `g` at the bottom from `b` at the top. -```{code-cell} ipython3 +```{code-cell} iet.zorichInduction() iet ``` Now, we subtract `b` at the top from `g` at the bottom. -```{code-cell} ipython3 +```{code-cell} iet.swap() iet.zorichInduction() iet.swap() @@ -242,7 +250,7 @@ iet We keep going like this for a few more iterations and end up with the starting labels `f` and `e` of the same length. -```{code-cell} ipython3 +```{code-cell} iet.zorichInduction() iet.swap() iet.zorichInduction() @@ -257,8 +265,8 @@ iet Therefore, we can simplify the interval exchange transformation by dropping the label `f`. -```{code-cell} ipython3 -iet.induce(0R) +```{code-cell} +iet.induce(int(0)) iet ``` @@ -266,15 +274,15 @@ Now, top and bottom start with the same label `a`. We found a cylinder. We continue with the remaining interval exchange transformation. -```{code-cell} ipython3 +```{code-cell} iet = iet.reduce().value() iet ``` A few more induction steps, let us drop the `e` label as we did before. -```{code-cell} ipython3 -iet.induce(-1R) +```{code-cell} +iet.induce(-int(1)) iet ``` @@ -282,14 +290,14 @@ Now, top and bottom start with the same labels `d` and `h`. The interval exchang Let us consider the first part on the labels `d` and `h`. -```{code-cell} ipython3 +```{code-cell} iet.reduce() iet ``` We see that this must be a minimal component. Indeed, consider a point somewhere on the bottom interval, i.e., on either `h` or `d`. As this point flows to the top, it either hits `d` or `h` there. If it hits `d` it gets translated by the length of `h`. If it hits `h` it gets translated by the length of `-d`. If there were a cylinder hidden in this somewhere, we would be able to chain such translations together to get a total translation of zero. However, there is no combination of positive integer multiples of `h` and `-d` that sums to zero. There cannot be a cylinder. -```{code-cell} ipython3 +```{code-cell} iet.boshernitzanNoPeriodicTrajectory() ``` @@ -303,10 +311,10 @@ Assertion (d) can be phrased as We can use sage-flatsurf to check this assertion for some small triangles. Let us consider the (2, 2, 3) triangle. -```{code-cell} ipython3 -from flatsurf import EquiangularPolygons, similarity_surfaces, GL2ROrbitClosure +```{code-cell} +from flatsurf import EuclideanPolygonsWithAngles, similarity_surfaces, GL2ROrbitClosure -Δ = EquiangularPolygons(2, 2, 3).an_element() +Δ = EuclideanPolygonsWithAngles(2, 2, 3).an_element() S = similarity_surfaces.billiard(Δ).minimal_cover(cover_type="translation") ``` @@ -318,11 +326,19 @@ Widget(S) We can compute flow decompositions for some short saddle connections in this surface and look for minimal components. -```{code-cell} ipython3 +```{code-cell} for connection in S.saddle_connections(4): decomposition = GL2ROrbitClosure(S).decomposition(connection.direction()) - if any(component.withoutPeriodicTrajectory() for component in decomposition.components()): - print("Found minimal component in", decomposition, "for direction", connection.direction()) + if any( + component.withoutPeriodicTrajectory() + for component in decomposition.components() + ): + print( + "Found minimal component in", + decomposition, + "for direction", + connection.direction(), + ) break ``` @@ -342,10 +358,10 @@ Assertion (e) can be phrased as The (7,8,15) triangle which is also a counterexample to (a) works here as well. -```{code-cell} ipython3 -from flatsurf import EquiangularPolygons, similarity_surfaces, GL2ROrbitClosure +```{code-cell} +from flatsurf import EuclideanPolygonsWithAngles, similarity_surfaces, GL2ROrbitClosure -Δ = EquiangularPolygons(7, 8, 15).an_element() +Δ = EuclideanPolygonsWithAngles(7, 8, 15).an_element() S = similarity_surfaces.billiard(Δ).minimal_cover(cover_type="translation") ``` @@ -355,8 +371,9 @@ from ipyvue_flatsurf import Widget Widget(S) ``` -```{code-cell} ipython3 +```{code-cell} from flatsurf import GL2ROrbitClosure + D = GL2ROrbitClosure(S).decomposition(vector(Δ.base_ring(), (1, 0))) D ``` diff --git a/doc/examples/defining_surfaces.md b/doc/examples/defining_surfaces.md index 29143da29..d94f6a756 100644 --- a/doc/examples/defining_surfaces.md +++ b/doc/examples/defining_surfaces.md @@ -5,61 +5,60 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath -author: W. Patrick Hooper --- # Defining Surfaces -```{code-cell} ipython3 -from flatsurf import * -``` - ## Built in surfaces Veech's double n-gon surfaces: -```{code-cell} ipython3 +```{code-cell} +from flatsurf import translation_surfaces + s = translation_surfaces.veech_double_n_gon(5) s.plot() ``` The Arnoux-Yoccoz surface of arbitrary genus is built in: -```{code-cell} ipython3 -s=translation_surfaces.arnoux_yoccoz(3) +```{code-cell} +s = translation_surfaces.arnoux_yoccoz(3) s.plot() ``` Chamanara's infinite translation surface: -```{code-cell} ipython3 -s=translation_surfaces.chamanara(1/2) +```{code-cell} +s = translation_surfaces.chamanara(1 / 2) ``` -```{code-cell} ipython3 -s.plot(polygon_labels=False,edge_labels=False) +```{code-cell} +s.plot(polygon_labels=False, edge_labels=False) ``` -```{code-cell} ipython3 -s=translation_surfaces.infinite_staircase() +```{code-cell} +s = translation_surfaces.infinite_staircase() ``` -```{code-cell} ipython3 +```{code-cell} s.plot() ``` ## Billiard tables -```{code-cell} ipython3 -s=similarity_surfaces.billiard(polygons(vertices=[(0,0), (3,0), (0,4)])) +```{code-cell} +from flatsurf import similarity_surfaces, Polygon + +s = similarity_surfaces.billiard(Polygon(vertices=[(0, 0), (3, 0), (0, 4)])) ``` -```{code-cell} ipython3 +```{code-cell} s.plot() ``` @@ -67,19 +66,19 @@ s.plot() Continuing the billiard example above, we get an infinite translation surface below: -```{code-cell} ipython3 -ss = s.minimal_cover(cover_type="translation").copy(relabel=True) +```{code-cell} +ss = s.minimal_cover(cover_type="translation") ``` -```{code-cell} ipython3 +```{code-cell} gs = ss.graphical_surface() ``` -```{code-cell} ipython3 +```{code-cell} gs.make_all_visible(limit=12) ``` -```{code-cell} ipython3 +```{code-cell} gs.plot() ``` @@ -87,107 +86,110 @@ gs.plot() This defines a regular 12-gon with algebraic real coordinates (AA) with first vector given by (1,0): -```{code-cell} ipython3 -p0 = polygons.regular_ngon(12,field=AA) -p1 = polygons.regular_ngon(3,field=AA) +```{code-cell} +from flatsurf import polygons + +p0 = polygons.regular_ngon(12, field=AA) +p1 = polygons.regular_ngon(3, field=AA) ``` -```{code-cell} ipython3 -p0.plot()+p1.plot() +```{code-cell} +p0.plot() + p1.plot() ``` The vertices of n-gons are numbered by $\{0,...,n-1\}$, with the $0$-th vertex at the origin. Edge $i$ joins vertex $i$ to vertex $i+1 \pmod{n}$. We can act on polygon with $2 \times 2$ matrices. We define the rotation by $\frac{\pi}{6}$ below: -```{code-cell} ipython3 -R = matrix(AA,[[cos(pi/6),-sin(pi/6)],[sin(pi/6),cos(pi/6)]]) +```{code-cell} +R = matrix(AA, [[cos(pi / 6), -sin(pi / 6)], [sin(pi / 6), cos(pi / 6)]]) show(R) ``` -```{code-cell} ipython3 -R*p1 +```{code-cell} +R * p1 ``` Define a surface over the field AA of algebraic reals. -```{code-cell} ipython3 -surface = Surface_dict(base_ring=AA) +```{code-cell} +from flatsurf import MutableOrientedSimilaritySurface + +surface = MutableOrientedSimilaritySurface(AA) ``` Add two polygons to the surface with labels 0 and 1: -```{code-cell} ipython3 -surface.add_polygon(p0,label=0) -``` - -```{code-cell} ipython3 -surface.add_polygon(p1,label=1) +```{code-cell} +surface.add_polygon(p0, label=0) ``` -Set the "base label" for the surface. This is just a choice of a favorite polygon label. - -```{code-cell} ipython3 -surface.change_base_label(0) +```{code-cell} +surface.add_polygon(p1, label=1) ``` Glue the edges of polygon 0 to the parallel edges of polygon 1. -```{code-cell} ipython3 -surface.change_edge_gluing(0,6,1,0) -surface.change_edge_gluing(0,10,1,1) -surface.change_edge_gluing(0,2,1,2) +```{code-cell} +surface.glue((0, 6), (1, 0)) +surface.glue((0, 10), (1, 1)) +surface.glue((0, 2), (1, 2)) ``` Add three more rotated triangles and glue them appropriately. -```{code-cell} ipython3 -for i in range(1,4): - surface.add_polygon((R**i)*p1,label=i+1) - surface.change_edge_gluing(0,6+i,i+1,0) - surface.change_edge_gluing(0,(10+i)%12,i+1,1) - surface.change_edge_gluing(0,2+i,i+1,2) +```{code-cell} +for i in range(1, 4): + surface.add_polygon((R**i) * p1, label=i + 1) + surface.glue((0, 6 + i), (i + 1, 0)) + surface.glue((0, (10 + i) % 12), (i + 1, 1)) + surface.glue((0, 2 + i), (i + 1, 2)) ``` -Now we have a closed surface. In fact we have defined a Translation Surface. The package also supports -SimilaritySurface, ConeSurface, HalfDilationSurface, DilationSurface, and HalfTranslationSurface. +Now we have a closed surface. In fact this is a translation surface: -```{code-cell} ipython3 -s=TranslationSurface(surface) +```{code-cell} +surface ``` -Test to insure that we created the translation surface correctly. (Errors would be printed if you did not glue parallel edges, or have some unglued edges, etc.) +Once we are done building the surface, it is recommended to make the surface immutable. This lets sage-flatsurf figure out of which nature this surface is, e.g., that it is a translation surface. This speeds up many operations on the surface and makes it possible to compute things that are only defined or implemented for some types of surfaces: -```{code-cell} ipython3 -TestSuite(s).run(verbose=True) +```{code-cell} +surface.set_immutable() +surface ``` +If you want to compute things, such as the stratum without making a surface immutable, please refer to the details in the documentation of the ``flatsurf.geometry.categories`` module in the module reference. + We can plot the surface. Edges are labeled according to the polygon they are glued to. -```{code-cell} ipython3 -s.plot() +```{code-cell} +surface.plot() ``` The field containing the vertices: -```{code-cell} ipython3 -s.base_ring() +```{code-cell} +surface.base_ring() ``` -Computations in the Algebraic Real Field (AA) are slow. It is better to use a NumberField. The following finds the smallest embedding into a NumberField: +Computations in the Algebraic Real Field (AA) are slow. It is better to use a NumberField. The following finds a smaller number field:: -```{code-cell} ipython3 -ss=s.copy(optimal_number_field=True) +```{code-cell} +vertices = [surface.polygon(p).vertex(v) for (p, v) in surface.edges()] +vertices = [vertex[0] for vertex in vertices] + [vertex[1] for vertex in vertices] +base_ring = Sequence( + [coordinate.as_number_field_element()[1] for coordinate in vertices] +).universe() +ss = surface.change_ring(base_ring) ``` -```{code-cell} ipython3 +```{code-cell} ss.base_ring() ``` ## Getting a surface from Flipper -This does not work as of SageMath 9.0. Code is commented out below. - Flipper is a program written by Mark Bell which understands mapping classes and can compute the flat structure associated to a pseudo-Anosov mapping class. FlatSurf can import this structure. This code below requires flipper to be installed. You can do this by running the shell within sage: @@ -196,153 +198,196 @@ Then within the shell execute: python -m pip install flipper --user --upgrade More information including pitfalls are described in Flipper's installation instructions. -```{code-cell} ipython3 -# import flipper +```{code-cell} +import flipper ``` -```{code-cell} ipython3 -# T = flipper.load('SB_4') +```{code-cell} +T = flipper.load("SB_4") ``` -```{code-cell} ipython3 -# h = T.mapping_class('s_0S_1s_2S_3s_1S_2') +```{code-cell} +h = T.mapping_class("s_0S_1s_2S_3s_1S_2") ``` -```{code-cell} ipython3 -# h.is_pseudo_anosov() +```{code-cell} +h.is_pseudo_anosov() ``` -```{code-cell} ipython3 -# s = translation_surfaces.from_flipper(h) +```{code-cell} +s = translation_surfaces.from_flipper(h) ``` The surface s is actually a half translation surface -```{code-cell} ipython3 -# type(s) +```{code-cell} +s ``` -```{code-cell} ipython3 -# s.plot() +```{code-cell} +s.plot() ``` ## From polyhedra -```{code-cell} ipython3 -from flatsurf.geometry.polyhedra import * -``` +```{code-cell} +from flatsurf.geometry.polyhedra import platonic_dodecahedron -```{code-cell} ipython3 -polyhedron,s,mapping = platonic_dodecahedron() +polyhedron, s, mapping = platonic_dodecahedron() ``` The surface $s$ is a Euclidean cone surface. -```{code-cell} ipython3 -type(s) +```{code-cell} +s ``` -```{code-cell} ipython3 +```{code-cell} s.plot() ``` Sage has a built in polyhedron class. You can build a polyhedron as a convex hull of a list of vertices. -```{code-cell} ipython3 -polyhedron=Polyhedron([(0,0,0),(1,0,0),(0,1,0),(0,0,1)]) +```{code-cell} +polyhedron = Polyhedron([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]) ``` -```{code-cell} ipython3 +```{code-cell} polyhedron.plot() ``` The following computes the boundary surface as a Euclidean cone surface. It also provides a map from the surface to the polyhedron. -```{code-cell} ipython3 -s,mapping = polyhedron_to_cone_surface(polyhedron) +```{code-cell} +from flatsurf.geometry.polyhedra import polyhedron_to_cone_surface + +s, mapping = polyhedron_to_cone_surface(polyhedron) +s ``` -```{code-cell} ipython3 +```{code-cell} s.plot() ``` ## Defining an infinite surface from scratch -The following demonstrates the implementation of a TranslationSurface. Each geometric structure has an underlying "Surface". The following defines a surface and then uses it to construct a translation surface. +Finite surfaces can be built by gluing polygons into a ``MutableOrientedSimilaritySurface``. For an infinite surface, we need to subclass ``OrientedSimilaritySurface`` and implement a few methods ourselves: -```{code-cell} ipython3 -from flatsurf.geometry.surface import Surface +```{code-cell} +from flatsurf.geometry.surface import OrientedSimilaritySurface +from flatsurf.geometry.categories import TranslationSurfaces -class ParabolaSurface(Surface): + +class ParabolaSurface(OrientedSimilaritySurface): def __init__(self): - # The space of polygons with vertices in the rationals: - self._P = Polygons(QQ) - self._inv = matrix(QQ,[[-1,0],[0,-1]]) - - # Set the base field to QQ, the base label to be 1, and note that the surface is infinite. - Surface.__init__(self, base_ring=QQ, base_label=ZZ(1), finite=False, mutable=False) - + # For finite surfaces, the category can be determined automotatically + # but for infinite surfaces, we need to make an explicit choice here. + super().__init__( + QQ, + category=TranslationSurfaces().InfiniteType().WithoutBoundary().Connected(), + ) + + def __repr__(self): + r""" + Return a printable representation of this surface. + """ + return "ParabolaSurface()" + + def roots(self): + r""" + Return a label for each connected component of the surface. + + Iterating the polygons of the connected component starts at these labels. + """ + return (1,) + + def is_mutable(self): + r""" + Return whether this surface can be modified by the user. + """ + return False + + def is_compact(self): + r""" + Return whether this surface is a compact space. + """ + return False + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + """ + return isinstance(other, ParabolaSurface) + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with ``__eq``. + """ + return hash(type(self)) + + def graphical_surface(self, **kwds): + r""" + Return a plottable representation of this surface. + """ + graphical_surface = super().graphical_surface(**kwds) + # Make the first six polygons of the surface visible by default when plotting. + graphical_surface.make_all_visible(limit=6) + return graphical_surface + def polygon(self, label): - if label not in ZZ: - raise ValueError("invalid label {!r}".format(lab)) - assert label != 0, "Label should not be zero." - if label >= 0: - if label==1: - return self._P(vertices=[(0,0),(1,1),(-1,1)]) - else: - return self._P( vertices=[ - (label-1, (label-1)**2), - (label, label**2), - (-label, label**2), - (-label+1, (label-1)**2) ] ) - else: - return self._inv*self.polygon(-label) + r""" + Return the polygon making up this surface labeled ``label``. + """ + if label not in ZZ or label == 0: + raise ValueError(f"invalid label {label}") + + if label < 0: + return matrix(QQ, [[-1, 0], [0, -1]]) * self.polygon(-label) + + if label == 1: + return Polygon(vertices=[(0, 0), (1, 1), (-1, 1)], base_ring=QQ) + + return Polygon( + vertices=[ + (label - 1, (label - 1) ** 2), + (label, label**2), + (-label, label**2), + (-label + 1, (label - 1) ** 2), + ], + base_ring=QQ, + ) def opposite_edge(self, label, e): - if label not in ZZ: - raise ValueError("invalid label {!r}".format(lab)) - assert label != 0, "Label should not be zero." - - if label==1 or label==-1: - if e==1: - return 2*label,3 - else: - return -label,e - else: - if e==0 or e==2: - return -label,e - if e==1: - if label>0: - return label+1,3 - else: - return label-1,3 - if e==3: - if label>0: - return label-1,1 - else: - return label+1,1 -``` - -```{code-cell} ipython3 -s = TranslationSurface(ParabolaSurface()) -``` - -```{code-cell} ipython3 -TestSuite(s).run(verbose=True) -``` + if label not in ZZ or label == 0: + raise ValueError(f"invalid label {label}") + if label in [-1, 1] and e not in [0, 1, 2]: + raise ValueError("no such edge") + if e not in [0, 1, 2, 3]: + raise ValueError("no such edge") -A graphical surface controls the display of graphical data. For an infinite surface you need to configure the display manually. + if label in [-1, 1] and e == 1: + return 2 * label, 3 -```{code-cell} ipython3 -gs=s.graphical_surface() -``` + if e in [0, 2]: + return -label, e -We make six polygons nearest to the polygon with the base label visible. + if e == 1: + return label + label.sign(), 3 -```{code-cell} ipython3 -gs.make_all_visible(limit=6) + return label - label.sign(), 1 ``` -```{code-cell} ipython3 +```{code-cell} +s = ParabolaSurface() +s +``` + +```{code-cell} s.plot() ``` + +We can run a test suite to ensure that we have implemented everything that is needed to make this a fully functional surface. + +```{code-cell} +TestSuite(s).run(verbose=True) +``` diff --git a/doc/examples/graphics_configuration.md b/doc/examples/graphics_configuration.md index 8dd0b4310..7b913dc8e 100644 --- a/doc/examples/graphics_configuration.md +++ b/doc/examples/graphics_configuration.md @@ -5,9 +5,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.14.0 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.6 + display_name: SageMath 9.7 language: sage name: sagemath --- @@ -16,89 +16,86 @@ kernelspec: ## Rearranging Polygons -```{code-cell} ipython3 -from flatsurf import * -from flatsurf.geometry.polyhedra import * -``` +```{code-cell} +from flatsurf.geometry.polyhedra import platonic_dodecahedron -```{code-cell} ipython3 s = platonic_dodecahedron()[1] ``` The default plot of this surface: -```{code-cell} ipython3 +```{code-cell} s.plot() ``` Labels in the center of each polygon indicate the label of the polygon. Edge labels above indicate which polygon the edge is glued to. -Plotting the surface is controlled by a GraphicalSurface object. You can get the surface as follows: +Plotting the surface is controlled by a GraphicalSurface object: -```{code-cell} ipython3 +```{code-cell} gs = s.graphical_surface() ``` The graphical surface controls where polygons are drawn. You can glue a polygon across an edge using `gs.make_adjacent(label, edge)`. A difficulty is that you need to know which edge is which. You can enable `zero_flags` to see the zero vertex of each polygon. -```{code-cell} ipython3 +```{code-cell} gs.will_plot_zero_flags = True ``` -```{code-cell} ipython3 +```{code-cell} gs.plot() ``` sage-flatsurf uses a simple algorithm to layout polygons. Sometimes polygons overlap. But in this example the main concern is maybe that the picture is not as symmetric as we would like it to be. For example, we could aim for things to be symmetric around the polygons 0 and 1. Let's say we would like to move polygon 2 so that it's glued to 10 instead of being glued to 0. We count the edges on polygon 10 until we reach the edge glued to 2. It's the first one. We can verify that this is correct: -```{code-cell} ipython3 +```{code-cell} s.opposite_edge(10, 0) ``` We can move polygon 2 so that it is adjacent to polygon 10 with the command: -```{code-cell} ipython3 +```{code-cell} gs.make_adjacent(10, 0) ``` Lets check that it worked: -```{code-cell} ipython3 -s.plot() +```{code-cell} +gs.plot() ``` Let's build the symmetric widget at polygon 1 by moving 7 to be adjacent to 5 and 3 to be adjacent to 7. Note that the order of the movements matter. If we do it in the wrong order, we detach things: -```{code-cell} ipython3 +```{code-cell} gs.make_adjacent(7, 3) gs.make_adjacent(5, 4) -s.plot() +gs.plot() ``` Indeed, we moved 3 to be adjacent to 7 but then moved 7 away. Let's do it in the correct order: -```{code-cell} ipython3 +```{code-cell} gs.make_adjacent(5, 4) gs.make_adjacent(7, 3) -s.plot() +gs.plot() ``` Finally, glue 9 to 8 for a symmetric picture: -```{code-cell} ipython3 +```{code-cell} gs.make_adjacent(8, 0) -s.plot() +gs.plot() ``` ## Moving between coordinate systems The Euclidean Cone Surface `s` works in a different coordinate system then the graphical surface `gs`. So, when we moved the polygon above, we had no affect on `s`. In fact, the polygons of `s` are all the same: -```{code-cell} ipython3 +```{code-cell} s.polygon(0) ``` -```{code-cell} ipython3 +```{code-cell} s.polygon(1) ``` @@ -106,23 +103,23 @@ So really `s` is a disjoint union of twelve copies of a standard pentagon with s Lets now look at "graphical coordinates" i.e., the coordinates in which `gs` works. -```{code-cell} ipython3 +```{code-cell} show(gs.plot(), axes=True) ``` We can tell that the point `(4, -4)` is in the unfolding, but we can't immediately tell if it is in polygon 5 or 7. The GraphicalSurface `gs` is made out of GraphicalPolygons which we can use to deal with this sort of thing. -```{code-cell} ipython3 +```{code-cell} gs.graphical_polygon(5).contains((4, -4)) ``` -```{code-cell} ipython3 +```{code-cell} gs.graphical_polygon(7).contains((4, -4)) ``` Great. Now we can get the position of the point on the surface! -```{code-cell} ipython3 +```{code-cell} gp = gs.graphical_polygon(7) pt = gp.transform_back((4, -4)) pt @@ -130,100 +127,102 @@ pt Here we plot polygon 7 in its geometric coordinates with `pt`. -```{code-cell} ipython3 +```{code-cell} s.polygon(7).plot() + point2d([pt], zorder=100, color="red") ``` Lets convert it to a surface point and plot it! -```{code-cell} ipython3 -spt = s.surface_point(7, pt) +Note that we will have to pass the graphical surface to the point so it plots with respect to ``gs`` and not with respect to ``s.graphical_surface``. + +```{code-cell} +spt = s.point(7, pt) spt ``` -```{code-cell} ipython3 -s.plot() + spt.plot(color="red", size=20) +```{code-cell} +gs.plot() + spt.plot(gs, color="red", size=20) ``` Now we want to plot an upward trajectory through this point. Again, we have to deal with the fact that the coordinates might not match. You can get access to the transformation (a similarity) from geometric coordinates to graphical coordinates: -```{code-cell} ipython3 +```{code-cell} transformation = gs.graphical_polygon(7).transformation() transformation ``` Really we want the inverse: -```{code-cell} ipython3 +```{code-cell} inverse_transformation = ~transformation inverse_transformation ``` We just want the derivative of this similarity to transform the vertical direction. The derivative is a $2 \times 2$ matrix. -```{code-cell} ipython3 +```{code-cell} show(inverse_transformation.derivative()) ``` -```{code-cell} ipython3 -direction = inverse_transformation.derivative() * vector((0,1)) +```{code-cell} +direction = inverse_transformation.derivative() * vector((0, 1)) direction ``` We can use the point and the direction to get a tangent vector, which we convert to a trajectory, flow and plot. -```{code-cell} ipython3 +```{code-cell} tangent_vector = s.tangent_vector(7, pt, direction) tangent_vector ``` -```{code-cell} ipython3 +```{code-cell} traj = tangent_vector.straight_line_trajectory() traj.flow(100) traj.is_closed() ``` -```{code-cell} ipython3 -s.plot() + spt.plot(color="red") + traj.plot(color="orange") +```{code-cell} +gs.plot() + spt.plot(gs, color="red") + traj.plot(gs, color="orange") ``` ## Multiple graphical surfaces It is possible to have more than one graphical surface. Maybe you want to have one where things look better. -To get a new suface, you can call `s.graphical_surface()` again but with a `cached=False` parameter. +To get a new suface, you can call `s.graphical_surface()` again. -```{code-cell} ipython3 -pretty_gs = s.graphical_surface(polygon_labels=False, edge_labels=False, cached=False) +```{code-cell} +pretty_gs = s.graphical_surface(polygon_labels=False, edge_labels=False) ``` -```{code-cell} ipython3 +```{code-cell} pretty_gs.plot() ``` Current polygon printing options: -```{code-cell} ipython3 +```{code-cell} pretty_gs.polygon_options ``` -```{code-cell} ipython3 +```{code-cell} del pretty_gs.polygon_options["color"] -pretty_gs.polygon_options["rgbcolor"]="#ffeeee" +pretty_gs.polygon_options["rgbcolor"] = "#ffeeee" ``` -```{code-cell} ipython3 +```{code-cell} pretty_gs.non_adjacent_edge_options["thickness"] = 0.5 pretty_gs.non_adjacent_edge_options["color"] = "lightblue" pretty_gs.will_plot_adjacent_edges = False ``` -```{code-cell} ipython3 +```{code-cell} pretty_gs.plot() ``` -To use a non-default graphical surface you need to pass the graphical surface as a parameter. +Again, to use a non-default graphical surface, we need to pass the graphical surface as a parameter. -```{code-cell} ipython3 +```{code-cell} pretty_gs.plot() + spt.plot(pretty_gs, color="red") + traj.plot(pretty_gs) ``` @@ -231,22 +230,22 @@ Lets make it prettier by drawing some stars on the faces! Find all saddle connections of length at most $\sqrt{16}$: -```{code-cell} ipython3 +```{code-cell} saddle_connections = s.saddle_connections(16) ``` The edges have length two so we will keep anything that has a different length. -```{code-cell} ipython3 +```{code-cell} saddle_connections2 = [] for sc in saddle_connections: h = sc.holonomy() - if h[0]**2 + h[1]**2 != 4: + if h[0] ** 2 + h[1] ** 2 != 4: saddle_connections2.append(sc) len(saddle_connections2) ``` -```{code-cell} ipython3 +```{code-cell} plot = pretty_gs.plot() for sc in saddle_connections2: plot += sc.plot(pretty_gs, color="red") @@ -255,7 +254,7 @@ plot Plot using the original graphical surface. -```{code-cell} ipython3 +```{code-cell} plot = s.plot() for sc in saddle_connections2: plot += sc.plot(color="red") @@ -264,50 +263,52 @@ plot ## Manipulating edge labels -```{code-cell} ipython3 +```{code-cell} +from flatsurf import translation_surfaces + s = translation_surfaces.arnoux_yoccoz(4) ``` -```{code-cell} ipython3 +```{code-cell} s.plot() ``` Here is an example with the edge labels centered on the edge. -```{code-cell} ipython3 +```{code-cell} gs = s.graphical_surface(cached=False) ``` -```{code-cell} ipython3 +```{code-cell} del gs.polygon_options["color"] -gs.polygon_options["rgbcolor"]="#eee" -gs.edge_label_options["position"]="edge" -gs.edge_label_options["t"]=0.5 -gs.edge_label_options["push_off"]=0 -gs.edge_label_options["color"]="green" -gs.adjacent_edge_options["thickness"]=0.5 -gs.will_plot_non_adjacent_edges=False +gs.polygon_options["rgbcolor"] = "#eee" +gs.edge_label_options["position"] = "edge" +gs.edge_label_options["t"] = 0.5 +gs.edge_label_options["push_off"] = 0 +gs.edge_label_options["color"] = "green" +gs.adjacent_edge_options["thickness"] = 0.5 +gs.will_plot_non_adjacent_edges = False ``` -```{code-cell} ipython3 +```{code-cell} gs.plot() ``` -```{code-cell} ipython3 +```{code-cell} gs = s.graphical_surface(cached=False) ``` -```{code-cell} ipython3 +```{code-cell} del gs.polygon_options["color"] -gs.polygon_options["rgbcolor"]="#eef" -gs.edge_label_options["position"]="outside" -gs.edge_label_options["t"]=0.5 -gs.edge_label_options["push_off"]=0.02 -gs.edge_label_options["color"]="green" -gs.adjacent_edge_options["thickness"]=0.5 -gs.non_adjacent_edge_options["thickness"]=0.25 +gs.polygon_options["rgbcolor"] = "#eef" +gs.edge_label_options["position"] = "outside" +gs.edge_label_options["t"] = 0.5 +gs.edge_label_options["push_off"] = 0.02 +gs.edge_label_options["color"] = "green" +gs.adjacent_edge_options["thickness"] = 0.5 +gs.non_adjacent_edge_options["thickness"] = 0.25 ``` -```{code-cell} ipython3 +```{code-cell} gs.plot() ``` diff --git a/doc/examples/linear_action_and_delaunay.md b/doc/examples/linear_action_and_delaunay.md index dfb9eae55..494fbec72 100644 --- a/doc/examples/linear_action_and_delaunay.md +++ b/doc/examples/linear_action_and_delaunay.md @@ -5,300 +5,219 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath -author: W. Patrick Hooper --- -+++ {"deletable": true, "editable": true} - # The GL(2,R) Action, the Veech Group, Delaunay Decomposition -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- -from flatsurf import * -``` - -+++ {"deletable": true, "editable": true} - ## Acting on surfaces by matrices. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- +from flatsurf import translation_surfaces + s = translation_surfaces.veech_double_n_gon(5) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -m=matrix([[2,1],[1,1]]) +m = matrix([[2, 1], [1, 1]]) ``` -+++ {"deletable": true, "editable": true} - You can act on surfaces with the $GL(2,R)$ action -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -ss = m*s +ss = m * s ss ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- ss.plot() ``` -+++ {"deletable": true, "editable": true} - To "renormalize" you can improve the presentation using the Delaunay decomposition. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -sss = ss.delaunay_decomposition().copy(relabel=True) +sss = ss.delaunay_decomposition() +sss ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- sss.plot() ``` -+++ {"deletable": true, "editable": true} - ## The Veech group Set $s$ to be the double pentagon again. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- s = translation_surfaces.veech_double_n_gon(5) ``` -+++ {"deletable": true, "editable": true} - -It is best to work in the field in which the surfact is defined. - -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: false ---- -p=s.polygon(0) -p -``` - -+++ {"deletable": true, "editable": true} - The surface has a horizontal cylinder decomposition all of whose moduli are given as below -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -modulus = (p.vertex(3)[1]-p.vertex(2)[1])/(p.vertex(2)[0]-p.vertex(4)[0]) +p = s.polygon(0) +modulus = (p.vertex(3)[1] - p.vertex(2)[1]) / (p.vertex(2)[0] - p.vertex(4)[0]) AA(modulus) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -m = matrix(s.base_ring(),[[1, 1/modulus],[0,1]]) +m = matrix(s.base_ring(), [[1, 1 / modulus], [0, 1]]) show(m) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -show(matrix(AA,m)) +show(matrix(AA, m)) ``` -+++ {"deletable": true, "editable": true} - The following can be used to check that $m$ is in the Veech group of $s$. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.canonicalize() == (m*s).canonicalize() +s.canonicalize() == (m * s).canonicalize() ``` -+++ {"deletable": true, "editable": true} - ## Infinite surfaces Infinite surfaces support multiplication by matrices and computing the Delaunay decomposition. (Computation is done "lazily") -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s=translation_surfaces.chamanara(1/2) +s = translation_surfaces.chamanara(1 / 2) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.plot(edge_labels=False,polygon_labels=False) +s.plot(edge_labels=False, polygon_labels=False) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -ss=s.delaunay_decomposition() +ss = s.delaunay_decomposition() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -ss.graphical_surface().make_all_visible(limit=20) +gs = ss.graphical_surface(edge_labels=False, polygon_labels=False) +gs.make_all_visible(limit=20) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -ss.plot(edge_labels=False,polygon_labels=False) +gs.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -m = matrix([[2,0],[0,1/2]]) +m = matrix([[2, 0], [0, 1 / 2]]) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -ms = m*s +ms = m * s ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -ms.graphical_surface().make_all_visible(limit=20) -ms.plot(edge_labels=False,polygon_labels=False) +gs = ms.graphical_surface(edge_labels=False, polygon_labels=False) +gs.make_all_visible(limit=20) +gs.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- mss = ms.delaunay_decomposition() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -mss.graphical_surface().make_all_visible(limit=20) -mss.plot(edge_labels=False,polygon_labels=False) +gs = mss.graphical_surface(edge_labels=False, polygon_labels=False) +gs.make_all_visible(limit=20) +gs.plot() ``` -+++ {"deletable": true, "editable": true} - You can tell from the above picture that $m$ is in the Veech group. diff --git a/doc/examples/rel_deformations.md b/doc/examples/rel_deformations.md index f1bff8364..1818dbeb0 100644 --- a/doc/examples/rel_deformations.md +++ b/doc/examples/rel_deformations.md @@ -5,67 +5,46 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath -author: W. Patrick Hooper --- -+++ {"deletable": true, "editable": true} - # Relative Period Deformations -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- -from flatsurf import * -``` - -+++ {"deletable": true, "editable": true} - ## The Arnoux-Yoccoz surface -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- +from flatsurf import translation_surfaces + s = translation_surfaces.arnoux_yoccoz(3).canonicalize() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -field=s.base_ring() +field = s.base_ring() field ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -73,99 +52,75 @@ alpha = field.gen() AA(alpha) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -m=matrix(field,[[alpha,0],[0,1/alpha]]) +m = matrix(field, [[alpha, 0], [0, 1 / alpha]]) show(m) ``` -+++ {"deletable": true, "editable": true} - Check that $m$ is the derivative of a pseudo-Anosov of $s$. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -(m*s).canonicalize()==s +(m * s).canonicalize() == s ``` -+++ {"deletable": true, "editable": true} - ## Rel deformation A singularity of the surface is an equivalence class of vertices of the polygons making up the surface. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.singularity(0,0) +s.point(0, 0) ``` -+++ {"deletable": true, "editable": true} - We'll move this singularity to the right by two different amounts: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s1=s.rel_deformation({s.singularity(0,0):vector(field,(alpha/(1-alpha),0))}).canonicalize() +s1 = s.rel_deformation( + {s.point(0, 0): vector(field, (alpha / (1 - alpha), 0))} +).canonicalize() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -s2=s.rel_deformation({s.singularity(0,0):vector(field,(1/(1-alpha),0))}).canonicalize() +s2 = s.rel_deformation( + {s.point(0, 0): vector(field, (1 / (1 - alpha), 0))} +).canonicalize() ``` -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- -# Note that by the action of the derivative of the pseudo-Anosov we have: -``` ++++ {"jupyter": {"outputs_hidden": true}} -```{code-cell} ipython3 +Note that by the action of the derivative of the pseudo-Anosov we have: + +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s1==m*s2 +s1 == (m * s2).canonicalize() ``` -+++ {"deletable": true, "editable": true} - By a Theorem of Barak Weiss and the author of this notebook, these surfaces are all periodic in the vertical direction. You can see the vertical cylinders: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- diff --git a/doc/examples/saddle_connections.md b/doc/examples/saddle_connections.md index a6e7b672f..be2c5d9d4 100644 --- a/doc/examples/saddle_connections.md +++ b/doc/examples/saddle_connections.md @@ -5,56 +5,37 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath -author: W. Patrick Hooper --- -+++ {"deletable": true, "editable": true} - # Working with Saddle Connections -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -from flatsurf import * -``` +from flatsurf import translation_surfaces -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- s = translation_surfaces.veech_double_n_gon(5) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot() ``` -+++ {"deletable": true, "editable": true} - We get a list of all saddle connections of length less than 10. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -62,14 +43,10 @@ sc_list = s.saddle_connections(10) len(sc_list) ``` -+++ {"deletable": true, "editable": true} - The following removes duplicate saddle connections which appear with opposite orientations. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -81,68 +58,50 @@ sc_list2 = [sc for sc in sc_set] len(sc_list2) ``` -+++ {"deletable": true, "editable": true} - We pick two saddle connections: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -sc1 = sc_list2[-1] -sc2 = sc_list2[-2] +sc1 = sc_list2[-15] +sc2 = sc_list2[-12] ``` -+++ {"deletable": true, "editable": true} - We can find their holonomies and other information about them using methods. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -print("Holonomy of sc1 is"+str(sc1.holonomy())+" = "+str(sc1.holonomy().n())) -print("Holonomy of sc2 is"+str(sc2.holonomy())+" = "+str(sc2.holonomy().n())) +print("Holonomy of sc1 is" + str(sc1.holonomy()) + " = " + str(sc1.holonomy().n())) +print("Holonomy of sc2 is" + str(sc2.holonomy()) + " = " + str(sc2.holonomy().n())) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.plot() + sc1.plot() + sc2.plot(color="green") +s.plot() + sc1.plot(color="orange") + sc2.plot(color="green") ``` -+++ {"deletable": true, "editable": true} - We can test that they intersect. By default the singularity does not count. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- sc1.intersects(sc2) ``` -+++ {"deletable": true, "editable": true} - We can get an iterator over the set of intersection points: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -150,49 +109,37 @@ for p in sc1.intersections(sc2): print(p) ``` -+++ {"deletable": true, "editable": true} - It is a good idea to store the intersections in a list if you want to reuse them: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- intersections = [p for p in sc1.intersections(sc2)] ``` -+++ {"deletable": true, "editable": true} - We can plot the intersection points: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -plot = s.plot() + sc1.plot() + sc2.plot(color="green") +plot = s.plot() + sc1.plot(color="orange") + sc2.plot(color="green") for p in intersections: plot += p.plot(color="red", zorder=3) plot ``` -+++ {"deletable": true, "editable": true} - We can plot all the saddle connections: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -plot=s.plot(edge_labels=False, polygon_labels=False) +plot = s.plot(edge_labels=False, polygon_labels=False) for sc in sc_list: plot += sc.plot(thickness=0.05) plot @@ -200,62 +147,48 @@ plot We will build a subset of the saddle connection graph where vertices are saddle connections and two vertices are joined by an edge if and only if the saddle connections do not intersect (on their interiors). -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -# Build intersection graph -d={} +d = {} + for i in range(len(sc_list2)): - for j in range(i+1,len(sc_list2)): + for j in range(i + 1, len(sc_list2)): if not sc_list2[i].intersects(sc_list2[j]): if i not in d: - d[i]=[j] + d[i] = [j] else: d[i].append(j) if j not in d: - d[j]=[i] + d[j] = [i] else: d[j].append(i) -``` -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: false ---- -g=Graph(d) +g = Graph(d) ``` We place the vertex of a saddle connection with holonomy $z \in {\mathbb C}$ at the point $z^2$. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -pos={} +pos = {} for i in range(len(sc_list2)): sc = sc_list2[i] val = sc.holonomy().n() - z = val[0]+I*val[1] - w = z**2/z.abs() - pos[i]=(w.real(),w.imag()) + z = val[0] + I * val[1] + w = z**2 / z.abs() + pos[i] = (w.real(), w.imag()) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -g.plot(pos=pos,vertex_labels=False,vertex_size=0) +g.plot(pos=pos, vertex_labels=False, vertex_size=0) ``` diff --git a/doc/examples/siegel_veech.md b/doc/examples/siegel_veech.md index 94b2ee871..2fc06a230 100644 --- a/doc/examples/siegel_veech.md +++ b/doc/examples/siegel_veech.md @@ -5,9 +5,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath --- @@ -23,27 +23,29 @@ our installation instructions if this library is not available on your system ye We start by creating a surface with [sage-flatsurf](https://github.com/flatsurf/sage-flatsurf). -```{code-cell} ipython3 +```{code-cell} from flatsurf import translation_surfaces + S = translation_surfaces.mcmullen_L(1, 1, 1, 1) ``` -```{code-cell} ipython3 +```{code-cell} S.plot() ``` Decomposition of a surface into cylinders is implemented in [pyflatsurf](https://github.com/flatsurf/flatsurf). We triangulate our surface and make sure that its vertices are singularities. -```{code-cell} ipython3 +```{code-cell} from flatsurf.geometry.pyflatsurf_conversion import to_pyflatsurf + S = to_pyflatsurf(S) S = S.eliminateMarkedPoints().surface() ``` We will iterate over all directions coming from saddle connections of length at most L (ignoring connections that have the same slope.) -```{code-cell} ipython3 -L = 16R +```{code-cell} +L = int(16) directions = S.connections().bound(L).slopes() ``` @@ -52,7 +54,7 @@ For each direction we want to compute a decomposition into cylinders and minimal Here we define the target of the decomposition, i.e., a predicate that determines when a decomposition of a component can be stopped: -```{code-cell} ipython3 +```{code-cell} def target(component): if component.cylinder(): # This component is a cylinder. No further decomposition needed. @@ -62,22 +64,26 @@ def target(component): return True height = component.height() - + # This height bounds the size of any cylinder. However, it is stretched by the length of the vector # defining the vertical direction. (That vector is not normalized because that is hard to do in # general rings…) from pyflatsurf import flatsurf - bound = (height * height) / flatsurf.Bound.upper(component.vertical().vertical()).squared() + + bound = (height * height) / flatsurf.Bound.upper( + component.vertical().vertical() + ).squared() return bound > L ``` Now we perform the actual decomposition and collect the cylinders of circumference $≤L$: -```{code-cell} ipython3 +```{code-cell} circumferences = [] for direction in directions: from pyflatsurf import flatsurf + decomposition = flatsurf.makeFlowDecomposition(S, direction.vector()) decomposition.decompose(target) for component in decomposition.components(): @@ -90,13 +96,14 @@ for direction in directions: We will plot a histogram of all the cylinders that we found ordered by their length. It would be easy to plot this differently, weighted by the area, … -```{code-cell} ipython3 -lengths = [sqrt(float(v.x())**2 + float(v.y())**2) for v in circumferences] +```{code-cell} +lengths = [sqrt(float(v.x()) ** 2 + float(v.y()) ** 2) for v in circumferences] import matplotlib.pyplot as plot + _ = plot.hist(lengths) _ = plot.xlim(0, L) _ = plot.title(f"{len(circumferences)} cylinders with length at most {L}") -_ = plot.xlabel('Length') -_ = plot.ylabel('Count') +_ = plot.xlabel("Length") +_ = plot.ylabel("Count") ``` diff --git a/doc/examples/straight_line_flow.md b/doc/examples/straight_line_flow.md index 2854e7c01..46a3415c5 100644 --- a/doc/examples/straight_line_flow.md +++ b/doc/examples/straight_line_flow.md @@ -5,198 +5,143 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath -author: W. Patrick Hooper --- -+++ {"deletable": true, "editable": true} - # Straight-Line Flow -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- -from flatsurf import * -``` - -+++ {"deletable": true, "editable": true} - ## Acting on surfaces by matrices. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- +from flatsurf import translation_surfaces + s = translation_surfaces.veech_double_n_gon(5) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot() ``` -+++ {"deletable": true, "editable": true} +Defines the tangent_bundle on the surface defined over the ``base_ring`` of s. -Defines the tangent_bundle on the surface defined over the base_ring of s. - -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- TB = s.tangent_bundle() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -baricenter = sum(s.polygon(0).vertices())/5 +baricenter = sum(s.polygon(0).vertices()) / 5 ``` -+++ {"deletable": true, "editable": true} - Define the tangent vector based at the baricenter of polygon 0 aimed downward. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -v = TB(0, baricenter, (0,-1)) +v = TB(0, baricenter, (0, -1)) ``` -+++ {"deletable": true, "editable": true} - Convert to a straight-line trajectory. Trajectories are unions of segments in polygons. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- traj = v.straight_line_trajectory() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.plot()+traj.plot() +s.plot() + traj.plot() ``` -+++ {"deletable": true, "editable": true} - Flow into the next $100$ polygons or until the trajectory hits a vertex. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- traj.flow(100) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.plot()+traj.plot() +s.plot() + traj.plot() ``` -+++ {"deletable": true, "editable": true} - We can tell its type. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- traj.is_saddle_connection() ``` -+++ {"deletable": true, "editable": true} - You can also test if a straight-line trajectory is closed or a forward/backward separatrix. Lets do it again but in the slope one direction. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -v = TB(0, baricenter, (1,1)) +v = TB(0, baricenter, (1, 1)) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -traj=v.straight_line_trajectory() +traj = v.straight_line_trajectory() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- traj.flow(100) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s.plot()+traj.plot() +s.plot() + traj.plot() ``` -+++ {"deletable": true, "editable": true} - We remark that it follows from work of Veech that the slope one direction is ergodic for the straight-line flow. diff --git a/doc/examples/warwick-2017.md b/doc/examples/warwick-2017.md index 4e2d93945..b6cefbf5d 100644 --- a/doc/examples/warwick-2017.md +++ b/doc/examples/warwick-2017.md @@ -5,49 +5,32 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.10.3 + jupytext_version: 1.14.6 kernelspec: - display_name: SageMath 9.2 + display_name: SageMath 9.7 language: sage name: sagemath -author: W. Patrick Hooper --- -+++ {"deletable": true, "editable": true} - # Notes from the Warwick EPSRC Symposium on "Computation in geometric topology" -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- -from flatsurf import * -``` - -+++ {"deletable": true, "editable": true} - ## Veech group elements (affine symmetries) Veech's double n-gon surfaces: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- +from flatsurf import translation_surfaces + s = translation_surfaces.veech_double_n_gon(5).canonicalize() s.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -56,114 +39,83 @@ modulus = (p.vertex(3)[1] - p.vertex(2)[1]) / (p.vertex(2)[0] - p.vertex(4)[0]) AA(modulus) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- m = matrix(s.base_ring(), [[1, 2], [0, 1]]) show(matrix(AA, m)) -ss = m*s +ss = m * s ss.plot() ``` -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: false ---- -ss.delaunay_decomposition().plot() +```{code-cell} +ss = ss.delaunay_decomposition() +ss.plot() ``` -+++ {"deletable": true, "editable": true} - -The following checks that the matrix m stabilizes s: +The following checks that the matrix m stabilizes s; actually, it does not, see [#230](https://github.com/flatsurf/sage-flatsurf/issues/230): -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- ss.canonicalize() == s ``` -+++ {"deletable": true, "editable": true} - ## Geodesics -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s = translation_surfaces.veech_double_n_gon(5) ``` -+++ {"deletable": true, "editable": true} - The tangent bundle of the surface: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- TB = s.tangent_bundle() ``` -+++ {"deletable": true, "editable": true} - Define a tangent vector in polygon $0$ starting at $(\frac{1}{2}, 0)$ and pointed in some direction: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -direction = s.polygon(0).vertex(2) + 3*s.polygon(0).vertex(3) -v = TB(0, (1/2, 0), direction) +direction = s.polygon(0).vertex(2) + 3 * s.polygon(0).vertex(3) +v = TB(0, (1 / 2, 0), direction) ``` -+++ {"deletable": true, "editable": true} - Convert the vector to a straight-line trajectory. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- traj = v.straight_line_trajectory() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot() + traj.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -173,83 +125,61 @@ print(traj.combinatorial_length()) s.plot() + traj.plot() ``` -+++ {"deletable": true, "editable": true} - ## Cone surfaces from polyhedra Polyhedra are built into Sage and you can use them to build a translation surface. In this demo we only use a built-in function for a Platonic Solid. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -from flatsurf.geometry.polyhedra import * -``` +from flatsurf.geometry.polyhedra import platonic_dodecahedron -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: true ---- polyhedron, s, mapping = platonic_dodecahedron() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- polyhedron.plot(frame=False) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot(polygon_labels=False, edge_labels=False) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- TB = s.tangent_bundle() -direction = s.polygon(0).vertex(2) + 2*s.polygon(0).vertex(3) -v = TB(0, (1/2, 0), direction) +direction = s.polygon(0).vertex(2) + 2 * s.polygon(0).vertex(3) +v = TB(0, (1 / 2, 0), direction) traj = v.straight_line_trajectory() traj.flow(100) print(traj.is_closed()) print(traj.combinatorial_length()) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- s.plot() + traj.plot() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -258,52 +188,42 @@ G += line3d(mapping(traj), radius=0.02, frame=False) G ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- TB = s.tangent_bundle() -direction = s.polygon(0).vertex(2) + 3*s.polygon(0).vertex(3) -v = TB(0, (1/2, 0), direction) +direction = s.polygon(0).vertex(2) + 3 * s.polygon(0).vertex(3) +v = TB(0, (1 / 2, 0), direction) traj = v.straight_line_trajectory() traj.flow(1000) print(traj.is_closed()) print(traj.combinatorial_length()) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- show(s.plot() + traj.plot()) ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- p = polyhedron.plot(frame=False, point=False, line=False, wireframe=None) p += line3d(mapping(traj), radius=0.02, frame=False) -p.show(viewer='tachyon', frame=False) +p.show(viewer="tachyon", frame=False) ``` -+++ {"deletable": true, "editable": true} - ## Relative period deformations -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- @@ -311,87 +231,68 @@ s = translation_surfaces.veech_2n_gon(5) s.plot(edge_labels=False, polygon_labels=False) ``` -+++ {"deletable": true, "editable": true} - Currently we have to triangulate to do a rel deformation. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -s = s.triangulate().copy(relabel=True, mutable=True) +s = s.triangulate() ``` -+++ {"deletable": true, "editable": true} - A singularity is an equivalence class of vertices of polygons. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -sing = s.singularity(0, 0) +sing = s.point(0, 0) sing ``` -+++ {"deletable": true, "editable": true} - We can now deform by moving one singularity relative to the others. Here is a small deformation in the slope one direction. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -ss = s.rel_deformation({sing: vector(s.base_ring(), (1/20, 1/20))}) +ss = s.rel_deformation({sing: vector(s.base_ring(), (1 / 20, 1 / 20))}) ss.plot() ``` -+++ {"deletable": true, "editable": true} - A larger deformation: -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -ss = s.rel_deformation({sing:vector(s.base_ring(), (100, 100))}) +ss = s.rel_deformation({sing: vector(s.base_ring(), (100, 100))}) ss.plot() ``` -+++ {"deletable": true, "editable": true} - ## The Necker Cube Surface I'm demonstrating a result (in progress) of Pavel Javornik, an undergraduate at City College of New York. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectory -class SurfaceToSpaceMapping(SageObject): +class SurfaceToSpaceMapping(SageObject): def __init__(self, similarity_surface, tranformation): self._s = similarity_surface from types import FunctionType + if isinstance(transformation, FunctionType): self.transformation = transformation @@ -403,7 +304,7 @@ class SurfaceToSpaceMapping(SageObject): is v mapsto m*v + t where v is a point in the polygon. """ return self._t[label] - + def image_polygon(self, label): r""" Return a 2-dimensional polyhedron in 3-space representing @@ -411,11 +312,19 @@ class SurfaceToSpaceMapping(SageObject): """ p = self._s.polygon(label) m, t = self.transformation(label) - vertices = [m*v + t for v in p.vertices()] + vertices = [m * v + t for v in p.vertices()] return Polyhedron(vertices=vertices) - def plot(self, labels, point=False, line=False, polygon=None, - wireframe=None, frame=False, label_to_color=None): + def plot( + self, + labels, + point=False, + line=False, + polygon=None, + wireframe=None, + frame=False, + label_to_color=None, + ): r""" Return a 3d plot of the polygonal images in 3-space corresponding to the collection of labels. @@ -426,37 +335,47 @@ class SurfaceToSpaceMapping(SageObject): it = iter(labels) label = next(it) if label_to_color is None: - p = self.image_polygon(label).plot(point=point, - line=line, - polygon=polygon, - wireframe=wireframe, - frame=frame, - color="pink") + p = self.image_polygon(label).plot( + point=point, + line=line, + polygon=polygon, + wireframe=wireframe, + frame=frame, + color="pink", + ) else: - p = self.image_polygon(label).plot(point=point, - line=line, - polygon=polygon, - wireframe=wireframe, - frame=frame, - color=label_to_color(label)) + p = self.image_polygon(label).plot( + point=point, + line=line, + polygon=polygon, + wireframe=wireframe, + frame=frame, + color=label_to_color(label), + ) for label in it: if label_to_color is None: - p += self.image_polygon(label).plot(point=point, - line=line, - polygon=polygon, - wireframe=wireframe, - frame=frame, - color="pink") + p += self.image_polygon(label).plot( + point=point, + line=line, + polygon=polygon, + wireframe=wireframe, + frame=frame, + color="pink", + ) else: - p += self.image_polygon(label).plot(point=point, - line=line, - polygon=polygon, - wireframe=wireframe, - frame=frame, - color=label_to_color(label)) + p += self.image_polygon(label).plot( + point=point, + line=line, + polygon=polygon, + wireframe=wireframe, + frame=frame, + color=label_to_color(label), + ) from sage.modules.free_module_element import vector - p.frame_aspect_ratio(tuple(vector(p.bounding_box()[1]) - - vector(p.bounding_box()[0]))) + + p.frame_aspect_ratio( + tuple(vector(p.bounding_box()[1]) - vector(p.bounding_box()[0])) + ) return p def __call__(self, o): @@ -476,18 +395,18 @@ class SurfaceToSpaceMapping(SageObject): s = next(it) label = s.polygon_label() m, t = self.transformation(label) - points.append(t + m*s.start().point()) - points.append(t + m*s.end().point()) + points.append(t + m * s.start().point()) + points.append(t + m * s.end().point()) for s in it: label = s.polygon_label() m, t = self.transformation(label) - points.append(t + m*s.end().point()) + points.append(t + m * s.end().point()) return points if isinstance(o, SegmentInPolygon): # Return the pair of images of the endpoints. label = o.polygon_label() m, t = self.transformation(label) - return (t + m*o.start().point(), t + m*o.end().point()) + return (t + m * o.start().point(), t + m * o.end().point()) if isinstance(o, SimilaritySurfaceTangentVector): # Map to a pair of vectors consisting of the image # of the basepoint and the image of the vector. @@ -495,28 +414,57 @@ class SurfaceToSpaceMapping(SageObject): m, t = self.transformation(label) point = o.point() vector = o.vector() - return (t + m*point, m*vector) + return (t + m * point, m * vector) raise ValueError("Failed to recognize type of passed object") ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- -from flatsurf.geometry.surface import Surface -from flatsurf.geometry.polygon import ConvexPolygons -from flatsurf.geometry.similarity import SimilarityGroup -class CubeSurf(Surface): +from flatsurf.geometry.surface import OrientedSimilaritySurface +from flatsurf.geometry.categories import ConeSurfaces +from flatsurf.geometry.polygon import Polygon + + +class CubeSurf(OrientedSimilaritySurface): def __init__(self, F): - ZZ3 = IntegerModRing(3) - P = ConvexPolygons(F) - self._faceA = P(vertices = [(0, 0), (1, 0), (1, 1), (0, 1)]) - self._faceB = P(vertices = [(0, 0), (1, 0), (1, 1), (0, 1)]) - self._faceC = P(vertices = [(0, 0), (1, 0), (1, 1), (0, 1)]) - Surface.__init__(self, F, (ZZ(0), ZZ(0), ZZ3(0)), finite=False, mutable=False) + self._faceA = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], base_ring=F) + self._faceB = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], base_ring=F) + self._faceC = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], base_ring=F) + super().__init__( + F, + category=ConeSurfaces() + .Rational() + .InfiniteType() + .WithoutBoundary() + .Connected(), + ) + + def is_mutable(self): + return False + + def is_compact(self): + return False + + def roots(self): + return ((0, 0, IntegerModRing(3)(0)),) + + def is_translation_surface(self, positive=True): + return False + + def is_dilation_surface(self, positive=False): + return False + + def __eq__(self, other): + if not isinstance(other, CubeSurf): + return False + + return self.base_ring() is other.base_ring() + + def __hash__(self): + return hash(self.base_ring()) def polygon(self, label): x, y, l = label @@ -532,77 +480,64 @@ class CubeSurf(Surface): # l(0) = A, l(1) = B, l(2) = C if l == 0: if edge == 0: - return((x, y - 1, l + 2), 2) + return ((x, y - 1, l + 2), 2) if edge == 1: - return((x, y, l + 1), 3) + return ((x, y, l + 1), 3) if edge == 2: - return((x, y, l + 2), 0) + return ((x, y, l + 2), 0) if edge == 3: - return((x - 1, y, l + 1), 1) + return ((x - 1, y, l + 1), 1) if l == 1: if edge == 0: - return((x + 1, y - 1, l + 1), 3) + return ((x + 1, y - 1, l + 1), 3) if edge == 1: - return((x + 1, y, l + 2), 3) + return ((x + 1, y, l + 2), 3) if edge == 2: - return((x, y, l + 1), 1) + return ((x, y, l + 1), 1) if edge == 3: - return((x, y, l + 2), 1) + return ((x, y, l + 2), 1) if l == 2: if edge == 0: - return((x, y, l + 1), 2) + return ((x, y, l + 1), 2) if edge == 1: - return((x, y, l + 2), 2) + return ((x, y, l + 2), 2) if edge == 2: - return((x, y + 1, l + 1), 0) + return ((x, y + 1, l + 1), 0) if edge == 3: - return((x - 1 , y + 1, l + 2), 0) + return ((x - 1, y + 1, l + 2), 0) +``` -SG = SimilarityGroup(QQ) -def default_position(label): - x, y, l = label - if(ZZ(l) == 0): - return SG(2*x, 2*y) # (b + c) x, (a + c) y - if(ZZ(l) == 1): - return SG(2*x + 1, 2*y) # (b + c) x + c, (a + c) y - if(ZZ(l) == 2): - return SG(2*x, 2*y + 1) # (b + c) x, (a + c) y + c - # Reminder to parameterize a, b, c here for positions. - # Rework this to work for surfaces of different sizes. +```{code-cell} +s = CubeSurf(QQ) ``` -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: false ---- -s = SimilaritySurface(CubeSurf(QQ)) +```{code-cell} +TestSuite(s).run() ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -MM = matrix(QQ,[[0, 1, 0], - [-1, 0, 0], - [0, 0, 1] -]) +MM = matrix(QQ, [[0, 1, 0], [-1, 0, 0], [0, 0, 1]]) + + def transformation(label): M = MatrixSpace(QQ, 3, 2) V = VectorSpace(QQ, 3) x, y, l = label if l == 0: - return MM*M([[1, 0], [0, 1], [0, 0]]), MM*V([x, y, -x - y]) + return MM * M([[1, 0], [0, 1], [0, 0]]), MM * V([x, y, -x - y]) elif l == 1: - return MM*M([[0, 0], [0, 1], [-1, 0]]), MM*V([x + 1, y, -x - y]) + return MM * M([[0, 0], [0, 1], [-1, 0]]), MM * V([x + 1, y, -x - y]) else: # l == 2 - return MM*M([[1, 0], [0, 0], [0, -1]]), MM*V([x, y + 1, -x - y]) + return MM * M([[1, 0], [0, 0], [0, -1]]), MM * V([x, y + 1, -x - y]) + + m = SurfaceToSpaceMapping(s, transformation) + + def label_to_color(label): if label[2] == 0: return "pink" @@ -612,47 +547,38 @@ def label_to_color(label): return "beige" ``` -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -it = s.label_iterator() -m.plot({next(it) for i in range(30)}, label_to_color=label_to_color) -``` +from itertools import islice -+++ {"deletable": true, "editable": true} +m.plot(set(islice(s.labels(), 30)), label_to_color=label_to_color) +``` Theorem (Pavel Javornik). A trajectory of rational slope (measured on one of the squares interpreted to have horizontal and vertical sides) on the Necker Cube Surface closes up if and only if the slope can be expressed as the ratio of two odd integers. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: true --- B = s.tangent_bundle() ``` -+++ {"deletable": true, "editable": true} - The following builds a trajectory starting in the base polygon at the point $(\frac{1}{4}, \frac{1}{4})$ and traveling in a direction of slope one. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -v = B(s.base_label(), (1/4, 1/4), (-1, 1)) +v = B(s.root(), (1 / 4, 1 / 4), (-1, 1)) traj = v.straight_line_trajectory() traj.flow(100) if traj.is_closed(): @@ -661,40 +587,32 @@ labels = [seg.polygon_label() for seg in traj.segments()] m.plot(labels, label_to_color=label_to_color) + line3d(m(traj), radius=0.02) ``` -+++ {"deletable": true, "editable": true} - A trajectory of slope $5/4$. -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -v = B(s.base_label(), (1/3, 1/4), (4, 5)) +v = B(s.root(), (1 / 3, 1 / 4), (4, 5)) traj = v.straight_line_trajectory() traj.flow(50) labels = [seg.polygon_label() for seg in traj.segments()] -p = m.plot(labels, label_to_color=label_to_color) + line3d(m(traj), - radius=0.04, label_to_color=label_to_color) -p.frame_aspect_ratio(tuple(vector(p.bounding_box()[1]) - - vector(p.bounding_box()[0]))) +p = m.plot(labels, label_to_color=label_to_color) + line3d( + m(traj), radius=0.04, label_to_color=label_to_color +) +p.frame_aspect_ratio(tuple(vector(p.bounding_box()[1]) - vector(p.bounding_box()[0]))) p ``` -+++ {"deletable": true, "editable": true} - A trajectory of slope $11/9$ -```{code-cell} ipython3 +```{code-cell} --- -deletable: true -editable: true jupyter: outputs_hidden: false --- -v = B(s.base_label(), (1/3, 1/4), (9, 11)) +v = B(s.root(), (1 / 3, 1 / 4), (9, 11)) traj = v.straight_line_trajectory() traj.flow(1000) while not traj.is_closed(): @@ -702,25 +620,5 @@ while not traj.is_closed(): labels = [seg.polygon_label() for seg in traj.segments()] p = m.plot(labels, label_to_color=label_to_color) p += line3d(m(traj), radius=0.04) -# p -``` - -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: false ---- -show(p, frame=False, viewer="tachyon") -``` - -```{code-cell} ipython3 ---- -deletable: true -editable: true -jupyter: - outputs_hidden: false ---- -# show(p, frame=False) +p ``` diff --git a/doc/geometry.rst b/doc/geometry.rst index aa7e1fafd..565c462e6 100644 --- a/doc/geometry.rst +++ b/doc/geometry.rst @@ -6,37 +6,45 @@ The flatsurf.geometry Package .. toctree:: :maxdepth: 1 + geometry/categories geometry/chamanara geometry/circle - geometry/cone_surface + geometry/cone_surfaces geometry/delaunay - geometry/dilation_surface + geometry/dilation_surfaces + geometry/euclidean geometry/finitely_generated_matrix_group geometry/fundamental_group geometry/gl2r_orbit_closure geometry/half_dilation_surface - geometry/half_translation_surface + geometry/half_translation_surfaces geometry/hyperbolic + geometry/hyperbolic_polygons geometry/interval_exchange_transformation geometry/l_infinity_delaunay_cells geometry/mappings - geometry/matrix_2x2 geometry/mega_wollmilchsau geometry/minimal_cover + geometry/origami geometry/polygon + geometry/polygonal_surfaces + geometry/polygons geometry/polyhedra geometry/pyflatsurf_conversion - geometry/rational_cone_surface - geometry/rational_similarity_surface + geometry/euclidean_polygonal_surfaces + geometry/euclidean_polygons + geometry/euclidean_polygons_with_angles geometry/relative_homology geometry/similarity + geometry/similarity_surfaces geometry/similarity_surface_generators - geometry/similarity_surface geometry/straight_line_trajectory geometry/subfield + geometry/surface_category + geometry/surface_legacy geometry/surface_objects geometry/surface geometry/tangent_bundle geometry/thurston_veech - geometry/translation_surface - geometry/xml + geometry/topological_surfaces + geometry/translation_surfaces diff --git a/doc/geometry/categories.rst b/doc/geometry/categories.rst new file mode 100644 index 000000000..c8fc9830d --- /dev/null +++ b/doc/geometry/categories.rst @@ -0,0 +1,4 @@ +``categories`` +============== + +.. automodule:: flatsurf.geometry.categories diff --git a/doc/geometry/cone_surface.rst b/doc/geometry/cone_surface.rst deleted file mode 100644 index 1c37c0fa9..000000000 --- a/doc/geometry/cone_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``cone_surface`` -================ - -.. automodule:: flatsurf.geometry.cone_surface - :members: - :undoc-members: diff --git a/doc/geometry/cone_surfaces.rst b/doc/geometry/cone_surfaces.rst new file mode 100644 index 000000000..c5152b25f --- /dev/null +++ b/doc/geometry/cone_surfaces.rst @@ -0,0 +1,8 @@ +``cone_surfaces`` +================= + +.. automodule:: flatsurf.geometry.categories.cone_surfaces + +.. autoclass:: flatsurf.geometry.categories.cone_surfaces.ConeSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/dilation_surface.rst b/doc/geometry/dilation_surface.rst deleted file mode 100644 index b9eb79ca5..000000000 --- a/doc/geometry/dilation_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``dilation_surface`` -==================== - -.. automodule:: flatsurf.geometry.dilation_surface - :members: - :undoc-members: diff --git a/doc/geometry/dilation_surfaces.rst b/doc/geometry/dilation_surfaces.rst new file mode 100644 index 000000000..ed4efe8e6 --- /dev/null +++ b/doc/geometry/dilation_surfaces.rst @@ -0,0 +1,8 @@ +``dilation_surfaces`` +===================== + +.. automodule:: flatsurf.geometry.categories.dilation_surfaces + +.. autoclass:: flatsurf.geometry.categories.dilation_surfaces.DilationSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/euclidean.rst b/doc/geometry/euclidean.rst new file mode 100644 index 000000000..a72116a24 --- /dev/null +++ b/doc/geometry/euclidean.rst @@ -0,0 +1,6 @@ +``euclidean`` +============= + +.. automodule:: flatsurf.geometry.euclidean + :members: + :undoc-members: diff --git a/doc/geometry/euclidean_polygonal_surfaces.rst b/doc/geometry/euclidean_polygonal_surfaces.rst new file mode 100644 index 000000000..cf2f2706a --- /dev/null +++ b/doc/geometry/euclidean_polygonal_surfaces.rst @@ -0,0 +1,8 @@ +``euclidean_polygonal_surfaces`` +====================================== + +.. automodule:: flatsurf.geometry.categories.euclidean_polygonal_surfaces + +.. autoclass:: flatsurf.geometry.categories.euclidean_polygonal_surfaces.EuclideanPolygonalSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/euclidean_polygons.rst b/doc/geometry/euclidean_polygons.rst new file mode 100644 index 000000000..d388eb67b --- /dev/null +++ b/doc/geometry/euclidean_polygons.rst @@ -0,0 +1,8 @@ +``euclidean_polygons`` +============================ + +.. automodule:: flatsurf.geometry.categories.euclidean_polygons + +.. autoclass:: flatsurf.geometry.categories.euclidean_polygons.EuclideanPolygons + :members: + :undoc-members: diff --git a/doc/geometry/euclidean_polygons_with_angles.rst b/doc/geometry/euclidean_polygons_with_angles.rst new file mode 100644 index 000000000..d1e0ceab8 --- /dev/null +++ b/doc/geometry/euclidean_polygons_with_angles.rst @@ -0,0 +1,8 @@ +``euclidean_polygons_with_angles`` +======================================== + +.. automodule:: flatsurf.geometry.categories.euclidean_polygons_with_angles + +.. autoclass:: flatsurf.geometry.categories.euclidean_polygons_with_angles.EuclideanPolygonsWithAngles + :members: + :undoc-members: diff --git a/doc/geometry/half_translation_surface.rst b/doc/geometry/half_translation_surface.rst deleted file mode 100644 index e9758a155..000000000 --- a/doc/geometry/half_translation_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``half_translation_surface`` -============================ - -.. automodule:: flatsurf.geometry.half_translation_surface - :members: - :undoc-members: diff --git a/doc/geometry/half_translation_surfaces.rst b/doc/geometry/half_translation_surfaces.rst new file mode 100644 index 000000000..4ff788931 --- /dev/null +++ b/doc/geometry/half_translation_surfaces.rst @@ -0,0 +1,8 @@ +``half_translation_surfaces`` +============================= + +.. automodule:: flatsurf.geometry.categories.half_translation_surfaces + +.. autoclass:: flatsurf.geometry.categories.half_translation_surfaces.HalfTranslationSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/hyperbolic_polygons.rst b/doc/geometry/hyperbolic_polygons.rst new file mode 100644 index 000000000..df1a5819d --- /dev/null +++ b/doc/geometry/hyperbolic_polygons.rst @@ -0,0 +1,8 @@ +``hyperbolic_polygons`` +======================= + +.. automodule:: flatsurf.geometry.categories.hyperbolic_polygons + +.. autoclass:: flatsurf.geometry.categories.hyperbolic_polygons.HyperbolicPolygons + :members: + :undoc-members: diff --git a/doc/geometry/matrix_2x2.rst b/doc/geometry/matrix_2x2.rst deleted file mode 100644 index 78217cd0c..000000000 --- a/doc/geometry/matrix_2x2.rst +++ /dev/null @@ -1,6 +0,0 @@ -``matrix_2x2`` -============== - -.. automodule:: flatsurf.geometry.matrix_2x2 - :members: - :undoc-members: diff --git a/doc/geometry/origami.rst b/doc/geometry/origami.rst new file mode 100644 index 000000000..aa2bc768c --- /dev/null +++ b/doc/geometry/origami.rst @@ -0,0 +1,6 @@ +``origami`` +=========== + +.. automodule:: flatsurf.geometry.origami + :members: + :undoc-members: diff --git a/doc/geometry/polygonal_surfaces.rst b/doc/geometry/polygonal_surfaces.rst new file mode 100644 index 000000000..54f7dbb78 --- /dev/null +++ b/doc/geometry/polygonal_surfaces.rst @@ -0,0 +1,8 @@ +``polygonal_surfaces`` +====================== + +.. automodule:: flatsurf.geometry.categories.polygonal_surfaces + +.. autoclass:: flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/polygons.rst b/doc/geometry/polygons.rst new file mode 100644 index 000000000..1bb5ab5fc --- /dev/null +++ b/doc/geometry/polygons.rst @@ -0,0 +1,8 @@ +``polygons`` +============= + +.. automodule:: flatsurf.geometry.categories.polygons + +.. autoclass:: flatsurf.geometry.categories.polygons.Polygons + :members: + :undoc-members: diff --git a/doc/geometry/rational_cone_surface.rst b/doc/geometry/rational_cone_surface.rst deleted file mode 100644 index 9905fc2e0..000000000 --- a/doc/geometry/rational_cone_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``rational_cone_surface`` -========================= - -.. automodule:: flatsurf.geometry.rational_cone_surface - :members: - :undoc-members: diff --git a/doc/geometry/rational_similarity_surface.rst b/doc/geometry/rational_similarity_surface.rst deleted file mode 100644 index b83d29258..000000000 --- a/doc/geometry/rational_similarity_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``rational_similarity_surface`` -=============================== - -.. automodule:: flatsurf.geometry.rational_similarity_surface - :members: - :undoc-members: diff --git a/doc/geometry/similarity_surface.rst b/doc/geometry/similarity_surface.rst deleted file mode 100644 index e0e29a0de..000000000 --- a/doc/geometry/similarity_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``similarity_surface`` -====================== - -.. automodule:: flatsurf.geometry.similarity_surface - :members: - :undoc-members: diff --git a/doc/geometry/similarity_surfaces.rst b/doc/geometry/similarity_surfaces.rst new file mode 100644 index 000000000..e1aed2781 --- /dev/null +++ b/doc/geometry/similarity_surfaces.rst @@ -0,0 +1,8 @@ +``similarity_surfaces`` +======================= + +.. automodule:: flatsurf.geometry.categories.similarity_surfaces + +.. autoclass:: flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces + :members: + :undoc-members: diff --git a/doc/geometry/surface_category.rst b/doc/geometry/surface_category.rst new file mode 100644 index 000000000..9a1b512e3 --- /dev/null +++ b/doc/geometry/surface_category.rst @@ -0,0 +1,6 @@ +``surface_category`` +==================== + +.. automodule:: flatsurf.geometry.categories.surface_category + :members: + :undoc-members: diff --git a/doc/geometry/surface_legacy.rst b/doc/geometry/surface_legacy.rst new file mode 100644 index 000000000..5f0b72885 --- /dev/null +++ b/doc/geometry/surface_legacy.rst @@ -0,0 +1,6 @@ +``surface_legacy`` +================== + +.. automodule:: flatsurf.geometry.surface_legacy + :members: + :undoc-members: diff --git a/doc/geometry/topological_surfaces.rst b/doc/geometry/topological_surfaces.rst new file mode 100644 index 000000000..9ffa439bf --- /dev/null +++ b/doc/geometry/topological_surfaces.rst @@ -0,0 +1,8 @@ +``topological_surfaces`` +======================== + +.. automodule:: flatsurf.geometry.categories.topological_surfaces + +.. autoclass:: flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/translation_surface.rst b/doc/geometry/translation_surface.rst deleted file mode 100644 index 44bda23e8..000000000 --- a/doc/geometry/translation_surface.rst +++ /dev/null @@ -1,6 +0,0 @@ -``translation_surface`` -======================= - -.. automodule:: flatsurf.geometry.translation_surface - :members: - :undoc-members: diff --git a/doc/geometry/translation_surfaces.rst b/doc/geometry/translation_surfaces.rst new file mode 100644 index 000000000..b24ca0b4c --- /dev/null +++ b/doc/geometry/translation_surfaces.rst @@ -0,0 +1,8 @@ +``translation_surfaces`` +======================== + +.. automodule:: flatsurf.geometry.categories.translation_surfaces + +.. autoclass:: flatsurf.geometry.categories.translation_surfaces.TranslationSurfaces + :members: + :undoc-members: diff --git a/doc/geometry/xml.rst b/doc/geometry/xml.rst deleted file mode 100644 index 677088f87..000000000 --- a/doc/geometry/xml.rst +++ /dev/null @@ -1,6 +0,0 @@ -``xml`` -======= - -.. automodule:: flatsurf.geometry.xml - :members: - :undoc-members: diff --git a/doc/news/category.rst b/doc/news/category.rst new file mode 100644 index 000000000..f2ebb9917 --- /dev/null +++ b/doc/news/category.rst @@ -0,0 +1,155 @@ +**Added:** + +* Added ``point()`` on surfaces which contains the features of both, ``singularity()`` and ``surface_point()``. (When passed a vertex id, it returns the point corresponding to that "singularity", when passed coordinates, it creates the point from the coordinates.) + +* Added a hierarchy of surface categories, ``TopologicalSurfaces``, ``PolygonalSurfaces``, ``SimilaritySurfaces``, ``DilationSurfaces``, ``ConeSurfaces``, ``TranslationSurfaces``, ``HyperbolicSurfaces``, and ``EuclideanPolygonalSurfaces`` together with axioms ``Connected``, ``Orientable``, ``Oriented``, ``WithBoundary``, ``WithoutBoundary``, ``FiniteType``, ``InfiniteType``, ``Positive``. These categories replace the existing hierarchy of ``SimilaritySurface``, ``DilationSurface``, ``HalfDilationSurface``, ``TranslationSurface``, ``HalfTranslationSurface``, ``RationalConeSurface``, ``RationalSimilaritySurface``, ``ConeSurface``. They serve essentially the same purpose, providing a place where to put functionality that applies to a certain kind of surface. However, this allows for more granularity, e.g., if some computation is only possible for translation surfaces of finite types, the method now lives in ``TranslationSurfaces.FiniteType`` and simply won't be available to surfaces that are of infinite type. + +* Added a hierarchy of polygon categories, ``Polygons``, ``HyperbolicPolygons``, ``EuclideanPolygons`` and ``EuclideanPolygonsWithAngles`` together with axioms ``Convex``, ``Rational``, and ``Simple``. These replace the existing differentiation between ``Polygon``, ``ConvexPolygon`` and ``EquiangularPolygons``. + +* Added a ``labels()`` method to surfaces. This replaces the deprecated ``label_iterator()`` method. The object returned by ``labels()`` can (often) be efficiently queried for its ``len`` and decide containment. + +* Added a ``polygons()`` method to surfaces. This essentially replaces the deprecated ``label_polygon_iterator()`` and ``label_iterator(polygons=True)`` method. The object returned by ``polygons()`` can be efficiently queried for its ``len`` so this also replaces the deprecated ``num_polygons``. + +* Added a ``roots()`` method to surfaces that returns labels from which iteration by ``labels()`` should start exploration of the connected components of a surface. There is also a ``root()`` method that returns the only such label on a connected surface. + +* Added a ``is_finite_type()`` method to surfaces. This replaces the deprecated ``is_finite()``. + +* Added an ``is_compact()`` method to surfaces that returns whether the surface is compact as a topological space. + +* Added an ``is_connected()`` method to surfaces that returns whether the surface is connected as a topological space. (Before such surfaces were not well supported.) + +* Added an ``is_with_boundary()`` method that returns whether a surface has polygons with unglued edges. (Before such surfaces were considered to be invalid now they are supported to some limited extent.) + +* Added an ``euler_characteristic()`` method for surfaces of finite type. + +* Added a ``change_ring()`` method to all surfaces to create a copy of the surface with polygons defined over a different base ring. + +* Added a ``vertices()`` method to all surfaces built from polygons that returns the set of equivalence classes of vertices of those polygons. + +* Added points of polygons as explicit objects (so that polygons become proper SageMath parents.) These points currently do not have many features. + +* Added a ``describe_polygon()`` method to all polygons to create nicer textual representation of polygons for error messages. + +* Added a ``is_degenerate()`` method to detect polygons with zero area or marked vertices. + +* Added a ``marked_vertices`` keyword to ``vertices()`` of polygons to control whether vertices with angle π are included in the output. + +* Added a ``erase_marked_vertices()`` method to polygons to produce a copy of the polygon without vertices with angle π. + +* Added ``is_equilateral()`` and ``is_equiangular()`` methods to Euclidean polygons. + +**Changed:** + +* Changed supported versions of SageMath. We now require at least SageMath 9.2 (released in October 2020.) + +* Changed the notion of "dilation surface" in some places. What was previously called a "half-dilation surface" is now called a "dilation surface", and what was previously called a "dilation surface" is now called a "positive dilation surface". (Existing code should not be affected by this but the documentation has been updated and new functions use this naming.) Note that the notions of a half-translation surface and a translation surface have not changed (though internally a translation surface is just a positive half-translation surface.) + +* Changed the mutability of the surface returned by ``polyhedron_to_cone_surface``; the returned surface is now immutable. + +* Changed the mutability of methods taking an ``in_place`` keyword argument. These methods now consistentlny return an immutable surface when ``in_place`` is ``False``. + +* Changed ``Polygon`` in ``flatsurf.geometry.polygon``; it has been renamed to ``EuclideanPolygon``. ``Polygon()`` is now a factory function that creates an euclidean polygon, compatible with the way that ``polygons()`` used to create such a polygon. + +* Changed the structure of hyperbolic sets. Points are now SageMath elements of their parent, the hyperbolic plane. Other sets are now (facade) parents. This change allows us to bring hyperbolic convex polygons as parents into the category of polygons. So, hyperbolic polygons and Euclidean polygons are on a similar footing and we can (eventually) build surfaces from both of them in the same way. + +* Renamed ``flatsurf.geometry.matrix_2x2`` to ``flatsurf.geometry.euclidean`` and moved more geometry helpers there. + +* Changed placement and naming of some geometry helper functions. For example, unified ``is_parallel`` and ``is_same_direction``, renamed ``is_opposite_direction`` to ``is_anti_parallel`` (and simplified implementation), renamed ``wedge_product`` to ``ccw`` and renamed ``wedge`` to ``wedge_product``. Most of these now live in ``flatsurf.geometry.euclidean``. + +* Changed the label parameter of ``add_polygon()``. It is now required to be a keyword argument. + +* Changed the meaning of the ``lengths`` parameter when creating a polygon from lengths and angles. Before, the slopes of a generic polygon with such angles were scaled by ``lengths`` (the slopes are a somewhat random implementation detail.) Now, the ``lengths`` are actually, the Euclidean lengths of the sides. Specifying ``lengths`` and ``angles`` might therefore lead to some square roots having to be computed. To get the old behaviour, one can specify angles and edges and use the slopes scaled by lengths as edges. + +**Deprecated:** + +* Deprecated the ``walker()`` method on surfaces. The ``labels()`` are now always guaranteed to be iterated in a canonical order (starting from the ``roots()``, a breadth-first search is performed.) + +* Deprecated the ``base_label()`` method on surfaces. The ``root()`` and ``roots()`` methods serve the same purpose but have clearer semantics for disconnected surfaces. + +* Deprecated the ``num_polygons()`` method on surfaces. For finite type surfaces, ``len(polygons())`` serves the same purpose (and is sufficiently fast.) + +* Deprecated the ``label_iterator()``, ``edge_iterator()``, ``label_polygon_iterator()``, ``edge_gluing_iterator()`` and ``polygon_iterator()`` methods on surfaces. The ``_iterator`` suffix has always been confusing to some. Also, returning an iterator has limitations, e.g., containment and length cannot be queried easily. + +* Deprecated the ``is_finite()`` method on surfaces. Since surfaces are now in the category of sets, ``is_finite()`` is also understood to answer whether the surface is a finite set of points. Eventually, ``is_finite()`` will change to return False for all non-empty surfaces. + +* Deprecated the ``field()`` method on polygons since its semantics were a bit confusing. (Does it return the fraction field of the base ring or complain if the base ring is not a field?) + +* Deprecated calling the object returned by ``Polygons()`` and ``ConvexPolygons()`` since they do not transfer well to the category framework (subcategories do not inherit ``__call__`` and ``Polygon()`` seems to be a more convenient alternative anyway.) + +* Deprecated ``convexity()`` and ``strict_convexity()`` from ``EquiangularPolygons`` in favor of ``is_convex(strict=False)`` that is identical to the convexity method on polygons. + +* Deprecated ``module()`` and ``vector_space()`` on polygons and ``EquiangularPolygons`` in favor of ``base_ring()**2``. + +* Deprecated ``num_singularities()`` in favor of ``vertices()`` since the count does not distinguish between singularities and marked points. + +* Deprecated the ``translation`` keyword argument of ``vertices()`` of a polygon; ``translate().vertices()`` seems to be the more straightforward approach. + +* Deprecated ``is_strictly_convex()`` for polygons; it has been replaced with a ``strict`` keyword for ``is_convex()``. + +* Deprecated ``num_edges()`` for polygons; it is essentially equivalent to ``len(vertices())`` (and "There should be one-- and preferably only one --obvious way to do it.") + +* Deprecated implicitly iterating over the vertices of a polygon; this is problematic since a polygon is now the parent of its infinitely many points (and iterating over vertices() is easier to understand and equivalent anyway.) + +* Deprecated ``add_polygons()`` for mutable surfaces; there is no benefit over adding polygons in a loop with ``add_polygon()``. + +* Deprecated ``change_base_label()`` on surfaces; it has been replaced by ``set_root()`` and ``set_roots()`` to also support disonnected surfaces. + +* Deprecated ``set_edge_pairing()`` and ``change_edge_gluing()`` for similarity surfaces; they have been replaced by ``glue()``. + +* Deprecated ``change_polygon_gluings()``; it has a confusing syntax (and semantics) and using ``glue()`` in a loop does the same. + +* Deprecated ``change_polygon()`` for surfaces; it had strange side effects in some cases; ``replace_polygon()`` should be easier to use. + +**Removed:** + +* Removed the ``cached`` parameter from ``.graphical_surface()`` of surfaces. The graphical surface returned is now never cached. If you want to customize the graphical surface returned you need to subclass the surface and add custom logic explicitly. (The "caching" that used to happen here made immutable surfaces in fact mutable and led to problems with serialization and equality testing in the past.) + +* Removed ``flatsurf.geometry.xml``. It did not correctly serialize all kinds of surfaces and most likely nobody has been using it. If you relied on this functionality please let us know so we can bring it back in some form. + +* Removed the ``limit`` keyword from ``delaunay_triangulation()``. + +* Removed undocumented and untested method ``delaunay_single_join()`` from surfaces. + +* Removed the ``_label_comparator`` from surfaces since it did not produce a consistent ordering on different architectures. There is now a ``min`` on labels, e.g., on a ``LabelView`` which just uses the builtin ``min`` when it works and otherwise compares the ``repr`` of the labels. This approach also has problems (see documentation) but at least it is not platform dependent on the most common inputs such as polygons labeled by strings or numbers. + +* Removed the untested ``standardize_polygons()`` for infinite type surfaces. + +* Removed the possibility to ``canonicalize()`` a translation surface in-place. (This is a very expensive operation anyway and there does not seem to be a benefit to do this operation in-place.) + +* Removed the ``n`` keyword argument in ``chamanara_surface(alpha, n)``. This keyword only affected plotting. It is ignored now and will be an error in a future version of sage-flatsurf. + +* Removed the ``relabel`` argument in ``LazyTriangulatedSurface``, ``LazyDelaunayTriangulatedSurface``, and ``LazyDelaunaySurface``. + +* Removed unused and untested ``translation_surface_cmp()`` from ``flatsurf.geometry.mappings``. + +* Removed ``set_default_graphical_surface()``; if we allow this, we need to add the graphical surface to equality checks and hashing which is confusing. + +**Fixed:** + +* Fixed conversion of surfaces from flipper to sage-flatsurf. + +* Fixed ``genus()`` for surfaces with self-glued edges. + +* Fixed ``stratum()`` for half-translation surfaces with self-glued edges. + +* Fixed (the deprecated) ``num_polygons()`` for disconnected surfaces. + +* Fixed (most of the deprecated) ``*_iterator()`` methods for disconnected surfaces. + +* Fixed ``subdivide_polygon(test=True)`` which sometimes returned ``None``. + +* Fixed ``is_delaunay_triangulated()`` which now does not print to stdout anymore. + +* Fixed ``is_delaunay_decomposed()`` which now checks not only the first polygon to decide whether a surface is Delaunay decomposed. + +* Fixed ``standardize_polygons()`` which is now available on all similarity surfaces and not only on translation surfaces. + +* Fixed ``LazyTriangulatedSurface``, ``LazyDelaunayTriangulatedSurface``, and ``LazyDelaunaySurface``. It is not necessary to walk the labels of such a surface before accessing the structure of the surface anymore. + +* Fixed ``change_ring()`` for hyperbolic polygons. We do not forget about marked points when changing the base ring anymore. + +* Fixed ``is_strictly_convex()`` for non-convex polygons. + +**Performance:** + +* Improved performance of computations related to angles (asking a triangle for its angles is immediate now, ``%timeit similarity_surfaces.billiard(polygons.triangle(2, 13, 26)).minimal_cover("translation")`` takes 200ms instead of 15s now.) diff --git a/environment.yml b/environment.yml index d9b9adc71..4694957a0 100644 --- a/environment.yml +++ b/environment.yml @@ -11,6 +11,7 @@ dependencies: - codespell >=2.2.2,<3 - gap-defaults - ipywidgets + - jupytext - matplotlib-base - pip - pylint >=2.16,<3 @@ -24,7 +25,7 @@ dependencies: - surface-dynamics>=0.4.7,<0.5 - pycodestyle >=2.9.1,<3 - pyeantic>=1,<2 # optional: eantic - - pyexactreal>=2,<3 # optional: exactreal + - pyexactreal>=3.1.0,<4 # optional: exactreal - pyflatsurf>=3.10.1,<4 # optional: pyflatsurf - pyintervalxt>=3,<4 # optional: pyflatsurf - pip: [flipper] # optional: flipper diff --git a/flatsurf.yml b/flatsurf.yml index 78641595d..934433dd1 100644 --- a/flatsurf.yml +++ b/flatsurf.yml @@ -13,12 +13,12 @@ dependencies: - matplotlib-base - pip - pyeantic>=1.2.1,<2 - - pyexactreal>=2.3.0,<3 - - pyflatsurf>=3.11.1,<4 + - pyexactreal>=3.1.0,<4 + - pyflatsurf>=3.13.3,<4 - python=3.9 - ruamel.yaml - sage-flatsurf=0.4.6 - - sagelib=9.5 + - sagelib=10.0 - scipy - surface-dynamics>=0.4.7,<0.5 - pip: diff --git a/flatsurf/__init__.py b/flatsurf/__init__.py index dd5b60ce4..749d4f12d 100644 --- a/flatsurf/__init__.py +++ b/flatsurf/__init__.py @@ -1,28 +1,38 @@ r""" -sage-flatsurf: Sagemath module for similitude surfaces +Flat Surfaces in SageMath """ -from .version import version as __version__ +from flatsurf.version import version as __version__ -from .geometry.polygon import polygons, EquiangularPolygons, Polygons, ConvexPolygons +from flatsurf.geometry.polygon import ( + Polygon, + polygons, + EquiangularPolygons, + EuclideanPolygonsWithAngles, + EuclideanPolygons as Polygons, + ConvexPolygons, +) -from .geometry.similarity_surface_generators import ( +from flatsurf.geometry.similarity_surface_generators import ( similarity_surfaces, dilation_surfaces, half_translation_surfaces, translation_surfaces, ) -from .geometry.surface import Surface_list, Surface_dict +from flatsurf.geometry.surface import MutableOrientedSimilaritySurface -# The various surface types -from .geometry.similarity_surface import SimilaritySurface -from .geometry.half_dilation_surface import HalfDilationSurface -from .geometry.dilation_surface import DilationSurface -from .geometry.cone_surface import ConeSurface -from .geometry.rational_cone_surface import RationalConeSurface -from .geometry.half_translation_surface import HalfTranslationSurface -from .geometry.translation_surface import TranslationSurface +from flatsurf.geometry.gl2r_orbit_closure import GL2ROrbitClosure -from .geometry.gl2r_orbit_closure import GL2ROrbitClosure +from flatsurf.geometry.hyperbolic import HyperbolicPlane -from .geometry.hyperbolic import HyperbolicPlane +from flatsurf.geometry.surface_legacy import ( + Surface_list, + Surface_dict, + SimilaritySurface, + HalfDilationSurface, + DilationSurface, + ConeSurface, + RationalConeSurface, + HalfTranslationSurface, + TranslationSurface, +) diff --git a/flatsurf/geometry/__init__.py b/flatsurf/geometry/__init__.py index aeeb87e34..b4804ebf9 100644 --- a/flatsurf/geometry/__init__.py +++ b/flatsurf/geometry/__init__.py @@ -3,11 +3,21 @@ translation surfaces and more generally to similarity surfaces. """ # **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper +# This file is part of sage-flatsurf. # -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# +# 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 . # **************************************************************************** diff --git a/flatsurf/geometry/categories/__init__.py b/flatsurf/geometry/categories/__init__.py new file mode 100644 index 000000000..a2a22021d --- /dev/null +++ b/flatsurf/geometry/categories/__init__.py @@ -0,0 +1,188 @@ +r""" +Categories of Surfaces and Polygons. + +sage-flatsurf uses SageMath categories to distinguish different kinds of +surfaces such as hyperbolic surfaces, translation surfaces, …. See +https://doc.sagemath.org/html/en/reference/categories/sage/categories/primer.html +for a detailed introduction of categories in SageMath. In short, "categories" are +not so much mathematical categories but more similar to a normal class +hierarchy in Python; however, they extend the idea of a classical class +hierarchy by allowing us to dynamically change the category (and methods) of a +surface as we learn more about it. + +Note that you normally don't have to create categories explicitly. Categories +are deduced automatically (at least for surfaces of finite type.) You should +think of categories as an implementation detail. As a user of sage-flatsurf, +you don't need to know about them. As a developer of sage-flatsurf, they +provide entry points to place your code; e.g., to add a method to all +translation surfaces, actually add a method to +:class:`translation_surfaces.TranslationSurfaces.ParentMethods`. + +A similar but smaller hierarchy of categories exists for polygons, Euclidean +polygons, Hyperbolic polygons. + +.. NOTE:: + + Categories are deduced by calling methods such as + :meth:`~topological_surfaces.TopologicalSurfaces.ParentMethods.is_orientable`, + :meth:`~topological_surfaces.TopologicalSurfaces.ParentMethods.is_with_boundary`, + :meth:`~topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`, + :meth:`~topological_surfaces.TopologicalSurfaces.ParentMethods.is_connected`, + :meth:`~polygonal_surfaces.PolygonalSurfaces.ParentMethods.is_finite_type`, + :meth:`~similarity_surfaces.SimilaritySurfaces.ParentMethods.is_cone_surface`, + :meth:`~similarity_surfaces.SimilaritySurfaces.ParentMethods.is_dilation_surface`, + :meth:`~similarity_surfaces.SimilaritySurfaces.ParentMethods.is_translation_surface`, + and + :meth:`~similarity_surfaces.SimilaritySurfaces.ParentMethods.is_rational_surface`. + There are default implementations for these for finite + type surfaces. Once a surfaces has been found to be in a certain + subcategory, these methods are replaced to simply return ``True`` instead + of computing anything. If a class explicitly overrides these methods, then + the category machinery cannot replace that method anymore when the category + of the surface gets refined. Consequently, it can be beneficial for the + override to shortcut the question by querying the category, e.g., + ``is_rational`` could start with ``if "Rational" in + self.category().axioms(): return True`` before actually performing any + computation. + +EXAMPLES: + +A single square without any gluings:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square(), label=0) + 0 + +This is considered to be a surface built from polygons with all gluings being +similarities (however there are none):: + + sage: S.category() + Category of finite type oriented similarity surfaces + +It does not really make sense to ask which stratum this surface belongs to:: + + sage: S.stratum() + Traceback (most recent call last): + ... + AttributeError: ... has no attribute 'stratum' + +Once we add gluings, this turns into a square torus:: + + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + +We signal to sage-flatsurf that we are done building this surface, and its +category gets refined:: + + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + +Since this is now a translation surface, we can ask for its stratum again:: + + sage: S.stratum() + H_1(0) + +There are a number of workarounds if you want to compute things such as +``.stratum()`` on a mutable surface. For the sake of this demonstration, lets +make our surface mutable again:: + + sage: S = MutableOrientedSimilaritySurface.from_surface(S) + +The recommended approach is to create a copy of the surface, make it immutable +and then call the methods you need:: + + sage: T = MutableOrientedSimilaritySurface.from_surface(S) + sage: T.set_immutable() + sage: T.stratum() + H_1(0) + +You might be worried about the performance implications but most of the time +that might be a `premature optimization +`_. + +Often enough, the copy is actually not the problem but the time that is spent +to figure out that that copy is actually a translation surface. If you already +know that to be true, you can simplify things to:: + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: T = MutableOrientedSimilaritySurface.from_surface(S, category=TranslationSurfaces().WithoutBoundary()) + sage: T.stratum() + H_1(0) + +You can also change the category of a mutable surface to provide all the +functionality that is available to surfaces in that category:: + + sage: S._refine_category_(TranslationSurfaces().WithoutBoundary()) + sage: S.stratum() + H_1(0) + +Note however, that the category of a surface cannot be generalized anymore. +This surface is now at least a "translation surface", no matter what mutations +you make to it. (And the system will not check that it is indeed a translation +surface.) That approach might still be beneficial if you make lots of minor +changes to the surface, e.g., lots of edge flips, and want to query such +methods frequently. + +Finally, we can try to call the +:meth:`~flatsurf.geometry.categories.translation_surfaces.TranslationSurfaces.FiniteType.WithoutBoundary.ParentMethods.stratum` +method directly but it might have dependencies on other methods that are not +available:: + + sage: S = MutableOrientedSimilaritySurface.from_surface(S) + + sage: TranslationSurfaces.FiniteType.WithoutBoundary.ParentMethods.stratum(S) + Traceback (most recent call last): + ... + AttributeError: ... no attribute 'angles' + +So this approach is quite brittle and might need a mix with the above to work:: + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: S._refine_category_(ConeSurfaces().WithoutBoundary()) + sage: TranslationSurfaces.FiniteType.WithoutBoundary.ParentMethods.stratum(S) + H_1(0) + +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2021-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +from flatsurf.geometry.categories.topological_surfaces import TopologicalSurfaces +from flatsurf.geometry.categories.polygonal_surfaces import PolygonalSurfaces +from flatsurf.geometry.categories.euclidean_polygonal_surfaces import ( + EuclideanPolygonalSurfaces, +) +from flatsurf.geometry.categories.similarity_surfaces import SimilaritySurfaces +from flatsurf.geometry.categories.cone_surfaces import ConeSurfaces +from flatsurf.geometry.categories.dilation_surfaces import DilationSurfaces +from flatsurf.geometry.categories.half_translation_surfaces import ( + HalfTranslationSurfaces, +) +from flatsurf.geometry.categories.translation_surfaces import TranslationSurfaces + +from flatsurf.geometry.categories.polygons import Polygons +from flatsurf.geometry.categories.euclidean_polygons import EuclideanPolygons +from flatsurf.geometry.categories.hyperbolic_polygons import HyperbolicPolygons +from flatsurf.geometry.categories.euclidean_polygons import EuclideanPolygons +from flatsurf.geometry.categories.euclidean_polygons_with_angles import ( + EuclideanPolygonsWithAngles, +) diff --git a/flatsurf/geometry/categories/cone_surfaces.py b/flatsurf/geometry/categories/cone_surfaces.py new file mode 100644 index 000000000..3bf1ec540 --- /dev/null +++ b/flatsurf/geometry/categories/cone_surfaces.py @@ -0,0 +1,445 @@ +r""" +The category of cone surfaces. + +A cone surface is a surface that can be built by gluing Euclidean polygons +along their edges such that the matrix describing `monodromy +`_ along a closed path +is an isometry; that matrix is given by multiplying the individual matrices +that describe how to transition between pairs of glued edges, see +:meth:`~.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.edge_matrix`. + +In sage-flatsurf, we restrict cone surfaces slightly by requiring that a cone +surface is given by polygons such that each edge matrix is an isometry. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES: + +We glue the sides of a square with a rotation of π/2. Since each gluing is just +a rotation, this is a cone surface:: + + sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(P, label=0) + 0 + sage: S.glue((0, 0), (0, 1)) + sage: S.glue((0, 2), (0, 3)) + sage: S.set_immutable() + + sage: C = S.category() + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: C.is_subcategory(ConeSurfaces()) + True + +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +from flatsurf.geometry.categories.surface_category import ( + SurfaceCategory, + SurfaceCategoryWithAxiom, +) + + +class ConeSurfaces(SurfaceCategory): + r""" + The category of surfaces built by gluing (Euclidean) polygons with + isometries on the edges. + + See :mod:`~flatsurf.geometry.categories.cone_surfaces` and + :meth:`~ParentMethods.is_cone_surface` on how this differs slightly from + the customary definition of a cone surface. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: ConeSurfaces() + Category of cone surfaces + + """ + + def super_categories(self): + r""" + Return the categories that a cone surfaces is also always a member of. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: ConeSurfaces().super_categories() + [Category of similarity surfaces] + + """ + from flatsurf.geometry.categories.similarity_surfaces import SimilaritySurfaces + + return [SimilaritySurfaces()] + + class ParentMethods: + r""" + Provides methods available to all cone surfaces. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def is_cone_surface(self): + r""" + Return whether this surface is a cone surface, i.e., whether its + edges are glued by isometries. + + .. NOTE:: + + This is a stronger requirement than the usual definition of a + cone surface, see + :mod:`~flatsurf.geometry.categories.cone_surfaces` for details. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.is_cone_surface() + True + + """ + return True + + @staticmethod + def _is_cone_surface(surface, limit=None): + r""" + Return whether ``surface`` is a cone surface by checking how its + polygons are glued. + + This is a helper method for :meth:`is_cone_surface` and + :meth:`_test_cone_surface`. + + INPUT: + + - ``surface`` -- an oriented similarity surface + + - ``limit`` -- an integer or ``None`` (default: ``None``); if set, only + the first ``limit`` polygons are checked + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: ConeSurfaces.ParentMethods._is_cone_surface(S, limit=8) + True + + :: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: ConeSurfaces.ParentMethods._is_cone_surface(S) + True + + """ + if "Oriented" not in surface.category().axioms(): + raise NotImplementedError( + "cannot check whether a non-oriented surface is a cone surface yet" + ) + + labels = surface.labels() + + if limit is not None: + from itertools import islice + + labels = islice(labels, limit) + + checked = set() + + for label in labels: + for edge in range(len(surface.polygon(label).vertices())): + cross = surface.opposite_edge(label, edge) + + if cross is None: + continue + + if cross in checked: + continue + + checked.add((label, edge)) + + # We do not call self.edge_matrix() since the surface might + # have overridden this (just returning the identity matrix e.g.) + # and we want to deduce the matrix from the attached polygon + # edges instead. + from flatsurf.geometry.categories import SimilaritySurfaces + + matrix = SimilaritySurfaces.Oriented.ParentMethods.edge_matrix.f( # pylint: disable=no-member + surface, label, edge + ) + + if matrix * matrix.transpose() != 1: + return False + + return True + + class FiniteType(SurfaceCategoryWithAxiom): + r""" + The category of cone surfaces built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: S in ConeSurfaces().FiniteType() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all cone surfaces built from finitely + many polygons. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def area(self): + r""" + Return the area of this surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.area() + 3 + + """ + return sum(p.area() for p in self.polygons()) + + class Oriented(SurfaceCategoryWithAxiom): + r""" + The category of oriented cone surfaces, i.e., orientable cone surfaces + whose orientation can be chosen to be compatible with the embedding of + its polygons in the real plane. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: S in ConeSurfaces().Oriented() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented cone surfaces. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def _test_cone_surface(self, **options): + r""" + Verify that this is a cone surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.set_immutable() + sage: S._test_cone_surface() + + """ + tester = self._tester(**options) + + limit = None + + if not self.is_finite_type(): + limit = 32 + + tester.assertTrue( + ConeSurfaces.ParentMethods._is_cone_surface(self, limit=limit) + ) + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The category of oriented cone surfaces without boundary. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: S in ConeSurfaces().Oriented().WithoutBoundary() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented cone surfaces + without boundary. + + If you want to add functionality for such surfaces you most + likely want to put it here. + """ + + def angles(self, numerical=False, return_adjacent_edges=False): + r""" + Return the set of angles around the vertices of the surface. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: T = polygons.triangle(3, 4, 5) + sage: S = similarity_surfaces.billiard(T) + sage: S.angles() + [1/3, 1/4, 5/12] + sage: S.angles(numerical=True) # abs tol 1e-14 + [0.333333333333333, 0.250000000000000, 0.416666666666667] + + sage: S.angles(return_adjacent_edges=True) + [(1/3, [(0, 1), (1, 2)]), (1/4, [(0, 0), (1, 0)]), (5/12, [(1, 1), (0, 2)])] + + """ + if not numerical and any( + not p.is_rational() for p in self.polygons() + ): + raise NotImplementedError( + "cannot compute exact angles in this surface built from non-rational polygons yet" + ) + + edges = [pair for pair in self.edges()] + edges = set(edges) + angles = [] + + if return_adjacent_edges: + while edges: + p, e = edges.pop() + adjacent_edges = [(p, e)] + angle = self.polygon(p).angle(e, numerical=numerical) + pp, ee = self.opposite_edge( + p, (e - 1) % len(self.polygon(p).vertices()) + ) + while pp != p or ee != e: + edges.remove((pp, ee)) + adjacent_edges.append((pp, ee)) + angle += self.polygon(pp).angle( + ee, numerical=numerical + ) + pp, ee = self.opposite_edge( + pp, (ee - 1) % len(self.polygon(pp).vertices()) + ) + angles.append((angle, adjacent_edges)) + else: + while edges: + p, e = edges.pop() + angle = self.polygon(p).angle(e, numerical=numerical) + pp, ee = self.opposite_edge( + p, (e - 1) % len(self.polygon(p).vertices()) + ) + while pp != p or ee != e: + edges.remove((pp, ee)) + angle += self.polygon(pp).angle( + ee, numerical=numerical + ) + pp, ee = self.opposite_edge( + pp, (ee - 1) % len(self.polygon(pp).vertices()) + ) + angles.append(angle) + + return angles + + class Connected(SurfaceCategoryWithAxiom): + r""" + The category of oriented connected cone surfaces without boundary. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: S in ConeSurfaces().Oriented().Connected() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented connected + cone surfaces without boundary. + + If you want to add functionality for such surfaces you + most likely want to put it here. + """ + + def _test_genus(self, **options): + r""" + Verify that the genus is compatible with the angles of the + singularities. + + ALGORITHM: + + We use the angles around the vertices of the surface to compute the + genus, see e.g. [Massart2021] p.17 and compare this + to the genus computed directly from the polygon + gluings. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: translation_surfaces.octagon_and_squares()._test_genus() + + .. [Massart2021] \D. Massart. A short introduction to translation + surfaces, Veech surfaces, and Teichműller dynamics. + https://hal.science/hal-03300179/document + + """ + tester = self._tester(**options) + + for edge in self.edges(): + if self.opposite_edge(*edge) == edge: + # The genus formula below is wrong when + # there is a non-materialized vertex on an + # edge. + return + + tester.assertAlmostEqual( + self.genus(), + sum(a - 1 for a in self.angles(numerical=True)) / 2.0 + + 1, + ) diff --git a/flatsurf/geometry/categories/dilation_surfaces.py b/flatsurf/geometry/categories/dilation_surfaces.py new file mode 100644 index 000000000..cc0dbbf9f --- /dev/null +++ b/flatsurf/geometry/categories/dilation_surfaces.py @@ -0,0 +1,599 @@ +r""" +The category of dilation surfaces. + +This module provides shared functionality for all surfaces in sage-flatsurf +that are built from Euclidean polygons that are glued by translation followed +by homothety, i.e., application of a diagonal matrix. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: C = S.category() + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: C.is_subcategory(DilationSurfaces()) + True + +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +from flatsurf.geometry.categories.surface_category import ( + SurfaceCategory, + SurfaceCategoryWithAxiom, +) +from sage.categories.category_with_axiom import all_axioms + + +class DilationSurfaces(SurfaceCategory): + r""" + The category of surfaces built from polygons with edges identified by + translations and homothety. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: DilationSurfaces() + Category of dilation surfaces + + """ + + def super_categories(self): + r""" + Return the categories that a dilation surfaces is also always a member + of. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: DilationSurfaces().super_categories() + [Category of rational similarity surfaces] + + """ + from flatsurf.geometry.categories.similarity_surfaces import SimilaritySurfaces + + return [SimilaritySurfaces().Rational()] + + class Positive(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by dilation surfaces that use homothety with + positive scaling. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: 'Positive' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all positive dilation surfaces. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def is_dilation_surface(self, positive=False): + r""" + Return whether this surface is a dilation surface. + + See + :meth:`.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_dilation_surface` + for details. + """ + return True + + def _test_positive_dilation_surface(self, **options): + r""" + Verify that this is a positive dilation surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.set_immutable() + sage: S._test_positive_dilation_surface() + + """ + tester = self._tester(**options) + + limit = None + + if not self.is_finite_type(): + limit = 32 + + tester.assertTrue( + DilationSurfaces.ParentMethods._is_dilation_surface( + self, positive=True, limit=limit + ) + ) + + class SubcategoryMethods: + def Positive(self): + r""" + Return the subcategory of surfaces glued by positive dilation. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: C = DilationSurfaces() + sage: C.Positive() + Category of positive dilation surfaces + + """ + return self._with_axiom("Positive") + + class ParentMethods: + r""" + Provides methods available to all dilation surfaces. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def is_dilation_surface(self, positive=False): + r""" + Return whether this surface is a dilation surface. + + See :meth:`.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_dilation_surface` + for details. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: S.is_dilation_surface(positive=True) + True + sage: S.is_dilation_surface(positive=False) + True + + """ + if not positive: + return True + + # We do not know whether this surface is a positive dilation + # surface or not so we have to rely on the generic implementation + # of this. + # pylint: disable-next=bad-super-call + return super(DilationSurfaces().parent_class, self).is_dilation_surface( + positive=positive + ) + + @staticmethod + def _is_dilation_surface(surface, positive=False, limit=None): + r""" + Return whether ``surface`` is a dilation surface by checking how + its polygons are glued. + + This is a helper method for + :meth:`~.similarity_surfaces.ParentMethods.is_dilation_surface`. + + INPUT: + + - ``surface`` -- an oriented similarity surface + + - ``positive`` -- a boolean (default: ``False``); whether the + entries of the diagonal matrix must be positive or are allowed to + be negative. + + - ``limit`` -- an integer or ``None`` (default: ``None``); if set, only + the first ``limit`` polygons are checked + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: DilationSurfaces.ParentMethods._is_dilation_surface(S, limit=8) + True + + :: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: DilationSurfaces.ParentMethods._is_dilation_surface(S, positive=True) + False + sage: DilationSurfaces.ParentMethods._is_dilation_surface(S) + True + + """ + if "Oriented" not in surface.category().axioms(): + raise NotImplementedError( + "cannot decide whether a non-oriented surface is dilation surface yet" + ) + + labels = surface.labels() + + if limit is not None: + from itertools import islice + + labels = islice(labels, limit) + + checked = set() + + for label in labels: + for edge in range(len(surface.polygon(label).vertices())): + cross = surface.opposite_edge(label, edge) + + if cross is None: + continue + + if cross in checked: + continue + + checked.add((label, edge)) + + # We do not call self.edge_matrix() since the surface might + # have overridden this (just returning the identity matrix e.g.) + # and we want to deduce the matrix from the attached polygon + # edges instead. + from flatsurf.geometry.categories import SimilaritySurfaces + + matrix = SimilaritySurfaces.Oriented.ParentMethods.edge_matrix.f( # pylint: disable=no-member + surface, label, edge + ) + + if not matrix.is_diagonal(): + return False + + if positive: + if matrix[0][0] < 0 or matrix[1][1] < 0: + return False + + return True + + def apply_matrix(self, m, in_place=True, mapping=False): + r""" + Carry out the GL(2,R) action of m on this surface and return the result. + + If in_place=True, then this is done in place and changes the surface. + This can only be carried out if the surface is finite and mutable. + + If mapping=True, then we return a GL2RMapping between this surface and its image. + In this case in_place must be False. + + If in_place=False, then a copy is made before the deformation. + + TESTS:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: T = S.apply_matrix(matrix([[1, 0], [0, 1]]), in_place=False) + + sage: T = S.apply_matrix(matrix([[1, 0], [0, 1]]), in_place=False, mapping=True) + + """ + if mapping is True: + if in_place: + raise NotImplementedError( + "can not modify in place and return a mapping" + ) + from flatsurf.geometry.half_dilation_surface import GL2RMapping + + return GL2RMapping(self, m) + if not in_place: + if self.is_finite_type(): + from sage.structure.element import get_coercion_model + + cm = get_coercion_model() + field = cm.common_parent(self.base_ring(), m.base_ring()) + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface( + self.change_ring(field), + category=DilationSurfaces(), + ) + s.apply_matrix(m, in_place=True) + s.set_immutable() + return s + else: + return m * self + else: + # Make sure m is in the right state + from sage.matrix.constructor import Matrix + + m = Matrix(self.base_ring(), 2, 2, m) + if m.det() == self.base_ring().zero(): + raise ValueError("can not deform by degenerate matrix") + if not self.is_finite_type(): + raise NotImplementedError( + "in-place GL(2,R) action only works for finite surfaces" + ) + us = self + if not us.is_mutable(): + raise ValueError("in-place changes only work for mutable surfaces") + for label in self.labels(): + us.replace_polygon(label, m * self.polygon(label)) + if m.det() < self.base_ring().zero(): + # Polygons were all reversed orientation. Need to redo gluings. + + # First pass record new gluings in a dictionary. + new_glue = {} + seen_labels = set() + for p1 in self.labels(): + n1 = len(self.polygon(p1).vertices()) + for e1 in range(n1): + p2, e2 = self.opposite_edge(p1, e1) + n2 = len(self.polygon(p2).vertices()) + if p2 in seen_labels: + pass + elif p1 == p2 and e1 > e2: + pass + else: + new_glue[(p1, n1 - 1 - e1)] = (p2, n2 - 1 - e2) + seen_labels.add(p1) + # Second pass: reassign gluings + for (p1, e1), (p2, e2) in new_glue.items(): + us.glue((p1, e1), (p2, e2)) + return self + + def _delaunay_edge_needs_flip_Linfinity(self, p1, e1, p2, e2): + r""" + Check whether the provided edge which bounds two triangles should be flipped + to get closer to the L-infinity Delaunay decomposition. + + TESTS:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon + sage: s = MutableOrientedSimilaritySurface(QQ) + sage: s.add_polygon(Polygon(vertices=[(0,0), (1,0), (0,1)])) + 0 + sage: s.add_polygon(Polygon(vertices=[(1,1), (0,1), (1,0)])) + 1 + sage: s.glue((0, 0), (1, 0)) + sage: s.glue((0, 1), (1, 1)) + sage: s.glue((0, 2), (1, 2)) + sage: s.set_immutable() + sage: [s._delaunay_edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] + [False, False, False] + + sage: ss = matrix(2, [1,1,0,1]) * s + sage: [ss._delaunay_edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] + [False, False, False] + sage: ss = matrix(2, [1,0,1,1]) * s + sage: [ss._delaunay_edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] + [False, False, False] + + sage: ss = matrix(2, [1,2,0,1]) * s + sage: [ss._delaunay_edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] + [False, False, True] + + sage: ss = matrix(2, [1,0,2,1]) * s + sage: [ss._delaunay_edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] + [True, False, False] + """ + assert self.opposite_edge(p1, e1) == (p2, e2), "not opposite edges" + + # triangles + poly1 = self.polygon(p1) + poly2 = self.polygon(p2) + if len(poly1.vertices()) != 3 or len(poly2.vertices()) != 3: + raise ValueError("edge must be adjacent to two triangles") + + edge1 = poly1.edge(e1) + edge1L = poly1.edge(e1 - 1) + edge1R = poly1.edge(e1 + 1) + edge2 = poly2.edge(e2) + edge2L = poly2.edge(e2 - 1) + edge2R = poly2.edge(e2 + 1) + + sim = self.edge_transformation(p2, e2) + m = sim.derivative() # matrix carrying p2 to p1 + if not m.is_one(): + edge2 = m * edge2 + edge2L = m * edge2L + edge2R = m * edge2R + + # convexity check of the quadrilateral + from flatsurf.geometry.euclidean import ccw + + if ccw(edge2L, edge1R) <= 0 or ccw(edge1L, edge2R) <= 0: + return False + + # compare the norms + new_edge = edge2L + edge1R + n1 = max(abs(edge1[0]), abs(edge1[1])) + n = max(abs(new_edge[0]), abs(new_edge[1])) + return n < n1 + + def _test_dilation_surface(self, **options): + r""" + Verify that this is a dilation surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_dilation_surface() + + """ + tester = self._tester(**options) + + limit = None + + if not self.is_finite_type(): + limit = 32 + + tester.assertTrue( + DilationSurfaces.ParentMethods._is_dilation_surface( + self, positive=False, limit=limit + ) + ) + + class FiniteType(SurfaceCategoryWithAxiom): + r""" + The category of dilation surfaces built from a finite number of polygons. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: S in DilationSurfaces().FiniteType() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all dilation surfaces built from + finitely many polygons. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def l_infinity_delaunay_triangulation( + self, triangulated=None, in_place=None, limit=None, direction=None + ): + r""" + Return an L-infinity Delaunay triangulation of a surface, or make + some triangle flips to get closer to the Delaunay decomposition. + + INPUT: + + - ``triangulated`` -- deprecated and ignored. + + - ``in_place`` -- deprecated and must be ``None`` (the default); + otherwise an error is produced + + - ``limit`` -- optional (positive integer) If provided, then at most ``limit`` + many diagonal flips will be done. + + - ``direction`` -- optional (vector). Used to determine labels when a + pair of triangles is flipped. Each triangle has a unique separatrix + which points in the provided direction or its negation. As such a + vector determines a sign for each triangle. A pair of adjacent + triangles have opposite signs. Labels are chosen so that this sign is + preserved (as a function of labels). + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s0 = translation_surfaces.veech_double_n_gon(5) + sage: field = s0.base_ring() + sage: a = field.gen() + sage: m = matrix(field, 2, [2,a,1,1]) + + sage: s = m*s0 + sage: s = s.l_infinity_delaunay_triangulation() + sage: TestSuite(s).run() + + sage: s = (m**2)*s0 + sage: s = s.l_infinity_delaunay_triangulation() # long time (.5s) + sage: TestSuite(s).run() # long time (see above) + + sage: s = (m**3)*s0 + sage: s = s.l_infinity_delaunay_triangulation() # long time (.8s) + sage: TestSuite(s).run() # long time (see above) + + TESTS: + + Verify that deprecated keywords do not cause errors:: + + sage: s.l_infinity_delaunay_triangulation(triangulated=True) # long time (.8s) + doctest:warning + ... + UserWarning: The triangulated keyword of l_infinity_delaunay_triangulation() has been deprecated and will be removed from a future version of sage-flatsurf. The keyword has no effect anymore. + Translation Surface in H_2(2) built from 6 triangles + sage: s.l_infinity_delaunay_triangulation(triangulated=False) # long time (.9s) + doctest:warning + ... + UserWarning: The triangulated keyword of l_infinity_delaunay_triangulation() has been deprecated and will be removed from a future version of sage-flatsurf. The keyword has no effect anymore. + Translation Surface in H_2(2) built from 6 triangles + + :: + + sage: s.l_infinity_delaunay_triangulation(in_place=True) + Traceback (most recent call last): + ... + NotImplementedError: The in_place keyword for l_infinity_delaunay_triangulation() is not supported anymore. It did not work correctly in previous versions of sage-flatsurf and will be fully removed in a future version of sage-flatsurf. + + """ + if triangulated is not None: + import warnings + + warnings.warn( + "The triangulated keyword of l_infinity_delaunay_triangulation() has been deprecated and will be removed from a future version of sage-flatsurf. The keyword has no effect anymore." + ) + if in_place is not None: + raise NotImplementedError( + "The in_place keyword for l_infinity_delaunay_triangulation() is not supported anymore. It did not work correctly in previous versions of sage-flatsurf and will be fully removed in a future version of sage-flatsurf." + ) + + self = self.triangulate() + + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + self = MutableOrientedSimilaritySurface.from_surface( + self, category=DilationSurfaces() + ) + + if direction is None: + base_ring = self.base_ring() + direction = (base_ring**2)((base_ring.zero(), base_ring.one())) + + if direction.is_zero(): + raise ValueError("direction must be non-zero") + + triangles = set(self.labels()) + if limit is None: + limit = -1 + else: + limit = int(limit) + while triangles and limit: + p1 = triangles.pop() + for e1 in range(3): + p2, e2 = self.opposite_edge(p1, e1) + if self._delaunay_edge_needs_flip_Linfinity(p1, e1, p2, e2): + self.triangle_flip( + p1, e1, in_place=True, direction=direction + ) + triangles.add(p1) + triangles.add(p2) + limit -= 1 + self.set_immutable() + return self + + +# Currently, there is no "Positive" axiom in SageMath so we make it known to +# the category framework. +all_axioms += ("Positive",) diff --git a/flatsurf/geometry/categories/euclidean_polygonal_surfaces.py b/flatsurf/geometry/categories/euclidean_polygonal_surfaces.py new file mode 100644 index 000000000..07ea7da50 --- /dev/null +++ b/flatsurf/geometry/categories/euclidean_polygonal_surfaces.py @@ -0,0 +1,245 @@ +r""" +The category of surfaces built by gluing Euclidean polygons. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: C = MutableOrientedSimilaritySurface(QQ).category() + + sage: from flatsurf.geometry.categories import EuclideanPolygonalSurfaces + sage: C.is_subcategory(EuclideanPolygonalSurfaces()) + True + +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 Vincent Delecroix +# 2020-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +from flatsurf.geometry.categories.surface_category import SurfaceCategory + + +class EuclideanPolygonalSurfaces(SurfaceCategory): + r""" + The category of surfaces built by gluing Euclidean polygons (or more + generally, polygons in two-dimensional real space.) + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygonalSurfaces + sage: EuclideanPolygonalSurfaces() + Category of euclidean polygonal surfaces + + """ + + def super_categories(self): + r""" + The categories such surfaces are also automatically contained in, + namely the category of surfaces built from polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygonalSurfaces + sage: C = EuclideanPolygonalSurfaces() + sage: C.super_categories() + [Category of polygonal surfaces] + + """ + from flatsurf.geometry.categories.polygonal_surfaces import PolygonalSurfaces + + return [PolygonalSurfaces()] + + class ParentMethods: + r""" + Provides methods available to all surfaces that are built from polygons + in the real plane. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def graphical_surface(self, *args, **kwargs): + r""" + Return a graphical representation of this surface. + + This method can be used to further configure or augment a plot + beyond the possibilities of :meth:`plot`. + + The documentation of sage-flatsurf contains a section of example + plots or consult the :mod:`flatsurf.graphical.surface` reference for all the + details. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.graphical_surface() + Graphical representation of Translation Surface in H_1(0) built from a square + + """ + if "cached" in kwargs: + import warnings + + warnings.warn( + "The cached keyword has been removed from graphical_surface(). The keyword is ignored in this version of sage-flatsurf and will be dropped completely in a future version of sage-flatsurf. " + "The result of graphical_surface() is never cached anymore." + ) + + kwargs.pop("cached") + + from flatsurf.graphical.surface import GraphicalSurface + + return GraphicalSurface(self, *args, **kwargs) + + def plot(self, **kwargs): + r""" + Return a plot of this surface. + + The documentation of sage-flatsurf contains a section of example + plots or consult the :mod:`flatsurf.graphical.surface` reference + for all the details. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.plot() + Graphics object consisting of 10 graphics primitives + + + """ + graphical_surface_keywords = { + key: kwargs.pop(key) + for key in [ + "cached", + "adjacencies", + "polygon_labels", + "edge_labels", + "default_position_function", + ] + if key in kwargs + } + return self.graphical_surface(**graphical_surface_keywords).plot(**kwargs) + + def plot_polygon( + self, + label, + graphical_surface=None, + plot_polygon=True, + plot_edges=True, + plot_edge_labels=True, + edge_labels=None, + polygon_options={"axes": True}, + edge_options=None, + edge_label_options=None, + ): + r""" + Returns a plot of the polygon with the provided label. + + Note that this method plots the polygon in its coordinates as opposed to + graphical coordinates that the :func:``plot`` method uses. This makes it useful + for visualizing the natural coordinates of the polygon. + + INPUT: + + - ``graphical_surface`` -- (default ``None``) If provided this function pulls graphical options + from the graphical surface. If not provided, we use the default graphical surface. + + - ``plot_polygon`` -- (default ``True``) If True, we plot the solid polygon. + + - ``polygon_options`` -- (default ``{"axes":True}``) Options for the rendering of the polygon. + These options will be passed to :func:`~flatsurf.graphical.polygon.GraphicalPolygon.plot_polygon`. + This should be either None or a dictionary. + + - ``plot_edges`` -- (default ``True``) If True, we plot the edges of the polygon as segments. + + - ``edge_options`` -- (default ``None``) Options for the rendering of the polygon edges. + These options will be passed to :func:`~flatsurf.graphical.polygon.GraphicalPolygon.plot_edge`. + This should be either None or a dictionary. + + - ``plot_edge_labels`` -- (default ``True``) If True, we plot labels on the edges. + + - ``edge_label_options`` -- (default ``None``) Options for the rendering of the edge labels. + These options will be passed to :func:`~flatsurf.graphical.polygon.GraphicalPolygon.plot_edge_label`. + This should be either None or a dictionary. + + - ``edge_labels`` -- (default ``None``) If None and plot_edge_labels is True, we write the edge + number on each edge. Otherwise edge_labels should be a list of strings of length equal to the + number of edges of the polygon. The strings will be printed on each edge. + + EXAMPLES:: + + sage: from flatsurf import similarity_surfaces + sage: s = similarity_surfaces.example() + sage: s.plot() + ...Graphics object consisting of 13 graphics primitives + sage: s.plot_polygon(1) + ...Graphics object consisting of 7 graphics primitives + + sage: labels = [] + sage: p = s.polygon(1) + sage: for e in range(len(p.vertices())): \ + labels.append(str(p.edge(e))) + sage: s.plot_polygon(1, polygon_options=None, plot_edges=False, \ + edge_labels=labels, edge_label_options={"color":"red"}) + ...Graphics object consisting of 4 graphics primitives + """ + if graphical_surface is None: + graphical_surface = self.graphical_surface() + p = self.polygon(label) + + from flatsurf.graphical.polygon import GraphicalPolygon + + gp = GraphicalPolygon(p) + + if plot_polygon: + if polygon_options is None: + o = graphical_surface.polygon_options + else: + o = graphical_surface.polygon_options.copy() + o.update(polygon_options) + plt = gp.plot_polygon(**o) + + if plot_edges: + if edge_options is None: + o = graphical_surface.non_adjacent_edge_options + else: + o = graphical_surface.non_adjacent_edge_options.copy() + o.update(edge_options) + for e in range(len(p.vertices())): + plt += gp.plot_edge(e, **o) + + if plot_edge_labels: + if edge_label_options is None: + o = graphical_surface.edge_label_options + else: + o = graphical_surface.edge_label_options.copy() + o.update(edge_label_options) + for e in range(len(p.vertices())): + if edge_labels is None: + el = str(e) + else: + el = edge_labels[e] + plt += gp.plot_edge_label(e, el, **o) + return plt diff --git a/flatsurf/geometry/categories/euclidean_polygons.py b/flatsurf/geometry/categories/euclidean_polygons.py new file mode 100644 index 000000000..2ed257d5e --- /dev/null +++ b/flatsurf/geometry/categories/euclidean_polygons.py @@ -0,0 +1,2191 @@ +r""" +The category of Euclidean polygons defined in the real plane. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category of a polygon is automatically determined. + +EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: C = EuclideanPolygons(QQ) + + sage: from flatsurf import polygons + sage: polygons.square() in C + True + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 Vincent Delecroix +# 2020-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** +from sage.categories.category_types import Category_over_base_ring +from sage.categories.category_with_axiom import CategoryWithAxiom_over_base_ring +from sage.misc.cachefunc import cached_method +from sage.all import FreeModule +from sage.misc.abstract_method import abstract_method +from sage.structure.element import get_coercion_model + +from flatsurf.geometry.categories.polygons import Polygons +from flatsurf.geometry.euclidean import ccw + +cm = get_coercion_model() + + +class EuclideanPolygons(Category_over_base_ring): + r""" + The category of Euclidean polygons defined in the real plane + over a fixed base ring. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons(QQ) + Category of euclidean polygons over Rational Field + + """ + + def super_categories(self): + r""" + Return the categories Euclidean polygons are also contained in, namely + the polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons(QQ).super_categories() + [Category of polygons over Rational Field] + + """ + return [Polygons(self.base_ring())] + + class ParentMethods: + r""" + Provides methods available to all Euclidean polygons in the real plane. + + If you want to add functionality to such polygons, you probably want to + put it here. + """ + + def vector_space(self): + r""" + Return the vector space of dimension 2 in which this polygon embeds. + + EXAMPLES:: + + sage: from flatsurf import Polygons + sage: C = Polygons(QQ) + sage: C.vector_space() + doctest:warning + ... + UserWarning: vector_space() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring().fraction_field()**2 instead + Vector space of dimension 2 over Rational Field + + """ + import warnings + + warnings.warn( + "vector_space() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring().fraction_field()**2 instead" + ) + + return self.base_ring().fraction_field() ** 2 + + def module(self): + r""" + Return the free module of rank 2 in which this polygon embeds. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.module() + doctest:warning + ... + UserWarning: module() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring()**2 instead + Vector space of dimension 2 over Rational Field + + """ + import warnings + + warnings.warn( + "module() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring()**2 instead" + ) + + return self.base_ring() ** 2 + + def field(self): + r""" + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.field() + doctest:warning + ... + UserWarning: field() has been deprecated and will be removed from a future version of sage-flatsurf; use base_ring() or base_ring().fraction_field() instead + Rational Field + + """ + import warnings + + warnings.warn( + "field() has been deprecated and will be removed from a future version of sage-flatsurf; use base_ring() or base_ring().fraction_field() instead" + ) + + return self.base_ring().fraction_field() + + def _mul_(self, g, switch_sides=None): + r""" + Apply the 2x2 matrix `g` to this polygon. + + The matrix must have non-zero determinant. If the determinant is + negative, then the vertices and edges are relabeled according to the + involutions `v \mapsto (n-v)%n` and `e \mapsto n-1-e` respectively. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices = [(1,0),(0,1),(-1,-1)]) + sage: p + Polygon(vertices=[(1, 0), (0, 1), (-1, -1)]) + + sage: matrix(ZZ,[[0, 1], [1, 0]]) * p + Polygon(vertices=[(0, 1), (-1, -1), (1, 0)]) + + sage: matrix(ZZ,[[2, 0], [0, 1]]) * p + Polygon(vertices=[(2, 0), (0, 1), (-2, -1)]) + + """ + from flatsurf import Polygon + + if g in self.base_ring(): + from sage.all import MatrixSpace + + g = MatrixSpace(self.base_ring(), 2)(g) + + det = g.det() + if det == 0: + raise ValueError( + "Can not act on a polygon with matrix with zero determinant" + ) + + if det < 0: + # Note that in this case we reverse the order + vertices = [g * self.vertex(0)] + for i in range(len(self.vertices()) - 1, 0, -1): + vertices.append(g * self.vertex(i)) + + return Polygon(vertices=vertices, check=False) + + return Polygon( + vertices=[g * v for v in self.vertices()], + check=False, + category=self.category(), + ) + + @cached_method + def is_rational(self): + r""" + Return whether this is a rational polygon, i.e., all its + :meth:`angles` are rational multiples of π. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices = [(1, 0), (0, 1), (-1, -1)]) + sage: p.is_rational() + False + + Note that determining rationality is somewhat costly. Once + established, this refines the category of the triangle:: + + sage: p = Polygon(vertices = [(0, 0), (1, 0), (0, 1)]) + sage: p.category() + Category of convex simple euclidean polygons over Rational Field + sage: p.is_rational() + True + sage: p.category() + Category of rational convex simple euclidean polygons over Rational Field + + """ + for e in range(len(self.vertices())): + u = self.edge(e) + v = -self.edge((e - 1) % len(self.vertices())) + + cos = u.dot_product(v) + sin = u[0] * v[1] - u[1] * v[0] + + from flatsurf.geometry.euclidean import is_cosine_sine_of_rational + + if not is_cosine_sine_of_rational(cos, sin, scaled=True): + return False + + self._refine_category_(self.category().Rational()) + + return True + + def is_simple(self): + r""" + Return whether this is a simple polygon, i.e., without + self-intersection. + + EXAMPLES: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.is_simple() + True + + """ + n = len(self.vertices()) + for i in range(n): + ei = (self.vertex(i), self.vertex(i + 1)) + for j in range(i + 2, n + 1): + if (i - j) % n in [-1, 0, 1]: + continue + + ej = (self.vertex(j), self.vertex(j + 1)) + + from flatsurf.geometry.euclidean import is_segment_intersecting + + if is_segment_intersecting(ei, ej): + return False + + return True + + @abstract_method + def vertices(self, marked_vertices=True): + r""" + Return the vertices of this polygon in counterclockwise order as + vectors in the real plane. + + INPUT: + + - ``marked_vertices`` -- a boolean (default: ``True``); whether to + include marked vertices that are not actually corners of the + polygon. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.vertices() + ((0, 0), (1, 0), (1, 1), (0, 1)) + + """ + + def vertex(self, i): + r""" + Return coordinates for the ``i``-th vertex of this polygon. + + EXAMPLES: + + The ``i`` wraps around if it is negative or exceeds the number of + vertices in this polygon:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.vertex(-1) + (0, 1) + sage: s.vertex(0) + (0, 0) + sage: s.vertex(1) + (1, 0) + sage: s.vertex(2) + (1, 1) + sage: s.vertex(3) + (0, 1) + sage: s.vertex(4) + (0, 0) + + """ + vertices = self.vertices() + return vertices[i % len(vertices)] + + def edges(self): + r""" + Return the edges of this polygon as vectors in the plane going from + one vertex to the next one. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.edges() + [(1, 0), (0, 1), (-1, 0), (0, -1)] + + """ + return [self.edge(i) for i in range(len(self.vertices()))] + + def edge(self, i): + r""" + Return the vector going from vertex ``i`` to the following vertex + in counter-clockwise order. + + EXAMPLES: + + Note that this wraps around if ``i`` is negative or exceeds the + number of vertices:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.edge(-1) + (0, -1) + sage: s.edge(0) + (1, 0) + sage: s.edge(1) + (0, 1) + sage: s.edge(2) + (-1, 0) + sage: s.edge(3) + (0, -1) + sage: s.edge(4) + (1, 0) + + """ + return self.vertex(i + 1) - self.vertex(i) + + def is_convex(self, strict=False): + r""" + Return whether this is a convex polygon. + + INPUT: + + - ``strict`` -- whether to check for strict convexity, i.e., a + polygon with a π angle is not considered convex. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.is_convex() + True + sage: S.is_convex(strict=True) + True + + """ + from flatsurf.geometry.euclidean import ccw + + for i in range(len(self.vertices())): + consecutive_ccw = ccw(self.edge(i), self.edge(i + 1)) + if strict: + if consecutive_ccw <= 0: + return False + else: + if consecutive_ccw < 0: + return False + + return True + + def _test_marked_vertices(self, **options): + r""" + Verify that :meth:`vertices` and :meth:`is_convex` are compatible. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S._test_marked_vertices() + + """ + tester = self._tester(**options) + + if self.is_convex(): + tester.assertEqual( + self.is_convex(strict=True), + self.vertices() == self.vertices(marked_vertices=False), + ) + + def is_degenerate(self): + r""" + Return whether this polygon is considered degenerate. + + This implements + :meth:`flatsurf.geometry.categories.polygons.Polygons.ParentMethods.is_degenerate`. + + EXAMPLES: + + Polygons with zero area are considered degenerate:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (1, 0)], check=False) + sage: p.is_degenerate() + True + + Polygons with marked vertices are considered degenerate:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (4, 0), (2, 2)]) + sage: p.is_degenerate() + True + + """ + if self.area() == 0: + return True + + if self.vertices() != self.vertices(marked_vertices=False): + return True + + return False + + def slopes(self, relative=False): + r""" + Return the slopes of this polygon as vectors in the plane. + + INPUT: + + - ``relative`` -- a boolean (default: ``False``); whether to return the + slopes not as absolute vectors parallel to the edges but relative to + the previous edge, i.e., after turning the previous edge to be + parallel to the x axis. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.slopes() + [(1, 0), (0, 1), (-1, 0), (0, -1)] + + sage: s.slopes(relative=True) + [(0, 1), (0, 1), (0, 1), (0, 1)] + + A polygon with a marked point:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (4, 0), (2, 2)]) + sage: p.slopes() + [(2, 0), (2, 0), (-2, 2), (-2, -2)] + sage: p.slopes(relative=True) + [(-4, 4), (4, 0), (-4, 4), (0, 8)] + + """ + if not relative: + return self.edges() + + edges = [ + (self.edge((e - 1) % len(self.vertices())), self.edge(e)) + for e in range(len(self.vertices())) + ] + + cos = [u.dot_product(v) for (u, v) in edges] + sin = [u[0] * v[1] - u[1] * v[0] for (u, v) in edges] + + from sage.all import vector + + return [vector((c, s)) for (c, s) in zip(cos, sin)] + + def erase_marked_vertices(self): + r""" + Return a copy of this polygon without marked vertices. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (4, 0), (2, 2)]) + sage: p.erase_marked_vertices() + Polygon(vertices=[(0, 0), (4, 0), (2, 2)]) + + """ + from flatsurf import Polygon + + return Polygon(vertices=self.vertices(marked_vertices=False)) + + def is_equilateral(self): + r""" + Return whether all sides of this polygon have the same length. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)]) + sage: p.is_equilateral() + True + + """ + return len(set(edge[0] ** 2 + edge[1] ** 2 for edge in self.edges())) == 1 + + def is_equiangular(self): + r""" + Return whether all sides of this polygon meet at the same angle. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)]) + sage: p.is_equiangular() + True + + """ + slopes = self.slopes(relative=True) + + from flatsurf.geometry.euclidean import is_parallel + + return all( + is_parallel(slopes[i - 1], slopes[i]) for i in range(len(slopes)) + ) + + def plot( + self, + translation=None, + polygon_options={}, + edge_options={}, + vertex_options={}, + ): + r""" + Return a plot of this polygon with the origin at ``translation``. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.plot() + ...Graphics object consisting of 3 graphics primitives + + We can specify an explicit ``zorder`` to render edges and vertices on + top of the axes which are rendered at z-order 3:: + + sage: S.plot(edge_options={'zorder': 3}, vertex_options={'zorder': 3}) + ...Graphics object consisting of 3 graphics primitives + + We can control the colors, e.g., we can render transparent polygons, + with red edges and blue vertices:: + + sage: S.plot(polygon_options={'fill': None}, edge_options={'color': 'red'}, vertex_options={'color': 'blue'}) + ...Graphics object consisting of 3 graphics primitives + + """ + from sage.plot.point import point2d + from sage.plot.line import line2d + from sage.plot.polygon import polygon2d + + P = self.vertices(translation) + + polygon_options = {"alpha": 0.3, "zorder": 1, **polygon_options} + edge_options = {"color": "orange", "zorder": 2, **edge_options} + vertex_options = {"color": "red", "zorder": 2, **vertex_options} + + return ( + polygon2d(P, **polygon_options) + + line2d(P + (P[0],), **edge_options) + + point2d(P, **vertex_options) + ) + + def angles(self, numerical=None, assume_rational=None): + r""" + Return the list of angles of this polygon (divided by `2 \pi`). + + EXAMPLES:: + + sage: from flatsurf import Polygon + + sage: T = Polygon(angles=[1, 2, 3]) + sage: [T.angle(i) for i in range(3)] + [1/12, 1/6, 1/4] + sage: T.angles() + (1/12, 1/6, 1/4) + sage: sum(T.angle(i) for i in range(3)) + 1/2 + """ + if assume_rational is not None: + import warnings + + warnings.warn( + "assume_rational has been deprecated as a keyword to angles() and will be removed from a future version of sage-flatsurf" + ) + + angles = tuple( + self.angle(i, numerical=numerical) for i in range(len(self.vertices())) + ) + + if not numerical: + self._refine_category_(self.category().WithAngles(angles)) + + return angles + + def angle(self, e, numerical=None, assume_rational=None): + r""" + Return the angle at the beginning of the start point of the edge ``e``. + + EXAMPLES:: + + sage: from flatsurf.geometry.polygon import polygons + sage: polygons.square().angle(0) + 1/4 + sage: polygons.regular_ngon(8).angle(0) + 3/8 + + sage: from flatsurf import Polygon + sage: T = Polygon(vertices=[(0,0), (3,1), (1,5)]) + sage: [T.angle(i, numerical=True) for i in range(3)] # abs tol 1e-13 + [0.16737532973071603, 0.22741638234956674, 0.10520828791971722] + sage: sum(T.angle(i, numerical=True) for i in range(3)) # abs tol 1e-13 + 0.5 + """ + if assume_rational is not None: + import warnings + + warnings.warn( + "assume_rational has been deprecated as a keyword to angle() and will be removed from a future version of sage-flatsurf" + ) + + if numerical is None: + numerical = not self.is_rational() + + if numerical: + import warnings + + warnings.warn( + "the behavior of angle() has been changed in recent versions of sage-flatsurf; for non-rational polygons, numerical=True must be set explicitly to get a numerical approximation of the angle" + ) + + from flatsurf.geometry.euclidean import angle + + return angle( + self.edge(e), + -self.edge((e - 1) % len(self.vertices())), + numerical=numerical, + ) + + def area(self): + r""" + Return the area of this polygon. + + EXAMPLES:: + + sage: from flatsurf.geometry.polygon import polygons + sage: polygons.regular_ngon(8).area() + 2*a + 2 + sage: _ == 2*AA(2).sqrt() + 2 + True + + sage: AA(polygons.regular_ngon(11).area()) + 9.36563990694544? + + sage: polygons.square().area() + 1 + sage: (2*polygons.square()).area() + 4 + """ + # Will use an area formula obtainable from Green's theorem. See for instance: + # http://math.blogoverflow.com/2014/06/04/greens-theorem-and-area-of-polygons/ + total = self.base_ring().zero() + for i in range(len(self.vertices())): + total += (self.vertex(i)[0] + self.vertex(i + 1)[0]) * self.edge(i)[1] + + from sage.all import ZZ + + return total / ZZ(2) + + def centroid(self): + r""" + Return the coordinates of the centroid of this polygon. + + ALGORITHM: + + We use the customary formula of the centroid of polygons, see + https://en.wikipedia.org/wiki/Centroid#Of_a_polygon + + EXAMPLES:: + + sage: from flatsurf.geometry.polygon import polygons + sage: P = polygons.regular_ngon(4) + sage: P + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: P.centroid() + (1/2, 1/2) + + sage: P = polygons.regular_ngon(8); P + Polygon(vertices=[(0, 0), (1, 0), (1/2*a + 1, 1/2*a), (1/2*a + 1, 1/2*a + 1), (1, a + 1), (0, a + 1), (-1/2*a, 1/2*a + 1), (-1/2*a, 1/2*a)]) + sage: P.centroid() + (1/2, 1/2*a + 1/2) + + sage: P = polygons.regular_ngon(11) + sage: C = P.centroid() + sage: P = P.translate(-C) + sage: P.centroid() + (0, 0) + + """ + x, y = list(zip(*self.vertices())) + nvertices = len(x) + A = self.area() + + from sage.all import vector + + return vector( + ( + ~(6 * A) + * sum( + [ + (x[i - 1] + x[i]) * (x[i - 1] * y[i] - x[i] * y[i - 1]) + for i in range(nvertices) + ] + ), + ~(6 * A) + * sum( + [ + (y[i - 1] + y[i]) * (x[i - 1] * y[i] - x[i] * y[i - 1]) + for i in range(nvertices) + ] + ), + ) + ) + + class Rational(CategoryWithAxiom_over_base_ring): + r""" + The category of rational Euclidean polygons. + + .. NOTE:: + + This category must be defined here to make SageMath's test suite + pass. Otherwise we get "The super categories of a category over + base should be a category over base (or the related Bimodules) or a + singleton category"; we did not investigate what exactly is going on + here. + + """ + + class SubcategoryMethods: + @cached_method + def module(self): + r""" + Return the free module of rank 2 in which these polygons embed. + + EXAMPLES:: + + sage: from flatsurf import Polygons + sage: C = Polygons(QQ) + sage: C.module() + doctest:warning + ... + UserWarning: module() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring()**2 instead + Vector space of dimension 2 over Rational Field + + :: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: C = EuclideanPolygonsWithAngles(1, 2, 3) + sage: C.module() + Vector space of dimension 2 over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + + """ + import warnings + + warnings.warn( + "module() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring()**2 instead" + ) + + return FreeModule(self.base_ring(), 2) + + @cached_method + def vector_space(self): + r""" + Return the vector space of dimension 2 in which these polygons embed. + + EXAMPLES:: + + sage: from flatsurf import Polygons + sage: C = Polygons(QQ) + sage: C.vector_space() + Vector space of dimension 2 over Rational Field + + :: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: C = EuclideanPolygonsWithAngles(1, 2, 3) + sage: C.vector_space() + doctest:warning + ... + UserWarning: vector_space() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring().fraction_field()**2 instead + Vector space of dimension 2 over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + + """ + import warnings + + warnings.warn( + "vector_space() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring().fraction_field()**2 instead" + ) + + from sage.all import VectorSpace + + return VectorSpace(self.base_ring().fraction_field(), 2) + + def WithAngles(self, angles): + r""" + Return the subcategory of polygons with fixed ``angles``. + + INPUT: + + - ``angles`` -- a finite sequence of numbers, the inner angles of + the polygon; the angles are automatically normalized to sum to + `(n-2)π`. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons(AA).WithAngles([1, 2, 3]) + Category of euclidean triangles with angles (1/12, 1/6, 1/4) over Algebraic Real Field + + """ + from flatsurf.geometry.categories.euclidean_polygons_with_angles import ( + EuclideanPolygonsWithAngles, + ) + + angles = EuclideanPolygonsWithAngles._normalize_angles(angles) + return EuclideanPolygonsWithAngles(self.base_ring(), angles) & self + + def __call__(self, *args, **kwds): + r""" + TESTS:: + + sage: from flatsurf import Polygons, ConvexPolygons + + sage: C = Polygons(QQ) + sage: p = C(vertices=[(0,0),(1,0),(2,0),(1,1)]) + doctest:warning + ... + UserWarning: Polygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead + sage: p + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + sage: C(p) is p + False + sage: C(p) == p + True + sage: C((1,0), (0,1), (-1, 1)) + Traceback (most recent call last): + ... + ValueError: the polygon does not close up + + sage: D = ConvexPolygons(QQbar) + doctest:warning + ... + UserWarning: ConvexPolygons() has been deprecated and will be removed from a future version of sage-flatsurf; use Polygon() to create polygons. + If you really need the category of convex polygons over a ring use EuclideanPolygons(ring).Simple().Convex() instead. + sage: D(p) + doctest:warning + ... + UserWarning: ConvexPolygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + sage: D(vertices=p.vertices()) + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + sage: D(edges=p.edges()) + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + """ + # We cannot have a __call__() in SubcategoryMethods so there is no good + # way to support this in the category framework. Also, this code is + # duplicated in several places and the Polygon() helper seems to be + # much more versatile. + import warnings + + warnings.warn( + "Polygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead" + ) + + check = kwds.pop("check", True) + + from flatsurf.geometry.polygon import EuclideanPolygon + + if len(args) == 1 and isinstance(args[0], EuclideanPolygon): + if args[0].category() is self: + return args[0] + vertices = [self.vector_space()(v) for v in args[0].vertices()] + args = () + + else: + vertices = kwds.pop("vertices", None) + edges = kwds.pop("edges", None) + base_point = kwds.pop("base_point", (0, 0)) + + if (vertices is None) and (edges is None): + if len(args) == 1: + edges = args[0] + elif args: + edges = args + else: + raise ValueError( + "exactly one of 'vertices' or 'edges' must be provided" + ) + if kwds: + raise ValueError("invalid keyword {!r}".format(next(iter(kwds)))) + + if edges is not None: + v = self.vector_space()(base_point) + vertices = [] + for e in map(self.vector_space(), edges): + vertices.append(v) + v += e + if v != vertices[0]: + raise ValueError("the polygon does not close up") + + from flatsurf.geometry.polygon import Polygon + + return Polygon( + base_ring=self.base(), vertices=vertices, category=self, check=check + ) + + class Convex(CategoryWithAxiom_over_base_ring): + r""" + The subcategory of convex Euclidean polygons in the real plane. + + EXAMPLES: + + For historic reasons, there is the shortcut ``ConvexPolygons`` to get + the Euclidean convex polygons:: + + sage: from flatsurf import ConvexPolygons + sage: C = ConvexPolygons(QQ) + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: C is EuclideanPolygons(QQ).Convex().Simple() + True + + sage: C(vertices=[(0,0), (2,0), (1,1)]) + Polygon(vertices=[(0, 0), (2, 0), (1, 1)]) + + sage: C(edges=[(1,0), (0,1), (-1,0), (0,-1)]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + This axiom can also be created over non-fields:: + + sage: ConvexPolygons(ZZ) + Category of convex simple euclidean polygons over Integer Ring + + TESTS:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: TestSuite(EuclideanPolygons(QQ).Convex()).run() + sage: TestSuite(EuclideanPolygons(QQbar).Convex()).run() + sage: TestSuite(EuclideanPolygons(ZZ).Convex()).run() + + """ + + class ParentMethods: + r""" + Provides methods available to all convex Euclidean polygons in the + real plane. + + If you want to add functionality to such polygons, you probably + want to put it here. + """ + + def is_convex(self, strict=False): + r""" + Return whether this is a convex polygon. + + INPUT: + + - ``strict`` -- whether to check for strict convexity, i.e., a + polygon with a π angle is not considered convex. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.is_convex() + True + sage: S.is_convex(strict=True) + True + + """ + if not strict: + return True + + return EuclideanPolygons.ParentMethods.is_convex(self, strict=strict) + + class Simple(CategoryWithAxiom_over_base_ring): + r""" + The subcategory of Euclidean polygons without self-intersection in the + real plane. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons(QQ).Simple() + Category of simple euclidean polygons over Rational Field + + """ + + class ParentMethods: + r""" + Provides methods available to all simple Euclidean polygons. + + If you want to add functionality to all polygons, independent of + implementation, you probably want to put it here. + """ + + def triangulation(self): + r""" + Return a list of pairs of indices of vertices that together with the boundary + form a triangulation. + + EXAMPLES: + + We triangulate a non-convex polygon:: + + sage: from flatsurf import Polygon + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1), (0,2), (-1,2), (-1,1), (-2,1), + ....: (-2,0), (-1,0), (-1,-1), (0,-1)]) + sage: P.triangulation() + [(0, 2), (2, 8), (3, 5), (6, 8), (8, 3), (3, 6), (9, 11), (0, 9), (2, 9)] + + TESTS:: + + sage: Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]).triangulation() + [(0, 2)] + + sage: quad = [(0,0), (1,-1), (0,1), (-1,-1)] + sage: for i in range(4): + ....: Polygon(vertices=quad[i:] + quad[:i]).triangulation() + [(0, 2)] + [(1, 3)] + [(0, 2)] + [(1, 3)] + + sage: poly = [(0,0),(1,1),(2,0),(3,1),(4,0),(4,2), + ....: (-4,2),(-4,0),(-3,1),(-2,0),(-1,1)] + sage: Polygon(vertices=poly).triangulation() + [(1, 3), (3, 5), (5, 8), (6, 8), (8, 10), (10, 1), (1, 5), (5, 10)] + sage: for i in range(len(poly)): + ....: Polygon(vertices=poly[i:] + poly[:i]).triangulation() + [(1, 3), (3, 5), (5, 8), (6, 8), (8, 10), (10, 1), (1, 5), (5, 10)] + [(0, 2), (2, 4), (4, 7), (5, 7), (7, 9), (9, 0), (0, 4), (4, 9)] + [(1, 3), (3, 6), (4, 6), (6, 8), (8, 10), (10, 1), (3, 8), (10, 3)] + [(0, 2), (2, 5), (3, 5), (5, 7), (7, 9), (9, 0), (2, 7), (9, 2)] + [(1, 4), (2, 4), (4, 6), (6, 8), (8, 10), (10, 1), (1, 6), (8, 1)] + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 9), (9, 0), (0, 5), (7, 0)] + [(0, 2), (2, 4), (4, 6), (6, 8), (8, 10), (10, 2), (4, 10), (6, 10)] + [(1, 3), (3, 5), (5, 7), (7, 9), (9, 1), (10, 1), (3, 9), (5, 9)] + [(0, 2), (2, 4), (4, 6), (6, 8), (8, 0), (9, 0), (2, 8), (4, 8)] + [(1, 3), (3, 5), (5, 7), (7, 10), (8, 10), (10, 1), (1, 7), (3, 7)] + [(0, 2), (2, 4), (4, 6), (6, 9), (7, 9), (9, 0), (0, 6), (2, 6)] + + sage: poly = [(0,0), (1,0), (2,0), (2,1), (2,2), (1,2), (0,2), (0,1)] + sage: Polygon(vertices=poly).triangulation() + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + sage: for i in range(len(poly)): + ....: Polygon(vertices=poly[i:] + poly[:i]).triangulation() + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + + sage: poly = [(0,0), (1,2), (3,3), (1,4), (0,6), (-1,4), (-3,-3), (-1,2)] + sage: Polygon(vertices=poly).triangulation() + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + sage: for i in range(len(poly)): + ....: Polygon(vertices=poly[i:] + poly[:i]).triangulation() + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + [(0, 2), (3, 5), (5, 7), (7, 3), (0, 3)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + [(0, 6), (1, 3), (3, 5), (5, 1), (6, 1)] + [(0, 2), (2, 4), (4, 6), (6, 0), (0, 4)] + + sage: x = polygen(QQ) + sage: p = x^4 - 5*x^2 + 5 + sage: r = AA.polynomial_root(p, RIF(1.17,1.18)) + sage: K. = NumberField(p, embedding=r) + + sage: poly = [(1/2*a^2 - 3/2, 1/2*a), + ....: (-a^3 + 2*a^2 + 2*a - 4, 0), + ....: (1/2*a^2 - 3/2, -1/2*a), + ....: (1/2*a^3 - a^2 - 1/2*a + 1, 1/2*a^2 - a), + ....: (-1/2*a^2 + 1, 1/2*a^3 - 3/2*a), + ....: (-1/2*a + 1, a^3 - 3/2*a^2 - 2*a + 5/2), + ....: (1, 0), + ....: (-1/2*a + 1, -a^3 + 3/2*a^2 + 2*a - 5/2), + ....: (-1/2*a^2 + 1, -1/2*a^3 + 3/2*a), + ....: (1/2*a^3 - a^2 - 1/2*a + 1, -1/2*a^2 + a)] + sage: Polygon(vertices=poly).triangulation() + [(0, 3), (1, 3), (3, 5), (5, 7), (7, 9), (9, 3), (3, 7)] + + sage: z = QQbar.zeta(24) + sage: pts = [(1+i%2) * z**i for i in range(24)] + sage: pts = [vector(AA, (x.real(), x.imag())) for x in pts] + sage: Polygon(vertices=pts).triangulation() + [(0, 2), ..., (16, 0)] + + This is https://github.com/flatsurf/sage-flatsurf/issues/87 :: + + sage: x = polygen(QQ) + sage: K. = NumberField(x^2 - 3, embedding=AA(3).sqrt()) + + sage: Polygon(vertices=[(0, 0), (1, 0), (1/2*c + 1, -1/2), (c + 1, 0), (-3/2*c + 1, 5/2), (0, c - 2)]).triangulation() + [(0, 4), (1, 3), (4, 1)] + + """ + vertices = self.vertices() + + n = len(vertices) + + if n < 3: + raise ValueError + + if n == 3: + return [] + + # NOTE: The algorithm is naive. We look at all possible chords between + # the i-th and j-th vertices. If the chord does not intersect any edge + # then we cut the polygon along this edge and call recursively + # triangulate on the two pieces. + for i in range(n - 1): + eiright = vertices[(i + 1) % n] - vertices[i] + eileft = vertices[(i - 1) % n] - vertices[i] + for j in range(i + 2, (n if i else n - 1)): + ejright = vertices[(j + 1) % n] - vertices[j] + ejleft = vertices[(j - 1) % n] - vertices[j] + chord = vertices[j] - vertices[i] + + from flatsurf.geometry.euclidean import is_between + + # check angles with neighbouring edges + if not ( + is_between(eiright, eileft, chord) + and is_between(ejright, ejleft, -chord) + ): + continue + + # check intersection with other edges + e = (vertices[i], vertices[j]) + good = True + for k in range(n): + if k == i or k == j or k == (i - 1) % n or k == (j - 1) % n: + continue + + f = (vertices[k], vertices[(k + 1) % n]) + + from flatsurf.geometry.euclidean import ( + is_segment_intersecting, + ) + + res = is_segment_intersecting(e, f) + + assert res != 1 + + if res == 2: + good = False + break + + if good: + from flatsurf import Polygon + + part0 = [ + (s + i, t + i) + for s, t in Polygon( + vertices=vertices[i : j + 1], check=False + ).triangulation() + ] + part1 = [] + for s, t in Polygon( + vertices=vertices[j:] + vertices[: i + 1], check=False + ).triangulation(): + if s < n - j: + s += j + else: + s -= n - j + if t < n - j: + t += j + else: + t -= n - j + part1.append((s, t)) + return [(i, j)] + part0 + part1 + + assert False + + class Convex(CategoryWithAxiom_over_base_ring): + r""" + The subcategory of the simple convex Euclidean polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons(QQ).Simple().Convex() + Category of convex simple euclidean polygons over Rational Field + + """ + + def __call__(self, *args, **kwds): + r""" + TESTS:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + + sage: C = EuclideanPolygons(QQ).Convex().Simple() + sage: p = C(vertices=[(0,0),(1,0),(2,0),(1,1)]) + doctest:warning + ... + UserWarning: ConvexPolygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead + sage: p + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + sage: C(p) is p + True + sage: C((1,0), (0,1), (-1, 1)) + Traceback (most recent call last): + ... + ValueError: the polygon does not close up + + sage: D = EuclideanPolygons(QQbar).Convex().Simple() + sage: D(p) + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + sage: D(vertices=p.vertices()) + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + sage: D(edges=p.edges()) + Polygon(vertices=[(0, 0), (1, 0), (2, 0), (1, 1)]) + + """ + # We cannot have a __call__() in SubcategoryMethods so there is no good + # way to support this in the category framework. Also, this code is + # duplicated in several places and the Polygon() helper seems to be + # much more versatile. + import warnings + + warnings.warn( + "ConvexPolygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead" + ) + + check = kwds.pop("check", True) + + from flatsurf.geometry.polygon import EuclideanPolygon + + if len(args) == 1 and isinstance(args[0], EuclideanPolygon): + if args[0].category() is self: + return args[0] + + vertices = [self.vector_space()(v) for v in args[0].vertices()] + args = () + + else: + vertices = kwds.pop("vertices", None) + edges = kwds.pop("edges", None) + base_point = kwds.pop("base_point", (0, 0)) + + if (vertices is None) and (edges is None): + if len(args) == 1: + edges = args[0] + elif args: + edges = args + else: + raise ValueError( + "exactly one of 'vertices' or 'edges' must be provided" + ) + if kwds: + raise ValueError( + "invalid keyword {!r}".format(next(iter(kwds))) + ) + + if edges is not None: + v = (self.base_ring() ** 2)(base_point) + vertices = [] + for e in map(self.base_ring() ** 2, edges): + vertices.append(v) + v += e + if v != vertices[0]: + raise ValueError("the polygon does not close up") + + from flatsurf.geometry.polygon import Polygon + + return Polygon( + base_ring=self.base(), vertices=vertices, category=self, check=check + ) + + class ParentMethods: + r""" + Provides methods available to all simple convex Euclidean polygons. + + If you want to add functionality to all polygons, independent of + implementation, you probably want to put it here. + """ + + def find_separatrix(self, direction=None, start_vertex=0): + r""" + Returns a pair (v,same) where v is a vertex and same is a boolean. + The provided parameter "direction" should be a non-zero vector with + two entries, or by default direction=(0,1). + + A separatrix is a ray leaving a vertex and entering the polygon. + + The vertex v will have a separatrix leaving it which is parallel to + direction. The returned value "same" answers the question if this separatrix + points in the same direction as "direction". There is a boundary case: + we allow the separatrix to be an edge if and only if traveling along + the sepatrix from the vertex would travel in a counter-clockwise + direction about the polygon. + + The vertex returned is uniquely defined from the above if the polygon + is a triangle. Otherwise, we return the first vertex with this property + obtained by inspecting starting at start_vertex (defaults to 0) and + then moving in the counter-clockwise direction. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: p=polygons.square() + sage: print(p.find_separatrix()) + (1, True) + sage: print(p.find_separatrix(start_vertex=2)) + (3, False) + + """ + if direction is None: + direction = (self.base_ring() ** 2)( + (self.base_ring().zero(), self.base_ring().one()) + ) + else: + assert not direction.is_zero() + v = start_vertex + n = len(self.vertices()) + for i in range(len(self.vertices())): + if ( + ccw(self.edge(v), direction) >= 0 + and ccw(self.edge(v + n - 1), direction) > 0 + ): + return v, True + if ( + ccw(self.edge(v), direction) <= 0 + and ccw(self.edge(v + n - 1), direction) < 0 + ): + return v, False + v = v + 1 % n + raise RuntimeError("Failed to find a separatrix") + + def contains_point(self, point, translation=None): + r""" + Return whether the point is within the polygon (after the polygon is possibly translated) + """ + return self.get_point_position( + point, translation=translation + ).is_inside() + + def get_point_position(self, point, translation=None): + r""" + Get a combinatorial position of a points position compared to the polygon + + INPUT: + + - ``point`` -- a point in the plane (vector over the underlying base_ring()) + + - ``translation`` -- optional translation to applied to the polygon (vector over the underlying base_ring()) + + OUTPUT: + + - a PolygonPosition object + + EXAMPLES:: + + sage: from flatsurf import polygons, Polygon + sage: s = polygons.square() + sage: V = s.base_ring()**2 + sage: s.get_point_position(V((1/2,1/2))) + point positioned in interior of polygon + sage: s.get_point_position(V((1,0))) + point positioned on vertex 1 of polygon + sage: s.get_point_position(V((1,1/2))) + point positioned on interior of edge 1 of polygon + sage: s.get_point_position(V((1,3/2))) + point positioned outside polygon + + sage: p=Polygon(edges=[(1,0),(1,0),(1,0),(0,1),(-3,0),(0,-1)]) + sage: V = p.base_ring()**2 + sage: p.get_point_position(V([10,0])) + point positioned outside polygon + sage: p.get_point_position(V([1/2,0])) + point positioned on interior of edge 0 of polygon + sage: p.get_point_position(V([3/2,0])) + point positioned on interior of edge 1 of polygon + sage: p.get_point_position(V([2,0])) + point positioned on vertex 2 of polygon + sage: p.get_point_position(V([5/2,0])) + point positioned on interior of edge 2 of polygon + sage: p.get_point_position(V([5/2,1/4])) + point positioned in interior of polygon + """ + from flatsurf.geometry.polygon import PolygonPosition + + if translation is None: + # Since we allow the initial vertex to be non-zero, this changed: + v1 = self.vertex(0) + else: + # Since we allow the initial vertex to be non-zero, this changed: + v1 = translation + self.vertex(0) + # Below, we only make use of edge vectors: + for i in range(len(self.vertices())): + v0 = v1 + e = self.edge(i) + v1 = v0 + e + w = ccw(e, point - v0) + if w < 0: + return PolygonPosition(PolygonPosition.OUTSIDE) + if w == 0: + # Lies on the line through edge i! + dp1 = e.dot_product(point - v0) + if dp1 == 0: + return PolygonPosition(PolygonPosition.VERTEX, vertex=i) + dp2 = e.dot_product(e) + if 0 < dp1 and dp1 < dp2: + return PolygonPosition( + PolygonPosition.EDGE_INTERIOR, edge=i + ) + # Loop terminated (on inside of each edge) + return PolygonPosition(PolygonPosition.INTERIOR) + + def flow_to_exit(self, point, direction): + r""" + Flow a point in the direction of holonomy until the point leaves the + polygon. Note that ValueErrors may be thrown if the point is not in the + polygon, or if it is on the boundary and the holonomy does not point + into the polygon. + + INPUT: + + - ``point`` -- a point in the closure of the polygon (as a vector) + + - ``holonomy`` -- direction of motion (a vector of non-zero length) + + OUTPUT: + + - The point in the boundary of the polygon where the trajectory exits + + - a PolygonPosition object representing the combinatorial position of the stopping point + """ + from flatsurf.geometry.polygon import PolygonPosition + + V = self.base_ring().fraction_field() ** 2 + if direction == V.zero(): + raise ValueError("Zero vector provided as direction.") + v0 = self.vertex(0) + for i in range(len(self.vertices())): + e = self.edge(i) + from sage.all import matrix + + m = matrix([[e[0], -direction[0]], [e[1], -direction[1]]]) + try: + ret = m.inverse() * (point - v0) + s = ret[0] + t = ret[1] + # What if the matrix is non-invertible? + + # Answer: You'll get a ZeroDivisionError which means that the edge is parallel + # to the direction. + + # s is location it intersects on edge, t is the portion of the direction to reach this intersection + if t > 0 and 0 <= s and s <= 1: + # The ray passes through edge i. + if s == 1: + # exits through vertex i+1 + v0 = v0 + e + return v0, PolygonPosition( + PolygonPosition.VERTEX, + vertex=(i + 1) % len(self.vertices()), + ) + if s == 0: + # exits through vertex i + return v0, PolygonPosition( + PolygonPosition.VERTEX, vertex=i + ) + # exits through vertex i + # exits through interior of edge i + prod = t * direction + return point + prod, PolygonPosition( + PolygonPosition.EDGE_INTERIOR, edge=i + ) + except ZeroDivisionError: + # Here we know the edge and the direction are parallel + if ccw(e, point - v0) == 0: + # In this case point lies on the edge. + # We need to work out which direction to move in. + from flatsurf.geometry.euclidean import is_parallel + + if (point - v0).is_zero() or is_parallel(e, point - v0): + # exits through vertex i+1 + return self.vertex(i + 1), PolygonPosition( + PolygonPosition.VERTEX, + vertex=(i + 1) % len(self.vertices()), + ) + else: + # exits through vertex i + return v0, PolygonPosition( + PolygonPosition.VERTEX, vertex=i + ) + pass + v0 = v0 + e + # Our loop has terminated. This can mean one of several errors... + pos = self.get_point_position(point) + if pos.is_outside(): + raise ValueError("Started with point outside polygon") + raise ValueError( + "Point on boundary of polygon and direction not pointed into the polygon." + ) + + def flow_map(self, direction): + r""" + Return a polygonal map associated to the flow in ``direction`` in this + polygon. + + EXAMPLES:: + + sage: from flatsurf.geometry.polygon import Polygon + sage: S = Polygon(vertices=[(0,0),(2,0),(2,2),(1,2),(0,2),(0,1)]) + sage: S.flow_map((0,1)) + Flow polygon map: + 3 2 + 0 + top lengths: [1, 1] + bot lengths: [2] + sage: S.flow_map((1,1)) + Flow polygon map: + 3 2 1 + 4 5 0 + top lengths: [1, 1, 2] + bot lengths: [1, 1, 2] + sage: S.flow_map((-1,-1)) + Flow polygon map: + 0 5 4 + 1 2 3 + top lengths: [2, 1, 1] + bot lengths: [2, 1, 1] + + sage: K. = NumberField(x^2 - 2, embedding=AA(2).sqrt()) + sage: S.flow_map((sqrt2,1)) + Flow polygon map: + 3 2 1 + 4 5 0 + top lengths: [1, 1, 2*sqrt2] + bot lengths: [sqrt2, sqrt2, 2] + """ + from sage.all import vector + + direction = vector(direction) + DP = direction.parent() + P = self.base_ring().fraction_field() ** 2 + if DP != P: + P = cm.common_parent(DP, P) + ring = P.base_ring() + direction = direction.change_ring(ring) + else: + ring = P.base_ring() + + # first compute the transversal length of each edge + t = P([direction[1], -direction[0]]) + lengths = [t.dot_product(e) for e in self.edges()] + n = len(lengths) + for i in range(n): + j = (i + 1) % len(lengths) + l0 = lengths[i] + l1 = lengths[j] + if l0 >= 0 and l1 < 0: + rt = j + if l0 > 0 and l1 <= 0: + rb = j + if l0 <= 0 and l1 > 0: + lb = j + if l0 < 0 and l1 >= 0: + lt = j + + if rt < lt: + top_lengths = lengths[rt:lt] + top_labels = list(range(rt, lt)) + else: + top_lengths = lengths[rt:] + lengths[:lt] + top_labels = list(range(rt, n)) + list(range(lt)) + top_lengths = [-x for x in reversed(top_lengths)] + top_labels.reverse() + + if lb < rb: + bot_lengths = lengths[lb:rb] + bot_labels = list(range(lb, rb)) + else: + bot_lengths = lengths[lb:] + lengths[:rb] + bot_labels = list(range(lb, n)) + list(range(rb)) + + from flatsurf.geometry.interval_exchange_transformation import ( + FlowPolygonMap, + ) + + return FlowPolygonMap( + ring, bot_labels, bot_lengths, top_labels, top_lengths + ) + + def flow(self, point, holonomy, translation=None): + r""" + Flow a point in the direction of holonomy for the length of the + holonomy, or until the point leaves the polygon. Note that ValueErrors + may be thrown if the point is not in the polygon, or if it is on the + boundary and the holonomy does not point into the polygon. + + INPUT: + + - ``point`` -- a point in the closure of the polygon (vector over the underlying base_ring()) + + - ``holonomy`` -- direction and magnitude of motion (vector over the underlying base_ring()) + + - ``translation`` -- optional translation to applied to the polygon (vector over the underlying base_ring()) + + OUTPUT: + + - The point within the polygon where the motion stops (or leaves the polygon) + + - The amount of holonomy left to flow + + - a PolygonPosition object representing the combinatorial position of the stopping point + + EXAMPLES:: + + sage: from flatsurf.geometry.polygon import polygons + sage: s = polygons.square() + sage: V = QQ**2 + sage: p = V((1/2, 1/2)) + sage: w = V((2, 0)) + sage: s.flow(p, w) + ((1, 1/2), (3/2, 0), point positioned on interior of edge 1 of polygon) + """ + from flatsurf.geometry.polygon import PolygonPosition + + V = self.base_ring().fraction_field() ** 2 + if holonomy == V.zero(): + # not flowing at all! + return ( + point, + V.zero(), + self.get_point_position(point, translation=translation), + ) + if translation is None: + v0 = self.vertex(0) + else: + v0 = self.vertex(0) + translation + for i in range(len(self.vertices())): + e = self.edge(i) + from sage.all import matrix + + m = matrix([[e[0], -holonomy[0]], [e[1], -holonomy[1]]]) + try: + ret = m.inverse() * (point - v0) + s = ret[0] + t = ret[1] + # What if the matrix is non-invertible? + + # s is location it intersects on edge, t is the portion of the holonomy to reach this intersection + if t > 0 and 0 <= s and s <= 1: + # The ray passes through edge i. + if t > 1: + # the segment from point with the given holonomy stays within the polygon + return ( + point + holonomy, + V.zero(), + PolygonPosition(PolygonPosition.INTERIOR), + ) + if s == 1: + # exits through vertex i+1 + v0 = v0 + e + return ( + v0, + point + holonomy - v0, + PolygonPosition( + PolygonPosition.VERTEX, + vertex=(i + 1) % len(self.vertices()), + ), + ) + if s == 0: + # exits through vertex i + return ( + v0, + point + holonomy - v0, + PolygonPosition( + PolygonPosition.VERTEX, vertex=i + ), + ) + # exits through vertex i + # exits through interior of edge i + prod = t * holonomy + return ( + point + prod, + holonomy - prod, + PolygonPosition( + PolygonPosition.EDGE_INTERIOR, edge=i + ), + ) + except ZeroDivisionError: + # can safely ignore this error. It means that the edge and the holonomy are parallel. + pass + v0 = v0 + e + # Our loop has terminated. This can mean one of several errors... + pos = self.get_point_position(point, translation=translation) + if pos.is_outside(): + raise ValueError("Started with point outside polygon") + raise ValueError( + "Point on boundary of polygon and holonomy not pointed into the polygon." + ) + + def circumscribing_circle(self): + r""" + Returns the circle which circumscribes this polygon. + Raises a ValueError if the polygon is not circumscribed by a circle. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: P = Polygon(vertices=[(0,0),(1,0),(2,1),(-1,1)]) + sage: P.circumscribing_circle() + Circle((1/2, 3/2), 5/2) + """ + from flatsurf.geometry.circle import circle_from_three_points + + circle = circle_from_three_points( + self.vertex(0), self.vertex(1), self.vertex(2), self.base_ring() + ) + for i in range(3, len(self.vertices())): + if not circle.point_position(self.vertex(i)) == 0: + raise ValueError( + "Vertex " + str(i) + " is not on the circle." + ) + return circle + + def subdivide(self): + r""" + Return a list of triangles that partition this polygon. + + For each edge of the polygon one triangle is created that joins this + edge to the + :meth:`~.EuclideanPolygons.ParentMethods.centroid` of + this polygon. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: P = polygons.regular_ngon(3); P + Polygon(vertices=[(0, 0), (1, 0), (1/2, 1/2*a)]) + sage: print(P.subdivide()) + [Polygon(vertices=[(0, 0), (1, 0), (1/2, 1/6*a)]), + Polygon(vertices=[(1, 0), (1/2, 1/2*a), (1/2, 1/6*a)]), + Polygon(vertices=[(1/2, 1/2*a), (0, 0), (1/2, 1/6*a)])] + + :: + + sage: P = polygons.regular_ngon(4) + sage: print(P.subdivide()) + [Polygon(vertices=[(0, 0), (1, 0), (1/2, 1/2)]), + Polygon(vertices=[(1, 0), (1, 1), (1/2, 1/2)]), + Polygon(vertices=[(1, 1), (0, 1), (1/2, 1/2)]), + Polygon(vertices=[(0, 1), (0, 0), (1/2, 1/2)])] + + Sometimes alternating with :meth:`subdivide_edges` can produce a more + uniform subdivision:: + + sage: P = polygons.regular_ngon(4) + sage: print(P.subdivide_edges(2).subdivide()) + [Polygon(vertices=[(0, 0), (1/2, 0), (1/2, 1/2)]), + Polygon(vertices=[(1/2, 0), (1, 0), (1/2, 1/2)]), + Polygon(vertices=[(1, 0), (1, 1/2), (1/2, 1/2)]), + Polygon(vertices=[(1, 1/2), (1, 1), (1/2, 1/2)]), + Polygon(vertices=[(1, 1), (1/2, 1), (1/2, 1/2)]), + Polygon(vertices=[(1/2, 1), (0, 1), (1/2, 1/2)]), + Polygon(vertices=[(0, 1), (0, 1/2), (1/2, 1/2)]), + Polygon(vertices=[(0, 1/2), (0, 0), (1/2, 1/2)])] + + """ + vertices = self.vertices() + center = self.centroid() + from flatsurf import Polygon + + return [ + Polygon( + vertices=( + vertices[i], + vertices[(i + 1) % len(vertices)], + center, + ), + ) + for i in range(len(vertices)) + ] + + def subdivide_edges(self, parts=2): + r""" + Return a copy of this polygon whose edges have been split into + ``parts`` equal parts each. + + INPUT: + + - ``parts`` -- a positive integer (default: 2) + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: P = polygons.regular_ngon(3); P + Polygon(vertices=[(0, 0), (1, 0), (1/2, 1/2*a)]) + sage: P.subdivide_edges(1) == P + True + sage: P.subdivide_edges(2) + Polygon(vertices=[(0, 0), (1/2, 0), (1, 0), (3/4, 1/4*a), (1/2, 1/2*a), (1/4, 1/4*a)]) + sage: P.subdivide_edges(3) + Polygon(vertices=[(0, 0), (1/3, 0), (2/3, 0), (1, 0), (5/6, 1/6*a), (2/3, 1/3*a), (1/2, 1/2*a), (1/3, 1/3*a), (1/6, 1/6*a)]) + + """ + if parts < 1: + raise ValueError("parts must be a positive integer") + + steps = [e / parts for e in self.edges()] + from flatsurf import Polygon + + return Polygon(edges=[e for e in steps for p in range(parts)]) + + def j_invariant(self): + r""" + Return the Kenyon-Smille J-invariant of this polygon. + + The base ring of the polygon must be a number field. + + The output is a triple ``(Jxx, Jyy, Jxy)`` that corresponds + respectively to the Sah-Arnoux-Fathi invariant of the vertical flow, + the Sah-Arnoux-Fathi invariant of the horizontal flow and the `xy`-matrix. + + EXAMPLES:: + + sage: from flatsurf import polygons + + sage: polygons.right_triangle(1/3,1).j_invariant() + ( + [0 0] + (0), (0), [1 0] + ) + + The regular 8-gon:: + + sage: polygons.regular_ngon(8).j_invariant() + ( + [2 2] + (0), (0), [2 1] + ) + + ( + [ 0 3/2] + (1/2), (-1/2), [3/2 0] + ) + + Some extra debugging:: + + sage: K. = NumberField(x^3 - 2, embedding=AA(2)**(1/3)) + sage: ux = 1 + a + a**2 + sage: uy = -2/3 + a + sage: vx = 1/5 - a**2 + sage: vy = a + 7/13*a**2 + + sage: from flatsurf import Polygon + sage: p = Polygon(edges=[(ux, uy), (vx,vy), (-ux-vx,-uy-vy)], base_ring=K) + sage: Jxx, Jyy, Jxy = p.j_invariant() + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons.Simple.Convex.ParentMethods._wedge_product(ux.vector(), vx.vector()) == Jxx + True + sage: EuclideanPolygons.Simple.Convex.ParentMethods._wedge_product(uy.vector(), vy.vector()) == Jyy + True + + """ + from sage.all import QQ, matrix + + if self.base_ring() is QQ: + raise NotImplementedError + + K = self.base_ring() + try: + V, from_V, to_V = K.vector_space() + except (AttributeError, ValueError): + raise ValueError( + "the surface needs to be define over a number field" + ) + + dim = K.degree() + M = K ** (dim * (dim - 1) // 2) + Jxx = Jyy = M.zero() + Jxy = matrix(K, dim) + vertices = list(self.vertices()) + vertices.append(vertices[0]) + + for i in range(len(vertices) - 1): + a = to_V(vertices[i][0]) + b = to_V(vertices[i][1]) + c = to_V(vertices[i + 1][0]) + d = to_V(vertices[i + 1][1]) + Jxx += self._wedge_product(a, c) + Jyy += self._wedge_product(b, d) + Jxy += self._tensor_product(a, d) + Jxy -= self._tensor_product(c, b) + + return (Jxx, Jyy, Jxy) + + @staticmethod + def _wedge_product(v, w): + r""" + Return the wedge product of ``v`` and ``w``. + + This is a helper method for :meth:`j_invariant`. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + + sage: EuclideanPolygons.Simple.Convex.ParentMethods._wedge_product(vector((1, 2)), vector((1, 2))) + (0) + sage: EuclideanPolygons.Simple.Convex.ParentMethods._wedge_product(vector((1, 2)), vector((2, 1))) + (-3) + + sage: EuclideanPolygons.Simple.Convex.ParentMethods._wedge_product(vector((1, 2, 3)), vector((2, 3, 4))) + (-1, -2, -1) + + """ + d = len(v) + + assert len(w) == d + + R = v.base_ring() + + from sage.all import free_module_element + + return free_module_element( + R, + d * (d - 1) // 2, + [ + (v[i] * w[j] - v[j] * w[i]) + for i in range(d - 1) + for j in range(i + 1, d) + ], + ) + + @staticmethod + def _tensor_product(u, v): + r""" + Return the tensor product of ``u`` and ``v``. + + This is a helper method for :meth:`j_invariant`. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons.Simple.Convex.ParentMethods._tensor_product(vector((2, 3, 5)), vector((7, 11, 13))) + [14 21 35] + [22 33 55] + [26 39 65] + + """ + from sage.all import vector + + u = vector(u) + v = vector(v) + + d = len(u) + R = u.base_ring() + + assert len(u) == len(v) and v.base_ring() == R + from sage.all import matrix + + return matrix( + R, d, [u[i] * v[j] for j in range(d) for i in range(d)] + ) + + def is_isometric(self, other, certificate=False): + r""" + Return whether ``self`` and ``other`` are isometric convex polygons via an orientation + preserving isometry. + + If ``certificate`` is set to ``True`` return also a pair ``(index, rotation)`` + of an integer ``index`` and a matrix ``rotation`` such that the given rotation + matrix identifies this polygon with the other and the edges 0 in this polygon + is mapped to the edge ``index`` in the other. + + .. TODO:: + + Implement ``is_linearly_equivalent`` and ``is_similar``. + + EXAMPLES:: + + sage: from flatsurf import Polygon, polygons + sage: S = polygons.square() + sage: S.is_isometric(S) + True + sage: U = matrix(2,[0,-1,1,0]) * S + sage: U.is_isometric(S) + True + + sage: x = polygen(QQ) + sage: K. = NumberField(x^2 - 2, embedding=AA(2)**(1/2)) + sage: S = S.change_ring(K) + sage: U = matrix(2, [sqrt2/2, -sqrt2/2, sqrt2/2, sqrt2/2]) * S + sage: U.is_isometric(S) + True + + sage: U2 = Polygon(edges=[(1,0), (sqrt2/2, sqrt2/2), (-1,0), (-sqrt2/2, -sqrt2/2)]) + sage: U2.is_isometric(U) + False + sage: U2.is_isometric(U, certificate=True) + (False, None) + + sage: S = Polygon(edges=[(1,0), (sqrt2/2, 3), (-2,3), (-sqrt2/2+1, -6)]) + sage: T = Polygon(edges=[(sqrt2/2,3), (-2,3), (-sqrt2/2+1, -6), (1,0)]) + sage: isometric, cert = S.is_isometric(T, certificate=True) + sage: assert isometric + sage: shift, rot = cert + sage: Polygon(edges=[rot * S.edge((k + shift) % 4) for k in range(4)]).translate(T.vertex(0)) == T + True + + + sage: T = (matrix(2, [sqrt2/2, -sqrt2/2, sqrt2/2, sqrt2/2]) * S).translate((3,2)) + sage: isometric, cert = S.is_isometric(T, certificate=True) + sage: assert isometric + sage: shift, rot = cert + sage: Polygon(edges=[rot * S.edge(k + shift) for k in range(4)]).translate(T.vertex(0)) == T + True + """ + from flatsurf.geometry.polygon import EuclideanPolygon + + if not isinstance(other, EuclideanPolygon): + raise TypeError("other must be a polygon") + + if not other.is_convex(): + raise TypeError("other must be convex") + + n = len(self.vertices()) + if len(other.vertices()) != n: + return False + sedges = self.edges() + oedges = other.edges() + + slengths = [x**2 + y**2 for x, y in sedges] + olengths = [x**2 + y**2 for x, y in oedges] + for i in range(n): + if slengths == olengths: + # we have a match of lengths after a shift by i + xs, ys = sedges[0] + xo, yo = oedges[0] + from sage.all import matrix + + ms = matrix(2, [xs, -ys, ys, xs]) + mo = matrix(2, [xo, -yo, yo, xo]) + rot = mo * ~ms + assert rot.det() == 1 and (rot * rot.transpose()).is_one() + assert oedges[0] == rot * sedges[0] + if all(oedges[i] == rot * sedges[i] for i in range(1, n)): + return ( + (True, (0 if i == 0 else n - i, rot)) + if certificate + else True + ) + olengths.append(olengths.pop(0)) + oedges.append(oedges.pop(0)) + return (False, None) if certificate else False + + def is_translate(self, other, certificate=False): + r""" + Return whether ``other`` is a translate of ``self``. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: S = Polygon(vertices=[(0,0), (3,0), (1,1)]) + sage: T1 = S.translate((2,3)) + sage: S.is_translate(T1) + True + sage: T2 = Polygon(vertices=[(-1,1), (1,0), (2,1)]) + sage: S.is_translate(T2) + False + sage: T3 = Polygon(vertices=[(0,0), (3,0), (2,1)]) + sage: S.is_translate(T3) + False + + sage: S.is_translate(T1, certificate=True) + (True, (0, 1)) + sage: S.is_translate(T2, certificate=True) + (False, None) + sage: S.is_translate(T3, certificate=True) + (False, None) + """ + if type(self) is not type(other): + raise TypeError + + n = len(self.vertices()) + if len(other.vertices()) != n: + return False + sedges = self.edges() + oedges = other.edges() + for i in range(n): + if sedges == oedges: + return (True, (i, 1)) if certificate else True + oedges.append(oedges.pop(0)) + return (False, None) if certificate else False + + def is_half_translate(self, other, certificate=False): + r""" + Return whether ``other`` is a translate or half-translate of ``self``. + + If ``certificate`` is set to ``True`` then return also a pair ``(orientation, index)``. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: S = Polygon(vertices=[(0,0), (3,0), (1,1)]) + sage: T1 = S.translate((2,3)) + sage: S.is_half_translate(T1) + True + sage: T2 = Polygon(vertices=[(-1,1), (1,0), (2,1)]) + sage: S.is_half_translate(T2) + True + sage: T3 = Polygon(vertices=[(0,0), (3,0), (2,1)]) + sage: S.is_half_translate(T3) + False + + sage: S.is_half_translate(T1, certificate=True) + (True, (0, 1)) + sage: half_translate, cert = S.is_half_translate(T2, certificate=True) + sage: assert half_translate + sage: shift, rot = cert + sage: Polygon(edges=[rot * S.edge(k + shift) for k in range(3)]).translate(T2.vertex(0)) == T2 + True + sage: S.is_half_translate(T3, certificate=True) + (False, None) + """ + if type(self) is not type(other): + raise TypeError + + n = len(self.vertices()) + if len(other.vertices()) != n: + return False + + sedges = self.edges() + oedges = other.edges() + for i in range(n): + if sedges == oedges: + return (True, (i, 1)) if certificate else True + oedges.append(oedges.pop(0)) + + assert oedges == other.edges() + oedges = [-e for e in oedges] + for i in range(n): + if sedges == oedges: + return ( + (True, (0 if i == 0 else n - i, -1)) + if certificate + else True + ) + oedges.append(oedges.pop(0)) + + return (False, None) if certificate else False diff --git a/flatsurf/geometry/categories/euclidean_polygons_with_angles.py b/flatsurf/geometry/categories/euclidean_polygons_with_angles.py new file mode 100644 index 000000000..2fafd927a --- /dev/null +++ b/flatsurf/geometry/categories/euclidean_polygons_with_angles.py @@ -0,0 +1,1200 @@ +r""" +The category of polygons in the real plane with fixed rational +angles. + +This module provides a common structure for all polygons with certain fixed +angles. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for polygons. + +EXAMPLES: + +The category of rectangles:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]) + +It is often tedious to create this category manually, since you need to +determine a base ring that can describe the coordinates of polygons with such +angles:: + + sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1]) + sage: C.slopes() + Traceback (most recent call last): + ... + TypeError: Unable to coerce c to a rational + + sage: C = EuclideanPolygons(AA).WithAngles([1, 1, 1]) + sage: C.slopes() + [(1, 0), (-0.5773502691896258?, 1), (-0.5773502691896258?, -1)] + +Instead, we can use :func:`~.polygon.EuclideanPolygonsWithAngles` to create this category +over a minimal number field:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: C = EuclideanPolygonsWithAngles([1, 1, 1]) + sage: C.slopes() + [(1, 0), (-c, 3), (-c, -3)] + +The category of polygons is automatically determined when using +:func:`~.polygon.Polygon`:: + + sage: from flatsurf import Polygon + sage: p = Polygon(angles=(1, 1, 1)) + sage: p.category() + Category of convex simple euclidean equilateral triangles over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + +However, it can be very costly to determine that a polygon is rational and what +its actual angles are (the "equilateral" in the previous example.) Therefore, +the category might get refined once these aspects have been determined:: + + sage: p = Polygon(edges=[(1, 0), (0, 1), (-1, 0), (0, -1)]) + sage: p.category() + Category of convex simple euclidean polygons over Rational Field + sage: p.is_rational() + True + sage: p.category() + Category of rational convex simple euclidean polygons over Rational Field + sage: p.angles() + (1/4, 1/4, 1/4, 1/4) + sage: p.category() + Category of convex simple euclidean rectangles over Rational Field + +Note that SageMath applies the same strategy when determining whether the +integers modulo N are a field:: + + sage: K = Zmod(1361) + sage: K.category() + Join of Category of finite commutative rings and Category of subquotients of monoids and Category of quotients of semigroups and Category of finite enumerated sets + sage: K.is_field() + True + sage: K.category() + Join of Category of finite enumerated fields and Category of subquotients of monoids and Category of quotients of semigroups + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 Vincent Delecroix +# 2020-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** +from sage.misc.cachefunc import cached_method, cached_function +from sage.categories.category_types import Category_over_base_ring +from sage.categories.category_with_axiom import CategoryWithAxiom_over_base_ring +from flatsurf.geometry.categories.euclidean_polygons import EuclideanPolygons + + +class EuclideanPolygonsWithAngles(Category_over_base_ring): + r""" + The category of euclidean polygons with fixed rational angles. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]) + + TESTS:: + + sage: TestSuite(C).run() + + """ + + def __init__(self, base_ring, angles): + self._angles = angles + + super().__init__(base_ring) + + def super_categories(self): + r""" + Return the other categories such polygons are automatically members of, + namely, the category of rational euclidean polygons polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: C = EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]) + sage: C.super_categories() + [Category of rational euclidean polygons over Rational Field] + + """ + return [EuclideanPolygons(self.base_ring()).Rational()] + + @staticmethod + def _normalize_angles(angles): + r""" + Return ``angles`` normalized such that they sum to n/2-1 where ``n`` is + the number of angles, i.e., they scale to the angle in an n-gon divided + by π. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles._normalize_angles([1, 1, 1, 1]) + (1/4, 1/4, 1/4, 1/4) + sage: EuclideanPolygonsWithAngles._normalize_angles([1, 2, 3, 4]) + (1/10, 1/5, 3/10, 2/5) + sage: EuclideanPolygonsWithAngles._normalize_angles([1, 2, 3]) + (1/12, 1/6, 1/4) + + """ + n = len(angles) + if n < 3: + raise ValueError("there must be at least three angles") + + from sage.all import QQ, ZZ + + angles = [QQ.coerce(a) for a in angles] + if any(angle <= 0 for angle in angles): + raise ValueError("angles must be positive rationals") + + # Store each angle as a multiple of 2π, i.e., normalize them such their sum is (n - 2)/2. + angles = [a / sum(angles) for a in angles] + angles = [a * ZZ(n - 2) / 2 for a in angles] + if any(angle <= 0 or angle >= 1 for angle in angles): + raise NotImplementedError("each angle must be in (0, 2π)") + + angles = tuple(angles) + + return angles + + @cached_method + def _slopes(self): + slopes = _slopes(self._angles) + + # We bring the slopes first into the minimal number field in which they + # are defined since otherwise conversion from the cosines ring to the + # base_ring might fail. E.g., when the base ring is the exact-reals + # over the minimal base ring. + minimal_base_ring = _base_ring(self._angles) + slopes = [ + (minimal_base_ring(slope[0]), minimal_base_ring(slope[1])) + for slope in slopes + ] + return [ + (self.base_ring()(slope[0]), self.base_ring()(slope[1])) for slope in slopes + ] + + @cached_method + def _cosines_ring(self): + slopes = _slopes(self._angles) + return slopes[0][0].parent() + + def _repr_object_names(self): + r""" + Helper method to create the name of this category. + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles([1/6, 1/6, 1/6]) + Category of simple euclidean equilateral triangles over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + sage: EuclideanPolygonsWithAngles([1/4, 1/4, 1/4, 1/4]) + Category of simple euclidean rectangles over Rational Field + sage: EuclideanPolygonsWithAngles([1/10, 2/10, 3/10, 4/10]) + Category of simple euclidean quadrilaterals with angles (1/10, 1/5, 3/10, 2/5) over Number Field in c with defining polynomial x^4 - 5*x^2 + 5 with c = 1.902113032590308? + + """ + names = super()._repr_object_names() + + equiangular = len(set(self._angles)) == 1 + + from flatsurf.geometry.categories.polygons import Polygons + + _, _, polygons = Polygons._describe_polygon( + len(self._angles), equiangular=equiangular + ) + + with_angles = "" if equiangular else f" with angles {self.angles(False)}" + + return names.replace(" with angles", with_angles).replace("polygons", polygons) + + class ParentMethods: + r""" + Provides methods available to all polygons with known angles. + + If you want to add functionality to all such polygons, you probably + want to put it here. + """ + + def is_convex(self, strict=False): + r""" + Return whether this is a convex polygon. + + INPUT: + + - ``strict`` -- whether to check for strict convexity, i.e., a + polygon with a π angle is not considered convex. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.is_convex() + True + sage: S.is_convex(strict=True) + True + + """ + return self.category().is_convex() + + def angle(self, e, numerical=None, assume_rational=None): + r""" + Return the angle at the beginning of the start point of the edge ``e``. + + EXAMPLES:: + + sage: from flatsurf.geometry.polygon import polygons + sage: polygons.square().angle(0) + 1/4 + sage: polygons.regular_ngon(8).angle(0) + 3/8 + + sage: from flatsurf import Polygon + sage: T = Polygon(vertices=[(0,0), (3,1), (1,5)]) + sage: [T.angle(i, numerical=True) for i in range(3)] # abs tol 1e-13 + [0.16737532973071603, 0.22741638234956674, 0.10520828791971722] + sage: sum(T.angle(i, numerical=True) for i in range(3)) # abs tol 1e-13 + 0.5 + + """ + if assume_rational is not None: + import warnings + + warnings.warn( + "assume_rational has been deprecated as a keyword to angle() and will be removed from a future version of sage-flatsurf" + ) + + if numerical is None: + numerical = not self.is_rational() + + if numerical: + import warnings + + warnings.warn( + "the behavior of angle() has been changed in recent versions of sage-flatsurf; for non-rational polygons, numerical=True must be set explicitly to get a numerical approximation of the angle" + ) + + angle = self.category().angles()[e] + if numerical: + from sage.all import RR + + angle = RR(angle) + + return angle + + class SubcategoryMethods: + def is_convex(self, strict=False): + r""" + Return whether the polygons in this category are convex. + + INPUT: + + - ``strict`` -- a boolean (default: ``False``); whether only to + consider polygons convex if all angles are <π. + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles(1, 2, 5).is_convex() + True + sage: EuclideanPolygonsWithAngles(2, 2, 3, 13).is_convex() + False + + :: + + sage: E = EuclideanPolygonsWithAngles([1, 1, 1, 1, 2]) + sage: E.angles() + (1/4, 1/4, 1/4, 1/4, 1/2) + sage: E.is_convex(strict=False) + True + sage: E.is_convex(strict=True) + False + + """ + if strict: + return all(2 * a < 1 for a in self.angles()) + + return all(2 * a <= 1 for a in self.angles()) + + def convexity(self): + r""" + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles(1, 2, 5).convexity() + doctest:warning + ... + UserWarning: convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex() instead + True + sage: EuclideanPolygonsWithAngles(2, 2, 3, 13).convexity() + False + + """ + import warnings + + warnings.warn( + "convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex() instead" + ) + + return self.is_convex() + + def strict_convexity(self): + r""" + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: E = EuclideanPolygonsWithAngles([1, 1, 1, 1, 2]) + sage: E.angles() + (1/4, 1/4, 1/4, 1/4, 1/2) + sage: E.convexity() + True + sage: E.strict_convexity() + doctest:warning + ... + UserWarning: strict_convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex(strict=True) instead + False + + """ + import warnings + + warnings.warn( + "strict_convexity() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex(strict=True) instead" + ) + + return self.is_convex(strict=True) + + def angles(self, integral=False): + r""" + Return the interior angles of this polygon as multiples of 2π. + + INPUT: + + - ``integral`` -- a boolean (default: ``False``); whether to return + the angles not as multiples of 2π but rescaled so that they have + no denominators. + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: E = EuclideanPolygonsWithAngles(1, 1, 1, 2, 6) + sage: E.angles() + (3/22, 3/22, 3/22, 3/11, 9/11) + + When ``integral`` is set, the output is scaled to eliminate + denominators:: + + sage: E.angles(integral=True) + (1, 1, 1, 2, 6) + + """ + angles = self.__angles() + if integral: + from sage.all import lcm, ZZ, gcd + + C = lcm([a.denominator() for a in self.angles()]) / gcd( + [a.numerator() for a in self.angles()] + ) + angles = tuple(ZZ(C * a) for a in angles) + return angles + + @cached_method + def __angles(self): + r""" + Helper method for :meth:`angles` to lookup the stored angles if + this is a subcategory of :class:`EuclideanPolygonsWithAngles`. + """ + if isinstance(self, EuclideanPolygonsWithAngles): + return self._angles + + for category in self.all_super_categories(): + if isinstance(category, EuclideanPolygonsWithAngles): + return category._angles + + assert ( + False + ), "EuclideanPolygonsWithAngles should be a supercategory of this category" + + def slopes(self, e0=(1, 0)): + r""" + Return the slopes of the edges as a list of vectors. + + INPUT: + + - ``e0`` -- the first slope returned (default: ``(1, 0)``) + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles(1, 2, 1, 2).slopes() + [(1, 0), (c, 3), (-1, 0), (-c, -3)] + + """ + V = self.base_ring() ** 2 + slopes = self.__slopes() + n = len(slopes) + cosines = [x[0] for x in slopes] + sines = [x[1] for x in slopes] + e = V(e0) + edges = [e] + for i in range(n - 1): + e = ( + -cosines[i + 1] * e[0] - sines[i + 1] * e[1], + sines[i + 1] * e[0] - cosines[i + 1] * e[1], + ) + from flatsurf.geometry.euclidean import projectivization + + e = projectivization(*e) + edges.append(V(e)) + return edges + + @cached_method + def __slopes(self): + r""" + Helper method for :meth:`slopes` to lookup the stored slopes if + this is a subcategory of :class:`EuclideanPolygonsWithAngles`. + """ + if isinstance(self, EuclideanPolygonsWithAngles): + return self._slopes() + + for category in self.all_super_categories(): + if isinstance(category, EuclideanPolygonsWithAngles): + return category._slopes() + + assert ( + False + ), "EuclideanPolygonsWithAngles should be a supercategory of this category" + + # TODO: rather than lengths, it would be more convenient to have access + # to the tangent space (that is the space of possible holonomies). However, + # since it is not defined over the real numbers, there are several possible ways + # to handle the data. + # TODO: here we ignored the direction SO(2) which provides additional symmetry + # in the tangent space + @cached_method + def lengths_polytope(self): + r""" + Return the polytope parametrizing the admissible vectors data. + + This polytope parametrizes the tangent space to the set of these + equiangular polygons. Be careful that even though the lengths are + admissible, they may not define a polygon without intersection. + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles(1, 2, 1, 2).lengths_polytope() + A 2-dimensional polyhedron in (Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878?)^4 defined as the convex hull of 1 vertex and 2 rays + """ + n = len(self.angles()) + slopes = self.slopes() + eqns = [[0] + [s[0] for s in slopes], [0] + [s[1] for s in slopes]] + ieqs = [] + for i in range(n): + ieq = [0] * (n + 1) + ieq[i + 1] = 1 + ieqs.append(ieq) + + from sage.geometry.polyhedron.constructor import Polyhedron + + return Polyhedron(eqns=eqns, ieqs=ieqs, base_ring=self.base_ring()) + + def an_element(self): + r""" + Return a polygon in this category. + + Since currently polygons must not be self-intersecting, the + construction used might fail. + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles(4, 3, 4, 4, 3, 4).an_element() + Polygon(vertices=[(0, 0), + (1/22*c + 1, 0), + (9*c^9 + 1/2*c^8 - 88*c^7 - 9/2*c^6 + 297*c^5 + 27/2*c^4 - 396*c^3 - 15*c^2 + 3631/22*c + 11/2, 1/2*c + 11), + (16*c^9 + c^8 - 154*c^7 - 9*c^6 + 506*c^5 + 27*c^4 - 638*c^3 - 30*c^2 + 4841/22*c + 9, c + 22), + (16*c^9 + c^8 - 154*c^7 - 9*c^6 + 506*c^5 + 27*c^4 - 638*c^3 - 30*c^2 + 220*c + 8, c + 22), + (7*c^9 + 1/2*c^8 - 66*c^7 - 9/2*c^6 + 209*c^5 + 27/2*c^4 - 242*c^3 - 15*c^2 + 55*c + 7/2, 1/2*c + 11)]) + """ + from flatsurf import Polygon + + p = Polygon(angles=self.angles()) + + if p not in self: # pylint: disable=unsupported-membership-test + raise NotImplementedError( + "cannot create an element in this category yet" + ) + + return p + + def random_element(self, ring=None, **kwds): + r""" + Return a random polygon in this category. + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: EuclideanPolygonsWithAngles(1, 1, 1, 2, 5).random_element() + Polygon(vertices=[(0, 0), ...]) + sage: EuclideanPolygonsWithAngles(1,1,1,15,15,15).random_element() + Polygon(vertices=[(0, 0), ...]) + sage: EuclideanPolygonsWithAngles(1,15,1,15,1,15).random_element() + Polygon(vertices=[(0, 0), ...]) + + """ + if ring is None: + from sage.all import QQ + + ring = QQ + + rays = [r.vector() for r in self.lengths_polytope().rays()] + + def random_element(): + while True: + coeffs = [] + while len(coeffs) < len(rays): + x = ring.random_element(**kwds) + while x < 0: + x = ring.random_element(**kwds) + coeffs.append(x) + + sol = sum(c * r for c, r in zip(coeffs, rays)) + if all(x > 0 for x in sol): + return coeffs, sol + + while True: + coeffs, lengths = random_element() + edges = [ + length * slope for (length, slope) in zip(lengths, self.slopes()) + ] + + from flatsurf import Polygon + + p = Polygon(edges=edges, check=False) + + from flatsurf.geometry.categories import EuclideanPolygons + + if not EuclideanPolygons.ParentMethods.is_simple(p): + continue + + p = Polygon(edges=edges, angles=self.angles(), check=False) + break + + if p not in self: # pylint: disable=unsupported-membership-test + raise NotImplementedError( + "cannot create a random element in this category yet" + ) + + return p + + def billiard_unfolding_angles(self, cover_type="translation"): + r""" + Return the angles of the unfolding rational, half-translation or translation surface. + + INPUT: + + - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, + ``"half-translation"`` or ``"translation"`` + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + + sage: E = EuclideanPolygonsWithAngles(1, 2, 5) + sage: E.billiard_unfolding_angles(cover_type="rational") + {1/8: 1, 1/4: 1, 5/8: 1} + sage: (1/8 - 1) + (1/4 - 1) + (5/8 - 1) # Euler characteristic (of the sphere) + -2 + sage: E.billiard_unfolding_angles(cover_type="half-translation") + {1/2: 3, 5/2: 1} + sage: E.billiard_unfolding_angles(cover_type="translation") + {1: 3, 5: 1} + + sage: E = EuclideanPolygonsWithAngles(1, 3, 1, 7) + sage: E.billiard_unfolding_angles(cover_type="rational") + {1/6: 2, 1/2: 1, 7/6: 1} + sage: 2 * (1/6 - 1) + (1/2 - 1) + (7/6 - 1) # Euler characteristic + -2 + sage: E.billiard_unfolding_angles(cover_type="half-translation") + {1/2: 5, 7/2: 1} + sage: E.billiard_unfolding_angles(cover_type="translation") + {1: 5, 7: 1} + + sage: E = EuclideanPolygonsWithAngles(1, 3, 5, 7) + sage: E.billiard_unfolding_angles(cover_type="rational") + {1/8: 1, 3/8: 1, 5/8: 1, 7/8: 1} + sage: (1/8 - 1) + (3/8 - 1) + (5/8 - 1) + (7/8 - 1) # Euler characteristic + -2 + sage: E.billiard_unfolding_angles(cover_type="half-translation") + {1/2: 1, 3/2: 1, 5/2: 1, 7/2: 1} + sage: E.billiard_unfolding_angles(cover_type="translation") + {1: 1, 3: 1, 5: 1, 7: 1} + + sage: E = EuclideanPolygonsWithAngles(1, 2, 8) + sage: E.billiard_unfolding_angles(cover_type="rational") + {1/11: 1, 2/11: 1, 8/11: 1} + sage: (1/11 - 1) + (2/11 - 1) + (8/11 - 1) # Euler characteristic + -2 + sage: E.billiard_unfolding_angles(cover_type="half-translation") + {1: 1, 2: 1, 8: 1} + sage: E.billiard_unfolding_angles(cover_type="translation") + {1: 1, 2: 1, 8: 1} + """ + rat_angles = {} + for a in self.angles(): + if 2 * a in rat_angles: + rat_angles[2 * a] += 1 + else: + rat_angles[2 * a] = 1 + if cover_type == "rational": + return rat_angles + + from sage.all import lcm + + N = lcm([x.denominator() for x in rat_angles]) + if N % 2: + N *= 2 + + cov_angles = {} + for x, mult in rat_angles.items(): + y = x.numerator() + d = x.denominator() + if d % 2: + d *= 2 + else: + y = y / 2 + assert N % d == 0 + if y in cov_angles: + cov_angles[y] += mult * N // d + else: + cov_angles[y] = mult * N // d + + if cover_type == "translation" and any( + y.denominator() == 2 for y in cov_angles + ): + covcov_angles = {} + for y, mult in cov_angles.items(): + yy = y.numerator() + if yy not in covcov_angles: + covcov_angles[yy] = 0 + covcov_angles[yy] += 2 // y.denominator() * mult + return covcov_angles + elif cover_type == "half-translation" or cover_type == "translation": + return cov_angles + else: + raise ValueError("unknown 'cover_type' {!r}".format(cover_type)) + + def billiard_unfolding_stratum( + self, cover_type="translation", marked_points=False + ): + r""" + Return the stratum of quadratic or Abelian differential obtained by + unfolding a billiard in a polygon of this equiangular family. + + INPUT: + + - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, + ``"half-translation"`` or ``"translation"`` + + - ``marked_poins`` (optional, default ``False``) - whether the stratum should + have regular marked points + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles, similarity_surfaces + + sage: E = EuclideanPolygonsWithAngles(1, 2, 5) + sage: E.billiard_unfolding_stratum("half-translation") + Q_1(3, -1^3) + sage: E.billiard_unfolding_stratum("translation") + H_3(4) + sage: E.billiard_unfolding_stratum("half-translation", True) + Q_1(3, -1^3) + sage: E.billiard_unfolding_stratum("translation", True) + H_3(4, 0^3) + + sage: E = EuclideanPolygonsWithAngles(1, 3, 1, 7) + sage: E.billiard_unfolding_stratum("half-translation") + Q_1(5, -1^5) + sage: E.billiard_unfolding_stratum("translation") + H_4(6) + sage: E.billiard_unfolding_stratum("half-translation", True) + Q_1(5, -1^5) + sage: E.billiard_unfolding_stratum("translation", True) + H_4(6, 0^5) + + sage: P = E.an_element() + sage: S = similarity_surfaces.billiard(P) + sage: S.minimal_cover("half-translation").stratum() + Q_1(5, -1^5) + sage: S.minimal_cover("translation").stratum() + H_4(6, 0^5) + + sage: E = EuclideanPolygonsWithAngles(1, 3, 5, 7) + sage: E.billiard_unfolding_stratum("half-translation") + Q_3(5, 3, 1, -1) + sage: E.billiard_unfolding_stratum("translation") + H_7(6, 4, 2) + + sage: P = E.an_element() + sage: S = similarity_surfaces.billiard(P) + sage: S.minimal_cover("half-translation").stratum() + Q_3(5, 3, 1, -1) + sage: S.minimal_cover("translation").stratum() + H_7(6, 4, 2, 0) + + sage: E = EuclideanPolygonsWithAngles(1, 2, 8) + sage: E.billiard_unfolding_stratum("half-translation") + H_5(7, 1) + sage: E.billiard_unfolding_stratum("translation") + H_5(7, 1) + + sage: E.billiard_unfolding_stratum("half-translation", True) + H_5(7, 1, 0) + sage: E.billiard_unfolding_stratum("translation", True) + H_5(7, 1, 0) + + sage: E = EuclideanPolygonsWithAngles(9, 6, 3, 2) + sage: p = E.an_element() + sage: B = similarity_surfaces.billiard(p) + sage: B.minimal_cover("half-translation").stratum() + Q_4(7, 4, 1, 0) + sage: E.billiard_unfolding_stratum("half-translation", True) + Q_4(7, 4, 1, 0) + sage: B.minimal_cover("translation").stratum() + H_8(8, 2^3, 0^2) + sage: E.billiard_unfolding_stratum("translation", True) + H_8(8, 2^3, 0^2) + """ + from sage.all import ZZ + + angles = self.billiard_unfolding_angles(cover_type) + if all(a.is_integer() for a in angles): + from surface_dynamics import AbelianStratum + + if not marked_points and len(angles) == 1 and 1 in angles: + return AbelianStratum([0]) + else: + return AbelianStratum( + { + ZZ(a - 1): mult + for a, mult in angles.items() + if marked_points or a != 1 + } + ) + else: + from surface_dynamics import QuadraticStratum + + return QuadraticStratum( + { + ZZ(2 * (a - 1)): mult + for a, mult in angles.items() + if marked_points or a != 1 + } + ) + + def billiard_unfolding_stratum_dimension( + self, cover_type="translation", marked_points=False + ): + r""" + Return the dimension of the stratum of quadratic or Abelian differential + obtained by unfolding a billiard in a polygon of this equiangular family. + + INPUT: + + - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, + ``"half-translation"`` or ``"translation"`` + + - ``marked_poins`` (optional, default ``False``) - whether the stratum should + have marked regular points + + EXAMPLES:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + + sage: E = EuclideanPolygonsWithAngles(1, 1, 1) + sage: E.billiard_unfolding_stratum_dimension("half-translation") + 2 + sage: E.billiard_unfolding_stratum_dimension("translation") + 2 + sage: E.billiard_unfolding_stratum_dimension("half-translation", True) + 4 + sage: E.billiard_unfolding_stratum_dimension("translation", True) + 4 + + sage: E = EuclideanPolygonsWithAngles(1, 2, 5) + sage: E.billiard_unfolding_stratum_dimension("half-translation") + 4 + sage: E.billiard_unfolding_stratum("half-translation").dimension() + 4 + sage: E.billiard_unfolding_stratum_dimension(cover_type="translation") + 6 + sage: E.billiard_unfolding_stratum("translation").dimension() + 6 + sage: E.billiard_unfolding_stratum_dimension("translation", True) + 9 + sage: E.billiard_unfolding_stratum("translation", True).dimension() + 9 + + sage: E = EuclideanPolygonsWithAngles(1, 3, 5) + sage: E.billiard_unfolding_stratum_dimension("half-translation") + 6 + sage: E.billiard_unfolding_stratum("half-translation").dimension() + 6 + sage: E.billiard_unfolding_stratum_dimension("translation") + 6 + sage: E.billiard_unfolding_stratum("translation").dimension() + 6 + + sage: E = EuclideanPolygonsWithAngles(1, 3, 1, 7) + sage: E.billiard_unfolding_stratum_dimension("half-translation") + 6 + + sage: E = EuclideanPolygonsWithAngles(1, 3, 5, 7) + sage: E.billiard_unfolding_stratum_dimension("half-translation") + 8 + + sage: E = EuclideanPolygonsWithAngles(1, 2, 8) + sage: E.billiard_unfolding_stratum_dimension() + 11 + sage: E.billiard_unfolding_stratum().dimension() + 11 + sage: E.billiard_unfolding_stratum_dimension(marked_points=True) + 12 + sage: E.billiard_unfolding_stratum(marked_points=True).dimension() + 12 + """ + from sage.all import ZZ + + if cover_type == "rational": + raise NotImplementedError + if cover_type != "translation" and cover_type != "half-translation": + raise ValueError + + angles = self.billiard_unfolding_angles(cover_type) + if not marked_points: + if 1 in angles: + del angles[1] + if not angles: + angles[ZZ.one()] = ZZ.one() + + abelian = all(a.is_integer() for a in angles) + s = sum(angles.values()) + chi = sum(mult * (a - 1) for a, mult in angles.items()) + assert chi.denominator() == 1 + chi = ZZ(chi) + assert chi % 2 == 0 + g = chi // 2 + 1 + return 2 * g + s - 1 if abelian else 2 * g + s - 2 + + class Simple(CategoryWithAxiom_over_base_ring): + r""" + The subcategory of simple Euclidean polygons with prescribed angles. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: EuclideanPolygons(QQ).WithAngles([1, 1, 1, 1]).Simple() + Category of simple euclidean rectangles over Rational Field + + """ + + # Due to some limitations in SageMath this does not work. Apparently, + # extra_super_categories cannot depend on parameters of the category. + # With this code, some polygons are randomly declared as convex. + # Unfortunately, this means that the category prints as "convex simple + # rectangles" and not just "rectangles". + # def extra_super_categories(self): + # r""" + # Return the categories that simple polygons with prescribed angles + # are additionally contained in; namely, in some cases the category + # of convex polygons. + + # EXAMPLES:: + + # sage: from flatsurf.geometry.categories import EuclideanPolygons + # sage: C = EuclideanPolygons(QQ).Simple().WithAngles([1, 1, 1, 1]) + # sage: "Convex" in C.axioms() + # True + + # sage: C = EuclideanPolygons(QQ).Simple().WithAngles([2, 2, 1, 6, 1]) + # sage: "Convex" in C.axioms() + # False + + # """ + # # We cannot call is_convex() yet because the SubcategoryMethods + # # have not been established yet. + # if self._base_category.is_convex(): + # return (self._base_category.Convex(),) + + # return () + + def __call__(self, *lengths, normalized=False, base_ring=None): + r""" + Return a polygon with these angles from ``lengths``. + + TESTS:: + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: P = EuclideanPolygonsWithAngles(1, 2, 1, 2) + sage: L = P.lengths_polytope() + sage: r0, r1 = [r.vector() for r in L.rays()] + sage: lengths = r0 + r1 + sage: P(*lengths[:-2]) + doctest:warning + ... + UserWarning: calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead. + To make the resulting polygon non-normalized, i.e., the lengths are not actual edge lengths but the multiple of slope vectors, + use Polygon(edges=[length * slope for (length, slope) in zip(lengths, EuclideanPolygonsWithAngles(angles).slopes())]). + Polygon(vertices=[(0, 0), (1, 0), (c + 1, 3), (c, 3)]) + + sage: from flatsurf import Polygon, EuclideanPolygonsWithAngles + sage: P = EuclideanPolygonsWithAngles([1, 2, 1, 2]) + sage: Polygon(angles=[1, 2, 1, 2], lengths=lengths[:-2]) + Polygon(vertices=[(0, 0), (1, 0), (3/2, 1/2*c), (1/2, 1/2*c)]) + sage: Polygon(angles=[1, 2, 1, 2], edges=[length * slope for (length, slope) in zip(lengths[:-2], P.slopes())]) + Polygon(vertices=[(0, 0), (1, 0), (c + 1, 3), (c, 3)]) + + sage: P = EuclideanPolygonsWithAngles(2, 2, 3, 13) + sage: r0, r1 = [r.vector() for r in P.lengths_polytope().rays()] + sage: P(r0 + r1) + Polygon(vertices=[(0, 0), (20, 0), (5, -15*c^3 + 60*c), (5, -5*c^3 + 20*c)]) + + sage: P = EuclideanPolygonsWithAngles([2, 2, 3, 13]) + sage: Polygon(angles=[2, 2, 3, 13], lengths=r0 + r1) + Traceback (most recent call last): + ... + ValueError: polygon not closed + sage: Polygon(angles=[2, 2, 3, 13], edges=[length * slope for (length, slope) in zip(r0 + r1, P.slopes())]) + Polygon(vertices=[(0, 0), (20, 0), (5, -15*c^3 + 60*c), (5, -5*c^3 + 20*c)]) + + """ + # __call__() cannot be properly inherited in subcategories since it + # cannot be in SubcategoryMethods; that's why we want to get rid of it. + import warnings + + warning = "calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead." + + if not normalized: + warning += ( + " To make the resulting polygon non-normalized, i.e., the lengths are not actual edge lengths but the multiple of slope vectors, use " + "Polygon(edges=[length * slope for (length, slope) in zip(lengths, EuclideanPolygonsWithAngles(angles).slopes())])." + ) + + warnings.warn(warning) + + from sage.structure.element import Vector + + if len(lengths) == 1 and isinstance(lengths[0], (tuple, list, Vector)): + lengths = lengths[0] + + n = len(self.angles()) + if len(lengths) != n - 2 and len(lengths) != n: + raise ValueError( + "must provide %d or %d lengths but provided %d" + % (n - 2, n, len(lengths)) + ) + + V = self.base_ring() ** 2 + slopes = self.slopes() + if normalized: + cosines_ring = ( + self._without_axiom("Simple") + ._without_axiom("Convex") + ._cosines_ring() + ) + V = V.change_ring(cosines_ring) + for i, s in enumerate(slopes): + x, y = map(cosines_ring, s) + norm2 = (x**2 + y**2).sqrt() + slopes[i] = V((x / norm2, y / norm2)) + + if base_ring is None: + from sage.all import Sequence + + base_ring = Sequence(lengths).universe() + + from sage.categories.pushout import pushout + + if normalized: + base_ring = pushout(base_ring, cosines_ring) + else: + base_ring = pushout(base_ring, self.base_ring()) + + v = V((0, 0)) + vertices = [v] + + from sage.all import vector, matrix + + if len(lengths) == n - 2: + for i in range(n - 2): + v += lengths[i] * slopes[i] + vertices.append(v) + s, t = ( + vector(vertices[0] - vertices[n - 2]) + * matrix([slopes[-1], slopes[n - 2]]).inverse() + ) + assert ( + vertices[0] - s * slopes[-1] == vertices[n - 2] + t * slopes[n - 2] + ) + if s <= 0 or t <= 0: + raise ValueError( + "the provided lengths do not give rise to a polygon" + ) + vertices.append(vertices[0] - s * slopes[-1]) + + elif len(lengths) == n: + for i in range(n): + v += lengths[i] * slopes[i] + vertices.append(v) + if not vertices[-1].is_zero(): + raise ValueError( + "the provided lengths do not give rise to a polygon" + ) + vertices.pop(-1) + + category = self.change_ring(base_ring) + if self.is_convex(): + category = category.Convex() + + from flatsurf.geometry.polygon import Polygon + + return Polygon(base_ring=base_ring, vertices=vertices, category=category) + + +@cached_function +def _slopes(angles): + r""" + Return the slopes of the sides of a polygon with ``angles`` in a + (possibly non-minimal) number field. + + .. NOTE:: + + This function gets called a lot from the other functions here. We + could refactor things and make sure that the function is only + invoked once. However, it seems easier to just cache its output. + Also, this speeds up the relative common case when multiple + polygons with the same angles are created. (If lots of polygons + with different angles get created then we pay this cache with RAM + of course but in our experiments it has not been an issue.) + + .. NOTE:: + + SageMath 9.1 does not support cached functions that are staticmethods. + Therefore we define this function at the module level. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories.euclidean_polygons_with_angles import _slopes + sage: _slopes((1/6, 1/6, 1/6)) + [(c, 3), (c, 3), (c, 3)] + sage: _slopes((1/4, 1/4, 1/4, 1/4)) + [(0, 1), (0, 1), (0, 1), (0, 1)] + sage: _slopes((1/10, 2/10, 3/10, 4/10)) + [(c^3, 5), (3*c^3 - 10*c, 5), (-3*c^3 + 10*c, 5), (-c^3, 5)] + + """ + from sage.all import QQ, RIF, lcm, AA, NumberField + from flatsurf.geometry.subfield import chebyshev_T, cos_minpoly + + # We determine the number field that contains the slopes of the sides, + # i.e., the cosines and sines of the inner angles of the polygon. + # Let us first write all angles as multiples of 2π/N with the smallest + # possible common N. + N = lcm(a.denominator() for a in angles) + # The field containing the cosine and sine of 2π/N might be too small + # to write down all the slopes when N is not divisible by 4. + if N == 1: + raise ValueError("there cannot be a polygon with all angles multiples of 2π") + if N == 2: + pass + elif N % 4: + while N % 4: + N *= 2 + + angles = [QQ(a * N) for a in angles] + + if N == 2: + base_ring = QQ + c = QQ.zero() + else: + # Construct the minimal polynomial f(x) of c = 2 cos(2π / N) + f = cos_minpoly(N // 2) + emb = AA.polynomial_root(f, 2 * (2 * RIF.pi() / N).cos()) + base_ring = NumberField(f, "c", embedding=emb) + c = base_ring.gen() + + # Construct the cosine and sine of each angle as an element of our number field. + def cosine(a): + return chebyshev_T(abs(a), c) / 2 + + def sine(a): + # Use sin(x) = cos(π/2 - x) + return cosine(N // 4 - a) + + slopes = [(cosine(a), sine(a)) for a in angles] + + assert all((x**2 + y**2).is_one() for x, y in slopes) + + from flatsurf.geometry.euclidean import projectivization + + return [projectivization(x, y) for x, y in slopes] + + +@cached_function +def _base_ring(angles): + r""" + Return a minimal number field containing all the :meth:`_slopes` of a + polygon with ``angles``. + + .. NOTE:: + + Internally, this uses + :func:`~flatsurf.geometry.subfield.subfield_from_element` which is + very slow. We therefore cache the result currently. + + .. NOTE:: + + SageMath 9.1 does not support cached functions that are staticmethods. + Therefore we define this function at the module level. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories.euclidean_polygons_with_angles import _base_ring + sage: _base_ring((1/6, 1/6, 1/6)) + Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + sage: _base_ring((1/4, 1/4, 1/4, 1/4)) + Rational Field + sage: _base_ring((1/10, 2/10, 3/10, 4/10)) + Number Field in c with defining polynomial x^4 - 5*x^2 + 5 with c = 1.902113032590308? + + """ + slopes = _slopes(angles) + base_ring = slopes[0][0].parent() + + # It might be the case that the slopes generate a smaller field. For + # now we use an ugly workaround via subfield_from_elements. + old_slopes = [] + for v in slopes: + old_slopes.extend(v) + from flatsurf.geometry.subfield import subfield_from_elements + + L, _, _ = subfield_from_elements(base_ring, old_slopes) + return L diff --git a/flatsurf/geometry/categories/half_translation_surfaces.py b/flatsurf/geometry/categories/half_translation_surfaces.py new file mode 100644 index 000000000..e74cabbba --- /dev/null +++ b/flatsurf/geometry/categories/half_translation_surfaces.py @@ -0,0 +1,564 @@ +r""" +The category of half-translation surfaces. + +A half-translation surface is a surface built by gluing Euclidean polygons. The +sides of the polygons can be glued with translations or half-translations +(translation followed by a rotation of angle π.) + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES: + +We glue all the sides of a square to themselves. Since each gluing is just a +rotation of π, this is a half-translation surface:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.set_immutable() + + sage: C = S.category() + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: C.is_subcategory(HalfTranslationSurfaces()) + True + +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +from flatsurf.geometry.categories.surface_category import ( + SurfaceCategory, + SurfaceCategoryWithAxiom, +) +from sage.misc.lazy_import import LazyImport +from sage.all import QQ, AA + + +class HalfTranslationSurfaces(SurfaceCategory): + r""" + The category of surfaces built by gluing (Euclidean) polygons with + translations and half-translations (translations followed by rotations + among an angle π.) + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: HalfTranslationSurfaces() + Category of half translation surfaces + + """ + + def super_categories(self): + r""" + Return the categories that a half-translation surface is always a + member of. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: HalfTranslationSurfaces().super_categories() + [Category of dilation surfaces, Category of rational cone surfaces] + + """ + from flatsurf.geometry.categories.dilation_surfaces import DilationSurfaces + from flatsurf.geometry.categories.cone_surfaces import ConeSurfaces + + return [DilationSurfaces(), ConeSurfaces().Rational()] + + # Declare that the "positive" half-translation surfaces are called + # "translation surfaces". + Positive = LazyImport( + "flatsurf.geometry.categories.translation_surfaces", "TranslationSurfaces" + ) + + class ParentMethods: + r""" + Provides methods available to all half-translation surfaces. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def is_translation_surface(self, positive=True): + r""" + Return whether this surface is a (half-)translation surface. + + This overrides + :meth:`.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_translation_surface`. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + + sage: H.is_translation_surface(positive=False) + True + sage: H.is_translation_surface(positive=True) + False + + """ + if not positive: + return True + + # If this is not explicitly a translation surface, we have to + # decide with the generic checks whether it is a positive + # half-translation surface. + return super( # pylint: disable=bad-super-call + HalfTranslationSurfaces().parent_class, self + ).is_translation_surface(positive=positive) + + class Orientable(SurfaceCategoryWithAxiom): + r""" + The category of orientable half-translation surfaces. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: H in HalfTranslationSurfaces().Orientable() + True + + """ + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The category of orientable half-translation surfaces without boundary. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: H in HalfTranslationSurfaces().Orientable().WithoutBoundary() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all orientable half-translation + surfaces. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def stratum(self): + r""" + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + sage: H.stratum() + Q_1(3, -1^3) + + TESTS: + + Verify that the stratum is correct for surfaces with self-glued edges:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.stratum() + Q_0(0, -1^4) + + """ + angles = self.angles() + + for a, b in self.gluings(): + if a == b: + angles.append(QQ(1 / 2)) + + if all(x.denominator() == 1 for x in angles): + raise NotImplementedError + + from surface_dynamics import QuadraticStratum + + return QuadraticStratum(*[2 * a - 2 for a in angles]) + + class Oriented(SurfaceCategoryWithAxiom): + r""" + The category of oriented half-translation surfaces, i.e., orientable + half-translation surfaces which can be oriented in a way compatible + with the embedding of their polygons in the real plane. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: H in HalfTranslationSurfaces().Oriented() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented half-translation + surfaces. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def holonomy_field(self): + r""" + Return the relative holonomy field of this translation or half-translation surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces, polygons, similarity_surfaces + + sage: S = translation_surfaces.veech_2n_gon(5) + sage: S.holonomy_field() + Number Field in a0 with defining polynomial x^2 - x - 1 with a0 = ... + sage: S.base_ring() + Number Field in a with defining polynomial y^4 - 5*y^2 + 5 with a = 1.175570504584947? + + sage: T = translation_surfaces.torus((1, AA(2).sqrt()), (AA(3).sqrt(), 3)) + sage: T.holonomy_field() + Rational Field + + sage: T = polygons.triangle(1,6,11) + sage: S = similarity_surfaces.billiard(T) + sage: S = S.minimal_cover("translation") + sage: S.base_ring() + Number Field in c with defining polynomial x^6 - 6*x^4 + 9*x^2 - 3 with c = 1.969615506024417? + sage: S.holonomy_field() + Number Field in c0 with defining polynomial x^3 - 3*x - 1 with c0 = 1.879385241571817? + """ + return self.normalized_coordinates()[0].base_ring() + + def _test_half_translation_surface(self, **options): + r""" + Verify that this is a half-translation surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_half_translation_surface() + + """ + tester = self._tester(**options) + + limit = None + + if not self.is_finite_type(): + limit = 32 + + from flatsurf.geometry.categories import TranslationSurfaces + + tester.assertTrue( + TranslationSurfaces.ParentMethods._is_translation_surface( + self, positive=False, limit=limit + ) + ) + + class FiniteType(SurfaceCategoryWithAxiom): + r""" + The category of oriented half-translation surfaces built from + finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: H in HalfTranslationSurfaces().Oriented().FiniteType() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented half-translation + surfaces built from finitely many polygons. + + If you want to add functionality for such surfaces you most + likely want to put it here. + """ + + def normalized_coordinates(self): + r""" + Return a pair ``(new_surface, matrix)`` where ``new_surface`` is defined over the + holonomy field and ``matrix`` is the transition matrix that maps this surface to + ``new_surface``. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces, polygons, similarity_surfaces + + sage: S = translation_surfaces.veech_2n_gon(5) + sage: U, mat = S.normalized_coordinates() + sage: U.base_ring() + Number Field in a0 with defining polynomial x^2 - x - 1 with a0 = ... + sage: mat + [ 0 -2/5*a^3 + 2*a] + [ -1 -3/5*a^3 + 2*a] + + sage: T = translation_surfaces.torus((1, AA(2).sqrt()), (AA(3).sqrt(), 3)) + sage: U, mat = T.normalized_coordinates() + sage: U.base_ring() + Rational Field + sage: U.holonomy_field() + Rational Field + sage: mat + [-2.568914100752347? 1.816496580927726?] + [-5.449489742783178? 3.146264369941973?] + sage: TestSuite(U).run() + + sage: T = polygons.triangle(1,6,11) + sage: S = similarity_surfaces.billiard(T) + sage: S = S.minimal_cover("translation") + sage: U, _ = S.normalized_coordinates() + sage: U.base_ring() + Number Field in c0 with defining polynomial x^3 - 3*x - 1 with c0 = 1.879385241571817? + sage: U.holonomy_field() == U.base_ring() + True + sage: S.base_ring() + Number Field in c with defining polynomial x^6 - 6*x^4 + 9*x^2 - 3 with c = 1.969615506024417? + sage: TestSuite(U).run() + + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: polygons = EuclideanPolygonsWithAngles((1, 3, 1, 1)) + sage: p = polygons.an_element() + sage: B = similarity_surfaces.billiard(p) + sage: B.minimal_cover("translation") + Minimal Translation Cover of Genus 0 Rational Cone Surface built from 2 equilateral triangles + sage: S = B.minimal_cover("translation") + sage: S, _ = S.normalized_coordinates() + sage: S + Translation Surface in H_1(0^6) built from 6 right triangles + + """ + from sage.all import matrix + + if self.base_ring() is QQ: + return (self, matrix(QQ, 2, 2, 1)) + + lab = next(iter(self.labels())) + p = self.polygon(lab) + u = p.edge(1) + v = -p.edge(0) + i = 1 + from flatsurf.geometry.euclidean import ccw + + while ccw(u, v) == 0: + i += 1 + u = p.edge(i) + v = -p.edge(i - 1) + M = matrix(2, [u, v]).transpose().inverse() + assert M.det() > 0 + hols = [] + for lab in self.labels(): + p = self.polygon(lab) + for e in range(len(p.vertices())): + w = M * p.edge(e) + hols.append(w[0]) + hols.append(w[1]) + if self.base_ring() is AA: + from flatsurf.geometry.subfield import ( + number_field_elements_from_algebraics, + ) + + K, new_hols = number_field_elements_from_algebraics(hols) + else: + from flatsurf.geometry.subfield import subfield_from_elements + + K, new_hols, _ = subfield_from_elements(self.base_ring(), hols) + + from flatsurf.geometry.polygon import Polygon + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + S = MutableOrientedSimilaritySurface(K) + relabelling = {} + k = 0 + for lab in self.labels(): + m = len(self.polygon(lab).vertices()) + relabelling[lab] = S.add_polygon( + Polygon( + edges=[ + (new_hols[k + 2 * i], new_hols[k + 2 * i + 1]) + for i in range(m) + ], + base_ring=K, + ) + ) + k += 2 * m + + for (p1, e1), (p2, e2) in self.gluings(): + S.glue((relabelling[p1], e1), (relabelling[p2], e2)) + + S._refine_category_(self.category()) + return S, M + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The category of oriented half-translation surfaces without + boundary built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: H in HalfTranslationSurfaces().Oriented().FiniteType().WithoutBoundary() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented half-translation + surfaces without boundary built from finitely many + polygons. + + If you want to add functionality for such surfaces you most + likely want to put it here. + """ + + def angles(self, numerical=False, return_adjacent_edges=False): + r""" + Return the set of angles around the vertices of the surface. + + These are given as multiple of `2 \pi`. + + EXAMPLES:: + + sage: import flatsurf.geometry.similarity_surface_generators as sfg + sage: sfg.translation_surfaces.regular_octagon().angles() + [3] + sage: S = sfg.translation_surfaces.veech_2n_gon(5) + sage: S.angles() + [2, 2] + sage: S.angles(numerical=True) + [2.0, 2.0] + sage: S.angles(return_adjacent_edges=True) # random output + [(2, [(0, 1), (0, 5), (0, 9), (0, 3), (0, 7)]), + (2, [(0, 0), (0, 4), (0, 8), (0, 2), (0, 6)])] + sage: S.angles(numerical=True, return_adjacent_edges=True) # random output + [(2.0, [(0, 1), (0, 5), (0, 9), (0, 3), (0, 7)]), + (2.0, [(0, 0), (0, 4), (0, 8), (0, 2), (0, 6)])] + + sage: sfg.translation_surfaces.veech_2n_gon(6).angles() + [5] + sage: sfg.translation_surfaces.veech_double_n_gon(5).angles() + [3] + sage: sfg.translation_surfaces.cathedral(1, 1).angles() + [3, 3, 3] + + sage: from flatsurf import polygons, similarity_surfaces + sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) + sage: H = B.minimal_cover(cover_type="half-translation") + sage: S = B.minimal_cover(cover_type="translation") + sage: H.angles() + [1/2, 5/2, 1/2, 1/2] + sage: S.angles() + [1, 5, 1, 1] + + sage: H.angles(return_adjacent_edges=True) + [(1/2, [...]), (5/2, [...]), (1/2, [...]), (1/2, [...])] + sage: S.angles(return_adjacent_edges=True) + [(1, [...]), (5, [...]), (1, [...]), (1, [...])] + + For self-glued edges, no angle is reported for the + "vertex" at the midpoint of the edge:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.angles() + [1] + + """ + edges = set(self.edges()) + angles = [] + + if return_adjacent_edges: + while edges: + # Note that iteration order here is different for different + # versions of Python. Therefore, the output in the doctest + # above is random. + pair = p, e = next(iter(edges)) + ve = self.polygon(p).edge(e) + angle = 0 + adjacent_edges = [] + while pair in edges: + adjacent_edges.append(pair) + edges.remove(pair) + f = (e - 1) % len(self.polygon(p).vertices()) + ve = self.polygon(p).edge(e) + vf = -self.polygon(p).edge(f) + ppair = pp, ff = self.opposite_edge(p, f) + angle += ( + (ve[0] > 0 and vf[0] <= 0) + or (ve[0] < 0 and vf[0] >= 0) + or (ve[0] == vf[0] == 0) + ) + pair, p, e = ppair, pp, ff + if numerical: + angles.append((float(angle) / 2, adjacent_edges)) + else: + angles.append((QQ((angle, 2)), adjacent_edges)) + else: + while edges: + pair = p, e = next(iter(edges)) + angle = 0 + while pair in edges: + edges.remove(pair) + f = (e - 1) % len(self.polygon(p).vertices()) + ve = self.polygon(p).edge(e) + vf = -self.polygon(p).edge(f) + ppair = pp, ff = self.opposite_edge(p, f) + angle += ( + (ve[0] > 0 and vf[0] <= 0) + or (ve[0] < 0 and vf[0] >= 0) + or (ve[0] == vf[0] == 0) + ) + pair, p, e = ppair, pp, ff + if numerical: + angles.append(float(angle) / 2) + else: + angles.append(QQ((angle, 2))) + + return angles diff --git a/flatsurf/geometry/categories/hyperbolic_polygons.py b/flatsurf/geometry/categories/hyperbolic_polygons.py new file mode 100644 index 000000000..00eab87bb --- /dev/null +++ b/flatsurf/geometry/categories/hyperbolic_polygons.py @@ -0,0 +1,74 @@ +r""" +The category of polygons in the hyperbolic plane. + +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, 2).left_half_space(), + ....: H.half_circle(0, 4).right_half_space(), + ....: ]) + + sage: P.category() + Category of facade convex simple hyperbolic polygons over Rational Field + + sage: from flatsurf.geometry.categories import HyperbolicPolygons + sage: P in HyperbolicPolygons(QQ) + True + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** +from sage.categories.category_types import Category_over_base_ring + +from flatsurf.geometry.categories.polygons import Polygons + + +class HyperbolicPolygons(Category_over_base_ring): + r""" + The category of polygons in the hyperbolic plane. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import HyperbolicPolygons + sage: C = HyperbolicPolygons(QQ) + + TESTS:: + + sage: TestSuite(C).run() + + """ + + def super_categories(self): + r""" + Return the categories that a hyperbolic polygon is also a member of. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import HyperbolicPolygons + sage: C = HyperbolicPolygons(QQ) + sage: C.super_categories() + [Category of polygons over Rational Field] + + """ + return [Polygons(self.base_ring())] diff --git a/flatsurf/geometry/categories/polygonal_surfaces.py b/flatsurf/geometry/categories/polygonal_surfaces.py new file mode 100644 index 000000000..4fa5f1c35 --- /dev/null +++ b/flatsurf/geometry/categories/polygonal_surfaces.py @@ -0,0 +1,1366 @@ +r""" +The category of surfaces built from polygons. + +This module provides shared functionality for all surfaces in sage-flatsurf +that are built from polygons (such as Euclidean polygons or hyperbolic +polygons.) + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: C = MutableOrientedSimilaritySurface(QQ).category() + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: C.is_subcategory(PolygonalSurfaces()) + True + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** + +from flatsurf.geometry.categories.surface_category import ( + SurfaceCategory, + SurfaceCategoryWithAxiom, +) +from sage.categories.category_with_axiom import all_axioms +from sage.misc.abstract_method import abstract_method + + +class PolygonalSurfaces(SurfaceCategory): + r""" + The category of surfaces built by gluing polygons defined in some space + such as the real plane (see + :mod:`~flatsurf.geometry.categories.euclidean_polygonal_surfaces`.) + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces() + Category of polygonal surfaces + + """ + + def super_categories(self): + r""" + Return the categories that a polygonal surface is always also a member + of. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().super_categories() + [Category of topological surfaces] + + """ + from flatsurf.geometry.categories.topological_surfaces import ( + TopologicalSurfaces, + ) + + return [TopologicalSurfaces()] + + class ParentMethods: + r""" + Provides methods available to all surfaces that are built from polygons. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def refined_category(self): + r""" + Return the smallest subcategory that this surface is in by + consulting which edges are glued to each other. + + Note that this does not take into account how the edges are glued + to each other exactly (e.g., by which similarity) since at this + level (i.e., without knowing about the space in which the polygons + live) the gluing is just described combinatorially. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, polygons + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square(), label=0) + 0 + sage: S.refined_category() + Category of connected with boundary finite type translation surfaces + + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + sage: S.refined_category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories.topological_surfaces import ( + TopologicalSurfaces, + ) + + category = TopologicalSurfaces.ParentMethods.refined_category(self) + + if self.is_finite_type(): + category &= category.FiniteType() + else: + category &= category.InfiniteType() + + return category + + def is_triangulated(self): + r""" + Return whether this surface is built from triangles. + + Surfaces of infinite type should override this method. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.is_triangulated() + False + + """ + roots = self.roots() + + if not roots: + return True + + for root in roots: + if len(self.polygon(root).vertices()) != 3: + return False + + raise NotImplementedError( + "cannot decide whether this (potentially infinite type) surface is triangulated" + ) + + def walker(self): + r""" + Return an iterable that walks the labels of the surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: walker = S.walker() + doctest:warning + ... + UserWarning: walker() is deprecated and will be removed from a future version of sage-flatsurf; use labels() instead. + sage: list(walker) + [0] + + """ + import warnings + + warnings.warn( + "walker() is deprecated and will be removed from a future version of sage-flatsurf; use labels() instead." + ) + + from flatsurf.geometry.surface_legacy import LabelWalker + + return LabelWalker(self, deprecation_warning=False) + + def labels(self): + r""" + Return the labels used to enumerate the polygons that make up this + surface. + + The labels are returned in a breadth first search order starting at + the :meth:`base_label`. This order is compatible with the order in + which polygons are returned by :meth:`polygons`. + + .. NOTE:: + + The generic implementation of this method returns a collection + that is very slow at computing its length and deciding + containment. To speed things up it is recommended to override + this method. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.labels() + (0,) + + :: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.labels() + (0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, …) + + """ + from flatsurf.geometry.surface import Labels + + return Labels(self, finite=self.is_finite_type()) + + def base_label(self): + r""" + Return the polygon label from which iteration by :meth:`labels` + should start. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.base_label() + doctest:warning + ... + UserWarning: base_label() has been deprecated and will be removed in a future version of sage-flatsurf; use root() instead for connected surfaces and roots() in general + 0 + + """ + import warnings + + warnings.warn( + "base_label() has been deprecated and will be removed in a future version of sage-flatsurf; use root() instead for connected surfaces and roots() in general" + ) + + return self.root() + + def root(self): + r""" + Return the polygon label from which iteration by :meth:`labels` + should start on this connected surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.root() + 0 + + When there are multiple connected components, :meth:`roots` must be + used instead:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, polygons + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + + sage: S.root() + Traceback (most recent call last): + ... + Exception: surface has more than one root label, use roots() instead + sage: S.roots() + (0, 1) + + """ + roots = self.roots() + + if not roots: + raise Exception( + "cannot return a root label for the connected component on an empty surface, use roots() instead" + ) + + if len(roots) > 1: + raise Exception( + "surface has more than one root label, use roots() instead" + ) + + return next(iter(roots)) + + def polygons(self): + r""" + Return the polygons that make up this surface (in the same order as + the labels are returned by :meth:`labels`) + + .. NOTE:: + + Unlike with :meth:`labels`, this method should usually not be + overridden. Things will be fast if :meth:`labels` is fast. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.polygons() + (Polygon(vertices=[(0, 0), (2, 0), (1, 4), (0, 5)]),) + + :: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.polygons() + (Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]), Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]), ...) + + """ + from flatsurf.geometry.surface import Polygons + + return Polygons(self) + + def _test_labels_polygons(self, **options): + r""" + Verify that :meth:`labels` and :meth:`polygons` are compatible. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_labels_polygons() + + """ + tester = self._tester(**options) + + labels = self.labels() + polygons = self.polygons() + + if not self.is_finite_type(): + import itertools + + labels = itertools.islice(labels, 32) + + for label, polygon in zip(labels, polygons): + tester.assertEqual(self.polygon(label), polygon) + + def num_polygons(self): + r""" + Return the number of polygons that make up this surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.num_polygons() + doctest:warning + ... + UserWarning: num_polygons() is deprecated and will be removed in a future version of sage-flatsurf; use len(polygons()) instead (and is_finite_type() for potentially infinite surfaces.) + 1 + sage: len(S.polygons()) + 1 + + :: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.num_polygons() + +Infinity + sage: S.is_finite_type() + False + + """ + import warnings + + warnings.warn( + "num_polygons() is deprecated and will be removed in a future version of sage-flatsurf; use len(polygons()) instead (and is_finite_type() for potentially infinite surfaces.)" + ) + + # Note that using len(self.polygons()) on + # MutableOrientedSimilaritySurface is only very slightly slower: + # %timeit num_polygons() + # 137ns + # %timeit len(polygons()) + # 159ns + + # On other surfaces, the effect can be much more pronounced. The + # overhead of calling through the category framework and creating a + # Labels object can lead to runtimes of about 1μs. + + if not self.is_finite_type(): + from sage.all import infinity + + return infinity + return len(self.polygons()) + + def label_iterator(self, polygons=False): + r""" + TESTS:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: list(S.label_iterator()) + doctest:warning + ... + UserWarning: label_iterator() has been deprecated and will be removed in a future version of sage-flatsurf; use labels() instead + [0] + sage: S.labels() + (0,) + + """ + import warnings + + if polygons: + warnings.warn( + "label_iterator() has been deprecated and will be removed in a future version of sage-flatsurf; use zip(labels(), polygons()) instead" + ) + for entry in zip(self.labels(), self.polygons()): + yield entry + else: + warnings.warn( + "label_iterator() has been deprecated and will be removed in a future version of sage-flatsurf; use labels() instead" + ) + for entry in self.labels(): + yield entry + + def edge_iterator(self, gluings=False): + r""" + Iterate over the edges of polygons, which are pairs (l,e) where l + is a polygon label, 0 <= e < N and N is the number of edges of the + polygon with label l. + + TESTS:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: list(S.edge_iterator()) + doctest:warning + ... + UserWarning: edge_iterator() has been deprecated and will be removed in a future version of sage-flatsurf; use edges() instead + [(0, 0), (0, 1), (0, 2), (0, 3)] + sage: S.edges() + ((0, 0), (0, 1), (0, 2), (0, 3)) + + :: + + sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(Polygon(edges=[(1,0),(0,1),(-1,-1)])) + 0 + sage: S.add_polygon(Polygon(edges=[(-1,0),(0,-1),(1,1)])) + 1 + sage: S.glue((0, 0), (1, 0)) + sage: S.glue((0, 1), (1, 1)) + sage: S.glue((0, 2), (1, 2)) + sage: list(S.edge_iterator()) + [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] + sage: S.edges() + ((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)) + + """ + import warnings + + if gluings: + warnings.warn( + "edge_iterator() has been deprecated and will be removed in a future version of sage-flatsurf; use gluings() instead" + ) + for entry in self.gluings(): + yield entry + return + for label, polygon in zip(self.labels(), self.polygons()): + warnings.warn( + "edge_iterator() has been deprecated and will be removed in a future version of sage-flatsurf; use edges() instead" + ) + for edge in range(len(polygon.vertices())): + yield label, edge + + def edges(self): + r""" + Return the edges of the polygons that make up this surface as pairs + (polygon label, edge index). + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.edges() + ((0, 0), (0, 1), (0, 2), (0, 3)) + + :: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.edges() + ((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (-1, 0), (-1, 1), (-1, 2), (-1, 3), (2, 0), (2, 1), (2, 2), (2, 3), …) + + """ + from flatsurf.geometry.surface import Edges + + return Edges(self, finite=self.is_finite_type()) + + def edge_gluing_iterator(self): + r""" + Iterate over the ordered pairs of edges being glued. + + TESTS:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: list(S.edge_gluing_iterator()) + doctest:warning + ... + UserWarning: edge_gluing_iterator() has been deprecated and will be removed in a future version of sage-flaturf; use gluings() instead + [((0, 0), (0, 0)), ((0, 1), (0, 1)), ((0, 2), (0, 2)), ((0, 3), (0, 3))] + sage: S.gluings() + (((0, 0), (0, 0)), ((0, 1), (0, 1)), ((0, 2), (0, 2)), ((0, 3), (0, 3))) + + """ + import warnings + + warnings.warn( + "edge_gluing_iterator() has been deprecated and will be removed in a future version of sage-flaturf; use gluings() instead" + ) + + for label_edge_pair in self.edges(): + yield ( + label_edge_pair, + self.opposite_edge(label_edge_pair[0], label_edge_pair[1]), + ) + + def gluings(self): + r""" + Return the pairs of edges being glued to each other. + + Each gluing is reported as a pair of pairs (polygon label, edge + index) and (glued polygon label, glued edge index). + + Note that each gluing is reported twice (unless it is a + self-gluing.) + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.gluings() + (((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1))) + + A surface with only self-gluings:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.gluings() + (((0, 0), (0, 0)), ((0, 1), (0, 1)), ((0, 2), (0, 2)), ((0, 3), (0, 3))) + + """ + from flatsurf.geometry.surface import Gluings + + return Gluings(self) + + def label_polygon_iterator(self): + r""" + Iterate over pairs (label, polygon). + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: print(list(S.label_polygon_iterator())) + doctest:warning + ... + UserWarning: label_polygon_iterator() has been deprecated and will be removed from a future version of sage-flatsurf; use zip(labels(), polygons()) instead + [(0, Polygon(vertices=[(0, 0), (2, 0), (1, 4), (0, 5)]))] + sage: print(list(zip(S.labels(), S.polygons()))) + [(0, Polygon(vertices=[(0, 0), (2, 0), (1, 4), (0, 5)]))] + + """ + import warnings + + warnings.warn( + "label_polygon_iterator() has been deprecated and will be removed from a future version of sage-flatsurf; use zip(labels(), polygons()) instead" + ) + + return zip(self.labels(), self.polygons()) + + @abstract_method + def polygon(self, label): + r""" + Return the polygon with ``label``. + + INPUT: + + - ``label`` -- one of the labels included in :meth:`labels` + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.polygon(0) + Polygon(vertices=[(0, 0), (2, 0), (1, 4), (0, 5)]) + + """ + + @abstract_method + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge that is glued to the ``edge`` of + the polygon with ``label``. + + INPUT: + + - ``label`` -- one of the labels included in :meth:`labels` + + - ``edge`` -- a non-negative integer to specify an edge (the edges + of a polygon are numbered starting from zero.) + + OUTPUT: + + A tuple ``(label, edge)`` with the semantics as in the input. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.opposite_edge(0, 0) + (0, 0) + + """ + + def is_finite(self): + r""" + Return whether this surface is constructed from finitely many polygons. + + .. NOTE:: + + The semantics of this function clash with the notion of finite + sets inherited from the category of sets. Therefore + :meth:`is_finite_type` should be used instead. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_finite_type() + True + + """ + import warnings + + warnings.warn( + "is_finite() has been deprecated and will be removed in a future version of sage-flatsurf; use is_finite_type() instead" + ) + return self.is_finite_type() + + @abstract_method + def is_finite_type(self): + r""" + Return whether this surface is constructed from finitely many polygons. + + .. NOTE:: + + This method is used to determine whether this surface satisfies + the :class:`~.PolygonalSurfaces.FiniteType` axiom or the + :class:`~.PolygonalSurfaces.InfiniteType` axiom. Surfaces can + override this method to perform specialized logic, see the note + in :mod:`flatsurf.geometry.categories` for performance + considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_finite_type() + True + + """ + + def num_edges(self): + r""" + Return the total number of edges of all polygons used. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.num_edges() + doctest:warning + ... + UserWarning: num_edges() has been deprecated and will be removed from a future version of sage-flatsurf; use sum(len(p.vertices()) for p in polygons()) instead + 4 + + """ + import warnings + + warnings.warn( + "num_edges() has been deprecated and will be removed from a future version of sage-flatsurf; use sum(len(p.vertices()) for p in polygons()) instead" + ) + + if self.is_finite_type(): + return sum(len(p.vertices()) for p in self.polygons()) + + from sage.rings.infinity import Infinity + + return Infinity + + def _test_gluings(self, **options): + r""" + Verify that the gluings of this surface are consistent. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_gluings() + + """ + tester = self._tester(**options) + + if self.is_finite_type(): + it = self.labels() + else: + from itertools import islice + + it = islice(self.labels(), 30) + + for lab in it: + p = self.polygon(lab) + for k in range(len(p.vertices())): + e = (lab, k) + f = self.opposite_edge(lab, k) + if f is None: + continue + g = self.opposite_edge(f[0], f[1]) + tester.assertEqual( + e, + g, + "edge gluing is not a pairing:\n{} -> {} -> {}".format(e, f, g), + ) + + @abstract_method + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + A root label is just any of the :meth:`labels` of the surface. + However, the iteration of :meth:`labels` starts from those root + labels so for some surfaces they might have been specifically + chosen. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.roots() + (0,) + + """ + + def is_connected(self): + r""" + Return whether this surface is connected. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_connected() + True + + """ + return len(self.roots()) <= 1 + + def component(self, root): + r""" + Return the labels contained in the connected component containing + the label ``root``. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.component(0) + (0,) + + """ + from flatsurf.geometry.surface import ComponentLabels + + return ComponentLabels(self, root) + + def components(self): + r""" + Return the connected components that make up this surface. + + OUTPUT: + + A sequence of connected components where each component is in turn + a sequence of the polygon labels contained in that component. + + EXAMPLES:: + + sage: from flatsurf import polygons, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + sage: S.add_polygon(polygons.square()) + 2 + sage: S.glue((0, 0), (1, 0)) + sage: S.components() + ((0, 1), (2,)) + + """ + return tuple(self.component(root) for root in self.roots()) + + def _test_components(self, **options): + r""" + Verify that :meth:`components` is compatible with :meth:`roots`. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_components() + + """ + tester = self._tester(**options) + + tester.assertEqual(len(self.components()), len(self.roots())) + + class FiniteType(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: 'FiniteType' in S.category().axioms() + True + + """ + + def extra_super_categories(self): + r""" + Return the categories that surfaces built from finitely many + polygons are additionally contained in; namely such a surface is a + compact space. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().FiniteType().extra_super_categories() + (Category of compact topological spaces,) + + """ + from sage.categories.topological_spaces import TopologicalSpaces + + return (TopologicalSpaces().Compact(),) + + class InfiniteType(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces that are built from finitely and + infinitely many polygons at the same time. + + This axiom does not exist and it is an error to create it. + + TESTS:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().FiniteType() & PolygonalSurfaces().InfiniteType() + Traceback (most recent call last): + ... + TypeError: surface cannot be finite type and infinite type at the same time + sage: PolygonalSurfaces().InfiniteType() & PolygonalSurfaces().FiniteType() + Traceback (most recent call last): + ... + TypeError: surface cannot be finite type and infinite type at the same time + + """ + + def __init__(self, *args, **kwargs): + raise TypeError( + "surface cannot be finite type and infinite type at the same time" + ) + + class Connected(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by connected surfaces built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().FiniteType().Connected() + Category of connected finite type polygonal surfaces + + """ + + class ParentMethods: + r""" + Provides methods available to all connected surfaces built from + finitely many polygons. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def _test_roots(self, **options): + r""" + Verify that :meth:`roots` only reports a single connected + component. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_roots() + + """ + tester = self._tester(**options) + + roots = self.roots() + + for root in roots: + label = [label for label in self.labels() if label == root] + tester.assertEqual(len(label), 1) + tester.assertEqual(type(label[0]), type(root)) + + if not roots: + tester.assertTrue(not any([True for label in self.labels()])) + else: + tester.assertTrue(next(iter(self.labels())) in roots) + + class Oriented(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by orientable surfaces with an orientation + which is compatible with the orientation of the ambient space of + the finitely many polygons that define the surface (assuming that + that ambient space is orientable.) + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: S.category().is_subcategory(PolygonalSurfaces().FiniteType().Oriented()) + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces built from finitely + many oriented polygons. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def euler_characteristic(self): + r""" + Return the Euler characteristic of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: S.euler_characteristic() + -4 + + """ + # Count the vertices + union_find = {edge: edge for edge in self.edges()} + + def find(node): + if union_find[node] == node: + return node + parent = find(union_find[node]) + union_find[node] = parent + return parent + + for label, edge in self.edges(): + previous = (edge - 1) % len(self.polygon(label).vertices()) + cross = self.opposite_edge(label, previous) + if cross is None: + continue + + union_find[find((label, edge))] = find(cross) + + V = len(set(find((label, edge)) for (label, edge) in self.edges())) + + # Count the edges + from sage.all import QQ, ZZ + + E = QQ(0) + for label, edge in self.edges(): + if self.opposite_edge(label, edge) is None: + E += 1 + elif self.opposite_edge(label, edge) == (label, edge): + E += 1 + V += 1 + else: + E += 1 / 2 + assert E in ZZ + + # Count the faces + F = len(self.polygons()) + + return ZZ(V - E + F) + + class ParentMethods: + r""" + Provides methods available to all surfaces built from finitely many polygons. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def is_finite_type(self): + r""" + Return whether this surface is built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_finite_type() + True + + """ + return True + + def is_triangulated(self): + r""" + Return whether this surfaces is built from triangles. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_triangulated() + False + + """ + for p in self.polygons(): + if len(p.vertices()) != 3: + return False + + return True + + def is_with_boundary(self): + r""" + Return whether this surface has a boundary, i.e., unglued polygon edges. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_with_boundary() + False + + """ + for label in self.labels(): + for edge in range(len(self.polygon(label).vertices())): + cross = self.opposite_edge(label, edge) + if cross is None: + return True + + return False + + def vertices(self): + r""" + Return the equivalence classes of the vertices of the polygons + that make up this surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.vertices() + {Vertex 0 of polygon 0} + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.regular_octagon() + sage: S.vertices() + {Vertex 0 of polygon 0} + + """ + return set( + # pylint: disable-next=not-callable + [self(label, vertex) for (label, vertex) in self.edges()] + ) + + def _test_labels(self, **options): + r""" + Verify that :meth:`labels` has been implemented correctly by + this surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_labels() + + """ + tester = self._tester(**options) + + tester.assertEqual( + len([label for label in self.labels()]), len(self.labels()) + ) + + class InfiniteType(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces built from infinitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: 'InfiniteType' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces built from infinitely + many polygons. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def is_finite_type(self): + r""" + Return whether this surfaces has been built from finitely many + polygons which it has not. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.is_finite_type() + False + + """ + return False + + class Oriented(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by orientable surfaces with an orientation which is + compatible with the orientation of the ambient space of the polygons + (assuming that that ambient space is orientable.) + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: 'Oriented' in S.category().axioms() + True + + """ + + def extra_super_categories(self): + r""" + Return the axioms that are automatically satisfied by a surfaces + which is oriented, namely, that such a surface is orientable. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: 'Orientable' in S.category().axioms() + True + + """ + from flatsurf.geometry.categories.topological_surfaces import ( + TopologicalSurfaces, + ) + + return (TopologicalSurfaces().Orientable(),) + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by oriented surfaces built from polygons that + have no unglued polygon edges. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: S.category().is_subcategory(PolygonalSurfaces().Oriented().WithoutBoundary()) + True + + """ + + class Connected(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by oriented connected surfaces built from + polygons that have no unglued polygon edges. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: S.category().is_subcategory(PolygonalSurfaces().Oriented().WithoutBoundary().Connected()) + True + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented connected + surfaces built from polygons without unglued edges. + + If you want to add functionality for such surfaces you most + likely want to put it here. + """ + + def genus(self): + r""" + Return the genus of this surface. + + ALGORITHM: + + We deduce the genus from the Euler characteristic. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: translation_surfaces.octagon_and_squares().genus() + 3 + + This method might not be functional if the Euler + characteristic has not been implemented for the surface:: + + sage: S = translation_surfaces.infinite_staircase() + sage: S.genus() + Traceback (most recent call last): + ... + AttributeError: ... has no attribute 'euler_characteristic' + + """ + return 1 - self.euler_characteristic() / 2 + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces built from polygons without any unglued + edges. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: S.category().is_subcategory(PolygonalSurfaces().WithoutBoundary()) + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces that are built from + polygons without unglued edges. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def _test_gluings_without_boundary(self, **options): + r""" + Verify that this surface has no unglued edges. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S._test_gluings_without_boundary() + + """ + tester = self._tester(**options) + + if self.is_finite_type(): + it = self.labels() + else: + from itertools import islice + + it = islice(self.labels(), 30) + + for lab in it: + p = self.polygon(lab) + for k in range(len(p.vertices())): + f = self.opposite_edge(lab, k) + tester.assertFalse( + f is None, "edge ({}, {}) is not glued".format(lab, k) + ) + + class SubcategoryMethods: + def FiniteType(self): + r""" + Return the subcategory of surfaces built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().FiniteType() + Category of finite type polygonal surfaces + + """ + return self._with_axiom("FiniteType") + + def InfiniteType(self): + r""" + Return the subcategory of surfaces built from infinitely many polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().InfiniteType() + Category of infinite type polygonal surfaces + + """ + return self._with_axiom("InfiniteType") + + def Oriented(self): + r""" + Return the subcategory of surfaces with an orientation that is + inherited from the polygons that it is built from. + + This assumes that the ambient space of the polygons is orientable. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import PolygonalSurfaces + sage: PolygonalSurfaces().Oriented() + Category of oriented polygonal surfaces + + """ + return self._with_axiom("Oriented") + + +# Currently, there is no "FiniteType", "InfiniteType", and "Oriented" +# axiom in SageMath so we make them known to the category framework. +all_axioms += ("FiniteType", "InfiniteType", "Oriented") diff --git a/flatsurf/geometry/categories/polygons.py b/flatsurf/geometry/categories/polygons.py new file mode 100644 index 000000000..83624e240 --- /dev/null +++ b/flatsurf/geometry/categories/polygons.py @@ -0,0 +1,614 @@ +r""" +The category of polyogons + +This module provides shared functionality for all polygons in sage-flatsurf. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category of a polygon is automatically determined. + +EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: C = Polygons(QQ) + + sage: from flatsurf import polygons + sage: polygons.square() in C + True + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 Vincent Delecroix +# 2020-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** +from sage.misc.cachefunc import cached_method +from sage.categories.category_types import Category_over_base_ring +from sage.categories.category_with_axiom import ( + CategoryWithAxiom_over_base_ring, + all_axioms, +) +from sage.misc.abstract_method import abstract_method + +from sage.categories.all import Sets + + +class Polygons(Category_over_base_ring): + r""" + The category of polygons defined over a base ring. + + This comprises arbitrary base ring, e.g., this category contains Euclidean + polygons and hyperbolic polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: Polygons(QQ) + Category of polygons over Rational Field + + """ + + def super_categories(self): + r""" + Return the categories polygons automatically belong to. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: C = Polygons(QQ) + sage: C.super_categories() + [Category of sets] + + """ + return [Sets()] + + @staticmethod + def _describe_polygon(num_edges, **kwargs): + r""" + Return a printable description of a polygon with ``num_edges`` edges + and additional features encoded as keyword arguments. + + The returned strings form a triple (indeterminate article, singular, plural). + + Usually, you don't call this method directly, but + :meth:`Polygons.ParentMethods.describe_polygon`. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: Polygons._describe_polygon(3) + ('a', 'triangle', 'triangles') + sage: Polygons._describe_polygon(3, equiangular=True, equilateral=True) + ('an', 'equilateral triangle', 'equilateral triangles') + sage: Polygons._describe_polygon(9, equiangular=True, equilateral=True) + ('a', 'regular nonagon', 'regular nonagons') + sage: Polygons._describe_polygon(4, equiangular=False, equilateral=True) + ('a', 'rhombus', 'rhombi') + + """ + from sage.all import infinity + + # From https://en.wikipedia.org/wiki/Polygon#Naming + ngon_names = { + 1: ("a", "monogon"), + 2: ("a", "digon"), + 3: ("a", "triangle"), + 4: ("a", "quadrilateral"), + 5: ("a", "pentagon"), + 6: ("a", "hexagon"), + 7: ("a", "heptagon"), + 8: ("an", "octagon"), + 9: ("a", "nonagon"), + 10: ("a", "decagon"), + 11: ("a", "hendecagon"), + 12: ("a", "dodecagon"), + 13: ("a", "tridecagon"), + 14: ("a", "tetradecagon"), + 15: ("a", "pentadecagon"), + 16: ("a", "hexadecagon"), + 17: ("a", "heptadecagon"), + 18: ("an", "octadecagon"), + 19: ("an", "enneadecagon"), + 20: ("an", "icosagon"), + # Most people probably don't know the prefixes after that. We + # keep a few easy/fun ones. + 100: ("a", "hectogon"), + 1000: ("a", "chiliagon"), + 10000: ("a", "myriagon"), + 1000000: ("a", "megagon"), + infinity: ("an", "apeirogon"), + } + + description = ngon_names.get(num_edges, f"{num_edges}-gon") + description = description + (description[1] + "s",) + + def augment(article, *attributes): + nonlocal description + description = ( + article, + " ".join(attributes + (description[1],)), + " ".join(attributes + (description[2],)), + ) + + def augment_if(article, attribute, *properties): + if all( + [ + kwargs.get(property, False) + for property in (properties or [attribute]) + ] + ): + augment(article, attribute) + return True + return False + + def augment_if_not(article, attribute, *properties): + if all( + [ + kwargs.get(property, True) is False + for property in (properties or [attribute]) + ] + ): + augment(article, attribute) + return True + return False + + if augment_if("a", "degenerate"): + return description + + if num_edges == 3: + if not augment_if("an", "equilateral", "equiangular"): + if not augment_if("an", "isosceles"): + augment_if("a", "right") + + return description + + if num_edges == 4: + if kwargs.get("equilateral", False) and kwargs.get("equiangular", False): + return "a", "square", "squares" + + if kwargs.get("equiangular", False): + return "a", "rectangle", "rectangles" + + if kwargs.get("equilateral", False): + return "a", "rhombus", "rhombi" + + augment_if("a", "regular", "equilateral", "equiangular") + + augment_if_not("a", "non-convex", "convex") + + marked_vertices = kwargs.get("marked_vertices", 0) + if marked_vertices: + if marked_vertices == 1: + suffix = "with a marked vertex" + else: + suffix = f"with {kwargs.get('marked_vertices')} marked vertices" + description = ( + description[0], + f"{description[1]} {suffix}", + f"{description[2]} {suffix}", + ) + + return description + + class ParentMethods: + r""" + Provides methods available to all polygons. + + If you want to add functionality to all polygons, independent of + implementation, you probably want to put it here. + """ + + @abstract_method + def change_ring(self, ring): + r""" + Return a copy of this polygon which is defined over ``ring``. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: K. = NumberField(x^2 - 2, embedding=AA(2)**(1/2)) + sage: S.change_ring(K) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + """ + + @abstract_method + def is_convex(self, strict=False): + r""" + Return whether this is a convex polygon. + + INPUT: + + - ``strict`` -- whether to check for strict convexity, i.e., a + polygon with a π angle is not considered convex. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.is_convex() + True + sage: S.is_convex(strict=True) + True + + """ + + @abstract_method + def is_degenerate(self): + r""" + Return whether this polygon is considered degenerate. + + EXAMPLES: + + Polygons with zero area are considered degenerate:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (1, 0)], check=False) + sage: p.is_degenerate() + True + + Polygons with marked vertices are considered degenerate:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (2, 0), (4, 0), (2, 2)]) + sage: p.is_degenerate() + True + + """ + + @abstract_method + def is_simple(self): + r""" + Return whether this polygon is not self-intersecting. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.is_simple() + True + + """ + + def base_ring(self): + r""" + Return the ring over which this polygon is defined. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: S = polygons.square() + sage: S.base_ring() + Rational Field + + """ + return self.category().base_ring() + + def describe_polygon(self): + r""" + Return a textual description of this polygon for generating + human-readable messages. + + The description is returned as a triple (indeterminate article, + singular, plural). + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.describe_polygon() + ('a', 'square', 'squares') + + """ + marked_vertices = set(self.vertices()).difference( + self.vertices(marked_vertices=False) + ) + + if marked_vertices and self.area() != 0: + self = self.erase_marked_vertices() + + properties = { + "degenerate": self.is_degenerate(), + "equilateral": self.is_equilateral(), + "equiangular": self.is_equiangular(), + "convex": self.is_convex(), + "marked_vertices": len(marked_vertices), + } + + if len(self.vertices()) == 3: + slopes = self.slopes(relative=True) + properties["right"] = any(slope[0] == 0 for slope in slopes) + + from flatsurf.geometry.euclidean import is_parallel + + properties["isosceles"] = ( + is_parallel(slopes[0], slopes[1]) + or is_parallel(slopes[0], slopes[2]) + or is_parallel(slopes[1], slopes[2]) + ) + + return Polygons._describe_polygon(len(self.vertices()), **properties) + + def _test_refined_category(self, **options): + r""" + Verify that the polygon is in the smallest category that can be + easily determined. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: P = polygons.square() + sage: P._test_refined_category() + + """ + tester = self._tester(**options) + + if self.is_convex(): + tester.assertTrue("Convex" in self.category().axioms()) + + if self.is_simple(): + tester.assertTrue("Simple" in self.category().axioms()) + + # We do not test for Rational and WithAngles since these are only + # determined on demand (computing angles can be very costly.) + + class Convex(CategoryWithAxiom_over_base_ring): + r""" + The axiom satisfied by convex polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: C = Polygons(QQ) + sage: C.Convex() + Category of convex polygons over Rational Field + + """ + + class ParentMethods: + r""" + Provides methods available to all convex polygons. + + If you want to add functionality to all such polygons, you probably + want to put it here. + """ + + def is_convex(self, strict=False): + r""" + Return whether this is a convex polygon, which it is. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: P = polygons.square() + sage: P.is_convex() + True + + """ + if strict: + raise NotImplementedError( + "cannot decide strict convexity for this polygon yet" + ) + + return True + + class Simple(CategoryWithAxiom_over_base_ring): + r""" + The axiom satisfied by polygons that are not self-intersecting. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: C = Polygons(QQ) + sage: C.Simple() + Category of simple polygons over Rational Field + + """ + + class ParentMethods: + r""" + Provides methods available to all simple polygons. + + If you want to add functionality to all such polygons, you probably + want to put it here. + """ + + def is_simple(self): + r""" + Return whether this polygon is not self-intersecting, i.e., + return ``True``. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.is_simple() + True + + """ + return True + + class Rational(CategoryWithAxiom_over_base_ring): + r""" + The axiom satisfied by polygons whose inner angles are rational + multiples of π. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: C = Polygons(QQ) + sage: C.Rational() + Category of rational polygons over Rational Field + + """ + + class ParentMethods: + r""" + Provides methods available to all rational polygons. + + If you want to add functionality to all such polygons, you probably + want to put it here. + """ + + def is_rational(self): + r""" + Return whether all inner angles of this polygon are rational + multiples of π, i.e., return ``True``. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.is_rational() + True + + """ + return True + + class SubcategoryMethods: + def Convex(self): + r""" + Return the subcategory of convex polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: Polygons(QQ).Convex() + Category of convex polygons over Rational Field + + """ + return self._with_axiom("Convex") + + def Simple(self): + r""" + Return the subcategyr of simple polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: Polygons(QQ).Simple() + Category of simple polygons over Rational Field + + """ + return self._with_axiom("Simple") + + def Rational(self): + r""" + Return the subcategory of polygons with rational angles. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import Polygons + sage: Polygons(QQ).Rational() + Category of rational polygons over Rational Field + + """ + return self._with_axiom("Rational") + + def field(self): + r""" + Return the field over which these polygons are defined. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: P = Polygon(vertices=[(0,0),(1,0),(2,1),(-1,1)]) + sage: P.category().field() + doctest:warning + ... + UserWarning: field() has been deprecated and will be removed from a future version of sage-flatsurf; use base_ring() or base_ring().fraction_field() instead + Rational Field + + """ + import warnings + + warnings.warn( + "field() has been deprecated and will be removed from a future version of sage-flatsurf; use base_ring() or base_ring().fraction_field() instead" + ) + + return self.base_ring().fraction_field() + + @cached_method + def base_ring(self): + r""" + Return the ring over which the polygons in this category are + defined. + + sage: from flatsurf.geometry.categories import Polygons + sage: C = Polygons(QQ).Rational().Simple().Convex() + + sage: C.base_ring() + Rational Field + + """ + # Copied this trick from SageMath's Modules category. + for C in self.super_categories(): + if hasattr(C, "base_ring"): + return C.base_ring() + assert False + + def _test_base_ring(self, **options): + tester = self._tester(**options) + + from sage.categories.all import Rings + + tester.assertTrue(self.base_ring() in Rings()) + + def change_ring(self, ring): + r""" + Return this category but defined over the ring ``ring``. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: C = s.category() + sage: C + Category of convex simple euclidean rectangles over Rational Field + sage: C.change_ring(AA) + Category of convex simple euclidean rectangles over Algebraic Real Field + + """ + from sage.categories.category import JoinCategory + + if isinstance(self, JoinCategory): + from sage.categories.category import Category + + return Category.join( + [S.change_ring(ring) for S in self.super_categories()] + ) + + # This is a hack to make the change ring of EuclideanPolygonsWithAngles subcategories work + if hasattr(self, "angles"): + return type(self)(ring, self.angles()) + + if isinstance(self, Category_over_base_ring): + return type(self)(ring) + + raise NotImplementedError("cannot change_ring() of this category yet") + + +# Currently, there is no "Convex" and "Simple" axiom in SageMath so we make it +# known to the category framework. Note that "Rational" is already defined by +# topological surfaces so we don't need to declare it here again. +all_axioms += ( + "Convex", + "Simple", +) diff --git a/flatsurf/geometry/categories/similarity_surfaces.py b/flatsurf/geometry/categories/similarity_surfaces.py new file mode 100644 index 000000000..54c6d00d3 --- /dev/null +++ b/flatsurf/geometry/categories/similarity_surfaces.py @@ -0,0 +1,2615 @@ +r""" +The category of similarity surfaces. + +This module provides shared functionality for all surfaces in sage-flatsurf +that are built from Euclidean polygons that are glued by similarities, i.e., +identified edges can be transformed into each other by application of rotation +and homothety (dilation) and translation. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: C = MutableOrientedSimilaritySurface(QQ).category() + + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: C.is_subcategory(SimilaritySurfaces()) + True + +The easiest way to construct a similarity surface is to use the constructions +from +:class:`flatsurf.geometry.similarity_surface_generators.SimilaritySurfaceGenerators`:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: similarity_surfaces.self_glued_polygon(P) + Half-Translation Surface in Q_0(0, -1^4) built from a quadrilateral + +Another way is to build a surface from scratch (using e.g. +:class:`flatsurf.geometry.surface.MutableOrientedSimilaritySurface`):: + + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(P) + 0 + sage: S.add_polygon(2*P) + 1 + sage: S.add_polygon(3*P) + 2 + sage: S.glue((0, 1), (1, 3)) + sage: S.glue((0, 0), (2, 2)) + sage: S.glue((0, 2), (2, 0)) + sage: S.glue((0, 3), (1, 1)) + sage: S.glue((1, 2), (2, 1)) + sage: S.glue((1, 0), (2, 3)) + sage: S + Surface built from 3 squares + +To perform a sanity check on the obtained surface, you can run its test +suite:: + + sage: TestSuite(S).run() + +If there are no errors reported, no consistency problems could be detected in +your surface. + +Once you mark the surface as immutable, it gets more functionality, e.g., +coming from its structure as a translation surface. This also adds more tests +to its test suite:: + + sage: S.category() + Category of finite type oriented similarity surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type oriented rational similarity surfaces + + sage: TestSuite(S).run() + +In the following example, we attempt to build a broken surface by gluing more +than two edges to each other; however, edges get unglued automatically:: + + sage: S = MutableOrientedSimilaritySurface.from_surface(S) + sage: S.glue((0, 0), (0, 3)) + sage: S.glue((0, 1), (0, 3)) + sage: S.glue((0, 2), (0, 3)) + + sage: S.gluings() + (((0, 2), (0, 3)), ((0, 3), (0, 2)), ((1, 0), (2, 3)), ((1, 2), (2, 1)), ((2, 1), (1, 2)), ((2, 3), (1, 0))) + + sage: S.set_immutable() + sage: S.category() + Category of with boundary finite type oriented rational similarity surfaces + sage: TestSuite(S).run() + +If we don't glue all the edges, we get a surface with boundary:: + + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(P) + 0 + sage: TestSuite(S).run() + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 Vincent Delecroix +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** + +from flatsurf.geometry.categories.surface_category import ( + SurfaceCategory, + SurfaceCategoryWithAxiom, +) +from sage.categories.category_with_axiom import all_axioms +from sage.misc.cachefunc import cached_method +from sage.all import QQ, AA + + +class SimilaritySurfaces(SurfaceCategory): + r""" + The category of surfaces built from polygons with edges identified by + similarities. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: SimilaritySurfaces() + Category of similarity surfaces + + """ + + def super_categories(self): + r""" + Return the categories that a similarity surface is also a member of, + namely the surfaces formed by Euclidean polygons. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: SimilaritySurfaces().super_categories() + [Category of euclidean polygonal surfaces] + + """ + from flatsurf.geometry.categories.euclidean_polygonal_surfaces import ( + EuclideanPolygonalSurfaces, + ) + + return [EuclideanPolygonalSurfaces()] + + class ParentMethods: + r""" + Provides methods available to all surfaces that are built from + Euclidean polygons that are glued by similarities. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def refined_category(self): + r""" + Return the smallest subcategory that this surface is in by consulting + how its edges are glued. + + The result of this method can be fed to ``_refine_category_`` to + change the category of the surface (and enable functionality + specific to the smaller classes of surfaces.) + + + .. NOTE:: + + If a surface cannot implement the various ``is_`` methods used in + the implementation of this method (i.e., if any of them throws a + ``NotImplementedError``,) then this method ``refined_category`` + must be overridden to skip that check. We don't want to actively + catch a ``NotImplementedError`` and instead encourage authors + to explicitly select the category their surfaces lives in. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square(), label=0) + 0 + sage: S.refined_category() + Category of connected with boundary finite type translation surfaces + + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + sage: S.refined_category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories.polygonal_surfaces import ( + PolygonalSurfaces, + ) + + category = PolygonalSurfaces.ParentMethods.refined_category(self) + + if self.is_cone_surface(): + from flatsurf.geometry.categories.cone_surfaces import ConeSurfaces + + category &= ConeSurfaces() + + if self.is_dilation_surface(): + from flatsurf.geometry.categories.dilation_surfaces import ( + DilationSurfaces, + ) + + category &= DilationSurfaces() + + if self.is_dilation_surface(positive=True): + category &= DilationSurfaces().Positive() + + if self.is_translation_surface(): + from flatsurf.geometry.categories.translation_surfaces import ( + TranslationSurfaces, + ) + + category &= TranslationSurfaces() + elif self.is_translation_surface(positive=False): + from flatsurf.geometry.categories.half_translation_surfaces import ( + HalfTranslationSurfaces, + ) + + category &= HalfTranslationSurfaces() + + if "Rational" not in category.axioms(): + if self.is_rational_surface(): + category = category.Rational() + + return category + + def is_cone_surface(self): + r""" + Return whether this surface is a cone surface, i.e., glued edges + can be transformed into each other with isometries. + + .. NOTE:: + + This is a stronger requirement than the usual + definition of a cone surface, see :mod:`.cone_surfaces` for + details. + + .. NOTE:: + + This method is used to determine whether this surface is in the + category of :class:`~.cone_surfaces.ConeSurfaces`. Surfaces can + override this method to perform specialized logic, see the note + in :mod:`flatsurf.geometry.categories` for performance + considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_cone_surface() + True + + """ + if self.is_translation_surface(): + return True + + from flatsurf.geometry.categories import ConeSurfaces + + return ConeSurfaces.ParentMethods._is_cone_surface(self) + + def is_dilation_surface(self, positive=False): + r""" + Return whether this surface is a dilation surface, i.e., whether + glued edges can be transformed into each other by translation + followed by a dilation (multiplication by a diagonal matrix.) + + .. NOTE:: + + This method is used to determine whether this surface is in the + category of :class:`~.dilation_surfaces.DilationSurfaces` or + :class:`~.dilation_surfaces.DilationSurfaces.Positive`. + Surfaces can override this method to perform specialized logic, + see the note in :mod:`~flatsurf.geometry.categories` for + performance considerations. + + INPUT: + + - ``positive`` -- a boolean (default: ``False``); whether the + entries of the diagonal matrix must be positive or are allowed to + be negative. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_dilation_surface() + True + sage: S.is_dilation_surface(positive=True) + False + + """ + if self.is_translation_surface(positive=positive): + return True + + from flatsurf.geometry.categories import DilationSurfaces + + return DilationSurfaces.ParentMethods._is_dilation_surface( + self, positive=positive + ) + + def is_translation_surface(self, positive=True): + r""" + Return whether this surface is a translation surface, i.e., glued + edges can be transformed into each other by translations. + + This method must be implemented if this surface is a dilation surface. + + .. NOTE:: + + This method is used to determine whether this surface is in the + category of + :class:`~.half_translation_surfaces.HalfTranslationSurfaces` or + :class:`~.translation_surfaces.TranslationSurfaces`. Surfaces + can override this method to perform specialized logic, see the + note in :mod:`~flatsurf.geometry.categories` for performance + considerations. + + INPUT: + + - ``positive`` -- a boolean (default: ``True``); whether the + transformation must be a translation or is allowed to be a + half-translation, i.e., a translation followed by a reflection in + a point (equivalently, a rotation by π.) + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_translation_surface() + False + sage: S.is_translation_surface(False) + True + + :: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.is_translation_surface() + True + + """ + from flatsurf.geometry.categories import TranslationSurfaces + + return TranslationSurfaces.ParentMethods._is_translation_surface( + self, positive=positive + ) + + def is_rational_surface(self): + r""" + Return whether this surface is a rational surface, i.e., the + rotational part of all gluings is a rational multiple of π. + + .. NOTE:: + + This method is used to determine whether this surface satisfies + the :class:`~.SimilaritySurfaces.Rational` axiom. Surfaces can + override this method to perform specialized logic, see the note + in :mod:`flatsurf.geometry.categories` for performance + considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_rational_surface() + True + + """ + if self.is_dilation_surface(positive=False): + return True + + return SimilaritySurfaces.Rational.ParentMethods._is_rational_surface(self) + + def _mul_(self, matrix, switch_sides=True): + r""" + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.infinite_staircase() + sage: s + The infinite staircase + sage: m=Matrix([[1,2],[0,1]]) + sage: s2=m*s + sage: TestSuite(s2).run() + sage: s2.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (3, 1), (2, 1)]) + + Testing multiplication by a matrix with negative determinant:: + + sage: from flatsurf import dilation_surfaces + sage: ds1 = dilation_surfaces.genus_two_square(1/2, 1/3, 1/4, 1/5) + sage: ds1.polygon(0) + Polygon(vertices=[(0, 0), (1/2, 0), (1, 1/3), (1, 1), (3/4, 1), (0, 4/5)]) + sage: m = matrix(QQ, [[0, 1], [1, 0]]) # maps (x,y) to (y, x) + sage: ds2 = m*ds1 + sage: ds2.polygon(0) + Polygon(vertices=[(0, 0), (4/5, 0), (1, 3/4), (1, 1), (1/3, 1), (0, 1/2)]) + """ + if not switch_sides: + raise NotImplementedError + + from sage.structure.element import is_Matrix + + if not is_Matrix(matrix): + raise NotImplementedError("only implemented for matrices") + if not matrix.dimensions != (2, 2): + raise NotImplementedError("only implemented for 2x2 matrices") + + from flatsurf.geometry.half_dilation_surface import GL2RImageSurface + + return GL2RImageSurface(self, matrix) + + class Oriented(SurfaceCategoryWithAxiom): + r""" + The category of oriented surfaces built from Euclidean polygons that + are glued by similarities with the orientation compatible with the + orientation of the real plane that polygons are defined in. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: SimilaritySurfaces().Oriented() + Category of oriented similarity surfaces + + """ + + class ParentMethods: + r""" + Provides methods available to all oriented surfaces that are built + from Euclidean polygons that are glued by similarities. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + @cached_method + def edge_matrix(self, p, e=None): + r""" + Returns the 2x2 matrix representing a similarity which when + applied to the polygon with label `p` makes it so the edge `e` + can be glued to its opposite edge by translation. + + If `e` is not provided, then `p` should be a pair consisting of + a polygon label and an edge. + + EXAMPLES:: + + sage: from flatsurf.geometry.similarity_surface_generators import SimilaritySurfaceGenerators + sage: s = SimilaritySurfaceGenerators.example() + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) + sage: s.polygon(1) + Polygon(vertices=[(0, 0), (2, 0), (1, 3)]) + sage: s.opposite_edge(0,0) + (1, 1) + sage: m = s.edge_matrix(0, 0) + sage: m + [ 1 1/2] + [-1/2 1] + sage: m * vector((2,-2)) == -vector((-1, 3)) + True + + """ + if e is None: + import warnings + + warnings.warn( + "passing only a single tuple argument to edge_matrix() has been deprecated and will be deprecated in a future version of sage-flatsurf; pass the label and edge index as separate arguments instead" + ) + p, e = p + + u = self.polygon(p).edge(e) + pp, ee = self.opposite_edge(p, e) + v = self.polygon(pp).edge(ee) + + # note the orientation, it is -v and not v + from flatsurf.geometry.similarity import similarity_from_vectors + from sage.matrix.matrix_space import MatrixSpace + + return similarity_from_vectors(u, -v, MatrixSpace(self.base_ring(), 2)) + + def _an_element_(self): + r""" + Return a point on this surface. + + EXAMPLES:: + + sage: from flatsurf.geometry.similarity_surface_generators import SimilaritySurfaceGenerators + sage: s = SimilaritySurfaceGenerators.example() + sage: s.an_element() + Point (4/3, -2/3) of polygon 0 + + :: + + sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface + + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + 0 + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + + sage: S.an_element() + Point (1/2, 1/2) of polygon 0 + + TESTS: + + Verify that this method works over non-fields (if 2 is + invertible):: + + sage: from flatsurf import similarity_surfaces + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: E = EuclideanPolygonsWithAngles((3, 3, 5)) + sage: from pyexactreal import ExactReals # optional: exactreal # random output due to pkg_resources deprecation warnings in some contexts + sage: R = ExactReals(E.base_ring()) # optional: exactreal + sage: angles = (3, 3, 5) + sage: slopes = EuclideanPolygonsWithAngles(*angles).slopes() + sage: P = Polygon(angles=angles, edges=[R.random_element() * slopes[0]]) # optional: exactreal + sage: S = similarity_surfaces.billiard(P) # optional: exactreal + sage: S.an_element() # optional: exactreal + Point ((1/2 ~ 0.50000000)*ℝ(0.303644…), 0) of polygon 0 + + """ + label = next(iter(self.labels())) + polygon = self.polygon(label) + + from sage.categories.all import Fields + + # We use a point that can be constructed without problems on an + # infinite surface. + if polygon.is_convex() and self.base_ring() in Fields(): + coordinates = polygon.centroid() + else: + # Sometimes, this is not implemented because it requires the edge + # transformation to be known, so we prefer the centroid. + coordinates = polygon.edge(0) / 2 + return self(label, coordinates) # pylint: disable=not-callable + + def underlying_surface(self): + r""" + Return this surface. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.underlying_surface() is S + doctest:warning + ... + UserWarning: underlying_surface() has been deprecated and will be removed in a future version of sage-flatsurf; this function has no effect anymore since there is no distinction between a surface and its underlying surface anymore + True + + """ + import warnings + + warnings.warn( + "underlying_surface() has been deprecated and will be removed in a future version of sage-flatsurf; this function has no effect anymore since there is no distinction between a surface and its underlying surface anymore" + ) + + return self + + def edge_transformation(self, p, e): + r""" + Return the similarity bringing the provided edge to the opposite edge. + + EXAMPLES:: + + sage: from flatsurf.geometry.similarity_surface_generators import SimilaritySurfaceGenerators + sage: s = SimilaritySurfaceGenerators.example() + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) + sage: s.polygon(1) + Polygon(vertices=[(0, 0), (2, 0), (1, 3)]) + sage: s.opposite_edge(0,0) + (1, 1) + sage: g = s.edge_transformation(0,0) + sage: g((0,0)) + (1, 3) + sage: g((2,-2)) + (2, 0) + + """ + from flatsurf.geometry.similarity import SimilarityGroup + + G = SimilarityGroup(self.base_ring()) + q = self.polygon(p) + a = q.vertex(e) + b = q.vertex(e + 1) + # This is the similarity carrying the origin to a and (1,0) to b: + g = G(b[0] - a[0], b[1] - a[1], a[0], a[1]) + + pp, ee = self.opposite_edge(p, e) + qq = self.polygon(pp) + # Be careful here: opposite vertices are identified + aa = qq.vertex(ee + 1) + bb = qq.vertex(ee) + # This is the similarity carrying the origin to aa and (1,0) to bb: + gg = G(bb[0] - aa[0], bb[1] - aa[1], aa[0], aa[1]) + + # This is the similarity carrying (a,b) to (aa,bb): + return gg / g + + def set_vertex_zero(self, label, v, in_place=False): + r""" + Applies a combinatorial rotation to the polygon with the provided label. + + This makes what is currently vertex v of this polygon vertex 0. In other words, + what is currently vertex (or edge) e will now become vertex (e-v)%n where + n is the number of sides of the polygon. + + For the updated polygons, the polygons will be translated so that vertex + 0 is the origin. + + EXAMPLES: + + Example with polygon glued to another polygon:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.veech_double_n_gon(4) + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: [s.opposite_edge(0,i) for i in range(4)] + [(1, 0), (1, 1), (1, 2), (1, 3)] + sage: ss = s.set_vertex_zero(0,1) + sage: ss.polygon(0) + Polygon(vertices=[(0, 0), (0, 1), (-1, 1), (-1, 0)]) + sage: [ss.opposite_edge(0,i) for i in range(4)] + [(1, 1), (1, 2), (1, 3), (1, 0)] + sage: TestSuite(ss).run() + + Example with polygon glued to self:: + + sage: s = translation_surfaces.veech_2n_gon(2) + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: [s.opposite_edge(0,i) for i in range(4)] + [(0, 2), (0, 3), (0, 0), (0, 1)] + sage: ss = s.set_vertex_zero(0,3) + sage: ss.polygon(0) + Polygon(vertices=[(0, 0), (0, -1), (1, -1), (1, 0)]) + sage: [ss.opposite_edge(0,i) for i in range(4)] + [(0, 2), (0, 3), (0, 0), (0, 1)] + sage: TestSuite(ss).run() + + """ + if in_place: + raise NotImplementedError( + "this surface does not support set_vertex_zero(mutable=True)" + ) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.set_vertex_zero(label, v, in_place=True) + s.set_immutable() + return s + + def relabel(self, relabeling_map, in_place=False): + r""" + Attempt to relabel the polygons according to a relabeling_map, which takes as input + a current label and outputs a new label for the same polygon. The method returns a pair + (surface,success) where surface is the relabeled surface, and success is a boolean value + indicating the success of the operation. The operation will fail if the implementation of the + underlying surface does not support labels used in the image of the relabeling map. In this case, + other (arbitrary) labels will be used to replace the labels of the surface, and the resulting + surface should still be okay. + + Currently, the relabeling_map must be a dictionary. + + If in_place is True then the relabeling is done to the current surface, otherwise a + mutable copy is made before relabeling. + + ToDo: + - Allow relabeling_map to be a function rather than just a dictionary. + This will allow it to work for infinite surfaces. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s=translation_surfaces.veech_double_n_gon(5) + sage: ss,valid=s.relabel({0:1, 1:2}) + sage: valid + True + sage: ss.root() + 1 + sage: ss.opposite_edge(1,0) + (2, 0) + sage: len(ss.polygons()) + 2 + sage: TestSuite(ss).run() + + """ + if in_place: + raise NotImplementedError( + "this surface does not implement relabel(in_place=True) yet" + ) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s, valid = s.relabel(relabeling_map=relabeling_map, in_place=True) + s.set_immutable() + return s, valid + + def copy( + self, + relabel=False, + mutable=False, + lazy=None, + new_field=None, + optimal_number_field=False, + ): + r""" + Returns a copy of this surface. The method takes several flags to modify how the copy is taken. + + If relabel is True, then instead of returning an exact copy, it returns a copy indexed by the + non-negative integers. This uses the Surface_list implementation. If relabel is False (default), + then we return an exact copy. The returned surface uses the Surface_dict implementation. + + The mutability flag returns if the resulting surface should be mutable or not. By default, the + resulting surface will not be mutable. + + If lazy is True, then the surface is copied by reference. This is the only type of copy + possible for infinite surfaces. The parameter defaults to False for finite surfaces, and + defaults to True for infinite surfaces. + + The new_field parameter can be used to place the vertices in a larger field than the basefield + for the original surface. + + The optimal_number_field option can be used to find a best NumberField containing the + (necessarily finite) surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: ss=translation_surfaces.ward(3) + sage: ss.is_mutable() + False + sage: s=ss.copy(mutable=True) + doctest:warning + ... + UserWarning: copy() has been deprecated and will be removed from a future version of sage-flatsurf; for surfaces of finite type use MutableOrientedSimilaritySurface.from_surface() instead. + sage: s.is_mutable() + True + sage: TestSuite(s).run() + sage: s == ss + False + + Changing the base field:: + + sage: s=translation_surfaces.veech_double_n_gon(5) + sage: ss=s.copy(mutable=False,new_field=AA) + doctest:warning + ... + UserWarning: copy() has been deprecated and will be removed from a future version of sage-flatsurf; for surfaces of finite type use MutableOrientedSimilaritySurface.from_surface() instead. + Use set_immutable() to make the resulting surface immutable. Use change_ring() to change the field over which the surface is defined. + sage: TestSuite(ss).run() + sage: ss.base_ring() + Algebraic Real Field + + Optimization of number field:: + + sage: s = translation_surfaces.arnoux_yoccoz(3) + sage: ss = s.copy(new_field=AA).copy(optimal_number_field=True) + doctest:warning + ... + UserWarning: copy() has been deprecated and will be removed from a future version of sage-flatsurf; for surfaces of finite type use MutableOrientedSimilaritySurface.from_surface() instead. + Use set_immutable() to make the resulting surface immutable. Use change_ring() to change the field over which the surface is defined. + doctest:warning + ... + UserWarning: copy() has been deprecated and will be removed from a future version of sage-flatsurf; for surfaces of finite type use MutableOrientedSimilaritySurface.from_surface() instead. + Use set_immutable() to make the resulting surface immutable. There is currently no replacement for optimal number field. + If you are relying on this features, let the authors of sage-flatsurf know and we will try to make it available again. + sage: TestSuite(ss).run() + sage: ss.base_ring().discriminant() + -44 + """ + message = "copy() has been deprecated and will be removed from a future version of sage-flatsurf; for surfaces of finite type use MutableOrientedSimilaritySurface.from_surface() instead." + + if not mutable: + message += ( + " Use set_immutable() to make the resulting surface immutable." + ) + + if relabel: + message += " Use relabel({old: new for (new, old) in enumerate(surface.labels())}) for integer labels." + + if not self.is_finite_type(): + message += " However, there is no immediate replacement for lazy copying of infinite surfaces. Have a look at the implementation of flatsurf.geometry.delaunay.LazyMutableSurface and adapt it to your needs." + + if new_field is not None: + message += " Use change_ring() to change the field over which the surface is defined." + + if optimal_number_field: + message += " There is currently no replacement for optimal number field. If you are relying on this features, let the authors of sage-flatsurf know and we will try to make it available again." + + import warnings + + warnings.warn(message) + + category = self.category() + s = None # This will be the surface we copy. (Likely we will set s=self below.) + if new_field is not None and optimal_number_field: + raise ValueError( + "You can not set a new_field and also set optimal_number_field=True." + ) + if optimal_number_field is True: + if not self.is_finite_type(): + raise NotImplementedError( + "can only optimize_number_field for a finite surface" + ) + if lazy: + raise NotImplementedError( + "lazy copying is unavailable when optimize_number_field=True" + ) + coordinates_AA = [] + for label, p in zip(self.labels(), self.polygons()): + for e in p.edges(): + coordinates_AA.append(AA(e[0])) + coordinates_AA.append(AA(e[1])) + from sage.rings.qqbar import number_field_elements_from_algebraics + + field, coordinates_NF, hom = number_field_elements_from_algebraics( + coordinates_AA, minimal=True + ) + if field is QQ: + new_field = QQ + # We pretend new_field = QQ was passed as a parameter. + # It will now get picked up by the "if new_field is not None:" line below. + else: + # Unfortunately field doesn't come with an real embedding (which is given by hom!) + # So, we make a copy of the field, and add the embedding. + from sage.all import NumberField + + field2 = NumberField( + field.polynomial(), name="a", embedding=hom(field.gen()) + ) + # The following converts from field to field2: + hom2 = field.hom(im_gens=[field2.gen()]) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + ss = MutableOrientedSimilaritySurface(field2) + index = 0 + + from flatsurf import Polygon + + for label, p in zip(self.labels(), self.polygons()): + new_edges = [] + for i in range(len(p.vertices())): + new_edges.append( + ( + hom2(coordinates_NF[index]), + hom2(coordinates_NF[index + 1]), + ) + ) + index += 2 + pp = Polygon(edges=new_edges, base_ring=field2) + ss.add_polygon(pp, label=label) + ss.set_roots(self.roots()) + for (l1, e1), (l2, e2) in self.gluings(): + ss.glue((l1, e1), (l2, e2)) + s = ss + if not relabel: + if not mutable: + s.set_immutable() + return s + # Otherwise we are supposed to relabel. We will make a relabeled copy of s below. + if new_field is not None: + s = self.change_ring(new_field) + if s is None: + s = self + if s.is_finite_type(): + if relabel: + from flatsurf.geometry.surface import Surface_list + + return Surface_list( + surface=s, + copy=not lazy, + mutable=mutable, + category=category, + deprecation_warning=False, + ) + else: + from flatsurf.geometry.surface import Surface_dict + + return Surface_dict( + surface=s, + copy=not lazy, + mutable=mutable, + category=category, + deprecation_warning=False, + ) + else: + if lazy is False: + raise ValueError( + "Only lazy copying available for infinite surfaces." + ) + if self.is_mutable(): + raise ValueError( + "An infinite surface can only be copied if it is immutable." + ) + if relabel: + from flatsurf.geometry.surface import Surface_list + + return Surface_list( + surface=s, + copy=False, + mutable=mutable, + category=category, + deprecation_warning=False, + ) + else: + from flatsurf.geometry.surface import Surface_dict + + return Surface_dict( + surface=s, + copy=False, + mutable=mutable, + category=category, + deprecation_warning=False, + ) + + def change_ring(self, ring): + r""" + Return a copy of this surface whose polygons are defined over + ``ring``. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.veech_2n_gon(4) + sage: T = S.change_ring(AA) + sage: T.base_ring() + Algebraic Real Field + + """ + from flatsurf.geometry.surface import BaseRingChangedSurface + + return BaseRingChangedSurface(self, ring) + + def triangle_flip(self, l1, e1, in_place=False, test=False, direction=None): + r""" + Flips the diagonal of the quadrilateral formed by two triangles + glued together along the provided edge (l1,e1). This can be broken + into two steps: join along the edge to form a convex quadilateral, + then cut along the other diagonal. Raises a ValueError if this + quadrilateral would be non-convex. In this case no changes to the + surface are made. + + The direction parameter defaults to (0,1). This is used to decide how + the triangles being glued in are labeled. Let p1 be the triangle + associated to label l1, and p2 be the triangle associated to l2 + but moved by a similarity to share the edge (l1,e1). Each triangle + has a exactly one separatrix leaving a vertex which travels in the + provided direction or its opposite. (For edges we only count as sepatrices + traveling counter-clockwise around the triangle.) This holds for p1 + and p2 and the separatrices must point in opposite directions. + + The above description gives two new triangles t1 and t2 which must be + glued in (obtained by flipping the diagonal of the quadrilateral). + Up to swapping t1 and t2 we can assume the separatrix in t1 in the + provided direction (or its opposite) points in the same direction as + that of p1. Further up to cyclic permutation of vertex labels we can + assume that the separatrices in p1 and t1 start at the vertex with the + same index (an element of {0,1,2}). The same can be done for p2 and t2. + We apply the label l1 to t1 and the label l2 to t2. This precisely + determines how t1 and t2 should be used to replace p1 and p2. + + INPUT: + + - ``l1`` - label of polygon + + - ``e1`` - (integer) edge of the polygon + + - ``in_place`` (boolean) - If True do the flip to the current surface + which must be mutable. In this case the updated surface will be + returned. Otherwise a mutable copy is made and then an edge is + flipped, which is then returned. + + - ``test`` (boolean) - If True we don't actually flip, and we return + True or False depending on whether or not the flip would be + successful. + + - ``direction`` (2-dimensional vector) - Defaults to (0,1). The choice + of this vector determines how the newly added triangles are labeled. + + EXAMPLES:: + + sage: from flatsurf import similarity_surfaces, MutableOrientedSimilaritySurface, Polygon + + sage: s = similarity_surfaces.right_angle_triangle(ZZ(1),ZZ(1)) + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) + sage: s.triangle_flip(0, 0, test=True) + False + sage: s.triangle_flip(0, 1, test=True) + True + sage: s.triangle_flip(0, 2, test=True) + False + + sage: s = similarity_surfaces.right_angle_triangle(ZZ(1),ZZ(1)) + sage: s = MutableOrientedSimilaritySurface.from_surface(s) + sage: s.triangle_flip(0, 0, in_place=True) + Traceback (most recent call last): + ... + ValueError: Gluing triangles along this edge yields a non-convex quadrilateral. + sage: s.triangle_flip(0,1,in_place=True) + Rational Cone Surface built from 2 isosceles triangles + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (1, 1), (0, 1)]) + sage: s.polygon(1) + Polygon(vertices=[(0, 0), (-1, -1), (0, -1)]) + sage: s.gluings() + (((0, 0), (1, 0)), ((0, 1), (0, 2)), ((0, 2), (0, 1)), ((1, 0), (0, 0)), ((1, 1), (1, 2)), ((1, 2), (1, 1))) + sage: s.triangle_flip(0,2,in_place=True) + Traceback (most recent call last): + ... + ValueError: Gluing triangles along this edge yields a non-convex quadrilateral. + + sage: p = Polygon(edges=[(2,0),(-1,3),(-1,-3)]) + sage: s = similarity_surfaces.self_glued_polygon(p) + sage: s = MutableOrientedSimilaritySurface.from_surface(s) + sage: s.triangle_flip(0,1,in_place=True) + Half-Translation Surface built from a triangle + + sage: s.set_immutable() + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: s in DilationSurfaces() + True + sage: s.labels() + (0,) + sage: s.polygons() + (Polygon(vertices=[(0, 0), (-3, -3), (-1, -3)]),) + sage: s.gluings() + (((0, 0), (0, 0)), ((0, 1), (0, 1)), ((0, 2), (0, 2))) + sage: TestSuite(s).run() + + """ + if test: + # Just test if the flip would be successful + p1 = self.polygon(l1) + if not len(p1.vertices()) == 3: + return False + l2, e2 = self.opposite_edge(l1, e1) + p2 = self.polygon(l2) + if not len(p2.vertices()) == 3: + return False + sim = self.edge_transformation(l2, e2) + hol = sim(p2.vertex((e2 + 2) % 3) - p1.vertex((e1 + 2) % 3)) + from flatsurf.geometry.euclidean import ccw + + return ( + ccw(p1.edge((e1 + 2) % 3), hol) > 0 + and ccw(p1.edge((e1 + 1) % 3), hol) > 0 + ) + + if in_place: + raise NotImplementedError( + "this surface does not support triangle_flip(in_place=True) yet" + ) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.triangle_flip( + l1=l1, e1=e1, in_place=True, test=test, direction=direction + ) + s.set_immutable() + return s + + def join_polygons(self, p1, e1, test=False, in_place=False): + r""" + Join polygons across the provided edge (p1,e1). By default, + it returns the surface obtained by joining the two polygons + together. It raises a ValueError if gluing the two polygons + together results in a non-convex polygon. This is done to the + current surface if in_place is True, and otherwise a mutable + copy is made and then modified. + + If test is True then instead of changing the surface, it just + checks to see if the change would be successful and returns + True if successful or False if not. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces, MutableOrientedSimilaritySurface + sage: ss = translation_surfaces.ward(3) + sage: s = MutableOrientedSimilaritySurface.from_surface(ss) + sage: s.join_polygons(0,0, in_place=True) + Translation Surface built from an equilateral triangle and a pentagon with 2 marked vertices + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (1, -a), (2, 0), (3, a), (2, 2*a), (0, 2*a), (-1, a)]) + sage: s.join_polygons(0,4, in_place=True) + Translation Surface built from a rhombus + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (1, -a), (2, 0), (3, a), (2, 2*a), (1, 3*a), (0, 2*a), (-1, a)]) + + TESTS:: + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: s.set_immutable() + sage: s in TranslationSurfaces() + True + + """ + if test: + in_place = False + + if in_place: + raise NotImplementedError( + "this surface does not implement join_polygons(in_place=True) yet" + ) + + if not test: + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.join_polygons(p1=p1, e1=e1, test=False, in_place=True) + s.set_immutable() + return s + + poly1 = self.polygon(p1) + p2, e2 = self.opposite_edge(p1, e1) + poly2 = self.polygon(p2) + + if p1 == p2: + return False + + t = self.edge_transformation(p2, e2) + dt = t.derivative() + es = [] + for i in range(e1): + es.append(poly1.edge(i)) + ne = len(poly2.vertices()) + for i in range(1, ne): + ee = (e2 + i) % ne + es.append(dt * poly2.edge(ee)) + for i in range(e1 + 1, len(poly1.vertices())): + es.append(poly1.edge(i)) + + try: + from flatsurf import Polygon + + Polygon(edges=es, base_ring=self.base_ring()) + except (ValueError, TypeError): + return False + + # Gluing would be successful + return True + + def subdivide_polygon(self, p, v1, v2, test=False, new_label=None): + r""" + Cut the polygon with label p along the diagonal joining vertex + v1 to vertex v2. This cuts p into two polygons, one will keep the same + label. The other will get a new label, which can be provided + via new_label. Otherwise a default new label will be provided. + If test=False, then the surface will be changed (in place). If + test=True, then it just checks to see if the change would be successful + + The convention is that the resulting subdivided polygon which has an oriented + edge going from the original vertex v1 to vertex v2 will keep the label p. + The other polygon will get a new label. + + The change will be done in place. + """ + if not test: + raise NotImplementedError( + "this surface does not implement subdivide_polygon(test=False) yet" + ) + + poly = self.polygon(p) + ne = len(poly.vertices()) + if v1 < 0 or v2 < 0 or v1 >= ne or v2 >= ne: + return False + if abs(v1 - v2) <= 1 or abs(v1 - v2) >= ne - 1: + return False + + return True + + def singularity(self, label, v, limit=None): + r""" + Represents the Singularity associated to the v-th vertex of the polygon + with label ``label``. + + If the surface is infinite, the limit can be set. In this case the + construction of the singularity is successful if the sequence of + vertices hit by passing through edges closes up in limit or less steps. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.square_torus() + sage: pc = s.minimal_cover(cover_type="planar") + sage: pc.singularity(pc.root(), 0) + doctest:warning + ... + UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead. + Vertex 0 of polygon (0, (x, y) |-> (x, y)) + sage: pc.singularity(pc.root(), 0, limit=1) + Traceback (most recent call last): + ... + ValueError: number of edges at singularity exceeds limit + + """ + from flatsurf.geometry.surface_objects import Singularity + + return Singularity(self, label, v, limit) + + def point(self, label, point, ring=None, limit=None): + r""" + Return a point in this surface. + + INPUT: + + - ``label`` - label of the polygon + + - ``point`` - coordinates of the point inside the polygon or + the index of the vertex of the polygon. + + - ``ring`` (optional) - a ring for the coordinates + + - ``limit`` (optional) - undocumented (only relevant if the point + corresponds to a singularity in an infinite surface) + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.square_torus() + sage: pc = s.minimal_cover(cover_type="planar") + sage: pc.point(pc.root(), (0, 0)) + Vertex 0 of polygon (0, (x, y) |-> (x, y)) + sage: pc.point(pc.root(), 0) + Vertex 0 of polygon (0, (x, y) |-> (x, y)) + sage: pc.point(pc.root(), 1) + Vertex 0 of polygon (0, (x, y) |-> (x + 1, y)) + sage: pc.point(pc.root(), (1, 1)) + Vertex 0 of polygon (0, (x, y) |-> (x + 1, y + 1)) + sage: z = pc.point(pc.root(),(sqrt(2)-1,sqrt(3)-1),ring=AA) + doctest:warning + ... + UserWarning: the ring parameter is deprecated and will be removed in a future version of sage-flatsurf; define the surface over a larger ring instead so that this points' coordinates live in the base ring + sage: next(iter(z.coordinates(next(iter(z.labels()))))).parent() + Vector space of dimension 2 over Algebraic Real Field + + :: + + sage: s = translation_surfaces.cathedral(2, 3) + sage: s.point(0, 0) + Vertex 0 of polygon 0 + sage: s.point(0, (0, 0)) + Vertex 0 of polygon 0 + sage: s.point(0, (1, 1)) + Point (1, 0) of polygon 0 + sage: s.point(0, 1) + Vertex 0 of polygon 1 + + """ + # pylint: disable-next=not-callable + return self(label, point, limit=limit, ring=ring) + + def surface_point(self, *args, **kwargs): + r""" + Return a point in this surface. + + This is an alias for :meth:`point`. + """ + import warnings + + warnings.warn( + "surface_point() has been deprecated and will be removed in a future version of sage-flatsurf; use point() instead" + ) + + return self.point(*args, **kwargs) + + def minimal_cover(self, cover_type="translation"): + r""" + Return the minimal translation or half-translation cover of the surface. + + Cover type may be either "translation", "half-translation" or "planar". + + The minimal planar cover of a surface S is the smallest cover C so that + the developing map from the universal cover U to the plane induces a + well defined map from C to the plane. This is an infinite translation + surface that is naturally a branched cover of the plane. + + EXAMPLES:: + + sage: from flatsurf import polygons, MutableOrientedSimilaritySurface + sage: s = MutableOrientedSimilaritySurface(QQ) + sage: square = polygons.square(base_ring=QQ) + sage: s.add_polygon(square) + 0 + sage: s.glue((0,0), (0,1)) + sage: s.glue((0,2) ,(0,3)) + sage: cs = s + sage: ts = cs.minimal_cover(cover_type="translation") + sage: ts + Minimal Translation Cover of Rational Cone Surface built from a square + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: ts in TranslationSurfaces() + True + sage: hts = cs.minimal_cover(cover_type="half-translation") + sage: hts + Minimal Half-Translation Cover of Genus 0 Rational Cone Surface built from a square + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces + sage: hts in HalfTranslationSurfaces() + True + sage: TestSuite(hts).run() + sage: ps = cs.minimal_cover(cover_type="planar") + sage: ps + Minimal Planar Cover of Genus 0 Rational Cone Surface built from a square + sage: ps in TranslationSurfaces() + True + sage: TestSuite(ps).run() + + sage: from flatsurf import similarity_surfaces + sage: S = similarity_surfaces.example() + sage: T = S.minimal_cover(cover_type="translation") + sage: T + Minimal Translation Cover of Genus 1 Surface built from 2 isosceles triangles + sage: T in TranslationSurfaces() + True + sage: T.polygon(T.root()) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) + + """ + if cover_type == "translation": + from flatsurf.geometry.minimal_cover import MinimalTranslationCover + + return MinimalTranslationCover(self) + + if cover_type == "half-translation": + from flatsurf.geometry.minimal_cover import ( + MinimalHalfTranslationCover, + ) + + return MinimalHalfTranslationCover(self) + + if cover_type == "planar": + from flatsurf.geometry.minimal_cover import MinimalPlanarCover + + return MinimalPlanarCover(self) + + raise ValueError("Provided cover_type is not supported.") + + def vector_space(self): + r""" + Return the vector space in which self naturally embeds. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.vector_space() + doctest:warning + ... + UserWarning: vector_space() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring()**2 or base_ring().fraction_field()**2 instead + Vector space of dimension 2 over Rational Field + + sage: S.base_ring()**2 + Vector space of dimension 2 over Rational Field + + """ + import warnings + + warnings.warn( + "vector_space() has been deprecated and will be removed in a future version of sage-flatsurf; use base_ring()**2 or base_ring().fraction_field()**2 instead" + ) + + from sage.modules.free_module import VectorSpace + + return VectorSpace(self.base_ring(), 2) + + @cached_method(key=lambda self, ring: ring or self.base_ring()) + def tangent_bundle(self, ring=None): + r""" + Return the tangent bundle + + INPUT: + + - ``ring`` -- an optional field (defaults to the coordinate field of the + surface) + """ + if ring is None: + ring = self.base_ring() + + if self.is_mutable(): + raise NotImplementedError( + "cannot compute the tangent bundle of a mutable surface" + ) + + from flatsurf.geometry.tangent_bundle import ( + SimilaritySurfaceTangentBundle, + ) + + return SimilaritySurfaceTangentBundle(self, ring) + + def tangent_vector(self, lab, p, v, ring=None): + r""" + Return a tangent vector. + + INPUT: + + - ``lab`` -- label of a polygon + + - ``p`` -- coordinates of a point in the polygon + + - ``v`` -- coordinates of a vector in R^2 + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import chamanara_surface + sage: S = chamanara_surface(1/2) + sage: S.tangent_vector(S.root(), (1/2,1/2), (1,1)) + SimilaritySurfaceTangentVector in polygon (1, -1, 0) based at (1/2, -3/2) with vector (1, 1) + sage: K. = QuadraticField(2) + sage: S.tangent_vector(S.root(), (1/2,1/2), (1,sqrt2), ring=K) + SimilaritySurfaceTangentVector in polygon (1, -1, 0) based at (1/2, -3/2) with vector (1, sqrt2) + """ + from sage.all import vector + + p = vector(p) + v = vector(v) + + if p.parent().dimension() != 2 or v.parent().dimension() != 2: + raise ValueError( + "p (={!r}) and v (={!v}) should have two coordinates" + ) + + return self.tangent_bundle(ring=ring)(lab, p, v) + + def triangulation_mapping(self): + r""" + Return a ``SurfaceMapping`` triangulating the surface + or ``None`` if the surface is already triangulated. + """ + from flatsurf.geometry.mappings import triangulation_mapping + + return triangulation_mapping(self) + + def triangulate(self, in_place=False, label=None, relabel=None): + r""" + Return a triangulated version of this surface. (This may be mutable + or not depending on the input.) + + If label=None (as default) all polygons are triangulated. Otherwise, + label should be a polygon label. In this case, just this polygon + is split into triangles. + + This is done in place if in_place is True (defaults to False). + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s=translation_surfaces.mcmullen_L(1,1,1,1) + sage: ss=s.triangulate() + sage: gs=ss.graphical_surface() + sage: gs.make_all_visible() + sage: gs + Graphical representation of Translation Surface in H_2(2) built from 6 isosceles triangles + + A non-strictly convex example that caused trouble: + + sage: from flatsurf import similarity_surfaces, Polygon + sage: s=similarity_surfaces.self_glued_polygon(Polygon(edges=[(1,1),(-3,-1),(1,0),(1,0)])) + sage: s=s.triangulate() + sage: len(s.polygon(0).vertices()) + 3 + """ + if relabel is not None: + import warnings + + warnings.warn( + "the relabel keyword argument of triangulate() is ignored, it has been deprecated and will be removed in a future version of sage-flatsurf" + ) + + if in_place: + raise NotImplementedError( + "this surface does not implement triangulate(in_place=True) yet" + ) + + if self.is_finite_type(): + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.triangulate(in_place=True, label=label, relabel=relabel) + s.set_immutable() + return s + + if label is not None: + raise NotImplementedError( + "triangulate(label=) not implemented for infinite type surfaces" + ) + + from flatsurf.geometry.delaunay import LazyTriangulatedSurface + + return LazyTriangulatedSurface(self) + + def _delaunay_edge_needs_flip(self, p1, e1): + r""" + Return whether edge ``e1`` of polygon ``p1`` should be flipped + to get closer to a Delaunay triangulated surface. + """ + p2, e2 = self.opposite_edge(p1, e1) + poly1 = self.polygon(p1) + poly2 = self.polygon(p2) + if len(poly1.vertices()) != 3 or len(poly2.vertices()) != 3: + raise ValueError("Edge must be adjacent to two triangles.") + from flatsurf.geometry.similarity import similarity_from_vectors + + sim1 = similarity_from_vectors(poly1.edge(e1 + 2), -poly1.edge(e1 + 1)) + sim2 = similarity_from_vectors(poly2.edge(e2 + 2), -poly2.edge(e2 + 1)) + sim = sim1 * sim2 + return sim[1][0] < 0 + + def _delaunay_edge_needs_join(self, p1, e1): + r""" + Return whether edge ``e1`` of polygon ``p1`` should be + eliminated and the polygons attached to it joined to get closer + to a Delaunay cell decomposition. + """ + p2, e2 = self.opposite_edge(p1, e1) + poly1 = self.polygon(p1) + poly2 = self.polygon(p2) + from flatsurf.geometry.similarity import similarity_from_vectors + + sim1 = similarity_from_vectors( + poly1.vertex(e1) - poly1.vertex(e1 + 2), -poly1.edge(e1 + 1) + ) + sim2 = similarity_from_vectors( + poly2.vertex(e2) - poly2.vertex(e2 + 2), -poly2.edge(e2 + 1) + ) + sim = sim1 * sim2 + + return sim[1][0] == 0 + + def is_delaunay_triangulated(self, limit=None): + r""" + Return whether the surface is triangulated and the + triangulation is Delaunay. + + INPUT: + + - ``limit`` -- an integer or ``None`` (default: ``None``); + check only ``limit`` many edges if set + + """ + if not self.is_finite_type() and limit is None: + raise NotImplementedError( + "a limit must be set for infinite surfaces." + ) + + count = 0 + + for (l1, e1), (l2, e2) in self.gluings(): + if limit is not None and count >= limit: + break + count += 1 + if len(self.polygon(l1).vertices()) != 3: + return False + if len(self.polygon(l2).vertices()) != 3: + return False + if self._delaunay_edge_needs_flip(l1, e1): + return False + + return True + + def is_delaunay_decomposed(self, limit=None): + r""" + Return if the decomposition of the surface into polygons is Delaunay. + + INPUT: + + - ``limit`` -- an integer or ``None`` (default: ``None``); + check only ``limit`` many polygons if set + + """ + if not self.is_finite_type() and limit is None: + raise NotImplementedError( + "a limit must be set for infinite surfaces." + ) + + count = 0 + + for l1, p1 in zip(self.labels(), self.polygons()): + if limit is not None and count >= limit: + break + + count += 1 + + try: + c1 = p1.circumscribing_circle() + except ValueError: + # p1 is not circumscribed + return False + + for e1 in range(len(p1.vertices())): + c2 = self.edge_transformation(l1, e1) * c1 + l2, e2 = self.opposite_edge(l1, e1) + if c2.point_position(self.polygon(l2).vertex(e2 + 2)) != -1: + # The circumscribed circle developed into the adjacent polygon + # contains a vertex in its interior or boundary. + return False + + return True + + def delaunay_triangulation( + self, + triangulated=False, + in_place=False, + direction=None, + relabel=None, + ): + r""" + Returns a Delaunay triangulation of a surface, or make some + triangle flips to get closer to the Delaunay decomposition. + + INPUT: + + - ``triangulated`` (boolean) - If true, the algorithm assumes the + surface is already triangulated. It does this without verification. + + - ``in_place`` (boolean) - If true, the triangulating and the + triangle flips are done in place. Otherwise, a mutable copy of the + surface is made. + + - ``direction`` (None or Vector) - with two entries in the base field + Used to determine labels when a pair of triangles is flipped. Each triangle + has a unique separatrix which points in the provided direction or its + negation. As such a vector determines a sign for each triangle. + A pair of adjacent triangles have opposite signs. Labels are chosen + so that this sign is preserved (as a function of labels). + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + + sage: m = matrix([[2,1],[1,1]]) + sage: s = m*translation_surfaces.infinite_staircase() + sage: ss = s.delaunay_triangulation() + sage: ss.root() + (0, (0, 1, 2)) + sage: ss.polygon((0, (0, 1, 2))) + Polygon(vertices=[(0, 0), (1, 0), (1, 1)]) + sage: TestSuite(ss).run() + sage: ss.is_delaunay_triangulated(limit=10) + True + """ + if in_place: + raise NotImplementedError( + "this surface does not implement delaunay_triangulation(in_place=True) yet" + ) + + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from delaunay_triangulation(); use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to delaunay_triangulation()" + ) + + if self.is_finite_type(): + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.delaunay_triangulation( + triangulated=triangulated, + in_place=True, + direction=direction, + relabel=relabel, + ) + s.set_immutable() + return s + + from flatsurf.geometry.delaunay import ( + LazyDelaunayTriangulatedSurface, + ) + + return LazyDelaunayTriangulatedSurface( + self, direction=direction, category=self.category() + ) + + def delaunay_decomposition( + self, + triangulated=False, + delaunay_triangulated=False, + in_place=False, + direction=None, + relabel=None, + ): + r""" + Return the Delaunay Decomposition of this surface. + + INPUT: + + - ``triangulated`` (boolean) - If true, the algorithm assumes the + surface is already triangulated. It does this without verification. + + - ``delaunay_triangulated`` (boolean) - If true, the algorithm assumes + the surface is already delaunay_triangulated. It does this without + verification. + + - ``in_place`` (boolean) - If true, the triangulating and the triangle + flips are done in place. Otherwise, a mutable copy of the surface is + made. + + - ``direction`` - (None or Vector with two entries in the base field) - + Used to determine labels when a pair of triangles is flipped. Each triangle + has a unique separatrix which points in the provided direction or its + negation. As such a vector determines a sign for each triangle. + A pair of adjacent triangles have opposite signs. Labels are chosen + so that this sign is preserved (as a function of labels). + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces, Polygon, similarity_surfaces + sage: s0 = translation_surfaces.octagon_and_squares() + sage: a = s0.base_ring().gens()[0] + sage: m = Matrix([[1,2+a],[0,1]]) + sage: s = m*s0 + sage: s = s.triangulate() + sage: ss = s.delaunay_decomposition(triangulated=True) + sage: len(ss.polygons()) + 3 + + sage: p = Polygon(edges=[(4,0),(-2,1),(-2,-1)]) + sage: s0 = similarity_surfaces.self_glued_polygon(p) + sage: s = s0.delaunay_decomposition() + sage: TestSuite(s).run() + + sage: m = matrix([[2,1],[1,1]]) + sage: s = m*translation_surfaces.infinite_staircase() + sage: ss = s.delaunay_decomposition() + sage: ss.root() + (0, (0, 1, 2)) + sage: ss.polygon(ss.root()) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: TestSuite(ss).run() + sage: ss.is_delaunay_decomposed(limit=10) + True + + """ + if in_place: + raise NotImplementedError( + "this surface does not implement delaunay_decomposition(in_place=True) yet" + ) + + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from delaunay_decomposition(); use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to delaunay_decomposition()" + ) + + if not self.is_finite_type(): + from flatsurf.geometry.delaunay import LazyDelaunaySurface + + return LazyDelaunaySurface( + self, direction=direction, category=self.category() + ) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.delaunay_decomposition( + triangulated=triangulated, + delaunay_triangulated=delaunay_triangulated, + in_place=True, + direction=direction, + relabel=relabel, + ) + s.set_immutable() + return s + + def saddle_connections( + self, + squared_length_bound, + initial_label=None, + initial_vertex=None, + sc_list=None, + check=False, + ): + r""" + Returns a list of saddle connections on the surface whose length squared is less than or equal to squared_length_bound. + The length of a saddle connection is measured using holonomy from polygon in which the trajectory starts. + + If initial_label and initial_vertex are not provided, we return all saddle connections satisfying the bound condition. + + If initial_label and initial_vertex are provided, it only provides saddle connections emanating from the corresponding + vertex of a polygon. If only initial_label is provided, the added saddle connections will only emanate from the + corresponding polygon. + + If sc_list is provided the found saddle connections are appended to this list and the resulting list is returned. + + If check==True it uses the checks in the SaddleConnection class to sanity check our results. + + EXAMPLES:: + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.square_torus() + sage: sc_list = s.saddle_connections(13, check=True) + sage: len(sc_list) + 32 + """ + if squared_length_bound <= 0: + raise ValueError + + if sc_list is None: + sc_list = [] + if initial_label is None: + if not self.is_finite_type(): + raise NotImplementedError + if initial_vertex is not None: + raise ValueError( + "when initial_label is not provided, then initial_vertex must not be provided either" + ) + for label in self.labels(): + self.saddle_connections( + squared_length_bound, initial_label=label, sc_list=sc_list + ) + return sc_list + if initial_vertex is None: + for vertex in range(len(self.polygon(initial_label).vertices())): + self.saddle_connections( + squared_length_bound, + initial_label=initial_label, + initial_vertex=vertex, + sc_list=sc_list, + ) + return sc_list + + # Now we have a specified initial_label and initial_vertex + from flatsurf.geometry.similarity import SimilarityGroup + + SG = SimilarityGroup(self.base_ring()) + start_data = (initial_label, initial_vertex) + from flatsurf.geometry.circle import Circle + + circle = Circle( + (0, 0), + squared_length_bound, + base_ring=self.base_ring(), + ) + p = self.polygon(initial_label) + v = p.vertex(initial_vertex) + last_sim = SG(-v[0], -v[1]) + + # First check the edge eminating rightward from the start_vertex. + e = p.edge(initial_vertex) + if e[0] ** 2 + e[1] ** 2 <= squared_length_bound: + from flatsurf.geometry.surface_objects import SaddleConnection + + sc_list.append(SaddleConnection(self, start_data, e)) + + # Represents the bounds of the beam of trajectories we are sending out. + wedge = ( + last_sim(p.vertex((initial_vertex + 1) % len(p.vertices()))), + last_sim( + p.vertex( + (initial_vertex + len(p.vertices()) - 1) % len(p.vertices()) + ) + ), + ) + + # This will collect the data we need for a depth first search. + chain = [ + ( + last_sim, + initial_label, + wedge, + [ + (initial_vertex + len(p.vertices()) - i) % len(p.vertices()) + for i in range(2, len(p.vertices())) + ], + ) + ] + + while len(chain) > 0: + # Should verts really be edges? + sim, label, wedge, verts = chain[-1] + if len(verts) == 0: + chain.pop() + continue + vert = verts.pop() + p = self.polygon(label) + # First check the vertex + vert_position = sim(p.vertex(vert)) + from flatsurf.geometry.euclidean import ccw + + if ( + ccw(wedge[0], vert_position) > 0 + and ccw(vert_position, wedge[1]) > 0 + and vert_position[0] ** 2 + vert_position[1] ** 2 + <= squared_length_bound + ): + sc_list.append( + SaddleConnection( + self, + start_data, + vert_position, + end_data=(label, vert), + end_direction=~sim.derivative() * -vert_position, + holonomy=vert_position, + end_holonomy=~sim.derivative() * -vert_position, + check=check, + ) + ) + # Now check if we should develop across the edge + vert_position2 = sim(p.vertex((vert + 1) % len(p.vertices()))) + if ( + ccw(vert_position, vert_position2) > 0 + and ccw(wedge[0], vert_position2) > 0 + and ccw(vert_position, wedge[1]) > 0 + and circle.line_segment_position(vert_position, vert_position2) + == 1 + ): + if ccw(wedge[0], vert_position) > 0: + # First in new_wedge should be vert_position + if ccw(vert_position2, wedge[1]) > 0: + new_wedge = (vert_position, vert_position2) + else: + new_wedge = (vert_position, wedge[1]) + else: + if ccw(vert_position2, wedge[1]) > 0: + new_wedge = (wedge[0], vert_position2) + else: + new_wedge = wedge + new_label, new_edge = self.opposite_edge(label, vert) + new_sim = sim * ~self.edge_transformation(label, vert) + p = self.polygon(new_label) + chain.append( + ( + new_sim, + new_label, + new_wedge, + [ + (new_edge + len(p.vertices()) - i) + % len(p.vertices()) + for i in range(1, len(p.vertices())) + ], + ) + ) + return sc_list + + def ramified_cover(self, d, data): + r""" + Return a ramified cover of this surface. + + INPUT: + + - ``d`` - integer (the degree of the cover) + + - ``data`` - a dictionary which to a pair ``(label, edge_num)`` associates a permutation + of {1,...,d} + + EXAMPLES: + + The L-shape origami:: + + sage: import flatsurf + sage: T = flatsurf.translation_surfaces.square_torus() + sage: T.ramified_cover(3, {(0,0): '(1,2)', (0,1): '(1,3)'}) + Translation Surface in H_2(2) built from 3 squares + sage: O = T.ramified_cover(3, {(0,0): '(1,2)', (0,1): '(1,3)'}) + sage: O.stratum() + H_2(2) + + TESTS:: + + sage: import flatsurf + sage: T = flatsurf.translation_surfaces.square_torus() + sage: T.ramified_cover(3, {(0,0): '(1,2)', (0,2): '(1,3)'}) + Traceback (most recent call last): + ... + ValueError: inconsistent covering data + + """ + from sage.groups.perm_gps.permgroup_named import SymmetricGroup + + G = SymmetricGroup(d) + for k in data: + data[k] = G(data[k]) + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + cover = MutableOrientedSimilaritySurface(self.base_ring()) + edges = set(self.edges()) + cover_labels = {} + for i in range(1, d + 1): + for lab in self.labels(): + cover_labels[(lab, i)] = cover.add_polygon(self.polygon(lab)) + while edges: + lab, e = elab = edges.pop() + llab, ee = eelab = self.opposite_edge(lab, e) + edges.remove(eelab) + if elab in data: + if eelab in data: + if not (data[elab] * data[eelab]).is_one(): + raise ValueError("inconsistent covering data") + s = data[elab] + elif eelab in data: + s = ~data[eelab] + else: + s = G.one() + + for i in range(1, d + 1): + p0 = cover_labels[(lab, i)] + p1 = cover_labels[(lab, s(i))] + cover.glue((p0, e), (p1, ee)) + cover.set_immutable() + return cover + + def subdivide(self): + r""" + Return a copy of this surface whose polygons have been partitioned into + smaller triangles with + :meth:`~.euclidean_polygons.EuclideanPolygons.Simple.Convex.ParentMethods.subdivide`. + + EXAMPLES: + + A surface consisting of a single triangle:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon + + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(Polygon(edges=[(1, 0), (0, 1), (-1, -1)]), label="Δ") + 'Δ' + + Subdivision of this surface yields a surface with three triangles:: + + sage: T = S.subdivide() + sage: T.labels() + (('Δ', 0), ('Δ', 1), ('Δ', 2)) + + Note that the new labels are old labels plus an index. We verify that + the triangles are glued correctly:: + + sage: list(T.gluings()) + [((('Δ', 0), 1), (('Δ', 1), 2)), + ((('Δ', 0), 2), (('Δ', 2), 1)), + ((('Δ', 1), 1), (('Δ', 2), 2)), + ((('Δ', 1), 2), (('Δ', 0), 1)), + ((('Δ', 2), 1), (('Δ', 0), 2)), + ((('Δ', 2), 2), (('Δ', 1), 1))] + + If we add another polygon to the original surface and glue things, we + can see how existing gluings are preserved when subdividing:: + + sage: S.add_polygon(Polygon(edges=[(1, 0), (0, 1), (-1, 0), (0, -1)]), label='□') + '□' + + sage: S.glue(("Δ", 0), ("□", 2)) + sage: S.glue(("□", 1), ("□", 3)) + + sage: T = S.subdivide() + + sage: T.labels() + (('Δ', 0), ('□', 2), ('Δ', 1), ('Δ', 2), ('□', 3), ('□', 1), ('□', 0)) + sage: list(sorted(T.gluings())) + [((('Δ', 0), 0), (('□', 2), 0)), + ((('Δ', 0), 1), (('Δ', 1), 2)), + ((('Δ', 0), 2), (('Δ', 2), 1)), + ((('Δ', 1), 1), (('Δ', 2), 2)), + ((('Δ', 1), 2), (('Δ', 0), 1)), + ((('Δ', 2), 1), (('Δ', 0), 2)), + ((('Δ', 2), 2), (('Δ', 1), 1)), + ((('□', 0), 1), (('□', 1), 2)), + ((('□', 0), 2), (('□', 3), 1)), + ((('□', 1), 0), (('□', 3), 0)), + ((('□', 1), 1), (('□', 2), 2)), + ((('□', 1), 2), (('□', 0), 1)), + ((('□', 2), 0), (('Δ', 0), 0)), + ((('□', 2), 1), (('□', 3), 2)), + ((('□', 2), 2), (('□', 1), 1)), + ((('□', 3), 0), (('□', 1), 0)), + ((('□', 3), 1), (('□', 0), 2)), + ((('□', 3), 2), (('□', 2), 1))] + + """ + labels = list(self.labels()) + polygons = [self.polygon(label) for label in labels] + + subdivisions = [p.subdivide() for p in polygons] + + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + surface = MutableOrientedSimilaritySurface(self.base()) + + # Add subdivided polygons + for s, subdivision in enumerate(subdivisions): + label = labels[s] + for p, polygon in enumerate(subdivision): + surface.add_polygon(polygon, label=(label, p)) + + surface.set_roots(((label, 0) for label in self.roots())) + + # Add gluings between subdivided polygons + for s, subdivision in enumerate(subdivisions): + label = labels[s] + for p in range(len(subdivision)): + surface.glue( + ((label, p), 1), ((label, (p + 1) % len(subdivision)), 2) + ) + + # Add gluing from original surface + opposite = self.opposite_edge(label, p) + if opposite is not None: + surface.glue(((label, p), 0), (opposite, 0)) + + return surface + + def subdivide_edges(self, parts=2): + r""" + Return a copy of this surface whose edges have been split into + ``parts`` equal pieces each. + + INPUT: + + - ``parts`` -- a positive integer (default: 2) + + EXAMPLES: + + A surface consisting of a single triangle:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: from flatsurf.geometry.polygon import Polygon + + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(Polygon(edges=[(1, 0), (0, 1), (-1, -1)]), label="Δ") + 'Δ' + + Subdividing this triangle yields a triangle with marked points along + the edges:: + + sage: T = S.subdivide_edges() + + If we add another polygon to the original surface and glue them, we + can see how existing gluings are preserved when subdividing:: + + sage: S.add_polygon(Polygon(edges=[(1, 0), (0, 1), (-1, 0), (0, -1)]), label='□') + '□' + + sage: S.glue(("Δ", 0), ("□", 2)) + sage: S.glue(("□", 1), ("□", 3)) + + sage: T = S.subdivide_edges() + sage: list(sorted(T.gluings())) + [(('Δ', 0), ('□', 5)), + (('Δ', 1), ('□', 4)), + (('□', 2), ('□', 7)), + (('□', 3), ('□', 6)), + (('□', 4), ('Δ', 1)), + (('□', 5), ('Δ', 0)), + (('□', 6), ('□', 3)), + (('□', 7), ('□', 2))] + + """ + labels = list(self.labels()) + polygons = [self.polygon(label) for label in labels] + + subdivideds = [p.subdivide_edges(parts=parts) for p in polygons] + + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + surface = MutableOrientedSimilaritySurface(self.base()) + + # Add subdivided polygons + for s, subdivided in enumerate(subdivideds): + surface.add_polygon(subdivided, label=labels[s]) + + surface.set_roots(self.roots()) + + # Reestablish gluings between polygons + for label, polygon, subdivided in zip(labels, polygons, subdivideds): + for e in range(len(polygon.vertices())): + opposite = self.opposite_edge(label, e) + if opposite is not None: + for p in range(parts): + surface.glue( + (label, e * parts + p), + ( + opposite[0], + opposite[1] * parts + (parts - p - 1), + ), + ) + + return surface + + class Rational(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by similarity surfaces where all similarities that + describe how edges are glued only use rational rotations, i.e., + rotations by a rational multiple of π. + + Note that this differs slightly from the usual definition of + "rational". Normally, a surface would be rational if it can be + described using only such similarities. Here we require that the + similarities used are actually of that kind. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: "Rational" in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces built from Euclidean + polygons glued by similarities that have rational monodromy, i.e., + `monodromy + `_ gives + similarities whose rotational part has finite order. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + @staticmethod + def _is_rational_surface(surface, limit=None): + r""" + Return whether the gluings of this surface lead to a rational + surface, i.e., whether all gluings use similarities whose + rotational part uses only a rational multiple of π as a + rotation. + + This is a helper method for + :meth:`flatsurf.geometry.categories.similarity_surfaces.ParentMethods.is_rational_surface`. + + INPUT: + + - ``limit`` -- an integer or ``None`` (default: ``None``); if set, only + the first ``limit`` polygons are checked + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: SimilaritySurfaces.Rational.ParentMethods._is_rational_surface(S, limit=8) + True + + """ + if "Oriented" not in surface.category().axioms(): + raise NotImplementedError + + labels = surface.labels() + + if limit is not None: + from itertools import islice + + labels = islice(labels, limit) + + checked = set() + + for label in labels: + for edge in range(len(surface.polygon(label).vertices())): + + cross = surface.opposite_edge(label, edge) + + if cross is None: + continue + + if cross in checked: + continue + + checked.add((label, edge)) + + # We do not call self.edge_matrix() since the surface might + # have overridden this (just returning the identity matrix e.g.) + # and we want to deduce the matrix from the attached polygon + # edges instead. + matrix = SimilaritySurfaces.Oriented.ParentMethods.edge_matrix.f( # pylint: disable=no-member + surface, label, edge + ) + + if matrix.is_diagonal(): + continue + + a = AA(matrix[0, 0]) + b = AA(matrix[1, 0]) + q = (a**2 + b**2).sqrt() + + from flatsurf.geometry.euclidean import ( + is_cosine_sine_of_rational, + ) + + if not is_cosine_sine_of_rational(a / q, b / q): + return False + + return True + + def is_rational_surface(self): + r""" + Return whether all edges of this surface are glued with + similarities whose rotational part is by a rational multiple of + π, i.e., return ``True`` since this is a rational surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.is_rational_surface() + True + + """ + return True + + def _test_rational_surface(self, **options): + r""" + Verify that this is a rational similarity surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S._test_rational_surface() + + """ + tester = self._tester(**options) + + limit = None + + if not self.is_finite_type(): + limit = 32 + + tester.assertTrue( + SimilaritySurfaces.Rational.ParentMethods._is_rational_surface( + self, limit=limit + ) + ) + + class FiniteType(SurfaceCategoryWithAxiom): + r""" + The category of surfaces built by gluing a finite number of Euclidean + polygons with similarities. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: "FiniteType" in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces that are built from + finitely many polygons in the real plane glued with similarities. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def num_singularities(self): + r""" + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + + sage: translation_surfaces.regular_octagon().num_singularities() + doctest:warning + ... + UserWarning: num_singularities() has been deprecated and will be removed in a future version of sage-flatsurf; use len(vertices()) instead + 1 + + sage: S = SymmetricGroup(4) + sage: r = S('(1,2)(3,4)') + sage: u = S('(2,3)') + sage: translation_surfaces.origami(r,u).num_singularities() + 2 + + sage: S = SymmetricGroup(8) + sage: r = S('(1,2,3,4,5,6,7,8)') + sage: u = S('(1,8,5,4)(2,3)(6,7)') + sage: translation_surfaces.origami(r,u).num_singularities() + 4 + """ + import warnings + + warnings.warn( + "num_singularities() has been deprecated and will be removed in a future version of sage-flatsurf; use len(vertices()) instead" + ) + + return len(self.vertices()) + + def _test_eq_surface(self, **options): + r""" + Verify that this surface follows our standards for equality of + surfaces. + + We want two surfaces to compare equal (`S == T`) iff they are + virtually indistinguishable; so without a lot of non-Pythonic + effort, you should not be able to tell them apart. They have + (virtually) the same type, are made from equally labeled + polygons with indistinguishable coordinates and equal gluings. + Any other data that was used when creating them should be + indistinguishable. They might of course live at different + memory addresses have differences in their internal caches and + representation but everything user-facing should be the same. + + People often want `==` to mean that the two surfaces are + isomorphic in some more-or-less strong sense. Such a notion for + `==` always leads to trouble down the road. The operator `==` + is used to identify surfaces in caches and identify surfaces in + sets. Sometimes "are isomorphic" is a good notion in such cases + but most of the time "are indistinguishable" is the much safer + default. Also, "are isomorphic" is often costly or, e.g. in the case + of infinite surfaces, not even decidable. + + Currently, we do treat two surfaces as equal even if they + differ by category because categories can presently be changed + for immutable surfaces (as more properties of the surface are + found.) + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_eq_surface() + + :meta public: + + """ + tester = self._tester(**options) + + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + copy = MutableOrientedSimilaritySurface.from_surface(self) + if not self.is_mutable(): + copy.set_immutable() + + if isinstance(self, MutableOrientedSimilaritySurface): + tester.assertEqual(self, copy) + tester.assertFalse(self != copy) + else: + tester.assertNotEqual(self, copy) + tester.assertTrue(self != copy) + + class Oriented(SurfaceCategoryWithAxiom): + r""" + The category of surfaces built from finitely many Euclidean + polygons glued with singularities with an orientation that is + compatible with the embedding that the polygons inherit from the + real plane. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (1,0), (1,1), (0,1)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: "Oriented" in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces that are built from + finitely many Euclidean polygons that are glued by similarities + and are oriented with the natural orientation of the polygons + in the real plane. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def reposition_polygons(self, in_place=False, relabel=None): + r""" + We choose a maximal tree in the dual graph of the decomposition into + polygons, and ensure that the gluings between two polygons joined by + an edge in this tree is by translation. + + This guarantees that the group generated by the edge identifications is + minimal among representations of the surface. In particular, if for instance + you have a translation surface which is anot representable as a translation + surface (because polygons are presented with rotations) then after this + change it will be representable as a translation surface. + """ + if in_place: + raise NotImplementedError( + "this surface does not implement reposition_polygons(in_place=True) yet" + ) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(self) + s.reposition_polygons(in_place=True, relabel=relabel) + s.set_immutable() + return s + + def standardize_polygons(self, in_place=False): + r""" + Return a surface with each polygon replaced with a new + polygon which differs by translation and reindexing. The + new polygon will have the property that vertex zero is the + origin, and all vertices lie either in the upper half + plane, or on the x-axis with non-negative x-coordinate. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s=translation_surfaces.veech_double_n_gon(4) + sage: s.polygon(1) + Polygon(vertices=[(0, 0), (-1, 0), (-1, -1), (0, -1)]) + sage: [s.opposite_edge(0,i) for i in range(4)] + [(1, 0), (1, 1), (1, 2), (1, 3)] + sage: ss=s.standardize_polygons() + sage: ss.polygon(1) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: [ss.opposite_edge(0,i) for i in range(4)] + [(1, 2), (1, 3), (1, 0), (1, 1)] + sage: TestSuite(ss).run() + + """ + if in_place: + raise NotImplementedError( + "cannot standardize polygons in_place anymore on this surface; use in_place=False to create a copy of the surface with standardized polygons" + ) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + S = MutableOrientedSimilaritySurface.from_surface( + self, category=self.category() + ) + S.standardize_polygons(in_place=True) + S.set_immutable() + return S + + def fundamental_group(self, base_label=None): + r""" + Return the fundamental group of this surface. + """ + if base_label is None: + base_label = self.root() + + from flatsurf.geometry.fundamental_group import FundamentalGroup + + return FundamentalGroup(self, base_label) + + class SubcategoryMethods: + def Rational(self): + r""" + Return the subcategory of surfaces with rational monodromy, see + :class:`~.SimilaritySurfaces.Rational`. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: C = SimilaritySurfaces() + sage: C.Rational() + Category of rational similarity surfaces + + """ + return self._with_axiom("Rational") + + +# Currently, there is no "Rational" axiom in SageMath so we make it known to +# the category framework. +all_axioms += ("Rational",) diff --git a/flatsurf/geometry/categories/surface_category.py b/flatsurf/geometry/categories/surface_category.py new file mode 100644 index 000000000..cc1547b25 --- /dev/null +++ b/flatsurf/geometry/categories/surface_category.py @@ -0,0 +1,73 @@ +r""" +Category types for surfaces. + +This module provides alternative implementations for the SageMath types +``Category`` and ``CategoryWithAxiom``. It patches the ``_cmp_key`` of these +types to produce a more stable sorting of surface categories exactly in the +same way that SageMath does for its builtin categories. + +Without this, the MRO of surfaces is session dependent and in particular we get +somewhat random printing of categories such as translation surfaces. + +While we do not claim to actually understand all the details here, you can +consult ``c3_controlled.py`` in SageMath for all the (rather technical) +details. +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +import sage.categories.category +import sage.categories.category_with_axiom +from sage.misc.c3_controlled import _cmp_key + +flags = { + atom: 1 << (30 - i) + for i, atom in enumerate( + [ + "TopologicalSurfaces", + "PolygonalSurfaces", + "EuclideanPolygonalSurfaces", + "SimilaritySurfaces", + "ConeSurfaces", + "DilationSurfaces", + "HalfTranslationSurfaces", + "TranslationSurfaces", + ] + ) +} + + +class SurfaceCmpKey: + def __get__(self, instance, cls): + (flag, counter) = instance._cmp_key_vanilla + flag |= flags.get(cls.__base__.__name__, 0) + for cat in instance._super_categories: + flag |= cat._cmp_key[0] + instance._cmp_key = (flag, counter) + return flag, counter + + +class SurfaceCategory(sage.categories.category.Category): + _cmp_key_vanilla = _cmp_key + _cmp_key = SurfaceCmpKey() + + +class SurfaceCategoryWithAxiom(sage.categories.category_with_axiom.CategoryWithAxiom): + _cmp_key_vanilla = _cmp_key + _cmp_key = SurfaceCmpKey() diff --git a/flatsurf/geometry/categories/topological_surfaces.py b/flatsurf/geometry/categories/topological_surfaces.py new file mode 100644 index 000000000..fbe44058a --- /dev/null +++ b/flatsurf/geometry/categories/topological_surfaces.py @@ -0,0 +1,567 @@ +r""" +The category of topological surfaces. + +This module provides a base category for all surfaces in sage-flatsurf. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: S in TopologicalSurfaces() + True + +""" +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** + +from sage.categories.category_with_axiom import all_axioms +from sage.categories.topological_spaces import TopologicalSpaces +from sage.misc.abstract_method import abstract_method +from flatsurf.geometry.categories.surface_category import ( + SurfaceCategory, + SurfaceCategoryWithAxiom, +) + + +class TopologicalSurfaces(SurfaceCategory): + r""" + The category of topological surfaces, i.e., surfaces that are locally + homeomorphic to the real plane or the closed upper half plane. + + This category does not provide much functionality but just a common base + for all the other categories defined in sage-flatsurf. + + In particular, this does not require a topology since there is no general + concept of open subsets of a surface in sage-flatsurf. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: TopologicalSurfaces() + Category of topological surfaces + + """ + + def super_categories(self): + r""" + Return the categories a topological surface is also a member of. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: TopologicalSurfaces().super_categories() + [Category of topological spaces] + + """ + return [TopologicalSpaces()] + + class ParentMethods: + r""" + Provides methods available to all surfaces in sage-flatsurf. + + If you want to add functionality for all surfaces you most likely want + to put it here. + """ + + def refined_category(self): + r""" + Return the smallest subcategory that this surface is in. + + The result of this method can be fed to ``_refine_category_`` to + change the category of the surface (and enable functionality + specific to the smaller classes of surfaces.) + + Note that this method does have much effect for a general + topological surface. Subcategories and implementations of surfaces + should override this method to derive more features. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square(), label=0) + 0 + sage: S.refined_category() + Category of connected with boundary finite type translation surfaces + + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + sage: S.refined_category() + Category of connected without boundary finite type translation surfaces + + """ + category = self.category() + + if self.is_orientable(): + category &= category.Orientable() + + if self.is_with_boundary(): + category &= category.WithBoundary() + else: + category &= category.WithoutBoundary() + + if self.is_compact(): + category &= category.Compact() + + if self.is_connected(): + category &= category.Connected() + + return category + + def _test_refined_category(self, **options): + r""" + Verify that all (immutable) surfaces are contained in their refined + category automatically. + + To pass this test, surfaces should either set their ``category`` + explicitly or ensure to run `_refine_category_(refined_category())` + at some point. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_refined_category() + + """ + tester = self._tester(**options) + + tester.assertTrue(self.category().is_subcategory(self.refined_category())) + + @abstract_method + def is_mutable(self): + r""" + Return whether this surface allows modifications. + + All surfaces in sage-flatsurf must implement this method. + + .. NOTE:: + + We do not specify the interface of such mutations. Any mutable + surface should come up with a good interface for its use case. The + point of this method is to signal that is likely unsafe to use this + surface in caches (since it might change later) and that the + category of the surface might still change. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S._test_refined_category() + """ + + @abstract_method + def is_orientable(self): + r""" + Return whether this surface is orientable. + + All surfaces in sage-flatsurf must implement this method. + + .. NOTE:: + + This method is used by :meth:`refined_category` to determine + whether this surface satisfies the axiom + :class:`.TopologicalSurfaces.Orientable`. Surfaces must + override this method to perform specialized logic, see the note + in :mod:`flatsurf.geometry.categories` for performance + considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_orientable() + True + + """ + + @abstract_method + def is_with_boundary(self): + r""" + Return whether this a topological surface with boundary. + + All surfaces in sage-flatsurf must implement this method. + + .. NOTE:: + + This method is used by :meth:`refined_category` to determine + whether this surface satisfies the axiom :class:`.WithBoundary` + or :class:`.TopologicalSurfaces.WithoutBoundary`. Surfaces must + override this method to perform specialized logic, see the note + in :mod:`flatsurf.geometry.categories` for performance + considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_with_boundary() + False + + """ + + @abstract_method + def is_compact(self): + r""" + Return whether this surface is compact. + + All surfaces in sage-flatsurf must implement this method. + + .. NOTE:: + + This method is used by :meth:`refined_category` to determine + whether this surface satisfies the axiom of compactness. + Surfaces can override this method to perform specialized logic, + see the note in :mod:`flatsurf.geometry.categories` for + performance considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_compact() + True + + """ + + @abstract_method + def is_connected(self): + r""" + Return whether this surface is connected. + + All surfaces in sage-flatsurf must implement this method. + + .. NOTE:: + + This method is used by :meth:`refined_category` to determine + whether this surface satisfies the axiom of connectedness. + Surfaces can override this method to perform specialized logic, + see the note in :mod:`flatsurf.geometry.categories` for + performance considerations. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_connected() + True + + """ + + @abstract_method(optional=True) + def genus(self): + r""" + Return the genus of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: translation_surfaces.octagon_and_squares().genus() + 3 + + """ + + class Orientable(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces that can be oriented. + + As of 2023, all surfaces in sage-flatsurf satisfy this axiom. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: 'Orientable' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all orientable surfaces in + sage-flatsurf. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def is_orientable(self): + r""" + Return whether this surface is orientable, i.e., return ``True``. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_orientable() + True + + """ + return True + + class WithBoundary(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces that have a boundary, i.e., at some + points this surface is homeomorphic to the closed upper half plane. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square(), label=0) + 0 + sage: S.set_immutable() + sage: 'WithBoundary' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces with boundary in + sage-flatsurf. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def is_with_boundary(self): + r""" + Return whether this is a surface with boundary, i.e., return ``True``. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square(), label=0) + 0 + sage: S.set_immutable() + sage: S.is_with_boundary() + True + + """ + return True + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + An impossible category, the surfaces with and without boundary. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: C = TopologicalSurfaces() + sage: C.WithBoundary().WithoutBoundary() + Traceback (most recent call last): + ... + TypeError: a surface cannot be both with and without boundary + sage: C.WithoutBoundary().WithBoundary() + Traceback (most recent call last): + ... + TypeError: a surface cannot be both with and without boundary + + """ + + def __init__(self, *args, **kwargs): + raise TypeError("a surface cannot be both with and without boundary") + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces that have no boundary, i.e., the + surface is everywhere homeomorphic to the real plane. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: 'WithoutBoundary' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all surfaces without boundary in + sage-flatsurf. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def is_with_boundary(self): + r""" + Return whether this is a surface with boundary, i.e., return ``False``. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_with_boundary() + False + + """ + return False + + class Connected(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces that are topologically connected. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: 'Connected' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all connected surfaces in + sage-flatsurf. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def is_connected(self): + r""" + Return whether this surface is connected, i.e., return + ``True``. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_connected() + True + + """ + return True + + class Compact(SurfaceCategoryWithAxiom): + r""" + The axiom satisfied by surfaces that are compact as topological spaces. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: 'Compact' in S.category().axioms() + True + + """ + + class ParentMethods: + r""" + Provides methods available to all compact surfaces in + sage-flatsurf. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def is_compact(self): + r""" + Return whether this surface is compact, i.e., return ``True``. + + EXAMPLES:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(vertices=[(0,0), (2,0), (1,4), (0,5)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + sage: S.is_compact() + True + + """ + return True + + class SubcategoryMethods: + def Orientable(self): + r""" + Return the subcategory of surfaces that can be oriented. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: TopologicalSurfaces().Orientable() + Category of orientable topological surfaces + + """ + return self._with_axiom("Orientable") + + def WithBoundary(self): + r""" + Return the subcategory of surfaces that have a boundary, i.e., + points at which they are homeomorphic to the closed upper half + plane. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: TopologicalSurfaces().WithBoundary() + Category of with boundary topological surfaces + + """ + return self._with_axiom("WithBoundary") + + def WithoutBoundary(self): + r""" + Return the subcategory of surfaces that have no boundary, i.e., + they are everywhere isomorphic to the real plane. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TopologicalSurfaces + sage: TopologicalSurfaces().WithoutBoundary() + Category of without boundary topological surfaces + + """ + return self._with_axiom("WithoutBoundary") + + +# Currently, there is no "Orientable", "WithBoundary", and "WithoutBoundary" +# axiom in SageMath so we make it known to the category framework. +all_axioms += ("Orientable", "WithBoundary", "WithoutBoundary") diff --git a/flatsurf/geometry/categories/translation_surfaces.py b/flatsurf/geometry/categories/translation_surfaces.py new file mode 100644 index 000000000..8c8d8f2b6 --- /dev/null +++ b/flatsurf/geometry/categories/translation_surfaces.py @@ -0,0 +1,716 @@ +r""" +The category of translation surfaces. + +This module provides shared functionality for all surfaces in sage-flatsurf +that are built from Euclidean polygons whose glued edges can be transformed +into each other with translations. + +See :mod:`flatsurf.geometry.categories` for a general description of the +category framework in sage-flatsurf. + +Normally, you won't create this (or any other) category directly. The correct +category is automatically determined for immutable surfaces. + +EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: C = S.category() + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: C.is_subcategory(TranslationSurfaces()) + True + +""" +# #################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# #################################################################### + +from flatsurf.geometry.categories.surface_category import SurfaceCategoryWithAxiom +from flatsurf.geometry.categories.half_translation_surfaces import ( + HalfTranslationSurfaces, +) + + +class TranslationSurfaces(SurfaceCategoryWithAxiom): + r""" + The category of surfaces built by gluing (Euclidean) polygons with + translations. + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: TranslationSurfaces() + Category of translation surfaces + + """ + # The category of translation surfaces is identical to the category of + # half-translation surfaces with the positive axiom. + _base_category_class_and_axiom = (HalfTranslationSurfaces, "Positive") + + def extra_super_categories(self): + r""" + Return the other categories that a translation surface is automatically + a member of (apart from being a positive half-translation surface, its + orientation is compatible with the orientation of the polygons in the + real plane, so it's "oriented.") + + EXAMPLES:: + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: C = TranslationSurfaces() + sage: C.extra_super_categories() + (Category of oriented polygonal surfaces,) + + """ + from flatsurf.geometry.categories.polygonal_surfaces import PolygonalSurfaces + + return (PolygonalSurfaces().Oriented(),) + + class ParentMethods: + r""" + Provides methods available to all translation surfaces in + sage-flatsurf. + + If you want to add functionality for such surfaces you most likely want + to put it here. + """ + + def is_translation_surface(self, positive=True): + r""" + Return whether this surface is a translation surface, i.e., return + ``True``. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.is_translation_surface(positive=True) + True + sage: S.is_translation_surface(positive=False) + True + + """ + return True + + @staticmethod + def _is_translation_surface(surface, positive=True, limit=None): + r""" + Return whether ``surface`` is a translation surface by checking how its + polygons are glued. + + This is a helper method for + :meth:`flatsurf.geometry.categories.similarity_surfaces.ParentMethods.is_translation_surface. + + INPUT: + + - ``surface`` -- an oriented similarity surface + + - ``positive`` -- a boolean (default: ``True``); whether the + transformation must be a translation or is allowed to be a + half-translation, i.e., a translation followed by a reflection in + a point (equivalently, a rotation by π.) + + - ``limit`` -- an integer or ``None`` (default: ``None``); if set, only + the first ``limit`` polygons are checked + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: TranslationSurfaces.ParentMethods._is_translation_surface(S, limit=8) + True + + :: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(edges=[(2, 0),(-1, 3),(-1, -3)]) + sage: S = similarity_surfaces.self_glued_polygon(P) + + sage: TranslationSurfaces.ParentMethods._is_translation_surface(S) + False + sage: TranslationSurfaces.ParentMethods._is_translation_surface(S, positive=False) + True + + """ + if "Oriented" not in surface.category().axioms(): + raise NotImplementedError( + "cannot decide whether a non-oriented surface is a translation surface yet" + ) + + labels = surface.labels() + + if limit is not None: + from itertools import islice + + labels = islice(labels, limit) + + checked = set() + + for label in labels: + for edge in range(len(surface.polygon(label).vertices())): + cross = surface.opposite_edge(label, edge) + + if cross is None: + continue + + if cross in checked: + continue + + checked.add((label, edge)) + + # We do not call self.edge_matrix() since the surface might + # have overridden this (just returning the identity matrix e.g.) + # and we want to deduce the matrix from the attached polygon + # edges instead. + from flatsurf.geometry.categories import SimilaritySurfaces + + matrix = SimilaritySurfaces.Oriented.ParentMethods.edge_matrix.f( # pylint: disable=no-member + surface, label, edge + ) + + if not matrix.is_diagonal(): + return False + + if matrix[0][0] == 1 and matrix[1][1] == 1: + continue + + if matrix[0][0] == -1 and matrix[1][1] == -1: + if not positive: + continue + + return False + + return True + + def minimal_translation_cover(self): + r""" + Return the minimal cover of this surface that makes this surface a + translation surface, i.e., return this surface itself. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.minimal_translation_cover() is S + True + + """ + return self + + def edge_matrix(self, p, e=None): + r""" + Returns the 2x2 matrix representing a similarity which when + applied to the polygon with label `p` makes it so the edge `e` + can be glued to its opposite edge by translation. + + Since this is a translation surface, this is just the identity. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.edge_matrix(0, 0) + [1 0] + [0 1] + + """ + if e is None: + import warnings + + warnings.warn( + "passing only a single tuple argument to edge_matrix() has been deprecated and will be deprecated in a future version of sage-flatsurf; pass the label and edge index as separate arguments instead" + ) + p, e = p + + if e < 0 or e >= len(self.polygon(p).vertices()): + raise ValueError("invalid edge index for this polygon") + + from sage.all import identity_matrix + + return identity_matrix(self.base_ring(), 2) + + def canonicalize_mapping(self): + r""" + Return a SurfaceMapping canonicalizing this translation surface. + """ + from flatsurf.geometry.mappings import ( + canonicalize_translation_surface_mapping, + ) + + return canonicalize_translation_surface_mapping(self) + + def rel_deformation(self, deformation, local=False, limit=100): + r""" + Perform a rel deformation of the surface and return the result. + + This algorithm currently assumes that all polygons affected by this deformation are + triangles. That should be fixable in the future. + + INPUT: + + - ``deformation`` (dictionary) - A dictionary mapping singularities of + the surface to deformation vectors (in some 2-dimensional vector + space). The rel deformation being done will move the singularities + (relative to each other) linearly to the provided vector for each + vertex. If a singularity is not included in the dictionary then the + vector will be treated as zero. + + - ``local`` - (boolean) - If true, the algorithm attempts to deform all + the triangles making up the surface without destroying any of them. + So, the area of the triangle must be positive along the full interval + of time of the deformation. If false, then the deformation must have + a particular form: all vectors for the deformation must be parallel. + In this case we achieve the deformation with the help of the SL(2,R) + action and Delaunay triangulations. + + - ``limit`` (integer) - Restricts the length of the size of SL(2,R) + deformations considered. The algorithm should be roughly worst time + linear in limit. + + .. TODO:: + + - Support arbitrary rel deformations. + - Remove the requirement that triangles be used. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.arnoux_yoccoz(4) + sage: field = s.base_ring() + sage: a = field.gen() + sage: V = VectorSpace(field,2) + sage: deformation1 = {s.singularity(0,0):V((1,0))} + doctest:warning + ... + UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead. + sage: s1 = s.rel_deformation(deformation1).canonicalize() # long time (.8s) + sage: deformation2 = {s.singularity(0,0):V((a,0))} # long time (see above) + sage: s2 = s.rel_deformation(deformation2).canonicalize() # long time (.6s) + sage: m = Matrix([[a,0],[0,~a]]) + sage: s2.cmp((m*s1).canonicalize()) # long time (see above) + 0 + + """ + s = self + # Find a common field + field = s.base_ring() + for singularity, v in deformation.items(): + if v.parent().base_field() != field: + from sage.structure.element import get_coercion_model + + cm = get_coercion_model() + field = cm.common_parent(field, v.parent().base_field()) + from sage.modules.free_module import VectorSpace + + vector_space = VectorSpace(field, 2) + + from collections import defaultdict + + vertex_deformation = defaultdict( + vector_space.zero + ) # dictionary associating the vertices. + deformed_labels = set() # list of polygon labels being deformed. + + for singularity, vect in deformation.items(): + for label, coordinates in singularity.representatives(): + v = self.polygon(label).get_point_position(coordinates).get_vertex() + vertex_deformation[(label, v)] = vect + deformed_labels.add(label) + assert len(s.polygon(label).vertices()) == 3 + + from flatsurf.geometry.euclidean import ccw + + if local: + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + ss = MutableOrientedSimilaritySurface.from_surface(s) + ss.set_immutable() + ss = MutableOrientedSimilaritySurface.from_surface( + ss.change_ring(field) + ) + us = ss + + for label in deformed_labels: + polygon = s.polygon(label) + a0 = vector_space(polygon.vertex(1)) + b0 = vector_space(polygon.vertex(2)) + v0 = vector_space(vertex_deformation[(label, 0)]) + v1 = vector_space(vertex_deformation[(label, 1)]) + v2 = vector_space(vertex_deformation[(label, 2)]) + a1 = v1 - v0 + b1 = v2 - v0 + # We deform by changing the triangle so that its vertices 1 and 2 have the form + # a0+t*a1 and b0+t*b1 + # respectively. We are deforming from t=0 to t=1. + # We worry that the triangle degenerates along the way. + # The area of the deforming triangle has the form + # A0 + A1*t + A2*t^2. + A0 = ccw(a0, b0) + A1 = ccw(a0, b1) + ccw(a1, b0) + A2 = ccw(a1, b1) + if A2: + # Critical point of area function + c = A1 / (-2 * A2) + if field.zero() < c and c < 1: + if A0 + A1 * c + A2 * c**2 <= 0: + raise ValueError( + "Triangle with label %r degenerates at critical point before endpoint" + % label + ) + if A0 + A1 + A2 <= field.zero(): + raise ValueError( + "Triangle with label %r degenerates at or before endpoint" + % label + ) + # Triangle does not degenerate. + from flatsurf import Polygon + + us.replace_polygon( + label, + Polygon( + vertices=[vector_space.zero(), a0 + a1, b0 + b1], + base_ring=field, + ), + ) + ss.set_immutable() + return ss + + else: # Non local deformation + # We can only do this deformation if all the rel vector are parallel. + # Check for this. + nonzero = None + for singularity, vect in deformation.items(): + vvect = vector_space(vect) + if vvect != vector_space.zero(): + if nonzero is None: + nonzero = vvect + else: + assert ( + ccw(nonzero, vvect) == 0 + ), "In non-local deformation all deformation vectos must be parallel" + assert nonzero is not None, "Deformation appears to be trivial." + from sage.matrix.constructor import Matrix + + m = Matrix([[nonzero[0], -nonzero[1]], [nonzero[1], nonzero[0]]]) + mi = ~m + g = Matrix([[1, 0], [0, 2]], ring=field) + prod = m * g * mi + ss = None + k = 0 + while True: + if ss is None: + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + ss = MutableOrientedSimilaritySurface.from_surface( + s.change_ring(field), + category=TranslationSurfaces(), + ) + else: + # In place matrix deformation + ss.apply_matrix(prod) + ss.delaunay_triangulation(direction=nonzero, in_place=True) + deformation2 = {} + for singularity, vect in deformation.items(): + found_start = None + for label, coordinates in singularity.representatives(): + v = ( + s.polygon(label) + .get_point_position(coordinates) + .get_vertex() + ) + if ( + ccw(s.polygon(label).edge(v), nonzero) >= 0 + and ccw(nonzero, -s.polygon(label).edge((v + 2) % 3)) + > 0 + ): + found_start = (label, v) + found = None + for vv in range(3): + if ( + ccw(ss.polygon(label).edge(vv), nonzero) >= 0 + and ccw( + nonzero, + -ss.polygon(label).edge((vv + 2) % 3), + ) + > 0 + ): + found = vv + deformation2[ + ss.point( + label, ss.polygon(label).vertex(vv) + ) + ] = vect + break + assert found is not None + break + assert found_start is not None + + try: + sss = ss.rel_deformation(deformation2, local=True) + except ValueError: + k += 1 + if limit is not None and k >= limit: + raise Exception("exceeded limit iterations") + continue + + sss = sss.apply_matrix(mi * g ** (-k) * m, in_place=False) + return sss.delaunay_triangulation(direction=nonzero) + + def j_invariant(self): + r""" + Return the Kenyon-Smillie J-invariant of this translation surface. + + It is assumed that the coordinates are defined over a number field. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: O = translation_surfaces.regular_octagon() + sage: O.j_invariant() + ( + [2 2] + (0), (0), [2 1] + ) + """ + it = iter(self.labels()) + lab = next(it) + P = self.polygon(lab) + Jxx, Jyy, Jxy = P.j_invariant() + for lab in it: + xx, yy, xy = self.polygon(lab).j_invariant() + Jxx += xx + Jyy += yy + Jxy += xy + return (Jxx, Jyy, Jxy) + + def erase_marked_points(self): + r""" + Return an isometric or similar surface with a minimal number of regular + vertices of angle 2π. + + EXAMPLES:: + + sage: import flatsurf + + sage: G = SymmetricGroup(4) + sage: S = flatsurf.translation_surfaces.origami(G('(1,2,3,4)'), G('(1,4,2,3)')) + sage: S.stratum() + H_2(2, 0) + sage: S.erase_marked_points().stratum() # optional: pyflatsurf # long time (1s) # random output due to matplotlib warnings with some combinations of setuptools and matplotlib + H_2(2) + + sage: for (a,b,c) in [(1,4,11), (1,4,15), (3,4,13)]: # long time (10s), optional: pyflatsurf + ....: T = flatsurf.polygons.triangle(a,b,c) + ....: S = flatsurf.similarity_surfaces.billiard(T) + ....: S = S.minimal_cover("translation") + ....: print(S.erase_marked_points().stratum()) + H_6(10) + H_6(2^5) + H_8(12, 2) + + If the surface had no marked points then it is returned unchanged by this + function:: + + sage: O = flatsurf.translation_surfaces.regular_octagon() + sage: O.erase_marked_points() is O + True + + TESTS: + + Verify that https://github.com/flatsurf/flatsurf/issues/263 has been resolved:: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(angles=(10, 8, 3, 1, 1, 1), lengths=(1, 1, 2, 4)) + sage: B = similarity_surfaces.billiard(P) + sage: S = B.minimal_cover(cover_type="translation") + sage: S = S.erase_marked_points() # long time (3s), optional: pyflatsurf + + :: + + sage: from flatsurf import Polygon, similarity_surfaces + sage: P = Polygon(angles=(10, 7, 2, 2, 2, 1), lengths=(1, 1, 2, 3)) + sage: B = similarity_surfaces.billiard(P) + sage: S_mp = B.minimal_cover(cover_type="translation") + sage: S = S_mp.erase_marked_points() # long time (3s), optional: pyflatsurf + + """ + if all(a != 1 for a in self.angles()): + # no 2π angle + return self + from flatsurf.geometry.pyflatsurf_conversion import ( + from_pyflatsurf, + to_pyflatsurf, + ) + + S = to_pyflatsurf(self) + S.delaunay() + S = S.eliminateMarkedPoints().surface() + S.delaunay() + return from_pyflatsurf(S) + + def _test_translation_surface(self, **options): + r""" + Verify that this is a translation surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S._test_translation_surface() + + """ + tester = self._tester(**options) + + limit = None + + if not self.is_finite_type(): + limit = 32 + + tester.assertTrue( + TranslationSurfaces.ParentMethods._is_translation_surface( + self, limit=limit + ) + ) + + class FiniteType(SurfaceCategoryWithAxiom): + r""" + The category of translation surfaces built from finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.octagon_and_squares() + sage: s.category() + Category of connected without boundary finite type translation surfaces + + """ + + class WithoutBoundary(SurfaceCategoryWithAxiom): + r""" + The category of translation surfaces without boundary built from + finitely many polygons. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.octagon_and_squares() + sage: s.category() + Category of connected without boundary finite type translation surfaces + + """ + + class ParentMethods: + r""" + Provides methods available to all translation surfaces that are + built from finitely many polygons. + + If you want to add functionality for such surfaces you most likely + want to put it here. + """ + + def stratum(self): + r""" + Return the stratum this surface belongs to. + + This uses the package ``surface-dynamics`` + (see http://www.labri.fr/perso/vdelecro/flatsurf_sage.html) + + EXAMPLES:: + + sage: import flatsurf.geometry.similarity_surface_generators as sfg + sage: sfg.translation_surfaces.octagon_and_squares().stratum() + H_3(4) + + """ + from surface_dynamics import AbelianStratum + from sage.rings.integer_ring import ZZ + + return AbelianStratum([ZZ(a - 1) for a in self.angles()]) + + def canonicalize(self, in_place=None): + r""" + Return a canonical version of this translation surface. + + EXAMPLES: + + We will check if an element lies in the Veech group:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.octagon_and_squares() + sage: s + Translation Surface in H_3(4) built from 2 squares and a regular octagon + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: s in TranslationSurfaces() + True + sage: a = s.base_ring().gen() + sage: mat = Matrix([[1,2+a],[0,1]]) + sage: s1 = s.canonicalize() + sage: s1.set_immutable() + sage: s2 = (mat*s).canonicalize() + sage: s2.set_immutable() + sage: s1.cmp(s2) == 0 + True + sage: hash(s1) == hash(s2) + True + + """ + if in_place is not None: + if in_place: + raise NotImplementedError( + "calling canonicalize(in_place=True) is not supported anymore" + ) + + import warnings + + warnings.warn( + "the in_place keyword of canonicalize() has been deprecated and will be removed in a future version of sage-flatsurf" + ) + + s = self.delaunay_decomposition().standardize_polygons() + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + s = MutableOrientedSimilaritySurface.from_surface(s) + + from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface, + ) + + ss = MutableOrientedSimilaritySurface.from_surface(s) + + for label in ss.labels(): + ss.set_roots([label]) + if ss.cmp(s) > 0: + s.set_roots([label]) + + # We have chosen the root label such that this surface is minimal. + # Now we relabel all the polygons so that they are natural + # numbers in the order of the walk on the surface. + labels = {label: i for (i, label) in enumerate(s.labels())} + s.relabel(labels, in_place=True) + s.set_immutable() + return s diff --git a/flatsurf/geometry/chamanara.py b/flatsurf/geometry/chamanara.py index 4c9a0667e..7e001dd71 100644 --- a/flatsurf/geometry/chamanara.py +++ b/flatsurf/geometry/chamanara.py @@ -1,7 +1,10 @@ r""" -Construction of Chamanara's surfaces which depend on a parameter alpha less than one. -See the paper "Affine automorphism groups of surfaces of infinite type" in which the surface -is called $X_\alpha$. +Chamanara's surfaces which depend on a parameter `\alpha` less than one. + +REFERENCES: + +- Defined as `X_\alpha` in Chamanara, Reza, "Affine automorphism groups of + surfaces of infinite type", City University of New York, 2002. EXAMPLES:: @@ -32,8 +35,8 @@ # along with sage-flatsurf. If not, see . # ******************************************************************** -from .surface import Surface -from .half_dilation_surface import HalfDilationSurface +from flatsurf.geometry.surface import OrientedSimilaritySurface +from flatsurf.geometry.minimal_cover import MinimalTranslationCover from sage.rings.integer_ring import ZZ @@ -47,20 +50,24 @@ def ChamanaraPolygon(alpha): ValueError("The value of alpha must be between zero and one.") # The value of x is $\sum_{n=0}^\infty \alpha^n$. x = 1 / (1 - alpha) - from .polygon import polygons + from flatsurf import Polygon - return polygons((1, 0), (-x, x), (0, -1), (x - 1, 1 - x)) + return Polygon(edges=[(1, 0), (-x, x), (0, -1), (x - 1, 1 - x)]) -class ChamanaraSurface(Surface): +class ChamanaraSurface(OrientedSimilaritySurface): r""" - The ChamanaraSurface $X_{\alpha}$. + The Chamanara surface $X_{\alpha}$. EXAMPLES:: sage: from flatsurf.geometry.chamanara import ChamanaraSurface - sage: ChamanaraSurface(1/2) + sage: S = ChamanaraSurface(1/2); S Chamanara surface with parameter 1/2 + + TESTS:: + + sage: TestSuite(S).run() """ def __init__(self, alpha): @@ -72,7 +79,118 @@ def __init__(self, alpha): self.rename("Chamanara surface with parameter {}".format(alpha)) - super().__init__(field, ZZ(0), finite=False, mutable=False) + from flatsurf.geometry.categories import DilationSurfaces + + super().__init__( + field, + category=DilationSurfaces() + .Oriented() + .InfiniteType() + .Compact() + .WithoutBoundary() + .Connected() + .Rational(), + ) + + def is_dilation_surface(self, positive=False): + r""" + Return whether this surface is a dilation surface, overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_dilation_surface`. + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: S = ChamanaraSurface(1/2) + sage: S.is_dilation_surface(positive=True) + False + sage: S.is_dilation_surface(positive=False) + True + + """ + return not positive + + def is_cone_surface(self): + r""" + Return whether this surface is a cone surfaces, overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_cone_surface`. + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: S = ChamanaraSurface(1/2) + sage: S.is_cone_surface() + False + + """ + return False + + def is_translation_surface(self, positive=True): + r""" + Return whether this surfaces is a (half-)translation surface, overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_translation_surface`. + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: S = ChamanaraSurface(1/2) + sage: S.is_translation_surface(positive=True) + False + sage: S.is_translation_surface(positive=False) + False + + """ + return False + + def labels(self): + r""" + Return the labels used to identify the polygons that make up this + surface, i.e., the integers. + + This overrides + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels` + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: S = ChamanaraSurface(1/2) + sage: S.labels() + (0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, …) + + """ + from flatsurf.geometry.surface import LabelsFromView + + return LabelsFromView(self, ZZ, finite=False) + + def roots(self): + r""" + Return a label in each connected component of this surface. + + This overrides + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: S = ChamanaraSurface(1/2) + sage: S.roots() + (0,) + + """ + return (ZZ(0),) + + def is_mutable(self): + r""" + Return whether this surface is mutable which it is not. + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: S = ChamanaraSurface(1/2) + sage: S.is_mutable() + False + + """ + return False def polygon(self, lab): r""" @@ -108,10 +226,33 @@ def opposite_edge(self, p, e): # p>=1 return p + 1, 1 + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with equality testing. + + EXAMPLES:: + + sage: from flatsurf.geometry.chamanara import ChamanaraSurface + sage: hash(ChamanaraSurface(1/2)) == hash(ChamanaraSurface(1/2)) + True + + """ + return hash((self._p, self.base_ring())) + + def graphical_surface(self, **kwds): + adjacencies = [(0, 1)] + for i in range(8): + adjacencies.append((-i, 3)) + adjacencies.append((i + 1, 3)) + return super().graphical_surface(adjacencies=adjacencies, **kwds) + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: sage: from flatsurf import translation_surfaces @@ -123,17 +264,13 @@ def __eq__(self, other): False """ - if isinstance(other, ChamanaraSurface): - return ( - self._p == other._p - and self._base_ring == other._base_ring - and self._base_label == other._base_label - ) + if not isinstance(other, ChamanaraSurface): + return False - return super().__eq__(other) + return self._p == other._p and self.base_ring() == other.base_ring() -def chamanara_half_dilation_surface(alpha, n=8): +def chamanara_half_dilation_surface(alpha, n=None): r""" Return Chamanara's surface thought of as a Half Dilation surface. @@ -143,16 +280,36 @@ def chamanara_half_dilation_surface(alpha, n=8): sage: s = chamanara_half_dilation_surface(1/2) sage: TestSuite(s).run() """ - s = HalfDilationSurface(ChamanaraSurface(alpha)) - adjacencies = [(0, 1)] - for i in range(n): - adjacencies.append((-i, 3)) - adjacencies.append((i + 1, 3)) - s.graphical_surface(adjacencies=adjacencies) - return s + if n is not None: + import warnings + + warnings.warn( + "the n keyword of chamanara_half_dilation_surface() is not supported anymore; it will be removed in a future version of sage-flatsurf" + ) + return ChamanaraSurface(alpha) -def chamanara_surface(alpha, n=8): + +class ChamanaraTranslationSurface(MinimalTranslationCover): + def __init__(self, alpha): + MinimalTranslationCover.__init__(self, ChamanaraSurface(alpha)) + self._refine_category_(self.category().Compact()) + + def graphical_surface(self, **kwds): + label = self.root() + adjacencies = [(label, 1)] + for i in range(8): + adjacencies.append((label, 3)) + label = self.opposite_edge(label, 3)[0] + label = self.root() + label = self.opposite_edge(label, 1)[0] + for i in range(8): + adjacencies.append((label, 3)) + label = self.opposite_edge(label, 3)[0] + return super().graphical_surface(adjacencies=adjacencies, **kwds) + + +def chamanara_surface(alpha, n=None): r""" Return Chamanara's surface thought of as a translation surface. @@ -162,16 +319,11 @@ def chamanara_surface(alpha, n=8): sage: s = chamanara_surface(1/2) sage: TestSuite(s).run() """ - s = chamanara_half_dilation_surface(alpha).minimal_cover(cover_type="translation") - label = s.base_label() - adjacencies = [(label, 1)] - for i in range(n): - adjacencies.append((label, 3)) - label = s.opposite_edge(label, 3)[0] - label = s.base_label() - label = s.opposite_edge(label, 1)[0] - for i in range(n): - adjacencies.append((label, 3)) - label = s.opposite_edge(label, 3)[0] - s.graphical_surface(adjacencies=adjacencies) - return s + if n is not None: + import warnings + + warnings.warn( + "the n keyword of chamanara_half_dilation_surface() is not supported anymore; it will be removed in a future version of sage-flatsurf" + ) + + return ChamanaraTranslationSurface(alpha) diff --git a/flatsurf/geometry/circle.py b/flatsurf/geometry/circle.py index 676139b04..8ecdb1922 100644 --- a/flatsurf/geometry/circle.py +++ b/flatsurf/geometry/circle.py @@ -5,13 +5,23 @@ Delaunay decomposition for infinite surfaces. """ # **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper +# This file is part of sage-flatsurf. # -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# +# 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 . # **************************************************************************** from sage.modules.free_module import VectorSpace @@ -203,8 +213,7 @@ def __rmul__(self, similarity): EXAMPLES:: - sage: from flatsurf import * - sage: from flatsurf.geometry.circle import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.square_torus() sage: c = s.polygon(0).circumscribing_circle() sage: c diff --git a/flatsurf/geometry/cone_surface.py b/flatsurf/geometry/cone_surface.py deleted file mode 100644 index 67ab17360..000000000 --- a/flatsurf/geometry/cone_surface.py +++ /dev/null @@ -1,96 +0,0 @@ -# **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper -# -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ -# **************************************************************************** - -from .similarity_surface import SimilaritySurface - - -class ConeSurface(SimilaritySurface): - r""" - A Euclidean cone surface. - """ - - def angles(self, numerical=False, return_adjacent_edges=False): - r""" - Return the set of angles around the vertices of the surface. - - EXAMPLES:: - - sage: from flatsurf import polygons, similarity_surfaces - sage: T = polygons.triangle(3, 4, 5) - sage: S = similarity_surfaces.billiard(T) - sage: S.angles() - [1/3, 1/4, 5/12] - sage: S.angles(numerical=True) # abs tol 1e-14 - [0.333333333333333, 0.250000000000000, 0.416666666666667] - - sage: S.angles(return_adjacent_edges=True) - [(1/3, [(0, 1), (1, 2)]), (1/4, [(0, 0), (1, 0)]), (5/12, [(1, 1), (0, 2)])] - """ - if not self.is_finite(): - raise NotImplementedError("the set of edges is infinite!") - - edges = [pair for pair in self.edge_iterator()] - edges = set(edges) - angles = [] - - if return_adjacent_edges: - while edges: - p, e = edges.pop() - adjacent_edges = [(p, e)] - angle = self.polygon(p).angle(e, numerical=numerical) - pp, ee = self.opposite_edge(p, (e - 1) % self.polygon(p).num_edges()) - while pp != p or ee != e: - edges.remove((pp, ee)) - adjacent_edges.append((pp, ee)) - angle += self.polygon(pp).angle(ee, numerical=numerical) - pp, ee = self.opposite_edge( - pp, (ee - 1) % self.polygon(pp).num_edges() - ) - angles.append((angle, adjacent_edges)) - else: - while edges: - p, e = edges.pop() - angle = self.polygon(p).angle(e, numerical=numerical) - pp, ee = self.opposite_edge(p, (e - 1) % self.polygon(p).num_edges()) - while pp != p or ee != e: - edges.remove((pp, ee)) - angle += self.polygon(pp).angle(ee, numerical=numerical) - pp, ee = self.opposite_edge( - pp, (ee - 1) % self.polygon(pp).num_edges() - ) - angles.append(angle) - - return angles - - def genus(self): - """ - Return the genus of the surface. - - EXAMPLES:: - - sage: import flatsurf.geometry.similarity_surface_generators as sfg - sage: sfg.translation_surfaces.octagon_and_squares().genus() - 3 - - sage: from flatsurf import * - sage: T = polygons.triangle(3,4,5) - sage: B = RationalConeSurface(similarity_surfaces.billiard(T)) - sage: B.genus() - 0 - sage: B.minimal_cover("translation").genus() - 3 - """ - return sum(a - 1 for a in self.angles()) // 2 + 1 - - def area(self): - r""" - Return the area of this surface. - """ - return self._s.area() diff --git a/flatsurf/geometry/delaunay.py b/flatsurf/geometry/delaunay.py index abfd6db82..18ace8c1a 100644 --- a/flatsurf/geometry/delaunay.py +++ b/flatsurf/geometry/delaunay.py @@ -1,7 +1,23 @@ r""" -This file contains classes implementing Surface which are used for -triangulating, Delaunay triangulating, and Delaunay decomposing infinite -surfaces. +Triangulations, Delaunay triangulations, and Delaunay decompositions of +infinite surfaces. + +EXAMPLES: + +Typically, you don't need to create these surfaces directly, they are created +by invoking methods on the underlying surfaces:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.triangulate() + Triangulation of The infinite staircase + + sage: S.delaunay_triangulation() + Delaunay triangulation of The infinite staircase + + sage: S.delaunay_decomposition() + Delaunay cell decomposition of The infinite staircase + """ # ******************************************************************** # This file is part of sage-flatsurf. @@ -24,199 +40,786 @@ # along with sage-flatsurf. If not, see . # ******************************************************************** -from flatsurf.geometry.surface import Surface +from sage.misc.cachefunc import cached_method + +from flatsurf.geometry.surface import ( + MutableOrientedSimilaritySurface_base, + OrientedSimilaritySurface, + Labels, +) -class LazyTriangulatedSurface(Surface): +class LazyTriangulatedSurface(OrientedSimilaritySurface): r""" Surface class used to triangulate an infinite surface. - EXAMPLES: + EXAMPLES:: - Example with relabel=False:: + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S = S.triangulate() - sage: from flatsurf import * - sage: from flatsurf.geometry.delaunay import * - sage: s=translation_surfaces.infinite_staircase() - sage: ss=TranslationSurface(LazyTriangulatedSurface(s,relabel=False)) - sage: ss.polygon(0).num_edges() - 3 - sage: TestSuite(ss).run() + TESTS:: - Example with relabel=True:: + sage: from flatsurf.geometry.delaunay import LazyTriangulatedSurface + sage: isinstance(S, LazyTriangulatedSurface) + True + sage: TestSuite(S).run() # long time (1s) - sage: from flatsurf import * - sage: from flatsurf.geometry.delaunay import * - sage: s=translation_surfaces.infinite_staircase() - sage: ss=TranslationSurface(LazyTriangulatedSurface(s,relabel=True)) - sage: ss.polygon(0).num_edges() - 3 - sage: TestSuite(ss).run() """ - def __init__(self, similarity_surface, relabel=True): + def __init__(self, similarity_surface, relabel=None, category=None): + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from LazyTriangulatedSurface; use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to LazyTriangulatedSurface()" + ) + if similarity_surface.is_mutable(): raise ValueError("Surface must be immutable.") - # This surface will converge to the Delaunay Triangulation - self._s = similarity_surface.copy(relabel=relabel, lazy=True, mutable=True) + self._reference = similarity_surface - Surface.__init__( + OrientedSimilaritySurface.__init__( self, - self._s.base_ring(), - self._s.base_label(), - mutable=False, - finite=self._s.is_finite(), + similarity_surface.base_ring(), + category=category or self._reference.category(), ) - def polygon(self, lab): + def is_mutable(self): r""" - Return the polygon with label ``lab``. + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S.is_mutable() + False + """ - polygon = self._s.polygon(lab) - if polygon.num_edges() > 3: - self._s.triangulate(in_place=True, label=lab) - return self._s.polygon(lab) - else: - return polygon + return False - def opposite_edge(self, p, e): + def is_compact(self): r""" - Given the label ``p`` of a polygon and an edge ``e`` in that polygon - returns the pair (``pp``, ``ee``) to which this edge is glued. + Return whether this surface is compact as a topological space. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S.is_compact() + False + """ - pp, ee = self._s.opposite_edge(p, e) - polygon = self._s.polygon(pp) - if polygon.num_edges() > 3: - self.polygon(pp) - return self._s.opposite_edge(p, e) - else: - return (pp, ee) + return self._reference.is_compact() + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S.roots() + ((0, (0, 1, 2)),) + + """ + return tuple( + (reference_label, self._triangulation(reference_label)[(0, 1)]) + for reference_label in self._reference.roots() + ) + + def _triangulation(self, reference_label): + r""" + Return a triangulated of the ``reference_label`` in the underlying + (typically non-triangulated) reference surface. + + INPUT: + + - ``reference_label`` -- a polygon label in the reference surface that + we are triangulating. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S._triangulation(0) + {(0, 1): (0, 1, 2), + (0, 2): (0, 2, 3), + (1, 2): (0, 1, 2), + (2, 0): (0, 1, 2), + (2, 3): (0, 2, 3), + (3, 0): (0, 2, 3)} + + """ + reference_polygon = self._reference.polygon(reference_label) + + outer_edges = [ + (vertex, (vertex + 1) % len(reference_polygon.vertices())) + for vertex in range(len(reference_polygon.vertices())) + ] + inner_edges = reference_polygon.triangulation() + inner_edges.extend([(w, v) for (v, w) in inner_edges]) + + edges = outer_edges + inner_edges + + def triangle(edge): + v, w = edge + next_edges = [edge for edge in edges if edge[0] == w] + previous_edges = [edge for edge in edges if edge[1] == v] + + next_vertices = [edge[1] for edge in next_edges] + previous_vertices = [edge[0] for edge in previous_edges] + + other_vertex = set(next_vertices).intersection(set(previous_vertices)) + + assert len(other_vertex) == 1 + + other_vertex = next(iter(other_vertex)) + + vertices = v, w, other_vertex + while vertices[0] != min(vertices): + vertices = vertices[1:] + vertices[:1] + + return vertices + + return {edge: triangle(edge) for edge in edges} + + def polygon(self, label): + r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S.polygon((0, (0, 1, 2))) + Polygon(vertices=[(0, 0), (1, 0), (1, 1)]) + + """ + reference_label, vertices = label + reference_polygon = self._reference.polygon(reference_label) + + from flatsurf import Polygon + + return Polygon( + vertices=[reference_polygon.vertex(v) for v in vertices], + category=reference_polygon.category(), + ) + + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S.opposite_edge((0, (0, 1, 2)), 0) + ((1, (0, 2, 3)), 1) + + """ + reference_label, vertices = label + reference_polygon = self._reference.polygon(reference_label) + + if vertices[(edge + 1) % 3] == (vertices[edge] + 1) % len( + reference_polygon.vertices() + ): + # This is an edge of the reference surface + cross_reference_label, cross_reference_edge = self._reference.opposite_edge( + reference_label, vertices[edge] + ) + cross_reference_polygon = self._reference.polygon(cross_reference_label) + cross_vertices = self._triangulation(cross_reference_label)[ + ( + cross_reference_edge, + (cross_reference_edge + 1) + % len(cross_reference_polygon.vertices()), + ) + ] + + cross_edge = cross_vertices.index(cross_reference_edge) + + return (cross_reference_label, cross_vertices), cross_edge + + # This is an edge that was added by the triangulation + edge = (vertices[edge], vertices[(edge + 1) % 3]) + cross_edge = (edge[1], edge[0]) + cross_vertices = self._triangulation(reference_label)[cross_edge] + return (reference_label, cross_vertices), cross_vertices.index(cross_edge[0]) + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: hash(S.triangulate()) == hash(S.triangulate()) + True + + """ + return hash(self._reference) def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: - sage: from flatsurf import translation_surfaces, TranslationSurface - sage: from flatsurf.geometry.delaunay import LazyTriangulatedSurface + sage: from flatsurf import translation_surfaces sage: S = translation_surfaces.infinite_staircase() - sage: S = TranslationSurface(LazyTriangulatedSurface(S)) - sage: S == S + sage: S.triangulate() == S.triangulate() True """ - if isinstance(other, LazyTriangulatedSurface): - if self._s == other._s: - return True + if not isinstance(other, LazyTriangulatedSurface): + return False + + return self._reference == other._reference + + def labels(self): + r""" + Return the labels of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S.labels() + ((0, (0, 1, 2)), (1, (0, 2, 3)), (-1, (0, 2, 3)), (0, (0, 2, 3)), (1, (0, 1, 2)), (2, (0, 1, 2)), (-1, (0, 1, 2)), (-2, (0, 1, 2)), (2, (0, 2, 3)), (3, (0, 2, 3)), + (-2, (0, 2, 3)), (-3, (0, 2, 3)), (3, (0, 1, 2)), (4, (0, 1, 2)), (-3, (0, 1, 2)), (-4, (0, 1, 2)), …) + + """ + + class LazyLabels(Labels): + def __contains__(self, label): + reference_label, vertices = label + if reference_label not in self._surface._reference.labels(): + return False - return super().__eq__(other) + return ( + vertices in self._surface._triangulation(reference_label).values() + ) + return LazyLabels(self, finite=self._reference.is_finite_type()) -class LazyDelaunayTriangulatedSurface(Surface): + def __repr__(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().triangulate() + sage: S + Triangulation of The infinite staircase + + """ + return f"Triangulation of {self._reference!r}" + + +class LazyMutableOrientedSimilaritySurface(MutableOrientedSimilaritySurface_base): r""" - Surface class used to find a Delaunay triangulation of an infinite surface. + A helper surface for :class:`LazyDelaunayTriangulatedSurface`. - EXAMPLES: + A mutable wrapper of an (infinite) reference surface. When a polygon is not + present in this wrapper yet, it is taken from the reference surface and can + then be modified. - Example with relabel=False:: + .. NOTE:: - sage: from flatsurf import * - sage: from flatsurf.geometry.delaunay import * - sage: s=translation_surfaces.infinite_staircase() - sage: ss=TranslationSurface(LazyDelaunayTriangulatedSurface(s,relabel=False)) - sage: ss.polygon(0).num_edges() - 3 - sage: TestSuite(ss).run() - sage: ss.is_delaunay_triangulated(limit=100) - True + This surface does not implement the entire surface interface correctly. + It just supports the operations in the way that they are necessary to + make :class:`LazyDelaunayTriangulatedSurface` work. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: p = T.polygon(0) + sage: p + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: q = p * 2 - Example with relabel=True:: + sage: S.replace_polygon(0, q) + Traceback (most recent call last): + ... + AttributeError: '_InfiniteStaircase_with_category' object has no attribute 'replace_polygon' + + sage: T.replace_polygon(0, q) + sage: T.polygon(0) + Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)]) + + """ + + def __init__(self, surface, category=None): + from flatsurf.geometry.categories import SimilaritySurfaces + + if surface not in SimilaritySurfaces().Oriented().WithoutBoundary(): + raise NotImplementedError("cannot handle surfaces with boundary yet") + + self._reference = surface + + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + self._surface = MutableOrientedSimilaritySurface(surface.base_ring()) + + super().__init__(surface.base_ring(), category=category or surface.category()) + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + .. NOTE:: + + This assumes that :meth:`glue` is never called to glue components. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.roots() + (0,) + + """ + return self._reference.roots() + + def labels(self): + r""" + Return the labels of this surface which are just the labels of the + underlying reference surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.labels() + (0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, …) + + """ + return self._reference.labels() + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``True``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.is_mutable() + True + + """ + return True + + def replace_polygon(self, label, polygon): + r""" + Swap out the polygon with the label ``label`` with ``polygon``. + + The polygons must have the same number of sides since gluings are kept. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.replace_polygon(0, T.polygon(0)) + + """ + self._ensure_polygon(label) + return self._surface.replace_polygon(label, polygon) + + def glue(self, x, y): + r""" + Glue the (label, edge) pair ``x`` with the pair ``y`` in this surface. + + This unglues any existing gluings of these edges. + + .. NOTE:: + + After a sequence of such glue operations, no edges must be unglued. + Otherwise, gluings get copied over from the underlying surface with + confusing side effects. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.gluings() + (((0, 0), (1, 2)), ((0, 1), (-1, 3)), ((0, 2), (1, 0)), ((0, 3), (-1, 1)), ((1, 0), (0, 2)), ((1, 1), (2, 3)), ((1, 2), (0, 0)), ((1, 3), (2, 1)), ((-1, 0), (-2, 2)), + ((-1, 1), (0, 3)), ((-1, 2), (-2, 0)), ((-1, 3), (0, 1)), ((2, 0), (3, 2)), ((2, 1), (1, 3)), ((2, 2), (3, 0)), ((2, 3), (1, 1)), …) + sage: T.glue((0, 0), (1, 0)) + sage: T.glue((1, 2), (0, 2)) + sage: T.gluings() + (((0, 0), (1, 0)), ((0, 1), (-1, 3)), ((0, 2), (1, 2)), ((0, 3), (-1, 1)), ((1, 0), (0, 0)), ((1, 1), (2, 3)), ((1, 2), (0, 2)), ((1, 3), (2, 1)), ((-1, 0), (-2, 2)), + ((-1, 1), (0, 3)), ((-1, 2), (-2, 0)), ((-1, 3), (0, 1)), ((2, 0), (3, 2)), ((2, 1), (1, 3)), ((2, 2), (3, 0)), ((2, 3), (1, 1)), …) + + """ + return self._surface.glue(x, y) + + def _ensure_gluings(self, label): + r""" + Make sure that the surface used to internally represent this surface + has copied over all the gluings for ``label`` from the underlying + surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T._ensure_polygon(0) + sage: T._ensure_gluings(0) + + """ + self._ensure_polygon(label) + for edge in range(len(self._surface.polygon(label).vertices())): + cross = self._surface.opposite_edge(label, edge) + if cross is None: + cross_label, cross_edge = self._reference.opposite_edge(label, edge) + self._ensure_polygon(cross_label) + + assert ( + self._surface.opposite_edge(cross_label, cross_edge) is None + ), "surface must not have a boundary" + + # Note that we cannot detect whether something has been + # explicitly unglued. So we just reestablish any gluings of + # this edge. + self._surface.glue((label, edge), (cross_label, cross_edge)) + + def _ensure_polygon(self, label): + r""" + Make sure that the surface used to internally represent this surface + has copied over the polygon ``label`` from the underlying surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T._ensure_polygon(0) + + """ + if label not in self._surface.labels(): + self._surface.add_polygon(self._reference.polygon(label), label=label) + + def polygon(self, label): + r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + """ + self._ensure_polygon(label) + return self._surface.polygon(label) + + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + + sage: from flatsurf.geometry.delaunay import LazyMutableOrientedSimilaritySurface + sage: T = LazyMutableOrientedSimilaritySurface(S) + sage: T.opposite_edge(0, 0) + (1, 2) + + """ + self._ensure_polygon(label) + self._ensure_gluings(label) + cross_label, cross_edge = self._surface.opposite_edge(label, edge) + self._ensure_polygon(cross_label) + self._ensure_gluings(cross_label) + return cross_label, cross_edge + + +class LazyDelaunayTriangulatedSurface(OrientedSimilaritySurface): + r""" + Delaunay triangulation of an (infinite type) surface. + + EXAMPLES:: - sage: from flatsurf import * - sage: from flatsurf.geometry.delaunay import * - sage: s=translation_surfaces.infinite_staircase() - sage: ss=TranslationSurface(LazyDelaunayTriangulatedSurface(s,relabel=True)) - sage: ss.polygon(0).num_edges() + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: len(S.polygon(S.root()).vertices()) 3 - sage: TestSuite(ss).run() - sage: ss.is_delaunay_triangulated(limit=100) + sage: TestSuite(S).run() # long time (.8s) + sage: S.is_delaunay_triangulated(limit=10) True - Chamanara example:: + sage: from flatsurf.geometry.delaunay import LazyDelaunayTriangulatedSurface + sage: isinstance(S, LazyDelaunayTriangulatedSurface) + True + + :: + + sage: from flatsurf.geometry.chamanara import chamanara_surface + sage: S = chamanara_surface(QQ(1/2)) + sage: m = matrix([[2,1],[1,1]])**4 + sage: S = (m*S).delaunay_triangulation() + sage: TestSuite(S).run() # long time (1s) + sage: S.is_delaunay_triangulated(limit=10) + True + sage: TestSuite(S).run() # long time (.5s) - sage: from flatsurf import * - sage: from flatsurf.geometry.chamanara import * - sage: s=chamanara_surface(QQ(1/2)) - sage: m=matrix([[2,1],[1,1]])**4 - sage: ss=(m*s).delaunay_triangulation() - sage: TestSuite(ss).run() - sage: ss.is_delaunay_triangulated(limit=100) + sage: from flatsurf.geometry.delaunay import LazyDelaunayTriangulatedSurface + sage: isinstance(S, LazyDelaunayTriangulatedSurface) True - sage: TestSuite(ss).run() + """ - def _setup_direction(self, direction): - # Our Delaunay will respect the provided direction. - if direction is None: - self._direction = self._s.vector_space()( - (self._s.base_ring().zero(), self._s.base_ring().one()) - ) - else: - self._direction = self._ss.vector_space()(direction) + def __init__(self, similarity_surface, direction=None, relabel=None, category=None): + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from LazyDelaunayTriangulatedSurface; use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings - def __init__(self, similarity_surface, direction=None, relabel=True): - r""" - Construct a lazy Delaunay triangulation of the provided similarity_surface. - """ - if similarity_surface.underlying_surface().is_mutable(): - raise ValueError("Surface must be immutable.") + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to LazyDelaunayTriangulatedSurface()" + ) + + if similarity_surface.is_mutable(): + raise ValueError("surface must be immutable") + + if not similarity_surface.is_connected(): + raise NotImplementedError("surface must be connected") + + self._reference = similarity_surface # This surface will converge to the Delaunay Triangulation - self._s = similarity_surface.copy(relabel=relabel, lazy=True, mutable=True) + self._surface = LazyMutableOrientedSimilaritySurface( + LazyTriangulatedSurface(similarity_surface) + ) - self._setup_direction(direction) + self._direction = (self._surface.base_ring() ** 2)(direction or (0, 1)) + self._direction.set_immutable() # Set of labels corresponding to known delaunay polygons self._certified_labels = set() # Triangulate the base polygon - base_label = self._s.base_label() - self._s.triangulate(in_place=True, label=base_label) + root = self._surface.root() # Certify the base polygon (or apply flips...) - while not self._certify_or_improve(base_label): + while not self._certify_or_improve(root): pass - Surface.__init__( + OrientedSimilaritySurface.__init__( self, - self._s.base_ring(), - base_label, - finite=self._s.is_finite(), - mutable=False, + self._surface.base_ring(), + category=category or self._surface.category(), ) + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: S.is_mutable() + False + + """ + return False + + def is_compact(self): + r""" + Return whether this surface is compact as a topological space. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: S.is_compact() + False + + """ + return self._reference.is_compact() + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: S.roots() + ((0, (0, 1, 2)),) + + """ + return self._surface.roots() + + @cached_method def polygon(self, label): - if label in self._certified_labels: - return self._s.polygon(label) - else: - raise ValueError( - "Asked for polygon not known to be Delaunay. Make sure you obtain polygon labels by walking through the surface." - ) + r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: S.polygon((0, (0, 1, 2))) + Polygon(vertices=[(0, 0), (1, 0), (1, 1)]) + + """ + if label not in self.labels(): + raise ValueError("no polygon with this label") + + if label not in self._certified_labels: + for certified_label in self._walk(): + if label == certified_label: + assert label in self._certified_labels + break + + return self._surface.polygon(label) + + def _walk(self): + visited = set() + from collections import deque + + next = deque( + [(self.root(), 0), (self.root(), 1), (self.root(), 2)], + ) + while next: + label, edge = next.popleft() + if label in visited: + continue + + yield label + + visited.add(label) + for edge in range(3): + next.append(self.opposite_edge(label, edge)) + + @cached_method def opposite_edge(self, label, edge): - if label in self._certified_labels: - ll, ee = self._s.opposite_edge(label, edge) - if ll in self._certified_labels: - return ll, ee - while not self._certify_or_improve(ll): - ll, ee = self._s.opposite_edge(label, edge) - return self._s.opposite_edge(label, edge) - else: - raise ValueError( - "Asked for an edge of a polygon not known to be Delaunay. Make sure you obtain polygon labels by walking through the surface." - ) + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: S.opposite_edge((0, (0, 1, 2)), 0) + ((1, (0, 2, 3)), 1) + + """ + self.polygon(label) + while True: + cross_label, cross_edge = self._surface.opposite_edge(label, edge) + if self._certify_or_improve(cross_label): + break + + return self._surface.opposite_edge(label, edge) def _certify_or_improve(self, label): r""" @@ -235,13 +838,9 @@ def _certify_or_improve(self, label): if label in self._certified_labels: # Already certified. return True - p = self._s.polygon(label) - if p.num_edges() > 3: - # not triangulated! - self._s.triangulate(in_place=True, label=label) - p = self._s.polygon(label) - # Made major changes to the polygon with label l. - return False + p = self._surface.polygon(label) + assert len(p.vertices()) == 3 + c = p.circumscribing_circle() # Develop through each of the 3 edges: @@ -260,20 +859,15 @@ def _certify_or_improve(self, label): edge_stack = [(label, e, 1, c)] ll, ee, step, cc = edge_stack[len(edge_stack) - 1] - lll, eee = self._s.opposite_edge(ll, ee) + lll, eee = self._surface.opposite_edge(ll, ee) if lll not in self._certified_labels: - ppp = self._s.polygon(lll) - if ppp.num_edges() > 3: - # not triangulated! - self._s.triangulate(in_place=True, label=lll) - lll, eee = self._s.opposite_edge(ll, ee) - ppp = self._s.polygon(lll) - # now ppp is a triangle - - if self._s._edge_needs_flip(ll, ee): + ppp = self._surface.polygon(lll) + assert len(ppp.vertices()) == 3 + + if self._surface._delaunay_edge_needs_flip(ll, ee): # Perform the flip - self._s.triangle_flip( + self._surface.triangle_flip( ll, ee, in_place=True, direction=self._direction ) @@ -295,7 +889,7 @@ def _certify_or_improve(self, label): continue # If we reach here then we know that no flip was needed. - ccc = self._s.edge_transformation(ll, ee) * cc + ccc = self._surface.edge_transformation(ll, ee) * cc # Check if the disk passes through the next edge in the chain. lp = ccc.line_segment_position( @@ -330,150 +924,406 @@ def _certify_or_improve(self, label): self._certified_labels.add(label) return True + def labels(self): + r""" + Return the labels of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + sage: S.labels() + ((0, (0, 1, 2)), (1, (0, 2, 3)), (-1, (0, 2, 3)), (0, (0, 2, 3)), (1, (0, 1, 2)), (2, (0, 1, 2)), (-1, (0, 1, 2)), (-2, (0, 1, 2)), (2, (0, 2, 3)), (3, (0, 2, 3)), + (-2, (0, 2, 3)), (-3, (0, 2, 3)), (3, (0, 1, 2)), (4, (0, 1, 2)), (-3, (0, 1, 2)), (-4, (0, 1, 2)), …) + + """ + return self._surface.labels() + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: hash(S.delaunay_triangulation()) == hash(S.delaunay_triangulation()) + True + + """ + return hash((self._reference, self._direction)) + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: - sage: from flatsurf import translation_surfaces, TranslationSurface - sage: from flatsurf.geometry.delaunay import LazyDelaunayTriangulatedSurface + sage: from flatsurf import translation_surfaces sage: S = translation_surfaces.infinite_staircase() - sage: S = TranslationSurface(LazyDelaunayTriangulatedSurface(S)) - sage: S == S + sage: S.delaunay_triangulation() == S.delaunay_triangulation() True """ - if isinstance(other, LazyDelaunayTriangulatedSurface): - if self._s == other._s and self._direction == other._direction: - return True + if not isinstance(other, LazyDelaunayTriangulatedSurface): + return False - return super().__eq__(other) + return ( + self._reference == other._reference and self._direction == other._direction + ) + + def __repr__(self): + r""" + Return a printable representation of this surface. + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_triangulation() + + """ + return f"Delaunay triangulation of {self._reference!r}" -class LazyDelaunaySurface(LazyDelaunayTriangulatedSurface): - # We just inherit to use some methods. +class LazyDelaunaySurface(OrientedSimilaritySurface): r""" - This is an implementation of Surface. It takes a surface (typically - infinite) from the constructor. This class represents the Delaunay - decomposition of this surface. We compute this decomposition lazily so that - it works for infinite surfaces. + Delaunay cell decomposition of an (infinite type) surface. EXAMPLES:: - sage: from flatsurf import * - sage: from flatsurf.geometry.delaunay import * - sage: s=translation_surfaces.infinite_staircase() - sage: m=matrix([[2,1],[1,1]]) - sage: ss=TranslationSurface(LazyDelaunaySurface(m*s,relabel=False)) - sage: ss.polygon(ss.base_label()) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: ss.is_delaunay_decomposed(limit=100) + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: m = matrix([[2, 1], [1, 1]]) + sage: S = (m * S).delaunay_decomposition() + + sage: S.polygon(S.root()) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + sage: S.is_delaunay_decomposed(limit=10) # long time (.7s) True - sage: TestSuite(ss).run() - - sage: from flatsurf import * - sage: from flatsurf.geometry.chamanara import * - sage: from flatsurf.geometry.delaunay import * - sage: s=chamanara_surface(QQ(1/2)) - sage: m=matrix([[3,4],[-4,3]])*matrix([[4,0],[0,1/4]]) - sage: ss=TranslationSurface(LazyDelaunaySurface(m*s)) - sage: ss.is_delaunay_decomposed(limit=100) + + sage: TestSuite(S).run() # long time (2s) + + sage: from flatsurf.geometry.delaunay import LazyDelaunaySurface + sage: isinstance(S, LazyDelaunaySurface) + True + + :: + + sage: from flatsurf.geometry.chamanara import chamanara_surface + sage: S = chamanara_surface(QQ(1/2)) + sage: m = matrix([[3, 4], [-4, 3]]) * matrix([[4, 0],[0, 1/4]]) + sage: S = (m * S).delaunay_decomposition() + sage: S.is_delaunay_decomposed(limit=10) # long time (.5s) True - sage: TestSuite(ss).run() + + sage: TestSuite(S).run() # long time (1.5s) + + sage: from flatsurf.geometry.delaunay import LazyDelaunaySurface + sage: isinstance(S, LazyDelaunaySurface) + True + """ - def __init__(self, similarity_surface, direction=None, relabel=True): + def __init__(self, similarity_surface, direction=None, relabel=None, category=None): + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from LazyDelaunaySurface; use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to LazyDelaunaySurface()" + ) + + if similarity_surface.is_mutable(): + raise ValueError("surface must be immutable.") + + self._reference = similarity_surface + + self._delaunay_triangulation = LazyDelaunayTriangulatedSurface( + self._reference, direction=direction, relabel=relabel + ) + + super().__init__( + similarity_surface.base_ring(), + category=category or similarity_surface.category(), + ) + + @cached_method + def polygon(self, label): r""" - Construct a lazy Delaunay triangulation of the provided similarity_surface. + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S.polygon((0, (0, 1, 2))) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) """ - if similarity_surface.underlying_surface().is_mutable(): - raise ValueError("Surface must be immutable.") + if label not in self._delaunay_triangulation.labels(): + raise ValueError("no polygon with this label") - # This surface will converge to the Delaunay Decomposition - self._s = similarity_surface.copy(relabel=relabel, lazy=True, mutable=True) + cell, edges = self._cell(label) - self._setup_direction(direction) + if label != self._label(cell): + raise ValueError("no polygon with this label") - # Set of labels corresponding to known delaunay polygons - self._certified_labels = set() - self._decomposition_certified_labels = set() + edges = [ + self._delaunay_triangulation.polygon(edge[0]).edge(edge[1]) + for edge in edges + ] - base_label = self._s.base_label() + from flatsurf import Polygon - # We will now try to get the base_polygon. - # Certify the base polygon (or apply flips...) - while not self._certify_or_improve(base_label): - pass - self._certify_decomposition(base_label) + return Polygon(edges=edges) - Surface.__init__( - self, - self._s.base_ring(), - base_label, - finite=self._s.is_finite(), - mutable=False, - ) + @cached_method + def _label(self, cell): + r""" + Return a canonical label for the Delaunay cell that is made up by the + Delaunay triangles ``cell``. - def _certify_decomposition(self, label): - if label in self._decomposition_certified_labels: - return - assert label in self._certified_labels - changed = True - while changed: - changed = False - p = self._s.polygon(label) - for e in range(p.num_edges()): - ll, ee = self._s.opposite_edge(label, e) - while not self._certify_or_improve(ll): - ll, ee = self._s.opposite_edge(label, e) - if self._s._edge_needs_join(label, e): - # ll should not have already been certified! - assert ll not in self._decomposition_certified_labels - self._s.join_polygons(label, e, in_place=True) - changed = True - break - self._decomposition_certified_labels.add(label) + EXAMPLES:: - def polygon(self, label): - if label in self._decomposition_certified_labels: - return self._s.polygon(label) - else: - raise ValueError( - "Asked for polygon not known to be Delaunay. Make sure you obtain polygon labels by walking through the surface." + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S._label(frozenset({ + ....: ((0, (0, 1, 2))), + ....: ((0, (0, 2, 3)))})) + (0, (0, 1, 2)) + + """ + for label in self._delaunay_triangulation.labels(): + if label in cell: + return label + + @cached_method + def _normalize_label(self, label): + r""" + Return a canonical label for the Delaunay cell that contains the + Delaunay triangle ``label``. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S._normalize_label((0, (0, 1, 2))) + (0, (0, 1, 2)) + sage: S._normalize_label((0, (0, 2, 3))) + (0, (0, 1, 2)) + + """ + cell, _ = self._cell(label) + return self._label(cell) + + @cached_method + def _cell(self, label): + r""" + Return the labels of the Delaunay triangles that contain the Delaunay + triangle ``label`` together with the interior edges in that cell. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + + This cell (a square) is formed by two triangles that form a cylinder, + i.e., the two triangles are glued at two of their edges:: + + sage: S._cell((0, (0, 1, 2))) + (frozenset({(0, (0, 1, 2)), (0, (0, 2, 3))}), + [((0, (0, 1, 2)), 0), + ((0, (0, 1, 2)), 1), + ((0, (0, 2, 3)), 1), + ((0, (0, 2, 3)), 2)]) + + """ + edges = [] + cell = set() + explore = [(label, 2), (label, 1), (label, 0)] + + while explore: + triangle, edge = explore.pop() + + cell.add(triangle) + + delaunay = self._delaunay_triangulation._delaunay_edge_needs_join( + triangle, edge ) - def opposite_edge(self, label, edge): - if label in self._decomposition_certified_labels: - ll, ee = self._s.opposite_edge(label, edge) - if ll in self._decomposition_certified_labels: - return ll, ee - self._certify_decomposition(ll) - return self._s.opposite_edge(label, edge) - else: - raise ValueError( - "Asked for polygon not known to be Delaunay. Make sure you obtain polygon labels by walking through the surface." + if not delaunay: + edges.append((triangle, edge)) + continue + + cross_triangle, cross_edge = self._delaunay_triangulation.opposite_edge( + triangle, edge ) + for shift in [2, 1]: + next_triangle, next_edge = cross_triangle, (cross_edge + shift) % 3 + + if (next_triangle, next_edge) in edges: + raise NotImplementedError + if (next_triangle, next_edge) in explore: + raise NotImplementedError + + explore.append((next_triangle, next_edge)) + + cell = frozenset(cell) + normalized_label = self._label(cell) + if normalized_label != label: + return self._cell(normalized_label) + + return cell, edges + + @cached_method + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S.opposite_edge((0, (0, 1, 2)), 0) + ((1, (0, 2, 3)), 2) + + """ + if label not in self._delaunay_triangulation.labels(): + raise ValueError + + cell, edges = self._cell(label) + + if label != self._label(cell): + raise ValueError + + edge = edges[edge] + + cross_triangle, cross_edge = self._delaunay_triangulation.opposite_edge(*edge) + + cross_cell, cross_edges = self._cell(cross_triangle) + cross_label = self._label(cross_cell) + return cross_label, cross_edges.index((cross_triangle, cross_edge)) + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S.roots() + ((0, (0, 1, 2)),) + + """ + return self._delaunay_triangulation.roots() + + def is_compact(self): + r""" + Return whether this surface is compact as a topological space. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S.is_compact() + False + + """ + return self._reference.is_compact() + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().delaunay_decomposition() + sage: S.is_mutable() + False + + """ + return False + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: hash(S.delaunay_decomposition()) == hash(S.delaunay_decomposition()) + True + + """ + return hash(self._delaunay_triangulation) + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: - sage: from flatsurf import translation_surfaces, TranslationSurface + sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.delaunay import LazyDelaunaySurface sage: S = translation_surfaces.infinite_staircase() sage: m = matrix([[2, 1], [1, 1]]) - sage: S = TranslationSurface(LazyDelaunaySurface(m*S)) + sage: S = LazyDelaunaySurface(m*S) sage: S == S True """ - if isinstance(other, LazyDelaunaySurface): - if self._s == other._s and self._direction == other._direction: - return True + if not isinstance(other, LazyDelaunaySurface): + return False + + return self._delaunay_triangulation == other._delaunay_triangulation - return super().__eq__(other) + def __repr__(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: S.delaunay_decomposition() + Delaunay cell decomposition of The infinite staircase + + """ + return f"Delaunay cell decomposition of {self._reference!r}" diff --git a/flatsurf/geometry/dilation_surface.py b/flatsurf/geometry/dilation_surface.py deleted file mode 100644 index 024760b56..000000000 --- a/flatsurf/geometry/dilation_surface.py +++ /dev/null @@ -1,21 +0,0 @@ -# **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper -# -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ -# **************************************************************************** - -from flatsurf.geometry.half_dilation_surface import HalfDilationSurface - - -class DilationSurface(HalfDilationSurface): - r""" - Dilation surface. - - A dilation surface is a (G,X) structure on a surface for the group - of positive dilatations `G = \RR_+` acting on the plane `X = \RR^2`. - """ - pass diff --git a/flatsurf/geometry/euclidean.py b/flatsurf/geometry/euclidean.py new file mode 100644 index 000000000..7dadfbc54 --- /dev/null +++ b/flatsurf/geometry/euclidean.py @@ -0,0 +1,609 @@ +r""" +A loose collection of tools for Euclidean geometry in the plane. + +.. SEEALSO:: + + :mod:`flatsurf.geometry.circle` for everything specific to circles in the plane + +""" +###################################################################### +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 Vincent Delecroix +# 2020-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +###################################################################### + + +def is_cosine_sine_of_rational(cos, sin, scaled=False): + r""" + Check whether the given pair is a cosine and sine of a same rational angle. + + INPUT: + + - ``cos`` -- a number + + - ``sin`` -- a number + + - ``scaled`` -- a boolean (default: ``False``); whether to allow ``cos`` + and ``sin`` to be scaled by the same positive algebraic number + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import is_cosine_sine_of_rational + + sage: c = s = AA(sqrt(2))/2 + sage: is_cosine_sine_of_rational(c, s) + True + + sage: c = AA(sqrt(3))/2 + sage: s = AA(1/2) + sage: is_cosine_sine_of_rational(c, s) + True + + sage: c = AA(sqrt(5)/2) + sage: s = (1 - c**2).sqrt() + sage: c**2 + s**2 + 1.000000000000000? + sage: is_cosine_sine_of_rational(c, s) + False + + sage: c = (AA(sqrt(5)) + 1)/4 + sage: s = (1 - c**2).sqrt() + sage: is_cosine_sine_of_rational(c, s) + True + + sage: K. = NumberField(x**2 - 2, embedding=1.414) + sage: is_cosine_sine_of_rational(K.zero(), -K.one()) + True + + TESTS:: + + sage: from pyexactreal import ExactReals # optional: exactreal # random output due to matplotlib warnings with some combinations of setuptools and matplotlib + sage: R = ExactReals() # optional: exactreal + sage: is_cosine_sine_of_rational(R.one(), R.zero()) # optional: exactreal + True + + """ + from sage.all import AA + + # We cannot check in AA due to https://github.com/flatsurf/exact-real/issues/172 + # We just trust that non-algebraic elements won't allow conversion to AA. + # if cos not in AA: + # return False + # if sin not in AA: + # return False + + if not scaled: + if cos**2 + sin**2 != 1: + return False + + try: + cos = AA(cos) + except ValueError: + # This is a replacement for the "in AA" checked disabled above. + return False + + cos = cos.as_number_field_element(embedded=True) + # We need an explicit conversion to the number field due to https://github.com/sagemath/sage/issues/35613 + cos = cos[0](cos[1]) + + try: + sin = AA(sin) + except ValueError: + # This is a replacement for the "in AA" checked disabled above. + return False + sin = sin.as_number_field_element(embedded=True) + # We need an explicit conversion to the number field due to https://github.com/sagemath/sage/issues/35613 + sin = sin[0](sin[1]) + + from sage.all import ComplexBallField + + CBF = ComplexBallField(53) + + x = CBF(cos) + CBF.gen(0) * CBF(sin) + xN = x + + # Suppose that (cos, sin) are indeed sine and cosine of a rational angle. + # Then x = cos + I*sin generates a cyclotomic field C and for some N we + # have x^N = ±1. Since C is contained in the compositum of K=Q(cos) and + # L=Q(i*sin) and Q(cos) and Q(sin) are both contained in C, the degree of C + # is bounded from above by twice (accounting for the imaginary unit) the + # degrees of K and L. The degree of C is the totient of N which is bounded + # from below by n / (e^γ loglog n + 3 / loglog n) [cf. wikipedia]. + degree_bound = 2 * cos.minpoly().degree() * sin.minpoly().degree() + + from itertools import count + + for n in count(2): + xN *= x + + c = xN.real() + s = xN.imag() + + if xN.real().contains_zero() or xN.imag().contains_zero(): + c, s = cos, sin + for i in range(n - 1): + c, s = c * cos - s * sin, s * cos + c * sin + + if c == 0 or s == 0: + return True + + CBF = ComplexBallField(CBF.precision() * 2) + x = CBF(cos) + CBF.gen(0) * CBF(sin) + xN = x**n + + from math import log + + if n / (2.0 * log(log(n)) + 3 / log(log(n))) > 2 * degree_bound: + return False + + +def angle(u, v, numerical=False): + r""" + Return the angle between the vectors ``u`` and ``v`` divided by `2 \pi`. + + INPUT: + + - ``u``, ``v`` - vectors + + - ``numerical`` - boolean (default: ``False``), whether to return floating + point numbers + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import angle + + As the implementation is dirty, we at least check that it works for all + denominator up to 20:: + + sage: u = vector((AA(1),AA(0))) + sage: for n in xsrange(1,20): # long time (1.5s) + ....: for k in xsrange(1,n): + ....: v = vector((AA(cos(2*k*pi/n)), AA(sin(2*k*pi/n)))) + ....: assert angle(u,v) == k/n + + The numerical version (working over floating point numbers):: + + sage: import math + sage: u = (1, 0) + sage: for n in xsrange(1,20): + ....: for k in xsrange(1,n): + ....: a = 2 * k * math.pi / n + ....: v = (math.cos(a), math.sin(a)) + ....: assert abs(angle(u,v,numerical=True) * 2 * math.pi - a) < 1.e-10 + + If the angle is not rational, then the method returns an element in the real + lazy field:: + + sage: v = vector((AA(sqrt(2)), AA(sqrt(3)))) + sage: a = angle(u, v) + Traceback (most recent call last): + ... + NotImplementedError: cannot recover a rational angle from these numerical results + sage: a = angle(u, v, numerical=True) + sage: a # abs tol 1e-14 + 0.14102355421224375 + sage: exp(2*pi.n()*CC(0,1)*a) + 0.632455532033676 + 0.774596669241483*I + sage: v / v.norm() + (0.6324555320336758?, 0.774596669241484?) + + """ + import math + + u0 = float(u[0]) + u1 = float(u[1]) + v0 = float(v[0]) + v1 = float(v[1]) + + cos_uv = (u0 * v0 + u1 * v1) / math.sqrt((u0 * u0 + u1 * u1) * (v0 * v0 + v1 * v1)) + if cos_uv < -1.0: + assert cos_uv > -1.0000001 + cos_uv = -1.0 + elif cos_uv > 1.0: + assert cos_uv < 1.0000001 + cos_uv = 1.0 + angle = math.acos(cos_uv) / (2 * math.pi) # rat number between 0 and 1/2 + + if numerical: + return 1.0 - angle if u0 * v1 - u1 * v0 < 0 else angle + + # fast and dirty way using floating point approximation + # (see below for a slow but exact method) + from sage.all import RR + + angle_rat = RR(angle).nearby_rational(0.00000001) + if angle_rat.denominator() > 256: + raise NotImplementedError( + "cannot recover a rational angle from these numerical results" + ) + return 1 - angle_rat if u0 * v1 - u1 * v0 < 0 else angle_rat + + # a neater way is provided below by working only with number fields + # but this method is slower... + # sqnorm_u = u[0]*u[0] + u[1]*u[1] + # sqnorm_v = v[0]*v[0] + v[1]*v[1] + # + # if sqnorm_u != sqnorm_v: + # # we need to take a square root in order that u and v have the + # # same norm + # u = (1 / AA(sqnorm_u)).sqrt() * u.change_ring(AA) + # v = (1 / AA(sqnorm_v)).sqrt() * v.change_ring(AA) + # sqnorm_u = AA.one() + # sqnorm_v = AA.one() + # + # cos_uv = (u[0]*v[0] + u[1]*v[1]) / sqnorm_u + # sin_uv = (u[0]*v[1] - u[1]*v[0]) / sqnorm_u + + +def ccw(v, w): + r""" + Return a positive number if the turn from ``v`` to ``w`` is + counterclockwise, a negative number if it is clockwise, and zero if the two + vectors are collinear. + + .. NOTE:: + + This function is sometimes also referred to as the wedge product or + simply the determinant. We chose the more customary name ``ccw`` from + computational geometry here. + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import ccw + sage: ccw((1, 0), (0, 1)) + 1 + sage: ccw((1, 0), (-1, 0)) + 0 + sage: ccw((1, 0), (0, -1)) + -1 + sage: ccw((1, 0), (1, 0)) + 0 + + """ + return v[0] * w[1] - v[1] * w[0] + + +def is_parallel(v, w): + r""" + Return whether the vectors ``v`` and ``w`` are parallel (but not + anti-parallel.) + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import is_parallel + sage: is_parallel((0, 1), (0, 1)) + True + sage: is_parallel((0, 1), (0, 2)) + True + sage: is_parallel((0, 1), (0, -2)) + False + sage: is_parallel((0, 1), (0, 0)) + False + sage: is_parallel((0, 1), (1, 0)) + False + + TESTS:: + + sage: V = QQ**2 + + sage: is_parallel(V((0,1)), V((0,2))) + True + sage: is_parallel(V((1,-1)), V((2,-2))) + True + sage: is_parallel(V((4,-2)), V((2,-1))) + True + sage: is_parallel(V((1,2)), V((2,4))) + True + sage: is_parallel(V((0,2)), V((0,1))) + True + + sage: is_parallel(V((1,1)), V((1,2))) + False + sage: is_parallel(V((1,2)), V((2,1))) + False + sage: is_parallel(V((1,2)), V((1,-2))) + False + sage: is_parallel(V((1,2)), V((-1,-2))) + False + sage: is_parallel(V((2,-1)), V((-2,1))) + False + + """ + if ccw(v, w) != 0: + # vectors are not collinear + return False + + return v[0] * w[0] + v[1] * w[1] > 0 + + +def is_anti_parallel(v, w): + r""" + Return whether the vectors ``v`` and ``w`` are anti-parallel, i.e., whether + ``v`` and ``-w`` are parallel. + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import is_anti_parallel + sage: V = QQ**2 + + sage: is_anti_parallel(V((0,1)), V((0,-2))) + True + sage: is_anti_parallel(V((1,-1)), V((-2,2))) + True + sage: is_anti_parallel(V((4,-2)), V((-2,1))) + True + sage: is_anti_parallel(V((-1,-2)), V((2,4))) + True + + sage: is_anti_parallel(V((1,1)), V((1,2))) + False + sage: is_anti_parallel(V((1,2)), V((2,1))) + False + sage: is_anti_parallel(V((0,2)), V((0,1))) + False + sage: is_anti_parallel(V((1,2)), V((1,-2))) + False + sage: is_anti_parallel(V((1,2)), V((-1,2))) + False + sage: is_anti_parallel(V((2,-1)), V((-2,-1))) + False + + """ + return is_parallel(v, -w) + + +def line_intersection(p1, p2, q1, q2): + r""" + Return the point of intersection between the line joining p1 to p2 + and the line joining q1 to q2. If the lines do not have a single point of + intersection, we return None. Here p1, p2, q1 and q2 should be vectors in + the plane. + """ + if ccw(p2 - p1, q2 - q1) == 0: + return None + + # Since the wedge product is non-zero, the following is invertible: + from sage.all import matrix + + m = matrix([[p2[0] - p1[0], q1[0] - q2[0]], [p2[1] - p1[1], q1[1] - q2[1]]]) + return p1 + (m.inverse() * (q1 - p1))[0] * (p2 - p1) + + +def is_segment_intersecting(e1, e2): + r""" + Return whether the segments ``e1`` and ``e2`` intersect. + + OUTPUT: + + - ``0`` - do not intersect + - ``1`` - one endpoint in common + - ``2`` - non-trivial intersection + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import is_segment_intersecting + sage: is_segment_intersecting(((0,0),(1,0)),((0,1),(0,3))) + 0 + sage: is_segment_intersecting(((0,0),(1,0)),((0,0),(0,3))) + 1 + sage: is_segment_intersecting(((0,0),(1,0)),((0,-1),(0,3))) + 2 + sage: is_segment_intersecting(((-1,-1),(1,1)),((0,0),(2,2))) + 2 + sage: is_segment_intersecting(((-1,-1),(1,1)),((1,1),(2,2))) + 1 + + """ + if e1[0] == e1[1] or e2[0] == e2[1]: + raise ValueError("degenerate segments") + + elts = [e[i][j] for e in (e1, e2) for i in (0, 1) for j in (0, 1)] + + from sage.structure.element import get_coercion_model + + cm = get_coercion_model() + + base_ring = cm.common_parent(*elts) + if isinstance(base_ring, type): + from sage.structure.coerce import py_scalar_parent + + base_ring = py_scalar_parent(base_ring) + + from sage.all import matrix + + m = matrix(base_ring, 3) + xs1, ys1 = map(base_ring, e1[0]) + xt1, yt1 = map(base_ring, e1[1]) + xs2, ys2 = map(base_ring, e2[0]) + xt2, yt2 = map(base_ring, e2[1]) + + m[0] = [xs1, ys1, 1] + m[1] = [xt1, yt1, 1] + m[2] = [xs2, ys2, 1] + s0 = m.det() + m[2] = [xt2, yt2, 1] + s1 = m.det() + if (s0 > 0 and s1 > 0) or (s0 < 0 and s1 < 0): + # e2 stands on one side of the line generated by e1 + return 0 + + m[0] = [xs2, ys2, 1] + m[1] = [xt2, yt2, 1] + m[2] = [xs1, ys1, 1] + s2 = m.det() + m[2] = [xt1, yt1, 1] + s3 = m.det() + if (s2 > 0 and s3 > 0) or (s2 < 0 and s3 < 0): + # e1 stands on one side of the line generated by e2 + return 0 + + if s0 == 0 and s1 == 0: + assert s2 == 0 and s3 == 0 + if xt1 < xs1 or (xt1 == xs1 and yt1 < ys1): + xs1, xt1 = xt1, xs1 + ys1, yt1 = yt1, ys1 + if xt2 < xs2 or (xt2 == xs2 and yt2 < ys2): + xs2, xt2 = xt2, xs2 + ys2, yt2 = yt2, ys2 + + if xs1 == xt1 == xs2 == xt2: + xs1, xt1, xs2, xt2 = ys1, yt1, ys2, yt2 + + assert xs1 < xt1 and xs2 < xt2, (xs1, xt1, xs2, xt2) + + if (xs2 > xt1) or (xt2 < xs1): + return 0 # no intersection + elif (xs2 == xt1) or (xt2 == xs1): + return 1 # one endpoint in common + else: + assert ( + xs1 <= xs2 < xt1 + or xs1 < xt2 <= xt1 + or (xs2 < xs1 and xt2 > xt1) + or (xs2 > xs1 and xt2 < xt1) + ), (xs1, xt1, xs2, xt2) + return 2 # one dimensional + + elif s0 == 0 or s1 == 0: + # treat alignment here + if s2 == 0 or s3 == 0: + return 1 # one endpoint in common + else: + return 2 # intersection in the middle + + return 2 # middle intersection + + +def is_between(e0, e1, f): + r""" + Check whether the vector ``f`` is strictly in the sector formed by the vectors + ``e0`` and ``e1`` (in counter-clockwise order). + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import is_between + sage: V = ZZ^2 + sage: is_between(V((1, 0)), V((1, 1)), V((2, 1))) + True + + """ + if e0[0] * e1[1] > e1[0] * e0[1]: + # positive determinant + # [ e0[0] e1[0] ]^-1 = [ e1[1] -e1[0] ] + # [ e0[1] e1[1] ] [-e0[1] e0[0] ] + # f[0] * e1[1] - e1[0] * f[1] > 0 + # - f[0] * e0[1] + e0[0] * f[1] > 0 + return e1[1] * f[0] > e1[0] * f[1] and e0[0] * f[1] > e0[1] * f[0] + elif e0[0] * e1[1] == e1[0] * e0[1]: + # aligned vector + return e0[0] * f[1] > e0[1] * f[0] + else: + # negative determinant + # [ e1[0] e0[0] ]^-1 = [ e0[1] -e0[0] ] + # [ e1[1] e0[1] ] [-e1[1] e1[0] ] + # f[0] * e0[1] - e0[0] * f[1] > 0 + # - f[0] * e1[1] + e1[0] * f[1] > 0 + return e0[1] * f[0] <= e0[0] * f[1] or e1[0] * f[1] <= e1[1] * f[0] + + +def solve(x, u, y, v): + r""" + Return (a,b) so that: x + au = y + bv + + INPUT: + + - ``x``, ``u``, ``y``, ``v`` -- two dimensional vectors + + EXAMPLES:: + + sage: from flatsurf.geometry.euclidean import solve + sage: K. = NumberField(x^2 - 2, embedding=AA(2).sqrt()) + sage: V = VectorSpace(K,2) + sage: x = V((1,-sqrt2)) + sage: y = V((1,1)) + sage: a = V((0,1)) + sage: b = V((-sqrt2, sqrt2+1)) + sage: u = V((0,1)) + sage: v = V((-sqrt2, sqrt2+1)) + sage: a, b = solve(x,u,y,v) + sage: x + a*u == y + b*v + True + + sage: u = V((1,1)) + sage: v = V((1,sqrt2)) + sage: a, b = solve(x,u,y,v) + sage: x + a*u == y + b*v + True + + """ + d = -u[0] * v[1] + u[1] * v[0] + if d.is_zero(): + raise ValueError("parallel vectors") + a = v[1] * (x[0] - y[0]) + v[0] * (y[1] - x[1]) + b = u[1] * (x[0] - y[0]) + u[0] * (y[1] - x[1]) + return (a / d, b / d) + + +def projectivization(x, y, signed=True, denominator=None): + r""" + Return a simplified version of the projective coordinate [x: y]. + + If ``signed`` (the default), the second coordinate is made non-negative; + otherwise the coordinates keep their signs. + + If ``denominator`` is ``False``, returns [x/y: 1] up to sign. Otherwise, + the returned coordinates have no denominator and no non-unit gcd. + + TESTS:: + + sage: from flatsurf.geometry.euclidean import projectivization + + sage: projectivization(2/3, -3/5, signed=True, denominator=True) + (10, -9) + sage: projectivization(2/3, -3/5, signed=False, denominator=True) + (-10, 9) + sage: projectivization(2/3, -3/5, signed=True, denominator=False) + (10/9, -1) + sage: projectivization(2/3, -3/5, signed=False, denominator=False) + (-10/9, 1) + + sage: projectivization(-1/2, 0, signed=True, denominator=True) + (-1, 0) + sage: projectivization(-1/2, 0, signed=False, denominator=True) + (1, 0) + sage: projectivization(-1/2, 0, signed=True, denominator=False) + (-1, 0) + sage: projectivization(-1/2, 0, signed=False, denominator=False) + (1, 0) + + """ + from sage.all import Sequence + + parent = Sequence([x, y]).universe() + if y: + z = x / y + if denominator is True or (denominator is None and hasattr(z, "denominator")): + d = parent(z.denominator()) + else: + d = parent(1) + if signed and y < 0: + d *= -1 + return (z * d, d) + elif signed and x < 0: + return (parent(-1), parent(0)) + else: + return (parent(1), parent(0)) diff --git a/flatsurf/geometry/finitely_generated_matrix_group.py b/flatsurf/geometry/finitely_generated_matrix_group.py index cb82efe09..acb936dbe 100644 --- a/flatsurf/geometry/finitely_generated_matrix_group.py +++ b/flatsurf/geometry/finitely_generated_matrix_group.py @@ -1,14 +1,6 @@ r""" Class for matrix groups generated by a finite number of elements. -.. TODO:: - - - The stupid test ``_test_change_ring`` fails because if a matrix ``m`` is - immutable then ``m.change_ring(m.base_ring())`` returns ``m`` and not a - copy. - - - ``m.multiplicative_order()`` - EXAMPLES:: sage: from flatsurf.geometry.finitely_generated_matrix_group import FinitelyGenerated2x2MatrixGroup @@ -30,14 +22,24 @@ sage: G = FinitelyGenerated2x2MatrixGroup([identity_matrix(2)]) """ # **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper -# 2023 Julian Rüth +# This file is part of sage-flatsurf. +# +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . # **************************************************************************** from sage.rings.integer import Integer diff --git a/flatsurf/geometry/fundamental_group.py b/flatsurf/geometry/fundamental_group.py index c1759fefa..033ca9ea7 100644 --- a/flatsurf/geometry/fundamental_group.py +++ b/flatsurf/geometry/fundamental_group.py @@ -1,11 +1,21 @@ # **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper +# This file is part of sage-flatsurf. # -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# +# 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 . # **************************************************************************** from sage.misc.cachefunc import cached_method @@ -81,7 +91,7 @@ def _reduce(self): pass def _poly_cross_dict(self): - d = {p: [] for p in self.parent()._s.label_iterator()} + d = {p: [] for p in self.parent()._s.labels()} d[self._polys[0]].append((self._edges_rev[-1], self._edges[0])) for i in range(1, len(self._polys) - 1): p = self._polys[i] @@ -91,13 +101,13 @@ def _poly_cross_dict(self): return d def __hash__(self): - return hash(self._polys) ^ hash(self._edges) + return hash((self._polys, self._edges)) def __eq__(self, other): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: t = translation_surfaces.square_torus() sage: F = t.fundamental_group() sage: a,b = F.gens() @@ -118,7 +128,7 @@ def __ne__(self, other): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: t = translation_surfaces.square_torus() sage: F = t.fundamental_group() sage: a,b = F.gens() @@ -156,7 +166,7 @@ def _mul_(self, other): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: t = translation_surfaces.square_torus() sage: a,b = t.fundamental_group().gens() sage: a*b @@ -189,7 +199,7 @@ def __invert__(self): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: o = translation_surfaces.octagon_and_squares() sage: F = o.fundamental_group() sage: a1,a2,a3,a4,a5,a6 = F.gens() @@ -209,7 +219,7 @@ def intersection(self, other): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: t = translation_surfaces.square_torus() sage: a,b = t.fundamental_group().gens() sage: a.intersection(b) @@ -269,7 +279,7 @@ def intersection(self, other): si = self._poly_cross_dict() oi = other._poly_cross_dict() n = 0 - for p in self.parent()._s.label_iterator(): + for p in self.parent()._s.labels(): for e0, e1 in si[p]: for f0, f1 in oi[p]: n += intersection(e0, e1, f0, f1) @@ -282,14 +292,14 @@ class FundamentalGroup(UniqueRepresentation, Group): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: t = translation_surfaces.square_torus() sage: TestSuite(t.fundamental_group()).run() """ Element = Path def __init__(self, surface, base): - if not surface.is_finite(): + if not surface.is_finite_type(): raise ValueError("the method only work for finite surfaces") self._s = surface self._b = base @@ -300,7 +310,7 @@ def _element_constructor_(self, *args): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: S = SymmetricGroup(4) sage: r = S('(1,2)(3,4)') sage: u = S('(2,3)') @@ -323,7 +333,7 @@ def _element_constructor_(self, *args): e = [] er = [] for i in args: - i = int(i) % s.polygon(p[-1]).num_edges() + i = int(i) % len(s.polygon(p[-1]).vertices()) q, j = s.opposite_edge(p[-1], i) p.append(q) e.append(i) @@ -344,7 +354,7 @@ def gens(self): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: S = SymmetricGroup(8) sage: r = S('(1,2,3,4,5,6,7,8)') sage: u = S('(1,8,5,4)(2,3)(6,7)') @@ -360,7 +370,7 @@ def gens(self): tree[p] = (None, None, None) wait = [] # list of edges of the dual graph, ie p1 -- (e1,e2) --> p2 - for e in range(s.polygon(p).num_edges()): + for e in range(len(s.polygon(p).vertices())): pp, ee = s.opposite_edge(p, e) wait.append((pp, ee, p, e)) while wait: @@ -398,7 +408,7 @@ def gens(self): else: # new branch tree[p1] = (p2, e1, e2) - for e in range(s.polygon(p1).num_edges()): + for e in range(len(s.polygon(p1).vertices())): if e != e1: pp, ee = s.opposite_edge(p1, e) wait.append((pp, ee, p1, e)) diff --git a/flatsurf/geometry/gl2r_orbit_closure.py b/flatsurf/geometry/gl2r_orbit_closure.py index 80c33b8bf..22b0f1918 100644 --- a/flatsurf/geometry/gl2r_orbit_closure.py +++ b/flatsurf/geometry/gl2r_orbit_closure.py @@ -60,7 +60,7 @@ sage: all((d.completelyPeriodic() == True) or (d.hasCylinder() == False) for d in O.decompositions(6)) # optional: pyflatsurf True """ -###################################################################### +# **************************************************************************** # This file is part of sage-flatsurf. # # Copyright (C) 2019-2022 Julian Rüth @@ -78,7 +78,7 @@ # # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . -###################################################################### +# **************************************************************************** from sage.all import FreeModule, matrix, identity_matrix, ZZ, QQ, Unknown, vector, prod @@ -101,21 +101,22 @@ class GL2ROrbitClosure: Computing an orbit closure over an exact real ring with transcendental elements:: - sage: from flatsurf import EquiangularPolygons + sage: from flatsurf import Polygon, EuclideanPolygonsWithAngles sage: from pyexactreal import ExactReals # optional: exactreal # random output due to matplotlib warnings with some combinations of setuptools and matplotlib - sage: E = EquiangularPolygons(1, 5, 5, 5) + sage: E = EuclideanPolygonsWithAngles((1, 5, 5, 5)) sage: R = ExactReals(E.base_ring()) # optional: exactreal - sage: T = E(R(1), R.random_element(1/4)) # optional: exactreal + sage: slopes = E.slopes() + sage: T = Polygon(angles=(1, 5, 5, 5), edges=[slopes[0], R.random_element(1/4) * slopes[1]]) # optional: exactreal sage: S = similarity_surfaces.billiard(T) # optional: exactreal sage: S = S.minimal_cover(cover_type="translation") # optional: exactreal - sage: O = GL2ROrbitClosure(S); O # optional: pyflatsurf + sage: O = GL2ROrbitClosure(S); O # optional: pyflatsurf, optional: exactreal GL(2,R)-orbit closure of dimension at least 4 in H_7(4^3, 0) (ambient dimension 17) sage: bound = E.billiard_unfolding_stratum('half-translation', marked_points=True).dimension() - sage: for decomposition in O.decompositions(1): # long time, optional: pyflatsurf + sage: for decomposition in O.decompositions(1): # long time, optional: pyflatsurf, optional: exactreal ....: O.update_tangent_space_from_flow_decomposition(decomposition) ....: if O.dimension() == bound: break - sage: O # long time, optional: pyflatsurf + sage: O # long time, optional: pyflatsurf, optional: exactreal GL(2,R)-orbit closure of dimension at least 8 in H_7(4^3, 0) (ambient dimension 17) TESTS:: @@ -149,9 +150,15 @@ class GL2ROrbitClosure: """ def __init__(self, surface): - from flatsurf.geometry.translation_surface import TranslationSurface + from flatsurf.geometry.categories import TranslationSurfaces + from flatsurf.geometry.surface import Surface_base + + if isinstance(surface, Surface_base): + if surface not in TranslationSurfaces(): + raise NotImplementedError( + "cannot compute orbit closure of a non-translation surface" + ) - if isinstance(surface, TranslationSurface): base_ring = surface.base_ring() from flatsurf.geometry.pyflatsurf_conversion import to_pyflatsurf @@ -225,16 +232,15 @@ def dimension(self): EXAMPLES:: - sage: from flatsurf import EquiangularPolygons, similarity_surfaces + sage: from flatsurf import Polygon, similarity_surfaces sage: from flatsurf import GL2ROrbitClosure # optional: pyflatsurf - sage: E = EquiangularPolygons(1, 3, 5) - sage: T = E(1) + sage: T = Polygon(angles=(1, 3, 5)) sage: S = similarity_surfaces.billiard(T) sage: S = S.minimal_cover(cover_type="translation") sage: O = GL2ROrbitClosure(S) # optional: pyflatsurf sage: O.dimension() # optional: pyflatsurf 2 - sage: bound = E.billiard_unfolding_stratum('half-translation', marked_points=True).dimension() + sage: bound = T.category().billiard_unfolding_stratum('half-translation', marked_points=True).dimension() sage: for decomposition in O.decompositions(1): # long time, optional: pyflatsurf ....: if O.dimension() == bound: break ....: O.update_tangent_space_from_flow_decomposition(decomposition) @@ -254,10 +260,9 @@ def ambient_stratum(self): EXAMPLES:: - sage: from flatsurf import EquiangularPolygons, similarity_surfaces + sage: from flatsurf import Polygon, similarity_surfaces sage: from flatsurf import GL2ROrbitClosure # optional: pyflatsurf - sage: E = EquiangularPolygons(1, 3, 5) - sage: T = E(1) + sage: T = Polygon(angles=(1, 3, 5)) sage: S = similarity_surfaces.billiard(T) sage: S = S.minimal_cover(cover_type="translation") sage: O = GL2ROrbitClosure(S) # optional: pyflatsurf @@ -288,33 +293,33 @@ def field_of_definition(self): EXAMPLES:: - sage: from flatsurf import polygons, similarity_surfaces, EquiangularPolygons + sage: from flatsurf import Polygon, similarity_surfaces, EuclideanPolygonsWithAngles sage: from flatsurf import GL2ROrbitClosure # optional: pyflatsurf sage: from pyexactreal import ExactReals # optional: exactreal - sage: E = EquiangularPolygons(1, 5, 5, 5) + sage: E = EuclideanPolygonsWithAngles((1, 5, 5, 5)) sage: R = ExactReals(E.base_ring()) # optional: exactreal - sage: T = E(R(1), R.random_element(1/4)) # optional: exactreal + sage: slopes = E.slopes() + sage: T = Polygon(angles=(1, 5, 5, 5), edges=[slopes[0], R.random_element(1/4) * slopes[1]]) # optional: exactreal sage: S = similarity_surfaces.billiard(T) # optional: exactreal sage: S = S.minimal_cover(cover_type="translation") # optional: exactreal - sage: O = GL2ROrbitClosure(S); O # optional: pyflatsurf + sage: O = GL2ROrbitClosure(S); O # optional: pyflatsurf, optional: exactreal GL(2,R)-orbit closure of dimension at least 4 in H_7(4^3, 0) (ambient dimension 17) - sage: O.field_of_definition() # optional: pyflatsurf + sage: O.field_of_definition() # optional: pyflatsurf, optional: exactreal Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? sage: bound = E.billiard_unfolding_stratum('half-translation', marked_points=True).dimension() - sage: for decomposition in O.decompositions(1): # long time, optional: pyflatsurf + sage: for decomposition in O.decompositions(1): # long time, optional: pyflatsurf, optional: exactreal ....: if O.dimension() == bound: break ....: O.update_tangent_space_from_flow_decomposition(decomposition) - sage: O.field_of_definition() # long time, optional: pyflatsurf + sage: O.field_of_definition() # long time, optional: pyflatsurf, optional: exactreal Rational Field - sage: E = EquiangularPolygons(1, 3, 5) - sage: T = E(1) + sage: T = Polygon(angles=(1, 3, 5)) sage: S = similarity_surfaces.billiard(T) sage: S = S.minimal_cover(cover_type="translation") sage: O = GL2ROrbitClosure(S) # optional: pyflatsurf sage: O.field_of_definition() # optional: pyflatsurf Number Field in c0 with defining polynomial x^3 - 3*x - 1 with c0 = 1.879385241571817? - sage: bound = E.billiard_unfolding_stratum('half-translation', marked_points=True).dimension() + sage: bound = T.category().billiard_unfolding_stratum('half-translation', marked_points=True).dimension() sage: for decomposition in O.decompositions(1): # long time, optional: pyflatsurf ....: if O.dimension() == bound: break ....: O.update_tangent_space_from_flow_decomposition(decomposition) @@ -391,8 +396,8 @@ def lift(self, v): sage: span([v0, v1]) # optional: pyflatsurf Vector space of degree 9 and dimension 2 over Real Embedded Number Field in l with defining polynomial x^2 - x - 8 with l = 3.372281323269015? Basis matrix: - [ 1 0 -1 (1/4*l-1/4 ~ 0.59307033) (-1/4*l+1/4 ~ -0.59307033) 0 (-1/4*l+1/4 ~ -0.59307033) 0 (-1/4*l+1/4 ~ -0.59307033)] - [ 0 1 -1 (1/8*l+7/8 ~ 1.2965352) (-1/8*l+1/8 ~ -0.29653517) -1 (3/8*l-11/8 ~ -0.11039450) (-1/2*l+3/2 ~ -0.18614066) (-1/8*l+1/8 ~ -0.29653517)] + [ 1 0 -1 0 (1/4*l-1/4 ~ 0.59307033) (-1/4*l+1/4 ~ -0.59307033) (1/4*l-1/4 ~ 0.59307033) (-1/4*l+1/4 ~ -0.59307033) 0] + [ 0 1 -1 -1 (1/8*l+7/8 ~ 1.2965352) (-1/8*l+1/8 ~ -0.29653517) (1/8*l-1/8 ~ 0.29653517) (3/8*l-11/8 ~ -0.11039450) (-1/2*l+3/2 ~ -0.18614066)] This can be used to deform the surface:: @@ -826,9 +831,9 @@ def cylinder_circumference(self, component, A, sc_index, proj): sage: c0, c1 = dec.components() # optional: pyflatsurf sage: kz = O.flow_decomposition_kontsevich_zorich_cocycle(dec) # optional: pyflatsurf sage: O.cylinder_circumference(c0, *kz) # optional: pyflatsurf - (1, 0, 0, -1) + (1, 0, -1, 0) sage: O.cylinder_circumference(c1, *kz) # optional: pyflatsurf - (0, 0, -1, 0) + (0, 0, 0, -1) """ if ( component.cylinder() != True diff --git a/flatsurf/geometry/half_dilation_surface.py b/flatsurf/geometry/half_dilation_surface.py index 784b729f0..e9058c0c4 100644 --- a/flatsurf/geometry/half_dilation_surface.py +++ b/flatsurf/geometry/half_dilation_surface.py @@ -1,9 +1,9 @@ # **************************************************************************** # This file is part of sage-flatsurf. # -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper -# 2023 Julian Rüth +# Copyright (C) 2013-2019 Vincent Delecroix +# 2013-2019 W. Patrick Hooper +# 2023 Julian Rüth # # sage-flatsurf is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,377 +19,308 @@ # along with sage-flatsurf. If not, see . # **************************************************************************** -from flatsurf.geometry.surface import Surface -from flatsurf.geometry.similarity_surface import SimilaritySurface +from flatsurf.geometry.surface import OrientedSimilaritySurface from flatsurf.geometry.mappings import SurfaceMapping -from flatsurf.geometry.polygon import ConvexPolygons +from sage.misc.cachefunc import cached_method -from sage.structure.element import is_Matrix - -class HalfDilationSurface(SimilaritySurface): +class GL2RImageSurface(OrientedSimilaritySurface): r""" - Half dilation surface. + The GL(2,R) image of an oriented similarity surface. + + EXAMPLE:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: SS = r * S + + sage: S.canonicalize() == SS.canonicalize() + True + + TESTS:: + + sage: TestSuite(SS).run() + + sage: from flatsurf.geometry.half_dilation_surface import GL2RImageSurface + sage: isinstance(SS, GL2RImageSurface) + True - A half-dilation surface is a (G,X) structure for the group of dilatations - `G = \RR^*` acting on the plane `X = \RR^2`. If you want to consider only - the oriented case, have a look at - :class:`~.dilation_surface.DilationSurface`. """ - def __rmul__(self, matrix): - r""" - EXAMPLES:: + def __init__(self, surface, m, ring=None, category=None): + if surface.is_mutable(): + if surface.is_finite_type(): + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface - sage: from flatsurf import * - sage: s=translation_surfaces.infinite_staircase() - sage: s.underlying_surface() - The infinite staircase - sage: m=Matrix([[1,2],[0,1]]) - sage: s2=m*s - sage: TestSuite(s2).run() - sage: s2.polygon(0) - Polygon: (0, 0), (1, 0), (3, 1), (2, 1) - - Testing multiplication by a matrix with negative determinant:: - - sage: from flatsurf import * - sage: ds1 = dilation_surfaces.genus_two_square(1/2, 1/3, 1/4, 1/5) - sage: ds1.polygon(0) - Polygon: (0, 0), (1/2, 0), (1, 1/3), (1, 1), (3/4, 1), (0, 4/5) - sage: m = matrix(QQ, [[0, 1], [1, 0]]) # maps (x,y) to (y, x) - sage: ds2 = m*ds1 - sage: ds2.polygon(0) - Polygon: (0, 0), (4/5, 0), (1, 3/4), (1, 1), (1/3, 1), (0, 1/2) - """ - if not is_Matrix(matrix): - raise NotImplementedError("Only implemented for matrices.") - if not matrix.dimensions != (2, 2): - raise NotImplementedError("Only implemented for 2x2 matrices.") - return self.__class__(GL2RImageSurface(self, matrix)).copy() + self._s = MutableOrientedSimilaritySurface.from_surface(surface) + else: + raise ValueError("Can not apply matrix to mutable infinite surface.") + else: + self._s = surface - def apply_matrix(self, m, in_place=True, mapping=False): - r""" - Carry out the GL(2,R) action of m on this surface and return the result. + det = m.determinant() + + if det > 0: + self._det_sign = 1 + elif det < 0: + self._det_sign = -1 + else: + raise ValueError("Can not apply matrix with zero determinant to surface.") - If in_place=True, then this is done in place and changes the surface. - This can only be carried out if the surface is finite and mutable. + if m.is_mutable(): + from sage.all import matrix - If mapping=True, then we return a GL2RMapping between this surface and its image. - In this case in_place must be False. + m = matrix(m, immutable=True) - If in_place=False, then a copy is made before the deformation. - """ - if mapping is True: - if in_place: - raise NotImplementedError( - "can not modify in place and return a mapping" - ) - return GL2RMapping(self, m) - if not in_place: - if self.is_finite(): + self._m = m + + if ring is None: + if m.base_ring() == self._s.base_ring(): + base_ring = self._s.base_ring() + else: from sage.structure.element import get_coercion_model cm = get_coercion_model() - field = cm.common_parent(self.base_ring(), m.base_ring()) - s = self.copy(mutable=True, new_field=field) - return s.apply_matrix(m) - else: - return m * self + base_ring = cm.common_parent(m.base_ring(), self._s.base_ring()) else: - # Make sure m is in the right state - from sage.matrix.constructor import Matrix - - m = Matrix(self.base_ring(), 2, 2, m) - if m.det() == self.base_ring().zero(): - raise ValueError("can not deform by degenerate matrix") - if not self.is_finite(): - raise NotImplementedError( - "in-place GL(2,R) action only works for finite surfaces" - ) - us = self.underlying_surface() - if not us.is_mutable(): - raise ValueError("in-place changes only work for mutable surfaces") - for label in self.label_iterator(): - us.change_polygon(label, m * self.polygon(label)) - if m.det() < self.base_ring().zero(): - # Polygons were all reversed orientation. Need to redo gluings. - - # First pass record new gluings in a dictionary. - new_glue = {} - seen_labels = set() - for p1 in self.label_iterator(): - n1 = self.polygon(p1).num_edges() - for e1 in range(n1): - p2, e2 = self.opposite_edge(p1, e1) - n2 = self.polygon(p2).num_edges() - if p2 in seen_labels: - pass - elif p1 == p2 and e1 > e2: - pass - else: - new_glue[(p1, n1 - 1 - e1)] = (p2, n2 - 1 - e2) - seen_labels.add(p1) - # Second pass: reassign gluings - for (p1, e1), (p2, e2) in new_glue.items(): - us.change_edge_gluing(p1, e1, p2, e2) - return self - - def _edge_needs_flip_Linfinity(self, p1, e1, p2, e2): - r""" - Check whether the provided edge which bounds two triangles should be flipped - to get closer to the L-infinity Delaunay decomposition. - - TESTS:: - - sage: from flatsurf import * - sage: s = Surface_list(base_ring=QQ) - sage: t1 = polygons((1,0),(-1,1),(0,-1)) - sage: t2 = polygons((0,1),(-1,0),(1,-1)) - sage: s.add_polygon(polygons(vertices=[(0,0), (1,0), (0,1)])) - 0 - sage: s.add_polygon(polygons(vertices=[(1,1), (0,1), (1,0)])) - 1 - sage: s.change_polygon_gluings(0, [(1,0), (1,1), (1,2)]) - sage: s = TranslationSurface(s) - sage: [s._edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] - [False, False, False] - - sage: ss = matrix(2, [1,1,0,1]) * s - sage: [ss._edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] - [False, False, False] - sage: ss = matrix(2, [1,0,1,1]) * s - sage: [ss._edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] - [False, False, False] - - sage: ss = matrix(2, [1,2,0,1]) * s - sage: [ss._edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] - [False, False, True] - - sage: ss = matrix(2, [1,0,2,1]) * s - sage: [ss._edge_needs_flip_Linfinity(0, i, 1, i) for i in range(3)] - [True, False, False] - """ - # safety check for now - assert self.opposite_edge(p1, e1) == (p2, e2), "not opposite edges" - - # triangles - poly1 = self.polygon(p1) - poly2 = self.polygon(p2) - if poly1.num_edges() != 3 or poly2.num_edges() != 3: - raise ValueError("edge must be adjacent to two triangles") - - edge1 = poly1.edge(e1) - edge1L = poly1.edge(e1 - 1) - edge1R = poly1.edge(e1 + 1) - edge2 = poly2.edge(e2) - edge2L = poly2.edge(e2 - 1) - edge2R = poly2.edge(e2 + 1) - - sim = self.edge_transformation(p2, e2) - m = sim.derivative() # matrix carrying p2 to p1 - if not m.is_one(): - edge2 = m * edge2 - edge2L = m * edge2L - edge2R = m * edge2R - - # convexity check of the quadrilateral - from flatsurf.geometry.polygon import wedge_product - - if wedge_product(edge2L, edge1R) <= 0 or wedge_product(edge1L, edge2R) <= 0: - return False + base_ring = ring + + if category is None: + category = surface.category() - # compare the norms - new_edge = edge2L + edge1R - n1 = max(abs(edge1[0]), abs(edge1[1])) - n = max(abs(new_edge[0]), abs(new_edge[1])) - return n < n1 + super().__init__(base_ring, category=category) - def l_infinity_delaunay_triangulation( - self, triangulated=False, in_place=False, limit=None, direction=None - ): + def roots(self): r""" - Returns a L-infinity Delaunay triangulation of a surface, or make some - triangle flips to get closer to the Delaunay decomposition. + Return root labels for the polygons forming the connected + components of this surface. - INPUT: + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. - - ``triangulated`` -- optional (boolean, default ``False``) If true, the - algorithm assumes the surface is already triangulated. It does this - without verification. + EXAMPLES:: - - ``in_place`` -- optional (boolean, default ``False``) If true, the - triangulating and the triangle flips are done in place. Otherwise, a - mutable copy of the surface is made. + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S - - ``limit`` -- optional (positive integer) If provided, then at most ``limit`` - many diagonal flips will be done. + sage: S.roots() + (0,) - - ``direction`` -- optional (vector). Used to determine labels when a - pair of triangles is flipped. Each triangle has a unique separatrix - which points in the provided direction or its negation. As such a - vector determines a sign for each triangle. A pair of adjacent - triangles have opposite signs. Labels are chosen so that this sign is - preserved (as a function of labels). + """ + return self._s.roots() - EXAMPLES:: + def is_compact(self): + r""" + Return whether this surface is compact as a topological space. - sage: from flatsurf import * - sage: s0 = translation_surfaces.veech_double_n_gon(5) - sage: field = s0.base_ring() - sage: a = field.gen() - sage: m = matrix(field, 2, [2,a,1,1]) + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. - sage: s = m*s0 - sage: s = s.l_infinity_delaunay_triangulation() - sage: TestSuite(s).run() + EXAMPLES:: - sage: s = (m**2)*s0 - sage: s = s.l_infinity_delaunay_triangulation() - sage: TestSuite(s).run() + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S + + sage: S.is_compact() + True - sage: s = (m**3)*s0 - sage: s = s.l_infinity_delaunay_triangulation() - sage: TestSuite(s).run() """ - if not self.is_finite(): - raise NotImplementedError( - "no L-infinity Delaunay implemented for infinite surfaces" - ) - if triangulated: - if in_place: - s = self - else: - from flatsurf.geometry.surface import Surface_dict + return self._s.is_compact() - s = self.__class__(Surface_dict(surface=self, mutable=True)) - else: - from flatsurf.geometry.surface import Surface_list + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. - s = self.__class__( - Surface_list(surface=self.triangulate(in_place=in_place), mutable=True) - ) + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. - if direction is None: - base_ring = self.base_ring() - direction = self.vector_space()((base_ring.zero(), base_ring.one())) + EXAMPLES:: - if direction.is_zero(): - raise ValueError("direction must be non-zero") + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S - triangles = set(s.label_iterator()) - if limit is None: - limit = -1 - else: - limit = int(limit) - while triangles and limit: - p1 = triangles.pop() - for e1 in range(3): - p2, e2 = s.opposite_edge(p1, e1) - if s._edge_needs_flip_Linfinity(p1, e1, p2, e2): - s.triangle_flip(p1, e1, in_place=True, direction=direction) - triangles.add(p1) - triangles.add(p2) - limit -= 1 - return s - - -class GL2RImageSurface(Surface): - r""" - This is a lazy implementation of the SL(2,R) image of a translation surface. + sage: S.is_mutable() + False - EXAMPLE:: + """ + return False - sage: import flatsurf - sage: s=flatsurf.translation_surfaces.octagon_and_squares() - sage: r=matrix(ZZ,[[0,1],[1,0]]) - sage: ss=r*s - sage: TestSuite(ss).run() - sage: s.canonicalize()==ss.canonicalize() - True + def is_translation_surface(self, positive=True): + r""" + Return whether this surface is a translation surface, i.e., glued + edges can be transformed into each other by translations. - """ + This implements + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.ParentMethods.is_translation_surface`. - def __init__(self, surface, m, ring=None): - if surface.is_mutable(): - if surface.is_finite(): - self._s = surface.copy() - else: - raise ValueError("Can not apply matrix to mutable infinite surface.") - else: - self._s = surface + EXAMPLES:: - det = m.determinant() + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S - if det > 0: - self._det_sign = 1 - elif det < 0: - self._det_sign = -1 - else: - raise ValueError("Can not apply matrix with zero determinant to surface.") + sage: S.is_translation_surface() + True - self._m = m + """ + return self._s.is_translation_surface(positive=positive) - if ring is None: - if m.base_ring() == self._s.base_ring(): - base_ring = self._s.base_ring() - else: - from sage.structure.element import get_coercion_model + @cached_method + def polygon(self, lab): + r""" + Return the polygon with ``label``. - cm = get_coercion_model() - base_ring = cm.common_parent(m.base_ring(), self._s.base_ring()) - else: - base_ring = ring + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: - self._P = ConvexPolygons(base_ring) + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S - super().__init__( - base_ring, self._s.base_label(), finite=self._s.is_finite(), mutable=False - ) + sage: S.polygon(0) + Polygon(vertices=[(0, 0), (a, -a), (a + 2, -a), (2*a + 2, 0), (2*a + 2, 2), (a + 2, a + 2), (a, a + 2), (0, 2)]) - def polygon(self, lab): + """ if self._det_sign == 1: p = self._s.polygon(lab) - edges = [self._m * p.edge(e) for e in range(p.num_edges())] - return self._P(edges) + edges = [self._m * p.edge(e) for e in range(len(p.vertices()))] + + from flatsurf import Polygon + + return Polygon(edges=edges, base_ring=self.base_ring()) else: p = self._s.polygon(lab) - edges = [self._m * (-p.edge(e)) for e in range(p.num_edges() - 1, -1, -1)] - return self._P(edges) + edges = [ + self._m * (-p.edge(e)) for e in range(len(p.vertices()) - 1, -1, -1) + ] + + from flatsurf import Polygon + + return Polygon(edges=edges, base_ring=self.base_ring()) + + def labels(self): + r""" + Return the labels of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S + + sage: S.labels() + (0, 1, 2) + + """ + return self._s.labels() def opposite_edge(self, p, e): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + sage: S = r * S + + sage: S.opposite_edge(0, 0) + (2, 0) + + """ if self._det_sign == 1: return self._s.opposite_edge(p, e) else: polygon = self._s.polygon(p) - pp, ee = self._s.opposite_edge(p, polygon.num_edges() - 1 - e) + pp, ee = self._s.opposite_edge(p, len(polygon.vertices()) - 1 - e) polygon2 = self._s.polygon(pp) - return pp, polygon2.num_edges() - 1 - ee + return pp, len(polygon2.vertices()) - 1 - ee + + def __repr__(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: matrix([[0, 1], [1, 0]]) * S + Translation Surface in H_3(4) built from 2 squares and a regular octagon + sage: matrix([[0, 2], [1, 0]]) * S + Translation Surface in H_3(4) built from a rhombus, a rectangle and an octagon + + """ + if self.is_finite_type(): + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + S = MutableOrientedSimilaritySurface.from_surface(self) + S.set_immutable() + return repr(S) + + return f"GL2RImageSurface of {self._s!r}" + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.octagon_and_squares() + sage: r = matrix(ZZ,[[0, 1], [1, 0]]) + + sage: hash(r * S) == hash(r * S) + True + + """ + return hash((self._s, self._m)) def __eq__(self, other): r""" Return whether this image is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: sage: from flatsurf import translation_surfaces sage: S = translation_surfaces.octagon_and_squares() sage: m = matrix(ZZ,[[0, 1], [1, 0]]) - sage: S = m * S - sage: S == S + sage: m * S == m * S True """ - if isinstance(other, GL2RImageSurface): - if ( - self._s == other._s - and self._m == other._m - and self.base_ring() == other.base_ring() - ): - return True + if not isinstance(other, GL2RImageSurface): + return False - return super().__eq__(other) + return ( + self._s == other._s + and self._m == other._m + and self.base_ring() == other.base_ring() + ) class GL2RMapping(SurfaceMapping): @@ -398,11 +329,11 @@ class GL2RMapping(SurfaceMapping): Note that for matrices of negative determinant we need to relabel edges (because edges must have a counterclockwise cyclic order). For each n-gon in the surface, - we relabel edges according to the involution e mapsto n-1-e. + we relabel edges according to the involution `e \mapsto n-1-e`. EXAMPLE:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s=translation_surfaces.veech_2n_gon(4) sage: from flatsurf.geometry.half_dilation_surface import GL2RMapping sage: mat=Matrix([[2,1],[1,1]]) @@ -410,11 +341,11 @@ class GL2RMapping(SurfaceMapping): sage: TestSuite(m.codomain()).run() """ - def __init__(self, s, m, ring=None): + def __init__(self, s, m, ring=None, category=None): r""" Hit the surface s with the 2x2 matrix m which should have positive determinant. """ - codomain = s.__class__(GL2RImageSurface(s, m, ring=ring)) + codomain = GL2RImageSurface(s, m, ring=ring, category=category or s.category()) self._m = m self._im = ~m SurfaceMapping.__init__(self, s, codomain) diff --git a/flatsurf/geometry/half_translation_surface.py b/flatsurf/geometry/half_translation_surface.py deleted file mode 100644 index 1886f5c46..000000000 --- a/flatsurf/geometry/half_translation_surface.py +++ /dev/null @@ -1,302 +0,0 @@ -# **************************************************************************** -# Copyright (C) 2013-2019 Vincent Delecroix <20100.delecroix@gmail.com> -# 2013-2019 W. Patrick Hooper -# 2023 Julian Rüth -# -# Distributed under the terms of the GNU General Public License (GPL) -# as published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# https://www.gnu.org/licenses/ -# **************************************************************************** - -from .polygon import wedge_product -from .half_dilation_surface import HalfDilationSurface -from .rational_cone_surface import RationalConeSurface - -from sage.rings.all import QQ, AA -from sage.matrix.constructor import matrix - - -class HalfTranslationSurface(HalfDilationSurface, RationalConeSurface): - r""" - A half translation surface has gluings between polygons whose monodromy is +I or -I. - """ - - def angles(self, numerical=False, return_adjacent_edges=False): - r""" - Return the set of angles around the vertices of the surface. - - These are given as multiple of `2 \pi`. - - EXAMPLES:: - - sage: import flatsurf.geometry.similarity_surface_generators as sfg - sage: sfg.translation_surfaces.regular_octagon().angles() - [3] - sage: S = sfg.translation_surfaces.veech_2n_gon(5) - sage: S.angles() - [2, 2] - sage: S.angles(numerical=True) - [2.0, 2.0] - sage: S.angles(return_adjacent_edges=True) # random output - [(2, [(0, 1), (0, 5), (0, 9), (0, 3), (0, 7)]), - (2, [(0, 0), (0, 4), (0, 8), (0, 2), (0, 6)])] - sage: S.angles(numerical=True, return_adjacent_edges=True) # random output - [(2.0, [(0, 1), (0, 5), (0, 9), (0, 3), (0, 7)]), - (2.0, [(0, 0), (0, 4), (0, 8), (0, 2), (0, 6)])] - - sage: sfg.translation_surfaces.veech_2n_gon(6).angles() - [5] - sage: sfg.translation_surfaces.veech_double_n_gon(5).angles() - [3] - sage: sfg.translation_surfaces.cathedral(1, 1).angles() - [3, 3, 3] - - sage: from flatsurf import polygons, similarity_surfaces - sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) - sage: H = B.minimal_cover(cover_type="half-translation") - sage: S = B.minimal_cover(cover_type="translation") - sage: H.angles() - [1/2, 5/2, 1/2, 1/2] - sage: S.angles() - [1, 5, 1, 1] - - sage: H.angles(return_adjacent_edges=True) - [(1/2, [...]), (5/2, [...]), (1/2, [...]), (1/2, [...])] - sage: S.angles(return_adjacent_edges=True) - [(1, [...]), (5, [...]), (1, [...]), (1, [...])] - """ - if not self.is_finite(): - raise NotImplementedError("the set of edges is infinite!") - - edges = set(self.edge_iterator()) - angles = [] - - if return_adjacent_edges: - while edges: - # Note that iteration order here is different for different - # versions of Python. Therefore, the output in the doctest - # above is random. - pair = p, e = next(iter(edges)) - ve = self.polygon(p).edge(e) - angle = 0 - adjacent_edges = [] - while pair in edges: - adjacent_edges.append(pair) - edges.remove(pair) - f = (e - 1) % self.polygon(p).num_edges() - ve = self.polygon(p).edge(e) - vf = -self.polygon(p).edge(f) - ppair = pp, ff = self.opposite_edge(p, f) - angle += ( - (ve[0] > 0 and vf[0] <= 0) - or (ve[0] < 0 and vf[0] >= 0) - or (ve[0] == vf[0] == 0) - ) - pair, p, e = ppair, pp, ff - if numerical: - angles.append((float(angle) / 2, adjacent_edges)) - else: - angles.append((QQ((angle, 2)), adjacent_edges)) - else: - while edges: - pair = p, e = next(iter(edges)) - angle = 0 - while pair in edges: - edges.remove(pair) - f = (e - 1) % self.polygon(p).num_edges() - ve = self.polygon(p).edge(e) - vf = -self.polygon(p).edge(f) - ppair = pp, ff = self.opposite_edge(p, f) - angle += ( - (ve[0] > 0 and vf[0] <= 0) - or (ve[0] < 0 and vf[0] >= 0) - or (ve[0] == vf[0] == 0) - ) - pair, p, e = ppair, pp, ff - if numerical: - angles.append(float(angle) / 2) - else: - angles.append(QQ((angle, 2))) - - return angles - - def stratum(self): - r""" - EXAMPLES:: - - sage: from flatsurf import polygons, similarity_surfaces - sage: B = similarity_surfaces.billiard(polygons.triangle(1, 2, 5)) - sage: H = B.minimal_cover(cover_type="half-translation") - sage: H.stratum() - Q_1(3, -1^3) - """ - angles = self.angles() - if all(x.denominator() == 1 for x in angles): - raise NotImplementedError - from surface_dynamics import QuadraticStratum - - return QuadraticStratum(*[2 * a - 2 for a in angles]) - - def _test_edge_matrix(self, **options): - r""" - Check the compatibility condition - """ - tester = self._tester(**options) - from flatsurf.geometry.similarity_surface import SimilaritySurface - - if self.is_finite(): - it = self.label_iterator() - else: - from itertools import islice - - it = islice(self.label_iterator(), 30) - - for lab in it: - p = self.polygon(lab) - for e in range(p.num_edges()): - # Warning: check the matrices computed from the edges, - # rather the ones overridden by TranslationSurface. - m = SimilaritySurface.edge_matrix(self, lab, e) - tester.assertTrue( - m.is_one() or (-m).is_one(), - "edge_matrix between edge e={} and e'={} has matrix\n{}\nwhich is neither a translation nor a rotation by pi".format( - (lab, e), self.opposite_edge((lab, e)), m - ), - ) - - def holonomy_field(self): - r""" - Return the relative holonomy field of this translation or half-translation surface. - - EXAMPLES:: - - sage: from flatsurf import * - - sage: S = translation_surfaces.veech_2n_gon(5) - sage: S.holonomy_field() - Number Field in a0 with defining polynomial x^2 - x - 1 with a0 = 1.618033988749895? - sage: S.base_ring() - Number Field in a with defining polynomial y^4 - 5*y^2 + 5 with a = 1.175570504584947? - - sage: T = translation_surfaces.torus((1, AA(2).sqrt()), (AA(3).sqrt(), 3)) - sage: T.holonomy_field() - Rational Field - - sage: T = polygons.triangle(1,6,11) - sage: S = similarity_surfaces.billiard(T) - sage: S = S.minimal_cover("translation") - sage: S.base_ring() - Number Field in c with defining polynomial x^6 - 6*x^4 + 9*x^2 - 3 with c = 1.969615506024417? - sage: S.holonomy_field() - Number Field in c0 with defining polynomial x^3 - 3*x - 1 with c0 = 1.879385241571817? - """ - return self.normalized_coordinates()[0].base_ring() - - def normalized_coordinates(self): - r""" - Return a pair ``(new_surface, matrix)`` where ``new_surface`` is defined over the - holonomy field and ``matrix`` is the transition matrix that maps this surface to - ``new_surface``. - - EXAMPLES:: - - sage: from flatsurf import * - - sage: S = translation_surfaces.veech_2n_gon(5) - sage: U, mat = S.normalized_coordinates() - sage: U.base_ring() - Number Field in a0 with defining polynomial x^2 - x - 1 with a0 = 1.618033988749895? - sage: mat - [ 0 -2/5*a^3 + 2*a] - [ -1 -3/5*a^3 + 2*a] - - sage: T = translation_surfaces.torus((1, AA(2).sqrt()), (AA(3).sqrt(), 3)) - sage: U, mat = T.normalized_coordinates() - sage: U.base_ring() - Rational Field - sage: U.holonomy_field() - Rational Field - sage: mat - [-2.568914100752347? 1.816496580927726?] - [-5.449489742783178? 3.146264369941973?] - sage: TestSuite(U).run() - - sage: T = polygons.triangle(1,6,11) - sage: S = similarity_surfaces.billiard(T) - sage: S = S.minimal_cover("translation") - sage: U, _ = S.normalized_coordinates() - sage: U.base_ring() - Number Field in c0 with defining polynomial x^3 - 3*x - 1 with c0 = 1.879385241571817? - sage: U.holonomy_field() == U.base_ring() - True - sage: S.base_ring() - Number Field in c with defining polynomial x^6 - 6*x^4 + 9*x^2 - 3 with c = 1.969615506024417? - sage: TestSuite(U).run() - - sage: from flatsurf import EquiangularPolygons - sage: E = EquiangularPolygons(1, 3, 1, 1) - sage: r1, r2 = [r.vector() for r in E.lengths_polytope().rays()] - sage: p = E(r1 + r2) - sage: B = similarity_surfaces.billiard(p) - sage: B.minimal_cover("translation") - TranslationSurface built from 6 polygons - sage: S = B.minimal_cover("translation") - sage: S, _ = S.normalized_coordinates() - sage: S - TranslationSurface built from 6 polygons - """ - if not self.is_finite(): - raise ValueError("the surface must be finite") - if self.base_ring() is QQ: - return (self, matrix(QQ, 2, 2, 1)) - - lab = next(self.label_iterator()) - p = self.polygon(lab) - u = p.edge(1) - v = -p.edge(0) - i = 1 - while wedge_product(u, v) == 0: - i += 1 - u = p.edge(i) - v = -p.edge(i - 1) - M = matrix(2, [u, v]).transpose().inverse() - assert M.det() > 0 - hols = [] - for lab in self.label_iterator(): - p = self.polygon(lab) - for e in range(p.num_edges()): - w = M * p.edge(e) - hols.append(w[0]) - hols.append(w[1]) - if self.base_ring() is AA: - from .subfield import number_field_elements_from_algebraics - - K, new_hols = number_field_elements_from_algebraics(hols) - else: - from .subfield import subfield_from_elements - - K, new_hols, _ = subfield_from_elements(self.base_ring(), hols) - - from .polygon import ConvexPolygons - from .surface import Surface_list - - S = Surface_list(K) - C = ConvexPolygons(K) - relabelling = {} - k = 0 - for lab in self.label_iterator(): - m = self.polygon(lab).num_edges() - relabelling[lab] = S.add_polygon( - C( - edges=[ - (new_hols[k + 2 * i], new_hols[k + 2 * i + 1]) for i in range(m) - ] - ) - ) - k += 2 * m - - for (p1, e1), (p2, e2) in self.edge_iterator(gluings=True): - S.set_edge_pairing(relabelling[p1], e1, relabelling[p2], e2) - - return (type(self)(S), M) diff --git a/flatsurf/geometry/hyperbolic.py b/flatsurf/geometry/hyperbolic.py index e46a2823d..c46bd1e99 100644 --- a/flatsurf/geometry/hyperbolic.py +++ b/flatsurf/geometry/hyperbolic.py @@ -188,7 +188,7 @@ using any symbolic expressions, and tries to produce better plots. """ -###################################################################### +# **************************************************************************** # This file is part of sage-flatsurf. # # Copyright (C) 2022-2023 Julian Rüth @@ -207,10 +207,11 @@ # # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . -###################################################################### +# **************************************************************************** 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 @@ -388,7 +389,7 @@ def __init__(self, base_ring, geometry, category): sage: from flatsurf import HyperbolicPlane sage: TestSuite(HyperbolicPlane(QQ)).run() - sage: TestSuite(HyperbolicPlane(AA)).run() + sage: TestSuite(HyperbolicPlane(AA)).run() # long time (.5s) sage: TestSuite(HyperbolicPlane(RR)).run() """ @@ -449,7 +450,7 @@ def _coerce_map_from_(self, other): def __contains__(self, x): r""" - Return whether the hyperboic plane contains ``x``. + Return whether the hyperbolic plane contains ``x``. EXAMPLES:: @@ -582,29 +583,27 @@ def _an_element_(self): """ return self.real(0) - def some_elements(self): + def some_subsets(self): r""" - Return some representative convex subsets for automated testing. + 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, ...] + [∞, 0, 1, -1, ...] """ from sage.all import ZZ - elements = [ + elements = self.some_elements() + + elements += [ self.empty_set(), - # Points - self.infinity(), - self.real(0), - self.real(1), - self.real(-1), - self.geodesic(0, 2).start(), - self.half_circle(0, 2).start(), # Oriented Geodesics self.vertical(1), self.half_circle(0, 1), @@ -654,6 +653,28 @@ def some_elements(self): return elements + 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. @@ -755,6 +776,39 @@ def random_element(self, kind=None): return kinds[kind].random_set(self) + 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. @@ -809,12 +863,12 @@ def _element_constructor_(self, x): 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) - if isinstance(x, HyperbolicConvexSet): - return x.change_ring(self.base_ring()) - from sage.categories.all import NumberFields if parent in NumberFields(): @@ -1797,7 +1851,7 @@ def polygon( ....: H.half_circle(0, 2).left_half_space(), ....: ]) sage: type(empty) - + :: @@ -1805,7 +1859,7 @@ def polygon( ....: H.half_circle(0, 1).right_half_space(), ....: ]) sage: type(half_space) - + If we add a marked point to such a half space, the underlying type is a polygon again:: @@ -1816,7 +1870,7 @@ def polygon( sage: half_space {(x^2 + y^2) - 1 ≤ 0} ∪ {I} sage: type(half_space) - + Marked points that coincide with vertices are ignored:: @@ -1826,7 +1880,7 @@ def polygon( sage: half_space {(x^2 + y^2) - 1 ≤ 0} sage: type(half_space) - + Marked points must be on an edge of the polygon:: @@ -2225,7 +2279,7 @@ def isometry( ... ValueError: no isometry can map these objects to each other - sage: H.isometry([0, 1, oo, I], [0, 1, oo, I + 1]) + 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 @@ -2256,20 +2310,20 @@ def isometry( 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) + 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) + 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") + sage: H.isometry(P, Q, model="klein") # long time (1s) [-1 0 0] [ 0 1 0] [ 0 0 1] @@ -2422,7 +2476,7 @@ def isometry( 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))) + 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] @@ -2476,7 +2530,7 @@ def isometry( ....: 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) + sage: H.isometry(P, Q) # long time (.4s) [ 0 -1] [1/2 1] @@ -4158,7 +4212,7 @@ def __repr__(self): return f"Epsilon geometry with ϵ={self._epsilon} over {self._ring}" -class HyperbolicConvexSet(Element): +class HyperbolicConvexSet(SageObject): r""" Base class for convex subsets of :class:`HyperbolicPlane`. @@ -4290,7 +4344,7 @@ def _normalize(self): 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 shuold be described as such. + then this is actually a geodesic and it should be described as such. EXAMPLES:: @@ -4836,7 +4890,7 @@ def plot(self, model="half_plane", **kwds): sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() - sage: H.vertical(0).plot() + sage: H.vertical(0).plot() # long time (.5s) ...Graphics object consisting of 1 graphics primitive """ @@ -5171,7 +5225,7 @@ def _test_is_subset(self, **options): else: tester.assertTrue(self.is_subset(self.parent().intersection(*half_spaces))) - def an_element(self): + def _an_element_(self): r""" Return a point of this set. @@ -5198,7 +5252,7 @@ def an_element(self): Exception: empty set has no points We get an element for geodesics without end points in the base ring, - see :meth:`HyperbolicGeodesic.an_element`:: + see :meth:`HyperbolicGeodesic._an_element_`:: sage: H.half_circle(0, 2).an_element() (0, 1/3) @@ -5454,9 +5508,9 @@ def is_oriented(self): :meth:`change` to pick an orientation on an unoriented set - :meth:`HyperbolicHalfSpace._neg_`, - :meth:`HyperbolicOrientedGeodesic._neg_`, - :meth:`HyperbolicOrientedSegment._neg_` i.e., the ``-`` operator, + :meth:`HyperbolicHalfSpace.__neg__`, + :meth:`HyperbolicOrientedGeodesic.__neg__`, + :meth:`HyperbolicOrientedSegment.__neg__` i.e., the ``-`` operator, to invert the orientation of a set """ @@ -5759,7 +5813,111 @@ class HyperbolicOrientedConvexSet(HyperbolicConvexSet): """ -class HyperbolicHalfSpace(HyperbolicConvexSet): +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 + + """ + + def __init__(self, parent, category=None): + Parent.__init__(self, facade=parent, category=category) + + 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 + + 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() + + +class HyperbolicHalfSpace(HyperbolicConvexFacade): r""" A closed half space of the hyperbolic plane. @@ -5907,7 +6065,7 @@ def half_spaces(self): """ return HyperbolicHalfSpaces([self], assume_sorted=True) - def _neg_(self): + def __neg__(self): r""" Return the closure of the complement of this half space. @@ -5980,7 +6138,7 @@ def __contains__(self, point): sage: H.half_circle(0, 2).start() in h Traceback (most recent call last): ... - ValueError: ... + NotImplementedError: cannot decide whether this ideal point is contained in the half space yet .. NOTE:: @@ -5997,7 +6155,20 @@ def __contains__(self, point): if not isinstance(point, HyperbolicPoint): raise TypeError("point must be a point in the hyperbolic plane") - x, y = point.coordinates(model="klein") + 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 point in self.boundary(): + return True + + raise NotImplementedError( + "cannot decide whether this ideal point is contained in the half space yet" + ) + a, b, c = self.equation(model="klein") # We should use a specialized predicate here to do something more @@ -6005,40 +6176,33 @@ def __contains__(self, point): # rings. return self.parent().geometry._sgn(a + b * x + c * y) >= 0 - def _richcmp_(self, other, op): + def __eq__(self, other): r""" - Return how this half space compares to ``other`` with respect to the - ``op`` operator. - - This is only implemented for the operators ``==`` and ``!=``. It - returns whether the two spaces are indistinguishable. + Return whether this set is indistinguishable from ``other``. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() - sage: H.vertical(0).left_half_space() == H.vertical(0).left_half_space() - True + sage: h = H.vertical(0).left_half_space() - sage: H.vertical(0).left_half_space() != H.vertical(0).right_half_space() + sage: h == H.vertical(0).left_half_space() True + sage: h == H.vertical(0).right_half_space() + False - sage: H.vertical(0).left_half_space() != H.vertical(0) + :: + + sage: h != H.vertical(0).left_half_space() + False + sage: h != H.vertical(0).right_half_space() True """ - 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, HyperbolicHalfSpace): - return False - return self._geodesic._richcmp_(other._geodesic, op) - - return super()._richcmp_(other, op) + if not isinstance(other, HyperbolicHalfSpace): + return False + return self._geodesic == other._geodesic def plot(self, model="half_plane", **kwds): r""" @@ -6306,7 +6470,7 @@ def random_set(cls, parent): return HyperbolicOrientedGeodesic.random_set(parent).left_half_space() -class HyperbolicGeodesic(HyperbolicConvexSet): +class HyperbolicGeodesic(HyperbolicConvexFacade): r""" A geodesic in the hyperbolic plane. @@ -7122,20 +7286,16 @@ def is_vertical(self): """ return self.parent().infinity() in self - def _richcmp_(self, other, op): + def __eq__(self, other): r""" - Return how this geodesic compares to ``other`` with respect to the - ``op`` operator. + Return whether this geodesic is identical to other up to (orientation + preserving) scaling of the defining equation. - This is only implemented for the operators ``==`` and ``!=``. It - returns whether the two geodesics are indistinguishable up to scaling - of their defining equations. EXAMPLES:: sage: from flatsurf import HyperbolicPlane sage: H = HyperbolicPlane() - sage: H.vertical(0) == H.vertical(0) True @@ -7143,11 +7303,15 @@ def _richcmp_(self, other, op): 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:: @@ -7159,6 +7323,8 @@ def _richcmp_(self, other, op): False sage: g == h True + sage: g != h + False .. NOTE:: @@ -7168,33 +7334,27 @@ def _richcmp_(self, other, op): implementation in the :class:`HyperbolicGeometry`. """ - from sage.structure.richcmp import op_EQ, op_NE - - if op == op_NE: - return not self._richcmp_(other, op_EQ) - - if op == op_EQ: - # See note in the docstring. We should use specialized geometry here. - equal = self.parent().geometry._equal - sgn = self.parent().geometry._sgn + if type(self) is not type(other): + return False - if type(self) is not type(other): - return False + other = self.parent()(other) - 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) - ) + # See note in the docstring. We should use specialized geometry here. + equal = self.parent().geometry._equal + sgn = self.parent().geometry._sgn - return super()._richcmp_(other, op) + 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) + ) def __contains__(self, point): r""" @@ -7620,7 +7780,7 @@ def _isometry_equations(self, isometry, image, λ): condition = vector((b, c, a)) * isometry - λ * vector(R, (fb, fc, fa)) return condition.list() - def an_element(self): + def _an_element_(self): r""" Return a finite point on this geodesic. @@ -7863,7 +8023,7 @@ class HyperbolicOrientedGeodesic(HyperbolicGeodesic, HyperbolicOrientedConvexSet """ - def _neg_(self): + def __neg__(self): r""" Return this geodesic with its orientation reversed. @@ -8236,7 +8396,7 @@ def random_set(cls, parent): return parent.geodesic(a, b) -class HyperbolicPoint(HyperbolicConvexSet): +class HyperbolicPoint(HyperbolicConvexSet, Element): r""" A (possibly infinite or even ultra-ideal) point in the :class:`HyperbolicPlane`. @@ -9706,7 +9866,7 @@ def _apply_isometry_klein(self, isometry, on_right=False): return image.start() -class HyperbolicConvexPolygon(HyperbolicConvexSet): +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 @@ -9738,7 +9898,7 @@ class HyperbolicConvexPolygon(HyperbolicConvexSet): """ - def __init__(self, parent, half_spaces, vertices): + def __init__(self, parent, half_spaces, vertices, category=None): r""" TESTS:: @@ -9757,7 +9917,12 @@ def __init__(self, parent, half_spaces, vertices): sage: TestSuite(P).run() """ - super().__init__(parent) + 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") @@ -10911,16 +11076,16 @@ def edges(self, as_segments=False): {{-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()] - [, - , - , - ] + [, + , + , + ] sage: [type(e) for e in P.edges(as_segments=True)] - [, - , - , - ] + [, + , + , + ] """ edges = [] @@ -11317,7 +11482,7 @@ def change(self, ring=None, geometry=None, oriented=None): ....: marked_vertices=[I]) sage: P.change(ring=AA) - {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} + {x - 1 ≤ 0} ∩ {x + 1 ≥ 0} ∩ {(x^2 + y^2) - 1 ≥ 0} ∪ {I} We cannot give a polygon an explicit orientation:: @@ -11342,6 +11507,10 @@ def change(self, ring=None, geometry=None, oriented=None): check=False, assume_sorted=True, assume_minimal=True, + marked_vertices=[ + vertex.change(ring=ring, geometry=geometry) + for vertex in self._marked_vertices + ], ) ) @@ -11353,19 +11522,14 @@ def change(self, ring=None, geometry=None, oriented=None): return self - def _richcmp_(self, other, op): + def __eq__(self, other): r""" - Return how this polygon compares to ``other`` with respect to the - ``op`` operator. - - This is only implemented for the operators ``==`` and ``!=``. It - returns whether polygons are essentially indistinguishable. + 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 @@ -11379,20 +11543,13 @@ def _richcmp_(self, other, op): False """ - 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, HyperbolicConvexPolygon): - return False - return ( - self._half_spaces == other._half_spaces - and self._marked_vertices == other._marked_vertices - ) + if not isinstance(other, HyperbolicConvexPolygon): + return False - return super()._richcmp_(other, op) + return ( + self._half_spaces == other._half_spaces + and self._marked_vertices == other._marked_vertices + ) def __hash__(self): r""" @@ -11592,8 +11749,67 @@ def random_set(cls, parent): if isinstance(polygon, HyperbolicConvexPolygon): return polygon + 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() + -class HyperbolicSegment(HyperbolicConvexSet): +class HyperbolicSegment(HyperbolicConvexFacade): r""" A segment (possibly infinite) in the hyperbolic plane. @@ -12018,7 +12234,7 @@ def plot(self, model="half_plane", **kwds): sage: H = HyperbolicPlane() sage: segment = H.segment(H.half_circle(0, 1), end=I) - sage: segment.plot() + sage: segment.plot() # long time (.25s) ...Graphics object consisting of 1 graphics primitive """ @@ -12046,13 +12262,10 @@ def plot(self, model="half_plane", **kwds): return self._enhance_plot(plot, model=model) - def _richcmp_(self, other, op): + def __eq__(self, other): r""" - Return how this segment compares to ``other`` with respect to the - ``op`` operator. - - This is only implemented for the operators ``==`` and ``!=``. It - returns whether two segments are essentially indistinguishable. + Return whether this segment is indistinguishable from ``other`` (except + for scaling in the defining geodesic's equation.) EXAMPLES:: @@ -12071,20 +12284,11 @@ def _richcmp_(self, other, op): True """ - 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 type(self) is not type(other): - return False - return ( - self.geodesic() == other.geodesic() - and self.vertices() == other.vertices() - ) - - return super()._richcmp_(other, op) + if type(self) is not type(other): + return False + return ( + self.geodesic() == other.geodesic() and self.vertices() == other.vertices() + ) def change(self, ring=None, geometry=None, oriented=None): r""" @@ -12511,7 +12715,7 @@ class HyperbolicOrientedSegment(HyperbolicSegment, HyperbolicOrientedConvexSet): """ - def _neg_(self): + def __neg__(self): r""" Return this segment with its orientation reversed. @@ -12682,7 +12886,7 @@ def random_set(cls, parent): return parent.segment(parent.geodesic(a, b), start=a, end=b) -class HyperbolicEmptySet(HyperbolicConvexSet): +class HyperbolicEmptySet(HyperbolicConvexFacade): r""" The empty subset of the hyperbolic plane. @@ -12714,28 +12918,63 @@ class HyperbolicEmptySet(HyperbolicConvexSet): """ - def _richcmp_(self, other, op): + def __eq__(self, other): r""" - Return how this set compares to ``other`` with respect to ``op``. - - This is only implemented for the operators ``==`` and ``!=``. It - returns whether both sets are empty. + 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 """ - from sage.structure.richcmp import rich_to_bool + return isinstance(other, HyperbolicEmptySet) and self.parent() == other.parent() - if isinstance(other, HyperbolicEmptySet): - return rich_to_bool(op, 0) + def some_elements(self): + r""" + Return some representative points of this set for testing. - return rich_to_bool(op, -1) + 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""" @@ -12925,12 +13164,12 @@ def vertices(self, marked_vertices=True): """ return HyperbolicVertices([]) - def an_element(self): + 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 + See :meth:`HyperbolicConvexSet._an_element_` for more interesting examples of this method. EXAMPLES:: diff --git a/flatsurf/geometry/l_infinity_delaunay_cells.py b/flatsurf/geometry/l_infinity_delaunay_cells.py index 65d145ed3..e4cd0da85 100644 --- a/flatsurf/geometry/l_infinity_delaunay_cells.py +++ b/flatsurf/geometry/l_infinity_delaunay_cells.py @@ -6,6 +6,26 @@ and vertical separatrices. Each triangle hence get one of the following types: bottom-left, bottom-right, top-left, top-right. """ +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2019 Vincent Delecroix +# 2016-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** from sage.misc.cachefunc import cached_method # the types of edges @@ -351,30 +371,27 @@ def barycenter(self): sage: T = LInfinityMarkedTriangulation(2, gluings, types) sage: S = T.barycenter() sage: S.polygon(0) - Polygon: (0, 0), (3/7, 13/21), (-3/7, 11/21) + Polygon(vertices=[(0, 0), (3/7, 13/21), (-3/7, 11/21)]) sage: S.polygon(1) - Polygon: (0, 0), (6/7, 2/21), (3/7, 13/21) + Polygon(vertices=[(0, 0), (6/7, 2/21), (3/7, 13/21)]) """ verts = [v.vector() for v in self.polytope().vertices()] b = sum(verts) / len(verts) - from .polygon import ConvexPolygons + from flatsurf import Polygon from sage.rings.rational_field import QQ - C = ConvexPolygons(QQ) + from flatsurf import MutableOrientedSimilaritySurface + + barycenter = MutableOrientedSimilaritySurface(QQ) - triangles = [] for p in range(self._n): e1 = (b[6 * p], b[6 * p + 1]) e2 = (b[6 * p + 2], b[6 * p + 3]) e3 = (b[6 * p + 4], b[6 * p + 5]) - triangles.append(C([e1, e2, e3])) + barycenter.add_polygon(Polygon(edges=[e1, e2, e3], base_ring=QQ)) - from .surface import surface_list_from_polygons_and_gluings - from .translation_surface import TranslationSurface + for gluing in self._edge_identifications.items(): + barycenter.glue(*gluing) - return TranslationSurface( - surface_list_from_polygons_and_gluings( - triangles, self._edge_identifications - ) - ) + return barycenter diff --git a/flatsurf/geometry/mappings.py b/flatsurf/geometry/mappings.py index 097c63dcc..1c6418029 100644 --- a/flatsurf/geometry/mappings.py +++ b/flatsurf/geometry/mappings.py @@ -19,8 +19,6 @@ # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . # ********************************************************************* -from flatsurf.geometry.polygon import ConvexPolygons, wedge_product -from flatsurf.geometry.surface import Surface_dict class SurfaceMapping: @@ -128,30 +126,27 @@ class SimilarityJoinPolygonsMapping(SurfaceMapping): EXAMPLES:: - sage: from flatsurf.geometry.surface import Surface_list - sage: from flatsurf.geometry.translation_surface import TranslationSurface - sage: from flatsurf.geometry.polygon import ConvexPolygons - sage: P = ConvexPolygons(QQ) - sage: s0=Surface_list(base_ring=QQ) - sage: s0.add_polygon(P([(1,0),(0,1),(-1,-1)])) # gets label=0 + sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon + sage: s = MutableOrientedSimilaritySurface(QQ) + sage: s.add_polygon(Polygon(edges=[(1,0),(0,1),(-1,-1)])) 0 - sage: s0.add_polygon(P([(-1,0),(0,-1),(1,1)])) # gets label=1 + sage: s.add_polygon(Polygon(edges=[(-1,0),(0,-1),(1,1)])) 1 - sage: s0.change_polygon_gluings(0,[(1,0),(1,1),(1,2)]) - sage: s0.set_immutable() - sage: s=TranslationSurface(s0) - sage: from flatsurf.geometry.mappings import * - sage: m=SimilarityJoinPolygonsMapping(s,0,2) + sage: s.glue((0, 0), (1, 0)) + sage: s.glue((0, 1), (1, 1)) + sage: s.glue((0, 2), (1, 2)) + sage: s.set_immutable() + + sage: from flatsurf.geometry.mappings import SimilarityJoinPolygonsMapping + sage: m=SimilarityJoinPolygonsMapping(s, 0, 2) sage: s2=m.codomain() - sage: for label,polygon in s2.label_iterator(polygons=True): - ....: print("Polygon "+str(label)+" is "+str(polygon)+".") - Polygon 0 is Polygon: (0, 0), (1, 0), (1, 1), (0, 1). - sage: for label,edge in s2.edge_iterator(): - ....: print(str((label,edge))+" is glued to "+str(s2.opposite_edge(label,edge))+".") - (0, 0) is glued to (0, 2). - (0, 1) is glued to (0, 3). - (0, 2) is glued to (0, 0). - (0, 3) is glued to (0, 1). + sage: s2.labels() + (0,) + sage: s2.polygons() + (Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]),) + sage: s2.gluings() + (((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1))) + """ def __init__(self, s, p1, e1): @@ -163,8 +158,10 @@ def __init__(self, s, p1, e1): "Can only construct SimilarityJoinPolygonsMapping for immutable surfaces." ) - ss2 = s.copy(lazy=True, mutable=True) - s2 = ss2.underlying_surface() + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + ss2 = MutableOrientedSimilaritySurface.from_surface(s) + s2 = ss2 poly1 = s.polygon(p1) p2, e2 = s.opposite_edge(p1, e1) @@ -176,12 +173,12 @@ def __init__(self, s, p1, e1): for i in range(e1): edge_map[len(vs)] = (p1, i) vs.append(poly1.edge(i)) - ne = poly2.num_edges() + ne = len(poly2.vertices()) for i in range(1, ne): ee = (e2 + i) % ne edge_map[len(vs)] = (p2, ee) vs.append(dt * poly2.edge(ee)) - for i in range(e1 + 1, poly1.num_edges()): + for i in range(e1 + 1, len(poly1.vertices())): edge_map[len(vs)] = (p1, i) vs.append(poly1.edge(i)) @@ -189,21 +186,25 @@ def __init__(self, s, p1, e1): for key, value in edge_map.items(): inv_edge_map[value] = (p1, key) - if s.base_label() == p2: + if p2 in s.roots(): # The polygon with the base label is being removed. - s2.change_base_label(p1) + s2.set_roots(tuple(p1 if label == p2 else label for label in s.roots())) - s2.change_polygon(p1, ConvexPolygons(s.base_ring())(vs)) + s2.remove_polygon(p1) + from flatsurf import Polygon + + s2.add_polygon(Polygon(edges=vs, base_ring=s.base_ring()), label=p1) for i in range(len(vs)): p3, e3 = edge_map[i] p4, e4 = s.opposite_edge(p3, e3) if p4 == p1 or p4 == p2: pp, ee = inv_edge_map[(p4, e4)] - s2.change_edge_gluing(p1, i, pp, ee) + s2.glue((p1, i), (pp, ee)) else: - s2.change_edge_gluing(p1, i, p4, e4) + s2.glue((p1, i), (p4, e4)) + s2.remove_polygon(p2) s2.set_immutable() self._saved_label = p1 @@ -225,7 +226,8 @@ def glued_vertices(self): """ return ( self._glued_edge, - self._glued_edge + self._domain.polygon(self._removed_label).num_edges(), + self._glued_edge + + len(self._domain.polygon(self._removed_label).vertices()), ) def push_vector_forward(self, tangent_vector): @@ -255,9 +257,9 @@ def pull_vector_back(self, tangent_vector): p = tangent_vector.point() v = self._domain.polygon(self._saved_label).vertex(self._glued_edge) e = self._domain.polygon(self._saved_label).edge(self._glued_edge) - from flatsurf.geometry.polygon import wedge_product + from flatsurf.geometry.euclidean import ccw - wp = wedge_product(p - v, e) + wp = ccw(p - v, e) if wp > 0: # in polygon with the removed label return self.domain().tangent_vector( @@ -276,7 +278,7 @@ def pull_vector_back(self, tangent_vector): ) # Otherwise wp==0 w = tangent_vector.vector() - wp = wedge_product(w, e) + wp = ccw(w, e) if wp > 0: # in polygon with the removed label return self.domain().tangent_vector( @@ -306,28 +308,19 @@ class SplitPolygonsMapping(SurfaceMapping): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s=translation_surfaces.veech_2n_gon(4) sage: from flatsurf.geometry.mappings import SplitPolygonsMapping sage: m = SplitPolygonsMapping(s,0,0,2) sage: s2=m.codomain() sage: TestSuite(s2).run() - sage: for pair in s2.label_iterator(polygons=True): - ....: print(pair) - (0, Polygon: (0, 0), (1/2*a + 1, 1/2*a), (1/2*a + 1, 1/2*a + 1), (1, a + 1), (0, a + 1), (-1/2*a, 1/2*a + 1), (-1/2*a, 1/2*a)) - (ExtraLabel(0), Polygon: (0, 0), (-1/2*a - 1, -1/2*a), (-1/2*a, -1/2*a)) - sage: for glue in s2.edge_iterator(gluings=True): - ....: print(glue) - ((0, 0), (ExtraLabel(0), 0)) - ((0, 1), (0, 5)) - ((0, 2), (0, 6)) - ((0, 3), (ExtraLabel(0), 1)) - ((0, 4), (ExtraLabel(0), 2)) - ((0, 5), (0, 1)) - ((0, 6), (0, 2)) - ((ExtraLabel(0), 0), (0, 0)) - ((ExtraLabel(0), 1), (0, 3)) - ((ExtraLabel(0), 2), (0, 4)) + sage: s2.labels() + (0, 1) + sage: s2.polygons() + (Polygon(vertices=[(0, 0), (1/2*a + 1, 1/2*a), (1/2*a + 1, 1/2*a + 1), (1, a + 1), (0, a + 1), (-1/2*a, 1/2*a + 1), (-1/2*a, 1/2*a)]), Polygon(vertices=[(0, 0), (-1/2*a - 1, -1/2*a), (-1/2*a, -1/2*a)])) + sage: s2.gluings() + (((0, 0), (1, 0)), ((0, 1), (0, 5)), ((0, 2), (0, 6)), ((0, 3), (1, 1)), ((0, 4), (1, 2)), ((0, 5), (0, 1)), ((0, 6), (0, 2)), ((1, 0), (0, 0)), ((1, 1), (0, 3)), ((1, 2), (0, 4))) + """ def __init__(self, s, p, v1, v2, new_label=None): @@ -340,7 +333,7 @@ def __init__(self, s, p, v1, v2, new_label=None): raise ValueError("The surface should be immutable.") poly = s.polygon(p) - ne = poly.num_edges() + ne = len(poly.vertices()) if v1 < 0 or v2 < 0 or v1 >= ne or v2 >= ne: raise ValueError("Provided vertices out of bounds.") if abs(v1 - v2) <= 1 or abs(v1 - v2) >= ne - 1: @@ -350,19 +343,25 @@ def __init__(self, s, p, v1, v2, new_label=None): v1 = v2 v2 = temp - newvertices1 = [poly.vertex(v2) - poly.vertex(v1)] + newedges1 = [poly.vertex(v2) - poly.vertex(v1)] for i in range(v2, v1 + ne): - newvertices1.append(poly.edge(i)) - newpoly1 = ConvexPolygons(s.base_ring())(newvertices1) + newedges1.append(poly.edge(i)) - newvertices2 = [poly.vertex(v1) - poly.vertex(v2)] + from flatsurf import Polygon + + newpoly1 = Polygon(edges=newedges1, base_ring=s.base_ring()) + + newedges2 = [poly.vertex(v1) - poly.vertex(v2)] for i in range(v1, v2): - newvertices2.append(poly.edge(i)) - newpoly2 = ConvexPolygons(s.base_ring())(newvertices2) + newedges2.append(poly.edge(i)) + newpoly2 = Polygon(edges=newedges2, base_ring=s.base_ring()) - ss2 = s.copy(mutable=True, lazy=True) - s2 = ss2.underlying_surface() - s2.change_polygon(p, newpoly1) + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + ss2 = MutableOrientedSimilaritySurface.from_surface(s) + s2 = ss2 + s2.remove_polygon(p) + s2.add_polygon(newpoly1, label=p) new_label = s2.add_polygon(newpoly2, label=new_label) old_to_new_labels = {} @@ -378,15 +377,15 @@ def __init__(self, s, p, v1, v2, new_label=None): new_to_old_labels[pair] = i # This glues the split polygons together. - s2.change_edge_gluing(p, 0, new_label, 0) + s2.glue((p, 0), (new_label, 0)) for e in range(ne): ll, ee = old_to_new_labels[e] lll, eee = s.opposite_edge(p, e) if lll == p: gl, ge = old_to_new_labels[eee] - s2.change_edge_gluing(ll, ee, gl, ge) + s2.glue((ll, ee), (gl, ge)) else: - s2.change_edge_gluing(ll, ee, lll, eee) + s2.glue((ll, ee), (lll, eee)) s2.set_immutable() @@ -409,7 +408,9 @@ def push_vector_forward(self, tangent_vector): vertex1 = self._domain.polygon(self._p).vertex(self._v1) vertex2 = self._domain.polygon(self._p).vertex(self._v2) - wp = wedge_product(vertex2 - vertex1, point - vertex1) + from flatsurf.geometry.euclidean import ccw + + wp = ccw(vertex2 - vertex1, point - vertex1) if wp > 0: # in new polygon 1 @@ -430,7 +431,7 @@ def push_vector_forward(self, tangent_vector): # Otherwise wp==0 w = tangent_vector.vector() - wp = wedge_product(vertex2 - vertex1, w) + wp = ccw(vertex2 - vertex1, w) if wp > 0: # in new polygon 1 return self.codomain().tangent_vector( @@ -486,15 +487,15 @@ def subdivide_a_polygon(s): r""" Return a SurfaceMapping which cuts one polygon along a diagonal or None if the surface is triangulated. """ - from flatsurf.geometry.polygon import wedge_product + from flatsurf.geometry.euclidean import ccw - for label, poly in s.label_iterator(polygons=True): - n = poly.num_edges() + for label, poly in zip(s.labels(), s.polygons()): + n = len(poly.vertices()) if n > 3: for i in range(n): e1 = poly.edge(i) e2 = poly.edge((i + 1) % n) - if wedge_product(e1, e2) != 0: + if ccw(e1, e2) != 0: return SplitPolygonsMapping(s, label, i, (i + 2) % n) raise ValueError( "Unable to triangulate polygon with label " @@ -506,26 +507,27 @@ def subdivide_a_polygon(s): def triangulation_mapping(s): - r"""Return a SurfaceMapping triangulating the provided surface. + r""" + Return a SurfaceMapping triangulating ``s``. EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s=translation_surfaces.veech_2n_gon(4) - sage: from flatsurf.geometry.mappings import * + sage: from flatsurf.geometry.mappings import triangulation_mapping sage: m=triangulation_mapping(s) sage: s2=m.codomain() sage: TestSuite(s2).run() - sage: for label,polygon in s2.label_iterator(polygons=True): - ....: print(str(polygon)) - Polygon: (0, 0), (-1/2*a, 1/2*a + 1), (-1/2*a, 1/2*a) - Polygon: (0, 0), (1/2*a, -1/2*a - 1), (1/2*a, 1/2*a) - Polygon: (0, 0), (-1/2*a - 1, -1/2*a - 1), (0, -1) - Polygon: (0, 0), (-1, -a - 1), (1/2*a, -1/2*a) - Polygon: (0, 0), (0, -a - 1), (1, 0) - Polygon: (0, 0), (-1/2*a - 1, -1/2*a), (-1/2*a, -1/2*a) + sage: s2.polygons() + (Polygon(vertices=[(0, 0), (-1/2*a, 1/2*a + 1), (-1/2*a, 1/2*a)]), + Polygon(vertices=[(0, 0), (1/2*a, -1/2*a - 1), (1/2*a, 1/2*a)]), + Polygon(vertices=[(0, 0), (-1/2*a - 1, -1/2*a - 1), (0, -1)]), + Polygon(vertices=[(0, 0), (-1, -a - 1), (1/2*a, -1/2*a)]), + Polygon(vertices=[(0, 0), (0, -a - 1), (1, 0)]), + Polygon(vertices=[(0, 0), (-1/2*a - 1, -1/2*a), (-1/2*a, -1/2*a)])) + """ - if not s.is_finite(): + if not s.is_finite_type(): raise NotImplementedError m = subdivide_a_polygon(s) @@ -558,9 +560,9 @@ def one_delaunay_flip_mapping(s): r""" Returns one delaunay flip, or none if no flips are needed. """ - for p, poly in s.label_iterator(polygons=True): - for e in range(poly.num_edges()): - if s._edge_needs_flip(p, e): + for p, poly in zip(s.labels(), s.polygons()): + for e in range(len(poly.vertices())): + if s._delaunay_edge_needs_flip(p, e): return flip_edge_mapping(s, p, e) return None @@ -569,7 +571,7 @@ def delaunay_triangulation_mapping(s): r""" Returns a mapping to a Delaunay triangulation or None if the surface already is Delaunay triangulated. """ - if not s.is_finite(): + if not s.is_finite_type(): raise NotImplementedError m = triangulation_mapping(s) @@ -602,13 +604,19 @@ def delaunay_decomposition_mapping(s): s1 = s else: s1 = m.codomain() + + joins = set() edge_vectors = [] - lc = s._label_comparator() - for p, poly in s1.label_iterator(polygons=True): - for e in range(poly.num_edges()): + + for p, poly in zip(s1.labels(), s1.polygons()): + for e in range(len(poly.vertices())): pp, ee = s1.opposite_edge(p, e) - if (lc.lt(p, pp) or (p == pp and e < ee)) and s1._edge_needs_join(p, e): + if (pp, ee) in joins: + continue + if s1._delaunay_edge_needs_join(p, e): + joins.add((p, e)) edge_vectors.append(s1.tangent_vector(p, poly.vertex(e), poly.edge(e))) + if len(edge_vectors) > 0: ev = edge_vectors.pop() p, e = ev.edge_pointing_along() @@ -635,7 +643,7 @@ def canonical_first_vertex(polygon): """ best = 0 best_pt = polygon.vertex(best) - for v in range(1, polygon.num_edges()): + for v in range(1, len(polygon.vertices())): pt = polygon.vertex(v) if pt[1] < best_pt[1]: best = v @@ -656,34 +664,40 @@ def __init__(self, s): r""" Split the polygon with label p of surface s along the diagonal joining vertex v1 to vertex v2. """ - if not s.is_finite(): + if not s.is_finite_type(): raise ValueError("Currently only works with finite surfaces.") ring = s.base_ring() from flatsurf.geometry.similarity import SimilarityGroup T = SimilarityGroup(ring) - P = ConvexPolygons(ring) cv = {} # dictionary for canonical vertices translations = {} # translations bringing the canonical vertex to the origin. - s2 = Surface_dict(base_ring=ring) - for label, polygon in s.label_iterator(polygons=True): + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + s2 = MutableOrientedSimilaritySurface(ring) + for label, polygon in zip(s.labels(), s.polygons()): cv[label] = cvcur = canonical_first_vertex(polygon) newedges = [] - for i in range(polygon.num_edges()): - newedges.append(polygon.edge((i + cvcur) % polygon.num_edges())) - s2.add_polygon(P(newedges), label=label) + for i in range(len(polygon.vertices())): + newedges.append(polygon.edge((i + cvcur) % len(polygon.vertices()))) + + from flatsurf import Polygon + + s2.add_polygon(Polygon(edges=newedges, base_ring=ring), label=label) translations[label] = T(-polygon.vertex(cvcur)) - for l1, polygon in s.label_iterator(polygons=True): - for e1 in range(polygon.num_edges()): + for l1, polygon in zip(s.labels(), s.polygons()): + for e1 in range(len(polygon.vertices())): l2, e2 = s.opposite_edge(l1, e1) - ee1 = (e1 - cv[l1] + polygon.num_edges()) % polygon.num_edges() + ee1 = (e1 - cv[l1] + len(polygon.vertices())) % len(polygon.vertices()) polygon2 = s.polygon(l2) - ee2 = (e2 - cv[l2] + polygon2.num_edges()) % polygon2.num_edges() + ee2 = (e2 - cv[l2] + len(polygon2.vertices())) % len( + polygon2.vertices() + ) # newgluing.append( ( (l1,ee1),(l2,ee2) ) ) - s2.change_edge_gluing(l1, ee1, l2, ee2) - s2.change_base_label(s.base_label()) + s2.glue((l1, ee1), (l2, ee2)) + s2.set_roots(s.roots()) s2.set_immutable() - ss2 = s.__class__(s2) + ss2 = s2 self._cv = cv self._translations = translations @@ -722,11 +736,11 @@ def __init__(self, s, relabler, new_base_label=None): r""" The parameters should be a surface and a dictionary which takes as input a label and produces a new label. """ - if not s.is_finite(): + if not s.is_finite_type(): raise ValueError("Currently only works with finite surfaces." "") f = {} # map for labels going forward. b = {} # map for labels going backward. - for label in s.label_iterator(): + for label in s.labels(): if label in relabler: l2 = relabler[label] f[label] = l2 @@ -748,13 +762,16 @@ def __init__(self, s, relabler, new_base_label=None): self._b = b if new_base_label is None: - if s.base_label() in f: - new_base_label = f[s.base_label()] + if s.root() in f: + new_base_label = f[s.root()] else: - new_base_label = s.base_label() - s2 = s.copy(mutable=True, lazy=True) + new_base_label = s.root() + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + s2 = MutableOrientedSimilaritySurface.from_surface(s) s2.relabel(relabler, in_place=True) - s2.underlying_surface().change_base_label(new_base_label) + s2.set_roots([new_base_label]) + s2.set_immutable() SurfaceMapping.__init__(self, s, s2) @@ -792,16 +809,16 @@ def my_sgn(val): def polygon_compare(poly1, poly2): r""" Compare two polygons first by area, then by number of sides, - then by lexigraphical ordering on edge vectors.""" + then by lexicographical ordering on edge vectors.""" # This should not be used is broken!! # from sage.functions.generalized import sgn res = my_sgn(-poly1.area() + poly2.area()) if res != 0: return res - res = my_sgn(poly1.num_edges() - poly2.num_edges()) + res = my_sgn(len(poly1.vertices()) - len(poly2.vertices())) if res != 0: return res - ne = poly1.num_edges() + ne = len(poly1.vertices()) for i in range(0, ne - 1): edge_diff = poly1.edge(i) - poly2.edge(i) res = my_sgn(edge_diff[0]) @@ -813,64 +830,28 @@ def polygon_compare(poly1, poly2): return 0 -def translation_surface_cmp(s1, s2): - r""" - Compare two finite surfaces. - The surfaces will be considered equal if and only if there is a translation automorphism - respecting the polygons and the base_labels. - """ - if not s1.is_finite() or not s2.is_finite(): - raise NotImplementedError - lw1 = s1.walker() - lw2 = s2.walker() - try: - from itertools import zip_longest - except ImportError: - from itertools import izip_longest as zip_longest - for p1, p2 in zip_longest(lw1.polygon_iterator(), lw2.polygon_iterator()): - if p1 is None: - # s2 has more polygons - return -1 - if p2 is None: - # s1 has more polygons - return 1 - ret = polygon_compare(p1, p2) - if ret != 0: - return ret - # Polygons are identical. Compare edge gluings. - for pair1, pair2 in zip_longest(lw1.edge_iterator(), lw2.edge_iterator()): - l1, e1 = s1.opposite_edge(pair1) - l2, e2 = s2.opposite_edge(pair2) - num1 = lw1.label_to_number(l1) - num2 = lw2.label_to_number(l2) - ret = (num1 > num2) - (num1 < num2) - if ret != 0: - return ret - ret = (e1 > e2) - (e1 < e2) - if ret != 0: - return ret - return 0 - - def canonicalize_translation_surface_mapping(s): r""" Return the translation surface in a canonical form. EXAMPLES:: - sage: from flatsurf import * - sage: s=translation_surfaces.octagon_and_squares().canonicalize() + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.octagon_and_squares().canonicalize() + sage: TestSuite(s).run() + sage: a = s.base_ring().gen() # a is the square root of 2. - sage: from flatsurf.geometry.mappings import * + sage: from flatsurf.geometry.half_dilation_surface import GL2RMapping + sage: from flatsurf.geometry.mappings import canonicalize_translation_surface_mapping sage: mat=Matrix([[1,2+a],[0,1]]) sage: from flatsurf.geometry.half_dilation_surface import GL2RMapping - sage: m1=GL2RMapping(s,mat) + sage: m1=GL2RMapping(s, mat) sage: m2=canonicalize_translation_surface_mapping(m1.codomain()) sage: m=m2*m1 - sage: translation_surface_cmp(m.domain(),m.codomain())==0 - True + sage: m.domain().cmp(m.codomain()) + 0 sage: TestSuite(m.codomain()).run() sage: s=m.domain() sage: v=s.tangent_vector(0,(0,0),(1,1)) @@ -878,11 +859,11 @@ def canonicalize_translation_surface_mapping(s): sage: print(w) SimilaritySurfaceTangentVector in polygon 0 based at (0, 0) with vector (a + 3, 1) """ - from flatsurf.geometry.translation_surface import TranslationSurface + from flatsurf.geometry.categories import TranslationSurfaces - if not s.is_finite(): + if not s.is_finite_type(): raise NotImplementedError - if not isinstance(s, TranslationSurface): + if s not in TranslationSurfaces(): raise ValueError("Only defined for TranslationSurfaces") m1 = delaunay_decomposition_mapping(s) if m1 is None: @@ -896,18 +877,22 @@ def canonicalize_translation_surface_mapping(s): m = SurfaceMappingComposition(m1, m2) s2 = m.codomain() - s2copy = s2.copy(mutable=True) - ss = s2.copy(mutable=True) - labels = {label for label in s2.label_iterator()} - labels.remove(s2.base_label()) + # This is essentially copy & paste from canonicalize() from TranslationSurfaces() + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + s2copy = MutableOrientedSimilaritySurface.from_surface(s2) + ss = MutableOrientedSimilaritySurface.from_surface(s2) + labels = {label for label in s2.labels()} for label in labels: - ss.underlying_surface().change_base_label(label) + ss.set_roots([label]) if ss.cmp(s2copy) > 0: - s2copy.underlying_surface().change_base_label(label) + s2copy.set_roots([label]) + + s2copy.set_immutable() + # We now have the base_label correct. - # We will use the label walker to generate the canonical labeling of polygons. - w = s2copy.walker() - w.find_all_labels() + # We will use the label walk to generate the canonical labeling of polygons. + labels = {label: i for (i, label) in enumerate(s2copy.labels())} - m3 = ReindexMapping(s2, w.label_dictionary(), 0) + m3 = ReindexMapping(s2, labels, 0) return SurfaceMappingComposition(m, m3) diff --git a/flatsurf/geometry/matrix_2x2.py b/flatsurf/geometry/matrix_2x2.py deleted file mode 100644 index 991db9519..000000000 --- a/flatsurf/geometry/matrix_2x2.py +++ /dev/null @@ -1,256 +0,0 @@ -r""" -Some tools for 2x2 matrices and planar geometry. -""" -###################################################################### -# This file is part of sage-flatsurf. -# -# Copyright (C) 2016-2020 Vincent Delecroix -# 2020-2022 Julian Rüth -# -# sage-flatsurf is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# sage-flatsurf is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with sage-flatsurf. If not, see . -###################################################################### -from sage.rings.all import AA, QQbar, RR - -from sage.modules.free_module_element import vector - - -def similarity_from_vectors(u, v, matrix_space=None): - r""" - Return the unique similarity matrix that maps ``u`` to ``v``. - - EXAMPLES:: - - sage: from flatsurf.geometry.matrix_2x2 import similarity_from_vectors - - sage: V = VectorSpace(QQ,2) - sage: u = V((1,0)) - sage: v = V((0,1)) - sage: m = similarity_from_vectors(u,v); m - [ 0 -1] - [ 1 0] - sage: m*u == v - True - - sage: u = V((2,1)) - sage: v = V((1,-2)) - sage: m = similarity_from_vectors(u,v); m - [ 0 1] - [-1 0] - sage: m * u == v - True - - An example built from the Pythagorean triple 3^2 + 4^2 = 5^2:: - - sage: u2 = V((5,0)) - sage: v2 = V((3,4)) - sage: m = similarity_from_vectors(u2,v2); m - [ 3/5 -4/5] - [ 4/5 3/5] - sage: m * u2 == v2 - True - - Some test over number fields:: - - sage: K. = NumberField(x^2-2, embedding=1.4142) - sage: V = VectorSpace(K,2) - sage: u = V((sqrt2,0)) - sage: v = V((1, 1)) - sage: m = similarity_from_vectors(u,v); m - [ 1/2*sqrt2 -1/2*sqrt2] - [ 1/2*sqrt2 1/2*sqrt2] - sage: m*u == v - True - - sage: m = similarity_from_vectors(u, 2*v); m - [ sqrt2 -sqrt2] - [ sqrt2 sqrt2] - sage: m*u == 2*v - True - """ - if u.parent() is not v.parent(): - raise ValueError - - if matrix_space is None: - from sage.matrix.matrix_space import MatrixSpace - - matrix_space = MatrixSpace(u.base_ring(), 2) - - if u == v: - return matrix_space.one() - - sqnorm_u = u[0] * u[0] + u[1] * u[1] - cos_uv = (u[0] * v[0] + u[1] * v[1]) / sqnorm_u - sin_uv = (u[0] * v[1] - u[1] * v[0]) / sqnorm_u - - m = matrix_space([cos_uv, -sin_uv, sin_uv, cos_uv]) - m.set_immutable() - return m - - -def is_cosine_sine_of_rational(c, s): - r""" - Check whether the given pair is a cosine and sine of a same rational angle. - - EXAMPLES:: - - sage: from flatsurf.geometry.matrix_2x2 import is_cosine_sine_of_rational - - sage: c = s = AA(sqrt(2))/2 - sage: is_cosine_sine_of_rational(c,s) - True - sage: c = AA(sqrt(3))/2; s = AA(1/2) - sage: is_cosine_sine_of_rational(c,s) - True - - sage: c = AA(sqrt(5)/2); s = (1 - c**2).sqrt() - sage: c**2 + s**2 - 1.000000000000000? - sage: is_cosine_sine_of_rational(c,s) - False - - sage: c = (AA(sqrt(5)) + 1)/4; s = (1 - c**2).sqrt() - sage: is_cosine_sine_of_rational(c,s) - True - - sage: K. = NumberField(x**2 - 2, embedding=1.414) - sage: is_cosine_sine_of_rational(K.zero(),-K.one()) - True - - TESTS:: - - sage: from pyexactreal import ExactReals # optional: exactreal # random output due to matplotlib warnings with some combinations of setuptools and matplotlib - sage: R = ExactReals() # optional: exactreal - sage: is_cosine_sine_of_rational(R.one(), R.zero()) # optional: exactreal - True - - """ - return (QQbar(c) + QQbar.gen() * QQbar(s)).minpoly().is_cyclotomic() - - -def angle(u, v, numerical=False, assume_rational=False): - r""" - Return the angle between the vectors ``u`` and ``v`` divided by `2 \pi`. - - INPUT: - - - ``u``, ``v`` - vectors - - - ``numerical`` - boolean, whether to return floating point numbers - - - ``assume_rational`` - whether we assume that the angle is a multiple - rational of ``pi``. By default it is ``False`` but if it is known in - advance that the result is rational then setting it to ``True`` might be - much faster. - - EXAMPLES:: - - sage: from flatsurf.geometry.matrix_2x2 import angle - - As the implementation is dirty, we at least check that it works for all - denominator up to 20:: - - sage: u = vector((AA(1),AA(0))) - sage: for n in xsrange(1,20): # long time (10 sec) - ....: for k in xsrange(1,n): - ....: v = vector((AA(cos(2*k*pi/n)), AA(sin(2*k*pi/n)))) - ....: assert angle(u,v) == k/n - - The numerical version (working over floating point numbers):: - - sage: import math - sage: u = (1, 0) - sage: for n in xsrange(1,20): - ....: for k in xsrange(1,n): - ....: a = 2 * k * math.pi / n - ....: v = (math.cos(a), math.sin(a)) - ....: assert abs(angle(u,v,numerical=True) * 2 * math.pi - a) < 1.e-10 - - And we test up to 50 when setting ``assume_rational`` to ``True``:: - - sage: for n in xsrange(1,20): # long time - ....: for k in xsrange(1,n): - ....: v = vector((AA(cos(2*k*pi/n)), AA(sin(2*k*pi/n)))) - ....: assert angle(u,v,assume_rational=True) == k/n - - If the angle is not rational, then the method returns an element in the real - lazy field:: - - sage: v = vector((AA(sqrt(2)), AA(sqrt(3)))) - sage: a = angle(u, v) - sage: a # abs tol 1e-14 - 0.14102355421224375 - sage: exp(2*pi.n()*CC(0,1)*a) - 0.632455532033676 + 0.774596669241483*I - sage: v / v.norm() - (0.6324555320336758?, 0.774596669241484?) - """ - if not assume_rational and not numerical: - sqnorm_u = u[0] * u[0] + u[1] * u[1] - sqnorm_v = v[0] * v[0] + v[1] * v[1] - - if sqnorm_u != sqnorm_v: - uu = vector(AA, u) - vv = (AA(sqnorm_u) / AA(sqnorm_v)).sqrt() * vector(AA, v) - else: - uu = u - vv = v - - cos_uv = (uu[0] * vv[0] + uu[1] * vv[1]) / sqnorm_u - sin_uv = (uu[0] * vv[1] - uu[1] * vv[0]) / sqnorm_u - - is_rational = is_cosine_sine_of_rational(cos_uv, sin_uv) - elif assume_rational: - is_rational = True - - import math - - u0 = float(u[0]) - u1 = float(u[1]) - v0 = float(v[0]) - v1 = float(v[1]) - - cos_uv = (u0 * v0 + u1 * v1) / math.sqrt((u0 * u0 + u1 * u1) * (v0 * v0 + v1 * v1)) - if cos_uv < -1.0: - assert cos_uv > -1.0000001 - cos_uv = -1.0 - elif cos_uv > 1.0: - assert cos_uv < 1.0000001 - cos_uv = 1.0 - angle = math.acos(cos_uv) / (2 * math.pi) # rat number between 0 and 1/2 - - if numerical or not is_rational: - return 1.0 - angle if u0 * v1 - u1 * v0 < 0 else angle - else: - # fast and dirty way using floating point approximation - # (see below for a slow but exact method) - angle_rat = RR(angle).nearby_rational(0.00000001) - if angle_rat.denominator() > 100: - raise NotImplementedError("the numerical method used is not smart enough!") - return 1 - angle_rat if u0 * v1 - u1 * v0 < 0 else angle_rat - - # a neater way is provided below by working only with number fields - # but this method is slower... - # sqnorm_u = u[0]*u[0] + u[1]*u[1] - # sqnorm_v = v[0]*v[0] + v[1]*v[1] - # - # if sqnorm_u != sqnorm_v: - # # we need to take a square root in order that u and v have the - # # same norm - # u = (1 / AA(sqnorm_u)).sqrt() * u.change_ring(AA) - # v = (1 / AA(sqnorm_v)).sqrt() * v.change_ring(AA) - # sqnorm_u = AA.one() - # sqnorm_v = AA.one() - # - # cos_uv = (u[0]*v[0] + u[1]*v[1]) / sqnorm_u - # sin_uv = (u[0]*v[1] - u[1]*v[0]) / sqnorm_u diff --git a/flatsurf/geometry/mega_wollmilchsau.py b/flatsurf/geometry/mega_wollmilchsau.py index 0dafc4241..7b72b42cb 100644 --- a/flatsurf/geometry/mega_wollmilchsau.py +++ b/flatsurf/geometry/mega_wollmilchsau.py @@ -1,3 +1,23 @@ +# **************************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2019 Vincent Delecroix +# 2016-2019 W. Patrick Hooper +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# **************************************************************************** from sage.groups.group import Group from sage.categories.groups import Groups from sage.structure.unique_representation import UniqueRepresentation @@ -5,7 +25,7 @@ from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra from sage.rings.integer_ring import ZZ -from .translation_surface import AbstractOrigami +from flatsurf.geometry.origami import AbstractOrigami _Q = QuaternionAlgebra(-1, -1) _i, _j, _k = _Q.gens() diff --git a/flatsurf/geometry/minimal_cover.py b/flatsurf/geometry/minimal_cover.py index cbc898308..85f975e2f 100644 --- a/flatsurf/geometry/minimal_cover.py +++ b/flatsurf/geometry/minimal_cover.py @@ -1,31 +1,67 @@ +r""" +EXAMPLES: + +Usually, you do not interact with the types in this module directly but call +``minimal_cover()`` on a surface:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)) + + sage: S.minimal_cover("translation") + Minimal Translation Cover of Genus 0 Rational Cone Surface built from 2 right triangles + sage: S.minimal_cover("half-translation") + Minimal Half-Translation Cover of Genus 0 Rational Cone Surface built from 2 right triangles + sage: S.minimal_cover("planar") + Minimal Planar Cover of Genus 0 Rational Cone Surface built from 2 right triangles + +""" +# ******************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2018-2019 W. Patrick Hooper +# 2020-2022 Vincent Delecroix +# 2021-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# ******************************************************************** from sage.matrix.constructor import matrix +from sage.misc.cachefunc import cached_method -from .surface import Surface +from flatsurf.geometry.surface import OrientedSimilaritySurface def _is_finite(surface): r""" Return whether ``surface`` is a finite rational cone surface. """ - if not surface.is_finite(): + if not surface.is_finite_type(): return False - from flatsurf.geometry.rational_cone_surface import RationalConeSurface + from flatsurf.geometry.categories import ConeSurfaces - if isinstance(surface, RationalConeSurface): + if surface in ConeSurfaces().Rational(): return True - surface = surface.reposition_polygons(relabel=True) + surface = surface.reposition_polygons() - for label in surface.label_iterator(): + for label in surface.labels(): polygon = surface.polygon(label) - for e in range(polygon.num_edges()): - from flatsurf.geometry.similarity_surface import SimilaritySurface + for e in range(len(polygon.vertices())): + m = surface.edge_matrix(label, e) - m = SimilaritySurface.edge_matrix(surface, label, e) - - from flatsurf.geometry.matrix_2x2 import is_cosine_sine_of_rational + from flatsurf.geometry.euclidean import is_cosine_sine_of_rational if not is_cosine_sine_of_rational(m[0][0], m[0][1]): return False @@ -33,27 +69,27 @@ def _is_finite(surface): return True -class MinimalTranslationCover(Surface): +class MinimalTranslationCover(OrientedSimilaritySurface): r""" - We label copy by cartesian product (polygon from bot, matrix). - EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon, similarity_surfaces, polygons sage: from flatsurf.geometry.minimal_cover import MinimalTranslationCover - sage: s = Surface_list(QQ) - sage: P = ConvexPolygons(QQ) - sage: s.add_polygon(P(vertices=[(0,0),(5,0),(0,5)])) + sage: s = MutableOrientedSimilaritySurface(QQ) + sage: s.add_polygon(Polygon(vertices=[(0,0),(5,0),(0,5)])) 0 - sage: s.add_polygon(P(vertices=[(0,0),(3,4),(-4,3)])) + sage: s.add_polygon(Polygon(vertices=[(0,0),(3,4),(-4,3)])) 1 - sage: s.change_polygon_gluings(0,[(1,2),(1,1),(1,0)]) + sage: s.glue((0, 0), (1, 2)) + sage: s.glue((0, 1), (1, 1)) + sage: s.glue((0, 2), (1, 0)) sage: s.set_immutable() - sage: s=SimilaritySurface(s) - sage: ss=TranslationSurface(MinimalTranslationCover(s)) - sage: ss.is_finite() + sage: ss = s.minimal_cover("translation") + sage: isinstance(ss, MinimalTranslationCover) True - sage: ss.num_polygons() + sage: ss.is_finite_type() + True + sage: len(ss.polygons()) 8 sage: TestSuite(ss).run() @@ -61,100 +97,259 @@ class MinimalTranslationCover(Surface): in https://github.com/flatsurf/sage-flatsurf/issues/47:: sage: T = polygons.triangle(2, 13, 26) - sage: S = similarity_surfaces.billiard(T, rational=True) + sage: S = similarity_surfaces.billiard(T) sage: S = S.minimal_cover("translation") sage: S - TranslationSurface built from 82 polygons + Minimal Translation Cover of Genus 0 Rational Cone Surface built from 2 triangles + + TESTS:: + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: S in TranslationSurfaces() + True + """ - def __init__(self, similarity_surface): - if similarity_surface.underlying_surface().is_mutable(): - if similarity_surface.is_finite(): - self._ss = similarity_surface.copy() + def __init__(self, similarity_surface, category=None): + if similarity_surface.is_mutable(): + if similarity_surface.is_finite_type(): + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + similarity_surface = MutableOrientedSimilaritySurface.from_surface( + similarity_surface + ) else: - raise ValueError( - "Can not construct MinimalTranslationCover of a surface that is mutable and infinite." + raise NotImplementedError( + "can not construct MinimalTranslationCover of a surface that is mutable and infinite" ) + + if similarity_surface.is_with_boundary(): + raise TypeError("surface must be without boundary") + + self._ss = similarity_surface + + from flatsurf.geometry.categories import TranslationSurfaces + + if category is None: + category = TranslationSurfaces() + + category &= TranslationSurfaces() + + category = category.WithoutBoundary() + + if _is_finite(self._ss): + category = category.FiniteType() else: - self._ss = similarity_surface + category = category.InfiniteType() + + if similarity_surface.is_connected(): + category = category.Connected() - # We are finite if and only if self._ss is a finite RationalConeSurface. - finite = _is_finite(self._ss) + if self.is_compact(): + category = category.Compact() + + OrientedSimilaritySurface.__init__( + self, self._ss.base_ring(), category=category + ) + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("translation") + sage: S.roots() + ((0, 1, 0),) + + """ self._F = self._ss.base_ring() - base_label = (self._ss.base_label(), self._F.one(), self._F.zero()) + return tuple( + (label, self._F.one(), self._F.zero()) for label in self._ss.roots() + ) + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("translation") + sage: S.is_mutable() + False + + """ + return False + + def is_compact(self): + r""" + Return whether this surface is compact as a topological space. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase().minimal_cover("translation") + sage: S.is_compact() + False + + :: + + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("translation") + sage: S.is_compact() + True + + """ + if not self._ss.is_compact(): + return False + + if not self._ss.is_rational_surface(): + return False + + return True + + @cached_method + def polygon(self, label): + r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. - Surface.__init__( - self, self._ss.base_ring(), base_label, finite=finite, mutable=False + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("translation") + sage: S.polygon((0, 1, 0)) + Polygon(vertices=[(0, 0), (1, 0), (1/4*c^2 - 1/4, 1/4*c)]) + + """ + if not isinstance(label, tuple) or len(label) != 3: + raise ValueError("invalid label {!r}".format(label)) + return matrix([[label[1], -label[2]], [label[2], label[1]]]) * self._ss.polygon( + label[0] ) - def polygon(self, lab): - if not isinstance(lab, tuple) or len(lab) != 3: - raise ValueError("invalid label {!r}".format(lab)) - return matrix([[lab[1], -lab[2]], [lab[2], lab[1]]]) * self._ss.polygon(lab[0]) + @cached_method + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("translation") + sage: S.opposite_edge((0, 1, 0), 0) + ((1, 1, 0), 2) - def opposite_edge(self, p, e): - pp, a, b = p # this is the polygon m * ss.polygon(p) - p2, e2 = self._ss.opposite_edge(pp, e) + """ + pp, a, b = label # this is the polygon m * ss.polygon(p) + p2, e2 = self._ss.opposite_edge(pp, edge) m = self._ss.edge_matrix(p2, e2) aa = a * m[0][0] - b * m[1][0] bb = b * m[0][0] + a * m[1][0] return ((p2, aa, bb), e2) + def _repr_(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("translation") + sage: S + Minimal Translation Cover of Genus 0 Rational Cone Surface built from 2 right triangles + + """ + return f"Minimal Translation Cover of {repr(self._ss)}" + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: hash(S.minimal_cover("translation")) == hash(S.minimal_cover("translation")) + True + + """ + return hash(self._ss) + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. - Note that this is not implemented in most non-trivial cases. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. EXAMPLES:: sage: from flatsurf import polygons, similarity_surfaces sage: T = polygons.triangle(2, 13, 26) - sage: S = similarity_surfaces.billiard(T, rational=True) - sage: S = S.minimal_cover("translation") - - sage: S == S + sage: S = similarity_surfaces.billiard(T) + sage: S.minimal_cover("translation") == S.minimal_cover("translation") True :: sage: TT = polygons.triangle(2, 15, 26) - sage: SS = similarity_surfaces.billiard(TT, rational=True) + sage: SS = similarity_surfaces.billiard(TT) sage: SS = SS.minimal_cover("translation") sage: S == SS False """ - if isinstance(other, MinimalTranslationCover): - if self._ss == other._ss and self._base_label == other._base_label: - return True + if not isinstance(other, MinimalTranslationCover): + return False - return super().__eq__(other) + return self._ss == other._ss -class MinimalHalfTranslationCover(Surface): +class MinimalHalfTranslationCover(OrientedSimilaritySurface): r""" - We label copy by cartesian product (polygon from bot, matrix). - EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon, similarity_surfaces, polygons sage: from flatsurf.geometry.minimal_cover import MinimalHalfTranslationCover - sage: s = Surface_list(QQ) - sage: P = ConvexPolygons(QQ) - sage: s.add_polygon(P(vertices=[(0,0),(5,0),(0,5)])) + sage: s = MutableOrientedSimilaritySurface(QQ) + sage: s.add_polygon(Polygon(vertices=[(0,0),(5,0),(0,5)])) 0 - sage: s.add_polygon(P(vertices=[(0,0),(3,4),(-4,3)])) + sage: s.add_polygon(Polygon(vertices=[(0,0),(3,4),(-4,3)])) 1 - sage: s.change_polygon_gluings(0,[(1,2),(1,1),(1,0)]) + sage: s.glue((0, 0), (1, 2)) + sage: s.glue((0, 1), (1, 1)) + sage: s.glue((0, 2), (1, 0)) sage: s.set_immutable() - sage: s=SimilaritySurface(s) - sage: ss=HalfTranslationSurface(MinimalHalfTranslationCover(s)) - sage: ss.is_finite() + sage: ss = s.minimal_cover("half-translation") + sage: isinstance(ss, MinimalHalfTranslationCover) + True + sage: ss.is_finite_type() True - sage: ss.num_polygons() + sage: len(ss.polygons()) 4 sage: TestSuite(ss).run() @@ -162,16 +357,28 @@ class MinimalHalfTranslationCover(Surface): in https://github.com/flatsurf/sage-flatsurf/issues/47:: sage: T = polygons.triangle(2, 13, 26) - sage: S = similarity_surfaces.billiard(T, rational=True) + sage: S = similarity_surfaces.billiard(T) sage: S = S.minimal_cover("half-translation") sage: S - HalfTranslationSurface built from 82 polygons + Minimal Half-Translation Cover of Genus 0 Rational Cone Surface built from 2 triangles + + TESTS:: + + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: S in DilationSurfaces() + True + """ - def __init__(self, similarity_surface): - if similarity_surface.underlying_surface().is_mutable(): - if similarity_surface.is_finite(): - self._ss = similarity_surface.copy() + def __init__(self, similarity_surface, category=None): + if similarity_surface.is_mutable(): + if similarity_surface.is_finite_type(): + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + self._ss = MutableOrientedSimilaritySurface.from_surface( + similarity_surface + ) + self._ss.set_immutable() else: raise ValueError( "Can not construct MinimalTranslationCover of a surface that is mutable and infinite." @@ -179,25 +386,127 @@ def __init__(self, similarity_surface): else: self._ss = similarity_surface - # We are finite if and only if self._ss is a finite RationalConeSurface. - finite = _is_finite(self._ss) + if similarity_surface.is_with_boundary(): + raise TypeError( + "can only build translation cover of surfaces without boundary" + ) + + from flatsurf.geometry.categories import HalfTranslationSurfaces + + if category is None: + category = HalfTranslationSurfaces() + category &= HalfTranslationSurfaces() + + if _is_finite(self._ss): + category = category.FiniteType() + else: + category = category.InfiniteType() + + category = category.WithoutBoundary() + + if similarity_surface.is_connected(): + category = category.Connected() + + OrientedSimilaritySurface.__init__( + self, self._ss.base_ring(), category=category + ) + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("half-translation") + sage: S.roots() + ((0, 1, 0),) + + """ self._F = self._ss.base_ring() - base_label = (self._ss.base_label(), self._F.one(), self._F.zero()) + return tuple( + (label, self._F.one(), self._F.zero()) for label in self._ss.roots() + ) + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("half-translation") + sage: S.is_mutable() + False + + """ + return False + + def _repr_(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: - Surface.__init__( - self, self._ss.base_ring(), base_label, finite=finite, mutable=False + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("half-translation") + sage: S + Minimal Half-Translation Cover of Genus 0 Rational Cone Surface built from 2 right triangles + + """ + return f"Minimal Half-Translation Cover of {repr(self._ss)}" + + def polygon(self, label): + r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("half-translation") + sage: S.polygon((0, 1, 0)) + Polygon(vertices=[(0, 0), (1, 0), (1/4*c^2 - 1/4, 1/4*c)]) + + """ + if not isinstance(label, tuple) or len(label) != 3: + raise ValueError("invalid label {!r}".format(label)) + return matrix([[label[1], -label[2]], [label[2], label[1]]]) * self._ss.polygon( + label[0] ) - def polygon(self, lab): - if not isinstance(lab, tuple) or len(lab) != 3: - raise ValueError("invalid label {!r}".format(lab)) - return matrix([[lab[1], -lab[2]], [lab[2], lab[1]]]) * self._ss.polygon(lab[0]) + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: - def opposite_edge(self, p, e): - pp, a, b = p # this is the polygon m * ss.polygon(p) - p2, e2 = self._ss.opposite_edge(pp, e) - m = self._ss.edge_matrix(pp, e) + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("half-translation") + sage: S.opposite_edge((0, 1, 0), 0) + ((1, 1, 0), 2) + + """ + pp, a, b = label # this is the polygon m * ss.polygon(p) + p2, e2 = self._ss.opposite_edge(pp, edge) + m = self._ss.edge_matrix(pp, edge) aa = a * m[0][0] + b * m[1][0] bb = b * m[0][0] - a * m[1][0] if aa > 0 or (aa == 0 and bb > 0): @@ -205,22 +514,69 @@ def opposite_edge(self, p, e): else: return ((p2, -aa, -bb), e2) + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: hash(S.minimal_cover("half-translation")) == hash(S.minimal_cover("half-translation")) + True + + """ + return hash(self._ss) + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + + EXAMPLES:: + + sage: from flatsurf import polygons, similarity_surfaces + sage: T = polygons.triangle(2, 13, 26) + sage: S = similarity_surfaces.billiard(T) + sage: S.minimal_cover("half-translation") == S.minimal_cover("half-translation") + True + + :: -class MinimalPlanarCover(Surface): + sage: TT = polygons.triangle(2, 15, 26) + sage: SS = similarity_surfaces.billiard(TT) + sage: SS = SS.minimal_cover("half-translation") + + sage: S == SS + False + + """ + if not isinstance(other, MinimalHalfTranslationCover): + return False + + return self._ss == other._ss + + +class MinimalPlanarCover(OrientedSimilaritySurface): r""" - The minimal planar cover of a surface S is the smallest cover C so that the - developing map from the universal cover U to the plane induces a well - defined map from C to the plane. This is a translation surface. + The minimal planar cover of a surface `S` is the smallest cover `C` so that + the developing map from the universal cover `U` to the plane induces a well + defined map from `C` to the plane. This is a translation surface. EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.square_torus() sage: from flatsurf.geometry.minimal_cover import MinimalPlanarCover - sage: pc = TranslationSurface(MinimalPlanarCover(s)) - sage: pc.is_finite() + sage: pc = s.minimal_cover("planar") + sage: isinstance(pc, MinimalPlanarCover) + True + sage: pc.is_finite_type() False - sage: sing = pc.singularity(pc.base_label(),0,limit=4) + sage: sing = pc.singularity(pc.root(), 0, limit=4) doctest:warning ... UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead. @@ -230,12 +586,18 @@ class MinimalPlanarCover(Surface): UserWarning: vertex_set() is deprecated and will be removed in a future version of sage-flatsurf; use representatives() and then vertex = surface.polygon(label).get_point_position(coordinates).get_vertex() instead 4 sage: TestSuite(s).run() + """ - def __init__(self, similarity_surface, base_label=None): - if similarity_surface.underlying_surface().is_mutable(): - if similarity_surface.is_finite(): - self._ss = similarity_surface.copy() + def __init__(self, similarity_surface, base_label=None, category=None): + if similarity_surface.is_mutable(): + if similarity_surface.is_finite_type(): + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface + + self._ss = MutableOrientedSimilaritySurface.from_surface( + similarity_surface + ) + self._ss.set_immutable() else: raise ValueError( "Can not construct MinimalPlanarCover of a surface that is mutable and infinite." @@ -243,58 +605,186 @@ def __init__(self, similarity_surface, base_label=None): else: self._ss = similarity_surface - if base_label is None: - base_label = self._ss.base_label() + if base_label is not None: + import warnings + + warnings.warn( + "the keyword argument base_label of a minimal planar cover is ignored and will be removed in a future version of sage-flatsurf; it had no effect in previous versions of sage-flatsurf" + ) + + if not self._ss.is_connected(): + raise NotImplementedError( + "can only create a minimal planar cover of connected surfaces" + ) # The similarity group containing edge identifications. - self._sg = self._ss.edge_transformation(self._ss.base_label(), 0).parent() + self._sg = self._ss.edge_transformation(self._ss.root(), 0).parent() + self._root = (self._ss.root(), self._sg.one()) - new_base_label = (self._ss.base_label(), self._sg.one()) + if similarity_surface.is_with_boundary(): + raise TypeError( + "can only build translation cover of surfaces without boundary" + ) - Surface.__init__( - self, self._ss.base_ring(), new_base_label, finite=False, mutable=False + from flatsurf.geometry.categories import TranslationSurfaces + + if category is None: + category = TranslationSurfaces() + + category &= TranslationSurfaces().InfiniteType() + + category = category.WithoutBoundary() + + if similarity_surface.is_connected(): + category = category.Connected() + + OrientedSimilaritySurface.__init__( + self, self._ss.base_ring(), category=category ) - def polygon(self, lab): + def _repr_(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus().minimal_cover("planar") + sage: S + Minimal Planar Cover of Translation Surface in H_1(0) built from a square + + """ + return f"Minimal Planar Cover of {repr(self._ss)}" + + def is_compact(self): + r""" + Return whether this surface is compact as a topological space, i.e., + return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus().minimal_cover("planar") + sage: S.is_compact() + False + + """ + return False + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus().minimal_cover("planar") + sage: S.roots() + ((0, (x, y) |-> (x, y)),) + + """ + return (self._root,) + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus().minimal_cover("planar") + sage: S.is_mutable() + False + + """ + return False + + def polygon(self, label): r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + EXAMPLES:: - sage: from flatsurf import * - sage: C = translation_surfaces.chamanara(1/2) - sage: C.polygon('a') - Traceback (most recent call last): - ... - ValueError: invalid label 'a' + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus().minimal_cover("planar") + sage: root = S.root() + sage: S.polygon(root) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + """ - if not isinstance(lab, tuple) or len(lab) != 2: - raise ValueError("invalid label {!r}".format(lab)) - return lab[1](self._ss.polygon(lab[0])) - - def opposite_edge(self, p, e): - pp, m = p # this is the polygon m * ss.polygon(p) - p2, e2 = self._ss.opposite_edge(pp, e) - me = self._ss.edge_transformation(pp, e) + if not isinstance(label, tuple) or len(label) != 2: + raise ValueError("invalid label {!r}".format(label)) + return label[1](self._ss.polygon(label[0])) + + def opposite_edge(self, label, edge): + r""" + Return the polygon label and edge index when crossing over the ``edge`` + of the polygon ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: from flatsurf import polygons, similarity_surfaces + sage: S = similarity_surfaces.billiard(polygons.triangle(2, 3, 5)).minimal_cover("planar") + sage: root = S.root() + sage: S.opposite_edge(root, 0) + ((1, (x, y) |-> (x, y)), 2) + + """ + pp, m = label # this is the polygon m * ss.polygon(p) + p2, e2 = self._ss.opposite_edge(pp, edge) + me = self._ss.edge_transformation(pp, edge) mm = m * ~me return ((p2, mm), e2) + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.infinite_staircase() + sage: hash(S.minimal_cover("planar")) == hash(S.minimal_cover("planar")) + True + + """ + return hash((self._ss, self._root)) + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. - Note that this is not implemented in most non-trivial cases. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. EXAMPLES:: - sage: from flatsurf import * - sage: s = translation_surfaces.square_torus() - sage: from flatsurf.geometry.minimal_cover import MinimalPlanarCover - sage: pc = TranslationSurface(MinimalPlanarCover(s)) - sage: pc == pc + sage: from flatsurf import polygons, similarity_surfaces + sage: T = polygons.triangle(2, 13, 26) + sage: S = similarity_surfaces.billiard(T) + sage: S.minimal_cover("planar") == S.minimal_cover("planar") True """ - if isinstance(other, MinimalPlanarCover): - if self._ss == other._ss and self._base_label == other._base_label: - return True + if not isinstance(other, MinimalPlanarCover): + return False - return super().__eq__(other) + return self._ss == other._ss and self._root == other._root diff --git a/flatsurf/geometry/origami.py b/flatsurf/geometry/origami.py new file mode 100644 index 000000000..89bcc042a --- /dev/null +++ b/flatsurf/geometry/origami.py @@ -0,0 +1,189 @@ +# ******************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016 W. Patrick Hooper +# 2022 Vincent Delecroix +# 2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# ******************************************************************** +from sage.rings.rational_field import QQ +from flatsurf.geometry.surface import OrientedSimilaritySurface + + +class AbstractOrigami(OrientedSimilaritySurface): + r""" + Abstract base class for (connected) origamis. + """ + + def __init__(self, domain, root=None, base_label=None, category=None): + self._domain = domain + + if base_label is not None: + import warnings + + warnings.warn( + "base_label has been deprecated as a keyword argument for AbstractOrigami and will be removed in a future version of sage-flatsurf; use root instead" + ) + root = base_label + base_label = None + + if root is None: + root = domain.an_element() + self._root = root + + from flatsurf.geometry.categories import TranslationSurfaces + + if category is None: + category = TranslationSurfaces() + + category &= TranslationSurfaces().WithoutBoundary().Connected() + + finite = domain.is_finite() + if finite: + category &= category.FiniteType() + else: + category &= category.InfiniteType() + + from flatsurf.geometry.polygon import polygons + + self._square = polygons.square() + + super().__init__(QQ, category=category) + + def roots(self): + return (self._root,) + + def labels(self): + from flatsurf.geometry.surface import LabelsFromView + + return LabelsFromView(self, self._domain) + + def is_mutable(self): + return False + + def up(self, label): + raise NotImplementedError + + def down(self, label): + raise NotImplementedError + + def right(self, label): + raise NotImplementedError + + def left(self, label): + raise NotImplementedError + + def _repr_(self): + return "Some AbstractOrigami" + + def polygon_labels(self): + return self._domain + + def polygon(self, lab): + if lab not in self._domain: + # Updated to print a possibly useful error message + raise ValueError("Label " + str(lab) + " is not in the domain") + + return self._square + + def opposite_edge(self, p, e): + if p not in self._domain: + raise ValueError + if e == 0: + return self.down(p), 2 + if e == 1: + return self.right(p), 3 + if e == 2: + return self.up(p), 0 + if e == 3: + return self.left(p), 1 + raise ValueError + + +class Origami(AbstractOrigami): + def __init__( + self, + r, + u, + rr=None, + uu=None, + domain=None, + root=None, + base_label=None, + category=None, + ): + if domain is None: + domain = r.parent().domain() + + self._r = r + self._u = u + if rr is None: + rr = ~r + else: + for a in domain.some_elements(): + if r(rr(a)) != a: + raise ValueError("r o rr is not identity on %s" % a) + if rr(r(a)) != a: + raise ValueError("rr o r is not identity on %s" % a) + if uu is None: + uu = ~u + else: + for a in domain.some_elements(): + if u(uu(a)) != a: + raise ValueError("u o uu is not identity on %s" % a) + if uu(u(a)) != a: + raise ValueError("uu o u is not identity on %s" % a) + + self._perms = [uu, r, u, rr] # down,right,up,left + AbstractOrigami.__init__( + self, domain, root=root, base_label=base_label, category=category + ) + + def opposite_edge(self, p, e): + if p not in self._domain: + raise ValueError( + "Polygon label p=" + str(p) + " is not in domain=" + str(self._domain) + ) + if e < 0 or e > 3: + raise ValueError("Edge value e=" + str(e) + " does not satisfy 0<=e<4.") + return self._perms[e](p), (e + 2) % 4 + + def up(self, label): + return self.opposite_edge(label, 2)[0] + + def down(self, label): + return self.opposite_edge(label, 0)[0] + + def right(self, label): + return self.opposite_edge(label, 1)[0] + + def left(self, label): + return self.opposite_edge(label, 3)[0] + + def _repr_(self): + return "Origami defined by r=%s and u=%s" % (self._r, self._u) + + def __eq__(self, other): + if not isinstance(other, Origami): + return False + + return ( + self._perms == other._perms + and self._domain is other._domain + and self.roots() == other.roots() + ) + + def __hash__(self): + return hash((Origami, tuple(self._perms), self._domain, self.roots())) diff --git a/flatsurf/geometry/polygon.py b/flatsurf/geometry/polygon.py index 0d4f6a1c5..bcdf037fb 100644 --- a/flatsurf/geometry/polygon.py +++ b/flatsurf/geometry/polygon.py @@ -1,29 +1,24 @@ r""" -Polygons embedded in the plane R^2. +Polygons embedded in the plane `\mathbb{R}^2`. -This file implements polygons with - - - action of matrices in GL^+(2,R) - - conversion between ground fields - -The emphasis is mostly on convex polygons but there is some limited support -for non-convex polygons. +The emphasis is mostly on convex polygons but there is some limited support for +non-convex polygons. EXAMPLES:: - sage: from flatsurf.geometry.polygon import polygons + sage: from flatsurf.geometry.polygon import Polygon sage: K. = NumberField(x^2 - 2, embedding=AA(2).sqrt()) - sage: p = polygons((1,0), (-sqrt2,1+sqrt2), (sqrt2-1,-1-sqrt2)) + sage: p = Polygon(edges=[(1,0), (-sqrt2,1+sqrt2), (sqrt2-1,-1-sqrt2)]) sage: p - Polygon: (0, 0), (1, 0), (-sqrt2 + 1, sqrt2 + 1) + Polygon(vertices=[(0, 0), (1, 0), (-sqrt2 + 1, sqrt2 + 1)]) sage: M = MatrixSpace(K,2) sage: m = M([[1,1+sqrt2],[0,1]]) sage: m * p - Polygon: (0, 0), (1, 0), (sqrt2 + 4, sqrt2 + 1) + Polygon(vertices=[(0, 0), (1, 0), (sqrt2 + 4, sqrt2 + 1)]) """ -###################################################################### +# **************************************************************************** # This file is part of sage-flatsurf. # # Copyright (C) 2016-2020 Vincent Delecroix @@ -41,604 +36,144 @@ # # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . -###################################################################### - -import operator +# **************************************************************************** from sage.all import ( cached_method, Parent, - UniqueRepresentation, - Sets, - Rings, - Fields, - AA, - ZZ, QQ, - RIF, matrix, vector, - free_module_element, - NumberField, - FreeModule, - lcm, - gcd, ) -from sage.misc.functional import numerical_approx -from sage.structure.element import get_coercion_model, Vector -from sage.structure.coerce import py_scalar_parent from sage.structure.element import Element -from sage.categories.action import Action -from sage.modules.free_module import VectorSpace from sage.structure.sequence import Sequence -from .matrix_2x2 import angle -from .subfield import ( +from flatsurf.geometry.subfield import ( number_field_elements_from_algebraics, - cos_minpoly, - chebyshev_T, - subfield_from_elements, ) -cm = get_coercion_model() - -# we implement action of GL(2,K) on polygons - -ZZ_0 = ZZ.zero() -ZZ_2 = ZZ(2) - - -def dot_product(v, w): - return v[0] * w[0] + v[1] * w[1] - - -def wedge_product(v, w): - return v[0] * w[1] - v[1] * w[0] - - -def wedge(u, v): - r""" - General wedge product of two vectors. - """ - d = len(u) - R = u.base_ring() - assert len(u) == len(v) and v.base_ring() == R - return free_module_element( - R, - d * (d - 1) // 2, - [(u[i] * v[j] - u[j] * v[i]) for i in range(d - 1) for j in range(i + 1, d)], - ) - - -def tensor(u, v): - r""" - General tensor product of two vectors. - """ - d = len(u) - R = u.base_ring() - assert len(u) == len(v) and v.base_ring() == R - return matrix(R, d, [u[i] * v[j] for j in range(d) for i in range(d)]) +from flatsurf.geometry.categories import EuclideanPolygons +from flatsurf.geometry.categories.euclidean_polygons_with_angles import ( + EuclideanPolygonsWithAngles as EuclideanPolygonsWithAnglesCategory, +) -def line_intersection(p1, p2, q1, q2): +class EuclideanPolygonPoint(Element): r""" - Return the point of intersection between the line joining p1 to p2 - and the line joining q1 to q2. If the lines are parallel we return - None. Here p1, p2, q1 and q2 should be vectors in the plane. - """ - if wedge_product(p2 - p1, q2 - q1) == 0: - return None - # Since the wedge product is non-zero, the following is invertible: - m = matrix([[p2[0] - p1[0], q1[0] - q2[0]], [p2[1] - p1[1], q1[1] - q2[1]]]) - return p1 + (m.inverse() * (q1 - p1))[0] * (p2 - p1) + A point in a polygon. - -def is_same_direction(v, w, zero=None): - r""" EXAMPLES:: - sage: from flatsurf.geometry.polygon import is_same_direction - sage: V = QQ**2 - - sage: is_same_direction(V((0,1)), V((0,2))) - True - sage: is_same_direction(V((1,-1)), V((2,-2))) - True - sage: is_same_direction(V((4,-2)), V((2,-1))) - True - sage: is_same_direction(V((1,2)), V((2,4))) - True - sage: is_same_direction(V((0,2)), V((0,1))) - True - - sage: is_same_direction(V((1,1)), V((1,2))) - False - sage: is_same_direction(V((1,2)), V((2,1))) - False - sage: is_same_direction(V((1,2)), V((1,-2))) - False - sage: is_same_direction(V((1,2)), V((-1,-2))) - False - sage: is_same_direction(V((2,-1)), V((-2,1))) - False - - sage: is_same_direction(V((1,0)), V.zero()) - Traceback (most recent call last): - ... - TypeError: zero vector has no direction - - """ - if not v or not w: - raise TypeError("zero vector has no direction") - return not wedge_product(v, w) and (v[0] * w[0] > 0 or v[1] * w[1] > 0) + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.an_element() + (0, 0) -def is_opposite_direction(v, w): - r""" - EXAMPLES:: + TESTS:: - sage: from flatsurf.geometry.polygon import is_opposite_direction - sage: V = QQ**2 + sage: p = s.an_element() - sage: is_opposite_direction(V((0,1)), V((0,-2))) + sage: from flatsurf.geometry.polygon import EuclideanPolygonPoint + sage: isinstance(p, EuclideanPolygonPoint) True - sage: is_opposite_direction(V((1,-1)), V((-2,2))) - True - sage: is_opposite_direction(V((4,-2)), V((-2,1))) - True - sage: is_opposite_direction(V((-1,-2)), V((2,4))) - True - - sage: is_opposite_direction(V((1,1)), V((1,2))) - False - sage: is_opposite_direction(V((1,2)), V((2,1))) - False - sage: is_opposite_direction(V((0,2)), V((0,1))) - False - sage: is_opposite_direction(V((1,2)), V((1,-2))) - False - sage: is_opposite_direction(V((1,2)), V((-1,2))) - False - sage: is_opposite_direction(V((2,-1)), V((-2,-1))) - False - - sage: is_opposite_direction(V((1,0)), V.zero()) - Traceback (most recent call last): - ... - TypeError: zero vector has no direction - - """ - if not v or not w: - raise TypeError("zero vector has no direction") - return not wedge_product(v, w) and (v[0] * w[0] < 0 or v[1] * w[1] < 0) - - -def solve(x, u, y, v): - r""" - Return (a,b) so that: x + au = y + bv - - INPUT: - - - ``x``, ``u``, ``y``, ``v`` -- two dimensional vectors - - EXAMPLES:: - sage: from flatsurf.geometry.polygon import solve - sage: K. = NumberField(x^2 - 2, embedding=AA(2).sqrt()) - sage: V = VectorSpace(K,2) - sage: x = V((1,-sqrt2)) - sage: y = V((1,1)) - sage: a = V((0,1)) - sage: b = V((-sqrt2, sqrt2+1)) - sage: u = V((0,1)) - sage: v = V((-sqrt2, sqrt2+1)) - sage: a, b = solve(x,u,y,v) - sage: x + a*u == y + b*v - True + sage: TestSuite(p).run() - sage: u = V((1,1)) - sage: v = V((1,sqrt2)) - sage: a, b = solve(x,u,y,v) - sage: x + a*u == y + b*v - True """ - d = -u[0] * v[1] + u[1] * v[0] - if d.is_zero(): - raise ValueError("parallel vectors") - a = v[1] * (x[0] - y[0]) + v[0] * (y[1] - x[1]) - b = u[1] * (x[0] - y[0]) + u[0] * (y[1] - x[1]) - return (a / d, b / d) - - -def segment_intersect(e1, e2, base_ring=None): - r""" - Return whether the segments ``e1`` and ``e2`` intersect. - - OUTPUT: - - ``0`` - do not intersect - - ``1`` - one endpoint in common - - ``2`` - non-trivial intersection - - EXAMPLES:: + def __init__(self, parent, xy): + self._xy = xy - sage: from flatsurf.geometry.polygon import segment_intersect - sage: segment_intersect(((0,0),(1,0)),((0,1),(0,3))) - 0 - sage: segment_intersect(((0,0),(1,0)),((0,0),(0,3))) - 1 - sage: segment_intersect(((0,0),(1,0)),((0,-1),(0,3))) - 2 - sage: segment_intersect(((-1,-1),(1,1)),((0,0),(2,2))) - 2 - sage: segment_intersect(((-1,-1),(1,1)),((1,1),(2,2))) - 1 + super().__init__(parent) - """ - if e1[0] == e1[1] or e2[0] == e2[1]: - raise ValueError("degenerate segments") + def position(self): + r""" + Describe the position of this point in the polygon. - if base_ring is None: - elts = [e[i][j] for e in (e1, e2) for i in (0, 1) for j in (0, 1)] - base_ring = cm.common_parent(*elts) - if isinstance(base_ring, type): - base_ring = py_scalar_parent(base_ring) - m = matrix(base_ring, 3) - xs1, ys1 = map(base_ring, e1[0]) - xt1, yt1 = map(base_ring, e1[1]) - xs2, ys2 = map(base_ring, e2[0]) - xt2, yt2 = map(base_ring, e2[1]) - - m[0] = [xs1, ys1, 1] - m[1] = [xt1, yt1, 1] - m[2] = [xs2, ys2, 1] - s0 = m.det() - m[2] = [xt2, yt2, 1] - s1 = m.det() - if (s0 > 0 and s1 > 0) or (s0 < 0 and s1 < 0): - # e2 stands on one side of the line generated by e1 - return 0 + OUTPUT: - m[0] = [xs2, ys2, 1] - m[1] = [xt2, yt2, 1] - m[2] = [xs1, ys1, 1] - s2 = m.det() - m[2] = [xt1, yt1, 1] - s3 = m.det() - if (s2 > 0 and s3 > 0) or (s2 < 0 and s3 < 0): - # e1 stands on one side of the line generated by e2 - return 0 + A :class:`PolygonPosition` object. - if s0 == 0 and s1 == 0: - assert s2 == 0 and s3 == 0 - if xt1 < xs1 or (xt1 == xs1 and yt1 < ys1): - xs1, xt1 = xt1, xs1 - ys1, yt1 = yt1, ys1 - if xt2 < xs2 or (xt2 == xs2 and yt2 < ys2): - xs2, xt2 = xt2, xs2 - ys2, yt2 = yt2, ys2 + EXAMPLES:: - if xs1 == xt1 == xs2 == xt2: - xs1, xt1, xs2, xt2 = ys1, yt1, ys2, yt2 + sage: from flatsurf import polygons + sage: s = polygons.square() - assert xs1 < xt1 and xs2 < xt2, (xs1, xt1, xs2, xt2) + sage: p = s.an_element() + sage: p + (0, 0) - if (xs2 > xt1) or (xt2 < xs1): - return 0 # no intersection - elif (xs2 == xt1) or (xt2 == xs1): - return 1 # one endpoint in common - else: - assert ( - xs1 <= xs2 < xt1 - or xs1 < xt2 <= xt1 - or (xs2 < xs1 and xt2 > xt1) - or (xs2 > xs1 and xt2 < xt1) - ), (xs1, xt1, xs2, xt2) - return 2 # one dimensional - - elif s0 == 0 or s1 == 0: - # treat alignment here - if s2 == 0 or s3 == 0: - return 1 # one endpoint in common - else: - return 2 # intersection in the middle + sage: p.position() + point positioned on vertex 0 of polygon - return 2 # middle intersection + .. SEEALSO:: + :meth:`~.categories.euclidean_polygons.EuclideanPolygons.Simple.Convex.ParentMethods.get_point_position` -def is_between(e0, e1, f): - r""" - Check whether the vector ``f`` is strictly in the sector formed by the vectors - ``e0`` and ``e1`` (in counter-clockwise order). + """ + return self.parent().get_point_position(self._xy) - EXAMPLES:: + def _repr_(self): + r""" + Return a printable representation of this point. - sage: from flatsurf.geometry.polygon import is_between - sage: V = ZZ^2 - sage: is_between(V((1, 0)), V((1, 1)), V((2, 1))) - True + EXAMPLES:: - """ - if e0[0] * e1[1] > e1[0] * e0[1]: - # positive determinant - # [ e0[0] e1[0] ]^-1 = [ e1[1] -e1[0] ] - # [ e0[1] e1[1] ] [-e0[1] e0[0] ] - # f[0] * e1[1] - e1[0] * f[1] > 0 - # - f[0] * e0[1] + e0[0] * f[1] > 0 - return e1[1] * f[0] > e1[0] * f[1] and e0[0] * f[1] > e0[1] * f[0] - elif e0[0] * e1[1] == e1[0] * e0[1]: - # aligned vector - return e0[0] * f[1] > e0[1] * f[0] - else: - # negative determinant - # [ e1[0] e0[0] ]^-1 = [ e0[1] -e0[0] ] - # [ e1[1] e0[1] ] [-e1[1] e1[0] ] - # f[0] * e0[1] - e0[0] * f[1] > 0 - # - f[0] * e1[1] + e1[0] * f[1] > 0 - return e0[1] * f[0] <= e0[0] * f[1] or e1[0] * f[1] <= e1[1] * f[0] + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: p = s.an_element() + sage: p + (0, 0) -def projectivization(x, y, signed=True, denominator=None): - r""" - TESTS:: + """ + return repr(self._xy) - sage: from flatsurf.geometry.polygon import projectivization - - sage: projectivization(2/3, -3/5, signed=True, denominator=True) - (10, -9) - sage: projectivization(2/3, -3/5, signed=False, denominator=True) - (-10, 9) - sage: projectivization(2/3, -3/5, signed=True, denominator=False) - (10/9, -1) - sage: projectivization(2/3, -3/5, signed=False, denominator=False) - (-10/9, 1) - - sage: projectivization(-1/2, 0, signed=True, denominator=True) - (-1, 0) - sage: projectivization(-1/2, 0, signed=False, denominator=True) - (1, 0) - sage: projectivization(-1/2, 0, signed=True, denominator=False) - (-1, 0) - sage: projectivization(-1/2, 0, signed=False, denominator=False) - (1, 0) - """ - if y: - z = x / y - if denominator is True or (denominator is None and hasattr(z, "denominator")): - d = z.denominator() - else: - d = 1 - if signed and y < 0: - d *= -1 - return (z * d, d) - elif signed and x < 0: - return (-1, 0) - else: - return (1, 0) + def __eq__(self, other): + r""" + Return whether this point is indistinguishable from ``other``. + EXAMPLES:: -def triangulate(vertices): - r""" - Return a triangulation of the list of vectors ``vertices``. + sage: from flatsurf import polygons + sage: s = polygons.square() - This function assumes that ``vertices`` form the vertices of a polygon - enumerated in counter-clockwise order. + sage: s.an_element() == s.an_element() + True - EXAMPLES:: + sage: t = polygons.square() - sage: from flatsurf.geometry.polygon import triangulate - sage: V = ZZ**2 - sage: verts = list(map(V, [(0,0), (1,0), (1,1), (0,1)])) - sage: triangulate(verts) - [(0, 2)] - - sage: quad = [(0,0), (1,-1), (0,1), (-1,-1)] - sage: quad = list(map(V, quad)) - sage: for i in range(4): - ....: print(triangulate(quad[i:] + quad[:i])) - [(0, 2)] - [(1, 3)] - [(0, 2)] - [(1, 3)] - - sage: poly = [(0,0),(1,1),(2,0),(3,1),(4,0),(4,2), - ....: (-4,2),(-4,0),(-3,1),(-2,0),(-1,1)] - sage: poly = list(map(V, poly)) - sage: triangulate(poly) - [(1, 3), (3, 5), (5, 8), (6, 8), (8, 10), (10, 1), (1, 5), (5, 10)] - sage: for i in range(len(poly)): - ....: _ = triangulate(poly[i:] + poly[:i]) - - sage: poly = [(0,0), (1,0), (2,0), (2,1), (2,2), (1,2), (0,2), (0,1)] - sage: poly = list(map(V, poly)) - sage: edges = triangulate(poly) - sage: edges - [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] - sage: for i in range(len(poly)): - ....: _ = triangulate(poly[i:] + poly[:i]) - - sage: poly = [(0,0), (1,2), (3,3), (1,4), (0,6), (-1,4), (-3,-3), (-1,2)] - sage: poly = list(map(V, poly)) - sage: triangulate(poly) - [(0, 3), (1, 3), (3, 5), (5, 7), (7, 3)] - sage: for i in range(len(poly)): - ....: _ = triangulate(poly[i:] + poly[:i]) - - sage: x = polygen(QQ) - sage: p = x^4 - 5*x^2 + 5 - sage: r = AA.polynomial_root(p, RIF(1.17,1.18)) - sage: K. = NumberField(p, embedding=r) - sage: V = K**2 - sage: poly = [(1/2*a^2 - 3/2, 1/2*a), - ....: (-a^3 + 2*a^2 + 2*a - 4, 0), - ....: (1/2*a^2 - 3/2, -1/2*a), - ....: (1/2*a^3 - a^2 - 1/2*a + 1, 1/2*a^2 - a), - ....: (-1/2*a^2 + 1, 1/2*a^3 - 3/2*a), - ....: (-1/2*a + 1, a^3 - 3/2*a^2 - 2*a + 5/2), - ....: (1, 0), - ....: (-1/2*a + 1, -a^3 + 3/2*a^2 + 2*a - 5/2), - ....: (-1/2*a^2 + 1, -1/2*a^3 + 3/2*a), - ....: (1/2*a^3 - a^2 - 1/2*a + 1, -1/2*a^2 + a)] - sage: poly = list(map(V, poly)) - sage: triangulate(poly) - [(0, 3), (1, 3), (3, 5), (5, 7), (7, 9), (9, 3), (3, 7)] - - sage: z = QQbar.zeta(24) - sage: pts = [(1+i%2) * z**i for i in range(24)] - sage: pts = [vector(AA, (x.real(), x.imag())) for x in pts] - sage: triangulate(pts) - [(0, 2), ..., (16, 0)] - - TESTS: - - This is https://github.com/flatsurf/sage-flatsurf/issues/87 :: - - sage: from flatsurf.geometry.polygon import triangulate - sage: x = polygen(QQ) - sage: K. = NumberField(x^2 - 3, embedding=AA(3).sqrt()) - sage: pts = [(0, 0), (1, 0), (1/2*c + 1, -1/2), (c + 1, 0), (-3/2*c + 1, 5/2), (0, c - 2)] - sage: pts = [vector(K, v) for v in pts] - sage: triangulate(pts) - [(0, 4), (1, 3), (4, 1)] - """ + sage: s.an_element() == t.an_element() + True - n = len(vertices) - if n < 3: - raise ValueError - if n == 3: - return [] - - # NOTE: The algorithm is naive. We look at all possible chords between - # the i-th and j-th vertices. If the chord does not intersect any edge - # then we cut the polygon along this edge and call recursively - # triangulate on the two pieces. - for i in range(n - 1): - eiright = vertices[(i + 1) % n] - vertices[i] - eileft = vertices[(i - 1) % n] - vertices[i] - for j in range(i + 2, (n if i else n - 1)): - ejright = vertices[(j + 1) % n] - vertices[j] - ejleft = vertices[(j - 1) % n] - vertices[j] - chord = vertices[j] - vertices[i] - - # check angles with neighbouring edges - if not ( - is_between(eiright, eileft, chord) - and is_between(ejright, ejleft, -chord) - ): - continue - - # check intersection with other edges - e = (vertices[i], vertices[j]) - good = True - for k in range(n): - f = (vertices[k], vertices[(k + 1) % n]) - res = segment_intersect(e, f) - if res == 2: - good = False - break - elif res == 1: - assert k == (i - 1) % n or k == i or k == (j - 1) % n or k == j - - if good: - part0 = [(s + i, t + i) for s, t in triangulate(vertices[i : j + 1])] - part1 = [] - for s, t in triangulate(vertices[j:] + vertices[: i + 1]): - if s < n - j: - s += j - else: - s -= n - j - if t < n - j: - t += j - else: - t -= n - j - part1.append((s, t)) - return [(i, j)] + part0 + part1 - raise RuntimeError("input {} must be wrong".format(vertices)) - - -def build_faces(n, edges): - r""" - Given a combinatorial list of pairs ``edges`` forming a cell-decomposition - of a polygon (with vertices labeled from ``0`` to ``n-1``) return the list - of cells. + """ + if not isinstance(other, EuclideanPolygonPoint): + return False - EXAMPLES:: + return self.parent() == other.parent() and self._xy == other._xy - sage: from flatsurf.geometry.polygon import build_faces - sage: build_faces(4, [(0,2)]) - [[0, 1, 2], [2, 3, 0]] - sage: build_faces(4, [(1,3)]) - [[1, 2, 3], [3, 0, 1]] - sage: build_faces(5, [(0,2), (0,3)]) - [[0, 1, 2], [3, 4, 0], [0, 2, 3]] - sage: build_faces(5, [(0,2)]) - [[0, 1, 2], [2, 3, 4, 0]] - sage: build_faces(5, [(1,4)]) - [[1, 2, 3, 4], [4, 0, 1]] - sage: build_faces(5, [(1,3),(3,0)]) - [[1, 2, 3], [3, 4, 0], [0, 1, 3]] - """ - polygons = [list(range(n))] - for u, v in edges: - j = None - for i, p in enumerate(polygons): - if u in p and v in p: - if j is not None: - raise RuntimeError - j = i - if j is None: - raise RuntimeError - p = polygons[j] - i0 = p.index(u) - i1 = p.index(v) - if i0 > i1: - i0, i1 = i1, i0 - polygons[j] = p[i0 : i1 + 1] - polygons.append(p[i1:] + p[: i0 + 1]) - return polygons - - -class MatrixActionOnPolygons(Action): - def __init__(self, polygons): - from sage.matrix.matrix_space import MatrixSpace - - R = polygons.base_ring() - Action.__init__(self, MatrixSpace(R, 2), polygons, True, operator.mul) - - def _act_(self, g, x): + def __hash__(self): r""" - Apply the 2x2 matrix `g` to the polygon `x`. - - The matrix must have non-zero determinant. If the determinant is - negative, then the vertices and edges are relabeled according to the - involutions `v \mapsto (n-v)%n` and `e \mapsto n-1-e` respectively. + Return a hash value of this point that is compatible with + :meth:`_richcmp_`. EXAMPLES:: sage: from flatsurf import polygons - sage: p = polygons(vertices = [(1,0),(0,1),(-1,-1)]) - sage: print(p) - Polygon: (1, 0), (0, 1), (-1, -1) - sage: r = matrix(ZZ,[[0,1], [1,0]]) - sage: print(r*p) - Polygon: (0, 1), (-1, -1), (1, 0) + sage: s = polygons.square() + + sage: hash(s.an_element()) == hash(s.an_element()) + True + """ - det = g.det() - if det > 0: - return x.parent()(vertices=[g * v for v in x.vertices()], check=False) - if det < 0: - # Note that in this case we reverse the order - vertices = [g * x.vertex(0)] - for i in range(x.num_edges() - 1, 0, -1): - vertices.append(g * x.vertex(i)) - return x.parent()(vertices=vertices, check=False) - raise ValueError("Can not act on a polygon with matrix with zero determinant") + return hash(self._xy) class PolygonPosition: r""" - Class for describing the position of a point within or outside of a polygon. + Describes the position of a point within or outside of a polygon. """ # Position Types: OUTSIDE = 0 @@ -714,124 +249,145 @@ def get_vertex(self): return self._vertex -class Polygon(Element): - def __init__(self, parent, vertices, check=True): - Element.__init__(self, parent) - V = parent.module() +class EuclideanPolygon(Parent): + r""" + A (possibly non-convex) simple polygon in the plane `\mathbb{R}^2`. + + EXAMPLES:: + + sage: from flatsurf import polygons, Polygon + sage: s = polygons.square() + sage: s + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + sage: Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + TESTS:: + + sage: from flatsurf.geometry.polygon import EuclideanPolygon + sage: isinstance(s, EuclideanPolygon) + True + + sage: TestSuite(s).run() + + """ + Element = EuclideanPolygonPoint + + def __init__(self, base_ring, vertices, category=None): + V = base_ring**2 self._v = tuple(map(V, vertices)) for vv in self._v: vv.set_immutable() - if check: - self._non_intersection_check() - self._inside_outside_check() + if category is None: + category = EuclideanPolygons(base_ring) + + category &= EuclideanPolygons(base_ring) - def _inside_outside_check(self): + super().__init__(base_ring, category=category) + + if "Convex" not in category.axioms() and self.is_convex(): + self._refine_category_(category.Convex()) + + # The category is not refined automatically to the WithAngles() + # subcategory since computation of angles can be very costly. + # The category gets further refined when angles() is invoked. + + def _an_element_(self): r""" - TESTS:: + Return a point of this polygon. + + EXAMPLES:: + + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.an_element() + (0, 0) - sage: from flatsurf import Polygons - sage: P = Polygons(QQ) - sage: P(vertices=[(0,0),(-1,-1),(0,1),(1,-1)]) - Traceback (most recent call last): - ... - ValueError: the vertices are in clockwise order """ - # NOTE: should we do something more efficient? - if self.area() < 0: - raise ValueError("the vertices are in clockwise order") + return self(self.vertices()[0]) - def _non_intersection_check(self): + def parent(self): r""" - TESTS:: + Return the category this polygon belongs to. + + EXAMPLES:: - sage: from flatsurf import Polygons - sage: P = Polygons(QQ) - sage: P(vertices=[(0,0),(2,0),(1,1),(1,-1)]) - Traceback (most recent call last): + sage: from flatsurf import polygons + sage: s = polygons.square() + sage: s.parent() + doctest:warning ... - ValueError: edge 0 (= ((0, 0), (2, 0))) and edge 2 (= ((1, 1), (1, -1))) intersect + UserWarning: parent() of a polygon has been deprecated and will be removed in a future version of sage-flatsurf; use category() instead + Category of convex simple euclidean rectangles over Rational Field + + Note that the parent may change during the lifetime of a polygon as + more of its features are discovered:: + + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (1, 0), (1, 1)]) + sage: p.parent() + Category of convex simple euclidean polygons over Rational Field + sage: p.angles() + (1/8, 1/4, 1/8) + sage: p.parent() + Category of convex simple euclidean triangles with angles (1/8, 1/4, 1/8) over Rational Field + """ - n = len(self._v) - for i in range(n - 1): - ei = (self._v[i], self._v[i + 1]) - for j in range(i + 1, n): - ej = (self._v[j], self._v[(j + 1) % n]) - res = segment_intersect(ei, ej) - if j == i + 1 or (i == 0 and j == n - 1): - if res > 1: - raise ValueError( - "edge %d (= %s) and edge %d (= %s) backtrack" - % (i, ei, j, ej) - ) - elif res > 0: - raise ValueError( - "edge %d (= %s) and edge %d (= %s) intersect" % (i, ei, j, ej) - ) + import warnings + + warnings.warn( + "parent() of a polygon has been deprecated and will be removed in a future version of sage-flatsurf; use category() instead" + ) + + return self.category() + @cached_method def __hash__(self): - # Apparently tuples do not cache their hash! - try: - return self._hash - except AttributeError: - self._hash = hash(self._v) - return self._hash + return hash(self._v) def __eq__(self, other): r""" TESTS:: - sage: from flatsurf.geometry.polygon import polygons + sage: from flatsurf import polygons, Polygon sage: p1 = polygons.square() - sage: p2 = polygons((1,0),(0,1),(-1,0),(0,-1), ring=QQbar) + sage: p2 = Polygon(edges=[(1,0),(0,1),(-1,0),(0,-1)], base_ring=QQbar) sage: p1 == p2 True - sage: p3 = polygons((2,0),(-1,1),(-1,-1)) + sage: p3 = Polygon(edges=[(2,0),(-1,1),(-1,-1)]) sage: p1 == p3 False - """ - if not isinstance(self, Polygon) or not isinstance(other, Polygon): - return NotImplemented - return self._v == other._v - def __ne__(self, other): - r""" TESTS:: - sage: from flatsurf.geometry.polygon import polygons + sage: from flatsurf import Polygon, polygons sage: p1 = polygons.square() - sage: p2 = polygons((1,0),(0,1),(-1,0),(0,-1), ring=QQbar) + sage: p2 = Polygon(edges=[(1,0),(0,1),(-1,0),(0,-1)], base_ring=QQbar) sage: p1 != p2 False - sage: p3 = polygons((2,0),(-1,1),(-1,-1)) + sage: p3 = Polygon(edges=[(2,0),(-1,1),(-1,-1)]) sage: p1 != p3 True """ - if not isinstance(self, Polygon) or not isinstance(other, Polygon): - return NotImplemented - return self._v != other._v - - def __lt__(self, other): - raise TypeError - - __le__ = __lt__ - - __gt__ = __lt__ + if not isinstance(other, EuclideanPolygon): + return False - __ge__ = __lt__ + return self._v == other._v def cmp(self, other): r""" Implement a total order on polygons """ - if not isinstance(other, Polygon): + if not isinstance(other, EuclideanPolygon): raise TypeError("__cmp__ only implemented for ConvexPolygons") - if not self.parent().base_ring() == other.parent().base_ring(): + if not self.base_ring() == other.base_ring(): raise ValueError( "__cmp__ only implemented for ConvexPolygons defined over the same base_ring" ) - sign = self.num_edges() - other.num_edges() + sign = len(self.vertices()) - len(other.vertices()) if sign > 0: return 1 if sign < 0: @@ -841,7 +397,7 @@ def cmp(self, other): return 1 if sign < self.base_ring().zero(): return -1 - for v in range(1, self.num_edges()): + for v in range(1, len(self.vertices())): p = self.vertex(v) q = other.vertex(v) sign = p[0] - q[0] @@ -856,38 +412,29 @@ def cmp(self, other): return -1 return 0 - def triangulation(self): - r""" - Return a list of pairs of indices of vertices that together with the boundary - form a triangulation. - - EXAMPLES:: - - sage: from flatsurf import polygons - sage: P = polygons(vertices=[(0,0), (1,0), (1,1), (0,1), (0,2), (-1,2), (-1,1), (-2,1), - ....: (-2,0), (-1,0), (-1,-1), (0,-1)], convex=False) - sage: P.triangulation() - [(0, 2), (2, 8), (3, 5), (6, 8), (8, 3), (3, 6), (9, 11), (0, 9), (2, 9)] - """ - if len(self._v) == 3: - return [] - return triangulate(self._v) - def translate(self, u): r""" + Return a copy of this polygon that has been translated by ``u``. + TESTS:: - sage: from flatsurf import polygons - sage: polygons(vertices=[(0,0), (2,0), (1,1)]).translate((3,-2)) - Polygon: (3, -2), (5, -2), (4, -1) + sage: from flatsurf import Polygon + sage: Polygon(vertices=[(0,0), (2,0), (1,1)]).translate((3,-2)) + Polygon(vertices=[(3, -2), (5, -2), (4, -1)]) + """ - P = self.parent() - u = P.module()(u) - return P.element_class(P, [u + v for v in self._v], check=False) + u = (self.base_ring() ** 2)(u) + return Polygon( + base_ring=self.base_ring(), + vertices=[u + v for v in self._v], + check=False, + category=self.category(), + ) - def change_ring(self, R): + def change_ring(self, ring): r""" - Return an equal polygon over the base ring ``R``. + Return a copy of this polygon whose vertices have coordinates over the + base ring ``ring``. EXAMPLES:: @@ -895,2493 +442,1189 @@ def change_ring(self, R): sage: S = polygons.square() sage: K. = NumberField(x^2 - 2, embedding=AA(2)**(1/2)) sage: S.change_ring(K) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) sage: S.change_ring(K).base_ring() Number Field in sqrt2 with defining polynomial x^2 - 2 with sqrt2 = 1.4142... + """ - if R is self.base_ring(): + if ring is self.base_ring(): return self - return ConvexPolygons(R)(vertices=self._v, check=False) - def is_convex(self): - for i in range(self.num_edges()): - if wedge_product(self.edge(i), self.edge(i + 1)) < 0: - return False - return True + return Polygon( + base_ring=ring, vertices=self._v, category=self.category().change_ring(ring) + ) def is_strictly_convex(self): r""" - Check whether the polygon is strictly convex + Return whether this polygon is strictly convex. EXAMPLES:: - sage: from flatsurf import * - sage: polygons(vertices=[(0,0), (1,0), (1,1)]).is_strictly_convex() + sage: from flatsurf import Polygon + sage: Polygon(vertices=[(0,0), (1,0), (1,1)]).is_strictly_convex() + doctest:warning + ... + UserWarning: is_strictly_convex() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex(strict=True) instead True - sage: polygons(vertices=[(0,0), (1,0), (2,0), (1,1)]).is_strictly_convex() + sage: Polygon(vertices=[(0,0), (1,0), (2,0), (1,1)]).is_strictly_convex() False - """ - for i in range(self.num_edges()): - if wedge_product(self.edge(i), self.edge(i + 1)).is_zero(): - return False - return True - def base_ring(self): - return self.parent().base_ring() - - field = base_ring + TESTS:: - def num_edges(self): - return len(self._v) + sage: Polygon(vertices=[(0, 0), (1, 1/2), (2, 0), (1, 1)]).is_strictly_convex() + False - def _repr_(self): - r""" - String representation. """ - return "Polygon: " + ", ".join(map(str, self.vertices())) + import warnings - @cached_method - def module(self): + warnings.warn( + "is_strictly_convex() has been deprecated and will be removed in a future version of sage-flatsurf; use is_convex(strict=True) instead" + ) + + return self.is_convex(strict=True) + + def num_edges(self): r""" - Return the free module of rank 2 in which this polygon embeds. + Return the number of edges of this polygon. EXAMPLES:: sage: from flatsurf import polygons sage: S = polygons.square() - sage: S.module() - Vector space of dimension 2 over Rational Field + sage: S.num_edges() + doctest:warning + ... + UserWarning: num_edges() has been deprecated and will be removed in a future version of sage-flatsurf; use len(vertices()) instead + 4 """ - return self.parent().module() + import warnings - @cached_method - def vector_space(self): + warnings.warn( + "num_edges() has been deprecated and will be removed in a future version of sage-flatsurf; use len(vertices()) instead" + ) + + return len(self.vertices()) + + def _repr_(self): r""" - Return the vector space of dimension 2 in which this polygon embeds. + Return a printable representation of this polygon. EXAMPLES:: sage: from flatsurf import polygons sage: S = polygons.square() - sage: S.vector_space() - Vector space of dimension 2 over Rational Field - - """ - return self.parent().vector_space() - - def vertices(self, translation=None): - r""" - Return the set of vertices as vectors. - """ - if translation is None: - return self._v - - translation = self.parent().vector_space()(translation) - return [translation + v for v in self.vertices()] + sage: S + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - def vertex(self, i): - r""" - Return the ``i``-th vertex as a vector """ - return self._v[i % len(self._v)] - - def __iter__(self): - return iter(self.vertices()) + return f"Polygon(vertices={repr(list(self.vertices()))})" - def edges(self): + def vertices(self, translation=None, marked_vertices=True): r""" - Return an iterator overt the edges - """ - return [self.edge(i) for i in range(self.num_edges())] + Return the vertices of this polygon in counterclockwise order as + vectors in the real plane. - def edge(self, i): - r""" - Return a vector representing the ``i``-th edge of the polygon. - """ - return self.vertex(i + 1) - self.vertex(i) + INPUT: - def plot( - self, translation=None, polygon_options={}, edge_options={}, vertex_options={} - ): - r""" - Plot the polygon with the origin at ``translation``. + - ``marked_vertices`` -- a boolean (default: ``True``); whether to + include vertices with a π angle in the output. EXAMPLES:: sage: from flatsurf import polygons - sage: S = polygons.square() - sage: S.plot() - ...Graphics object consisting of 3 graphics primitives - - We can specify an explicit ``zorder`` to render edges and vertices on - top of the axes which are rendered at z-order 3:: - - sage: S.plot(edge_options={'zorder': 3}, vertex_options={'zorder': 3}) - ...Graphics object consisting of 3 graphics primitives - - We can control the colors, e.g., we can render transparent polygons, - with red edges and blue vertices:: - - sage: S.plot(polygon_options={'fill': None}, edge_options={'color': 'red'}, vertex_options={'color': 'blue'}) - ...Graphics object consisting of 3 graphics primitives + sage: s = polygons.square() + sage: s.vertices() + ((0, 0), (1, 0), (1, 1), (0, 1)) """ - from sage.plot.point import point2d - from sage.plot.line import line2d - from sage.plot.polygon import polygon2d + if translation is not None: + import warnings - P = self.vertices(translation) + warnings.warn( + "the translation keyword of vertices() has been deprecated and will be removed in a future version of sage-flatsurf; use translate().vertices() instead" + ) - polygon_options = {"alpha": 0.3, "zorder": 1, **polygon_options} - edge_options = {"color": "orange", "zorder": 2, **edge_options} - vertex_options = {"color": "red", "zorder": 2, **vertex_options} + return self.translate(translation).vertices(marked_vertices=marked_vertices) - return ( - polygon2d(P, **polygon_options) - + line2d(P + (P[0],), **edge_options) - + point2d(P, **vertex_options) - ) + if not marked_vertices: + return tuple( + vertex + for (vertex, slope) in zip(self._v, self.slopes(relative=True)) + if slope[1] != 0 + ) - def angle(self, e, numerical=False, assume_rational=False): - r""" - Return the angle at the beginning of the start point of the edge ``e``. + return self._v - EXAMPLES:: + def __iter__(self): + import warnings - sage: from flatsurf.geometry.polygon import polygons - sage: polygons.square().angle(0) - 1/4 - sage: polygons.regular_ngon(8).angle(0) - 3/8 - - sage: T = polygons(vertices=[(0,0), (3,1), (1,5)]) - sage: [T.angle(i, numerical=True) for i in range(3)] # abs tol 1e-13 - [0.16737532973071603, 0.22741638234956674, 0.10520828791971722] - sage: sum(T.angle(i, numerical=True) for i in range(3)) # abs tol 1e-13 - 0.5 - """ - return angle( - self.edge(e), - -self.edge((e - 1) % self.num_edges()), - numerical=numerical, - assume_rational=assume_rational, + warnings.warn( + "iterating over the vertices of a polygon implicitly has been deprecated, this functionality will be removed in a future version of sage-flatsurf; iterate over vertices() instead" ) + return iter(self.vertices()) - def angles(self, numerical=False, assume_rational=False): - r""" - Return the list of angles of this polygon (divided by `2 \pi`). +class PolygonsConstructor: + def square(self, side=1, **kwds): + r""" EXAMPLES:: sage: from flatsurf.geometry.polygon import polygons - sage: T = polygons(angles=[1,2,3]) - sage: [T.angle(i) for i in range(3)] - [1/12, 1/6, 1/4] - sage: sum(T.angle(i) for i in range(3)) - 1/2 + sage: polygons.square() + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: polygons.square(base_ring=QQbar).category() + Category of convex simple euclidean rectangles over Algebraic Field + """ - return [self.angle(i) for i in range(self.num_edges())] + return self.rectangle(side, side, **kwds) - def area(self): + def rectangle(self, width, height, **kwds): r""" - Return the area of this polygon. - EXAMPLES:: sage: from flatsurf.geometry.polygon import polygons - sage: polygons.regular_ngon(8).area() - 2*a + 2 - sage: _ == 2*AA(2).sqrt() + 2 - True - - sage: AA(polygons.regular_ngon(11).area()) - 9.36563990694544? - sage: polygons.square().area() - 1 - sage: (2*polygons.square()).area() - 4 - """ - # Will use an area formula obtainable from Green's theorem. See for instance: - # http://math.blogoverflow.com/2014/06/04/greens-theorem-and-area-of-polygons/ - total = self.field().zero() - for i in range(self.num_edges()): - total += (self.vertex(i)[0] + self.vertex(i + 1)[0]) * self.edge(i)[1] - return total / ZZ_2 - - def centroid(self): - r""" - Return the coordinates of the centroid of this polygon. + sage: polygons.rectangle(1,2) + Polygon(vertices=[(0, 0), (1, 0), (1, 2), (0, 2)]) - ALGORITHM: + sage: K. = QuadraticField(2) + sage: polygons.rectangle(1,sqrt2) + Polygon(vertices=[(0, 0), (1, 0), (1, sqrt2), (0, sqrt2)]) + sage: _.category() + Category of convex simple euclidean rectangles over Number Field in sqrt2 with defining polynomial x^2 - 2 with sqrt2 = 1.414213562373095? - We use the customary formula of the centroid of polygons, see - https://en.wikipedia.org/wiki/Centroid#Of_a_polygon + """ + if width <= 0: + raise ValueError("width must be positive") - EXAMPLES:: + if height <= 0: + raise ValueError("height must be positive") - sage: from flatsurf.geometry.polygon import polygons - sage: P = polygons.regular_ngon(4); P - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: P.centroid() - (1/2, 1/2) - - sage: P = polygons.regular_ngon(8); P - Polygon: (0, 0), (1, 0), (1/2*a + 1, 1/2*a), (1/2*a + 1, 1/2*a + 1), (1, a + 1), (0, a + 1), (-1/2*a, 1/2*a + 1), (-1/2*a, 1/2*a) - sage: P.centroid() - (1/2, 1/2*a + 1/2) - - sage: P = polygons.regular_ngon(11) - sage: C = P.centroid() - sage: P = P.translate(-C) - sage: P.centroid() - (0, 0) + if not kwds: + # No need to verify that the edges and the angles are consistent. + kwds = {"check": False} - """ - x, y = list(zip(*self.vertices())) - nvertices = len(x) - A = self.area() - - from sage.all import vector - - return vector( - ( - ~(6 * A) - * sum( - [ - (x[i - 1] + x[i]) * (x[i - 1] * y[i] - x[i] * y[i - 1]) - for i in range(nvertices) - ] - ), - ~(6 * A) - * sum( - [ - (y[i - 1] + y[i]) * (x[i - 1] * y[i] - x[i] * y[i - 1]) - for i in range(nvertices) - ] - ), - ) + return Polygon( + edges=[(width, 0), (0, height), (-width, 0), (0, -height)], + angles=(1, 1, 1, 1), + **kwds, ) - def j_invariant(self): - r""" - Return the Kenyon-Smille J-invariant of this polygon. + def triangle(self, a, b, c): + """ + Return the triangle with angles a*pi/N,b*pi/N,c*pi/N where N=a+b+c. - The base ring of the polygon must be a number field. + INPUT: - The output is a triple ``(Jxx, Jyy, Jxy)`` that corresponds - respectively to the Sah-Arnoux-Fathi invariant of the vertical flow, - the Sah-Arnoux-Fathi invariant of the horizontal flow and the `xy`-matrix. + - ``a``, ``b``, ``c`` -- integers EXAMPLES:: - sage: from flatsurf import * - - sage: polygons.right_triangle(1/3,1).j_invariant() - ( - [0 0] - (0), (0), [1 0] - ) + sage: from flatsurf.geometry.polygon import polygons + sage: T = polygons.triangle(3,4,5) + sage: T + Polygon(vertices=[(0, 0), (1, 0), (-1/2*c0 + 3/2, -1/2*c0 + 3/2)]) + sage: T.base_ring() + Number Field in c0 with defining polynomial x^2 - 3 with c0 = 1.732050807568878? - The regular 8-gon:: + sage: polygons.triangle(1,2,3).angles() + (1/12, 1/6, 1/4) - sage: polygons.regular_ngon(8).j_invariant() - ( - [2 2] - (0), (0), [2 1] - ) + Some fairly complicated examples:: - ( - [ 0 3/2] - (1/2), (-1/2), [3/2 0] - ) + sage: polygons.triangle(1, 15, 21) # long time (2s) + Polygon(vertices=[(0, 0), + (1, 0), + (1/2*c^34 - 17*c^32 + 264*c^30 - 2480*c^28 + 15732*c^26 - 142481/2*c^24 + 237372*c^22 - 1182269/2*c^20 + + 1106380*c^18 - 1552100*c^16 + 3229985/2*c^14 - 2445665/2*c^12 + 654017*c^10 - 472615/2*c^8 + 107809/2*c^6 - 13923/2*c^4 + 416*c^2 - 6, + -1/2*c^27 + 27/2*c^25 - 323/2*c^23 + 1127*c^21 - 10165/2*c^19 + 31009/2*c^17 - 65093/2*c^15 + 46911*c^13 - 91091/2*c^11 + 57355/2*c^9 - 10994*c^7 + 4621/2*c^5 - 439/2*c^3 + 6*c)]) - Some extra debugging:: - - sage: from flatsurf.geometry.polygon import wedge - sage: K. = NumberField(x^3 - 2, embedding=AA(2)**(1/3)) - sage: ux = 1 + a + a**2 - sage: uy = -2/3 + a - sage: vx = 1/5 - a**2 - sage: vy = a + 7/13*a**2 - sage: p = polygons((ux, uy), (vx,vy), (-ux-vx,-uy-vy), ring=K) - sage: Jxx, Jyy, Jxy = p.j_invariant() - sage: wedge(ux.vector(), vx.vector()) == Jxx - True - sage: wedge(uy.vector(), vy.vector()) == Jyy - True + sage: polygons.triangle(2, 13, 26) # long time (3s) + Polygon(vertices=[(0, 0), + (1, 0), + (1/2*c^30 - 15*c^28 + 405/2*c^26 - 1625*c^24 + 8625*c^22 - 31878*c^20 + 168245/2*c^18 - 159885*c^16 + 218025*c^14 - 209950*c^12 + 138567*c^10 - 59670*c^8 + 15470*c^6 - 2100*c^4 + 225/2*c^2 - 1/2, + -1/2*c^39 + 19*c^37 - 333*c^35 + 3571*c^33 - 26212*c^31 + 139593*c^29 - 557844*c^27 + 1706678*c^25 - 8085237/2*c^23 + 7449332*c^21 - + 10671265*c^19 + 11812681*c^17 - 9983946*c^15 + 6317339*c^13 - 5805345/2*c^11 + 1848183/2*c^9 - 378929/2*c^7 + 44543/2*c^5 - 2487/2*c^3 + 43/2*c)]) """ - if self.base_ring() is QQ: - raise NotImplementedError - - K = self.base_ring() - try: - V, from_V, to_V = K.vector_space() - except (AttributeError, ValueError): - raise ValueError("the surface needs to be define over a number field") - - dim = K.degree() - Jxx = Jyy = free_module_element(K, dim * (dim - 1) // 2) - Jxy = matrix(K, dim) - vertices = list(self.vertices()) - vertices.append(vertices[0]) - for i in range(len(vertices) - 1): - a = to_V(vertices[i][0]) - b = to_V(vertices[i][1]) - c = to_V(vertices[i + 1][0]) - d = to_V(vertices[i + 1][1]) - Jxx += wedge(a, c) - Jyy += wedge(b, d) - Jxy += tensor(a, d) - Jxy -= tensor(c, b) - - return (Jxx, Jyy, Jxy) - - def is_isometric(self, other, certificate=False): - r""" - Return whether ``self`` and ``other`` are isometric convex polygons via an orientation - preserving isometry. + return Polygon(angles=[a, b, c], check=False) - If ``certificate`` is set to ``True`` return also a pair ``(index, rotation)`` - of an integer ``index`` and a matrix ``rotation`` such that the given rotation - matrix identifies this polygon with the other and the edges 0 in this polygon - is mapped to the edge ``index`` in the other. - - .. TODO:: + @staticmethod + def regular_ngon(n, field=None): + r""" + Return a regular n-gon with unit length edges, first edge horizontal, and other vertices lying above this edge. - Implement ``is_linearly_equivalent`` and ``is_similar``. + Assuming field is None (by default) the polygon is defined over a NumberField (the minimal number field determined by n). + Otherwise you can set field equal to AA to define the polygon over the Algebraic Reals. Other values for the field + parameter will result in a ValueError. EXAMPLES:: - sage: from flatsurf import polygons - sage: S = polygons.square() - sage: S.is_isometric(S) - True - sage: U = matrix(2,[0,-1,1,0]) * S - sage: U.is_isometric(S) - True + sage: from flatsurf.geometry.polygon import polygons - sage: x = polygen(QQ) - sage: K. = NumberField(x^2 - 2, embedding=AA(2)**(1/2)) - sage: S = S.change_ring(K) - sage: U = matrix(2, [sqrt2/2, -sqrt2/2, sqrt2/2, sqrt2/2]) * S - sage: U.is_isometric(S) - True + sage: p = polygons.regular_ngon(17) + sage: p + Polygon(vertices=[(0, 0), (1, 0), ..., (-1/2*a^14 + 15/2*a^12 - 45*a^10 + 275/2*a^8 - 225*a^6 + 189*a^4 - 70*a^2 + 15/2, 1/2*a)]) - sage: U2 = polygons((1,0), (sqrt2/2, sqrt2/2), (-1,0), (-sqrt2/2, -sqrt2/2)) - sage: U2.is_isometric(U) - False - sage: U2.is_isometric(U, certificate=True) - (False, None) - - sage: S = polygons((1,0), (sqrt2/2, 3), (-2,3), (-sqrt2/2+1, -6)) - sage: T = polygons((sqrt2/2,3), (-2,3), (-sqrt2/2+1, -6), (1,0)) - sage: isometric, cert = S.is_isometric(T, certificate=True) - sage: assert isometric - sage: shift, rot = cert - sage: polygons(edges=[rot * S.edge((k + shift) % 4) for k in range(4)], base_point=T.vertex(0)) == T - True + sage: polygons.regular_ngon(3,field=AA) + Polygon(vertices=[(0, 0), (1, 0), (1/2, 0.866025403784439?)]) + """ + # The code below crashes for n=4! + if n == 4: + return polygons.square(QQ(1), base_ring=field) + from sage.rings.qqbar import QQbar - sage: T = (matrix(2, [sqrt2/2, -sqrt2/2, sqrt2/2, sqrt2/2]) * S).translate((3,2)) - sage: isometric, cert = S.is_isometric(T, certificate=True) - sage: assert isometric - sage: shift, rot = cert - sage: polygons(edges=[rot * S.edge(k + shift) for k in range(4)], base_point=T.vertex(0)) == T - True - """ - if type(self) is not type(other): - raise TypeError + c = QQbar.zeta(n).real() + s = QQbar.zeta(n).imag() - n = self.num_edges() - if other.num_edges() != n: - return False - sedges = self.edges() - oedges = other.edges() - - slengths = [x**2 + y**2 for x, y in sedges] - olengths = [x**2 + y**2 for x, y in oedges] - for i in range(n): - if slengths == olengths: - # we have a match of lengths after a shift by i - xs, ys = sedges[0] - xo, yo = oedges[0] - ms = matrix(2, [xs, -ys, ys, xs]) - mo = matrix(2, [xo, -yo, yo, xo]) - rot = mo * ~ms - assert rot.det() == 1 and (rot * rot.transpose()).is_one() - assert oedges[0] == rot * sedges[0] - if all(oedges[i] == rot * sedges[i] for i in range(1, n)): - return ( - (True, (0 if i == 0 else n - i, rot)) if certificate else True - ) - olengths.append(olengths.pop(0)) - oedges.append(oedges.pop(0)) - return (False, None) if certificate else False + if field is None: + field, (c, s) = number_field_elements_from_algebraics((c, s)) + cn = field.one() + sn = field.zero() + edges = [(cn, sn)] + for _ in range(n - 1): + cn, sn = c * cn - s * sn, c * sn + s * cn + edges.append((cn, sn)) - def is_translate(self, other, certificate=False): + ngon = Polygon(base_ring=field, edges=edges) + ngon._refine_category_(ngon.category().WithAngles([1] * n)) + return ngon + + @staticmethod + def right_triangle(angle, leg0=None, leg1=None, hypotenuse=None): r""" - Return whether ``other`` is a translate of ``self``. + Return a right triangle in a number field with an angle of pi*angle. + + You can specify the length of the first leg (``leg0``), the second leg (``leg1``), + or the ``hypotenuse``. EXAMPLES:: sage: from flatsurf import polygons - sage: S = polygons(vertices=[(0,0), (3,0), (1,1)]) - sage: T1 = S.translate((2,3)) - sage: S.is_translate(T1) - True - sage: T2 = polygons(vertices=[(-1,1), (1,0), (2,1)]) - sage: S.is_translate(T2) - False - sage: T3 = polygons(vertices=[(0,0), (3,0), (2,1)]) - sage: S.is_translate(T3) - False - sage: S.is_translate(T1, certificate=True) - (True, (0, 1)) - sage: S.is_translate(T2, certificate=True) - (False, None) - sage: S.is_translate(T3, certificate=True) - (False, None) + sage: P = polygons.right_triangle(1/3, 1) + sage: P + Polygon(vertices=[(0, 0), (1, 0), (1, a)]) + sage: P.base_ring() + Number Field in a with defining polynomial y^2 - 3 with a = 1.732050807568878? + + sage: polygons.right_triangle(1/4,1) + Polygon(vertices=[(0, 0), (1, 0), (1, 1)]) + sage: polygons.right_triangle(1/4,1).base_ring() + Rational Field """ - if type(self) is not type(other): - raise TypeError + from sage.rings.qqbar import QQbar - n = self.num_edges() - if other.num_edges() != n: - return False - sedges = self.edges() - oedges = other.edges() - for i in range(n): - if sedges == oedges: - return (True, (i, 1)) if certificate else True - oedges.append(oedges.pop(0)) - return (False, None) if certificate else False - - def is_half_translate(self, other, certificate=False): - r""" - Return whether ``other`` is a translate or half-translate of ``self``. + angle = QQ(angle) + if angle <= 0 or angle > QQ((1, 2)): + raise ValueError("angle must be in ]0,1/2]") - If ``certificate`` is set to ``True`` then return also a pair ``(orientation, index)``. + z = QQbar.zeta(2 * angle.denom()) ** angle.numerator() + c = z.real() + s = z.imag() - EXAMPLES:: + nargs = (leg0 is not None) + (leg1 is not None) + (hypotenuse is not None) - sage: from flatsurf import polygons - sage: S = polygons(vertices=[(0,0), (3,0), (1,1)]) - sage: T1 = S.translate((2,3)) - sage: S.is_half_translate(T1) - True - sage: T2 = polygons(vertices=[(-1,1), (1,0), (2,1)]) - sage: S.is_half_translate(T2) - True - sage: T3 = polygons(vertices=[(0,0), (3,0), (2,1)]) - sage: S.is_half_translate(T3) - False + if nargs == 0: + leg0 = 1 + elif nargs > 1: + raise ValueError("only one length can be specified") - sage: S.is_half_translate(T1, certificate=True) - (True, (0, 1)) - sage: half_translate, cert = S.is_half_translate(T2, certificate=True) - sage: assert half_translate - sage: shift, rot = cert - sage: polygons(edges=[rot * S.edge(k + shift) for k in range(3)], base_point=T2.vertex(0)) == T2 - True - sage: S.is_half_translate(T3, certificate=True) - (False, None) - """ - if type(self) is not type(other): - raise TypeError + if leg0 is not None: + c, s = leg0 * c / c, leg0 * s / c + elif leg1 is not None: + c, s = leg1 * c / s, leg1 * s / s + elif hypotenuse is not None: + c, s = hypotenuse * c, hypotenuse * s - n = self.num_edges() - if other.num_edges() != n: - return False + field, (c, s) = number_field_elements_from_algebraics((c, s)) - sedges = self.edges() - oedges = other.edges() - for i in range(n): - if sedges == oedges: - return (True, (i, 1)) if certificate else True - oedges.append(oedges.pop(0)) + return Polygon( + base_ring=field, edges=[(c, field.zero()), (field.zero(), s), (-c, -s)] + ) - assert oedges == other.edges() - oedges = [-e for e in oedges] - for i in range(n): - if sedges == oedges: - return (True, (0 if i == 0 else n - i, -1)) if certificate else True - oedges.append(oedges.pop(0)) + def __call__(self, *args, **kwargs): + r""" + EXAMPLES:: - return (False, None) if certificate else False + sage: from flatsurf import polygons + sage: polygons((1,0),(0,1),(-1,0),(0,-1)) + doctest:warning + ... + UserWarning: calling Polygon() with positional arguments has been deprecated and will not be supported in a future version of sage-flatsurf; use edges= or vertices= explicitly instead + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: polygons((1,0),(0,1),(-1,0),(0,-1), ring=QQbar) + doctest:warning + ... + UserWarning: ring has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use base_ring instead + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: _.category() + Category of convex simple euclidean polygons over Algebraic Field -class ConvexPolygon(Polygon): - r""" - A convex polygon in the plane RR^2 - """ + sage: polygons(vertices=[(0,0), (1,0), (0,1)]) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) - def __init__(self, parent, vertices, check=True): - r""" - To construct the polygon you should either use a list of edge vectors - or a list of vertices. Using both will result in a ValueError. The polygon - needs to be convex with postively oriented boundary. + sage: polygons(edges=[(2,0),(-1,1),(-1,-1)], base_point=(3,3)) + doctest:warning + ... + UserWarning: base_point has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use .translate() on the resulting polygon instead + Polygon(vertices=[(3, 3), (5, 3), (4, 4)]) + sage: polygons(vertices=[(0,0),(2,0),(1,1)], base_point=(3,3)) + Polygon(vertices=[(3, 3), (5, 3), (4, 4)]) - INPUT: + sage: polygons(angles=[1,1,1,2], length=1) + doctest:warning + ... + UserWarning: length has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use lengths instead + Polygon(vertices=[(0, 0), (1, 0), (-1/2*c^2 + 5/2, 1/2*c), (-1/2*c^2 + 2, 1/2*c^3 - 3/2*c)]) + sage: polygons(angles=[1,1,1,2], length=2) + Polygon(vertices=[(0, 0), (2, 0), (-c^2 + 5, c), (-c^2 + 4, c^3 - 3*c)]) + sage: polygons(angles=[1,1,1,2], length=AA(2)**(1/2)) # tol 1e-9 + Polygon(vertices=[(0, 0), (1.414213562373095?, 0), (0.9771975379242739?, 1.344997023927915?), (0.270090756737727?, 0.831253875554907?)]) - - ``parent`` -- a parent + sage: polygons(angles=[1]*5).angles() + (3/10, 3/10, 3/10, 3/10, 3/10) + sage: polygons(angles=[1]*8).angles() + (3/8, 3/8, 3/8, 3/8, 3/8, 3/8, 3/8, 3/8) - - ``vertices`` -- a list of vertices of the polygon - """ - Polygon.__init__(self, parent, vertices, check=False) - if check: - self._convexity_check() + sage: P = polygons(angles=[1,1,3,3], lengths=[3,1]) + sage: P.angles() + (1/8, 1/8, 3/8, 3/8) + sage: e0 = P.edge(0); assert e0[0]**2 + e0[1]**2 == 3**2 + sage: e1 = P.edge(1); assert e1[0]**2 + e1[1]**2 == 1 - def is_convex(self): - return True + sage: polygons(angles=[1, 1, 1, 2]) + Polygon(vertices=[(0, 0), (1/10*c^3 + c^2 - 1/5*c - 3, 0), (1/20*c^3 + 1/2*c^2 - 1/20*c - 3/2, 1/20*c^2 + 1/2*c), (1/2*c^2 - 3/2, 1/2*c)]) + + sage: polygons(angles=[1,1,1,8]) + Polygon(vertices=[(0, 0), (c^6 - 6*c^4 + 8*c^2 + 3, 0), (1/2*c^4 - 3*c^2 + 9/2, 1/2*c^9 - 9/2*c^7 + 13*c^5 - 11*c^3 - 3*c), (1/2*c^6 - 7/2*c^4 + 7*c^2 - 3, 1/2*c^9 - 5*c^7 + 35/2*c^5 - 49/2*c^3 + 21/2*c)]) + sage: polygons(angles=[1,1,1,8], lengths=[1, 1]) + Polygon(vertices=[(0, 0), (1, 0), (-1/2*c^4 + 2*c^2, 1/2*c^7 - 7/2*c^5 + 7*c^3 - 7/2*c), (1/2*c^6 - 7/2*c^4 + 13/2*c^2 - 3/2, 1/2*c^9 - 9/2*c^7 + 27/2*c^5 - 29/2*c^3 + 5/2*c)]) - def _convexity_check(self): - r""" TESTS:: - sage: from flatsurf import * - sage: polygons(vertices=[(0,0),(1,0)]) - Traceback (most recent call last): - ... - ValueError: a polygon should have more than two edges! - sage: polygons(vertices=[(0,0),(1,2),(0,1),(-1,2)]) - Traceback (most recent call last): - ... - ValueError: not convex - sage: polygons(vertices=[(0,0),(1,0),(2,0)]) - Traceback (most recent call last): - ... - ValueError: degenerate polygon + sage: from itertools import product + sage: for a,b,c in product(range(1,5), repeat=3): # long time (1.5s) + ....: if gcd([a,b,c]) != 1: + ....: continue + ....: T = polygons(angles=[a,b,c]) + ....: D = 2*(a+b+c) + ....: assert T.angles() == (a/D, b/D, c/D) + """ - if self.num_edges() <= 2: - raise ValueError("a polygon should have more than two edges!") + import warnings - if not sum(self.edges()).is_zero(): - raise ValueError("the sum over the edges do not sum up to 0") + warnings.warn( + "calling polygons() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead" + ) + return Polygon(*args, **kwargs) - for i in range(self.num_edges()): - if self.edge(i).is_zero(): - raise ValueError("zero edge") - if wedge_product(self.edge(i), self.edge(i + 1)) < 0: - raise ValueError("not convex") - if is_opposite_direction(self.edge(i), self.edge(i + 1)): - raise ValueError("degenerate polygon") - def find_separatrix(self, direction=None, start_vertex=0): - r""" - Returns a pair (v,same) where v is a vertex and same is a boolean. - The provided parameter "direction" should be a non-zero vector with - two entries, or by default direction=(0,1). +polygons = PolygonsConstructor() - A separatrix is a ray leaving a vertex and entering the polygon. - The vertex v will have a separatrix leaving it which is parallel to - direction. The returned value "same" answers the question if this separatrix - points in the same direction as "direction". There is a boundary case: - we allow the separatrix to be an edge if and only if traveling along - the sepatrix from the vertex would travel in a counter-clockwise - direction about the polygon. +def ConvexPolygons(base_ring): + r""" + EXAMPLES:: - The vertex returned is uniquely defined from the above if the polygon - is a triangle. Otherwise, we return the first vertex with this property - obtained by inspecting starting at start_vertex (defaults to 0) and - then moving in the counter-clockwise direction. + sage: from flatsurf import ConvexPolygons + sage: P = ConvexPolygons(QQ) + doctest:warning + ... + UserWarning: ConvexPolygons() has been deprecated and will be removed from a future version of sage-flatsurf; use Polygon() to create polygons. + If you really need the category of convex polygons over a ring use EuclideanPolygons(ring).Simple().Convex() instead. + sage: P(vertices=[(0, 0), (1, 0), (0, 1)]) + doctest:warning + ... + UserWarning: ConvexPolygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) - EXAMPLES:: + """ + import warnings - sage: from flatsurf import polygons - sage: p=polygons.square() - sage: print(p.find_separatrix()) - (1, True) - sage: print(p.find_separatrix(start_vertex=2)) - (3, False) - """ - if direction is None: - direction = self.module()((self.base_ring().zero(), self.base_ring().one())) - else: - assert not direction.is_zero() - v = start_vertex - n = self.num_edges() - zero = self.base_ring().zero() - for i in range(self.num_edges()): - if ( - wedge_product(self.edge(v), direction) >= zero - and wedge_product(self.edge(v + n - 1), direction) > zero - ): - return v, True - if ( - wedge_product(self.edge(v), direction) <= zero - and wedge_product(self.edge(v + n - 1), direction) < zero - ): - return v, False - v = v + 1 % n - raise RuntimeError("Failed to find a separatrix") - - def contains_point(self, point, translation=None): - r""" - Return true if the point is within the polygon (after the polygon is possibly translated) - """ - return self.get_point_position(point, translation=translation).is_inside() + warnings.warn( + "ConvexPolygons() has been deprecated and will be removed from a future version of sage-flatsurf; use Polygon() to create polygons. " + "If you really need the category of convex polygons over a ring use EuclideanPolygons(ring).Simple().Convex() instead." + ) + return EuclideanPolygons(base_ring).Simple().Convex() + + +def Polygon( + *args, + vertices=None, + edges=None, + angles=None, + lengths=None, + base_ring=None, + category=None, + check=True, + **kwds, +): + r""" + Return a polygon from the given ``vertices``, ``edges``, or ``angles``. - def get_point_position(self, point, translation=None): - r""" - Get a combinatorial position of a points position compared to the polygon + INPUT: - INPUT: + - ``vertices`` -- a sequence of vertices or ``None`` (default: ``None``); the + vertices of the polygon as points in the real plane - - ``point`` -- a point in the plane (vector over the underlying base_ring()) + - ``edges`` -- a sequence of vectors or ``None`` (default: ``None``); the + vectors connecting the vertices of the polygon - - ``translation`` -- optional translation to applied to the polygon (vector over the underlying base_ring()) + - ``angles`` -- a sequence of numbers that prescribe the inner angles of + the polygon or ``None`` (default: ``None``); the angles are rescaled so + that their sum matches the sum of the angles in an ngon. - OUTPUT: + - ``lengths`` -- a sequence of numbers that prescribe the lengths of the + edges of the polygon or ``None`` (default: ``None``) - - a PolygonPosition object + - ``base_ring`` -- a ring or ``None`` (default: ``None``); the ring over + which the polygon will be defined - EXAMPLES:: + - ``category`` -- a category or ``None`` (default: ``None``); the category + the polygon will be in (further refined from the features of the polygon + that are found during the construction.) - sage: from flatsurf.geometry.polygon import polygons - sage: s = polygons.square() - sage: V = s.parent().vector_space() - sage: s.get_point_position(V((1/2,1/2))) - point positioned in interior of polygon - sage: s.get_point_position(V((1,0))) - point positioned on vertex 1 of polygon - sage: s.get_point_position(V((1,1/2))) - point positioned on interior of edge 1 of polygon - sage: s.get_point_position(V((1,3/2))) - point positioned outside polygon - - sage: p=polygons(edges=[(1,0),(1,0),(1,0),(0,1),(-3,0),(0,-1)]) - sage: V=p.vector_space() - sage: p.get_point_position(V([10,0])) - point positioned outside polygon - sage: p.get_point_position(V([1/2,0])) - point positioned on interior of edge 0 of polygon - sage: p.get_point_position(V([3/2,0])) - point positioned on interior of edge 1 of polygon - sage: p.get_point_position(V([2,0])) - point positioned on vertex 2 of polygon - sage: p.get_point_position(V([5/2,0])) - point positioned on interior of edge 2 of polygon - sage: p.get_point_position(V([5/2,1/4])) - point positioned in interior of polygon - """ - V = self.vector_space() - if translation is None: - # Since we allow the initial vertex to be non-zero, this changed: - v1 = self.vertex(0) - else: - # Since we allow the initial vertex to be non-zero, this changed: - v1 = translation + self.vertex(0) - # Below, we only make use of edge vectors: - for i in range(self.num_edges()): - v0 = v1 - e = self.edge(i) - v1 = v0 + e - w = wedge_product(e, point - v0) - if w < 0: - return PolygonPosition(PolygonPosition.OUTSIDE) - if w == 0: - # Lies on the line through edge i! - dp1 = dot_product(e, point - v0) - if dp1 == 0: - return PolygonPosition(PolygonPosition.VERTEX, vertex=i) - dp2 = dot_product(e, e) - if 0 < dp1 and dp1 < dp2: - return PolygonPosition(PolygonPosition.EDGE_INTERIOR, edge=i) - # Loop terminated (on inside of each edge) - return PolygonPosition(PolygonPosition.INTERIOR) - - def flow_to_exit(self, point, direction): - r""" - Flow a point in the direction of holonomy until the point leaves the - polygon. Note that ValueErrors may be thrown if the point is not in the - polygon, or if it is on the boundary and the holonomy does not point - into the polygon. + - ``check`` -- a boolean (default: ``True``); whether to check the + consistency of the parameters or blindly trust them. Setting this to + ``False`` allows creation of degenerate polygons in some cases. While + they might be somewhat functional, no guarantees are made about such + polygons. - INPUT: + EXAMPLES: - - ``point`` -- a point in the closure of the polygon (as a vector) + A right triangle:: - - ``holonomy`` -- direction of motion (a vector of non-zero length) + sage: from flatsurf import Polygon + sage: Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) - OUTPUT: + A right triangle that is not based at the origin:: - - The point in the boundary of the polygon where the trajectory exits + sage: Polygon(vertices=[(1, 0), (2, 0), (1, 1)]) + Polygon(vertices=[(1, 0), (2, 0), (1, 1)]) - - a PolygonPosition object representing the combinatorial position of the stopping point - """ - V = self.parent().vector_space() - if direction == V.zero(): - raise ValueError("Zero vector provided as direction.") - v0 = self.vertex(0) - w = direction - for i in range(self.num_edges()): - e = self.edge(i) - m = matrix([[e[0], -direction[0]], [e[1], -direction[1]]]) - try: - ret = m.inverse() * (point - v0) - s = ret[0] - t = ret[1] - # What if the matrix is non-invertible? - - # Answer: You'll get a ZeroDivisionError which means that the edge is parallel - # to the direction. - - # s is location it intersects on edge, t is the portion of the direction to reach this intersection - if t > 0 and 0 <= s and s <= 1: - # The ray passes through edge i. - if s == 1: - # exits through vertex i+1 - v0 = v0 + e - return v0, PolygonPosition( - PolygonPosition.VERTEX, vertex=(i + 1) % self.num_edges() - ) - if s == 0: - # exits through vertex i - return v0, PolygonPosition(PolygonPosition.VERTEX, vertex=i) - # exits through vertex i - # exits through interior of edge i - prod = t * direction - return point + prod, PolygonPosition( - PolygonPosition.EDGE_INTERIOR, edge=i - ) - except ZeroDivisionError: - # Here we know the edge and the direction are parallel - if wedge_product(e, point - v0) == 0: - # In this case point lies on the edge. - # We need to work out which direction to move in. - if (point - v0).is_zero() or is_same_direction(e, point - v0): - # exits through vertex i+1 - return self.vertex(i + 1), PolygonPosition( - PolygonPosition.VERTEX, vertex=(i + 1) % self.num_edges() - ) - else: - # exits through vertex i - return v0, PolygonPosition(PolygonPosition.VERTEX, vertex=i) - pass - v0 = v0 + e - # Our loop has terminated. This can mean one of several errors... - pos = self.get_point_position(point) - if pos.is_outside(): - raise ValueError("Started with point outside polygon") - raise ValueError( - "Point on boundary of polygon and direction not pointed into the polygon." - ) + A right triangle at the origin, specified by giving the edge vectors:: - def flow_map(self, direction): - r""" - Return a polygonal map associated to the flow in ``direction`` in this - polygon. + sage: Polygon(edges=[(1, 0), (-1, 1), (0, -1)]) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) - EXAMPLES:: + When redundant information is given, it is checked for consistency:: - sage: from flatsurf.geometry.polygon import polygons - sage: S = polygons(vertices=[(0,0),(2,0),(2,2),(1,2),(0,2),(0,1)]) - sage: S.flow_map((0,1)) - Flow polygon map: - 3 2 - 0 - top lengths: [1, 1] - bot lengths: [2] - sage: S.flow_map((1,1)) - Flow polygon map: - 3 2 1 - 4 5 0 - top lengths: [1, 1, 2] - bot lengths: [1, 1, 2] - sage: S.flow_map((-1,-1)) - Flow polygon map: - 0 5 4 - 1 2 3 - top lengths: [2, 1, 1] - bot lengths: [2, 1, 1] - - sage: K. = NumberField(x^2 - 2, embedding=AA(2).sqrt()) - sage: S.flow_map((sqrt2,1)) - Flow polygon map: - 3 2 1 - 4 5 0 - top lengths: [1, 1, 2*sqrt2] - bot lengths: [sqrt2, sqrt2, 2] - """ - direction = vector(direction) - DP = direction.parent() - P = self.vector_space() - if DP != P: - P = cm.common_parent(DP, P) - ring = P.base_ring() - direction = direction.change_ring(ring) - else: - ring = P.base_ring() - - # first compute the transversal length of each edge - t = P([direction[1], -direction[0]]) - lengths = [t.dot_product(e) for e in self.edges()] - n = len(lengths) - for i in range(n): - j = (i + 1) % len(lengths) - l0 = lengths[i] - l1 = lengths[j] - if l0 >= 0 and l1 < 0: - rt = j - if l0 > 0 and l1 <= 0: - rb = j - if l0 <= 0 and l1 > 0: - lb = j - if l0 < 0 and l1 >= 0: - lt = j - - if rt < lt: - top_lengths = lengths[rt:lt] - top_labels = list(range(rt, lt)) - else: - top_lengths = lengths[rt:] + lengths[:lt] - top_labels = list(range(rt, n)) + list(range(lt)) - top_lengths = [-x for x in reversed(top_lengths)] - top_labels.reverse() - - if lb < rb: - bot_lengths = lengths[lb:rb] - bot_labels = list(range(lb, rb)) - else: - bot_lengths = lengths[lb:] + lengths[:rb] - bot_labels = list(range(lb, n)) + list(range(rb)) + sage: Polygon(vertices=[(0, 0), (1, 0), (0, 1)], edges=[(1, 0), (-1, 1), (0, -1)]) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) + sage: Polygon(vertices=[(1, 0), (2, 0), (1, 1)], edges=[(1, 0), (-1, 1), (0, -1)]) + Polygon(vertices=[(1, 0), (2, 0), (1, 1)]) + sage: Polygon(vertices=[(0, 0), (2, 0), (1, 1)], edges=[(1, 0), (-1, 1), (0, -1)]) + Traceback (most recent call last): + ... + ValueError: vertices and edges are not compatible - from .interval_exchange_transformation import FlowPolygonMap + Polygons given by edges must be closed (in particular we do not add an edge + automatically to close things up since this is often not what the user + wanted):: - return FlowPolygonMap(ring, bot_labels, bot_lengths, top_labels, top_lengths) + sage: Polygon(edges=[(1, 0), (0, 1), (1, 1)]) + Traceback (most recent call last): + ... + ValueError: polygon not closed - def flow(self, point, holonomy, translation=None): - r""" - Flow a point in the direction of holonomy for the length of the - holonomy, or until the point leaves the polygon. Note that ValueErrors - may be thrown if the point is not in the polygon, or if it is on the - boundary and the holonomy does not point into the polygon. + A polygon with prescribed angles:: - INPUT: + sage: Polygon(angles=[2, 1, 1]) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) - - ``point`` -- a point in the closure of the polygon (vector over the underlying base_ring()) + Again, if vertices and edges are also specified, they must be compatible + with the angles:: - - ``holonomy`` -- direction and magnitude of motion (vector over the underlying base_ring()) + sage: Polygon(angles=[2, 1, 1], vertices=[(0, 0), (1, 0), (0, 1)], edges=[(1, 0), (-1, 1), (0, -1)]) + Polygon(vertices=[(0, 0), (1, 0), (0, 1)]) - - ``translation`` -- optional translation to applied to the polygon (vector over the underlying base_ring()) + sage: Polygon(angles=[1, 2, 3], vertices=[(0, 0), (1, 0), (0, 1)], edges=[(1, 0), (-1, 1), (0, -1)]) + Traceback (most recent call last): + ... + ValueError: polygon does not have the prescribed angles - OUTPUT: + When angles are specified, side lengths can also be prescribed:: - - The point within the polygon where the motion stops (or leaves the polygon) + sage: Polygon(angles=[1, 1, 1], lengths=[1, 1, 1]) + Polygon(vertices=[(0, 0), (1, 0), (1/2, 1/2*c)]) - - The amount of holonomy left to flow + The function will deduce lengths if one or two are missing:: - - a PolygonPosition object representing the combinatorial position of the stopping point + sage: Polygon(angles=[1, 1, 1, 1], lengths=[1, 1, 1]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - EXAMPLES:: + sage: Polygon(angles=[1, 1, 1, 1], lengths=[1, 1]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - sage: from flatsurf.geometry.polygon import polygons - sage: s = polygons.square() - sage: V = s.parent().vector_space() - sage: p = V((1/2,1/2)) - sage: w = V((2,0)) - sage: s.flow(p,w) - ((1, 1/2), (3/2, 0), point positioned on interior of edge 1 of polygon) - """ - V = self.parent().vector_space() - if holonomy == V.zero(): - # not flowing at all! - return ( - point, - V.zero(), - self.get_point_position(point, translation=translation), - ) - if translation is None: - v0 = self.vertex(0) - else: - v0 = self.vertex(0) + translation - w = holonomy - for i in range(self.num_edges()): - e = self.edge(i) - m = matrix([[e[0], -holonomy[0]], [e[1], -holonomy[1]]]) - try: - ret = m.inverse() * (point - v0) - s = ret[0] - t = ret[1] - # What if the matrix is non-invertible? - - # s is location it intersects on edge, t is the portion of the holonomy to reach this intersection - if t > 0 and 0 <= s and s <= 1: - # The ray passes through edge i. - if t > 1: - # the segment from point with the given holonomy stays within the polygon - return ( - point + holonomy, - V.zero(), - PolygonPosition(PolygonPosition.INTERIOR), - ) - if s == 1: - # exits through vertex i+1 - v0 = v0 + e - return ( - v0, - point + holonomy - v0, - PolygonPosition( - PolygonPosition.VERTEX, - vertex=(i + 1) % self.num_edges(), - ), - ) - if s == 0: - # exits through vertex i - return ( - v0, - point + holonomy - v0, - PolygonPosition(PolygonPosition.VERTEX, vertex=i), - ) - # exits through vertex i - # exits through interior of edge i - prod = t * holonomy - return ( - point + prod, - holonomy - prod, - PolygonPosition(PolygonPosition.EDGE_INTERIOR, edge=i), - ) - except ZeroDivisionError: - # can safely ignore this error. It means that the edge and the holonomy are parallel. - pass - v0 = v0 + e - # Our loop has terminated. This can mean one of several errors... - pos = self.get_point_position(point, translation=translation) - if pos.is_outside(): - raise ValueError("Started with point outside polygon") - raise ValueError( - "Point on boundary of polygon and holonomy not pointed into the polygon." - ) + sage: Polygon(angles=[1, 1, 1, 1], lengths=[1]) + Traceback (most recent call last): + ... + NotImplementedError: cannot construct a quadrilateral from 4 angles and 2 vertices - def circumscribing_circle(self): - r""" - Returns the circle which circumscribes this polygon. - Raises a ValueError if the polygon is not circumscribed by a circle. + Equally, we deduce vertices or edges:: - EXAMPLES:: + sage: Polygon(angles=[1, 1, 1, 1], vertices=[(0, 0), (1, 0), (1, 1)]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - sage: from flatsurf import polygons - sage: P = polygons(vertices=[(0,0),(1,0),(2,1),(-1,1)]) - sage: P.circumscribing_circle() - Circle((1/2, 3/2), 5/2) - """ - from .circle import circle_from_three_points + sage: Polygon(angles=[1, 1, 1, 1], edges=[(1, 0), (0, 1)]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - circle = circle_from_three_points( - self.vertex(0), self.vertex(1), self.vertex(2), self.base_ring() - ) - for i in range(3, self.num_edges()): - if not circle.point_position(self.vertex(i)) == 0: - raise ValueError("Vertex " + str(i) + " is not on the circle.") - return circle + When the angles are incompatible with the data, an error is reported (that + might be somewhat cryptic at times):: - def subdivide(self): - r""" - Return a list of triangles that partition this polygon. + sage: Polygon(angles=[1, 1, 1, 1], edges=[(1, 0), (0, 1), (1, 2)]) + Traceback (most recent call last): + ... + NotImplementedError: cannot recover a rational angle from these numerical results - For each edge of the polygon one triangle is created that joins this - edge to the :meth:`centroid ` of this polygon. + When lengths are given in addition to vertices or edges, they are checked for consistency:: - EXAMPLES:: + sage: Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)], lengths=[1, 1, 1, 1]) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - sage: from flatsurf import polygons - sage: P = polygons.regular_ngon(3); P - Polygon: (0, 0), (1, 0), (1/2, 1/2*a) - sage: P.subdivide() - [Polygon: (0, 0), (1, 0), (1/2, 1/6*a), - Polygon: (1, 0), (1/2, 1/2*a), (1/2, 1/6*a), - Polygon: (1/2, 1/2*a), (0, 0), (1/2, 1/6*a)] - - :: - - sage: P = polygons.regular_ngon(4) - sage: P.subdivide() - [Polygon: (0, 0), (1, 0), (1/2, 1/2), - Polygon: (1, 0), (1, 1), (1/2, 1/2), - Polygon: (1, 1), (0, 1), (1/2, 1/2), - Polygon: (0, 1), (0, 0), (1/2, 1/2)] - - Sometimes alternating with :meth:`subdivide_edges` can produce a more - uniform subdivision:: - - sage: P = polygons.regular_ngon(4) - sage: P.subdivide_edges(2).subdivide() - [Polygon: (0, 0), (1/2, 0), (1/2, 1/2), - Polygon: (1/2, 0), (1, 0), (1/2, 1/2), - Polygon: (1, 0), (1, 1/2), (1/2, 1/2), - Polygon: (1, 1/2), (1, 1), (1/2, 1/2), - Polygon: (1, 1), (1/2, 1), (1/2, 1/2), - Polygon: (1/2, 1), (0, 1), (1/2, 1/2), - Polygon: (0, 1), (0, 1/2), (1/2, 1/2), - Polygon: (0, 1/2), (0, 0), (1/2, 1/2)] + sage: Polygon(vertices=[(0, 0), (1, 0), (0, 1)], lengths=[1, 1, 1]) + Traceback (most recent call last): + ... + ValueError: polygon does not have the prescribed lengths - """ - vertices = self.vertices() - center = self.centroid() - return [ - self.parent()( - vertices=(vertices[i], vertices[(i + 1) % len(vertices)], center) - ) - for i in range(len(vertices)) - ] + Currently, we cannot create a polygon from just lengths:: - def subdivide_edges(self, parts=2): - r""" - Return a copy of this polygon whose edges have been split into - ``parts`` equal parts each. + sage: Polygon(lengths=[1, 1, 1]) + Traceback (most recent call last): + ... + NotImplementedError: one of vertices, edges, or angles must be set - INPUT: + Polygons do not have to be convex:: - - ``parts`` -- a positive integer (default: 2) + sage: p = Polygon(vertices=[(0, 0), (1, 1), (2, 0), (2, 4), (0, 4)]) + sage: p.describe_polygon() + ('a', 'non-convex pentagon', 'non-convex pentagons') - EXAMPLES:: + Polygons must be positively oriented:: - sage: from flatsurf import polygons - sage: P = polygons.regular_ngon(3); P - Polygon: (0, 0), (1, 0), (1/2, 1/2*a) - sage: P.subdivide_edges(1) == P - True - sage: P.subdivide_edges(2) - Polygon: (0, 0), (1/2, 0), (1, 0), (3/4, 1/4*a), (1/2, 1/2*a), (1/4, 1/4*a) - sage: P.subdivide_edges(3) - Polygon: (0, 0), (1/3, 0), (2/3, 0), (1, 0), (5/6, 1/6*a), (2/3, 1/3*a), (1/2, 1/2*a), (1/3, 1/3*a), (1/6, 1/6*a) + sage: Polygon(vertices=[(0, 0), (0, 1), (1, 0)]) + Traceback (most recent call last): + ... + ValueError: polygon has negative area; probably the vertices are not in counter-clockwise order - """ - if parts < 1: - raise ValueError("parts must be a positive integer") + Polygons must have at least three sides:: - steps = [e / parts for e in self.edges()] - return self.parent()(edges=[e for e in steps for p in range(parts)]) + sage: Polygon(vertices=[(0, 0), (1, 0)]) + Traceback (most recent call last): + ... + ValueError: polygon must have at least three sides + sage: Polygon(vertices=[(0, 0), (1, 0), (2, 0)]) + Traceback (most recent call last): + ... + ValueError: polygon has zero area -class Polygons(UniqueRepresentation, Parent): - Element = Polygon + Currently, polygons must not self-intersect:: - def __init__(self, ring): - Parent.__init__(self, category=Sets()) - if ring not in Rings(): - raise ValueError("'ring' must be a ring") - self._ring = ring - self.register_action(MatrixActionOnPolygons(self)) + sage: p = Polygon(vertices=[(0, 0), (2, 0), (0, 1), (1, -1), (2, 1)]) + Traceback (most recent call last): + ... + NotImplementedError: polygon self-intersects - def base_ring(self): - return self._ring + Currently, all angles must be less than 2π:: - def field(self): - r""" - Return the field over which this polygon is defined. + sage: p = Polygon(angles=[14, 1, 1, 1, 1]) + Traceback (most recent call last): + ... + NotImplementedError: each angle must be in (0, 2π) - EXAMPLES:: + """ + if "base_point" in kwds: + base_point = kwds.pop("base_point") + import warnings - sage: from flatsurf import polygons - sage: P = polygons(vertices=[(0,0),(1,0),(2,1),(-1,1)]) - sage: P.field() - Rational Field + warnings.warn( + "base_point has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use .translate() on the resulting polygon instead" + ) + return Polygon( + *args, + vertices=vertices, + edges=edges, + angles=angles, + lengths=lengths, + base_ring=base_ring, + category=category, + **kwds, + ).translate(base_point) + + if "ring" in kwds: + import warnings + + warnings.warn( + "ring has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use base_ring instead" + ) + base_ring = kwds.pop("ring") - """ - return self._ring.fraction_field() + if "field" in kwds: + import warnings - @cached_method - def module(self): - r""" - Return the free module of rank 2 in which these polygons embed. + warnings.warn( + "field has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use base_ring instead" + ) + base_ring = kwds.pop("field") - EXAMPLES:: + convex = None + if "convex" in kwds: + convex = kwds.pop("convex") + import warnings - sage: from flatsurf import Polygons - sage: C = Polygons(QQ) - sage: C.module() - Vector space of dimension 2 over Rational Field + if convex: + warnings.warn( + "convex has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; it has no effect other than checking the input for convexity so you may just drop it" + ) + else: + warnings.warn( + "convex has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; it has no effect anymore, polygons are always allowed to be non-convex" + ) - """ - return FreeModule(self.base_ring(), 2) + if args: + import warnings - @cached_method - def vector_space(self): - r""" - Return the vector space of dimension 2 in which these polygons embed. + warnings.warn( + "calling Polygon() with positional arguments has been deprecated and will not be supported in a future version of sage-flatsurf; use edges= or vertices= explicitly instead" + ) - EXAMPLES:: + edges = args - sage: from flatsurf import Polygons - sage: C = Polygons(QQ) - sage: C.vector_space() - Vector space of dimension 2 over Rational Field + if angles: + if "length" in kwds: + import warnings - """ - return VectorSpace(self.base_ring().fraction_field(), 2) + warnings.warn( + "length has been deprecated as a keyword argument to Polygon() and will be removed in a future version of sage-flatsurf; use lengths instead" + ) - def _repr_(self): - return "Polygons(%s)" % self.base_ring() - - def _element_constructor_(self, *args, **kwds): - r""" - TESTS:: - - sage: from flatsurf import Polygons, ConvexPolygons - - sage: C = Polygons(QQ) - sage: p = C(vertices=[(0,0),(1,0),(2,0),(1,1)]) - sage: p - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - sage: C(p) is p - True - sage: C((1,0), (0,1), (-1, 1)) - Traceback (most recent call last): - ... - ValueError: the polygon does not close up - - sage: D = ConvexPolygons(QQbar) - sage: D(p) - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - sage: D(vertices=p.vertices()) - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - sage: D(edges=p.edges()) - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - """ - check = kwds.pop("check", True) - - if len(args) == 1 and isinstance(args[0], Polygon): - vertices = map(self.vector_space(), args[0].vertices()) - args = () - - else: - vertices = kwds.pop("vertices", None) - edges = kwds.pop("edges", None) - base_point = kwds.pop("base_point", (0, 0)) - - if (vertices is None) and (edges is None): - if len(args) == 1: - edges = args[0] - elif args: - edges = args - else: - raise ValueError( - "exactly one of 'vertices' or 'edges' must be provided" - ) - if kwds: - raise ValueError("invalid keyword {!r}".format(next(iter(kwds)))) - - if edges is not None: - v = self.vector_space()(base_point) - vertices = [] - for e in map(self.vector_space(), edges): - vertices.append(v) - v += e - if v != vertices[0]: - raise ValueError("the polygon does not close up") - - return self.element_class(self, vertices, check) + lengths = [kwds.pop("length")] * (len(angles) - 2) + if kwds: + raise ValueError("keyword argument not supported by Polygon()") -class ConvexPolygons(Polygons): - r""" - The set of convex polygons with a fixed base field. - - EXAMPLES:: - - sage: from flatsurf import ConvexPolygons - sage: C = ConvexPolygons(QQ) - sage: C(vertices=[(0,0), (2,0), (1,1)]) - Polygon: (0, 0), (2, 0), (1, 1) - sage: C(edges=[(1,0), (0,1), (-1,0), (0,-1)]) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - - A set of polygons can also be created over non-fields:: - - sage: ConvexPolygons(ZZ) - ConvexPolygons(Integer Ring) - - TESTS:: + # Determine the number of sides of this polygon. + if angles: + n = len(angles) + elif edges: + n = len(edges) + elif vertices: + n = len(vertices) + else: + raise NotImplementedError("one of vertices, edges, or angles must be set") - sage: ConvexPolygons(QQ) is ConvexPolygons(QQ) - True - sage: TestSuite(ConvexPolygons(QQ)).run() - sage: TestSuite(ConvexPolygons(QQbar)).run() - sage: TestSuite(ConvexPolygons(ZZ)).run() - """ - Element = ConvexPolygon + if n < 3: + raise ValueError("polygon must have at least three sides") - def has_coerce_map_from(self, other): - r""" - TESTS:: + # Determine the base ring of the polygon + if base_ring is None: + base_ring = _Polygon_base_ring(vertices, edges, angles, lengths) - sage: from flatsurf import ConvexPolygons - sage: C1 = ConvexPolygons(QQ) - sage: C2 = ConvexPolygons(AA) - sage: C2.has_coerce_map_from(C1) - True - sage: C1.has_coerce_map_from(C2) - False - """ - return isinstance(other, ConvexPolygons) and self.field().has_coerce_map_from( - other.field() - ) + if category is None: + from flatsurf.geometry.categories import EuclideanPolygons - def _an_element_(self): - return self([(1, 0), (0, 1), (-1, 0), (0, -1)]) + # Currently, all polygons are assumed to be without self-intersection, i.e., simple. + category = EuclideanPolygons(base_ring).Simple() + if angles: + category = category.WithAngles(angles) - def _repr_(self): - return "ConvexPolygons(%s)" % self.base_ring() + if n == 3: + category = category.Convex() - def _element_constructor_(self, *args, **kwds): - r""" - TESTS:: + # We now rewrite the given data into vertices. Whenever there is + # redundancy, we check that things are compatible. Note that much of the + # complication of the below comes from the "angles" keyword. When angles + # are given, some of the vertex coordinates can be deduced automatically. - sage: from flatsurf import ConvexPolygons + choice, vertices, edges, angles, lengths = _Polygon_normalize_arguments( + category, n, vertices, edges, angles, lengths + ) - sage: C = ConvexPolygons(QQ) - sage: p = C(vertices=[(0,0),(1,0),(2,0),(1,1)]) - sage: p - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - sage: C(p) is p - True - sage: C((1,0), (0,1), (-1, 1)) - Traceback (most recent call last): - ... - ValueError: the polygon does not close up + assert vertices - sage: D = ConvexPolygons(QQbar) - sage: D(p) - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - sage: D(vertices=p.vertices()) - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) - sage: D(edges=p.edges()) - Polygon: (0, 0), (1, 0), (2, 0), (1, 1) + vertices = [vector(base_ring, vertex) for vertex in vertices] - """ - check = kwds.pop("check", True) + # Deduce missing vertices for prescribed angles + if angles and len(vertices) != n: + vertices = _Polygon_complete_vertices(n, vertices, angles, choice=choice) + angles = None - if len(args) == 1 and isinstance(args[0], Polygon): - vertices = map(self.vector_space(), args[0].vertices()) - args = () + assert ( + len(vertices) == n + ), f"expected to build {n}-gon from {n} vertices but found {vertices}" - else: - vertices = kwds.pop("vertices", None) - edges = kwds.pop("edges", None) - base_point = kwds.pop("base_point", (0, 0)) - - if (vertices is None) and (edges is None): - if len(args) == 1: - edges = args[0] - elif args: - edges = args - else: - raise ValueError( - "exactly one of 'vertices' or 'edges' must be provided" - ) - if kwds: - raise ValueError("invalid keyword {!r}".format(next(iter(kwds)))) + p = EuclideanPolygon(base_ring=base_ring, vertices=vertices, category=category) - if edges is not None: - v = self.module()(base_point) - vertices = [] - for e in map(self.module(), edges): - vertices.append(v) - v += e - if v != vertices[0]: - raise ValueError("the polygon does not close up") + if check: + _Polygon_check(p, vertices, edges, angles, lengths, convex) - return self.element_class(self, vertices, check) + return p -class EquiangularPolygons: +def _Polygon_base_ring(vertices, edges, angles, lengths): r""" - Polygons with fixed (rational) angles. - - EXAMPLES:: - - sage: from flatsurf import EquiangularPolygons + Return the base ring a polygon can be defined over. - The polygons with inner angles `\pi/4`, `\pi/2`, `5\pi/4`:: - - sage: P = EquiangularPolygons(1, 2, 5) - sage: P - EquiangularPolygons(1, 2, 5) + This is a helper function for :func:`Polygon`. - Internally, polygons are given by their vertices' coordinates over some - number field, in this case a quadratic field:: - - sage: P.base_ring() - Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - - Polygons can also be defined over other number field implementations:: - - sage: from pyeantic import RealEmbeddedNumberField # optional: eantic # random output due to matplotlib warnings with some combinations of setuptools and matplotlib - sage: K = RealEmbeddedNumberField(P.base_ring()) # optional: eantic - sage: P(K(1)) # optional: eantic - Polygon: (0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1) - sage: _.base_ring() # optional: eantic - Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - - However, specific instances of such polygons might be defined over another ring:: - - sage: P(1) - Polygon: (0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1) - sage: _.base_ring() - Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? + EXAMPLES:: - sage: P(AA(1)) - Polygon: (0, 0), (1, 0), (0.7071067811865475?, 0.2928932188134525?) - sage: _.base_ring() + sage: from flatsurf.geometry.polygon import _Polygon_base_ring + sage: _Polygon_base_ring(vertices=[(0, 0), (1, 0), (0, 1)], edges=None, angles=None, lengths=None) + Rational Field + sage: _Polygon_base_ring(vertices=None, edges=[(1, 0), (-1, 1), (0, -1)], angles=None, lengths=None) + Rational Field + sage: _Polygon_base_ring(vertices=None, edges=None, angles=[1, 1, 1], lengths=None) + Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + sage: _Polygon_base_ring(vertices=None, edges=None, angles=[1, 1, 1], lengths=[AA(2).sqrt(), 1]) Algebraic Real Field - Polygons can also be defined over a module containing transcendent parameters:: - - sage: from pyexactreal import ExactReals # optional: exactreal - sage: R = ExactReals(P.base_ring()) # optional: exactreal - sage: P(R(1)) # optional: exactreal - Polygon: (0, 0), (1, 0), ((1/2*c0 ~ 0.70710678), (-1/2*c0+1 ~ 0.29289322)) - sage: P(R(R.random_element([0.2, 0.3]))) # random output, optional: exactreal - Polygon: (0, 0), - (ℝ(0.287373=2588422249976937p-53 + ℝ(0.120809…)p-54), 0), - (((12*c0+17 ~ 33.970563)*ℝ(0.287373=2588422249976937p-53 + ℝ(0.120809…)p-54))/((17*c0+24 ~ 48.041631)), - ((5*c0+7 ~ 14.071068)*ℝ(0.287373=2588422249976937p-53 + ℝ(0.120809…)p-54))/((17*c0+24 ~ 48.041631))) - sage: _.base_ring() # optional: exactreal - Real Numbers as (Real Embedded Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095?)-Module - - :: - - sage: L = P.lengths_polytope() # polytope of admissible lengths for edges - sage: L - A 1-dimensional polyhedron in (Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095?)^3 defined as the convex hull of 1 vertex and 1 ray - sage: lengths = L.rays()[0].vector() - sage: lengths - (1, -1/2*c0 + 1, -1/2*c0 + 1) - sage: p = P(*lengths) # build one polygon with the given lengths - sage: p - Polygon: (0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1) - sage: p.angles() - [1/16, 1/8, 5/16] - sage: P.angles(integral=False) - [1/16, 1/8, 5/16] - - sage: P = EquiangularPolygons(1, 2, 1, 2, 2, 1) - sage: L = P.lengths_polytope() - sage: L - A 4-dimensional polyhedron in (Number Field in c with defining polynomial x^6 - 6*x^4 + 9*x^2 - 3 with c = 1.969615506024417?)^6 defined as the convex hull of 1 vertex and 6 rays - sage: rays = [r.vector() for r in L.rays()] - sage: rays - [(1, 0, 0, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c, -1/6*c^5 + 5/6*c^3 - 2/3*c), - (0, 1, 0, 0, c^2 - 3, c^2 - 2), - (1/3*c^4 - 2*c^2 + 3, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c, 0, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c), - (-c^4 + 4*c^2, 0, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c), - (0, 1/3*c^4 - 2*c^2 + 3, c^2 - 3, 0, 0, 1/3*c^4 - c^2), - (0, -c^4 + 4*c^2, 0, c^2 - 3, 0, -c^4 + 5*c^2 - 3)] - sage: lengths = 3*rays[0] + rays[2] + 2*rays[3] + rays[4] - sage: p = P(*lengths) - sage: p - Polygon: (0, 0), - (-5/3*c^4 + 6*c^2 + 6, 0), - (3*c^5 - 5/3*c^4 - 16*c^3 + 6*c^2 + 18*c + 6, c^4 - 6*c^2 + 9), - (2*c^5 - 2*c^4 - 10*c^3 + 15/2*c^2 + 9*c + 5, -1/2*c^5 + c^4 + 5/2*c^3 - 3*c^2 - 2*c), - (2*c^5 - 10*c^3 - 3/2*c^2 + 9*c + 9, -3/2*c^5 + c^4 + 15/2*c^3 - 3*c^2 - 6*c), - (2*c^5 - 10*c^3 - 3*c^2 + 9*c + 12, -3*c^5 + c^4 + 15*c^3 - 3*c^2 - 12*c) - - sage: p.angles() - [2/9, 4/9, 2/9, 4/9, 4/9, 2/9] - - sage: EquiangularPolygons(1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1) - EquiangularPolygons(1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1) - - A regular pentagon:: - - sage: E = EquiangularPolygons(1, 1, 1, 1, 1) - sage: E(1, 1, 1, 1, 1, normalized=True) - Polygon: (0, 0), (1, 0), (1/2*c^2 - 1/2, 1/2*c), (1/2, 1/2*c^3 - c), (-1/2*c^2 + 3/2, 1/2*c) """ + from sage.categories.pushout import pushout - def __init__(self, *angles, **kwds): - if "number_field" in kwds: - from warnings import warn - - warn( - "The number_field parameter has been removed in this release of sage-flatsurf. " - "To create an equiangular polygon over a number field, do not pass this parameter; to create an equiangular polygon over the algebraic numbers, do not pass this parameter but call the returned object with algebraic lengths." - ) - kwds.pop("number_field") - - if kwds: - raise ValueError("invalid keyword {!r}".format(next(iter(kwds)))) - if len(angles) == 1 and isinstance(angles[0], (tuple, list)): - angles = angles[0] - - n = len(angles) - if n < 3: - raise ValueError("'angles' should be a list of at least 3 numbers") - angles = [QQ.coerce(a) for a in angles] - if any(angle <= 0 for angle in angles): - raise ValueError("'angles' must be positive rational numbers") - - # Store each angle as a multiple of 2π, i.e., normalize them such their sum is (n - 2)/2. - angles = [a / sum(angles) for a in angles] - angles = [a * ZZ(n - 2) / 2 for a in angles] - if any(angle <= 0 or angle >= 1 for angle in angles): - raise ValueError("each angle must be > 0 and < 2 pi") - self._angles = angles - assert sum(self._angles) == ZZ(n - 2) / 2 - - # We determine the number field that contains the slopes of the sides, - # i.e., the cosines and sines of the inner angles of the polygon. - # Let us first write all angles as multiples of 2π/N with the smallest - # possible common N. - N = lcm(a.denominator() for a in angles) - # The field containing the cosine and sine of 2π/N might be too small - # to write down all the slopes when N is not divisible by 4. - assert N != 1, "there cannot be a polygon with all angles multiples of 2π" - if N == 2: - pass - elif N % 4: - while N % 4: - N *= 2 - - angles = [ZZ(a * N) for a in angles] - - if N == 2: - self._base_ring = QQ - c = QQ.zero() - else: - # Construct the minimal polynomial f(x) of c = 2 cos(2π / N) - f = cos_minpoly(N // 2) - emb = AA.polynomial_root(f, 2 * (2 * RIF.pi() / N).cos()) - self._base_ring = NumberField(f, "c", embedding=emb) - c = self._base_ring.gen() - - # Construct the cosine and sine of each angle as an element of our number field. - def cosine(a): - return chebyshev_T(abs(a), c) / 2 - - def sine(a): - # Use sin(x) = cos(π/2 - x) - return cosine(N // 4 - a) - - slopes = [(cosine(a), sine(a)) for a in angles] - assert all((x**2 + y**2).is_one() for x, y in slopes) - - self._slopes = [projectivization(x, y) for x, y in slopes] - self._cosines_ring = self._base_ring - - # TODO: It might be the case that the slopes generate a smaller - # field. For now we use an ugly workaround via subfield_from_elements. - old_slopes = [] - for v in self._slopes: - old_slopes.extend(v) - L, new_slopes, _ = subfield_from_elements(self._base_ring, old_slopes) - if L != self._base_ring: - self._slopes = [ - projectivization(*new_slopes[i : i + 2]) - for i in range(0, len(old_slopes), 2) - ] - self._base_ring = L - - def convexity(self): - r""" - EXAMPLES:: - - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(1, 2, 5).convexity() - True - sage: EquiangularPolygons(2, 2, 3, 13).convexity() - False - """ - return all(2 * a <= 1 for a in self._angles) - - def base_ring(self): - r""" - Return the number field over which the coordinates of the vertices of - this family of polygons are represented internally. - - EXAMPLES:: - - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(1, 2, 5).base_ring() - Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - - """ - return self._base_ring - - def strict_convexity(self): - r""" - EXAMPLES:: - - sage: from flatsurf import EquiangularPolygons - sage: E = EquiangularPolygons([1, 1, 1, 1, 2]) - sage: E.angles() - [1/4, 1/4, 1/4, 1/4, 1/2] - sage: E.convexity() - True - sage: E.strict_convexity() - False - - """ - return all(2 * a < 1 for a in self._angles) - - def angles(self, integral=False): - r""" - Return the interior angles of this polygon as multiples 2π. - - EXAMPLES:: - - sage: from flatsurf import EquiangularPolygons - sage: E = EquiangularPolygons(1, 1, 1, 2, 6) - sage: E.angles() - [3/22, 3/22, 3/22, 3/11, 9/11] - - When ``integral`` is set, the output is scaled to eliminate - denominators:: - - sage: E.angles(integral=True) - [1, 1, 1, 2, 6] - - """ - angles = self._angles - if integral: - C = lcm([a.denominator() for a in self._angles]) / gcd( - [a.numerator() for a in self._angles] - ) - angles = [ZZ(C * a) for a in angles] - return angles - - def __repr__(self): - r""" - TESTS:: - - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(1, 2, 3) - EquiangularPolygons(1, 2, 3) - """ - return "EquiangularPolygons({})".format(", ".join(map(str, self.angles(True)))) - - @cached_method - def module(self): - r""" - Return the free module of rank 2 in which these polygons embed. - - EXAMPLES:: - - sage: from flatsurf import EquiangularPolygons - sage: C = EquiangularPolygons(1, 2, 3) - sage: C.module() - Vector space of dimension 2 over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? - - """ - return FreeModule(self._base_ring, 2) - - @cached_method - def vector_space(self): - r""" - Return the vector space of dimension 2 in which these polygons embed. + base_ring = QQ - EXAMPLES:: + if angles: + from flatsurf import EuclideanPolygonsWithAngles - sage: from flatsurf import EquiangularPolygons - sage: C = EquiangularPolygons(1, 2, 3) - sage: C.vector_space() - Vector space of dimension 2 over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? + base_ring = pushout(base_ring, EuclideanPolygonsWithAngles(angles).base_ring()) - """ - return VectorSpace(self._base_ring.fraction_field(), 2) + if vertices: + base_ring = pushout( + base_ring, + Sequence([v[0] for v in vertices] + [v[1] for v in vertices]).universe(), + ) - def slopes(self, e0=(1, 0)): - r""" - List of slopes of the edges as a list of vectors. + if edges: + base_ring = pushout( + base_ring, + Sequence([e[0] for e in edges] + [e[1] for e in edges]).universe(), + ) - EXAMPLES:: + if lengths: + base_ring = pushout(base_ring, Sequence(lengths).universe()) - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(1, 2, 1, 2).slopes() - [(1, 0), (c, 3), (-1, 0), (-c, -3)] - """ - V = self.module() - slopes = self._slopes - n = len(slopes) - cosines = [x[0] for x in slopes] - sines = [x[1] for x in slopes] - v = V.zero() - e = V(e0) - edges = [e] - for i in range(n - 1): - e = ( - -cosines[i + 1] * e[0] - sines[i + 1] * e[1], - sines[i + 1] * e[0] - cosines[i + 1] * e[1], + if angles and not edges: + with_angles = ( + EuclideanPolygonsWithAngles(angles) + ._without_axiom("Simple") + ._without_axiom("Convex") ) - e = projectivization(*e) - edges.append(V(e)) - return edges - - # TODO: rather than lengths, it would be more convenient to have access - # to the tangent space (that is the space of possible holonomies). However, - # since it is not defined over the real numbers, there are several possible ways - # to handle the data. - # TODO: here we ignored the direction SO(2) which provides additional symmetry - # in the tangent space - @cached_method - def lengths_polytope(self): - r""" - Return the polytope parametrizing the admissible vectors data. + for slope, length in zip(with_angles.slopes(), lengths): + scale = base_ring(length**2 / (slope[0] ** 2 + slope[1] ** 2)) + try: + is_square = scale.is_square() + except NotImplementedError: + import warnings + + warnings.warn( + "Due to https://github.com/flatsurf/exact-real/issues/173, we cannot compute the minimal base ring over which this polygon is defined. The polygon could possibly have been defined over a smaller ring." + ) + is_square = False - This polytope parametrizes the tangent space to the set of these - equiangular polygons. Be careful that even though the lengths are - admissible, they may not define a polygon without intersection. + if not is_square: + # Note that this ring might not be minimal. + base_ring = pushout(base_ring, with_angles._cosines_ring()) - EXAMPLES:: + return base_ring - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(1, 2, 1, 2).lengths_polytope() - A 2-dimensional polyhedron in (Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878?)^4 defined as the convex hull of 1 vertex and 2 rays - """ - n = len(self._angles) - slopes = self.slopes() - eqns = [[0] + [s[0] for s in slopes], [0] + [s[1] for s in slopes]] - ieqs = [] - for i in range(n): - ieq = [0] * (n + 1) - ieq[i + 1] = 1 - ieqs.append(ieq) - from sage.geometry.polyhedron.constructor import Polyhedron +def _Polygon_normalize_arguments(category, n, vertices, edges, angles, lengths): + r""" + Return the normalized arguments defining a polygon. Additionally, a flag is + returned that indicates whether we made a choice in normalizing these + arguments. - return Polyhedron(eqns=eqns, ieqs=ieqs, base_ring=self._base_ring) + This is a helper function for :func:`Polygon`. - def an_element(self): - r""" - Return a polygon in this family. - - Note that this might fail due to intersection. + EXAMPLES:: - EXAMPLES:: + sage: from flatsurf.geometry.polygon import _Polygon_normalize_arguments + sage: from flatsurf.geometry.categories import EuclideanPolygons + sage: category = EuclideanPolygons(AA) + sage: _Polygon_normalize_arguments(category=category, n=3, vertices=[(0, 0), (1, 0), (0, 1)], edges=None, angles=None, lengths=None) + (False, [(0, 0), (1, 0), (0, 1)], None, None, None) + sage: _Polygon_normalize_arguments(category=category, n=3, vertices=None, edges=[(1, 0), (-1, 1), (0, -1)], angles=None, lengths=None) + (False, [(0, 0), (1, 0), (0, 1)], None, None, None) + + sage: category = category.WithAngles([1, 1, 1]) + sage: _Polygon_normalize_arguments(category=category, n=3, vertices=None, edges=None, angles=[1, 1, 1], lengths=None) + (True, [(0, 0), (1, 0), (1/2, 0.866025403784439?)], None, None, None) + sage: _Polygon_normalize_arguments(category=category, n=3, vertices=None, edges=None, angles=[1, 1, 1], lengths=[AA(2).sqrt(), 1]) + (False, + [(0, 0), (1.414213562373095?, 0), (0.9142135623730951?, 0.866025403784439?)], + None, + [1, 1, 1], + None) - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(4, 3, 4, 4, 3, 4).an_element() - Polygon: (0, 0), - (1/22*c + 1, 0), - (9*c^9 + 1/2*c^8 - 88*c^7 - 9/2*c^6 + 297*c^5 + 27/2*c^4 - 396*c^3 - 15*c^2 + 3631/22*c + 11/2, 1/2*c + 11), - (16*c^9 + c^8 - 154*c^7 - 9*c^6 + 506*c^5 + 27*c^4 - 638*c^3 - 30*c^2 + 4841/22*c + 9, c + 22), - (16*c^9 + c^8 - 154*c^7 - 9*c^6 + 506*c^5 + 27*c^4 - 638*c^3 - 30*c^2 + 220*c + 8, c + 22), - (7*c^9 + 1/2*c^8 - 66*c^7 - 9/2*c^6 + 209*c^5 + 27/2*c^4 - 242*c^3 - 15*c^2 + 55*c + 7/2, 1/2*c + 11) - """ - return self(sum(r.vector() for r in self.lengths_polytope().rays())) + """ + base_ring = category.base_ring() - def random_element(self, ring=None, **kwds): - r""" - Return a random polygon. + # Track whether we made a choice that possibly is the reason that we fail + # to find a polygon with the given data. + choice = False - EXAMPLES:: + # Rewrite angles and lengths as angles and edges. + if angles and lengths and not edges: + edges = [] + for slope, length in zip(category.slopes(), lengths): + scale = base_ring((length**2 / (slope[0] ** 2 + slope[1] ** 2)).sqrt()) + edges.append(scale * slope) - sage: from flatsurf import EquiangularPolygons - sage: EquiangularPolygons(1, 1, 1, 2, 5).random_element() - Polygon: (0, 0), ... - sage: EquiangularPolygons(1,1,1,15,15,15).random_element() - Polygon: (0, 0), ... - sage: EquiangularPolygons(1,15,1,15,1,15).random_element() - Polygon: (0, 0), ... - """ - if ring is None: - ring = QQ - - rays = [r.vector() for r in self.lengths_polytope().rays()] - - def random_element(): - while True: - coeffs = [] - while len(coeffs) < len(rays): - x = ring.random_element(**kwds) - while x < 0: - x = ring.random_element(**kwds) - coeffs.append(x) - - sol = sum(c * r for c, r in zip(coeffs, rays)) - if all(x > 0 for x in sol): - return coeffs, sol - - while True: - coeffs, r = random_element() - try: - return self(*r) - except ValueError as e: - if ( - not e.args[0].startswith("edge ") - or not e.args[0].endswith("intersect") - or e.args[0].count(" and edge ") != 1 - ): - raise RuntimeError( - "unexpected error with coeffs {!r} ~ {!r}: {!r}".format( - coeffs, [numerical_approx(x) for x in coeffs], e - ) - ) + if len(edges) == n: + angles = 0 - def __call__(self, *lengths, normalized=False, base_ring=None): - r""" - TESTS:: + lengths = None - sage: from flatsurf import EquiangularPolygons - sage: P = EquiangularPolygons(1, 2, 1, 2) - sage: L = P.lengths_polytope() - sage: r0, r1 = [r.vector() for r in L.rays()] - sage: lengths = r0 + r1 - sage: P(*lengths[:-2]) - Polygon: (0, 0), (1, 0), (c + 1, 3), (c, 3) - - sage: P = EquiangularPolygons(2, 2, 3, 13) - sage: r0, r1 = [r.vector() for r in P.lengths_polytope().rays()] - sage: P(r0 + r1) - Polygon: (0, 0), (20, 0), (5, -15*c^3 + 60*c), (5, -5*c^3 + 20*c) - """ - if len(lengths) == 1 and isinstance(lengths[0], (tuple, list, Vector)): - lengths = lengths[0] + # Deduce edges if only angles are given + if angles and not edges and not vertices: + assert not lengths - n = len(self._angles) - if len(lengths) != n - 2 and len(lengths) != n: - raise ValueError( - "must provide %d or %d lengths but provided %d" - % (n - 2, n, len(lengths)) - ) + choice = True - V = self.module() - slopes = self.slopes() - if normalized: - V = V.change_ring(self._cosines_ring) - for i, s in enumerate(slopes): - x, y = map(self._cosines_ring, s) - norm2 = (x**2 + y**2).sqrt() - slopes[i] = V((x / norm2, y / norm2)) - - if base_ring is None: - from sage.all import Sequence - - base_ring = Sequence(lengths).universe() - - from sage.categories.pushout import pushout - - if normalized: - base_ring = pushout(base_ring, self._cosines_ring) - else: - base_ring = pushout(base_ring, self._base_ring) - - v = V((0, 0)) - vertices = [v] - - if len(lengths) == n - 2: - for i in range(n - 2): - v += lengths[i] * slopes[i] - vertices.append(v) - s, t = ( - vector(vertices[0] - vertices[n - 2]) - * matrix([slopes[-1], slopes[n - 2]]).inverse() + # We pick the edges such that they form a closed polygon with the + # prescribed angles. However, there might be self-intersection which + # currently leads to an error. + edges = [ + length * slope + for (length, slope) in zip( + sum(r.vector() for r in category.lengths_polytope().rays()), + category.slopes(), ) - assert vertices[0] - s * slopes[-1] == vertices[n - 2] + t * slopes[n - 2] - if s <= 0 or t <= 0: - raise ValueError("the provided lengths do not give rise to a polygon") - vertices.append(vertices[0] - s * slopes[-1]) - - elif len(lengths) == n: - for i in range(n): - v += lengths[i] * slopes[i] - vertices.append(v) - if not vertices[-1].is_zero(): - raise ValueError("the provided lengths do not give rise to a polygon") - vertices.pop(-1) - - if self.convexity(): - return ConvexPolygons(base_ring)(vertices=vertices) - else: - return Polygons(base_ring)(vertices=vertices) + ] - def billiard_unfolding_angles(self, cover_type="translation"): - r""" - Return the angles of the unfolding rational, half-translation or translation surface. + angles = None - INPUT: + # Rewrite edges as vertices. + if edges and not vertices: + vertices = [vector(base_ring, (0, 0))] + for edge in edges: + vertices.append(vertices[-1] + vector(base_ring, edge)) - - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, - ``"half-translation"`` or ``"translation"`` + if len(vertices) == n + 1: + if vertices[-1]: + raise ValueError("polygon not closed") + vertices.pop() - EXAMPLES:: + edges = None - sage: from flatsurf import EquiangularPolygons - - sage: E = EquiangularPolygons(1, 2, 5) - sage: E.billiard_unfolding_angles(cover_type="rational") - {1/8: 1, 1/4: 1, 5/8: 1} - sage: (1/8 - 1) + (1/4 - 1) + (5/8 - 1) # Euler characteristic (of the sphere) - -2 - sage: E.billiard_unfolding_angles(cover_type="half-translation") - {1/2: 3, 5/2: 1} - sage: E.billiard_unfolding_angles(cover_type="translation") - {1: 3, 5: 1} - - sage: E = EquiangularPolygons(1, 3, 1, 7) - sage: E.billiard_unfolding_angles(cover_type="rational") - {1/6: 2, 1/2: 1, 7/6: 1} - sage: 2 * (1/6 - 1) + (1/2 - 1) + (7/6 - 1) # Euler characteristic - -2 - sage: E.billiard_unfolding_angles(cover_type="half-translation") - {1/2: 5, 7/2: 1} - sage: E.billiard_unfolding_angles(cover_type="translation") - {1: 5, 7: 1} - - sage: E = EquiangularPolygons(1, 3, 5, 7) - sage: E.billiard_unfolding_angles(cover_type="rational") - {1/8: 1, 3/8: 1, 5/8: 1, 7/8: 1} - sage: (1/8 - 1) + (3/8 - 1) + (5/8 - 1) + (7/8 - 1) # Euler characteristic - -2 - sage: E.billiard_unfolding_angles(cover_type="half-translation") - {1/2: 1, 3/2: 1, 5/2: 1, 7/2: 1} - sage: E.billiard_unfolding_angles(cover_type="translation") - {1: 1, 3: 1, 5: 1, 7: 1} - - sage: E = EquiangularPolygons(1, 2, 8) - sage: E.billiard_unfolding_angles(cover_type="rational") - {1/11: 1, 2/11: 1, 8/11: 1} - sage: (1/11 - 1) + (2/11 - 1) + (8/11 - 1) # Euler characteristic - -2 - sage: E.billiard_unfolding_angles(cover_type="half-translation") - {1: 1, 2: 1, 8: 1} - sage: E.billiard_unfolding_angles(cover_type="translation") - {1: 1, 2: 1, 8: 1} - """ - rat_angles = {} - for a in self.angles(): - if 2 * a in rat_angles: - rat_angles[2 * a] += 1 - else: - rat_angles[2 * a] = 1 - if cover_type == "rational": - return rat_angles - - N = lcm([x.denominator() for x in rat_angles]) - if N % 2: - N *= 2 - - cov_angles = {} - for x, mult in rat_angles.items(): - y = x.numerator() - d = x.denominator() - if d % 2: - d *= 2 - else: - y = y / 2 - assert N % d == 0 - if y in cov_angles: - cov_angles[y] += mult * N // d - else: - cov_angles[y] = mult * N // d - - if cover_type == "translation" and any( - y.denominator() == 2 for y in cov_angles - ): - covcov_angles = {} - for y, mult in cov_angles.items(): - yy = y.numerator() - if yy not in covcov_angles: - covcov_angles[yy] = 0 - covcov_angles[yy] += 2 // y.denominator() * mult - return covcov_angles - elif cover_type == "half-translation" or cover_type == "translation": - return cov_angles - else: - raise ValueError("unknown 'cover_type' {!r}".format(cover_type)) + return choice, vertices, edges, angles, lengths - def billiard_unfolding_stratum(self, cover_type="translation", marked_points=False): - r""" - Return the stratum of quadratic or Abelian differential obtained by - unfolding a billiard in a polygon of this equiangular family. - INPUT: +def _Polygon_complete_vertices(n, vertices, angles, choice): + r""" + Return vertices that define a polygon by completing the ``vertices`` to an + ``n``-gon with ``angles``. - - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, - ``"half-translation"`` or ``"translation"`` + This is a helper function for :func:`Polygon`. - - ``marked_poins`` (optional, default ``False``) - whether the stratum should - have regular marked points + EXAMPLES:: - EXAMPLES:: + sage: from flatsurf.geometry.polygon import _Polygon_complete_vertices + sage: _Polygon_complete_vertices(3, [vector((0, 0)), vector((1, 0))], [1, 1, 1], choice=False) + [(0, 0), (1, 0), (1/2, 1/2*c)] - sage: from flatsurf import EquiangularPolygons, similarity_surfaces - - sage: E = EquiangularPolygons(1, 2, 5) - sage: E.billiard_unfolding_stratum("half-translation") - Q_1(3, -1^3) - sage: E.billiard_unfolding_stratum("translation") - H_3(4) - sage: E.billiard_unfolding_stratum("half-translation", True) - Q_1(3, -1^3) - sage: E.billiard_unfolding_stratum("translation", True) - H_3(4, 0^3) - - sage: E = EquiangularPolygons(1, 3, 1, 7) - sage: E.billiard_unfolding_stratum("half-translation") - Q_1(5, -1^5) - sage: E.billiard_unfolding_stratum("translation") - H_4(6) - sage: E.billiard_unfolding_stratum("half-translation", True) - Q_1(5, -1^5) - sage: E.billiard_unfolding_stratum("translation", True) - H_4(6, 0^5) - - sage: P = E.an_element() - sage: S = similarity_surfaces.billiard(P) - sage: S.minimal_cover("half-translation").stratum() - Q_1(5, -1^5) - sage: S.minimal_cover("translation").stratum() - H_4(6, 0^5) - - sage: E = EquiangularPolygons(1, 3, 5, 7) - sage: E.billiard_unfolding_stratum("half-translation") - Q_3(5, 3, 1, -1) - sage: E.billiard_unfolding_stratum("translation") - H_7(6, 4, 2) - - sage: P = E.an_element() - sage: S = similarity_surfaces.billiard(P) - sage: S.minimal_cover("half-translation").stratum() - Q_3(5, 3, 1, -1) - sage: S.minimal_cover("translation").stratum() - H_7(6, 4, 2, 0) - - sage: E = EquiangularPolygons(1, 2, 8) - sage: E.billiard_unfolding_stratum("half-translation") - H_5(7, 1) - sage: E.billiard_unfolding_stratum("translation") - H_5(7, 1) - - sage: E.billiard_unfolding_stratum("half-translation", True) - H_5(7, 1, 0) - sage: E.billiard_unfolding_stratum("translation", True) - H_5(7, 1, 0) - - sage: E = EquiangularPolygons(9, 6, 3, 2) - sage: p = E.an_element() - sage: B = similarity_surfaces.billiard(p) - sage: B.minimal_cover("half-translation").stratum() - Q_4(7, 4, 1, 0) - sage: E.billiard_unfolding_stratum("half-translation", True) - Q_4(7, 4, 1, 0) - sage: B.minimal_cover("translation").stratum() - H_8(8, 2^3, 0^2) - sage: E.billiard_unfolding_stratum("translation", True) - H_8(8, 2^3, 0^2) - """ - angles = self.billiard_unfolding_angles(cover_type) - if all(a.is_integer() for a in angles): - from surface_dynamics import AbelianStratum - - if not marked_points and len(angles) == 1 and 1 in angles: - return AbelianStratum([0]) - else: - return AbelianStratum( - { - ZZ(a - 1): mult - for a, mult in angles.items() - if marked_points or a != 1 - } - ) - else: - from surface_dynamics import QuadraticStratum - - return QuadraticStratum( - { - ZZ(2 * (a - 1)): mult - for a, mult in angles.items() - if marked_points or a != 1 - } + """ + if len(vertices) == n - 1: + # We do not use category.slopes() since the matrix formed by such + # slopes might not be invertible (because exact-reals do not have a + # fraction field implemented.) + slopes = EuclideanPolygonsWithAngles(angles).slopes() + + # We do not use solve_left() because the vertices might not live in + # a ring that has a fraction field implemented (such as an + # exact-real ring.) + s, t = (vertices[0] - vertices[n - 2]) * matrix( + [slopes[-1], slopes[n - 2]] + ).inverse() + assert vertices[0] - s * slopes[-1] == vertices[n - 2] + t * slopes[n - 2] + + if s <= 0 or t <= 0: + raise (NotImplementedError if choice else ValueError)( + "cannot determine polygon with these angles from the given data" ) - def billiard_unfolding_stratum_dimension( - self, cover_type="translation", marked_points=False - ): - r""" - Return the dimension of the stratum of quadratic or Abelian differential - obtained by unfolding a billiard in a polygon of this equiangular family. - - INPUT: - - - ``cover_type`` (optional, default ``"translation"``) - either ``"rational"``, - ``"half-translation"`` or ``"translation"`` + vertices.append(vertices[0] - s * slopes[-1]) - - ``marked_poins`` (optional, default ``False``) - whether the stratum should - have marked regular points - - EXAMPLES:: + if len(vertices) != n: + from flatsurf.geometry.categories import Polygons - sage: from flatsurf import EquiangularPolygons - - sage: E = EquiangularPolygons(1, 1, 1) - sage: E.billiard_unfolding_stratum_dimension("half-translation") - 2 - sage: E.billiard_unfolding_stratum_dimension("translation") - 2 - sage: E.billiard_unfolding_stratum_dimension("half-translation", True) - 4 - sage: E.billiard_unfolding_stratum_dimension("translation", True) - 4 - - sage: E = EquiangularPolygons(1, 2, 5) - sage: E.billiard_unfolding_stratum_dimension("half-translation") - 4 - sage: E.billiard_unfolding_stratum("half-translation").dimension() - 4 - sage: E.billiard_unfolding_stratum_dimension(cover_type="translation") - 6 - sage: E.billiard_unfolding_stratum("translation").dimension() - 6 - sage: E.billiard_unfolding_stratum_dimension("translation", True) - 9 - sage: E.billiard_unfolding_stratum("translation", True).dimension() - 9 - - sage: E = EquiangularPolygons(1, 3, 5) - sage: E.billiard_unfolding_stratum_dimension("half-translation") - 6 - sage: E.billiard_unfolding_stratum("half-translation").dimension() - 6 - sage: E.billiard_unfolding_stratum_dimension("translation") - 6 - sage: E.billiard_unfolding_stratum("translation").dimension() - 6 - - sage: E = EquiangularPolygons(1, 3, 1, 7) - sage: E.billiard_unfolding_stratum_dimension("half-translation") - 6 - - sage: E = EquiangularPolygons(1, 3, 5, 7) - sage: E.billiard_unfolding_stratum_dimension("half-translation") - 8 - - sage: E = EquiangularPolygons(1, 2, 8) - sage: E.billiard_unfolding_stratum_dimension() - 11 - sage: E.billiard_unfolding_stratum().dimension() - 11 - sage: E.billiard_unfolding_stratum_dimension(marked_points=True) - 12 - sage: E.billiard_unfolding_stratum(marked_points=True).dimension() - 12 - """ - if cover_type == "rational": - raise NotImplementedError - if cover_type != "translation" and cover_type != "half-translation": - raise ValueError - - angles = self.billiard_unfolding_angles(cover_type) - if not marked_points: - if 1 in angles: - del angles[1] - if not angles: - angles[ZZ.one()] = ZZ.one() - - abelian = all(a.is_integer() for a in angles) - s = sum(angles.values()) - chi = sum(mult * (a - 1) for a, mult in angles.items()) - assert chi.denominator() == 1 - chi = ZZ(chi) - assert chi % 2 == 0 - g = chi // 2 + 1 - return 2 * g + s - 1 if abelian else 2 * g + s - 2 - - -class PolygonsConstructor: - def square(self, side=1, **kwds): - r""" - EXAMPLES:: - - sage: from flatsurf.geometry.polygon import polygons - - sage: polygons.square() - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: polygons.square(field=QQbar).parent() - ConvexPolygons(Algebraic Field) - """ - return self.rectangle(side, side, **kwds) + raise NotImplementedError( + f"cannot construct {' '.join(Polygons._describe_polygon(n)[:2])} from {n} angles and {len(vertices)} vertices" + ) - def rectangle(self, width, height, **kwds): - r""" - EXAMPLES:: + return vertices - sage: from flatsurf.geometry.polygon import polygons - sage: polygons.rectangle(1,2) - Polygon: (0, 0), (1, 0), (1, 2), (0, 2) +def _Polygon_check(p, vertices, edges, angles, lengths, convex): + r""" + Verify that ``p`` is a valid polygon and that it satisfies the constraints + given. - sage: K. = QuadraticField(2) - sage: polygons.rectangle(1,sqrt2) - Polygon: (0, 0), (1, 0), (1, sqrt2), (0, sqrt2) - sage: _.parent() - ConvexPolygons(Number Field in sqrt2 with defining polynomial x^2 - 2 with sqrt2 = 1.414213562373095?) - """ - return self((width, 0), (0, height), (-width, 0), (0, -height), **kwds) + This is a helper function for :func:`Polygon`. - def triangle(self, a, b, c): - """ - Return the triangle with angles a*pi/N,b*pi/N,c*pi/N where N=a+b+c. + EXAMPLES:: - INPUT: + sage: from flatsurf.geometry.polygon import _Polygon_check, Polygon + sage: p = Polygon(angles=[1, 1, 1]) + sage: _Polygon_check(p, vertices=None, edges=None, angles=[1, 1, 1], lengths=None, convex=None) - - ``a``, ``b``, ``c`` -- integers + """ + # Check that the polygon satisfies the assumptions of EuclideanPolygon + area = p.area() - EXAMPLES:: + if area < 0: + raise ValueError( + "polygon has negative area; probably the vertices are not in counter-clockwise order" + ) - sage: from flatsurf.geometry.polygon import polygons - sage: T = polygons.triangle(3,4,5) - sage: T - Polygon: (0, 0), (1, 0), (-1/2*c0 + 3/2, -1/2*c0 + 3/2) - sage: T.base_ring() - Number Field in c0 with defining polynomial x^2 - 3 with c0 = 1.732050807568878? + if area == 0: + raise ValueError("polygon has zero area") - sage: polygons.triangle(1,2,3).angles() - [1/12, 1/6, 1/4] + if any(edge == 0 for edge in p.edges()): + raise ValueError("polygon has zero edge") - Some fairly complicated examples:: + for i in range(len(p.vertices())): + from flatsurf.geometry.euclidean import is_anti_parallel - sage: polygons.triangle(1, 15, 21) # long time (2s) - Polygon: (0, 0), - (1, 0), - (1/2*c^34 - 17*c^32 + 264*c^30 - 2480*c^28 + 15732*c^26 - 142481/2*c^24 + 237372*c^22 - 1182269/2*c^20 + - 1106380*c^18 - 1552100*c^16 + 3229985/2*c^14 - 2445665/2*c^12 + 654017*c^10 - 472615/2*c^8 + 107809/2*c^6 - 13923/2*c^4 + 416*c^2 - 6, - -1/2*c^27 + 27/2*c^25 - 323/2*c^23 + 1127*c^21 - 10165/2*c^19 + 31009/2*c^17 - 65093/2*c^15 + 46911*c^13 - 91091/2*c^11 + 57355/2*c^9 - 10994*c^7 + 4621/2*c^5 - 439/2*c^3 + 6*c) + if is_anti_parallel(p.edge(i), p.edge(i + 1)): + raise ValueError("polygon has anti-parallel edges") - sage: polygons.triangle(2, 13, 26) # long time (3s) - Polygon: (0, 0), - (1, 0), - (1/2*c^30 - 15*c^28 + 405/2*c^26 - 1625*c^24 + 8625*c^22 - 31878*c^20 + 168245/2*c^18 - 159885*c^16 + 218025*c^14 - 209950*c^12 + 138567*c^10 - 59670*c^8 + 15470*c^6 - 2100*c^4 + 225/2*c^2 - 1/2, - -1/2*c^39 + 19*c^37 - 333*c^35 + 3571*c^33 - 26212*c^31 + 139593*c^29 - 557844*c^27 + 1706678*c^25 - 8085237/2*c^23 + 7449332*c^21 - - 10671265*c^19 + 11812681*c^17 - 9983946*c^15 + 6317339*c^13 - 5805345/2*c^11 + 1848183/2*c^9 - 378929/2*c^7 + 44543/2*c^5 - 2487/2*c^3 + 43/2*c) - """ - return EquiangularPolygons(a, b, c)([1]) + from flatsurf.geometry.categories import EuclideanPolygons - @staticmethod - def regular_ngon(n, field=None): - r""" - Return a regular n-gon with unit length edges, first edge horizontal, and other vertices lying above this edge. + if not EuclideanPolygons.ParentMethods.is_simple(p): + raise NotImplementedError("polygon self-intersects") - Assuming field is None (by default) the polygon is defined over a NumberField (the minimal number field determined by n). - Otherwise you can set field equal to AA to define the polygon over the Algebraic Reals. Other values for the field - parameter will result in a ValueError. + # Check that any redundant data is compatible + if edges: + # Check compatibility of vertices and edges + edges = [vector(p.base_ring(), edge) for edge in edges] + if len(edges) != len(vertices): + raise ValueError("vertices and edges must have the same length") - EXAMPLES:: + for i in range(len(p.vertices())): + if vertices[i - 1] + edges[i - 1] != vertices[i]: + raise ValueError("vertices and edges are not compatible") - sage: from flatsurf.geometry.polygon import polygons + if angles: + # Check that the polygon has the prescribed angles + from flatsurf.geometry.categories.euclidean_polygons_with_angles import ( + EuclideanPolygonsWithAngles, + ) + from flatsurf.geometry.categories.euclidean_polygons import ( + EuclideanPolygons, + ) - sage: p = polygons.regular_ngon(17) - sage: p - Polygon: (0, 0), (1, 0), ..., (-1/2*a^14 + 15/2*a^12 - 45*a^10 + 275/2*a^8 - 225*a^6 + 189*a^4 - 70*a^2 + 15/2, 1/2*a) + # Use EuclideanPolygon's angle() so we do not use the precomputed angles set by the category. + if EuclideanPolygonsWithAnglesCategory._normalize_angles(angles) != tuple( + EuclideanPolygons.ParentMethods.angle(p, i) + for i in range(len(p.vertices())) + ): + raise ValueError("polygon does not have the prescribed angles") - sage: polygons.regular_ngon(3,field=AA) - Polygon: (0, 0), (1, 0), (1/2, 0.866025403784439?) - """ - # The code below crashes for n=4! - if n == 4: - return polygons.square(QQ(1), field=field) + if lengths: + for edge, length in zip(p.edges(), lengths): + if edge.norm() != length: + raise ValueError("polygon does not have the prescribed lengths") - from sage.rings.qqbar import QQbar + if convex and not p.is_convex(): + raise ValueError("polygon is not convex") - c = QQbar.zeta(n).real() - s = QQbar.zeta(n).imag() - if field is None: - field, (c, s) = number_field_elements_from_algebraics((c, s)) - cn = field.one() - sn = field.zero() - edges = [(cn, sn)] - for _ in range(n - 1): - cn, sn = c * cn - s * sn, c * sn + s * cn - edges.append((cn, sn)) +def EuclideanPolygonsWithAngles(*angles): + r""" + Return the category of Euclidean polygons with prescribed ``angles`` + over a (minimal) number field. - return ConvexPolygons(field)(edges=edges) + This method is a convenience to interact with that category. To create + polygons with prescribed angles over such a field, one should just use + ``Polygon()`` directly, see below. - @staticmethod - def right_triangle(angle, leg0=None, leg1=None, hypotenuse=None): - r""" - Return a right triangle in a number field with an angle of pi*angle. + INPUT: - You can specify the length of the first leg (``leg0``), the second leg (``leg1``), - or the ``hypotenuse``. + - ``angles`` -- a sequence of integers or rationals describing the angles + of the polygon (the number get normalized so that they sum to (n-2)π + automatically. - EXAMPLES:: + TESTS:: - sage: from flatsurf import * + sage: from flatsurf import EuclideanPolygonsWithAngles - sage: P = polygons.right_triangle(1/3, 1) - sage: P - Polygon: (0, 0), (1, 0), (1, a) - sage: P.base_ring() - Number Field in a with defining polynomial y^2 - 3 with a = 1.732050807568878? + The polygons with inner angles `\pi/4`, `\pi/2`, `5\pi/4`:: - sage: polygons.right_triangle(1/4,1) - Polygon: (0, 0), (1, 0), (1, 1) - sage: polygons.right_triangle(1/4,1).field() - Rational Field - """ - from sage.rings.qqbar import QQbar + sage: P = EuclideanPolygonsWithAngles(1, 2, 5) + sage: P + Category of simple euclidean triangles with angles (1/16, 1/8, 5/16) over Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - angle = QQ(angle) - if angle <= 0 or angle > QQ((1, 2)): - raise ValueError("angle must be in ]0,1/2]") + Internally, polygons are given by their vertices' coordinates over some + number field, in this case a quadratic field:: - z = QQbar.zeta(2 * angle.denom()) ** angle.numerator() - c = z.real() - s = z.imag() + sage: P.base_ring() + Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - nargs = (leg0 is not None) + (leg1 is not None) + (hypotenuse is not None) + Polygons with these angles can be created by providing a single length, + however this feature is deprecated:: - if nargs == 0: - leg0 = 1 - elif nargs > 1: - raise ValueError("only one length can be specified") + sage: P(1) + doctest:warning + ... + UserWarning: calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead. + To make the resulting polygon non-normalized, i.e., the lengths are not actual edge lengths but the multiple of slope vectors, use Polygon(edges=[length * slope for (length, slope) in zip(lengths, EuclideanPolygonsWithAngles(angles).slopes())]). + Polygon(vertices=[(0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1)]) - if leg0 is not None: - c, s = leg0 * c / c, leg0 * s / c - elif leg1 is not None: - c, s = leg1 * c / s, leg1 * s / s - elif hypotenuse is not None: - c, s = hypotenuse * c, hypotenuse * s + Instead, one should use :func:`Polygon`:: - field, (c, s) = number_field_elements_from_algebraics((c, s)) - return ConvexPolygons(field)( - edges=[(c, field.zero()), (field.zero(), s), (-c, -s)] - ) + sage: from flatsurf import Polygon + sage: Polygon(angles=[1, 2, 5], lengths=[1]) + Polygon(vertices=[(0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1)]) - def __call__(self, *args, **kwds): - r""" - EXAMPLES:: + It is actually faster not to specify lengths since normalization can be + costly (only relevant for polygons living in big number fields):: - sage: from flatsurf import * + sage: Polygon(angles=[1, 2, 5]) + Polygon(vertices=[(0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1)]) - sage: polygons((1,0),(0,1),(-1,0),(0,-1)) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: polygons((1,0),(0,1),(-1,0),(0,-1), ring=QQbar) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: _.parent() - ConvexPolygons(Algebraic Field) + Polygons can also be defined over other number field implementations:: - sage: polygons(vertices=[(0,0), (1,0), (0,1)]) - Polygon: (0, 0), (1, 0), (0, 1) + sage: from pyeantic import RealEmbeddedNumberField # optional: eantic # random output due to matplotlib warnings with some combinations of setuptools and matplotlib + sage: K = RealEmbeddedNumberField(P.base_ring()) # optional: eantic + sage: P(K(1)) # optional: eantic + doctest:warning + ... + UserWarning: calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead. + To make the resulting polygon non-normalized, i.e., the lengths are not actual edge lengths but the multiple of slope vectors, use Polygon(edges=[length * slope for (length, slope) in zip(lengths, EuclideanPolygonsWithAngles(angles).slopes())]). + Polygon(vertices=[(0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1)]) + sage: _.base_ring() # optional: eantic + Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - sage: polygons(edges=[(2,0),(-1,1),(-1,-1)], base_point=(3,3)) - Polygon: (3, 3), (5, 3), (4, 4) - sage: polygons(vertices=[(0,0),(2,0),(1,1)], base_point=(3,3)) - Traceback (most recent call last): - ... - ValueError: invalid keyword 'base_point' + However, specific instances of such polygons might be defined over another ring:: + sage: P(1).base_ring() + Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095? - sage: polygons(angles=[1,1,1,2], length=1) - Polygon: (0, 0), (1, 0), (-1/2*c^2 + 5/2, 1/2*c), (-1/2*c^2 + 2, 1/2*c^3 - 3/2*c) - sage: polygons(angles=[1,1,1,2], length=2) - Polygon: (0, 0), (2, 0), (-c^2 + 5, c), (-c^2 + 4, c^3 - 3*c) - sage: polygons(angles=[1,1,1,2], length=AA(2)**(1/2)) - Polygon: (0, 0), (1.414213562373095?, 0), (0.9771975379242739?, 1.344997023927915?), (0.2700907567377265?, 0.8312538755549069?) + sage: P(AA(1)) + Polygon(vertices=[(0, 0), (1, 0), (0.7071067811865475?, 0.2928932188134525?)]) + sage: _.base_ring() + Algebraic Real Field - sage: polygons(angles=[1]*5).angles() - [3/10, 3/10, 3/10, 3/10, 3/10] - sage: polygons(angles=[1]*8).angles() - [3/8, 3/8, 3/8, 3/8, 3/8, 3/8, 3/8, 3/8] + Polygons can also be defined over a module containing transcendent parameters:: - sage: P = polygons(angles=[1,1,3,3], lengths=[3,1]) - sage: P.angles() - [1/8, 1/8, 3/8, 3/8] - sage: e0 = P.edge(0); assert e0[0]**2 + e0[1]**2 == 3**2 - sage: e1 = P.edge(1); assert e1[0]**2 + e1[1]**2 == 1 + sage: from pyexactreal import ExactReals # optional: exactreal # random output due to deprecation warnings with some versions of pkg_resources + sage: R = ExactReals(P.base_ring()) # optional: exactreal + sage: P(R(1)) # optional: exactreal + Polygon(vertices=[(0, 0), (1, 0), ((1/2*c0 ~ 0.70710678), (-1/2*c0+1 ~ 0.29289322))]) + sage: P(R(R.random_element([0.2, 0.3]))) # random output, optional: exactreal + Polygon(vertices=[(0, 0),]) + (ℝ(0.287373=2588422249976937p-53 + ℝ(0.120809…)p-54), 0), + (((12*c0+17 ~ 33.970563)*ℝ(0.287373=2588422249976937p-53 + ℝ(0.120809…)p-54))/((17*c0+24 ~ 48.041631)), + ((5*c0+7 ~ 14.071068)*ℝ(0.287373=2588422249976937p-53 + ℝ(0.120809…)p-54))/((17*c0+24 ~ 48.041631))) + sage: _.base_ring() # optional: exactreal + Real Numbers as (Real Embedded Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095?)-Module - sage: polygons(angles=[1,1,1,2]) - Polygon: (0, 0), (1, 0), (-1/2*c^2 + 5/2, 1/2*c), (-1/2*c^2 + 2, 1/2*c^3 - 3/2*c) + :: - sage: polygons(angles=[1,1,1,8]) - Traceback (most recent call last): - ... - ValueError: 'angles' do not determine convex polygon; you might want to set the option 'convex=False' - sage: polygons(angles=[1,1,1,8], convex=False) - Traceback (most recent call last): - ... - ValueError: non-convex equiangular polygon; lengths must be provided - sage: polygons(angles=[1,1,1,8], lengths=[1,1], convex=False) - Polygon: (0, 0), (1, 0), (-1/2*c^4 + 2*c^2, 1/2*c^7 - 7/2*c^5 + 7*c^3 - 7/2*c), (1/2*c^6 - 7/2*c^4 + 13/2*c^2 - 3/2, 1/2*c^9 - 9/2*c^7 + 27/2*c^5 - 29/2*c^3 + 5/2*c) + sage: L = P.lengths_polytope() # polytope of admissible lengths for edges + sage: L + A 1-dimensional polyhedron in (Number Field in c0 with defining polynomial x^2 - 2 with c0 = 1.414213562373095?)^3 defined as the convex hull of 1 vertex and 1 ray + sage: lengths = L.rays()[0].vector() + sage: lengths + (1, -1/2*c0 + 1, -1/2*c0 + 1) + sage: p = P(*lengths) # build one polygon with the given lengths + sage: p + Polygon(vertices=[(0, 0), (1, 0), (1/2*c0, -1/2*c0 + 1)]) + sage: p.angles() + (1/16, 1/8, 5/16) + sage: P.angles(integral=False) + (1/16, 1/8, 5/16) + sage: P.angles(integral=True) + (1, 2, 5) - TESTS:: + sage: P = EuclideanPolygonsWithAngles(1, 2, 1, 2, 2, 1) + sage: L = P.lengths_polytope() + sage: L + A 4-dimensional polyhedron in (Number Field in c with defining polynomial x^6 - 6*x^4 + 9*x^2 - 3 with c = 1.969615506024417?)^6 defined as the convex hull of 1 vertex and 6 rays + sage: rays = [r.vector() for r in L.rays()] + sage: rays + [(1, 0, 0, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c, -1/6*c^5 + 5/6*c^3 - 2/3*c), + (0, 1, 0, 0, c^2 - 3, c^2 - 2), + (1/3*c^4 - 2*c^2 + 3, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c, 0, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c), + (-c^4 + 4*c^2, 0, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c, 0, -1/6*c^5 + 5/6*c^3 - 2/3*c), + (0, 1/3*c^4 - 2*c^2 + 3, c^2 - 3, 0, 0, 1/3*c^4 - c^2), + (0, -c^4 + 4*c^2, 0, c^2 - 3, 0, -c^4 + 5*c^2 - 3)] + sage: lengths = 3*rays[0] + rays[2] + 2*rays[3] + rays[4] + sage: p = P(*lengths) + sage: p + Polygon(vertices=[(0, 0), + (-5/3*c^4 + 6*c^2 + 6, 0), + (3*c^5 - 5/3*c^4 - 16*c^3 + 6*c^2 + 18*c + 6, c^4 - 6*c^2 + 9), + (2*c^5 - 2*c^4 - 10*c^3 + 15/2*c^2 + 9*c + 5, -1/2*c^5 + c^4 + 5/2*c^3 - 3*c^2 - 2*c), + (2*c^5 - 10*c^3 - 3/2*c^2 + 9*c + 9, -3/2*c^5 + c^4 + 15/2*c^3 - 3*c^2 - 6*c), + (2*c^5 - 10*c^3 - 3*c^2 + 9*c + 12, -3*c^5 + c^4 + 15*c^3 - 3*c^2 - 12*c)]) - sage: from itertools import product - sage: for a,b,c in product(range(1,5), repeat=3): # long time (3s) - ....: if gcd([a,b,c]) != 1: - ....: continue - ....: T = polygons(angles=[a,b,c]) - ....: D = 2*(a+b+c) - ....: assert T.angles() == [a/D, b/D, c/D] - ....: assert T.edge(0) == T.vector_space()((1,0)) - """ - base_ring = None - if "ring" in kwds: - base_ring = kwds.pop("ring") - elif "base_ring" in kwds: - base_ring = kwds.pop("base_ring") - elif "field" in kwds: - base_ring = kwds.pop("field") + sage: p.angles() + (2/9, 4/9, 2/9, 4/9, 4/9, 2/9) - convex = kwds.pop("convex", True) + sage: EuclideanPolygonsWithAngles(1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1) + Category of simple euclidean pentadecagons with angles (13/46, 13/23, 13/46, 13/23, 13/46, 13/23, 13/46, 13/23, 13/23, 13/23, 13/23, 13/46, 13/46, 13/23, 13/46) over Number Field in c with defining polynomial ... - vertices = None - edges = None - angles = None - base_point = None - length = None - lengths = None + A regular pentagon:: - if "edges" in kwds: - edges = kwds.pop("edges") - base_point = kwds.pop("base_point", (0, 0)) - elif "vertices" in kwds: - vertices = kwds.pop("vertices") - elif "angles" in kwds: - angles = kwds.pop("angles") - lengths = kwds.pop("lengths", None) - length = kwds.pop("length", None) - base_point = kwds.pop("base_point", (0, 0)) - number_field = kwds.pop("number_field", True) - elif args: - edges = args - args = () - base_point = kwds.pop("base_point", (0, 0)) - - if (vertices is not None) + (edges is not None) + (angles is not None) != 1: - raise ValueError( - "exactly one of 'vertices', 'edges' or 'angles' should be provided" - ) + sage: E = EuclideanPolygonsWithAngles(1, 1, 1, 1, 1) + sage: E(1, 1, 1, 1, 1, normalized=True) + doctest:warning + ... + UserWarning: calling EuclideanPolygonsWithAngles() has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon(angles=[...], lengths=[...]) instead. + Polygon(vertices=[(0, 0), (1, 0), (1/2*c^2 - 1/2, 1/2*c), (1/2, 1/2*c^3 - c), (-1/2*c^2 + 3/2, 1/2*c)]) - if vertices is None and edges is None and angles is None and lengths is None: - raise ValueError("either vertices, edges or angles should be provided") - if args: - raise ValueError("invalid argument {!r}".format(args)) - if kwds: - raise ValueError("invalid keyword {!r}".format(next(iter(kwds)))) - - if vertices is not None: - vertices = list(map(vector, vertices)) - if base_ring is None: - base_ring = Sequence( - [x for x, _ in vertices] + [y for _, y in vertices] - ).universe() - if isinstance(base_ring, type): - base_ring = py_scalar_parent(base_ring) - - elif edges is not None: - edges = list(map(vector, edges)) - if base_ring is None: - base_ring = Sequence( - [x for x, _ in edges] + [y for _, y in edges] + list(base_point) - ).universe() - if isinstance(base_ring, type): - base_ring = py_scalar_parent(base_ring) - - elif angles is not None: - E = EquiangularPolygons(*angles) - if convex and not E.convexity(): - raise ValueError( - "'angles' do not determine convex polygon; you might want to set the option 'convex=False'" - ) - n = len(angles) - if length is not None and lengths is not None: - raise ValueError( - "only one of 'length' or 'lengths' can be set together with 'angles'" - ) - if lengths is None: - if not E.convexity(): - raise ValueError( - "non-convex equiangular polygon; lengths must be provided" - ) - lengths = [length or 1] * (n - 2) + """ + if len(angles) == 1 and isinstance(angles[0], (tuple, list)): + angles = angles[0] - if len(lengths) != n - 2: - raise ValueError( - "'lengths' must be a list of n-2 numbers (one less than 'angles')" - ) + angles = EuclideanPolygonsWithAnglesCategory._normalize_angles(angles) - return E(lengths, normalized=True) + from flatsurf.geometry.categories.euclidean_polygons_with_angles import ( + _base_ring, + ) - if base_ring is ZZ: - # Typically, we do not want to go to the fraction field of the base - # ring, e.g., we do not want to go to FractionField(ExactReals()). - # However, manual input of parameters often leads to the - # automatically detected base ring ZZ which is essentially never - # what the user wanted. - base_ring = QQ + base_ring = _base_ring(angles) - if convex: - return ConvexPolygons(base_ring)( - vertices=vertices, edges=edges, base_point=base_point - ) - else: - return Polygons(base_ring)( - vertices=vertices, edges=edges, base_point=base_point - ) + return EuclideanPolygons(base_ring).WithAngles(angles).Simple() -polygons = PolygonsConstructor() +def EquiangularPolygons(*angles, **kwds): + r""" + EXAMPLES:: + sage: from flatsurf import EquiangularPolygons + sage: EquiangularPolygons(1, 1, 1) + doctest:warning + ... + UserWarning: EquiangularPolygons() has been deprecated and will be removed in a future version of sage-flatsurf; use EuclideanPolygonsWithAngles() instead + Category of simple euclidean equilateral triangles over Number Field in c with defining polynomial x^2 - 3 with c = 1.732050807568878? -class PolygonCreator: - r""" - Class for iteratively constructing a polygon over the field. """ + import warnings - def __init__(self, field=QQ): - r"""Create a polygon in the provided field.""" - self._v = [] - self._w = [] - - if field not in Fields(): - raise TypeError("field must be a field") + warnings.warn( + "EquiangularPolygons() has been deprecated and will be removed in a future version of sage-flatsurf; use EuclideanPolygonsWithAngles() instead" + ) - self._field = field + if "number_field" in kwds: + from warnings import warn - def vector_space(self): - r""" - Return the vector space in which self naturally embeds. - """ - return VectorSpace(self._field, 2) + warn( + "The number_field parameter has been removed in this release of sage-flatsurf. " + "To create an equiangular polygon over a number field, do not pass this parameter; to create an equiangular polygon over the algebraic numbers, do not pass this parameter but call the returned object with algebraic lengths." + ) + kwds.pop("number_field") - def add_vertex(self, new_vertex): - r""" - Add a vertex to the polygon. - Returns 1 if successful and 0 if not, in which case the resulting - polygon would not have been convex. - """ - V = self.vector_space() - newv = V(new_vertex) - if len(self._v) == 0: - self._v.append(newv) - self._w.append(V.zero()) - return 1 - if len(self._v) == 1: - if self._v[0] == newv: - return 0 - else: - self._w[-1] = newv - self._v[-1] - self._w.append(self._v[0] - newv) - self._v.append(newv) - return 1 - if len(self._v) >= 2: - neww1 = newv - self._v[-1] - if wedge_product(self._w[-2], neww1) <= 0: - return 0 - neww2 = self._v[0] - newv - if wedge_product(neww1, neww2) <= 0: - return 0 - if wedge_product(neww2, self._w[0]) <= 0: - return 0 - self._w[-1] = newv - self._v[-1] - self._w.append(self._v[0] - newv) - self._v.append(newv) - return 1 + if kwds: + raise ValueError("invalid keyword {!r}".format(next(iter(kwds)))) - def get_polygon(self): - r""" - Return the polygon. - Raises a ValueError if less than three vertices have been accepted. - """ - if len(self._v) < 2: - raise ValueError("Not enough vertices!") - return ConvexPolygons(self._field)(self._w) + return EuclideanPolygonsWithAngles(*angles) diff --git a/flatsurf/geometry/polyhedra.py b/flatsurf/geometry/polyhedra.py index 4eafda301..0c823260d 100644 --- a/flatsurf/geometry/polyhedra.py +++ b/flatsurf/geometry/polyhedra.py @@ -28,9 +28,6 @@ from sage.geometry.polyhedron.constructor import Polyhedron from sage.functions.other import sqrt -from flatsurf.geometry.polygon import ConvexPolygons -from flatsurf.geometry.surface import surface_list_from_polygons_and_gluings -from flatsurf.geometry.cone_surface import ConeSurface from flatsurf.geometry.straight_line_trajectory import ( StraightLineTrajectory, SegmentInPolygon, @@ -102,9 +99,9 @@ def __eq__(self, other): ....: for j in range(-1,3,2): ....: vertices.append(j*temp) sage: octahedron=Polyhedron(vertices=vertices) - sage: surface, surface_to_octahedron = polyhedron_to_cone_surface(octahedron,scaling_factor=AA(1/sqrt(2))) + sage: surface, surface_to_octahedron = polyhedron_to_cone_surface(octahedron,scaling_factor=AA(1/sqrt(2))) # long time (.5s) - sage: surface_to_octahedron == surface_to_octahedron + sage: surface_to_octahedron == surface_to_octahedron # long time (see above) True """ @@ -125,9 +122,9 @@ def __ne__(self, other): ....: for j in range(-1,3,2): ....: vertices.append(j*temp) sage: octahedron=Polyhedron(vertices=vertices) - sage: surface, surface_to_octahedron = polyhedron_to_cone_surface(octahedron,scaling_factor=AA(1/sqrt(2))) + sage: surface, surface_to_octahedron = polyhedron_to_cone_surface(octahedron,scaling_factor=AA(1/sqrt(2))) # long time (.3s) - sage: surface_to_octahedron != surface_to_octahedron + sage: surface_to_octahedron != surface_to_octahedron # long time (see above) False """ @@ -135,12 +132,13 @@ def __ne__(self, other): def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): - r"""Construct the Euclidean Cone Surface associated to the surface of a polyhedron and a map + r""" + Construct the Euclidean Cone Surface associated to the surface of a polyhedron and a map from the cone surface to the polyhedron. INPUT: - - ``polyhedron`` -- A 3-dimensional polyhedron, which should be define over something that coerces into AA + - ``polyhedron`` -- A 3-dimensional polyhedron, which should be defined over something that coerces into AA - ``use_AA`` -- If True, the surface returned will be defined over AA. If false, the algorithm will find the smallest NumberField and write the field there. @@ -152,7 +150,7 @@ def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): EXAMPLES:: - sage: from flatsurf.geometry.polyhedra import * + sage: from flatsurf.geometry.polyhedra import Polyhedron, polyhedron_to_cone_surface sage: vertices=[] sage: for i in range(3): ....: temp=vector([1 if k==i else 0 for k in range(3)]) @@ -162,8 +160,8 @@ def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): sage: surface,surface_to_octahedron = \ ....: polyhedron_to_cone_surface(octahedron,scaling_factor=AA(1/sqrt(2))) sage: TestSuite(surface).run() - sage: TestSuite(surface_to_octahedron).run() - sage: surface.num_polygons() + sage: TestSuite(surface_to_octahedron).run() # long time (.4s) + sage: len(surface.polygons()) 8 sage: surface.base_ring() Number Field in a with defining polynomial y^2 - 3 with a = 1.732050807568878? @@ -245,13 +243,13 @@ def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): w = w / AA(w.norm()) m = 1 / scaling_factor * matrix(AA, [w, n.cross_product(w), n]).transpose() mi = ~m - mis = mi.submatrix(0, 0, 2, 3) + mi_submatrix = mi.submatrix(0, 0, 2, 3) face_map_data.append( ( v0, # translation to bring origin in plane to v0 m.submatrix(0, 0, 3, 2), - -mis * v0, - mis, + -mi_submatrix * v0, + mi_submatrix, ) ) @@ -291,12 +289,17 @@ def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): m = face_map_data[p][3] polygon_vertices_AA.append([trans + m * v for v in vs]) + from flatsurf import MutableOrientedSimilaritySurface + if use_AA is True: - Polys = ConvexPolygons(AA) - polygons = [] + from flatsurf import Polygon + + S = MutableOrientedSimilaritySurface(AA) for vs in polygon_vertices_AA: - polygons.append(Polys(vertices=vs)) - S = ConeSurface(surface_list_from_polygons_and_gluings(polygons, gluings)) + S.add_polygon(Polygon(vertices=vs, base_ring=AA)) + for x, y in gluings.items(): + S.glue(x, y) + S.set_immutable() return S, ConeSurfaceToPolyhedronMap(S, polyhedron, face_map_data) else: elts = [] @@ -317,11 +320,14 @@ def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): vs2.append(vector(field, [elts2[j], elts2[j + 1]])) j = j + 2 polygon_vertices_field2.append(vs2) - Polys = ConvexPolygons(field) - polygons = [] + S = MutableOrientedSimilaritySurface(field) + from flatsurf import Polygon + for vs in polygon_vertices_field2: - polygons.append(Polys(vertices=vs)) - S = ConeSurface(surface_list_from_polygons_and_gluings(polygons, gluings)) + S.add_polygon(Polygon(vertices=vs, base_ring=field)) + for x, y in gluings.items(): + S.glue(x, y) + S.set_immutable() return S, ConeSurfaceToPolyhedronMap(S, polyhedron, face_map_data) else: @@ -341,11 +347,14 @@ def polyhedron_to_cone_surface(polyhedron, use_AA=False, scaling_factor=ZZ(1)): vs2.append(vector(field2, [hom2(elts2[j]), hom2(elts2[j + 1])])) j = j + 2 polygon_vertices_field2.append(vs2) - Polys = ConvexPolygons(field2) - polygons = [] + S = MutableOrientedSimilaritySurface(field2) + from flatsurf import Polygon + for vs in polygon_vertices_field2: - polygons.append(Polys(vertices=vs)) - S = ConeSurface(surface_list_from_polygons_and_gluings(polygons, gluings)) + S.add_polygon(Polygon(vertices=vs, base_ring=field2)) + for x, y in gluings.items(): + S.glue(x, y) + S.set_immutable() return S, ConeSurfaceToPolyhedronMap(S, polyhedron, face_map_data) @@ -398,8 +407,8 @@ def platonic_octahedron(): EXAMPLES:: sage: from flatsurf.geometry.polyhedra import platonic_octahedron - sage: polyhedron,surface,surface_to_polyhedron = platonic_octahedron() - sage: TestSuite(surface).run() + sage: polyhedron,surface,surface_to_polyhedron = platonic_octahedron() # long time (.3s) + sage: TestSuite(surface).run() # long time (see above) """ vertices = [] for i in range(3): @@ -421,8 +430,8 @@ def platonic_dodecahedron(): EXAMPLES:: sage: from flatsurf.geometry.polyhedra import platonic_dodecahedron - sage: polyhedron,surface,surface_to_polyhedron = platonic_dodecahedron() - sage: TestSuite(surface).run() + sage: polyhedron, surface, surface_to_polyhedron = platonic_dodecahedron() # long time (1s) + sage: TestSuite(surface).run() # long time (.8s) """ vertices = [] phi = AA(1 + sqrt(5)) / 2 @@ -451,8 +460,8 @@ def platonic_icosahedron(): EXAMPLES:: sage: from flatsurf.geometry.polyhedra import platonic_icosahedron - sage: polyhedron,surface,surface_to_polyhedron = platonic_icosahedron() - sage: TestSuite(surface).run() + sage: polyhedron,surface,surface_to_polyhedron = platonic_icosahedron() # long time (.9s) + sage: TestSuite(surface).run() # long time (see above) """ vertices = [] phi = AA(1 + sqrt(5)) / 2 diff --git a/flatsurf/geometry/pyflatsurf_conversion.py b/flatsurf/geometry/pyflatsurf_conversion.py index 67b260adb..a49e9f219 100644 --- a/flatsurf/geometry/pyflatsurf_conversion.py +++ b/flatsurf/geometry/pyflatsurf_conversion.py @@ -83,21 +83,21 @@ def to_pyflatsurf(S): Given S a translation surface from sage-flatsurf return a flatsurf::FlatTriangulation from libflatsurf/pyflatsurf. """ - from flatsurf.geometry.translation_surface import TranslationSurface + from flatsurf.geometry.categories import TranslationSurfaces - if not isinstance(S, TranslationSurface): + if S not in TranslationSurfaces(): raise TypeError("S must be a translation surface") - if not S.is_finite(): + if not S.is_finite_type(): raise ValueError("the surface S must be finite") S = S.triangulate() # populate half edges and vectors - n = sum(S.polygon(lab).num_edges() for lab in S.label_iterator()) + n = sum(len(S.polygon(lab).vertices()) for lab in S.labels()) half_edge_labels = {} # map: (face lab, edge num) in faltsurf -> integer vec = [] # vectors k = 1 # half edge label in {1, ..., n} - for t0, t1 in S.edge_iterator(gluings=True): + for t0, t1 in S.gluings(): if t0 in half_edge_labels: continue @@ -113,9 +113,9 @@ def to_pyflatsurf(S): # compute vertex and face permutations vp = [None] * (n + 1) # vertex permutation fp = [None] * (n + 1) # face permutation - for t in S.edge_iterator(gluings=False): + for t in S.edges(): e = half_edge_labels[t] - j = (t[1] + 1) % S.polygon(t[0]).num_edges() + j = (t[1] + 1) % len(S.polygon(t[0]).vertices()) fp[e] = half_edge_labels[(t[0], j)] vp[fp[e]] = -e @@ -261,29 +261,33 @@ def from_pyflatsurf(T): sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.pyflatsurf_conversion import to_pyflatsurf, from_pyflatsurf # optional: pyflatsurf sage: S = translation_surfaces.veech_double_n_gon(5) # optional: pyflatsurf - sage: from_pyflatsurf(to_pyflatsurf(S)) # optional: pyflatsurf - TranslationSurface built from 6 polygons + sage: T = from_pyflatsurf(to_pyflatsurf(S)) # optional: pyflatsurf + sage: T # optional: pyflatsurf + Translation Surface in H_2(2) built from 6 isosceles triangles - TESTS: + TESTS:: + + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: T in TranslationSurfaces() # optional: pyflatsurf + True Verify that #137 has been resolved:: - sage: from flatsurf import polygons - sage: from flatsurf.geometry.surface import Surface_list - sage: from flatsurf.geometry.translation_surface import TranslationSurface + sage: from flatsurf import polygons, MutableOrientedSimilaritySurface sage: from flatsurf.geometry.gl2r_orbit_closure import GL2ROrbitClosure sage: from flatsurf.geometry.pyflatsurf_conversion import from_pyflatsurf sage: P = polygons.regular_ngon(10) - sage: S = Surface_list(P.base_ring()) + sage: S = MutableOrientedSimilaritySurface(P.base_ring()) sage: S.add_polygon(P) 0 - sage: for i in range(5): S.set_edge_pairing(0, i, 0, 5+i) - sage: M = TranslationSurface(S) + sage: for i in range(5): S.glue((0, i), (0, 5+i)) + sage: S.set_immutable() + sage: M = S sage: X = GL2ROrbitClosure(M) # optional: pyflatsurf sage: D0 = list(X.decompositions(2))[2] # optional: pyflatsurf sage: T0 = D0.triangulation() # optional: pyflatsurf sage: from_pyflatsurf(T0) # optional: pyflatsurf - TranslationSurface built from 8 polygons + Translation Surface in H_2(1^2) built from 2 isosceles triangles and 6 triangles """ from flatsurf.features import pyflatsurf_feature @@ -293,15 +297,13 @@ def from_pyflatsurf(T): ring = sage_ring(T) - from flatsurf.geometry.surface import Surface_list - - S = Surface_list(ring) + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface - from flatsurf.geometry.polygon import ConvexPolygons + S = MutableOrientedSimilaritySurface(ring) - P = ConvexPolygons(ring) + from flatsurf.geometry.polygon import Polygon - V = P.module() + V = ring**2 half_edges = {} @@ -312,7 +314,7 @@ def from_pyflatsurf(T): vectors = [ V([ring(to_sage_ring(v.x())), ring(to_sage_ring(v.y()))]) for v in vectors ] - triangle = P(vectors) + triangle = Polygon(edges=vectors) face_id = S.add_polygon(triangle) assert a not in half_edges @@ -324,10 +326,7 @@ def from_pyflatsurf(T): for half_edge, (face, id) in half_edges.items(): _face, _id = half_edges[-half_edge] - S.change_edge_gluing(face, id, _face, _id) + S.glue((face, id), (_face, _id)) S.set_immutable() - - from flatsurf.geometry.translation_surface import TranslationSurface - - return TranslationSurface(S) + return S diff --git a/flatsurf/geometry/rational_cone_surface.py b/flatsurf/geometry/rational_cone_surface.py deleted file mode 100644 index d9e4c6b93..000000000 --- a/flatsurf/geometry/rational_cone_surface.py +++ /dev/null @@ -1,32 +0,0 @@ -from .cone_surface import ConeSurface -from .matrix_2x2 import is_cosine_sine_of_rational - - -class RationalConeSurface(ConeSurface): - r""" - A Euclidean cone surface such that the rotational part of the monodromy around any loop - is a finite order rotation. - """ - - def _test_edge_matrix(self, **options): - r""" - Check the compatibility condition - """ - tester = self._tester(**options) - - from .similarity_surface import SimilaritySurface - - if self.is_finite(): - it = self.label_iterator() - else: - from itertools import islice - - it = islice(self.label_iterator(), 30) - - for lab in it: - p = self.polygon(lab) - for e in range(p.num_edges()): - # Warning: check the matrices computed from the edges, - # rather the ones overridden by TranslationSurface. - m = SimilaritySurface.edge_matrix(self, lab, e) - tester.assertTrue(is_cosine_sine_of_rational(m[0][0], m[0][1])) diff --git a/flatsurf/geometry/rational_similarity_surface.py b/flatsurf/geometry/rational_similarity_surface.py deleted file mode 100644 index fd68ebb3d..000000000 --- a/flatsurf/geometry/rational_similarity_surface.py +++ /dev/null @@ -1,76 +0,0 @@ -from .similarity_surface import SimilaritySurface -from .matrix_2x2 import is_cosine_sine_of_rational - - -class RationalSimilaritySurface(SimilaritySurface): - r""" - A similarity surface such that the monodromy around any loop is similarity - whose rotational part has finite order. - - EXAMPLES:: - - sage: from flatsurf import * - sage: s = Surface_list(AA) - sage: CP = ConvexPolygons(AA) - sage: s.add_polygon(CP(vertices=[(0,0),(2,0),(1,1)])) - 0 - sage: s.add_polygon(CP(vertices=[(0,0),(1,0),(1,sqrt(3))])) - 1 - sage: for i in range(3): - ....: s.change_edge_gluing(0,i,1,i) - sage: s.change_base_label(0) - sage: s.set_immutable() - sage: TestSuite(s).run() - sage: from flatsurf.geometry.rational_similarity_surface import RationalSimilaritySurface - sage: ss = RationalSimilaritySurface(s) - sage: TestSuite(ss).run() - - Example of a similarity surface which is not rational:: - - sage: from flatsurf import * - sage: s = Surface_list(QQ) - sage: CP = ConvexPolygons(QQ) - sage: s.add_polygon(CP(vertices=[(0,0),(2,0),(1,1)])) - 0 - sage: s.add_polygon(CP(vertices=[(0,0),(1,0),(1,2)])) - 1 - sage: for i in range(3): - ....: s.change_edge_gluing(0,i,1,i) - sage: s.change_base_label(0) - sage: s.set_immutable() - sage: TestSuite(s).run() - sage: from flatsurf.geometry.rational_similarity_surface import RationalSimilaritySurface - sage: ss = RationalSimilaritySurface(s) - sage: TestSuite(ss).run() - ... - The following tests failed: _test_edge_matrix - """ - - def _test_edge_matrix(self, **options): - r""" - Check the compatibility condition - """ - tester = self._tester(**options) - - from .similarity_surface import SimilaritySurface - from sage.rings.qqbar import AA - - if self.is_finite(): - it = self.label_iterator() - else: - from itertools import islice - - it = islice(self.label_iterator(), 30) - - for lab in it: - p = self.polygon(lab) - for e in range(p.num_edges()): - # Warning: check the matrices computed from the edges, - # rather the ones overridden by TranslationSurface. - m = SimilaritySurface.edge_matrix(self, lab, e) - a = AA(m[0, 0]) - b = AA(m[1, 0]) - q = (a**2 + b**2).sqrt() - a /= q - b /= q - tester.assertTrue(is_cosine_sine_of_rational(a, b)) diff --git a/flatsurf/geometry/relative_homology.py b/flatsurf/geometry/relative_homology.py index f9cdc5d6d..ff9005268 100644 --- a/flatsurf/geometry/relative_homology.py +++ b/flatsurf/geometry/relative_homology.py @@ -34,8 +34,6 @@ from sage.modules.module import Module from sage.rings.integer_ring import ZZ -from .similarity_surface import SimilaritySurface - def cmp(x, y): r""" @@ -134,7 +132,9 @@ class RelativeHomology(Module): def __init__(self, surface, base_ring=ZZ): self._base_ring = base_ring - if not isinstance(surface, SimilaritySurface): + from flatsurf.geometry.categories import SimilaritySurfaces + + if surface not in SimilaritySurfaces(): raise ValueError( "RelativeHomology only defined for SimilaritySurfaces (and better)." ) @@ -176,7 +176,7 @@ def edge(self, label, e): return self._cached_edges[(label, e)] except KeyError: # not cached! - num_edges = self._s.polygon(label).num_edges() + num_edges = len(self._s.polygon(label).vertices()) # Check to see if all other edges of the polygon are cached. has_all_others = True for i in range(1, num_edges): diff --git a/flatsurf/geometry/similarity.py b/flatsurf/geometry/similarity.py index 4cf976499..4afd6a869 100644 --- a/flatsurf/geometry/similarity.py +++ b/flatsurf/geometry/similarity.py @@ -1,4 +1,4 @@ -# ********************************************************************* +# **************************************************************************** # This file is part of sage-flatsurf. # # Copyright (C) 2016-2020 Vincent Delecroix @@ -34,8 +34,6 @@ from sage.structure.element import is_Matrix -from flatsurf.geometry.polygon import ConvexPolygon, ConvexPolygons - ZZ_0 = Integer(0) ZZ_1 = Integer(1) ZZ_m1 = -ZZ_1 @@ -254,9 +252,10 @@ def __hash__(self): def __call__(self, w, ring=None): r""" - Return the image of ``w`` under the similarity. Here ``w`` may be a ConvexPolygon or a vector - (or something that can be indexed in the same way as a vector). If a ring is provided, - the objects returned will be defined over this ring. + Return the image of ``w`` under the similarity. Here ``w`` may be a + convex polygon or a vector (or something that can be indexed in the + same way as a vector). If a ring is provided, the objects returned will + be defined over this ring. TESTS:: @@ -270,35 +269,36 @@ def __call__(self, w, ring=None): sage: from flatsurf.geometry.similarity import SimilarityGroup sage: SG = SimilarityGroup(QQ) - sage: from flatsurf import ConvexPolygons - sage: P = ConvexPolygons(QQ) - sage: p = P.an_element() - sage: p - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) + sage: from flatsurf import Polygon + sage: p = Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) sage: g = SG.an_element()**2 sage: g (x, y) |-> (25*x + 4, 25*y + 10) sage: g(p) - Polygon: (4, 10), (29, 10), (29, 35), (4, 35) - sage: g(p, ring=AA).parent() - ConvexPolygons(Algebraic Real Field) + Polygon(vertices=[(4, 10), (29, 10), (29, 35), (4, 35)]) + sage: g(p, ring=AA).category() + Category of convex simple euclidean polygons over Algebraic Real Field + """ if ring is not None and ring not in Rings(): raise TypeError("ring must be a ring") - if isinstance(w, ConvexPolygon): + from flatsurf.geometry.polygon import EuclideanPolygon + + if isinstance(w, EuclideanPolygon) and w.is_convex(): if ring is None: ring = self.parent().base_ring() - P = ConvexPolygons(ring) + + from flatsurf import Polygon try: - return P(vertices=[self(v) for v in w.vertices()]) - except ValueError as e: + return Polygon(vertices=[self(v) for v in w.vertices()], base_ring=ring) + except ValueError: if not self._sign.is_one(): raise ValueError("Similarity must be orientation preserving.") - else: - # Not sure why this would happen: - raise + + # Not sure why this would happen: + raise if ring is None: if self._sign.is_one(): @@ -613,3 +613,77 @@ def is_abelian(self): def base_ring(self): return self._ring + + +def similarity_from_vectors(u, v, matrix_space=None): + r""" + Return the unique similarity matrix that maps ``u`` to ``v``. + + EXAMPLES:: + + sage: from flatsurf.geometry.similarity import similarity_from_vectors + + sage: V = VectorSpace(QQ,2) + sage: u = V((1,0)) + sage: v = V((0,1)) + sage: m = similarity_from_vectors(u,v); m + [ 0 -1] + [ 1 0] + sage: m*u == v + True + + sage: u = V((2,1)) + sage: v = V((1,-2)) + sage: m = similarity_from_vectors(u,v); m + [ 0 1] + [-1 0] + sage: m * u == v + True + + An example built from the Pythagorean triple 3^2 + 4^2 = 5^2:: + + sage: u2 = V((5,0)) + sage: v2 = V((3,4)) + sage: m = similarity_from_vectors(u2,v2); m + [ 3/5 -4/5] + [ 4/5 3/5] + sage: m * u2 == v2 + True + + Some test over number fields:: + + sage: K. = NumberField(x^2-2, embedding=1.4142) + sage: V = VectorSpace(K,2) + sage: u = V((sqrt2,0)) + sage: v = V((1, 1)) + sage: m = similarity_from_vectors(u,v); m + [ 1/2*sqrt2 -1/2*sqrt2] + [ 1/2*sqrt2 1/2*sqrt2] + sage: m*u == v + True + + sage: m = similarity_from_vectors(u, 2*v); m + [ sqrt2 -sqrt2] + [ sqrt2 sqrt2] + sage: m*u == 2*v + True + + """ + if u.parent() is not v.parent(): + raise ValueError + + if matrix_space is None: + from sage.matrix.matrix_space import MatrixSpace + + matrix_space = MatrixSpace(u.base_ring(), 2) + + if u == v: + return matrix_space.one() + + sqnorm_u = u[0] * u[0] + u[1] * u[1] + cos_uv = (u[0] * v[0] + u[1] * v[1]) / sqnorm_u + sin_uv = (u[0] * v[1] - u[1] * v[0]) / sqnorm_u + + m = matrix_space([cos_uv, -sin_uv, sin_uv, cos_uv]) + m.set_immutable() + return m diff --git a/flatsurf/geometry/similarity_surface.py b/flatsurf/geometry/similarity_surface.py deleted file mode 100644 index 9f5bd8fe2..000000000 --- a/flatsurf/geometry/similarity_surface.py +++ /dev/null @@ -1,2499 +0,0 @@ -r""" -Similarity surfaces. -""" -# ********************************************************************* -# This file is part of sage-flatsurf. -# -# Copyright (C) 2016-2020 Vincent Delecroix -# 2020-2023 Julian Rüth -# -# sage-flatsurf is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# sage-flatsurf is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with sage-flatsurf. If not, see . -# ********************************************************************* - -from sage.misc.cachefunc import cached_method -from sage.misc.sage_unittest import TestSuite - -from sage.structure.sage_object import SageObject - -from sage.rings.infinity import Infinity - -from sage.rings.all import ZZ, QQ, AA, NumberField - -from sage.modules.free_module_element import vector - -from .similarity import SimilarityGroup -from .polygon import ConvexPolygons, wedge_product - -from .surface import Surface, Surface_dict, Surface_list, LabelComparator -from .surface_objects import Singularity, SaddleConnection, SurfacePoint -from .circle import Circle -from .matrix_2x2 import similarity_from_vectors - -ZZ_1 = ZZ.one() -ZZ_2 = ZZ_1 + ZZ_1 - - -class SimilaritySurface(SageObject): - r""" - An oriented surface built from a set of polygons and edges identified with - similarities (i.e. composition of homothety, rotations and translations). - - Each polygon is identified with a unique key (its label). The choice of the - label of the polygons is done at startup. If the set is finite then by - default the labels are the first non-negative integers 0,1,... - - The edges are labeled by a pair ``(polygon label, edge number)``. - - EXAMPLES: - - The easiest way to construct a similarity surface is to use the pre-built - constructions from - :class:`flatsurf.geometry.similarity_surface_generators.SimilaritySurfaceGenerators`:: - - sage: from flatsurf import polygons, similarity_surfaces - sage: P = polygons(vertices=[(0,0), (2,0), (1,4), (0,5)]) - sage: similarity_surfaces.self_glued_polygon(P) - HalfTranslationSurface built from 1 polygon - - The second way is to build a surface (using e.g. :class:`flatsurf.geometry.surface.Surface_list`) - and then use this surface as an argument for class:`SimilaritySurface`):: - - sage: from flatsurf.geometry.similarity_surface import SimilaritySurface - sage: from flatsurf.geometry.surface import Surface_list - sage: P = polygons(vertices=[(0,0), (1,0), (1,1), (0,1)]) - sage: Stop = Surface_list(QQ) - sage: Stop.add_polygon(P) - 0 - sage: Stop.add_polygon(2*P) - 1 - sage: Stop.add_polygon(3*P) - 2 - sage: Stop.set_edge_pairing(0, 1, 1, 3) - sage: Stop.set_edge_pairing(0, 0, 2, 2) - sage: Stop.set_edge_pairing(0, 2, 2, 0) - sage: Stop.set_edge_pairing(0, 3, 1, 1) - sage: Stop.set_edge_pairing(1, 2, 2, 1) - sage: Stop.set_edge_pairing(1, 0, 2, 3) - sage: S = SimilaritySurface(Stop) - sage: S - SimilaritySurface built from 3 polygons - - To perform a sanity check on the obtained surface, you can run its test - suite:: - - sage: TestSuite(S).run() - - In the following example, we build two broken surfaces and - check that the test suite fails as expected:: - - sage: P = polygons(vertices=[(0,0), (1,0), (1,1), (0,1)]) - sage: Stop = Surface_list(QQ) - sage: Stop.add_polygon(P) - 0 - sage: S = SimilaritySurface(Stop) - sage: TestSuite(S).run() - ... - AssertionError: edge (0, 0) is not glued - ------------------------------------------------------------ - The following tests failed: _test_gluings - Failure in _test_underlying_surface - The following tests failed: _test_underlying_surface - - sage: Stop.set_edge_pairing(0, 0, 0, 3) - sage: Stop.set_edge_pairing(0, 1, 0, 3) - sage: Stop.set_edge_pairing(0, 2, 0, 3) - sage: S = SimilaritySurface(Stop) - sage: TestSuite(S).run() - ... - AssertionError: edge gluing is not a pairing: - (0, 0) -> (0, 3) -> (0, 2) - ------------------------------------------------------------ - The following tests failed: _test_gluings - Failure in _test_underlying_surface - The following tests failed: _test_underlying_surface - - Finally, you can also implement a similarity surface by inheriting from - :class:`SimilaritySurface` and implement the methods: - - - ``base_ring(self)``: the base ring in which coordinates lives - - - ``polygon(self, lab)``: the polygon associated to the label ``lab`` - - - ``base_label(self)``: which label to use as the base one - - - ``opposite_edge(self, lab, edge)``: a pair (``other_label``, - ``other_edge``) representing the edge being glued - - - ``is_finite(self)``: whether the surface is built from finitely many polygons - """ - - def __init__(self, surface): - r""" - TESTS:: - - sage: from flatsurf.geometry.similarity_surface import SimilaritySurface - sage: SimilaritySurface(3) - Traceback (most recent call last): - ... - TypeError: invalid argument surface=3 to build a similarity surface - """ - if isinstance(surface, SimilaritySurface): - self._s = surface.underlying_surface() - elif isinstance(surface, Surface): - self._s = surface - else: - raise TypeError( - "invalid argument surface={} to build a similarity surface".format( - surface - ) - ) - - @cached_method - def _matrix_space(self): - from sage.matrix.matrix_space import MatrixSpace - - return MatrixSpace(self.base_ring(), 2) - - def underlying_surface(self): - r""" - Return the surface underlying this SimilaritySurface. - """ - return self._s - - def _test_underlying_surface(self, **options): - is_sub_testsuite = "tester" in options - tester = self._tester(**options) - tester.info("") - - TestSuite(self._s).run( - verbose=tester._verbose, - prefix=tester._prefix + " ", - raise_on_failure=is_sub_testsuite, - ) - tester.info(tester._prefix + " ", newline=False) - - def base_ring(self): - r""" - The field on which the coordinates of ``self`` live. - - This method must be overridden in subclasses! - """ - return self._s.base_ring() - - def polygon(self, lab): - r""" - Return the polygon with label ``lab``. - """ - return self._s.polygon(lab) - - def base_label(self): - r""" - Always returns the same label. - """ - return self._s.base_label() - - def opposite_edge(self, label, e=None): - r""" - Given the label ``label`` of a polygon and an edge ``e`` in that polygon - returns the pair (``ll``, ``ee``) to which this edge is glued. - If e is not provided, then it expects the only parameter to be - the pair (``label``,``e``) and will again return a the pair (``ll``,``ee``). - """ - if e is None: - return self._s.opposite_edge(label[0], label[1]) - return self._s.opposite_edge(label, e) - - def is_finite(self): - r""" - Return whether or not the surface is finite. - """ - return self._s.is_finite() - - def is_mutable(self): - r""" - Return if the surface is mutable. - """ - return self._s.is_mutable() - - def set_immutable(self): - r""" - Mark the surface as immutable. - """ - self._s.set_immutable() - - def is_triangulated(self, limit=None): - return self._s.is_triangulated(limit=limit) - - # - # generic methods - # - - # def compute_surface_type_from_gluings(self,limit=None): - # r""" - # Compute the surface type by looking at the edge gluings. - # If limit is defined, we try to guess the type by looking at limit many edges. - # """ - # if limit is None: - # if not self.is_finite(): - # raise ValueError("Need a limit when working with an infinite surface.") - # it = self.edge_iterator() - # label,edge = it.next() - # # Use honest matrices! - # m = SimilaritySurface_generic.edge_matrix(self,label,edge) - # surface_type = surface_type_from_matrix(m) - # for label,edge in it: - # # Use honest matrices! - # m = SimilaritySurface_generic.edge_matrix(self,label,edge) - # surface_type = combine_surface_types(surface_type, surface_type_from_matrix(m)) - # return surface_type - # else: - # count=0 - # it = self.edge_iterator() - # label,edge = it.next() - # # Use honest matrices! - # m = SimilaritySurface_generic.edge_matrix(self,label,edge) - # surface_type = surface_type_from_matrix(m) - # for label,edge in it: - # # Use honest matrices! - # m = SimilaritySurface_generic.edge_matrix(self,label,edge) - # surface_type = combine_surface_types(surface_type, surface_type_from_matrix(m)) - # count=count+1 - # if count >= limit: - # return surface_type - # return surface_type - - def walker(self): - return self._s.walker() - - def label_iterator(self, polygons=False): - r""" - Iterator over all polygon labels. - - If the keyword polygons is True then we return pairs (label, polygon) - instead of just labels. - """ - if polygons: - return self._s.label_polygon_iterator() - else: - return self._s.label_iterator() - - def edge_iterator(self, gluings=False): - r""" - Iterate over the edges of polygons, which are pairs (l,e) where l is a polygon label, 0 <= e < N and N is the number of edges of the polygon with label l. - - If the keyword gluings is set to true, then we iterate over ordered - pairs of edges ((l,e),(ll,ee)) where edge (l,e) is glued to (ll,ee). - - EXAMPLES:: - - sage: from flatsurf import ConvexPolygons - sage: P = ConvexPolygons(QQ) - sage: tri0=P([(1,0),(0,1),(-1,-1)]) - sage: tri1=P([(-1,0),(0,-1),(1,1)]) - sage: gluings=[((0,0),(1,0)),((0,1),(1,1)),((0,2),(1,2))] - sage: from flatsurf.geometry.surface import surface_list_from_polygons_and_gluings - sage: from flatsurf.geometry.translation_surface import TranslationSurface - sage: s=TranslationSurface(surface_list_from_polygons_and_gluings([tri0,tri1], gluings)) - sage: for edge in s.edge_iterator(): - ....: print(edge) - (0, 0) - (0, 1) - (0, 2) - (1, 0) - (1, 1) - (1, 2) - """ - if gluings: - return self._s.edge_gluing_iterator() - else: - return self._s.edge_iterator() - - def num_polygons(self): - r""" - Return the number of polygons. - """ - return self._s.num_polygons() - - def num_edges(self): - r""" - Return the total number of edges of all polygons used. - """ - return self._s.num_edges() - - def num_singularities(self): - r""" - EXAMPLES:: - - sage: from flatsurf import * - - sage: translation_surfaces.regular_octagon().num_singularities() - 1 - - sage: S = SymmetricGroup(4) - sage: r = S('(1,2)(3,4)') - sage: u = S('(2,3)') - sage: translation_surfaces.origami(r,u).num_singularities() - 2 - - sage: S = SymmetricGroup(8) - sage: r = S('(1,2,3,4,5,6,7,8)') - sage: u = S('(1,8,5,4)(2,3)(6,7)') - sage: translation_surfaces.origami(r,u).num_singularities() - 4 - """ - if not self.is_finite(): - raise ValueError("the method only work for finite surfaces") - - # NOTE: - # the very same code is implemented in the method angles (translation - # surfaces). we should factor out the code - edges = set( - (p, e) - for p in self.label_iterator() - for e in range(self.polygon(p).num_edges()) - ) - - n = ZZ(0) - while edges: - p, e = edges.pop() - n += 1 - ee = (e - 1) % self.polygon(p).num_edges() - p, e = self.opposite_edge(p, ee) - while (p, e) in edges: - edges.remove((p, e)) - ee = (e - 1) % self.polygon(p).num_edges() - p, e = self.opposite_edge(p, ee) - return n - - def _repr_(self): - if self.num_polygons() == Infinity: - num = "infinitely many" - else: - num = str(self.num_polygons()) - - if self.num_polygons() == 1: - end = "" - else: - end = "s" - - return "{} built from {} polygon{}".format(self.__class__.__name__, num, end) - - @cached_method - def edge_matrix(self, p, e=None): - r""" - Returns the 2x2 matrix representing a similarity which when applied to the polygon with label `p` - makes it so the edge `e` can be glued to its opposite edge by translation. - - If `e` is not provided, then `p` should be a pair consisting of a polygon label and an edge. - - EXAMPLES:: - - sage: from flatsurf.geometry.similarity_surface_generators import SimilaritySurfaceGenerators - sage: s = SimilaritySurfaceGenerators.example() - sage: print(s.polygon(0)) - Polygon: (0, 0), (2, -2), (2, 0) - sage: print(s.polygon(1)) - Polygon: (0, 0), (2, 0), (1, 3) - sage: s.opposite_edge(0,0) - (1, 1) - sage: m = s.edge_matrix(0, 0) - sage: m - [ 1 1/2] - [-1/2 1] - sage: m * vector((2,-2)) == -vector((-1, 3)) - True - """ - if e is None: - import warnings - - warnings.warn("edge_matrix will now only take two arguments") - p, e = p - u = self.polygon(p).edge(e) - pp, ee = self.opposite_edge(p, e) - v = self.polygon(pp).edge(ee) - - # be careful, because of the orientation, it is -v and not v - return similarity_from_vectors(u, -v, self._matrix_space()) - - def edge_transformation(self, p, e): - r""" - Return the similarity bringing the provided edge to the opposite edge. - - EXAMPLES:: - - sage: from flatsurf.geometry.similarity_surface_generators import SimilaritySurfaceGenerators - sage: s = SimilaritySurfaceGenerators.example() - sage: print(s.polygon(0)) - Polygon: (0, 0), (2, -2), (2, 0) - sage: print(s.polygon(1)) - Polygon: (0, 0), (2, 0), (1, 3) - sage: print(s.opposite_edge(0,0)) - (1, 1) - sage: g = s.edge_transformation(0,0) - sage: g((0,0)) - (1, 3) - sage: g((2,-2)) - (2, 0) - """ - G = SimilarityGroup(self.base_ring()) - q = self.polygon(p) - a = q.vertex(e) - b = q.vertex(e + 1) - # This is the similarity carrying the origin to a and (1,0) to b: - g = G(b[0] - a[0], b[1] - a[1], a[0], a[1]) - - pp, ee = self.opposite_edge(p, e) - qq = self.polygon(pp) - # Be careful here: opposite vertices are identified - aa = qq.vertex(ee + 1) - bb = qq.vertex(ee) - # This is the similarity carrying the origin to aa and (1,0) to bb: - gg = G(bb[0] - aa[0], bb[1] - aa[1], aa[0], aa[1]) - - # This is the similarity carrying (a,b) to (aa,bb): - return gg / g - - def set_vertex_zero(self, label, v, in_place=False): - r""" - Applies a combinatorial rotation to the polygon with the provided label. - - This makes what is currently vertex v of this polygon vertex 0. In other words, - what is currently vertex (or edge) e will now become vertex (e-v)%n where - n is the number of sides of the polygon. - - For the updated polygons, the polygons will be translated so that vertex - 0 is the origin. - - EXAMPLES: - - Example with polygon glued to another polygon:: - - sage: from flatsurf import * - sage: s = translation_surfaces.veech_double_n_gon(4) - sage: s.polygon(0) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: [s.opposite_edge(0,i) for i in range(4)] - [(1, 0), (1, 1), (1, 2), (1, 3)] - sage: ss = s.set_vertex_zero(0,1) - sage: ss.polygon(0) - Polygon: (0, 0), (0, 1), (-1, 1), (-1, 0) - sage: [ss.opposite_edge(0,i) for i in range(4)] - [(1, 1), (1, 2), (1, 3), (1, 0)] - sage: TestSuite(ss).run() - - Example with polygon glued to self:: - - sage: s = translation_surfaces.veech_2n_gon(2) - sage: s.polygon(0) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: [s.opposite_edge(0,i) for i in range(4)] - [(0, 2), (0, 3), (0, 0), (0, 1)] - sage: ss = s.set_vertex_zero(0,3) - sage: ss.polygon(0) - Polygon: (0, 0), (0, -1), (1, -1), (1, 0) - sage: [ss.opposite_edge(0,i) for i in range(4)] - [(0, 2), (0, 3), (0, 0), (0, 1)] - sage: TestSuite(ss).run() - """ - if in_place: - us = self.underlying_surface() - if not us.is_mutable(): - raise ValueError( - "set_vertex_zero can only be done in_place for a mutable surface." - ) - p = us.polygon(label) - n = p.num_edges() - if not (0 <= v < n): - raise ValueError - glue = [] - P = ConvexPolygons(us.base_ring()) - pp = P(edges=[p.edge((i + v) % n) for i in range(n)]) - - for i in range(n): - e = (v + i) % n - ll, ee = us.opposite_edge(label, e) - if ll == label: - ee = (ee + n - v) % n - glue.append((ll, ee)) - - us.change_polygon(label, pp, gluing_list=glue) - return self - else: - return self.copy(mutable=True).set_vertex_zero(label, v, in_place=True) - - def _label_comparator(self): - r""" - Return a LabelComparator, which provides a fixed total ordering on the polygon labels. - """ - try: - return self._lc - except AttributeError: - self._lc = LabelComparator() - return self._lc - - def relabel(self, relabeling_map, in_place=False): - r""" - Attempt to relabel the polygons according to a relabeling_map, which takes as input - a current label and outputs a new label for the same polygon. The method returns a pair - (surface,success) where surface is the relabeled surface, and success is a boolean value - indicating the success of the operation. The operation will fail if the implementation of the - underlying surface does not support labels used in the image of the relabeling map. In this case, - other (arbitrary) labels will be used to replace the labels of the surface, and the resulting - surface should still be okay. - - Currently, the relabeling_map must be a dictionary. - - If in_place is True then the relabeling is done to the current surface, otherwise a - mutable copy is made before relabeling. - - ToDo: - - Allow relabeling_map to be a function rather than just a dictionary. - This will allow it to work for infinite surfaces. - - EXAMPLES:: - - sage: from flatsurf import * - sage: s=translation_surfaces.veech_double_n_gon(5) - sage: ss,valid=s.relabel({0:1,1:2}) - sage: valid - True - sage: ss.base_label() - 1 - sage: ss.opposite_edge(1,0) - (2, 0) - sage: ss.num_polygons() - 2 - sage: TestSuite(ss).run() - """ - if in_place: - us = self.underlying_surface() - if not us.is_mutable(): - raise ValueError( - "Your surface is not mutable, so can not be relabeled in place." - ) - if not isinstance(relabeling_map, dict): - raise NotImplementedError( - "Currently relabeling is only implemented via a dictionary." - ) - domain = set() - codomain = set() - data = {} - for l1, l2 in relabeling_map.items(): - p = us.polygon(l1) - glue = [] - for e in range(p.num_edges()): - ll, ee = us.opposite_edge(l1, e) - try: - lll = relabeling_map[ll] - except KeyError: - lll = ll - glue.append((lll, ee)) - data[l2] = (p, glue) - domain.add(l1) - codomain.add(l2) - if len(domain) != len(codomain): - raise ValueError( - "The relabeling_map must be injective. Received " - + str(relabeling_map) - ) - changed_labels = domain.intersection(codomain) - added_labels = codomain.difference(domain) - removed_labels = domain.difference(codomain) - # Pass to add_polygons - relabel_errors = {} - for l2 in added_labels: - p, glue = data[l2] - l3 = us.add_polygon(p, label=l2) - if not l2 == l3: - # This means the label l2 could not be added for some reason. - # Perhaps the implementation does not support this type of label. - # Or perhaps there is already a polygon with this label. - relabel_errors[l2] = l3 - # Pass to change polygons - for l2 in changed_labels: - p, glue = data[l2] - # This should always work since the domain of the relabeling map should be labels for polygons. - us.change_polygon(l2, p) - # Deal with the base_label - base_label = us.base_label() - if base_label in relabeling_map: - base_label = relabeling_map[base_label] - if base_label in relabel_errors: - base_label = relabel_errors[base_label] - us.change_base_label(base_label) - # Pass to remove polygons: - for l1 in removed_labels: - us.remove_polygon(l1) - # Pass to update the edge gluings - if len(relabel_errors) == 0: - # No problems. Update the gluings. - for l2 in codomain: - p, glue = data[l2] - us.change_polygon_gluings(l2, glue) - else: - # Use the gluings provided by relabel_errors when necessary - for l2 in codomain: - p, glue = data[l2] - for e in range(p.num_edges()): - ll, ee = glue[e] - try: - # First try the error dictionary - us.change_edge_gluing(l2, e, relabel_errors[ll], ee) - except KeyError: - us.change_edge_gluing(l2, e, ll, ee) - return self, len(relabel_errors) == 0 - else: - return self.copy(mutable=True).relabel(relabeling_map, in_place=True) - - def copy( - self, - relabel=False, - mutable=False, - lazy=None, - new_field=None, - optimal_number_field=False, - ): - r""" - Returns a copy of this surface. The method takes several flags to modify how the copy is taken. - - If relabel is True, then instead of returning an exact copy, it returns a copy indexed by the - non-negative integers. This uses the Surface_list implementation. If relabel is False (default), - then we return an exact copy. The returned surface uses the Surface_dict implementation. - - The mutability flag returns if the resulting surface should be mutable or not. By default, the - resulting surface will not be mutable. - - If lazy is True, then the surface is copied by reference. This is the only type of copy - possible for infinite surfaces. The parameter defaults to False for finite surfaces, and - defaults to True for infinite surfaces. - - The new_field parameter can be used to place the vertices in a larger field than the basefield - for the original surface. - - The optimal_number_field option can be used to find a best NumberField containing the - (necessarily finite) surface. - - EXAMPLES:: - - sage: from flatsurf import * - sage: ss=translation_surfaces.ward(3) - sage: ss.is_mutable() - False - sage: s=ss.copy(mutable=True) - sage: s.is_mutable() - True - sage: TestSuite(s).run() - sage: s == ss - False - - sage: # Changing the base field - sage: from flatsurf import * - sage: s=translation_surfaces.veech_double_n_gon(5) - sage: ss=s.copy(mutable=False,new_field=AA) - sage: TestSuite(ss).run() - sage: ss.base_ring() - Algebraic Real Field - - sage: # Optimization of number field - sage: from flatsurf import * - sage: s = translation_surfaces.arnoux_yoccoz(3) - sage: ss = s.copy(new_field=AA).copy(optimal_number_field=True) - sage: TestSuite(ss).run() - sage: ss.base_ring().discriminant() - -44 - """ - s = None # This will be the surface we copy. (Likely we will set s=self below.) - if new_field is not None and optimal_number_field: - raise ValueError( - "You can not set a new_field and also set optimal_number_field=True." - ) - if optimal_number_field is True: - if not self.is_finite(): - raise NotImplementedError( - "can only optimize_number_field for a finite surface" - ) - if lazy: - raise NotImplementedError( - "lazy copying is unavailable when optimize_number_field=True" - ) - coordinates_AA = [] - for label, p in self.label_iterator(polygons=True): - for e in p.edges(): - coordinates_AA.append(AA(e[0])) - coordinates_AA.append(AA(e[1])) - from sage.rings.qqbar import number_field_elements_from_algebraics - - field, coordinates_NF, hom = number_field_elements_from_algebraics( - coordinates_AA, minimal=True - ) - if field is QQ: - new_field = QQ - # We pretend new_field = QQ was passed as a parameter. - # It will now get picked up by the "if new_field is not None:" line below. - else: - # Unfortunately field doesn't come with an real embedding (which is given by hom!) - # So, we make a copy of the field, and add the embedding. - field2 = NumberField( - field.polynomial(), name="a", embedding=hom(field.gen()) - ) - # The following converts from field to field2: - hom2 = field.hom(im_gens=[field2.gen()]) - - ss = Surface_dict(base_ring=field2) - index = 0 - P = ConvexPolygons(field2) - for label, p in self.label_iterator(polygons=True): - new_edges = [] - for i in range(p.num_edges()): - new_edges.append( - ( - hom2(coordinates_NF[index]), - hom2(coordinates_NF[index + 1]), - ) - ) - index += 2 - pp = P(edges=new_edges) - ss.add_polygon(pp, label=label) - ss.change_base_label(self.base_label()) - for (l1, e1), (l2, e2) in self.edge_iterator(gluings=True): - ss.change_edge_gluing(l1, e1, l2, e2) - s = self.__class__(ss) - if not relabel: - if not mutable: - s.set_immutable() - return s - # Otherwise we are supposed to relabel. We will make a relabeled copy of s below. - if new_field is not None: - from flatsurf.geometry.surface import BaseRingChangedSurface - - s = BaseRingChangedSurface(self, new_field) - if s is None: - s = self - if s.is_finite(): - if relabel: - return self.__class__( - Surface_list(surface=s, copy=not lazy, mutable=mutable) - ) - else: - return self.__class__( - Surface_dict(surface=s, copy=not lazy, mutable=mutable) - ) - else: - if lazy is False: - raise ValueError("Only lazy copying available for infinite surfaces.") - if self.underlying_surface().is_mutable(): - raise ValueError( - "An infinite surface can only be copied if it is immutable." - ) - if relabel: - return self.__class__( - Surface_list(surface=s, copy=False, mutable=mutable) - ) - else: - return self.__class__( - Surface_dict(surface=s, copy=False, mutable=mutable) - ) - - def triangle_flip(self, l1, e1, in_place=False, test=False, direction=None): - r""" - Flips the diagonal of the quadrilateral formed by two triangles - glued together along the provided edge (l1,e1). This can be broken - into two steps: join along the edge to form a convex quadilateral, - then cut along the other diagonal. Raises a ValueError if this - quadrilateral would be non-convex. In this case no changes to the - surface are made. - - The direction parameter defaults to (0,1). This is used to decide how - the triangles being glued in are labeled. Let p1 be the triangle - associated to label l1, and p2 be the triangle associated to l2 - but moved by a similarity to share the edge (l1,e1). Each triangle - has a exactly one separatrix leaving a vertex which travels in the - provided direction or its opposite. (For edges we only count as sepatrices - traveling counter-clockwise around the triangle.) This holds for p1 - and p2 and the separatrices must point in opposite directions. - - The above description gives two new triangles t1 and t2 which must be - glued in (obtained by flipping the diagonal of the quadrilateral). - Up to swapping t1 and t2 we can assume the separatrix in t1 in the - provided direction (or its opposite) points in the same direction as - that of p1. Further up to cyclic permutation of vertex labels we can - assume that the separatrices in p1 and t1 start at the vertex with the - same index (an element of {0,1,2}). The same can be done for p2 and t2. - We apply the label l1 to t1 and the label l2 to t2. This precisely - determines how t1 and t2 should be used to replace p1 and p2. - - INPUT: - - - ``l1`` - label of polygon - - - ``e1`` - (integer) edge of the polygon - - - ``in_place`` (boolean) - If True do the flip to the current surface - which must be mutable. In this case the updated surface will be - returned. Otherwise a mutable copy is made and then an edge is - flipped, which is then returned. - - - ``test`` (boolean) - If True we don't actually flip, and we return - True or False depending on whether or not the flip would be - successful. - - - ``direction`` (2-dimensional vector) - Defaults to (0,1). The choice - of this vector determines how the newly added triangles are labeled. - - EXAMPLES:: - - sage: from flatsurf import * - - sage: s = similarity_surfaces.right_angle_triangle(ZZ(1),ZZ(1)) - sage: s.polygon(0) - Polygon: (0, 0), (1, 0), (0, 1) - sage: s.triangle_flip(0, 0, test=True) - False - sage: s.triangle_flip(0, 1, test=True) - True - sage: s.triangle_flip(0, 2, test=True) - False - - sage: s = similarity_surfaces.right_angle_triangle(ZZ(1),ZZ(1)) - sage: from flatsurf.geometry.surface import Surface_list - sage: s = s.__class__(Surface_list(surface=s, mutable=True)) - sage: try: - ....: s.triangle_flip(0,0,in_place=True) - ....: except ValueError as e: - ....: print(e) - Gluing triangles along this edge yields a non-convex quadrilateral. - sage: s.triangle_flip(0,1,in_place=True) - ConeSurface built from 2 polygons - sage: s.polygon(0) - Polygon: (0, 0), (1, 1), (0, 1) - sage: s.polygon(1) - Polygon: (0, 0), (-1, -1), (0, -1) - sage: for p in s.edge_iterator(gluings=True): - ....: print(p) - ((0, 0), (1, 0)) - ((0, 1), (0, 2)) - ((0, 2), (0, 1)) - ((1, 0), (0, 0)) - ((1, 1), (1, 2)) - ((1, 2), (1, 1)) - sage: try: - ....: s.triangle_flip(0,2,in_place=True) - ....: except ValueError as e: - ....: print(e) - ....: - Gluing triangles along this edge yields a non-convex quadrilateral. - - sage: p = polygons((2,0),(-1,3),(-1,-3)) - sage: s = similarity_surfaces.self_glued_polygon(p) - sage: from flatsurf.geometry.surface import Surface_list - sage: s = s.__class__(Surface_list(surface=s,mutable=True)) - sage: s.triangle_flip(0,1,in_place=True) - HalfTranslationSurface built from 1 polygon - sage: for x in s.label_iterator(polygons=True): - ....: print(x) - (0, Polygon: (0, 0), (-3, -3), (-1, -3)) - sage: for x in s.edge_iterator(gluings=True): - ....: print(x) - ((0, 0), (0, 0)) - ((0, 1), (0, 1)) - ((0, 2), (0, 2)) - sage: TestSuite(s).run() - """ - if test: - # Just test if the flip would be successful - p1 = self.polygon(l1) - if not p1.num_edges() == 3: - return False - l2, e2 = self.opposite_edge(l1, e1) - p2 = self.polygon(l2) - if not p2.num_edges() == 3: - return False - sim = self.edge_transformation(l2, e2) - hol = sim(p2.vertex((e2 + 2) % 3) - p1.vertex((e1 + 2) % 3)) - from flatsurf.geometry.polygon import wedge_product - - return ( - wedge_product(p1.edge((e1 + 2) % 3), hol) > 0 - and wedge_product(p1.edge((e1 + 1) % 3), hol) > 0 - ) - - if in_place: - s = self - if not s.is_mutable(): - raise ValueError("surface must be mutable for in place triangle_flip") - else: - s = self.copy(mutable=True) - - p1 = s.polygon(l1) - if not p1.num_edges() == 3: - raise ValueError("The polygon with the provided label is not a triangle.") - l2, e2 = s.opposite_edge(l1, e1) - - sim = s.edge_transformation(l2, e2) - p2 = s.polygon(l2) - if not p2.num_edges() == 3: - raise ValueError( - "The polygon opposite the provided edge is not a triangle." - ) - P = p1.parent() - p2 = P(vertices=[sim(v) for v in p2.vertices()]) - - if direction is None: - direction = s.vector_space()((0, 1)) - # Get vertices corresponding to separatices in the provided direction. - v1 = p1.find_separatrix(direction=direction)[0] - v2 = p2.find_separatrix(direction=direction)[0] - # Our quadrilateral has vertices labeled: - # * 0=p1.vertex(e1+1)=p2.vertex(e2) - # * 1=p1.vertex(e1+2) - # * 2=p1.vertex(e1)=p2.vertex(e2+1) - # * 3=p2.vertex(e2+2) - # Record the corresponding vertices of this quadrilateral. - q1 = (3 + v1 - e1 - 1) % 3 - q2 = (2 + (3 + v2 - e2 - 1) % 3) % 4 - - new_diagonal = p2.vertex((e2 + 2) % 3) - p1.vertex((e1 + 2) % 3) - # This list will store the new triangles which are being glued in. - # (Unfortunately, they may not be cyclically labeled in the correct way.) - new_triangle = [] - try: - new_triangle.append( - P(edges=[p1.edge((e1 + 2) % 3), p2.edge((e2 + 1) % 3), -new_diagonal]) - ) - new_triangle.append( - P(edges=[p2.edge((e2 + 2) % 3), p1.edge((e1 + 1) % 3), new_diagonal]) - ) - # The above triangles would be glued along edge 2 to form the diagonal of the quadrilateral being removed. - except ValueError: - raise ValueError( - "Gluing triangles along this edge yields a non-convex quadrilateral." - ) - - # Find the separatrices of the two new triangles, and in particular which way they point. - new_sep = [] - new_sep.append(new_triangle[0].find_separatrix(direction=direction)[0]) - new_sep.append(new_triangle[1].find_separatrix(direction=direction)[0]) - # The quadrilateral vertices corresponding to these separatrices are - # new_sep[0]+1 and (new_sep[1]+3)%4 respectively. - - # i=0 if the new_triangle[0] should be labeled l1 and new_triangle[1] should be labeled l2. - # i=1 indicates the opposite labeling. - if new_sep[0] + 1 == q1: - assert (new_sep[1] + 3) % 4 == q2, ( - "Bug: new_sep[1]=" + str(new_sep[1]) + " and q2=" + str(q2) - ) - i = 0 - else: - assert (new_sep[1] + 3) % 4 == q1 - assert new_sep[0] + 1 == q2 - i = 1 - - # These quantities represent the cyclic relabeling of triangles needed. - cycle1 = (new_sep[i] - v1 + 3) % 3 - cycle2 = (new_sep[1 - i] - v2 + 3) % 3 - - # This will be the new triangle with label l1: - tri1 = P( - edges=[ - new_triangle[i].edge(cycle1), - new_triangle[i].edge((cycle1 + 1) % 3), - new_triangle[i].edge((cycle1 + 2) % 3), - ] - ) - # This will be the new triangle with label l2: - tri2 = P( - edges=[ - new_triangle[1 - i].edge(cycle2), - new_triangle[1 - i].edge((cycle2 + 1) % 3), - new_triangle[1 - i].edge((cycle2 + 2) % 3), - ] - ) - # In the above, edge 2-cycle1 of tri1 would be glued to edge 2-cycle2 of tri2 - diagonal_glue_e1 = 2 - cycle1 - diagonal_glue_e2 = 2 - cycle2 - - assert p1.find_separatrix(direction=direction) == tri1.find_separatrix( - direction=direction - ) - assert p2.find_separatrix(direction=direction) == tri2.find_separatrix( - direction=direction - ) - - # Two opposite edges will not change their labels (label,edge) under our regluing operation. - # The other two opposite ones will change and in fact they change labels. - # The following finds them (there are two cases). - # At the end of the if statement, the following will be true: - # * new_glue_e1 and new_glue_e2 will be the edges of the new triangle with label l1 and l2 which need regluing. - # * old_e1 and old_e2 will be the corresponding edges of the old triangles. - # (Note that labels are swapped between the pair. The appending 1 or 2 refers to the label used for the triangle.) - if p1.edge(v1) == tri1.edge(v1): - # We don't have to worry about changing gluings on edge v1 of the triangles with label l1 - # We do have to worry about the following edge: - new_glue_e1 = ( - 3 - diagonal_glue_e1 - v1 - ) # returns the edge which is neither diagonal_glue_e1 nor v1. - # This corresponded to the following old edge: - old_e1 = 3 - e1 - v1 # Again this finds the edge which is neither e1 nor v1 - else: - temp = (v1 + 2) % 3 - assert p1.edge(temp) == tri1.edge(temp) - # We don't have to worry about changing gluings on edge (v1+2)%3 of the triangles with label l1 - # We do have to worry about the following edge: - new_glue_e1 = ( - 3 - diagonal_glue_e1 - temp - ) # returns the edge which is neither diagonal_glue_e1 nor temp. - # This corresponded to the following old edge: - old_e1 = ( - 3 - e1 - temp - ) # Again this finds the edge which is neither e1 nor temp - if p2.edge(v2) == tri2.edge(v2): - # We don't have to worry about changing gluings on edge v2 of the triangles with label l2 - # We do have to worry about the following edge: - new_glue_e2 = ( - 3 - diagonal_glue_e2 - v2 - ) # returns the edge which is neither diagonal_glue_e2 nor v2. - # This corresponded to the following old edge: - old_e2 = 3 - e2 - v2 # Again this finds the edge which is neither e2 nor v2 - else: - temp = (v2 + 2) % 3 - assert p2.edge(temp) == tri2.edge(temp) - # We don't have to worry about changing gluings on edge (v2+2)%3 of the triangles with label l2 - # We do have to worry about the following edge: - new_glue_e2 = ( - 3 - diagonal_glue_e2 - temp - ) # returns the edge which is neither diagonal_glue_e2 nor temp. - # This corresponded to the following old edge: - old_e2 = ( - 3 - e2 - temp - ) # Again this finds the edge which is neither e2 nor temp - - # remember the old gluings. - old_opposite1 = s.opposite_edge(l1, old_e1) - old_opposite2 = s.opposite_edge(l2, old_e2) - - # We make changes to the underlying surface - us = s.underlying_surface() - - # Replace the triangles. - us.change_polygon(l1, tri1) - us.change_polygon(l2, tri2) - # Glue along the new diagonal of the quadrilateral - us.change_edge_gluing(l1, diagonal_glue_e1, l2, diagonal_glue_e2) - # Now we deal with that pair of opposite edges of the quadrilateral that need regluing. - # There are some special cases: - if old_opposite1 == (l2, old_e2): - # These opposite edges were glued to each other. - # Do the same in the new surface: - us.change_edge_gluing(l1, new_glue_e1, l2, new_glue_e2) - else: - if old_opposite1 == (l1, old_e1): - # That edge was "self-glued". - us.change_edge_gluing(l2, new_glue_e2, l2, new_glue_e2) - else: - # The edge (l1,old_e1) was glued in a standard way. - # That edge now corresponds to (l2,new_glue_e2): - us.change_edge_gluing( - l2, new_glue_e2, old_opposite1[0], old_opposite1[1] - ) - if old_opposite2 == (l2, old_e2): - # That edge was "self-glued". - us.change_edge_gluing(l1, new_glue_e1, l1, new_glue_e1) - else: - # The edge (l2,old_e2) was glued in a standard way. - # That edge now corresponds to (l1,new_glue_e1): - us.change_edge_gluing( - l1, new_glue_e1, old_opposite2[0], old_opposite2[1] - ) - return s - - def join_polygons(self, p1, e1, test=False, in_place=False): - r""" - Join polygons across the provided edge (p1,e1). By default, - it returns the surface obtained by joining the two polygons - together. It raises a ValueError if gluing the two polygons - together results in a non-convex polygon. This is done to the - current surface if in_place is True, and otherwise a mutable - copy is made and then modified. - - If test is True then instead of changing the surface, it just - checks to see if the change would be successful and returns - True if successful or False if not. - - EXAMPLES:: - - sage: from flatsurf import * - sage: ss=translation_surfaces.ward(3) - sage: s=ss.copy(mutable=True) - sage: s.join_polygons(0,0, in_place=True) - TranslationSurface built from 2 polygons - sage: s.polygon(0) - Polygon: (0, 0), (1, -a), (2, 0), (3, a), (2, 2*a), (0, 2*a), (-1, a) - sage: s.join_polygons(0,4, in_place=True) - TranslationSurface built from 1 polygon - sage: s.polygon(0) - Polygon: (0, 0), (1, -a), (2, 0), (3, a), (2, 2*a), (1, 3*a), (0, 2*a), (-1, a) - """ - poly1 = self.polygon(p1) - p2, e2 = self.opposite_edge(p1, e1) - poly2 = self.polygon(p2) - if p1 == p2: - if test: - return False - else: - raise ValueError("Can't glue polygon to itself.") - t = self.edge_transformation(p2, e2) - dt = t.derivative() - vs = [] - edge_map = {} # Store the pairs for the old edges. - for i in range(e1): - edge_map[len(vs)] = (p1, i) - vs.append(poly1.edge(i)) - ne = poly2.num_edges() - for i in range(1, ne): - ee = (e2 + i) % ne - edge_map[len(vs)] = (p2, ee) - vs.append(dt * poly2.edge(ee)) - for i in range(e1 + 1, poly1.num_edges()): - edge_map[len(vs)] = (p1, i) - vs.append(poly1.edge(i)) - - try: - new_polygon = ConvexPolygons(self.base_ring())(vs) - except (ValueError, TypeError): - if test: - return False - else: - raise ValueError( - "Joining polygons along this edge results in a non-convex polygon." - ) - - if test: - # Gluing would be successful - return True - - # Now no longer testing. Do the gluing. - if in_place: - ss = self - else: - ss = self.copy(mutable=True) - s = ss.underlying_surface() - - inv_edge_map = {} - for key, value in edge_map.items(): - inv_edge_map[value] = (p1, key) - - glue_list = [] - for i in range(len(vs)): - p3, e3 = edge_map[i] - p4, e4 = self.opposite_edge(p3, e3) - if p4 == p1 or p4 == p2: - glue_list.append(inv_edge_map[(p4, e4)]) - else: - glue_list.append((p4, e4)) - - if s.base_label() == p2: - s.change_base_label(p1) - - s.remove_polygon(p2) - - s.change_polygon(p1, new_polygon, glue_list) - - return ss - - def subdivide_polygon(self, p, v1, v2, test=False, new_label=None): - r""" - Cut the polygon with label p along the diagonal joining vertex - v1 to vertex v2. This cuts p into two polygons, one will keep the same - label. The other will get a new label, which can be provided - via new_label. Otherwise a default new label will be provided. - If test=False, then the surface will be changed (in place). If - test=True, then it just checks to see if the change would be successful - - The convention is that the resulting subdivided polygon which has an oriented - edge going from the original vertex v1 to vertex v2 will keep the label p. - The other polygon will get a new label. - - The change will be done in place. - """ - poly = self.polygon(p) - ne = poly.num_edges() - if v1 < 0 or v2 < 0 or v1 >= ne or v2 >= ne: - if test: - return False - else: - raise ValueError("Provided vertices out of bounds.") - if abs(v1 - v2) <= 1 or abs(v1 - v2) >= ne - 1: - if test: - return False - else: - raise ValueError("Provided diagonal is not actually a diagonal.") - - if v2 < v1: - v2 = v2 + ne - - newedges1 = [poly.vertex(v2) - poly.vertex(v1)] - for i in range(v2, v1 + ne): - newedges1.append(poly.edge(i)) - newpoly1 = ConvexPolygons(self.base_ring())(newedges1) - - newedges2 = [poly.vertex(v1) - poly.vertex(v2)] - for i in range(v1, v2): - newedges2.append(poly.edge(i)) - newpoly2 = ConvexPolygons(self.base_ring())(newedges2) - - # Store the old gluings - old_gluings = {(p, i): self.opposite_edge(p, i) for i in range(ne)} - - # Update the polygon with label p, add a new polygon. - self.underlying_surface().change_polygon(p, newpoly1) - if new_label is None: - new_label = self.underlying_surface().add_polygon(newpoly2) - else: - new_label = self.underlying_surface().add_polygon(newpoly2, label=new_label) - # This gluing is the diagonal we used. - self.underlying_surface().change_edge_gluing(p, 0, new_label, 0) - - # Setup conversion from old to new labels. - old_to_new_labels = {} - for i in range(v1, v2): - old_to_new_labels[(p, i % ne)] = (new_label, i - v1 + 1) - for i in range(v2, ne + v1): - old_to_new_labels[(p, i % ne)] = (p, i - v2 + 1) - - for e in range(1, newpoly1.num_edges()): - pair = old_gluings[(p, (v2 + e - 1) % ne)] - if pair in old_to_new_labels: - pair = old_to_new_labels[pair] - self.underlying_surface().change_edge_gluing(p, e, pair[0], pair[1]) - - for e in range(1, newpoly2.num_edges()): - pair = old_gluings[(p, (v1 + e - 1) % ne)] - if pair in old_to_new_labels: - pair = old_to_new_labels[pair] - self.underlying_surface().change_edge_gluing(new_label, e, pair[0], pair[1]) - - def subdivide(self): - r""" - Return a copy of this surface whose polygons have been partitioned into - smaller triangles with - :meth:`.polygon.ConvexPolygon.subdivide`. - - EXAMPLES:: - - sage: from flatsurf import translation_surfaces - sage: S = translation_surfaces.veech_double_n_gon(5) - sage: S.subdivide() - TranslationSurface built from 10 polygons - sage: S.subdivide().subdivide() - TranslationSurface built from 30 polygons - - Sometimes a more uniform subdivision can be obtained by alternating - between :meth:`subdivide_edges` and this method:: - - sage: S.subdivide_edges().subdivide() - TranslationSurface built from 20 polygons - - """ - return self.__class__(self._s.subdivide()) - - def subdivide_edges(self, parts=2): - r""" - Return a copy of this surface whose edges have been split into - ``parts`` equal pieces each. - - INPUT: - - - ``parts`` -- a positive integer (default: 2) - - EXAMPLES: - - sage: from flatsurf import translation_surfaces - sage: S = translation_surfaces.veech_double_n_gon(5) - sage: S.subdivide_edges() - TranslationSurface built from 2 polygons - - """ - return self.__class__(self._s.subdivide_edges(parts=parts)) - - def singularity(self, label, v, limit=None): - r""" - Represents the Singularity associated to the v-th vertex of the polygon - with label ``label``. - - If the surface is infinite, the limit can be set. In this case the - construction of the singularity is successful if the sequence of - vertices hit by passing through edges closes up in limit or less steps. - - EXAMPLES:: - - sage: from flatsurf import * - sage: s = translation_surfaces.square_torus() - sage: pc = s.minimal_cover(cover_type="planar") - sage: pc.singularity(pc.base_label(),0) - doctest:warning - ... - UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead. - Vertex 0 of polygon (0, (x, y) |-> (x, y)) - sage: pc.singularity(pc.base_label(),0,limit=1) - Traceback (most recent call last): - ... - ValueError: number of edges at singularity exceeds limit - - """ - return Singularity(self, label, v, limit) - - def point(self, label, point, ring=None, limit=None): - r""" - Return a point in this surface. - - INPUT: - - - ``label`` - label of the polygon - - - ``point`` - coordinates of the point inside the polygon - - - ``ring`` (optional) - a ring for the coordinates - - - ``limit`` (optional) - undocumented (only relevant if the point - corresponds to a singularity in an infinite surface) - - EXAMPLES:: - - sage: from flatsurf import * - sage: s = translation_surfaces.square_torus() - sage: pc = s.minimal_cover(cover_type="planar") - sage: pc.surface_point(pc.base_label(),(0,0)) - Vertex 0 of polygon (0, (x, y) |-> (x, y)) - sage: z = pc.surface_point(pc.base_label(),(sqrt(2)-1,sqrt(3)-1),ring=AA) - doctest:warning - ... - UserWarning: the ring parameter is deprecated and will be removed in a future version of sage-flatsurf; define the surface over a larger ring instead so that this points' coordinates live in the base ring - sage: next(iter(z.coordinates(next(iter(z.labels()))))).parent() - Vector space of dimension 2 over Algebraic Real Field - """ - return SurfacePoint(self, label, point, ring=ring, limit=limit) - - # TODO: deprecate - surface_point = point - - def ramified_cover(self, degree, data): - r""" - Build a ramified cover of this surface with given ``degree`` and ramification ``data``. - - INPUT: - - - ``degree`` (integer) -- the degree of the cover - - - ``data`` -- dictionary that associates a pair ``(polygon_label, edge_number)`` a permutation - of ``{1, 2, ..., d}`` - - EXAMPLES: - - The L-shape origami:: - - sage: import flatsurf - sage: T = flatsurf.translation_surfaces.square_torus() - sage: T.ramified_cover(3, {(0,0): '(1,2)', (0,1): '(1,3)'}) - TranslationSurface built from 3 polygons - sage: O = T.ramified_cover(3, {(0,0): '(1,2)', (0,1): '(1,3)'}) - sage: O.stratum() - H_2(2) - - TESTS:: - - sage: import flatsurf - sage: T = flatsurf.translation_surfaces.square_torus() - sage: T.ramified_cover(3, {(0,0): '(1,2)', (0,2): '(1,3)'}) - Traceback (most recent call last): - ... - ValueError: inconsistent covering data - """ - if not self.is_finite(): - raise ValueError("this method is only available for finite surfaces") - return type(self)(self._s.ramified_cover(degree, data)) - - def minimal_cover(self, cover_type="translation"): - r""" - Return the minimal translation or half-translation cover of the surface. - - Cover type may be either "translation", "half-translation" or "planar". - - The minimal planar cover of a surface S is the smallest cover C so that - the developing map from the universal cover U to the plane induces a - well defined map from C to the plane. This is an infinite translation - surface that is naturally a branched cover of the plane. - - EXAMPLES:: - - sage: from flatsurf.geometry.surface import Surface_list - sage: s = Surface_list(QQ) - sage: from flatsurf.geometry.polygon import polygons - sage: square = polygons.square(field=QQ) - sage: s.add_polygon(square) - 0 - sage: s.change_edge_gluing(0,0,0,1) - sage: s.change_edge_gluing(0,2,0,3) - sage: from flatsurf.geometry.cone_surface import ConeSurface - sage: cs = ConeSurface(s) - sage: ts = cs.minimal_cover(cover_type="translation") - sage: ts - TranslationSurface built from 4 polygons - sage: hts = cs.minimal_cover(cover_type="half-translation") - sage: hts - HalfTranslationSurface built from 2 polygons - sage: TestSuite(hts).run() - sage: ps = cs.minimal_cover(cover_type="planar") - sage: ps - TranslationSurface built from infinitely many polygons - sage: TestSuite(ps).run() - - sage: from flatsurf import similarity_surfaces - sage: S = similarity_surfaces.example() - sage: T = S.minimal_cover(cover_type="translation") - sage: T - TranslationSurface built from infinitely many polygons - sage: T.polygon(T.base_label()) - Polygon: (0, 0), (2, -2), (2, 0) - """ - if cover_type == "translation": - from flatsurf.geometry.translation_surface import TranslationSurface - from flatsurf.geometry.minimal_cover import MinimalTranslationCover - - return TranslationSurface(MinimalTranslationCover(self)) - if cover_type == "half-translation": - from flatsurf.geometry.half_translation_surface import ( - HalfTranslationSurface, - ) - from flatsurf.geometry.minimal_cover import MinimalHalfTranslationCover - - return HalfTranslationSurface(MinimalHalfTranslationCover(self)) - if cover_type == "planar": - from flatsurf.geometry.translation_surface import TranslationSurface - from flatsurf.geometry.minimal_cover import MinimalPlanarCover - - return TranslationSurface(MinimalPlanarCover(self)) - raise ValueError("Provided cover_type is not supported.") - - def vector_space(self): - r""" - Return the vector space in which self naturally embeds. - """ - from sage.modules.free_module import VectorSpace - - return VectorSpace(self.base_ring(), 2) - - def fundamental_group(self, base_label=None): - r""" - Return the fundamental group of this surface. - """ - if not self.is_finite(): - raise ValueError("the method only work for finite surfaces") - if base_label is None: - base_label = self.base_label() - from .fundamental_group import FundamentalGroup - - return FundamentalGroup(self, base_label) - - def tangent_bundle(self, ring=None): - r""" - Return the tangent bundle - - INPUT: - - - ``ring`` -- an optional field (defaults to the coordinate field of the - surface) - """ - if ring is None: - ring = self.base_ring() - - try: - return self._tangent_bundle_cache[ring] - except AttributeError: - self._tangent_bundle_cache = {} - except KeyError: - pass - - from .tangent_bundle import SimilaritySurfaceTangentBundle - - self._tangent_bundle_cache[ring] = SimilaritySurfaceTangentBundle(self, ring) - return self._tangent_bundle_cache[ring] - - def tangent_vector(self, lab, p, v, ring=None): - r""" - Return a tangent vector. - - INPUT: - - - ``lab`` -- label of a polygon - - - ``p`` -- coordinates of a point in the polygon - - - ``v`` -- coordinates of a vector in R^2 - - EXAMPLES:: - - sage: from flatsurf.geometry.chamanara import chamanara_surface - sage: S = chamanara_surface(1/2) - sage: S.tangent_vector(S.base_label(), (1/2,1/2), (1,1)) - SimilaritySurfaceTangentVector in polygon (1, -1, 0) based at (1/2, -3/2) with vector (1, 1) - sage: K. = QuadraticField(2) - sage: S.tangent_vector(S.base_label(), (1/2,1/2), (1,sqrt2), ring=K) - SimilaritySurfaceTangentVector in polygon (1, -1, 0) based at (1/2, -3/2) with vector (1, sqrt2) - """ - p = vector(p) - v = vector(v) - - if p.parent().dimension() != 2 or v.parent().dimension() != 2: - raise ValueError("p (={!r}) and v (={!v}) should have two coordinates") - - if ring is None: - ring = self.base_ring() - try: - return self.tangent_bundle(ring)(lab, p, v) - except TypeError: - raise TypeError( - "Use the ring=??? option to construct tangent vectors in other field different from the base_ring()." - ) - # Old version seemed to be to accepting of inputs (eg, from Symbolic Ring) - # R = p.base_ring() - # if R != v.base_ring(): - # from sage.structure.element import get_coercion_model - # cm = get_coercion_model() - # R = cm.common_parent(R, v.base_ring()) - # p = p.change_ring(R) - # v = v.change_ring(R) - - # R2 = self.base_ring() - # if R != R2: - # if R2.has_coerce_map_from(R): - # p = p.change_ring(R2) - # v = v.change_ring(R2) - # R = R2 - # elif not R.has_coerce_map_from(R2): - # raise ValueError("not able to find a common ring for arguments") - # return self.tangent_bundle(R)(lab, p, v) - else: - return self.tangent_bundle(ring)(lab, p, v) - - def reposition_polygons(self, in_place=False, relabel=False): - r""" - We choose a maximal tree in the dual graph of the decomposition into - polygons, and ensure that the gluings between two polygons joined by - an edge in this tree is by translation. - - This guarantees that the group generated by the edge identifications is - minimal among representations of the surface. In particular, if for instance - you have a translation surface which is anot representable as a translation - surface (because polygons are presented with rotations) then after this - change it will be representable as a translation surface. - """ - if not self.is_finite(): - raise NotImplementedError("Only implemented for finite surfaces.") - if in_place: - if not self.is_mutable(): - raise ValueError( - "reposition_polygons in_place is only available " - + "for mutable surfaces." - ) - s = self - else: - s = self.copy(relabel=relabel, mutable=True) - w = s.walker() - from flatsurf.geometry.similarity import SimilarityGroup - - S = SimilarityGroup(self.base_ring()) - identity = S.one() - it = iter(w) - label = next(it) - changes = {label: identity} - for label in it: - edge = w.edge_back(label) - label2, edge2 = s.opposite_edge(label, edge) - changes[label] = changes[label2] * s.edge_transformation(label, edge) - it = iter(w) - # Skip the base label: - label = next(it) - for label in it: - p = s.polygon(label) - p = changes[label].derivative() * p - s.underlying_surface().change_polygon(label, p) - return s - - def triangulation_mapping(self): - r""" - Return a ``SurfaceMapping`` triangulating the surface - or ``None`` if the surface is already triangulated. - """ - from flatsurf.geometry.mappings import triangulation_mapping - - return triangulation_mapping(self) - - def triangulate(self, in_place=False, label=None, relabel=False): - r""" - Return a triangulated version of this surface. (This may be mutable - or not depending on the input.) - - If label=None (as default) all polygons are triangulated. Otherwise, - label should be a polygon label. In this case, just this polygon - is split into triangles. - - This is done in place if in_place is True (defaults to False). - - If we are not doing triangulation in_place, then we must make a copy. - This can be a relabeled copy (indexed by the non-negative ints) - or a label preserving copy. The copy is relabeled if relabel=True - (default False). - - EXAMPLES:: - - sage: from flatsurf import * - sage: s=translation_surfaces.mcmullen_L(1,1,1,1) - sage: ss=s.triangulate() - sage: gs=ss.graphical_surface() - sage: gs.make_all_visible() - sage: gs - Graphical version of Similarity Surface TranslationSurface built from 6 polygons - - A non-strictly convex example that caused trouble: - - sage: from flatsurf import * - sage: s=similarity_surfaces.self_glued_polygon(polygons(edges=[(1,1),(-3,-1),(1,0),(1,0)])) - sage: s=s.triangulate() - sage: s.polygon(0).num_edges() - 3 - """ - if label is None: - # We triangulate the whole surface - if self.is_finite(): - # Store the current labels. - labels = [label for label in self.label_iterator()] - if in_place: - s = self - else: - s = self.copy(mutable=True) - # Subdivide each polygon in turn. - for label in labels: - s = s.triangulate(in_place=True, label=label) - return s - else: - if in_place: - raise ValueError( - "You can't triangulate an infinite surface in place." - ) - from flatsurf.geometry.delaunay import LazyTriangulatedSurface - - return self.__class__(LazyTriangulatedSurface(self)) - else: - poly = self.polygon(label) - n = poly.num_edges() - if n > 3: - if in_place: - s = self - else: - s = self.copy(mutable=True) - else: - # This polygon is already a triangle. - return self - from flatsurf.geometry.polygon import wedge_product - - for i in range(n - 3): - poly = s.polygon(label) - n = poly.num_edges() - for i in range(n): - e1 = poly.edge(i) - e2 = poly.edge((i + 1) % n) - if wedge_product(e1, e2) != 0: - # This is in case the polygon is a triangle with subdivided edge. - e3 = poly.edge((i + 2) % n) - if wedge_product(e1 + e2, e3) != 0: - s.subdivide_polygon(label, i, (i + 2) % n) - break - return s - raise RuntimeError("Failed to return anything!") - - def _edge_needs_flip(self, p1, e1): - r""" - Returns -1 if the the provided edge incident to two triangles which - should be flipped to get closer to the Delaunay decomposition. - Returns 0 if the quadrilateral formed by the triangles is inscribed - in a circle, and returns 1 otherwise. - - A ValueError is raised if the edge is not indident to two triangles. - """ - p2, e2 = self.opposite_edge(p1, e1) - poly1 = self.polygon(p1) - poly2 = self.polygon(p2) - if poly1.num_edges() != 3 or poly2.num_edges() != 3: - raise ValueError("Edge must be adjacent to two triangles.") - from flatsurf.geometry.matrix_2x2 import similarity_from_vectors - - sim1 = similarity_from_vectors(poly1.edge(e1 + 2), -poly1.edge(e1 + 1)) - sim2 = similarity_from_vectors(poly2.edge(e2 + 2), -poly2.edge(e2 + 1)) - sim = sim1 * sim2 - return sim[1][0] < 0 - - def _edge_needs_join(self, p1, e1): - r""" - Returns -1 if the the provided edge incident to two triangles which - should be flipped to get closer to the Delaunay decomposition. - Returns 0 if the quadrilateral formed by the triangles is inscribed - in a circle, and returns 1 otherwise. - - A ValueError is raised if the edge is not indident to two triangles. - """ - p2, e2 = self.opposite_edge(p1, e1) - poly1 = self.polygon(p1) - poly2 = self.polygon(p2) - from flatsurf.geometry.matrix_2x2 import similarity_from_vectors - - sim1 = similarity_from_vectors( - poly1.vertex(e1) - poly1.vertex(e1 + 2), -poly1.edge(e1 + 1) - ) - sim2 = similarity_from_vectors( - poly2.vertex(e2) - poly2.vertex(e2 + 2), -poly2.edge(e2 + 1) - ) - sim = sim1 * sim2 - from sage.functions.generalized import sgn - - return sim[1][0] == 0 - - def delaunay_single_flip(self): - r""" - Does a single in place flip of a triangulated mutable surface. - """ - if not self.is_finite(): - raise NotImplementedError("Not implemented for infinite surfaces.") - lc = self._label_comparator() - for (l1, e1), (l2, e2) in self.edge_iterator(gluings=True): - if (lc.lt(l1, l2) or (l1 == l2 and e1 <= e2)) and self._edge_needs_flip( - l1, e1 - ): - self.triangle_flip(l1, e1, in_place=True) - return True - return False - - def is_delaunay_triangulated(self, limit=None): - r""" - Return if the surface is triangulated and the triangulation is Delaunay. - If limit is set, then it checks this only limit many edges. - Limit must be set for infinite surfaces. - """ - if limit is None: - if not self.is_finite(): - raise NotImplementedError("A limit must be set for infinite surfaces.") - limit = self.num_edges() - count = 0 - for (l1, e1), (l2, e2) in self.edge_iterator(gluings=True): - if count >= limit: - break - count = count + 1 - if self.polygon(l1).num_edges() != 3: - print("Polygon with label " + str(l1) + " is not a triangle.") - return False - if self.polygon(l2).num_edges() != 3: - print("Polygon with label " + str(l2) + " is not a triangle.") - return False - if self._edge_needs_flip(l1, e1): - print("Edge " + str((l1, e1)) + " needs to be flipped.") - print("This edge is glued to " + str((l2, e2)) + ".") - return False - return True - - def is_delaunay_decomposed(self, limit=None): - r""" - Return if the decomposition of the surface into polygons is Delaunay. - If limit is set, then it checks this only limit many polygons. - Limit must be set for infinite surfaces. - """ - if limit is None: - if not self.is_finite(): - raise NotImplementedError("A limit must be set for infinite surfaces.") - limit = self.num_polygons() - count = 0 - for l1, p1 in self.label_iterator(polygons=True): - try: - c1 = p1.circumscribing_circle() - except ValueError: - # p1 is not circumscribed - return False - for e1 in range(p1.num_edges()): - c2 = self.edge_transformation(l1, e1) * c1 - l2, e2 = self.opposite_edge(l1, e1) - if c2.point_position(self.polygon(l2).vertex(e2 + 2)) != -1: - # The circumscribed circle developed into the adjacent polygon - # contains a vertex in its interior or boundary. - return False - return True - - def delaunay_triangulation( - self, - triangulated=False, - in_place=False, - limit=None, - direction=None, - relabel=False, - ): - r""" - Returns a Delaunay triangulation of a surface, or make some - triangle flips to get closer to the Delaunay decomposition. - - INPUT: - - - ``triangulated`` (boolean) - If true, the algorithm assumes the - surface is already triangulated. It does this without verification. - - - ``in_place`` (boolean) - If true, the triangulating and the - triangle flips are done in place. Otherwise, a mutable copy of the - surface is made. - - - ``limit`` (None or Integer) - If None, this will return a - Delaunay triangulation. If limit is an integer 1 or larger, then at - most limit many diagonal flips will be done. - - - ``direction`` (None or Vector) - with two entries in the base field - Used to determine labels when a pair of triangles is flipped. Each triangle - has a unique separatrix which points in the provided direction or its - negation. As such a vector determines a sign for each triangle. - A pair of adjacent triangles have opposite signs. Labels are chosen - so that this sign is preserved (as a function of labels). - - - ``relabel`` (boolean) - If in_place is False, then a copy must be - made. By default relabel is False and labels will be respected by - this copy. If relabel is True then polygons will be reindexed in an - arbitrary way by the non-negative integers. - - EXAMPLES:: - - sage: from flatsurf import * - sage: from flatsurf.geometry.delaunay import * - - sage: m = matrix([[2,1],[1,1]]) - sage: s = m*translation_surfaces.infinite_staircase() - sage: ss = s.delaunay_triangulation(relabel=True) - sage: ss.base_label() - 0 - sage: ss.polygon(0) - Polygon: (0, 0), (1, 1), (0, 1) - sage: TestSuite(ss).run() - sage: ss.is_delaunay_triangulated(limit=10) - True - """ - if not self.is_finite() and limit is None: - if in_place: - raise ValueError( - "in_place delaunay triangulation is not possible for infinite surfaces unless a limit is set." - ) - if self.underlying_surface().is_mutable(): - raise ValueError( - "delaunay_triangulation only works on infinite " - + "surfaces if they are immutable or if a limit is set." - ) - from flatsurf.geometry.delaunay import LazyDelaunayTriangulatedSurface - - return self.__class__( - LazyDelaunayTriangulatedSurface( - self, direction=direction, relabel=relabel - ) - ) - if in_place and not self.is_mutable(): - raise ValueError( - "in_place delaunay_triangulation only defined for mutable surfaces" - ) - if triangulated: - if in_place: - s = self - else: - s = self.copy(mutable=True, relabel=False) - else: - if in_place: - s = self - self.triangulate(in_place=True) - else: - s = self.copy(relabel=True, mutable=True) - s.triangulate(in_place=True) - loop = True - if direction is None: - base_ring = self.base_ring() - direction = self.vector_space()((base_ring.zero(), base_ring.one())) - - if direction.is_zero(): - raise ValueError - - if s.is_finite() and limit is None: - from collections import deque - - unchecked_labels = deque(label for label in s.label_iterator()) - checked_labels = set() - while unchecked_labels: - label = unchecked_labels.popleft() - flipped = False - for edge in range(3): - if s._edge_needs_flip(label, edge): - # Record the current opposite edge: - label2, edge2 = s.opposite_edge(label, edge) - # Perform the flip. - s.triangle_flip(label, edge, in_place=True, direction=direction) - # Move the opposite polygon to the list of labels we need to check. - if label2 != label: - try: - checked_labels.remove(label2) - unchecked_labels.append(label2) - except KeyError: - # Occurs if label2 is not in checked_labels - pass - flipped = True - break - if flipped: - unchecked_labels.append(label) - else: - checked_labels.add(label) - return s - else: - # Old method for infinite surfaces, or limits. - count = 0 - lc = self._label_comparator() - while loop: - loop = False - for (l1, e1), (l2, e2) in s.edge_iterator(gluings=True): - if ( - lc.lt(l1, l2) or (l1 == l2 and e1 <= e2) - ) and s._edge_needs_flip(l1, e1): - s.triangle_flip(l1, e1, in_place=True, direction=direction) - count += 1 - if limit is not None and count >= limit: - return s - loop = True - break - return s - - def delaunay_single_join(self): - if not self.is_finite(): - raise NotImplementedError("Not implemented for infinite surfaces.") - lc = self._label_comparator() - for (l1, e1), (l2, e2) in self.edge_iterator(gluings=True): - if (lc.lt(l1, l2) or (l1 == l2 and e1 <= e2)) and self._edge_needs_join( - l1, e1 - ): - self.join_polygons(l1, e1, in_place=True) - return True - return False - - def delaunay_decomposition( - self, - triangulated=False, - delaunay_triangulated=False, - in_place=False, - direction=None, - relabel=False, - ): - r""" - Return the Delaunay Decomposition of this surface. - - INPUT: - - - ``triangulated`` (boolean) - If true, the algorithm assumes the - surface is already triangulated. It does this without verification. - - - ``delaunay_triangulated`` (boolean) - If true, the algorithm assumes - the surface is already delaunay_triangulated. It does this without - verification. - - - ``in_place`` (boolean) - If true, the triangulating and the triangle - flips are done in place. Otherwise, a mutable copy of the surface is - made. - - - ``relabel`` (None or Integer) - If in_place is False, then a copy - must be made of the surface. If relabel is False (as default), the - copy has the same labels as the original surface. Note that in this - case, labels will be added if it is necessary to subdivide polygons - into triangles. If relabel is True, the new surface will have - polygons labeled by the non-negative integers in an arbitrary way. - - - ``direction`` - (None or Vector with two entries in the base field) - - Used to determine labels when a pair of triangles is flipped. Each triangle - has a unique separatrix which points in the provided direction or its - negation. As such a vector determines a sign for each triangle. - A pair of adjacent triangles have opposite signs. Labels are chosen - so that this sign is preserved (as a function of labels). - - EXAMPLES:: - - sage: from flatsurf import * - sage: s0 = translation_surfaces.octagon_and_squares() - sage: a = s0.base_ring().gens()[0] - sage: m = Matrix([[1,2+a],[0,1]]) - sage: s = m*s0 - sage: s = s.triangulate() - sage: ss = s.delaunay_decomposition(triangulated=True) - sage: ss.num_polygons() - 3 - - sage: p = polygons((4,0),(-2,1),(-2,-1)) - sage: s0 = similarity_surfaces.self_glued_polygon(p) - sage: s = s0.delaunay_decomposition() - sage: TestSuite(s).run() - - sage: m = matrix([[2,1],[1,1]]) - sage: s = m*translation_surfaces.infinite_staircase() - sage: ss = s.delaunay_decomposition() - sage: ss.base_label() - 0 - sage: ss.polygon(0) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: TestSuite(ss).run() - sage: ss.is_delaunay_decomposed(limit=10) - True - """ - if not self.is_finite(): - if in_place: - raise ValueError( - "in_place delaunay_decomposition is not possible for infinite surfaces." - ) - if self.underlying_surface().is_mutable(): - raise ValueError( - "delaunay_decomposition only works on infinite " - + "surfaces if they are immutable." - ) - from flatsurf.geometry.delaunay import LazyDelaunaySurface - - return self.__class__( - LazyDelaunaySurface(self, direction=direction, relabel=relabel) - ) - if in_place: - s = self - else: - s = self.copy(mutable=True, relabel=relabel) - if not delaunay_triangulated: - s.delaunay_triangulation( - triangulated=triangulated, in_place=True, direction=direction - ) - # Now s is Delaunay Triangulated - loop = True - lc = self._label_comparator() - while loop: - loop = False - for (l1, e1), (l2, e2) in s.edge_iterator(gluings=True): - if (lc.lt(l1, l2) or (l1 == l2 and e1 <= e2)) and s._edge_needs_join( - l1, e1 - ): - s.join_polygons(l1, e1, in_place=True) - loop = True - break - return s - - def saddle_connections( - self, - squared_length_bound, - initial_label=None, - initial_vertex=None, - sc_list=None, - check=False, - ): - r""" - Returns a list of saddle connections on the surface whose length squared is less than or equal to squared_length_bound. - The length of a saddle connection is measured using holonomy from polygon in which the trajectory starts. - - If initial_label and initial_vertex are not provided, we return all saddle connections satisfying the bound condition. - - If initial_label and initial_vertex are provided, it only provides saddle connections emanating from the corresponding - vertex of a polygon. If only initial_label is provided, the added saddle connections will only emanate from the - corresponding polygon. - - If sc_list is provided the found saddle connections are appended to this list and the resulting list is returned. - - If check==True it uses the checks in the SaddleConnection class to sanity check our results. - - EXAMPLES:: - sage: from flatsurf import * - sage: s = translation_surfaces.square_torus() - sage: sc_list = s.saddle_connections(13, check=True) - sage: len(sc_list) - 32 - """ - if squared_length_bound <= 0: - raise ValueError - - if sc_list is None: - sc_list = [] - if initial_label is None: - if not self.is_finite(): - raise NotImplementedError - if initial_vertex is not None: - raise ValueError( - "when initial_label is not provided, then initial_vertex must not be provided either" - ) - for label in self.label_iterator(): - self.saddle_connections( - squared_length_bound, initial_label=label, sc_list=sc_list - ) - return sc_list - if initial_vertex is None: - for vertex in range(self.polygon(initial_label).num_edges()): - self.saddle_connections( - squared_length_bound, - initial_label=initial_label, - initial_vertex=vertex, - sc_list=sc_list, - ) - return sc_list - - # Now we have a specified initial_label and initial_vertex - SG = SimilarityGroup(self.base_ring()) - start_data = (initial_label, initial_vertex) - circle = Circle( - self.vector_space().zero(), squared_length_bound, base_ring=self.base_ring() - ) - p = self.polygon(initial_label) - v = p.vertex(initial_vertex) - last_sim = SG(-v[0], -v[1]) - - # First check the edge eminating rightward from the start_vertex. - e = p.edge(initial_vertex) - if e[0] ** 2 + e[1] ** 2 <= squared_length_bound: - sc_list.append(SaddleConnection(self, start_data, e)) - - # Represents the bounds of the beam of trajectories we are sending out. - wedge = ( - last_sim(p.vertex((initial_vertex + 1) % p.num_edges())), - last_sim(p.vertex((initial_vertex + p.num_edges() - 1) % p.num_edges())), - ) - - # This will collect the data we need for a depth first search. - chain = [ - ( - last_sim, - initial_label, - wedge, - [ - (initial_vertex + p.num_edges() - i) % p.num_edges() - for i in range(2, p.num_edges()) - ], - ) - ] - - while len(chain) > 0: - # Should verts really be edges? - sim, label, wedge, verts = chain[-1] - if len(verts) == 0: - chain.pop() - continue - vert = verts.pop() - p = self.polygon(label) - # First check the vertex - vert_position = sim(p.vertex(vert)) - if ( - wedge_product(wedge[0], vert_position) > 0 - and wedge_product(vert_position, wedge[1]) > 0 - and vert_position[0] ** 2 + vert_position[1] ** 2 - <= squared_length_bound - ): - sc_list.append( - SaddleConnection( - self, - start_data, - vert_position, - end_data=(label, vert), - end_direction=~sim.derivative() * -vert_position, - holonomy=vert_position, - end_holonomy=~sim.derivative() * -vert_position, - check=check, - ) - ) - # Now check if we should develop across the edge - vert_position2 = sim(p.vertex((vert + 1) % p.num_edges())) - if ( - wedge_product(vert_position, vert_position2) > 0 - and wedge_product(wedge[0], vert_position2) > 0 - and wedge_product(vert_position, wedge[1]) > 0 - and circle.line_segment_position(vert_position, vert_position2) == 1 - ): - if wedge_product(wedge[0], vert_position) > 0: - # First in new_wedge should be vert_position - if wedge_product(vert_position2, wedge[1]) > 0: - new_wedge = (vert_position, vert_position2) - else: - new_wedge = (vert_position, wedge[1]) - else: - if wedge_product(vert_position2, wedge[1]) > 0: - new_wedge = (wedge[0], vert_position2) - else: - new_wedge = wedge - new_label, new_edge = self.opposite_edge(label, vert) - new_sim = sim * ~self.edge_transformation(label, vert) - p = self.polygon(new_label) - chain.append( - ( - new_sim, - new_label, - new_wedge, - [ - (new_edge + p.num_edges() - i) % p.num_edges() - for i in range(1, p.num_edges()) - ], - ) - ) - return sc_list - - def set_default_graphical_surface(self, graphical_surface): - r""" - Replace the default graphical surface with the provided GraphicalSurface. - """ - from flatsurf.graphical.surface import GraphicalSurface - - if not isinstance(graphical_surface, GraphicalSurface): - raise ValueError("graphical_surface must be a GraphicalSurface") - if self != graphical_surface.get_surface(): - raise ValueError( - "The provided graphical_surface renders a different surface!" - ) - self._gs = graphical_surface - - def graphical_surface(self, *args, **kwds): - r""" - Return a GraphicalSurface representing this surface. - - By default this returns a cached version of the GraphicalSurface. If - ``cached=False`` is provided as a keyword option then a new - GraphicalSurface is returned. - - All other parameters are passed on to - :class:`~flatsurf.graphical.surface.GraphicalSurface` or its - :meth:`~flatsurf.graphical.surface.GraphicalSurface.process_options`. - Note that this mutates the cached graphical surface for future calls. - - EXAMPLES: - - Test the difference between the cached graphical_surface and the uncached version:: - - sage: from flatsurf import * - sage: s = translation_surfaces.octagon_and_squares() - sage: s.plot() - ...Graphics object consisting of 32 graphics primitives - sage: s.graphical_surface(cached=False,adjacencies=[]).plot() - ...Graphics object consisting of 18 graphics primitives - - """ - from flatsurf.graphical.surface import GraphicalSurface - - if "cached" in kwds: - if not kwds["cached"]: - # cached=False: return a new surface. - kwds.pop("cached", None) - return GraphicalSurface(self, *args, **kwds) - kwds.pop("cached", None) - if hasattr(self, "_gs"): - self._gs.process_options(*args, **kwds) - else: - self._gs = GraphicalSurface(self, *args, **kwds) - return self._gs - - def plot(self, *args, **kwds): - r""" - Return a plot of the surface. - - The parameters are passed on to :meth:`graphical_surface` and - :meth:`flatsurf.graphical.surface.GraphicalSurface.plot`. Consult their - documentation for details. - - EXAMPLES:: - - sage: import flatsurf - sage: S = flatsurf.translation_surfaces.veech_double_n_gon(5) - sage: S.plot() - ...Graphics object consisting of 21 graphics primitives - - Keywords are passed on to the underlying plotting routines, see - :meth:`flatsurf.graphical.surface.GraphicalSurface.plot` for details:: - - sage: S.plot(fill=None) - ...Graphics object consisting of 21 graphics primitives - - Note that some keywords mutate the underlying cached graphical surface, - see :meth:`graphical_surface`:: - - sage: S.plot(edge_labels='gluings and number') - ...Graphics object consisting of 23 graphics primitives - - """ - if len(args) > 1: - raise ValueError("plot() can take at most one non-keyword argument") - - graphical_surface_keywords = { - key: kwds.pop(key) - for key in [ - "cached", - "adjacencies", - "polygon_labels", - "edge_labels", - "default_position_function", - ] - if key in kwds - } - - if len(args) == 1: - from flatsurf.graphical.surface import GraphicalSurface - - if not isinstance(args[0], GraphicalSurface): - raise TypeError("non-keyword argument must be a GraphicalSurface") - - import warnings - - warnings.warn( - "Passing a GraphicalSurface to plot() is deprecated because it mutates that GraphicalSurface. This functionality will be removed in a future version of sage-flatsurf. " - "Call process_options() and plot() on the GraphicalSurface explicitly instead." - ) - - gs = args[0] - gs.process_options(**graphical_surface_keywords) - else: - # It's very surprising that plot mutates the underlying cached - # graphical surface. We should change that and make the graphical - # surface not cached. See - # https://github.com/flatsurf/sage-flatsurf/issues/97 - gs = self.graphical_surface(**graphical_surface_keywords) - - return gs.plot(**kwds) - - def plot_polygon( - self, - label, - graphical_surface=None, - plot_polygon=True, - plot_edges=True, - plot_edge_labels=True, - edge_labels=None, - polygon_options={"axes": True}, - edge_options=None, - edge_label_options=None, - ): - r""" - Returns a plot of the polygon with the provided label. - - Note that this method plots the polygon in its coordinates as opposed to - graphical coordinates that the :func:``plot`` method uses. This makes it useful - for visualizing the natural coordinates of the polygon. - - INPUT: - - - ``graphical_surface`` -- (default ``None``) If provided this function pulls graphical options - from the graphical surface. If not provided, we use the default graphical surface. - - - ``plot_polygon`` -- (default ``True``) If True, we plot the solid polygon. - - - ``polygon_options`` -- (default ``{"axes":True}``) Options for the rendering of the polygon. - These options will be passed to :func:`~flatsurf.graphical.polygon.GraphicalPolygon.plot_polygon`. - This should be either None or a dictionary. - - - ``plot_edges`` -- (default ``True``) If True, we plot the edges of the polygon as segments. - - - ``edge_options`` -- (default ``None``) Options for the rendering of the polygon edges. - These options will be passed to :func:`~flatsurf.graphical.polygon.GraphicalPolygon.plot_edge`. - This should be either None or a dictionary. - - - ``plot_edge_labels`` -- (default ``True``) If True, we plot labels on the edges. - - - ``edge_label_options`` -- (default ``None``) Options for the rendering of the edge labels. - These options will be passed to :func:`~flatsurf.graphical.polygon.GraphicalPolygon.plot_edge_label`. - This should be either None or a dictionary. - - - ``edge_labels`` -- (default ``None``) If None and plot_edge_labels is True, we write the edge - number on each edge. Otherwise edge_labels should be a list of strings of length equal to the - number of edges of the polygon. The strings will be printed on each edge. - - EXAMPLES:: - - sage: from flatsurf import * - sage: s = similarity_surfaces.example() - sage: s.plot() - ...Graphics object consisting of 13 graphics primitives - sage: s.plot_polygon(1) - ...Graphics object consisting of 7 graphics primitives - - sage: labels = [] - sage: p = s.polygon(1) - sage: for e in range(p.num_edges()): \ - labels.append(str(p.edge(e))) - sage: s.plot_polygon(1, polygon_options=None, plot_edges=False, \ - edge_labels=labels, edge_label_options={"color":"red"}) - ...Graphics object consisting of 4 graphics primitives - """ - if graphical_surface is None: - graphical_surface = self.graphical_surface() - p = self.polygon(label) - from flatsurf.graphical.polygon import GraphicalPolygon - - gp = GraphicalPolygon(p) - - if plot_polygon: - if polygon_options is None: - o = graphical_surface.polygon_options - else: - o = graphical_surface.polygon_options.copy() - o.update(polygon_options) - plt = gp.plot_polygon(**o) - - if plot_edges: - if edge_options is None: - o = graphical_surface.non_adjacent_edge_options - else: - o = graphical_surface.non_adjacent_edge_options.copy() - o.update(edge_options) - for e in range(p.num_edges()): - plt += gp.plot_edge(e, **o) - - if plot_edge_labels: - if edge_label_options is None: - o = graphical_surface.edge_label_options - else: - o = graphical_surface.edge_label_options.copy() - o.update(edge_label_options) - for e in range(p.num_edges()): - if edge_labels is None: - el = str(e) - else: - el = edge_labels[e] - plt += gp.plot_edge_label(e, el, **o) - return plt - - def __eq__(self, other): - r""" - Return whether this surface is indistinguishable from ``other`` as a - similarity surface. - - """ - if self is other: - return True - - if not isinstance(other, SimilaritySurface): - return False - - return self.underlying_surface() == other.underlying_surface() - - def __ne__(self, other): - return not self == other - - @cached_method - def __hash__(self): - r""" - Hash compatible with equals. - """ - if self._s.is_mutable(): - raise ValueError("Attempting to hash with mutable underlying surface.") - # Compute the hash - h = 17 * hash(self.base_ring()) + 23 * hash(self.base_label()) - for pair in self.label_iterator(polygons=True): - h = h + 7 * hash(pair) - for edgepair in self.edge_iterator(gluings=True): - h = h + 3 * hash(edgepair) - return h diff --git a/flatsurf/geometry/similarity_surface_generators.py b/flatsurf/geometry/similarity_surface_generators.py index 8b26275bb..f02a368d0 100644 --- a/flatsurf/geometry/similarity_surface_generators.py +++ b/flatsurf/geometry/similarity_surface_generators.py @@ -25,16 +25,17 @@ from sage.misc.cachefunc import cached_method from sage.structure.sequence import Sequence -from .polygon import polygons, ConvexPolygons, Polygon, ConvexPolygon, build_faces +from flatsurf.geometry.polygon import ( + polygons, + EuclideanPolygon, + Polygon, +) -from .surface import Surface, Surface_list -from .translation_surface import TranslationSurface -from .dilation_surface import DilationSurface -from .similarity_surface import SimilaritySurface -from .half_translation_surface import HalfTranslationSurface -from .cone_surface import ConeSurface -from .rational_cone_surface import RationalConeSurface -from .translation_surface import Origami +from flatsurf.geometry.surface import ( + OrientedSimilaritySurface, + MutableOrientedSimilaritySurface, +) +from flatsurf.geometry.origami import Origami ZZ_1 = ZZ(1) @@ -92,7 +93,7 @@ def flipper_nf_element_to_sage(x, K=None): return K(coeffs) -class EInfinitySurface(Surface): +class EInfinitySurface(OrientedSimilaritySurface): r""" The surface based on the `E_\infinity` graph. @@ -118,34 +119,100 @@ def __init__(self, lambda_squared=None, field=None): field = NumberField( x**3 - ZZ(5) * x**2 + ZZ(4) * x - ZZ(1), "r", embedding=AA(ZZ(4)) ) - self._l = field.gen() + self._lambda_squared = field.gen() else: if field is None: - self._l = lambda_squared + self._lambda_squared = lambda_squared field = lambda_squared.parent() else: - self._l = field(lambda_squared) - super().__init__(field, ZZ.zero(), finite=False, mutable=False) + self._lambda_squared = field(lambda_squared) + from flatsurf.geometry.categories import TranslationSurfaces + + super().__init__( + field, + category=TranslationSurfaces().InfiniteType().Connected().WithoutBoundary(), + ) + + def is_compact(self): + r""" + Return whether this surface is compact as a topological space, i.e., + return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_compact`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.e_infinity_surface() + sage: S.is_compact() + False + + """ + return False + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.e_infinity_surface() + sage: S.is_mutable() + False + + """ + return False + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.e_infinity_surface() + sage: S.roots() + (0,) + + """ + return (ZZ(0),) def _repr_(self): r""" - String representation. + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.e_infinity_surface() + sage: S + EInfinitySurface(r) + """ - return "The E-infinity surface" + return f"EInfinitySurface({repr(self._lambda_squared)})" @cached_method def get_white(self, n): r"""Get the weight of the white endpoint of edge n.""" if n == 0 or n == 1: - return self._l + return self._lambda_squared if n == -1: - return self._l - 1 + return self._lambda_squared - 1 if n == 2: - return 1 - 3 * self._l + self._l**2 + return 1 - 3 * self._lambda_squared + self._lambda_squared**2 if n > 2: x = self.get_white(n - 1) y = self.get_black(n) - return self._l * y - x + return self._lambda_squared * y - x return self.get_white(-n) @cached_method @@ -154,7 +221,7 @@ def get_black(self, n): if n == 0: return self.base_ring().one() if n == 1 or n == -1 or n == 2: - return self._l - 1 + return self._lambda_squared - 1 if n > 2: x = self.get_black(n - 1) y = self.get_white(n - 1) @@ -165,15 +232,28 @@ def polygon(self, lab): r""" Return the polygon labeled by ``lab``. """ - if lab not in self.polygon_labels(): + if lab not in self.labels(): raise ValueError("lab (=%s) not a valid label" % lab) return polygons.rectangle(2 * self.get_black(lab), self.get_white(lab)) - def polygon_labels(self): + def labels(self): r""" - The set of labels used for the polygons. + Return the labels of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.e_infinity_surface() + sage: S.labels() + (0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, …) + """ - return ZZ + from flatsurf.geometry.surface import LabelsFromView + + return LabelsFromView(self, ZZ, finite=False) def opposite_edge(self, p, e): r""" @@ -226,10 +306,27 @@ def opposite_edge(self, p, e): else: return 1 - p, (e + 2) % 4 + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: hash(translation_surfaces.e_infinity_surface()) == hash(translation_surfaces.e_infinity_surface()) + True + + """ + return hash((self.base_ring(), self._lambda_squared)) + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: sage: from flatsurf import translation_surfaces @@ -238,13 +335,16 @@ def __eq__(self, other): True """ - if isinstance(other, EInfinitySurface): - return self._l == other._l and self.base_ring() == other.base_ring() + if not isinstance(other, EInfinitySurface): + return False - return super().__eq__(other) + return ( + self._lambda_squared == other._lambda_squared + and self.base_ring() == other.base_ring() + ) -class TFractalSurface(Surface): +class TFractalSurface(OrientedSimilaritySurface): r""" The TFractal surface. @@ -278,6 +378,18 @@ def __init__(self, w=ZZ_1, r=ZZ_2, h1=ZZ_1, h2=ZZ_1): field = Sequence([w, r, h1, h2]).universe() if not field.is_field(): field = field.fraction_field() + + from flatsurf.geometry.categories import TranslationSurfaces + + super().__init__( + field, + category=TranslationSurfaces() + .InfiniteType() + .WithoutBoundary() + .Compact() + .Connected(), + ) + self._w = field(w) self._r = field(r) self._h1 = field(h1) @@ -286,11 +398,42 @@ def __init__(self, w=ZZ_1, r=ZZ_2, h1=ZZ_1, h2=ZZ_1): self._wL = self._words("L") self._wR = self._words("R") - base_label = self.polygon_labels()._cartesian_product_of_elements( - (self._words(""), 0) - ) + self._root = (self._words(""), 0) + + def roots(self): + r""" + Return root labels for the polygons forming the connected + components of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.t_fractal() + sage: S.roots() + ((word: , 0),) - super().__init__(field, base_label, finite=False, mutable=False) + """ + return (self._root,) + + def is_mutable(self): + r""" + Return whether this surface is mutable, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.t_fractal() + sage: S.is_mutable() + False + + """ + return False def _repr_(self): return "The T-fractal surface with parameters w=%s, r=%s, h1=%s, h2=%s" % ( @@ -301,11 +444,29 @@ def _repr_(self): ) @cached_method - def polygon_labels(self): + def labels(self): + r""" + Return the labels of this surface. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.t_fractal() + sage: S.labels() + ((word: , 0), (word: , 2), (word: , 3), (word: , 1), (word: R, 2), (word: R, 0), (word: L, 2), (word: L, 0), (word: R, 3), (word: R, 1), (word: L, 3), (word: L, 1), (word: RR, 2), (word: RR, 0), (word: RL, 2), (word: RL, 0), …) + + """ from sage.sets.finite_enumerated_set import FiniteEnumeratedSet from sage.categories.cartesian_product import cartesian_product - return cartesian_product([self._words, FiniteEnumeratedSet([0, 1, 2, 3])]) + labels = cartesian_product([self._words, FiniteEnumeratedSet([0, 1, 2, 3])]) + + from flatsurf.geometry.surface import LabelsFromView + + return LabelsFromView(self, labels, finite=False) def opposite_edge(self, p, e): r""" @@ -329,7 +490,7 @@ def opposite_edge(self, p, e): sage: import flatsurf.geometry.similarity_surface_generators as sfg sage: T = sfg.tfractal_surface() - sage: W = T.underlying_surface()._words + sage: W = T._words sage: w = W('LLRLRL') sage: T.opposite_edge((w,0),0) ((word: LLRLR, 1), 2) @@ -406,7 +567,6 @@ def opposite_edge(self, p, e): raise ValueError("i (={!r}) must be either 0,1,2 or 3".format(i)) # the fastest label constructor - lab = self.polygon_labels()._cartesian_product_of_elements(lab) return lab, f def polygon(self, lab): @@ -427,9 +587,9 @@ def polygon(self, lab): sage: import flatsurf.geometry.similarity_surface_generators as sfg sage: T = sfg.tfractal_surface() sage: T.polygon(('L',0)) - Polygon: (0, 0), (1/2, 0), (1/2, 1/2), (0, 1/2) + Polygon(vertices=[(0, 0), (1/2, 0), (1/2, 1/2), (0, 1/2)]) sage: T.polygon(('LRL',0)) - Polygon: (0, 0), (1/8, 0), (1/8, 1/8), (0, 1/8) + Polygon(vertices=[(0, 0), (1/8, 0), (1/8, 1/8), (0, 1/8)]) """ w = self._words(lab[0]) return (1 / self._r ** w.length()) * self._base_polygon(lab[1]) @@ -445,22 +605,51 @@ def _base_polygon(self, i): if i == 2: w = self._w h = self._h2 - return ConvexPolygons(self.base_ring())([(w, 0), (0, h), (-w, 0), (0, -h)]) + return Polygon( + base_ring=self.base_ring(), edges=[(w, 0), (0, h), (-w, 0), (0, -h)] + ) + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: hash(translation_surfaces.t_fractal()) == hash(translation_surfaces.t_fractal()) + True + + """ + return hash((self._w, self._h1, self._r, self._h2)) def __eq__(self, other): - if isinstance(other, TFractalSurface): - return ( - self._w == other._w - and self._h1 == other._h1 - and self._r == other._r - and self._h2 == other._h2 - ) + r""" + Return whether this surface is indistinguishable from ``other``. - return super().__eq__(other) + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: translation_surfaces.t_fractal() == translation_surfaces.t_fractal() + True + + """ + if not isinstance(other, TFractalSurface): + return False + + return ( + self._w == other._w + and self._h1 == other._h1 + and self._r == other._r + and self._h2 == other._h2 + ) def tfractal_surface(w=ZZ_1, r=ZZ_2, h1=ZZ_1, h2=ZZ_1): - return TranslationSurface(TFractalSurface(w, r, h1, h2)) + return TFractalSurface(w, r, h1, h2) class SimilaritySurfaceGenerators: @@ -473,24 +662,32 @@ def example(): r""" Construct a SimilaritySurface from a pair of triangles. - TESTS:: + EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import similarity_surfaces sage: ex = similarity_surfaces.example() sage: ex - SimilaritySurface built from 2 polygons + Genus 1 Surface built from 2 isosceles triangles + + TESTS:: + sage: TestSuite(ex).run() + sage: from flatsurf.geometry.categories import SimilaritySurfaces + sage: ex in SimilaritySurfaces() + True + """ - s = Surface_list(base_ring=QQ) - s.add_polygon( - polygons(vertices=[(0, 0), (2, -2), (2, 0)], ring=QQ) - ) # gets label 0 + s = MutableOrientedSimilaritySurface(QQ) + s.add_polygon( - polygons(vertices=[(0, 0), (2, 0), (1, 3)], ring=QQ) - ) # gets label 1 - s.change_polygon_gluings(0, [(1, 1), (1, 2), (1, 0)]) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)], base_ring=QQ), label=0 + ) + s.add_polygon(Polygon(vertices=[(0, 0), (2, 0), (1, 3)], base_ring=QQ), label=1) + s.glue((0, 0), (1, 1)) + s.glue((0, 1), (1, 2)) + s.glue((0, 2), (1, 0)) s.set_immutable() - return SimilaritySurface(s) + return s @staticmethod def self_glued_polygon(P): @@ -499,46 +696,59 @@ def self_glued_polygon(P): EXAMPLES:: - sage: from flatsurf import * - sage: p = polygons((2,0),(-1,3),(-1,-3)) + sage: from flatsurf import Polygon, similarity_surfaces + sage: p = Polygon(edges=[(2,0),(-1,3),(-1,-3)]) sage: s = similarity_surfaces.self_glued_polygon(p) + sage: s + Half-Translation Surface in Q_0(-1^4) built from an isosceles triangle sage: TestSuite(s).run() + """ - s = Surface_list(base_ring=P.base_ring(), mutable=True) - s.add_polygon(P, [(0, i) for i in range(P.num_edges())]) + s = MutableOrientedSimilaritySurface(P.base_ring()) + s.add_polygon(P) + for i in range(len(P.vertices())): + s.glue((0, i), (0, i)) s.set_immutable() - return HalfTranslationSurface(s) + return s @staticmethod - def billiard(P, rational=False): + def billiard(P, rational=None): r""" Return the ConeSurface associated to the billiard in the polygon ``P``. INPUT: - - ``P`` - a polygon + - ``P`` -- a polygon - - ``rational`` (boolean, default ``False``) - whether to assume that the polygon - has all its angle rational multiple of pi. + - ``rational`` -- a boolean or ``None`` (default: ``None``) -- whether + to assume that all the angles of ``P`` are a rational multiple of π. EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import Polygon, similarity_surfaces - sage: P = polygons(vertices=[(0,0), (1,0), (0,1)]) - sage: from flatsurf.geometry.rational_cone_surface import RationalConeSurface + sage: P = Polygon(vertices=[(0,0), (1,0), (0,1)]) sage: Q = similarity_surfaces.billiard(P, rational=True) + doctest:warning + ... + UserWarning: the rational keyword argument of billiard() has been deprecated and will be removed in a future version of sage-flatsurf; rationality checking is now faster so this is not needed anymore sage: Q - RationalConeSurface built from 2 polygons + Genus 0 Rational Cone Surface built from 2 isosceles triangles + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: Q in ConeSurfaces().Rational() + True sage: M = Q.minimal_cover(cover_type="translation") sage: M - TranslationSurface built from 8 polygons + Minimal Translation Cover of Genus 0 Rational Cone Surface built from 2 isosceles triangles sage: TestSuite(M).run() + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: M in TranslationSurfaces() + True A non-convex examples (L-shape polygon):: - sage: P = polygons(vertices=[(0,0), (2,0), (2,1), (1,1), (1,2), (0,2)], convex=False) - sage: Q = similarity_surfaces.billiard(P, rational=True) + sage: P = Polygon(vertices=[(0,0), (2,0), (2,1), (1,1), (1,2), (0,2)]) + sage: Q = similarity_surfaces.billiard(P) sage: TestSuite(Q).run() sage: M = Q.minimal_cover(cover_type="translation") sage: TestSuite(M).run() @@ -547,8 +757,8 @@ def billiard(P, rational=False): A quadrilateral from Eskin-McMullen-Mukamel-Wright:: - sage: E = EquiangularPolygons(1, 1, 1, 7) - sage: P = E.an_element() + sage: from flatsurf import Polygon + sage: P = Polygon(angles=(1, 1, 1, 7)) sage: S = similarity_surfaces.billiard(P) sage: TestSuite(S).run() sage: S = S.minimal_cover(cover_type="translation") @@ -560,33 +770,58 @@ def billiard(P, rational=False): Unfolding a triangle with non-algebraic lengths:: - sage: E = EquiangularPolygons(3, 3, 5) + sage: from flatsurf import EuclideanPolygonsWithAngles + sage: E = EuclideanPolygonsWithAngles((3, 3, 5)) sage: from pyexactreal import ExactReals # optional: exactreal sage: R = ExactReals(E.base_ring()) # optional: exactreal - sage: P = E(R.random_element()) # optional: exactreal + sage: angles = (3, 3, 5) + sage: slopes = EuclideanPolygonsWithAngles(*angles).slopes() + sage: P = Polygon(angles=angles, edges=[R.random_element() * slopes[0]]) # optional: exactreal sage: S = similarity_surfaces.billiard(P); S # optional: exactreal - ConeSurface built from 2 polygons + Genus 0 Rational Cone Surface built from 2 isosceles triangles sage: TestSuite(S).run() # long time (6s), optional: exactreal + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: S in ConeSurfaces() + True """ - if not isinstance(P, Polygon): + if not isinstance(P, EuclideanPolygon): raise TypeError("invalid input") - V = P.module() + if rational is not None: + import warnings + + warnings.warn( + "the rational keyword argument of billiard() has been deprecated and will be removed in a future version of sage-flatsurf; rationality checking is now faster so this is not needed anymore" + ) + + from flatsurf.geometry.categories import ConeSurfaces + + category = ConeSurfaces() + if P.is_rational(): + category = category.Rational() - if not isinstance(P, ConvexPolygon): + V = P.base_ring() ** 2 + + if not P.is_convex(): # triangulate non-convex ones base_ring = P.base_ring() - C = ConvexPolygons(base_ring) comb_edges = P.triangulation() vertices = P.vertices() - comb_triangles = build_faces(len(vertices), comb_edges) + comb_triangles = SimilaritySurfaceGenerators._billiard_build_faces( + len(vertices), comb_edges + ) triangles = [] internal_edges = [] # list (p1, e1, p2, e2) external_edges = [] # list (p1, e1) edge_to_lab = {} for num, (i, j, k) in enumerate(comb_triangles): - triangles.append(C(vertices=[vertices[i], vertices[j], vertices[k]])) + triangles.append( + Polygon( + vertices=[vertices[i], vertices[j], vertices[k]], + base_ring=base_ring, + ) + ) edge_to_lab[(i, j)] = (num, 0) edge_to_lab[(j, k)] = (num, 1) edge_to_lab[(k, i)] = (num, 2) @@ -609,34 +844,76 @@ def billiard(P, rational=False): P = triangles else: internal_edges = [] - external_edges = [(0, i) for i in range(P.num_edges())] + external_edges = [(0, i) for i in range(len(P.vertices()))] base_ring = P.base_ring() P = [P] + surface = MutableOrientedSimilaritySurface(base_ring, category=category) + m = len(P) - Q = [] - surface = Surface_list(base_ring=base_ring) + for p in P: surface.add_polygon(p) for p in P: surface.add_polygon( - polygons(edges=[V((-x, y)) for x, y in reversed(p.edges())]) + Polygon(edges=[V((-x, y)) for x, y in reversed(p.edges())]) ) for p1, e1, p2, e2 in internal_edges: - surface.set_edge_pairing(p1, e1, p2, e2) - ne1 = surface.polygon(p1).num_edges() - ne2 = surface.polygon(p2).num_edges() - surface.set_edge_pairing(m + p1, ne1 - e1 - 1, m + p2, ne2 - e2 - 1) + surface.glue((p1, e1), (p2, e2)) + ne1 = len(surface.polygon(p1).vertices()) + ne2 = len(surface.polygon(p2).vertices()) + surface.glue((m + p1, ne1 - e1 - 1), (m + p2, ne2 - e2 - 1)) for p, e in external_edges: - ne = surface.polygon(p).num_edges() - surface.set_edge_pairing(p, e, m + p, ne - e - 1) + ne = len(surface.polygon(p).vertices()) + surface.glue((p, e), (m + p, ne - e - 1)) surface.set_immutable() - if rational: - s = RationalConeSurface(surface) - else: - s = ConeSurface(surface) - return s + + return surface + + @staticmethod + def _billiard_build_faces(n, edges): + r""" + Given a combinatorial list of pairs ``edges`` forming a cell-decomposition + of a polygon (with vertices labeled from ``0`` to ``n-1``) return the list + of cells. + + This is a helper method for :meth:`billiard`. + + EXAMPLES:: + + sage: from flatsurf.geometry.similarity_surface_generators import SimilaritySurfaceGenerators + sage: SimilaritySurfaceGenerators._billiard_build_faces(4, [(0,2)]) + [[0, 1, 2], [2, 3, 0]] + sage: SimilaritySurfaceGenerators._billiard_build_faces(4, [(1,3)]) + [[1, 2, 3], [3, 0, 1]] + sage: SimilaritySurfaceGenerators._billiard_build_faces(5, [(0,2), (0,3)]) + [[0, 1, 2], [3, 4, 0], [0, 2, 3]] + sage: SimilaritySurfaceGenerators._billiard_build_faces(5, [(0,2)]) + [[0, 1, 2], [2, 3, 4, 0]] + sage: SimilaritySurfaceGenerators._billiard_build_faces(5, [(1,4)]) + [[1, 2, 3, 4], [4, 0, 1]] + sage: SimilaritySurfaceGenerators._billiard_build_faces(5, [(1,3),(3,0)]) + [[1, 2, 3], [3, 4, 0], [0, 1, 3]] + """ + polygons = [list(range(n))] + for u, v in edges: + j = None + for i, p in enumerate(polygons): + if u in p and v in p: + if j is not None: + raise RuntimeError + j = i + if j is None: + raise RuntimeError + p = polygons[j] + i0 = p.index(u) + i1 = p.index(v) + if i0 > i1: + i0, i1 = i1, i0 + polygons[j] = p[i0 : i1 + 1] + polygons.append(p[i1:] + p[: i0 + 1]) + return polygons @staticmethod def polygon_double(P): @@ -647,42 +924,49 @@ def polygon_double(P): """ from sage.matrix.constructor import matrix - n = P.num_edges() + n = len(P.vertices()) r = matrix(2, [-1, 0, 0, 1]) - Q = polygons(edges=[r * v for v in reversed(P.edges())]) + Q = Polygon(edges=[r * v for v in reversed(P.edges())]) - surface = Surface_list(base_ring=P.base_ring()) - surface.add_polygon(P) # gets label 0) - surface.add_polygon(Q) # gets label 1 - surface.change_polygon_gluings(0, [(1, n - i - 1) for i in range(n)]) + surface = MutableOrientedSimilaritySurface(P.base_ring()) + surface.add_polygon(P, label=0) + surface.add_polygon(Q, label=1) + for i in range(n): + surface.glue((0, i), (1, n - i - 1)) surface.set_immutable() - return ConeSurface(surface) + return surface @staticmethod def right_angle_triangle(w, h): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import similarity_surfaces sage: R = similarity_surfaces.right_angle_triangle(2, 3) sage: R - ConeSurface built from 2 polygons + Genus 0 Cone Surface built from 2 right triangles + sage: from flatsurf.geometry.categories import ConeSurfaces + sage: R in ConeSurfaces() + True sage: TestSuite(R).run() """ - from sage.modules.free_module_element import vector - F = Sequence([w, h]).universe() if not F.is_field(): F = F.fraction_field() V = VectorSpace(F, 2) - P = ConvexPolygons(F) - s = Surface_list(base_ring=F) - s.add_polygon(P([V((w, 0)), V((-w, h)), V((0, -h))])) # gets label 0 - s.add_polygon(P([V((0, h)), V((-w, -h)), V((w, 0))])) # gets label 1 - s.change_polygon_gluings(0, [(1, 2), (1, 1), (1, 0)]) + s = MutableOrientedSimilaritySurface(F) + s.add_polygon( + Polygon(base_ring=F, edges=[V((w, 0)), V((-w, h)), V((0, -h))]), label=0 + ) + s.add_polygon( + Polygon(base_ring=F, edges=[V((0, h)), V((-w, -h)), V((w, 0))]), label=1 + ) + s.glue((0, 0), (1, 2)) + s.glue((0, 1), (1, 1)) + s.glue((0, 2), (1, 0)) s.set_immutable() - return ConeSurface(s) + return s similarity_surfaces = SimilaritySurfaceGenerators() @@ -707,24 +991,33 @@ def basic_dilation_torus(a): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import dilation_surfaces sage: ds = dilation_surfaces.basic_dilation_torus(AA(sqrt(2))) sage: ds - DilationSurface built from 2 polygons + Genus 1 Positive Dilation Surface built from a square and a rectangle + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: ds in DilationSurfaces().Positive() + True sage: TestSuite(ds).run() """ - s = Surface_list(base_ring=a.parent().fraction_field()) - CP = ConvexPolygons(s.base_ring()) - s.add_polygon(CP(edges=[(0, 1), (-1, 0), (0, -1), (1, 0)])) # label 0 - s.add_polygon(CP(edges=[(0, 1), (-a, 0), (0, -1), (a, 0)])) # label 1 - s.change_edge_gluing(0, 0, 1, 2) - s.change_edge_gluing(0, 1, 1, 3) - s.change_edge_gluing(0, 2, 1, 0) - s.change_edge_gluing(0, 3, 1, 1) - s.change_base_label(0) + s = MutableOrientedSimilaritySurface(a.parent().fraction_field()) + s.add_polygon( + Polygon(base_ring=s.base_ring(), edges=[(0, 1), (-1, 0), (0, -1), (1, 0)]), + label=0, + ) + s.add_polygon( + Polygon(base_ring=s.base_ring(), edges=[(0, 1), (-a, 0), (0, -1), (a, 0)]), + label=1, + ) + # label 1 + s.glue((0, 0), (1, 2)) + s.glue((0, 1), (1, 3)) + s.glue((0, 2), (1, 0)) + s.glue((0, 3), (1, 1)) + s.set_roots([0]) s.set_immutable() - return DilationSurface(s) + return s @staticmethod def genus_two_square(a, b, c, d): @@ -755,32 +1048,36 @@ def genus_two_square(a, b, c, d): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import dilation_surfaces sage: ds = dilation_surfaces.genus_two_square(1/2, 1/3, 1/4, 1/5) sage: ds - DilationSurface built from 3 polygons + Genus 2 Positive Dilation Surface built from 2 right triangles and a hexagon + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: ds in DilationSurfaces().Positive() + True sage: TestSuite(ds).run() """ field = Sequence([a, b, c, d]).universe().fraction_field() - s = Surface_list(base_ring=QQ) - CP = ConvexPolygons(field) - hexagon = CP( - edges=[(a, 0), (1 - a, b), (0, 1 - b), (-c, 0), (c - 1, -d), (0, d - 1)] + s = MutableOrientedSimilaritySurface(QQ) + + hexagon = Polygon( + edges=[(a, 0), (1 - a, b), (0, 1 - b), (-c, 0), (c - 1, -d), (0, d - 1)], + base_ring=field, ) - s.add_polygon(hexagon) # polygon 0 - s.change_base_label(0) - triangle1 = CP(edges=[(1 - a, 0), (0, b), (a - 1, -b)]) - s.add_polygon(triangle1) # polygon 1 - triangle2 = CP(edges=[(1 - c, d), (c - 1, 0), (0, -d)]) - s.add_polygon(triangle2) # polygon 2 - s.change_edge_gluing(0, 0, 0, 3) - s.change_edge_gluing(0, 2, 0, 5) - s.change_edge_gluing(0, 1, 1, 2) - s.change_edge_gluing(0, 4, 2, 0) - s.change_edge_gluing(1, 0, 2, 1) - s.change_edge_gluing(1, 1, 2, 2) + s.add_polygon(hexagon, label=0) + s.set_roots([0]) + triangle1 = Polygon(base_ring=field, edges=[(1 - a, 0), (0, b), (a - 1, -b)]) + s.add_polygon(triangle1, label=1) + triangle2 = Polygon(base_ring=field, edges=[(1 - c, d), (c - 1, 0), (0, -d)]) + s.add_polygon(triangle2, label=2) + s.glue((0, 0), (0, 3)) + s.glue((0, 2), (0, 5)) + s.glue((0, 1), (1, 2)) + s.glue((0, 4), (2, 0)) + s.glue((1, 0), (2, 1)) + s.glue((1, 1), (2, 2)) s.set_immutable() - return DilationSurface(s) + return s dilation_surfaces = DilationSurfaceGenerators() @@ -799,7 +1096,10 @@ def step_billiard(w, h): sage: from flatsurf import half_translation_surfaces sage: S = half_translation_surfaces.step_billiard([1,1,1,1], [1,1/2,1/3,1/5]) sage: S - HalfTranslationSurface built from 8 polygons + StepBilliard(w=[1, 1, 1, 1], h=[1, 1/2, 1/3, 1/5]) + sage: from flatsurf.geometry.categories import DilationSurfaces + sage: S in DilationSurfaces() + True sage: TestSuite(S).run() """ n = len(h) @@ -811,7 +1111,7 @@ def step_billiard(w, h): W = sum(w) R = Sequence(w + h).universe() - C = ConvexPolygons(R.fraction_field()) + base_ring = R.fraction_field() P = [] Prev = [] @@ -819,53 +1119,67 @@ def step_billiard(w, h): y = H for i in range(n - 1): P.append( - C( + Polygon( vertices=[ (x, 0), (x + w[i], 0), (x + w[i], y - h[i]), (x + w[i], y), (x, y), - ] + ], + base_ring=base_ring, ) ) x += w[i] y -= h[i] assert x == W - w[-1] assert y == h[-1] - P.append(C(vertices=[(x, 0), (x + w[-1], 0), (x + w[-1], y), (x, y)])) + P.append( + Polygon( + vertices=[(x, 0), (x + w[-1], 0), (x + w[-1], y), (x, y)], + base_ring=base_ring, + ) + ) - Prev = [C(vertices=[(x, -y) for x, y in reversed(p.vertices())]) for p in P] + Prev = [ + Polygon( + vertices=[(x, -y) for x, y in reversed(p.vertices())], + base_ring=base_ring, + ) + for p in P + ] - S = Surface_list(base_ring=C.base_ring()) + S = MutableOrientedSimilaritySurface(base_ring) S.rename( "StepBilliard(w=[%s], h=[%s])" % (", ".join(map(str, w)), ", ".join(map(str, h))) ) - S.add_polygons(P) # get labels 0, ..., n-1 - S.add_polygons(Prev) # get labels n, n+1, ..., 2n-1 + for p in P: + S.add_polygon(p) # get labels 0, ..., n-1 + for p in Prev: + S.add_polygon(p) # get labels n, n+1, ..., 2n-1 # reflection gluings # (gluings between the polygon and its reflection) - S.set_edge_pairing(0, 4, n, 4) - S.set_edge_pairing(n - 1, 0, 2 * n - 1, 2) - S.set_edge_pairing(n - 1, 1, 2 * n - 1, 1) - S.set_edge_pairing(n - 1, 2, 2 * n - 1, 0) + S.glue((0, 4), (n, 4)) + S.glue((n - 1, 0), (2 * n - 1, 2)) + S.glue((n - 1, 1), (2 * n - 1, 1)) + S.glue((n - 1, 2), (2 * n - 1, 0)) for i in range(n - 1): - # set_edge_pairing(polygon1, edge1, polygon2, edge2) - S.set_edge_pairing(i, 0, n + i, 3) - S.set_edge_pairing(i, 2, n + i, 1) - S.set_edge_pairing(i, 3, n + i, 0) + # glue((polygon1, edge1), (polygon2, edge2)) + S.glue((i, 0), (n + i, 3)) + S.glue((i, 2), (n + i, 1)) + S.glue((i, 3), (n + i, 0)) # translation gluings - S.set_edge_pairing(n - 2, 1, n - 1, 3) - S.set_edge_pairing(2 * n - 2, 2, 2 * n - 1, 3) + S.glue((n - 2, 1), (n - 1, 3)) + S.glue((2 * n - 2, 2), (2 * n - 1, 3)) for i in range(n - 2): - S.set_edge_pairing(i, 1, i + 1, 4) - S.set_edge_pairing(n + i, 2, n + i + 1, 4) + S.glue((i, 1), (i + 1, 4)) + S.glue((n + i, 2), (n + i + 1, 4)) S.set_immutable() - return HalfTranslationSurface(S) + return S half_translation_surfaces = HalfTranslationSurfaceGenerators() @@ -884,10 +1198,13 @@ def square_torus(a=1): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: T = translation_surfaces.square_torus() sage: T - TranslationSurface built from 1 polygon + Translation Surface in H_1(0) built from a square + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: T in TranslationSurfaces() + True sage: TestSuite(T).run() Rational directions are completely periodic:: @@ -915,9 +1232,13 @@ def torus(u, v): sage: from flatsurf import translation_surfaces sage: T = translation_surfaces.torus((1, AA(2).sqrt()), (AA(3).sqrt(), 3)) sage: T - TranslationSurface built from 1 polygon + Translation Surface in H_1(0) built from a quadrilateral sage: T.polygon(0) - Polygon: (0, 0), (1, 1.414213562373095?), (2.732050807568878?, 4.414213562373095?), (1.732050807568878?, 3) + Polygon(vertices=[(0, 0), (1, 1.414213562373095?), (2.732050807568878?, 4.414213562373095?), (1.732050807568878?, 3)]) + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: T in TranslationSurfaces() + True + """ u = vector(u) v = vector(v) @@ -926,11 +1247,13 @@ def torus(u, v): field = py_scalar_parent(field) if not field.is_field(): field = field.fraction_field() - s = Surface_list(base_ring=field) - p = polygons(vertices=[(0, 0), u, u + v, v], base_ring=field) - s.add_polygon(p, [(0, 2), (0, 3), (0, 0), (0, 1)]) + s = MutableOrientedSimilaritySurface(field) + p = Polygon(vertices=[(0, 0), u, u + v, v], base_ring=field) + s.add_polygon(p) + s.glue((0, 0), (0, 2)) + s.glue((0, 1), (0, 3)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def veech_2n_gon(n): @@ -939,17 +1262,20 @@ def veech_2n_gon(n): EXAMPLES:: - sage: from flatsurf import * - sage: s=translation_surfaces.veech_2n_gon(5) - sage: s.polygon(0) - Polygon: (0, 0), (1, 0), (-1/2*a^2 + 5/2, 1/2*a), (-a^2 + 7/2, -1/2*a^3 + 2*a), (-1/2*a^2 + 5/2, -a^3 + 7/2*a), (1, -a^3 + 4*a), (0, -a^3 + 4*a), (1/2*a^2 - 3/2, -a^3 + 7/2*a), (a^2 - 5/2, -1/2*a^3 + 2*a), (1/2*a^2 - 3/2, 1/2*a) + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.veech_2n_gon(5) + sage: s + Translation Surface in H_2(1^2) built from a regular decagon sage: TestSuite(s).run() + """ p = polygons.regular_ngon(2 * n) - s = Surface_list(base_ring=p.base_ring()) - s.add_polygon(p, [(0, (i + n) % (2 * n)) for i in range(2 * n)]) + s = MutableOrientedSimilaritySurface(p.base_ring()) + s.add_polygon(p) + for i in range(2 * n): + s.glue((0, i), (0, (i + n) % (2 * n))) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def veech_double_n_gon(n): @@ -958,19 +1284,24 @@ def veech_double_n_gon(n): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s=translation_surfaces.veech_double_n_gon(5) + sage: s + Translation Surface in H_2(2) built from 2 regular pentagons sage: TestSuite(s).run() + """ from sage.matrix.constructor import Matrix p = polygons.regular_ngon(n) - s = Surface_list(base_ring=p.base_ring()) + s = MutableOrientedSimilaritySurface(p.base_ring()) m = Matrix([[-1, 0], [0, -1]]) - s.add_polygon(p) # label=0 - s.add_polygon(m * p, [(0, i) for i in range(n)]) + s.add_polygon(p, label=0) + s.add_polygon(m * p, label=1) + for i in range(n): + s.glue((0, i), (1, i)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def regular_octagon(): @@ -980,13 +1311,14 @@ def regular_octagon(): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: T = translation_surfaces.regular_octagon() sage: T - TranslationSurface built from 1 polygon - sage: T.stratum() - H_2(2) + Translation Surface in H_2(2) built from a regular octagon sage: TestSuite(T).run() + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: T in TranslationSurfaces() + True """ return translation_surfaces.veech_2n_gon(4) @@ -1023,7 +1355,7 @@ def mcmullen_genus2_prototype(w, h, t, e, rel=0, base_ring=None): ....: 24: [(1,2,0,-4), (2,1,0,-4), (3,2,0,0)], ....: 25: [(2,2,0,-3), (2,2,1,-3), (3,2,0,-1), (4,1,0,-3)]} - sage: for D in sorted(prototypes): + sage: for D in sorted(prototypes): # long time (.5s) ....: for w,h,t,e in prototypes[D]: ....: T = translation_surfaces.mcmullen_genus2_prototype(w,h,t,e) ....: assert T.stratum() == AbelianStratum(2) @@ -1032,7 +1364,11 @@ def mcmullen_genus2_prototype(w, h, t, e, rel=0, base_ring=None): An example with some relative homology:: sage: U8 = translation_surfaces.mcmullen_genus2_prototype(2,1,0,0,1/4) # discriminant 8 + sage: U8 + Translation Surface in H_2(1^2) built from a rectangle and a quadrilateral sage: U12 = translation_surfaces.mcmullen_genus2_prototype(3,1,0,0,3/10) # discriminant 12 + sage: U12 + Translation Surface in H_2(1^2) built from a rectangle and a quadrilateral sage: U8.stratum() H_2(1^2) @@ -1102,15 +1438,15 @@ def mcmullen_genus2_prototype(w, h, t, e, rel=0, base_ring=None): # (lambda,lambda) square on top # twisted (w,0), (t,h) - s = Surface_list(base_ring=K) + s = MutableOrientedSimilaritySurface(K) if rel: if rel < 0 or rel > w - λ: raise ValueError("invalid rel argument") s.add_polygon( - polygons(vertices=[(0, 0), (λ, 0), (λ + rel, λ), (rel, λ)], ring=K) + Polygon(vertices=[(0, 0), (λ, 0), (λ + rel, λ), (rel, λ)], base_ring=K) ) s.add_polygon( - polygons( + Polygon( vertices=[ (0, 0), (rel, 0), @@ -1121,30 +1457,32 @@ def mcmullen_genus2_prototype(w, h, t, e, rel=0, base_ring=None): (t + λ, h), (t, h), ], - ring=K, + base_ring=K, ) ) - s.set_edge_pairing(0, 1, 0, 3) - s.set_edge_pairing(0, 0, 1, 6) - s.set_edge_pairing(0, 2, 1, 1) - s.set_edge_pairing(1, 2, 1, 4) - s.set_edge_pairing(1, 3, 1, 7) - s.set_edge_pairing(1, 0, 1, 5) + s.glue((0, 1), (0, 3)) + s.glue((0, 0), (1, 6)) + s.glue((0, 2), (1, 1)) + s.glue((1, 2), (1, 4)) + s.glue((1, 3), (1, 7)) + s.glue((1, 0), (1, 5)) else: - s.add_polygon(polygons(vertices=[(0, 0), (λ, 0), (λ, λ), (0, λ)], ring=K)) s.add_polygon( - polygons( + Polygon(vertices=[(0, 0), (λ, 0), (λ, λ), (0, λ)], base_ring=K) + ) + s.add_polygon( + Polygon( vertices=[(0, 0), (λ, 0), (w, 0), (w + t, h), (λ + t, h), (t, h)], - ring=K, + base_ring=K, ) ) - s.set_edge_pairing(0, 1, 0, 3) - s.set_edge_pairing(0, 0, 1, 4) - s.set_edge_pairing(0, 2, 1, 0) - s.set_edge_pairing(1, 1, 1, 3) - s.set_edge_pairing(1, 2, 1, 5) + s.glue((0, 1), (0, 3)) + s.glue((0, 0), (1, 4)) + s.glue((0, 2), (1, 0)) + s.glue((1, 1), (1, 3)) + s.glue((1, 2), (1, 5)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def lanneau_nguyen_genus3_prototype(w, h, t, e): @@ -1214,16 +1552,19 @@ def lanneau_nguyen_genus3_prototype(w, h, t, e): # (λ, λ) square in the middle # twisted (w, 0), (t, h) on top and bottom - s = Surface_list(base_ring=K) + s = MutableOrientedSimilaritySurface(K) + + from flatsurf import Polygon + s.add_polygon( - polygons( + Polygon( vertices=[(0, 0), (λ, 0), (w, 0), (w + t, h), (λ + t, h), (t, h)], - ring=K, + base_ring=K, ) ) - s.add_polygon(polygons(vertices=[(0, 0), (λ, 0), (λ, λ), (0, λ)], ring=K)) + s.add_polygon(Polygon(vertices=[(0, 0), (λ, 0), (λ, λ), (0, λ)], base_ring=K)) s.add_polygon( - polygons( + Polygon( vertices=[ (0, 0), (w - λ, 0), @@ -1232,19 +1573,19 @@ def lanneau_nguyen_genus3_prototype(w, h, t, e): (w - λ + t, h), (t, h), ], - ring=K, + base_ring=K, ) ) - s.set_edge_pairing(0, 0, 2, 3) - s.set_edge_pairing(0, 1, 0, 3) - s.set_edge_pairing(0, 2, 0, 5) - s.set_edge_pairing(0, 4, 1, 0) - s.set_edge_pairing(1, 1, 1, 3) - s.set_edge_pairing(1, 2, 2, 1) - s.set_edge_pairing(2, 0, 2, 4) - s.set_edge_pairing(2, 2, 2, 5) + s.glue((0, 0), (2, 3)) + s.glue((0, 1), (0, 3)) + s.glue((0, 2), (0, 5)) + s.glue((0, 4), (1, 0)) + s.glue((1, 1), (1, 3)) + s.glue((1, 2), (2, 1)) + s.glue((2, 0), (2, 4)) + s.glue((2, 2), (2, 5)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def lanneau_nguyen_genus4_prototype(w, h, t, e): @@ -1318,12 +1659,17 @@ def lanneau_nguyen_genus4_prototype(w, h, t, e): # (λ/2, λ/2) squares on top and bottom # twisted (w/2, 0), (t/2, h/2) in the middle - s = Surface_list(base_ring=K) + s = MutableOrientedSimilaritySurface(K) + + from flatsurf import Polygon + s.add_polygon( - polygons(vertices=[(0, 0), (λ / 2, 0), (λ / 2, λ / 2), (0, λ / 2)], ring=K) + Polygon( + vertices=[(0, 0), (λ / 2, 0), (λ / 2, λ / 2), (0, λ / 2)], base_ring=K + ) ) s.add_polygon( - polygons( + Polygon( vertices=[ (0, 0), (w / 2 - λ, 0), @@ -1333,11 +1679,11 @@ def lanneau_nguyen_genus4_prototype(w, h, t, e): (w / 2 - λ + t / 2, h / 2), (t / 2, h / 2), ], - ring=K, + base_ring=K, ) ) s.add_polygon( - polygons( + Polygon( vertices=[ (0, 0), (λ, 0), @@ -1347,25 +1693,27 @@ def lanneau_nguyen_genus4_prototype(w, h, t, e): (λ / 2 + t / 2, h / 2), (t / 2, h / 2), ], - ring=K, + base_ring=K, ) ) s.add_polygon( - polygons(vertices=[(0, 0), (λ / 2, 0), (λ / 2, λ / 2), (0, λ / 2)], ring=K) + Polygon( + vertices=[(0, 0), (λ / 2, 0), (λ / 2, λ / 2), (0, λ / 2)], base_ring=K + ) ) - s.set_edge_pairing(0, 0, 2, 4) - s.set_edge_pairing(0, 1, 0, 3) - s.set_edge_pairing(0, 2, 1, 2) - s.set_edge_pairing(1, 0, 1, 5) - s.set_edge_pairing(1, 1, 3, 2) - s.set_edge_pairing(1, 3, 1, 6) - s.set_edge_pairing(1, 4, 2, 0) - s.set_edge_pairing(2, 1, 2, 3) - s.set_edge_pairing(2, 2, 2, 6) - s.set_edge_pairing(2, 5, 3, 0) - s.set_edge_pairing(3, 1, 3, 3) + s.glue((0, 0), (2, 4)) + s.glue((0, 1), (0, 3)) + s.glue((0, 2), (1, 2)) + s.glue((1, 0), (1, 5)) + s.glue((1, 1), (3, 2)) + s.glue((1, 3), (1, 6)) + s.glue((1, 4), (2, 0)) + s.glue((2, 1), (2, 3)) + s.glue((2, 2), (2, 6)) + s.glue((2, 5), (3, 0)) + s.glue((3, 1), (3, 3)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def mcmullen_L(l1, l2, l3, l4): @@ -1388,15 +1736,19 @@ def mcmullen_L(l1, l2, l3, l4): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.mcmullen_L(1,1,1,1) + sage: s + Translation Surface in H_2(2) built from 3 squares sage: TestSuite(s).run() TESTS:: sage: from flatsurf import translation_surfaces - sage: translation_surfaces.mcmullen_L(1r, 1r, 1r, 1r) - TranslationSurface built from 3 polygons + sage: L = translation_surfaces.mcmullen_L(1r, 1r, 1r, 1r) + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: L in TranslationSurfaces() + True """ field = Sequence([l1, l2, l3, l4]).universe() if isinstance(field, type): @@ -1404,18 +1756,24 @@ def mcmullen_L(l1, l2, l3, l4): if not field.is_field(): field = field.fraction_field() - s = Surface_list(base_ring=field) - s.add_polygon(polygons((l3, 0), (0, l2), (-l3, 0), (0, -l2), ring=field)) - s.add_polygon(polygons((l3, 0), (0, l1), (-l3, 0), (0, -l1), ring=field)) - s.add_polygon(polygons((l4, 0), (0, l2), (-l4, 0), (0, -l2), ring=field)) - s.change_edge_gluing(0, 0, 1, 2) - s.change_edge_gluing(0, 1, 2, 3) - s.change_edge_gluing(0, 2, 1, 0) - s.change_edge_gluing(0, 3, 2, 1) - s.change_edge_gluing(1, 1, 1, 3) - s.change_edge_gluing(2, 0, 2, 2) + s = MutableOrientedSimilaritySurface(field) + s.add_polygon( + Polygon(edges=[(l3, 0), (0, l2), (-l3, 0), (0, -l2)], base_ring=field) + ) + s.add_polygon( + Polygon(edges=[(l3, 0), (0, l1), (-l3, 0), (0, -l1)], base_ring=field) + ) + s.add_polygon( + Polygon(edges=[(l4, 0), (0, l2), (-l4, 0), (0, -l2)], base_ring=field) + ) + s.glue((0, 0), (1, 2)) + s.glue((0, 1), (2, 3)) + s.glue((0, 2), (1, 0)) + s.glue((0, 3), (2, 1)) + s.glue((1, 1), (1, 3)) + s.glue((2, 0), (2, 2)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def ward(n): @@ -1425,25 +1783,30 @@ def ward(n): EXAMPLES:: - sage: from flatsurf import * - sage: s=translation_surfaces.ward(3) + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.ward(3) + sage: s + Translation Surface in H_1(0^3) built from 2 equilateral triangles and a regular hexagon sage: TestSuite(s).run() - sage: s=translation_surfaces.ward(7) + sage: s = translation_surfaces.ward(7) + sage: s + Translation Surface in H_6(10) built from 2 regular heptagons and a regular tetradecagon sage: TestSuite(s).run() """ if n < 3: raise ValueError o = ZZ_2 * polygons.regular_ngon(2 * n) - p1 = polygons(*[o.edge((2 * i + n) % (2 * n)) for i in range(n)]) - p2 = polygons(*[o.edge((2 * i + n + 1) % (2 * n)) for i in range(n)]) - s = Surface_list(base_ring=o.parent().field()) + p1 = Polygon(edges=[o.edge((2 * i + n) % (2 * n)) for i in range(n)]) + p2 = Polygon(edges=[o.edge((2 * i + n + 1) % (2 * n)) for i in range(n)]) + s = MutableOrientedSimilaritySurface(o.base_ring()) s.add_polygon(o) s.add_polygon(p1) s.add_polygon(p2) - s.change_polygon_gluings(1, [(0, 2 * i) for i in range(n)]) - s.change_polygon_gluings(2, [(0, 2 * i + 1) for i in range(n)]) + for i in range(n): + s.glue((1, i), (0, 2 * i)) + s.glue((2, i), (0, 2 * i + 1)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def octagon_and_squares(): @@ -1453,8 +1816,11 @@ def octagon_and_squares(): sage: from flatsurf import translation_surfaces sage: os = translation_surfaces.octagon_and_squares() sage: os - TranslationSurface built from 3 polygons + Translation Surface in H_3(4) built from 2 squares and a regular octagon sage: TestSuite(os).run() + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: os in TranslationSurfaces() + True """ return translation_surfaces.ward(4) @@ -1494,14 +1860,16 @@ def cathedral(a, b): sage: from flatsurf import translation_surfaces sage: C = translation_surfaces.cathedral(1,2) - sage: C.stratum() - H_4(2^3) + sage: C + Translation Surface in H_4(2^3) built from 2 squares, a hexagon with 4 marked vertices and an octagon sage: TestSuite(C).run() sage: from pyexactreal import ExactReals # optional: exactreal sage: K = QuadraticField(5, embedding=AA(5).sqrt()) sage: R = ExactReals(K) # optional: exactreal sage: C = translation_surfaces.cathedral(K.gen(), R.random_element([0.1, 0.2])) # optional: exactreal + sage: C # optional: exactreal + Translation Surface in H_4(2^3) built from 2 rectangles, a hexagon with 4 marked vertices and an octagon sage: C.stratum() # optional: exactreal H_4(2^3) sage: TestSuite(C).run() # long time (6s), optional: exactreal @@ -1513,11 +1881,11 @@ def cathedral(a, b): ring = ring.fraction_field() a = ring(a) b = ring(b) - P = ConvexPolygons(ring) - s = Surface_list(base_ring=ring) + s = MutableOrientedSimilaritySurface(ring) half = QQ((1, 2)) - p0 = P(vertices=[(0, 0), (a, 0), (a, 1), (0, 1)]) - p1 = P( + p0 = Polygon(base_ring=ring, vertices=[(0, 0), (a, 0), (a, 1), (0, 1)]) + p1 = Polygon( + base_ring=ring, vertices=[ (a, 0), (a, -b), @@ -1529,10 +1897,14 @@ def cathedral(a, b): (a + half, b + 1 + half), (a, b + 1), (a, 1), - ] + ], + ) + p2 = Polygon( + base_ring=ring, + vertices=[(a + 1, 0), (2 * a + 1, 0), (2 * a + 1, 1), (a + 1, 1)], ) - p2 = P(vertices=[(a + 1, 0), (2 * a + 1, 0), (2 * a + 1, 1), (a + 1, 1)]) - p3 = P( + p3 = Polygon( + base_ring=ring, vertices=[ (2 * a + 1, 0), (2 * a + 1 + half, -half), @@ -1542,27 +1914,27 @@ def cathedral(a, b): (4 * a + 1 + half, 1 + half), (2 * a + 1 + half, 1 + half), (2 * a + 1, 1), - ] + ], ) s.add_polygon(p0) s.add_polygon(p1) s.add_polygon(p2) s.add_polygon(p3) - s.set_edge_pairing(0, 0, 0, 2) - s.set_edge_pairing(0, 1, 1, 9) - s.set_edge_pairing(0, 3, 3, 3) - s.set_edge_pairing(1, 0, 1, 3) - s.set_edge_pairing(1, 1, 3, 4) - s.set_edge_pairing(1, 2, 3, 6) - s.set_edge_pairing(1, 4, 2, 3) - s.set_edge_pairing(1, 5, 1, 8) - s.set_edge_pairing(1, 6, 3, 0) - s.set_edge_pairing(1, 7, 3, 2) - s.set_edge_pairing(2, 0, 2, 2) - s.set_edge_pairing(2, 1, 3, 7) - s.set_edge_pairing(3, 1, 3, 5) + s.glue((0, 0), (0, 2)) + s.glue((0, 1), (1, 9)) + s.glue((0, 3), (3, 3)) + s.glue((1, 0), (1, 3)) + s.glue((1, 1), (3, 4)) + s.glue((1, 2), (3, 6)) + s.glue((1, 4), (2, 3)) + s.glue((1, 5), (1, 8)) + s.glue((1, 6), (3, 0)) + s.glue((1, 7), (3, 2)) + s.glue((2, 0), (2, 2)) + s.glue((2, 1), (3, 7)) + s.glue((3, 1), (3, 5)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def arnoux_yoccoz(genus): @@ -1575,12 +1947,16 @@ def arnoux_yoccoz(genus): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.arnoux_yoccoz(4) + sage: s + Translation Surface in H_4(3^2) built from 16 triangles sage: TestSuite(s).run() sage: s.is_delaunay_decomposed() True sage: s = s.canonicalize() + sage: s + Translation Surface in H_4(3^2) built from 16 triangles sage: field=s.base_ring() sage: a = field.gen() sage: from sage.matrix.constructor import Matrix @@ -1637,8 +2013,7 @@ def arnoux_yoccoz(genus): (a - a ** (g - i + 2)) / (1 - a), ) ) - P = ConvexPolygons(field) - s = Surface_list(field) + s = MutableOrientedSimilaritySurface(field) T = [None] * (2 * g + 1) Tp = [None] * (2 * g + 1) from sage.matrix.constructor import Matrix @@ -1647,40 +2022,46 @@ def arnoux_yoccoz(genus): for i in range(1, g + 1): # T_i is (P_0,Q_i,Q_{i-1}) T[i] = s.add_polygon( - P(edges=[q[i] - p[0], q[i - 1] - q[i], p[0] - q[i - 1]]) + Polygon( + base_ring=field, + edges=[q[i] - p[0], q[i - 1] - q[i], p[0] - q[i - 1]], + ) ) # T_{g+i} is (P_i,Q_{i-1},Q_{i}) T[g + i] = s.add_polygon( - P(edges=[q[i - 1] - p[i], q[i] - q[i - 1], p[i] - q[i]]) + Polygon( + base_ring=field, + edges=[q[i - 1] - p[i], q[i] - q[i - 1], p[i] - q[i]], + ) ) # T'_i is (P'_0,Q'_{i-1},Q'_i) Tp[i] = s.add_polygon(m * s.polygon(T[i])) # T'_{g+i} is (P'_i,Q'_i, Q'_{i-1}) Tp[g + i] = s.add_polygon(m * s.polygon(T[g + i])) for i in range(1, g): - s.change_edge_gluing(T[i], 0, T[i + 1], 2) - s.change_edge_gluing(Tp[i], 2, Tp[i + 1], 0) + s.glue((T[i], 0), (T[i + 1], 2)) + s.glue((Tp[i], 2), (Tp[i + 1], 0)) for i in range(1, g + 1): - s.change_edge_gluing(T[i], 1, T[g + i], 1) - s.change_edge_gluing(Tp[i], 1, Tp[g + i], 1) + s.glue((T[i], 1), (T[g + i], 1)) + s.glue((Tp[i], 1), (Tp[g + i], 1)) # P 0 Q 0 is paired with P' 0 Q' 0, ... - s.change_edge_gluing(T[1], 2, Tp[g], 2) - s.change_edge_gluing(Tp[1], 0, T[g], 0) + s.glue((T[1], 2), (Tp[g], 2)) + s.glue((Tp[1], 0), (T[g], 0)) # P1Q1 is paired with P'_g Q_{g-1} - s.change_edge_gluing(T[g + 1], 2, Tp[2 * g], 2) - s.change_edge_gluing(Tp[g + 1], 0, T[2 * g], 0) + s.glue((T[g + 1], 2), (Tp[2 * g], 2)) + s.glue((Tp[g + 1], 0), (T[2 * g], 0)) # P1Q0 is paired with P_{g-1} Q_{g-1} - s.change_edge_gluing(T[g + 1], 0, T[2 * g - 1], 2) - s.change_edge_gluing(Tp[g + 1], 2, Tp[2 * g - 1], 0) + s.glue((T[g + 1], 0), (T[2 * g - 1], 2)) + s.glue((Tp[g + 1], 2), (Tp[2 * g - 1], 0)) # PgQg is paired with Q1P2 - s.change_edge_gluing(T[2 * g], 2, T[g + 2], 0) - s.change_edge_gluing(Tp[2 * g], 0, Tp[g + 2], 2) + s.glue((T[2 * g], 2), (T[g + 2], 0)) + s.glue((Tp[2 * g], 0), (Tp[g + 2], 2)) for i in range(2, g - 1): # PiQi is paired with Q'_i P'_{i+1} - s.change_edge_gluing(T[g + i], 2, Tp[g + i + 1], 2) - s.change_edge_gluing(Tp[g + i], 0, T[g + i + 1], 0) + s.glue((T[g + i], 2), (Tp[g + i + 1], 2)) + s.glue((Tp[g + i], 0), (T[g + i + 1], 0)) s.set_immutable() - return TranslationSurface(s) + return s @staticmethod def from_flipper(h): @@ -1689,7 +2070,7 @@ def from_flipper(h): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: import flipper # optional - flipper A torus example:: @@ -1704,9 +2085,12 @@ def from_flipper(h): sage: h = h1*h2^(-1r) # optional - flipper sage: f = h.flat_structure() # optional - flipper sage: ts = translation_surfaces.from_flipper(h) # optional - flipper - sage: ts # optional - flipper - HalfTranslationSurface built from 2 polygons + sage: ts # optional - flipper; computation of the stratum fails here, see #227 + Half-Translation Surface built from 2 triangles sage: TestSuite(ts).run() # optional - flipper + sage: from flatsurf.geometry.categories import HalfTranslationSurfaces # optional: flipper + sage: ts in HalfTranslationSurfaces() # optional: flipper + True A non-orientable example:: @@ -1716,13 +2100,12 @@ def from_flipper(h): True sage: S = translation_surfaces.from_flipper(h) # optional - flipper sage: TestSuite(S).run() # optional - flipper - sage: S.num_polygons() # optional - flipper + sage: len(S.polygons()) # optional - flipper 4 sage: from flatsurf.geometry.similarity_surface_generators import flipper_nf_element_to_sage sage: a = flipper_nf_element_to_sage(h.dilatation()) # optional - flipper - """ - from .surface import surface_list_from_polygons_and_gluings + """ f = h.flat_structure() x = next(iter(f.edge_vectors.values())).x @@ -1739,26 +2122,29 @@ def from_flipper(h): k: (i, j) for i, t in enumerate(f.triangulation) for j, k in enumerate(t) } - C = ConvexPolygons(K) + from flatsurf import MutableOrientedSimilaritySurface + + S = MutableOrientedSimilaritySurface(K) - polys = [] - adjacencies = {} for i, t in enumerate(f.triangulation): - for j, k in enumerate(t): - adjacencies[(i, j)] = to_polygon_number[~k] try: - poly = C([edge_vectors[i] for i in tuple(t)]) + poly = Polygon(base_ring=K, edges=[edge_vectors[i] for i in tuple(t)]) except ValueError: raise ValueError( "t = {}, edges = {}".format( t, [edge_vectors[i].n(digits=6) for i in t] ) ) - polys.append(poly) - return HalfTranslationSurface( - surface_list_from_polygons_and_gluings(polys, adjacencies) - ) + S.add_polygon(poly) + + for i, t in enumerate(f.triangulation): + for j, k in enumerate(t): + S.glue((i, j), to_polygon_number[~k]) + + S.set_immutable() + + return S @staticmethod def origami(r, u, rr=None, uu=None, domain=None): @@ -1773,15 +2159,14 @@ def origami(r, u, rr=None, uu=None, domain=None): sage: r = S('(1,2)') sage: u = S('(1,3)') sage: o = translation_surfaces.origami(r,u) - sage: o.underlying_surface() + sage: o Origami defined by r=(1,2) and u=(1,3) sage: o.stratum() H_2(2) sage: TestSuite(o).run() - """ - from .translation_surface import Origami - return TranslationSurface(Origami(r, u, rr, uu, domain)) + """ + return Origami(r, u, rr, uu, domain) @staticmethod def infinite_staircase(): @@ -1793,17 +2178,11 @@ def infinite_staircase(): sage: from flatsurf import translation_surfaces sage: S = translation_surfaces.infinite_staircase() - sage: S.underlying_surface() + sage: S The infinite staircase sage: TestSuite(S).run() """ - - o = TranslationSurfaceGenerators._InfiniteStaircase() - s = TranslationSurface(o) - - gs = s.graphical_surface(default_position_function=o._position_function) - gs.make_all_visible(limit=10) - return s + return TranslationSurfaceGenerators._InfiniteStaircase() class _InfiniteStaircase(Origami): def __init__(self): @@ -1813,9 +2192,12 @@ def __init__(self): self._vertical, self._horizontal, domain=ZZ, - base_label=ZZ(0), + root=ZZ(0), ) + def is_compact(self): + return False + def _vertical(self, x): if x % 2: return x + 1 @@ -1838,10 +2220,16 @@ def _position_function(self, n): def __repr__(self): return "The infinite staircase" + def __hash__(self): + return 1337 + def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See :meth:`SimilaritySurfaces.FiniteType._test_eq_surface` for details + on this notion of equality. + EXAMPLES:: sage: from flatsurf import translation_surfaces @@ -1850,10 +2238,17 @@ def __eq__(self, other): True """ - if isinstance(other, TranslationSurfaceGenerators._InfiniteStaircase): - return True + return isinstance(other, TranslationSurfaceGenerators._InfiniteStaircase) - return super().__eq__(other) + def graphical_surface(self, *args, **kwargs): + default_position_function = kwargs.pop( + "default_position_function", self._position_function + ) + graphical_surface = super().graphical_surface( + *args, default_position_function=default_position_function, **kwargs + ) + graphical_surface.make_all_visible(limit=10) + return graphical_surface @staticmethod def t_fractal(w=ZZ_1, r=ZZ_2, h1=ZZ_1, h2=ZZ_1): @@ -1863,10 +2258,10 @@ def t_fractal(w=ZZ_1, r=ZZ_2, h1=ZZ_1, h2=ZZ_1): EXAMPLES:: sage: from flatsurf import translation_surfaces - sage: tf = translation_surfaces.t_fractal().underlying_surface() - sage: tf + sage: tf = translation_surfaces.t_fractal() # long time (.4s) + sage: tf # long time (see above) The T-fractal surface with parameters w=1, r=2, h1=1, h2=1 - sage: TestSuite(tf).run() + sage: TestSuite(tf).run() # long time (see above) """ return tfractal_surface(w, r, h1, h2) @@ -1889,11 +2284,11 @@ def e_infinity_surface(lambda_squared=None, field=None): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.e_infinity_surface() - sage: TestSuite(s).run() + sage: TestSuite(s).run() # long time (1s) """ - return TranslationSurface(EInfinitySurface(lambda_squared, field)) + return EInfinitySurface(lambda_squared, field) @staticmethod def chamanara(alpha): @@ -1905,8 +2300,15 @@ def chamanara(alpha): sage: from flatsurf import translation_surfaces sage: C = translation_surfaces.chamanara(1/2) sage: C - TranslationSurface built from infinitely many polygons + Minimal Translation Cover of Chamanara surface with parameter 1/2 + + TESTS:: + sage: TestSuite(C).run() + sage: from flatsurf.geometry.categories import TranslationSurfaces + sage: C in TranslationSurfaces() + True + """ from .chamanara import chamanara_surface diff --git a/flatsurf/geometry/straight_line_trajectory.py b/flatsurf/geometry/straight_line_trajectory.py index bb6d80700..1a7212016 100644 --- a/flatsurf/geometry/straight_line_trajectory.py +++ b/flatsurf/geometry/straight_line_trajectory.py @@ -20,8 +20,8 @@ # ********************************************************************* from collections import deque -from .polygon import line_intersection -from .surface_objects import SaddleConnection +from flatsurf.geometry.euclidean import line_intersection +from flatsurf.geometry.surface_objects import SaddleConnection # Vincent question: # using deque has the disadvantage of losing the initial points @@ -82,7 +82,7 @@ class SegmentInPolygon: EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import similarity_surfaces sage: from flatsurf.geometry.straight_line_trajectory import SegmentInPolygon sage: s = similarity_surfaces.example() sage: v = s.tangent_vector(0, (1/3,-1/4), (0,1)) @@ -130,7 +130,7 @@ def __repr__(self): r""" TESTS:: - sage: from flatsurf import * + sage: from flatsurf import similarity_surfaces sage: from flatsurf.geometry.straight_line_trajectory import SegmentInPolygon sage: s = similarity_surfaces.example() sage: v = s.tangent_vector(0, (0,0), (3,-1)) @@ -165,9 +165,9 @@ def is_edge(self): vv = self.start().vector() vertex = self.start().vertex() ww = self.start().polygon().edge(vertex) - from flatsurf.geometry.polygon import is_same_direction + from flatsurf.geometry.euclidean import is_parallel - return is_same_direction(vv, ww) + return is_parallel(vv, ww) def edge(self): if not self.is_edge(): @@ -186,14 +186,14 @@ def next(self): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import similarity_surfaces sage: from flatsurf.geometry.straight_line_trajectory import SegmentInPolygon sage: s = similarity_surfaces.example() sage: s.polygon(0) - Polygon: (0, 0), (2, -2), (2, 0) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) sage: s.polygon(1) - Polygon: (0, 0), (2, 0), (1, 3) + Polygon(vertices=[(0, 0), (2, 0), (1, 3)]) sage: v = s.tangent_vector(0, (0,0), (3,-1)) sage: seg = SegmentInPolygon(v) sage: seg @@ -249,7 +249,7 @@ def plot(self, *args, **options): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: T = translation_surfaces.square_torus() sage: v = T.tangent_vector(0, (0,0), (5,7)) sage: L = v.straight_line_trajectory() @@ -292,7 +292,7 @@ def cylinder(self): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.regular_octagon() sage: v = s.tangent_vector(0,(1/2,0),(sqrt(2),1)) sage: traj = v.straight_line_trajectory() @@ -332,7 +332,7 @@ def coding(self, alphabet=None): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: t = translation_surfaces.square_torus() sage: v = t.tangent_vector(0, (1/2,0), (5,6)) @@ -368,7 +368,7 @@ def coding(self, alphabet=None): Check that the saddle connections that are obtained in the torus get the expected coding:: - sage: for _ in range(10): + sage: for _ in range(10): # long time (.6s) ....: x = ZZ.random_element(1,30) ....: y = ZZ.random_element(1,30) ....: x,y = x/gcd(x,y), y/gcd(x,y) @@ -508,7 +508,7 @@ def intersections(self, traj, count_singularities=False, include_segments=False) if pos.is_inside() and ( count_singularities or not pos.is_vertex() ): - new_point = self.surface().surface_point( + new_point = self.surface().point( seg1.polygon_label(), x ) if new_point not in intersection_points: @@ -531,7 +531,7 @@ class StraightLineTrajectory(AbstractStraightLineTrajectory): EXAMPLES:: # Demonstrate the handling of edges - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectory sage: p = SymmetricGroup(2)('(1,2)') sage: s = translation_surfaces.origami(p,p) @@ -562,7 +562,7 @@ def segment(self, i): r""" EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: O = translation_surfaces.regular_octagon() sage: v = O.tangent_vector(0, (1,1), (33,45)) @@ -625,13 +625,15 @@ def is_closed(self): An example in a cone surface covered by the torus:: - sage: from flatsurf import * + sage: from flatsurf import MutableOrientedSimilaritySurface, polygons sage: p = polygons.square() - sage: s = Surface_list(base_ring=p.base_ring()) - sage: s.add_polygon(p,[(0,3),(0,2),(0,1),(0,0)]) + sage: s = MutableOrientedSimilaritySurface(p.base_ring()) + sage: s.add_polygon(p) 0 + sage: s.glue((0, 0), (0, 3)) + sage: s.glue((0, 1), (0, 2)) sage: s.set_immutable() - sage: t = RationalConeSurface(s) + sage: t = s sage: v = t.tangent_vector(0, (1/2,0), (1/3,7/5)) sage: l = v.straight_line_trajectory() @@ -665,7 +667,7 @@ def flow(self, steps): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import similarity_surfaces sage: s = similarity_surfaces.example() sage: v = s.tangent_vector(0, (1,-1/2), (3,-1)) @@ -714,7 +716,6 @@ class StraightLineTrajectoryTranslation(AbstractStraightLineTrajectory): """ def __init__(self, tangent_vector): - t = tangent_vector.polygon_label() self._vector = tangent_vector.vector() self._s = tangent_vector.surface() @@ -751,7 +752,7 @@ def _next(self, p, e, x): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectoryTranslation sage: S = SymmetricGroup(3) sage: r = S('(1,2)') @@ -798,7 +799,7 @@ def segment(self, i): r""" EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectoryTranslation sage: O = translation_surfaces.regular_octagon() @@ -840,7 +841,7 @@ def segments(self): r""" EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectoryTranslation sage: s = translation_surfaces.square_torus() @@ -872,7 +873,7 @@ def is_saddle_connection(self): r""" EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: from flatsurf.geometry.straight_line_trajectory import StraightLineTrajectoryTranslation sage: torus = translation_surfaces.square_torus() diff --git a/flatsurf/geometry/subfield.py b/flatsurf/geometry/subfield.py index 98821dd01..6e375fa69 100644 --- a/flatsurf/geometry/subfield.py +++ b/flatsurf/geometry/subfield.py @@ -154,7 +154,7 @@ def subfield_from_elements(self, alpha, name=None, polred=True, threshold=None): sage: R. = QQ[] sage: p1 = x^3 - x - 1 sage: roots1 = p1.roots(QQbar, False) - sage: for _ in range(10): + sage: for _ in range(10): # long time (1.5s) ....: p2 = R.random_element(degree=2) ....: while not p2.is_irreducible(): p2 = R.random_element(degree=2) ....: roots2 = p2.roots(QQbar, False) @@ -312,9 +312,9 @@ def chebyshev_T(n, c): # T_0(x) = 2 # T_1(x) = x # and T_{n+1}(x) = x T_n(x) - T_{n-1}(x) + T0 = parent(c)(2) if n == 0: - return parent(c)(2) - T0 = 2 + return T0 T1 = c for i in range(n - 1): T0, T1 = T1, c * T1 - T0 diff --git a/flatsurf/geometry/surface.py b/flatsurf/geometry/surface.py index 8e55cb3a5..11ae3a919 100644 --- a/flatsurf/geometry/surface.py +++ b/flatsurf/geometry/surface.py @@ -1,61 +1,48 @@ r""" -Data structures for surfaces built from polygons. - -All surfaces in sage-flatsurf are built from polygons whose sides are -identified by similarities. This module provides data structures to describe -such surfaces. Currently, there are two fundamental such data structures, -namely :class:`Surface_list` and `Surface_dict`. The former labels the polygons -that make up a surface by non-negative integers and the latter can use -arbitrary labels. Additionally, there are lots of other surface representations -that are not really implementing data structures but essentially just wrap -these two, e.g., a :class:`.minimal_cover.MinimalTranslationCover`. - -All these surface implementations inherit from :class:`Surface` which describes -the contract that all surfaces must satisfy. As an absolute minimum, they -implement :meth:`Surface.polygon` which maps polygon labels to actual polygons, -and :meth:`Surface.opposite_edge` which describes the gluing of polygons. +Generic mutable and immutable surfaces + +This module provides base classes and implementations of surfaces. Most +surfaces in sage-flatsurf inherit from some of the classes in this module. + +The most important class in this module is +:class:`MutableOrientedSimilaritySurface` which allows you to create a surface +by gluing polygons with similarities. EXAMPLES: -We built a torus by gluing the opposite sides of a square:: +We build a translation surface by gluing two hexagons, labeled 0 and 1:: - sage: from flatsurf import polygons - sage: from flatsurf.geometry.surface import Surface_list + sage: from flatsurf import MutableOrientedSimilaritySurface, polygons + sage: S = MutableOrientedSimilaritySurface(QuadraticField(3)) - sage: S = Surface_list(QQ) - sage: S.add_polygon(polygons(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + sage: S.add_polygon(polygons.regular_ngon(6)) 0 - sage: S.set_edge_pairing(0, 0, 0, 2) - sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S.add_polygon(polygons.regular_ngon(6)) + 1 - sage: S.polygon(0) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: S.opposite_edge(0, 0) - (0, 2) + sage: S.glue((0, 0), (1, 3)) + sage: S.glue((0, 1), (1, 4)) + sage: S.glue((0, 2), (1, 5)) + sage: S.glue((0, 3), (1, 0)) + sage: S.glue((0, 4), (1, 1)) + sage: S.glue((0, 5), (1, 2)) -There are two separate hierarchies of surfaces in sage-flatsurf. The underlying -data of a surface described by the subclasses of :class:`Surface` here and the -:class:`.similarity_surface.SimilaritySurface` and its subclasses which wrap a -:class:`Surface`. While a :class:`Surface` essentially provides the raw data of -a surface, a :class:`.similarity_surface.SimilaritySurface` then adds -mathematical knowledge to that data structure, e.g., by declaring that the data -describes a :class:`.translation_surface.TranslationSurface`:: + sage: S + Translation Surface built from 2 regular hexagons - sage: from flatsurf import TranslationSurface - sage: T = TranslationSurface(S) +We signal that the construction is complete. This refines the category of the +surface and makes more functionality available:: -We can recover the underlying surface again:: - - sage: T.underlying_surface() is S - True + sage: S.set_immutable() + sage: S + Translation Surface in H_2(1^2) built from 2 regular hexagons """ # ******************************************************************** # This file is part of sage-flatsurf. # -# Copyright (C) 2016-2020 W. Patrick Hooper -# 2019-2020 Vincent Delecroix -# 2020-2023 Julian Rüth +# Copyright (C) 2016-2020 Vincent Delecroix +# 2023 Julian Rüth # # sage-flatsurf is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -70,2128 +57,3103 @@ # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . # ******************************************************************** -from collections import deque - -from sage.structure.sage_object import SageObject +import collections.abc +from sage.structure.parent import Parent from sage.misc.cachefunc import cached_method +from flatsurf.geometry.surface_objects import SurfacePoint -class Surface(SageObject): + +class Surface_base(Parent): r""" - Abstract base class of all surfaces that are built from a set of polygons - with edges identified. The identifications are compositions of homothety, - rotations and translations, i.e., similarities that ensure that the surface - is oriented. + A base class for all surfaces in sage-flatsurf. - Concrete implementations of a surface must implement at least - :meth:`polygon` and :meth:`opposite_edge`. + This class patches bits of the category framework in SageMath that assume + that all parent structures are immutable. - To be able to modify a surface, subclasses should also implement - :meth:`_change_polygon`, :meth:`_set_edge_pairing`, :meth:`_add_polygon`, - :meth:`_remove_polygon`. + EXAMPLES:: - For concrete implementations of a Surface, see, e.g., :class:`Surface_list` - and :class:`Surface_dict`. + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() - INPUT: + sage: from flatsurf.geometry.surface import Surface_base + sage: isinstance(S, Surface_base) + True - - ``base_ring`` -- the ring containing the coordinates of the vertices of - the polygons + """ - - ``base_label`` -- the label of a chosen special polygon in the surface, - see :meth:`base_label` + def _refine_category_(self, category): + r""" + Refine the category of this surface to a subcategory ``category``. - - ``finite`` -- whether this is a finite surface, see :meth:`is_finite` + We need to override this method from ``Parent`` since we need to + disable a hashing check that is otherwise enabled when doctesting. - - ``mutable`` -- whether this is a mutable surface; can be changed later - with :meth:`set_immutable` + EXAMPLES:: - EXAMPLES:: + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.category() + Category of finite type oriented similarity surfaces - sage: from flatsurf.geometry.surface import Surface, Surface_list, Surface_dict + sage: S._refine_category_(S.refined_category()) + sage: S.category() + Category of connected without boundary finite type translation surfaces - sage: S = Surface_list(QQ) - sage: isinstance(S, Surface) - True + """ + from sage.structure.debug_options import debug - sage: S = Surface_dict(QQ) - sage: isinstance(S, Surface) - True + old_refine_category_hash_check = debug.refine_category_hash_check + debug.refine_category_hash_check = False + try: + super()._refine_category_(category) + finally: + debug.refine_category_hash_check = old_refine_category_hash_check - TESTS: + # The (cached) an_element is going to fail its test suite because it has the wrong category now. + # Make sure that we create a new copy of an_element when requested. + self._cache_an_element = None - Users are being warned if they try to define a surface over an inexact ring:: - sage: S = Surface_list(RR) - ... - UserWarning: surface defined over an inexact ring; many operations in sage-flatsurf are not going to work correctly over this ring - sage: isinstance(S, Surface) - True +class MutablePolygonalSurface(Surface_base): + r""" + A base class for mutable surfaces that are built by gluing polygons. - """ + EXAMPLES:: - def __init__(self, base_ring, base_label, finite, mutable): - if finite not in [False, True]: - raise ValueError("finite must be either True or False") + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - from sage.all import Rings + sage: from flatsurf.geometry.surface import MutablePolygonalSurface + sage: isinstance(S, MutablePolygonalSurface) + True - if base_ring not in Rings(): - raise ValueError("base_ring must be a ring") + """ - if not base_ring.is_exact(): - from warnings import warn + def __init__(self, base, category=None): + from sage.all import ZZ - warn( - "surface defined over an inexact ring; many operations in sage-flatsurf are not going to work correctly over this ring" - ) + self._next_label = ZZ(0) + self._roots = () - if mutable not in [False, True]: - raise ValueError("mutable must be either True or False") + self._polygons = {} - self._base_ring = base_ring - self._base_label = base_label - self._finite = finite - self._mutable = mutable + self._mutable = True - self._cache = {} + super().__init__(base, category=category) - def is_triangulated(self, limit=None): + def _test_refined_category(self, **options): r""" - EXAMPLES:: - - sage: import flatsurf - sage: G = SymmetricGroup(4) - sage: S = flatsurf.translation_surfaces.origami(G('(1,2,3,4)'), G('(1,4,2,3)')) - sage: S.is_triangulated() - False - sage: S.triangulate().is_triangulated() - True - """ - it = self.label_iterator() - if not self.is_finite(): - if limit is None: - raise ValueError( - "for infinite polygon, 'limit' must be set to a positive integer" - ) - else: - from itertools import islice + Test that this surface has been refined to its best possible + subcategory (that can be computed cheaply.) - it = islice(it, limit) - for p in it: - if self.polygon(p).num_edges() != 3: - return False - return True + We override this method here to disable this check for mutable + surfaces. Mutable surfaces have not been refined yet since changes in + the surface might require a widening of the category which is not + possible. - def polygon(self, label): - r""" - Return the polygon with the provided label. + EXAMPLES:: - This method must be overridden in subclasses. - """ - raise NotImplementedError + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - def opposite_edge(self, label, e): - r""" - Given the label ``label`` of a polygon and an edge ``e`` in that - polygon returns the pair (``ll``, ``ee``) to which this edge is glued. + sage: S._test_refined_category() + sage: S.set_immutable() + sage: S._test_refined_category() - This method must be overridden in subclasses. """ - raise NotImplementedError - - def _change_polygon(self, label, new_polygon, gluing_list=None): - r""" - Internal method used by change_polygon(). Should not be called directly. + if self._mutable: + return - Mutable surfaces should implement this method. - """ - raise NotImplementedError + super()._test_refined_category(**options) - def _set_edge_pairing(self, label1, edge1, label2, edge2): + def add_polygon(self, polygon, *, label=None): r""" - Internal method used by change_edge_gluing(). Should not be called directly. + Add an unglued polygon to this surface and return its label. - Mutable surfaces should implement this method. - """ - raise NotImplementedError + INPUT: - def _add_polygon(self, new_polygon, gluing_list=None, label=None): - r""" - Internal method used by add_polygon(). Should not be called directly. + - ``polygon`` -- a simple Euclidean polygon - Mutable surfaces should implement this method. - """ - raise NotImplementedError + - ``label`` -- a hashable identifier or ``None`` (default: ``None``); + if ``None`` an integer identifier is automatically selected - def _remove_polygon(self, label): - r""" - Internal method used by remove_polygon(). Should not be called directly. + EXAMPLES:: - Mutable surfaces should implement this method. - """ - raise NotImplementedError + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - def num_polygons(self): - r""" - Return the number of polygons making up the surface, or - sage.rings.infinity.Infinity if the surface is infinite. + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + + sage: S.add_polygon(polygons.square(), label=0) + Traceback (most recent call last): + ... + ValueError: polygon label already present in this surface - This is a generic method. On a finite surface it will be linear time in - the edges the first time it is run, then constant time (assuming no - mutation occurs). + sage: S.add_polygon(polygons.square(), label='X') + 'X' - Subclasses should consider overriding this method for increased - performance. """ - if self.is_finite(): - lw = self.walker() - lw.find_all_labels() - return len(lw) - else: - from sage.rings.infinity import Infinity + if not self._mutable: + raise Exception("cannot modify an immutable surface") - return Infinity + if label is None: + while self._next_label in self._polygons: + self._next_label += 1 + label = self._next_label - def label_iterator(self): - r""" - Iterator over all polygon labels. + if label in self._polygons: + raise ValueError("polygon label already present in this surface") - Subclasses should consider overriding this method for increased - performance. - """ - return iter(self.walker()) + self._polygons[label] = polygon.change_ring(self.base_ring()) + return label - def label_polygon_iterator(self): + def add_polygons(self, polygons): r""" - Iterate over pairs (label, polygon). + Add several polygons with automatically assigned labels at once. - Subclasses should consider overriding this method for increased - performance. - """ - for label in self.label_iterator(): - yield label, self.polygon(label) + EXAMPLES:: - def num_edges(self): - r""" - Return the total number of edges of all polygons used. - """ - if self.is_finite(): - try: - return self._cache["num_edges"] - except KeyError: - num_edges = sum( - p.num_edges() for label, p in self.label_polygon_iterator() - ) - self._cache["num_edges"] = num_edges - return num_edges - else: - from sage.rings.infinity import Infinity + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - return Infinity + sage: from flatsurf import polygons + sage: S.add_polygons([polygons.square(), polygons.square()]) + doctest:warning + ... + UserWarning: add_polygons() has been deprecated and will be removed in a future version of sage-flatsurf; use labels = [add_polygon(p) for p in polygons] instead + [0, 1] - def area(self): - r""" - Return the area of this surface. """ - if self.is_finite(): - try: - return self._cache["area"] - except KeyError: - area = sum(p.area() for label, p in self.label_polygon_iterator()) - self._cache["area"] = area - return area - raise NotImplementedError( - "area is not implemented for surfaces built from an infinite number of polygons" + import warnings + + warnings.warn( + "add_polygons() has been deprecated and will be removed in a future version of sage-flatsurf; use labels = [add_polygon(p) for p in polygons] instead" ) - def edge_iterator(self): - r""" - Iterate over the edges of polygons, which are pairs (l,e) where l is a polygon label, 0 <= e < N and N is the number of edges of the polygon with label l. - """ - for label, polygon in self.label_polygon_iterator(): - for edge in range(polygon.num_edges()): - yield label, edge + return [self.add_polygon(p) for p in polygons] - def edge_gluing_iterator(self): + def set_default_graphical_surface(self, graphical_surface): r""" - Iterate over the ordered pairs of edges being glued. - """ - for label_edge_pair in self.edge_iterator(): - yield ( - label_edge_pair, - self.opposite_edge(label_edge_pair[0], label_edge_pair[1]), - ) + EXAMPLES: - def base_ring(self): - r""" - The field on which the coordinates of ``self`` live. + This has been disabled because it tends to break caching:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.set_default_graphical_surface(S.graphical_surface()) + Traceback (most recent call last): + ... + NotImplementedError: set_default_graphical_surface() has been removed from this version of sage-flatsurf. If you want to change the default plotting of a surface, create a subclass and override graphical_surface() instead - This method must be overridden in subclasses! """ - return self._base_ring + raise NotImplementedError( + "set_default_graphical_surface() has been removed from this version of sage-flatsurf. If you want to change the default plotting of a surface, create a subclass and override graphical_surface() instead" + ) - def base_label(self): + def remove_polygon(self, label): r""" - Return the label of a special chosen polygon in the surface. + Remove the polygon with label ``label`` from this surface (and all data + associated to it.) - When no specific choice was made with :meth:`change_base_label`, this - might just be the initial polygon, i.e., the one that was first added - in the construction of this surface. + EXAMPLES:: - This label is for example used as the starting position when walking - the surface in a canonical order with :meth:`walker`. + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - EXAMPLES:: + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 - sage: import flatsurf - sage: G = SymmetricGroup(4) - sage: S = flatsurf.translation_surfaces.origami(G('(1,2,3,4)'), G('(1,4,2,3)')) - sage: S.base_label() - 1 + sage: S.remove_polygon(0) - """ - if self._base_label is None: - raise Exception("base label has not been set for this surface") - return self._base_label + sage: S.add_polygon(polygons.square()) + 0 - def is_finite(self): - r""" - Return whether or not the surface is finite. """ - return self._finite + if not self._mutable: + raise Exception("cannot modify an immutable surface") - def is_mutable(self): - r""" - Return if this surface is mutable. - """ - return self._mutable + self._polygons.pop(label) + self._roots = tuple(root for root in self._roots if root != label) - def set_immutable(self): + def roots(self): r""" - Mark this surface as immutable. - """ - self._mutable = False + Return a label for each connected component on this surface. - def walker(self): - r""" - Return a LabelWalker which walks over the surface in a canonical way. - """ - try: - return self._cache["lw"] - except KeyError: - lw = LabelWalker(self) - self._cache["lw"] = lw - return lw + This implements :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. - def __mutate(self): - r""" - Called before a mutation occurs. Do not call directly. - """ - if not self.is_mutable(): - raise Exception("surface must be mutable") + EXAMPLES:: - # Remove the cache which will likely be invalidated. - self._cache = {} + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - def change_polygon(self, label, new_polygon, gluing_list=None): - r""" - Assuming label is currently in the list of labels, change the - poygon assigned to the provided label to new_polygon, and - glue the edges according to gluing_list (which must be a list - of pairs of length equal to number of edges of the polygon). - """ - self.__mutate() - if not (gluing_list is None or new_polygon.num_edges() == len(gluing_list)): - raise ValueError - self._change_polygon(label, new_polygon, gluing_list) + sage: S.roots() + () - def set_edge_pairing(self, label1, edge1, label2, edge2): - r""" - Update the gluing so that (``label1``, ``edge1``) is glued to - (``label2``, ``edge2``). - """ - self.__mutate() - self._set_edge_pairing(label1, edge1, label2, edge2) + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 - # TODO: deprecation alias? - change_edge_gluing = set_edge_pairing + sage: S.roots() + (0,) - def change_polygon_gluings(self, label, glue_list): - r""" - Updates the list of glued polygons according to the provided list, - which is a list of pairs (pp,ee) whose position in the list - describes the edge of the polygon with the provided label. + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 1 - This method updates both the edges of the polygon with label "label" - and updates the edges listed in the glue_list. - """ - self.__mutate() - p = self.polygon(label) - if p.num_edges() != len(glue_list): - raise ValueError( - "len(glue_list)=" - + str(len(glue_list)) - + " and number of sides of polygon=" - + str(p.num_edges()) - + " should be the same." - ) - for e, (pp, ee) in enumerate(glue_list): - self._set_edge_pairing(label, e, pp, ee) + sage: S.roots() + (0, 1) - def add_polygons(self, polygons): - return [self.add_polygon(p) for p in polygons] + sage: S.glue((0, 0), (1, 0)) + sage: S.roots() + (0,) - def add_polygon(self, new_polygon, gluing_list=None, label=None): - r""" - Adds a the provided polygon to the surface. Utilizes gluing_list - for the gluing data for edges (which must be a list - of pairs representing edges of length equal to number of edges - of the polygon). + .. SEEALSO:: - If the parameter label is provided, the Surface attempts to use - this as the label for the new_polygon. However, this may fail - depending on the implementation. + :meth:`components` - Returns the label assigned to the new_polygon (which may differ - from the label provided). """ - self.__mutate() - if not (gluing_list is None or new_polygon.num_edges() == len(gluing_list)): - raise ValueError - label = self._add_polygon(new_polygon, gluing_list, label) - if self._base_label is None: - self.change_base_label(label) - return label + return LabeledView( + self, RootedComponents_MutablePolygonalSurface(self).keys(), finite=True + ) - def remove_polygon(self, label): + def components(self): r""" - Remove the polygon with the provided label. Causes a ValueError - if the base_label is removed. - """ - if label == self._base_label: - raise ValueError("Can not remove the base_label.") - self.__mutate() - return self._remove_polygon(label) + Return the connected components as the sequence of their respective + polygon labels. - def change_base_label(self, new_base_label): - r""" - Change the base_label to the provided label. - """ - self.__mutate() - self._base_label = new_base_label + EXAMPLES:: - def subdivide(self): - r""" - Return a copy of this surface whose polygons have been partitioned into - smaller triangles with - :meth:`.polygon.ConvexPolygon.subdivide`. + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - EXAMPLES: + sage: S.components() + () + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + + sage: S.components() + ((0,),) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 1 - A surface consisting of a single triangle:: - - sage: from flatsurf.geometry.surface import Surface_dict - sage: from flatsurf.geometry.polygon import Polygon, ConvexPolygons - - sage: S = Surface_dict(QQ) - sage: P = ConvexPolygons(QQ) - sage: S.add_polygon(P([(1, 0), (0, 1), (-1, -1)]), label="Δ") - 'Δ' - - Subdivision of this surface yields a surface with three triangles:: - - sage: T = S.subdivide() - sage: list(T.label_iterator()) - [('Δ', 0), ('Δ', 1), ('Δ', 2)] - - Note that the new labels are old labels plus an index. We verify that - the triangles are glued correctly:: - - sage: list(T.edge_gluing_iterator()) - [((('Δ', 0), 0), None), - ((('Δ', 0), 1), (('Δ', 1), 2)), - ((('Δ', 0), 2), (('Δ', 2), 1)), - ((('Δ', 1), 0), None), - ((('Δ', 1), 1), (('Δ', 2), 2)), - ((('Δ', 1), 2), (('Δ', 0), 1)), - ((('Δ', 2), 0), None), - ((('Δ', 2), 1), (('Δ', 0), 2)), - ((('Δ', 2), 2), (('Δ', 1), 1))] - - If we add another polygon to the original surface and glue things, we - can see how existing gluings are preserved when subdividing:: - - sage: S.add_polygon(P([(1, 0), (0, 1), (-1, 0), (0, -1)]), label='□') - '□' - - sage: S.change_edge_gluing("Δ", 0, "□", 2) - sage: S.change_edge_gluing("□", 1, "□", 3) - - sage: T = S.subdivide() - - sage: list(T.label_iterator()) - [('Δ', 0), ('Δ', 1), ('Δ', 2), ('□', 0), ('□', 1), ('□', 2), ('□', 3)] - sage: list(sorted(T.edge_gluing_iterator())) - [((('Δ', 0), 0), (('□', 2), 0)), - ((('Δ', 0), 1), (('Δ', 1), 2)), - ((('Δ', 0), 2), (('Δ', 2), 1)), - ((('Δ', 1), 0), None), - ((('Δ', 1), 1), (('Δ', 2), 2)), - ((('Δ', 1), 2), (('Δ', 0), 1)), - ((('Δ', 2), 0), None), - ((('Δ', 2), 1), (('Δ', 0), 2)), - ((('Δ', 2), 2), (('Δ', 1), 1)), - ((('□', 0), 0), None), - ((('□', 0), 1), (('□', 1), 2)), - ((('□', 0), 2), (('□', 3), 1)), - ((('□', 1), 0), (('□', 3), 0)), - ((('□', 1), 1), (('□', 2), 2)), - ((('□', 1), 2), (('□', 0), 1)), - ((('□', 2), 0), (('Δ', 0), 0)), - ((('□', 2), 1), (('□', 3), 2)), - ((('□', 2), 2), (('□', 1), 1)), - ((('□', 3), 0), (('□', 1), 0)), - ((('□', 3), 1), (('□', 0), 2)), - ((('□', 3), 2), (('□', 2), 1))] + sage: S.components() + ((0,), (1,)) + + sage: S.glue((0, 0), (1, 0)) + sage: S.components() + ((0, 1),) """ - labels = list(self.label_iterator()) - polygons = [self.polygon(label) for label in labels] + return LabeledView( + self, RootedComponents_MutablePolygonalSurface(self).values(), finite=True + ) - subdivisions = [p.subdivide() for p in polygons] + def polygon(self, label): + r""" + Return the polygon with label ``label`` in this surface. - from flatsurf.geometry.surface import Surface_dict + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. - surface = Surface_dict(base_ring=self._base_ring) + EXAMPLES:: - # Add subdivided polygons - for s, subdivision in enumerate(subdivisions): - label = labels[s] - for p, polygon in enumerate(subdivision): - surface.add_polygon(polygon, label=(label, p)) + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - surface.change_base_label((self._base_label, 0)) + sage: S.polygon(0) + Traceback (most recent call last): + ... + KeyError: 0 - # Add gluings between subdivided polygons - for s, subdivision in enumerate(subdivisions): - label = labels[s] - for p in range(len(subdivision)): - surface.change_edge_gluing( - (label, p), 1, (label, (p + 1) % len(subdivision)), 2 - ) + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 - # Add gluing from original surface - opposite = self.opposite_edge(label, p) - if opposite is not None: - surface.change_edge_gluing((label, p), 0, opposite, 0) + sage: S.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) - return surface + """ + return self._polygons[label] - def subdivide_edges(self, parts=2): + def set_immutable(self): r""" - Return a copy of this surface whose edges have been split into - ``parts`` equal pieces each. - - INPUT: + Make this surface immutable. - - ``parts`` -- a positive integer (default: 2) + Any mutation attempts from now on will be an error. - EXAMPLES: + EXAMPLES:: - A surface consisting of a single triangle:: + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - sage: from flatsurf.geometry.surface import Surface_dict - sage: from flatsurf.geometry.polygon import Polygon, ConvexPolygons + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 - sage: S = Surface_dict(QQ) - sage: P = ConvexPolygons(QQ) - sage: S.add_polygon(P([(1, 0), (0, 1), (-1, -1)]), label="Δ") - 'Δ' + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) - Subdividing this triangle yields a triangle with marked points along - the edges:: + Note that declaring a surface immutable refines its category and + thereby unlocks more methods that are available to such a surface:: - sage: T = S.subdivide_edges() + sage: S.category() + Category of finite type oriented similarity surfaces + sage: old_methods = set(method for method in dir(S) if not method.startswith('_')) - If we add another polygon to the original surface and glue them, we - can see how existing gluings are preserved when subdividing:: + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + sage: new_methods = set(method for method in dir(S) if not method.startswith('_')) + sage: new_methods - old_methods + {'angles', + 'apply_matrix', + 'area', + 'canonicalize', + 'canonicalize_mapping', + 'erase_marked_points', + 'holonomy_field', + 'j_invariant', + 'l_infinity_delaunay_triangulation', + 'minimal_translation_cover', + 'normalized_coordinates', + 'rel_deformation', + 'stratum'} - sage: S.add_polygon(P([(1, 0), (0, 1), (-1, 0), (0, -1)]), label='□') - '□' + An immutable surface cannot be mutated anymore:: - sage: S.change_edge_gluing("Δ", 0, "□", 2) - sage: S.change_edge_gluing("□", 1, "□", 3) + sage: S.remove_polygon(0) + Traceback (most recent call last): + ... + Exception: cannot modify an immutable surface - sage: T = S.subdivide_edges() - sage: list(sorted(T.edge_gluing_iterator())) - [(('Δ', 0), ('□', 5)), - (('Δ', 1), ('□', 4)), - (('Δ', 2), None), - (('Δ', 3), None), - (('Δ', 4), None), - (('Δ', 5), None), - (('□', 0), None), - (('□', 1), None), - (('□', 2), ('□', 7)), - (('□', 3), ('□', 6)), - (('□', 4), ('Δ', 1)), - (('□', 5), ('Δ', 0)), - (('□', 6), ('□', 3)), - (('□', 7), ('□', 2))] + However, the category of an immutable might be further refined as + (expensive to determine) features of the surface are deduced. """ - labels = list(self.label_iterator()) - polygons = [self.polygon(label) for label in labels] + if self._mutable: + self.set_roots(self.roots()) + self._mutable = False - subdivideds = [p.subdivide_edges(parts=parts) for p in polygons] + self._refine_category_(self.refined_category()) - from flatsurf.geometry.surface import Surface_dict + def is_mutable(self): + r""" + Return whether this surface can be modified. - surface = Surface_dict(base_ring=self._base_ring) + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. - # Add subdivided polygons - for s, subdivided in enumerate(subdivideds): - surface.add_polygon(subdivided, label=labels[s]) + EXAMPLES:: - surface.change_base_label(self._base_label) + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - # Reestablish gluings between polygons - for label, polygon, subdivided in zip(labels, polygons, subdivideds): - for e in range(polygon.num_edges()): - opposite = self.opposite_edge(label, e) - if opposite is not None: - for p in range(parts): - surface.change_edge_gluing( - label, - e * parts + p, - opposite[0], - opposite[1] * parts + (parts - p - 1), - ) + sage: S.is_mutable() + True - return surface + sage: S.set_immutable() + sage: S.is_mutable() + False - @cached_method - def __hash__(self): - r""" - Hash compatible with equals. """ - if self.is_mutable(): - raise ValueError("Attempting to hash mutable surface.") - if not self.is_finite(): - raise ValueError("Attempting to hash infinite surface.") - - return hash( - ( - self.base_ring(), - self.base_label(), - tuple(self.label_polygon_iterator()), - tuple(self.edge_gluing_iterator()), - ) - ) + return self._mutable def __eq__(self, other): r""" Return whether this surface is indistinguishable from ``other``. + See + :meth:`~.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.ParentMethods._test_eq_surface` + for details on this notion of equality. + EXAMPLES:: - sage: from flatsurf.geometry.surface import Surface_dict - sage: from flatsurf.geometry.polygon import Polygon, ConvexPolygons + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: T = MutableOrientedSimilaritySurface(QQ) - sage: S = Surface_dict(QQ) - sage: P = ConvexPolygons(QQ) - sage: S.add_polygon(P([(1, 0), (0, 1), (-1, -1)]), label=0) - 0 - sage: S == S + sage: S == T True - sage: T = Surface_dict(QQ) + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S == T False TESTS:: - sage: S == 42 + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: T = MutableOrientedSimilaritySurface(QQ) + + sage: S != T False - """ - if self is other: - return True + sage: S.add_polygon(polygons.square()) + 0 - if not isinstance(other, Surface): - return False + sage: S != T + True - if self.is_finite() != other.is_finite(): - return False - if self.base_ring() != other.base_ring(): - return False - if self.is_mutable() != other.is_mutable(): - return False - if self.num_polygons() == 0: - return other.num_polygons() == 0 - if other.num_polygons() == 0: - return False - if self.base_label() != other.base_label(): + """ + if not isinstance(other, MutablePolygonalSurface): return False - if self.num_polygons() != other.num_polygons(): + + if self.base() != other.base(): return False - if self.polygon(self.base_label()) != other.polygon(self.base_label()): + if self._polygons != other._polygons: return False - if not self.is_finite(): - raise NotImplementedError("cannot compare these infinite surfaces yet") + # Note that the order of the root labels matters since it changes the order of iteration in labels() + if self._roots != other._roots: + return False - for label, polygon in self.label_polygon_iterator(): - try: - polygon2 = other.polygon(label) - except ValueError: - return False - if polygon != polygon2: - return False - for edge in range(polygon.num_edges()): - if self.opposite_edge(label, edge) != other.opposite_edge(label, edge): - return False + if self._mutable != other._mutable: + return False return True - def __ne__(self, other): - return not self == other - - def _test_base_ring(self, **options): - # Test that the base_label is associated to a polygon - tester = self._tester(**options) - from sage.all import Rings - - tester.assertTrue(self.base_ring() in Rings()) - - def _test_base_label(self, **options): - # Test that the base_label is associated to a polygon - tester = self._tester(**options) - from .polygon import ConvexPolygon - - tester.assertTrue( - isinstance(self.polygon(self.base_label()), ConvexPolygon), - "polygon(base_label) does not return a ConvexPolygon. " - + "Here base_label=" - + str(self.base_label()), - ) - - def _test_gluings(self, **options): - # iterate over pairs with pair1 glued to pair2 - tester = self._tester(**options) + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. - if self.is_finite(): - it = self.label_iterator() - else: - from itertools import islice + EXAMPLES:: - it = islice(self.label_iterator(), 30) + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: T = MutableOrientedSimilaritySurface(QQ) + sage: hash(S) == hash(T) + Traceback (most recent call last): + ... + TypeError: cannot hash a mutable surface + + sage: S.set_immutable() + sage: T.set_immutable() + sage: hash(S) == hash(T) + True - for lab in it: - p = self.polygon(lab) - for k in range(p.num_edges()): - e = (lab, k) - f = self.opposite_edge(lab, k) - tester.assertFalse( - f is None, "edge ({}, {}) is not glued".format(lab, k) - ) - g = self.opposite_edge(f[0], f[1]) - tester.assertEqual( - e, - g, - "edge gluing is not a pairing:\n{} -> {} -> {}".format(e, f, g), - ) + """ + if self._mutable: + raise TypeError("cannot hash a mutable surface") - def _test_override(self, **options): - # Test that the required methods have been overridden and that some other methods have not been overridden. + return hash((tuple(self.labels()), tuple(self.polygons()), self._roots)) - # Of course, we don't care if the methods are overridden or not we just want to warn the programmer. - if "tester" in options: - tester = options["tester"] - else: - tester = self._tester(**options) - - # Check for override: - tester.assertNotEqual( - self.polygon.__func__, - Surface.polygon, - "Method polygon of Surface must be overridden. The Surface is of type " - + str(type(self)) - + ".", - ) - tester.assertNotEqual( - self.opposite_edge.__func__, - Surface.opposite_edge, - "Method opposite_edge of Surface must be overridden. The Surface is of type " - + str(type(self)) - + ".", - ) + def _repr_(self): + r""" + Return a printable representation of this surface. - if self.is_mutable(): - # Check for override: - tester.assertNotEqual( - self._change_polygon.__func__, - Surface._change_polygon, - "Method _change_polygon of Surface must be overridden in a mutable surface. " - + "The Surface is of type " - + str(type(self)) - + ".", - ) - tester.assertNotEqual( - self._set_edge_pairing.__func__, - Surface._set_edge_pairing, - "Method _set_edge_pairing of Surface must be overridden in a mutable surface. " - + "The Surface is of type " - + str(type(self)) - + ".", - ) - tester.assertNotEqual( - self._add_polygon.__func__, - Surface._add_polygon, - "Method _add_polygon of Surface must be overridden in a mutable surface. " - + "The Surface is of type " - + str(type(self)) - + ".", - ) - tester.assertNotEqual( - self._remove_polygon.__func__, - Surface._remove_polygon, - "Method _remove_polygon of Surface must be overridden in a mutable surface. " - + "The Surface is of type " - + str(type(self)) - + ".", - ) + EXAMPLES:: - def _test_polygons(self, **options): - # Test that the base_label is associated to a polygon - if "tester" in options: - tester = options["tester"] - else: - tester = self._tester(**options) - from .polygon import ConvexPolygon + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S + Empty Surface - if self.is_finite(): - it = self.label_iterator() - else: - from itertools import islice + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S + Translation Surface with boundary built from a square - it = islice(self.label_iterator(), 30) - for label in it: - tester.assertTrue( - isinstance(self.polygon(label), ConvexPolygon), - "polygon(label) does not return a ConvexPolygon when label=" - + str(label), - ) + """ + if not self.is_finite_type(): + return "Surface built from infinitely many polygons" + if len(self.labels()) == 0: + return "Empty Surface" -class Surface_list(Surface): - r""" - A fast mutable :class:`Surface` using a list to store polygons and gluings. + return f"{self._describe_surface()} built from {self._describe_polygons()}" - ALGORITHM: + def _describe_surface(self): + r""" + Return a string describing this kind of surface. - Internally, we maintain a list ``_p`` for storing polygons together with - gluing data. + This is a helper method for :meth:`_repr_`. - Each ``_p[label]`` is typically a pair ``(polygon, gluing_list)`` where - ``gluing_list`` is a list of pairs ``(other_label, other_edge)`` such that - :meth:`opposite_edge(label, edge) ` returns - ``_p[label][1][edge]``. + EXAMPLES:: - INPUT: + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S._describe_surface() + 'Translation Surface' - - ``base_ring`` -- ring or ``None`` (default: ``None``); the ring - containing the coordinates of the vertices of the polygons. If ``None``, - the :meth:`Surface.base_ring` will be the one of ``surface``. + """ + return "Surface" - - ``surface`` -- :class:`Surface`, - :class:`.similarity_surface.SimilaritySurface`, or ``None`` (default: - ``None``); a surface to be copied or referenced (see ``copy``). If - ``None``, the surface is initially empty. + def _describe_polygons(self): + r""" + Return a string describing the nature of the polygons that make up this surface. - - ``copy`` -- boolean or ``None`` (default: ``None``); whether the data - underlying ``surface`` is copied into this surface or just a reference to - that surface is kept. If ``None``, a sensible default is chosen, namely - ``surface.is_mutable()``. + This is a helper method for :meth:`_repr_`. - - ``mutable`` -- boolean or ``None`` (default: ``None``); whether this - surface is mutable. When ``None``, the surface will be mutable iff - ``surface`` is ``None``. + EXAMPLES:: - EXAMPLES:: + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S._describe_polygons() + '' - sage: from flatsurf import * - sage: from flatsurf.geometry.surface import Surface_list - sage: p=polygons.regular_ngon(5) - sage: s=Surface_list(base_ring=p.base_ring()) - sage: s.add_polygon(p) # gets label 0 - 0 - sage: s.add_polygon( (-matrix.identity(2))*p ) # gets label 1 - 1 - sage: s.change_polygon_gluings(0,[(1,e) for e in range(5)]) - sage: # base label defaults to zero. - sage: s.set_immutable() - sage: TestSuite(s).run() - - We surgically add a square into an infinite billiard surface:: - - sage: p = polygons(vertices=[(0,0),(4,0),(0,3)]) - sage: s = similarity_surfaces.billiard(p) - sage: ts=s.minimal_cover(cover_type="translation").copy(relabel=True, mutable=True) - sage: # Explore the surface a bit - sage: ts.polygon(0) - Polygon: (0, 0), (4, 0), (0, 3) - sage: ts.opposite_edge(0,0) - (1, 2) - sage: ts.polygon(1) - Polygon: (0, 0), (0, -3), (4, 0) - sage: s = ts.underlying_surface() - sage: l=s.add_polygon(polygons.square(side=4)) - sage: s.change_edge_gluing(0,0,l,2) - sage: s.change_edge_gluing(1,2,l,0) - sage: s.change_edge_gluing(l,1,l,3) - sage: print("Glued in label is "+str(l)) - Glued in label is 2 - sage: count = 0 - sage: for x in ts.edge_iterator(gluings=True): - ....: print(x) - ....: count=count+1 - ....: if count>15: - ....: break - ((0, 0), (2, 2)) - ((0, 1), (3, 1)) - ((0, 2), (4, 0)) - ((2, 0), (1, 2)) - ((2, 1), (2, 3)) - ((2, 2), (0, 0)) - ((2, 3), (2, 1)) - ((3, 0), (5, 2)) - ((3, 1), (0, 1)) - ((3, 2), (6, 0)) - ((4, 0), (0, 2)) - ((4, 1), (7, 1)) - ((4, 2), (8, 0)) - ((1, 0), (8, 2)) - ((1, 1), (9, 1)) - ((1, 2), (2, 0)) - sage: count = 0 - sage: for l,p in ts.label_iterator(polygons=True): - ....: print(str(l)+" -> "+str(p)) - ....: count=count+1 - ....: if count>5: - ....: break - 0 -> Polygon: (0, 0), (4, 0), (0, 3) - 2 -> Polygon: (0, 0), (4, 0), (4, 4), (0, 4) - 3 -> Polygon: (0, 0), (-72/25, -21/25), (28/25, -96/25) - 4 -> Polygon: (0, 0), (0, 3), (-4, 0) - 1 -> Polygon: (0, 0), (0, -3), (4, 0) - 5 -> Polygon: (0, 0), (-28/25, 96/25), (-72/25, -21/25) + """ + polygons = [ + (-len(p.erase_marked_vertices().vertices()), p.describe_polygon()) + for p in self.polygons() + ] + polygons.sort() + polygons = [description for (edges, description) in polygons] - """ + if not polygons: + return "" - def __init__(self, base_ring=None, surface=None, copy=None, mutable=None): - self._p = [] # list of pairs (polygon, gluings) - self._reference_surface = None - self._removed_labels = [] - self._num_polygons = 0 - - # Validate input parameters and fill in defaults - ( - base_ring, - surface, - copy, - mutable, - finite, - ) = Surface_list._validate_init_parameters( - base_ring=base_ring, - surface=surface, - copy=copy, - mutable=mutable, - finite=None, - ) + collated = [] + while polygons: + count = 1 + polygon = polygons.pop() + while polygons and polygons[-1] == polygon: + count += 1 + polygons.pop() - Surface.__init__(self, base_ring, base_label=0, finite=finite, mutable=True) - - # Initialize surface from reference surface - if surface is not None: - if copy is True: - reference_label_to_label = { - label: self.add_polygon(polygon) - for label, polygon in surface.label_polygon_iterator() - } - - for ( - (label, edge), - (glued_label, glued_edge), - ) in surface.edge_gluing_iterator(): - self.set_edge_pairing( - reference_label_to_label[label], - edge, - reference_label_to_label[glued_label], - glued_edge, - ) - - self.change_base_label(reference_label_to_label[surface.base_label()]) + if count == 1: + collated.append(f"{polygon[0]} {polygon[1]}") else: - self._reference_surface = surface - self._ref_to_int = {} - self._int_to_ref = [] + collated.append(f"{count} {polygon[2]}") - self._num_polygons = surface.num_polygons() + description = collated.pop() - # Cache the base polygon - self.change_base_label(self.__get_label(surface.base_label())) - assert self.base_label() == 0 + if collated: + description = ", ".join(collated) + " and " + description - if not mutable: - self.set_immutable() + return description - @classmethod - def _validate_init_parameters(cls, base_ring, surface, copy, mutable, finite): + def set_root(self, root): r""" - Helper method for ``__init__`` that validates the parameters and - returns them in the same order with defaults filled in. - """ - if surface is None and base_ring is None: - raise ValueError("Either surface or base_ring must be provided.") + Set ``root`` as the label at which exploration of a connected component + starts. - if surface is None: - if copy is not None: - raise ValueError("Cannot copy when surface was provided.") + This method can be called for connected and disconnected surfaces. In + either case, it establishes ``root`` as the new label from which + enumeration of the connected component containing it starts. If another + label for this component had been set earlier, it is replaced. - if mutable is None: - mutable = True + .. NOTE:: - finite = True - else: - from .similarity_surface import SimilaritySurface + After roots have been declared explicitly, gluing operations come + at an additional cost since the root labels have to be updated + sometimes. It is therefore good practice to declare the root labels + after all the gluings have been established when creating a + surface. - if isinstance(surface, SimilaritySurface): - surface = surface.underlying_surface() + INPUT: - if not isinstance(surface, Surface): - raise TypeError("surface must be a Surface or a SimilaritySurface") + - ``root`` -- a polygon label in this surface - if not surface.is_finite() and surface.is_mutable(): - raise NotImplementedError( - "Cannot create surface from infinite mutable surface yet." - ) + EXAMPLES:: - if base_ring is None: - base_ring = surface.base_ring() + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) - if base_ring != surface.base_ring(): - raise NotImplementedError( - "Cannot provide both a surface and a base_ring yet." - ) + sage: S.set_root(0) + Traceback (most recent call last): + ... + ValueError: root must be a label in the surface - if mutable is None: - mutable = True + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 - if copy is None: - copy = surface.is_mutable() + sage: S.set_root(0) + sage: S.set_root(1) - if copy and not surface.is_finite(): - raise ValueError("Cannot copy infinite surface.") + sage: S.roots() + (0, 1) - if surface.is_mutable() and not copy: - raise ValueError("Cannot reference mutable surface.") + Note that the roots get updated automatically when merging components:: - finite = surface.is_finite() + sage: S.glue((0, 0), (1, 0)) + sage: S.roots() + (0,) - return base_ring, surface, copy, mutable, finite + The purpose of changing the root label is to modify the order of + exploration, e.g., in :meth:`labels`:: - def __get_label(self, ref_label): - r""" - Returns a corresponding label. Creates a new label if necessary. - """ - try: - return self._ref_to_int[ref_label] - except KeyError: - polygon = self._reference_surface.polygon(ref_label) - data = [polygon, [None for i in range(polygon.num_edges())]] - if len(self._removed_labels) > 0: - i = self._removed_labels.pop() - self._p[i] = data - self._ref_to_int[ref_label] = i - self._int_to_ref[i] = ref_label - else: - i = len(self._p) - if i != len(self._int_to_ref): - raise RuntimeError( - "length of self._int_to_ref is " - + str(len(self._int_to_ref)) - + " should be the same as i=" - + str(i) - ) - self._p.append(data) - self._ref_to_int[ref_label] = i - self._int_to_ref.append(ref_label) - return i - - def polygon(self, lab): - r""" - Return the polygon with label ``lab``. - """ - try: - data = self._p[lab] - except IndexError: - if self._reference_surface is None: - raise ValueError(f"No polygon with label {lab}.") + sage: S.labels() + (0, 1) - for label in self.label_iterator(): - if label >= lab: - break + sage: S.set_root(1) + sage: S.labels() + (1, 0) - if lab >= len(self._p): - raise ValueError(f"no polygon with label {lab}") + .. SEEALSO:: - data = self._p[lab] + :meth:`set_roots` to replace all the root labels - if data is None: - raise ValueError("Provided label was removed.") + """ + if not self._mutable: + raise Exception("cannot modify an immutable surface") - return data[0] + root = [label for label in self.labels() if label == root] + if not root: + raise ValueError("root must be a label in the surface") + assert len(root) == 1 + root = root[0] - def opposite_edge(self, p, e): - r""" - Given the label ``p`` of a polygon and an edge ``e`` in that polygon - returns the pair (``pp``, ``ee``) to which this edge is glued. - """ - try: - data = self._p[p] - except KeyError: - raise ValueError("No known polygon with provided label") - if data is None: - raise ValueError("Provided label was removed.") - glue = data[1] - try: - oe = glue[e] - except KeyError: - raise ValueError("Edge out of range of polygon.") - if oe is None: - if self._reference_surface is None: - # Perhaps the user of this class left an edge unglued? - return None - else: - ref_p = self._int_to_ref[p] - ref_pp, ref_ee = self._reference_surface.opposite_edge(ref_p, e) - pp = self.__get_label(ref_pp) - return_value = (pp, ref_ee) - glue[e] = return_value - return return_value - else: - # Successfully return edge data - return oe + component = [component for component in self.components() if root in component] + assert len(component) == 1 + component = component[0] - # Methods for changing the surface + self._roots = tuple(r for r in self._roots if r not in component) + (root,) - def _change_polygon(self, label, new_polygon, gluing_list=None): + def set_roots(self, roots): r""" - Internal method used by change_polygon(). Should not be called directly. - """ - try: - data = self._p[label] - except KeyError: - raise ValueError("No known polygon with provided label") - if data is None: - raise ValueError("Provided label was removed from the surface.") - data[0] = new_polygon - if data[1] is None or new_polygon.num_edges() != len(data[1]): - data[1] = [None for e in range(new_polygon.num_edges())] - if gluing_list is not None: - self.change_polygon_gluings(label, gluing_list) + Declare that the labels in ``roots`` are the labels from which their + corresponding connected components should be enumerated. - def _set_edge_pairing(self, label1, edge1, label2, edge2): - r""" - Internal method used by change_edge_gluing(). Should not be called directly. - """ - try: - data = self._p[label1] - except KeyError: - raise ValueError("No known polygon with provided label1=" + str(label1)) - if data is None: - raise ValueError( - "Provided label1=" + str(label1) + " was removed from the surface." - ) - data[1][edge1] = (label2, edge2) - try: - data = self._p[label2] - except KeyError: - raise ValueError("No known polygon with provided label2=" + str(label2)) - if data is None: - raise ValueError( - "Provided label2=" + str(label2) + " was removed from the surface." - ) - data[1][edge2] = (label1, edge1) + There must be at most one label for each connected component in + ``roots``. Components that have no label set explicitly will have their + label chosen automatically. - # TODO: deprecation alias? - _change_edge_gluing = _set_edge_pairing + INPUT: - def _add_polygon(self, new_polygon, gluing_list=None, label=None): - r""" - Internal method used by add_polygon(). Should not be called directly. + - ``roots`` -- a sequence of polygon labels in this surface EXAMPLES:: - sage: from flatsurf import * - sage: from flatsurf.geometry.surface import Surface_list - sage: p=polygons.regular_ngon(5) - sage: s=Surface_list(base_ring=p.base_ring()) - sage: s.add_polygon(p, label=3) - 3 - sage: s.add_polygon( (-matrix.identity(2))*p, label=30) - 30 - sage: s.change_polygon_gluings(3,[(30,e) for e in range(5)]) - sage: s.change_base_label(30) - sage: s.num_polygons() + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + sage: S.add_polygon(polygons.square()) 2 - sage: TestSuite(s).run() - sage: s.remove_polygon(3) - sage: s.add_polygon(p, label=6) - 6 - sage: s.change_polygon_gluings(6,[(30,e) for e in range(5)]) - sage: s.num_polygons() + + sage: S.glue((0, 0), (1, 0)) + + sage: S.set_roots([1]) + sage: S.roots() + (1, 2) + + Setting the roots of connected components affects their enumeration in :meth:`labels`:: + + sage: S.labels() + (1, 0, 2) + + sage: S.set_roots([0, 2]) + sage: S.labels() + (0, 1, 2) + + There must be at most one root per component:: + + sage: S.set_roots([0, 1, 2]) + Traceback (most recent call last): + ... + ValueError: there must be at most one root label for each connected component + + """ + if not self._mutable: + raise Exception("cannot modify an immutable surface") + + roots = [[label for label in self.labels() if label == root] for root in roots] + + if any(len(root) == 0 for root in roots): + raise ValueError("roots must be existing labels in the surface") + + assert all(len(root) == 1 for root in roots) + + roots = tuple(root[0] for root in roots) + + for component in self.components(): + if len([root for root in roots if root in component]) > 1: + raise ValueError( + "there must be at most one root label for each connected component" + ) + + self._roots = tuple(roots) + + def change_base_label(self, label): + r""" + This is a deprecated alias for :meth:`set_root`. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + + sage: S.change_base_label(0) + doctest:warning + ... + UserWarning: change_base_label() has been deprecated and will be removed in a future version of sage-flatsurf; use set_root() instead + + """ + import warnings + + warnings.warn( + "change_base_label() has been deprecated and will be removed in a future version of sage-flatsurf; use set_root() instead" + ) + + self.set_root(label) + + @cached_method + def labels(self): + r""" + Return the polygon labels in this surface. + + This replaces the generic + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels` + method with a more efficient implementation. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: S.labels() + () + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + + sage: S.labels() + (0,) + + .. SEEALSO:: + + :meth:`polygon` to translate polygon labels to the corresponding polygons + + :meth:`polygons` for the corresponding sequence of polygons + + """ + return LabelsFromView(self, self._polygons.keys(), finite=True) + + @cached_method + def polygons(self): + r""" + Return the polygons that make up this surface. + + The order the polygons are returned is guaranteed to be compatible with + the order of the labels in :meth:`~.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.labels`. + + This replaces the generic + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygons` + with a more efficient implementation. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: S.polygons() + () + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + sage: S.add_polygon(polygons.square()) 2 - sage: TestSuite(s).run() - sage: s.change_base_label(6) - sage: s.remove_polygon(30) - sage: label = s.add_polygon((-matrix.identity(2))*p) - sage: s.change_polygon_gluings(6,[(label,e) for e in range(5)]) - sage: TestSuite(s).run() + + sage: S.polygons() + (Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]), Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]), Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + + .. SEEALSO:: + + :meth:`polygon` to get a single polygon + + """ + return Polygons_MutableOrientedSimilaritySurface(self) + + +class OrientedSimilaritySurface(Surface_base): + r""" + Base class for surfaces built from Euclidean polygons that are glued with + orientation preserving similarities. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf.geometry.surface import OrientedSimilaritySurface + sage: isinstance(S, OrientedSimilaritySurface) + True + + """ + Element = SurfacePoint + + def __init__(self, base, category=None): + from sage.categories.all import Rings + + if base not in Rings(): + raise TypeError("base ring must be a ring") + + from flatsurf.geometry.categories import SimilaritySurfaces + + if category is None: + category = SimilaritySurfaces().Oriented() + + category &= SimilaritySurfaces().Oriented() + + super().__init__(base, category=category) + + def _describe_surface(self): + if not self.is_finite_type(): + return "Surface built from infinitely many polygons" + + if not self.is_connected(): + # Many checks do not work yet if a surface is not connected, so we stop here. + return "Disconnected Surface" + + if self.is_translation_surface(positive=True): + description = "Translation Surface" + elif self.is_translation_surface(positive=False): + description = "Half-Translation Surface" + elif self.is_dilation_surface(positive=True): + description = "Positive Dilation Surface" + elif self.is_dilation_surface(positive=False): + description = "Dilation Surface" + elif self.is_cone_surface(): + description = "Cone Surface" + if self.is_rational_surface(): + description = f"Rational {description}" + else: + description = "Surface" + + if hasattr(self, "stratum"): + try: + description += f" in {self.stratum()}" + except NotImplementedError: + # Computation of the stratum might fail due to #227. + pass + elif self.genus is not NotImplemented: + description = f"Genus {self.genus()} {description}" + + if self.is_with_boundary(): + description += " with boundary" + + return description + + +class MutableOrientedSimilaritySurface_base(OrientedSimilaritySurface): + r""" + Base class for surface built from Euclidean polyogns glued by orientation + preserving similarities. + + This provides the features of :class:`MutableOrientedSimilaritySurface` + without making a choice about how data is stored internally; it is a + generic base class for other implementations of mutable surfaces. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf.geometry.surface import MutableOrientedSimilaritySurface_base + sage: isinstance(S, MutableOrientedSimilaritySurface_base) + True + + """ + + def triangle_flip(self, l1, e1, in_place=False, test=False, direction=None): + r""" + Overrides + :meth:`.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.triangle_flip` + to provide in-place flipping of triangles. + + See that method for details. + """ + if not in_place: + return super().triangle_flip( + l1=l1, e1=e1, in_place=in_place, test=test, direction=direction + ) + + s = self + + p1 = s.polygon(l1) + if not len(p1.vertices()) == 3: + raise ValueError("The polygon with the provided label is not a triangle.") + l2, e2 = s.opposite_edge(l1, e1) + + sim = s.edge_transformation(l2, e2) + p2 = s.polygon(l2) + if not len(p2.vertices()) == 3: + raise ValueError( + "The polygon opposite the provided edge is not a triangle." + ) + + from flatsurf import Polygon + + p2 = Polygon(vertices=[sim(v) for v in p2.vertices()], base_ring=p1.base_ring()) + + if direction is None: + direction = (s.base_ring() ** 2)((0, 1)) + # Get vertices corresponding to separatices in the provided direction. + v1 = p1.find_separatrix(direction=direction)[0] + v2 = p2.find_separatrix(direction=direction)[0] + # Our quadrilateral has vertices labeled: + # * 0=p1.vertex(e1+1)=p2.vertex(e2) + # * 1=p1.vertex(e1+2) + # * 2=p1.vertex(e1)=p2.vertex(e2+1) + # * 3=p2.vertex(e2+2) + # Record the corresponding vertices of this quadrilateral. + q1 = (3 + v1 - e1 - 1) % 3 + q2 = (2 + (3 + v2 - e2 - 1) % 3) % 4 + + new_diagonal = p2.vertex((e2 + 2) % 3) - p1.vertex((e1 + 2) % 3) + # This list will store the new triangles which are being glued in. + # (Unfortunately, they may not be cyclically labeled in the correct way.) + new_triangle = [] + try: + new_triangle.append( + Polygon( + edges=[ + p1.edge((e1 + 2) % 3), + p2.edge((e2 + 1) % 3), + -new_diagonal, + ], + base_ring=p1.base_ring(), + ) + ) + new_triangle.append( + Polygon( + edges=[ + p2.edge((e2 + 2) % 3), + p1.edge((e1 + 1) % 3), + new_diagonal, + ], + base_ring=p1.base_ring(), + ) + ) + # The above triangles would be glued along edge 2 to form the diagonal of the quadrilateral being removed. + except ValueError: + raise ValueError( + "Gluing triangles along this edge yields a non-convex quadrilateral." + ) + + # Find the separatrices of the two new triangles, and in particular which way they point. + new_sep = [] + new_sep.append(new_triangle[0].find_separatrix(direction=direction)[0]) + new_sep.append(new_triangle[1].find_separatrix(direction=direction)[0]) + # The quadrilateral vertices corresponding to these separatrices are + # new_sep[0]+1 and (new_sep[1]+3)%4 respectively. + + # i=0 if the new_triangle[0] should be labeled l1 and new_triangle[1] should be labeled l2. + # i=1 indicates the opposite labeling. + if new_sep[0] + 1 == q1: + assert (new_sep[1] + 3) % 4 == q2 + i = 0 + else: + assert (new_sep[1] + 3) % 4 == q1 + assert new_sep[0] + 1 == q2 + i = 1 + + # These quantities represent the cyclic relabeling of triangles needed. + cycle1 = (new_sep[i] - v1 + 3) % 3 + cycle2 = (new_sep[1 - i] - v2 + 3) % 3 + + # This will be the new triangle with label l1: + tri1 = Polygon( + edges=[ + new_triangle[i].edge(cycle1), + new_triangle[i].edge((cycle1 + 1) % 3), + new_triangle[i].edge((cycle1 + 2) % 3), + ], + base_ring=p1.base_ring(), + ) + # This will be the new triangle with label l2: + tri2 = Polygon( + edges=[ + new_triangle[1 - i].edge(cycle2), + new_triangle[1 - i].edge((cycle2 + 1) % 3), + new_triangle[1 - i].edge((cycle2 + 2) % 3), + ], + base_ring=p1.base_ring(), + ) + # In the above, edge 2-cycle1 of tri1 would be glued to edge 2-cycle2 of tri2 + diagonal_glue_e1 = 2 - cycle1 + diagonal_glue_e2 = 2 - cycle2 + + assert p1.find_separatrix(direction=direction) == tri1.find_separatrix( + direction=direction + ) + assert p2.find_separatrix(direction=direction) == tri2.find_separatrix( + direction=direction + ) + + # Two opposite edges will not change their labels (label,edge) under our regluing operation. + # The other two opposite ones will change and in fact they change labels. + # The following finds them (there are two cases). + # At the end of the if statement, the following will be true: + # * new_glue_e1 and new_glue_e2 will be the edges of the new triangle with label l1 and l2 which need regluing. + # * old_e1 and old_e2 will be the corresponding edges of the old triangles. + # (Note that labels are swapped between the pair. The appending 1 or 2 refers to the label used for the triangle.) + if p1.edge(v1) == tri1.edge(v1): + # We don't have to worry about changing gluings on edge v1 of the triangles with label l1 + # We do have to worry about the following edge: + new_glue_e1 = ( + 3 - diagonal_glue_e1 - v1 + ) # returns the edge which is neither diagonal_glue_e1 nor v1. + # This corresponded to the following old edge: + old_e1 = 3 - e1 - v1 # Again this finds the edge which is neither e1 nor v1 + else: + temp = (v1 + 2) % 3 + assert p1.edge(temp) == tri1.edge(temp) + # We don't have to worry about changing gluings on edge (v1+2)%3 of the triangles with label l1 + # We do have to worry about the following edge: + new_glue_e1 = ( + 3 - diagonal_glue_e1 - temp + ) # returns the edge which is neither diagonal_glue_e1 nor temp. + # This corresponded to the following old edge: + old_e1 = ( + 3 - e1 - temp + ) # Again this finds the edge which is neither e1 nor temp + if p2.edge(v2) == tri2.edge(v2): + # We don't have to worry about changing gluings on edge v2 of the triangles with label l2 + # We do have to worry about the following edge: + new_glue_e2 = ( + 3 - diagonal_glue_e2 - v2 + ) # returns the edge which is neither diagonal_glue_e2 nor v2. + # This corresponded to the following old edge: + old_e2 = 3 - e2 - v2 # Again this finds the edge which is neither e2 nor v2 + else: + temp = (v2 + 2) % 3 + assert p2.edge(temp) == tri2.edge(temp) + # We don't have to worry about changing gluings on edge (v2+2)%3 of the triangles with label l2 + # We do have to worry about the following edge: + new_glue_e2 = ( + 3 - diagonal_glue_e2 - temp + ) # returns the edge which is neither diagonal_glue_e2 nor temp. + # This corresponded to the following old edge: + old_e2 = ( + 3 - e2 - temp + ) # Again this finds the edge which is neither e2 nor temp + + # remember the old gluings. + old_opposite1 = s.opposite_edge(l1, old_e1) + old_opposite2 = s.opposite_edge(l2, old_e2) + + us = s + + # Replace the triangles. + us.replace_polygon(l1, tri1) + us.replace_polygon(l2, tri2) + # Glue along the new diagonal of the quadrilateral + us.glue((l1, diagonal_glue_e1), (l2, diagonal_glue_e2)) + # Now we deal with that pair of opposite edges of the quadrilateral that need regluing. + # There are some special cases: + if old_opposite1 == (l2, old_e2): + # These opposite edges were glued to each other. + # Do the same in the new surface: + us.glue((l1, new_glue_e1), (l2, new_glue_e2)) + else: + if old_opposite1 == (l1, old_e1): + # That edge was "self-glued". + us.glue((l2, new_glue_e2), (l2, new_glue_e2)) + else: + # The edge (l1,old_e1) was glued in a standard way. + # That edge now corresponds to (l2,new_glue_e2): + us.glue((l2, new_glue_e2), (old_opposite1[0], old_opposite1[1])) + if old_opposite2 == (l2, old_e2): + # That edge was "self-glued". + us.glue((l1, new_glue_e1), (l1, new_glue_e1)) + else: + # The edge (l2,old_e2) was glued in a standard way. + # That edge now corresponds to (l1,new_glue_e1): + us.glue((l1, new_glue_e1), (old_opposite2[0], old_opposite2[1])) + return s + + def standardize_polygons(self, in_place=False): + r""" + Replace each polygon with a new polygon which differs by + translation and reindexing. The new polygon will have the property + that vertex zero is the origin, and all vertices lie either in the + upper half plane, or on the x-axis with non-negative x-coordinate. + + This is done to the current surface if in_place=True. A mutable + copy is created and returned if in_place=False (as default). + + This overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.Oriented.ParentMethods.standardize_polygons` + to provide in-place standardizing of surfaces. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, Polygon + sage: p = Polygon(vertices = ([(1,1),(2,1),(2,2),(1,2)])) + sage: s = MutableOrientedSimilaritySurface(QQ) + sage: s.add_polygon(p) + 0 + sage: s.glue((0, 0), (0, 2)) + sage: s.glue((0, 1), (0, 3)) + sage: s.set_root(0) + sage: s.set_immutable() + + sage: s.standardize_polygons().polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + + """ + if not in_place: + S = MutableOrientedSimilaritySurface.from_surface(self) + S.standardize_polygons(in_place=True) + return S + + cv = {} # dictionary for non-zero canonical vertices + for label, polygon in zip(self.labels(), self.polygons()): + best = 0 + best_pt = polygon.vertex(best) + for v in range(1, len(polygon.vertices())): + pt = polygon.vertex(v) + if (pt[1] < best_pt[1]) or (pt[1] == best_pt[1] and pt[0] < best_pt[0]): + best = v + best_pt = pt + # We replace the polygon if the best vertex is not the zero vertex, or + # if the coordinates of the best vertex differs from the origin. + if not (best == 0 and best_pt.is_zero()): + cv[label] = best + for label, v in cv.items(): + self.set_vertex_zero(label, v, in_place=True) + + return self + + +class MutableOrientedSimilaritySurface( + MutableOrientedSimilaritySurface_base, MutablePolygonalSurface +): + r""" + A surface built from Euclidean polyogns glued by orientation preserving + similarities. + + This is the main tool to create new surfaces of finite type in + sage-flatsurf. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + + sage: S.set_immutable() + + sage: S + Translation Surface in H_1(0) built from a square + + TESTS:: + + sage: TestSuite(S).run() + + """ + + def __init__(self, base, category=None): + self._gluings = {} + + from flatsurf.geometry.categories import SimilaritySurfaces + + if category is None: + category = SimilaritySurfaces().Oriented().FiniteType() + + category &= SimilaritySurfaces().Oriented().FiniteType() + + super().__init__(base, category=category) + + @classmethod + def from_surface(cls, surface, category=None): + r""" + Return a mutable copy of ``surface``. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces, MutableOrientedSimilaritySurface, polygons + + sage: T = translation_surfaces.square_torus() + + We build a surface that is made from two tori: + + sage: S = MutableOrientedSimilaritySurface.from_surface(T) + sage: S.add_polygon(polygons.square()) + 1 + sage: S.glue((1, 0), (1, 2)) + sage: S.glue((1, 1), (1, 3)) + + sage: S.set_immutable() + + sage: S + Disconnected Surface built from 2 squares + + """ + if not surface.is_finite_type(): + raise TypeError + self = MutableOrientedSimilaritySurface(surface.base_ring(), category=category) + + for label in surface.labels(): + self.add_polygon(surface.polygon(label), label=label) + + for label in surface.labels(): + for edge in range(len(surface.polygon(label).vertices())): + cross = surface.opposite_edge(label, edge) + if cross: + self.glue((label, edge), cross) + + if isinstance(surface, MutablePolygonalSurface): + # Only copy explicitly set roots over + self._roots = surface._roots + else: + self.set_roots(surface.roots()) + + return self + + def add_polygon(self, polygon, *, label=None): + # Overrides add_polygon from MutablePolygonalSurface + label = super().add_polygon(polygon, label=label) + assert label not in self._gluings + self._gluings[label] = [None] * len(polygon.vertices()) + + if self._roots: + self._roots = self._roots + (label,) + + return label + + def remove_polygon(self, label): + # Overrides remove_polygon from MutablePolygonalSurface + self._unglue_polygon(label) + self._gluings.pop(label) + + super().remove_polygon(label) + + def glue(self, x, y): + r""" + Glue ``x`` and ``y`` with an (orientation preserving) similarity. + + INPUT: + + - ``x`` -- a pair consisting of a polygon label and an edge index for + that polygon + + - ``y`` -- a pair consisting of a polygon label and an edge index for + that polygon + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, polygons + + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(polygons.square()) + 0 + + Glue two opposite sides of the square to each other:: + + sage: S.glue((0, 1), (0, 3)) + + Glue the other sides of the square to themselves:: + + sage: S.glue((0, 0), (0, 0)) + sage: S.glue((0, 2), (0, 2)) + + Note that existing gluings are removed when gluing already glued + sides:: + + sage: S.glue((0, 0), (0, 2)) + sage: S.set_immutable() + + sage: S + Translation Surface in H_1(0) built from a square + + """ + if not self._mutable: + raise Exception( + "cannot modify immutable surface; create a copy with MutableOrientedSimilaritySurface.from_surface()" + ) + + if x[0] not in self._polygons: + raise ValueError + + if y[0] not in self._polygons: + raise ValueError + + self.unglue(*x) + self.unglue(*y) + + if self._roots: + component = set(self.component(x[0])) + if y[0] not in component: + # Gluing will join two connected components. + cross_component = set(self.component(y[0])) + for root in reversed(self._roots): + if root in component or root in cross_component: + self._roots = tuple([r for r in self._roots if r != root]) + break + else: + assert False, "did not find any root to eliminate" + + self._gluings[x[0]][x[1]] = y + self._gluings[y[0]][y[1]] = x + + def unglue(self, label, edge): + r""" + Unglue the side ``edge`` of the polygon ``label`` if it is glued. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, translation_surfaces + + sage: T = translation_surfaces.square_torus() + + sage: S = MutableOrientedSimilaritySurface.from_surface(T) + + sage: S.unglue(0, 0) + + sage: S.gluings() + (((0, 1), (0, 3)), ((0, 3), (0, 1))) + + sage: S.set_immutable() + sage: S + Translation Surface with boundary built from a square + + """ + if not self._mutable: + raise Exception( + "cannot modify immutable surface; create a copy with MutableOrientedSimilaritySurface.from_surface()" + ) + + cross = self._gluings[label][edge] + if cross is not None: + self._gluings[cross[0]][cross[1]] = None + + self._gluings[label][edge] = None + + if cross is not None and self._roots: + component = set(self.component(label)) + if cross[0] not in component: + # Ungluing created a new connected component. + cross_component = set(self.component(cross[0])) + assert label not in cross_component + for root in self._roots: + if root in component: + self._roots = self._roots + ( + LabeledView( + surface=self, view=cross_component, finite=True + ).min(), + ) + break + if root in cross_component: + self._roots = self._roots + ( + LabeledView( + surface=self, view=component, finite=True + ).min(), + ) + break + else: + assert False, "did not find any root to split" + + def _unglue_polygon(self, label): + r""" + Remove all gluigns from polygon ``label``. + + This is a helper method to completely unglue a polygon before removing + or replacing it. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, translation_surfaces + + sage: T = translation_surfaces.square_torus() + sage: S = MutableOrientedSimilaritySurface.from_surface(T) + + sage: S._unglue_polygon(0) + sage: S.gluings() + () + + """ + for edge, cross in enumerate(self._gluings[label]): + if cross is None: + continue + cross_label, cross_edge = cross + self._gluings[cross_label][cross_edge] = None + self._gluings[label] = [None] * len(self.polygon(label).vertices()) + + def set_edge_pairing(self, label0, edge0, label1, edge1): + r""" + TESTS:: + + sage: from flatsurf import polygons, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + doctest:warning + ... + UserWarning: set_edge_pairing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead + sage: S.set_edge_pairing(0, 1, 0, 3) + + sage: S.gluings() + (((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1))) + + """ + import warnings + + warnings.warn( + "set_edge_pairing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead" + ) + return self.glue((label0, edge0), (label1, edge1)) + + def change_edge_gluing(self, label0, edge0, label1, edge1): + r""" + TESTS:: + + sage: from flatsurf import polygons, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.change_edge_gluing(0, 0, 0, 2) + doctest:warning + ... + UserWarning: change_edge_gluing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead + sage: S.change_edge_gluing(0, 1, 0, 3) + + sage: S.gluings() + (((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1))) + + """ + import warnings + + warnings.warn( + "change_edge_gluing(label0, edge0, label1, edge1) has been deprecated and will be removed in a future version of sage-flatsurf; use glue((label0, edge0), (label1, edge1)) instead" + ) + return self.glue((label0, edge0), (label1, edge1)) + + def change_polygon_gluings(self, label, gluings): + r""" + TESTS:: + + sage: from flatsurf import polygons, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.change_polygon_gluings(0, [(0, 2), (0, 3), (0, 0), (0, 1)]) + doctest:warning + ... + UserWarning: change_polygon_gluings() has been deprecated and will be removed in a future version of sage-flatsurf; use glue() in a loop instead + + sage: S.gluings() + (((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1))) + + """ + import warnings + + warnings.warn( + "change_polygon_gluings() has been deprecated and will be removed in a future version of sage-flatsurf; use glue() in a loop instead" + ) + + for edge0, cross in enumerate(gluings): + if cross is None: + self.unglue(label, edge0) + else: + self.glue((label, edge0), cross) + + def change_polygon(self, label, polygon, gluing_list=None): + r""" + TESTS:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, translation_surfaces + + sage: T = translation_surfaces.square_torus() + sage: S = MutableOrientedSimilaritySurface.from_surface(T) + + sage: S.change_polygon(0, 2 * S.polygon(0)) + doctest:warning + ... + UserWarning: change_polygon() has been deprecated and will be removed in a future version of sage-flatsurf; use replace_polygon() or remove_polygon() and add_polygon() instead + + sage: S.polygon(0) + Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)]) + + """ + import warnings + + warnings.warn( + "change_polygon() has been deprecated and will be removed in a future version of sage-flatsurf; use replace_polygon() or remove_polygon() and add_polygon() instead" + ) + + if not self._mutable: + raise Exception( + "cannot modify immutable surface; create a copy with MutableOrientedSimilaritySurface.from_surface()" + ) + + # Note that this obscure feature. If the number of edges is unchanged, we keep the gluings, otherwise we trash them all. + if len(polygon.vertices()) != len(self.polygon(label).vertices()): + self._unglue_polygon(label) + self._gluings[label] = [None] * len(polygon.vertices()) + + self._polygons[label] = polygon + + if gluing_list is not None: + for i, cross in enumerate(gluing_list): + self.glue((label, i), cross) + + def replace_polygon(self, label, polygon): + r""" + Replace the polygon ``label`` with ``polygon`` while keeping its + gluings intact. + + INPUT: + + - ``label`` -- an element of :meth:`~.MutablePolygonalSurface.labels` + + - ``polygon`` -- a Euclidean polygon + + EXAMPLES:: + + sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + 0 + sage: S.glue((0, 0), (0, 2)) + sage: S.glue((0, 1), (0, 3)) + + sage: S.replace_polygon(0, Polygon(vertices=[(0, 0), (2, 0), (2, 2), (0, 2)])) + + The replacement of a polygon must have the same number of sides:: + + sage: S.replace_polygon(0, Polygon(vertices=[(0, 0), (2, 0), (2, 2)])) + Traceback (most recent call last): + ... + ValueError: polygon must be a quadrilateral + + To replace the polygon without keeping its glueings, remove the polygon + first and then add a new one:: + + sage: S.remove_polygon(0) + sage: S.add_polygon(Polygon(vertices=[(0, 0), (2, 0), (2, 2)]), label=0) + 0 + + """ + old = self.polygon(label) + + if len(old.vertices()) != len(polygon.vertices()): + from flatsurf.geometry.categories.polygons import Polygons + + article, singular, plural = Polygons._describe_polygon(len(old.vertices())) + raise ValueError(f"polygon must be {article} {singular}") + + self._polygons[label] = polygon + + def opposite_edge(self, label, edge=None): + r""" + Return the edge that ``edge`` of ``label`` is glued to or ``None`` if this edge is unglued. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + INPUT: + + - ``label`` -- one of the labels included in :meth:`~.MutablePolygonalSurface.labels` + + - ``edge`` -- a non-negative integer to specify an edge (the edges + of a polygon are numbered starting from zero.) + + EXAMPLES:: + + sage: from flatsurf import Polygon, MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + 0 + + sage: S.glue((0, 0), (0, 1)) + sage: S.glue((0, 2), (0, 2)) + + sage: S.opposite_edge(0, 0) + (0, 1) + sage: S.opposite_edge(0, 1) + (0, 0) + sage: S.opposite_edge(0, 2) + (0, 2) + sage: S.opposite_edge(0, 3) + + sage: S.opposite_edge((0, 0)) + doctest:warning + ... + UserWarning: calling opposite_edge() with a single argument has been deprecated and will be removed in a future version of sage-flatsurf; use opposite_edge(label, edge) instead + (0, 1) + + """ + if edge is None: + import warnings + + warnings.warn( + "calling opposite_edge() with a single argument has been deprecated and will be removed in a future version of sage-flatsurf; use opposite_edge(label, edge) instead" + ) + label, edge = label + return self._gluings[label][edge] + + def set_vertex_zero(self, label, v, in_place=False): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.set_vertex_zero` + to make it possible to set the zero vertex in-place. + """ + if not in_place: + return super().set_vertex_zero(label, v, in_place=in_place) + + us = self + if not us.is_mutable(): + raise ValueError( + "set_vertex_zero can only be done in_place for a mutable surface." + ) + p = us.polygon(label) + n = len(p.vertices()) + if not (0 <= v < n): + raise ValueError + glue = [] + + from flatsurf import Polygon + + pp = Polygon( + edges=[p.edge((i + v) % n) for i in range(n)], base_ring=us.base_ring() + ) + + for i in range(n): + e = (v + i) % n + ll, ee = us.opposite_edge(label, e) + if ll == label: + ee = (ee + n - v) % n + glue.append((ll, ee)) + + us.remove_polygon(label) + us.add_polygon(pp, label=label) + for e, cross in enumerate(glue): + us.glue((label, e), cross) + return self + + def relabel(self, relabeling_map, in_place=False): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.relabel` + to allow relabeling in-place. + """ + if not in_place: + return super().relabel(relabeling_map=relabeling_map, in_place=in_place) + + us = self + if not isinstance(relabeling_map, dict): + raise NotImplementedError( + "Currently relabeling is only implemented via a dictionary." + ) + domain = set() + codomain = set() + data = {} + for l1, l2 in relabeling_map.items(): + p = us.polygon(l1) + glue = [] + for e in range(len(p.vertices())): + ll, ee = us.opposite_edge(l1, e) + try: + lll = relabeling_map[ll] + except KeyError: + lll = ll + glue.append((lll, ee)) + data[l2] = (p, glue) + domain.add(l1) + codomain.add(l2) + if len(domain) != len(codomain): + raise ValueError( + "The relabeling_map must be injective. Received " + str(relabeling_map) + ) + changed_labels = domain.intersection(codomain) + added_labels = codomain.difference(domain) + removed_labels = domain.difference(codomain) + # Pass to add_polygons + roots = list(us.roots()) + relabel_errors = {} + for l2 in added_labels: + p, glue = data[l2] + l3 = us.add_polygon(p, label=l2) + if not l2 == l3: + # This means the label l2 could not be added for some reason. + # Perhaps the implementation does not support this type of label. + # Or perhaps there is already a polygon with this label. + relabel_errors[l2] = l3 + # Pass to change polygons + for l2 in changed_labels: + p, glue = data[l2] + us.remove_polygon(l2) + us.add_polygon(p, label=l2) + us.replace_polygon(l2, p) + # Deal with the component roots + roots = [relabeling_map.get(label, label) for label in roots] + roots = [relabel_errors.get(label, label) for label in roots] + # Pass to remove polygons: + for l1 in removed_labels: + us.remove_polygon(l1) + # Pass to update the edge gluings + if len(relabel_errors) == 0: + # No problems. Update the gluings. + for l2 in codomain: + p, glue = data[l2] + for e, cross in enumerate(glue): + us.glue((l2, e), cross) + else: + # Use the gluings provided by relabel_errors when necessary + for l2 in codomain: + p, glue = data[l2] + for e in range(len(p.vertices())): + ll, ee = glue[e] + try: + # First try the error dictionary + us.glue((l2, e), (relabel_errors[ll], ee)) + except KeyError: + us.glue((l2, e), (ll, ee)) + us.set_roots(roots) + return self, len(relabel_errors) == 0 + + def join_polygons(self, p1, e1, test=False, in_place=False): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.join_polygons` + to allow joining in-place. + """ + if test: + in_place = False + + if not in_place: + return super().join_polygon(p1, e1, test=test, in_place=in_place) + + poly1 = self.polygon(p1) + p2, e2 = self.opposite_edge(p1, e1) + poly2 = self.polygon(p2) + if p1 == p2: + raise ValueError("Can't glue polygon to itself.") + t = self.edge_transformation(p2, e2) + dt = t.derivative() + es = [] + edge_map = {} # Store the pairs for the old edges. + for i in range(e1): + edge_map[len(es)] = (p1, i) + es.append(poly1.edge(i)) + ne = len(poly2.vertices()) + for i in range(1, ne): + ee = (e2 + i) % ne + edge_map[len(es)] = (p2, ee) + es.append(dt * poly2.edge(ee)) + for i in range(e1 + 1, len(poly1.vertices())): + edge_map[len(es)] = (p1, i) + es.append(poly1.edge(i)) + + from flatsurf import Polygon + + new_polygon = Polygon(edges=es, base_ring=self.base_ring()) + + # Do the gluing. + ss = self + s = ss + + inv_edge_map = {} + for key, value in edge_map.items(): + inv_edge_map[value] = (p1, key) + + glue_list = [] + for i in range(len(es)): + p3, e3 = edge_map[i] + p4, e4 = self.opposite_edge(p3, e3) + if p4 == p1 or p4 == p2: + glue_list.append(inv_edge_map[(p4, e4)]) + else: + glue_list.append((p4, e4)) + + if p2 in s.roots(): + s.set_roots((p1 if label == p2 else label for label in s.roots())) + + s.remove_polygon(p2) + + s.remove_polygon(p1) + s.add_polygon(new_polygon, label=p1) + for e, cross in enumerate(glue_list): + s.glue((p1, e), cross) + + return s + + def subdivide_polygon(self, p, v1, v2, test=False, new_label=None): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.subdivide_polygon` + to allow subdividing in-place. """ - if new_polygon is None: - data = [None, None] + if test: + return super().subdivide_polygon( + p=p, v1=v1, v2=v2, test=test, new_label=new_label + ) + + poly = self.polygon(p) + ne = len(poly.vertices()) + if v1 < 0 or v2 < 0 or v1 >= ne or v2 >= ne: + raise ValueError("Provided vertices out of bounds.") + if abs(v1 - v2) <= 1 or abs(v1 - v2) >= ne - 1: + raise ValueError("Provided diagonal is not actually a diagonal.") + + if v2 < v1: + v2 = v2 + ne + + newedges1 = [poly.vertex(v2) - poly.vertex(v1)] + for i in range(v2, v1 + ne): + newedges1.append(poly.edge(i)) + + from flatsurf import Polygon + + newpoly1 = Polygon(edges=newedges1, base_ring=self.base_ring()) + + newedges2 = [poly.vertex(v1) - poly.vertex(v2)] + for i in range(v1, v2): + newedges2.append(poly.edge(i)) + newpoly2 = Polygon(edges=newedges2, base_ring=self.base_ring()) + + # Store the old gluings + old_gluings = {(p, i): self.opposite_edge(p, i) for i in range(ne)} + + # Update the polygon with label p, add a new polygon. + self.remove_polygon(p) + self.add_polygon(newpoly1, label=p) + if new_label is None: + new_label = self.add_polygon(newpoly2) else: - data = [new_polygon, [None for i in range(new_polygon.num_edges())]] + new_label = self.add_polygon(newpoly2, label=new_label) + # This gluing is the diagonal we used. + self.glue((p, 0), (new_label, 0)) + + # Setup conversion from old to new labels. + old_to_new_labels = {} + for i in range(v1, v2): + old_to_new_labels[(p, i % ne)] = (new_label, i - v1 + 1) + for i in range(v2, ne + v1): + old_to_new_labels[(p, i % ne)] = (p, i - v2 + 1) + + for e in range(1, len(newpoly1.vertices())): + pair = old_gluings[(p, (v2 + e - 1) % ne)] + if pair in old_to_new_labels: + pair = old_to_new_labels[pair] + self.glue((p, e), (pair[0], pair[1])) + + for e in range(1, len(newpoly2.vertices())): + pair = old_gluings[(p, (v1 + e - 1) % ne)] + if pair in old_to_new_labels: + pair = old_to_new_labels[pair] + self.glue((new_label, e), (pair[0], pair[1])) + + def reposition_polygons(self, in_place=False, relabel=None): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.Oriented.ParentMethods.reposition_polygons` + to allow normalizing in-place. + """ + if not in_place: + return super().reposition_polygons(in_place=in_place, relabel=relabel) + + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from reposition_polygon; use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to reposition_polygons()" + ) + + s = self + + labels = list(s.labels()) + from flatsurf.geometry.similarity import SimilarityGroup + + S = SimilarityGroup(self.base_ring()) + identity = S.one() + it = iter(labels) + label = next(it) + changes = {label: identity} + for label in it: + polygon = self.polygon(label) + adjacencies = { + edge: self.opposite_edge(label, edge)[0] + for edge in range(len(polygon.vertices())) + } + edge = min( + adjacencies, + # pylint: disable-next=cell-var-from-loop + key=lambda edge: labels.index(adjacencies[edge]), + ) + label2, edge2 = s.opposite_edge(label, edge) + changes[label] = changes[label2] * s.edge_transformation(label, edge) + it = iter(labels) + # Skip the base label: + label = next(it) + for label in it: + p = s.polygon(label) + p = changes[label].derivative() * p + s.replace_polygon(label, p) + return s + + def triangulate(self, in_place=False, label=None, relabel=None): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.triangulate` + to allow triangulating in-place. + """ + if relabel is not None: + import warnings + + warnings.warn( + "the relabel keyword argument of triangulate() is ignored, it has been deprecated and will be removed in a future version of sage-flatsurf" + ) + + if not in_place: + return super().triangulate(in_place=in_place, label=label) + if label is None: - if len(self._removed_labels) > 0: - new_label = self._removed_labels.pop() - self._p[new_label] = data + # We triangulate the whole surface + # Store the current labels. + labels = [label for label in self.labels()] + s = self + # Subdivide each polygon in turn. + for label in labels: + s = s.triangulate(in_place=True, label=label) + return s + + poly = self.polygon(label) + n = len(poly.vertices()) + if n > 3: + s = self + else: + # This polygon is already a triangle. + return self + from flatsurf.geometry.euclidean import ccw + + for i in range(n - 3): + poly = s.polygon(label) + n = len(poly.vertices()) + for i in range(n): + e1 = poly.edge(i) + e2 = poly.edge((i + 1) % n) + if ccw(e1, e2) != 0: + # This is in case the polygon is a triangle with subdivided edge. + e3 = poly.edge((i + 2) % n) + if ccw(e1 + e2, e3) != 0: + s.subdivide_polygon(label, i, (i + 2) % n) + break + return s + + def delaunay_single_flip(self): + r""" + Perform a single in place flip of a triangulated mutable surface + in-place. + """ + lc = self._label_comparator() + for (l1, e1), (l2, e2) in self.gluings(): + if ( + lc.lt(l1, l2) or (l1 == l2 and e1 <= e2) + ) and self._delaunay_edge_needs_flip(l1, e1): + self.triangle_flip(l1, e1, in_place=True) + return True + return False + + def delaunay_triangulation( + self, + triangulated=False, + in_place=False, + direction=None, + relabel=None, + ): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.delaunay_triangulation` + to allow triangulating in-place. + """ + if not in_place: + return super().delaunay_triangulation( + triangulated=triangulated, + in_place=in_place, + direction=direction, + relabel=relabel, + ) + + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from delaunay_triangulation(); use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to delaunay_triangulation()" + ) + + if triangulated: + s = self + else: + s = self + self.triangulate(in_place=True) + + if direction is None: + direction = (self.base_ring() ** 2)((0, 1)) + + if direction.is_zero(): + raise ValueError + + from collections import deque + + unchecked_labels = deque(s.labels()) + checked_labels = set() + while unchecked_labels: + label = unchecked_labels.popleft() + flipped = False + for edge in range(3): + if s._delaunay_edge_needs_flip(label, edge): + # Record the current opposite edge: + label2, edge2 = s.opposite_edge(label, edge) + # Perform the flip. + s.triangle_flip(label, edge, in_place=True, direction=direction) + # Move the opposite polygon to the list of labels we need to check. + if label2 != label: + try: + checked_labels.remove(label2) + unchecked_labels.append(label2) + except KeyError: + # Occurs if label2 is not in checked_labels + pass + flipped = True + break + if flipped: + unchecked_labels.append(label) + else: + checked_labels.add(label) + return s + + def delaunay_decomposition( + self, + triangulated=False, + delaunay_triangulated=False, + in_place=False, + direction=None, + relabel=None, + ): + r""" + Overrides + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.delaunay_decomposition` + to allow normalizing in-place. + """ + if not in_place: + return super().delaunay_decomposition( + triangulated=triangulated, + delaunay_triangulated=delaunay_triangulated, + in_place=in_place, + direction=direction, + relabel=relabel, + ) + + if relabel is not None: + if relabel: + raise NotImplementedError( + "the relabel keyword has been removed from delaunay_decomposition(); use relabel({old: new for (new, old) in enumerate(surface.labels())}) to use integer labels instead" + ) + else: + import warnings + + warnings.warn( + "the relabel keyword will be removed in a future version of sage-flatsurf; do not pass it explicitly anymore to delaunay_decomposition()" + ) + + s = self + if not delaunay_triangulated: + s = s.delaunay_triangulation( + triangulated=triangulated, + in_place=True, + direction=direction, + relabel=relabel, + ) + + while True: + for (l1, e1), (l2, e2) in s.gluings(): + if s._delaunay_edge_needs_join(l1, e1): + s.join_polygons(l1, e1, in_place=True) + break + else: + return s + + def cmp(self, s2, limit=None): + r""" + Compare two surfaces. This is an ordering returning -1, 0, or 1. + + The surfaces will be considered equal if and only if there is a translation automorphism + respecting the polygons and the root labels. + + If the two surfaces are infinite, we just examine the first limit polygons. + """ + if self.is_finite_type(): + if s2.is_finite_type(): + if limit is not None: + raise ValueError("limit only enabled for finite surfaces") + + sign = len(self.polygons()) - len(s2.polygons()) + if sign > 0: + return 1 + if sign < 0: + return -1 + + lw1 = self.labels() + labels1 = list(lw1) + + lw2 = s2.labels() + labels2 = list(lw2) + + for l1, l2 in zip(lw1, lw2): + ret = self.polygon(l1).cmp(self.polygon(l2)) + if ret != 0: + return ret + + for e in range(len(self.polygon(l1).vertices())): + ll1, e1 = self.opposite_edge(l1, e) + ll2, e2 = s2.opposite_edge(l2, e) + num1 = labels1.index(ll1) + num2 = labels2.index(ll2) + + ret = (num1 > num2) - (num1 < num2) + if ret: + return ret + ret = (e1 > e2) - (e1 < e2) + if ret: + return ret + return 0 else: - new_label = len(self._p) - self._p.append(data) - if self._reference_surface is not None: - # Need a blank in this list for algorithmic reasons - self._int_to_ref.append(None) + # s1 is finite but s2 is infinite. + return -1 else: - new_label = int(label) - if new_label < len(self._p): - if self._p[new_label] is not None: - raise ValueError( - "Trying to add a polygon with label=" - + str(label) - + " which already indexes a polygon." - ) - self._p[new_label] = data + if s2.is_finite_type(): + # s1 is infinite but s2 is finite. + return 1 else: - if new_label - len(self._p) > 100: - raise ValueError( - "Adding a polygon with label=" - + str(label) - + " would add more than 100 entries in our list." - ) - for i in range(len(self._p), new_label): - self._p.append(None) - self._removed_labels.append(i) - if self._reference_surface is not None: - # Need a blank in this list for algorithmic reasons - self._int_to_ref.append(None) - - self._p.append(data) - if self._reference_surface is not None: - # Need a blank in this list for algorithmic reasons - self._int_to_ref.append(None) + if limit is None: + raise NotImplementedError + + # both surfaces are infinite. + from itertools import islice + + lw1 = self.labels() + labels1 = list(islice(lw1, limit)) + + lw2 = s2.labels() + labels2 = list(islice(lw2, limit)) + + count = 0 + for l1, l2 in zip(lw1, lw2): + ret = self.polygon(l1).cmp(s2.polygon(l2)) + if ret != 0: + return ret + + for e in range(len(self.polygon(l1).vertices())): + ll1, ee1 = self.opposite_edge(l1, e) + ll2, ee2 = s2.opposite_edge(l2, e) + num1 = labels1.index(ll1) + num2 = labels2.index(ll2) + ret = (num1 > num2) - (num1 < num2) + if ret: + return ret + ret = (ee1 > ee2) - (ee1 < ee2) + if ret: + return ret + if count >= limit: + break + count += 1 + return 0 + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + + See + :meth:`~.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.ParentMethods._test_eq_surface` + for details on this notion of equality. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: T = MutableOrientedSimilaritySurface(AA) + + sage: S == T + False + + sage: S == S + True + + """ + if not isinstance(other, MutableOrientedSimilaritySurface): + return False + + if not super().__eq__(other): + return False + + if self._gluings != other._gluings: + return False + + return True + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + + sage: S = MutableOrientedSimilaritySurface(QQ) + sage: T = MutableOrientedSimilaritySurface(QQ) + + sage: hash(S) == hash(T) + Traceback (most recent call last): + ... + TypeError: cannot hash a mutable surface + + sage: S.set_immutable() + sage: T.set_immutable() + sage: hash(S) == hash(T) + True + + """ + if self._mutable: + raise TypeError("cannot hash a mutable surface") + + return hash((super().__hash__(), tuple(self.gluings()))) + + +class BaseRingChangedSurface(OrientedSimilaritySurface): + r""" + Changes the ring over which a surface is defined. + + EXAMPLES: + + This class is used in the implementation of + :meth:`flatsurf.geometry.categories.similarity_surfaces.SimilaritySurfaces.Oriented.ParentMethods.change_ring`:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: S = T.change_ring(AA) + + sage: from flatsurf.geometry.surface import BaseRingChangedSurface + sage: isinstance(S, BaseRingChangedSurface) + True + + sage: TestSuite(S).run() + + """ + + def __init__(self, surface, ring, category=None): + if surface.is_mutable(): + raise NotImplementedError("surface must be immutable") + + self._reference = surface + super().__init__(ring, category=category or surface.category()) + + def is_mutable(self): + r""" + Return whether this surface can be modified, i.e., return ``False``. + + This implements + :meth:`flatsurf.geometry.categories.topological_surfaces.TopologicalSurfaces.ParentMethods.is_mutable`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: S = T.change_ring(AA) + + sage: S.is_mutable() + False + + """ + return False + + def labels(self): + r""" + Return the labels of the polygons of this surface. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: S = T.change_ring(AA) + sage: S.labels() + (0,) + + """ + return self._reference.labels() + + def roots(self): + r""" + Return a label for each connected component on this surface. + + This implements :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.roots`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: S = T.change_ring(AA) + + sage: S.roots() + (0,) + + """ + return self._reference.roots() + + def polygon(self, label): + r""" + Return the polygon with ``label``. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.polygon`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: S = T.change_ring(AA) + + sage: p = S.polygon(0) + sage: p.base_ring() + Algebraic Real Field + + """ + return self._reference.polygon(label).change_ring(self.base_ring()) + + def opposite_edge(self, label, edge): + r""" + Return the edge that ``edge`` of ``label`` is glued to or ``None`` if this edge is unglued. + + This implements + :meth:`flatsurf.geometry.categories.polygonal_surfaces.PolygonalSurfaces.ParentMethods.opposite_edge`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: S = T.change_ring(AA) + + sage: S.opposite_edge(0, 0) + (0, 2) + + """ + return self._reference.opposite_edge(label, edge) + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + + See + :meth:`~.categories.similarity_surfaces.SimilaritySurfaces.FiniteType.ParentMethods._test_eq_surface` + for details on this notion of equality. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: T.change_ring(AA) == T.change_ring(AA) + True + + """ + if not isinstance(other, BaseRingChangedSurface): + return False + + return self._reference == other._reference and self.base() == other.base() + + def __hash__(self): + r""" + Return a hash value for this surface that is compatible with + :meth:`__eq__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: T = translation_surfaces.square_torus() + sage: hash(T.change_ring(AA)) == hash(T.change_ring(AA)) + True + + """ + return hash((self._reference, self.base())) + + +class RootedComponents_MutablePolygonalSurface(collections.abc.Mapping): + r""" + Connected components of a :class:`MutablePolygonalSurface`. + + The components are represented as a mapping that maps the root labels to + the labels of the corresponding component. + + This is a helper method for :meth:`MutablePolygonalSurface.components` and + :meth:`MutablePolygonalSurface.roots`. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + + sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface + sage: components = RootedComponents_MutablePolygonalSurface(S) + + """ + + def __init__(self, surface): + self._surface = surface + + def __getitem__(self, root): + r""" + Return the labels of the connected component rooted at the label + ``root``. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + sage: S.glue((0, 0), (1, 0)) + + sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface + sage: components = RootedComponents_MutablePolygonalSurface(S) + sage: components[0] + (0, 1) + + """ + return self._surface.component(root) + + def __iter__(self): + r""" + Iterate over the keys of this mapping, i.e., the root labels of the + connected components. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + sage: S.glue((0, 0), (1, 0)) + + sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface + sage: components = RootedComponents_MutablePolygonalSurface(S) + sage: list(components) + [0] + + """ + # Shortcut enumeration if this is known to be a connected surface. + connected = "Connected" in self._surface.category().axioms() + + for root in self._surface._roots: + yield root + if connected: + return + + labels = set(self._surface._polygons) + for root in self._surface._roots: + for label in self._surface.component(root): + labels.remove(label) + + while labels: + root = LabeledView(surface=self._surface, view=labels, finite=True).min() + + yield root + if connected: + return + for label in self._surface.component(root): + labels.remove(label) + + def __len__(self): + r""" + Return the number of connected components of this surface. + + EXAMPLES:: + + sage: from flatsurf import MutableOrientedSimilaritySurface + sage: S = MutableOrientedSimilaritySurface(QQ) + + sage: from flatsurf import polygons + sage: S.add_polygon(polygons.square()) + 0 + sage: S.add_polygon(polygons.square()) + 1 + sage: S.glue((0, 0), (1, 0)) + + sage: from flatsurf.geometry.surface import RootedComponents_MutablePolygonalSurface + sage: components = RootedComponents_MutablePolygonalSurface(S) + sage: len(components) + 1 + + """ + components = 0 + for root in self: + components += 1 + return components + - if gluing_list is not None: - self.change_polygon_gluings(new_label, gluing_list) - self._num_polygons += 1 +class LabeledCollection: + r""" + Abstract base class for collection of labels as returned by ``labels()`` + methods of surfaces. - return new_label + This also serves as a base clas for things such as ``polygons()`` that are + tied to labels. - def num_polygons(self): - r""" - Return the number of polygons making up the surface in constant time. - """ - return self._num_polygons + INPUT: - def label_iterator(self): - r""" - Iterator over all polygon labels. - """ - if self._reference_surface is not None: - for i in Surface.label_iterator(self): - yield i - elif self._num_polygons == len(self._p): - for i in range(self.num_polygons()): - yield i - else: - # We've removed some labels - found = 0 - i = 0 - while found < self._num_polygons: - if self._p[i] is not None: - found += 1 - yield i - i += 1 + - ``surface`` -- a polygonal surface, the labels are taken from that + surface; subclasses might change this to only represent a subset of the + labels of this surface - def _remove_polygon(self, label): - r""" - Internal method used by remove_polygon(). Should not be called directly. - """ - if label == len(self._p) - 1: - self._p.pop() - if self._reference_surface is not None: - ref_label = self._int_to_ref.pop() - assert len(self._int_to_ref) == label - if ref_label is not None: - del self._ref_to_int[ref_label] - else: - self._p[label] = None - self._removed_labels.append(label) - if self._reference_surface is not None: - ref_label = self._int_to_ref[label] - if ref_label is not None: - self._int_to_ref[label] = None - del self._ref_to_int[ref_label] - self._num_polygons -= 1 - - def ramified_cover(self, d, data): + - ``finite`` -- a boolean or ``None`` (default: ``None``); whether this is + a finite set; if ``None``, it is not known whether the set is finite + (some operations might not be supported in that case or not terminate if + the set is actually infinite.) + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: labels = S.labels() + + sage: from flatsurf.geometry.surface import LabeledCollection + sage: isinstance(labels, LabeledCollection) + True + + """ + + def __init__(self, surface, finite=None): + if finite is None and surface.is_finite_type(): + finite = True + + self._surface = surface + self._finite = finite + + def __repr__(self): r""" - Make a ramified cover of this surface. + Return a printable representation of this set. - INPUT: + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: S.labels() + (0,) - - ``d`` - integer (the degree of the cover) + sage: S = translation_surfaces.infinite_staircase() + sage: S.labels() + (0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, …) - - ``data`` - a dictionary which to a pair ``(label, edge_num)`` associates a permutation - of {1,...,d} """ - from sage.groups.perm_gps.permgroup_named import SymmetricGroup - - G = SymmetricGroup(d) - for k in data: - data[k] = G(data[k]) - cover = Surface_list(base_ring=self.base_ring()) - labels = list(self.label_iterator()) - edges = set(self.edge_iterator()) - cover_labels = {} - for i in range(1, d + 1): - for lab in self.label_iterator(): - cover_labels[(lab, i)] = cover.add_polygon(self.polygon(lab)) - while edges: - lab, e = elab = edges.pop() - llab, ee = eelab = self.opposite_edge(lab, e) - edges.remove(eelab) - if elab in data: - if eelab in data: - if not (data[elab] * data[eelab]).is_one(): - raise ValueError("inconsistent covering data") - s = data[elab] - elif eelab in data: - s = ~data[eelab] - else: - s = G.one() + from itertools import islice - for i in range(1, d + 1): - p0 = cover_labels[(lab, i)] - p1 = cover_labels[(lab, s(i))] - cover.set_edge_pairing(p0, e, p1, ee) - return cover + items = list(islice(self, 17)) - def __eq__(self, other): + if self._finite is True or len(items) < 17: + return repr(tuple(self)) + + return f"({', '.join(str(x) for x in islice(self, 16))}, …)" + + def __len__(self): r""" - Return whether this surface is indistinguishable from ``other``. + Return the size of this set. EXAMPLES:: - sage: from flatsurf import Surface_list, polygons - sage: P=polygons.regular_ngon(5) - sage: S = Surface_list(base_ring=P.base_ring()) - sage: T = Surface_list(base_ring=P.base_ring()) - - sage: S == T - True + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: len(S.labels()) + 1 - sage: S.add_polygon(P, label=3) - 3 + Python does not allow ``__len__`` to return anything but an integer, so + we cannot return infinity:: - sage: S == T - False + sage: S = translation_surfaces.infinite_staircase() + sage: len(S.labels()) + Traceback (most recent call last): + ... + NotImplementedError: len() of an infinite set """ - if not isinstance(other, Surface_list): - return False + if self._finite is False: + raise TypeError("infinite set has no integer length") - if self._reference_surface is not None: - equal = self._eq_reference_surface(other) - if equal is True: - return True - if equal is False: - return False + length = 0 + for x in self: # pylint: disable=not-an-iterable + length += 1 - return super().__eq__(other) + return length - def _eq_reference_surface(self, other): + def __contains__(self, x): r""" - Return whether this surface is indistinguishable from ``other`` by - comparing their reference surfaces. + Return whether ``x`` is contained in this set. + + EXAMPLES:: - Returns ``None``, when no conclusion could be reached. + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: labels = S.labels() + sage: 0 in labels + True + sage: 1 in labels + False - This is a helper method for :meth:`__eq__`. """ - if self._reference_surface != other._reference_surface: - return None + for item in self: # pylint: disable=not-an-iterable + if x == item: + return True - for label in range(len(self._p)): - if self._p[label] is None: - if label >= len(other._p) or other._p[label] is not None: - return None - continue - try: - if self.polygon(label) != other.polygon(label): - return False - except ValueError: - return False - - for label in range(len(other._p)): - if other._p[label] is None: - if label >= len(self._p) or self._p[label] is not None: - return None - continue - try: - if self.polygon(label) != other.polygon(label): - return False - except ValueError: - return False + return False - if self.base_ring() != other.base_ring(): - return False - if self.is_mutable() != other.is_mutable(): - return False - if self.base_label() != other.base_label(): - return False - return True +class LabeledView(LabeledCollection): + r""" + A set of labels (or something resembling labels such as ``polygons()``) + backed by another collection ``view``. + INPUT: + + - ``surface`` -- a polygonal surface, the labels in ``view`` are labels of + that surface + + - ``view`` -- a collection that all queries are going to be redirected to. + Note that ``labels()`` guarantees that iteration over labels happens in a + breadth-first-search so iteration over ``view`` must follow that same + order. However, subclasses can remove this requirement by overriding + :meth:`__iter__`. + + - ``finite`` -- a boolean or ``None`` (default: ``None``); whether this is + a finite set; if ``None``, it is not known whether the set is finite + (some operations might not be supported in that case or not terminate if + the set is actually infinite.) + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.t_fractal() + sage: labels = S.labels() + + sage: from flatsurf.geometry.surface import LabeledView + sage: isinstance(labels, LabeledView) + True -def surface_list_from_polygons_and_gluings(polygons, gluings, mutable=False): - r""" - Take a list of polygons and gluings (given either as a list of pairs of edges, or as a dictionary), - and produce a Surface_list from it. The mutable parameter determines the mutability of the resulting - surface. """ - if not (isinstance(polygons, list) or isinstance(polygons, tuple)): - raise ValueError("polygons must be a list or tuple.") - field = polygons[0].parent().field() - s = Surface_list(base_ring=field) - for p in polygons: - s.add_polygon(p) - try: - # dict case: - it = gluings.items() - except AttributeError: - # list case: - it = gluings - for (l1, e1), (l2, e2) in it: - s.change_edge_gluing(l1, e1, l2, e2) - if not mutable: - s.set_immutable() - return s - - -class Surface_dict(Surface): - r""" - A mutable :class:`Surface` using a dict to store polygons and gluings. - Unlike :class:`Surface_list`, this surface is not limited to integer - labels. However, :class:`Surface_list` is likely more efficient for most - applications. + def __init__(self, surface, view, finite=None): + super().__init__(surface, finite=finite) + self._view = view - ALGORITHM: + def __iter__(self): + return iter(self._view) - Internally, we maintain a dict ``_p`` for storing polygons together with - gluing data. + def __contains__(self, x): + return x in self._view - Each ``_p[label]`` is typically a pair ``(polygon, gluing_dict)`` where - ``gluing_dict`` is maps ``other_label`` to ``other_edge`` such that - :meth:`opposite_edge(label, edge) ` returns - ``_p[label][1][edge]``. + def __len__(self): + return len(self._view) - INPUT: + def min(self): + r""" + Return a minimal item in this set. - - ``base_ring`` -- ring or ``None`` (default: ``None``); the ring - containing the coordinates of the vertices of the polygons. If ``None``, - the :meth:`Surface.base_ring` will be the one of ``surface``. + If the items can be compared, this is just the actual ``min`` of the + items. - - ``surface`` -- :class:`Surface`, :class:`.similarity_surface.SimilaritySurface`, or - ``None`` (default: ``None``); a surface to be copied or referenced (see - ``copy``). If ``None``, the surface is initially empty. + Otherwise, we take the one with minimal ``repr``. - - ``copy`` -- boolean or ``None`` (default: ``None``); whether the data - underlying ``surface`` is copied into this surface or just a reference to - that surface is kept. If ``None``, a sensible default is chosen, namely - ``surface.is_mutable()``. + .. NOTE:: - - ``mutable`` -- boolean or ``None`` (default: ``None``); whether this - surface is mutable. When ``None``, the surface will be mutable iff - ``surface`` is ``None``. + If the items cannot be compared, and there are clashes in the + ``repr``, this method will fail. - EXAMPLES:: + Also, if comparison of items is not consistent, then this can + produce somewhat random output. - sage: from flatsurf import * - sage: p=polygons.regular_ngon(10) - sage: s=Surface_dict(base_ring=p.base_ring()) - sage: s.add_polygon(p,label="A") - 'A' - sage: s.change_polygon_gluings("A",[("A",(e+5)%10) for e in range(10)]) - sage: s.change_base_label("A") - sage: s.set_immutable() - sage: TestSuite(s).run() - """ + Finally, note with this approach the min of a set is not the always + the min of the mins of a each partition of that set. - def __init__(self, base_ring=None, surface=None, copy=None, mutable=None): - self._p = {} - self._reference_surface = None - - # Validate input parameters and fill in defaults - ( - base_ring, - surface, - copy, - mutable, - finite, - ) = Surface_list._validate_init_parameters( - base_ring=base_ring, - surface=surface, - copy=copy, - mutable=mutable, - finite=None, - ) + EXAMPLES:: - Surface.__init__(self, base_ring, base_label=None, finite=finite, mutable=True) - - # Initialize surface from reference surface - if surface is not None: - if copy is True: - reference_label_to_label = { - label: self.add_polygon(polygon, label=label) - for label, polygon in surface.label_polygon_iterator() - } - - for ( - (label, edge), - (glued_label, glued_edge), - ) in surface.edge_gluing_iterator(): - self.set_edge_pairing( - reference_label_to_label[label], - edge, - reference_label_to_label[glued_label], - glued_edge, - ) - - self.change_base_label(reference_label_to_label[surface.base_label()]) - else: - self._reference_surface = surface + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.t_fractal() + sage: S.labels().min() + Traceback (most recent call last): + ... + NotImplementedError: cannot determine minimum of an infinite set - self.change_base_label(surface.base_label()) + :: - if not mutable: - self.set_immutable() + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1,2) + sage: C.labels().min() + 0 - def polygon(self, lab): - r""" - Return the polygon with label ``lab``. - """ - try: - data = self._p[lab] - except KeyError: - if self._reference_surface is None: - raise ValueError(f"No polygon with label {lab}.") - - polygon = self._reference_surface.polygon(lab) - data = [ - self._reference_surface.polygon(lab), - [ - self._reference_surface.opposite_edge(lab, e) - for e in range(polygon.num_edges()) - ], - ] - self._p[lab] = data - - if data is None: - raise ValueError("Label " + str(lab) + " was removed from the surface.") - return data[0] - - def opposite_edge(self, p, e): - r""" - Given the label ``p`` of a polygon and an edge ``e`` in that polygon - returns the pair (``pp``, ``ee``) to which this edge is glued. - """ - try: - data = self._p[p] - except KeyError: - self.polygon(p) - data = self._p[p] - if data is None: - raise ValueError("Label " + str(p) + " was removed from the surface.") - gluing_data = data[1] - try: - return gluing_data[e] - except IndexError: - raise ValueError( - "Edge e=" + str(e) + " is out of range in polygon with label " + str(p) - ) + :: - # Methods for changing the surface + sage: labels = list(C.labels())[:3] + sage: from flatsurf.geometry.surface import LabeledView + sage: LabeledView(C, labels).min() + 0 - def _change_polygon(self, label, new_polygon, gluing_list=None): - r""" - Internal method used by change_polygon(). Should not be called directly. """ - try: - data = self._p[label] - if data is None: - raise ValueError( - "Label " + str(label) + " was removed from the surface." - ) - data[0] = new_polygon - except KeyError: - # Polygon probably lies in reference surface - if self._reference_surface is None: - raise ValueError("No known polygon with provided label") - else: - # Ensure the reference surface had a polygon with the provided label: - old_polygon = self._reference_surface.polygon(label) - if old_polygon.num_edges() == new_polygon.num_edges(): - data = [ - new_polygon, - [ - self._reference_surface.opposite_edge(label, e) - for e in range(new_polygon.num_edges()) - ], - ] - self._p[label] = data - else: - data = [new_polygon, [None for e in range(new_polygon.num_edges())]] - self._p[label] = data - if len(data[1]) != new_polygon.num_edges(): - data[1] = [None for e in range(new_polygon.num_edges())] - if gluing_list is not None: - self.change_polygon_gluings(label, gluing_list) + if self._finite is False: + raise NotImplementedError("cannot determine minimum of an infinite set") - def _set_edge_pairing(self, label1, edge1, label2, edge2): - r""" - Internal method used by set_edge_pairing(). Should not be called directly. - """ try: - data = self._p[label1] - except KeyError: - if self._reference_surface is None: - raise ValueError( - "No known polygon with provided label1 = {}".format(label1) - ) - else: - # Failure likely because reference_surface contains the polygon. - # import the data into this surface. - polygon = self._reference_surface.polygon(label1) - data = [ - polygon, - [ - self._reference_surface.opposite_edge(label1, e) - for e in range(polygon.num_edges()) - ], - ] - self._p[label1] = data - try: - data[1][edge1] = (label2, edge2) - except IndexError: - # break down error - if data is None: - raise ValueError("polygon with label1={} was removed".format(label1)) - data1 = data[1] - try: - data1[edge1] = (label2, edge2) - except IndexError: - raise ValueError( - "edge1={} is out of range in polygon with label1={}".format( - edge1, label1 - ) - ) - try: - data = self._p[label2] - except KeyError: - if self._reference_surface is None: - raise ValueError("no polygon with label2={}".format(label2)) - else: - # Failure likely because reference_surface contains the polygon. - # import the data into this surface. - polygon = self._reference_surface.polygon(label2) - data = [ - polygon, - [ - self._reference_surface.opposite_edge(label2, e) - for e in range(polygon.num_edges()) - ], - ] - self._p[label2] = data - try: - data[1][edge2] = (label1, edge1) - except IndexError: - # break down error - if data is None: - raise ValueError("polygon with label1={} was removed".format(label1)) - data1 = data[1] - try: - data1[edge2] = (label1, edge1) - except IndexError: - raise ValueError( - "edge {} is out of range in polygon with label2={}".format( - edge2, label2 - ) + return min(self) + except TypeError: + reprs = {repr(item): item for item in self} + if len(reprs) != len(self): + raise TypeError( + "cannot determine minimum of tset without ordering and with non-unique repr()" ) + return reprs[min(reprs)] - _change_edge_gluing = _set_edge_pairing - def _add_polygon(self, new_polygon, gluing_list=None, label=None): - r""" - Internal method used by add_polygon(). Should not be called directly. - """ - data = [new_polygon, [None for i in range(new_polygon.num_edges())]] - if label is None: - new_label = ExtraLabel() - else: - try: - old_data = self._p[label] - if old_data is None: - # already removed this polygon. That's good, we can add. - new_label = label - else: - raise ValueError( - "label={} already used by another polygon".format(label) - ) - except KeyError: - # This seems inconvenient to enforce: - # - # if not self._reference_surface is None: - # # Can not be sure we are not overwriting a polygon in the reference surface. - # raise ValueError("Can not assign this label to a Surface_dict containing a reference surface,"+\ - # "which may already contain this label.") - new_label = label - self._p[new_label] = data - if gluing_list is not None: - self.change_polygon_gluings(new_label, gluing_list) - return new_label +class ComponentLabels(LabeledCollection): + r""" + The labels of a connected component. - def num_polygons(self): - r""" - Return the number of polygons making up the surface in constant time. - """ - if self.is_finite(): - if self._reference_surface is None: - return len(self._p) - else: - # Unfortunately, I don't see a good way to compute this. - return Surface.num_polygons(self) - else: - from sage.rings.infinity import Infinity + INPUT: - return Infinity + - ``surface`` -- a polygonal surface - def label_iterator(self): - r""" - Iterator over all polygon labels. - """ - if self._reference_surface is None: - for i in self._p: - yield i - else: - for i in Surface.label_iterator(self): - yield i + - ``root`` -- a label of the connected component from which enumeration of + the component starts. - def _remove_polygon(self, label): - r""" - Internal method used by remove_polygon(). Should not be called directly. - """ - if self._reference_surface is None: - try: - data = self._p[label] - except KeyError: - raise ValueError("Label " + str(label) + " is not in the surface.") - del self._p[label] - else: - try: - data = self._p[label] - # Success. - if data is None: - raise ValueError( - "Label " + str(label) + " was already removed from the surface." - ) - self._p[label] = None - except KeyError: - # Assume on faith we are removing a polygon in the base_surface. - self._p[label] = None + - ``finite`` -- a boolean or ``None`` (default: ``None``); whether this is + a finite component; if ``None``, it is not known whether the component is + finite (some operations might not be supported in that case or not + terminate if the component is actually infinite.) - def __eq__(self, other): + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.t_fractal() + sage: component = S.component(0) + + sage: from flatsurf.geometry.surface import ComponentLabels + sage: isinstance(component, ComponentLabels) + True + + """ + + def __init__(self, surface, root, finite=None): + super().__init__(surface, finite=finite) + self._root = root + + def __iter__(self): r""" - Return whether this surface is indistinguishable from ``other``. + Return an iterator of this component that enumerates labels starting + from the root label in a breadth-first-search. EXAMPLES:: - sage: from flatsurf import Surface_dict, polygons - sage: P=polygons.regular_ngon(5) - sage: S = Surface_dict(base_ring=P.base_ring()) - sage: T = Surface_dict(base_ring=P.base_ring()) + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: component = C.component(0) + sage: list(component) + [0, 1, 3, 2] - sage: S == T - True + """ + from collections import deque - sage: S.add_polygon(P, label=3) - 3 + seen = set() + pending = deque([self._root]) - sage: S == T - False + while pending: + label = pending.popleft() + if label in seen: + continue - """ - if not isinstance(other, Surface_dict): - return False + seen.add(label) - if self._reference_surface is not None: - equal = self._eq_reference_surface(other) - if equal is True: - return True - if equal is False: - return False + yield label + for e in range(len(self._surface.polygon(label).vertices())): + cross = self._surface.opposite_edge(label, e) + if cross is not None: + pending.append(cross[0]) - return super().__eq__(other) - def _eq_reference_surface(self, other): - r""" - Return whether this surface is indistinguishable from ``other`` by - comparing their reference surfaces. +class Labels(LabeledCollection, collections.abc.Set): + r""" + The labels of a surface. - Returns ``None``, when no conclusion could be reached. + .. NOTE:: - This is a helper method for :meth:`__eq__`. - """ - if self._reference_surface != other._reference_surface: - return None + This is a generic implementation that represents the set of labels of a + surface in a breadth-first iteration starting from the root labels of the + connected components. - for label, polygon in self._p.items(): - if polygon is None: - if label not in other._p or other._p[label] is not None: - return None - continue - try: - if self.polygon(label) != other.polygon(label): - return False - except ValueError: - return False - - for label, polygon in other._p.items(): - if polygon is None: - if label not in self._p or self._p[label] is not None: - return None - continue - try: - if self.polygon(label) != other.polygon(label): - return False - except ValueError: - return False + This implementation makes no assumption on the surface and can be very + slow to answer, e.g., containment or compute the number of labels in + the surface (because it needs to iterate over the entire surface.) - if self.base_ring() != other.base_ring(): - return False - if self.is_mutable() != other.is_mutable(): - return False - if self.base_label() != other.base_label(): - return False + When possible, a faster implementation should be used such as + :class:`LabelsFromView`. - return True + EXAMPLES:: + sage: from flatsurf import polygons, similarity_surfaces + sage: T = polygons.triangle(1, 2, 5) + sage: S = similarity_surfaces.billiard(T) + sage: S = S.minimal_cover("translation") -class BaseRingChangedSurface(Surface): - r""" - A surface with a different base_ring. - """ + sage: labels = S.labels() + sage: labels + ((0, 1, 0), (1, 1, 0), (1, 0, -1), (1, 1/2*c0, 1/2*c0), (0, 1/2*c0, -1/2*c0), (0, 0, 1), (0, -1/2*c0, -1/2*c0), (0, 0, -1), (0, -1/2*c0, 1/2*c0), (0, 1/2*c0, 1/2*c0), + (1, 1/2*c0, -1/2*c0), (1, -1/2*c0, -1/2*c0), (1, 0, 1), (1, -1/2*c0, 1/2*c0), (1, -1, 0), (0, -1, 0)) - def __init__(self, surface, ring): - self._s = surface - self._base_ring = ring - from flatsurf.geometry.polygon import ConvexPolygons + TESTS:: - self._P = ConvexPolygons(self._base_ring) - Surface.__init__( - self, ring, self._s.base_label(), mutable=False, finite=self._s.is_finite() - ) + sage: from flatsurf.geometry.surface import Labels + sage: type(labels) == Labels + True - def polygon(self, lab): - return self._P(self._s.polygon(lab)) + """ - def opposite_edge(self, p, e): - return self._s.opposite_edge(p, e) + def __iter__(self): + for component in self._surface.components(): + for label in component: + yield label -class LabelWalker: +class LabelsFromView(Labels, LabeledView): r""" - Take a canonical walk around the surface and find the labels of polygons. + The labels of a surface backed by another set that can quickly compute the + length of the labels and decide containment in the set. + + .. NOTE:: + + Iteration of the view collection does not have to be in breadth-first + search order in the surface since this class is picking up the generic + :meth:`Labels.__iter__`. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: labels = C.labels() + + sage: from flatsurf.geometry.surface import LabelsFromView + sage: type(labels) == LabelsFromView + True - We start at the base_label(). - Then the labels are visited in order involving the combinatorial distance from the base_label(), - where combinatorial distance measures the minimal number of edges which need to be crossed to reach the - polygon with a givel label. Ties are broken using lexigraphical order on the numbers associated to edges crossed - (labels are not used in this lexigraphical ordering). """ - class LabelWalkerIterator: - def __init__(self, label_walker): - self._lw = label_walker - self._i = 0 - - def __next__(self): - if self._i < len(self._lw): - label = self._lw.number_to_label(self._i) - self._i = self._i + 1 - return label - if self._i == len(self._lw): - label = self._lw.find_a_new_label() - if label is None: - raise StopIteration() - self._i = self._i + 1 - return label - raise StopIteration() - - next = __next__ # for Python 2 - - def __iter__(self): - return self - def __init__(self, surface): - self._s = surface - self._labels = [self._s.base_label()] - self._label_dict = {self._s.base_label(): 0} +class Polygons(LabeledCollection, collections.abc.Collection): + r""" + The collection of polygons of a surface. - # This will stores an edge to move through to get to a polygon closer to the base_polygon - self._label_edge_back = {self._s.base_label(): None} + The polygons are returned in the same order as labels of the surface are + returned by :class:`.Labels`. - self._walk = deque() - self._walk.append((self._s.base_label(), 0)) + EXAMPLES:: - def label_dictionary(self): - r""" - Return a dictionary mapping labels to integers which gives a canonical order on labels. - """ - return self._label_dict + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: polygons = C.polygons() - def edge_back(self, label, limit=None): - r""" - Return the "canonical" edge to walk through to get closer to the base_label, - or None if label already is the base_label. + sage: from flatsurf.geometry.surface import Polygons + sage: isinstance(polygons, Polygons) + True - Remark: This could be slow on infinite surfaces! - """ - try: - return self._label_edge_back[label] - except KeyError: - if limit is None: - if not self._s.is_finite(): - limit = 1000 - else: - limit = self._s.num_polygons() - for i in range(limit): - new_label = self.find_a_new_label() - if label == new_label: - return self._label_edge_back[label] - # Maybe the surface is not connected? - raise KeyError( - "Unable to find label %s. Are you sure the surface is connected?" % (label) - ) + """ def __iter__(self): - return LabelWalker.LabelWalkerIterator(self) + r""" + Iterate over the polygons in the same order as ``labels()`` does. - def polygon_iterator(self): - for label in self: - yield self._s.polygon(label) + EXAMPLES:: - def label_polygon_iterator(self): - for label in self: - yield label, self._s.polygon(label) + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: labels = C.labels() + sage: polygons = C.polygons() - def edge_iterator(self): - for label, polygon in self.label_polygon_iterator(): - for e in range(polygon.num_edges()): - yield label, e + sage: for entry in zip(labels, polygons): + ....: print(entry) + (0, Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + (1, Polygon(vertices=[(1, 0), (1, -2), (3/2, -5/2), (2, -2), (2, 0), (2, 1), (2, 3), (3/2, 7/2), (1, 3), (1, 1)])) + (3, Polygon(vertices=[(3, 0), (7/2, -1/2), (11/2, -1/2), (6, 0), (6, 1), (11/2, 3/2), (7/2, 3/2), (3, 1)])) + (2, Polygon(vertices=[(2, 0), (3, 0), (3, 1), (2, 1)])) - def __len__(self): - r""" - Return the number of labels found. """ - return len(self._labels) + for label in self._surface.labels(): + yield self._surface.polygon(label) - def find_a_new_label(self): - r""" - Finds a new label, stores it, and returns it. Returns None if we have already found all labels. - """ - while len(self._walk) > 0: - label, e = self._walk.popleft() - opposite_label, opposite_edge = self._s.opposite_edge(label, e) - e = e + 1 - if e < self._s.polygon(label).num_edges(): - self._walk.appendleft((label, e)) - if opposite_label not in self._label_dict: - n = len(self._labels) - self._labels.append(opposite_label) - self._label_dict[opposite_label] = n - self._walk.append((opposite_label, 0)) - self._label_edge_back[opposite_label] = opposite_edge - return opposite_label - return None - - def find_new_labels(self, n): + def __len__(self): r""" - Look for n new labels. Return the list of labels found. - """ - new_labels = [] - for i in range(n): - label = self.find_a_new_label() - if label is None: - return new_labels - else: - new_labels.append(label) - return new_labels + Return the number of polygons in this surface. + + EXAMPLES:: - def find_all_labels(self): - if not self._s.is_finite(): - raise NotImplementedError - label = self.find_a_new_label() - while label is not None: - label = self.find_a_new_label() + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: polygons = C.polygons() + sage: len(polygons) + 4 - def number_to_label(self, n): - r""" - Return the n-th label where n is less than the length. """ - return self._labels[n] + return len(self._surface.labels()) - def label_to_number(self, label, search=False, limit=100): - r""" - Return the number associated to the provided label. - Returns an error if the label has not already been found by the walker - unless search=True in which case we look for the label. We look by - continuing to look for the label by walking over the surface visiting - the next limit many polygons. - """ - if not search: - return self._label_dict[label] - else: - if label in self._label_dict: - return self._label_dict[label] - for i in range(limit): - new_label = self.find_a_new_label() - if label == new_label: - return self._label_dict[label] - raise ValueError( - "Failed to find label even after searching. limit=" + str(limit) - ) +class Polygons_MutableOrientedSimilaritySurface(Polygons): + r""" + The collection of polygons of a :class:`MutableOrientedSimilaritySurface`. - def surface(self): - return self._s + This is a faster version of :class:`Polygons`. + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: polygons = C.polygons() + + sage: from flatsurf.geometry.surface import Polygons_MutableOrientedSimilaritySurface + sage: isinstance(polygons, Polygons_MutableOrientedSimilaritySurface) + True -class ExtraLabel: - r""" - Used to spit out new labels. """ - _next = int(0) - def __init__(self, value=None): - r""" - Construct a new label. - """ - if value is None: - self._label = int(ExtraLabel._next) - ExtraLabel._next = ExtraLabel._next + 1 - else: - self._label = value + def __init__(self, surface): + # This hack makes __len__ 20% faster (it saves one attribute lookup.) + self._polygons = surface._polygons + super().__init__(surface) - def __eq__(self, other): - return isinstance(other, self.__class__) and self._label == other._label + def __len__(self): + return len(self._polygons) - def __ne__(self, other): - return not self.__eq__(other) - def __hash__(self): - return hash(23 * self._label) +class Edges(LabeledCollection, collections.abc.Set): + r""" + The set of edges of a surface. - def __str__(self): - return "E" + str(self._label) + The set of edges contains of pairs (label, index) with the labels of the + polygons and the actual edges indexed from 0 in the second component. - def __repr__(self): - return "ExtraLabel(" + str(self._label) + ")" + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: C = translation_surfaces.cathedral(1, 2) + sage: edges = C.edges() + sage: edges + ((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (2, 0), (2, 1), (2, 2), (2, 3)) + TESTS:: -class LabelComparator(object): + sage: from flatsurf.geometry.surface import Edges + sage: isinstance(edges, Edges) + True + + """ + + def __iter__(self): + for label, polygon in zip(self._surface.labels(), self._surface.polygons()): + for edge in range(len(polygon.vertices())): + yield (label, edge) + + def __contains__(self, x): + label, edge = x + if label not in self._surface.labels(): + return False + + polygon = self._surface.polygon(label) + return 0 <= len(polygon.vertices()) < edge + + +class Gluings(LabeledCollection, collections.abc.Set): r""" - Implements a total ordering on labels, which may be of varying types. + The set of gluings of the surface. + + Each gluing consists of two pairs (label, index) that describe the edges + being glued. + + Note that each gluing (that is not a self-gluing) is reported twice. + + EXAMPLES:: + + sage: from flatsurf import translation_surfaces + sage: S = translation_surfaces.square_torus() + sage: gluings = S.gluings() + sage: gluings + (((0, 0), (0, 2)), ((0, 1), (0, 3)), ((0, 2), (0, 0)), ((0, 3), (0, 1))) + + TESTS:: - We use hashes, so if hash(label1) < hash(label2) we declare label1 < label2. + sage: from flatsurf.geometry.surface import Gluings + sage: isinstance(gluings, Gluings) + True - For objects with the same hash, we store an arbitrary ordering. """ - def __init__(self): - r""" - Initialize a label comparator. - """ - self._hash_collision_resolver = {} + def __iter__(self): + for label, edge in self._surface.edges(): + cross = self._surface.opposite_edge(label, edge) + if cross is None: + continue + yield (label, edge), cross - def _get_resolver_index(self, label_hash, label): - try: - lst = self._hash_collision_resolver[label_hash] - except KeyError: - lst = [] - self._hash_collision_resolver[label_hash] = lst - for i, stored_label in enumerate(lst): - if label == stored_label: - return i - # At this point we know label is not in lst - lst.append(label) - return len(lst) - 1 - - def lt(self, l1, l2): - r""" - Return the truth value of l1 < l2. - """ - h1 = hash(l1) - h2 = hash(l2) - if h1 < h2: - return True - if h1 > h2: + def __contains__(self, x): + x, y = x + + if x not in self._surface.edges(): return False - # Otherwise the hashes are equal. - if l1 == l2: + + cross = self._surface.opposite_edge(*x) + if cross is None: return False - return self._get_resolver_index(h1, l1) < self._get_resolver_index(h1, l2) - def le(self, l1, l2): - r""" - Return the truth value of l1 <= l2. - """ - return self.lt(l1, l2) or l1 == l2 + return y == cross - def gt(self, l1, l2): - r""" - Return the truth value of l1 > l2. - """ - return self.lt(l2, l1) - def ge(self, l1, l2): - r""" - Return the truth value of l1 >= l2. - r""" - return self.lt(l2, l1) or l1 == l2 +# Import deprecated symbols so imports using flatsurf.geometry.surface do not break. +from flatsurf.geometry.surface_legacy import ( # noqa, we import at the bottom of the file to break a circular import # pylint: disable=wrong-import-position + Surface, + Surface_list, + Surface_dict, + surface_list_from_polygons_and_gluings, +) diff --git a/flatsurf/geometry/surface_legacy.py b/flatsurf/geometry/surface_legacy.py new file mode 100644 index 000000000..4b459015d --- /dev/null +++ b/flatsurf/geometry/surface_legacy.py @@ -0,0 +1,2489 @@ +r""" +Legacy data structures for surfaces built from polygons. + +All surfaces in sage-flatsurf are built from polygons whose sides are +identified by similarities. This module provides deprecated data structures to +describe such surfaces. Currently, there are two fundamental such data +structures, namely :class:`Surface_list` and `Surface_dict`. The former labels +the polygons that make up a surface by non-negative integers and the latter can +use arbitrary labels. + +In principle there is nothing wrong with this approach. However, the +implementation is plagued by feature creep so we tried to clean things up in +2023 and reimplemented these in :mod:`flatsurf.geometry.surface`. + +All the surfaces here inherit from :class:`Surface` which describes the +contract that surfaces used to satisfy. As an absolute minimum, they implement +:meth:`Surface.polygon` which maps polygon labels to actual polygons, and +:meth:`Surface.opposite_edge` which describes the gluing of polygons. + +EXAMPLES: + +We built a torus by gluing the opposite sides of a square:: + + sage: from flatsurf import Polygon + sage: from flatsurf.geometry.surface import Surface_list + + sage: S = Surface_list(QQ) + doctest:warning + ... + UserWarning: Surface_list has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + + sage: S.polygon(0) + Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)]) + sage: S.opposite_edge(0, 0) + (0, 2) + +""" +# ******************************************************************** +# This file is part of sage-flatsurf. +# +# Copyright (C) 2016-2020 W. Patrick Hooper +# 2019-2020 Vincent Delecroix +# 2020-2023 Julian Rüth +# +# sage-flatsurf is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# sage-flatsurf is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sage-flatsurf. If not, see . +# ******************************************************************** +import collections.abc + +from sage.misc.cachefunc import cached_method + +from flatsurf.geometry.surface import OrientedSimilaritySurface + + +class Surface(OrientedSimilaritySurface): + r""" + Abstract base class of all surfaces that are built from a set of polygons + with edges identified. The identifications are compositions of homothety, + rotations and translations, i.e., similarities that ensure that the surface + is oriented. + + Concrete implementations of a surface must implement at least + :meth:`polygon` and :meth:`opposite_edge`. + + To be able to modify a surface, subclasses should also implement + ``_change_polygon``, ``_set_edge_pairing``, ``_add_polygon``, + ``_remove_polygon``. + + For concrete implementations of a Surface, see, e.g., :class:`Surface_list` + and :class:`Surface_dict`. + + INPUT: + + - ``base_ring`` -- the ring containing the coordinates of the vertices of + the polygons + + - ``base_label`` -- the label of a chosen special polygon in the surface, + see :meth:`.base_label` + + - ``finite`` -- whether this is a finite surface, see :meth:`is_finite_type` + + - ``mutable`` -- whether this is a mutable surface; can be changed later + with :meth:`.set_immutable` + + EXAMPLES:: + + sage: from flatsurf.geometry.surface import Surface, Surface_list, Surface_dict + + sage: S = Surface_list(QQ) + sage: isinstance(S, Surface) + True + + sage: S = Surface_dict(QQ) + doctest:warning + ... + UserWarning: Surface_dict has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: isinstance(S, Surface) + True + + TESTS: + + Users are being warned if they try to define a surface over an inexact ring:: + + sage: S = Surface_list(RR) + ... + UserWarning: surface defined over an inexact ring; many operations in sage-flatsurf are not going to work correctly over this ring + sage: isinstance(S, Surface) + True + + """ + + def __init__( + self, + base_ring, + base_label, + finite, + mutable, + category=None, + deprecation_warning=True, + ): + if deprecation_warning: + import warnings + + warnings.warn( + "base class Surface has been deprecated and will be removed in a future version of sage-flatsurf; use OrientedSimilaritySurface instead" + ) + + if finite not in [False, True]: + raise ValueError("finite must be either True or False") + + from sage.all import Rings + + if base_ring not in Rings(): + raise ValueError("base_ring must be a ring") + + if not base_ring.is_exact(): + from warnings import warn + + warn( + "surface defined over an inexact ring; many operations in sage-flatsurf are not going to work correctly over this ring" + ) + + if mutable not in [False, True]: + raise ValueError("mutable must be either True or False") + + self._base_label = base_label + self._finite = finite + self._mutable = mutable + + self._cache = {} + + from flatsurf.geometry.categories import SimilaritySurfaces + + if category is None: + category = SimilaritySurfaces() + + # Previously, all surfaces were assumed to be connected and without + # boundary (even though it was possible to construct non-connected + # surfaces but only the base_label component was really functional + # then.) + category &= SimilaritySurfaces().Oriented().WithoutBoundary().Connected() + + if finite: + category = category.FiniteType() + + OrientedSimilaritySurface.__init__(self, base=base_ring, category=category) + + if not mutable: + self._refine_category_(self.refined_category()) + + def _test_refined_category(self, **options): + if self.is_mutable(): + return + + super()._test_refined_category(**options) + + def _repr_(self): + r""" + Return a printable representation of this surface. + + EXAMPLES:: + + sage: from flatsurf.geometry.surface import Surface, Surface_list, Surface_dict + sage: S = Surface_list(QQ) + sage: S + Surface built from 0 polygons + + """ + if not self.is_finite_type(): + return "Surface built from infinitely many polygons" + if self.num_polygons() == 1: + return "Surface built from 1 polygon" + + return "Surface built from {} polygons".format(self.num_polygons()) + + def labels(self): + return LabelsView(self) + + def is_triangulated(self, limit=None): + r""" + EXAMPLES:: + + sage: import flatsurf + sage: G = SymmetricGroup(4) + sage: S = flatsurf.translation_surfaces.origami(G('(1,2,3,4)'), G('(1,4,2,3)')) + sage: S.is_triangulated() + False + sage: S.triangulate().is_triangulated() + True + """ + it = self.label_iterator() + if not self.is_finite_type(): + if limit is None: + raise ValueError( + "for infinite polygon, 'limit' must be set to a positive integer" + ) + else: + from itertools import islice + + it = islice(it, limit) + for p in it: + if len(self.polygon(p).vertices()) != 3: + return False + return True + + def polygon(self, label): + r""" + Return the polygon with the provided label. + + This method must be overridden in subclasses. + """ + raise NotImplementedError + + def opposite_edge(self, label, e=None): + r""" + Given the label ``label`` of a polygon and an edge ``e`` in that + polygon returns the pair (``ll``, ``ee``) to which this edge is glued. + + This method must be overridden in subclasses. + """ + raise NotImplementedError + + def _change_polygon(self, label, new_polygon, gluing_list=None): + r""" + Internal method used by change_polygon(). Should not be called directly. + + Mutable surfaces should implement this method. + """ + raise NotImplementedError + + def _set_edge_pairing(self, label1, edge1, label2, edge2): + r""" + Internal method used by change_edge_gluing(). Should not be called directly. + + Mutable surfaces should implement this method. + """ + raise NotImplementedError + + def _add_polygon(self, new_polygon, gluing_list=None, label=None): + r""" + Internal method used by add_polygon(). Should not be called directly. + + Mutable surfaces should implement this method. + """ + raise NotImplementedError + + def _remove_polygon(self, label): + r""" + Internal method used by remove_polygon(). Should not be called directly. + + Mutable surfaces should implement this method. + """ + raise NotImplementedError + + def num_polygons(self): + r""" + Return the number of polygons making up the surface, or + sage.rings.infinity.Infinity if the surface is infinite. + + This is a generic method. On a finite surface it will be linear time in + the edges the first time it is run, then constant time (assuming no + mutation occurs). + + Subclasses should consider overriding this method for increased + performance. + """ + if self.is_finite_type(): + lw = self.walker() + lw.find_all_labels() + return len(lw) + else: + from sage.rings.infinity import Infinity + + return Infinity + + def label_iterator(self, polygons=False): + r""" + Iterator over all polygon labels. + + Subclasses should consider overriding this method for increased + performance. + """ + if polygons: + return zip(self.labels(), self.polygons()) + return iter(self.walker()) + + def roots(self): + if self._base_label is None: + return () + return (self._base_label,) + + def is_finite_type(self): + r""" + Return whether or not the surface is finite. + """ + return self._finite + + def is_mutable(self): + r""" + Return if this surface is mutable. + """ + return self._mutable + + def set_immutable(self): + r""" + Mark this surface as immutable. + """ + self._mutable = False + + self._refine_category_(self.refined_category()) + + def walker(self): + r""" + Return a LabelWalker which walks over the surface in a canonical way. + """ + try: + return self._cache["lw"] + except KeyError: + lw = LabelWalker(self, deprecation_warning=False) + self._cache["lw"] = lw + return lw + + def __mutate(self): + r""" + Called before a mutation occurs. Do not call directly. + """ + if not self.is_mutable(): + raise Exception("surface must be mutable") + + # Remove the cache which will likely be invalidated. + self._cache = {} + + def change_polygon(self, label, new_polygon, gluing_list=None): + r""" + Assuming label is currently in the list of labels, change the + poygon assigned to the provided label to new_polygon, and + glue the edges according to gluing_list (which must be a list + of pairs of length equal to number of edges of the polygon). + """ + self.__mutate() + if not (gluing_list is None or len(new_polygon.vertices()) == len(gluing_list)): + raise ValueError + self._change_polygon(label, new_polygon, gluing_list) + + def set_edge_pairing(self, label1, edge1, label2, edge2): + r""" + Update the gluing so that (``label1``, ``edge1``) is glued to + (``label2``, ``edge2``). + """ + self.__mutate() + self._set_edge_pairing(label1, edge1, label2, edge2) + + change_edge_gluing = set_edge_pairing + + def change_polygon_gluings(self, label, glue_list): + r""" + Updates the list of glued polygons according to the provided list, + which is a list of pairs (pp,ee) whose position in the list + describes the edge of the polygon with the provided label. + + This method updates both the edges of the polygon with label "label" + and updates the edges listed in the glue_list. + """ + self.__mutate() + p = self.polygon(label) + if len(p.vertices()) != len(glue_list): + raise ValueError( + "len(glue_list)=" + + str(len(glue_list)) + + " and number of sides of polygon=" + + str(len(p.vertices())) + + " should be the same." + ) + for e, (pp, ee) in enumerate(glue_list): + self._set_edge_pairing(label, e, pp, ee) + + def add_polygons(self, polygons): + return [self.add_polygon(p) for p in polygons] + + def add_polygon(self, new_polygon, gluing_list=None, label=None): + r""" + Adds a the provided polygon to the surface. Utilizes gluing_list + for the gluing data for edges (which must be a list + of pairs representing edges of length equal to number of edges + of the polygon). + + If the parameter label is provided, the Surface attempts to use + this as the label for the new_polygon. However, this may fail + depending on the implementation. + + Returns the label assigned to the new_polygon (which may differ + from the label provided). + """ + self.__mutate() + if not (gluing_list is None or len(new_polygon.vertices()) == len(gluing_list)): + raise ValueError + label = self._add_polygon(new_polygon, gluing_list, label) + if self._base_label is None: + self.change_base_label(label) + return label + + def remove_polygon(self, label): + r""" + Remove the polygon with the provided label. Causes a ValueError + if the base_label is removed. + """ + if label == self._base_label: + raise ValueError("Can not remove the base_label.") + self.__mutate() + return self._remove_polygon(label) + + def change_base_label(self, new_base_label): + r""" + Change the base_label to the provided label. + """ + self.__mutate() + self._base_label = new_base_label + + @cached_method + def __hash__(self): + r""" + Hash compatible with equals. + """ + if self.is_mutable(): + raise TypeError("mutable surface is not hashable") + + if not self.is_finite_type(): + raise TypeError("cannot hash this infinite surface") + + return hash( + ( + self.base_ring(), + self.root(), + tuple(zip(self.labels(), self.polygons())), + tuple(self.gluings()), + ) + ) + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + + EXAMPLES:: + + sage: from flatsurf.geometry.surface import Surface_dict + sage: from flatsurf.geometry.polygon import Polygon, ConvexPolygons + + sage: S = Surface_dict(QQ) + sage: P = ConvexPolygons(QQ) + doctest:warning + ... + UserWarning: ConvexPolygons() has been deprecated and will be removed from a future version of sage-flatsurf; use Polygon() to create polygons. + If you really need the category of convex polygons over a ring use EuclideanPolygons(ring).Simple().Convex() instead. + sage: S.add_polygon(P([(1, 0), (0, 1), (-1, -1)]), label=0) + doctest:warning + ... + UserWarning: ConvexPolygons(…)(…) has been deprecated and will be removed in a future version of sage-flatsurf; use Polygon() instead + 0 + sage: S == S + True + + sage: T = Surface_dict(QQ) + sage: S == T + False + + TESTS:: + + sage: S == 42 + False + + """ + if self is other: + return True + + if not isinstance(other, Surface): + return False + + if self.is_mutable() != other.is_mutable(): + return False + + if not self._eq_oriented_similarity_surfaces(other): + return False + + if self.num_polygons() == 0: + # Only compare base labels when the surfaces are not empty. + return True + + if self.root() != other.root(): + return False + + return True + + def __ne__(self, other): + return not self == other + + def _eq_oriented_similarity_surfaces(self, other): + # Whether this surface equals other in terms of oriented similarity surfaces + if self is other: + return True + + if not isinstance(other, OrientedSimilaritySurface): + return False + + if self.base_ring() != other.base_ring(): + return False + + if self.category() != other.category(): + return False + + if self.is_finite_type() != other.is_finite_type(): + return False + + if self.is_finite_type(): + if len(self.polygons()) == 0: + return len(other.polygons()) == 0 + if len(other.polygons()) == 0: + return False + + if self.roots() != other.roots(): + return False + + for label in self.roots(): + if self.polygon(label) != other.polygon(label): + return False + + if not self.is_finite_type(): + raise NotImplementedError("cannot compare these infinite surfaces yet") + + if len(self.polygons()) != len(other.polygons()): + return False + + for label, polygon in zip(self.labels(), self.polygons()): + try: + polygon2 = other.polygon(label) + except ValueError: + return False + if polygon != polygon2: + return False + for edge in range(len(polygon.vertices())): + if self.opposite_edge(label, edge) != other.opposite_edge(label, edge): + return False + + return True + + def _test_base_ring(self, **options): + # Test that the base_label is associated to a polygon + tester = self._tester(**options) + from sage.all import Rings + + tester.assertTrue(self.base_ring() in Rings()) + + def _test_base_label(self, **options): + # Test that the base_label is associated to a polygon + tester = self._tester(**options) + + tester.assertTrue( + self.polygon(self.root()).is_convex(), + "polygon(base_label) does not return a ConvexPolygon. " + + "Here base_label=" + + str(self.root()), + ) + + def _test_override(self, **options): + # Test that the required methods have been overridden and that some other methods have not been overridden. + + # Of course, we don't care if the methods are overridden or not we just want to warn the programmer. + if "tester" in options: + tester = options["tester"] + else: + tester = self._tester(**options) + + # Check for override: + tester.assertNotEqual( + self.polygon.__func__, + Surface.polygon, + "Method polygon of Surface must be overridden. The Surface is of type " + + str(type(self)) + + ".", + ) + tester.assertNotEqual( + self.opposite_edge.__func__, + Surface.opposite_edge, + "Method opposite_edge of Surface must be overridden. The Surface is of type " + + str(type(self)) + + ".", + ) + + if self.is_mutable(): + # Check for override: + tester.assertNotEqual( + self._change_polygon.__func__, + Surface._change_polygon, + "Method _change_polygon of Surface must be overridden in a mutable surface. " + + "The Surface is of type " + + str(type(self)) + + ".", + ) + tester.assertNotEqual( + self._set_edge_pairing.__func__, + Surface._set_edge_pairing, + "Method _set_edge_pairing of Surface must be overridden in a mutable surface. " + + "The Surface is of type " + + str(type(self)) + + ".", + ) + tester.assertNotEqual( + self._add_polygon.__func__, + Surface._add_polygon, + "Method _add_polygon of Surface must be overridden in a mutable surface. " + + "The Surface is of type " + + str(type(self)) + + ".", + ) + tester.assertNotEqual( + self._remove_polygon.__func__, + Surface._remove_polygon, + "Method _remove_polygon of Surface must be overridden in a mutable surface. " + + "The Surface is of type " + + str(type(self)) + + ".", + ) + + def _test_polygons(self, **options): + # Test that the base_label is associated to a polygon + if "tester" in options: + tester = options["tester"] + else: + tester = self._tester(**options) + + if self.is_finite_type(): + it = self.label_iterator() + else: + from itertools import islice + + it = islice(self.label_iterator(), 30) + for label in it: + tester.assertTrue( + self.polygon(label).is_convex(), + "polygon(label) does not return a ConvexPolygon when label=" + + str(label), + ) + + def point(self, label, position, limit=None, ring=None): + r""" + Return the :class:`flatsurf.geometry.surface_objects.SurfacePoint` of + this surface at ``position`` in the polygon ``label``. + + INPUT: + + - ``label`` -- a label of a polygon in this surface, see :meth:`label_iterator` + + - ``position`` -- a vector with coordinates in this surface's base ring + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: from flatsurf.geometry.surface import Surface_list + + sage: S = Surface_list(QQ) + sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + + sage: S.point(0, (0, 0)) + Vertex 0 of polygon 0 + + """ + return self(label, position, limit=limit, ring=ring) + + def _an_element_(self): + r""" + Return a point of this surface. + + EXAMPLES:: + + sage: from flatsurf import Polygon + sage: from flatsurf.geometry.surface import Surface_list + + sage: S = Surface_list(QQ) + sage: S.add_polygon(Polygon(vertices=[(0, 0), (1, 0), (1, 1), (0, 1)])) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + + sage: S.an_element() + Point (1/2, 1/2) of polygon 0 + + """ + label = next(self.label_iterator()) + polygon = self.polygon(label) + + # We use a point that can be constructed without problems on an + # infinite surface. + if polygon.is_convex(): + coordinates = polygon.centroid() + else: + # Sometimes, this is not implemented because it requires the edge + # transformation to be known, so we prefer the centroid. + coordinates = polygon.edge(0) / 2 + return self.point(label, coordinates) + + def set_default_graphical_surface(self, graphical_surface): + r""" + Replace the default graphical surface with the provided GraphicalSurface. + """ + from flatsurf.graphical.surface import GraphicalSurface + + if not isinstance(graphical_surface, GraphicalSurface): + raise ValueError("graphical_surface must be a GraphicalSurface") + if self != graphical_surface.get_surface(): + raise ValueError( + "The provided graphical_surface renders a different surface!" + ) + self._gs = graphical_surface + + def graphical_surface(self, *args, **kwds): + r""" + Return a GraphicalSurface representing this surface. + + By default this returns a cached version of the GraphicalSurface. If + ``cached=False`` is provided as a keyword option then a new + GraphicalSurface is returned. + + All other parameters are passed on to + :class:`~flatsurf.graphical.surface.GraphicalSurface` or its + :meth:`~flatsurf.graphical.surface.GraphicalSurface.process_options`. + Note that this mutates the cached graphical surface for future calls. + + EXAMPLES: + + Test the difference between the cached graphical_surface and the uncached version:: + + sage: from flatsurf import translation_surfaces + sage: s = translation_surfaces.octagon_and_squares() + sage: s.plot() + ...Graphics object consisting of 32 graphics primitives + sage: s.graphical_surface(cached=False,adjacencies=[]).plot() + ...Graphics object consisting of 18 graphics primitives + + """ + from flatsurf.graphical.surface import GraphicalSurface + + if "cached" in kwds: + if not kwds["cached"]: + # cached=False: return a new surface. + kwds.pop("cached", None) + return GraphicalSurface(self, *args, **kwds) + kwds.pop("cached", None) + if hasattr(self, "_gs"): + self._gs.process_options(*args, **kwds) + else: + self._gs = GraphicalSurface(self, *args, **kwds) + return self._gs + + def plot(self, *args, **kwds): + r""" + Return a plot of the surface. + + The parameters are passed on to :meth:`graphical_surface` and + :meth:`flatsurf.graphical.surface.GraphicalSurface.plot`. Consult their + documentation for details. + + EXAMPLES:: + + sage: import flatsurf + sage: S = flatsurf.translation_surfaces.veech_double_n_gon(5) + sage: S.plot() + ...Graphics object consisting of 21 graphics primitives + + Keywords are passed on to the underlying plotting routines, see + :meth:`flatsurf.graphical.surface.GraphicalSurface.plot` for details:: + + sage: S.plot(fill=None) + ...Graphics object consisting of 21 graphics primitives + + Note that some keywords mutate the underlying cached graphical surface, + see :meth:`graphical_surface`:: + + sage: S.plot(edge_labels='gluings and number') + ...Graphics object consisting of 23 graphics primitives + + """ + if len(args) > 1: + raise ValueError("plot() can take at most one non-keyword argument") + + graphical_surface_keywords = { + key: kwds.pop(key) + for key in [ + "cached", + "adjacencies", + "polygon_labels", + "edge_labels", + "default_position_function", + ] + if key in kwds + } + + if len(args) == 1: + from flatsurf.graphical.surface import GraphicalSurface + + if not isinstance(args[0], GraphicalSurface): + raise TypeError("non-keyword argument must be a GraphicalSurface") + + import warnings + + warnings.warn( + "Passing a GraphicalSurface to plot() is deprecated because it mutates that GraphicalSurface. This functionality will be removed in a future version of sage-flatsurf. " + "Call process_options() and plot() on the GraphicalSurface explicitly instead." + ) + + gs = args[0] + gs.process_options(**graphical_surface_keywords) + else: + # It's very surprising that plot mutates the underlying cached + # graphical surface. We should change that and make the graphical + # surface not cached. See + # https://github.com/flatsurf/sage-flatsurf/issues/97 + gs = self.graphical_surface(**graphical_surface_keywords) + + return gs.plot(**kwds) + + +class Surface_list(Surface): + r""" + A fast mutable :class:`Surface` using a list to store polygons and gluings. + + ALGORITHM: + + Internally, we maintain a list ``_p`` for storing polygons together with + gluing data. + + Each ``_p[label]`` is typically a pair ``(polygon, gluing_list)`` where + ``gluing_list`` is a list of pairs ``(other_label, other_edge)`` such that + :meth:`opposite_edge(label, edge) ` returns + ``_p[label][1][edge]``. + + INPUT: + + - ``base_ring`` -- ring or ``None`` (default: ``None``); the ring + containing the coordinates of the vertices of the polygons. If ``None``, + the base ring will be the one of ``surface``. + + - ``surface`` -- a surface or ``None`` (default: ``None``); a surface to be + copied or referenced (see ``copy``). If ``None``, the surface is + initially empty. + + - ``copy`` -- boolean or ``None`` (default: ``None``); whether the data + underlying ``surface`` is copied into this surface or just a reference to + that surface is kept. If ``None``, a sensible default is chosen, namely + ``surface.is_mutable()``. + + - ``mutable`` -- boolean or ``None`` (default: ``None``); whether this + surface is mutable. When ``None``, the surface will be mutable iff + ``surface`` is ``None``. + + EXAMPLES:: + + sage: from flatsurf import polygons, Surface_list, Polygon, similarity_surfaces + sage: p=polygons.regular_ngon(5) + sage: s=Surface_list(base_ring=p.base_ring()) + doctest:warning + ... + UserWarning: Surface_list has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: s.add_polygon(p) # gets label 0 + 0 + sage: s.add_polygon( (-matrix.identity(2))*p ) # gets label 1 + 1 + sage: s.change_polygon_gluings(0,[(1,e) for e in range(5)]) + sage: # base label defaults to zero. + sage: s.set_immutable() + sage: TestSuite(s).run() + + We surgically add a square into an infinite billiard surface:: + + sage: p = Polygon(vertices=[(0,0),(4,0),(0,3)]) + sage: s = similarity_surfaces.billiard(p) + sage: ts=s.minimal_cover(cover_type="translation").copy(relabel=True, mutable=True) + doctest:warning + ... + UserWarning: copy() has been deprecated and will be removed from a future version of sage-flatsurf; for surfaces of finite type use MutableOrientedSimilaritySurface.from_surface() instead. + Use relabel({old: new for (new, old) in enumerate(surface.labels())}) for integer labels. However, there is no immediate replacement for lazy copying of infinite surfaces. + Have a look at the implementation of flatsurf.geometry.delaunay.LazyMutableSurface and adapt it to your needs. + sage: # Explore the surface a bit + sage: ts.polygon(0) + Polygon(vertices=[(0, 0), (4, 0), (0, 3)]) + sage: ts.opposite_edge(0,0) + (1, 2) + sage: ts.polygon(1) + Polygon(vertices=[(0, 0), (0, -3), (4, 0)]) + sage: s = ts + sage: l=s.add_polygon(polygons.square(side=4)) + sage: s.change_edge_gluing(0,0,l,2) + sage: s.change_edge_gluing(1,2,l,0) + sage: s.change_edge_gluing(l,1,l,3) + sage: print("Glued in label is "+str(l)) + Glued in label is 2 + sage: count = 0 + sage: for x in ts.gluings(): + ....: print(x) + ....: count=count+1 + ....: if count>15: + ....: break + ((0, 0), (2, 2)) + ((0, 1), (3, 1)) + ((0, 2), (4, 0)) + ((2, 0), (1, 2)) + ((2, 1), (2, 3)) + ((2, 2), (0, 0)) + ((2, 3), (2, 1)) + ((3, 0), (5, 2)) + ((3, 1), (0, 1)) + ((3, 2), (6, 0)) + ((4, 0), (0, 2)) + ((4, 1), (7, 1)) + ((4, 2), (8, 0)) + ((1, 0), (8, 2)) + ((1, 1), (9, 1)) + ((1, 2), (2, 0)) + sage: count = 0 + sage: for l,p in ts.label_iterator(polygons=True): + ....: print(str(l)+" -> "+str(p)) + ....: count=count+1 + ....: if count>5: + ....: break + 0 -> Polygon(vertices=[(0, 0), (4, 0), (0, 3)]) + 2 -> Polygon(vertices=[(0, 0), (4, 0), (4, 4), (0, 4)]) + 3 -> Polygon(vertices=[(0, 0), (-72/25, -21/25), (28/25, -96/25)]) + 4 -> Polygon(vertices=[(0, 0), (0, 3), (-4, 0)]) + 1 -> Polygon(vertices=[(0, 0), (0, -3), (4, 0)]) + 5 -> Polygon(vertices=[(0, 0), (-28/25, 96/25), (-72/25, -21/25)]) + + TESTS: + + Verify that the replacement for this class implements all the features that + used to be around:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, Surface_list + sage: legacy_methods = set(dir(Surface_list(QQ))) + doctest:warning + ... + UserWarning: Surface_list has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: non_legacy_methods = set(dir(MutableOrientedSimilaritySurface(QQ))) + sage: [method for method in legacy_methods if method not in non_legacy_methods and not method.startswith("_")] + [] + + """ + + def __init__( + self, + base_ring=None, + surface=None, + copy=None, + mutable=None, + category=None, + deprecation_warning=True, + ): + if deprecation_warning: + import warnings + + warnings.warn( + "Surface_list has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead" + ) + + self._p = [] # list of pairs (polygon, gluings) + self._reference_surface = None + self._removed_labels = [] + self._num_polygons = 0 + + # Validate input parameters and fill in defaults + ( + base_ring, + surface, + copy, + mutable, + finite, + ) = Surface_list._validate_init_parameters( + base_ring=base_ring, + surface=surface, + copy=copy, + mutable=mutable, + finite=None, + ) + + Surface.__init__( + self, + base_ring=base_ring, + base_label=0, + finite=finite, + mutable=True, + category=category, + deprecation_warning=False, + ) + + # Initialize surface from reference surface + if surface is not None: + if copy is True: + reference_label_to_label = { + label: self.add_polygon(polygon) + for label, polygon in zip(surface.labels(), surface.polygons()) + } + + for ( + (label, edge), + (glued_label, glued_edge), + ) in surface.gluings(): + self.set_edge_pairing( + reference_label_to_label[label], + edge, + reference_label_to_label[glued_label], + glued_edge, + ) + + self.change_base_label(reference_label_to_label[surface.root()]) + else: + self._reference_surface = surface + self._ref_to_int = {} + self._int_to_ref = [] + + if not surface.is_finite_type(): + from sage.all import infinity + + self._num_polygons = infinity + else: + self._num_polygons = len(surface.polygons()) + + # Cache the base polygon + self.change_base_label(self.__get_label(surface.root())) + assert self.root() == 0 + + if not mutable: + self.set_immutable() + + @classmethod + def _validate_init_parameters(cls, base_ring, surface, copy, mutable, finite): + r""" + Helper method for ``__init__`` that validates the parameters and + returns them in the same order with defaults filled in. + """ + if surface is None and base_ring is None: + raise ValueError("Either surface or base_ring must be provided.") + + if surface is None: + if copy is not None: + raise ValueError("Cannot copy when surface was provided.") + + if mutable is None: + mutable = True + + finite = True + else: + from flatsurf.geometry.surface import OrientedSimilaritySurface + + if not isinstance(surface, OrientedSimilaritySurface): + raise TypeError("surface must be an OrientedSimilaritySurface") + + if not surface.is_finite_type() and surface.is_mutable(): + raise NotImplementedError( + "Cannot create surface from infinite mutable surface yet." + ) + + if base_ring is None: + base_ring = surface.base_ring() + + if base_ring != surface.base_ring(): + raise NotImplementedError( + "Cannot provide both a surface and a base_ring yet." + ) + + if mutable is None: + mutable = True + + if copy is None: + copy = surface.is_mutable() + + if copy and not surface.is_finite_type(): + raise ValueError("Cannot copy infinite surface.") + + if surface.is_mutable() and not copy: + raise ValueError("Cannot reference mutable surface.") + + finite = surface.is_finite_type() + + return base_ring, surface, copy, mutable, finite + + def __get_label(self, ref_label): + r""" + Returns a corresponding label. Creates a new label if necessary. + """ + try: + return self._ref_to_int[ref_label] + except KeyError: + polygon = self._reference_surface.polygon(ref_label) + data = [polygon, [None for i in range(len(polygon.vertices()))]] + if len(self._removed_labels) > 0: + i = self._removed_labels.pop() + self._p[i] = data + self._ref_to_int[ref_label] = i + self._int_to_ref[i] = ref_label + else: + i = len(self._p) + if i != len(self._int_to_ref): + raise RuntimeError( + "length of self._int_to_ref is " + + str(len(self._int_to_ref)) + + " should be the same as i=" + + str(i) + ) + self._p.append(data) + self._ref_to_int[ref_label] = i + self._int_to_ref.append(ref_label) + return i + + def is_compact(self): + if self._reference_surface is not None: + # Since we are only modifying a finite number of polygons, this + # surface is compact iff its reference surface is. + return self._reference_surface.is_compact() + + return True + + def change_base_label(self, new_base_label): + r""" + Change the base_label to the provided label. + """ + super().change_base_label(int(new_base_label)) + + def polygon(self, lab): + r""" + Return the polygon with label ``lab``. + """ + try: + data = self._p[lab] + except IndexError: + if self._reference_surface is None: + raise ValueError(f"No polygon with label {lab}.") + + for label in self.label_iterator(): + if label >= lab: + break + + if lab >= len(self._p): + raise ValueError(f"no polygon with label {lab}") + + data = self._p[lab] + + if data is None: + raise ValueError("Provided label was removed.") + + return data[0] + + def opposite_edge(self, p, e=None): + r""" + Given the label ``p`` of a polygon and an edge ``e`` in that polygon + returns the pair (``pp``, ``ee``) to which this edge is glued. + """ + if e is None: + p, e = p + try: + data = self._p[p] + except KeyError: + raise ValueError("No known polygon with provided label") + if data is None: + raise ValueError("Provided label was removed.") + glue = data[1] + try: + oe = glue[e] + except KeyError: + raise ValueError("Edge out of range of polygon.") + if oe is None: + if self._reference_surface is None: + # Perhaps the user of this class left an edge unglued? + return None + else: + ref_p = self._int_to_ref[p] + ref_pp, ref_ee = self._reference_surface.opposite_edge(ref_p, e) + pp = self.__get_label(ref_pp) + return_value = (pp, ref_ee) + glue[e] = return_value + return return_value + else: + # Successfully return edge data + return oe + + # Methods for changing the surface + + def _change_polygon(self, label, new_polygon, gluing_list=None): + r""" + Internal method used by change_polygon(). Should not be called directly. + """ + try: + data = self._p[label] + except KeyError: + raise ValueError("No known polygon with provided label") + if data is None: + raise ValueError("Provided label was removed from the surface.") + data[0] = new_polygon + if data[1] is None or len(new_polygon.vertices()) != len(data[1]): + data[1] = [None for e in range(len(new_polygon.vertices()))] + if gluing_list is not None: + self.change_polygon_gluings(label, gluing_list) + + def _set_edge_pairing(self, label1, edge1, label2, edge2): + r""" + Internal method used by change_edge_gluing(). Should not be called directly. + """ + try: + data = self._p[label1] + except KeyError: + raise ValueError("No known polygon with provided label1=" + str(label1)) + if data is None: + raise ValueError( + "Provided label1=" + str(label1) + " was removed from the surface." + ) + data[1][edge1] = (label2, edge2) + try: + data = self._p[label2] + except KeyError: + raise ValueError("No known polygon with provided label2=" + str(label2)) + if data is None: + raise ValueError( + "Provided label2=" + str(label2) + " was removed from the surface." + ) + data[1][edge2] = (label1, edge1) + + _change_edge_gluing = _set_edge_pairing + + def _add_polygon(self, new_polygon, gluing_list=None, label=None): + r""" + Internal method used by add_polygon(). Should not be called directly. + + EXAMPLES:: + + sage: from flatsurf import polygons, Surface_list + sage: p=polygons.regular_ngon(5) + sage: s=Surface_list(base_ring=p.base_ring()) + sage: s.add_polygon(p, label=3) + 3 + sage: s.add_polygon( (-matrix.identity(2))*p, label=30) + 30 + sage: s.change_polygon_gluings(3,[(30,e) for e in range(5)]) + sage: s.change_base_label(30) + sage: len(s.polygons()) + 2 + sage: TestSuite(s).run() + sage: s.remove_polygon(3) + sage: s.add_polygon(p, label=6) + 6 + sage: s.change_polygon_gluings(6,[(30,e) for e in range(5)]) + sage: len(s.polygons()) + 2 + sage: TestSuite(s).run() + sage: s.change_base_label(6) + sage: s.remove_polygon(30) + sage: label = s.add_polygon((-matrix.identity(2))*p) + sage: s.change_polygon_gluings(6,[(label,e) for e in range(5)]) + sage: TestSuite(s).run() + """ + if new_polygon is None: + data = [None, None] + else: + data = [new_polygon, [None for i in range(len(new_polygon.vertices()))]] + if label is None: + if len(self._removed_labels) > 0: + new_label = self._removed_labels.pop() + self._p[new_label] = data + else: + new_label = len(self._p) + self._p.append(data) + if self._reference_surface is not None: + # Need a blank in this list for algorithmic reasons + self._int_to_ref.append(None) + else: + new_label = int(label) + if new_label < len(self._p): + if self._p[new_label] is not None: + raise ValueError( + "Trying to add a polygon with label=" + + str(label) + + " which already indexes a polygon." + ) + self._p[new_label] = data + else: + if new_label - len(self._p) > 100: + raise ValueError( + "Adding a polygon with label=" + + str(label) + + " would add more than 100 entries in our list." + ) + for i in range(len(self._p), new_label): + self._p.append(None) + self._removed_labels.append(i) + if self._reference_surface is not None: + # Need a blank in this list for algorithmic reasons + self._int_to_ref.append(None) + + self._p.append(data) + if self._reference_surface is not None: + # Need a blank in this list for algorithmic reasons + self._int_to_ref.append(None) + + if gluing_list is not None: + self.change_polygon_gluings(new_label, gluing_list) + self._num_polygons += 1 + + return new_label + + def num_polygons(self): + r""" + Return the number of polygons making up the surface in constant time. + """ + return self._num_polygons + + def _test_roots(self, **options): + # Surface_list does not iterate labels in a canonical order from the + # roots(). Instead, it iterates by increasing labels. + pass + + def label_iterator(self, polygons=False): + r""" + Iterator over all polygon labels. + """ + if polygons: + for entry in zip(self.labels(), self.polygons()): + yield entry + return + if self._reference_surface is not None: + for i in Surface.label_iterator(self): + yield i + elif self._num_polygons == len(self._p): + for i in range(self.num_polygons()): + yield i + else: + # We've removed some labels + found = 0 + i = 0 + while found < self._num_polygons: + if self._p[i] is not None: + found += 1 + yield i + i += 1 + + def _remove_polygon(self, label): + r""" + Internal method used by remove_polygon(). Should not be called directly. + """ + if label == len(self._p) - 1: + self._p.pop() + if self._reference_surface is not None: + ref_label = self._int_to_ref.pop() + assert len(self._int_to_ref) == label + if ref_label is not None: + del self._ref_to_int[ref_label] + else: + self._p[label] = None + self._removed_labels.append(label) + if self._reference_surface is not None: + ref_label = self._int_to_ref[label] + if ref_label is not None: + self._int_to_ref[label] = None + del self._ref_to_int[ref_label] + self._num_polygons -= 1 + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons + sage: P=polygons.regular_ngon(5) + sage: S = Surface_list(base_ring=P.base_ring()) + doctest:warning + ... + UserWarning: Surface_list has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: T = Surface_list(base_ring=P.base_ring()) + + sage: S == T + True + + sage: S.add_polygon(P, label=3) + 3 + + sage: S == T + False + + """ + if not isinstance(other, Surface_list): + return False + + if self._reference_surface is not None: + equal = self._eq_reference_surface(other) + if equal is True: + return True + if equal is False: + return False + + return super().__eq__(other) + + def _eq_reference_surface(self, other): + r""" + Return whether this surface is indistinguishable from ``other`` by + comparing their reference surfaces. + + Returns ``None``, when no conclusion could be reached. + + This is a helper method for :meth:`__eq__`. + """ + if self._reference_surface != other._reference_surface: + return None + + for label in range(len(self._p)): + if self._p[label] is None: + if label >= len(other._p) or other._p[label] is not None: + return None + continue + try: + if self.polygon(label) != other.polygon(label): + return False + except ValueError: + return False + + for label in range(len(other._p)): + if other._p[label] is None: + if label >= len(self._p) or self._p[label] is not None: + return None + continue + try: + if self.polygon(label) != other.polygon(label): + return False + except ValueError: + return False + + if self.base_ring() != other.base_ring(): + return False + if self.is_mutable() != other.is_mutable(): + return False + if self.base_label() != other.base_label(): + return False + + return True + + +def surface_list_from_polygons_and_gluings(polygons, gluings, mutable=False): + r""" + Take a list of polygons and gluings (given either as a list of pairs of edges, or as a dictionary), + and produce a Surface_list from it. The mutable parameter determines the mutability of the resulting + surface. + """ + import warnings + + warnings.warn( + "surface_list_from_polygons_and_gluings() has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead and add polygons and gluings explicitly" + ) + + if not (isinstance(polygons, list) or isinstance(polygons, tuple)): + raise ValueError("polygons must be a list or tuple.") + field = polygons[0].parent().field() + s = Surface_list(base_ring=field, deprecation_warning=False) + for p in polygons: + s.add_polygon(p) + try: + # dict case: + it = gluings.items() + except AttributeError: + # list case: + it = gluings + for (l1, e1), (l2, e2) in it: + s.change_edge_gluing(l1, e1, l2, e2) + if not mutable: + s.set_immutable() + return s + + +class Surface_dict(Surface): + r""" + A mutable :class:`Surface` using a dict to store polygons and gluings. + + Unlike :class:`Surface_list`, this surface is not limited to integer + labels. However, :class:`Surface_list` is likely more efficient for most + applications. + + ALGORITHM: + + Internally, we maintain a dict ``_p`` for storing polygons together with + gluing data. + + Each ``_p[label]`` is typically a pair ``(polygon, gluing_dict)`` where + ``gluing_dict`` is maps ``other_label`` to ``other_edge`` such that + :meth:`opposite_edge(label, edge) ` returns + ``_p[label][1][edge]``. + + INPUT: + + - ``base_ring`` -- ring or ``None`` (default: ``None``); the ring + containing the coordinates of the vertices of the polygons. If ``None``, + the base ring will be the one of ``surface``. + + - ``surface`` -- a surface or ``None`` (default: ``None``); a surface to be + copied or referenced (see ``copy``). If ``None``, the surface is + initially empty. + + - ``copy`` -- boolean or ``None`` (default: ``None``); whether the data + underlying ``surface`` is copied into this surface or just a reference to + that surface is kept. If ``None``, a sensible default is chosen, namely + ``surface.is_mutable()``. + + - ``mutable`` -- boolean or ``None`` (default: ``None``); whether this + surface is mutable. When ``None``, the surface will be mutable iff + ``surface`` is ``None``. + + EXAMPLES:: + + sage: from flatsurf import polygons, Surface_dict + sage: p=polygons.regular_ngon(10) + sage: s=Surface_dict(base_ring=p.base_ring()) + doctest:warning + ... + UserWarning: Surface_dict has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: s.add_polygon(p,label="A") + 'A' + sage: s.change_polygon_gluings("A",[("A",(e+5)%10) for e in range(10)]) + sage: s.change_base_label("A") + sage: s.set_immutable() + sage: TestSuite(s).run() + + TESTS: + + Verify that the replacement for this class implements all the features that + used to be around:: + + sage: from flatsurf import MutableOrientedSimilaritySurface, Surface_dict + sage: legacy_methods = set(dir(Surface_dict(QQ))) + doctest:warning + ... + UserWarning: Surface_dict has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: non_legacy_methods = set(dir(MutableOrientedSimilaritySurface(QQ))) + sage: [method for method in legacy_methods if method not in non_legacy_methods and not method.startswith("_")] + [] + + """ + + def __init__( + self, + base_ring=None, + surface=None, + copy=None, + mutable=None, + category=None, + deprecation_warning=True, + ): + if deprecation_warning: + import warnings + + warnings.warn( + "Surface_dict has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead" + ) + + self._p = {} + self._reference_surface = None + + # Validate input parameters and fill in defaults + ( + base_ring, + surface, + copy, + mutable, + finite, + ) = Surface_list._validate_init_parameters( + base_ring=base_ring, + surface=surface, + copy=copy, + mutable=mutable, + finite=None, + ) + + Surface.__init__( + self, + base_ring=base_ring, + base_label=None, + finite=finite, + mutable=True, + category=category, + deprecation_warning=False, + ) + + # Initialize surface from reference surface + if surface is not None: + base_label = surface.root() + + if copy is True: + reference_label_to_label = { + label: self.add_polygon(polygon, label=label) + for label, polygon in zip(surface.labels(), surface.polygons()) + } + + for ( + (label, edge), + (glued_label, glued_edge), + ) in surface.gluings(): + self.set_edge_pairing( + reference_label_to_label[label], + edge, + reference_label_to_label[glued_label], + glued_edge, + ) + + self.change_base_label(reference_label_to_label[base_label]) + else: + self._reference_surface = surface + + self.change_base_label(base_label) + + if not mutable: + self.set_immutable() + + def is_compact(self): + if self._reference_surface is not None: + return self._reference_surface.is_compact() + + return True + + def polygon(self, lab): + r""" + Return the polygon with label ``lab``. + """ + try: + data = self._p[lab] + except KeyError: + if self._reference_surface is None: + raise ValueError(f"No polygon with label {lab}.") + + polygon = self._reference_surface.polygon(lab) + data = [ + self._reference_surface.polygon(lab), + [ + self._reference_surface.opposite_edge(lab, e) + for e in range(len(polygon.vertices())) + ], + ] + self._p[lab] = data + + if data is None: + raise ValueError("Label " + str(lab) + " was removed from the surface.") + return data[0] + + def opposite_edge(self, p, e=None): + r""" + Given the label ``p`` of a polygon and an edge ``e`` in that polygon + returns the pair (``pp``, ``ee``) to which this edge is glued. + """ + if e is None: + p, e = p + try: + data = self._p[p] + except KeyError: + self.polygon(p) + data = self._p[p] + if data is None: + raise ValueError("Label " + str(p) + " was removed from the surface.") + gluing_data = data[1] + try: + return gluing_data[e] + except IndexError: + raise ValueError( + "Edge e=" + str(e) + " is out of range in polygon with label " + str(p) + ) + + # Methods for changing the surface + + def _change_polygon(self, label, new_polygon, gluing_list=None): + r""" + Internal method used by change_polygon(). Should not be called directly. + """ + try: + data = self._p[label] + if data is None: + raise ValueError( + "Label " + str(label) + " was removed from the surface." + ) + data[0] = new_polygon + except KeyError: + # Polygon probably lies in reference surface + if self._reference_surface is None: + raise ValueError("No known polygon with provided label") + else: + # Ensure the reference surface had a polygon with the provided label: + old_polygon = self._reference_surface.polygon(label) + if len(old_polygon.vertices()) == len(new_polygon.vertices()): + data = [ + new_polygon, + [ + self._reference_surface.opposite_edge(label, e) + for e in range(len(new_polygon.vertices())) + ], + ] + self._p[label] = data + else: + data = [ + new_polygon, + [None for e in range(len(new_polygon.vertices()))], + ] + self._p[label] = data + if len(data[1]) != len(new_polygon.vertices()): + data[1] = [None for e in range(len(new_polygon.vertices()))] + if gluing_list is not None: + self.change_polygon_gluings(label, gluing_list) + + def _set_edge_pairing(self, label1, edge1, label2, edge2): + r""" + Internal method used by set_edge_pairing(). Should not be called directly. + """ + try: + data = self._p[label1] + except KeyError: + if self._reference_surface is None: + raise ValueError( + "No known polygon with provided label1 = {}".format(label1) + ) + else: + # Failure likely because reference_surface contains the polygon. + # import the data into this surface. + polygon = self._reference_surface.polygon(label1) + data = [ + polygon, + [ + self._reference_surface.opposite_edge(label1, e) + for e in range(len(polygon.vertices())) + ], + ] + self._p[label1] = data + try: + data[1][edge1] = (label2, edge2) + except IndexError: + # break down error + if data is None: + raise ValueError("polygon with label1={} was removed".format(label1)) + data1 = data[1] + try: + data1[edge1] = (label2, edge2) + except IndexError: + raise ValueError( + "edge1={} is out of range in polygon with label1={}".format( + edge1, label1 + ) + ) + try: + data = self._p[label2] + except KeyError: + if self._reference_surface is None: + raise ValueError("no polygon with label2={}".format(label2)) + else: + # Failure likely because reference_surface contains the polygon. + # import the data into this surface. + polygon = self._reference_surface.polygon(label2) + data = [ + polygon, + [ + self._reference_surface.opposite_edge(label2, e) + for e in range(len(polygon.vertices())) + ], + ] + self._p[label2] = data + try: + data[1][edge2] = (label1, edge1) + except IndexError: + # break down error + if data is None: + raise ValueError("polygon with label1={} was removed".format(label1)) + data1 = data[1] + try: + data1[edge2] = (label1, edge1) + except IndexError: + raise ValueError( + "edge {} is out of range in polygon with label2={}".format( + edge2, label2 + ) + ) + + _change_edge_gluing = _set_edge_pairing + + def _add_polygon(self, new_polygon, gluing_list=None, label=None): + r""" + Internal method used by add_polygon(). Should not be called directly. + """ + data = [new_polygon, [None for i in range(len(new_polygon.vertices()))]] + if label is None: + new_label = ExtraLabel() + else: + try: + old_data = self._p[label] + if old_data is None: + # already removed this polygon. That's good, we can add. + new_label = label + else: + raise ValueError( + "label={} already used by another polygon".format(label) + ) + except KeyError: + # This seems inconvenient to enforce: + # + # if not self._reference_surface is None: + # # Can not be sure we are not overwriting a polygon in the reference surface. + # raise ValueError("Can not assign this label to a Surface_dict containing a reference surface,"+\ + # "which may already contain this label.") + new_label = label + self._p[new_label] = data + if gluing_list is not None: + self.change_polygon_gluings(new_label, gluing_list) + return new_label + + def num_polygons(self): + r""" + Return the number of polygons making up the surface in constant time. + """ + if self.is_finite_type(): + if self._reference_surface is None: + return len(self._p) + else: + # Unfortunately, I don't see a good way to compute this. + return Surface.num_polygons(self) + else: + from sage.rings.infinity import Infinity + + return Infinity + + def label_iterator(self, polygons=False): + r""" + Iterator over all polygon labels. + """ + if polygons: + for entry in zip(self.labels(), self.polygons()): + yield entry + return + if self._reference_surface is None: + for i in self._p: + yield i + else: + for i in Surface.label_iterator(self): + yield i + + def _remove_polygon(self, label): + r""" + Internal method used by remove_polygon(). Should not be called directly. + """ + if self._reference_surface is None: + try: + data = self._p[label] + except KeyError: + raise ValueError("Label " + str(label) + " is not in the surface.") + del self._p[label] + else: + try: + data = self._p[label] + # Success. + if data is None: + raise ValueError( + "Label " + str(label) + " was already removed from the surface." + ) + self._p[label] = None + except KeyError: + # Assume on faith we are removing a polygon in the base_surface. + self._p[label] = None + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + r""" + Return whether this surface is indistinguishable from ``other``. + + EXAMPLES:: + + sage: from flatsurf import Surface_dict, polygons + sage: P=polygons.regular_ngon(5) + sage: S = Surface_dict(base_ring=P.base_ring()) + sage: T = Surface_dict(base_ring=P.base_ring()) + + sage: S == T + True + + sage: S.add_polygon(P, label=3) + 3 + + sage: S == T + False + + """ + if not isinstance(other, Surface_dict): + return False + + if self._reference_surface is not None: + equal = self._eq_reference_surface(other) + if equal is True: + return True + if equal is False: + return False + + return super().__eq__(other) + + def _eq_reference_surface(self, other): + r""" + Return whether this surface is indistinguishable from ``other`` by + comparing their reference surfaces. + + Returns ``None``, when no conclusion could be reached. + + This is a helper method for :meth:`__eq__`. + """ + if self._reference_surface != other._reference_surface: + return None + + for label, polygon in self._p.items(): + if polygon is None: + if label not in other._p or other._p[label] is not None: + return None + continue + try: + if self.polygon(label) != other.polygon(label): + return False + except ValueError: + return False + + for label, polygon in other._p.items(): + if polygon is None: + if label not in self._p or self._p[label] is not None: + return None + continue + try: + if self.polygon(label) != other.polygon(label): + return False + except ValueError: + return False + + if self.base_ring() != other.base_ring(): + return False + if self.is_mutable() != other.is_mutable(): + return False + if self.base_label() != other.base_label(): + return False + + return True + + +class LabelWalker: + r""" + Take a canonical walk around the surface and find the labels of polygons. + + We start at the base_label(). + Then the labels are visited in order involving the combinatorial distance from the base_label(), + where combinatorial distance measures the minimal number of edges which need to be crossed to reach the + polygon with a givel label. Ties are broken using lexicographical order on the numbers associated to edges crossed + (labels are not used in this lexicographical ordering). + """ + + class LabelWalkerIterator: + def __init__(self, label_walker): + self._lw = label_walker + self._i = 0 + + def __next__(self): + if self._i < len(self._lw): + label = self._lw.number_to_label(self._i) + self._i = self._i + 1 + return label + if self._i == len(self._lw): + label = self._lw.find_a_new_label() + if label is None: + raise StopIteration() + self._i = self._i + 1 + return label + raise StopIteration() + + next = __next__ # for Python 2 + + def __iter__(self): + return self + + def __init__(self, surface, deprecation_warning=True): + if deprecation_warning: + import warnings + + warnings.warn( + "LabelWalker has been deprecated and will be removed in a future version of sage-flatsurf; use labels() instead" + ) + + self._s = surface + self._labels = [self._s.root()] + self._label_dict = {self._labels[0]: 0} + + # This will stores an edge to move through to get to a polygon closer to the base_polygon + self._label_edge_back = {self._labels[0]: None} + + from collections import deque + + self._walk = deque() + self._walk.append((self._labels[0], 0)) + + def label_dictionary(self): + r""" + Return a dictionary mapping labels to integers which gives a canonical order on labels. + """ + return self._label_dict + + def edge_back(self, label, limit=None): + r""" + Return the "canonical" edge to walk through to get closer to the base_label, + or None if label already is the base_label. + + Remark: This could be slow on infinite surfaces! + """ + try: + return self._label_edge_back[label] + except KeyError: + if limit is None: + if not self._s.is_finite_type(): + limit = 1000 + else: + limit = self._s.num_polygons() + for i in range(limit): + new_label = self.find_a_new_label() + if label == new_label: + return self._label_edge_back[label] + # Maybe the surface is not connected? + raise KeyError( + "Unable to find label %s. Are you sure the surface is connected?" % (label) + ) + + def __iter__(self): + return LabelWalker.LabelWalkerIterator(self) + + def polygon_iterator(self): + for label in self: + yield self._s.polygon(label) + + def label_polygon_iterator(self): + for label in self: + yield label, self._s.polygon(label) + + def edge_iterator(self, gluings=False): + if gluings: + for entry in self._s.gluings(): + yield entry + return + for label, polygon in self.label_polygon_iterator(): + for e in range(len(polygon.vertices())): + yield label, e + + def __len__(self): + r""" + Return the number of labels found. + """ + return len(self._labels) + + def find_a_new_label(self): + r""" + Finds a new label, stores it, and returns it. Returns None if we have already found all labels. + """ + while len(self._walk) > 0: + label, e = self._walk.popleft() + opposite_label, opposite_edge = self._s.opposite_edge(label, e) + e = e + 1 + if e < len(self._s.polygon(label).vertices()): + self._walk.appendleft((label, e)) + if opposite_label not in self._label_dict: + n = len(self._labels) + self._labels.append(opposite_label) + self._label_dict[opposite_label] = n + self._walk.append((opposite_label, 0)) + self._label_edge_back[opposite_label] = opposite_edge + return opposite_label + return None + + def find_new_labels(self, n): + r""" + Look for n new labels. Return the list of labels found. + """ + new_labels = [] + for i in range(n): + label = self.find_a_new_label() + if label is None: + return new_labels + else: + new_labels.append(label) + return new_labels + + def find_all_labels(self): + if not self._s.is_finite_type(): + raise NotImplementedError + label = self.find_a_new_label() + while label is not None: + label = self.find_a_new_label() + + def number_to_label(self, n): + r""" + Return the n-th label where n is less than the length. + """ + return self._labels[n] + + def label_to_number(self, label, search=False, limit=100): + r""" + Return the number associated to the provided label. + + Returns an error if the label has not already been found by the walker + unless search=True in which case we look for the label. We look by + continuing to look for the label by walking over the surface visiting + the next limit many polygons. + """ + if not search: + return self._label_dict[label] + else: + if label in self._label_dict: + return self._label_dict[label] + for i in range(limit): + new_label = self.find_a_new_label() + if label == new_label: + return self._label_dict[label] + raise ValueError( + "Failed to find label even after searching. limit=" + str(limit) + ) + + def surface(self): + return self._s + + +class ExtraLabel: + r""" + Used to spit out new labels. + """ + _next = int(0) + + def __init__(self, value=None): + r""" + Construct a new label. + """ + if value is None: + self._label = int(ExtraLabel._next) + ExtraLabel._next = ExtraLabel._next + 1 + else: + self._label = value + + def __eq__(self, other): + return isinstance(other, self.__class__) and self._label == other._label + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(23 * self._label) + + def __str__(self): + return "E" + str(self._label) + + def __repr__(self): + return "ExtraLabel(" + str(self._label) + ")" + + +class LabelsView(collections.abc.Set): + def __init__(self, surface): + self._surface = surface + + def __contains__(self, x): + try: + self._surface.polygon(x) + except ValueError: + return False + + return True + + def __iter__(self): + return self._surface.label_iterator() + + def __len__(self): + return self._surface.num_polygons() + + def __repr__(self): + if self._surface.is_finite_type(): + return repr(tuple(self)) + + from itertools import islice + + return f"({', '.join(str(x) for x in islice(self, 16))}, …)" + + +def SurfaceClass(surface, name, category, *args, **kwargs): + category = category.Oriented().Connected().WithoutBoundary() + + message = f"{name} has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore." + + if surface.is_finite_type(): + message += f" Calling set_immutable() on this surface should determine the category of this surface automatically so calling {name} should not be necessary in this case." + else: + message += " For this surface of infinite type, you should create a subclass of OrientedSimilaritySurface and set the category in the __init__ method; see flatsurf.geometry.similarity_surface_generators.EInfinitySurface for an example" + + message += f" You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. We will now refine the category of this surface to make sure that it is in the {category}." + + if args or kwargs: + message += " Note that we are ignoring any other parameters that were passed to this function." + + import warnings + + warnings.warn(message) + + surface._refine_category_(category) + + return surface + + +def SimilaritySurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, SimilaritySurface + sage: S = Surface_list(QQ) + doctest:warning + ... + UserWarning: Surface_list has been deprecated and will be removed in a future version of sage-flatsurf; use MutableOrientedSimilaritySurface instead + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = SimilaritySurface(S) + doctest:warning + ... + UserWarning: SimilaritySurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling SimilaritySurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary oriented similarity surfaces. + sage: S.category() + Category of connected without boundary finite type oriented similarity surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import SimilaritySurfaces + + return SurfaceClass( + surface, "SimilaritySurface()", SimilaritySurfaces(), *args, **kwargs + ) + + +def HalfDilationSurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, HalfDilationSurface + sage: S = Surface_list(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = HalfDilationSurface(S) + doctest:warning + ... + UserWarning: HalfDilationSurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling HalfDilationSurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary oriented dilation surfaces. + sage: S.category() + Category of connected without boundary finite type oriented dilation surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import DilationSurfaces + + return SurfaceClass( + surface, "HalfDilationSurface()", DilationSurfaces(), *args, **kwargs + ) + + +def DilationSurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, DilationSurface + sage: S = Surface_list(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = DilationSurface(S) + doctest:warning + ... + UserWarning: DilationSurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling DilationSurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary oriented positive dilation surfaces. + sage: S.category() + Category of connected without boundary finite type oriented positive dilation surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import DilationSurfaces + + return SurfaceClass( + surface, "DilationSurface()", DilationSurfaces().Positive(), *args, **kwargs + ) + + +def ConeSurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, ConeSurface + sage: S = Surface_list(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = ConeSurface(S) + doctest:warning + ... + UserWarning: ConeSurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling ConeSurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary oriented cone surfaces. + sage: S.category() + Category of connected without boundary finite type oriented cone surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import ConeSurfaces + + return SurfaceClass(surface, "ConeSurface()", ConeSurfaces(), *args, **kwargs) + + +def RationalConeSurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, RationalConeSurface + sage: S = Surface_list(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = RationalConeSurface(S) + doctest:warning + ... + UserWarning: RationalConeSurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling RationalConeSurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary oriented rational cone surfaces. + sage: S.category() + Category of connected without boundary finite type oriented rational cone surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import ConeSurfaces + + return SurfaceClass( + surface, "RationalConeSurface()", ConeSurfaces().Rational(), *args, **kwargs + ) + + +def HalfTranslationSurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, HalfTranslationSurface + sage: S = Surface_list(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = HalfTranslationSurface(S) + doctest:warning + ... + UserWarning: HalfTranslationSurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling HalfTranslationSurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary oriented half translation surfaces. + sage: S.category() + Category of connected without boundary finite type oriented half translation surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import HalfTranslationSurfaces + + return SurfaceClass( + surface, "HalfTranslationSurface()", HalfTranslationSurfaces(), *args, **kwargs + ) + + +def TranslationSurface(surface, *args, **kwargs): + r""" + Refine the category of ``surface``. + + This function is deprecated and should not be used anymore. + + EXAMPLES:: + + sage: from flatsurf import Surface_list, polygons, TranslationSurface + sage: S = Surface_list(QQ) + sage: S.add_polygon(polygons.square()) + 0 + sage: S.set_edge_pairing(0, 0, 0, 2) + sage: S.set_edge_pairing(0, 1, 0, 3) + sage: S = TranslationSurface(S) + doctest:warning + ... + UserWarning: TranslationSurface() has been deprecated and will be removed in a future version of sage-flatsurf; there is no distinction between an (underlying) Surface and the SimilaritySurface types anymore. + Calling set_immutable() on this surface should determine the category of this surface automatically so calling TranslationSurface() should not be necessary in this case. + You can still explicitly refine the category of a surface with _refine_category_() but this is not recommended. + We will now refine the category of this surface to make sure that it is in the Category of connected without boundary translation surfaces. + sage: S.category() + Category of connected without boundary finite type translation surfaces + sage: S.set_immutable() + sage: S.category() + Category of connected without boundary finite type translation surfaces + + """ + from flatsurf.geometry.categories import TranslationSurfaces + + return SurfaceClass( + surface, "TranslationSurface()", TranslationSurfaces(), *args, **kwargs + ) diff --git a/flatsurf/geometry/surface_objects.py b/flatsurf/geometry/surface_objects.py index 2c1686d0b..dccf71c77 100644 --- a/flatsurf/geometry/surface_objects.py +++ b/flatsurf/geometry/surface_objects.py @@ -3,7 +3,7 @@ This includes singularities, saddle connections and cylinders. """ -###################################################################### +# **************************************************************************** # This file is part of sage-flatsurf. # # Copyright (C) 2017-2020 W. Patrick Hooper @@ -22,7 +22,7 @@ # # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . -###################################################################### +# **************************************************************************** from sage.misc.cachefunc import cached_method from sage.modules.free_module_element import vector @@ -30,8 +30,8 @@ from sage.plot.polygon import polygon2d from sage.rings.qqbar import AA from sage.structure.sage_object import SageObject +from sage.structure.element import Element -from flatsurf.geometry.polygon import ConvexPolygons, wedge_product from flatsurf.geometry.similarity import SimilarityGroup @@ -68,19 +68,19 @@ def Singularity(similarity_surface, label, v, limit=None): ) -class SurfacePoint(SageObject): +class SurfacePoint(Element): r""" A point on ``surface``. INPUT: - - ``surface`` -- a :class:`flatsurf.geometry.surface.Surface` or a - :class:`flatsurf.geometry.similarity_surface.SimilaritySurface`. + - ``surface`` -- a similarity surface - ``label`` -- a polygon label for the polygon with respect to which the ``point`` coordinates can be made sense of - - ``point`` -- coordinates of a point in the polygon ``label`` + - ``point`` -- coordinates of a point in the polygon ``label`` or the index + of the vertex of the polygon with ``label`` - ``ring`` -- a SageMath ring or ``None`` (default: ``None``); the coordinate ring for ``point`` @@ -135,9 +135,12 @@ def __init__(self, surface, label, point, ring=None, limit=None): polygon = surface.polygon(label) - from sage.modules.free_module import VectorSpace + from sage.all import ZZ - point = VectorSpace(ring, 2)(point) + if point in ZZ: + point = surface.polygon(label).vertex(point) + + point = (ring**2)(point) point.set_immutable() position = polygon.get_point_position(point) @@ -166,7 +169,7 @@ def __init__(self, surface, label, point, ring=None, limit=None): # Rotate to the next edge that is leaving at the vertex label, source_edge = surface.opposite_edge(label, source_edge) - source_edge = (source_edge + 1) % surface.polygon(label).num_edges() + source_edge = (source_edge + 1) % len(surface.polygon(label).vertices()) if limit is not None: limit -= 1 @@ -182,14 +185,12 @@ def __init__(self, surface, label, point, ring=None, limit=None): self._representatives = frozenset(self._representatives) + super().__init__(surface) + def surface(self): r""" Return the surface containing this point. - Depending on how this point was created, this can be either a - :class:`flatsurf.geometry.surface.Surface` or a - :class:`flatsurf.geometry.similarity_surface.SimilaritySurface`. - EXAMPLES:: sage: from flatsurf import translation_surfaces @@ -541,6 +542,28 @@ def __eq__(self, other): return False return self._representatives == other._representatives + def _test_category(self, **options): + r""" + Check that this point inherits from the element class of its surface's + category. + + Overridden to disable these tests when this is a point of a mutable + surface since the category might then change as the surface becomes + immutable. + + EXAMPLES:: + + sage: from flatsurf import half_translation_surfaces + sage: S = half_translation_surfaces.step_billiard([1, 1, 1, 1], [1, 1/2, 1/3, 1/4]) + sage: p = S.point(0, (1/2, 1/2)) + sage: p._test_category() + + """ + if self.surface().is_mutable(): + return + + super()._test_category(**options) + def __hash__(self): r""" Return a hash value of this point. @@ -645,15 +668,15 @@ def __init__( The combinatorial limit (in terms of number of polygons crossed) to flow forward to check the saddle connection geometry. """ - from .similarity_surface import SimilaritySurface + from flatsurf.geometry.categories import SimilaritySurfaces - if not isinstance(surface, SimilaritySurface): + if surface not in SimilaritySurfaces(): raise TypeError self._surface = surface # Sanitize the direction vector: - V = self._surface.vector_space() + V = self._surface.base_ring().fraction_field() ** 2 self._direction = V(direction) if self._direction == V.zero(): raise ValueError("Direction must be nonzero.") @@ -677,20 +700,21 @@ def __init__( self._surfacetart_data = tuple(start_data) if end_direction is None: - from .half_dilation_surface import HalfDilationSurface - from .dilation_surface import DilationSurface + from flatsurf.geometry.categories import DilationSurfaces # Attempt to infer the end_direction. - if isinstance(self._surface, DilationSurface): + if self._surface in DilationSurfaces().Positive(): end_direction = -self._direction - elif ( - isinstance(self._surface, HalfDilationSurface) and end_data is not None - ): + elif self._surface in DilationSurfaces() and end_data is not None: p = self._surface.polygon(end_data[0]) + from flatsurf.geometry.euclidean import ccw + if ( - wedge_product(p.edge(end_data[1]), self._direction) >= 0 - and wedge_product( - p.edge((p.num_edges() + end_data[1] - 1) % p.num_edges()), + ccw(p.edge(end_data[1]), self._direction) >= 0 + and ccw( + p.edge( + (len(p.vertices()) + end_data[1] - 1) % len(p.vertices()) + ), self._direction, ) > 0 @@ -701,12 +725,14 @@ def __init__( if end_holonomy is None and holonomy is not None: # Attempt to infer the end_holonomy: - from .half_translation_surface import HalfTranslationSurface - from .translation_surface import TranslationSurface + from flatsurf.geometry.categories import ( + HalfTranslationSurfaces, + TranslationSurfaces, + ) - if isinstance(self._surface, TranslationSurface): + if self._surface in TranslationSurfaces(): end_holonomy = -holonomy - if isinstance(self._surface, HalfTranslationSurface): + if self._surface in HalfTranslationSurfaces(): if direction == end_direction: end_holonomy = holonomy else: @@ -859,9 +885,9 @@ def length(self): this may not lie in the field of definition of the surface, it is returned as an element of the Algebraic Real Field. """ - from .cone_surface import ConeSurface + from flatsurf.geometry.categories import ConeSurfaces - if not isinstance(self._surface, ConeSurface): + if self._surface not in ConeSurfaces(): raise NotImplementedError( "length of a saddle connection only makes sense for cone surfaces" ) @@ -1056,7 +1082,7 @@ class Cylinder(SageObject): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.octagon_and_squares() sage: from flatsurf.geometry.surface_objects import Cylinder sage: cyl = Cylinder(s, 0, [2, 3, 3, 3, 2, 0, 1, 3, 2, 0]) @@ -1104,12 +1130,12 @@ def __init__(self, s, label0, edges): ) m = trans.matrix() v = vector(s.base_ring(), (m[0][2], m[1][2])) # translation vector - from flatsurf.geometry.polygon import wedge_product + from flatsurf.geometry.euclidean import ccw p = ss.polygon(labels[0]) e = edges[0] - min_y = wedge_product(v, p.vertex(e)) - max_y = wedge_product(v, p.vertex((e + 1) % p.num_edges())) + min_y = ccw(v, p.vertex(e)) + max_y = ccw(v, p.vertex((e + 1) % len(p.vertices()))) if min_y >= max_y: raise ValueError("Combinatorial data does not represent a cylinder") @@ -1120,7 +1146,7 @@ def __init__(self, s, label0, edges): for i in range(1, len(edges)): e = edges[i] p = ss.polygon(labels[i]) - y = wedge_product(v, p.vertex(e)) + y = ccw(v, p.vertex(e)) if y == min_y: min_list.append(i) elif y > min_y: @@ -1128,7 +1154,7 @@ def __init__(self, s, label0, edges): min_y = y if min_y >= max_y: raise ValueError("Combinatorial data does not represent a cylinder") - y = wedge_product(v, p.vertex((e + 1) % p.num_edges())) + y = ccw(v, p.vertex((e + 1) % len(p.vertices()))) if y == max_y: max_list.append(i) elif y < max_y: @@ -1156,7 +1182,7 @@ def __init__(self, s, label0, edges): lj = labels[j] sc = SaddleConnection( s, - (lio[0][0], (lio[1] + 1) % ss.polygon(lio[0]).num_edges()), + (lio[0][0], (lio[1] + 1) % len(ss.polygon(lio[0]).vertices())), (~lio[0][1])(vert_j) - (~lio[0][1])(vert_i), ) sc_set_right.add(sc) @@ -1170,7 +1196,7 @@ def __init__(self, s, label0, edges): lj = labels[j] sc = SaddleConnection( s, - (lio[0][0], (lio[1] + 1) % ss.polygon(lio[0]).num_edges()), + (lio[0][0], (lio[1] + 1) % len(ss.polygon(lio[0]).vertices())), (~lio[0][1])(vert_j) - (~lio[0][1])(vert_i), limit=j - i, ) @@ -1184,7 +1210,7 @@ def __init__(self, s, label0, edges): for i in max_list: label = labels[i] p = ss.polygon(label) - vertices.append((i, p.vertex((edges[i] + 1) % p.num_edges()))) + vertices.append((i, p.vertex((edges[i] + 1) % len(p.vertices())))) i, vert_i = vertices[-1] vert_i = vert_i - v j, vert_j = vertices[0] @@ -1195,7 +1221,7 @@ def __init__(self, s, label0, edges): lj = labels[j] sc = SaddleConnection( s, - (lj[0], (edges[j] + 1) % ss.polygon(lj).num_edges()), + (lj[0], (edges[j] + 1) % len(ss.polygon(lj).vertices())), (~lj[1])(vert_i) - (~lj[1])(vert_j), ) sc_set_left.add(sc) @@ -1208,7 +1234,7 @@ def __init__(self, s, label0, edges): lj = labels[j] sc = SaddleConnection( s, - (lj[0], (edges[j] + 1) % ss.polygon(lj).num_edges()), + (lj[0], (edges[j] + 1) % len(ss.polygon(lj).vertices())), (~lj[1])(vert_i) - (~lj[1])(vert_j), ) sc_set_left.add(sc) @@ -1226,15 +1252,15 @@ def __init__(self, s, label0, edges): i = max_list[0] label = labels[i] p = ss.polygon(label) - left_point = p.vertex((edges[i] + 1) % p.num_edges()) - from flatsurf.geometry.polygon import solve + left_point = p.vertex((edges[i] + 1) % len(p.vertices())) + from flatsurf.geometry.euclidean import solve for i in range(len(edges)): label = labels[i] p = ss.polygon(label) e = edges[i] v1 = p.vertex(e) - v2 = p.vertex((e + 1) % p.num_edges()) + v2 = p.vertex((e + 1) % len(p.vertices())) a, b = solve(left_point, v, v1, v2 - v1) w1 = (~(label[1]))(v1 + b * (v2 - v1)) a, b = solve(right_point, v, v1, v2 - v1) @@ -1242,7 +1268,6 @@ def __init__(self, s, label0, edges): edge_intersections.append((w1, w2)) polygons = [] - P = ConvexPolygons(s.base_ring()) pair1 = edge_intersections[-1] l1 = labels[-2][0] e1 = edges[-1] @@ -1257,7 +1282,12 @@ def __init__(self, s, label0, edges): polygon_verts.append(pair2[1]) if pair2[0] != pair1p[0]: polygon_verts.append(pair2[0]) - polygons.append((l2, P(vertices=polygon_verts))) + + from flatsurf import Polygon + + polygons.append( + (l2, Polygon(vertices=polygon_verts, base_ring=s.base_ring())) + ) l1 = l2 pair1 = pair2 e1 = e2 @@ -1301,10 +1331,11 @@ def area(self): r""" Return the area of this cylinder if it is contained in a ConeSurface. """ - from .cone_surface import ConeSurface + from flatsurf.geometry.categories import ConeSurfaces - if not isinstance(self._surface, ConeSurface): + if self._surface not in ConeSurfaces(): raise NotImplementedError("area only makes sense for cone surfaces") + area = 0 for label, p in self.polygons(): area += p.area() @@ -1368,13 +1399,13 @@ def next(self, sc): v = sc.end_tangent_vector() v = v.clockwise_to(-v.vector()) - from flatsurf.geometry.polygon import is_same_direction + from flatsurf.geometry.euclidean import is_parallel for sc2 in self._boundary: if sc2.start_data() == ( v.polygon_label(), v.vertex(), - ) and is_same_direction(sc2.direction(), v.vector()): + ) and is_parallel(sc2.direction(), v.vector()): return sc2 raise ValueError("Failed to find next saddle connection in boundary set.") @@ -1387,10 +1418,10 @@ def previous(self, sc): raise ValueError v = sc.start_tangent_vector() v = v.counterclockwise_to(-v.vector()) - from flatsurf.geometry.polygon import is_same_direction + from flatsurf.geometry.euclidean import is_parallel for sc2 in self._boundary: - if sc2.end_data() == (v.polygon_label(), v.vertex()) and is_same_direction( + if sc2.end_data() == (v.polygon_label(), v.vertex()) and is_parallel( sc2.end_direction(), v.vector() ): return sc2 @@ -1402,14 +1433,14 @@ def holonomy(self): In a translation surface, return one of the two holonomy vectors of the cylinder, which differ by a sign. """ - from .translation_surface import TranslationSurface + from flatsurf.geometry.categories import TranslationSurfaces - if not isinstance(self._surface, TranslationSurface): + if self._surface not in TranslationSurfaces(): raise NotImplementedError( "holonomy currently only computable for translation surfaces" ) - V = self._surface.vector_space() + V = self._surface.base_ring() ** 2 total = V.zero() for sc in self._boundary1: total += sc.holonomy() @@ -1432,9 +1463,9 @@ def circumference(self): not lie in the field of definition of the surface, it is returned as an element of the Algebraic Real Field. """ - from .cone_surface import ConeSurface + from flatsurf.geometry.categories import ConeSurfaces - if not isinstance(self._surface, ConeSurface): + if self._surface not in ConeSurfaces(): raise NotImplementedError( "circumference only makes sense for cone surfaces" ) diff --git a/flatsurf/geometry/tangent_bundle.py b/flatsurf/geometry/tangent_bundle.py index 1b0a505f0..946a586e5 100644 --- a/flatsurf/geometry/tangent_bundle.py +++ b/flatsurf/geometry/tangent_bundle.py @@ -18,7 +18,6 @@ # You should have received a copy of the GNU General Public License # along with sage-flatsurf. If not, see . # ******************************************************************** -from .polygon import wedge_product, is_same_direction, is_opposite_direction # Limit for clockwise_to and counter_clockwise_to in SimilaritySurfaceTangentVector. rotate_limit = 100 @@ -30,52 +29,59 @@ class SimilaritySurfaceTangentVector: EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces Examples on edges in direction of edges:: sage: s = translation_surfaces.square_torus() - sage: for y in [0,1]: - ....: for d in [1,-1]: - ....: print(s.tangent_vector(0, (1/2, y), (d, 0))) + sage: s.tangent_vector(0, (1/2, 0), (1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (1/2, 0) with vector (1, 0) + sage: s.tangent_vector(0, (1/2, 0), (-1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (1/2, 1) with vector (-1, 0) + sage: s.tangent_vector(0, (1/2, 1), (1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (1/2, 0) with vector (1, 0) + sage: s.tangent_vector(0, (1/2, 1), (-1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (1/2, 1) with vector (-1, 0) - sage: for x in [0,1]: - ....: for d in [1,-1]: - ....: print(s.tangent_vector(0, (x, 1/2), (0, d))) + sage: s.tangent_vector(0, (0, 1/2), (0, 1)) SimilaritySurfaceTangentVector in polygon 0 based at (1, 1/2) with vector (0, 1) + sage: s.tangent_vector(0, (0, 1/2), (0, -1)) SimilaritySurfaceTangentVector in polygon 0 based at (0, 1/2) with vector (0, -1) + sage: s.tangent_vector(0, (1, 1/2), (0, 1)) SimilaritySurfaceTangentVector in polygon 0 based at (1, 1/2) with vector (0, 1) + sage: s.tangent_vector(0, (1, 1/2), (0, -1)) SimilaritySurfaceTangentVector in polygon 0 based at (0, 1/2) with vector (0, -1) Examples on vertices in direction of edges:: sage: s = translation_surfaces.square_torus() - sage: for y in [0,1]: - ....: print(s.tangent_vector(0, (0, y), (1, 0))) - ....: print(s.tangent_vector(0, (1, y), (-1, 0))) + sage: s.tangent_vector(0, (0, 0), (1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (0, 0) with vector (1, 0) + sage: s.tangent_vector(0, (1, 0), (-1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (1, 1) with vector (-1, 0) + sage: s.tangent_vector(0, (0, 1), (1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (0, 0) with vector (1, 0) + sage: s.tangent_vector(0, (1, 1), (-1, 0)) SimilaritySurfaceTangentVector in polygon 0 based at (1, 1) with vector (-1, 0) - sage: for x in [0,1]: - ....: print(s.tangent_vector(0, (x, 0), (0, 1))) - ....: print(s.tangent_vector(0, (x, 1), (0, -1))) + sage: s.tangent_vector(0, (0, 0), (0, 1)) SimilaritySurfaceTangentVector in polygon 0 based at (1, 0) with vector (0, 1) + sage: s.tangent_vector(0, (0, 1), (0, -1)) SimilaritySurfaceTangentVector in polygon 0 based at (0, 1) with vector (0, -1) + sage: s.tangent_vector(0, (1, 0), (0, 1)) SimilaritySurfaceTangentVector in polygon 0 based at (1, 0) with vector (0, 1) + sage: s.tangent_vector(0, (1, 1), (0, -1)) SimilaritySurfaceTangentVector in polygon 0 based at (0, 1) with vector (0, -1) + """ def __init__(self, tangent_bundle, polygon_label, point, vector): + from flatsurf.geometry.euclidean import ccw, is_anti_parallel + self._bundle = tangent_bundle p = self.surface().polygon(polygon_label) pos = p.get_point_position(point) - if vector == self._bundle.vector_space().zero(): + if not vector: raise NotImplementedError("vector must be non-zero") if pos.is_in_interior(): self._polygon_label = polygon_label @@ -85,9 +91,8 @@ def __init__(self, tangent_bundle, polygon_label, point, vector): elif pos.is_in_edge_interior(): e = pos.get_edge() edge_v = p.edge(e) - if wedge_product(edge_v, vector) < 0 or is_opposite_direction( - edge_v, vector - ): + + if ccw(edge_v, vector) < 0 or is_anti_parallel(edge_v, vector): # Need to move point and vector to opposite edge. label2, e2 = self.surface().opposite_edge(polygon_label, e) similarity = self.surface().edge_transformation(polygon_label, e) @@ -110,9 +115,9 @@ def __init__(self, tangent_bundle, polygon_label, point, vector): # subsequent edge: edge1 = p.edge(v) # prior edge: - edge0 = p.edge((v - 1) % p.num_edges()) - wp1 = wedge_product(edge1, vector) - wp0 = wedge_product(edge0, vector) + edge0 = p.edge((v - 1) % len(p.vertices())) + wp1 = ccw(edge1, vector) + wp0 = ccw(edge0, vector) if wp1 < 0 or wp0 < 0: raise ValueError( "Singular point with vector pointing away from polygon" @@ -120,10 +125,10 @@ def __init__(self, tangent_bundle, polygon_label, point, vector): if wp0 == 0: # vector points backward along edge 0 label2, e2 = self.surface().opposite_edge( - polygon_label, (v - 1) % p.num_edges() + polygon_label, (v - 1) % len(p.vertices()) ) similarity = self.surface().edge_transformation( - polygon_label, (v - 1) % p.num_edges() + polygon_label, (v - 1) % len(p.vertices()) ) point2 = similarity(point) vector2 = similarity.derivative() * vector @@ -273,10 +278,12 @@ def differs_by_scaling(self, another_tangent_vector): Returns true if the other vector just differs by scaling. This means they should lie in the same polygon, be based at the same point, and point in the same direction. """ + from flatsurf.geometry.euclidean import is_parallel + return ( self.polygon_label() == another_tangent_vector.polygon_label() and self.point() == another_tangent_vector.point() - and is_same_direction(self.vector(), another_tangent_vector.vector()) + and is_parallel(self.vector(), another_tangent_vector.vector()) ) def invert(self): @@ -307,19 +314,19 @@ def forward_to_polygon_boundary(self): sage: s = SimilaritySurfaceGenerators.example() sage: from flatsurf.geometry.tangent_bundle import SimilaritySurfaceTangentBundle sage: tb = SimilaritySurfaceTangentBundle(s) - sage: print("Polygon 0 is "+str(s.polygon(0))) - Polygon 0 is Polygon: (0, 0), (2, -2), (2, 0) - sage: print("Polygon 1 is "+str(s.polygon(1))) - Polygon 1 is Polygon: (0, 0), (2, 0), (1, 3) + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) + sage: s.polygon(1) + Polygon(vertices=[(0, 0), (2, 0), (1, 3)]) sage: from flatsurf.geometry.tangent_bundle import SimilaritySurfaceTangentVector - sage: V = tb.surface().vector_space() + sage: V = tb.surface().base_ring()**2 sage: v = SimilaritySurfaceTangentVector(tb, 0, V((0,0)), V((3,-1))) - sage: print(v) + sage: v SimilaritySurfaceTangentVector in polygon 0 based at (0, 0) with vector (3, -1) sage: v2 = v.forward_to_polygon_boundary() - sage: print(v2) + sage: v2 SimilaritySurfaceTangentVector in polygon 0 based at (2, -2/3) with vector (-3, 1) - sage: print(v2.invert()) + sage: v2.invert() SimilaritySurfaceTangentVector in polygon 1 based at (2/3, 2) with vector (4, -3) """ p = self.polygon() @@ -336,7 +343,7 @@ def straight_line_trajectory(self): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s = translation_surfaces.square_torus() sage: v = s.tangent_vector(0, (0,0), (1,1)) @@ -381,7 +388,7 @@ def clockwise_to(self, w, code=False): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s=translation_surfaces.regular_octagon() sage: v=s.tangent_vector(0,(0,0),(1,1)) sage: v.clockwise_to((-1,-1)) @@ -391,7 +398,7 @@ def clockwise_to(self, w, code=False): sage: v.clockwise_to((1,1), code=True) (SimilaritySurfaceTangentVector in polygon 0 based at (-1/2*a, 1/2*a) with vector (1, 1), [0, 5, 2]) """ - if w == self.surface().vector_space().zero(): + if not w: raise ValueError("w must be non-zero") if self.is_based_at_singularity(): @@ -405,8 +412,11 @@ def clockwise_to(self, w, code=False): der = Matrix(s.base_ring(), [[1, 0], [0, 1]]) if code: codes = [] + + from flatsurf.geometry.euclidean import ccw + for count in range(rotate_limit): - if wedge_product(v2, w) >= 0 and wedge_product(w, v1) > 0: + if ccw(v2, w) >= 0 and ccw(w, v1) > 0: # We've found it! break if code: @@ -415,7 +425,7 @@ def clockwise_to(self, w, code=False): der = der * s.edge_matrix(label2, edge2) v1 = der * (-s.polygon(label2).edge(edge2)) label = label2 - vertex = (edge2 + 1) % s.polygon(label2).num_edges() + vertex = (edge2 + 1) % len(s.polygon(label2).vertices()) v2 = der * (s.polygon(label2).edge(vertex)) assert count < rotate_limit, "Reached limit!" if code: @@ -457,7 +467,7 @@ def counterclockwise_to(self, w, code=False): EXAMPLES:: - sage: from flatsurf import * + sage: from flatsurf import translation_surfaces sage: s=translation_surfaces.regular_octagon() sage: v=s.tangent_vector(0,(0,0),(1,1)) sage: v.counterclockwise_to((-1,-1)) @@ -467,7 +477,7 @@ def counterclockwise_to(self, w, code=False): sage: v.counterclockwise_to((1,1), code=True) (SimilaritySurfaceTangentVector in polygon 0 based at (1, 0) with vector (1, 1), [7, 2, 5]) """ - if w == self.surface().vector_space().zero(): + if not w: raise ValueError("w must be non-zero") if self.is_based_at_singularity(): @@ -475,16 +485,19 @@ def counterclockwise_to(self, w, code=False): v1 = self.vector() label = self.polygon_label() vertex = self.vertex() - previous_vertex = (vertex - 1 + s.polygon(label).num_edges()) % s.polygon( - label - ).num_edges() + previous_vertex = (vertex - 1 + len(s.polygon(label).vertices())) % len( + s.polygon(label).vertices() + ) v2 = -s.polygon(label).edge(previous_vertex) from sage.matrix.constructor import Matrix der = Matrix(s.base_ring(), [[1, 0], [0, 1]]) if code: codes = [] - if not (wedge_product(v1, w) > 0 and wedge_product(w, v2) > 0): + + from flatsurf.geometry.euclidean import ccw + + if not (ccw(v1, w) > 0 and ccw(w, v2) > 0): for count in range(rotate_limit): label2, edge2 = s.opposite_edge(label, previous_vertex) if code: @@ -493,11 +506,11 @@ def counterclockwise_to(self, w, code=False): label = label2 vertex = edge2 previous_vertex = ( - vertex - 1 + s.polygon(label).num_edges() - ) % s.polygon(label).num_edges() + vertex - 1 + len(s.polygon(label).vertices()) + ) % len(s.polygon(label).vertices()) v1 = der * (s.polygon(label).edge(vertex)) v2 = der * (-s.polygon(label).edge(previous_vertex)) - if wedge_product(v1, w) >= 0 and wedge_product(w, v2) > 0: + if ccw(v1, w) >= 0 and ccw(w, v2) > 0: # We've found it! break assert count < rotate_limit, "Reached limit!" @@ -596,9 +609,9 @@ def edge(self, polygon_label, edge_index): sage: s = SimilaritySurfaceGenerators.example() sage: from flatsurf.geometry.tangent_bundle import SimilaritySurfaceTangentBundle sage: tb = SimilaritySurfaceTangentBundle(s) - sage: print(s.polygon(0)) - Polygon: (0, 0), (2, -2), (2, 0) - sage: print(tb.edge(0,0)) + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) + sage: tb.edge(0,0) SimilaritySurfaceTangentVector in polygon 0 based at (0, 0) with vector (2, -2) """ polygon = self.surface().polygon(polygon_label) @@ -618,13 +631,13 @@ def clockwise_edge(self, polygon_label, edge_index): sage: s = SimilaritySurfaceGenerators.example() sage: from flatsurf.geometry.tangent_bundle import SimilaritySurfaceTangentBundle sage: tb = SimilaritySurfaceTangentBundle(s) - sage: print("Polygon 0 is "+str(s.polygon(0))) - Polygon 0 is Polygon: (0, 0), (2, -2), (2, 0) - sage: print("Polygon 1 is "+str(s.polygon(1))) - Polygon 1 is Polygon: (0, 0), (2, 0), (1, 3) - sage: print("Opposite edge to (0,0) is "+repr(s.opposite_edge(0,0))) - Opposite edge to (0,0) is (1, 1) - sage: print(tb.clockwise_edge(0,0)) + sage: s.polygon(0) + Polygon(vertices=[(0, 0), (2, -2), (2, 0)]) + sage: s.polygon(1) + Polygon(vertices=[(0, 0), (2, 0), (1, 3)]) + sage: s.opposite_edge(0, 0) + (1, 1) + sage: tb.clockwise_edge(0,0) SimilaritySurfaceTangentVector in polygon 1 based at (2, 0) with vector (-1, 3) """ polygon = self.surface().polygon(polygon_label) diff --git a/flatsurf/geometry/thurston_veech.py b/flatsurf/geometry/thurston_veech.py index f630ca673..120a9788b 100644 --- a/flatsurf/geometry/thurston_veech.py +++ b/flatsurf/geometry/thurston_veech.py @@ -45,10 +45,6 @@ from surface_dynamics.flat_surfaces.origamis.origami import Origami from surface_dynamics.misc.permutation import perm_dense_cycles -from .polygon import ConvexPolygons -from .surface import Surface_list -from .translation_surface import TranslationSurface - class ThurstonVeech: def __init__(self, hp, vp): @@ -70,9 +66,7 @@ def __init__(self, hp, vp): sage: S = TV([1,2], [3,1,1]) sage: S - TranslationSurface built from 4 polygons - sage: S.stratum() - H_2(1^2) + Translation Surface in H_2(1^2) built from 4 rectangles sage: S.base_ring() Number Field in a with defining polynomial x^2 - 2 with a = 1.414213562373095? @@ -80,10 +74,10 @@ def __init__(self, hp, vp): sage: S.base_ring() Rational Field """ - o = self._o = Origami(hp, vp) - n = o.nb_squares() - hcycles, hsizes = perm_dense_cycles(o.r_tuple(), n) - vcycles, vsizes = perm_dense_cycles(o.u_tuple(), n) + self._origami = Origami(hp, vp) + n = self._origami.nb_squares() + hcycles, hsizes = perm_dense_cycles(self._origami.r_tuple(), n) + vcycles, vsizes = perm_dense_cycles(self._origami.u_tuple(), n) self._hcycles = hcycles self._vcycles = vcycles self._num_hcyls = len(hsizes) @@ -95,15 +89,15 @@ def __init__(self, hp, vp): def __repr__(self): return 'ThurstonVeech("{}", "{}")'.format( - self._o.r().cycle_string(singletons=True), - self._o.u().cycle_string(singletons=True), + self._origami.r().cycle_string(singletons=True), + self._origami.u().cycle_string(singletons=True), ) def stratum(self): - return self._o.stratum() + return self._origami.stratum() def stratum_component(self): - return self._o.stratum_component() + return self._origami.stratum_component() def cylinder_intersection_matrix(self): return self._E @@ -165,20 +159,23 @@ def __call__(self, hmult, vmult): h = [hcirc[i] * hmult[i] / c for i in range(self._num_hcyls)] v = [vcirc[i] * vmult[i] / d for i in range(self._num_vcyls)] - C = ConvexPolygons(K) + from flatsurf import Polygon + P = [] - for i in range(self._o.nb_squares()): + for i in range(self._origami.nb_squares()): hi = h[self._hcycles[i]] vi = v[self._vcycles[i]] - P.append(C(edges=[(vi, 0), (0, hi), (-vi, 0), (0, -hi)])) + P.append(Polygon(edges=[(vi, 0), (0, hi), (-vi, 0), (0, -hi)], base_ring=K)) + + from flatsurf.geometry.surface import MutableOrientedSimilaritySurface - surface = Surface_list(base_ring=K) + surface = MutableOrientedSimilaritySurface(K) for p in P: surface.add_polygon(p) - r = self._o.r_tuple() - u = self._o.u_tuple() - for i in range(self._o.nb_squares()): - surface.set_edge_pairing(i, 1, r[i], 3) - surface.set_edge_pairing(i, 0, u[i], 2) + r = self._origami.r_tuple() + u = self._origami.u_tuple() + for i in range(self._origami.nb_squares()): + surface.glue((i, 1), (r[i], 3)) + surface.glue((i, 0), (u[i], 2)) surface.set_immutable() - return TranslationSurface(surface) + return surface diff --git a/flatsurf/geometry/translation_surface.py b/flatsurf/geometry/translation_surface.py deleted file mode 100644 index 771171759..000000000 --- a/flatsurf/geometry/translation_surface.py +++ /dev/null @@ -1,806 +0,0 @@ -r""" -Translation Surfaces. -""" - -from sage.matrix.constructor import identity_matrix - -from .surface import Surface -from .half_translation_surface import HalfTranslationSurface -from .dilation_surface import DilationSurface - - -class TranslationSurface(HalfTranslationSurface, DilationSurface): - r""" - A surface with a flat metric and conical singularities whose cone angles are a multiple of pi. - """ - - def minimal_translation_cover(self): - return self - - def _test_edge_matrix(self, **options): - r""" - Check the compatibility condition - """ - tester = self._tester(**options) - - from flatsurf.geometry.similarity_surface import SimilaritySurface - - if self.is_finite(): - it = self.label_iterator() - else: - from itertools import islice - - it = islice(self.label_iterator(), 30) - - for lab in it: - p = self.polygon(lab) - for e in range(p.num_edges()): - # Warning: check the matrices computed from the edges, - # rather the ones overridden by TranslationSurface. - m = SimilaritySurface.edge_matrix(self, lab, e) - tester.assertTrue( - m.is_one(), - "edge_matrix of edge " + str((lab, e)) + " is not a translation.", - ) - - def edge_matrix(self, p, e=None): - if e is None: - p, e = p - if e < 0 or e >= self.polygon(p).num_edges(): - raise ValueError - return identity_matrix(self.base_ring(), 2) - - def stratum(self): - r""" - Return the stratum this surface belongs to. - - This uses the package ``surface-dynamics`` - (see http://www.labri.fr/perso/vdelecro/flatsurf_sage.html) - - EXAMPLES:: - - sage: import flatsurf.geometry.similarity_surface_generators as sfg - sage: sfg.translation_surfaces.octagon_and_squares().stratum() - H_3(4) - """ - from surface_dynamics import AbelianStratum - from sage.rings.integer_ring import ZZ - - return AbelianStratum([ZZ(a - 1) for a in self.angles()]) - - def standardize_polygons(self, in_place=False): - r""" - Replaces each polygon with a polygon with a new polygon which differs by translation - and reindexing. The new polygon will have the property that vertex zero is the origin, - and all vertices lie either in the upper half plane, or on the x-axis with non-negative - x-coordinate. - - This is done to the current surface if in_place=True. A mutable copy is created and returned - if in_place=False (as default). - - EXAMPLES:: - - sage: from flatsurf import * - sage: s=translation_surfaces.veech_double_n_gon(4) - sage: s.polygon(1) - Polygon: (0, 0), (-1, 0), (-1, -1), (0, -1) - sage: [s.opposite_edge(0,i) for i in range(4)] - [(1, 0), (1, 1), (1, 2), (1, 3)] - sage: ss=s.standardize_polygons() - sage: ss.polygon(1) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - sage: [ss.opposite_edge(0,i) for i in range(4)] - [(1, 2), (1, 3), (1, 0), (1, 1)] - sage: TestSuite(ss).run() - - Make sure first vertex is sent to origin:: - sage: from flatsurf import * - sage: P = ConvexPolygons(QQ) - sage: p = P(vertices = ([(1,1),(2,1),(2,2),(1,2)])) - sage: s = Surface_list(QQ) - sage: s.add_polygon(p) - 0 - sage: s.change_polygon_gluings(0, [(0,2),(0,3),(0,0),(0,1)]) - sage: s.change_base_label(0) - sage: ts = TranslationSurface(s) - sage: ts.standardize_polygons().polygon(0) - Polygon: (0, 0), (1, 0), (1, 1), (0, 1) - """ - if self.is_finite(): - if in_place: - if not self.is_mutable(): - raise ValueError( - "An in_place call for standardize_polygons can only be done for a mutable surface." - ) - s = self - else: - s = self.copy(mutable=True) - cv = {} # dictionary for non-zero canonical vertices - translations = ( - {} - ) # translations bringing the canonical vertex to the origin. - for label, polygon in s.label_iterator(polygons=True): - best = 0 - best_pt = polygon.vertex(best) - for v in range(1, polygon.num_edges()): - pt = polygon.vertex(v) - if (pt[1] < best_pt[1]) or ( - pt[1] == best_pt[1] and pt[0] < best_pt[0] - ): - best = v - best_pt = pt - # We replace the polygon if the best vertex is not the zero vertex, or - # if the coordinates of the best vertex differs from the origin. - if not (best == 0 and best_pt.is_zero()): - cv[label] = best - for label, v in cv.items(): - s.set_vertex_zero(label, v, in_place=True) - return s - else: - if in_place: - raise NotImplementedError( - "in place standardization only available for finite surfaces" - ) - - return TranslationSurface(LazyStandardizedPolygonSurface(self)) - - def cmp(self, s2, limit=None): - r""" - Compare two surfaces. This is an ordering returning -1, 0, or 1. - - The surfaces will be considered equal if and only if there is a translation automorphism - respecting the polygons and the base_labels. - - If the two surfaces are infinite, we just examine the first limit polygons. - """ - if self.is_finite(): - if s2.is_finite(): - if limit is not None: - raise ValueError("limit only enabled for finite surfaces") - - # print("comparing number of polygons") - sign = self.num_polygons() - s2.num_polygons() - if sign > 0: - return 1 - if sign < 0: - return -1 - # print("comparing polygons") - lw1 = self.walker() - lw2 = s2.walker() - for p1, p2 in zip(lw1.polygon_iterator(), lw2.polygon_iterator()): - # Uses Polygon.cmp: - ret = p1.cmp(p2) - if ret != 0: - return ret - # Polygons are identical. Compare edge gluings. - # print("comparing edge gluings") - for pair1, pair2 in zip(lw1.edge_iterator(), lw2.edge_iterator()): - l1, e1 = self.opposite_edge(pair1) - l2, e2 = s2.opposite_edge(pair2) - num1 = lw1.label_to_number(l1) - num2 = lw2.label_to_number(l2) - ret = (num1 > num2) - (num1 < num2) - if ret: - return ret - ret = (e1 > e2) - (e1 < e2) - if ret: - return ret - return 0 - else: - # s1 is finite but s2 is infinite. - return -1 - else: - if s2.is_finite(): - # s1 is infinite but s2 is finite. - return 1 - else: - # both surfaces are infinite. - lw1 = self.walker() - lw2 = s2.walker() - count = 0 - for (l1, p1), (l2, p2) in zip( - lw1.label_polygon_iterator(), lw2.label_polygon_iterator() - ): - # Uses Polygon.cmp: - ret = p1.cmp(p2) - if ret != 0: - print("Polygons differ") - return ret - # If here the number of edges should be equal. - for e in range(p1.num_edges()): - ll1, ee1 = self.opposite_edge(l1, e) - ll2, ee2 = s2.opposite_edge(l2, e) - num1 = lw1.label_to_number(ll1, search=True, limit=limit) - num2 = lw2.label_to_number(ll2, search=True, limit=limit) - ret = (num1 > num2) - (num1 < num2) - if ret: - return ret - ret = (ee1 > ee2) - (ee1 < ee2) - if ret: - return ret - if count >= limit: - break - count += 1 - return 0 - - # TODO: deprecation - cmp_translation_surface = cmp - - def canonicalize_mapping(self): - r""" - Return a SurfaceMapping canonicalizing this translation surface. - """ - from flatsurf.geometry.mappings import ( - canonicalize_translation_surface_mapping, - IdentityMapping, - ) - - return canonicalize_translation_surface_mapping(self) - - def canonicalize(self, in_place=False): - r""" - Return a canonical version of this translation surface. - - EXAMPLES: - - We will check if an element lies in the Veech group:: - - sage: from flatsurf import * - sage: s = translation_surfaces.octagon_and_squares() - sage: s - TranslationSurface built from 3 polygons - sage: a = s.base_ring().gen() - sage: mat = Matrix([[1,2+a],[0,1]]) - sage: s1 = s.canonicalize() - sage: s1.underlying_surface().set_immutable() - sage: s2 = (mat*s).canonicalize() - sage: s2.underlying_surface().set_immutable() - sage: s1.cmp(s2) == 0 - True - sage: hash(s1) == hash(s2) - True - """ - # Old version - # return self.canonicalize_mapping().codomain() - if in_place: - if not self.is_mutable(): - raise ValueError( - "canonicalize with in_place=True is only defined for mutable translation surfaces." - ) - s = self - else: - s = self.copy(mutable=True) - if not s.is_finite(): - raise ValueError( - "canonicalize is only defined for finite translation surfaces." - ) - ret = s.delaunay_decomposition(in_place=True) - s.standardize_polygons(in_place=True) - ss = s.copy(mutable=True) - labels = {label for label in s.label_iterator()} - labels.remove(s.base_label()) - for label in labels: - ss.underlying_surface().change_base_label(label) - if ss.cmp(s) > 0: - s.underlying_surface().change_base_label(label) - # We now have the base_label correct. - # We will use the label walker to generate the canonical labeling of polygons. - w = s.walker() - w.find_all_labels() - s.relabel(w.label_dictionary(), in_place=True) - # Set immutable - s.underlying_surface().set_immutable() - return s - - def rel_deformation(self, deformation, local=False, limit=100): - r""" - Perform a rel deformation of the surface and return the result. - - This algorithm currently assumes that all polygons affected by this deformation are - triangles. That should be fixable in the future. - - INPUT: - - - ``deformation`` (dictionary) - A dictionary mapping singularities of - the surface to deformation vectors (in some 2-dimensional vector - space). The rel deformation being done will move the singularities - (relative to each other) linearly to the provided vector for each - vertex. If a singularity is not included in the dictionary then the - vector will be treated as zero. - - - ``local`` - (boolean) - If true, the algorithm attempts to deform all - the triangles making up the surface without destroying any of them. - So, the area of the triangle must be positive along the full interval - of time of the deformation. If false, then the deformation must have - a particular form: all vectors for the deformation must be parallel. - In this case we achieve the deformation with the help of the SL(2,R) - action and Delaunay triangulations. - - - ``limit`` (integer) - Restricts the length of the size of SL(2,R) - deformations considered. The algorithm should be roughly worst time - linear in limit. - - .. TODO:: - - - Support arbitrary rel deformations. - - Remove the requirement that triangles be used. - - EXAMPLES:: - - sage: from flatsurf import * - sage: s = translation_surfaces.arnoux_yoccoz(4) - sage: field = s.base_ring() - sage: a = field.gen() - sage: V = VectorSpace(field,2) - sage: deformation1 = {s.singularity(0,0):V((1,0))} - doctest:warning - ... - UserWarning: Singularity() is deprecated and will be removed in a future version of sage-flatsurf. Use surface.point() instead. - sage: s1 = s.rel_deformation(deformation1).canonicalize() - sage: deformation2 = {s.singularity(0,0):V((a,0))} - sage: s2 = s.rel_deformation(deformation2).canonicalize() - sage: m = Matrix([[a,0],[0,~a]]) - sage: s2.cmp((m*s1).canonicalize()) - 0 - """ - s = self - # Find a common field - field = s.base_ring() - for singularity, v in deformation.items(): - if v.parent().base_field() != field: - from sage.structure.element import get_coercion_model - - cm = get_coercion_model() - field = cm.common_parent(field, v.parent().base_field()) - from sage.modules.free_module import VectorSpace - - vector_space = VectorSpace(field, 2) - - from collections import defaultdict - - vertex_deformation = defaultdict( - vector_space.zero - ) # dictionary associating the vertices. - deformed_labels = set() # list of polygon labels being deformed. - - for singularity, vect in deformation.items(): - for label, coordinates in singularity.representatives(): - v = self.polygon(label).get_point_position(coordinates).get_vertex() - vertex_deformation[(label, v)] = vect - deformed_labels.add(label) - assert s.polygon(label).num_edges() == 3 - - from flatsurf.geometry.polygon import wedge_product, ConvexPolygons - - if local: - ss = s.copy(mutable=True, new_field=field) - us = ss.underlying_surface() - - P = ConvexPolygons(field) - for label in deformed_labels: - polygon = s.polygon(label) - a0 = vector_space(polygon.vertex(1)) - b0 = vector_space(polygon.vertex(2)) - v0 = vector_space(vertex_deformation[(label, 0)]) - v1 = vector_space(vertex_deformation[(label, 1)]) - v2 = vector_space(vertex_deformation[(label, 2)]) - a1 = v1 - v0 - b1 = v2 - v0 - # We deform by changing the triangle so that its vertices 1 and 2 have the form - # a0+t*a1 and b0+t*b1 - # respectively. We are deforming from t=0 to t=1. - # We worry that the triangle degenerates along the way. - # The area of the deforming triangle has the form - # A0 + A1*t + A2*t^2. - A0 = wedge_product(a0, b0) - A1 = wedge_product(a0, b1) + wedge_product(a1, b0) - A2 = wedge_product(a1, b1) - if A2 != field.zero(): - # Critical point of area function - c = A1 / (-2 * A2) - if field.zero() < c and c < field.one(): - if A0 + A1 * c + A2 * c**2 <= field.zero(): - raise ValueError( - "Triangle with label %r degenerates at critical point before endpoint" - % label - ) - if A0 + A1 + A2 <= field.zero(): - raise ValueError( - "Triangle with label %r degenerates at or before endpoint" - % label - ) - # Triangle does not degenerate. - us.change_polygon( - label, P(vertices=[vector_space.zero(), a0 + a1, b0 + b1]) - ) - return ss - - else: # Non local deformation - # We can only do this deformation if all the rel vector are parallel. - # Check for this. - nonzero = None - for singularity, vect in deformation.items(): - vvect = vector_space(vect) - if vvect != vector_space.zero(): - if nonzero is None: - nonzero = vvect - else: - assert ( - wedge_product(nonzero, vvect) == 0 - ), "In non-local deformation all deformation vectos must be parallel" - assert nonzero is not None, "Deformation appears to be trivial." - from sage.matrix.constructor import Matrix - - m = Matrix([[nonzero[0], -nonzero[1]], [nonzero[1], nonzero[0]]]) - mi = ~m - g = Matrix([[1, 0], [0, 2]], ring=field) - prod = m * g * mi - ss = None - k = 0 - while True: - if ss is None: - ss = s.copy(mutable=True, new_field=field) - else: - # In place matrix deformation - ss.apply_matrix(prod) - ss.delaunay_triangulation(direction=nonzero, in_place=True) - deformation2 = {} - for singularity, vect in deformation.items(): - found_start = None - for label, coordinates in singularity.representatives(): - v = ( - s.polygon(label) - .get_point_position(coordinates) - .get_vertex() - ) - if ( - wedge_product(s.polygon(label).edge(v), nonzero) >= 0 - and wedge_product( - nonzero, -s.polygon(label).edge((v + 2) % 3) - ) - > 0 - ): - found_start = (label, v) - found = None - for vv in range(3): - if ( - wedge_product(ss.polygon(label).edge(vv), nonzero) - >= 0 - and wedge_product( - nonzero, -ss.polygon(label).edge((vv + 2) % 3) - ) - > 0 - ): - found = vv - deformation2[ss.singularity(label, vv)] = vect - break - assert found is not None - break - assert found_start is not None - try: - sss = ss.rel_deformation(deformation2, local=True) - except ValueError: - k += 1 - if limit is not None and k >= limit: - raise Exception("exceeded limit iterations") - continue - - sss.apply_matrix(mi * g ** (-k) * m) - sss.delaunay_triangulation(direction=nonzero, in_place=True) - return sss - - def j_invariant(self): - r""" - Return the Kenyon-Smillie J-invariant of this translation surface. - - It is assumed that the coordinates are defined over a number field. - - EXAMPLES:: - - sage: from flatsurf import * - sage: O = translation_surfaces.regular_octagon() - sage: O.j_invariant() - ( - [2 2] - (0), (0), [2 1] - ) - """ - it = self.label_iterator() - lab = next(it) - P = self.polygon(lab) - Jxx, Jyy, Jxy = P.j_invariant() - for lab in it: - xx, yy, xy = self.polygon(lab).j_invariant() - Jxx += xx - Jyy += yy - Jxy += xy - return (Jxx, Jyy, Jxy) - - def erase_marked_points(self): - r""" - Return an isometric or similar surface with a minimal number of regular - vertices of angle 2π. - - EXAMPLES:: - - sage: import flatsurf - - sage: G = SymmetricGroup(4) - sage: S = flatsurf.translation_surfaces.origami(G('(1,2,3,4)'), G('(1,4,2,3)')) - sage: S.stratum() - H_2(2, 0) - sage: S.erase_marked_points().stratum() # optional: pyflatsurf # long time (1s) # random output due to matplotlib warnings with some combinations of setuptools and matplotlib - H_2(2) - - sage: for (a,b,c) in [(1,4,11), (1,4,15), (3,4,13)]: # long time (10s), optional: pyflatsurf - ....: T = flatsurf.polygons.triangle(a,b,c) - ....: S = flatsurf.similarity_surfaces.billiard(T) - ....: S = S.minimal_cover("translation") - ....: print(S.erase_marked_points().stratum()) - H_6(10) - H_6(2^5) - H_8(12, 2) - - If the surface had no marked points then it is returned unchanged by this - function:: - - sage: O = flatsurf.translation_surfaces.regular_octagon() - sage: O.erase_marked_points() is O - True - - TESTS: - - Verify that https://github.com/flatsurf/flatsurf/issues/263 has been resolved:: - - sage: from flatsurf import EquiangularPolygons, similarity_surfaces - sage: E = EquiangularPolygons((10, 8, 3, 1, 1, 1)) - sage: P = E((1, 1, 2, 4), normalized=True) - sage: B = similarity_surfaces.billiard(P, rational=True) - sage: S = B.minimal_cover(cover_type="translation") - sage: S = S.erase_marked_points() # long time (3s), optional: pyflatsurf - - :: - - sage: from flatsurf import EquiangularPolygons, similarity_surfaces - sage: E = EquiangularPolygons((10, 7, 2, 2, 2, 1)) - sage: P = E((1, 1, 2, 3), normalized=True) - sage: B = similarity_surfaces.billiard(P, rational=True) - sage: S_mp = B.minimal_cover(cover_type="translation") - sage: S = S_mp.erase_marked_points() # long time (3s), optional: pyflatsurf - - """ - if all(a != 1 for a in self.angles()): - # no 2π angle - return self - from .pyflatsurf_conversion import from_pyflatsurf, to_pyflatsurf - - S = to_pyflatsurf(self) - S.delaunay() - S = S.eliminateMarkedPoints().surface() - S.delaunay() - return from_pyflatsurf(S) - - -class MinimalTranslationCover(Surface): - r""" - Do not use translation_surface.MinimalTranslationCover. Use - minimal_cover.MinimalTranslationCover instead. This class is being - deprecated. - """ - - def __init__(self, similarity_surface): - if similarity_surface.underlying_surface().is_mutable(): - if similarity_surface.is_finite(): - self._ss = similarity_surface.copy() - else: - raise ValueError( - "Can not construct MinimalTranslationCover of a surface that is mutable and infinite." - ) - else: - self._ss = similarity_surface - - # We are finite if and only if self._ss is a finite RationalConeSurface. - from flatsurf.geometry.minimal_cover import _is_finite - - finite = _is_finite(self._ss) - - identity = identity_matrix(self._ss.base_ring(), 2) - identity.set_immutable() - base_label = (self._ss.base_label(), identity) - - Surface.__init__( - self, self._ss.base_ring(), base_label, finite=finite, mutable=False - ) - - def polygon(self, lab): - r""" - EXAMPLES:: - - sage: from flatsurf import * - sage: C = translation_surfaces.chamanara(1/2) - sage: C.polygon('a') - Traceback (most recent call last): - ... - ValueError: invalid label 'a' - """ - if not isinstance(lab, tuple) or len(lab) != 2: - raise ValueError("invalid label {!r}".format(lab)) - return lab[1] * self._ss.polygon(lab[0]) - - def opposite_edge(self, p, e): - pp, m = p # this is the polygon m * ss.polygon(p) - p2, e2 = self._ss.opposite_edge(pp, e) - me = self._ss.edge_matrix(pp, e) - mm = ~me * m - mm.set_immutable() - return ((p2, mm), e2) - - -class AbstractOrigami(Surface): - r"""Abstract base class for origamis. - Realization needs just to define a _domain and four cardinal directions. - """ - - def __init__(self, domain, base_label=None): - self._domain = domain - if base_label is None: - base_label = domain.an_element() - from sage.rings.rational_field import QQ - - Surface.__init__(self, QQ, base_label, finite=domain.is_finite(), mutable=False) - - def up(self, label): - raise NotImplementedError - - def down(self, label): - raise NotImplementedError - - def right(self, label): - raise NotImplementedError - - def left(self, label): - raise NotImplementedError - - def _repr_(self): - return "Some AbstractOrigami" - - def num_polygons(self): - r""" - Returns the number of polygons. - """ - return self._domain.cardinality() - - def polygon_labels(self): - return self._domain - - def polygon(self, lab): - if lab not in self._domain: - # Updated to print a possibly useful error message - raise ValueError("Label " + str(lab) + " is not in the domain") - from flatsurf.geometry.polygon import polygons - - return polygons.square() - - def opposite_edge(self, p, e): - if p not in self._domain: - raise ValueError - if e == 0: - return self.down(p), 2 - if e == 1: - return self.right(p), 3 - if e == 2: - return self.up(p), 0 - if e == 3: - return self.left(p), 1 - raise ValueError - - -class Origami(AbstractOrigami): - def __init__(self, r, u, rr=None, uu=None, domain=None, base_label=None): - if domain is None: - domain = r.parent().domain() - - self._r = r - self._u = u - if rr is None: - rr = ~r - else: - for a in domain.some_elements(): - if r(rr(a)) != a: - raise ValueError("r o rr is not identity on %s" % a) - if rr(r(a)) != a: - raise ValueError("rr o r is not identity on %s" % a) - if uu is None: - uu = ~u - else: - for a in domain.some_elements(): - if u(uu(a)) != a: - raise ValueError("u o uu is not identity on %s" % a) - if uu(u(a)) != a: - raise ValueError("uu o u is not identity on %s" % a) - - self._perms = [uu, r, u, rr] # down,right,up,left - AbstractOrigami.__init__(self, domain, base_label) - - def opposite_edge(self, p, e): - if p not in self._domain: - raise ValueError( - "Polygon label p=" + str(p) + " is not in domain=" + str(self._domain) - ) - if e < 0 or e > 3: - raise ValueError("Edge value e=" + str(e) + " does not satisfy 0<=e<4.") - return self._perms[e](p), (e + 2) % 4 - - def up(self, label): - return self.opposite_edge(label, 2)[0] - - def down(self, label): - return self.opposite_edge(label, 0)[0] - - def right(self, label): - return self.opposite_edge(label, 1)[0] - - def left(self, label): - return self.opposite_edge(label, 3)[0] - - def _repr_(self): - return "Origami defined by r=%s and u=%s" % (self._r, self._u) - - -class LazyStandardizedPolygonSurface(Surface): - r""" - This class handles standardizing polygons for infinite translation surfaces. - See the TranslationSurface.standardize_polygons method. - - This class should not be instantiated directly. - Instead use TranslationSurface.standardize_polygons. - """ - - def __init__(self, surface, relabel=False): - self._s = surface.copy(mutable=True, relabel=relabel) - self._labels = set() - Surface.__init__( - self, - self._s.base_ring(), - self._s.base_label(), - finite=self._s.is_finite(), - mutable=False, - ) - - def standardize(self, label): - best = 0 - polygon = self._s.polygon(label) - best_pt = polygon.vertex(best) - for v in range(1, polygon.num_edges()): - pt = polygon.vertex(v) - if (pt[1] < best_pt[1]) or (pt[1] == best_pt[1] and pt[0] < best_pt[0]): - best = v - best_pt = pt - if best != 0: - self._s.set_vertex_zero(label, best, in_place=True) - self._labels.add(label) - - def polygon(self, label): - r""" - Return the polygon with the provided label. - - This method must be overridden in subclasses. - """ - if label in self._labels: - return self._s.polygon(label) - else: - self.standardize(label) - return self._s.polygon(label) - - def opposite_edge(self, label, e): - r""" - Given the label ``label`` of a polygon and an edge ``e`` in that - polygon returns the pair (``ll``, ``ee``) to which this edge is glued. - """ - if label not in self._labels: - self.standardize(label) - ll, ee = self._s.opposite_edge(label, e) - if ll in self._labels: - return (ll, ee) - self.standardize(ll) - return self._s.opposite_edge(label, e) diff --git a/flatsurf/geometry/xml.py b/flatsurf/geometry/xml.py deleted file mode 100644 index 420ebceea..000000000 --- a/flatsurf/geometry/xml.py +++ /dev/null @@ -1,265 +0,0 @@ -r""" -This module contains functions to output surfaces to a string or a file in a human and computer -readable format using XML. - -We also have a function to recreate the surface from the string/file. - -EXAMPLES:: - - sage: from flatsurf import * - sage: from flatsurf.geometry.xml import * - sage: s = translation_surfaces.square_torus().underlying_surface() - sage: ss = surface_from_xml_string(surface_to_xml_string(s)) - sage: ss==s - True -""" - -from __future__ import absolute_import, print_function, division -from six.moves import range, map, filter, zip - - -def surface_to_xml_string(s, complain=True): - r""" - Convert the surface s into a XML string. - - Note that this only encodes the Surface part of the object and not the SimilaritySurface - wrapper. - - By default complain=True, and it will print warning messages and raise errors if it doesn't - think you will be able to reconstruct the surface using surface_from_xml_string. - - Currently s should be Surface_list (or a wrapped Surface_list). If not and complain=True, - an error message will be printed and the surface will be converted to Surface_list. If - complain=False, the surface will be encoded but you may not be able to recover it. - - Also the surface should be defined over the rationals. Otherwise a ValueError is raised, which - can be disabled by setting complain=False. - """ - if not s.is_finite(): - raise ValueError("Can only xml encode a finite surface.") - from flatsurf.geometry.similarity_surface import SimilaritySurface - - if isinstance(s, SimilaritySurface): - s = s.underlying_surface() - from flatsurf.geometry.surface import Surface_list - - if complain: - if not isinstance(s, Surface_list): - # Convert to surface list - print( - "Warning:surface_to_xml_string is converting to Surface_list before encoding, " - + " labels will likely be changed. " - + "If this is not desired, " - + "call surface_to_xml_string with complain=False." - ) - s = Surface_list(surface=s) - - from xml.sax.saxutils import escape - - output = [] - output.append("") - - # Include the type of surface (in case we care about it in the future) - output.append("") - output.append(escape(repr(type(s)))) - output.append("") - - from sage.rings.rational_field import QQ - - if complain and s.base_ring() != QQ: - raise ValueError( - "Refusing to encode surface with base_ring!=QQ, " - + "because we can not guarantee we bring the surface back. " - + "If you would like to encode it anyway, " - + "call surface_to_xml_string with complain=False." - ) - - output.append("") - output.append(escape(repr(s.base_ring()))) - output.append("") - - output.append("") - output.append(escape(repr(s.base_label()))) - output.append("") - - output.append("") - for label in s.label_iterator(): - output.append("") - output.append("") - p = s.polygon(label) - for e in range(p.num_edges()): - output.append("") - v = p.edge(e) - output.append("") - output.append(escape(repr(v[0]))) - output.append("") - output.append("") - output.append(escape(repr(v[1]))) - output.append("") - output.append("") - output.append("") - output.append("") - - output.append("") - for label, e in s.edge_iterator(): - ll, ee = s.opposite_edge(label, e) - if ll < label or (ll == label and ee < e): - continue - output.append("") - output.append("") - output.append(escape(repr(label))) - output.append("") - output.append("") - output.append(escape(repr(e))) - output.append("") - output.append("") - output.append(escape(repr(ll))) - output.append("") - output.append("") - output.append(escape(repr(ee))) - output.append("") - output.append("") - output.append("") - - output.append("") - return "".join(output) - - -def surface_to_xml_file(s, filename, complain=True): - r""" - Convert the surface s to a string using surface_to_xml_string, - then write an XML header and this string to the file with filename provided. - - The complain flag is passed to surface_to_xml_string. - """ - string = surface_to_xml_string(s, complain=complain) - f = open(filename, "w") - f.write('\n') - f.write(string) - f.close() - - -def surface_from_xml_string(string): - r""" - Attempts to reconstruct a Surface from a string storing an XML representation of the surface. - - Currently, this works only if the surface stored was a Surface_list and the surface was defined - over QQ. - """ - import xml.etree.ElementTree as ET - - tree = ET.fromstring(string) - return _surface_from_ElementTree(tree) - - -def surface_from_xml_file(filename): - r""" - Attempts to reconstruct a Surface from a string storing an XML representation of the surface. - - Currently, this works only if the surface stored was a Surface_list and the surface was defined - over QQ. - """ - import xml.etree.ElementTree as ET - - tree = ET.parse(filename) - return _surface_from_ElementTree(tree) - - -def _surface_from_ElementTree(tree): - from flatsurf.geometry.surface import Surface_list - - node = tree.find("type") - if node is None: - raise ValueError('Failed to find tag named "node"') - if node.text != repr(Surface_list): - raise NotImplementedError( - "Currently can only reconstruct from Surface_list. " - + "Found type " - + node.text - ) - - node = tree.find("base_ring") - if node is None: - raise ValueError('Failed to find tag named "base_ring"') - from sage.rings.rational_field import QQ - - if node.text == repr(QQ): - base_ring = QQ - else: - raise NotImplementedError( - "Can only reconstruct when base_ring=QQ. " - + "Found " - + tree.find("base_ring").text - ) - - s = Surface_list(QQ) - - # Import the polygons - polygons = tree.find("polygons") - if polygons is None: - raise ValueError('Failed to find tag named "polygons"') - from flatsurf.geometry.polygon import ConvexPolygons - - P = ConvexPolygons(base_ring) - for polygon in polygons.findall("polygon"): - node = polygon.find("label") - if node is None: - raise ValueError("Failed to find tag