diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
index 5e551989..dd4ce848 100644
--- a/.github/workflows/doc.yml
+++ b/.github/workflows/doc.yml
@@ -11,19 +11,20 @@ jobs:
build-manual:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
with: { submodules: recursive }
- - uses: conda-incubator/setup-miniconda@v2
- with: { miniforge-variant: "Mambaforge", miniforge-version: "latest" }
+ - uses: conda-incubator/setup-miniconda@v3
+ with:
+ channels: conda-forge
- name: install dependencies
shell: bash -l {0}
run: |
- mamba env update --quiet -n test -f environment.yml
+ conda env update --quiet -n test -f environment.yml
conda list
- name: install veerer
shell: bash -l {0}
run: |
- pip install --no-index .
+ pip install --no-index --no-build-isolation .
- name: fix permissions
shell: bash -l {0}
run: |
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 9b31dbe4..49d3c284 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -11,14 +11,14 @@ jobs:
codespell:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
with: { submodules: recursive }
- - uses: conda-incubator/setup-miniconda@v2
- with: { miniforge-variant: "Mambaforge", miniforge-version: "latest" }
+ - uses: conda-incubator/setup-miniconda@v3
+ with: { miniforge-version: "latest" }
- name: install codespell
shell: bash -l {0}
run: |
- mamba install -y codespell
+ conda install -y codespell
- name: run codespell
shell: bash -l {0}
run: |
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ac518078..4b12306f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,42 +16,62 @@ jobs:
- optionals: "sage"
sagelib: "10.0"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
- optionals: "sage,sage_flatsurf,pyeantic,pyintervalxt,pyflatsurf,pynormaliz,surface_dynamics"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
# Test optional dependencies in isolation
- optionals: "sage"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
# Recent sage-flatsurf depends on surface-dynamics
- optionals: "sage,surface_dynamics,sage_flatsurf"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
- optionals: "sage,pyeantic"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
- optionals: "sage,pyintervalxt"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
- optionals: "sage,pynormaliz"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
- optionals: "sage,surface_dynamics"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
- optionals: "sage,pyflatsurf"
sagelib: "10.3"
python: "3.10"
+ pip: "24.3.1"
+ setuptools: "70.3.0"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
with: { submodules: recursive }
- - uses: conda-incubator/setup-miniconda@v2
- with: { miniforge-variant: "Mambaforge", miniforge-version: "latest", python-version: "${{ matrix.python }}" }
+ - uses: conda-incubator/setup-miniconda@v3
+ with:
+ channels: conda-forge
+ python-version: ${{ matrix.python }}
- name: Install dependencies
shell: bash -l {0}
run: |
if (echo "${{ matrix.optionals }}" | grep sage); then
- mamba install -n test sagelib=${{ matrix.sagelib }}
+ conda install -n test sagelib=${{ matrix.sagelib }}
echo "sagelib ==${{ matrix.sagelib }}" >> $CONDA_PREFIX/conda-meta/pinned
fi
@@ -60,25 +80,25 @@ jobs:
(test "$optional" == '' || (echo "${{ matrix.optionals }}" | grep -E '\b'"$optional"'\b') > /dev/null) && echo "$REPLY" || true
done < environment.yml > environment.test.yml
- mamba env update --quiet -n test -f environment.test.yml
+ conda env update --quiet -n test -f environment.test.yml
conda list
- name: Install veerer
shell: bash -l {0}
run: |
- pip install --verbose --no-index .
+ pip install --verbose --no-index --no-build-isolation .
# Show message about cppyy regenerating pre-compiled headers so it does not show during the tests
python -c 'import cppyy' || true
- name: Run SageMath doctests
shell: bash -l {0}
run: |
- mamba remove -y pytest # sage -t breaks in some old versions of SageMath if pytest is installed
+ conda remove -y pytest # sage -t breaks in some old versions of SageMath if pytest is installed
sage -tp --force-lib --long --optional=${{ matrix.optionals }} veerer doc
- name: Run pytest
shell: bash -l {0}
working-directory: tests
run: |
- mamba install -y pytest pytest-xdist
+ conda install -y pytest pytest-xdist
pytest -n auto
- uses: flatsurf/actions/show-logs@main
if: ${{ always() }}
diff --git a/README.rst b/README.rst
index 4c3a3a72..ca5f0f4a 100644
--- a/README.rst
+++ b/README.rst
@@ -89,4 +89,5 @@ Authors
- Mark Bell
- Vincent Delecroix
+- Kai Fu
- Saul Schleimer
diff --git a/doc/source/background.rst b/doc/source/background.rst
index 2c5e908a..405c21cc 100644
--- a/doc/source/background.rst
+++ b/doc/source/background.rst
@@ -4,10 +4,18 @@
Veering triangulations
======================
+Geometry
+--------
+
Veering triangulations were originally defined as 3-dimensional
triangulation of mapping tori in [Ag11]_, see also [Gu09]_, [Gu16]_. It
turns out that they also encode nicely train-tracks splitting sequences of
-pseudo-Anosov.
+pseudo-Anosov mapping classes.
The veerer library implements most of the tools from [BeDeGaGuSc]_ which
shares a lot of ideas with [Ha09]_.
+
+- Embedded graphs
+- Veering triangulation, Strebel graph and parametrization of Abelian and quadratic differentials
+- Linear subvarieties and Veering-Strebel flip graph
+- Degenerations and multiscale compactification
diff --git a/doc/source/ferenczi_zamboni.rst b/doc/source/ferenczi_zamboni.rst
index cd0066cf..5d33d17c 100644
--- a/doc/source/ferenczi_zamboni.rst
+++ b/doc/source/ferenczi_zamboni.rst
@@ -17,15 +17,15 @@ There are only two possible moves::
sage: from veerer import VeeringTriangulation, VeeringFlipSequence # random output due to deprecation warnings from realalg
sage: V = VeeringTriangulation("(0,1,2)", "PBR")
- sage: R = VeeringFlipSequence(V, "0R", [2,1,0])
- sage: L = VeeringFlipSequence(V, "0B", [1,0,2])
+ sage: R = VeeringFlipSequence(V, "0R", "(0,2)")
+ sage: L = VeeringFlipSequence(V, "0B", "(0,1)")
The smallest dilatation is the golden rotation::
sage: assert R.is_closed() and L.is_closed()
sage: fp = R * L
sage: fp
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)", "PBR"), "0R 2B", "(0,2,1)")
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)", "PBR"), "0R 2B", "(0,2,1)(~0,~2,~1)")
sage: a, S = fp.self_similar_surface()
sage: SS = S.copy(mutable=True)
sage: SS.flip(0)
@@ -140,22 +140,22 @@ Some pseudo-Anosov with small dilatation in H(2)
sage: assert f.is_pseudo_anosov()
sage: f.self_similar_surface()
(a,
- FlatVeeringTriangulation(Triangulation("(0,6,5)(1,2,~6)(3,4,~5)"), [(1, -1), (a, a^3 - a^2 - a - 1), (a^3 - 2*a - 2, a^2), (-a^3 + a^2 + a + 1, -a), (2*a^3 - a^2 - 2*a - 2, a^3 - 2), (-a^3 + a + 1, -a^3 + a + 2), (a^3 - a - 2, a^3 - a - 1), (-a^3 + a + 2, -a^3 + a + 1), (-a^3 + a + 1, -a^3 + a + 2)]))
+ FlatVeeringTriangulation("(0,6,5)(1,2,~6)(3,4,~5)", "BBBBRRR", (1, a, -a^3 + 2*a + 2, -a^3 + a^2 + a + 1, 2*a^3 - a^2 - 2*a - 2, a^3 - a - 1, a^3 - a - 2), (1, -a^3 + a^2 + a + 1, a^2, a, a^3 - 2, a^3 - a - 2, a^3 - a - 1)))
sage: f = R1 * R1 * R5
sage: assert f.is_pseudo_anosov()
sage: f.self_similar_surface()
(a,
- FlatVeeringTriangulation(Triangulation("(0,6,5)(1,2,~6)(3,4,~5)"), [(1, -1), (a^2, 2*a^3 - 3*a^2 - 2*a - 4), (a^3 - 2*a^2 - 2, a), (a, a^3 - 2*a^2 - 2), (a^3 - a^2 - a - 1, a^3 - a^2 - a - 3), (-a^3 + a^2 + 1, -2*a^3 + 3*a^2 + a + 5), (a^3 - a^2 - 2, 2*a^3 - 3*a^2 - a - 4), (-a^3 + a^2 + 2, -2*a^3 + 3*a^2 + a + 4), (-a^3 + a^2 + 1, -2*a^3 + 3*a^2 + a + 5)]))
+ FlatVeeringTriangulation("(0,6,5)(1,2,~6)(3,4,~5)", "BBBBRRR", (1, a^2, -a^3 + 2*a^2 + 2, a, a^3 - a^2 - a - 1, a^3 - a^2 - 1, a^3 - a^2 - 2), (1, -2*a^3 + 3*a^2 + 2*a + 4, a, -a^3 + 2*a^2 + 2, a^3 - a^2 - a - 3, 2*a^3 - 3*a^2 - a - 5, 2*a^3 - 3*a^2 - a - 4)))
sage: f = R3 * R1 * R2 * CL5
sage: assert f.is_pseudo_anosov()
sage: f.self_similar_surface()
(a,
- FlatVeeringTriangulation(Triangulation("(0,~5,4)(1,2,~6)(3,5,6)"), [(1, 1), (1, 1), (-1/2*a + 3/2, 1/2*a - 1/2), (1/2*a - 1/2, -1/2*a + 3/2), (a - 4, -a), (-a + 3, a - 1), (1/2*a - 5/2, -1/2*a - 1/2), (1/2*a - 5/2, -1/2*a - 1/2), (-a + 3, a - 1)]))
+ FlatVeeringTriangulation("(0,~5,4)(1,2,~6)(3,5,6)", "RRBBRBR", (1, 1, 1/2*a - 3/2, 1/2*a - 1/2, -a + 4, a - 3, -1/2*a + 5/2), (1, 1, 1/2*a - 1/2, 1/2*a - 3/2, a, a - 1, 1/2*a + 1/2)))
sage: f = R3 * R1 * R2 * CL5 * CR5
sage: assert f.is_pseudo_anosov()
sage: f.self_similar_surface()
(a,
- FlatVeeringTriangulation(Triangulation("(0,~5,4)(1,2,~6)(3,5,6)"), [(1, 1), (7/33*a^3 - 23/33*a^2 - 19/33*a - 25/33, -10/33*a^3 + 32/33*a^2 + 37/33*a + 16/33), (-20/33*a^3 + 61/33*a^2 + 92/33*a + 62/33, 5/33*a^3 - 16/33*a^2 - 2/33*a - 8/33), (-1/33*a^3 + 8/33*a^2 - 2/33*a - 20/33, -8/33*a^3 + 19/33*a^2 + 56/33*a + 26/33), (4/11*a^3 - 10/11*a^2 - 25/11*a - 30/11, -1/11*a^3 + 1/11*a^2 + 7/11*a - 5/11), (-4/11*a^3 + 10/11*a^2 + 25/11*a + 19/11, 1/11*a^3 - 1/11*a^2 - 7/11*a - 6/11), (13/33*a^3 - 38/33*a^2 - 73/33*a - 37/33, 5/33*a^3 - 16/33*a^2 - 35/33*a - 8/33), (13/33*a^3 - 38/33*a^2 - 73/33*a - 37/33, 5/33*a^3 - 16/33*a^2 - 35/33*a - 8/33), (-4/11*a^3 + 10/11*a^2 + 25/11*a + 19/11, 1/11*a^3 - 1/11*a^2 - 7/11*a - 6/11)]))
+ FlatVeeringTriangulation("(0,~5,4)(1,2,~6)(3,5,6)", "RBBBRBR", (1, 7/33*a^3 - 23/33*a^2 - 19/33*a - 25/33, 20/33*a^3 - 61/33*a^2 - 92/33*a - 62/33, -1/33*a^3 + 8/33*a^2 - 2/33*a - 20/33, -4/11*a^3 + 10/11*a^2 + 25/11*a + 30/11, 4/11*a^3 - 10/11*a^2 - 25/11*a - 19/11, -13/33*a^3 + 38/33*a^2 + 73/33*a + 37/33), (1, 10/33*a^3 - 32/33*a^2 - 37/33*a - 16/33, 5/33*a^3 - 16/33*a^2 - 2/33*a - 8/33, 8/33*a^3 - 19/33*a^2 - 56/33*a - 26/33, 1/11*a^3 - 1/11*a^2 - 7/11*a + 5/11, 1/11*a^3 - 1/11*a^2 - 7/11*a - 6/11, -5/33*a^3 + 16/33*a^2 + 35/33*a + 8/33)))
diff --git a/doc/source/veerer_demo.rst b/doc/source/veerer_demo.rst
index 57537d5f..e153b759 100644
--- a/doc/source/veerer_demo.rst
+++ b/doc/source/veerer_demo.rst
@@ -31,11 +31,12 @@ part of a project that also involve
with SageMath (see below).
- The Python library `PyNormaliz `_
(polytope, in particular over number fields).
-- The software `SageMath `_ (plotting, linear algebra
- and many other things)
-- The SageMath library `surface_dynamics
+- The Sagemath libraries `sage-flatsruf
+ `_ (plotting and construction of
+ surfaces) and `surface_dynamics
`_ (for analyzing stratum
components)
+- The software `SageMath `_ (many things)
To import all features from veerer one usually starts with the following
lines::
@@ -76,12 +77,12 @@ colors.
::
sage: FS0 = T0.flat_structure_min()
- sage: FS0.plot().show(figsize=5)
+ sage: FS0.plot().show(figsize=5) # optional - sage_flatsurf
::
sage: FFS0 = T0.flat_structure_geometric_middle()
- sage: FFS0.plot().show(figsize=5)
+ sage: FFS0.plot().show(figsize=5) # optional - sage_flatsurf
::
@@ -100,12 +101,12 @@ colors.
::
sage: FS1 = T1.flat_structure_min()
- sage: FS1.plot().show(figsize=5)
+ sage: FS1.plot().show(figsize=5) # optional - sage_flatsurf
::
sage: FFS1 = T1.flat_structure_geometric_middle()
- sage: FFS1.plot().show(figsize=5)
+ sage: FFS1.plot().show(figsize=5) # optional - sage_flatsurf
::
@@ -124,20 +125,11 @@ colors.
::
sage: FS2 = T2.flat_structure_min()
- sage: FS2.plot().show(figsize=5) # not tested (warning from matplotlib)
+ sage: FS2.plot().show(figsize=5) # optional - sage_flatsurf # not tested
-Viewing train-tracks!
----------------------
-
-Recall that a veering triangulation is just a pair of transversal
-train-tracks.
-
-::
-
- sage: TT_horiz = FS1.plot(horizontal_train_track=True, edge_labels=False)
- sage: TT_vert = FS1.plot(vertical_train_track=True, edge_labels=False)
- sage: graphics_array([TT_horiz, TT_vert], 1, 2).show(figsize=6)
+More examples
+-------------
::
@@ -211,7 +203,7 @@ Core vs not core
::
sage: FS = S.flat_structure_min()
- sage: FS.plot()
+ sage: FS.plot() # optional - sage_flatsurf
Graphics object consisting of ... graphics primitives
::
@@ -223,17 +215,17 @@ Core vs not core
::
- sage: print(S.train_track_polytope(HORIZONTAL))
+ sage: print(S.cone(HORIZONTAL))
Cone of dimension 4 in ambient dimension 9 made of 5 facets (backend=ppl)
- sage: print(S.train_track_polytope(VERTICAL))
+ sage: print(S.cone(VERTICAL))
Cone of dimension 3 in ambient dimension 9 made of 3 facets (backend=ppl)
::
sage: # check that we indeed started with a core veering triangulation
- sage: print(T1.train_track_polytope(HORIZONTAL))
+ sage: print(T1.cone(HORIZONTAL))
Cone of dimension 4 in ambient dimension 9 made of 4 facets (backend=ppl)
- sage: print(T1.train_track_polytope(VERTICAL))
+ sage: print(T1.cone(VERTICAL))
Cone of dimension 4 in ambient dimension 9 made of 5 facets (backend=ppl)
@@ -258,15 +250,19 @@ of the product of the two train-track polytopes.
sage: print(T1.is_delaunay())
True
sage: print(T1.delaunay_cone())
- Cone of dimension 8 in ambient dimension 18 made of 13 facets (backend=ppl)
+ 8-dimensional Delaunay cone of VeeringTriangulation("(0,~3,4)(~0,7,~6)(1,2,~7)(~1,~2,3)(~4,5,~8)(~5,8,6)", "RBRRBRBRB") made of
+ 2 forward-flip facets
+ 2 backward-flip facets
+ 5 x-degeneration facets
+ 4 y-degeneration facets
Core automaton
--------------
-The core automaton of a given triangulations `T_0` is the directed graph whose
-vertices are core veering triangulations that can be reached from `T_0` by a
-sequence of flips and there is a directed edge `T_i \to T_j` if `T_j` is obtained
-from `T_i` by a flip.
+The core automaton of a given triangulations `T_0` is the automaton (or
+directed graph) whose states (or vertices) are core veering triangulations that
+can be reached from `T_0` by a sequence of flips and there is a transition (or
+directed edge) `T_i \to T_j` if `T_j` is obtained from `T_i` by a flip.
::
@@ -278,7 +274,7 @@ from `T_i` by a flip.
sage: A0.run()
0
sage: A0
- Core veering automaton with 2 vertices
+ Core veering automaton with 2 states
::
@@ -348,8 +344,8 @@ Some data (orientable case)
To give an idea about the complexity and timings when generating the
above data, here are the steps involved. The timings are for the stratum
component H(4)^hyp that is the fourth row in the above array: -
-generating the core graph ~20 secs for H(4)^hyp (the graph has 9116
-vertices and 44664 edges) - filtering the geometric triangulations
+generating the core graph ~20 secs for H(4)^hyp (the automaton has 9116
+states and 44664 transitions) - filtering the geometric triangulations
(single test involves a polytope computation) ~20 secs for H(4)^hyp -
filtering cylindrical (single test is cheap) ~2 sec for H(4)^hyp
diff --git a/tests/test_flip.py b/tests/test_flip.py
index 60c2a84f..25c1bd41 100644
--- a/tests/test_flip.py
+++ b/tests/test_flip.py
@@ -101,7 +101,7 @@ def test_flip_reduced(fp, cols, repeat):
# actually do the flip
V.flip(e, col, reduced=True)
assert V.edge_has_curve(e)
- assert V.forward_flippable_edges() == [e[0] for e in V.edges() if V.edge_colour(e[0]) == PURPLE], (V0, e, col)
+ assert V.forward_flippable_edges() == V.purple_edges(), (V0, e, col)
if __name__ == '__main__':
import sys
diff --git a/tests/test_triangulation_relabel.py b/tests/test_triangulation_relabel.py
index 21f25ee5..2717b48b 100644
--- a/tests/test_triangulation_relabel.py
+++ b/tests/test_triangulation_relabel.py
@@ -24,10 +24,9 @@
def test_relabel():
T = Triangulation([(0,1,2), (-1,-2,-3)], mutable=True)
- p = perm_init([1,5,0,2,4,3])
+ p = perm_init([3,2,0,1,4,5])
T.relabel(p)
- assert T.faces() == [[0, 1, 5], [2, 3, 4]]
- assert T.edges() == [[0, 2], [1, 3], [4, 5]]
+ assert T.faces() == [[0, 4, 3], [1, 5, 2]]
T._check()
if __name__ == '__main__':
diff --git a/veerer/__init__.py b/veerer/__init__.py
index 80c599f7..91a19416 100644
--- a/veerer/__init__.py
+++ b/veerer/__init__.py
@@ -26,12 +26,9 @@
from .constants import RED, BLUE, PURPLE, GREEN, HORIZONTAL, VERTICAL, RIGHT, LEFT, UP, DOWN
from .triangulation import Triangulation
-from .cover import TriangulationCover
from .veering_triangulation import VeeringTriangulation, VeeringTriangulations
-from .linear_family import VeeringTriangulationLinearFamily, StrebelGraphLinearFamily
+from .linear_family import VeeringTriangulationLinearFamily, StrebelGraphLinearFamily, VeeringTriangulationLinearFamilies
from .strebel_graph import StrebelGraph
from .automaton import FlipGraph, CoreAutomaton, ReducedCoreAutomaton, DelaunayAutomaton, DelaunayStrebelAutomaton
from .flip_sequence import VeeringFlipSequence
from .flat_structure import FlatVeeringTriangulation
-from .layout import FlatVeeringTriangulationLayout
-from .measured_train_track import MeasuredTrainTrack
diff --git a/veerer/automaton.py b/veerer/automaton.py
index 7e880631..2a4f4e51 100644
--- a/veerer/automaton.py
+++ b/veerer/automaton.py
@@ -45,7 +45,7 @@
sage: C.run()
0
sage: C
- Core veering automaton with 1074 vertices
+ Core veering automaton with 1074 states
sage: R = ReducedCoreAutomaton()
sage: R.add_seed(vt)
@@ -53,13 +53,13 @@
sage: R.run()
0
sage: R
- Reduced core veering automaton with 356 vertices
+ Reduced core veering automaton with 356 states
Exploring strata::
sage: from surface_dynamics import Stratum # optional - surface_dynamics
- sage: strata = [Stratum([2], 1), Stratum([2,-1,-1], 2), # optional - surface_dynamics
- ....: Stratum([2,2], 2), Stratum([1,1], 1)]
+ sage: strata = [Stratum([2], 1), Stratum([2, -1, -1], 2), # optional - surface_dynamics
+ ....: Stratum([2, 2], 2), Stratum([1,1], 1)]
sage: for stratum in strata: # optional - surface_dynamics
....: print(stratum)
....: vt = VeeringTriangulation.from_stratum(stratum)
@@ -72,17 +72,17 @@
....: print(C)
....: print(R)
H_2(2)
- Core veering automaton with 86 vertices
- Reduced core veering automaton with 28 vertices
+ Core veering automaton with 86 states
+ Reduced core veering automaton with 28 states
Q_1(2, -1^2)
- Core veering automaton with 160 vertices
- Reduced core veering automaton with 68 vertices
+ Core veering automaton with 160 states
+ Reduced core veering automaton with 68 states
Q_2(2^2)
- Core veering automaton with 846 vertices
- Reduced core veering automaton with 305 vertices
+ Core veering automaton with 846 states
+ Reduced core veering automaton with 305 states
H_2(1^2)
- Core veering automaton with 876 vertices
- Reduced core veering automaton with 234 vertices
+ Core veering automaton with 876 states
+ Reduced core veering automaton with 234 states
"""
# ****************************************************************************
# This file is part of veerer
@@ -116,7 +116,9 @@
from .strebel_graph import StrebelGraph
from .linear_family import VeeringTriangulationLinearFamily
from .constants import RED, BLUE, PURPLE, VERTICAL, HORIZONTAL, PROPERTIES_COLOURS, colour_to_char, colour_to_string
-from .permutation import perm_invert
+from .permutation import perm_invert, perm_compose
+
+from sage.graphs.digraph import DiGraph
# TODO: when set to True a lot of intermediate checks are performed
CHECK = False
@@ -154,8 +156,7 @@ def __init__(self, backward=False, method='BFS', verbosity=0, **extra_kwds):
# the automaton is encoded in two dictionaries where the keys are the states
# and the values are respectively the outgoing or incoming edges.
# In both cases, the neighbors are encoded by pairs (neighbor, label)
- self._forward_neighbors = {}
- self._backward_neighbors = {}
+ self._graph = DiGraph(multiedges=True, loops=True)
# list of seeds to be considered to explore the automaton from
self._seeds = []
@@ -174,27 +175,20 @@ def __init__(self, backward=False, method='BFS', verbosity=0, **extra_kwds):
self._setup(**extra_kwds)
+ def is_connected(self):
+ return self._graph.is_connected()
+
def _check(self):
r"""
Some consistency checks.
"""
if not (self._branch or self._backward_flip_queue or self._seeds):
- d1 = set(self._forward_neighbors).difference(self._backward_neighbors)
- assert not d1, d1
-
- d2 = set(self._backward_neighbors).difference(self._forward_neighbors)
- assert not d2, d2
-
- num_incoming_edges = sum(len(v) for v in self._backward_neighbors.values())
- num_outgoing_edges = sum(len(v) for v in self._forward_neighbors.values())
- assert num_outgoing_edges == num_incoming_edges, (num_outgoing_edges, num_incoming_edges)
-
for state in self:
- in_neighbors1 = set(x for x, label in self._backward_neighbors[state])
+ in_neighbors1 = set(self._graph.neighbors_in(state))
in_neighbors2 = set(x for x, label in self._in_neighbors(state))
- assert in_neighbors1 == in_neighbors2, (state, in_neighbors1, in_neighbors2)
+ assert in_neighbors2.issubset(in_neighbors1), (state, in_neighbors1, in_neighbors2)
- out_neighbors1 = set(x for x, label in self._forward_neighbors[state])
+ out_neighbors1 = set(self._graph.neighbors_out(state))
out_neighbors2 = set(x for x, label in self._out_neighbors(state))
assert out_neighbors1 == out_neighbors2, (state, out_neighbors1, out_neighbors2)
@@ -209,7 +203,8 @@ def __str__(self):
else:
status = ''
name = self._name[0].upper() + self._name[1:]
- return ("%s%s automaton with %s %s" % (status, name, len(self._forward_neighbors), 'vertex' if len(self._forward_neighbors) <= 1 else 'vertices'))
+ nv = self._graph.num_verts()
+ return ("%s%s automaton with %s %s" % (status, name, nv, 'state' if nv <= 1 else 'states'))
def __repr__(self):
return str(self)
@@ -235,7 +230,7 @@ def __len__(self):
sage: A.number_of_states()
86
"""
- return len(self._forward_neighbors)
+ return self._graph.num_verts()
number_of_states = __len__
num_states = __len__
@@ -261,7 +256,7 @@ def number_of_transitions(self):
sage: A.number_of_transitions()
300
"""
- return sum(len(v) for v in self._forward_neighbors.values())
+ return self._graph.num_edges()
num_transitions = number_of_transitions
@@ -292,7 +287,7 @@ def __iter__(self):
0
sage: assert all(t.angles() == [6] for t in A)
"""
- return iter(self._forward_neighbors)
+ return iter(self._graph)
states = __iter__
@@ -310,27 +305,17 @@ def sources(self):
In the Delaunay automaton, the sources are the horizontal-Strebel veering triangulations::
- sage: A = DelaunayAutomaton(backward=True)
+ sage: A = DelaunayAutomaton()
sage: A.add_seed(vt)
1
sage: A.run()
0
sage: list(A.sources())
- [VeeringTriangulation("(0,~3,2)(1,3,~2)", boundary="(~1:2,~0:2)", colouring="RRBR")]
+ [VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:2,~3:2)", "RBRR")]
sage: set(A.sources()) == set(vt for vt in A if vt.is_strebel(HORIZONTAL))
True
-
- In the Delaunay-Strebel automaton, the sources are the horizontal Strebel graphs::
-
- sage: A = DelaunayStrebelAutomaton(backward=True)
- sage: A.add_seed(vt)
- 1
- sage: A.run()
- 0
- sage: list(A.sources())
- [('horizontal-strebel', StrebelGraph("(0,~1:1,~0,1:1)"))]
"""
- return (x for x, backward_neighbors in self._backward_neighbors.items() if not backward_neighbors)
+ return self._graph.sources()
def sinks(self):
r"""
@@ -346,41 +331,39 @@ def sinks(self):
In the Delaunay automaton, the sinks are the vertical-Strebel veering triangulations::
- sage: A = DelaunayAutomaton(backward=True)
+ sage: A = DelaunayAutomaton()
sage: A.add_seed(vt)
1
sage: A.run()
0
sage: list(A.sinks())
- [VeeringTriangulation("(0,~3,2)(1,3,~2)", boundary="(~1:2,~0:2)", colouring="RRRB")]
+ [VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:2,~3:2)", "BRRR")]
sage: set(A.sinks()) == set(vt for vt in A if vt.is_strebel(VERTICAL))
True
In the Delaunay-Strebel, the sinks are the vertical Strebel graphs::
- sage: A = DelaunayStrebelAutomaton(backward=True)
+ sage: A = DelaunayStrebelAutomaton()
sage: A.add_seed(vt)
1
sage: A.run()
0
sage: list(A.sinks())
- [('vertical-strebel', StrebelGraph("(0,~1:1,~0,1:1)"))]
+ [StrebelGraph("(0,1:1,~0,~1:1)")]
"""
- return (x for x, forward_neighbors in self._forward_neighbors.items() if not forward_neighbors)
+ return self._graph.sinks()
def transitions(self):
r"""
Run through the transitions of this automaton.
"""
- for s, out_neighbors in self._forward_neighbors.items():
- for t, label in out_neighbors:
- yield (s, t, label)
+ return self._graph.edges(sort=False)
def __contains__(self, state):
r"""
Return whether ``state`` is contained in the automaton.
"""
- return state.copy(mutable=False) in self._forward_neighbors
+ return state.copy(mutable=False) in self._graph
# TODO: provide vertex_map and edge_map functions
def to_graph(self, directed=True, multiedges=True, loops=True):
@@ -408,7 +391,7 @@ def to_graph(self, directed=True, multiedges=True, loops=True):
sage: A.run()
0
sage: A
- Core veering automaton with 86 vertices
+ Core veering automaton with 86 states
sage: A.to_graph()
Looped multi-digraph on 86 vertices
@@ -423,16 +406,13 @@ def to_graph(self, directed=True, multiedges=True, loops=True):
from sage.graphs.graph import Graph
G = Graph(loops=loops, multiedges=multiedges)
- for g, neighb in self._forward_neighbors.items():
- for gg, label in neighb:
- G.add_edge(g, gg, label)
-
+ G.add_edges(self._graph.edges(sort=False))
return G
# TODO: move or deprecate (not a generic method)
def rotation_automorphism(self):
r"""
- Return the automorphism of the vertices that corresponds to rotation.
+ Return the automorphism of the states that corresponds to rotation.
Note that this automorphism reverses the edge direction.
@@ -453,7 +433,7 @@ def rotation_automorphism(self):
True
"""
aut = {}
- for vt in self._forward_neighbors:
+ for vt in self:
vt2 = vt.copy(mutable=True)
vt2.rotate()
vt2.set_canonical_labels()
@@ -464,7 +444,7 @@ def rotation_automorphism(self):
# TODO: move or deprecate (not a generic method)
def conjugation_automorphism(self):
"""
- Return the automorphism of the vertices that corresponds to complex conjugation.
+ Return the automorphism of the states that corresponds to complex conjugation.
EXAMPLES::
@@ -489,7 +469,7 @@ def conjugation_automorphism(self):
True
"""
aut = {}
- for vt in self._forward_neighbors:
+ for vt in self:
vt2 = vt.copy(mutable=True)
vt2.conjugate()
vt2.set_canonical_labels()
@@ -555,7 +535,7 @@ def export_dot(self, filename=None, triangulations=False):
f.write('/* */\n')
f.write('/* $ sfdp -Tpdf -o file.pdf file.dot */\n')
f.write('/* */\n')
- seed_line = '/* seed: %s' % min(self._forward_neighbors)
+ seed_line = '/* seed: %s' % min(self._graph)
seed_line += ' ' * (70 - len(seed_line)) + '*/\n'
f.write(seed_line)
f.write('/* */\n')
@@ -564,7 +544,7 @@ def export_dot(self, filename=None, triangulations=False):
f.write('digraph MyGraph {\n')
f.write(' node [shape=circle style=filled margin=0.1 width=0 height=0]\n')
- for T in self._forward_neighbors:
+ for T in self:
g = T.to_string()
if triangulations:
t_filename = os.path.join(path, g + '.svg')
@@ -582,7 +562,7 @@ def export_dot(self, filename=None, triangulations=False):
else:
f.write(' %s [label="%d" color="%s"];\n' % (g, aut_size, colour))
- for TT, flip_data in self._forward_neighbors[T]:
+ for _, TT, flip_data in self._graph.outgoing_edges(T):
gg = TT.to_string()
# TODO: restore coloring options
# f.write(' %s -> %s [color="%s;%f:%s"];\n' % (g, gg, old_col, 0.5, new_col))
@@ -672,23 +652,19 @@ def from_stratum(self, stratum, **kwds):
sage: from veerer import *
sage: from surface_dynamics import Stratum # optional - surface_dynamics
sage: CoreAutomaton.from_stratum(Stratum([2], 1)) # optional - surface_dynamics
- Core veering automaton with 86 vertices
+ Core veering automaton with 86 states
sage: DelaunayAutomaton.from_stratum(Stratum([2], 1)) # optional - surface_dynamics
- Delaunay automaton with 54 vertices
+ Delaunay automaton with 54 states
sage: Q = Stratum([8], 2) # optional - surface_dynamics
sage: A = CoreAutomaton.from_stratum(Q, max_size=100) # optional - surface_dynamics
sage: A # optional - surface_dynamics
- Partial core veering automaton with 101 vertices
+ Partial core veering automaton with 101 states
"""
return self.from_triangulation(VeeringTriangulation.from_stratum(stratum), **kwds)
def out_neighbors(self, state):
- state = state.copy(mutable=False)
- neighbors = self._forward_neighbors.get(state, None)
- if neighbors is None:
- raise ValueError('state not in the automaton')
- return neighbors
+ return self._graph.neighbors_out(state.copy(mutable=False))
######################
# search implementation #
@@ -705,7 +681,7 @@ def add_seed(self, state, setup=True):
if setup:
state = self._seed_setup(state)
- if state in self._forward_neighbors:
+ if state in self._graph:
if self._verbosity >= 2:
print('[add_seed] state=%s already in the graph' % (state,))
return 0
@@ -726,7 +702,7 @@ def _next_seed(self):
while self._seeds or self._backward_flip_queue:
while self._seeds:
seed = self._seeds.pop()
- if seed not in self._forward_neighbors:
+ if seed not in self._graph:
# not explored forward yet
return seed
@@ -741,6 +717,9 @@ def _next_seed(self):
print('[_next_seed] add back_neighbor %s' % (back_neighbor,))
self.add_seed(back_neighbor, setup=False)
+ if self._verbosity >= 2:
+ print('[_next seed] done')
+
def run(self, max_size=None):
r"""
Discover new states and transitions in the automaton.
@@ -763,7 +742,7 @@ def run(self, max_size=None):
sage: A.run()
0
sage: A
- Core veering automaton with 2 vertices
+ Core veering automaton with 2 states
sage: A = ReducedCoreAutomaton()
sage: A.add_seed(T)
@@ -771,7 +750,7 @@ def run(self, max_size=None):
sage: A.run()
0
sage: A
- Reduced core veering automaton with 1 vertex
+ Reduced core veering automaton with 1 state
A more complicated surface in Q_1(1^2, -1^2)::
@@ -785,11 +764,11 @@ def run(self, max_size=None):
sage: C.run(10)
1
sage: C
- Partial core veering automaton with 10 vertices
+ Partial core veering automaton with 10 states
sage: C.run()
0
sage: C
- Core veering automaton with 1074 vertices
+ Core veering automaton with 1074 states
sage: C = ReducedCoreAutomaton()
sage: C.add_seed(T)
@@ -797,11 +776,11 @@ def run(self, max_size=None):
sage: C.run(10)
1
sage: C
- Partial reduced core veering automaton with 10 vertices
+ Partial reduced core veering automaton with 10 states
sage: C.run()
0
sage: C
- Reduced core veering automaton with 356 vertices
+ Reduced core veering automaton with 356 states
TESTS::
@@ -811,27 +790,26 @@ def run(self, max_size=None):
sage: A.add_seed(T)
1
sage: A
- Partial core veering automaton with 0 vertex
+ Partial core veering automaton with 0 state
sage: A.run(1)
1
sage: A
- Partial core veering automaton with 1 vertex
+ Partial core veering automaton with 1 state
sage: A.run(1)
1
sage: A
- Partial core veering automaton with 2 vertices
+ Partial core veering automaton with 2 states
sage: A.run(1)
0
sage: A
- Core veering automaton with 2 vertices
+ Core veering automaton with 2 states
"""
- forward_neighbors = self._forward_neighbors
- backward_neighbors = self._backward_neighbors
+ graph = self._graph
backward_flip_queue = self._backward_flip_queue
branch = collections.deque(self._branch)
count = 0
- old_size = len(self._forward_neighbors)
+ old_size = self._graph.num_verts()
while max_size is None or count < max_size:
if self._verbosity >= 2:
print('[automaton] new loop')
@@ -852,8 +830,7 @@ def run(self, max_size=None):
print('[automaton] seed %s' % (state,))
sys.stdout.flush()
- forward_neighbors[state] = []
- backward_neighbors[state] = []
+ graph.add_vertex(state)
if self._backward:
self._backward_flip_queue.append(state)
branch.clear()
@@ -877,10 +854,8 @@ def run(self, max_size=None):
sys.stdout.flush()
for out_neighbor, label in self._out_neighbors(state):
- if out_neighbor not in forward_neighbors:
- assert out_neighbor not in backward_neighbors
- forward_neighbors[out_neighbor] = []
- backward_neighbors[out_neighbor] = []
+ if out_neighbor not in graph:
+ graph.add_vertex(out_neighbor)
branch.append(out_neighbor)
if self._backward:
@@ -888,8 +863,7 @@ def run(self, max_size=None):
count += 1
- forward_neighbors[state].append((out_neighbor, label))
- backward_neighbors[out_neighbor].append((state, label))
+ graph.add_edge(state, out_neighbor, label)
self._branch = list(branch)
return 1
@@ -942,7 +916,7 @@ class FlipGraph(Automaton):
sage: A.run()
0
sage: A
- Triangulation automaton with 1 vertex
+ Triangulation automaton with 1 state
sage: fp = '(0,1,2)(~0,~3,~8)(3,5,4)(~4,~1,~5)(6,7,8)(~6,9,~2)'
sage: T = Triangulation(fp)
@@ -952,7 +926,7 @@ class FlipGraph(Automaton):
sage: A.run()
0
sage: A
- Triangulation automaton with 236 vertices
+ Triangulation automaton with 236 states
"""
_name = 'triangulation'
@@ -966,9 +940,9 @@ def _out_neighbors(self, state):
for e in state.flippable_edges():
new_state = state.copy(mutable=True)
new_state.flip(e)
- new_state.set_canonical_labels()
+ r = new_state.set_canonical_labels(mapping=True)
new_state.set_immutable()
- yield (new_state, e)
+ yield (new_state, (e, r))
_in_neighbors = _out_neighbors
@@ -989,26 +963,26 @@ def _out_neighbors(self, state):
state = state.copy(mutable=True)
for e in state.forward_flippable_edges():
for col in (BLUE, RED):
- old_col = state.colour(e)
+ old_col = state.edge_colour(e)
state.flip(e, col, check=CHECK)
if state.edge_has_curve(e):
new_state = state.copy(mutable=True)
- new_state.set_canonical_labels()
+ r = new_state.set_canonical_labels(mapping=True)
new_state.set_immutable()
- yield (new_state, (e, col))
+ yield (new_state, (e, col, r))
state.flip_back(e, old_col, check=CHECK)
def _in_neighbors(self, state):
state = state.copy(mutable=True)
for e in state.backward_flippable_edges():
for col in (BLUE, RED):
- old_col = state.colour(e)
+ old_col = state.edge_colour(e)
state.flip_back(e, col, check=CHECK)
if state.edge_has_curve(e):
new_state = state.copy(mutable=True)
- new_state.set_canonical_labels()
+ r = new_state.set_canonical_labels(mapping=True)
new_state.set_immutable()
- yield (new_state, (e, col))
+ yield (new_state, (e, col, r))
state.flip(e, col, check=CHECK)
@@ -1030,7 +1004,7 @@ class ReducedCoreAutomaton(Automaton):
sage: A.run()
0
sage: A
- Reduced core veering automaton with 1 vertex
+ Reduced core veering automaton with 1 state
A more complicated surface in Q_1(1^2, -1^2)::
@@ -1044,7 +1018,7 @@ class ReducedCoreAutomaton(Automaton):
sage: C_BFS.run()
0
sage: C_BFS
- Reduced core veering automaton with 356 vertices
+ Reduced core veering automaton with 356 states
sage: C_DFS = ReducedCoreAutomaton(method='DFS')
sage: C_DFS.add_seed(T)
@@ -1052,7 +1026,7 @@ class ReducedCoreAutomaton(Automaton):
sage: C_DFS.run()
0
sage: C_DFS
- Reduced core veering automaton with 356 vertices
+ Reduced core veering automaton with 356 states
"""
_name = 'reduced core veering'
@@ -1083,23 +1057,31 @@ def _flip(self, state, e, col, check=CHECK):
return (False, ())
recolorings = []
- a, b, c, d = state.square_about_edge(e)
+ a, b, c, d = state.square_about_half_edge(2 * e)
+ a //= 2
+ b //= 2
+ c //= 2
+ d //= 2
# assertions to be removed
- assert state._colouring[a] == RED, (a, colour_to_string(state._colouring[a]))
- assert state._colouring[b] == BLUE, (b, colour_to_string(state._colouring[b]))
- assert state._colouring[c] == RED, (c, colour_to_string(state._colouring[c]))
- assert state._colouring[d] == BLUE, (d, colour_to_string(state._colouring[d]))
+ cola = state._colouring[a]
+ colb = state._colouring[b]
+ colc = state._colouring[c]
+ cold = state._colouring[d]
+ cole = state._colouring[e]
+
+ assert cola == RED, (a, cola)
+ assert colb == BLUE, (b, colb)
+ assert colc == RED, (c, colc)
+ assert cold == BLUE, (d, cold)
if col == BLUE:
if state.is_forward_flippable(b):
recolorings.append((b, BLUE))
state._colouring[b] = PURPLE
- state._colouring[state._ep[b]] = PURPLE
if b != d and state.is_forward_flippable(d):
recolorings.append((d, BLUE))
state._colouring[d] = PURPLE
- state._colouring[state._ep[d]] = PURPLE
# assertions to be removed
if CHECK:
@@ -1111,11 +1093,9 @@ def _flip(self, state, e, col, check=CHECK):
if state.is_forward_flippable(a):
recolorings.append((a, RED))
state._colouring[a] = PURPLE
- state._colouring[state._ep[a]] = PURPLE
if a != c and state.is_forward_flippable(c):
recolorings.append((c, RED))
state._colouring[c] = PURPLE
- state._colouring[state._ep[c]] = PURPLE
if CHECK:
assert not state.is_forward_flippable(e)
@@ -1127,7 +1107,6 @@ def _flip(self, state, e, col, check=CHECK):
def _flip_back(self, state, e, recolorings, check=CHECK):
for ee, ccol in recolorings:
state._colouring[ee] = ccol
- state._colouring[state._ep[ee]] = ccol
state.flip_back(e, PURPLE, check=CHECK)
def _out_neighbors(self, state):
@@ -1137,9 +1116,9 @@ def _out_neighbors(self, state):
status, recolorings = self._flip(state, e, col, check=CHECK)
if state.edge_has_curve(e):
new_state = state.copy(mutable=True)
- new_state.set_canonical_labels()
+ r = new_state.set_canonical_labels(mapping=True)
new_state.set_immutable()
- yield (new_state, (e, col))
+ yield (new_state, (e, col, r))
self._flip_back(state, e, recolorings, check=CHECK)
# TODO: implement in_neighbors
@@ -1165,7 +1144,7 @@ class DelaunayAutomaton(Automaton):
sage: A.run()
0
sage: A
- Delaunay automaton with 54 vertices
+ Delaunay automaton with 54 states
One can check that the cardinality is indeed correct::
@@ -1197,7 +1176,7 @@ class DelaunayAutomaton(Automaton):
sage: A0.run()
0
sage: A0
- Delaunay automaton with 3 vertices
+ Delaunay automaton with 3 states
sage: A1 = DelaunayAutomaton()
sage: A1.add_seed(vt1)
@@ -1205,7 +1184,7 @@ class DelaunayAutomaton(Automaton):
sage: A1.run()
0
sage: A1
- Delaunay automaton with 2 vertices
+ Delaunay automaton with 2 states
sage: A2 = DelaunayAutomaton()
sage: A2.add_seed(vt2)
@@ -1213,7 +1192,7 @@ class DelaunayAutomaton(Automaton):
sage: A2.run()
0
sage: A2
- Delaunay automaton with 1 vertex
+ Delaunay automaton with 1 state
sage: B0 = DelaunayAutomaton(backward=True)
sage: B0.add_seed(vt0)
@@ -1221,7 +1200,7 @@ class DelaunayAutomaton(Automaton):
sage: B0.run()
0
sage: B0
- Delaunay automaton with 3 vertices
+ Delaunay automaton with 3 states
sage: B1 = DelaunayAutomaton(backward=True)
sage: B1.add_seed(vt1)
@@ -1229,7 +1208,7 @@ class DelaunayAutomaton(Automaton):
sage: B1.run()
0
sage: B1
- Delaunay automaton with 3 vertices
+ Delaunay automaton with 3 states
sage: B2 = DelaunayAutomaton(backward=True)
sage: B2.add_seed(vt2)
@@ -1237,7 +1216,7 @@ class DelaunayAutomaton(Automaton):
sage: B2.run()
0
sage: B2
- Delaunay automaton with 3 vertices
+ Delaunay automaton with 3 states
An example with linear constraint : the L-shape surface in the stratum H(2)
made of three squares::
@@ -1250,7 +1229,7 @@ class DelaunayAutomaton(Automaton):
sage: A.run()
0
sage: A
- Delaunay automaton with 6 vertices
+ Delaunay automaton with 6 states
Some more L-shape surfaces::
@@ -1261,10 +1240,10 @@ class DelaunayAutomaton(Automaton):
....: _ = A.add_seed(f)
....: _ = A.run()
....: print('n={}: {}'.format(n, A))
- n=3: Delaunay automaton with 6 vertices
- n=4: Delaunay automaton with 86 vertices
- n=5: Delaunay automaton with 276 vertices
- n=6: Delaunay automaton with 800 vertices
+ n=3: Delaunay automaton with 6 states
+ n=4: Delaunay automaton with 86 states
+ n=5: Delaunay automaton with 276 states
+ n=6: Delaunay automaton with 800 states
"""
_name = 'Delaunay'
@@ -1289,7 +1268,7 @@ def _out_neighbors(self, state, check=CHECK):
Run through the list of out neighbors.
"""
for edges, col in state.delaunay_flips(backend=self._backend):
- assert all(state.colour(e) == state.colour(edges[0]) for e in edges)
+ assert all(state.edge_colour(e) == state.edge_colour(edges[0]) for e in edges)
out_neighbor = state.copy(mutable=True)
for e in edges:
out_neighbor.flip(e, col, check=CHECK)
@@ -1306,7 +1285,7 @@ def _in_neighbors(self, state, check=CHECK):
Run through the list of in neighbors.
"""
for edges, col in state.backward_delaunay_flips(backend=self._backend):
- assert all(state.colour(e) == state.colour(edges[0]) for e in edges)
+ assert all(state.edge_colour(e) == state.edge_colour(edges[0]) for e in edges)
in_neighbor = state.copy(mutable=True)
for e in edges:
in_neighbor.flip_back(e, col, check=CHECK)
@@ -1376,7 +1355,7 @@ def cylinder_diagrams(self, col=RED):
"""
cylindricals = set()
orbits = []
- for x in self._forward_neighbors:
+ for x in self:
if x in cylindricals or not x.is_cylindrical(col):
continue
orbit = set()
@@ -1384,7 +1363,7 @@ def cylinder_diagrams(self, col=RED):
while todo:
x = todo.pop()
orbit.add(x)
- for y, (edges, new_col) in self._forward_neighbors[x]:
+ for _, y, (edges, new_col) in self._graph.outgoing_edges(x):
if new_col == col and y not in orbit:
assert y.is_cylindrical(col)
orbit.add(y)
@@ -1400,16 +1379,12 @@ def cylinder_diagrams(self, col=RED):
# transitions from Strebel graphs to Delauany triangulations
class DelaunayStrebelAutomaton(Automaton):
r"""
+ for vt in veering_triangulations:
+ # compute outgoing edges and record Strebel so that we deduce the Strebel -> veering
Delaunay-Strebel automaton.
The states of the Delaunay-Strebel automaton are Delaunay triangulations,
- vertical Strebel graphs and horizontal Strebel graphs. They are stored
- as pairs ``(kind, state)`` where
-
- - ``kind`` is either one of the strings ``'delaunay'``,
- ``'vertical-strebel'`` or ``'horizontal-strebel'``
- - ``state`` is either a :class:`VeeringTriangulation` or
- :class:`StrebelGraph`
+ vertical Strebel graphs.
EXAMPLES:
@@ -1433,12 +1408,10 @@ class DelaunayStrebelAutomaton(Automaton):
sage: DS.run()
0
sage: DS
- Delaunay-Strebel automaton with 11 vertices
- sage: sum(kind == 'vertical-strebel' for kind, state in DS)
+ Delaunay-Strebel automaton with 10 states
+ sage: sum(isinstance(state, StrebelGraph) for state in DS)
1
- sage: sum(kind == 'horizontal-strebel' for kind, state in DS)
- 1
- sage: sum(kind == 'delaunay' for kind, state in DS)
+ sage: sum(isinstance(state, VeeringTriangulation) for state in DS)
9
Some one and two dimensional examples in genus 0::
@@ -1455,40 +1428,39 @@ class DelaunayStrebelAutomaton(Automaton):
....: _ = DS.run()
....: DS._check()
....: print(DS)
- ....: n_hs = sum(kind == 'horizontal-strebel' for kind, state in DS)
- ....: n_vs = sum(kind == 'vertical-strebel' for kind, state in DS)
- ....: n_d = sum(kind == 'delaunay' for kind, state in DS)
- ....: print(n_hs, n_vs, n_d)
- StrebelGraph("(0,~1)(1,~0)")
- Delaunay-Strebel automaton with 10 vertices
- 1 1 8
- StrebelGraph("(0:2,~1)(1,~0)")
- Delaunay-Strebel automaton with 26 vertices
- 3 3 20
- StrebelGraph("(0:2,~1)(1,~0:2)")
- Delaunay-Strebel automaton with 16 vertices
- 2 2 12
- StrebelGraph("(0,~1)(1)(~0)")
- Delaunay-Strebel automaton with 6 vertices
- 1 1 4
- StrebelGraph("(0:2,~1)(1)(~0)")
- Delaunay-Strebel automaton with 20 vertices
- 3 3 14
+ ....: n_s = sum(isinstance(state, StrebelGraph) for state in DS)
+ ....: n_d = sum(isinstance(state, VeeringTriangulation) for state in DS)
+ ....: print(n_s, n_d)
+ StrebelGraph("(0,~1)(~0,1)")
+ Delaunay-Strebel automaton with 9 states
+ 1 8
+ StrebelGraph("(0:2,~1)(~0,1)")
+ Delaunay-Strebel automaton with 23 states
+ 3 20
+ StrebelGraph("(0:2,~1)(~0:2,1)")
+ Delaunay-Strebel automaton with 14 states
+ 2 12
+ StrebelGraph("(0,~1)(~0)(1)")
+ Delaunay-Strebel automaton with 5 states
+ 1 4
+ StrebelGraph("(0:2,~1)(~0)(1)")
+ Delaunay-Strebel automaton with 17 states
+ 3 14
StrebelGraph("(0,~0,~1:1,1)")
- Delaunay-Strebel automaton with 13 vertices
- 1 1 11
+ Delaunay-Strebel automaton with 12 states
+ 1 11
StrebelGraph("(0,~0,~1)(1)")
- Delaunay-Strebel automaton with 10 vertices
- 1 1 8
+ Delaunay-Strebel automaton with 9 states
+ 1 8
StrebelGraph("(0,2,~0,~1)(1)(~2)")
- Delaunay-Strebel automaton with 34 vertices
- 2 2 30
- StrebelGraph("(0,2,~1)(1)(~2,~0)")
- Delaunay-Strebel automaton with 52 vertices
- 1 1 50
- StrebelGraph("(0:2,2,~1)(1,~0)(~2)")
- Delaunay-Strebel automaton with 126 vertices
- 6 6 114
+ Delaunay-Strebel automaton with 32 states
+ 2 30
+ StrebelGraph("(0,2,~1)(~0,~2)(1)")
+ Delaunay-Strebel automaton with 51 states
+ 1 50
+ StrebelGraph("(0:2,2,~1)(~0,1)(~2)")
+ Delaunay-Strebel automaton with 120 states
+ 6 114
An example with linear constraints::
@@ -1500,7 +1472,7 @@ class DelaunayStrebelAutomaton(Automaton):
sage: A.run()
0
sage: A
- Delaunay-Strebel automaton with 250 vertices
+ Delaunay-Strebel automaton with 248 states
An example with folded edges (quadratic differential with simple poles on
edges)::
@@ -1512,7 +1484,20 @@ class DelaunayStrebelAutomaton(Automaton):
sage: A.run()
0
sage: A
- Delaunay-Strebel automaton with 46 vertices
+ Delaunay-Strebel automaton with 45 states
+
+ TESTS::
+
+
+ sage: from veerer.automaton import DelaunayStrebelAutomaton
+ sage: from veerer import *
+ sage: vt = VeeringTriangulationLinearFamily("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~4,~6)(0:1)(~5:1)", "RBRBRRB", [(1, 0, 1, 0, 0, 1, 0), (0, 1, -1, 0, 1, 0, 1), (0, 0, 0, 1, -1, 0, 0)])
+ sage: DS = DelaunayStrebelAutomaton(backward=True)
+ sage: DS.add_seed(vt)
+ 1
+ sage: DS.run()
+ 0
+ sage: DS._check()
"""
_name = 'Delaunay-Strebel'
@@ -1521,126 +1506,119 @@ def _check(self):
from .features import surface_dynamics_feature
if surface_dynamics_feature.is_present():
- s = set(state.stratum() for kind, state in self)
+ s = set(state.stratum() for state in self)
if len(s) != 1:
raise ValueError('got different strata: {}'.format(sorted(s)))
- horizontal_strebel = set()
- vertical_strebel = set()
- for kind, state in self:
- if kind == 'horizontal-strebel':
- horizontal_strebel.add(state)
- elif kind == 'vertical-strebel':
- vertical_strebel.add(state)
-
- assert horizontal_strebel == vertical_strebel, horizontal_strebel.symmetric_difference(vertical_strebel)
+ for source, target, label in self._graph.edges(sort=False):
+ kind = label[0]
+ if kind == "flip":
+ assert len(label) == 5
+ edges = label[1]
+ old_col = label[2]
+ new_col = label[3]
+ r = label[4]
+ state = source.copy(mutable=True)
+ for e in edges:
+ assert state.edge_colour(e) == old_col
+ state.flip(e, new_col)
+ state.relabel(r)
+ assert state == target, (source, state, target)
+ elif kind == "rotate":
+ assert len(label) == 2
+ r = label[1]
+ state = source.copy(mutable=True)
+ state.rotate()
+ state.relabel(r)
+ assert state == target, (source, state, target)
+ elif kind == "strebel":
+ assert len(label) == 3
+ r1 = label[1] # strebel edges (-1 at positions which are not strebel)
+ r2 = label[2] # strebel to veering boundary
+ for h in target.half_edges():
+ a1 = target.face_angle(h)
+ a2 = source.face_angle(r2[h])
+ assert a1 == a2, (source, target, h, r2[h], a1, a2)
def _setup(self, backend=None):
self._backend = backend
def _seed_setup(self, state):
- if isinstance(state, tuple) and len(state) == 2:
- kind, state = state
- elif isinstance(state, VeeringTriangulation):
- kind = 'delaunay'
- elif isinstance(state, StrebelGraph):
- kind = 'vertical-strebel'
- else:
+ if not isinstance(state, (VeeringTriangulation, StrebelGraph)):
raise TypeError('invalid state')
if self._backend is None:
from .polyhedron.cone import default_backend
self._backend = default_backend(state.base_ring())
- if kind == 'delaunay':
+ if isinstance(state, VeeringTriangulation):
if not state.is_delaunay(backend=self._backend):
raise ValueError('invalid seed: non-Delaunay veering triangulation {}'.format(state))
state = state.copy(mutable=True)
state.set_canonical_labels()
state.set_immutable()
- return (kind, state)
+ return state
def _out_neighbors(self, state, check=CHECK):
r"""
Return the list of out-neighbors.
+
+ These are
+ - forward flips
+ - strebelization (if no forward flip)
+ - rotation
"""
- kind, state = state
- if kind == 'delaunay':
+ if isinstance(state, VeeringTriangulation):
flips = state.delaunay_flips(backend=self._backend)
if not flips:
+ # strebelization
+ if self._verbosity >= 2:
+ print('[_out_neighbors] strebelization')
if CHECK:
assert state.is_strebel(VERTICAL)
- out_neighbor = state.strebel_graph(VERTICAL, mutable=True)
- out_neighbor.set_canonical_labels()
+ # r1: index_strebel
+ # r2: strebel_to_veering_boundary
+ out_neighbor, r1, r2 = state.strebel_graph(VERTICAL, mapping=True, mutable=True)
+ # r3: relabelling of the Strebel graph
+ r3 = out_neighbor.set_canonical_labels(mapping=True)
out_neighbor.set_immutable()
- yield (('vertical-strebel', out_neighbor), 'strebel')
- return
+ yield (out_neighbor, ('strebel', perm_compose(r1, r3), perm_compose(perm_invert(r3), r2)))
- for edges, col in flips:
- assert all(state.colour(e) == state.colour(edges[0]) for e in edges)
+ # rotation
+ if self._verbosity >= 2:
+ print('[_out_neighbors] rotate')
out_neighbor = state.copy(mutable=True)
- for e in edges:
- out_neighbor.flip(e, col, check=CHECK)
- if CHECK:
- out_neighbor._check(RuntimeError)
- if not out_neighbor.is_delaunay(backend=self._backend):
- raise RuntimeError
- out_neighbor.set_canonical_labels()
+ out_neighbor.rotate()
+ r = out_neighbor.set_canonical_labels(mapping=True)
out_neighbor.set_immutable()
- yield ((kind, out_neighbor), (edges, col))
-
- elif kind == 'vertical-strebel':
- return
+ yield (out_neighbor, ('rotate', r))
- elif kind == 'horizontal-strebel':
- for colouring in state.colourings():
- for vt in state.delaunay_triangulations(colouring, HORIZONTAL, mutable=True):
- vt.set_canonical_labels()
- vt.set_immutable()
- yield (('delaunay', vt), colouring)
-
- else:
- raise RuntimeError
-
- def _in_neighbors(self, state):
- """
- Return the list of Delaunay backward flippable edges from ``state``.
- """
- kind, state = state
- if kind == 'delaunay':
- flips = state.backward_delaunay_flips(backend=self._backend)
- if not flips:
- if CHECK:
- assert state.is_strebel(HORIZONTAL)
- in_neighbor = state.strebel_graph(HORIZONTAL, mutable=True)
- in_neighbor.set_canonical_labels()
- in_neighbor.set_immutable()
- yield (('horizontal-strebel', in_neighbor), 'strebel')
- return
-
- for edges, col in flips:
- assert all(state.colour(e) == state.colour(edges[0]) for e in edges)
- in_neighbor = state.copy(mutable=True)
- for e in edges:
- in_neighbor.flip_back(e, col, check=CHECK)
+ else:
+ # forward flips
+ if self._verbosity >= 2:
+ print('[_out_neighbors] forward_flips')
+ for edges, new_col in flips:
+ old_col = state.edge_colour(edges[0])
+ assert all(state.edge_colour(e) == old_col for e in edges)
+ out_neighbor = state.copy(mutable=True)
+ for e in edges:
+ out_neighbor.flip(e, new_col, check=CHECK)
if CHECK:
- in_neighbor._check(RuntimeError)
- if not in_neighbor.is_delaunay(backend=self._backend):
+ out_neighbor._check(RuntimeError)
+ if not out_neighbor.is_delaunay(backend=self._backend):
raise RuntimeError
- in_neighbor.set_canonical_labels()
- in_neighbor.set_immutable()
- yield ((kind, in_neighbor), (edges, col))
+ r = out_neighbor.set_canonical_labels(mapping=True)
+ out_neighbor.set_immutable()
+ yield (out_neighbor, ('flip', edges, old_col, new_col, r))
- elif kind == 'vertical-strebel':
+ def _in_neighbors(self, state, check=CHECK):
+ if isinstance(state, StrebelGraph):
for colouring in state.colourings():
for vt in state.delaunay_triangulations(colouring, VERTICAL, mutable=True):
vt.set_canonical_labels()
vt.set_immutable()
- yield (('delaunay', vt), colouring)
+ yield (vt, colouring)
+
- elif kind == 'horizontal-strebel':
- return
- else:
- raise RuntimeError
diff --git a/veerer/constellation.py b/veerer/constellation.py
index 27654294..7298189f 100644
--- a/veerer/constellation.py
+++ b/veerer/constellation.py
@@ -27,71 +27,151 @@
# ****************************************************************************
import collections
+import itertools
import numbers
from array import array
from sage.structure.richcmp import op_LT, op_LE, op_EQ, op_NE, op_GT, op_GE, rich_to_bool
-from .permutation import (perm_init, perm_check, perm_cycles, perm_dense_cycles,
+from .permutation import (perm_init, perm_check, perm_cycles, perm_on_array, perm_on_edge_array,
perm_invert, perm_conjugate, perm_cycle_string, perm_cycles_lengths,
- perm_cycles_to_string, perm_on_list, perm_cycle_type,
+ perm_cycles_to_string, perm_on_list, perm_on_edge_list, perm_cycle_type,
perm_num_cycles, str_to_cycles, str_to_cycles_and_data, perm_compose, perm_from_base64_str,
uint_base64_str, uint_from_base64_str, perm_base64_str,
- perms_are_transitive, perms_orbits, triangulation_relabelling_from)
+ perms_are_transitive, perms_orbits, perm_edge_orbits, edge_relabelling_from)
+
+
+def check_relabelling(arg, ne):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.constellation import check_relabelling
+ sage: from veerer.permutation import perm_cycle_string
+ sage: p = check_relabelling("(0,1,2)", 3)
+ sage: perm_cycle_string(p, edge_like=True)
+ '(0,1,2)(~0,~1,~2)'
+ sage: p = check_relabelling("(0,1,2)(~0,~1,~2)", 3)
+ sage: perm_cycle_string(p, edge_like=True)
+ '(0,1,2)(~0,~1,~2)'
+ sage: p = check_relabelling("(0,~1,2)", 3)
+ sage: perm_cycle_string(p, edge_like=True)
+ '(0,~1,2)(~0,1,~2)'
+ sage: p = check_relabelling("(0,1,2,~0,~1,~2)", 3)
+ sage: perm_cycle_string(p, edge_like=True)
+ '(0,1,2,~0,~1,~2)'
+ """
+ n = 2 * ne
+ if isinstance(arg, str):
+ p = perm_init(arg, n, edge_like=True, partial=True)
+ else:
+ p = perm_init(arg, n, partial=True)
+
+ if len(p) != n:
+ raise ValueError("len(p) = {} while n = {}".format(len(p), n))
+
+ for h in range(n):
+ if p[h] == -1 and p[h ^ 1] == -1:
+ p[h] = h
+ p[h ^ 1] = h ^ 1
+ elif p[h] == -1:
+ p[h] = p[h ^ 1] ^ 1
+ elif p[h ^ 1] == -1:
+ p[h ^ 1] = p[h] ^ 1
+ elif p[h ^ 1] != p[h] ^ 1:
+ raise ValueError("invalid input for relabelling (arg={})".format(arg))
+
+ if not perm_check(p, n):
+ raise ValueError("invalid input for relabelling (arg={})".format(arg))
+
+ return p
+
+
-# TODO: maybe do a class Constellation with boundary?
class Constellation:
- __slots__ = ['_mutable', '_n', '_fp', '_ep', '_vp', '_data']
+ __slots__ = ['_mutable', # mutability flag
+ '_ne', # number of edges
+ '_vp', # vertex permutation
+ '_fp', # face permutation
+ '_half_edges_data', # a list of half-edges data: each element is an array of length 2 * _ne
+ '_edges_data', # a list of edges data: each element is an array of length _ne
+ ]
- def __init__(self, n, vp, ep, fp, data, mutable=False, check=True):
- self._n = n
+ def __init__(self, ne, vp, fp, half_edges_data, edges_data, mutable=False, check=True):
+ self._ne = ne
if vp is None:
- vp = self._vp = array('i', [-1] * n)
- for i in range(n):
- vp[fp[ep[i]]] = i
+ vp = self._vp = array('i', [-1] * (2 * ne))
+ for i in range(2 * ne):
+ if fp[i] == -1:
+ continue
+ ii = fp[i ^ 1 if fp[i ^ 1] != -1 else i]
+ vp[ii] = i
else:
self._vp = vp
- self._ep = ep
- self._fp = fp
+ if fp is None:
+ fp = self._fp = array('i', [-1] * (2 * ne))
+ for i in range(2 * ne):
+ if vp[i] == -1:
+ continue
+ ii = vp[i] ^ 1 if vp[i] != -1 else vp[i]
+ fp[ii] = i
+ else:
+ self._fp = fp
- self._data = data
+ self._half_edges_data = half_edges_data
+ self._edges_data = edges_data
self._mutable = mutable
self._set_data_pointers()
if check:
self._check(ValueError)
+ def _ep(self, i):
+ if self._vp[i] == -1:
+ return -1
+ elif self._vp[i ^ 1] == -1:
+ return i
+ else:
+ return i ^ 1
+
def _set_data_pointers(self):
pass
def _check(self, error=RuntimeError):
- n = self._n
+ ne = self._ne
- if not (hasattr(self, '_vp') and hasattr(self, '_ep') and hasattr(self, '_fp') and hasattr(self, '_data')):
+ if not (hasattr(self, '_vp') and hasattr(self, '_fp') and hasattr(self, '_half_edges_data') and hasattr(self, '_edges_data')):
raise error('missing attributes: these must be _vp, _ep, _fp, _data')
- if not perm_check(self._vp, n):
+ if not perm_check(self._vp, 2 * ne):
raise error('vp is not a permutation: {}'.format(self._vp))
- if not perm_check(self._ep, n):
- raise error('ep is not permutation: {}'.format(self._ep))
- if not perm_check(self._fp, n):
+ if not perm_check(self._fp, 2 * ne):
raise error('fp is not a permutation: {}'.format(self._fp))
- # NOTE: we do not necessarily want to assume connectedness of the underlying surface
- # if not perms_are_transitive([self._vp, self._ep, self._fp]):
- # raise error('(fp, ep, vp) do not generate a transitive group')
- for l in self._data:
- if not isinstance(l, collections.abc.Sequence) or len(l) != n:
- raise error('each data must be a sequence of same length as the underlying permutations got a {} of length {}'.format(type(l).__name__, len(l)))
- if self._mutable and not isinstance(l, collections.abc.MutableSequence):
- raise error('immutable data in mutable object')
- for i in range(n):
- if self._ep[self._ep[i]] != i:
- raise error('invalid edge permutation at half-edge i={} (vp={} ep={} fp={})'.format(self._half_edge_string(i), self._vp, self._ep, self._fp))
- if self._fp[self._ep[self._vp[i]]] != i:
+ for i in range(2 * ne):
+ if (self._vp[i] == -1) != (self._fp[i] == -1):
+ raise ValueError("vp and fp with different domains")
+
+ for i in range(2 * ne):
+ if self._vp[i] != -1 and self._fp[self._ep(self._vp[i])] != i:
raise error('fev relation not satisfied at half-edge i={}'.format(self._half_edge_string(i)))
+ if not isinstance(self._half_edges_data, tuple):
+ raise ValueError("half_edges_data must be a tuple")
+ for l in self._half_edges_data:
+ if not isinstance(l, array) or l.typecode != 'i' or len(l) != 2 * ne:
+ raise ValueError("each half edge data must be an array of length twice the number of edges: got a {} of length {}".format(type(l).__name__, len(l)))
+ for i in range(2 * ne):
+ if self._vp[i] == -1:
+ if l[i]:
+ raise error('non-zero entry {} in half-edge data at the non-active half-edge {}'.format(l[i], i))
+
+ if not isinstance(self._edges_data, tuple):
+ raise ValueError("edges_data must be a tuple")
+ for l in self._edges_data:
+ if not isinstance(l, array) or l.typecode != 'i' or len(l) != ne:
+ raise error("each edges data must be an array of length the number of edges; got a {} of length {}".format(type(l).__name__, len(l)))
+
def _check_alloc(self, n):
if len(self._vp) < n or len(self._ep) < n or len(self._fp) < n:
raise TypeError("reallocation needed")
@@ -110,22 +190,20 @@ def __getstate__(self):
sage: from veerer import Triangulation, VeeringTriangulation, StrebelGraph
sage: t = Triangulation("(0,1,2)")
- sage: dumps(t) # indirect doctest
- b'x\x9ck`J.KM-J-\xd2+)\xcaL\xccK/\xcdI,\xc9\xcc\xcf\xe3\nA\xe1\x152h6\x162\xc6\x162ix3{3vz3z3y3\x00!\x8cfHM\xd2\x03\x00\xb9\xd6\x15\xd9'
-
+ sage: _ = dumps(t) # indirect doctest
sage: t = VeeringTriangulation("(0,1,2)", "BBR")
- sage: dumps(t) # indirect doctest
- b'x\x9ck`J.KM-J-\xd2\x03Q\x99y\xe9\xf1%E\x99\x89y\xe9\xa59\x89%\x99\xf9y\\a\x10\xd1\x10\x14\xc1B\x06\xcd\xc6B\xc6\xd8B&\rofo\xa6NoFo&o\x06 \x84\xd1\x0c@\x9a\xc9\x9b15I\x0f\x00Q\x1f\x1c\xdf'
+ sage: _ = dumps(t) # indirect doctest
sage: t = VeeringTriangulation("(0,1,2)(~0,3,4)", "(~1:1)(~2:1)(~3:1)(~4:1)", "RBRBR")
- sage: dumps(t) # indirect doctest
- b'x\x9ck`J.KM-J-\xd2\x03Q\x99y\xe9\xf1%E\x99\x89y\xe9\xa59\x89%\x99\xf9y\\a\x10\xd1\x10\x14\xc1B\x06\xcd\xc6B\xc6\xd8B&\ro.o\xa6NoFo&o\x06o\x16oNoVo6ovo\x0eof \x9b\x03\xc8b\x03\x8a\xb0\x00yL@5\x0cH\x90\x11\n\x19\xc0z!\x18\xceJM\xd2\x03\x00\x8a)%{'
+ sage: _ = dumps(t) # indirect doctest
"""
- a = [self._n]
- a.append(len(self._data))
+ a = [self._ne]
+ a.append(len(self._half_edges_data))
+ a.append(len(self._edges_data))
a.append(self._mutable)
a.extend(self._fp)
- a.extend(self._ep)
- for l in self._data:
+ for l in self._half_edges_data:
+ a.extend(l)
+ for l in self._edges_data:
a.extend(l)
return a
@@ -137,15 +215,19 @@ def __setstate__(self, arg):
sage: t0 = Triangulation("(0,1,2)", mutable=False)
sage: t1 = Triangulation("(0,1,2)", mutable=True)
sage: s0 = loads(dumps(t0)) # indirect doctest
- sage: assert s0 == t0
- sage: s0._mutable
- False
+ sage: assert s0 == t0 and s0._mutable is False
sage: s0._check()
+ sage: s1 = loads(dumps(t1)) # indirect doctest
+ sage: assert s1 == t1 and s1._mutable is True
+ sage: s1._check()
+ sage: t0 = Triangulation("(0,1,2)(~0:1)(~1:1)", mutable=False)
+ sage: t1 = Triangulation("(0,1,2)(~0:1)(~1:1)", mutable=True)
+ sage: s0 = loads(dumps(t0)) # indirect doctest
+ sage: assert s0 == t0 and s0._mutable is False
+ sage: s0._check()
sage: s1 = loads(dumps(t1)) # indirect doctest
- sage: assert s1 == t1
- sage: s1._mutable
- True
+ sage: assert s1 == t1 and s1._mutable is True
sage: s1._check()
sage: from veerer import VeeringTriangulation
@@ -154,35 +236,46 @@ def __setstate__(self, arg):
sage: t2 = VeeringTriangulation("(0,1,2)(~0,3,4)", "(~1:1)(~2:1)(~3:1)(~4:1)", "RBRBR")
sage: s0 = loads(dumps(t0)) # indirect doctest
- sage: assert s0 == t0
- sage: s0._mutable
- False
+ sage: assert s0 == t0 and s0._mutable is False
sage: s0._check()
sage: s1 = loads(dumps(t1)) # indirect doctest
- sage: assert s1 == t1
- sage: s1._mutable
- True
+ sage: assert s1 == t1 and s1._mutable is True
sage: s1._check()
sage: s2 = loads(dumps(t2))
sage: assert s2 == t2
"""
# We do not know how many slots we have in data
- n = self._n = arg[0]
- k = arg[1] # length of data
- self._mutable = arg[2]
- self._fp = array('i', arg[3 : n + 3])
- self._ep = array('i', arg[n + 3 : 2 * n + 3])
- data = []
- for i in range(2, k + 2):
- data.append(array('i', arg[i * n + 3 : (i + 1) * n + 3]))
+ ne = self._ne = arg[0]
+ n = 2 * ne
+ k_half_edges = arg[1] # length of half-edges data
+ k_edges = arg[2] # length of edges data
+ self._mutable = arg[3]
+ shift = 4
+ self._fp = array('i', arg[shift : shift + n])
+ shift += n
+
+ half_edges_data = []
+ for _ in range(k_half_edges):
+ half_edges_data.append(array('i', arg[shift: shift + n]))
+ shift += n
+ edges_data = []
+ for i in range(k_edges):
+ edges_data.append(array('i', arg[shift: shift + ne]))
+ shift += ne
+
+ assert shift == len(arg)
self._vp = array('i', [-1] * n)
for i in range(n):
- self._vp[self._fp[self._ep[i]]] = i
+ if self._fp[i] == -1:
+ continue
+ ii = (i ^ 1) if self._fp[i ^ 1] != -1 else i
+ self._vp[self._fp[ii]] = i
- self._data = tuple(data)
+ self._half_edges_data = tuple(half_edges_data)
+ self._edges_data = tuple(edges_data)
self._set_data_pointers()
def set_immutable(self):
@@ -273,22 +366,37 @@ def __hash__(self):
raise ValueError('mutable veering triangulation not hashable')
x = 140737488617563
- x = ((x ^ hash(self._vp.tobytes())) * 2147483693) + 82520 + self._n + self._n
- x = ((x ^ hash(self._ep.tobytes())) * 2147483693) + 82520 + self._n + self._n
+ n = 2 * self._ne
+ x = ((x ^ hash(self._vp.tobytes())) * 2147483693) + 82520 + n
- for l in self._data:
- x = ((x ^ hash(l.tobytes())) * 2147483693) + 82520 + self._n + self._n
+ for l in self._half_edges_data:
+ x = ((x ^ hash(l.tobytes())) * 2147483693) + 82520 + n
+ for l in self._edges_data:
+ x = ((x ^ hash(l.tobytes())) * 2147483693) + 82520 + n
return x
- def _check_half_edge(self, e):
+ def _check_half_edge(self, h):
+ if not isinstance(h, numbers.Integral):
+ raise TypeError('invalid half-edge {}'.format(h))
+ h = int(h)
+ if h < 0 or h >= 2 * self._ne:
+ raise ValueError('half-edge number out of range e={}'.format(e))
+ if self._vp[h] == -1:
+ raise ValueError("invalid half-edge h={}; the underlying edges is folded".format(h))
+ return h
+
+ def _check_edge(self, e):
if not isinstance(e, numbers.Integral):
- raise TypeError('invalid half-edge {}'.format(e))
+ raise TypeError("invalid edge {}".format(e))
e = int(e)
- if e < 0 or e >= self._n:
- raise ValueError('half-edge number out of range e={}'.format(e))
+ if e < 0 or e >= self._ne:
+ raise ValueError("edge number out of range e={}".format(e))
return e
+ def constellation(self):
+ return self
+
def to_string(self):
r"""
Serialize this triangulation as a string.
@@ -298,23 +406,29 @@ def to_string(self):
sage: from veerer import Triangulation, VeeringTriangulation, StrebelGraph
sage: Triangulation("(0,1,2)(~0,~1,~2)").to_string()
- '6_354102_543210_120534_000000'
+ '3_1___234501_000000'
sage: Triangulation("(0,1,2)", boundary="(~0:1)(~1:1,~2:1)").to_string()
- '6_354120_543210_120435_000111'
+ '3_1___214503_010101'
sage: VeeringTriangulation("(0,1,2)", "RRB").to_string()
- '3_201_012_120_000_112'
+ '3_1_1__2~4~0~_000000_112'
sage: StrebelGraph("(0,1,2)(~0,~1:1,~2:2)").to_string()
- '6_354102_543210_120534_000210'
+ '3_1___234501_000102'
"""
- return uint_base64_str(self._n) + '_' + perm_base64_str(self._vp) + '_' + perm_base64_str(self._ep) + '_' + perm_base64_str(self._fp) + '_' + '_'.join(perm_base64_str(l) for l in self._data)
-
- def from_face_edge_perms(self, fp, ep, data=(), mutable=False, check=True):
- raise ValueError
+ data = [uint_base64_str(self._ne),
+ uint_base64_str(len(self._half_edges_data)),
+ uint_base64_str(len(self._edges_data)),
+ uint_base64_str(self._mutable),
+ perm_base64_str(self._fp)]
+ for l in self._half_edges_data:
+ data.append(perm_base64_str(l))
+ for l in self._edges_data:
+ data.append(perm_base64_str(l))
+ return '_'.join(data)
@classmethod
- def from_permutations(cls, vp, ep, fp, data=(), mutable=False, check=True):
+ def from_permutations(cls, vp, fp, half_edges_data=(), edges_data=(), mutable=False, check=True):
r"""
INPUT:
@@ -330,51 +444,48 @@ def from_permutations(cls, vp, ep, fp, data=(), mutable=False, check=True):
sage: from veerer import Triangulation, VeeringTriangulation, StrebelGraph
sage: from array import array
- sage: vp = array('i', [2, 8, 7, 0, 3, 1, 5, 6, 4])
- sage: ep = array('i', [8, 7, 2, 3, 4, 5, 6, 1, 0])
- sage: fp = array('i', [1, 2, 0, 4, 8, 6, 7, 5, 3])
- sage: Triangulation.from_permutations(vp, ep, fp, (array('i', [0] * 9),))
- Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- sage: Triangulation.from_permutations(None, ep, fp, (array('i', [0] * 9),))
- Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- sage: Triangulation.from_permutations(vp, None, fp, (array('i', [0] * 9),))
- Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- sage: Triangulation.from_permutations(vp, ep, None, (array('i', [0] * 9),))
- Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
-
- sage: vp = array('i', [1, 3, 0, 2])
- sage: ep = array('i', [3, 2, 1, 0])
- sage: StrebelGraph.from_permutations(vp, ep, None, data=(array('i', [1, 0, 0, 1]),))
+ sage: vp = array('i', [4, 8, 1, 12, 3, -1, 0, -1, 6, -1, 2, -1, 10, -1])
+ sage: fp = array('i', [2, 6, 4, 10, 0, -1, 8, -1, 1, -1, 12, -1, 3, -1])
+
+ sage: Triangulation.from_permutations(vp, fp, (array('i', [0] * 14),))
+ Triangulation("(0,1,2)(~0,3,4)(~1,5,6)")
+ sage: Triangulation.from_permutations(vp, None, (array('i', [0] * 14),))
+ Triangulation("(0,1,2)(~0,3,4)(~1,5,6)")
+ sage: Triangulation.from_permutations(None, fp, (array('i', [0] * 14),))
+ Triangulation("(0,1,2)(~0,3,4)(~1,5,6)")
+
+ sage: vp = array('i', [2, 3, 1, 0])
+ sage: StrebelGraph.from_permutations(vp, None, (array('i', [1, 1, 0, 0]),))
StrebelGraph("(0:1,1,~0:1,~1)")
"""
- if (vp is None) + (ep is None) + (fp is None) > 1:
- raise ValueError('at most one of vp, ep, fp could be None')
+ if (vp is None) and (fp is None):
+ raise ValueError('at most one of vp or fp could be None')
C = cls.__new__(cls)
- if vp is not None:
- n = len(vp)
- elif ep is not None:
- n = len(ep)
+ n = len(vp) if vp is not None else len(fp)
+ if n % 2:
+ raise ValueError("permutations must be even length")
if vp is None:
vp = array('i', [-1] * n)
for i in range(n):
- vp[fp[ep[i]]] = i
- elif ep is None:
- ep = array('i', [-1] * n)
- for i in range(n):
- ep[vp[fp[i]]] = i
+ if fp[i] == -1:
+ continue
+ ii = (i ^ 1) if fp[i ^ 1] != -1 else i
+ vp[fp[ii]] = i
elif fp is None:
fp = array('i', [-1] * n)
for i in range(n):
- fp[ep[vp[i]]] = i
+ if vp[i] != -1:
+ ii = (vp[i] ^ 1) if vp[vp[i] ^ 1] != -1 else vp[i]
+ fp[ii] = i
- C._n = n
+ C._ne = n // 2
C._vp = vp
- C._ep = ep
C._fp = fp
+ C._half_edges_data = tuple(half_edges_data)
+ C._edges_data = tuple(edges_data)
C._mutable = mutable
- C._data = data
C._set_data_pointers()
if check:
@@ -396,17 +507,15 @@ def from_string(cls, s, mutable=False, check=True):
True
"""
parts = s.split('_')
- n = parts[0]
- vp = parts[1]
- ep = parts[2]
- fp = parts[3]
- data = parts[4:]
- n = uint_from_base64_str(n)
- vp = perm_from_base64_str(vp, n)
- ep = perm_from_base64_str(ep, n)
- fp = perm_from_base64_str(fp, n)
- data = tuple(perm_from_base64_str(ss, n) for ss in data)
- return cls.from_permutations(vp, ep, fp, data, mutable, check)
+ ne = uint_from_base64_str(parts[0])
+ k_half_edges = uint_from_base64_str(parts[1])
+ k_edges = uint_from_base64_str(parts[2])
+ mutable = bool(uint_from_base64_str(parts[3]))
+ fp = perm_from_base64_str(parts[4], 2 * ne)
+ shift = 5
+ half_edges_data = tuple(perm_from_base64_str(parts[i], 2 * ne) for i in range(5, 5 + k_half_edges))
+ edges_data = tuple(perm_from_base64_str(parts[i], ne) for i in range(5 + k_half_edges, 5 + k_half_edges + k_edges))
+ return cls.from_permutations(None, fp, half_edges_data, edges_data, mutable=mutable, check=check)
def __eq__(self, other):
r"""
@@ -426,9 +535,7 @@ def __eq__(self, other):
sage: StrebelGraph("(0,1,2,~0,~1,~2)") == StrebelGraph("(0,1:1,2,~0,~1,~2)")
False
"""
- if type(self) != type(other):
- raise TypeError
- return self._n == other._n and self._fp == other._fp and self._ep == other._ep and self._data == other._data
+ return self._ne == other._ne and self._fp == other._fp and self._half_edges_data == other._half_edges_data and self._edges_data == other._edges_data
def __ne__(self, other):
r"""
@@ -448,31 +555,72 @@ def __ne__(self, other):
sage: StrebelGraph("(0,1,2,~0,~1,~2)") != StrebelGraph("(0,1:1,2,~0,~1,~2)")
True
"""
- if type(self) != type(other):
- raise TypeError
- return self._n != other._n or self._fp != other._fp or self._ep != other._ep or self._data != other._data
+ return self._ne != other._ne or self._fp != other._fp or self._half_edges_data != other._half_edges_data or self._edges_data != other._edges_data
- def _richcmp_(self, other, op):
+ def _cmp_(self, other):
r"""
- Compare ``self`` and ``other`` according to the operator ``op``.
+ TESTS::
+
+ sage: import itertools
+ sage: from veerer import Triangulation
+ sage: ts = [Triangulation("(0,1,2)"), Triangulation("(0:1,1:1,2:1)"), Triangulation("(0:1,1:1,2:2)"), Triangulation("(0,1,2)(~0,~1,~2)"), Triangulation("(0,1,2)(~0,~1,~2)"), Triangulation("(0,~0,1)(~1,2,~2)")]
+ sage: for t1, t2 in itertools.product(ts, repeat=2):
+ ....: c1 = t1._cmp_(t2)
+ ....: c2 = t2._cmp_(t1)
+ ....: assert c1 == -c2
+ ....: assert (c1 == 0) == (t1 == t2)
"""
- if type(self) != type(other):
- raise TypeError
+ if type(self) is not type(other):
+ raise TypeError("can not compare {} with {}".format(type(self).__name__, type(other).__name__))
- c = (self._n > other._n) - (self._n < other._n)
+ c = (self._ne > other._ne) - (self._ne < other._ne)
if c:
- return rich_to_bool(op, c)
+ return c
c = (self._fp > other._fp) - (self._fp < other._fp)
if c:
- return rich_to_bool(op, c)
+ return c
- c = (self._ep > other._ep) - (self._ep < other._ep)
+ c = (self._half_edges_data > other._half_edges_data) - (self._half_edges_data < other._half_edges_data)
if c:
- return rich_to_bool(op, c)
+ return c
+
+ c = (self._edges_data > other._edges_data) - (self._edges_data < other._edges_data)
+ return c
- c = (self._data > other._data) - (self._data < other._data)
- return rich_to_bool(op, c)
+ def _richcmp_(self, other, op):
+ r"""
+ Compare ``self`` and ``other`` according to the operator ``op``.
+
+ EXAMPLES::
+
+ sage: import itertools
+ sage: from veerer import Triangulation, VeeringTriangulation
+
+ sage: ts = [Triangulation("(0,1,2)"), Triangulation("(0:1,1:1,2:1)"), Triangulation("(0:1,1:1,2:2)"), Triangulation("(0,1,2)(~0,~1,~2)"), Triangulation("(0,1,2)(~0,~1,~2)"), Triangulation("(0,~0,1)(~1,2,~2)")]
+ sage: for t1, t2 in itertools.product(ts, repeat=2):
+ ....: if t1 == t2:
+ ....: assert (t1 <= t2)
+ ....: assert (t1 >= t2)
+ ....: assert not (t1 < t2)
+ ....: assert not (t1 > t2)
+ ....: else:
+ ....: assert (t1 < t2) + (t2 < t1) == 1
+ ....: assert (t1 > t2) + (t2 > t1) == 1
+ ....: assert (t1 < t2) == (t1 <= t2)
+ ....: assert (t1 > t2) == (t1 >= t2)
+
+ sage: vt0 = VeeringTriangulation("(0:1)(~0:1,1:1,2:1)(~1:1,~2:1,3:1)(~3:1)", "RRBR")
+ sage: vt1 = VeeringTriangulation("(0:1)(~0:1,1:1,2:1)(~1:1,~2:1,3:1)(~3:1)", "BRRB")
+ sage: (vt0 < vt1) + (vt0 == vt1) + (vt0 > vt1)
+ 1
+ sage: (vt1 < vt0) + (vt1 == vt0) + (vt1 > vt0)
+ 1
+ """
+ if type(self) is not type(other):
+ raise TypeError("can not compare {} with {}".format(type(self).__name__, type(other).__name__))
+
+ return rich_to_bool(op, self._cmp_(other))
def __lt__(self, other):
return self._richcmp_(other, op_LT)
@@ -507,14 +655,14 @@ def copy(self, mutable=None, cls=None):
sage: U = T.copy(mutable=True)
sage: U.flip(0)
sage: T
- Triangulation("(0,2,~1)(1,~0,~2)")
+ Triangulation("(0,2,~1)(~0,~2,1)")
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], "RRB", mutable=True)
sage: S1 = T.copy()
sage: S2 = T.copy()
sage: T == S1 == S2
True
- sage: S1.flip(1,BLUE)
+ sage: S1.flip(1, BLUE)
sage: T == S1
False
sage: T == S2
@@ -543,19 +691,19 @@ def copy(self, mutable=None, cls=None):
return self
else:
T = cls.__new__(cls)
- T._n = self._n
+ T._ne = self._ne
T._fp = self._fp
- T._ep = self._ep
T._vp = self._vp
- T._data = self._data
+ T._half_edges_data = self._half_edges_data
+ T._edges_data = self._edges_data
T._mutable = mutable
else:
T = cls.__new__(cls)
- T._n = self._n
+ T._ne = self._ne
T._fp = self._fp[:]
- T._ep = self._ep[:]
T._vp = self._vp[:]
- T._data = tuple(l[:] for l in self._data)
+ T._half_edges_data = tuple(l[:] for l in self._half_edges_data)
+ T._edges_data = tuple(l[:] for l in self._edges_data)
T._mutable = mutable
T._set_data_pointers()
@@ -575,11 +723,11 @@ def next_at_vertex(self, e, check=True):
sage: T = Triangulation("(~11,4,~3)(~10,~0,11)(~9,0,10)(~8,9,1)(~7,8,~1)(~6,7,2)(~5,6,~2)(~4,5,3)")
sage: T.next_at_vertex(0)
- 9
+ 18
sage: T.next_at_vertex(9)
- 8
+ 7
sage: T.next_at_vertex(5)
- 4
+ 13
"""
if check:
e = self._check_half_edge(e)
@@ -593,26 +741,33 @@ def previous_at_vertex(self, e, check=True):
sage: T = Triangulation("(~11,4,~3)(~10,~0,11)(~9,0,10)(~8,9,1)(~7,8,~1)(~6,7,2)(~5,6,~2)(~4,5,3)")
sage: T.previous_at_vertex(9)
- 0
+ 7
sage: T.previous_at_vertex(8)
- 9
+ 10
sage: T.previous_at_vertex(4)
- 5
+ 11
"""
if check:
e = self._check_half_edge(e)
- return self._fp[self._ep[e]]
+ return self._fp[self._ep(e)]
def edge_permutation(self, copy=True):
- if copy:
- return self._ep[:]
- else:
- return self._ep
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+
+ sage: Triangulation("(0,1,2)(~0,~1,~2)").edge_permutation()
+ array('i', [1, 0, 3, 2, 5, 4])
+ sage: Triangulation("(0,1,2)").edge_permutation()
+ array('i', [0, -1, 2, -1, 4, -1])
+ """
+ return array('i', [self._ep(e) for e in range(2 * self._ne)])
def next_in_edge(self, e, check=True):
if check:
self._check_half_edge(e)
- return self._ep[e]
+ return self._ep(e)
def previous_in_edge(self, e, check=True):
if check:
@@ -620,6 +775,22 @@ def previous_in_edge(self, e, check=True):
return self._vp[self._fp[e]]
def face_permutation(self, copy=True):
+ r"""
+ Return the face permutation.
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+ sage: T = Triangulation("(0,3,5)(1,~3,2)(~2,4,~5)(~4,~1,~0)")
+ sage: T.face_permutation()
+ array('i', [6, 9, 7, 1, 2, 8, 10, 4, 11, 3, 0, 5])
+
+ If the triangulation has folded edges, then the face permutation is only partial::
+
+ sage: T = Triangulation("(0,1,2)(~0,3,4)")
+ sage: T.face_permutation()
+ array('i', [2, 6, 4, -1, 0, -1, 8, -1, 1, -1])
+ """
if copy:
return self._fp[:]
else:
@@ -638,64 +809,103 @@ def previous_in_face(self, e, check=True):
sage: T = Triangulation("(~11,4,~3)(~10,~0,11)(~9,0,10)(~8,9,1)(~7,8,~1)(~6,7,2)(~5,6,~2)(~4,5,3)")
sage: T.previous_in_face(10)
- 0
- sage: T.previous_in_face(1)
9
+ sage: T.previous_in_face(1)
+ 21
sage: T.previous_in_face(3)
- 5
+ 16
"""
if check:
e = self._check_half_edge(e)
- return self._ep[self._vp[e]]
+ return self._ep(self._vp[e])
- def boundary_vector(self, copy=True):
- if copy:
- return self._data[0][:]
- else:
- return self._data[0]
+ def half_edges(self):
+ for e in range(self._ne):
+ yield 2 * e
+ if self._vp[2 * e + 1] != -1:
+ yield 2 * e + 1
def num_half_edges(self):
r"""
Return the number of half edges.
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+ sage: Triangulation("(0,1,2)(~1,3,4)").num_half_edges()
+ 6
"""
- return self._n
+ return sum(self._vp[i] != -1 for i in range(2 * self._ne))
+
+ def has_folded_edge(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+ sage: Triangulation("(0,1,2)(~0,~1,~2)").has_folded_edge()
+ False
+ sage: Triangulation("(0,1,2)").has_folded_edge()
+ True
+ """
+ return any(self._vp[2 * i + 1] == -1 for i in range(self._ne))
def folded_edges(self):
r"""
- Return the list of darts that belong to folded edges.
+ Iterate through half-edges on a folded edge.
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+ sage: list(Triangulation("(0,1,2)(~0,~1,~2)").folded_edges())
+ []
+ sage: list(Triangulation("(0,1,2)").folded_edges())
+ [0, 2, 4]
"""
- n = self._n
- ep = self._ep
- return [i for i in range(n) if ep[i] == i]
+ vp = self._vp
+ for i in range(self._ne):
+ if vp[2 * i + 1] == -1:
+ yield 2 * i
def num_folded_edges(self):
r"""
Return the number of folded edges.
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+ sage: Triangulation("(0,1,2)(~0,~1,~2)").num_folded_edges()
+ 0
+ sage: Triangulation("(0,1,2)").num_folded_edges()
+ 3
"""
- n = self._n
- ep = self._ep
- return sum(ep[i] == i for i in range(n))
+ return sum(self._vp[i] == -1 for i in range(1, 2 * self._ne, 2))
def num_edges(self):
r"""
Return the number of edges.
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation
+ sage: Triangulation("(0,1,2)(~0,~1,~2)").num_edges()
+ 3
+ sage: Triangulation("(0,1,2)").num_edges()
+ 3
"""
- return (self._n + self.num_folded_edges()) // 2
+ return self._ne
def _edge_rep(self, e):
- f = self._ep[e]
- if f < e:
- return '~%d' % f
- else:
- return str(e)
+ import warnings
+ warnings.warn("Constellation._edge_rep is deprecated")
+ return self._half_edge_string(e)
def _norm(self, e):
- f = self._ep[e]
- return f if f < e else e
+ import warnings
+ warnings.warn("Constellation._norm is deprecated")
+ return e ^ 1 if e % 2 else e
def _half_edge_string(self, e):
- f = self._ep[e]
- return '~%d' % f if f < e else '%d' % e
+ return '~%d' % (e // 2) if e % 2 else '%d' % (e // 2)
def edges(self):
r"""
@@ -707,9 +917,10 @@ def edges(self):
sage: T = Triangulation("(0,1,2)(3,4,5)(~0,~3,6)")
sage: T.edges()
- [[0, 8], [1], [2], [3, 7], [4], [5], [6]]
+ [[0, 1], [2], [4], [6, 7], [8], [10], [12]]
"""
- return perm_cycles(self._ep, True, self._n)
+ vp = self._vp
+ return [[2 * i] if vp[2 * i + 1] == -1 else [2 * i, 2 * i + 1] for i in range(self._ne)]
def vertices(self):
r"""
@@ -721,9 +932,9 @@ def vertices(self):
sage: T = Triangulation("(0,1,2)(3,4,5)(~0,~3,6)")
sage: T.vertices()
- [[0, 2, 1, 8, 6, 3, 5, 4, 7]]
+ [[0, 4, 2, 1, 12, 6, 10, 8, 7]]
"""
- return perm_cycles(self._vp, True, self._n)
+ return perm_cycles(self._vp, True, 2 * self._ne)
def num_vertices(self):
r"""
@@ -737,7 +948,7 @@ def num_vertices(self):
sage: T.num_vertices()
1
"""
- return perm_num_cycles(self._vp, self._n)
+ return perm_num_cycles(self._vp, 2 * self._ne)
def faces(self):
r"""
@@ -749,9 +960,9 @@ def faces(self):
sage: T = Triangulation("(0,1,2)(3,4,5)(~0,~3,6)")
sage: T.faces()
- [[0, 1, 2], [3, 4, 5], [6, 8, 7]]
+ [[0, 2, 4], [1, 7, 12], [6, 8, 10]]
"""
- return perm_cycles(self._fp, True, self._n)
+ return perm_cycles(self._fp, True, 2 * self._ne)
def num_faces(self):
r"""
@@ -765,7 +976,9 @@ def num_faces(self):
sage: T.num_faces()
3
"""
- return perm_num_cycles(self._fp, self._n)
+ return perm_num_cycles(self._fp, 2 * self._ne)
+
+ num_internal_faces = num_faces
def is_connected(self):
r"""
@@ -779,63 +992,83 @@ def is_connected(self):
sage: Triangulation("(0,1,2)(3,4,5)").is_connected()
False
"""
- return perms_are_transitive((self._vp, self._ep), self._n)
+ return perms_are_transitive((self._vp, self._fp), 2 * self._ne)
def connected_components(self):
r"""
- Return the connected components as a list of lists of half-edges.
+ Return the connected components as a list of lists of edges.
EXAMPLES::
sage: from veerer import Triangulation
sage: T = Triangulation("(0,1,3)(~0,~1,~3)(2,4,5)(~2,~4,~5)")
sage: T.connected_components()
- [[0, 1, 3, 8, 10, 11], [2, 4, 5, 6, 7, 9]]
+ [[0, 1, 3], [2, 4, 5]]
To construct the triangulation induced on each connected component, one can
use the method :meth:`subgraph`::
sage: c0, c1 = T.connected_components()
sage: T.subgraph(c0)
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
sage: T.subgraph(c1)
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
"""
- return perms_orbits((self._vp, self._ep), self._n)
+ return perm_edge_orbits(self._vp, self._ne)
- def subgraph(self, half_edges, mutable=False, check=True):
+ def subgraph(self, edges, mutable=False, check=True):
r"""
- Return the subgraph of this constellation induced on ``half_edges``.
+ Return the subgraph of this constellation induced on ``edges``.
+
+ The numbering used on the returned subgraph corresponds to
+ the order of ``edges`` given as input.
+
+ EXAMPLES::
+
+ sage: from veerer import StrebelGraph
+ sage: t = StrebelGraph("(0,1,2)(~0,3,4)(~1,~3,5)(~2,~5,6)(~7,~6,7)")
+ sage: t.subgraph([0, 3, 5])
+ StrebelGraph("(0,~1,2,~2)(~0,1)")
+ sage: t.subgraph([5, 0, 3]) # isomorphic graph
+ StrebelGraph("(0,~0,1,~2)(~1,2)")
+
+ Note that the result might not be connected::
- Note that ``half_edges`` must be invariant under the edge permutation.
+ sage: t.subgraph([1, 6])
+ StrebelGraph("(0,~0)(1)(~1)")
"""
if check:
- half_edges = [self._check_half_edge(e) for e in half_edges]
- S = set(half_edges)
- if len(S) != len(half_edges):
- raise ValueError('redundant half_edges')
- if any(self._ep[e] not in S for e in S):
- raise ValueError('half_edges not stable under the edge permutation')
-
- n = len(half_edges)
- relabel = [None] * self._n
- for i, j in enumerate(half_edges):
- relabel[j] = i
- vp = array('i', [-1] * n)
- ep = array('i', [-1] * n)
-
- for e_induced, e_orig in enumerate(half_edges):
- ep[e_induced] = relabel[self._ep[e_orig]]
-
- e = self._vp[e_orig]
- while relabel[e] is None:
- e = self._vp[e]
- vp[e_induced] = relabel[e]
-
- data = []
- for l in self._data:
- data.append(array('i', [l[e] for e in half_edges]))
- return self.__class__.from_permutations(vp, ep, None, data, mutable=mutable, check=True)
+ edges = [self._check_edge(e) for e in edges]
+ S = set(edges)
+ if len(S) != len(edges):
+ raise ValueError('redundant edges')
+
+ ne = len(edges)
+ relabel = [-1] * (2 * self._ne)
+ half_edges = []
+ for i, j in enumerate(edges):
+ relabel[2 * j] = 2 * i
+ relabel[2 * j + 1] = 2 * i + 1
+ half_edges.append(2 * j)
+ half_edges.append(2 * j + 1)
+ vp = array('i', [-1] * (2 * len(edges)))
+
+ for h in half_edges:
+ if self._vp[h] == -1:
+ continue
+ h_image = self._vp[h]
+ while relabel[h_image] == -1:
+ h_image = self._vp[h_image]
+ vp[relabel[h]] = relabel[h_image]
+
+ half_edges_data = []
+ for l in self._half_edges_data:
+ half_edges_data.append(array('i', [l[h] for h in half_edges]))
+ edges_data = []
+ for l in self._edges_data:
+ edges_data.append(array('i', [l[e] for e in edges]))
+
+ return self.__class__.from_permutations(vp, None, half_edges_data, edges_data, mutable=mutable, check=True)
def connected_components_subgraphs(self, mutable=False):
r"""
@@ -846,10 +1079,65 @@ def connected_components_subgraphs(self, mutable=False):
sage: from veerer import Triangulation
sage: T = Triangulation("(0,1,3)(~0,~1,~3)(2,4,5)(~2,~4,~5)")
sage: list(T.connected_components_subgraphs())
- [Triangulation("(0,1,2)(~2,~0,~1)"), Triangulation("(0,1,2)(~2,~0,~1)")]
+ [Triangulation("(0,1,2)(~0,~1,~2)"), Triangulation("(0,1,2)(~0,~1,~2)")]
"""
for comp in self.connected_components():
- yield self.subgraph(comp)
+ yield self.subgraph(comp, mutable=mutable)
+
+ def euler_characteristic(self):
+ r"""
+ Return the Euler characteristic of this constellation.
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation, StrebelGraph
+
+ A sphere::
+
+ sage: T = Triangulation("(0,1,2)")
+ sage: T.euler_characteristic()
+ 2
+
+ Disks::
+
+ sage: T = Triangulation("(0:1,~0:1)")
+ sage: T.euler_characteristic()
+ 1
+ sage: T = Triangulation("(0:1)")
+ sage: T.euler_characteristic()
+ 1
+
+ A torus::
+
+ sage: T = Triangulation("(0,1,2)(~0,~1,~2)")
+ sage: T.euler_characteristic()
+ 0
+
+ A genus 2 surface::
+
+ sage: T = Triangulation("(0,1,2)(~2,3,4)(~4,5,6)(~6,~0,7)(~7,~1,8)(~8,~3,~5)")
+ sage: T.euler_characteristic()
+ -2
+
+ A cylinder::
+
+ sage: T = Triangulation("(0,1,2)(~0,3,4)(~1,~2)(~3,~4)", {"~1": 1, "~2": 1, "~3": 1, "~4": 1})
+ sage: T.euler_characteristic()
+ 0
+
+ A pair of pants::
+
+ sage: T = Triangulation("(0,1,2)(~0)(~1)(~2)", {"~0": 1, "~1": 1, "~2": 1})
+ sage: T.euler_characteristic()
+ -1
+
+ A Strebel graph example::
+
+ sage: sg = StrebelGraph("(0,1)(~0,2,3)(~1,4,5,6)(~2,7,~3,~5)(~4,8,~6)(~7,~8)")
+ sage: sg.euler_characteristic()
+ 0
+ """
+ return self.num_internal_faces() - self.num_edges() + (self.num_vertices() + self.num_folded_edges())
def swap(self, e, check=True):
r"""
@@ -862,13 +1150,13 @@ def swap(self, e, check=True):
sage: T = Triangulation("(0,1,2)(~0,~1,~2)", mutable=True)
sage: T.swap(0)
sage: T
- Triangulation("(0,~1,~2)(1,2,~0)")
+ Triangulation("(0,~1,~2)(~0,1,2)")
sage: T.swap(1)
sage: T
- Triangulation("(0,1,~2)(2,~0,~1)")
+ Triangulation("(0,1,~2)(~0,~1,2)")
sage: T.swap(2)
sage: T
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
sage: T = Triangulation("(0,~5,4)(3,5,6)(1,2,~6)", mutable=True)
sage: T.swap(0)
@@ -888,16 +1176,16 @@ def swap(self, e, check=True):
sage: V.swap(0)
sage: V.swap(10)
sage: V
- VeeringTriangulation("(0,1,~3)(2,~0,~1)(3,4,~5)(5,~9,~7)(6,~2,~4)(7,~6,8)(9,~10,~11)(10,11,~8)", "BRBBBRRBBBBR")
+ VeeringTriangulation("(0,1,~3)(~0,~1,2)(~2,~4,6)(3,4,~5)(5,~9,~7)(~6,8,7)(~8,10,11)(9,~10,~11)", "BRBBBRRBBBBR")
One can alternatively use ``relabel``::
- sage: T = Triangulation("(0,~5,4)(3,5,6)(1,2,~6)", mutable=True)
+ sage: T = Triangulation("(0,~5,4)(1,2,~6)(3,5,6)", mutable=True)
sage: T1 = T.copy()
- sage: T1.swap(3)
+ sage: T1.swap(5)
sage: T1.swap(6)
sage: T2 = T.copy()
- sage: T2.relabel("(3,~3)(6,~6)")
+ sage: T2.relabel("(5,~5)(6,~6)")
sage: T1 == T2
True
"""
@@ -905,43 +1193,47 @@ def swap(self, e, check=True):
raise ValueError('immutable triangulation; use a mutable copy instead')
if check:
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
vp = self._vp
ep = self._ep
fp = self._fp
- E = ep[e]
- if e == E:
+ h = 2 * e
+ H = self._ep(h)
+ if h == H:
return
# images/preimages by vp
- e_vp = vp[e]
- E_vp = vp[E]
- e_vp_inv = fp[E]
- E_vp_inv = fp[e]
- assert vp[e_vp_inv] == e
- assert vp[E_vp_inv] == E
+ h_vp = vp[h]
+ H_vp = vp[H]
+ h_vp_inv = fp[H]
+ H_vp_inv = fp[h]
+ assert vp[h_vp_inv] == h
+ assert vp[H_vp_inv] == H
# images/preimages by fp
- e_fp = fp[e]
- E_fp = fp[E]
- e_fp_inv = ep[e_vp]
- E_fp_inv = ep[E_vp]
- assert fp[e_fp_inv] == e
- assert fp[E_fp_inv] == E
-
- fp[e_fp_inv] = E
- fp[E_fp_inv] = e
- vp[e_vp_inv] = E
- vp[E_vp_inv] = e
- fp[e] = E_fp
- fp[E] = e_fp
- vp[e] = E_vp
- vp[E] = e_vp
-
- for l in self._data:
- l[e], l[E] = l[E], l[e]
+ h_fp = fp[h]
+ H_fp = fp[H]
+ h_fp_inv = ep(h_vp)
+ H_fp_inv = ep(H_vp)
+ assert fp[h_fp_inv] == h
+ assert fp[H_fp_inv] == H
+
+ fp[h_fp_inv] = H
+ fp[H_fp_inv] = h
+ vp[h_vp_inv] = H
+ vp[H_vp_inv] = h
+ fp[h] = H_fp
+ fp[H] = h_fp
+ vp[h] = H_vp
+ vp[H] = h_vp
+
+ for l in self._half_edges_data:
+ l[h], l[H] = l[H], l[h]
+
+ def _extra_relabelling(self, p):
+ pass
def relabel(self, p, check=True):
r"""
@@ -954,10 +1246,10 @@ def relabel(self, p, check=True):
sage: T = Triangulation("(0,1,2)(~0,~1,~2)", mutable=True)
sage: T.relabel("(0,~0)")
sage: T
- Triangulation("(0,~1,~2)(1,2,~0)")
- sage: T.relabel("(0,1,~2)")
+ Triangulation("(0,~1,~2)(~0,1,2)")
+ sage: T.relabel("(0,1,~2)(~0,~1,2)")
sage: T
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
sage: T.set_immutable()
sage: T.relabel("(0,~1)")
@@ -973,34 +1265,35 @@ def relabel(self, p, check=True):
sage: T.flip_back(3)
sage: T.flip_back(0)
sage: T.flip_back(2)
- sage: T.relabel("(0,2)(1,3)")
+ sage: T.relabel("(0,2)(1,3)(~0,~2)(~1,~3)")
sage: T == T0
True
An example with boundary::
sage: t = Triangulation("(0,1,2)(~0,3,4)(~4,~3,~2,~1)", {"~1": 1, "~2": 1, "~3": 1, "~4": 1}, mutable=True)
- sage: t.relabel("(0,3)(1,~2)")
+ sage: t.relabel("(0,3)(1,~2)(~0,~3)(~1,2)")
sage: t
- Triangulation("(0,4,~3)(3,~2,~1)", boundary="(1:1,2:1,~4:1,~0:1)")
+ Triangulation("(0,4,~3)(~1,3,~2)(~0:1,1:1,2:1,~4:1)")
Veering triangulations::
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RBB", mutable=True)
sage: T.relabel([0,1,3,2,5,4])
sage: T
- VeeringTriangulation("(0,1,~2)(2,~0,~1)", "RBB")
+ VeeringTriangulation("(0,~1,~2)(~0,1,2)", "RBB")
sage: T._check()
- Composing relabellings and permutation composition::
+ Composing relabellings and permutation composition (from left to right)::
sage: from veerer.permutation import perm_compose, perm_random_centralizer
sage: fp = "(0,16,~15)(1,19,~18)(2,22,~21)(3,21,~20)(4,20,~19)(5,23,~22)(6,18,~17)(7,17,~16)(8,~1,~23)(9,~2,~8)(10,~3,~9)(11,~4,~10)(12,~5,~11)(13,~6,~12)(14,~7,~13)(15,~0,~14)"
sage: cols = "RRRRRRRRBBBBBBBBBBBBBBBB"
sage: T0 = VeeringTriangulation(fp, cols)
+ sage: ep = T0.edge_permutation()
sage: for _ in range(10):
- ....: p1 = perm_random_centralizer(T0.edge_permutation(copy=False))
- ....: p2 = perm_random_centralizer(T0.edge_permutation(copy=False))
+ ....: p1 = perm_random_centralizer(ep)
+ ....: p2 = perm_random_centralizer(ep)
....: T1 = T0.copy(mutable=True)
....: T1.relabel(p1)
....: T1.relabel(p2)
@@ -1013,42 +1306,135 @@ def relabel(self, p, check=True):
This example used to be wrong::
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [RED, RED, BLUE], mutable=True)
- sage: T.relabel([1,5,0,2,4,3])
- sage: T.edge_colour(0) == BLUE
- True
- sage: T.edge_colour(1) == RED
- True
- sage: T._check()
-
- sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [RED, RED, BLUE], mutable=True)
- sage: from veerer.permutation import perm_random
+ sage: from veerer.permutation import perm_random_centralizer
sage: for _ in range(10):
- ....: r = perm_random(6)
+ ....: r = perm_random_centralizer(T.edge_permutation())
....: T.relabel(r)
....: T._check()
"""
if not self._mutable:
raise ValueError('immutable triangulation; use a mutable copy instead')
- n = self._n
- if check and not perm_check(p, n):
- # if the input is not a valid permutation, we assume that half-edges
- # are not separated
- p = perm_init(p, n, self._ep)
- if not perm_check(p, n):
- raise ValueError('invalid relabeling permutation')
+ if check:
+ p = check_relabelling(p, self._ne)
# TODO: would better be inplace!!
self._vp = perm_conjugate(self._vp, p)
- self._ep = perm_conjugate(self._ep, p)
self._fp = perm_conjugate(self._fp, p)
- for l in self._data:
- perm_on_list(p, l, n)
+ for l in self._half_edges_data:
+ perm_on_array(l, l, p, 2 * self._ne)
+
+ for l in self._edges_data:
+ perm_on_edge_array(l, l, p, 2 * self._ne)
- def _relabelling_from(self, start_edge):
+ self._extra_relabelling(p)
+ self._check()
+
+ # TODO: consider listing all quotients by looking at blocks under the monodromy group
+ def automorphism_quotient(self, mapping=False, mutable=False, check=True):
+ r"""
+ Return the quotient under the automorphism group.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+
+ Veering triangulation example::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)(7,~2,8)(9,~3,~6)(10,~7,~4)(11,~5,12)(13,14,~8)(15,~9,16)(17,18,~10)(19,~17,~11)(20,~13,~12)(21,~14,~18)(22,~21,~15)(23,24,~16)(25,~23,~19)(26,~20,~25)(~26,~24,~22)", "RBBRBRBRRBRBBRBBRRBRRRBBRRB")
+ sage: len(vt.automorphisms())
+ 2
+ sage: qvt = vt.automorphism_quotient()
+ sage: qvt
+ VeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,8,7)(~3,~6,9)(~4,10,~7)(~5,12,11)(~8,13,~12)(~10,14,~11)", "RBBRBRBRRBRBBRR")
+ sage: (vt.stratum(), qvt.stratum()) # optional - surface_dynamics
+ (H_4(2^3), Q_1(1^3, -1^3))
+
+ sage: vt.automorphism_quotient(mapping=True)
+ (VeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,8,7)(~3,~6,9)(~4,10,~7)(~5,12,11)(~8,13,~12)(~10,14,~11)", "RBBRBRBRRBRBBRR"),
+ array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 18, 20, 21, 22, 23, 24, 25, 26, 26, 25, 24, 13, 12, 7, 6, 28, 28, 23, 22, 21, 20, 17, 16, 11, 10, 3, 2, 8, 9, 1, 0, 15, 14, 5, 4]))
+
+ Strebel graph example::
+
+ sage: sg = StrebelGraph("(0,~0,~1)(1,2,~2)")
+ sage: sg.automorphism_quotient()
+ StrebelGraph("(0,~0,1)")
+
+ TESTS::
+
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,7,8)(~4,~7,9)(~6,10,11)(~8,12,13)(~9,14,15)(~10,16,17)(~11,18,~17)(~12,19,~18)(~14,20,~16)(0:1)(~5:1)(~13:1)(~15:1)(~19:1)(~20:1)", "RBBBRRBRBBRRBRBRBBBRR")
+ sage: vt.automorphism_quotient()
+ VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,7,8)(~4,~7,~6)(~8,9,10)(0:1)(~5:1)(~10:1)", "RBBBRRBRBBR")
+ """
+ return self.quotient(perms_orbits(self.automorphisms()), mapping, mutable, check)
+
+ def quotient(self, blocks, mapping=False, mutable=False, check=True):
+ if check:
+ if not all(blocks):
+ raise ValueError("each block must be non empty")
+ blocks = [[self._check_half_edge(h) for h in block] for block in blocks]
+ half_edges = set().union(*blocks)
+ if half_edges != set(self.half_edges()):
+ raise ValueError("invalid blocks")
+ for block in blocks:
+ for l in self._half_edges_data:
+ if len(set(l[h] for h in block)) != 1:
+ raise ValueError("block must be constant on half-edges data")
+ for l in self._edges_data:
+ if len(set(l[h // 2] for h in block)) != 1:
+ raise ValueError("block must be constant on edges data")
+
+ half_edge_to_block = [-1] * (2 * self._ne)
+ for i, block in enumerate(blocks):
+ for j in block:
+ half_edge_to_block[j] = i
+
+ if check:
+ for block in blocks:
+ if len(set(half_edge_to_block[self._vp[h]] for h in block)) != 1:
+ raise ValueError("invalid blocks")
+ if len(set(half_edge_to_block[self._fp[h]] for h in block)) != 1:
+ raise ValueError("invalid blocks")
+
+ ne = 0
+ block_relabelling = {-1: -1}
+ for e in range(self._ne):
+ i = half_edge_to_block[2 * e]
+ if i in block_relabelling:
+ continue
+ block_relabelling[i] = 2 * ne
+ if self._vp[2 * e + 1] != -1:
+ ii = half_edge_to_block[2 * e + 1]
+ if ii in block_relabelling:
+ assert i == ii # folding
+ else:
+ block_relabelling[ii] = 2 * ne + 1
+ ne += 1
+
+ vp = array('i', [-1] * (2 * ne))
+ fp = array('i', [-1] * (2 * ne))
+ half_edges_data = [array('i', [0] * (2 * ne)) for _ in self._half_edges_data]
+ edges_data = [array('i', [0] * ne) for _ in self._edges_data]
+ for i, block in enumerate(blocks):
+ h = block[0]
+ ii = half_edge_to_block[self._vp[h]]
+ vp[block_relabelling[i]] = block_relabelling[ii]
+
+ ii = half_edge_to_block[self._fp[h]]
+ fp[block_relabelling[i]] = block_relabelling[ii]
+
+ for ldest, lsrc in zip(half_edges_data, self._half_edges_data):
+ ldest[block_relabelling[i]] = lsrc[h]
+ for ldest, lsrc in zip(edges_data, self._edges_data):
+ ldest[block_relabelling[i] // 2] = lsrc[h // 2]
+
+ quotient = self.from_permutations(vp, fp, half_edges_data, edges_data, mutable, check)
+ return (quotient, array('i', [block_relabelling[half_edge_to_block[h]] for h in range(2 * self._ne)])) if mapping else quotient
+
+ def _relabelling_from(self, root):
r"""
When connected, return a canonical relabelling map obtained from walking
- along the triangulation starting at ``start_edge``.
+ along the triangulation starting at ``root``.
The returned relabelling array maps the current edge to the new
labelling.
@@ -1060,12 +1446,11 @@ def _relabelling_from(self, start_edge):
The torus example (6 symmetries)::
- sage: fp = array('i', [1, 5, 4, 2, 3, 0])
- sage: ep = array('i', [4, 3, 5, 1, 0, 2])
- sage: vp = array('i', [2, 4, 1, 0, 5, 3])
- sage: T = Triangulation.from_permutations(vp, ep, fp, (array('i', [0]*6),), mutable=True)
+ sage: fp = array('i', [2, 3, 5, 4, 1, 0])
+ sage: vp = array('i', [4, 5, 1, 0, 2, 3])
+ sage: T = Triangulation.from_permutations(vp, fp, (array('i', [0]*6),), mutable=True)
sage: T._relabelling_from(3)
- array('i', [4, 5, 3, 0, 1, 2])
+ array('i', [5, 4, 1, 0, 2, 3])
sage: p = T._relabelling_from(0)
sage: T.relabel(p)
@@ -1077,16 +1462,15 @@ def _relabelling_from(self, start_edge):
The sphere example (3 symmetries)::
- sage: fp = array('i', [1, 2, 0])
- sage: ep = array('i', [0, 1, 2])
- sage: vp = array('i', [2, 0, 1])
- sage: T = Triangulation.from_permutations(vp, ep, fp, (array('i', [0]*3),), mutable=True)
- sage: T._relabelling_from(1)
- array('i', [1, 0, 2])
+ sage: fp = array('i', [2, -1, 4, -1, 0, -1])
+ sage: vp = array('i', [4, -1, 0, -1, 2, -1])
+ sage: T = Triangulation.from_permutations(vp, fp, (array('i', [0]*6),), mutable=True)
+ sage: T._relabelling_from(2)
+ array('i', [4, 5, 0, 1, 2, 3])
sage: p = T._relabelling_from(0)
sage: T.relabel(p)
sage: for i in range(3):
- ....: p = T._relabelling_from(i)
+ ....: p = T._relabelling_from(2 * i)
....: S = T.copy()
....: S.relabel(p)
....: assert S == T
@@ -1096,22 +1480,26 @@ def _relabelling_from(self, start_edge):
sage: T = Triangulation("(0,1,2)(3,4,5)(~0,~3,6)", mutable=True)
sage: p = T._relabelling_from(0)
sage: T.relabel(p)
- sage: for i in range(1, 9):
+ sage: for i in T.half_edges():
+ ....: if i == 0: continue
....: p = T._relabelling_from(i)
....: S = T.copy()
....: S.relabel(p)
....: S._check()
....: assert S != T
"""
- return triangulation_relabelling_from(self._vp, self._ep, start_edge)
-
- def _automorphism_good_starts(self):
- # TODO: should we try to discriminate using vp, ep, fp, data?
- return range(self._n)
+ root = self._check_half_edge(root)
+ relabelling = array('i', [-1] * (2 * self._ne))
+ fp_new = array('i', [-1] * (2 * self._ne))
+ last = edge_relabelling_from(relabelling, fp_new, self._fp, self._ne * 2, root, 0)
+ assert fp_new == perm_conjugate(self._fp, relabelling), (fp_new, perm_conjugate(self._fp, relabelling))
+ if last // 2 != self._ne:
+ raise ValueError("non-connected constellation")
+ return relabelling
def automorphisms(self):
r"""
- Return the list of automorphisms of this triangulation.
+ Return the list of automorphisms of this constellation.
The output is a list of arrays that are permutations acting on the set
of half edges.
@@ -1144,10 +1532,6 @@ def automorphisms(self):
sage: A = V.automorphisms()
sage: len(A)
4
- sage: S = V.copy(mutable=True)
- sage: for a in A:
- ....: S.relabel(a)
- ....: assert S == V
Examples with boundaries::
@@ -1160,15 +1544,43 @@ def automorphisms(self):
sage: t = Triangulation("(0,1,2)", boundary="(~0:1,~1:1,~2:2)")
sage: len(t.automorphisms())
1
+
+ Linear families::
+
+ sage: s = StrebelGraph("(0,3,7,~6,~2,1)(2,5,~4,~3,~1,~0)(4,8,~5)(6,~8,~7)")
+ sage: f = StrebelGraphLinearFamily(s, [(2, 0, 0, 0, 1, 0, 1, 0, 2), (0, 2, 0, 0, 0, 1, 0, 1, 2), (0, 0, 1, 1, 0, 0, 0, 0, 2)])
+ sage: len(s.automorphisms())
+ 2
+ sage: len(f.automorphisms())
+ 2
+
+ A non-connected example::
+
+ sage: t = Triangulation("(0,1,3)(2,4,~4)(~2,5,~5)(6,7,8)")
+ sage: len(t.automorphisms())
+ 36
+
+ TESTS::
+
+ sage: examples = []
+ sage: examples.append(Triangulation("(0,~1,2)(~0,1,~3)(4,~5,3)(~4,6,~2)(7,~6,8)(~7,5,~9)(10,~11,9)(~10,11,~8)"))
+ sage: examples.append(Triangulation("(0,8,~7)(1,9,~0)(2,10,~1)(3,11,~2)(4,12,~3)(5,13,~4)(6,14,~5)(7,15,~6)"))
+ sage: examples.append(Triangulation("(0,1,2)", boundary="(~0:1)(~1:1)(~2:1)"))
+ sage: examples.append(Triangulation("(0,1,3)(2,4,~4)(~2,5,~5)(6,7,8)"))
+
+ sage: examples.append(StrebelGraph("(0,3,7,~6,~2,1)(2,5,~4,~3,~1,~0)(4,8,~5)(6,~8,~7)"))
+ sage: for G in examples:
+ ....: H = G.copy(mutable=True)
+ ....: for a in G.automorphisms():
+ ....: assert H == G
+ ....: H.relabel(a)
+ ....: assert H == G, (G, H, a)
"""
- if self.is_connected():
- best_relabellings = self.best_relabelling(all=True)[0]
- p0 = perm_invert(best_relabellings[0])
- return [perm_compose(p, p0) for p in best_relabellings]
- else:
- raise NotImplementedError
+ best_relabellings = self.best_relabelling(return_all=True)[0]
+ p0 = perm_invert(best_relabellings[0])
+ return [perm_compose(p, p0) for p in best_relabellings]
- def best_relabelling(self, all=False):
+ def best_relabelling(self, return_all=False):
r"""
Return a pair ``(r, data)`` where ``r`` is a relabelling that
brings this constellation to the canonical one.
@@ -1181,55 +1593,190 @@ def best_relabelling(self, all=False):
sage: examples = []
sage: triangles = "(0,~1,2)(~0,1,~3)(4,~5,3)(~4,6,~2)(7,~6,8)(~7,5,~9)(10,~11,9)(~10,11,~8)"
sage: examples.append(Triangulation(triangles, mutable=True))
+ sage: examples.append(Triangulation("(0,1,3)(2,4,~4)(~2,5,~5)(6,7,8)", mutable=True))
sage: fp = "(0,~1,2)(~0,1,~3)(4,~5,3)(~4,6,~2)(7,~6,8)(~7,5,~9)(10,~11,9)(~10,11,~8)"
sage: cols = "BRBBBRRBBBBR"
sage: examples.append(VeeringTriangulation(fp, cols, mutable=True))
+ sage: fp = "(0,16,~15)(1,19,~18)(2,22,~21)(3,21,~20)(4,20,~19)(5,23,~22)(6,18,~17)(7,17,~16)(8,~1,~23)(9,~2,~8)(10,~3,~9)(11,~4,~10)(12,~5,~11)(13,~6,~12)(14,~7,~13)(15,~0,~14)"
+ sage: cols = "RRRRRRRRBBBBBBBBBBBBBBBB"
+ sage: examples.append(VeeringTriangulation(fp, cols, mutable=True))
+ sage: examples.append(StrebelGraph("(0,6,~5,~3,~1,4,~4,2,~2)(1)(3,~0)(5)(~6)", mutable=True))
sage: examples.append(StrebelGraph("(0,6,~5,~3,~1,4,~4:3,2,~2:3)(1:2)(3:2,~0)(5:2)(~6)", mutable=True))
sage: for G in examples:
....: print(G)
- ....: r, (fp, ep, data) = G.best_relabelling()
+ ....: r, fp, half_edges_data, edges_data = G.best_relabelling()
....: for _ in range(10):
- ....: p = perm_random_centralizer(G.edge_permutation(copy=False))
+ ....: p = perm_random_centralizer(G.edge_permutation())
....: G.relabel(p)
- ....: r2, (fp2, ep2, data2) = G.best_relabelling()
- ....: assert fp2 == fp, (G, fp, fp2)
- ....: assert ep2 == ep, (G, ep, ep2)
- ....: assert data2 == data, (G, data, data2)
- Triangulation("(0,~1,2)(1,~3,~0)(3,4,~5)(5,~9,~7)(6,~2,~4)(7,~6,8)(9,10,~11)(11,~8,~10)")
- VeeringTriangulation("(0,~1,2)(1,~3,~0)(3,4,~5)(5,~9,~7)(6,~2,~4)(7,~6,8)(9,10,~11)(11,~8,~10)", "BRBBBRRBBBBR")
- StrebelGraph("(0,6,~5,~3,~1,4,~4:3,2,~2:3)(1:2)(3:2,~0)(5:2)(~6)")
+ ....: r2, fp2, half_edges_data2, edges_data2 = G.best_relabelling()
+ ....: assert fp2 == fp, G
+ ....: assert half_edges_data2 == half_edges_data, (G, half_edges_data2, half_edges_data)
+ ....: assert edges_data2 == edges_data, (G, edges_data2, edges_data)
+ Triangulation("(0,~1,2)(~0,1,~3)(~2,~4,6)(3,4,~5)(5,~9,~7)(~6,8,7)(~8,~10,11)(9,10,~11)")
+ Triangulation("(0,1,3)(2,4,~4)(~2,5,~5)(6,7,8)")
+ VeeringTriangulation("(0,~1,2)(~0,1,~3)(~2,~4,6)(3,4,~5)(5,~9,~7)(~6,8,7)(~8,~10,11)(9,10,~11)", "BRBBBRRBBBBR")
+ VeeringTriangulation("(0,16,~15)(~0,~14,15)(1,19,~18)(~1,~23,8)(2,22,~21)(~2,~8,9)(3,21,~20)(~3,~9,10)(4,20,~19)(~4,~10,11)(5,23,~22)(~5,~11,12)(6,18,~17)(~6,~12,13)(7,17,~16)(~7,~13,14)", "RRRRRRRRBBBBBBBBBBBBBBBB")
+ StrebelGraph("(0,6,~5,~3,~1,4,~4,2,~2)(~0,3)(1)(5)(~6)")
+ StrebelGraph("(0,6,~5,~3,~1,4,~4:3,2,~2:3)(~0,3:2)(1:2)(5:2)(~6)")
"""
- n = self._n
- fp = self._fp
- ep = self._ep
+ ne = self._ne
+ n = 2 * ne
+
+ if not self.is_connected():
+ # each component is labelled with consecutive half-edge labels
+ # we use canonical labels for each of them, and then use a total ordering on the components
+ components = {}
+ for cc in self.connected_components():
+ # TODO: set check to False
+ comp = self.subgraph(cc, check=True)
+ relabelling_best, fp_best, half_edges_data_best, edges_data_best = comp.best_relabelling(return_all=return_all)
+
+ comp_hashable = [fp_best.tobytes()]
+ comp_hashable.extend(data.tobytes() for data in half_edges_data_best)
+ comp_hashable.extend(data.tobytes() for data in edges_data_best)
+ comp_hashable = tuple(comp_hashable)
+ if comp_hashable not in components:
+ components[comp_hashable] = []
+ if return_all:
+ data = (cc, relabelling_best[0], relabelling_best, fp_best, half_edges_data_best, edges_data_best)
+ else:
+ data = (cc, relabelling_best, None, fp_best, half_edges_data_best, edges_data_best)
+
+ components[comp_hashable].append(data)
+
+ relabelling_best = array('i', [-1] * n)
+ fp_best = array('i', [-1] * n)
+ half_edges_data_best = array('i', [0] * n)
+ edges_data_best = array('i', [0] * n)
+
+ shift = 0
+ for comp_hashable in sorted(components):
+ value = components[comp_hashable]
+ for comp, comp_relabelling_best, _, _, _, _ in components[comp_hashable]:
+ # NOTE: elements in comp are edges, not half-edges
+ for i, j in enumerate(comp):
+ i0 = comp_relabelling_best[2 * i]
+ i1 = comp_relabelling_best[2 * i + 1]
+ relabelling_best[2 * j] = shift + i0
+ relabelling_best[2 * j + 1] = shift + i1
+ shift += 2 * len(comp)
+
+ fp_best = perm_conjugate(self._fp, relabelling_best)
+ half_edges_data_best = tuple(l[:] for l in self._half_edges_data)
+ for ldest, lsrc in zip(half_edges_data_best, self._half_edges_data):
+ perm_on_array(ldest, lsrc, relabelling_best, n)
+ edges_data_best = tuple(l[:] for l in self._edges_data)
+ for ldest, lsrc in zip(edges_data_best, self._edges_data):
+ perm_on_edge_array(ldest, lsrc, relabelling_best, n)
+
+ if not return_all:
+ return (relabelling_best, fp_best, half_edges_data_best, edges_data_best)
- best = None
- if all:
relabellings = []
-
- for start_edge in range(self._n):
- relabelling = self._relabelling_from(start_edge)
-
- fp_new = perm_conjugate(fp, relabelling)
- ep_new = perm_conjugate(ep, relabelling)
- data_new = [l[:] for l in self._data]
- for l in data_new:
- perm_on_list(relabelling, l, self._n)
-
- T = (fp_new, ep_new, data_new)
- if best is None or T < best:
- best_relabelling = relabelling
- best = T
- if all:
- del relabellings[:]
+ for oc in itertools.product(*[itertools.permutations(components[comp_hashable]) for comp_hashable in sorted(components)]):
+ # run through all permutations of isomorphic components
+ comps = [data[0] for isom_comps in oc for data in isom_comps]
+ for comp_relabellings in itertools.product(*[data[2] for isom_comps in oc for data in isom_comps]):
+ # run through products available relabellings
+ relabelling = array('i', [-1] * n)
+ shift = 0
+ for comp, comp_relabelling in zip(comps, comp_relabellings):
+ # NOTE: elements in comp are edges, not half-edges
+ for i, j in enumerate(comp):
+ i0 = comp_relabelling[2 * i]
+ i1 = comp_relabelling[2 * i + 1]
+ relabelling[2 * j] = shift + i0
+ relabelling[2 * j + 1] = shift + i1
+ shift += 2 * len(comp)
relabellings.append(relabelling)
- elif all and T == best:
- relabellings.append(relabelling)
- return (relabellings, best) if all else (best_relabelling, best)
+ return (relabellings, fp_best, half_edges_data_best, edges_data_best)
+
+ else:
+ # connected case
+ fp = self._fp
+ half_edges_data = self._half_edges_data
+ edges_data = self._edges_data
+ relabellings = []
- def set_canonical_labels(self):
+ relabelling_new = array('i', [-1] * n)
+ relabelling_best = array('i', [-1] * n)
+ fp_new = array('i', [-1] * n)
+ fp_best = array('i', [-1] * n)
+ half_edges_data_new = tuple(l[:] for l in half_edges_data)
+ half_edges_data_best = tuple(l[:] for l in half_edges_data)
+ edges_data_new = tuple(l[:] for l in edges_data)
+ edges_data_best = tuple(l[:] for l in edges_data)
+ k_half_edges = len(half_edges_data)
+ k_edges = len(edges_data)
+
+ half_edges = self.half_edges()
+ edge_relabelling_from(relabelling_best, fp_best, self._fp, 2 * ne, next(half_edges), 0)
+ for i in range(k_half_edges):
+ perm_on_array(half_edges_data_best[i], half_edges_data[i], relabelling_best, 2 * ne)
+ for i in range(k_edges):
+ perm_on_edge_array(edges_data_best[i], edges_data[i], relabelling_best, 2 * ne)
+
+ if return_all:
+ relabellings.append(relabelling_best[:])
+
+ for start_half_edge in half_edges:
+ # reinitialize relabelling_new as intended by edge_relabelling_from
+ for i in range(n):
+ relabelling_new[i] = fp_new[i] = -1
+ end_image = edge_relabelling_from(relabelling_new, fp_new, self._fp, n, start_half_edge, 0)
+ assert end_image == 2 * ne, (end_image, ne)
+ assert sum(x == -1 for x in fp_new) == sum(x == -1 for x in self._fp)
+ assert all(x != -1 for x in relabelling_new)
+
+ c = 0
+ if fp_new < fp_best:
+ # no need to compare anything else
+ c = -1
+ elif fp_new > fp_best:
+ # no need to go further
+ c = 1
+ continue
+
+ for i in range(k_half_edges):
+ perm_on_array(half_edges_data_new[i], half_edges_data[i], relabelling_new, 2 * ne)
+ if not c:
+ if half_edges_data_new[i] < half_edges_data_best[i]:
+ c = -1
+ elif half_edges_data_new[i] > half_edges_data_best[i]:
+ c = 1
+ break
+ if c == 1:
+ continue
+
+ for i in range(k_edges):
+ perm_on_edge_array(edges_data_new[i], edges_data[i], relabelling_new, 2 * ne)
+ if not c:
+ if edges_data_new[i] < edges_data_best[i]:
+ c = -1
+ elif edges_data_new[i] > edges_data_best[i]:
+ c = 1
+ break
+ if c == 1:
+ continue
+
+ # at this stage either c=0 and relabelling is identical or c=-1 and we found something better
+ if c == -1:
+ fp_best, fp_new = fp_new, fp_best
+ relabelling_best, relabelling_new = relabelling_new, relabelling_best
+ half_edges_data_best, half_edges_data_new = half_edges_data_new, half_edges_data_best
+ edges_data_best, edges_data_new = edges_data_new, edges_data_best
+ if return_all:
+ relabellings.clear()
+ relabellings.append(relabelling_best[:])
+ elif return_all:
+ assert c == 0
+ relabellings.append(relabelling_new[:])
+
+ return (relabellings, fp_best, half_edges_data_best, edges_data_best) if return_all else (relabelling_best, fp_best, half_edges_data_best, edges_data_best)
+
+ def set_canonical_labels(self, mapping=False):
r"""
Set labels in a canonical way in its automorphism class.
@@ -1242,16 +1789,24 @@ def set_canonical_labels(self):
....: (-8, 8, -2), (-7, 7, 2), (-6, 6, -3), (-5, 5, 3)]
sage: T = Triangulation(t, mutable=True)
sage: T
- Triangulation("(0,10,~9)(1,~8,9)(2,~6,7)(3,~4,5)(4,~3,~11)(6,~2,~5)(8,~1,~7)(11,~10,~0)")
+ Triangulation("(0,10,~9)(~0,11,~10)(1,~8,9)(~1,~7,8)(2,~6,7)(~2,~5,6)(3,~4,5)(~3,~11,4)")
+ sage: T._check()
sage: T.set_canonical_labels()
sage: T
- Triangulation("(0,1,8)(2,11,~3)(3,~11,~4)(4,10,~5)(5,~10,~6)(6,9,~7)(7,~9,~8)(~2,~1,~0)")
+ Triangulation("(0,1,2)(~0,~2,3)(~1,4,5)(~3,6,7)(~4,8,~5)(~6,9,~7)(~8,10,11)(~9,~11,~10)")
+ sage: T._check()
"""
if not self._mutable:
raise ValueError('immutable triangulation; use a mutable copy instead')
- r, _ = self.best_relabelling()
- self.relabel(r, check=False)
+ r, fp_best, half_edges_data_best, edges_data_best = self.best_relabelling()
+ self._fp = fp_best
+ self._vp = perm_conjugate(self._vp, r)
+ self._half_edges_data = half_edges_data_best
+ self._edges_data = edges_data_best
+ self._set_data_pointers()
+ if mapping:
+ return r
def iso_sig(self):
r"""
@@ -1262,33 +1817,33 @@ def iso_sig(self):
sage: from veerer import *
sage: T = Triangulation("(0,3,1)(~0,4,2)(~1,~2,~4)")
sage: T.iso_sig()
- '9_583764021_876543210_134052786_000000000'
+ '5_1__1_2~46098537_0000000000'
sage: TT = Triangulation.from_string(T.iso_sig())
sage: TT
- Triangulation("(0,1,3)(2,4,~3)(~2,~1,~0)")
+ Triangulation("(0,1,2)(~1,3,4)(~2,~4,~3)")
sage: TT.iso_sig() == T.iso_sig()
True
sage: T = Triangulation("(0,10,~6)(1,12,~2)(2,14,~3)(3,16,~4)(4,~13,~5)(5,~1,~0)(6,~17,~7)(7,~14,~8)(8,13,~9)(9,~11,~10)(11,~15,~12)(15,17,~16)")
sage: T.iso_sig()
- 'A_f23456y89azcxesvromwphuiqbjl0dkgnt71_zyxwvutsrqponmlkjihgfedcba9876543210_a6cjfmxegokhwiruqnlv0dtbp987y54321zs_000000000000000000000000000000000000'
+ 'i_1__1_264a0e8i1mcj3sgr5tkq7xov9dbupwfzhyln_000000000000000000000000000000000000'
sage: Triangulation.from_string(T.iso_sig())
- Triangulation("(0,10,~15)(1,6,~2)(2,12,~3)(3,~16,~4)(4,15,~5)(5,~13,~6)(7,14,~8)(8,16,~9)(9,~11,~10)(11,17,~12)(13,~17,~14)(~7,~1,~0)")
+ Triangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,7,8)(~3,9,10)(~4,11,12)(~5,~9,13)(~6,14,~12)(~7,~13,15)(~8,~14,16)(~10,~16,17)(~11,~15,~17)")
sage: t = [(-12, 4, -4), (-11, -1, 11), (-10, 0, 10), (-9, 9, 1),
....: (-8, 8, -2), (-7, 7, 2), (-6, 6, -3), (-5, 5, 3)]
sage: cols = [RED, RED, RED, RED, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE]
sage: T = VeeringTriangulation(t, cols, mutable=True)
sage: T.iso_sig()
- 'o_fn345678mhjlkig9eadbc021_nmlkjihgfedcba9876543210_18bcad9e0gikjhf765432mnl_000000000000000000000000_122121212222222212121221'
+ 'c_1_1_1_2548061cag39ei7dbkfnmjhl_000000000000000000000000_122212122212'
If we relabel the triangulation, the isomorphic signature does not change::
- sage: from veerer.permutation import perm_random
- sage: p = perm_random(24)
+ sage: from veerer.permutation import perm_random_centralizer
+ sage: p = perm_random_centralizer(T.edge_permutation())
sage: T.relabel(p)
sage: T.iso_sig()
- 'o_fn345678mhjlkig9eadbc021_nmlkjihgfedcba9876543210_18bcad9e0gikjhf765432mnl_000000000000000000000000_122121212222222212121221'
+ 'c_1_1_1_2548061cag39ei7dbkfnmjhl_000000000000000000000000_122212122212'
An isomorphic triangulation can be reconstructed from the isomorphic
signature via::
@@ -1311,12 +1866,14 @@ def iso_sig(self):
sage: T = VeeringTriangulation(t, cols, mutable=True)
sage: iso_sig = T.iso_sig()
sage: for _ in range(10):
- ....: p = perm_random(24)
+ ....: p = perm_random_centralizer(T.edge_permutation())
....: T.relabel(p)
....: assert T.iso_sig() == iso_sig
sage: VeeringTriangulation("(0,1,2)(3,4,~1)(5,6,~4)", "RBGGRBG").iso_sig()
- '9_523706841_872345610_631270584_000000000_218281812'
+ '7_1_1_1_2~4~068~5ac~9~_00000000000000_8128128'
+ sage: VeeringTriangulation.from_string('7_1_1_1_2~4~068~5ac~9~_00000000000000_8128128')
+ VeeringTriangulation("(0,1,2)(~2,3,4)(~4,5,6)", "GRBGRBG")
"""
T = self.copy(mutable=True)
T.set_canonical_labels()
@@ -1326,10 +1883,12 @@ def _non_isom_easy(self, other):
r"""
A quick certificate of non-isomorphism that does not require relabellings.
"""
- return (perm_cycle_type(self._vp) != perm_cycle_type(other._vp) or
- perm_cycle_type(self._ep) != perm_cycle_type(other._ep) or
+ return (self._ne != other._ne or
+ perm_cycle_type(self._vp) != perm_cycle_type(other._vp) or
+ self.num_folded_edges() != other.num_folded_edges() or
perm_cycle_type(self._fp) != perm_cycle_type(other._fp) or
- any(sorted(l_self) != sorted(l_other) for l_self, l_other in zip(self._data, other._data)))
+ any(sorted(l_self) != sorted(l_other) for l_self, l_other in zip(self._half_edges_data, other._half_edges_data)) or
+ any(sorted(l_self) != sorted(l_other) for l_self, l_other in zip(self._edges_data, other._edges_data)))
def is_isomorphic(self, other, certificate=False):
r"""
@@ -1343,7 +1902,7 @@ def is_isomorphic(self, other, certificate=False):
sage: T = Triangulation("(0,5,1)(~0,4,2)(~1,~2,~4)(3,6,~5)", mutable=True)
sage: TT = T.copy()
sage: for _ in range(10):
- ....: rel = perm_random_centralizer(TT.edge_permutation(False))
+ ....: rel = perm_random_centralizer(TT.edge_permutation())
....: TT.relabel(rel)
....: assert T.is_isomorphic(TT)
@@ -1351,7 +1910,7 @@ def is_isomorphic(self, other, certificate=False):
sage: cols = "BRBBBRRBBBBR"
sage: V = VeeringTriangulation(fp, cols, mutable=True)
sage: W = V.copy()
- sage: p = perm_random_centralizer(V.edge_permutation(copy=False))
+ sage: p = perm_random_centralizer(V.edge_permutation())
sage: W.relabel(p)
sage: assert V.is_isomorphic(W) is True
sage: ans, cert = V.is_isomorphic(W, True)
@@ -1364,12 +1923,17 @@ def is_isomorphic(self, other, certificate=False):
if self._non_isom_easy(other):
return (False, None) if certificate else False
- r1, data1 = self.best_relabelling()
- r2, data2 = other.best_relabelling()
+ r1, fp1, half_edges_data1, edges_data1 = self.best_relabelling()
+ r2, fp2, half_edges_data2, edges_data2 = other.best_relabelling()
- if data1 != data2:
+ if fp1 != fp2 or half_edges_data1 != half_edges_data2 or edges_data1 != edges_data2:
return (False, None) if certificate else False
elif certificate:
return (True, perm_compose(r1, perm_invert(r2)))
else:
return True
+
+ # TODO: deprecate
+ is_isomorphic_to = is_isomorphic
+
+
diff --git a/veerer/cover.py b/veerer/cover.py
deleted file mode 100644
index b3bd527a..00000000
--- a/veerer/cover.py
+++ /dev/null
@@ -1,375 +0,0 @@
-r"""
-Covering of triangulations
-"""
-# ****************************************************************************
-# This file is part of veerer
-#
-# Copyright (C) 2018 Mark Bell
-# 2018-2023 Vincent Delecroix
-# 2018 Saul Schleimer
-#
-# This program 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.
-#
-# This program 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 this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# ****************************************************************************
-
-from array import array
-
-from .permutation import *
-from .triangulation import Triangulation
-
-
-class TriangulationCover(object):
- r"""
- A triangulation cover
-
- We consider ramified cover with arbitrary ramifications at the vertices and
- at most double ramification at the middle of folded edges. A covering of
- degree d of a triangulation with n half edges is determined by a tuple of n
- permutations in S_d.
-
- As this is also a triangulation, we choose a canonical labeling of its
- edges as follows: i + n*j for the i-th edge in the j-th copy of the
- original triangulation. This is used in homology computations.
-
- This object has attributes:
-
- * _t: underlying triangulation of type ``Triangulation``
- * _d: degree of the cover of type ``int``
- * _c: covering cocycle, a list of permutations, one per half-edge
-
- EXAMPLES:
-
- The torus as a double of a triangle::
-
- sage: from veerer import Triangulation # random output due to deprecation warnings from realalg
- sage: T = Triangulation("(0,1,2)")
- sage: C = T.cover([[1,0], [1,0], [1,0]])
- sage: C.euler_characteristic()
- 0
- sage: C.as_triangulation().euler_characteristic()
- 0
-
- The quadratic L-shape surface::
-
- sage: from veerer import Triangulation
- sage: T = Triangulation("(0,1,2)")
- sage: C = T.cover([[1,0,2], [2,1,0], [0,1,2]])
- sage: C.euler_characteristic()
- 2
- sage: C.as_triangulation().euler_characteristic()
- 2
- """
- __slots__ = ['_t', '_d', '_c']
-
- def __init__(self, triangulation, cover, mutable=False, check=True):
- if not isinstance(triangulation, Triangulation):
- self._t = Triangulation(triangulation, mutable=mutable, check=False)
- else:
- self._t = triangulation.copy(mutable=mutable)
-
- if not isinstance(cover, (tuple, list)) or \
- (len(cover) != self._t.num_half_edges() and len(cover) != self._t.num_edges()):
- raise ValueError("the argument 'cover' must be a list with as many elements as the number of edges (or half edges) of the triangulation")
-
- d = None
- cover = list(cover)
- for i,p in enumerate(cover):
- if p is not None:
- cover[i] = p = perm_init(p)
- if d is None:
- d = len(p)
- elif len(p) != d:
- raise ValueError("inconsistent cover degree")
- if d is None:
- raise ValueError("can not determine cover degree")
- for i,p in enumerate(cover):
- if p is None:
- cover[i] = perm_init(range(d))
-
- ep = self._t.edge_permutation(copy=False)
- n = self._t.num_half_edges()
- ne = self._t.num_edges()
- nf = self._t.num_folded_edges()
- if len(cover) == ne:
- for i in range(ne, n):
- if ep[i] >= ne:
- raise ValueError("non standard edge labeling, you need to provide the full list of permutations for the cover")
- cover.append(perm_invert(cover[ep[i]]))
-
- self._d = d
- self._c = cover
-
- if check:
- self._check(ValueError)
-
- def _check(self, error=RuntimeError):
- n = self._t.num_half_edges()
- d = self._d
-
- if len(self._c) != n:
- raise error("wrong length for _c attribute")
-
- for p in self._c:
- if not perm_check(p, d):
- raise error("invalid %d-th permutation %s in covering data" % (d, p))
-
- ep = self._t.edge_permutation(copy=False)
- for i,p in enumerate(self._c):
- q = self._c[ep[i]]
- if perm_invert(p) != q:
- raise error("inconsistent covering data on edge (%d,%d)" %(i, ep[i]))
-
- # NOTE: should we check connectedness?
-
- def __eq__(self, other):
- if type(self) != type(other):
- raise TypeError
- return self._t == other._t and self._c == other._c
-
- def __ne__(self, other):
- if type(self) != type(other):
- raise TypeError
- return self._t != other._t or self._c != other._c
-
- def copy(self, mutable=None):
- T = TriangulationCover.__new__(TriangulationCover)
- T._t = self._t.copy(mutable=mutable)
- T._d = self._d
- T._c = tuple(p[:] for p in self._c)
- return T
-
- def __str__(self):
- t = self._t
- ep = t.edge_permutation(copy=False)
- n = t.num_half_edges()
- faces = t.faces()
- fstr = ''.join('(' + ','.join(t._edge_rep(e) for e in f) + ')' for f in faces)
- cstr = ',\n '.join('[' + ','.join(str(i) for i in self._c[e]) + ']' for e in range(n) if ep[e] >= e)
- return 'TriangulationCover("%s",\n [%s])' % (fstr, cstr)
-
- def __repr__(self):
- return str(self)
-
- def base(self, copy=True):
- if copy:
- return self._t.copy()
- else:
- return self._t
-
- ############
- # topology #
- ############
-
- def degree(self):
- return self._d
-
- def vertex_permutation(self):
- n = self._t.num_half_edges()
- d = self._d
- vp = self._t.vertex_permutation(copy=False)
- ep = self._t.edge_permutation(copy=False)
- cvp = array('i', [-1] * (d * n))
-
- for a in range(n):
- b = vp[a]
- p = self._c[ep[b]]
- for i in range(d):
- ai = a + n * i
- bj = b + n * p[i]
- cvp[ai] = bj
-
- return cvp
-
- def vertices(self):
- return perm_cycles(self.vertex_permutation())
-
- def edge_permutation(self):
- n = self._t.num_half_edges()
- d = self._d
- ep = self._t.edge_permutation(copy=False)
- c = self._c
- cep = array('i', [-1] * (d * n))
-
- for a in range(n):
- b = ep[a]
- p = c[a]
- for j in range(d):
- cep[a + n * j] = b + n * p[j]
-
- return cep
-
- def edges(self):
- return perm_cycles(self.edge_permutation())
-
- def face_permutation(self):
- n = self._t.num_half_edges()
- d = self._d
- fp = self._t.face_permutation(copy=False)
- cfp = array('i', [-1] * (d * n))
-
- for a in range(n):
- b = fp[a]
- for i in range(d):
- cfp[a + n * i] = b + n * i
-
- return cfp
-
- def faces(self):
- return perm_cycles(self.face_permutation())
-
- def as_triangulation(self, mutable=None):
- r"""
- Return this cover as a triangulation.
-
- EXAMPLES::
-
- sage: from veerer import *
-
- sage: T = Triangulation("(0,1,~1)(~0,2,3)(~2,4,~4)")
- sage: C = T.cover([[0,1,3,2],[1,0,3,2],[1,2,3,0],[3,2,1,0],[0,1,3,2]])
- sage: C.as_triangulation()
- Triangulation("(0,1,7)(2,3,~0)(4,~4,6)...(~6,~3,~18)(~23,~22,~20)")
- """
- if mutable is None:
- mutable = self._t._mutable
- return Triangulation.from_permutations(
- self.vertex_permutation(),
- self.edge_permutation(),
- self.face_permutation(),
- (array('i', [0] * (self._d * self._t._n)),),
- mutable=mutable)
-
- def num_folded_edges(self):
- n = self._t.num_half_edges()
- d = self._d
- ep = self._t.edge_permutation(copy=False)
- c = self._c
- nfe = 0
- for i in range(n):
- if ep[i] != i:
- continue
- for j in range(d):
- nfe += c[i][j] == j
- return nfe
-
- def num_edges(self):
- return ((self._d * self._t.num_half_edges()) + self.num_folded_edges()) // 2
-
- def num_faces(self):
- return self._t.num_faces() * self._d
-
- def num_vertices(self):
- return len(self.vertices())
-
- def euler_characteristic(self):
- return self.num_faces() - self.num_edges() + (self.num_vertices() + self.num_folded_edges())
-
- def genus(self):
- # 2 - 2g = \chi so
- return (2 - self.euler_characteristic()) // 2
-
- ########
- # flip #
- ########
-
- def flip(self, e):
- r"""
- Flip the edge ``e`` in the underlying triangulation.
-
- EXAMPLES::
-
- sage: from veerer import Triangulation
-
- sage: T = Triangulation("(0,1,2)")
- sage: C = T.cover([[1,0], [1,0], [1,0]], mutable=True)
- sage: C.flip(0)
- sage: C
- TriangulationCover("(0,2,1)",
- [[1,0],
- [1,0],
- [1,0]])
-
- sage: T = Triangulation("(0,1,2)")
- sage: C = T.cover([[1,0,2], [2,1,0], [0,1,2]], mutable=True)
- sage: C.flip(0); C._check()
- sage: C.flip(1); C._check()
- sage: C.flip(2); C._check()
-
- TESTS::
-
- sage: T = Triangulation("(0,1,2)(~0,~1,~2)")
- sage: n = T.num_half_edges()
- sage: d = 5
- sage: for e in range(n):
- ....: if T.is_flippable(e):
- ....: C = T.cover([[2,4,1,3,0],[4,1,3,2,0],[3,0,1,2,4]], mutable=True)
- ....: TT = C.as_triangulation()
- ....: C.flip(e)
- ....: C._check()
- ....: for i in range(d):
- ....: assert TT.is_flippable(e + n * i)
- ....: TT.flip(e + n * i)
- ....: assert C.as_triangulation().is_isomorphic_to(TT)
- """
- # v<----------u v<----------u
- # | a ^^ |^ a ^
- # | / | | \ |
- # | F / | | \ G |
- # | / | | \ |
- # |b e/ d| --> |b \e d|
- # | / | | \ |
- # | / | | \ |
- # | / | | \ |
- # | / G | | F \ |
- # | / | | \ |
- # v/ c | v c \|
- # w---------->x w---------->x
- if not self._t._mutable:
- raise ValueError("immutable veering triangulation cover; use a mutable copy instead")
-
- e = int(e)
- ep = self._t.edge_permutation(copy=False)
- fp = self._t.face_permutation(copy=False)
- E = ep[e]
- a = fp[e]
- A = ep[a]
- c = fp[E]
- C = ep[c]
-
- self._t.flip(e)
-
- # TODO: do the compositions inplace
- self._c[a] = perm_compose(self._c[E], self._c[a])
- self._c[A] = perm_compose(self._c[A], self._c[e])
- self._c[c] = perm_compose(self._c[e], self._c[c])
- self._c[C] = perm_compose(self._c[C], self._c[E])
-
- ################################
- # relabeling and automorphisms #
- ################################
-
- # In a triangle (a, b, c) we are allowed to multiply the three
- # permutations (theta_a, theta_b, theta_c) that define the cover
- # by a common permutation g, (g theta_a, g theta_b, g theta_c)
-
- # To make a canonical numbering, we choose a spanning tree of the dual and
- # set the permutations on this spanning tree to be identity
-
-
- ###################
- # homology action #
- ###################
-
- def _check_homology_matrix(self, m):
- pass
diff --git a/veerer/delaunay_cone.py b/veerer/delaunay_cone.py
new file mode 100644
index 00000000..a776e25c
--- /dev/null
+++ b/veerer/delaunay_cone.py
@@ -0,0 +1,237 @@
+r"""
+Delaunay cone of veering triangulations and veering triangulation families.
+"""
+# ****************************************************************************
+# This file is part of veerer
+#
+# Copyright (C) 2024 Vincent Delecroix
+#
+# veerer is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License version 3 as published by the Free
+# Software Foundation.
+#
+# veerer 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 veerer. If not, see .
+# ****************************************************************************
+
+import collections
+import itertools
+
+from sage.misc.cachefunc import cached_method
+from sage.rings.integer_ring import ZZ
+from sage.rings.rational_field import QQ
+from sage.matrix.constructor import matrix
+from sage.modules.free_module import FreeModule
+from sage.geometry.polyhedron.combinatorial_polyhedron.base import CombinatorialPolyhedron
+
+from .constants import BLUE, RED, HORIZONTAL, VERTICAL
+from .polyhedron.linear_algebra import vector_normalize
+from .polyhedron.linear_expression import LinearExpressions, ConstraintSystem
+
+
+class DelaunayCone:
+ r"""
+ The Delaunay cone of a veering triangulation.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRBBBR")
+ sage: vt.delaunay_cone()
+ 8-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRBBBR") made of
+ 2 forward-flip facets
+ 2 backward-flip facets
+ 5 x-degeneration facets
+ 4 y-degeneration facets
+
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~0,~7,~5)(~3,~4,~2)(~6,~1,~8)", "RRBRRBRRB")
+ sage: vt.delaunay_cone()
+ 8-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~7,~5)(~1,~8,~6)(~2,~3,~4)(3,4,5)(6,7,8)", "RRBRRBRRB") made of
+ 2 forward-flip facets
+ 3 backward-flip facets
+ 5 x-degeneration facets
+ 4 y-degeneration facets
+
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~0,~1,~5)(~3,~7,~8)(~6,~4,~2)", "RRBRRBRBB")
+ sage: vt.delaunay_cone()
+ 8-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,~5)(~2,~6,~4)(3,4,5)(~3,~7,~8)(6,7,8)", "RRBRRBRBB") made of
+ 3 forward-flip facets
+ 2 backward-flip facets
+ 4 x-degeneration facets
+ 4 y-degeneration facets
+ """
+ def __init__(self, vt, cone):
+ self._vt = vt
+ self._cone = cone
+ self._V = FreeModule(vt.base_ring(), 2 * self._vt._ne)
+
+ @cached_method
+ def facets_kind_and_data(self):
+ kinds = [None] * len(self.facets())
+ data = [[] for _ in range(len(self.facets()))]
+
+ for face, edges in self.x_vanishing_facets():
+ for i in face.ambient_H_indices():
+ kinds[i] = 'x'
+ data[i] = edges
+ for face, edges in self.y_vanishing_facets():
+ for i in face.ambient_H_indices():
+ kinds[i] = 'y'
+ data[i] = edges
+ for face, edges in self.forward_delaunay_facets():
+ for i in face.ambient_H_indices():
+ kinds[i] = 'f'
+ data[i] = edges
+ for face, edges in self.backward_delaunay_facets():
+ for i in face.ambient_H_indices():
+ kinds[i] = 'b'
+ data[i] = edges
+
+ assert not any(v is None for v in kinds)
+ return tuple(kinds), tuple(data)
+
+ @cached_method
+ def rays(self):
+ ans = list(map(self._V, self._cone.rays()))
+ for r in ans:
+ r.set_immutable()
+ return ans
+
+ def eqns(self):
+ return self._cone.eqns()
+
+ def space_dimension(self):
+ return 2 * self._vt._ne
+
+ @cached_method
+ def facets(self):
+ ans = list(map(self._V, self._cone.ieqs()))
+ for f in ans:
+ f.set_immutable()
+ return ans
+
+ ieqs = facets
+
+ @cached_method
+ def affine_dimension(self):
+ return self._cone.affine_dimension()
+
+ @cached_method
+ def combinatorial_polyhedron(self):
+ r"""
+ Columns correspond to inequalities the rows correspond to rays.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
+ sage: vt.delaunay_cone().combinatorial_polyhedron()
+ A 3-dimensional combinatorial polyhedron with 6 facets
+ """
+ return CombinatorialPolyhedron(incidence_matrix(self.facets(), self.rays()))
+
+ def x_vanishing_face(self, e):
+ r"""
+ Return the x-vanishing face of the edge ``e``.
+ """
+ CP = self.combinatorial_polyhedron()
+ Vrep = [i for i, r in enumerate(self.rays()) if r[e] == 0]
+ return CP.join_of_Vrep(*Vrep)
+
+ def y_vanishing_face(self, e):
+ r"""
+ Return the y-vanishing face of the edge ``e``.
+ """
+ CP = self.combinatorial_polyhedron()
+ ne = self._vt._ne
+ Vrep = [i for i, r in enumerate(self.rays()) if r[ne + e] == 0]
+ return CP.join_of_Vrep(*Vrep)
+
+ def vanishing_face(self, e):
+ r"""
+ Return the vanishing face of the edge ``e``.
+ """
+ CP = self.combinatorial_polyhedron()
+ return CP.meet_of_Hrep(*self.x_vanishing_face(e).ambient_H_indices(),
+ *self.y_vanishing_face(e).ambient_H_indices())
+
+ def forward_delaunay_face(self, e, check=True):
+ r"""
+ Return the forward Delaunay face of the edge ``e``.
+
+ The edge ``e`` must be a forward flippable edge.
+ """
+ # x[e] = y[a] + y[d]
+ if check and not self._vt.is_forward_flippable(e):
+ raise ValueError("non forward-flippable edge e={}".format(e))
+ ne = self._vt._ne
+ CP = self.combinatorial_polyhedron()
+ a, b, c, d = self._vt.square_about_half_edge(2 * e)
+ Vrep = [i for i, r in enumerate(self.rays()) if r[e] == r[ne + a//2] + r[ne + d//2]]
+ return CP.join_of_Vrep(*Vrep)
+
+ def backward_delaunay_face(self, e, check=True):
+ r"""
+ Return the backward Delaunay face of the edge ``e``.
+
+ The edge ``e`` must be a backward flippable edge.
+ """
+ # y[e] = x[a] + x[d]
+ if check and not self._vt.is_backward_flippable(e):
+ raise ValueError("non backward-flippable edge e={}".format(e))
+ ne = self._vt._ne
+ CP = self.combinatorial_polyhedron()
+ a, b, c, d = self._vt.square_about_half_edge(2 * e)
+ Vrep = [i for i, r in enumerate(self.rays()) if r[ne + e] == r[a // 2] + r[d // 2]]
+ return CP.join_of_Vrep(*Vrep)
+
+ def _filter_facets(self, edges_and_faces):
+ ans = collections.defaultdict(list)
+ facets = {}
+ for e, face in edges_and_faces:
+ if face.dimension() == self.affine_dimension() - 2:
+ Hrep = face.ambient_H_indices()
+ ans[Hrep].append(e)
+ facets[Hrep] = face
+ return [(facets[Hrep], ans[Hrep]) for Hrep in facets]
+
+ def x_vanishing_facets(self):
+ return self._filter_facets((e, self.x_vanishing_face(e)) for e in range(self._vt._ne))
+
+ def y_vanishing_facets(self):
+ return self._filter_facets((e, self.y_vanishing_face(e)) for e in range(self._vt._ne))
+
+ def forward_delaunay_facets(self):
+ return self._filter_facets((e, self.forward_delaunay_face(e)) for e in self._vt.forward_flippable_edges())
+
+ def backward_delaunay_facets(self):
+ return self._filter_facets((e, self.backward_delaunay_face(e)) for e in self._vt.backward_flippable_edges())
+
+ def __repr__(self):
+ s = "{}-dimensional Delaunay cone of {} made of\n"
+ s += " {} forward-flip facets\n"
+ s += " {} backward-flip facets\n"
+ s += " {} x-degeneration facets\n"
+ s += " {} y-degeneration facets"
+ return s.format(self.affine_dimension(), self._vt,
+ len(self.forward_delaunay_facets()),
+ len(self.backward_delaunay_facets()),
+ len(self.x_vanishing_facets()),
+ len(self.y_vanishing_facets()))
+
+
+def incidence_matrix(facets, rays, mutable=False):
+ ans = matrix(ZZ, len(rays), len(facets))
+ for i, r in enumerate(rays):
+ for j, f in enumerate(facets):
+ ans[i, j] = r.dot_product(f).is_zero()
+ if not mutable:
+ ans.set_immutable()
+ return ans
diff --git a/veerer/delaunay_strebel_graph.py b/veerer/delaunay_strebel_graph.py
new file mode 100644
index 00000000..12c7ba7b
--- /dev/null
+++ b/veerer/delaunay_strebel_graph.py
@@ -0,0 +1,504 @@
+r"""
+Delaunay-Strebel graph (for prime components).
+"""
+
+from array import array
+
+from sage.misc.cachefunc import cached_method
+from sage.graphs.digraph import DiGraph
+from sage.misc.prandom import randrange
+
+from .permutation import perm_preimage, perm_orbit
+from .labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+from .veering_triangulation import VeeringTriangulation
+from .strebel_graph import StrebelGraph
+
+
+class DelaunayStrebelGraph(LabelledDiGraph):
+ r"""
+ Delaunay-Strebel graph.
+
+ The Delaunay-Strebel graph encodes a decomposition of an irreducible linear
+ subvarieties of the moduli space of Abelian or quadratic differentials. The
+ subset of vertices of the graph that are ``VeeringTriangulation``
+ corresponds to cells. The edges encode some (but not all) adjacencies
+ between cells.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+
+ Let us build a Delaunay-Strebel graph in H(3^2, -3^2)::
+
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:2)(~4:2)", "BRRRBBRRRB")
+ sage: ds_graph = vt.delaunay_strebel_graph()
+ sage: ds_graph
+ Delaunay-Strebel graph of VeeringTriangulation("(0:1,1:1,2:1,3:1)(~0:1,~1:1,~2:1,~3:1)", "RRRB") made of
+ 446 veering Delaunay states
+ 1 Strebel states
+ 1200 flip transitions
+ 42 rotation transitions
+ 42 Strebel transitions
+
+ The vertices and edges are indexed by integers (for efficiency purposes). In order to get
+ access to the underlying geometric data, one needs to call the methods ``vertex_label``
+ and ``edge_label``::
+
+ sage: ds_graph.vertex_label(122)
+ VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,7,~6)(~5,8,9)(~7,~8,~9)(0:2)(~4:2)", "RBBBRBRBRB")
+ sage: ds_graph.edge_label(37)
+ ('flip',
+ [5],
+ 1,
+ 2,
+ array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 11, 10, 14, 15, 16, 17, 18, 19]))
+ """
+ def __init__(self, ds_graph):
+ root = min(vt for vt in ds_graph if isinstance(vt, VeeringTriangulation))
+ LabelledDiGraph.__init__(self, ds_graph, root)
+
+ # store both the spanning tree oriented "towards" the root and "from" the root
+ self._spanning_tree_to, self._complementary_edges = self.spanning_tree()
+ self._spanning_tree_from = [[] for _ in range(len(self))]
+ for i in self._spanning_tree_to:
+ if i is not None:
+ self._spanning_tree_from[self.edge_target(i)].append(~i)
+
+ def __eq__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._vertices[0] == other._vertices[0]
+
+ def __ne__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._vertices[0] != other._vertices[0]
+
+ def __lt__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._vertices[0] < other._vertices[0]
+
+ def __le__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._vertices[0] <= other._vertices[0]
+
+ def __gt__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._vertices[0] > other._vertices[0]
+
+ def __ge__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._vertices[0] >= other._vertices[0]
+
+ def num_veering_states(self):
+ return sum(isinstance(state, VeeringTriangulation) for state in self._vertices)
+
+ def num_strebel_states(self):
+ return sum(isinstance(state, StrebelGraph) for state in self._vertices)
+
+ def num_flip_transitions(self):
+ return sum(label[0] == "flip" for label in self._edge_labels)
+
+ def num_rotation_transitions(self):
+ return sum(label[0] == "rotate" for label in self._edge_labels)
+
+ def num_strebel_transitions(self):
+ return sum(label[0] == "strebel" for label in self._edge_labels)
+
+ def __str__(self):
+ return "Delaunay-Strebel graph on {} vertices and {} edges".format(self.num_verts(), self.num_edges())
+
+ def __repr__(self):
+ s = ["Delaunay-Strebel graph of {} made of".format(self._vertices[0])]
+ s.append(" {} veering Delaunay states".format(self.num_veering_states()))
+ s.append(" {} Strebel states".format(self.num_strebel_states()))
+ s.append(" {} flip transitions".format(self.num_flip_transitions()))
+ s.append(" {} rotation transitions".format(self.num_rotation_transitions()))
+ s.append(" {} Strebel transitions".format(self.num_strebel_transitions()))
+ return "\n".join(s)
+
+ def root(self):
+ return self._vertices[0]
+
+ @cached_method
+ def separatrix_trivialization(self):
+ r"""
+ Return a trivialization of singularity labelling and choice of separatrices along the spanning tree.
+ """
+ from .monodromy import SeparatrixMonodromy
+ monodromy = SeparatrixMonodromy(self)
+
+ vertex_separatrices = [None] * len(self)
+ face_separatrices = [None] * len(self)
+ infinite_cylinders = [None] * len(self)
+
+ root = self._vertices[0]
+
+ seps = [(len(sep), sep[0]) for sep in root.vertex_separatrices(flat=False)]
+ seps.sort()
+ seps = [sep for a, sep in seps]
+ vertex_separatrices[0] = seps
+
+ seps = [(len(sep), sep[0]) for sep in root.face_separatrices(flat=False)]
+ seps.sort()
+ seps = [sep for a, sep in seps]
+ face_separatrices[0] = seps
+
+ infinite_cylinders[0] = [min(f) for f in root.boundary_faces() if root.face_angle(f[0]) == 0]
+
+ todo = self._spanning_tree_from[0][:]
+ while todo:
+ i = todo.pop()
+ u = self.edge_source(i)
+ v = self.edge_target(i)
+
+ assert vertex_separatrices[u] is not None
+ assert face_separatrices[u] is not None
+ assert infinite_cylinders[u] is not None
+
+ assert vertex_separatrices[v] is None
+ assert face_separatrices[v] is None
+ assert infinite_cylinders[v] is None
+
+ # TODO: we might want to avoid building a path if we just do parallel transport
+ # along a single edge
+ edge = self.path(u, [i])
+
+ vertex_separatrices[v] = [monodromy.vertex_separatrix_transport(edge, h, a) for h, a in vertex_separatrices[u]]
+ face_separatrices[v] = [monodromy.face_separatrix_transport(edge, h, a) for h, a in face_separatrices[u]]
+ infinite_cylinders[v] = [monodromy.infinite_cylinder_transport(edge, h) for h in infinite_cylinders[u]]
+
+ todo.extend(self._spanning_tree_from[v])
+
+ return tuple(vertex_separatrices), tuple(face_separatrices), tuple(infinite_cylinders)
+
+ def _ambient_framing_group(self):
+ r"""
+ Return the ambient framing group.
+
+ The framing group is the group of permutation of singularities and
+ separatrices. Any such permutation should respect the degree of
+ singularties and the cyclic ordering of separatrices.
+ """
+ from .framing_group import FramingGroup, runs
+
+ root = self._vertices[0]
+ vseps, fseps, cseps = self.separatrix_trivialization()
+ vertex_angles = [root.vertex_angle(h) for h, a in vseps[0]]
+ face_angles = [-root.face_angle(h) for h, a in fseps[0]]
+
+ angles = []
+ multiplicities = []
+ for a, m in runs(vertex_angles):
+ angles.append(a)
+ multiplicities.append(m)
+ for a, m in runs(face_angles):
+ angles.append(a)
+ multiplicities.append(m)
+ if cseps[0]:
+ angles.append(1)
+ multiplicities.append(len(cseps[0]))
+
+ return FramingGroup(angles, multiplicities)
+
+ def path_framing_group(self, path):
+ r"""
+ Return the framing monodromy of a (not necessarily closed) path.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:1)(~4:1)", "BRRRBBRRRB")
+ sage: ds_graph = vt.delaunay_strebel_graph() # long time
+ sage: tree, complementary_edges = ds_graph.spanning_tree() # long time
+
+ The monodromy is trivial along the spanning tree::
+
+ sage: for i in tree: # long time
+ ....: if i is None:
+ ....: continue
+ ....: path = ds_graph.path(ds_graph.edge_source(i), [i])
+ ....: assert ds_graph.path_framing_group(path).is_one()
+ ....: assert ds_graph.path_framing_group(~path).is_one()
+
+ And for complementary edges, it coincides with the canonical loop taken along the spanning tree::
+
+ sage: for i in complementary_edges: # long time
+ ....: path0 = ds_graph.path(ds_graph.edge_source(i), [i])
+ ....: g0 = ds_graph.path_framing_group(path0)
+ ....: path1 = ds_graph.path(ds_graph.edge_target(i), [-i-1])
+ ....: g1 = ~ds_graph.path_framing_group(path1)
+ ....: assert g0 == g1, (path0, path1, g0, g1, g0._p, g0._r, g1._p, g1._r)
+ ....: path2 = ds_graph.path(ds_graph.edge_source(i), [i])
+ ....: v = ds_graph.edge_source(i)
+ ....: while v != 0:
+ ....: i = tree[v]
+ ....: path2.appendleft(-i - 1)
+ ....: v = ds_graph.edge_target(i)
+ ....: v = ds_graph.edge_target(i)
+ ....: while v != 0:
+ ....: i = tree[v]
+ ....: path2.append(i)
+ ....: v = ds_graph.edge_target(i)
+ ....: g2 = ds_graph.path_framing_group(path2)
+ ....: assert g0 == g2, (path0, path2, g0, g2, g0._p, g0._r, g2._p, g2._r)
+ """
+ if path._graph is not self:
+ raise ValueError
+
+ from .monodromy import SeparatrixMonodromy, framing_group_element
+
+ monodromy = SeparatrixMonodromy(self)
+
+ u = path.start()
+ v = path.end()
+
+ vseps, fseps, cseps = self.separatrix_trivialization()
+
+ vseps0 = vseps[v]
+ vseps1 = [monodromy.vertex_separatrix_transport(path, h, a) for h, a in vseps[u]]
+
+ fseps0 = fseps[v]
+ fseps1 = [monodromy.face_separatrix_transport(path, h, a) for h, a in fseps[u]]
+
+ cseps0 = cseps[v]
+ cseps1 = [monodromy.infinite_cylinder_transport(path, h) for h in cseps[u]]
+
+ return framing_group_element(self._vertices[v], self._ambient_framing_group(), vseps0, vseps1, fseps0, fseps1, cseps0, cseps1)
+
+ @cached_method
+ def framing_group(self):
+ r"""
+ Return the monodromy of framing obtained by parallel transport along
+ this Delaunay-Strebel graph.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+
+ The case of H(1^2)::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,9)(~8,10,11)(~9,~10,~11)", "BRBBRBBRBBRB")
+ sage: ds_graph = vt.delaunay_strebel_graph() # long time
+ sage: G = ds_graph.framing_group() # long time
+ sage: G.cardinality() # long time
+ 8
+ sage: G.structure_description() # long time
+ 'C4 x C2'
+
+ The case of H(1^2, -1^2)::
+
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:1)(~4:1)", "BRRRBBRRRB")
+ sage: ds_graph = vt.delaunay_strebel_graph() # long time
+ sage: G = ds_graph.framing_group() # long time
+ sage: G.cardinality() # long time
+ 16
+ sage: G.structure_description() # long time
+ 'C4 x C2 x C2'
+
+ The case of H(3^2, -3^2)::
+
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:2)(~4:2)", "BRRRBBRRRB")
+ sage: ds_graph = vt.delaunay_strebel_graph() # long time
+ sage: G = ds_graph.framing_group() # long time
+ sage: G.cardinality() # long time
+ 20
+ sage: G.structure_description() # long time
+ 'C10 x C2'
+ """
+ from .monodromy import SeparatrixMonodromy, framing_group_element
+
+ vseps, fseps, cseps = self.separatrix_trivialization()
+
+ G = self._ambient_framing_group()
+ H = G.subgroup(mutable=True)
+ monodromy = SeparatrixMonodromy(self)
+
+ H.add_generator(G.rotation())
+
+ for edge in self._complementary_edges:
+ u = self.edge_source(edge)
+ v = self.edge_target(edge)
+ path = self.path(u, [edge])
+
+ vseps0 = vseps[v]
+ vseps1 = [monodromy.vertex_separatrix_transport(path, h, a) for h, a in vseps[u]]
+
+ fseps0 = fseps[v]
+ fseps1 = [monodromy.face_separatrix_transport(path, h, a) for h, a in fseps[u]]
+
+ cseps0 = cseps[v]
+ cseps1 = [monodromy.infinite_cylinder_transport(path, h) for h in cseps[u]]
+
+ g = framing_group_element(self._vertices[v], G, vseps0, vseps1, fseps0, fseps1, cseps0, cseps1)
+
+ H.add_generator(g)
+
+ H.set_immutable()
+ return H
+
+ def framing_group_element_permutation(self, g, v=0):
+ r"""
+ Given an element of the framing group ``g`` return a triple of
+ dictionaries ``(d_vseps, d_fseps, d_cseps)`` encoding permutations of
+ the separatrices.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: f = VeeringTriangulationLinearFamily("(0:1)(~0:1,1:1,2:1,~1:1)(~2:1)", "RBR", [(1, 0, 1), (0, 1, 0)])
+ sage: ds_graph = f.delaunay_strebel_graph()
+ sage: G = ds_graph.framing_group()
+
+ sage: g = G("(0:1)(1:1)(2:1)")
+ sage: d_vseps, d_fseps, d_cseps = ds_graph.framing_group_element_permutation(g)
+ sage: print(", ".join(f"{sep} -> {d_vseps[sep]}" for sep in sorted(d_vseps)))
+ (0, 0) -> (1, 0), (1, 0) -> (1, 1), (1, 1) -> (2, 0), (2, 0) -> (0, 0), (3, 0) -> (5, 0), (4, 0) -> (4, 1), (4, 1) -> (3, 0), (5, 0) -> (4, 0)
+ sage: print(", ".join(f"{sep} -> {d_fseps[sep]}" for sep in sorted(d_fseps)))
+ (1, 0) -> (4, 0), (4, 0) -> (1, 0)
+ sage: print(", ".join(f"{sep} -> {d_cseps[sep]}" for sep in sorted(d_cseps)))
+ 0 -> 0, 5 -> 5
+
+ sage: g = G("(0:0, 1:2)")
+ sage: d_vseps, d_fseps, d_cseps = ds_graph.framing_group_element_permutation(g)
+ sage: print(", ".join(f"{sep} -> {d_vseps[sep]}" for sep in sorted(d_vseps)))
+ (0, 0) -> (3, 0), (1, 0) -> (5, 0), (1, 1) -> (4, 0), (2, 0) -> (4, 1), (3, 0) -> (1, 1), (4, 0) -> (0, 0), (4, 1) -> (1, 0), (5, 0) -> (2, 0)
+ sage: print(", ".join(f"{sep} -> {d_fseps[sep]}" for sep in sorted(d_fseps)))
+ (1, 0) -> (1, 0), (4, 0) -> (4, 0)
+ sage: print(", ".join(f"{sep} -> {d_cseps[sep]}" for sep in sorted(d_cseps)))
+ 0 -> 0, 5 -> 5
+ """
+ state = self._vertices[v]
+ vseps, fseps, cseps = self.separatrix_trivialization()
+
+ all_seps = []
+ for h, a in vseps[v]:
+ orbit = [(h, b) for b in range(a, state.half_edge_num_separatrices(h, check=False))]
+ for hh in perm_orbit(state._vp, h)[1:]:
+ orbit.extend((hh, b) for b in range(state.half_edge_num_separatrices(hh, check=False)))
+ orbit.extend((h, b) for b in range(a))
+ all_seps.append(orbit)
+
+ for h, a in fseps[v]:
+ orbit = [(h, b) for b in range(a, -1, -1)]
+ for hh in perm_orbit(state._fp, h)[1:]:
+ orbit.extend((hh, b) for b in range(state.half_edge_num_separatrices(hh, check=False) - 2, -1, -1))
+ orbit.extend((h, b) for b in range(state.half_edge_num_separatrices(h, check=False) - 2, a, -1))
+ all_seps.append(orbit)
+
+ all_seps.extend(cseps[v])
+
+ nv = len(vseps[v])
+ nf = len(fseps[v])
+ nc = len(cseps[v])
+
+ d_vseps = {}
+ d_fseps = {}
+ d_cseps = {}
+
+ for i in range(nv):
+ seps = all_seps[i]
+ for a, sep in enumerate(seps):
+ j, b = g(i, a)
+ d_vseps[sep] = all_seps[j][b]
+ for i in range(nv, nv + nf):
+ seps = all_seps[i]
+ for a, sep in enumerate(seps):
+ j, b = g(i, a)
+ d_fseps[sep] = all_seps[j][b]
+ for i in range(nv + nf, nv + nf + nc):
+ sep = all_seps[i]
+ j = g(i)
+ d_cseps[sep] = all_seps[j]
+
+ return (d_vseps, d_fseps, d_cseps)
+
+ # The following is clearly not the optimal strategy. We could have a self-loop
+ # that commutes with many other flips. We could delete all such loops but one
+ # and still generate.
+ # For a self-loop with flip "e" one can look at the connected component of the
+ # graph where "e" is not flipped and remove all but one "e loop".
+ def commuting_loops_and_squares(self, v):
+ r"""
+ Return loops and squares at ``v``.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,8,~7)(~1,~8,3)(2,7,~6)(~2,~3,4)", "RRRBBBBBB")
+ """
+ distance2 = {}
+ for i0 in self.outgoing_edges(v, reverse=False):
+ label0 = self._edge_labels[i0]
+ if label0[0] != "flip":
+ continue
+ edges0 = tuple(label0[1])
+ col0 = label0[3]
+ relabel = label0[4]
+ v1 = self._edge_targets[i0]
+ for i1 in self.outgoing_edges(v1, reverse=False):
+ label1 = self._edge_labels[i1]
+ if label1[0] != "flip":
+ continue
+ edges1 = tuple(sorted(perm_preimage(relabel, 2 * e) // 2 for e in label1[1]))
+ col1 = label1[3]
+ distance2[(edges0, col0, edges1, col1)] = (i0, i1)
+
+ loops = []
+ squares = []
+ for (edges0, col0, edges1, col1), (i0, i1) in distance2.items():
+ key = distance2.get((edges1, col1, edges0, col0))
+ if key is None:
+ continue
+ j0, j1 = key
+ assert i0 != j0 and i1 != j1
+ if i0 == j1:
+ loops.append((j0, j1, i1))
+ elif i0 < j0:
+ squares.append((i0, i1, j0, j1))
+
+ return (loops, squares)
+
+ def reduced(self):
+ r"""
+ Return a graph whose fundamental group also generates the fundamental
+ group of the underlying stratum.
+ """
+ kept = [True] * self.num_edges()
+ for v in range(self.num_verts()):
+ loops, squares = self.commuting_loops_and_squares(v)
+ for (i, _, j) in loops:
+ if kept[i] and kept[j]:
+ r = randrange(2)
+ if r == 0:
+ kept[i] = False
+ else:
+ kept[j] = False
+
+ for (i0, i1, j0, j1) in squares:
+ if kept[i0] and kept[i1] and kept[j0] and kept[j1]:
+ r = randrange(4)
+ if r == 0:
+ kept[i0] = False
+ elif r == 1:
+ kept[i1] = False
+ elif r == 2:
+ kept[j0] = False
+ else:
+ kept[j1] = False
+
+ G = DiGraph(self.num_verts(), loops=self._digraph.allows_loops(), multiedges=self._digraph.allows_multiple_edges())
+ for i, b in enumerate(kept):
+ if b:
+ G.add_edge(self._edge_sources[i], self._edge_targets[i], i)
+ return G
diff --git a/veerer/flat_structure.py b/veerer/flat_structure.py
index 962a6cb7..020bbe70 100644
--- a/veerer/flat_structure.py
+++ b/veerer/flat_structure.py
@@ -1,10 +1,10 @@
r"""
-Veering triangulations endowed with a flat structure.
+Flat structures on veering triangulations and Strebel graphs.
"""
# ****************************************************************************
# This file is part of veerer
#
-# Copyright (C) 2018-2023 Vincent Delecroix
+# Copyright (C) 2018-2024 Vincent Delecroix
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -21,287 +21,197 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# ****************************************************************************
+import copy
+
from sage.structure.sequence import Sequence
+from sage.structure.element import Vector
+from sage.categories.rings import Rings
from sage.categories.fields import Fields
from sage.rings.all import ZZ, QQ, AA, RDF, NumberField
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
-from sage.modules.free_module import VectorSpace
+from sage.modules.free_module import VectorSpace, FreeModule
from sage.modules.free_module_element import vector
-from .constants import BLUE, RED, PURPLE, GREEN, LEFT, RIGHT
+from .constants import BLUE, RED, PURPLE, GREEN, LEFT, RIGHT, HORIZONTAL, VERTICAL
from .constellation import Constellation
-from .permutation import perm_cycles, perm_check, perm_init, perm_conjugate, perm_on_list
+from .permutation import perm_cycles, perm_check, perm_init, perm_conjugate, perm_on_list, perm_on_edge_list
from .triangulation import Triangulation
from .veering_triangulation import VeeringTriangulation
from .misc import flipper_edge, flipper_edge_perm, flipper_nf_to_sage, flipper_nf_element_to_sage, det2, flipper_face_edge_perms
+_Rings = Rings()
_Fields = Fields()
-def vec_slope(v):
+
+def slope(x, y):
r"""
Return the slope of a 2d vector ``v``.
EXAMPLES::
sage: from veerer.constants import RED, BLUE, PURPLE, GREEN # random output due to deprecation warnings from realalg
- sage: from veerer.flat_structure import vec_slope
+ sage: from veerer.flat_structure import slope
- sage: vec_slope((1,0)) == PURPLE
+ sage: slope(1, 0) == PURPLE
True
- sage: vec_slope((0,1)) == GREEN
+ sage: slope(0, 1) == GREEN
True
- sage: vec_slope((1,1)) == vec_slope((-1,-1)) == RED
+ sage: slope(1, 1) == slope(-1 ,-1) == RED
True
- sage: vec_slope((1,-1)) == vec_slope((1,-1)) == BLUE
+ sage: slope(1, -1) == slope(1, -1) == BLUE
True
"""
- if v[0].is_zero():
+ if x.is_zero() and y.is_zero():
+ raise ValueError("zero vector")
+ if x.is_zero():
return GREEN
- elif v[1].is_zero():
+ elif y.is_zero():
return PURPLE
- elif v[0] * v[1] > 0:
+ elif x * y > 0:
return RED
else:
return BLUE
-class FlatVeeringTriangulation(Triangulation):
+
+class FlatStructure:
r"""
- A triangulation with flat structures with veering compatible slopes.
+ Abstract class to handle coordinates on either veering triangulation or
+ Strebel graph.
+ """
+ def _constellation_class_init(self):
+ bases = self.__class__.__bases__
+ if len(bases) != 2 or bases[0] != FlatStructure or not issubclass(bases[1], Constellation):
+ raise TypeError("invalid class")
+ self._constellation_class = bases[1]
+
+ def __init__(self, *args, mutable=False, check=False):
+ self._constellation_class_init()
+ if len(args) < 3:
+ raise ValueError("require at least three arguments")
+ constellation_args = args[:-2]
+ x = args[-2]
+ y = args[-1]
+ if len(constellation_args) == 1:
+ arg = constellation_args[0]
+ if not isinstance(arg, VeeringTriangulation):
+ # guess colors based on input
+ if not isinstance(arg, Triangulation):
+ arg = Triangulation(arg)
+ colouring = arg.colouring_from_xy(x, y)
+ vt = VeeringTriangulation(arg, colouring)
+ constellation_args = (vt,)
+ self._constellation_class.__init__(self, *constellation_args, mutable=mutable, check=False)
+
+ if not isinstance(x, Vector) or not isinstance(y, Vector):
+ S = Sequence(list(x) + list(y))
+ base_ring = S.universe()
+ if base_ring not in _Rings:
+ raise ValueError("invalid coordinates: x={} y={} with universe={}".format(x, y, base_ring))
+ if base_ring not in _Fields:
+ base_ring = base_ring.fraction_field()
+ x = vector(list(map(base_ring, x)))
+ y = vector(list(map(base_ring, y)))
+ elif x.parent() != y.parent():
+ V = x.common_parent(y)
+ x = V(x)
+ y = V(y)
+ base_ring = V.base_ring()
+
+ self._x = x
+ self._y = y
- The vectors are kept coherently within triangles (ie a+b+c = 0). A pair of
- edges (e, E) can either be glued via translation or point symmetry. If the
- surface is a translation surface, these are only translations.
+ if not mutable:
+ self._x.set_immutable()
+ self._y.set_immutable()
+ else:
+ if not x.is_mutable():
+ self._x = copy.copy(self._x)
+ if not y.is_mutable():
+ self._y = copy.copy(self._y)
+
+ if check:
+ self._check()
+
+ def constellation(self, mutable=False):
+ return Constellation.copy(self, mutable, cls=self._constellation_class)
+
+
+class FlatVeeringTriangulation(FlatStructure, VeeringTriangulation):
+ r"""
+ A veering triangulation with a flat structure.
EXAMPLES::
sage: from veerer import FlatVeeringTriangulation
- sage: vecs = [(1, 2), (-2, -1), (1, -1), (1, -1), (-2, -1), (1, 2)]
- sage: FlatVeeringTriangulation("(0,1,2)(~0,~1,~2)", vecs)
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)])
-
- For translation structures (or Abelian differential) the holonomies might be modified
- by a sign so that ``holonomies[e] = - holonomies[~e]``::
-
- sage: vecs = [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)]
- sage: vecs = [vector(ZZ, v) for v in vecs]
- sage: fp = "(0,1,2)(~0,~1,~2)"
- sage: FlatVeeringTriangulation(fp, vecs)
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)])
- sage: vecs = [vecs[0], vecs[1], vecs[2], -vecs[3], -vecs[4], -vecs[5]]
- sage: FlatVeeringTriangulation(fp, vecs)
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)])
- sage: vecs = [-vecs[0], -vecs[1], -vecs[2], vecs[3], vecs[4], vecs[5]]
- sage: FlatVeeringTriangulation(fp, vecs)
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)])
- sage: vecs = [-vecs[0], -vecs[1], -vecs[2], vecs[3], vecs[4], vecs[5]]
- sage: FlatVeeringTriangulation(fp, vecs)
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)])
+ sage: x = (1, 2, 1)
+ sage: y = (2, 1, 1)
+ sage: FlatVeeringTriangulation("(0,1,2)(~0,~1,~2)", x, y)
+ FlatVeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB", (1, 2, 1), (2, 1, 1))
TESTS::
sage: from veerer import Triangulation, FlatVeeringTriangulation
sage: T = Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- sage: fl = FlatVeeringTriangulation(T, [(-47, 51), (-27, -67), (74, 16), (22, 79), (-69, -28), (-61, -31), (34, -36)], mutable=True)
+ sage: fl = FlatVeeringTriangulation(T, [47, 27, 74, 22, 69, 61, 34], [51, 67, 16, 79, 28, 31, 36], mutable=True)
sage: fl.relabel('(1,0)(3,5,4,6)')
sage: fl
- FlatVeeringTriangulation(Triangulation("(0,2,1)(3,~0,4)(5,6,~1)"), [(-27, -67), (-47, 51), (74, 16), (34, -36), (-61, -31), (22, 79), (-69, -28), (47, -51), (27, 67)])
+ FlatVeeringTriangulation("(0,2,1)(~0,4,3)(~1,5,6)", "RBRBRRR", (27, 47, 74, 34, 61, 22, 69), (67, 51, 16, 36, 31, 79, 28))
"""
- def _set_data_pointers(self):
- self._bdry = self._data[0]
- self._holonomies = self._data[1]
-
- def __init__(self, triangulation, holonomies=None, base_ring=None, mutable=False, check=True):
- if not isinstance(triangulation, Triangulation):
- triangulation = Triangulation(triangulation)
- if any(triangulation._bdry):
- raise NotImplementedError
- if isinstance(triangulation, FlatVeeringTriangulation):
- holonomies = triangulation._holonomies[:]
- self._base_ring = triangulation._K
- self._V = triangulation._V
- self._K = triangulation._K
- self._translation = triangulation._translation
- else:
- if base_ring is None:
- S = Sequence([vector(v) for v in holonomies])
- self._V = S.universe()
- self._K = self._V.base_ring()
- holonomies = list(S)
- else:
- self._K = base_ring
- self._V = VectorSpace(self._K, 2)
- holonomies = [self._V(v) for v in holonomies]
-
- if self._K not in _Fields:
- self._K = self._K.fraction_field()
- self._V = self._V.change_ring(self._K)
- holonomies = [v.change_ring(self._K) for v in holonomies]
-
- n = triangulation._n
- m = triangulation.num_edges()
- ep = triangulation._ep
-
- if len(holonomies) == m:
- for e in range(m):
- E = ep[e]
- if e != E and E < m:
- raise ValueError("edge perm not in standard form")
- holonomies.extend([-holonomies[ep[e]] for e in range(m,n)])
- if len(holonomies) != n:
- raise ValueError('wrong number of vectors')
-
- Constellation.__init__(self, triangulation._n, triangulation._vp[:], triangulation._ep[:], triangulation._fp[:], (triangulation._bdry[:], holonomies), True, False)
-
- cols = [vec_slope(self._holonomies[e]) for e in range(self._n)]
- if isinstance(triangulation, VeeringTriangulation):
- # check that colours are compatible
- for e in range(triangulation.num_edges()):
- tcol = triangulation.edge_colour(e)
- scol = self.edge_colour(e)
- if scol == PURPLE or scol == GREEN:
- continue
- if tcol != scol:
- raise ValueError("incompatible colours")
-
- V = self.to_veering_triangulation()
- ans, cert = V.is_abelian(certificate=True)
- self._translation = False
- if ans:
- # translation surface (Abelian differential)
- # fix holonomies so that
- # holonomies[ep[e]] = - holonomies[e]
- self._translation = True
- ep = self._ep
- for e, right in enumerate(cert):
- E = ep[e]
- if right != (self._holonomies[e][0] > 0):
- self._holonomies[e] = -self._holonomies[e]
- if right != (self._holonomies[E][0] < 0):
- self._holonomies[E] = -self._holonomies[E]
+ __slots__ = ['_x', '_y']
- if not mutable:
- self.set_immutable()
-
- if check:
- self._check()
+ def base_ring(self):
+ return self._x.base_ring()
- @classmethod
- def from_coloured_triangulation(cls, T):
+ def _check(self, error=ValueError):
r"""
- Construct a flat triangulation associated to a given coloured triangulation.
-
EXAMPLES::
sage: from veerer import *
-
- sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [RED, RED, BLUE])
- sage: FlatVeeringTriangulation.from_coloured_triangulation(T)
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)])
- """
- return T.flat_structure_min()
-
- def triangle_upside_down(self, e):
- r"""
- Apply a 180 degree rotation to the triangle containing the edge ``e``.
+ sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
+ sage: assert T.is_core()
+ sage: F = T.flat_structure_min()
+ sage: F._check()
"""
- if not self._mutable:
- raise ValueError("immutable flat veering triangulation; use a mutable copy instead")
+ self._constellation_class._check(self, error)
+ x = self._x
+ y = self._y
+ if not isinstance(x, Vector) or not isinstance(y, Vector) or len(x) != self._ne or len(y) != self._ne:
+ raise error("invalid coordinates")
+ if self._mutable != x.is_mutable() or self._mutable != y.is_mutable():
+ raise error("incoherent mutability state: self._mutable={}, x.is_mutable()={}, y.is_mutable()={}".format(self._mutable, x.is_mutable(), y.is_mutable()))
- f = self._fp[e]
- g = self._fp[f]
- self._holonomies[e] = -self._holonomies[e]
- self._holonomies[f] = -self._holonomies[f]
- self._holonomies[g] = -self._holonomies[g]
+ self._set_subspace_constraints(lambda c: self._constraint_check(c, error), x, VERTICAL)
+ self._set_subspace_constraints(lambda c: self._constraint_check(c, error), y, HORIZONTAL)
- self._check()
-
- def colours_about_edge(self, e):
- e = int(e)
- return [self.edge_colour(f) for f in self.square_about_edge(e)]
-
- def alternating_square(self, e):
+ def __str__(self):
r"""
- Return whether there is an alternating square around the edge ``e``.
+ Return a string representation.
"""
- e = int(e)
- colours = self.colours_about_edge(e)
- if any(colours[f] == GREEN or colours[f] == PURPLE for f in range(4)):
- return False
- return all(colours[f] != colours[(f+1) % 4] for f in range(4))
-
- def is_flippable(self, e):
- return Triangulation.is_flippable(self, e) and self.alternating_square(e)
+ cls_name = self._constellation_class.__name__
+ s = str(self._constellation_class.__str__(self))
+ i = s.find('(')
+ return 'Flat' + s[:i] + s[i:-1] + ', ' + str(self._x) + ', ' + str(self._y) + ')'
- def is_forward_flippable(self, e):
+ @classmethod
+ def from_coloured_triangulation(cls, T):
r"""
- Return whether ``e`` is a forward flippable edge
+ Construct a flat triangulation associated to a given coloured triangulation.
EXAMPLES::
- sage: from veerer import Triangulation, FlatVeeringTriangulation
- sage: T = Triangulation("(0,4,3)(1,~3,5)(2,6,~4)")
- sage: hols = [(-2, 10), (6, -2), (3, 3), (4, -4), (-2, -6), (-2, -2), (-1, 3), (-2, -6), (-4, 4)]
- sage: fl = FlatVeeringTriangulation(T, hols)
- sage: fl.is_forward_flippable(1)
- True
- sage: fl.is_forward_flippable(3)
- False
- """
- return Triangulation.is_flippable(self, e) and self.colours_about_edge(e) == [BLUE, RED, BLUE, RED]
-
- def forward_flippable_edges(self):
- ep = self._ep
- n = self._n
- return [e for e in range(n) if e <= ep[e] and self.is_forward_flippable(e)]
-
- def is_backward_flippable(self, e):
- return Triangulation.is_flippable(self, e) and self.colours_about_edge(e) == [RED, BLUE, RED, BLUE]
-
- def backward_flippable_edges(self):
- ep = self._ep
- n = self._n
- return [e for e in range(n) if e <= ep[e] and self.is_backward_flippable(e)]
+ sage: from veerer import *
- def swap(self, e):
- r"""
- Swap the orientation of the edge ``e``.
+ sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [RED, RED, BLUE])
+ sage: FlatVeeringTriangulation.from_coloured_triangulation(T)
+ FlatVeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB", (1, 2, 1), (2, 1, 1))
"""
- if not self._mutable:
- raise ValueError("immutable flat veering triangulation; use a mutable copy instead")
- E = self._ep[e]
- if e != E:
- Triangulation.swap(e)
- self._holonomies[e], self._holonomies[E] = self._holonomies[E], self._holonomies[e]
+ return T.flat_structure_min()
def boshernitzan_criterion(self):
r"""
"""
- if not self._translation:
- raise NotImplementedError
-
- n = self.num_edges()
- X = []
- for h in self._holonomies[:n]:
- if h[1] == 0:
- continue
- elif h[1] < 0:
- X.append(-h[0])
- else:
- X.append(h[0])
- X = [x.vector() for x in X]
- m = len(X[0])
- X = [[0] + [X[i][j] for i in range(len(X))] for j in range(m)]
-
- # non-negativity
- ieqs = []
- for i in range(n):
- e = [0] * n
- e[i] = 1
- ieqs.append([0] + e)
-
- from sage.geometry.polyhedron.constructor import Polyhedron
- return Polyhedron(eqns=X, ieqs=ieqs)
+ raise NotImplementedError
@classmethod
def from_flipper_pseudo_anosov(cls, h):
@@ -345,97 +255,177 @@ def from_flipper_pseudo_anosov(cls, h):
return FlatVeeringTriangulation(T, vectors, K)
- def to_veering_triangulation(self):
- return VeeringTriangulation(self, [self.edge_colour(e) for e in range(self._n)])
-
- def to_pyflatsurf(self):
- if not self._translation:
- raise ValueError("pyflatsurf only works with translation surfaces")
- from pyflatsurf.factory import make_surface
- from pyflatsurf.sage_conversion import make_vectors
- verts = [(i+1 if i >=0 else i for i in c) for c in perm_cycles(self._vp, True, self._n)]
- vectors = makeVectors(self._holonomies[e] for e in range(self.num_edges()))
- return make_surface(verts, vectors)
-
- def __str__(self):
- return "FlatVeeringTriangulation({}, {})".format(Triangulation.__str__(self),
- self._holonomies)
-
def __repr__(self):
return str(self)
- def _check(self, error=RuntimeError):
+ def copy(self, mutable=None, cls=None):
r"""
EXAMPLES::
sage: from veerer import *
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
- sage: assert T.is_core()
sage: F = T.flat_structure_min()
- sage: F._check()
+ sage: F.copy()
+ FlatVeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR", (1, 1, 2, 2), (1, 2, 1, 1))
+ sage: F.copy(mutable=True)._check()
+ sage: F.copy(mutable=False)._check()
"""
- Triangulation._check(self, error)
-
- n = self.num_half_edges()
- ep = self.edge_permutation(copy=False)
- vectors = self._holonomies
- if len(vectors) != n:
- raise error("invalid list of vectors")
-
- for a in range(n):
- A = ep[a]
- u = vectors[a]
- v = vectors[A]
- if u != v and u != -v:
- raise error('ep[%s] = %s but vec[%s] = %s' % (a, u, A, v))
-
- for a,b,c in self.faces():
- va = vectors[a]
- vb = vectors[b]
- vc = vectors[c]
- if va + vb + vc:
- raise error('vec[%s] = %s, vec[%s] = %s and vec[%s] = %s do not sum to zero' % (a, va, b, vb, c, vc))
-
- if det2(va, vb) <= 0 or det2(vb, vc) <= 0 or det2(vc, va) <= 0:
- raise error('(%s, %s, %s) is a clockwise triangle' %
- (a, b, c))
-
- if self._translation:
- for e in range(self.num_edges()):
- E = ep[e]
- assert self._holonomies[e] == -self._holonomies[E]
-
- def copy(self, mutable=None):
if mutable is None:
mutable = self._mutable
- if not self._mutable and not mutable:
- # avoid copies of mutable objects
+ if cls is None:
+ cls = self.__class__
+
+ if cls is self.__class__ and (not self._mutable and not mutable):
+ # avoid copies of immutable objects
return self
- res = FlatVeeringTriangulation.__new__(FlatVeeringTriangulation)
- res._n = self._n
- res._vp = self._vp[:]
- res._ep = self._ep[:]
- res._fp = self._fp[:]
- res._mutable = mutable
- res._V = self._V
- res._K = self._K
- res._holonomies = [v.__copy__() for v in self._holonomies]
- res._translation = self._translation
- res._bdry = self._bdry[:]
- res._data = (res._bdry, res._holonomies)
- return res
-
- def edge_colour(self, e):
- return vec_slope(self._holonomies[e])
-
- def layout(self):
+ if cls is not self.__class__:
+ return super().copy(mutable, cls)
+
+ F = self._constellation_class.copy(self, mutable=True, cls=self.__class__)
+ F._constellation_class = self._constellation_class
+ F._x = self._x[:]
+ F._y = self._y[:]
+ if not mutable:
+ F._x.set_immutable()
+ F._y.set_immutable()
+ return F
+
+ def flat_triangle(self, h, x_start=0, y_start=0, sign=1, check=True):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
+ sage: F = T.flat_structure_min()
+ sage: F.flat_triangle(0)
+ [(0, 0), (1, -1), (2, 1), (0, 0)]
+ sage: F.flat_triangle(0, sign=-1)
+ [(0, 0), (-1, 1), (-2, -1), (0, 0)]
+ """
+ if check:
+ h = self._check_half_edge(h)
+ V = FreeModule(self._x.base_ring(), 2)
+ fp = self._fp
+ x = x_start
+ y = y_start
+ pts = [V((x, y))]
+ col = self.edge_colour(h // 2)
+ for _ in range(3):
+ x += sign * self._x[h // 2]
+ if col == RED:
+ y += sign * self._y[h // 2]
+ elif col == BLUE:
+ y -= sign * self._y[h // 2]
+ pts.append(V((x, y)))
+ hh = fp[h]
+ ccol = self.edge_colour(hh // 2)
+ if col != BLUE or ccol != RED:
+ sign *= -1
+ h = hh
+ col = ccol
+ assert pts[0] == pts[-1]
+ return pts
+
+ def vectors(self):
+ ans, oris = self.is_abelian(certificate=True)
+ vecs = [None] * (2 * self._ne)
+ for a, b, c in self.triangles():
+ if ans:
+ t = self.flat_triangle(a, sign=(1 if oris[a] else -1))
+ else:
+ t = self.flat_triangle(a)
+ vecs[a] = t[1] - t[0]
+ vecs[b] = t[2] - t[1]
+ vecs[c] = t[3] - t[2]
+ return vecs
+
+ def is_delaunay(self):
+ for e in self.backward_flippable_edges(self):
+ a, _, _, d = self.square_about_half_edge(2 * e)
+ a //= 2
+ d //= 2
+ if self._x[a] + self._x[d] < self._y[e]:
+ return False
+ for e in self.forward_flippable_edges(self):
+ a, _, _, d = self.square_about_half_edge(2 * e)
+ a //= 2
+ d //= 2
+ if self._y[a] + self._y[d] < self._x[e]:
+ return False
+ return True
+
+ def delaunay_flip_sequence(self):
r"""
- Return a layout object that can be used to produce various plots of the surface.
+ Return the list of backward and forward flips to be performed in order
+ to turn this flat structure into its Delaunay triangulation.
+
+ EXAMPLES::
+
+ sage: from veerer import FlatVeeringTriangulation
+ sage: fl = FlatVeeringTriangulation("(0,3,4)(~0,1,2)(~1,5,6)", "BRRRRRB", (47, 27, 74, 22, 69, 61, 34), (51, 67, 16, 79, 28, 31, 36))
"""
- from .layout import FlatVeeringTriangulationLayout
- return FlatVeeringTriangulationLayout(self)
+ backward_flips = []
+ state = self.copy(mutable=True)
+ todo_backward = set(state.backward_flippable_edges())
+ todo_forward = set(state.forward_flippable_edges())
+ while todo_backward:
+ e = todo_backward.pop()
+ a, b, c, d = state.square_about_half_edge(2 * e)
+ a //= 2
+ b //= 2
+ c //= 2
+ d //= 2
+ if state._x[a] + state._x[d] < state._y[e]:
+ state.flip_back(e)
+ col = state.edge_colour(e)
+ backward_flips.append((e, col))
+ # TODO: depending on the colour col we only have
+ # two possible backward flippable edges
+ if state.is_backward_flippable(a):
+ todo_backward.add(a)
+ if state.is_backward_flippable(b):
+ todo_backward.add(b)
+ if state.is_backward_flippable(c):
+ todo_backward.add(c)
+ if state.is_backward_flippable(d):
+ todo_backward.add(d)
+
+ forward_flips = []
+ while todo_forward:
+ e = todo_forward.pop()
+ a, b, c, d = state.square_about_half_edge(2 * e)
+ a //= 2
+ b //= 2
+ c //= 2
+ d //= 2
+ if state._y[a] + state._y[d] < state._x[e]:
+ state.flip(e)
+ col = state.edge_colour(e)
+ forward_flips.append((e, col))
+ # TODO: depending on the colour col we only have
+ # two possible forward flippable edges
+ if state.is_forward_flippable(a):
+ todo_forward.add(a)
+ if state.is_forward_flippable(b):
+ todo_forward.add(b)
+ if state.is_forward_flippable(c):
+ todo_forward.add(c)
+ if state.is_forward_flippable(d):
+ todo_forward.add(d)
+
+ return backward_flips, forward_flips
+
+ def to_pyflatsurf(self):
+ ans, oris = self.is_abelian(certificate=True)
+ if not ans:
+ raise ValueError("pyflatsurf only works with translation surfaces")
+ from pyflatsurf.factory import make_surface
+ from pyflatsurf.sage_conversion import make_vectors
+ verts = [(i+1 if i >=0 else i for i in c) for c in perm_cycles(self._vp, True, self._n)]
+ vectors = makeVectors((x[e], y[e]) for e in range(self._ne))
+ return make_surface(verts, vectors)
def plot(self, *args, **kwds):
r"""
@@ -444,101 +434,171 @@ def plot(self, *args, **kwds):
sage: from veerer import *
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
sage: F = T.flat_structure_min()
- sage: F.plot() # not tested (warning in matplotlib)
- Graphics object consisting of 15 graphics primitives
-
- sage: F.plot(horizontal_train_track=True) # not tested (matplotlib warning)
- Graphics object consisting of 19 graphics primitives
- sage: F.plot(vertical_train_track=True) # not tested (matplotlib warning)
- Graphics object consisting of 19 graphics primitives
- sage: F.plot(horizontal_train_track=True, vertical_train_track=True) # not tested (matplotlib warning)
- Graphics object consisting of 23 graphics primitives
+ sage: F.plot() # optional - sage_flatsurf
+ Graphics object consisting of ... graphics primitives
"""
- return self.layout().plot(*args, **kwds)
-
- def flip(self, e, folded_edge_convention=RIGHT):
+ from .flatsurf_conversion import flat_structure_to_sage_flatsurf
+ S, m = flat_structure_to_sage_flatsurf(self)
+ edge_labels = {v: self._half_edge_string(h) for h, v in enumerate(m) if v is not None}
+ options = {BLUE: {"color": "blue"},
+ RED: {"color": "red"},
+ GREEN: {"color": "green"},
+ PURPLE: {"color": "purple"}}
+ edge_options = {v: options[self._colouring[h // 2]] for h, v in enumerate(m) if v is not None}
+ for h, v in enumerate(m):
+ if self._bdry[h]:
+ edge_options[v]["thickness"] = 3
+ G = S.graphical_surface(edge_labels=edge_labels,
+ edge_label_options={"color": "black"},
+ edge_options=edge_options,
+ self_glued_edge_options={},
+ polygon_labels=False)
+ G.will_plot_adjacent_edge_labels = True
+ G.will_plot_self_glued_edge_labels = True
+ G.will_plot_non_adjacent_edge_labels = True
+ return G.plot()
+
+ def flip(self, e, col=None, check=True):
r"""
Flip the edge ``e``.
EXAMPLES::
sage: from veerer import Triangulation, FlatVeeringTriangulation, RIGHT, LEFT
- sage: T = Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- sage: fl = FlatVeeringTriangulation(T, [(-47, 51), (-27, -67), (74, 16), (22, 79), (-69, -28), (-61, -31), (34, -36)], mutable=True)
+ sage: fl = FlatVeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)", (47, 27, 74, 22, 69, 61, 34), (51, 67, 16, 79, 28, 31, 36), mutable=True)
sage: fl.flip(2)
sage: fl.flip(4)
sage: fl.flip(0)
sage: fl
- FlatVeeringTriangulation(Triangulation("(0,1,4)(2,~0,3)(5,6,~1)"), [(2, 197), (-27, -67), (-20, 118), (22, 79), (25, -130), (-61, -31), (34, -36), (27, 67), (-2, -197)])
+ FlatVeeringTriangulation("(0,1,4)(~0,3,2)(~1,5,6)", "RRBRBRB", (2, 27, 20, 22, 25, 61, 34), (197, 67, 118, 79, 130, 31, 36))
- sage: fl = FlatVeeringTriangulation(T, [(-47, 51), (-27, -67), (74, 16), (22, 79), (-69, -28), (-61, -31), (34, -36)], mutable=False)
- sage: fl.flip(2)
+ sage: fl = FlatVeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)", (47, 27, 74, 22, 69, 61, 34,), (51, 67, 16, 79, 28, 31, 36))
+ sage: fl.flip(1)
Traceback (most recent call last):
...
ValueError: immutable flat veering triangulation; use a mutable copy instead
- sage: T = Triangulation("(0,1,2)")
- sage: fl = FlatVeeringTriangulation(T, [(13,8), (-21, -3), (8,-5)], mutable=True)
- sage: fl.flip(1, folded_edge_convention=RIGHT)
+ sage: fl = FlatVeeringTriangulation("(0, 1, 2)", (13, 21, 8), (8, 3, 5), mutable=True)
+ sage: fl.flip(1)
sage: fl
- FlatVeeringTriangulation(Triangulation("(0,2,1)"), [(13, 8), (-5, -13), (-8, 5)])
- sage: fl = FlatVeeringTriangulation(T, [(13,8), (-21, -3), (8,-5)], mutable=True)
- sage: fl.flip(1, folded_edge_convention=LEFT)
- sage: fl
- FlatVeeringTriangulation(Triangulation("(0,2,1)"), [(-13, -8), (5, 13), (8, -5)])
+ FlatVeeringTriangulation("(0,2,1)", "RRB", (13, 5, 8), (8, 13, 5))
"""
- if not self._mutable:
- raise ValueError("immutable flat veering triangulation; use a mutable copy instead")
+ if check:
+ if not self._mutable:
+ raise ValueError("immutable flat veering triangulation; use a mutable copy instead")
+
+ e = self._check_edge(e)
+ if not self.is_forward_flippable(e):
+ raise ValueError("invalid edge e={} for forward flip".format(e))
+
+ h = 2 * e
+ H = self._ep(h)
+
+ # x<----------x
+ # | a ^^
+ # | / |
+ # | / |
+ # | / |
+ # |b e/ d|
+ # | / |
+ # | / |
+ # | / |
+ # | / |
+ # | / |
+ # v/ c |
+ # x---------->x
+ a, b, c, d = self.square_about_half_edge(h)
+ ea = a // 2
+ eb = b // 2
+ ec = c // 2
+ ed = d // 2
+
+ assert self._x[e] == self._x[ea] + self._x[eb] == self._x[ec] + self._x[ed]
+ assert self._y[e] == abs(self._y[ea] - self._y[eb]) == abs(self._y[ec] - self._y[ed])
+
+ if self._x[ea] > self._x[ed]:
+ self._x[e] = self._x[ea] - self._x[ed]
+ if col is not None:
+ assert col == BLUE
+ col = BLUE
+ elif self._x[ea] < self._x[ed]:
+ self._x[e] = self._x[ed] - self._x[ea]
+ if col is not None:
+ assert col == RED
+ col = RED
+ else:
+ # equality
+ self._x[e] = self._x[ea] - self._x[ed]
+ if col is not None:
+ assert col == GREEN
+ col = GREEN
- if not self.is_forward_flippable(e):
- raise ValueError("invalid edge")
-
- E = self._ep[e]
- if e == E:
- # folded edge: two possible choices
- #
- # / | | \
- # /b | | \
- # / | |e a\
- # / | | \
- # ------------- | |
- # \ e / \ | or | /
- # \a / -> \a | | /
- # \ / \ e| | /
- # \ b/ \ | | b/
- # \ / \ | | /
- # LEFT RIGHT
- a = self._fp[e]
- b = self._fp[a]
-
- if folded_edge_convention == RIGHT:
- self._holonomies[a] = -self._holonomies[a]
- elif folded_edge_convention == LEFT:
- self._holonomies[b] = -self._holonomies[b]
- else:
- raise ValueError('folded_edge_convention must be RIGHT (={}) or LEFT (={})'.format(RIGHT, LEFT))
+ self._y[e] = self._y[ea] + self._y[ed]
- self._holonomies[e] = -(self._holonomies[a] + self._holonomies[b])
- Triangulation.flip(self, e)
+ self._constellation_class.flip(self, e, col, check=True)
- else:
- if self._holonomies[e] == self._holonomies[E]:
- # Make it so that e is well oriented
- self.triangle_upside_down(e)
- assert self._holonomies[e] == -self._holonomies[E]
+ if check:
+ self._check()
+
+ def flip_back(self, e, col=None, check=True):
+ r"""
+ Flip back the edge ``e``.
+
+ EXAMPLES::
- a = self._fp[e]
- b = self._fp[a]
- c = self._fp[E]
- d = self._fp[c]
+ sage: from veerer import Triangulation, FlatVeeringTriangulation, RIGHT, LEFT
+ sage: fl = FlatVeeringTriangulation("(0,1,4)(~0,3,2)(~1,5,6)", (2, 27, 20, 22, 25, 61, 34), (197, 67, 118, 79, 130, 31, 36), mutable=True)
+ sage: fl.flip_back(0)
+ sage: fl.flip_back(4)
+ sage: fl.flip_back(2)
+ sage: fl
+ FlatVeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)", "BRRRRRB", (47, 27, 74, 22, 69, 61, 34), (51, 67, 16, 79, 28, 31, 36))
+ """
+ if check:
+ if not self._mutable:
+ raise ValueError("immutable flat veering triangulation; use a mutable copy instead")
+
+ e = self._check_edge(e)
+ if not self.is_backward_flippable(e):
+ raise ValueError("invalid edge e={} for backward flip".format(e))
+
+ h = 2 * e
+ H = self._ep(h)
+ a, b, c, d = self.square_about_half_edge(h)
+ ea = a // 2
+ eb = b // 2
+ ec = c // 2
+ ed = d // 2
+
+ assert self._y[e] == self._y[ea] + self._y[eb] == self._y[ec] + self._y[ed]
+ assert self._x[e] == abs(self._x[ea] - self._x[eb]) == abs(self._x[ec] - self._x[ed])
+
+ if self._y[ea] > self._y[ed]:
+ self._y[e] = self._y[ea] - self._y[ed]
+ if col is not None:
+ assert col == RED
+ col = RED
+ elif self._y[ea] < self._y[ed]:
+ self._y[e] = self._y[ed] - self._y[ea]
+ if col is not None:
+ assert col == BLUE
+ col = BLUE
+ else:
+ # equality
+ self._y[e] = self._y[ea] - self._y[ed]
+ if col is not None:
+ assert col == PURPLE
+ col = PURPLE
- assert self._holonomies[d] + self._holonomies[a] + self._holonomies[b] + self._holonomies[c] == 0
+ self._x[e] = self._x[ea] + self._x[ed]
- Triangulation.flip(self, e)
- self._holonomies[e] = self._holonomies[d] + self._holonomies[a]
- self._holonomies[E] = -self._holonomies[e]
+ self._constellation_class.flip_back(self, e, col, check=False)
+ if check:
+ self._check
- self._check()
+ def _extra_relabelling(self, p):
+ perm_on_edge_list(p, self._x)
+ perm_on_edge_list(p, self._y)
def xy_scaling(self, a, b):
r"""
@@ -547,14 +607,14 @@ def xy_scaling(self, a, b):
EXAMPLES::
sage: from veerer import Triangulation, FlatVeeringTriangulation
- sage: T = Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- sage: fl = FlatVeeringTriangulation(T, [(-47, 51), (-27, -67), (74, 16), (22, 79), (-69, -28), (-61, -31), (34, -36)], mutable=True)
+ sage: fl = FlatVeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)", (47, 27, 74, 22, 69, 61, 34), (51, 67, 16, 79, 28, 31, 36), mutable=True)
sage: fl.xy_scaling(2, 1/3)
sage: fl
- FlatVeeringTriangulation(Triangulation("(0,1,2)(3,4,~0)(5,6,~1)"), [(-94, 17), (-54, -67/3), (148, 16/3), (44, 79/3), (-138, -28/3), (-122, -31/3), (68, -12), (54, 67/3), (94, -17)])
+ FlatVeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)", "BRRRRRB", (94, 54, 148, 44, 138, 122, 68), (17, 67/3, 16/3, 79/3, 28/3, 31/3, 12))
"""
if not self._mutable:
raise ValueError("immutable flat veering triangulation; use a mutable copy instead")
- for i, (x, y) in enumerate(self._holonomies):
- self._holonomies[i] = self._V((a*x, b*y))
+ for i in range(self._ne):
+ self._x[i] *= a
+ self._y[i] *= b
diff --git a/veerer/flatsurf_conversion.py b/veerer/flatsurf_conversion.py
index 99fa24cf..c01f2486 100644
--- a/veerer/flatsurf_conversion.py
+++ b/veerer/flatsurf_conversion.py
@@ -26,6 +26,7 @@
from sage.matrix.constructor import matrix
+from .constants import RED, BLUE
from .features import sage_flatsurf_feature, pyflatsurf_feature
from .triangulation import Triangulation
from .veering_triangulation import VeeringTriangulation
@@ -120,11 +121,68 @@ def oriented_slope(a, rotate=1):
raise ValueError("invalid argument rotate={}".format(rotate))
+def flat_structure_to_sage_flatsurf(flat_structure):
+ r"""
+ Construct a sage-flatsurf surface associated to the given veerer flat structure.
+
+ Return a pair ``(surface, mapping_of_half_edges)``.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.flatsurf_conversion import flat_structure_to_sage_flatsurf
+
+ sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "BRR")
+ sage: F = T.flat_structure_min()
+ sage: flat_structure_to_sage_flatsurf(F) # optional - sage_flatsurf
+ (Translation Surface in H_1(0) built from 2 isosceles triangles,
+ [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2), (1, 2)])
+
+ sage: T = VeeringTriangulation("(0,1,2)", "BRR")
+ sage: F = T.flat_structure_min()
+ sage: flat_structure_to_sage_flatsurf(F) # optional - sage_flatsurf
+ (Half-Translation Surface in Q_0(-1^4) built from an isosceles triangle,
+ [(0, 0), None, (0, 1), None, (0, 2), None])
+ """
+ sage_flatsurf_feature.require()
+ import flatsurf
+
+ ep = flat_structure._ep
+ vp = flat_structure._vp
+ bdry = flat_structure._bdry
+ ne = flat_structure.num_edges()
+ vecs = flat_structure.vectors()
+
+ base_ring = flat_structure._x.base_ring()
+ half_edge_to_face = [None] * (2 * ne)
+ half_edge_to_pos = [None] * (2 * ne)
+ S = flatsurf.MutableOrientedSimilaritySurface(base_ring)
+ for i, t in enumerate(flat_structure.triangles()):
+ for j, h in enumerate(t):
+ half_edge_to_face[h] = i
+ half_edge_to_pos[h] = j
+ S.add_polygon(flatsurf.Polygon(edges=[vecs[t[0]], vecs[t[1]], vecs[t[2]]]))
+ for e in range(ne):
+ if not bdry[2 * e] and not bdry[ep(2 * e)]:
+ S.glue((half_edge_to_face[2 * e], half_edge_to_pos[2 * e]),
+ (half_edge_to_face[ep(2 * e)], half_edge_to_pos[ep(2 * e)]))
+ S.set_immutable()
+
+ m = [None] * (2 * ne)
+ for h in range(2 * ne):
+ if vp[h] == -1:
+ continue
+ label = half_edge_to_face[h]
+ e = half_edge_to_pos[h]
+ m[h] = (label, e)
+ return S, m
+
+
def pyflatsurf_surface_to_veerer_veering_triangulation(surface):
r"""
Convert a pyflatsurf surface in a veering triangulation.
- Note that the flatstructure is lost in the process.
+ Note that the flat structure is lost in the process.
EXAMPLES::
@@ -138,16 +196,16 @@ def pyflatsurf_surface_to_veerer_veering_triangulation(surface):
True
sage: S3 = S2.pyflatsurf().codomain().flat_triangulation() # optional - sage_flatsurf pyflatsurf
sage: pyflatsurf_surface_to_veerer_veering_triangulation(S3) # optional - sage_flatsurf pyflatsurf
- (VeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)(7,8,~2)(9,~3,10)(11,~8,~4)(12,13,~5)(14,15,~6)(16,~11,~10)(17,18,~12)(19,20,~13)(~20,~15,~18)(~19,~16,~17)(~14,~7,~9)", "BRRRRRBRBBBRBRRRRRBBR"), [-1, -1, 1, 1, ..., -1, 1, 1])
+ (VeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,7,8)(~3,10,9)(~4,11,~8)(~5,12,13)(~6,14,15)(~7,~9,~14)(~10,16,~11)(~12,17,18)(~13,19,20)(~15,~18,~20)(~16,~17,~19)", "BRRRRRBRBBBRBRRRRRBBR"),
+ [-1, -1, ..., -1])
"""
pyflatsurf_feature.require()
faces = surface.faces()
n = 3 * faces.size()
- ep = array('i', [n - i - 1 for i in range(n)])
fp = array('i', [-1] * n)
- slopes = [None] * n
- x_orientation = [None] * n
+ slopes = [-1] * (n // 2)
+ orientations = [None] * (n // 2)
for face in faces:
a, b, c = face
va = surface.fromHalfEdge(a)
@@ -160,30 +218,38 @@ def pyflatsurf_surface_to_veerer_veering_triangulation(surface):
b = b.id()
c = c.id()
if a < 0 :
- a = n + a
+ a = 2 * (-a - 1) + 1
elif a > 0:
- a = a - 1
+ a = 2 * (a - 1)
if b < 0:
- b = n + b
+ b = 2 * (-b - 1) + 1
elif b > 0:
- b = b - 1
+ b = 2 * (b - 1)
if c < 0:
- c = n + c
+ c = 2 * (-c - 1) + 1
elif c > 0:
- c = c - 1
+ c = 2 * (c - 1)
fp[a] = b
fp[b] = c
fp[c] = a
- x_orientation[a] = sa[0]
- slopes[a] = sa[0] * sa[1]
- x_orientation[b] = sb[0]
- slopes[b] = sb[0] * sb[1]
- x_orientation[c] = sc[0]
- slopes[c] = sc[0] * sc[1]
+ if a % 2:
+ orientations[a // 2] = -sa[0]
+ else:
+ orientations[a // 2] = sa[0]
+ slopes[a // 2] = sa[0] * sa[1]
+ if b % 2:
+ orientations[b // 2] = -sb[0]
+ else:
+ orientations[b // 2] = sb[0]
+ slopes[b // 2] = sb[0] * sb[1]
+ if c % 2:
+ orientations[c // 2] = -sc[0]
+ else:
+ orientations[c // 2] = sc[0]
+ slopes[c // 2] = sc[0] * sc[1]
- colors = "".join("R" if x == 1 else "B" for x in slopes)
- t = Triangulation.from_permutations(None, ep, fp, (array('i', [0] * n),))
- return VeeringTriangulation(t, colors), x_orientation
+ colouring = array('i', [RED if x == 1 else BLUE for x in slopes])
+ return VeeringTriangulation.from_permutations(None, fp, (array('i', [0] * n),), (colouring,)), orientations
def sage_flatsurf_orbit_closure_to_veerer_linear_family(orbit_closure):
@@ -207,11 +273,11 @@ def sage_flatsurf_orbit_closure_to_veerer_linear_family(orbit_closure):
sage: F.base_ring() # optional - sage_flatsurf pyflatsurf
Number Field in c0 with defining polynomial x^2 - x - 1 with c0 = 1.618033988749895?
sage: F # optional - sage_flatsurf pyflatsurf
- VeeringTriangulationLinearFamily("(0,1,2)(3,4,~0)(5,6,~1)(7,8,~2)(9,~3,10)(11,~8,~4)(12,13,~5)(14,15,~6)(16,~11,~10)(17,18,~12)(19,20,~13)(~20,~15,~18)(~19,~16,~17)(~14,~7,~9)", "BRRRRRBRBBBRBRRRRRBBR", [(1, 0, 1, 0, 1, c0, c0, 0, -1, -c0, -c0, 0, c0, 0, c0, 2*c0, c0, 0, c0, -c0, c0), (0, 1, 1, 0, 0, c0, c0 - 1, 0, -1, -1, -1, -1, 1, c0 - 1, 1, c0, 0, -c0 + 1, -c0 + 2, -c0 + 1, 2*c0 - 2), (0, 0, 0, 1, 1, 0, 0, 0, 0, -c0, -c0 + 1, 1, 0, 0, c0, c0, c0, c0 - 1, c0 - 1, -1, 1), (0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, c0 - 1, c0 - 1, c0 - 1, -c0 + 1)])
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,3,4)(~1,5,6)(~2,7,8)(~3,10,9)(~4,11,~8)(~5,12,13)(~6,14,15)(~7,~9,~14)(~10,16,~11)(~12,17,18)(~13,19,20)(~15,~18,~20)(~16,~17,~19)", "BRRRRRBRBBBRBRRRRRBBR", [(1, 0, 1, 0, 1, c0, c0, 0, -1, -c0, -c0, 0, c0, 0, c0, 2*c0, c0, 0, c0, -c0, c0), (0, 1, 1, 0, 0, c0, c0 - 1, 0, -1, -1, -1, -1, 1, c0 - 1, 1, c0, 0, -c0 + 1, -c0 + 2, -c0 + 1, 2*c0 - 2), (0, 0, 0, 1, 1, 0, 0, 0, 0, -c0, -c0 + 1, 1, 0, 0, c0, c0, c0, c0 - 1, c0 - 1, -1, 1), (0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, c0 - 1, c0 - 1, c0 - 1, -c0 + 1)])
"""
sage_flatsurf_feature.require()
- vt, x_orientation = pyflatsurf_surface_to_veerer_veering_triangulation(orbit_closure._surface)
+ vt, orientations = pyflatsurf_surface_to_veerer_veering_triangulation(orbit_closure._surface)
# build generators for the tangent space
phi = orbit_closure.V2.base_ring().coerce_embedding()
@@ -219,7 +285,7 @@ def sage_flatsurf_orbit_closure_to_veerer_linear_family(orbit_closure):
subspace = []
for i in range(orbit_closure._U_rank):
v = orbit_closure.lift(orbit_closure._U[i])
- v = [x_orientation[j] * phi(v[j]) for j in range(len(v))]
+ v = [orientations[j] * phi(v[j]) for j in range(len(v))]
subspace.append(v)
R = orbit_closure.field_of_definition()
diff --git a/veerer/flip_sequence.py b/veerer/flip_sequence.py
index bdb0d2e8..35cd0cf8 100644
--- a/veerer/flip_sequence.py
+++ b/veerer/flip_sequence.py
@@ -22,13 +22,13 @@
sage: a, fs = fp.self_similar_surface(mutable=True)
sage: fs
- FlatVeeringTriangulation(Triangulation("(0,1,2)"), [(1, -1), (a - 3, a), (-a + 2, -a + 1)])
+ FlatVeeringTriangulation("(0,1,2)", "BBR", (1, -a + 3, a - 2), (1, a, a - 1))
sage: fs.flip(0)
sage: fs.flip(2)
sage: fs.relabel(fp._relabelling)
sage: fs.xy_scaling(a, 1/a)
sage: fs
- FlatVeeringTriangulation(Triangulation("(0,1,2)"), [(-1, 1), (-a + 3, -a), (a - 2, a - 1)])
+ FlatVeeringTriangulation("(0,1,2)", "BBR", (1, -a + 3, a - 2), (1, a, a - 1))
The same flip sequence defined in one line::
@@ -56,7 +56,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# ****************************************************************************
+from sage.rings.integer_ring import ZZ
+from sage.modules.free_module_element import vector
+
from .constants import colour_from_char, colour_to_char, RED, BLUE, PURPLE, GREEN, HORIZONTAL, VERTICAL
+from .constellation import check_relabelling
from .permutation import perm_init, perm_check, perm_id, perm_is_one, perm_preimage, perm_invert, perm_cycle_string, perm_compose, perm_pow, perm_conjugate
from .veering_triangulation import VeeringTriangulation
@@ -76,19 +80,17 @@ class VeeringFlipSequence(object):
sage: from veerer import VeeringTriangulation, VeeringFlipSequence, BLUE, RED
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
sage: F = VeeringFlipSequence(T)
- sage: F
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB"), "", "(0)(1)(2)(~2)(~1)(~0)")
sage: F.append_flip(1, RED)
sage: F.append_flip(0, RED)
sage: F
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB"), "1R 0R", "(0)(1)(2)(~2)(~1)(~0)")
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB"), "1R 0R", "()")
The flips can also be specified in the input as a string or as a list::
sage: VeeringFlipSequence(T, "1R 0R")
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB"), "1R 0R", "(0)(1)(2)(~2)(~1)(~0)")
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB"), "1R 0R", "()")
sage: VeeringFlipSequence(T, [(1, RED), (0, RED)])
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB"), "1R 0R", "(0)(1)(2)(~2)(~1)(~0)")
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB"), "1R 0R", "()")
"""
def __init__(self, start, sequence=None, relabelling=None, reduced=None):
if not isinstance(start, VeeringTriangulation):
@@ -104,7 +106,7 @@ def __init__(self, start, sequence=None, relabelling=None, reduced=None):
if reduced:
self._start.forgot_forward_flippable_colour()
self._end = self._start.copy(mutable=True)
- self._relabelling = perm_id(self._start._n)
+ self._relabelling = perm_id(2 * self._start._ne)
self._flips = [] # list of triples (e, col_after, col_before)
if sequence is not None:
@@ -140,17 +142,17 @@ def __repr__(self):
sage: T = VeeringTriangulation("(0,1,2)(~1,~2,~0)", "RRB")
sage: VeeringFlipSequence(T, "1R 0R")
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB"), "1R 0R", "(0)(1)(2)(~2)(~1)(~0)")
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB"), "1R 0R", "()")
sage: VeeringFlipSequence(T, "1R 0R", reduced=True)
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RPB"), "1R 0R", "(0)(1)(2)(~2)(~1)(~0)")
- sage: VeeringFlipSequence(T, "1R 0R", relabelling="(0,5)(1,3)", reduced=True)
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RPB"), "1R 0R", "(0,~0)(1,~2)(2,~1)")
- sage: VeeringFlipSequence(T, "1R 0R", relabelling="(0,5)(1,3)", reduced=True)
- VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RPB"), "1R 0R", "(0,~0)(1,~2)(2,~1)")
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RPB"), "1R 0R", "()")
+ sage: VeeringFlipSequence(T, "1R 0R", relabelling="(0,~0)(1,~2)", reduced=True)
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RPB"), "1R 0R", "(0,~0)(1,~2)(~1,2)")
+ sage: VeeringFlipSequence(T, "1R 0R", relabelling="(0,~0)(1,~2)", reduced=True)
+ VeeringFlipSequence(VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RPB"), "1R 0R", "(0,~0)(1,~2)(~1,2)")
"""
args = [repr(self._start)]
args.append("\"%s\"" % flip_sequence_to_string(self.flips()))
- args.append("\"%s\"" % perm_cycle_string(self._relabelling, self._end._n, involution=self._end._ep))
+ args.append("\"%s\"" % perm_cycle_string(self._relabelling, singletons=False, n=2 * self._end._ne, edge_like=True))
return "VeeringFlipSequence({})".format(", ".join(args))
# properties
@@ -226,16 +228,14 @@ def end_colouring(self):
"""
ne = self._end.num_edges()
ep = self._end._ep
- colours = self._end._colouring[:ne]
+ colours = self._end._colouring[:]
undetermined = set(i for i in range(ne) if colours[i] == PURPLE)
# run backward through flipped edges
i = len(self._flips) - 1
while i >= 0 and undetermined:
e, col, oldcol = self._flips[i]
- e = self._relabelling[e]
- if e >= ne:
- e = ep[e]
+ e = self._relabelling[2 * e] // 2
if e in undetermined:
undetermined.remove(e)
colours[e] = col
@@ -243,16 +243,17 @@ def end_colouring(self):
# for unflipped edges, look at colours of the initial triangulation
for e in undetermined:
- re = perm_preimage(self._relabelling, e)
+ re = perm_preimage(self._relabelling, 2 * e) // 2
col = self._start._colouring[re]
if col != PURPLE:
- if re >= ne:
- re = ep[re]
colours[e] = col
return colours
def coloured_start(self):
+ r"""
+ Return the colouring of the start of the flip sequence imposed by the choice of flips.
+ """
V = self._start.copy()
ne = V.num_edges()
if any(V._colouring[e] == PURPLE for e in range(ne)):
@@ -275,11 +276,18 @@ def inverse(self):
sage: V = VeeringTriangulation("(0,6,5)(1,2,~6)(3,4,~5)", "BPBBRPR")
sage: B = VeeringFlipSequence(V, "1B", "(1,2)")
sage: R = VeeringFlipSequence(V, "1R 5R", "(0,2,3)(1,4)(5,6)")
+ sage: B * R
+ VeeringFlipSequence(VeeringTriangulation("(0,6,5)(1,2,~6)(3,4,~5)", "BPBBRPR"), "1B 2R 5R", "(0,2,4,1,3)(~0,~2,~4,~1,~3)(5,6)(~5,~6)")
sage: (B * R).inverse()
- VeeringFlipSequence(VeeringTriangulation("(0,6,5)(1,2,~6)(3,4,~5)", "RBRRPBP"), "6B 4R 3B", "(0,3,1,4,2)(5,6,~5,~6)")
+ VeeringFlipSequence(VeeringTriangulation("(0,6,5)(1,2,~6)(3,4,~5)", "RBRRPBP"), "6B 4R 3B", "(0,3,1,4,2)(~0,~3,~1,~4,~2)(5,6,~5,~6)")
+
+ sage: (B * R).is_pseudo_anosov()
+ True
+ sage: (B * R).inverse().is_pseudo_anosov()
+ True
"""
start = self._start
- reduced = any(start._colouring[e] == PURPLE for e in range(start.num_edges()))
+ reduced = any(start._colouring[e] == PURPLE for e in range(start._ne))
if reduced:
# determine the coloured flip sequence
coloured_flips = []
@@ -298,22 +306,29 @@ def inverse(self):
V = self._end.copy()
assert all(oldcol == BLUE or oldcol == RED for (_, _, oldcol) in coloured_flips)
- inverse_flips = [(self._relabelling[e], BLUE if oldcol == RED else RED) for (e, _, oldcol) in reversed(coloured_flips)]
+ inverse_flips = [(self._relabelling[2 * e] // 2, BLUE if oldcol == RED else RED) for (e, _, oldcol) in reversed(coloured_flips)]
# NOTE: the relabelling might need some conjugation by edge flip
# (more precisely, the edge flipped an odd number of times are
- # flipped)
+ # swapped)
+ vp = self._start._vp
ep = self._start._ep
ne = self._start.num_edges()
- c = perm_id(self._start._n)
- for i,_ in inverse_flips:
- c[i], c[ep[i]] = c[ep[i]], c[i]
- r = perm_invert(self._relabelling, self._start._n)
- r = perm_compose(c, r, self._start._n)
+ c = perm_id(2 * self._start._ne)
+ for e, _ in inverse_flips:
+ if vp[2 * e + 1] != -1:
+ c[2 * e], c[2 * e + 1] = c[2 * e + 1], c[2 * e]
+ r = perm_invert(self._relabelling, 2 * self._start._ne)
+ r = perm_compose(c, r, 2 * self._start._ne)
V.rotate()
F = VeeringFlipSequence(V, inverse_flips, r, reduced=reduced)
+ # NOTE: it is not necessarily the case that F._end and self._start
+ # coincide up to colouring (some edges might have been swapped)
+ assert F._start._vp == self._end._vp, (F._start, self._end)
+ assert (self._start == self._end) == (F._start == F._end), (self, self._start, self._end, F._start, F._end)
+
# TODO: remove this expensive check
F._check()
@@ -324,7 +339,7 @@ def matrix_inverse(self, twist=True):
from sage.matrix.special import identity_matrix
m = identity_matrix(ZZ, self._start.num_edges())
V = self._start.copy()
- V.relabel_homological_action(perm_invert(self._relabelling, self._start._n), m, twist)
+ V.relabel_homological_action(perm_invert(self._relabelling, 2 * self._start._ne), m, twist)
for e, col, oldcol in reversed(self._flips):
pass
@@ -380,30 +395,28 @@ def unflipped_edges(self):
"""
if self._start != self._end:
raise TypeError
- n = self._start.num_edges()
+ ne = self._start.num_edges()
ep = self._start._ep
flipped = set(x[0] for x in self._flips)
- if len(flipped) == n:
+ if len(flipped) == ne:
return set()
new = list(flipped)
very_new = []
modified = True
r = self._relabelling
- while len(flipped) < n and modified:
+ while len(flipped) < ne and modified:
modified = False
for e in new:
- e = r[e]
- if e >= n:
- e = ep[e]
+ e = r[2 * e] // 2
if e not in flipped:
modified = True
flipped.add(e)
very_new.append(e)
new, very_new = very_new, new
- del very_new[:]
+ very_new.clear()
- E = set(range(n))
+ E = set(range(ne))
assert flipped.issubset(E)
E.difference_update(flipped)
return E
@@ -472,7 +485,7 @@ def flat_structure_middle(self, mutable=False, check=True):
sage: from veerer import VeeringTriangulation, VeeringFlipSequence, BLUE, RED
sage: V = VeeringTriangulation("(0,3,4)(1,~3,5)(2,6,~4)", "RBRBRRB")
sage: VeeringFlipSequence(V, "0B 1B").flat_structure_middle()
- FlatVeeringTriangulation(Triangulation("(0,3,4)(1,~3,5)(2,6,~4)"), [(6, 2), (6, -2), (3, 3), (-4, 4), (-2, -6), (-2, -2), (-1, 3), (-2, -6), (-4, 4)])
+ FlatVeeringTriangulation("(0,3,4)(1,~3,5)(2,6,~4)", "RBRBRRB", (6, 6, 3, 4, 2, 2, 1), (2, 2, 3, 4, 6, 2, 3))
sage: fs = VeeringFlipSequence(V, "0B 1R 2B 5R 6B", "(3,4)(1,5,6)")
sage: fl = fs.flat_structure_middle(mutable=True)
@@ -491,16 +504,15 @@ def flat_structure_middle(self, mutable=False, check=True):
# TODO: make this function support various polytope backends
n = self._start.num_edges()
- PH = self.train_track_polytope(HORIZONTAL, backend='ppl')._cone
PV = self.train_track_polytope(VERTICAL, backend='ppl')._cone
+ x = [g.coefficients() for g in PV.generators() if g.is_ray()]
+ x = vector(ZZ, [sum(v[i] for v in x) for i in range(n)])
- # pick sum of rays
- VH = [g.coefficients() for g in PH.generators() if g.is_ray()]
- VH = [sum(v[i] for v in VH) for i in range(n)]
- VV = [g.coefficients() for g in PV.generators() if g.is_ray()]
- VV = [sum(v[i] for v in VV) for i in range(n)]
+ PH = self.train_track_polytope(HORIZONTAL, backend='ppl')._cone
+ y = [g.coefficients() for g in PH.generators() if g.is_ray()]
+ y = vector(ZZ, [sum(v[i] for v in y) for i in range(n)])
- return self._start._flat_structure_from_train_track_lengths(VH, VV, mutable=mutable, check=check)
+ return self._start.flat_structure(x, y, mutable=mutable, check=check)
def self_similar_widths_and_heights(self):
r"""
@@ -574,43 +586,43 @@ def self_similar_surface(self, mutable=False, check=True):
sage: f = B0*R0*B1*R1
sage: f.self_similar_surface()
(a,
- FlatVeeringTriangulation(Triangulation("(0,~2,1)(2,~8,~3)(3,~7,~4)(4,6,~5)(5,8,~6)(7,~1,~0)"), [(1, 1), (a^3 - 7*a^2 + 13*a - 7, -a), ..., (-a^3 + 7*a^2 - 13*a + 7, a), (-1, -1)]))
+ FlatVeeringTriangulation("(0,~2,1)(~0,7,~1)...(5,8,~6)", "RRBRRBRBR", (1, -a^3 + 7*a^2 - 13*a + 7, ..., -4*a^3 + 27*a^2 - 45*a + 16), (1, a, ..., a^2 - 3*a + 1)))
sage: f = B1*B0*R0*B1*R1*R1*R1*R0*B1*R0*B1
sage: f.self_similar_surface()
(a,
- FlatVeeringTriangulation(Triangulation("(0,~2,1)(2,~8,~3)(3,~7,~4)(4,6,~5)(5,8,~6)(7,~1,~0)"), [(1, 1), ..., (-1/183*a^3 + 8/61*a^2 - 42/61*a + 274/183, 4/183*a^3 - 30/61*a^2 + 119/61*a + 8/183), (-1, -1)]))
+ FlatVeeringTriangulation("(0,~2,1)(~0,7,~1)...(5,8,~6)", "RRBRRBBBR", (1, -1/183*a^3 + 8/61*a^2 - 42/61*a + 274/183, ..., -89/183*a^3 + 651/61*a^2 - 2274/61*a + 1511/183), (1, 4/183*a^3 - 30/61*a^2 + 119/61*a + 8/183, ..., -10/183*a^3 + 75/61*a^2 - 267/61*a + 163/183)))
"""
r, w, h = self.self_similar_widths_and_heights()
- return r, self.coloured_start()._flat_structure_from_train_track_lengths(h, w, base_ring=r.parent(), mutable=mutable, check=True)
+ return r, self.coloured_start().flat_structure(w, h, mutable=mutable, check=check)
# change
- def append_flip(self, e, col):
+ def append_flip(self, e, col, check=True):
r"""
Append the flip ``(e, col)`` to this flip sequence.
"""
+ if check:
+ e = self._start._check_edge(e)
+ if col != BLUE and col != RED:
+ raise ValueError("col must be BLUE or RED")
+
ep = self._end._ep
oldcol = self._end._colouring[e]
- E = ep[e]
- if E < e:
- e = E
self._end.flip(e, col)
- if self._relabelling[e] != e:
+ if self._relabelling[2 * e] != 2 * e:
# push the flip to the left of relabelling
- e = perm_preimage(self._relabelling, e)
- E = ep[e]
- if E < e:
- e = E
+ e = perm_preimage(self._relabelling, 2 * e) // 2
self._flips.append((e, col, oldcol))
def swap(self, e):
r"""
Swap the orientation of the edge ``e`` by modifying the relabelling of this flip sequence.
"""
- E = self._end._ep[e]
+ h = 2 * e
+ H = self._end._ep(h)
self._end.swap(e)
- self._relabelling[e] = E
- self._relabelling[E] = e
+ self._relabelling[h] = H
+ self._relabelling[H] = h
# TODO: remove check
self._check()
@@ -629,21 +641,18 @@ def find_closure(self):
True
sage: fp.append_flip(2, BLUE)
sage: fp.find_closure()
- array('i', [2, 0, 1])
+ array('i', [4, 5, 0, 1, 2, 3])
"""
ans, r = self._end.is_isomorphic_to(self._start, certificate=True)
return r if ans else None
- def append_relabelling(self, r):
+ def append_relabelling(self, r, check=True):
r"""
Append the relabelling ``r`` to this flip sequence.
"""
end = self._end
- if not perm_check(r, end._n):
- r = perm_init(r, end._n, end._ep)
- if not perm_check(r, end._n):
- raise ValueError('invalid relabelling permutation')
-
+ if check:
+ r = check_relabelling(r, end._ne)
end.relabel(r)
self._relabelling = perm_compose(self._relabelling, r)
@@ -658,18 +667,18 @@ def __imul__(self, other):
sage: assert F.is_closed()
sage: F *= F
sage: F
- VeeringFlipSequence(VeeringTriangulation("(0,3,4)(1,~3,5)(2,6,~4)", "PPPBRRB"), "2B 6B", "(0)(1)(2)(3)(4)(5)(6)(~4)(~3)")
+ VeeringFlipSequence(VeeringTriangulation("(0,3,4)(1,~3,5)(2,6,~4)", "PPPBRRB"), "2B 6B", "()")
"""
if type(self) != type(other):
raise TypeError
if self._end != other._start:
raise ValueError("composition undefined")
- n = self._start._n
+ n = 2 * self._start._ne
ne = self._start.num_edges()
r = perm_invert(self._relabelling, n)
ep = self._start._ep
- self._flips.extend([((r[e] if r[e] < ne else ep[r[e]]), col, oldcol) for e, col, oldcol in other._flips])
+ self._flips.extend([(r[2 * e] // 2, col, oldcol) for e, col, oldcol in other._flips])
self._end = other._end.copy()
self._relabelling = perm_compose(self._relabelling, other._relabelling)
@@ -688,6 +697,13 @@ def __mul__(self, other):
sage: F3 = VeeringFlipSequence(V3, "3B", "(0,1)")
sage: (F2 * F3) * (F2 * F3) == (F2 * (F3 * F2)) * F3 == F2 * ((F3 * F2) * F3)
True
+
+ sage: Vc = VeeringTriangulation("(0,~5,4)(3,5,6)(1,2,~6)", "PPBPRBR")
+ sage: CR5 = VeeringFlipSequence(Vc, "1B", "(1,2)")
+ sage: CL5 = VeeringFlipSequence(Vc, "0R", "(0,4)")
+ sage: L32 = VeeringFlipSequence(Vc, "1R 3R 6R", "(1,3)(6,~6)")
+ sage: (L32 * CR5) * CL5 == L32 * (CR5 * CL5)
+ True
"""
res = self.copy()
res *= other
@@ -709,12 +725,10 @@ def __pow__(self, k):
res._relabelling = perm_pow(res._relabelling, k)
m = len(res._flips)
- r = perm_invert(self._relabelling, self._start._n)
- ne = self._start.num_edges()
- ep = self._start._ep
+ r = perm_invert(self._relabelling, 2 * self._start._ne)
for _ in range(m * (k-1)):
e, col, oldcol = res._flips[-m]
- res._flips.append(((r[e] if r[e] < ne else ep[r[e]]), col, oldcol))
+ res._flips.append(((r[2 * e] // 2), col, oldcol))
# TODO: remove this expensive check
res._check()
diff --git a/veerer/framing_group.py b/veerer/framing_group.py
new file mode 100644
index 00000000..6a36bd27
--- /dev/null
+++ b/veerer/framing_group.py
@@ -0,0 +1,787 @@
+r"""
+Framing group
+"""
+# ****************************************************************************
+# This file is part of veerer
+#
+# Copyright (C) 2024 Vincent Delecroix
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# ****************************************************************************
+
+import numbers
+import array
+import itertools
+
+from sage.misc.cachefunc import cached_method
+from sage.misc.misc_c import prod
+from sage.categories.groups import Groups
+from sage.structure.richcmp import op_LT, op_LE, op_EQ, op_NE, op_GT, op_GE, rich_to_bool
+from sage.structure.element import Element, parent
+from sage.structure.parent import Parent
+from sage.structure.unique_representation import UniqueRepresentation
+from sage.rings.integer_ring import ZZ
+from sage.groups.libgap_wrapper import ElementLibGAP, ParentLibGAP
+from sage.libs.gap.libgap import libgap
+from sage.arith.functions import lcm
+from sage.arith.misc import gcd
+from sage.functions.other import factorial
+from sage.groups.perm_gps.permgroup import PermutationGroup
+from sage.groups.perm_gps.permgroup_named import SymmetricGroup
+
+from veerer.permutation import perm_cycles, perm_id, perm_init, str_to_cycles_and_data, perm_compose, perm_check, perm_invert, perm_is_one
+
+
+def runs(l):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import runs
+ sage: list(runs([0, 0, 1, 1, 1, 2, 1]))
+ [(0, 2), (1, 3), (2, 1), (1, 1)]
+ """
+ if isinstance(l, (tuple, list)):
+ i = 0
+ while i < len(l):
+ j0 = i
+ j = i + 1
+ while j < len(l) and l[j0] == l[j]:
+ j += 1
+ yield (l[j0], j - j0)
+ i = j
+ elif isinstance(l, dict):
+ for i in sorted(l, reverse=True):
+ yield i, l[i]
+ else:
+ raise TypeError
+
+
+# TODO: in order to handle multiscale monodromy, it would be convenient to be able to consider
+# any (permutation) group obtained by the following operations
+# basic blocks: cyclic groups Ck
+# direct products: G1 x G2
+# wreath products: G wreath Sym(n)
+class FramingGroupElement(Element):
+ r"""
+ Holds two attributes ``p`` (a permutation) and ``r`` (an array)
+ """
+ def __init__(self, parent, p, r=None, check=True):
+ self._p = p # list of length n (permutation for each factor)
+ if r is None:
+ self._r = array.array('i', [0] * parent._n)
+ else:
+ self._r = r
+
+ Element.__init__(self, parent)
+
+ if check:
+ # TODO: check that the permutation preserves the blocks
+ self._check()
+
+ def _check(self):
+ P = self.parent()
+
+ if not perm_check(self._p, P._n):
+ raise ValueError("wrong p={}".format(self._p))
+
+ if not isinstance(self._r, array.array) or self._r.typecode != 'i' or len(self._r) != P._n:
+ raise ValueError("wrong r={}".format(self._r))
+
+ # check that we stay in blocks
+ j = 0
+ block_start = 0
+ block_end = P._multiplicities[0]
+ A = P._angles[0]
+ for x, (y, a) in enumerate(zip(self._p, self._r)):
+ if x == block_end:
+ j += 1
+ if j < P._n:
+ block_start, block_end = block_end, block_end + P._multiplicities[j]
+ assert block_start <= x < block_end
+ if y < block_start or y >= block_end:
+ raise ValueError("permutation does not preserve blocks")
+ if a < 0 or a >= P._angles_flat[x]:
+ raise ValueError("angle out of range a={} at x={}".format(a, x))
+
+ def __call__(self, i, a=None):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([1,3],[2,3])
+ sage: g = G("(0,1)(2:1,3:2,4:0)")
+ sage: g(0, 0)
+ (1, 0)
+ sage: g(2, 0)
+ (3, 1)
+ sage: g(2, 1)
+ (3, 2)
+ sage: g(3, 0)
+ (4, 2)
+ """
+ if not isinstance(i, numbers.Integral):
+ raise TypeError
+ P = self.parent()
+ i = int(i)
+ if i < 0 or i >= P._n:
+ raise ValueError("index out of range")
+ if a is not None:
+ if not isinstance(a, numbers.Integral):
+ raise TypeError
+ return (self._p[i], (a + self._r[i]) % P._angles_flat[i])
+ return self._p[i]
+
+ def _richcmp_(self, other, op):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([1,3],[2,3])
+ sage: for i, g in enumerate(G):
+ ....: for j, h in enumerate(G):
+ ....: assert (g == h) == (i == j)
+ ....: assert (g != h) == (i != j)
+ """
+ if op == op_EQ:
+ return self._p == other._p and self._r == other._r
+ elif op == op_NE:
+ return self._p != other._p or self._r != other._r
+ raise NotImplementedError
+
+ def __hash__(self):
+ x = 140737488617563
+ x = ((x ^ hash(self._p.tobytes())) * 2147483693) + 82520 + len(self._p)
+ x = ((x ^ hash(self._r.tobytes())) * 2147483693) + 82520 + len(self._r)
+ return x
+
+ def is_one(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([0,1,2,3], [2,1,3,2])
+ sage: [g for g in G if g.is_one()]
+ [()]
+ """
+ P = self.parent()
+ return perm_is_one(self._p, P._n) and not any(self._r)
+
+ def __invert__(self):
+ P = self.parent()
+ p = perm_invert(self._p, P._n)
+ r = array.array('i', [-1] * P._n)
+ for i in range(P._n):
+ r[i] = (-self._r[p[i]]) % P._angles_flat[i]
+ return P.element_class(P, p, r)
+
+ def _mul_(self, other):
+ r"""
+ TESTS::
+
+ sage: from veerer.framing_group import FramingGroup
+
+ sage: G = FramingGroup([0,1,2,3], [2,1,3,2])
+ sage: a = G("(3:0, 4:0, 5:0)")
+ sage: b = G("(6:0, 7:0)")
+ sage: c = G("(3:1,4:1,5:1)(6:2,7:1)")
+ sage: d = G("(0:0,1:0)(3:1)(4:1)")
+ sage: assert (a * b) * (c * d) == (a * (b * c)) * d == a * ((b * c) * d) == ((a * b) * c) * d == (a * (b * (c * d))) == (a * b) * (c * d)
+
+ sage: G = FramingGroup([0,1,2,3], [2,1,3,2])
+ sage: gens = G.gens()
+ sage: todo = list(gens)
+ sage: elts = set(todo)
+ sage: while todo:
+ ....: g = todo.pop()
+ ....: for h in gens:
+ ....: hh = h * g
+ ....: if hh not in elts:
+ ....: elts.add(hh)
+ ....: todo.append(hh)
+ sage: assert len(elts) == G.cardinality() == 1728
+ """
+ P = self.parent()
+ p = perm_compose(self._p, other._p, P._n)
+ r = array.array('i', [-1] * P._n)
+ for i in range(P._n):
+ r[i] = (self._r[i] + other._r[self._p[i]]) % P._angles_flat[i]
+ return P.element_class(P, p, r)
+
+ def permutation(self):
+ r"""
+ Return the permutation representative.
+
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([3,4], [4,4])
+ sage: g = G("(0:0,1:1,3:0)(4:2,6:1,5:1,7:1)")
+ sage: g.permutation()
+ array('i', [3, 4, 5, 10, 11, 9, 6, 7, 8, 0, 1, 2, 22, 23, 20, 21, 25, 26, 27, 24, 17, 18, 19, 16, 13, 14, 15, 12])
+ sage: G.from_permutation(g.permutation()) == g
+ True
+
+ sage: g = G("(0:3)(1:2,2:0)(5:0,7:1)")
+ sage: G.from_permutation(g.permutation()) == g
+ True
+ """
+ P = self.parent()
+ p = array.array('i', [-1] * sum(a * m for a, m in zip(P._angles, P._multiplicities)))
+ i = 0
+ j = 0
+ for a, m in zip(P._angles, P._multiplicities):
+ for ii in range(i, i + m):
+ for jj in range(a):
+ p[j + a * (ii - i) + jj] = j + a * (self._p[ii] - i) + (jj + self._r[ii]) % a
+ i += m
+ j += a * m
+ return p
+
+ def symmetric_group_element(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([3,4], [4,4])
+ sage: g1 = G("(0:0,1:1,3:0)(4:2,6:1,5:1,7:1)")
+ sage: g1.symmetric_group_element()
+ (1,4,11,2,5,12,3,6,10)(13,23,20,25,14,24,17,26,15,21,18,27,16,22,19,28)
+ sage: G.from_symmetric_group_element(g1.symmetric_group_element()) == g1
+ True
+
+ sage: g2 = G("(0:3)(1:2,2:0)(5:0,7:1)")
+ sage: g2.symmetric_group_element()
+ (4,9,6,8,5,7)(17,25,18,26,19,27,20,28)
+ sage: G.from_symmetric_group_element(g2.symmetric_group_element()) == g2
+ True
+
+ sage: assert (~g1).symmetric_group_element() == ~(g1.symmetric_group_element())
+ sage: assert (~g2).symmetric_group_element() == ~(g2.symmetric_group_element())
+ sage: (g1 * g2 * ~g1).symmetric_group_element() == (g1.symmetric_group_element() * g2.symmetric_group_element() * (~g1).symmetric_group_element())
+ True
+ """
+ return self.parent().symmetric_group()([i + 1 for i in self.permutation()])
+
+ def cycle_string(self, singletons=False):
+ s = []
+ for cyc in perm_cycles(self._p, True, self.parent()._n):
+ if singletons or len(cyc) != 1 or self._r[cyc[0]] != 0:
+ c = ", ".join("{}:{}".format(j, self._r[j]) for j in cyc)
+ s.append("(" + c + ")")
+ return "()" if not s else "".join(s)
+
+ def _repr_(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([2], [2])
+ sage: G([1,0])
+ (0:0, 1:0)
+ sage: G([1,0], [1,0])
+ (0:1, 1:0)
+ sage: G([1,0], [0,1])
+ (0:0, 1:1)
+ sage: G([1,0], [1,1])
+ (0:1, 1:1)
+ """
+ return self.cycle_string()
+
+ def order(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: from veerer.permutation import perm_order
+ sage: G = FramingGroup([2], [2])
+ sage: for g in G:
+ ....: print(g, g.order(), perm_order(g.permutation()))
+ () 1 1
+ (1:1) 2 2
+ (0:1) 2 2
+ (0:1)(1:1) 2 2
+ (0:0, 1:0) 2 2
+ (0:0, 1:1) 4 4
+ (0:1, 1:0) 4 4
+ (0:1, 1:1) 2 2
+ """
+ P = self.parent()
+ o = ZZ.one()
+ for cyc in perm_cycles(self._p, True, P._n):
+ a = P._angles_flat[cyc[0]]
+ o = lcm(o, len(cyc) * a // gcd(sum(self._r[j] for j in cyc), a))
+ return o
+
+
+class FramingGroup(Parent, UniqueRepresentation):
+ r"""
+ A product of wreath products of cyclic group.
+
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: FramingGroup([1]).cardinality()
+ 1
+ sage: FramingGroup([1, 1]).cardinality()
+ 2
+ sage: FramingGroup([1, 1, 1]).cardinality()
+ 6
+ sage: FramingGroup([3]).cardinality()
+ 3
+ sage: FramingGroup([2, 2, 2, 2]).cardinality()
+ 384
+ sage: FramingGroup([3] * 4 + [0] * 4).cardinality()
+ 46656
+ sage: factorial(4) * 3**4 * factorial(4)
+ 46656
+ """
+ Element = FramingGroupElement
+
+ @staticmethod
+ def __classcall_private__(cls, angles, multiplicities=None):
+ if multiplicities is None:
+ if isinstance(angles, dict):
+ it = angles.items()
+ elif isinstance(angles, (tuple, list)):
+ if not angles or isinstance(angles[0], (tuple, list)) and len(angles[0]) == 2:
+ it = angles
+ else:
+ it = runs(angles)
+ else:
+ raise TypeError
+ else:
+ it = zip(angles, multiplicities)
+
+ clean_angles = []
+ clean_multiplicities = []
+ for a, m in it:
+ if not isinstance(a, numbers.Integral) or a < 0 or not isinstance(m, numbers.Integral) or m <= 0:
+ raise ValueError
+ clean_angles.append(max(1, int(a)))
+ clean_multiplicities.append(int(m))
+
+ return super().__classcall__(cls, tuple(clean_angles), tuple(clean_multiplicities))
+
+ def __init__(self, angles, multiplicities):
+ self._angles = angles
+ self._multiplicities = multiplicities
+ self._n = sum(self._multiplicities)
+ self._angles_flat = []
+
+ for a, m in zip(self._angles, self._multiplicities):
+ self._angles_flat.extend([a] * m)
+
+ Parent.__init__(self, category=Groups().Finite())
+
+ def _repr_(self):
+ return "FramingGroup({})".format(", ".join("%d^%d" %(a, m) if m > 1 else "%d" % a for a, m in zip(self._angles, self._multiplicities)))
+
+ @cached_method
+ def symmetric_group(self):
+ return SymmetricGroup(sum(a ** m for a, m in zip(self._angles, self._multiplicities)))
+
+ def gens(self):
+ r"""
+ Return a generating set of this framing group.
+
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: FramingGroup([0], [1]).gens()
+ [()]
+ sage: FramingGroup([0], [2]).gens()
+ [(0:0, 1:0)]
+ sage: FramingGroup([0], [3]).gens()
+ [(0:0, 1:0), (0:0, 1:0, 2:0)]
+
+ sage: FramingGroup([1], [1]).gens()
+ [()]
+ sage: FramingGroup([1], [2]).gens()
+ [(0:0, 1:0)]
+ sage: FramingGroup([1], [3]).gens()
+ [(0:0, 1:0), (0:0, 1:0, 2:0)]
+
+ sage: FramingGroup([2], [1]).gens()
+ [(0:1)]
+ sage: FramingGroup([2], [2]).gens()
+ [(0:1), (0:0, 1:0)]
+ sage: FramingGroup([2], [3]).gens()
+ [(0:1), (0:0, 1:0), (0:0, 1:0, 2:0)]
+
+ sage: FramingGroup([0,1,2,3], [2,1,3,2]).gens()
+ [(0:0, 1:0), (3:1), (3:0, 4:0), (3:0, 4:0, 5:0), (6:1), (6:0, 7:0)]
+ """
+ ans = []
+ i = 0
+ for a, m in zip(self._angles, self._multiplicities):
+ if m == 1 and a <= 1:
+ i += 1
+ continue
+
+ if a >= 2:
+ p = perm_id(self._n)
+ r = array.array('i', [0] * i + [1] + [0] * (self._n - i - 1))
+ ans.append(self(p, r))
+
+ if m == 1:
+ i += 1
+ continue
+
+ p = array.array('i', list(range(i)) + [i + 1, i] + list(range(i + 2, self._n)))
+ ans.append(self(p))
+
+ if m >= 3:
+ p = array.array('i', list(range(i)) + list(range(i + 1, i + m)) + [i] + list(range(i + m, self._n)))
+ ans.append(self(p))
+
+ i += m
+
+ return [self.one()] if not ans else ans
+
+ def _element_constructor_(self, p, r=None):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([2, 3], [3, 3])
+ sage: G("(0:1,2:0,1:10)")
+ (0:1, 2:0, 1:0)
+ sage: G([2,1,0,5,4,3], [1,10,3,0,1,0])
+ (0:1, 2:1)(3:0, 5:0)(4:1)
+ """
+ if isinstance(p, str):
+ if r is None:
+ p, r = str_to_cycles_and_data(p)
+ p = perm_init(p, self._n)
+ r = array.array('i', [r.get(i, 0) for i in range(self._n)])
+ for i in range(self._n):
+ r[i] = r[i] % self._angles_flat[i]
+ else:
+ p = perm_init(p, self._n)
+ elif isinstance(p, (tuple, list)):
+ p = perm_init(p, self._n)
+ elif not p or p == 1:
+ return self.one()
+
+ if isinstance(r, (tuple, list)):
+ r = array.array('i', [x % a for x, a in zip(r, self._angles_flat)])
+
+ return self.element_class(self, p, r)
+
+ @cached_method
+ def one(self):
+ return self.element_class(self, perm_id(self._n), array.array('i', [0] * self._n))
+
+ @cached_method
+ def rotation(self):
+ r"""
+ Return the element which corresponds to add +1 in each factor.
+
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: FramingGroup([2, 2]).rotation()
+ (0:1)(1:1)
+ sage: FramingGroup([2, 2]).rotation().order()
+ 2
+
+ sage: FramingGroup([1,2,3], [1,3,1]).rotation()
+ (1:1)(2:1)(3:1)(4:1)
+ sage: FramingGroup([1,2,3], [1,3,1]).rotation().order()
+ 6
+ """
+ return self.element_class(self, perm_id(self._n), array.array('i', [1 % a for a in self._angles_flat]))
+
+ def from_permutation(self, q):
+ r"""
+ Build an element of this framing group from a "flat" permutation.
+ """
+ if not perm_check(q, sum(a * m for a, m in zip(self._angles, self._multiplicities))):
+ raise ValueError("invalid input q")
+ i = 0
+ j = 0
+ p = array.array('i', [-1] * self._n)
+ r = array.array('i', [-1] * self._n)
+ for a, m in zip(self._angles, self._multiplicities):
+ for ii in range(i, i + m):
+ for jj in range(a):
+ im = q[j + a * (ii - i) + jj]
+
+ x = (im - j) % a # (jj + self._r[ii])
+ xx = (x - jj) % a
+ if xx >= a:
+ raise ValueError("invalid")
+ if r[ii] == -1:
+ r[ii] = xx
+ elif r[ii] != xx:
+ raise ValueError("invalid input")
+
+ x = (im - j) // a # (self._p[ii] - i)
+ if x < 0 or x >= m:
+ raise ValueError("invalid input")
+ if p[ii] == -1:
+ p[ii] = i + x
+ elif p[ii] != i + x:
+ raise ValueError("invalid input")
+ i += m
+ j += a * m
+ return self.element_class(self, p, r)
+
+ def from_symmetric_group_element(self, q):
+ S = self.symmetric_group()
+ if q not in S:
+ raise ValueError
+ return self.from_permutation(array.array('i', [i - 1 for i in S(q).domain()]))
+
+ def __iter__(self):
+ r"""
+ TESTS::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: list(FramingGroup([2], [2]))
+ [(), (1:1), (0:1), (0:1)(1:1), (0:0, 1:0), (0:0, 1:1), (0:1, 1:0), (0:1, 1:1)]
+ """
+ i = 0
+ perm_blocks = []
+ for a, m in zip(self._angles, self._multiplicities):
+ perm_blocks.append(itertools.permutations(range(i, i + m)))
+ i += m
+ for p in itertools.product(*perm_blocks):
+ p = array.array('i', sum(p, ()))
+ angle_blocks = []
+ for a, m in zip(self._angles, self._multiplicities):
+ angle_blocks.append(itertools.product(range(a), repeat=m))
+ i += m
+ for r in itertools.product(*angle_blocks):
+ r = array.array('i', sum(r, ()))
+ yield self.element_class(self, p, r, check=True)
+
+ def cardinality(self):
+ r"""
+ TESTS::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([2], [2])
+ sage: libgap(G).Size() == G.cardinality()
+ True
+ sage: G = FramingGroup([0,1,2,3], [1,3,4,1])
+ sage: libgap(G).Size() == G.cardinality()
+ True
+ """
+ return prod(ZZ(m).factorial() * ZZ(a) ** m for a, m in zip(self._angles, self._multiplicities))
+
+ def subgroup(self, gens=None, mutable=False):
+ return FramingSubgroup(self, gens, mutable)
+
+ def _libgap_(self):
+ # the underlying group is a direct product of separatrix permutation
+ # of zeros/poles of fixed degree. For each degree we either consider
+ # * ignore it if there is a single choice
+ # * a symmetric group if there is <= 1 separatrix
+ # * a cyclic group if the multiplicity is one
+ # * a wreath product otherwise
+ try:
+ return self._libgap
+ except AttributeError:
+ pass
+ from sage.libs.gap.libgap import libgap
+ components = []
+ for a, m in zip(self._angles, self._multiplicities):
+ if a == 1 and m == 1:
+ continue
+ if a == 1:
+ components.append(libgap.SymmetricGroup(m))
+ elif m == 1:
+ components.append(libgap.CyclicGroup(a))
+ else:
+ G = libgap.CyclicGroup(a)
+ H = libgap.SymmetricGroup(m)
+ components.append(libgap.WreathProduct(G, H))
+ if not components:
+ self._libgap = libgap.TrivialGroup()
+ else:
+ self._libgap = libgap.DirectProduct(components)
+ return self._libgap
+
+
+class FramingSubgroup(Parent):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([1, 2, 3], [5, 3, 4])
+ sage: g1 = G("(0:1,4:2)(5:0,6:1)(7:2)(9:1,10:1)")
+ sage: g2 = G("(0:0,1:0,2:0,3:0)")
+ sage: H = G.subgroup([g1, g2], mutable=True)
+ sage: H
+ FramingSubgroup([(0:0, 4:0)(5:0, 6:1)(9:1, 10:1), (0:0, 1:0, 2:0, 3:0)])
+ sage: H.index()
+ 7776
+ sage: assert g1 in H and g2 in H and (g1 * g2) in H
+ sage: g3 = G("(5:0,7:0)")
+ sage: H.add_generator(g3)
+ sage: assert (g1 * g3 * g2) in H
+ sage: H.index()
+ 648
+ sage: g4 = G("(8:0,10:0,11:0,9:0)")
+ sage: H.add_generator(g4)
+ sage: assert (g1 * g3 * g4) in H
+ sage: H.index()
+ 54
+ """
+ def __init__(self, ambient_group, gens=None, mutable=False):
+ self._ambient_group = ambient_group
+ self._mutable = True
+ self._generators = []
+ Parent.__init__(self, category=Groups().Finite(), facade=self._ambient_group)
+ if gens is not None:
+ for g in gens:
+ self.add_generator(g)
+ self._mutable = mutable
+
+ def gens(self):
+ r"""
+ Return generators of this framing subgroup.
+ """
+ return tuple(self._generators)
+
+ def _element_constructor_(self, *args, **kwds):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: f = VeeringTriangulationLinearFamily("(0:1)(~0:1,1:1,2:1,~1:1)(~2:1)", "RBR", [(1, 0, 1), (0, 1, 0)])
+ sage: ds_graph = f.delaunay_strebel_graph()
+ sage: G = ds_graph.framing_group()
+ sage: G("(0:1)(1:1)(2:1)")
+ (0:1)(1:1)(2:1)
+ sage: G("(0:1)(1:1)")
+ Traceback (most recent call last):
+ ...
+ ValueError: not an element of this framing group subgroup
+ """
+ g = self._ambient_group(*args, **kwds)
+ if g not in self:
+ raise ValueError("not an element of this framing group subgroup")
+ return g
+
+ def set_immutable(self):
+ self._mutable = False
+
+ def _repr_(self):
+ return "FramingSubgroup({})".format(self._generators)
+
+ def __contains__(self, g):
+ return parent(g) is self._ambient_group and g.symmetric_group_element() in self.permutation_group()
+
+ def __le__(self, other):
+ r"""
+ Test whether self is a subgroup of other
+ """
+ if isinstance(self, FramingSubgroup) and isinstance(other, FramingSubgroup):
+ if self._ambient_group != other._ambient_group:
+ raise TypeError
+
+ elif isinstance(self, FramingGroup):
+ if other._ambient_group != self:
+ raise TypeError
+
+ elif isinstance(other, FramingGroup):
+ if self._ambient_group != other:
+ raise TypeError
+
+ return True
+
+ else:
+ return NotImplemented
+
+ for g in self.gens():
+ if g not in other:
+ return False
+
+ return True
+
+ def __eq__(self, other):
+ if type(self) is type(other) or (isinstance(self, (FramingGroup, FramingSubgroup)) and isinstance(other, (FramingGroup, FramingSubgroup))):
+ return self <= other and other <= self
+
+ return NotImplemented
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __gt__(self, other):
+ return not (self <= other)
+
+ def __lt__(self, other):
+ return (self <= other) and not (other <= self)
+
+ def __ge__(self, other):
+ return other <= self
+
+ def copy(self, mutable=None):
+ if mutable is None:
+ mutable = self._mutable
+ if not mutable and not self._mutable:
+ return self
+ F = FramingSubgroup(self._ambient_group)
+ F._generators = self._generators[:]
+ F._mutable = mutable
+ return F
+
+ def permutation_group(self):
+ try:
+ return self._permutation_group
+ except AttributeError:
+ pass
+
+ permutation_group = PermutationGroup([g.symmetric_group_element() for g in self._generators])
+
+ if not self._mutable:
+ self._permutation_group = permutation_group
+ return permutation_group
+
+ def add_generator(self, g):
+ if not self._mutable:
+ raise ValueError("immutable framing subgroup; use a mutable copy instead")
+ if parent(g) is not self._ambient_group:
+ raise ValueError
+ if g not in self:
+ self._generators.append(g)
+
+ def cardinality(self):
+ return self.permutation_group().cardinality()
+
+ def structure_description(self):
+ return self.permutation_group().structure_description()
+
+ __len__ = cardinality
+
+ def index(self):
+ return self._ambient_group.cardinality() // self.cardinality()
+
+ def __iter__(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.framing_group import FramingGroup
+ sage: G = FramingGroup([1, 2, 3], [2, 2, 2])
+ sage: g1 = G("(0:0,1:0)(2:0,3:0)(4:0,5:0)")
+ sage: g2 = G("(0:1)(2:1)(4:1)")
+ sage: assert all(g in G for g in G)
+ sage: sum(1 for _ in G.subgroup([g1, g2]))
+ 72
+ """
+ yield from (self._ambient_group.from_symmetric_group_element(g) for g in self.permutation_group())
diff --git a/veerer/labelled_digraph.py b/veerer/labelled_digraph.py
new file mode 100644
index 00000000..af9a613c
--- /dev/null
+++ b/veerer/labelled_digraph.py
@@ -0,0 +1,529 @@
+r"""
+Labelled directed graphs and fundamental group
+"""
+# ****************************************************************************
+# This file is part of veerer
+#
+# Copyright (C) 2024 Vincent Delecroix
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# ****************************************************************************
+
+import collections
+import numbers
+
+from sage.graphs.digraph import DiGraph
+from sage.misc.prandom import choice, randrange
+
+
+class LabelledDiGraph:
+ r"""
+ Small overlay over a digraph so that vertices and labels gets labells from
+ 0 to n-1 and 0 to m-1 respectively.
+
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: G.vertex_label(3)
+ ('00000', 3)
+ sage: G.vertex_index(('00000', 3))
+ 3
+ sage: G.outgoing_edges(3)
+ [7, 6, -5, -45]
+ sage: G.incoming_edges(3)
+ [4, 44, -8, -7]
+ """
+ def __init__(self, digraph, root=None, sort=False):
+ self._digraph = DiGraph(len(digraph), loops=digraph.allows_loops(), multiedges=digraph.allows_multiple_edges())
+
+ self._vertices = list(digraph.vertices(sort=sort))
+ if root is not None:
+ i = self._vertices.index(root)
+ self._vertices[0], self._vertices[i] = self._vertices[i], self._vertices[0]
+ self._vertex_index = {v: i for i, v in enumerate(self._vertices)}
+
+ self._edge_sources = []
+ self._edge_targets = []
+ self._edge_labels = []
+
+ edges = list(digraph.edges(sort=sort))
+ for k, (u, v, label) in enumerate(edges):
+ i = self._vertex_index[u]
+ j = self._vertex_index[v]
+ self._edge_sources.append(i)
+ self._edge_targets.append(j)
+ self._edge_labels.append(label)
+ self._digraph.add_edge(i, j, k)
+
+ def __repr__(self):
+ return "LabelledDiGraph on {} vert{} and {} edge{}".format(self.num_verts(), "ex" if self.num_verts() <= 1 else "ices",
+ self.num_edges(), "" if self._edge_sources else "s")
+ def num_verts(self):
+ return len(self._vertices)
+
+ __len__ = num_verts
+
+ def num_edges(self):
+ return len(self._edge_sources)
+
+ def vertex_label(self, i):
+ return self._vertices[i]
+
+ def edge_label(self, i):
+ return self._edge_labels[i] if i >= 0 else self._edge_labels[~i]
+
+ def vertex_index(self, v):
+ return self._vertex_index[v]
+
+ def edge_source(self, i):
+ r"""
+ TESTS::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: for e in range(G.num_edges()):
+ ....: assert G.edge_source(e) == G.edge_target(~e)
+ ....: assert G.edge_source(~e) == G.edge_target(e)
+ """
+ i = int(i)
+ if i >= 0:
+ return self._edge_sources[i]
+ else:
+ return self._edge_targets[~i]
+
+ def edge_target(self, i):
+ i = int(i)
+ if i >= 0:
+ return self._edge_targets[i]
+ else:
+ return self._edge_sources[~i]
+
+ def outgoing_edges(self, i, reverse=True):
+ r"""
+ TESTS::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: for v in range(G.num_verts()):
+ ....: assert all(G.edge_source(e) == v for e in G.outgoing_edges(v))
+ ....: assert all(G.edge_target(e) == v for e in G.incoming_edges(v))
+ """
+ ans = [label for _, _, label in self._digraph.outgoing_edges(i)]
+ if reverse:
+ ans.extend(~label for _, _, label in self._digraph.incoming_edges(i))
+ return ans
+
+ def incoming_edges(self, i, reverse=True):
+ ans = [label for _, _, label in self._digraph.incoming_edges(i)]
+ if reverse:
+ ans.extend(~label for _, _, label in self._digraph.outgoing_edges(i))
+ return ans
+
+ def spanning_tree(self, root=0):
+ r"""
+ Return a pair ``(tree, complementary_edges)`` that form a spanning
+ tree of this labelled digraph.
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: tree, edges = G.spanning_tree()
+ sage: tree[0] is None
+ True
+ sage: any(x is None for x in tree[1:])
+ False
+ sage: len(tree) + len(edges) - 1 == G.num_edges()
+ True
+ """
+ tree = [None] * len(self) # tree[vertex] = edge to follow to go to the root
+ complementary_edges = []
+ seen = [False] * len(self)
+ seen[root] = True
+ todo = [root]
+ while todo:
+ v = todo.pop()
+ for i in self.incoming_edges(v):
+ u = self.edge_source(i)
+ if not seen[u]:
+ tree[u] = i
+ seen[u] = True
+ todo.append(u)
+ elif i >= 0 and tree[v] != ~i:
+ complementary_edges.append(i)
+ return tree, complementary_edges
+
+ def path(self, start, edges=None):
+ return LabelledDiGraphPath(self, start, edges)
+
+ def fundamental_group_basis(self, root=0):
+ r"""
+ Iterate through a basis of the fundamental group of the digraph ``G``.
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph
+ sage: G = LabelledDiGraph(digraphs.DeBruijn(2, 3), sort=True)
+ sage: for path in G.fundamental_group_basis():
+ ....: print(path)
+ Path of length 1 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[0]
+ Path of length 3 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[-9, 9, -2]
+ Path of length 7 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 3, 7, 15, -8, -4, -2]
+ Path of length 7 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 3, 7, 14, -7, -4, -2]
+ Path of length 6 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 2, 5, 11, -4, -2]
+ Path of length 7 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 3, 6, 13, 11, -4, -2]
+ Path of length 6 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 3, -12, 10, -3, -2]
+ Path of length 4 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 2, 4, 8]
+ Path of length 5 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=0 made of edges=[1, 3, 6, 12, 8]
+ """
+ tree, complementary_edges = self.spanning_tree(root)
+ for i in complementary_edges:
+ path = LabelledDiGraphPath(self, self.edge_source(i))
+ path.append(i)
+
+ v = path.start()
+ while v != root:
+ i = tree[v]
+ path.appendleft(~i)
+ v = self.edge_target(i)
+
+ v = path.end()
+ while v != root:
+ i = tree[v]
+ path.append(i)
+ v = self.edge_target(i)
+
+ yield path
+
+
+class LabelledDiGraphPath:
+ r"""
+ Path in a labelled digraph.
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: path.append(0)
+ sage: path.append(-161)
+ sage: path.appendleft(-2)
+ sage: path.appendleft(-163)
+ sage: path
+ Path of length 4 in LabelledDiGraph on 192 vertices and 320 edge from start=98 to target=96 made of edges=[-163, -2, 0, -161]
+ """
+ def __init__(self, graph, start, edges=None):
+ self._graph = graph
+ self._vertices = collections.deque([start]) # vertices
+ self._edges = collections.deque([]) # edge labels (>= 0 for forward and < 0 for backward)
+
+ if edges is not None:
+ for i in edges:
+ self.append(i)
+
+ def __eq__(self, other):
+ if self._graph is not other._graph:
+ raise TypeError
+ return self._vertices == other._vertices and self._edges == other._edges
+
+ def __ne__(self, other):
+ if self._graph is not other._graph:
+ raise TypeError
+ return self._vertices != other._vertices or self._edges != other._edges
+
+ def copy(self):
+ ans = type(self).__new__(type(self))
+ ans._graph = self._graph
+ ans._vertices = self._vertices.copy()
+ ans._edges = self._edges.copy()
+ return ans
+
+ def __bool__(self):
+ return bool(self._edges)
+
+ def __len__(self):
+ return len(self._edges)
+
+ def __iter__(self):
+ return iter(self._edges)
+
+ def __repr__(self):
+ return "Path of length {} in {} from start={} to target={} made of edges={}".format(len(self), self._graph, self.start(), self.end(), list(self._edges))
+
+ def start(self):
+ return self._vertices[0]
+
+ def end(self):
+ return self._vertices[-1]
+
+ def __getitem__(self, k):
+ r"""
+ TESTS::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: path = LabelledDiGraphPath(G, 98, [-163, -2, 0, -161])
+ sage: path[0]
+ -163
+ sage: path[1]
+ -2
+ sage: path[2]
+ 0
+ sage: path[3]
+ -161
+ sage: path[4]
+ Traceback (most recent call last):
+ ...
+ IndexError: deque index out of range
+ sage: path[-1]
+ -161
+ sage: path[-2]
+ 0
+ sage: path[-3]
+ -2
+ sage: path[-4]
+ -163
+ sage: path[-5]
+ Traceback (most recent call last):
+ ...
+ IndexError: deque index out of range
+
+ sage: path[0:2] == G.path(98, [-163, -2])
+ True
+ sage: path[1:3] == G.path(97, [-2, 0])
+ True
+ """
+ if isinstance(k, numbers.Integral):
+ return self._edges[k]
+ elif isinstance(k, slice):
+ start, stop, stride = k.indices(len(self))
+ if stride == 1:
+ ans = type(self).__new__(type(self))
+ ans._graph = self._graph
+ ans._vertices = collections.deque(list(self._vertices)[start : stop + 1])
+ ans._edges = collections.deque(list(self._edges)[start : stop])
+ elif stride == -1:
+ raise NotImplementedError
+ else:
+ raise ValueError("stride must be 1 or -1")
+
+ return ans
+ else:
+ raise TypeError("path index must be integer or slice")
+
+ def __invert__(self):
+ r"""
+ TESTS::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.ButterflyGraph(5), sort=True)
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: ~path
+ Path of length 0 in LabelledDiGraph on 192 vertices and 320 edge from start=0 to target=0 made of edges=[]
+ sage: path = LabelledDiGraphPath(G, 98, [-163, -2, 0, -161])
+ sage: ~path
+ Path of length 4 in LabelledDiGraph on 192 vertices and 320 edge from start=96 to target=98 made of edges=[160, -1, 1, 162]
+ """
+ ans = self.copy()
+ ans._vertices.reverse()
+ ans._edges.reverse()
+ for i, j in enumerate(ans._edges):
+ ans._edges[i] = ~j
+ return ans
+
+ def __mul__(self, other):
+ if type(self) != type(other):
+ raise TypeError
+ if self._graph is not other._graph:
+ raise ValueError("path on different graphs")
+ if self.end() != other.start():
+ raise ValueError("incompatible paths: the end of the first path must be the start of the second one")
+
+ ans = self.copy()
+ ans._vertices.extend(other._vertices[1:])
+ ans._edges.extend(other._edges)
+ return ans
+
+ def append(self, i):
+ r"""
+ Append the i-th edge to the right of the path.
+ """
+ i = int(i)
+ if i >= 0:
+ source = self._graph._edge_sources[i]
+ target = self._graph._edge_targets[i]
+ else:
+ source = self._graph._edge_targets[~i]
+ target = self._graph._edge_sources[~i]
+ if source != self._vertices[-1]:
+ raise ValueError("invalid edge i={}".format(i))
+ self._vertices.append(target)
+ self._edges.append(i)
+
+ def random_append(self, repeat=1, reverse=True):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+ sage: G = LabelledDiGraph(digraphs.DeBruijn(2, 3), sort=True)
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: path.random_append(10)
+ sage: path
+ Path of length 10 in LabelledDiGraph on 8 vertices and 16 edge from start=0 to target=... made of edges=[...]
+
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: path.random_append(100, reverse=True)
+ sage: path.is_oriented()
+ False
+
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: path.random_append(100, reverse=False)
+ sage: path.is_oriented()
+ True
+ """
+ for _ in range(repeat):
+ edges = self._graph.outgoing_edges(self.end(), reverse=reverse and randrange(2))
+ if not edges:
+ print("dead end at {}".format(self.end()))
+ self.append(choice(edges))
+
+ def pop(self):
+ r"""
+ Pop the last edge of the path and return ``(source, target, edge_label)``.
+ """
+ if not self:
+ raise IndexError("pop from an empty path")
+ target = self._vertices.pop()
+ edge = self._edges.pop()
+ return (self.end(), target, edge)
+
+ def appendleft(self, i):
+ r"""
+ Append an edge on the left.
+ """
+ i = int(i)
+ if i >= 0:
+ source = self._graph._edge_sources[i]
+ target = self._graph._edge_targets[i]
+ else:
+ source = self._graph._edge_targets[~i]
+ target = self._graph._edge_sources[~i]
+ if target != self._vertices[0]:
+ raise ValueError("invalid edge")
+ self._vertices.appendleft(source)
+ self._edges.appendleft(i)
+
+ def random_appendleft(self, repeat=1, reverse=True):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph, LabelledDiGraphPath
+
+ sage: G = LabelledDiGraph(digraphs.DeBruijn(2, 3), sort=True)
+ sage: path = G.path(0)
+ sage: path.random_appendleft(10)
+ sage: path
+ Path of length 10 in LabelledDiGraph on 8 vertices and 16 edge from start=... to target=0 made of edges=[...]
+
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: path.random_appendleft(100, reverse=True)
+ sage: path.is_oriented()
+ False
+
+ sage: path = LabelledDiGraphPath(G, 0)
+ sage: path.random_appendleft(100, reverse=False)
+ sage: path.is_oriented()
+ True
+ """
+ for _ in range(repeat):
+ edges = self._graph.incoming_edges(self.start(), reverse=reverse and randrange(2))
+ if not edges:
+ raise ValueError("dead end at {}".format(self.start()))
+ self.appendleft(choice(edges))
+
+ def popleft(self):
+ r"""
+ Pop the first edge of the path and return ``(source, target, edge_label)``.
+ """
+ if not self:
+ raise IndexError("pop from an empty path")
+ source = self._vertices.popleft()
+ edge = self._edges.popleft()
+ return (source, self.start(), edge)
+
+ def is_oriented(self):
+ r"""
+ Return whether the path is oriented.
+
+ A path is *oriented* if it follows each edge orientation.
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph
+
+ sage: G = LabelledDiGraph(digraphs.DeBruijn(2, 3), sort=True)
+ sage: G.path(2).is_oriented()
+ True
+ sage: G.path(0, [1, 2, 5, 10, 4]).is_oriented()
+ True
+ sage: G.path(0, [0, 1, -10, 8]).is_oriented()
+ False
+ """
+ return all(edge >= 0 for edge in self._edges)
+
+ def is_closed(self):
+ r"""
+ Return whether the path is closed.
+
+ A path is *closed* if it starts and ends at the same vertex.
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph
+
+ sage: G = LabelledDiGraph(digraphs.DeBruijn(2, 3), sort=True)
+ sage: G.path(2).is_closed()
+ True
+ sage: G.path(0, [0]).is_closed()
+ True
+ sage: G.path(0, [-9, 9, 2, -11, 10, 5, 11, 7, 14, 12, 8]).is_closed()
+ True
+ sage: G.path(0, [1, 2, 5, 10, 4]).is_closed()
+ False
+ """
+
+ return self.start() == self.end()
+
+ # TODO: should maybe be called is_reduced?
+ def is_non_backtracking(self):
+ r"""
+ Return whether the path is non-backtracking.
+
+ A path is *non-backtracking* if it does not contain as a sub-path an
+ edge followed by its inverse.
+
+ EXAMPLES::
+
+ sage: from veerer.labelled_digraph import LabelledDiGraph
+
+ sage: G = LabelledDiGraph(digraphs.DeBruijn(2, 3), sort=True)
+ sage: G.path(2).is_non_backtracking()
+ True
+ sage: G.path(0, [1, 2, -3]).is_non_backtracking()
+ False
+ """
+ return all(self._edges[i] != ~self._edges[i + 1] for i in range(len(self._edges) - 1))
diff --git a/veerer/layout.py b/veerer/layout.py
deleted file mode 100644
index 2fcd8ffd..00000000
--- a/veerer/layout.py
+++ /dev/null
@@ -1,1095 +0,0 @@
-r"""
-Flat veering triangulations layout in the plane
-
-This module is mostly intended for plotting features. A layout is defined by a
-choice of a rooted spanning forest together with a coordinate for each root.
-Each tree in the spanning forest corresponds to triangulated polygon.
-
-Note:
-
-- when graphviz generates a svg it specifies a given size with the
- attributes "width" and "height". This would better be redefined
- to width="100%".
-"""
-# ****************************************************************************
-# This file is part of veerer
-#
-# Copyright (C) 2018 Mark Bell
-# 2018-2023 Vincent Delecroix
-# 2018 Saul Schleimer
-#
-# This program 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.
-#
-# This program 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 this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# ****************************************************************************
-
-import math
-import itertools
-
-from sage.categories.fields import Fields
-
-from sage.rings.all import RDF
-
-from sage.modules.free_module import VectorSpace
-
-from sage.misc.prandom import shuffle
-from sage.plot.graphics import Graphics
-from sage.plot.line import line2d
-from sage.plot.polygon import polygon2d
-from sage.plot.text import text
-from sage.plot.bezier_path import bezier_path
-from sage.plot.point import point2d
-
-from .constants import BLUE, RED, PURPLE, GREEN, HORIZONTAL, VERTICAL, RIGHT
-from .permutation import perm_init, perm_check, perm_on_list
-from .misc import flipper_edge, flipper_edge_perm, flipper_nf_to_sage, flipper_nf_element_to_sage, det2, flipper_face_edge_perms
-from .triangulation import Triangulation
-from .veering_triangulation import VeeringTriangulation
-from .flat_structure import FlatVeeringTriangulation
-from .flat_structure import vec_slope
-
-_Fields = Fields()
-
-EDGE_COLORS = {
- BLUE: 'blue',
- RED: 'red',
- PURPLE: 'purple',
- GREEN: 'green'}
-
-TIKZ_EDGE_COLORS = {
- BLUE: 'veeringblue',
- RED: 'veeringred',
- PURPLE: 'veeringpurple',
- GREEN: 'veeringgreen'}
-
-TIKZ_FACE_COLORS = {
- BLUE: 'veeringblue!20',
- RED: 'veeringred!20'}
-
-
-class FlatVeeringTriangulationLayout(object):
- r"""
- A flat triangulation layout in the plane.
-
- EXAMPLES:
-
- sage: from veerer import * # random output due to deprecation warnings from realalg
-
- A can be built from a list of triangles and vectors::
-
- sage: triangles = [(0, 1, 2), (-1, -2, -3)]
- sage: holonomies = [(1, 2), (-2, -1), (1, -1), (1, -1), (-2, -1), (1, 2)]
- sage: fvt = FlatVeeringTriangulation(triangles, holonomies)
- sage: layout = FlatVeeringTriangulationLayout(fvt)
- sage: layout # random
- FlatTriangulationLayout(FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)]))
- sage: layout.set_pos()
- sage: layout # random
- FlatTriangulationLayout(FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(1, 2), (-2, -1), (1, -1), (-1, 1), (2, 1), (-1, -2)]), [(1, 0), (2, 2), (0, 1), (3, 1), (1, 0), (2, 2)])
-
- Or from a preexisting flat surface::
-
- sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRBB")
- sage: F = FlatVeeringTriangulation.from_coloured_triangulation(T)
- sage: F.layout() # random
- FlatTriangulationLayout(FlatVeeringTriangulation(Triangulation("(0,1,2)(3,~0,~1)"), [(1, -2), (1, 1), (-2, 1), (2, -1), (-1, -1), (-1, 2)]))
- """
- def __init__(self, flat_triangulation, pos=None):
- r"""
- INPUT:
-
- - flat_triangulation - a flat triangulation
- """
-
- self._triangulation = FlatVeeringTriangulation(flat_triangulation, mutable=True)
-
- # for actual display.
- self._pos = pos # vertex positions (list of length n)
-
- self._check()
-
- def _check(self):
- r"""
- EXAMPLES::
-
- sage: from veerer import *
- sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
- sage: assert T.is_core()
- sage: F = T.flat_structure_min().layout()
- sage: F.set_pos()
- sage: F._check()
- """
- self._triangulation._check()
-
- n = self._triangulation.num_half_edges()
- ep = self._triangulation.edge_permutation(copy=False)
- vectors = self._triangulation._holonomies
-
- for a in range(n):
- A = ep[a]
- u = vectors[a]
- v = vectors[A]
- if u != v and u != -v:
- raise ValueError('ep[%s] = %s but vec[%s] = %s' % (a, u, A, v))
-
- for a, b, c in self._triangulation.faces():
- va = vectors[a]
- vb = vectors[b]
- vc = vectors[c]
- if va + vb + vc:
- raise ValueError('vec[%s] = %s, vec[%s] = %s and vec[%s] = %s do not sum to zero' % (a, va, b, vb, c, vc))
-
- if det2(va, vb) <= 0 or det2(vb, vc) <= 0 or det2(vc, va) <= 0:
- raise ValueError('(%s, %s, %s) is a clockwise triangle' %
- (a, b, c))
-
- pos = self._pos
- if pos is not None:
- for a, b, c in self._triangulation.faces():
- if (pos[a] is not None and pos[b] is not None and pos[a] + vectors[a] != pos[b]) or \
- (pos[b] is not None and pos[c] is not None and pos[b] + vectors[b] != pos[c]) or \
- (pos[c] is not None and pos[a] is not None and pos[c] + vectors[c] != pos[a]):
- raise ValueError('pos[%s] = %s, pos[%s] = %s, pos[%s] = %s while vec[%s] = %s, vec[%s] = %s, vec[%s] = %s' % (
- a, pos[a],
- b, pos[b],
- c, pos[c],
- a, vectors[a],
- b, vectors[b],
- c, vectors[c]))
-
- def copy(self):
- res = FlatVeeringTriangulationLayout.__new__(FlatVeeringTriangulationLayout)
- res._triangulation = self._triangulation.copy()
- if self._pos is not None:
- res._pos = [v.__copy__() for v in self._pos]
- else:
- res._pos = None
- return res
-
- def xy_scaling(self, sx, sy):
- r"""
- Apply Teichmueller flow to actually see things.
- """
- for i, v in enumerate(self._triangulation._holonomies):
- self._triangulation._holonomies[i] = self._triangulation._V((sx * v[0], sy * v[1]))
-
- def _edge_slope(self, e):
- return vec_slope(self._triangulation._holonomies[e])
-
- def __repr__(self):
- if self._pos is None:
- return 'FlatTriangulationLayout({})'.format(
- self._triangulation)
- else:
- return 'FlatTriangulationLayout({}, {})'.format(
- self._triangulation, self._pos)
-
- def _edge_is_boundary(self, e):
- r"""
- Test whether the edge ``e`` is on the boundary of the display.
- """
- if self._pos is None:
- return False
- fp = self._triangulation.face_permutation(copy=False)
- ep = self._triangulation.edge_permutation(copy=False)
- pos = self._pos
- vectors = self._triangulation._holonomies
- E = ep[e]
- return pos[fp[e]] is None or pos[E] is None or pos[fp[e]] != pos[E] or vectors[e] != -vectors[E]
-
- # there is something wrong with edge gluing
- # sometimes we end up with non-valid positions...
- def glue_edge(self, e):
- r"""
- Glue the triangle across the edge ``e`` to ``E`` so that
- we have a quadrilateral around ``e``.
-
- TESTS::
-
- sage: from veerer import *
- sage: t = "(0,~10,9)(1,~14,~2)(2,13,~3)(3,14,~4)(4,~13,~5)(5,~11,~6)(6,10,~7)(7,~12,~8)(8,11,~9)(12,~1,~0)"
- sage: cols = "RBBBBBBRBBBRBRR"
- sage: T = VeeringTriangulation(t, cols)
- sage: F = T.flat_structure_middle().layout()
- sage: F.set_pos()
- sage: for _ in range(100):
- ....: e = randrange(15)
- ....: _ = F.glue_edge(e)
- ....: F._check()
- """
- holonomies = self._triangulation._holonomies
- fp = self._triangulation.face_permutation(False)
- ep = self._triangulation.edge_permutation(False)
- a = ep[e]
- b = fp[a]
- c = fp[b]
-
- if a == e:
- return
-
- if a != e and holonomies[a] == holonomies[e]:
- # apply point-symmetry to the triangle (a,b,c)
- holonomies[a] *= -1
- holonomies[b] *= -1
- holonomies[c] *= -1
-
- if self._edge_is_boundary(e):
- pos = self._pos
- if pos is None or pos[e] is None:
- raise RuntimeError
-
- if a != e:
- pos[a] = pos[e] + holonomies[e]
- pos[b] = pos[a] + holonomies[a]
- pos[c] = pos[b] + holonomies[b]
- xmin = min(pos[a][0], pos[b][0], pos[c][0])
- xmax = max(pos[a][0], pos[b][0], pos[c][0])
- ymin = min(pos[a][1], pos[b][1], pos[c][1])
- ymax = max(pos[a][1], pos[b][1], pos[c][1])
-
- self._check()
- return xmin, xmax, ymin, ymax
-
- def sublayout(self, e):
- r"""
- Return the edges of the connected part of the layout reachable from e
- but not going through e
-
- EXAMPLES::
-
- sage: from veerer import *
-
- """
- if self._pos is None:
- return
-
- T = self._triangulation
- n = T._n
- ep = T._ep
- fp = T._fp
- holonomies = T._holonomies
- pos = self._pos
-
- triangle_list = [] # list of triangles
- seen = [False] * n # visited edges (not needed unless we have 2pi angles)
- seen[e] = seen[ep[e]] = True
- todo = [e]
- while todo:
- # run around triangle
- e = todo.pop()
- a = fp[e]
- b = fp[a]
- triangle_list.append((e, a, b))
- # traverse a and b if needed
- if not seen[a]:
- A = ep[a]
- seen[a] = seen[A] = True
- if a != A and pos[A] == pos[a] + holonomies[a]:
- todo.append(A)
- if not seen[b]:
- B = ep[b]
- seen[b] = seen[B] = True
- if b != B and pos[B] == pos[b] + holonomies[b]:
- todo.append(B)
-
- return triangle_list
-
- def glue_subsurface(self, e):
- r"""
- Move the sublayout glued from `e` to its paired half edge.
- """
- if self._pos is None:
- return
-
- T = self._triangulation
- ep = T._ep
- holonomies = T._holonomies
-
- pos = self._pos
-
- E = ep[e]
- if e == E or pos[E] == pos[e] + holonomies[e]:
- # self-glued or already glued edge
- return
-
- if holonomies[e] == holonomies[E]:
- # reflection
- s = -1
- t = pos[E] + holonomies[E] + pos[e]
- else:
- # translation
- s = 1
- t = pos[E] + holonomies[E] - pos[e]
-
- for (a, b, c) in self.sublayout(e):
- if s == -1:
- T.triangle_upside_down(a)
- pos[a] = s * pos[a] + t
- pos[b] = s * pos[b] + t
- pos[c] = s * pos[c] + t
-
- self._check()
-
- def set_pos_outgoing_separatrices(self, x_space=1):
- r"""
- Set position according to ...
- """
- if not self._triangulation._translation:
- raise ValueError("only available for translation surface, not for half-translation")
-
- # determine outgoing separatrices
- T = self._triangulation
- V = T._V
- n = T.num_half_edges()
- pos = self._pos = [None] * n
- vp = T.vertex_permutation(copy=False)
- ep = T.edge_permutation(copy=False)
- fp = T.face_permutation(copy=False)
- hol = T._holonomies
-
- wedges = []
- bdries = [False] * n
- for e in range(n):
- f = vp[e]
- if T.edge_colour(e) == RED and \
- T.edge_colour(f) == BLUE and \
- hol[e][1] > 0:
- assert hol[f][1] > 0
- wedges.append((e, f))
- bdries[e] = bdries[f] = bdries[ep[e]] = bdries[ep[f]] = True
-
- x = 0 # current x coordinate
- for er, el in wedges:
- modif = []
- xmin = x
- xmax = x
- v = V((x, 0))
- El = ep[el]
- e = fp[er]
- pos[er] = v
- pos[El] = v + hol[el]
- pos[e] = v + hol[er]
- modif.extend((er, El, e))
- xmax = max(xmax, pos[e][0])
- xmin = min(xmin, pos[El][0])
- todo = []
- if not bdries[e]:
- todo.append(ep[e])
- while todo:
- a = todo.pop()
- b = fp[a]
- c = fp[b]
- A = ep[a]
- pos[a] = pos[A] + hol[A]
- pos[b] = pos[a] + hol[a]
- pos[c] = pos[b] + hol[b]
- modif.extend((a, b, c))
- xmax = max([xmax, pos[a][0], pos[b][0], pos[c][0]])
- xmin = min([xmin, pos[a][0], pos[b][0], pos[c][0]])
- if not bdries[b]:
- todo.append(ep[b])
- if not bdries[c]:
- todo.append(ep[c])
- v = V((x - xmin, 0))
- for e in modif:
- pos[e] += v
- x = x - xmin + xmax + x_space
-
- self._pos = pos
-
- def set_pos(self, cylinders=None, y_space=0.1):
- r"""
- Set position randomly.
-
- INPUT:
-
- - ``cylinders`` - an optional list of cylinders
-
- EXAMPLES::
-
- sage: from veerer import *
-
- sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "BBR")
- sage: F = T.flat_structure_min(allow_degenerations=True) # not tested
- sage: F.set_pos(cylinders=T.cylinders(BLUE) + T.cylinders(RED)) # not tested
- sage: F.plot() # not tested
- Graphics object consisting of 13 graphics primitives
-
- TO be tested
-
- RBBRBRBRBRBR_791508a432b6_ba2987654310
- RBBBRBBRBBBR_64a5978301b2_ba9875643210
- """
- n = self._triangulation.num_half_edges()
- half_edge_to_face = [None] * n
- faces = self._triangulation.faces()
-
- for i, (e0, e1, e2) in enumerate(faces):
- half_edge_to_face[e0] = i
- half_edge_to_face[e1] = i
- half_edge_to_face[e2] = i
-
- nf = self._triangulation.num_faces()
- face_seen = [False] * nf
- fp = self._triangulation.face_permutation(copy=False)
- ep = self._triangulation.edge_permutation(copy=False)
- vectors = self._triangulation._holonomies
- pos = self._pos = [None] * (3 * nf)
-
- y = 0 # current height
-
- # set the cylinders
- if cylinders:
- for cyl, _, _, _ in cylinders:
- xmin = xmax = ymin = ymax = self._K.zero()
- a = cyl[0]
- pos[a] = self._triangulation._V((0, 0))
-
- if a == ep[a]:
- cyl = cyl[:-1]
-
- for e in cyl:
- x0min, x0max, y0min, y0max = self.glue_edge(e)
- face_seen[half_edge_to_face[ep[e]]] = True
- xmin = min(xmin, x0min)
- xmax = max(xmax, x0max)
- ymin = min(ymin, y0min)
- ymax = max(ymax, y0max)
-
- # 2. translate positions according to bounding box
- # and the current height position
- t = self._triangulation._V((-xmin, y - ymin))
- for e in cyl:
- a = ep[e]
- b = fp[a]
- c = fp[b]
- pos[a] += t
- pos[b] += t
- pos[c] += t
-
- # 3. set new starting height
- y += ymax - ymin + y_space
-
- n = self._triangulation.num_half_edges()
- for e in range(n):
- f = half_edge_to_face[e]
- if (pos[e] is None) != (face_seen[half_edge_to_face[e]] is False):
- a, b, c = faces[f]
- raise RuntimeError('cylinder badly set: pos[%s] = %s while its face (%s,%s,%s) is %s' % (e, self._pos[e], a, b, c, 'seen' if face_seen[half_edge_to_face[e]] else 'unseen'))
-
- # random forest
- while any(x is False for x in face_seen):
- # choose a starting face
- for start in range(nf):
- if face_seen[start] is False:
- break
-
- # sets its position
- a, b, c = faces[start]
- pos[a] = self._triangulation._V.zero()
- pos[b] = pos[a] + vectors[a]
- pos[c] = pos[b] + vectors[b]
- xmin = xmax = ymin = ymax = 0
- xmin = min(xmin, pos[a][0], pos[b][0], pos[c][0])
- xmax = max(xmax, pos[a][0], pos[b][0], pos[c][0])
- ymin = min(ymin, pos[a][1], pos[b][1], pos[c][1])
- ymax = max(ymax, pos[a][1], pos[b][1], pos[c][1])
-
- # spanning tree
- edges = {t: [] for t in range(nf)}
- wait = [start]
- face_seen[start] = True
- q = [start]
- while wait:
- shuffle(wait)
- t = wait.pop()
- t_edges = list(faces[t])
- shuffle(t_edges)
- for e in t_edges:
- a = ep[e]
- assert pos[e] is not None
- assert half_edge_to_face[e] == t
- f = half_edge_to_face[a]
- if face_seen[f] is False:
- edges[t].append(e)
- face_seen[f] = True
- q.append(f)
- wait.append(f)
-
- x0min, x0max, y0min, y0max = self.glue_edge(e)
- xmin = min(xmin, x0min)
- xmax = max(xmax, x0max)
- ymin = min(ymin, y0min)
- ymax = max(ymax, y0max)
-
- # translate
- t = self._triangulation._V((-xmin, y - ymin))
- for f in q:
- a, b, c = faces[f]
- pos[a] += t
- pos[b] += t
- pos[c] += t
-
- # set new height
- y += ymax - ymin + y_space
-
- n = self._triangulation.num_half_edges()
- for e in range(n):
- if self._pos[e] is None:
- raise RuntimeError('pos[%s] not set properly' % e)
-
- def relabel(self, p):
- r"""
- Relabel according to the permutation ``p``.
-
- EXAMPLES::
-
- sage: import veerer
- sage: T = veerer.VeeringTriangulation("(0,1,2)(~0,~1,~2)", [1, 2, 2])
- sage: F = T.flat_structure_middle().layout()
- sage: F.set_pos()
- sage: F._check()
- sage: F.relabel('(0,1,2)')
- sage: F._check()
- """
- n = self._triangulation._n
- if not perm_check(p, n):
- p = perm_init(p, n, self._triangulation.edge_permutation(False))
- if not perm_check(p, n):
- raise ValueError('invalid relabeling permutation')
-
- self._triangulation.relabel(p)
- if self._pos is not None:
- perm_on_list(p, self._pos)
-
- def forward_flippable_edges(self):
- return self._triangulation.forward_flippable_edges()
-
- def backward_flippable_edges(self):
- return self._triangulation.backward_flippable_edges()
-
- def flip(self, e, folded_edge_convention=RIGHT):
- r"""
- Flip an edge of the layout of a flat triangulation.
-
- EXAMPLES::
-
- sage: from veerer import Triangulation, FlatVeeringTriangulation, LEFT, RIGHT
- sage: t = Triangulation("(0,~5,4)(1,2,~6)(3,5,6)")
- sage: holonomies = [(8, 1), (7, 2), (-4, 2), (9, 2), (-2, -3), (-6, 2), (-3, -4), (-3, -4), (-6, 2)]
- sage: l = FlatVeeringTriangulation(t, holonomies).layout()
- sage: l.set_pos()
- sage: l.flip(0, folded_edge_convention=RIGHT)
-
- sage: l = FlatVeeringTriangulation(t, holonomies).layout()
- sage: l.set_pos()
- sage: l.flip(0, folded_edge_convention=LEFT)
- sage: l._check()
-
- sage: t = Triangulation("(0,1,2)(~0,3,4)(~1,5,6)")
- sage: holonomies = [(-47,51), (-27, -67), (74, 16), (22, 79),
- ....: (-69, -28), (-61, -31), (34, -36)]
- sage: l = FlatVeeringTriangulation(t, holonomies).layout()
- sage: l.set_pos()
- sage: l.flip(2)
- sage: l._check()
- """
- if not self._triangulation.is_forward_flippable(e):
- raise ValueError("not flippable")
-
- # be sure that e is in the middle of a quadrilateral
- # for the layout
- ep = self._triangulation.edge_permutation(copy=False)
- fp = self._triangulation.face_permutation(copy=False)
- pos = self._pos
- holonomies = self._triangulation._holonomies
- E = ep[e]
-
- if e != E and pos[e] + holonomies[e] != pos[E]:
- raise ValueError("can not flip an unglued edge")
-
- # -------------
- # \ e /
- # \a /
- # \ /
- # \ b/
- # \ /
- a = fp[e]
- A = ep[a]
- b = fp[a]
- B = ep[b]
-
- aglued = (a != A) and (pos[A] == pos[a] + holonomies[a])
- bglued = (b != B) and (pos[B] == pos[b] + holonomies[b])
-
- # Do the flip on the triangulation first
- self._triangulation.flip(e, folded_edge_convention=folded_edge_convention)
-
- if pos is not None:
- holonomies = self._triangulation._holonomies
- fp = self._triangulation.face_permutation(copy=False)
- if e == E:
- # folded edge
- # / | | \
- # /b | | \
- # / | |e a\
- # E / | | \
- # ------------- | |
- # \ e / \ | or | /
- # \a / -> \a | | /
- # \ / \ e| | /
- # \ b/ \ | | b/
- # \ / \ | | /
- # LEFT RIGHT
- assert fp[e] == b and fp[b] == a
- if folded_edge_convention == RIGHT:
- # we should move the a neighbors
- A = ep[a]
- pos[a] = pos[e]
- pos[e] = pos[a] + holonomies[a]
- if aglued:
- self.glue_subsurface(A)
- else:
- # we should move the b neighbors
- B = ep[b]
- pos[e] = pos[b]
- pos[b] = pos[e] + holonomies[e]
- if bglued:
- self.glue_subsurface(B)
-
- else:
- # normal edge
- # / \ / | \
- # /b \ /b | \
- # / a\ / |E a\
- # / e \ / | \
- # ---------- -> |
- # \ E / \ e| /
- # \c / \c | /
- # \ d/ \ | d/
- # \ / \ | /
- b = fp[e]
- d = fp[E]
-
- pos[e] = pos[d]
- pos[E] = pos[b]
-
- self._check()
-
- ###################################################################
- # Plotting functions
- ###################################################################
-
- def _plot_edge(self, e, **opts):
- r"""
- Plot the edge ``e``.
- """
- assert self._pos is not None
-
- pos = self._pos
- fp = self._triangulation._fp
- ep = self._triangulation._ep
-
- oopts = {}
- E = ep[e]
- u = pos[e]
- v = pos[fp[e]]
- if not self._edge_is_boundary(e):
- # the edge is between adjacent faces
- oopts['alpha'] = 0.5
- oopts['linestyle'] = 'dotted'
-
- oopts['color'] = EDGE_COLORS[self._edge_slope(e)]
-
- oopts.update(opts)
-
- L = line2d([u, v], **oopts)
-
- if e == E:
- # folded edge
- L += point2d([(u + v) / 2], color='black', marker='x', pointsize=100)
-
- return L
-
- def _tikz_edge(self, output, e, edge_label, **opts):
- r"""
- Plot the edge ``e``.
- """
- assert self._pos is not None
- assert edge_label is True or edge_label is False
-
- pos = self._pos
- fp = self._triangulation._fp
- ep = self._triangulation._ep
-
- E = ep[e]
- u = pos[e]
- v = pos[fp[e]]
-
- edge_tikz_opts = TIKZ_EDGE_COLORS[self._edge_slope(e)]
- if not self._edge_is_boundary(e):
- # the edge is between adjacent faces
- edge_tikz_opts += ',dotted'
-
- if edge_label:
- if self._edge_is_boundary(e) and e > E:
- lab = "$\\sim%d$" % (E)
- else:
- lab = "$%d$" % e
-
- output.write('\\draw[%s] (%s,%s) -- node[sloped] {\\contour{white}{%s}} (%s,%s);\n' % (edge_tikz_opts, u[0], u[1], lab, v[0], v[1]))
-
- else:
- output.write('\\draw[%s] (%s,%s) -- (%s,%s);\n' % (edge_tikz_opts, u[0], u[1], v[0], v[1]))
-
- if e == E:
- # folded edge
- output.write('\\draw mid +(.1,.1) -- (-.1,-.1);\n')
- output.write('\\draw mid +(-.1,.1) -- (.1,-.1);\n')
-
- def _plot_edge_label(self, a, tilde=None, **opts):
- assert self._pos is not None
-
- fp = self._triangulation.face_permutation(copy=False)
- ep = self._triangulation.edge_permutation(copy=False)
- pos = self._pos
- vectors = self._triangulation._holonomies
-
- b = fp[a]
- c = fp[b]
-
- posa = pos[a].n()
- posb = pos[b].n()
- vc = vectors[c].n()
- vc /= vc.norm()
- relposc = posa - vc
-
- if a == ep[a]:
- # folded edge
- pos = (6.5 * posa + 6.5 * posb + relposc) / 14
- else:
- pos = (8 * posa + 5 * posb + relposc) / 14
-
- if tilde is None:
- tilde = self._edge_is_boundary(a)
-
- if tilde:
- if a > ep[a]:
- #lab = "%s=~%s" % (a, ep[a])
- # nicer shorter version
- lab = "~" + str(ep[a])
- else:
- lab = str(a)
- else:
- if a > ep[a]:
- from sage.plot.graphics import Graphics
- return Graphics()
- lab = str(a)
-
- x, y = self._triangulation._holonomies[a]
- if y.is_zero():
- angle = 0
- elif x.is_zero():
- if y > 0:
- angle = 90
- else:
- angle = 270
- else:
- x = float(x)
- y = float(y)
- angle = math.acos(x / math.sqrt(x**2 + y**2))
- if y < 0:
- angle *= -1.0
- angle *= 180 / math.pi
-
- return text(lab, pos, rotation=angle, color='black')
-
- def _plot_face(self, a, edge_labels=True, fill=True):
- r"""
- Plot the face that contains the edge ``a``.
- """
- assert self._pos is not None
-
- fp = self._triangulation.face_permutation(copy=False)
- b = fp[a]
- c = fp[b]
-
- G = Graphics()
-
- if fill:
- # computing slopes in order to determine filling color
- pos = self._pos
-
- nred = nblue = 0
- for e in (a, b, c):
- slope = self._edge_slope(e)
- if slope == RED:
- nred += 1
- elif slope == BLUE:
- nblue += 1
-
- if nred == 2:
- color = (.75, 0.5, 0.5)
- elif nblue == 2:
- color = (.5, 0.5, .75)
- else:
- color = (.5, 0.5, 0.5)
-
- G += polygon2d([pos[a], pos[b], pos[c], pos[a]], alpha=0.41, color=color)
-
- if edge_labels:
- G += self._plot_edge_label(a)
- G += self._plot_edge_label(b)
- G += self._plot_edge_label(c)
-
- return G
-
- def _tikz_face(self, output, a,
- red='red!20', blue='blue!20', neutral='gray!20', tikz_face_options=None):
- assert self._pos is not None
-
- fp = self._triangulation.face_permutation(copy=False)
- b = fp[a]
- c = fp[b]
- pos = self._pos
-
- # computing slopes in order to determine filling color
- nred = nblue = 0
- for e in (a, b, c):
- slope = self._edge_slope(e)
- if slope == RED:
- nred += 1
- elif slope == BLUE:
- nblue += 1
-
- if nred == 2:
- color = TIKZ_FACE_COLORS[RED]
- elif nblue == 2:
- color = TIKZ_FACE_COLORS[BLUE]
- else:
- color = 'gray!20'
-
- output.write('\\fill[%s] (%s,%s) -- (%s,%s) -- (%s,%s) -- cycle;\n' % (
- color if tikz_face_options is None else color + ',' + tikz_face_options,
- pos[a][0], pos[a][1],
- pos[b][0], pos[b][1],
- pos[c][0], pos[c][1]))
-
- def _plot_train_track(self, slope):
- pos = self._pos
- vectors = self._triangulation._holonomies
-
- V2 = VectorSpace(RDF, 2)
- G = Graphics()
-
- if slope == HORIZONTAL:
- POS = RED
- NEG = BLUE
- color = 'purple'
- else:
- POS = BLUE
- NEG = RED
- color = 'green'
-
- for e0, e1, e2 in self._triangulation.faces():
- # determine the large edge
- v0 = vectors[e0]
- v1 = vectors[e1]
- v2 = vectors[e2]
- col0 = RED if v0[0] * v0[1] > 0 else BLUE
- col1 = RED if v1[0] * v1[1] > 0 else BLUE
- col2 = RED if v2[0] * v2[1] > 0 else BLUE
- if col0 == POS and col1 == NEG:
- # e2 is large
- l, s1, s2 = e2, e0, e1
- elif col1 == POS and col2 == NEG:
- # i is large
- l, s1, s2 = e0, e1, e2
- elif col2 == POS and col0 == NEG:
- # j is large
- l, s1, s2 = e1, e2, e0
-
- pl = V2(pos[l])
- ps1 = V2(pos[s1])
- ps2 = V2(pos[s2])
-
- cl = (pl + ps1) / 2
- vl = (ps1 - pl)
- cs1 = (ps1 + ps2) / 2
- vs1 = ps2 - ps1
- cs2 = (ps2 + pl) / 2
- vs2 = pl - ps2
-
- ol = V2((-vl[1], vl[0]))
- ol /= ol.norm()
- os1 = V2((-vs1[1], vs1[0]))
- os1 /= os1.norm()
- os2 = V2((-vs2[1], vs2[0]))
- os2 /= os2.norm()
-
- G += bezier_path([[cl, cl + 0.3 * ol,
- cs1 + 0.3 * os1, cs1]], rgbcolor=color)
- G += bezier_path([[cl, cl + 0.3 * ol,
- cs2 + 0.3 * os2, cs2]], rgbcolor=color)
- return G
-
- def _tikz_train_track(self, slope):
- raise NotImplementedError
-
- def plot(self, horizontal_train_track=False, vertical_train_track=False, edge_labels=True, fill=True):
- r"""
- Return a graphics.
-
- INPUT:
-
- - ``horizontal_train_track`` - boolean - whether to plot the horizontal
- train-track on the surface
-
- - ``vertical_train_track`` - boolean - whether to plot the vertical
- train-track on the surface
-
- EXAMPLES::
-
- sage: from veerer import *
- sage: faces = "(0, ~3, 4)(1, 2, ~7)(3, ~1, ~2)(5, ~8, ~4)(6, ~5, 8)(7, ~6, ~0)"
- sage: colours = 'RBRRBRBRB'
- sage: T = VeeringTriangulation(faces, colours)
- sage: FS = T.flat_structure_min().layout()
- sage: FS.plot(fill=False)
- Graphics object consisting of ... graphics primitives
- sage: FS.plot(fill=True)
- Graphics object consisting of ... graphics primitives
- sage: FS.plot(horizontal_train_track=True)
- Graphics object consisting of ... graphics primitives
- sage: FS.plot(vertical_train_track=True)
- Graphics object consisting of ... graphics primitives
- """
- if self._pos is None:
- self.set_pos()
-
- G = Graphics()
- n = self._triangulation.num_half_edges()
- fp = self._triangulation.face_permutation(copy=False)
- ep = self._triangulation.edge_permutation(copy=False)
-
- # 1. plot faces
- for e in range(n):
- if e < fp[e] and e < fp[fp[e]]:
- G += self._plot_face(e, edge_labels=edge_labels, fill=fill)
-
- # 2. plot edges
- for e in range(n):
- if self._edge_is_boundary(e) or e <= ep[e]:
- G += self._plot_edge(e)
-
- # 3. possibly plot train tracks
- if horizontal_train_track:
- G += self._plot_train_track(HORIZONTAL)
- if vertical_train_track:
- G += self._plot_train_track(VERTICAL)
-
- G.set_aspect_ratio(1)
- G.axes(False)
- return G
-
- def tikz(self, filename=None, horizontal_train_track=False, vertical_train_track=False, edge_labels=True):
- if filename is None:
- from sys import stdout as output
- else:
- output = open(filename, 'w')
-
- if self._pos is None:
- self.set_pos()
-
- n = self._triangulation.num_half_edges()
- fp = self._triangulation.face_permutation(copy=False)
- ep = self._triangulation.edge_permutation(copy=False)
-
- # 1. plot faces
- for e in range(n):
- if e < fp[e] and e < fp[fp[e]]:
- self._tikz_face(output, e)
-
- # 2. plot edges
- for e in range(n):
- if self._edge_is_boundary(e) or e <= ep[e]:
- self._tikz_edge(output, e, edge_labels)
-
- # 3. possibly plot train tracks
- if horizontal_train_track:
- self._tikz_train_track(output, HORIZONTAL)
- if vertical_train_track:
- self._tikz_train_track(output, VERTICAL)
-
- if filename is not None:
- output.close()
-
- def train_track(self, slope=VERTICAL):
- r"""
- Return the measured train-track corresponding to the vertical
- (or horizontal) direction.
-
- EXAMPLES::
-
- sage: from veerer import *
- sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
- sage: F = T.flat_structure_min().layout()
- sage: F.train_track()
- MeasuredTrainTrack(Triangulation("(0,1,2)(3,~0,~1)"), (1, 1, 2, 2, 1, 1))
- sage: F.train_track(HORIZONTAL)
- MeasuredTrainTrack(Triangulation("(0,1,2)(3,~0,~1)"), (1, 2, 1, 1, 2, 1))
- """
- from .measured_train_track import MeasuredTrainTrack
- if slope == VERTICAL:
- return MeasuredTrainTrack(self._triangulation,
- [x.abs() for x, y in self._triangulation._holonomies])
- elif slope == HORIZONTAL:
- return MeasuredTrainTrack(self._triangulation,
- [y.abs() for x, y in self._triangulation._holonomies])
-
- def plot_orbit(self, p, n, slope=VERTICAL, **kwds):
- r"""
- Plot a piece of orbit of the vertical flow.
-
- EXAMPLES::
-
- sage: from veerer import *
- sage: T = VeeringTriangulation("(0,1,2)(~0,~1,3)", "BRRR")
- sage: F = T.flat_structure_min().layout()
- sage: F.plot() + F.plot_orbit((0,1/4),4) # not tested (warning in matplotlib)
- Graphics object consisting of 19 graphics primitives
- """
- G = Graphics()
-
- if slope == HORIZONTAL:
- raise NotImplementedError
-
- tt = self.train_track(slope)
- L = tt.lengths()
- V = self._triangulation._holonomies
-
- O = [q for q in itertools.islice(tt.orbit(p), 2 * n)]
-
- if self._pos is None:
- self.set_pos()
-
- for i in range(n):
- i1, x1 = O[2 * i]
- i2, x2 = O[2 * i + 1]
-
- p1 = self._pos[i1]
- p2 = self._pos[i2]
-
- G += line2d([p1 + x1 / L[i1] * V[i1], p2 + x2 / L[i2] * V[i2]], **kwds)
-
- return G
diff --git a/veerer/linear_family.py b/veerer/linear_family.py
index 3e89400f..0d6df9b8 100644
--- a/veerer/linear_family.py
+++ b/veerer/linear_family.py
@@ -28,7 +28,9 @@
import numbers
from random import choice, shuffle
+from sage.structure.richcmp import op_LT, op_LE, op_EQ, op_NE, op_GT, op_GE, rich_to_bool
from sage.structure.element import get_coercion_model, Matrix
+from sage.structure.richcmp import op_EQ, rich_to_bool
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.matrix.constructor import matrix
@@ -38,8 +40,10 @@
from sage.categories.number_fields import NumberFields
from .constants import VERTICAL, HORIZONTAL, BLUE, RED
-from .permutation import perm_cycle_string, perm_cycles, perm_check, perm_conjugate, perm_on_list
+from .constellation import Constellation
+from .permutation import perm_init, perm_cycle_string, perm_cycles, perm_check, perm_conjugate, perm_on_list, perm_on_edge_list, perm_relabel_on_edges
from .polyhedron import LinearExpressions, ConstraintSystem
+from .polyhedron.linear_expression import LinearConstraint
from .strebel_graph import StrebelGraph
from .veering_triangulation import VeeringTriangulation
@@ -115,8 +119,8 @@ def subspace_cmp(subspace1, subspace2, check=True):
return False
base_ring = cm.common_parent(subspace1.base_ring(), subspace2.base_ring())
- subspace1 = subspace1.echelon_form()
- subspace2 = subspace2.echelon_form()
+ # subspace1 = subspace1.echelon_form()
+ # subspace2 = subspace2.echelon_form()
for r1, r2 in zip(subspace1, subspace2):
c = (r1 > r2) - (r1 < r2)
if c:
@@ -124,45 +128,6 @@ def subspace_cmp(subspace1, subspace2, check=True):
return 0
-def relabel_on_edges(ep, r, n, m):
- r"""
- INPUT:
-
- - ep - edge permutation
-
- - r - relabelling permutation on half edges (list of length n)
-
- - n - num half edges
-
- - m - num edges
-
- OUTPUT: list of length m
-
- EXAMPLES::
-
- sage: from array import array
- sage: from veerer.linear_family import relabel_on_edges
-
- sage: ep = array('i', [8, 1, 2, 7, 4, 5, 6, 3, 0])
- sage: r = array('i', [3, 0, 5, 4, 6, 2, 1, 8, 7])
- sage: relabel_on_edges(ep, r, 9, 7)
- array('i', [3, 0, 5, 4, 6, 2, 1])
- """
- rr = array('i', [-1] * m)
- for i in range(m):
- if ep[i] < i:
- raise ValueError("not in canonical form")
- j = r[i]
- k = r[ep[i]]
- if (j >= m and k >= m):
- raise ValueError("relabelling not preserving canonical form")
- if j < k:
- rr[i] = j
- else:
- rr[i] = k
- return rr
-
-
def matrix_permutation(mat, perm):
r"""
Permute in place the columns of ``mat`` according to the permutation ``perm``
@@ -191,6 +156,12 @@ class LinearFamily:
The subspace is given by generators.
"""
+ def _constellation_class_init(self):
+ bases = self.__class__.__bases__
+ if len(bases) != 2 or bases[0] != LinearFamily:
+ raise ValueError
+ self._constellation_class = bases[1]
+
def __init__(self, *args, mutable=False, check=True):
self._constellation_class_init()
@@ -211,12 +182,6 @@ def __init__(self, *args, mutable=False, check=True):
if check:
self._check(ValueError)
- def _constellation_class_init(self):
- bases = self.__class__.__bases__
- if len(bases) != 2 or bases[0] != LinearFamily:
- raise ValueError
- self._constellation_class = bases[1]
-
def _check(self, error=ValueError):
self._constellation_class._check(self, error)
subspace = self._subspace
@@ -238,7 +203,7 @@ def __str__(self):
sage: from veerer import *
sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
sage: str(vt.as_linear_family())
- 'VeeringTriangulationLinearFamily("(0,1,2)(~2,~0,~1)", "RRB", [(1, 0, -1), (0, 1, 1)])'
+ 'VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)])'
"""
cls_name = self._constellation_class.__name__
s = str(self._constellation_class.__str__(self))
@@ -293,9 +258,6 @@ def __setstate__(self, arg):
self._constellation_class_init()
a, R, raw_entries = arg
self._constellation_class.__setstate__(self, a)
- n = self._n
- for i in range(n):
- self._vp[self._fp[self._ep[i]]] = i
if R is ZZ:
entries = [ZZ(x) for x in raw_entries]
@@ -315,6 +277,24 @@ def __setstate__(self, arg):
if not self._mutable:
self._subspace.set_immutable()
+ def constellation(self, mutable=False):
+ r"""
+ Return the underlying combinatorial veering triangulation or Strebel graph.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, StrebelGraph
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
+ sage: vt.as_linear_family().constellation() == vt
+ True
+
+ sage: sg = StrebelGraph("(0,1:1,2,~1,~0,~2)")
+ sage: sg.as_linear_family().constellation() == sg
+ True
+ """
+ return Constellation.copy(self, mutable, cls=self._constellation_class)
+
def copy(self, mutable=None, cls=None):
r"""
Return a copy of this linear family.
@@ -326,10 +306,10 @@ def copy(self, mutable=None, cls=None):
sage: cols = "BRRBRR"
sage: f = VeeringTriangulation(fp, cols).as_linear_family(mutable=False)
sage: f
- VeeringTriangulationLinearFamily("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "BRRBRR", [(1, 0, 1, 0, 0, 0), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 0, 1)])
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~4,~2)(~1,~5,~3)(3,4,5)", "BRRBRR", [(1, 0, 1, 0, 0, 0), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 0, 1)])
sage: f2 = f.copy(mutable=True)
sage: f2
- VeeringTriangulationLinearFamily("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "BRRBRR", [(1, 0, 1, 0, 0, 0), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 0, 1)])
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~4,~2)(~1,~5,~3)(3,4,5)", "BRRBRR", [(1, 0, 1, 0, 0, 0), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 0, 1)])
sage: f2._check()
sage: f2._mutable
True
@@ -376,7 +356,7 @@ def __hash__(self):
raise ValueError('mutable veering triangulation linear family not hashable')
x = self._constellation_class.__hash__(self)
- x = ((x ^ hash(self._subspace) * 2147483693)) + 82520 + self._n + self._n
+ x = ((x ^ hash(self._subspace) * 2147483693)) + 82520 + self._ne + self._ne
return x
@@ -397,13 +377,36 @@ def __eq__(self, other):
sage: f3 = VeeringTriangulationLinearFamily(vt2, [s2, t2])
sage: f1 == f3
False
- sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
- sage: vt.as_linear_family() == f1
+ sage: vt3 = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
+ sage: vt3 == vt3.as_linear_family()
+ True
+ sage: vt3.as_linear_family() == vt3
+ True
+ sage: vt3.as_linear_family() == f1
+ False
+ sage: f1 == vt3.as_linear_family()
False
"""
- if type(self) is not type(other):
- raise TypeError
- return self._constellation_class.__eq__(self, other) and self._subspace == other._subspace
+ if type(self) is type(other):
+ return self._constellation_class.__eq__(self, other) and self._subspace == other._subspace
+
+ if isinstance(self, LinearFamily):
+ if self._constellation_class.__ne__(self, other):
+ return False
+ elif isinstance(other, LinearFamily):
+ return False
+ else:
+ # self is a linear family while other is a constellation
+ return self._constellation_class.__eq__(self, other) and self.dimension() == other.dimension()
+
+ elif isinstance(other, LinearFamily):
+ if other._constellation_class.__ne__(self, other):
+ return False
+ elif type(self) is other._constellation_class:
+ # one of self or other is a VeeringTriangulation
+ return other._constellation_class.__eq__(other, self) and self.dimension() == other.dimension()
+ else:
+ return False
def __ne__(self, other):
r"""
@@ -422,28 +425,58 @@ def __ne__(self, other):
sage: f3 = VeeringTriangulationLinearFamily(vt2, [s2, t2])
sage: f1 != f3
True
- sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
- sage: vt.as_linear_family() != f1
+
+ sage: vt1 = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
+ sage: vt2 = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, BLUE, BLUE])
+ sage: vt1.as_linear_family() != f1
+ True
+ sage: vt1 != vt1.as_linear_family()
+ False
+ sage: vt1.as_linear_family() != vt1
+ False
+ sage: vt1 != vt2.as_linear_family()
+ True
+ sage: vt2.as_linear_family() != vt1
True
"""
- if type(self) is not type(other):
- raise TypeError
- return self._constellation_class.__ne__(self, other) or self._subspace != other._subspace
+ if type(self) is type(other):
+ return self._constellation_class.__ne__(self, other) or self._subspace != other._subspace
+
+ if isinstance(self, LinearFamily):
+ if self._constellation_class.__ne__(self, other):
+ return True
+ elif isinstance(other, LinearFamily):
+ return True
+ else:
+ # self is a linear family while other is a constellation
+ return self._constellation_class.__ne__(self, other) or self.dimension() != other.dimension()
+
+ elif isinstance(other, LinearFamily):
+ if other._constellation_class.__ne__(self, other):
+ return True
+ elif type(self) is other._constellation_class:
+ # one of self or other is a VeeringTriangulation
+ return other._constellation_class.__ne__(other, self) or self.dimension() != other.dimension()
+ else:
+ return True
def _richcmp_(self, other, op):
- c = (self._n > other._n) - (self._n < other._n)
- if c:
- return rich_to_bool(op, c)
-
- c = (self._colouring > other._colouring) - (self._colouring < other._colouring)
- if c:
- return rich_to_bool(op, c)
+ r"""
+ TESTS::
- c = (self._fp > other._fp) - (self._fp < other._fp)
- if c:
- return rich_to_bool(op, c)
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+ sage: f = VeeringTriangulationLinearFamily("(0:1)(~0:1,1:1,2:2)(~1:1,~2:2,3:1)(~3:1)", "RRBR", [(1, 0, 0, 1), (0, 1, 0, 0), (0, 0, 1, 0)])
+ sage: f == min(x for x in f.delaunay_strebel_graph()._vertices if isinstance(x, VeeringTriangulation))
+ True
- c = (self._ep > other._ep) - (self._ep < other._ep)
+ sage: f0 = VeeringTriangulationLinearFamily("(0:1)(~0:1,1:1,2:1)(~1:1,~2:1,3:1)(~3:1)", "RRBR", [(1, 0, 0, 1), (0, 1, 0, 0), (0, 0, 1, 0)])
+ sage: f1 = VeeringTriangulationLinearFamily("(0:1)(~0:1,1:1,2:1)(~1:1,~2:1,3:1)(~3:1)", "BRRB", [(1, 0, 0, 1), (0, 1, 0, 0), (0, 0, 1, 0)])
+ sage: (f0 < f1) + (f0 == f1) + (f0 > f1)
+ 1
+ sage: (f1 < f0) + (f1 == f0) + (f1 > f0)
+ 1
+ """
+ c = self._constellation_class._cmp_(self, other)
if c:
return rich_to_bool(op, c)
@@ -474,7 +507,7 @@ def constraints_matrix(self, mutable=None):
sage: vt.as_linear_family().constraints_matrix().echelon_form()
[ 1 -1 1]
- sage: F = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)").add_residue_constraints([[1, 2, 0, 0]])
+ sage: F = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)").add_residue_constraints([[1, 0, 2, 0]])
sage: F.constraints_matrix().echelon_form()
[ 1 -1 1 1]
"""
@@ -492,7 +525,7 @@ def generators_matrix(self, mutable=None):
[ 1 0 -1]
[ 0 1 1]
- sage: F = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)").add_residue_constraints([[1, 2, 0, 0]])
+ sage: F = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)").add_residue_constraints([[1, 0, 2, 0]])
sage: F.generators_matrix().echelon_form()
[ 1 0 0 -1]
[ 0 1 0 1]
@@ -517,7 +550,7 @@ def residue_constraints(self):
EXAMPLES::
- sage: from veerer import StrebelGraph, VeeringTriangulation
+ sage: from veerer import StrebelGraph, VeeringTriangulation, VeeringTriangulationLinearFamily, StrebelGraphLinearFamily
sage: G = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)")
sage: G.as_linear_family().residue_constraints().echelon_form()
@@ -545,6 +578,79 @@ def residue_constraints(self):
sage: f = VeeringTriangulation(fp, cols).as_linear_family(mutable=True)
sage: f.residue_constraints()
[]
+
+ Examples in the gothic locus::
+
+ sage: f = VeeringTriangulationLinearFamily("(~0,1,2)(~2,3,4)(~3,~4,5)(~5,6,7)(0:1)(~6:1,~7:1,8:1)(~8:1)(9:1)", "RRBRRBRRRR", [(1, 0, -1, 0, -1, -1, 1, 0, 1, 2), (0, 2, 2, 0, 2, 2, -1, 1, 0, 0), (0, 0, 0, 1, 1, 0, 0, 0, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 0 1 -1]
+ [ 0 1 0 -1]
+ [ 0 0 2 -1]
+ sage: f = VeeringTriangulationLinearFamily("(~2,3,4)(~3,5,6)(~4,7,~5)(0:1)(1:1,2:1)(~6:1,8:1)(~7:1,~8:1)", "RRRRBBRRR", [(2, 0, 2, 0, -2, -1, 1, 1, 0), (0, 2, -2, 0, 2, 1, -1, -1, 1), (0, 0, 0, 1, 1, 1, 0, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 0 0 -2]
+ [ 0 1 0 -2]
+ [ 0 0 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(1,2,3)(~3,6,7)(0:1)(~2:1,4:1,5:1)(~4:1,~7:1)(~5:1,~6:1)", "BBBRBBBB", [(2, 0, 0, 0, 1, 1, 0, 0), (0, 2, 0, 2, 1, -1, 1, -1), (0, 0, 1, -1, -1, 0, 0, 1)])
+ sage: f.residue_constraints()
+ [ 1 0 0 -2]
+ [ 0 1 -1 -1]
+ [ 0 0 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0:1)(1:1,2:1,3:1,4:1)(~2:1,~4:1,5:1)(~3:1,~5:1)", "RRRRRR", [(2, 0, 0, 1, 1, 0), (0, 2, 0, -1, -1, 1), (0, 0, 1, 0, -1, 0)])
+ sage: f.residue_constraints()
+ [ 1 0 0 -2]
+ [ 0 1 -1 -1]
+ [ 0 0 1 -1]
+ sage: f = StrebelGraphLinearFamily("(0)(1,2,3,4)(~2,~4,5)(~3,~5)", [(2, 0, 0, 1, 1, 0), (0, 2, 0, -1, -1, 1), (0, 0, 1, 0, -1, 0)])
+ sage: f.residue_constraints()
+ [ 1 0 0 -2]
+ [ 0 1 -1 -1]
+ [ 0 0 1 -1]
+
+ sage: f = VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~2,4,5)(~3,6,~5)(~4:1,~6:1)(7:1,8:1,9:1)", "BBRRBRBBBB", [(1, 0, 1, 1, 0, 1, 0, 0, 0, 0), (0, 1, -1, -1, 0, -1, 0, 0, 0, 0), (0, 0, 0, 0, 3, -3, 3, 2, 2, 2)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~2:1,4:1,~3:1,~4:1)(5:1,6:1,7:1)", "BRRRRRRR", [(1, 0, 1, 1, 2, 2, 2, 2), (0, 1, 1, 1, 2, 2, 2, 2), (0, 0, 0, 0, 3, 2, 2, 2)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0:1,1:1,2:1,~0:1,~1:1,~2:1)(3:1,4:1,5:1)", "RRRRRR", [(1, 0, 2, 2, 2, 2), (0, 1, 2, 2, 2, 2), (0, 0, 3, 2, 2, 2)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = StrebelGraphLinearFamily("(0,1,2,~0,~1,~2)(3,4,5)", [(1, 0, 2, 2, 2, 2), (0, 1, 2, 2, 2, 2), (0, 0, 3, 2, 2, 2)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+
+ sage; f = VeeringTriangulationLinearFamily("(~0,1,2)(~1,3,4)(5,6,7)(~5,~6,8)(~7,9,10)(~8,~9,11)(0:1)(~10:1,~11:1)", "BRRBRBRRRRBB", [(2, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 1), (0, 2, 2, 0, 2, 0, 1, 1, 1, 1, 0, 0), (0, 0, 0, 2, -2, 1, -1, 0, 0, 0, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~6,7,8)(~2:1,4:1,~3:1,~4:1)(5:1,6:1)", "BRRRRRRBR", [(1, 0, 1, 1, 0, 0, 2, 2, 0), (0, 1, 1, 1, 0, 0, 2, 0, 2), (0, 0, 0, 0, 1, 2, 0, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0:1,1:1,2:1,~0:1,~1:1,~2:1)(3:1,4:1,5:1)", "BBBBBB", [(1, 0, 0, 0, 0, 2), (0, 1, 0, 2, 0, 0), (0, 0, 1, 0, 2, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = StrebelGraphLinearFamily("(0,1,2,~0,~1,~2)(3,4,5)", [(1, 0, 0, 0, 0, 2), (0, 1, 0, 2, 0, 0), (0, 0, 1, 0, 2, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+
+ sage: f = VeeringTriangulationLinearFamily("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~4,8,9)(~7,10,11)(~8,12,~10)(0:1)(~11:1)", "BBRRBBBBBRRBR", [(1, 0, 1, 2, 2, 1, 0, -1, 0, 2, 2, 1, 2), (0, 1, -1, 0, 1, 1, 2, 1, 1, 0, -1, 0, -2), (0, 0, 0, 3, 3, 2, 2, -1, 1, 2, 1, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(~0,1,2)(~1,3,4)(~2,5,6)(~3,7,~6)(~5,9,10)(~8,11,~10)(0:1)(~4:1,8:1)", "RBRRRRBBRRBR", [(1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0), (0, 3, -3, 0, 3, -1, 2, 2, -3, 0, 1, -2), (0, 0, 0, 1, 1, 1, 1, 2, -1, 2, 1, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(~1,2,3)(~2,4,5)(~3,6,~4)(~6,8,9)(~7,10,~9)(0:1,1:1)(~5:1,7:1)", "BBRRRBBBRRR", [(2, 0, 0, 0, 1, 1, -1, 1, 2, 1, 0), (0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0), (0, 0, 1, 1, 1, 0, 0, 0, 2, 2, 2)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0,1,2)(~2,4,5)(~3,6,7)(~5,~7,8)(~1:1,3:1)(~6:1,~8:1,9:1)", "RRBRRRRBRR", [(2, 0, -2, 0, 2, 0, -1, -1, 1, 0), (0, 1, 1, 0, 0, 1, 1, 1, 0, 0), (0, 0, 0, 1, -2, -2, 0, -1, -1, 2)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = VeeringTriangulationLinearFamily("(0:1,1:1,2:1,3:1)(~1:1,~3:1,4:1,~2:1,5:1)", "BBBBBB", [(2, 0, 0, 1, 0, 2), (0, 1, 0, -1, 2, -2), (0, 0, 1, 0, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
+ sage: f = StrebelGraphLinearFamily("(0,1,2,3)(~1,~3,4,~2,5)", [(2, 0, 0, 1, 0, 2), (0, 1, 0, -1, 2, -2), (0, 0, 1, 0, 0, 0)])
+ sage: f.residue_constraints()
+ [ 1 -1]
"""
# We want vectors that are linear combinations of self.residue_matrix()
# and self.constraints_matrix().
@@ -571,7 +677,7 @@ def relabel(self, p, check=True):
sage: vt, s, t = VeeringTriangulations.L_shaped_surface(2, 3, 4, 5, 1, 2)
sage: f = VeeringTriangulationLinearFamily(vt, [s, t], mutable=True)
sage: for _ in range(10):
- ....: p = f._relabelling_from(choice(range(9)))
+ ....: p = f._relabelling_from(choice(list(f.half_edges())))
....: f.relabel(p)
....: f._check()
@@ -580,59 +686,79 @@ def relabel(self, p, check=True):
....: f.relabel(p)
....: f._check()
"""
- n = self.num_half_edges()
m = self.num_edges()
- ep = self._ep
- if check and not perm_check(p, n):
- p = perm_init(p, n, ep)
- if not perm_check(p, n):
+ if check and not perm_check(p, 2 * self._ne):
+ p = perm_init(p, 2 * self._ne, edge_like=True)
+ if not perm_check(p, 2 * self._ne):
raise ValueError('invalid relabelling permutation')
- rr = relabel_on_edges(self._ep, p, n, m)
+ rr = perm_relabel_on_edges(p, self._ne)[0]
matrix_permutation(self._subspace, rr)
self._subspace.echelonize()
self._constellation_class.relabel(self, p, False)
- # TODO: remove check
- self._check()
-
- def best_relabelling(self, all=False):
+ def best_relabelling(self, return_all=False):
r"""
Return the smallest relabelling of this linear family.
- """
- n = self.num_half_edges()
- m = self.num_edges()
- best = None
- if all:
- relabellings = []
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, StrebelGraph, VeeringTriangulationLinearFamily, StrebelGraphLinearFamily
- for start_edge in self._automorphism_good_starts():
- relabelling = self._relabelling_from(start_edge)
- rr = relabel_on_edges(self._ep, relabelling, n, m)
- fp_new = perm_conjugate(self._fp, relabelling)
- ep_new = perm_conjugate(self._ep, relabelling)
- data_new = [l[:] for l in self._data]
- for l in data_new:
- perm_on_list(relabelling, l, self._n)
+ sage: f = VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~7,10,11)(~8,13,14)(~9,~13,~14)(~10,~11,~12)(~2:1,~6:1,8:1,5:1)(~3:1,6:1,9:1,~5:1)(4:1,12:1)(~4:1,7:1)", "RBBBBBBBBBRBBRB", [(1, 0, -1, -1, 0, 0, 0, -2, -1, -1, 2, 0, -2, 1, 0), (0, 1, 1, 1, 0, 0, 0, 2, 1, 1, 0, 2, 2, 0, 1), (0, 0, 0, 0, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0)])
+ sage: len(f.best_relabelling(return_all=True)[0])
+ 8
+ sage: f = StrebelGraphLinearFamily("(0,2,3,1,5,6)(~0,~5,7,~8,~2,~6)(~1,4,~3)(~4,~7,8)", [(2, 1, 0, 0, 2, 0, 0, 0, 1), (0, 0, 1, 0, 2, 1, 0, 0, 0), (0, 0, 0, 1, 2, 0, 2, 1, 0)])
+ sage: len(f.best_relabelling(return_all=True)[0])
+ 2
+ """
+ ne = self._ne
+ n = 2 * ne
+ relabellings, fp_best, half_edges_data_best, edges_data_best = self.constellation().best_relabelling(return_all=True)
+
+ subspace_best = copy(self._subspace)
+ relabelling_best = relabellings[0]
+ r = perm_relabel_on_edges(relabelling_best, ne)[0]
+ matrix_permutation(subspace_best, r)
+ subspace_best.echelonize()
+ if return_all:
+ family_relabellings = [relabelling_best]
+
+ for i in range(1, len(relabellings)):
+ relabelling_new = relabellings[i]
subspace_new = copy(self._subspace)
- matrix_permutation(subspace_new, rr)
+ r = perm_relabel_on_edges(relabelling_new, ne)[0]
+ matrix_permutation(subspace_new, r)
subspace_new.echelonize()
- subspace_new.set_immutable()
-
- T = (fp_new, ep_new, data_new, subspace_new)
- if best is None or T < best:
- best = T
- best_relabelling = relabelling
- if all:
- del relabellings[:]
- relabellings.append(relabelling)
- elif all and T == best:
- relabellings.append(relabelling)
-
- return (relabellings, best) if all else (best_relabelling, best)
+ if subspace_new < subspace_best:
+ relabelling_best = relabelling_new
+ subspace_best = subspace_new
+ if return_all:
+ family_relabellings.clear()
+ family_relabellings.append(relabelling_new)
+ elif return_all and subspace_new == subspace_best:
+ family_relabellings.append(relabelling_new)
+
+ return (family_relabellings, fp_best, half_edges_data_best, edges_data_best, subspace_best) if return_all else (relabelling_best, fp_best, half_edges_data_best, edges_data_best, subspace_best)
+
+ def set_canonical_labels(self, mapping=False):
+ r"""
+ Set labels in a canonical way in its automorphism class.
+ """
+ if not self._mutable:
+ raise ValueError('immutable triangulation; use a mutable copy instead')
+
+ r, fp_best, half_edges_data_best, edges_data_best, subspace_best = self.best_relabelling()
+ self._fp = fp_best
+ self._vp = perm_conjugate(self._vp, r)
+ self._half_edges_data = half_edges_data_best
+ self._edges_data = edges_data_best
+ self._set_data_pointers()
+ self._subspace = subspace_best
+ if mapping:
+ return r
def _non_isom_easy(self, other):
return (self._constellation_class._non_isom_easy(self, other) or
@@ -652,37 +778,53 @@ def abelian_cover(self, mutable=False):
sage: from veerer.linear_family import VeeringTriangulationLinearFamilies, StrebelGraphLinearFamily
sage: X9 = VeeringTriangulationLinearFamilies.prototype_H1_1(0, 2, 1, -1)
- sage: Y9 = X9.abelian_cover()
- sage: Y9.dimension() == X9.dimension()
+ sage: Y9 = X9.abelian_cover() # not tested
+ sage: Y9.dimension() == X9.dimension() # not tested
True
- sage: Y9.stratum() # optional - surface_dynamics
+ sage: Y9.stratum() # optional - surface_dynamics # not tested
H_2(1^2)
sage: F = StrebelGraphLinearFamily("(0,1:1,~0,~1:1)", [[2, 1]])
- sage: Fab = F.abelian_cover()
- sage: Fab
+ sage: Fab = F.abelian_cover() # not tested
+ sage: Fab # not tested
StrebelGraphLinearFamily("(0,~2:1,~0,2:1)(1:1,3,~1:1,~3)", [(2, 1, 1, 2)])
- sage: print(F.stratum(), Fab.stratum()) # optional - surface_dynamics
+ sage: print(F.stratum(), Fab.stratum()) # optional - surface_dynamics # not tested
H_1(2, -2) (H_1(2, -2), H_1(2, -2))
"""
- t = self._constellation_class.abelian_cover(self)
- assert t._n == 2 * self._n
- assert t.num_edges() == 2 * self.num_edges() - self.num_folded_edges() == self._n
- nr = self._subspace.nrows()
- subspace = self._subspace.new_matrix(nr, self._n)
+ t, inv, quot = self._constellation_class.abelian_cover(self, involution_and_quotient=True)
+ assert t.num_edges() == 2 * self._ne - self.num_folded_edges()
+ nr = self._subspace.nrows() + self.num_edges() - self.num_folded_edges()
+ # TODO: we have equations coming from the quotient and equations coming from the involution
+ raise NotImplementedError
+
+ def quotient(self, blocks, mapping=False, mutable=False, check=True):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)(7,~2,8)(9,~3,~6)(10,~7,~4)(11,~5,12)(13,14,~8)(15,~9,16)(17,18,~10)(19,~17,~11)(20,~13,~12)(21,~14,~18)(22,~21,~15)(23,24,~16)(25,~23,~19)(26,~20,~25)(~26,~24,~22)", "RBBRBRBRRBRBBRBBRRBRRRBBRRB")
+ sage: subspace = [(1, 0, -1, 0, -1, 0, 0, 0, 1, 0, 1, -1, -1, 0, -1, 0, 0, 0, -1, 1, 1, 0, 0, -1, 1, 0, -1),
+ ....: (0, 1, 1, 0, 0, 1, 2, 0, -1, 2, 0, 0, 1, 0, 1, 2, 0, 0, 0, 0, -1, 1, 1, 0, 0, 0, 1),
+ ....: (0, 0, 0, 1, 1, 1, 1, 0, 0, 0, -1, 1, 2, 2, 2, 1, 1, 0, 1, -1, 0, 1, 0, 1, 0, 0, 0),
+ ....: (0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 2, 1, 0, 0, 2, 1, 1, 1, 0, 0, 0, 0, 1, 0)]
+ sage: f = VeeringTriangulationLinearFamily(vt, subspace)
+ sage: f.automorphism_quotient()
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,3,4)(~1,5,6)(~2,8,7)(~3,~6,9)(~4,10,~7)(~5,12,11)(~8,13,~12)(~10,14,~11)", "RBBRBRBRRBRBBRR", [(1, 0, -1, 0, -1, 0, 0, 0, 1, 0, 1, -1, -1, 0, 0), (0, 1, 1, 0, 0, 1, 2, 0, -1, 2, 0, 0, 1, 0, 0), (0, 0, 0, 1, 1, 1, 1, 0, 0, 0, -1, 1, 2, 2, 0), (0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 2, 2)])
+ """
+ constellation_quotient, mapping = self.constellation().quotient(blocks, True, mutable, check)
+
+ if check:
+ for block in blocks:
+ for gen in self._subspace.rows():
+ if len(set(gen[h // 2] for h in block)) != 1:
+ raise ValueError("invalid blocks")
+
+ subspace = matrix(self.base_ring(), self._subspace.nrows(), constellation_quotient._ne)
for i in range(self._subspace.nrows()):
- for e in range(self._n):
- if self._ep[e] < e:
- subspace[i, e] = self._subspace[i, self._ep[e]]
- else:
- subspace[i, e] = self._subspace[i, e]
- f = self.__class__.from_permutations(t._vp, t._ep, t._fp, t._data, mutable=True, check=False)
- f._constellation_class_init()
- f._subspace = subspace
- if not mutable:
- f.set_immutable()
- f._check()
- return f
+ for block in blocks:
+ subspace[i, mapping[block[0]] // 2] = self._subspace[i, block[0] // 2]
+
+ return self.__class__(constellation_quotient, subspace, mutable=mutable, check=check)
class VeeringTriangulationLinearFamily(LinearFamily, VeeringTriangulation):
@@ -692,26 +834,11 @@ class VeeringTriangulationLinearFamily(LinearFamily, VeeringTriangulation):
"""
__slots__ = ['_constellation_class', '_subspace']
- def veering_triangulation(self, mutable=False):
- r"""
- Return the underlying veering triangulation.
-
- EXAMPLES::
-
- sage: from veerer import VeeringTriangulation
- sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
- sage: vt.as_linear_family().veering_triangulation() == vt
- True
- """
- return VeeringTriangulation.copy(self, mutable, cls=VeeringTriangulation)
def _horizontal_subspace(self):
mat = copy(self._subspace)
ne = self.num_edges()
- ep = self._ep
for j in range(ne):
- if ep[j] < j:
- raise ValueError('not in standard form')
if self._colouring[j] == BLUE:
for i in range(mat.nrows()):
mat[i, j] *= -1
@@ -747,22 +874,22 @@ def rotate(self):
sage: f = VeeringTriangulation(fp, cols).as_linear_family(mutable=True)
sage: f.rotate()
sage: f
- VeeringTriangulationLinearFamily("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "RBBRBB", [(1, 0, -1, 0, 0, 0), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 0, -1)])
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~4,~2)(~1,~5,~3)(3,4,5)", "RBBRBB", [(1, 0, -1, 0, 0, 0), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 0, -1)])
sage: fp = "(0,12,~11)(1,13,~12)(2,14,~13)(3,15,~14)(4,17,~16)(5,~10,11)(6,~3,~17)(7,~2,~6)(8,~5,~7)(9,~0,~8)(10,~4,~9)(16,~15,~1)"
sage: cols = "RRRRRRBBBBBBBBBBBB"
sage: f = VeeringTriangulation(fp, cols).as_linear_family(mutable=True)
sage: f.rotate()
sage: f
- VeeringTriangulationLinearFamily("(0,12,~11)(1,13,~12)(2,14,~13)(3,15,~14)(4,17,~16)(5,~10,11)(6,~3,~17)(7,~2,~6)(8,~5,~7)(9,~0,~8)(10,~4,~9)(16,~15,~1)", "BBBBBBRRRRRRRRRRRR", [(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0), (0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 2, 2, 1, 1, 1, 0, 0), (0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0), (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1), (0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0), (0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)])
+ VeeringTriangulationLinearFamily("(0,12,~11)(~0,~8,9)(1,13,~12)(~1,16,~15)(2,14,~13)(~2,~6,7)(3,15,~14)(~3,~17,6)(4,17,~16)(~4,~9,10)(5,~10,11)(~5,~7,8)", "BBBBBBRRRRRRRRRRRR", [(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0), (0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 2, 2, 1, 1, 1, 0, 0), (0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0), (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1), (0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0), (0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)])
"""
if not self._mutable:
raise ValueError('immutable veering triangulation family; use a mutable copy instead')
subspace = self._horizontal_subspace()
subspace.echelonize()
- super().rotate()
self._subspace = subspace
+ super().rotate()
# TODO: remove check
self._check()
@@ -776,40 +903,24 @@ def _set_subspace_constraints(self, insert, x, slope):
for row in subspace.right_kernel_matrix():
insert(sum(row[i] * x[i] for i in range(ambient_dim)) == 0)
+ def _set_subspace_constraints_fast(self, cs, L, slope, shift):
+ zero = L.base_ring().zero()
+ if slope == VERTICAL:
+ subspace = self._subspace
+ for row in subspace.right_kernel_matrix():
+ cs.insert(LinearConstraint(op_EQ, L.element_class(L, row.dict(), zero)), check=False)
+ elif slope == HORIZONTAL:
+ subspace = self._horizontal_subspace()
+ for row in subspace.right_kernel_matrix():
+ cs.insert(LinearConstraint(op_EQ, L.element_class(L, {key + shift: value for key, value in row.dict().items()}, zero)), check=False)
+
def _check(self, error=ValueError):
LinearFamily._check(self, error)
# test that elements satisfy the switch condition
subspace = self._subspace
for v in subspace.rows():
- self._set_switch_conditions(self._tt_check, v, VERTICAL)
-
- def train_track_polytope(self, slope=VERTICAL, low_bound=0, backend=None):
- r"""
- Return the polytope of non-negative elements in the subspace.
-
- EXAMPLES::
-
- sage: from veerer import *
- sage: vt, s, t = VeeringTriangulations.L_shaped_surface(1, 3, 1, 1)
- sage: f = VeeringTriangulationLinearFamily(vt, [s, t])
- sage: f.train_track_polytope(VERTICAL)
- Cone of dimension 2 in ambient dimension 7 made of 2 facets (backend=ppl)
- sage: f.train_track_polytope(HORIZONTAL)
- Cone of dimension 2 in ambient dimension 7 made of 2 facets (backend=ppl)
-
- sage: sorted(f.train_track_polytope(VERTICAL).rays())
- [[0, 1, 3, 3, 1, 1, 0], [1, 0, 0, 1, 1, 1, 1]]
- sage: sorted(f.train_track_polytope(HORIZONTAL).rays())
- [[1, 0, 0, 1, 1, 1, 1], [3, 1, 3, 0, 2, 2, 3]]
- """
- ne = self.num_edges()
- L = LinearExpressions(self.base_ring())
- cs = ConstraintSystem()
- for i in range(ne):
- cs.insert(L.variable(i) >= low_bound)
- self._set_subspace_constraints(cs.insert, [L.variable(i) for i in range(ne)], slope)
- return cs.cone(backend)
+ self.constellation()._set_subspace_constraints(self._constraint_check, v, VERTICAL)
def flip(self, e, col, check=True):
r"""
@@ -825,23 +936,23 @@ def flip(self, e, col, check=True):
sage: T.flip(3, 2)
sage: L.flip(3, 2)
sage: T
- VeeringTriangulation("(0,3,2)(1,4,~0)(5,6,~1)", "BRRBBBB")
+ VeeringTriangulation("(0,3,2)(~0,1,4)(~1,5,6)", "BRRBBBB")
sage: L
- VeeringTriangulationLinearFamily("(0,3,2)(1,4,~0)(5,6,~1)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, -1, 1, 1, 0)])
+ VeeringTriangulationLinearFamily("(0,3,2)(~0,1,4)(~1,5,6)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, -1, 1, 1, 0)])
sage: L.flip(4, 2)
sage: T.flip(4, 2)
sage: T
- VeeringTriangulation("(0,3,2)(1,~0,4)(5,6,~1)", "BRRBBBB")
+ VeeringTriangulation("(0,3,2)(~0,4,1)(~1,5,6)", "BRRBBBB")
sage: L
- VeeringTriangulationLinearFamily("(0,3,2)(1,~0,4)(5,6,~1)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, -1, -1, 1, 0)])
+ VeeringTriangulationLinearFamily("(0,3,2)(~0,4,1)(~1,5,6)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, -1, -1, 1, 0)])
sage: T.flip(5, 2)
sage: L.flip(5, 2)
sage: T
- VeeringTriangulation("(0,3,2)(1,~0,4)(5,~1,6)", "BRRBBBB")
+ VeeringTriangulation("(0,3,2)(~0,4,1)(~1,6,5)", "BRRBBBB")
sage: L
- VeeringTriangulationLinearFamily("(0,3,2)(1,~0,4)(5,~1,6)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, -1, -1, -1, 0)])
+ VeeringTriangulationLinearFamily("(0,3,2)(~0,4,1)(~1,6,5)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, -1, -1, -1, 0)])
"""
super().flip(e, col, Gx=self._subspace, check=check)
self._subspace.echelonize()
@@ -880,9 +991,9 @@ def is_cylindrical(self, col=None):
C = [] # middle edges
for cyl in cylinders:
- c = [0] * self.num_edges() # indicatrix of the middle edges
+ c = [0] * self._ne
for e in cyl[0]:
- c[self._norm(e)] = 1
+ c[e // 2] = 1
C.append(c)
# take intersection of the cylinder twists in the tangent space
@@ -896,52 +1007,6 @@ def is_cylindrical(self, col=None):
mid_edges.discard(i)
return not mid_edges
- def delaunay_cone(self, x_low_bound=0, y_low_bound=0, hw_bound=0, backend=None):
- r"""
- Return the geometric polytope.
-
- EXAMPLES::
-
- sage: from veerer import *
-
- sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
- sage: T.delaunay_cone()
- Cone of dimension 4 in ambient dimension 6 made of 6 facets (backend=ppl)
- sage: T.as_linear_family().delaunay_cone(backend='ppl')
- Cone of dimension 4 in ambient dimension 6 made of 6 facets (backend=ppl)
- sage: T.as_linear_family().delaunay_cone(backend='sage')
- Cone of dimension 4 in ambient dimension 6 made of 6 facets (backend=sage)
-
- An example in genus 2 involving a linear constraint::
-
- sage: vt, s, t = VeeringTriangulations.L_shaped_surface(1, 1, 1, 1)
- sage: f = VeeringTriangulationLinearFamily(vt, [s, t])
- sage: PG = f.delaunay_cone(backend='ppl')
- sage: PG
- Cone of dimension 4 in ambient dimension 14 made of 6 facets (backend=ppl)
- sage: sorted(PG.rays())
- [[0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1],
- [0, 1, 1, 1, 1, 1, 0, 2, 0, 0, 2, 2, 2, 2],
- [0, 1, 1, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 2],
- [0, 2, 2, 2, 2, 2, 0, 1, 1, 1, 0, 0, 0, 1],
- [1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1],
- [1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1],
- [2, 0, 0, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 1]]
- """
- ne = self._subspace.ncols()
- L = LinearExpressions(self.base_ring())
- x = [L.variable(i) for i in range(ne)]
- y = [L.variable(ne + i) for i in range(ne)]
- cs = ConstraintSystem()
- for i in range(ne):
- cs.insert(x[i] >= x_low_bound)
- for i in range(ne):
- cs.insert(y[i] >= y_low_bound)
- self._set_subspace_constraints(cs.insert, x, VERTICAL)
- self._set_subspace_constraints(cs.insert, y, HORIZONTAL)
- self._set_delaunay_constraints(cs.insert, x, y, hw_bound=hw_bound)
- return cs.cone(backend)
-
def random_forward_flip(self, repeat=1):
r"""
Apply a random forward flip randomly among the ones that keeps the family core.
@@ -968,7 +1033,7 @@ def random_forward_flip(self, repeat=1):
cols = [RED, BLUE]
for _ in range(repeat):
e = choice(self.forward_flippable_edges())
- old_col = self._colouring[e]
+ old_col = self._colouring[e // 2]
shuffle(cols)
for c in cols:
self.flip(e, c)
@@ -980,7 +1045,7 @@ def random_forward_flip(self, repeat=1):
else:
self.flip_back(e, old_col)
- def strebel_graph(self, slope=VERTICAL, mutable=False):
+ def strebel_graph(self, slope=VERTICAL, mapping=False, mutable=False):
r"""
EXAMPLES::
@@ -1001,21 +1066,35 @@ def strebel_graph(self, slope=VERTICAL, mutable=False):
sage: vt = VeeringTriangulation("(1,4,~2)(2,~6,~3)(3,~5,~4)", boundary="(0:1)(5:1)(6:1)(~1:1,~0:1)", colouring="BBRRBBB")
sage: F = VeeringTriangulationLinearFamily(vt, [(1, 2, 0, -1, 2, 1, 1), (0, 0, 1, 1, -1, 0, 0)])
sage: F
- VeeringTriangulationLinearFamily("(1,4,~2)(2,~6,~3)(3,~5,~4)", boundary="(0:1)(5:1)(6:1)(~1:1,~0:1)", colouring="BBRRBBB", [(1, 2, 0, -1, 2, 1, 1), (0, 0, 1, 1, -1, 0, 0)])
+ VeeringTriangulationLinearFamily("(1,4,~2)(2,~6,~3)(3,~5,~4)(0:1)(~0:1,~1:1)(5:1)(6:1)", "BBRRBBB", [(1, 2, 0, -1, 2, 1, 1), (0, 0, 1, 1, -1, 0, 0)])
sage: F.strebel_graph()
- StrebelGraphLinearFamily("(0)(1,~2)(2,~3,~1,~0)(3)", [(1, 0, 1, 1), (0, 1, -1, 0)])
+ StrebelGraphLinearFamily("(0)(~0,2,~3,~1)(1,~2)(3)", [(1, 0, 1, 1), (0, 1, -1, 0)])
sage: vt = VeeringTriangulation("(0,5,~4)(2,6,~5)(3,~0,7)(4,~3,~1)", boundary="(1:1)(~7:1)(~6:1)(~2:1)", colouring="RRRBBBBB")
sage: f1 = vt.add_residue_constraints([[1, 1, 1, 0]])
sage: f1.strebel_graph().residue_constraints().echelon_form()
- [1 1 1 0]
- [0 0 0 1]
+ [1 0 1 1]
+ [0 1 0 0]
+
+ An example with folded edges::
+
+ sage: x = polygen(QQ)
+ sage: K. = NumberField(x^2 - 17, embedding=AA(17).sqrt())
+ sage: vt = VeeringTriangulation("", boundary="(0:1)(1:1,~0:1)", colouring="RR")
+ sage: f = VeeringTriangulationLinearFamily(vt, [(1, 1/4*sqrt17 - 3/4)])
+ sage: f.strebel_graph()
+ StrebelGraphLinearFamily("(0)(~0,1)", [(1, 1/4*sqrt17 - 3/4)])
"""
- indices = [e for e in range(self._n // 2) if self.is_half_edge_strebel(e, slope) and self.is_half_edge_strebel(self._ep[e], slope)]
- G = VeeringTriangulation.strebel_graph(self, slope, mutable=False)
- subspace = self.generators_matrix(slope).matrix_from_columns(indices)
- return StrebelGraphLinearFamily(G, subspace, mutable=mutable)
+ indices = []
+ fp = self._fp
+ for e in range(self._ne):
+ if self.is_half_edge_strebel(2 * e, slope) and (fp[2 * e + 1] == -1 or self.is_half_edge_strebel(2 * e + 1)):
+ indices.append(e)
+ G, index_strebel, index_non_strebel = VeeringTriangulation.strebel_graph(self, slope, mapping=True, mutable=False)
+ subspace = self.generators_matrix(slope).matrix_from_columns(indices)
+ sg = StrebelGraphLinearFamily(G, subspace, mutable=mutable)
+ return (sg, index_strebel, index_non_strebel) if mapping else sg
class StrebelGraphLinearFamily(LinearFamily, StrebelGraph):
@@ -1025,7 +1104,7 @@ class StrebelGraphLinearFamily(LinearFamily, StrebelGraph):
sage: from veerer import StrebelGraphLinearFamily
sage: G = StrebelGraphLinearFamily("(0,1,2)(~0,~1:1,~2:2)", [(1, 1, 0), (1, 0, 1)])
sage: G
- StrebelGraphLinearFamily("(0,1,2)(~2:2,~0,~1:1)", [(1, 0, 1), (0, 1, -1)])
+ StrebelGraphLinearFamily("(0,1,2)(~0,~1:1,~2:2)", [(1, 0, 1), (0, 1, -1)])
sage: G.dimension()
2
"""
@@ -1123,14 +1202,14 @@ def veering_triangulations(self, colouring, slope=VERTICAL, mutable=False):
....: print(colouring, sum(1 for _ in G.veering_triangulations(colouring)), sum(1 for _ in G.delaunay_triangulations(colouring)))
....: assert all(vt.strebel_graph() == G for vt in G.veering_triangulations(colouring))
....: assert all(vt.strebel_graph() == G for vt in G.delaunay_triangulations(colouring))
- array('i', [1, 1, 1, 1, 1, 1]) 1 1
- array('i', [1, 1, 2, 2, 1, 1]) 6 2
- array('i', [1, 2, 1, 1, 2, 1]) 3 1
- array('i', [1, 2, 2, 2, 2, 1]) 6 0
- array('i', [2, 1, 1, 1, 1, 2]) 3 0
- array('i', [2, 1, 2, 2, 1, 2]) 3 2
- array('i', [2, 2, 1, 1, 2, 2]) 3 1
- array('i', [2, 2, 2, 2, 2, 2]) 1 1
+ array('i', [1, 1, 1]) 1 1
+ array('i', [1, 1, 2]) 6 2
+ array('i', [1, 2, 1]) 3 1
+ array('i', [1, 2, 2]) 6 0
+ array('i', [2, 1, 1]) 3 0
+ array('i', [2, 1, 2]) 3 2
+ array('i', [2, 2, 1]) 3 1
+ array('i', [2, 2, 2]) 1 1
"""
for vt in StrebelGraph.veering_triangulations(self, colouring, slope, mutable):
switch = vt.generators_matrix(mutable=True)
@@ -1233,7 +1312,7 @@ def prototype_H2(a, b, c, e, mutable=False):
sage: X9.is_delaunay()
True
sage: X9.delaunay_automaton()
- Delaunay automaton with 6 vertices
+ Delaunay automaton with 6 states
sage: X17 = VeeringTriangulationLinearFamilies.prototype_H2(0, 2, 2, -1)
sage: X17.base_ring()
@@ -1241,7 +1320,7 @@ def prototype_H2(a, b, c, e, mutable=False):
sage: X17.is_delaunay()
True
sage: X17.delaunay_automaton() # long time
- Delaunay automaton with 210 vertices
+ Delaunay automaton with 210 states
We check below part of McMullen theorem about connectedness::
@@ -1319,8 +1398,8 @@ def prototype_H1_1(a, b, c, e, mutable=False):
sage: X9 = VeeringTriangulationLinearFamilies.prototype_H1_1(0, 2, 1, -1)
sage: X9.base_ring()
Rational Field
- sage: X9.delaunay_automaton() # long time
- Delaunay automaton with 1244 vertices
+ sage: X9.delaunay_automaton() # long time ~3.5secs
+ Delaunay automaton with 1244 states
"""
# (a+r,c) (a+b,c)
# x-------x------o--x (a+b+r,c)
@@ -1382,10 +1461,13 @@ def triangle_3_4_13_unfolding_orbit_closure():
sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
sage: f = VeeringTriangulationLinearFamilies.triangle_3_4_13_unfolding_orbit_closure()
sage: f
- VeeringTriangulationLinearFamily("(0,9,~8)(1,8,2)(3,11,~10)(4,~14,15)(5,~15,12)(6,~16,13)(7,~0,22)(10,14,~9)(16,~12,~4)(17,20,~18)(18,~5,~23)(19,~22,~21)(21,~13,23)(~20,~6,~17)(~19,~7,~1)(~11,~2,~3)", "BBRBRBRRRRRRRRBRBBRRRBRR", [(1, phi, 0, 0, 0, 1, 0, 0, -phi, -phi - 1, 0, 0, -phi, -phi, phi + 1, -phi - 1, phi, 0, 0, -phi, 0, phi - 1, -1, -1), (0, 0, 1, 0, 0, 0, phi - 1, 0, 1, 1, 1, 1, 0, phi - 1, 0, 0, 0, 0, phi - 1, 0, phi - 1, 0, 0, phi - 1), (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, phi - 1, 0, 0, -phi + 1, 0, 0, 0), (0, 0, 0, 0, 1, 0, 0, phi - 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, phi - 1, 0, 0, phi - 1, 0)])
-
+ VeeringTriangulationLinearFamily("(0,9,~8)(~0,22,7)(1,8,2)(~1,~19,~7)(~2,~3,~11)(3,11,~10)(4,~14,15)(~4,16,~12)(5,~15,12)(~5,~23,18)(6,~16,13)(~6,~17,~20)(~9,10,14)(~13,23,21)(17,20,~18)(19,~22,~21)", "BBRBRBRRRRRRRRBRBBRRRBRR", [(1, phi, 0, 0, 0, 1, 0, 0, -phi, -phi - 1, 0, 0, -phi, -phi, phi + 1, -phi - 1, phi, 0, 0, -phi, 0, phi - 1, -1, -1), (0, 0, 1, 0, 0, 0, phi - 1, 0, 1, 1, 1, 1, 0, phi - 1, 0, 0, 0, 0, phi - 1, 0, phi - 1, 0, 0, phi - 1), (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, phi - 1, 0, 0, -phi + 1, 0, 0, 0), (0, 0, 0, 0, 1, 0, 0, phi - 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, phi - 1, 0, 0, phi - 1, 0)])
sage: f.stratum() # optional - surface_dynamics
Q_4(11, 1)
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
"""
from sage.rings.rational_field import QQ
from sage.rings.number_field.number_field import NumberField
@@ -1396,31 +1478,216 @@ def triangle_3_4_13_unfolding_orbit_closure():
fp = "(0,9,~8)(1,8,2)(10,14,~9)(3,11,~10)(~2,~3,~11)(~14,15,4)(12,5,~15)(~4,16,~12)(13,6,~16)(23,21,~13)(18,~5,~23)(17,20,~18)(~6,~17,~20)(~0,22,7)(~21,19,~22)(~7,~1,~19)"
cols = "BBRBRBRRRRRRRRBRBBRRRBRR"
vt = VeeringTriangulation(fp, cols)
-
- # equations (beyond switches)
- # B = phi A
- # S = phi T
- # C = phi D
- # U = phi V
- # where A = 0, B = 1, C = 2, D = 6, S = 3, T = 17, U = 4, V = 7
K = NumberField(x**2 - x - 1, 'phi', embedding=(AA(5).sqrt() + 1)/2)
phi = K.gen()
L = LinearExpressions(K)
- cs = ConstraintSystem()
- vt._set_switch_conditions(cs.insert, [L.variable(e) for e in range(24)])
- A = L.variable(0)
- B = L.variable(1)
- C = L.variable(2)
- C = L.variable(2)
- D = L.variable(6)
- S = L.variable(3)
- T = L.variable(17)
- U = L.variable(4)
- V = L.variable(7)
- cs.insert(B == phi * A)
- cs.insert(S == phi * T)
- cs.insert(C == phi * D)
- cs.insert(U == phi * V)
+ cs = ConstraintSystem(24)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(24)])
+ cs.insert(L.variable(1) == phi * L.variable(0))
+ cs.insert(L.variable(3) == phi * L.variable(17))
+ cs.insert(L.variable(2) == phi * L.variable(6))
+ cs.insert(L.variable(4) == phi * L.variable(7))
+ return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
+
+ @staticmethod
+ def quadrilateral_1_1_1_7_unfolding_orbit_closure():
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: f = VeeringTriangulationLinearFamilies.quadrilateral_1_1_1_7_unfolding_orbit_closure()
+ sage: f
+ VeeringTriangulationLinearFamily("(0,~14,~18)(~0,~20,13)(1,8,20)(~1,6,~2)(2,19,5)(3,~5,~6)(~3,16,~17)(4,7,~10)(~4,17,~19)(~7,~8,~9)(9,~11,~13)(10,14,~12)(11,15,~16)(12,~15,18)", "RBRBBRRRRBRRRBBBBRRBR", [(1, 0, phi - 1, 0, 0, phi - 1, phi - 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1), (0, 1, 0, 0, 0, -1, -1, phi - 1, 0, -phi + 1, phi - 1, phi, 0, 1, phi - 1, phi - 1, -1, 1, -phi + 1, 1, 1), (0, 0, 0, 1, 0, -1, 0, phi, phi, 0, phi, phi, phi, phi, 0, phi, 0, 1, 0, 1, phi), (0, 0, 0, 0, 1, 0, 0, -phi, -1, phi - 1, -phi + 1, -phi, -phi + 1, -1, 0, -phi + 1, 1, -1, 0, 0, -1)])
+ sage: f.stratum() # optional - surface_dynamics
+ H_4(6)
+ sage: f.base_ring()
+ Number Field in phi with defining polynomial x^2 - x - 1 with phi = 1.618033988749895?
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
+ """
+ from sage.rings.number_field.number_field import NumberField
+ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
+ from sage.rings.qqbar import AA
+ fp = "(0,~14,~18)(~0,~20,13)(1,8,20)(~1,6,~2)(2,19,5)(3,~5,~6)(~3,16,~17)(4,7,~10)(~4,17,~19)(~7,~8,~9)(9,~11,~13)(10,14,~12)(11,15,~16)(12,~15,18)"
+ cols = "RBRBBRRRRBRRRBBBBRRBR"
+ vt = VeeringTriangulation(fp, cols)
+ R = PolynomialRing(QQ, 'x')
+ x = R.gen()
+ K = NumberField(x**2 - x - 1, 'phi', embedding=(1 + AA(5).sqrt()) / 2)
+ phi = K.gen()
+ L = LinearExpressions(K)
+ cs = ConstraintSystem(21)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(21)])
+ cs.insert(L.variable(0) == phi * L.variable(2))
+ cs.insert(L.variable(1) == phi * L.variable(14))
+ cs.insert(L.variable(16) == phi * L.variable(9))
+ cs.insert(L.variable(11) == phi * L.variable(17))
+ return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
+
+ @staticmethod
+ def quadrilateral_1_1_1_9_unfolding_orbit_closure():
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: f = VeeringTriangulationLinearFamilies.quadrilateral_1_1_1_9_unfolding_orbit_closure()
+ sage: f
+ VeeringTriangulationLinearFamily("(0,7,4)(~0,14,13)(1,10,~7)(~1,5,9)(2,~11,~12)(~2,11,6)(3,~8,~5)(~3,~4,~6)(8,~10,12)", "RBRRBRRRBRBBRBR", [(1, 0, 0, 0, -1/2, 0, 1/2, 1/2, 0, 0, -1/2, -1/2, 1/2, -1, 0), (0, 1, 0, 0, 0, -1, 0, 0, 1, 0, 1, 0, 0, 0, 0), (0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 2), (0, 0, 0, 1, 1, 2, 0, 1, -1, 2, -1, 0, 0, 0, 0)])
+ sage: f.stratum() # optional - surface_dynamics
+ Q_1(1^3, -1^3)
+ sage: f.base_ring()
+ Rational Field
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
+ """
+ fp = "(0,7,4)(~0,14,13)(1,10,~7)(~1,5,9)(2,~11,~12)(~2,11,6)(3,~8,~5)(~3,~4,~6)(8,~10,12)"
+ cols = "RBRRBRRRBRBBRBR"
+ vt = VeeringTriangulation(fp, cols)
+ L = LinearExpressions(QQ)
+ cs = ConstraintSystem(15)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(15)])
+ cs.insert(L.variable(0) == 2 * L.variable(6))
+ cs.insert(L.variable(14) == 2 * L.variable(2))
+ return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
+
+ @staticmethod
+ def quadrilateral_1_1_2_8_unfolding_orbit_closure():
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: f = VeeringTriangulationLinearFamilies.quadrilateral_1_1_2_8_unfolding_orbit_closure()
+ sage: f
+ VeeringTriangulationLinearFamily("(0,4,~6)(~0,11,5)(1,~9,10)(~1,7,~3)(2,~7,6)(3,~10,~8)(~4,8,~12)(~5,9,12)", "RRRBRBBRRRBRB", [(1, 0, 0, 0, 1, 1, 0, 0, 1, -1, -1, 2, 0), (0, 1, 0, 0, 1, 2, 1, 1, 2, -1, -2, 2, 1), (0, 0, 1, 0, -1, -2, -1, 0, -3/2, 3/2, 3/2, -2, -1/2), (0, 0, 0, 1, 1, 2, 1, 1, 2, -1, -1, 2, 1)])
+ sage: f.stratum() # optional - surface_dynamics
+ Q_2(6, -1^2)
+ sage: f.base_ring()
+ Rational Field
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
+ """
+ fp = "(0,4,~6)(~0,11,5)(1,~9,10)(~1,7,~3)(2,~7,6)(3,~10,~8)(~4,8,~12)(~5,9,12)"
+ cols = "RRRBRBBRRRBRB"
+ vt = VeeringTriangulation(fp, cols)
+ L = LinearExpressions(QQ)
+ cs = ConstraintSystem(13)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(13)])
+ cs.insert(L.variable(11) == 2 * L.variable(4))
+ return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
+
+ @staticmethod
+ def quadrilateral_1_1_2_12_unfolding_orbit_closure():
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: f = VeeringTriangulationLinearFamilies.quadrilateral_1_1_2_12_unfolding_orbit_closure()
+ sage: f
+ VeeringTriangulationLinearFamily("(0,~6,~14)(1,~4,15)(~1,4,~2)(2,12,~3)(3,17,8)(5,~7,9)(~5,~12,~8)(6,~10,16)(7,~13,10)(~9,18,~11)(11,13,19)(14,~18,~15)", "BBRBRRRBRBRBBRBRBRRR", [(1, 0, 0, 1/2*sqrt2, 0, 0, -1, 0, -1/2*sqrt2, 0, 0, 0, 1/2*sqrt2, 0, 0, 0, 1, -sqrt2, 0, 0), (0, 1, 0, 0, -1, 0, 0, sqrt2 + 1, 0, sqrt2 + 1, 0, sqrt2 + 1, 0, -sqrt2 - 1, 0, 0, 0, 0, 0, 0), (0, 0, 1, 0, 1, 0, -1/2*sqrt2, -1/2*sqrt2 - 1, -1, -1/2*sqrt2 - 1, 1/2*sqrt2, 0, 1, sqrt2 + 1, -1/2*sqrt2, 1, sqrt2, -1, 1/2*sqrt2 + 1, sqrt2 + 1), (0, 0, 0, 0, 0, 1, 1/2*sqrt2, 1/2*sqrt2 + 1, 1, 1/2*sqrt2, 1/2*sqrt2, 0, 0, -1, 1/2*sqrt2, 0, 0, 1, -1/2*sqrt2, -1)])
+ sage: f.stratum() # optional - surface_dynamics
+ Q_1(1^4, -1^4)
+ sage: f.base_ring()
+ Number Field in sqrt2 with defining polynomial x^2 - 2 with sqrt2 = 1.414213562373095?
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
+ """
+ from sage.rings.number_field.number_field import NumberField
+ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
+ from sage.rings.qqbar import AA
+ fp = "(0,~6,~14)(1,~4,15)(~1,4,~2)(2,12,~3)(3,17,8)(5,~7,9)(~5,~12,~8)(6,~10,16)(7,~13,10)(~9,18,~11)(11,13,19)(14,~18,~15)"
+ cols = "BBRBRRRBRBRBBRBRBRRR"
+ vt = VeeringTriangulation(fp, cols)
+ R = PolynomialRing(QQ, 'x')
+ x = R.gen()
+ K = NumberField(x**2 - 2, 'sqrt2', embedding=AA(2).sqrt())
+ sqrt2 = K.gen()
+ L = LinearExpressions(K)
+ cs = ConstraintSystem(20)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(20)])
+ cs.insert(L.variable(0) == sqrt2 * L.variable(3))
+ cs.insert(L.variable(1) == (sqrt2 - 1) * L.variable(11))
+ cs.insert(L.variable(17) == sqrt2 * L.variable(6))
+ cs.insert(L.variable(16) == sqrt2 * L.variable(12))
+ return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
+
+ @staticmethod
+ def quadrilateral_1_2_2_11_unfolding_orbit_closure():
+ r"""
+ EXAMPLES::
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: f = VeeringTriangulationLinearFamilies.quadrilateral_1_2_2_11_unfolding_orbit_closure()
+ sage: f
+ VeeringTriangulationLinearFamily("(0,~17,~6)(~0,~3,4)(1,7,18)(~1,12,3)(2,13,~14)(~2,10,9)(5,~9,~16)(~5,6,~7)(8,11,14)(~10,~13,15)(~11,16,17)", "RBBBRBRRBRBRRRRBRBB", [(1, 0, 0, 0, 1, 1/2*sqrt2, -1/2*sqrt2, 0, 1, 0, 0, -1, 0, 0, 0, 0, 1/2*sqrt2, 1/2*sqrt2 + 1, 0), (0, 1, 0, 0, 0, 1/2*sqrt2, -1/2*sqrt2 - 1, -1, 1, -1/2*sqrt2, 1/2*sqrt2, -1/2*sqrt2 - 1, -1, -1/2*sqrt2, -1/2*sqrt2, 0, 0, 1/2*sqrt2 + 1, 0), (0, 0, 1, 0, 0, -1, sqrt2 + 1, sqrt2, -sqrt2, 1, 0, sqrt2 + 1, 0, 0, 1, 0, 0, -sqrt2 - 1, sqrt2), (0, 0, 0, 1, -1, 0, 0, 0, sqrt2, -1/2*sqrt2, 1/2*sqrt2, -1/2*sqrt2, 1, 1/2*sqrt2, 1/2*sqrt2, sqrt2, -1/2*sqrt2, 0, 0)])
+ sage: f.stratum() # optional - surface_dynamics
+ Q_2(9, -1^5)
+ sage: f.base_ring()
+ Number Field in sqrt2 with defining polynomial x^2 - 2 with sqrt2 = 1.414213562373095?
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
+ """
+ from sage.rings.number_field.number_field import NumberField
+ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
+ from sage.rings.qqbar import AA
+ fp = "(0,~17,~6)(~0,~3,4)(1,7,18)(~1,12,3)(2,13,~14)(~2,10,9)(5,~9,~16)(~5,6,~7)(8,11,14)(~10,~13,15)(~11,16,17)"
+ cols = "RBBBRBRRBRBRRRRBRBB"
+ vt = VeeringTriangulation(fp, cols)
+ R = PolynomialRing(QQ, 'x')
+ x = R.gen()
+ K = NumberField(x**2 - 2, 'sqrt2', embedding=AA(2).sqrt())
+ sqrt2 = K.gen()
+ L = LinearExpressions(K)
+ cs = ConstraintSystem(19)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(19)])
+ cs.insert(L.variable(18) == sqrt2 * L.variable(2))
+ cs.insert(L.variable(15) == sqrt2 * L.variable(3))
+ cs.insert(L.variable(4) == sqrt2 * L.variable(16))
+ cs.insert(L.variable(5) == (sqrt2 - 1) * L.variable(17))
return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
+ @staticmethod
+ def quadrilateral_1_2_2_15_unfolding_orbit_closure():
+ r"""
+ EXAMPLES::
+
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: f = VeeringTriangulationLinearFamilies.quadrilateral_1_2_2_15_unfolding_orbit_closure()
+ sage: f
+ VeeringTriangulationLinearFamily("(0,11,9)(~0,5,20)(1,~11,17)(~1,4,~3)(2,18,~7)(~2,16,7)(3,12,~20)(~4,~16,6)(~5,~18,~13)(~6,~14,22)(8,19,~12)(~8,10,~21)(~9,21,~15)(~10,14,~17)(13,15,~19)", "RBBBRBRRBBRRRRRBBBBRBRB", [(1, 0, 0, 0, 0, 2, phi - 1, phi - 1, -1, phi - 1, 1, phi, 1, phi + 1, phi + 1, phi - 1, -phi + 1, phi, -phi + 1, 2, 1, 0, 2), (0, 1, 0, 0, -1, 0, -1, 0, 0, phi - 1, -phi + 1, phi - 1, 0, 0, 1, 0, 0, phi, 0, 0, 0, -phi + 1, 2), (0, 0, 1, 0, 0, phi, 0, 1, 0, 0, 0, 0, phi, phi, 0, 0, 0, 0, 0, phi, phi, 0, 0), (0, 0, 0, 1, 1, -phi + 1, 0, -1, phi - 1, 0, 0, 0, -phi, -phi, 0, phi - 1, 1, 0, 1, -2*phi + 1, -phi + 1, phi - 1, 0)])
+ sage: f.stratum() # optional - surface_dynamics
+ Q_2(1^5, -1)
+ sage: f.base_ring()
+ Number Field in phi with defining polynomial x^2 - x - 1 with phi = 1.618033988749895?
+ sage: f.dimension()
+ 4
+ sage: f.rank()
+ 2
+ """
+ from sage.rings.number_field.number_field import NumberField
+ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
+ from sage.rings.qqbar import AA
+ fp = "(0,11,9)(~0,5,20)(1,~11,17)(~1,4,~3)(2,18,~7)(~2,16,7)(3,12,~20)(~4,~16,6)(~5,~18,~13)(~6,~14,22)(8,19,~12)(~8,10,~21)(~9,21,~15)(~10,14,~17)(13,15,~19)"
+ cols = "RBBBRBRRBBRRRRRBBBBRBRB"
+ vt = VeeringTriangulation(fp, cols)
+ R = PolynomialRing(QQ, 'x')
+ x = R.gen()
+ K = NumberField(x**2 - x - 1, 'phi', embedding=(1 + AA(5).sqrt()) / 2)
+ phi = K.gen()
+ L = LinearExpressions(K)
+ cs = ConstraintSystem(23)
+ vt._set_subspace_constraints(cs.insert, [L.variable(e) for e in range(23)])
+ cs.insert(L.variable(4) == phi * L.variable(21))
+ cs.insert(L.variable(7) == (phi - 1) * L.variable(12))
+ cs.insert(L.variable(9) == (2 - phi) * L.variable(17))
+ cs.insert(L.variable(11) == (phi - 1) * L.variable(14))
+ return VeeringTriangulationLinearFamily(vt, cs.linear_generators_matrix())
diff --git a/veerer/linear_subvariety.py b/veerer/linear_subvariety.py
new file mode 100644
index 00000000..6afe923a
--- /dev/null
+++ b/veerer/linear_subvariety.py
@@ -0,0 +1,826 @@
+r"""
+Real linear subvarieties in the moduli space of meromorphic Abelian differentials.
+"""
+# ****************************************************************************
+# This file is part of veerer
+#
+# Copyright (C) 2024 Vincent Delecroix
+# 2024 Kai Fu
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# ****************************************************************************
+
+import collections
+import itertools
+import numbers
+import sys
+
+from .automaton import DelaunayStrebelAutomaton
+from .veering_triangulation import VeeringTriangulation
+from .strebel_graph import StrebelGraph
+from .delaunay_strebel_graph import DelaunayStrebelGraph
+from .polyhedron.linear_algebra import is_rank_one
+
+from sage.structure.richcmp import op_LT, op_LE, op_EQ, op_NE, op_GT, op_GE, rich_to_bool
+from sage.misc.cachefunc import cached_method
+from sage.graphs.digraph import DiGraph
+
+# TODO: optimization: vertical/horizontal degenerations commute
+class PrimeDegenerations:
+ r"""
+ Helper class for computing successive degenerations of (prime) linear
+ subvarieties and their decomposition in prime components.
+
+ This class maintains the following attributes:
+
+ - ``_components``: list of ``DelaunayStrebelGraph``
+
+ - ``_to_components``: dictionary whose keys are the union of veering
+ triangulations in the graphs in ``_components`` and the corresponding value
+ is the index in ``_components``.
+
+ - ``_horizontal_degenerations``: a list of lists of dictionaries, the list
+ at index ``i`` encodes the horizontal degenerations of ``_components[i]``.
+ Each key is a tuple of ordered indices ``(i1, ..., ik)`` which corresponds
+ to the decomposition into prime components of a codimension one horizontal
+ degeneration. The corresponding values are the triples ``(vt, edges_up,
+ edges_low)`` where ``vt`` is a veering triangulation in ``_components[i]``
+ and whose degeneration along the given ``edges_up`` and ``edges_low`` gives
+ a WYYISWYG differential in the product of ``_components[i1]``,
+ ``_components[i2]``, ..., ``_components[ik]`` (possibly after applying
+ canonical relabelling).
+
+ - ``vertical_degenerations``: similar to ``_horizontal_degenerations`` but for
+ vertical degenerations. In that case, the keys are pairs of ordered tuples
+ ``((i1, ..., ik), (j1, ..., jl)`` which corresponds to the prime decompositions
+ of the two level obtained after degeneration.
+
+ The data structure is updated after calls to the (low level) methods :meth:`add`,
+ :meth:`compute_vertical_degenerations` and
+ :meth:`compute_horizontal_degenerations`.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.linear_subvariety import PrimeDegenerations
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:2)(~4:2)", "BRRRBBRRRB")
+ sage: D = PrimeDegenerations()
+ sage: D
+ Degenerations of 0 prime components (0 veering triangulations)
+ sage: D.add(vt.delaunay_strebel_graph())
+ 0
+ sage: D
+ Degenerations of 1 prime components (446 veering triangulations)
+ sage: D.compute_vertical_degenerations(0)
+ sage: D
+ Degenerations of 12 prime components (648 veering triangulations)
+ sage: D.compute_horizontal_degenerations(0)
+ sage: D
+ Degenerations of 13 prime components (848 veering triangulations)
+
+ We check below that some of the vertical degeneration information is correct::
+
+ sage: degeneration_indices, root_degenerations = choice(list(D._vertical_degenerations[0].items()))
+ sage: for vt, edges_up, edges_low in root_degenerations:
+ ....: vt_up, vt_low, _, _ = vt.degeneration(edges_up=edges_up, edges_low=edges_low, mutable=True)
+ ....: vt_up = [x[1] for x in vt_up.prime_decomposition(mutable=True)]
+ ....: vt_low = [x[1] for x in vt_low.prime_decomposition(mutable=True)]
+ ....: for comp in vt_up + vt_low:
+ ....: comp.set_canonical_labels()
+ ....: comp.set_immutable()
+ ....: indices_up = tuple(sorted(D._to_components[comp] for comp in vt_up))
+ ....: indices_low = tuple(sorted(D._to_components[comp] for comp in vt_low))
+ ....: assert degeneration_indices == (indices_up, indices_low)
+ """
+ def __init__(self):
+ self._components = [] # list of Delaunay-Strebel graphs of prime veering triangulation
+ self._to_components = {} # mapping: veering triangulation -> position in self._components
+ self._horizontal_degenerations = [] # list of lists of dictionaries
+ self._vertical_degenerations = [] # list of lists of dictionaries
+
+ def __repr__(self):
+ return "Degenerations of {} prime components ({} veering triangulations)".format(len(self._components), len(self._to_components))
+
+ def _check_component_number(self, component_number):
+ if not isinstance(component_number, numbers.Integral):
+ raise TypeError("component_number must be an integer")
+ component_number = int(component_number)
+ if component_number < 0 or component_number >= len(self._components):
+ raise ValueError("component_number (={}) must a positive integer smaller than {}".format(component_number, len(self._components)))
+ return component_number
+
+ def find(self, ds_graph):
+ r"""
+ Find the Delaunay-Strebel graph ``ds_graph`` in the already computed
+ list or add it to the list and return the associated index.
+ """
+ vt = ds_graph.root()
+ if not vt.is_prime():
+ raise ValueError("not prime")
+ if vt in self._to_components:
+ return self._to_components[vt]
+ else:
+ return self.add(ds_graph)
+
+ def add(self, ds_graph):
+ r"""
+ Add the Delaunay-Strebel graph ``ds_graph`` in the list of prime components.
+ """
+ num = len(self._components)
+ self._components.append(ds_graph)
+ self._horizontal_degenerations.append(None)
+ self._vertical_degenerations.append(None)
+ for state in ds_graph._vertices:
+ if isinstance(state, VeeringTriangulation):
+ self._to_components[state] = num
+ return num
+
+ def pending_vertical_degenerations(self):
+ r"""
+ Return the list of component indices whose list of vertical degenerations is still unknown.
+ """
+ return [i for i, degenerations in enumerate(self._vertical_degenerations) if degenerations is None]
+
+ def pending_horizontal_degenerations(self):
+ r"""
+ Return the list of component indices whose list of horizontal degenerations is still unknown.
+ """
+ return [i for i, degenerations in enumerate(self._horizontal_degenerations) if degenerations is None]
+
+ def find_and_decompose(self, f):
+ r"""
+ Given a linear family ``f`` decompose it into prime components and
+ return a triple ``(known_prime_component_indices, unknown_prime_components, all_known_components_are_roots)``
+ """
+ all_roots = True
+
+ if f.is_prime():
+ f.set_canonical_labels()
+ f.set_immutable()
+ if f in self._to_components:
+ component_number = self._to_components[f]
+ all_roots = f == self._components[component_number].root()
+ return (component_number,), (), all_roots
+ else:
+ return (), (f,), all_roots
+ else:
+ prime_components = [comp for atom, comp in f.prime_decomposition(mutable=True, check=True)]
+ if f.is_abelian() and not all(ff.is_abelian() for ff in prime_components):
+ raise ValueError("{}\n{}".format(f, prime_components))
+ prime_components_known = []
+ prime_components_unknown = []
+ for ff in prime_components:
+ ff.set_canonical_labels()
+ ff.set_immutable()
+ if ff in self._to_components:
+ component_number = self._to_components[ff]
+ all_roots = all_roots and ff == self._components[component_number].root()
+ prime_components_known.append(component_number)
+ else:
+ prime_components_unknown.append(ff)
+ prime_components_known.sort()
+ prime_components_unknown.sort()
+ return tuple(prime_components_known), tuple(prime_components_unknown), all_roots
+
+ def compute_horizontal_degenerations(self, component_number):
+ r"""
+ Compute the horizontal degenerations of ``component_number``.
+ """
+ component_number = self._check_component_number(component_number)
+ if self._horizontal_degenerations[component_number] is not None:
+ return
+
+ degenerations = []
+ degenerations_prime_components = set()
+ ds_graph = self._components[component_number]
+ for state in ds_graph._vertices:
+ if isinstance(state, VeeringTriangulation):
+ for edges_up in state.horizontal_degeneration_up_edges_subsets():
+ edges_low = tuple([e for e in range(state._ne) if e not in edges_up])
+ f_up, f_low, _, _ = state.degeneration(edges_up=edges_up, edges_low=edges_low, mutable=True, check=False)
+ assert f_up is None
+ assert f_low.dimension() == state.dimension() - 1
+ known, unknown, all_roots = self.find_and_decompose(f_low)
+ if all_roots:
+ degenerations.append((state, edges_up, edges_low, known, unknown))
+ degenerations_prime_components.update(unknown)
+
+ # NOTE: since we have the full list of Delaunay cells, we do not need to run
+ # the expensive Strebel -> Delaunay (ie we can set backward=False in the
+ # construction of the automata below).
+ from .automaton import DelaunayStrebelAutomaton
+ ds_graph = DelaunayStrebelAutomaton(backward=False)
+ for state in degenerations_prime_components:
+ ds_graph.add_seed(state, setup=False)
+ ds_graph.run()
+ assert set(state for state in ds_graph if isinstance(state, VeeringTriangulation)) == degenerations_prime_components
+
+ for g in ds_graph._graph.connected_components_subgraphs():
+ self.add(DelaunayStrebelGraph(g))
+
+ ans = self._horizontal_degenerations[component_number] = {}
+ for state, edges_up, edges_low, known, unknown in degenerations:
+ if all(x == self._components[self._to_components[x]].root() for x in unknown):
+ degeneration = tuple(sorted(known + tuple(self._to_components[x] for x in unknown)))
+ if degeneration not in ans:
+ ans[degeneration] = []
+ ans[degeneration].append((state, edges_up, edges_low))
+
+ def compute_vertical_degenerations(self, component_number):
+ r"""
+ Compute the vertical degenerations of ``component_number``.
+ """
+ component_number = self._check_component_number(component_number)
+ if self._vertical_degenerations[component_number] is not None:
+ return
+
+ degenerations = []
+ degenerations_prime_components = set()
+ ds_graph = self._components[component_number]
+ for state in ds_graph._vertices:
+ if isinstance(state, VeeringTriangulation):
+ for edges_low in state.vertical_degeneration_low_edges_subsets():
+ edges_up = tuple([e for e in range(state._ne) if e not in edges_low])
+ f_up, f_low, _, _ = state.degeneration(edges_up=edges_up, edges_low=edges_low, mutable=True, check=False)
+ assert f_up is not None, (state,)
+ # NOTE: the projectivization makes us loose one dimension
+ assert f_low.dimension() + f_up.dimension() == state.dimension()
+
+ # too expensive!!
+ # assert f_low.is_delaunay()
+ # too expensive!
+ # assert f_up.is_delaunay()
+
+ known_up, unknown_up, all_roots_up = self.find_and_decompose(f_up)
+ f_up_decomposed = tuple(known_up + unknown_up)
+ degenerations_prime_components.update(unknown_up)
+ known_low, unknown_low, all_roots_low = self.find_and_decompose(f_low)
+ f_low_decomposed = tuple(known_low + unknown_low)
+ degenerations_prime_components.update(unknown_low)
+ if all_roots_up and all_roots_low:
+ degenerations.append((state, edges_up, edges_low, known_up, known_low, unknown_up, unknown_low))
+
+ # NOTE: since we have the full list of Delaunay cells, we do not need to run
+ # the expensive Strebel -> Delaunay
+ from .automaton import DelaunayStrebelAutomaton
+ ds_graph = DelaunayStrebelAutomaton(backward=False)
+ for state in degenerations_prime_components:
+ ds_graph.add_seed(state, setup=False)
+ ds_graph.run()
+ assert set(state for state in ds_graph if isinstance(state, VeeringTriangulation)) == degenerations_prime_components
+
+ for g in ds_graph._graph.connected_components_subgraphs():
+ self.add(DelaunayStrebelGraph(g))
+
+ ans = self._vertical_degenerations[component_number] = {}
+ for state, edges_up, edges_low, known_up, known_low, unknown_up, unknown_low in degenerations:
+ if all(x == self._components[self._to_components[x]].root() for x in unknown_up + unknown_low):
+ degeneration_up = tuple(sorted(known_up + tuple(self._to_components[x] for x in unknown_up)))
+ degeneration_low = tuple(sorted(known_low + tuple(self._to_components[x] for x in unknown_low)))
+ degeneration = (degeneration_up, degeneration_low)
+ if degeneration not in ans:
+ ans[degeneration] = []
+ ans[degeneration].append((state, edges_up, edges_low))
+
+ def compute_all(self):
+ r"""
+ Compute all degenerations up to dimension 0.
+ """
+ vpending = self.pending_vertical_degenerations()
+ hpending = self.pending_horizontal_degenerations()
+ while vpending or hpending:
+ for i in vpending:
+ self.compute_vertical_degenerations(i)
+ for i in hpending:
+ self.compute_horizontal_degenerations(i)
+ vpending = self.pending_vertical_degenerations()
+ hpending = self.pending_horizontal_degenerations()
+
+ def codimension_one_vertical_degenerations(self, ds_graph):
+ r"""
+ Return the codimension one vertical degenerations of the Delaunay-Strebel graph ``ds_graph``.
+
+ If the graph ``ds_graph`` is not already part of the stored prime
+ components it will be added to the list.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.linear_subvariety import PrimeDegenerations
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:2)(~4:2)", "BRRRBBRRRB")
+ sage: D = PrimeDegenerations()
+ sage: ds_graph = vt.delaunay_strebel_graph()
+ sage: D.codimension_one_vertical_degenerations(ds_graph)
+ [((Delaunay-Strebel graph ...), (Delaunay-Strebel graph ...)),
+ ((Delaunay-Strebel graph ...), (Delaunay-Strebel graph ...)),
+ ((Delaunay-Strebel graph ...), (Delaunay-Strebel graph ...)),
+ ((Delaunay-Strebel graph ...), (Delaunay-Strebel graph ...)),
+ ((Delaunay-Strebel graph ...), (Delaunay-Strebel graph ...)),
+ ((Delaunay-Strebel graph ...), (Delaunay-Strebel graph ...))]
+ """
+ component_number = self.find(ds_graph)
+ self.compute_vertical_degenerations(component_number)
+ ans = []
+ for up, low in self._vertical_degenerations[component_number]:
+ ans.append((tuple(self._components[i] for i in up), tuple(self._components[i] for i in low)))
+ return ans
+
+ def codimension_one_horizontal_degenerations(self, ds_graph):
+ r"""
+ Return the codimension one horizontal degenerations of the Delaunay-Strebel graph ``ds_graph``.
+
+ If the graph ``ds_graph`` is not already part of the stored prime
+ components it will be added to the list.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.linear_subvariety import PrimeDegenerations
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:2)(~4:2)", "BRRRBBRRRB")
+ sage: D = PrimeDegenerations()
+ sage: ds_graph = vt.delaunay_strebel_graph()
+ sage: D.codimension_one_horizontal_degenerations(ds_graph)
+ [(Delaunay-Strebel graph of VeeringTriangulationLinearFamily("(0:1)(~0:1,1:1,2:1)(~1:1,~2:1,3:1)(~3:1)", "RRBR", [(1, 0, 0, 1), (0, 1, 0, 0), (0, 0, 1, 0)]) made of
+ 200 veering Delaunay states
+ 6 Strebel states
+ 232 flip transitions
+ 92 rotation transitions
+ 92 Strebel transitions,)]
+ """
+ component_number = self.find(ds_graph)
+ self.compute_horizontal_degenerations(component_number)
+ ans = []
+ for degeneration in self._horizontal_degenerations[component_number]:
+ ans.append(tuple(self._components[i] for i in degeneration))
+ return ans
+
+
+# TODO: change the name; we should reserve the name IrreducibleRealLinearSubvariety for
+# an element in BCGGM compactification
+class IrreducibleRealLinearSubvariety:
+ r"""
+ Irreducible real linear subvariety of the moduli space of multiscale
+ Abelian or quadratic differentials.
+
+ Note that each level is decomposed into prime components.
+
+ TESTS::
+
+ sage: from veerer import *
+ sage: vt = VeeringTriangulation("(1,2,3)(~1,~2,~3)(0:1)(~0:1)", "BRBB")
+ sage: L = vt.linear_subvariety()
+ sage: L # optional - surface_dynamics
+ Irreducible real linear subvariety of projective dimension 1 in [[H_0(0, -1^2)], [H_1(0)]]
+ sage: L.codimension_one_horizontal_degenerations() # optional - surface_dynamics
+ [Irreducible real linear subvariety of projective dimension 0 in [[H_0(0, -1^2)], [H_0(0, -1^2)]]]
+ sage: L.codimension_one_vertical_degenerations()
+ []
+
+ sage: vt = VeeringTriangulationLinearFamily("(0:2,1:2)(~0:2,~1:2)", "RR", [(1, 1)])
+ sage: M = vt.linear_subvariety().multiscale_compactification()
+ sage: M # optional - surface_dynamics
+ MultiscaleCompactification Irreducible real linear subvariety of projective dimension 0 in [[H_0(1^2, -2^2)]]
+ """
+ def __init__(self, ds_graphs):
+ if isinstance(ds_graphs, DelaunayStrebelGraph):
+ ds_graphs = [[ds_graphs]]
+ elif isinstance(ds_graphs, (tuple, list)):
+ ds_graphs_new = []
+ for elt in ds_graphs:
+ if isinstance(elt, DiGraph):
+ ds_graphs_new.append([DelaunayStrebelGraph(elt)])
+ elif isinstance(elt, DelaunayStrebelGraph):
+ ds_graphs_new.append([elt])
+ elif isinstance(elt, (tuple, list)):
+ ds_graphs_new.append(list(elt))
+ else:
+ raise ValueError("invalid input")
+ ds_graphs = ds_graphs_new
+
+ # NOTE: in order to normalize we sort the components
+ levels = list(map(list, ds_graphs))
+ mins = []
+ for j, level in enumerate(levels):
+ levels[j] = sorted(level)
+ self._levels = tuple(map(tuple, levels))
+
+ def _check(self, error=RuntimeError):
+ if not isinstance(self._levels, tuple) or not all(isinstance(level, tuple) for level in self._levels):
+ raise error
+
+ def __hash__(self):
+ return hash(tuple(comp.root() for level in self._levels for comp in level))
+
+ def __eq__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+ return self._levels == other._levels
+
+ def __ne__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+ return self._levels != other._levels
+
+ def _cmp_(self, other):
+ if type(self) is not type(other):
+ raise TypeError("can not compare {} with {}".format(type(self).__name__, type(other).__name__))
+
+ data0 = len(self._levels)
+ data1 = len(other._levels)
+ c = (data0 > data1) - (data0 < data1)
+ if c:
+ return c
+
+ data0 = list(map(len, self._levels))
+ data1 = list(map(len, other._levels))
+ c = (data0 > data1) - (data0 < data1)
+ if c:
+ return c
+
+ data0 = self._levels
+ data1 = other._levels
+ c = (data0 > data1) - (data0 < data1)
+ return c
+
+ def _richcmp_(self, other, op):
+ if type(self) is not type(other):
+ raise TypeError("can not compare {} with {}".format(type(self).__name__, type(other).__name__))
+
+ return rich_to_bool(op, self._cmp_(other))
+
+ def __lt__(self, other):
+ return self._richcmp_(other, op_LT)
+
+ def __le__(self, other):
+ return self._richcmp_(other, op_LE)
+
+ def __gt__(self, other):
+ return self._richcmp_(other, op_GT)
+
+ def __ge__(self, other):
+ return self._richcmp_(other, op_GE)
+
+ def _check_level(self, level):
+ if not isinstance(level, numbers.Integral):
+ raise TypeError("level must be integral")
+ level = int(level)
+ if level < 0:
+ level = -level
+ if not 0 <= level < len(self._levels):
+ raise ValueError("level out of range")
+ return level
+
+ def __repr__(self):
+ return "Irreducible real linear subvariety of projective dimension {} in {}".format(
+ self.projective_dimension(), self.ambient_stratum())
+
+ def num_levels(self):
+ r"""
+ Return the number of levels.
+ """
+ return len(self._levels)
+
+ def levels(self):
+ r"""
+ Return the set of levels
+ """
+ return range(len(self._levels))
+
+ def an_element(self):
+ r"""
+ Return a translation or half-translation surface in this subvariety.
+ """
+ raise NotImplementedError
+
+ def signature(self, level=None):
+ r"""
+ Return the signature (ie order of zeros).
+
+ If level is provided, return a list of degrees. Otherwise, return a
+ list of lists.
+ """
+ if level is None:
+ return tuple(self.signature(level) for level in self.levels())
+ level = self._check_level(level)
+ return tuple(sorted(sum((comp.root().stratum().signature() for comp in self._levels[level]), tuple())))
+
+ def ambient_stratum(self, level=None):
+ r"""
+ Return the ambient stratum of this subvariety.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~3,5,6)(~6,~2,~5)(~4,7,8)(~8,~0,~7)", "RBBBRRBBR")
+ sage: L = vt.linear_subvariety()
+ sage: L.ambient_stratum() # optional - surface_dynamics
+ [[H_2(2)]]
+ sage: for Ldeg in sorted(L.codimension_one_vertical_degenerations()): # optional - surface_dynamics
+ ....: print(Ldeg.ambient_stratum())
+ [[H_1(0)], [H_1(2, -2)]]
+ [[H_1(0^2)], [H_0(2, -2^2)]]
+ """
+ if level is None:
+ return [self.ambient_stratum(level) for level in self.levels()]
+ level = self._check_level(level)
+ return [comp.root().stratum() for comp in self._levels[level]]
+
+ def dimension(self, level=None):
+ r"""
+ Return the ambient stratum of this subvariety.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~3,5,6)(~6,~2,~5)(~4,7,8)(~8,~0,~7)", "RBBBRRBBR")
+ sage: L = vt.linear_subvariety()
+ sage: L.dimension()
+ 4
+ sage: for Ldeg in L.codimension_one_vertical_degenerations():
+ ....: print(Ldeg.dimension())
+ 4
+ 4
+ """
+ if level is None:
+ return sum(self.dimension(level) for level in self.levels())
+ level = self._check_level(level)
+ return sum(comp.root().dimension() for comp in self._levels[level])
+
+ def projective_dimension(self, level=None):
+ r"""
+ Return the projective dimension.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~3,5,6)(~6,~2,~5)(~4,7,8)(~8,~0,~7)", "RBBBRRBBR")
+ sage: L = vt.linear_subvariety()
+ sage: L.projective_dimension()
+ 3
+ sage: for Ldeg in L.codimension_one_vertical_degenerations():
+ ....: print(Ldeg.projective_dimension())
+ 2
+ 2
+ """
+ if level is None:
+ return self.dimension() - len(self._levels)
+ else:
+ level = self._check_level(level)
+ return self.dimension(level) - 1
+
+ def rank(self):
+ raise NotImplementedError
+
+ def delaunay_strebel_graph(self, level):
+ level = self._check_level(level)
+ return self._levels[level]
+
+ def codimension_one_horizontal_degenerations(self, level=None, degeneration_helper=None):
+ r"""
+ Return the list of codimension one horizontal degenerations as a list of Delaunay-Strebel automata.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamilies, DelaunayStrebelAutomaton
+
+ The example of the stratum H(2)::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~3,5,6)(~6,~2,~5)(~4,7,8)(~8,~0,~7)", "RBBBRRBBR")
+ sage: L = vt.linear_subvariety()
+ sage: L.dimension()
+ 4
+ sage: L.codimension_one_horizontal_degenerations() # optional - surface_dynamics
+ [Irreducible real linear subvariety of projective dimension 2 in [[H_1(2, -1^2)]]]
+
+ Degenerations of the eigenform loci of discriminant 17 in the stratum H(1,1). Even though these are
+ two distinct linear subvarieties (distinguished by the spin) they have the same number of codimension
+ one horizontal degenerations::
+
+ sage: a0, b0, c0, e0 = next(VeeringTriangulationLinearFamilies.H2_prototype_parameters(17, spin=0))
+ sage: X17_0 = VeeringTriangulationLinearFamilies.prototype_H2(a0, b0, c0, e0)
+ sage: L0 = X17_0.linear_subvariety() # long time ~5secs # not tested
+ sage: L0.codimension_one_horizontal_degenerations() # long time # optional - surface_dynamics # not tested
+ [Irreducible real linear subvariety of dimension 1 in Q_0(1, -1, -2^2),
+ Irreducible real linear subvariety of dimension 1 in Q_0(1, -1, -2^2),
+ Irreducible real linear subvariety of dimension 1 in Q_0(1, -1, -2^2)]
+
+ sage: a1, b1, c1, e1 = next(VeeringTriangulationLinearFamilies.H2_prototype_parameters(17, spin=1))
+ sage: X17_1 = VeeringTriangulationLinearFamilies.prototype_H2(a1, b1, c1, e1)
+ sage: L1 = X17_1.linear_subvariety() # long time ~5secs # not tested
+ sage: L1.codimension_one_horizontal_degenerations() # long time # optional - surface_dynamics # not tested
+ [Irreducible real linear subvariety of dimension 1 in Q_0(1, -1, -2^2),
+ Irreducible real linear subvariety of dimension 1 in Q_0(1, -1, -2^2),
+ Irreducible real linear subvariety of dimension 1 in Q_0(1, -1, -2^2)]
+ """
+ if level is None:
+ return [degeneration for level in self.levels() for degeneration in self.codimension_one_horizontal_degenerations(level, degeneration_helper)]
+
+ level = self._check_level(level)
+
+ if degeneration_helper is None:
+ degeneration_helper = PrimeDegenerations()
+
+ ans = []
+ ds_graphs = self.delaunay_strebel_graph(level)
+ for ds_graph_num, ds_graph in enumerate(ds_graphs):
+ for ds_degeneration in degeneration_helper.codimension_one_horizontal_degenerations(ds_graph):
+ new_level = ds_graphs[:ds_graph_num] + ds_degeneration + ds_graphs[ds_graph_num + 1:]
+ new_levels = self._levels[:level] + (new_level,) + self._levels[level + 1:]
+ new_subvariety = IrreducibleRealLinearSubvariety(new_levels)
+ assert new_subvariety.projective_dimension() == self.projective_dimension() - 1
+ assert new_subvariety.num_levels() == self.num_levels()
+ ans.append(new_subvariety)
+ return ans
+
+ def codimension_one_vertical_degenerations(self, level=None, degeneration_helper=None):
+ r"""
+ Return the list of codiemsnion one vertical degenerations as a list of Delauany-Strebel automata.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~3,5,6)(~6,~2,~5)(~4,7,8)(~8,~0,~7)", "RBBBRRBBR")
+ sage: L = vt.linear_subvariety()
+ sage: L # optional - surface_dynamics
+ Irreducible real linear subvariety of projective dimension 3 in [[H_2(2)]]
+ sage: deg_first = sorted(L.codimension_one_vertical_degenerations()) # optional - surface_dynamics
+ sage: deg_first # optional - surface_dynamics
+ [Irreducible real linear subvariety of projective dimension 2 in [[H_1(0)], [H_1(2, -2)]],
+ Irreducible real linear subvariety of projective dimension 2 in [[H_1(0^2)], [H_0(2, -2^2)]]]
+ sage: deg_second = []
+ sage: for L1 in deg_first: # optional - surface_dynamics
+ ....: degs = sorted(L1.codimension_one_vertical_degenerations())
+ ....: print(L1, degs)
+ ....: deg_second.extend(degs)
+ Irreducible real linear subvariety of projective dimension 2 in [[H_1(0)], [H_1(2, -2)]] [Irreducible real linear subvariety of projective dimension 1 in [[H_1(0)], [H_0(0^2, -2)], [H_0(2, -2^2)]]]
+ Irreducible real linear subvariety of projective dimension 2 in [[H_1(0^2)], [H_0(2, -2^2)]] [Irreducible real linear subvariety of projective dimension 1 in [[H_1(0)], [H_0(0^2, -2)], [H_0(2, -2^2)]]]
+ sage: for L2 in deg_second: # optional - surface_dynamics
+ ....: assert not list(L2.codimension_one_vertical_degenerations())
+ """
+ if level is None:
+ return [degeneration for level in self.levels() for degeneration in self.codimension_one_vertical_degenerations(level, degeneration_helper)]
+
+ level = self._check_level(level)
+
+ if degeneration_helper is None:
+ degeneration_helper = PrimeDegenerations()
+
+ ds_graphs = self.delaunay_strebel_graph(level)
+ ans = []
+ for ds_graph_num, ds_graph in enumerate(ds_graphs):
+ for (ds_up, ds_low) in degeneration_helper.codimension_one_vertical_degenerations(ds_graph):
+ new_level_up = (ds_graphs[:ds_graph_num] + ds_up + ds_graphs[ds_graph_num+1:],)
+ new_level_low = (ds_low,)
+ new_levels = self._levels[:level] + new_level_up + new_level_low + self._levels[level + 1:]
+ new_subvariety = IrreducibleRealLinearSubvariety(new_levels)
+ assert new_subvariety.num_levels() == self.num_levels() + 1, (new_subvariety.num_levels(), self.num_levels())
+ assert new_subvariety.projective_dimension() == self.projective_dimension() - 1
+ ans.append(new_subvariety)
+ return ans
+
+ def multiscale_compactification(self):
+ r"""
+ Return the multiscale compactification of this linear subvariety.
+ """
+ return MultiscaleCompactification(self)
+
+ # TODO
+ @cached_method
+ def framing_group(self):
+ r"""
+ Return the monodromy of framing obtained by parallel transport in each
+ prime component and exchange of isomorphic components in the same
+ level.
+ """
+ raise NotImplementedError
+
+ def framing_group_element_permutation(self, g, v=None):
+ r"""
+ Given an element of the framing group ``g`` return a quadruple of
+ dictionaries ``(d_vseps, d_fseps, d_cseps, d_fedges)`` encoding
+ permutations of vertex separatrices, face separatrices, infinite
+ cylinders and folded edges.
+
+ The keys and values
+ - for ``d_vseps`` are quadruples ``(level, component, half_edge, angle)``
+ - for ``d_fseps`` are quadruples ``(level, component, half_edge, angle)``
+ - for ``d_cseps`` are triples ``(level, component, half_edge)``
+ - for ``d_fedges`` are triples ``(level, component, half_edge)
+
+ The argument ``v`` is an optional vertex
+ """
+ raise NotImplementedError
+
+
+# TODO: this class could also easily handle LinearSubvariety by not performing
+# any degeneration but accepting a linear family as input
+class MultiscaleCompactification:
+ r"""
+ EXAMPLES:
+
+ The example of H(2)::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,8,~7)(~1,~8,3)(2,7,~6)(~2,~3,4)", "RRRBBBBBB")
+ sage: L = vt.linear_subvariety()
+ sage: M = L.multiscale_compactification()
+ sage: M # optional - surface_dynamics
+ MultiscaleCompactification of Irreducible real linear subvariety of projective dimension 3 in [[H_2(2)]] made of
+ 3 components in codimension 1
+ 5 components in codimension 2
+ 3 components in codimension 3
+
+ Equivalently in Q(1,-1^5)::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~2,3,4)(~4,5,6)", "BRBRBRR")
+ sage: L = vt.linear_subvariety()
+ sage: M = L.multiscale_compactification()
+ sage: M # optional - surface_dynamics
+ MultiscaleCompactification of Irreducible real linear subvariety of projective dimension 3 in [[Q_0(1, -1^5)]] made of
+ 3 components in codimension 1
+ 5 components in codimension 2
+ 3 components in codimension 3
+
+ The example of H(1,1)::
+
+ sage: vt = VeeringTriangulation("(0,8,~7)(~0,~6,7)(1,11,~10)(~1,~11,4)(2,10,~9)(~2,~4,5)(3,9,~8)(~3,~5,6)", "RRRRBBBBBBBB")
+ sage: L = vt.linear_subvariety()
+ sage: M = L.multiscale_compactification()
+ sage: M # optional - surface_dynamics
+ MultiscaleCompactification of Irreducible real linear subvariety of projective dimension 4 in [[H_2(1^2)]] made of
+ 5 components in codimension 1
+ 11 components in codimension 2
+ 13 components in codimension 3
+ 6 components in codimension 4
+
+ Equivalently in Q(2,-1^6)::
+
+ sage: vt = VeeringTriangulation("(0,~6,7)(1,8,4)(2,~4,5)(3,~5,6)", "RRRRBBBBB")
+ sage: L = vt.linear_subvariety()
+ sage: L.multiscale_compactification() # optional - surface_dynamics
+ MultiscaleCompactification of Irreducible real linear subvariety of projective dimension 4 in [[Q_0(2, -1^6)]] made of
+ 5 components in codimension 1
+ 11 components in codimension 2
+ 13 components in codimension 3
+ 6 components in codimension 4
+ """
+ def __init__(self, L):
+ self._L = L
+ self._degeneration_helper = PrimeDegenerations()
+
+ # at position (i, j) = i vertical and j horizontal degenerations
+ self._components = collections.defaultdict(set)
+ self._components[0, 0].add(L)
+ d = L.projective_dimension()
+
+ # vertical degenerations
+ for codim in range(d - 1):
+ for comp in self._components[codim, 0]:
+ for comp_deg in comp.codimension_one_vertical_degenerations(degeneration_helper=self._degeneration_helper):
+ self._components[codim + 1, 0].add(comp_deg)
+
+ for codim in range(d):
+ sys.stdout.flush()
+ h = 0
+ has_horiz = True
+ while has_horiz:
+ sys.stdout.flush()
+ has_horiz = False
+ for comp in self._components[codim, h]:
+ for comp_deg in comp.codimension_one_horizontal_degenerations(degeneration_helper=self._degeneration_helper):
+ has_horiz = True
+ self._components[codim, h + 1].add(comp_deg)
+ h += 1
+
+ def projective_dimension(self):
+ return self._L.projective_dimension()
+
+ def components(self, codim):
+ if codim < 0 or codim > self._L.projective_dimension():
+ raise ValueError("invalid codimension")
+ ans = []
+ for i in range(codim + 1):
+ j = codim - i
+ if (i, j) in self._components:
+ ans.extend(self._components[i, j])
+ return ans
+
+ def __repr__(self):
+ if self.projective_dimension() == 0:
+ return "MultiscaleCompactification {}".format(self._L)
+ s = ["MultiscaleCompactification of {} made of".format(self._L)]
+ for codim in range(1, self.projective_dimension() + 1):
+ s.append("{} components in codimension {}".format(len(self.components(codim)), codim))
+ return "\n".join(s)
diff --git a/veerer/measured_train_track.py b/veerer/measured_train_track.py
deleted file mode 100644
index ad83a837..00000000
--- a/veerer/measured_train_track.py
+++ /dev/null
@@ -1,159 +0,0 @@
-# ****************************************************************************
-# This file is part of veerer
-#
-# Copyright (C) 2018-2023 Vincent Delecroix
-#
-# This program 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.
-#
-# This program 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 this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# ****************************************************************************
-
-from sage.categories.fields import Fields
-
-from sage.structure.sequence import Sequence
-
-from .triangulation import Triangulation
-
-_Fields = Fields()
-
-class MeasuredTrainTrack(object):
- r"""
- Train-track endowed with a transverse measure.
- """
- def __init__(self, triangulation, lengths, base_ring=None):
- self._triangulation = Triangulation(triangulation)
-
- n = self._triangulation.num_half_edges()
- m = self._triangulation.num_edges()
- ep = self._triangulation.edge_permutation(copy=False)
- if len(lengths) == m:
- for e in range(m):
- E = ep[e]
- if e != E and E < m:
- raise ValueError("edge perm not in standard form")
- lengths = list(lengths) + [lengths[ep[e]] for e in range(m,n)]
- if len(lengths) != n:
- raise ValueError('wrong number of vectors')
-
-
- if base_ring is None:
- lengths = Sequence(lengths)
- base_ring = lengths.universe()
- lengths = list(lengths)
- else:
- lengths = [base_ring.coerce(l) for l in lengths]
-
- self._base_ring = base_ring
- self._lengths = tuple(lengths)
-
- fp = self._triangulation.face_permutation(copy=False)
-
- for e in range(n):
- E = ep[e]
- if self._lengths[e] != self._lengths[E]:
- raise ValueError("non-compatible length data")
-
- # we record below whether the given size is (horizontally) big or not
- self._edge_type = [None] * n
-
- for a in range(n):
- b = fp[a]
- c = fp[b]
- if lengths[a] >= lengths[b] and lengths[a] >= lengths[c]:
- if lengths[a] != lengths[b] + lengths[c]:
- raise ValueError("non-compatible length data")
- self._edge_type[a] = 0
- self._edge_type[b] = 1
- self._edge_type[c] = 2
-
- def lengths(self):
- return self._lengths
-
- def __repr__(self):
- return "MeasuredTrainTrack({}, {})".format(
- self._triangulation, self._lengths)
-
- def __call__(self, p, iterations=1):
- r"""
- p = (i,x) where
- i = half-edge
- x = value
-
- EXAMPLES::
-
- sage: from veerer import * # random output due to deprecation warnings from realalg
- sage: v0 = vector((1, 0, 1, 1))
- sage: v1 = vector((0, 1, 1, 1))
- sage: t = Triangulation("(0,1,2)(~0,~1,3)")
- sage: tt = MeasuredTrainTrack(t, 2*v0 + 3*v1)
- sage: tt((2,3/2))
- (4, 3/2)
- sage: tt((2,3/2),2) == tt((4,3/2))
- True
- sage: tt((2,3/2),5) == tt((4,3/2),4)
- True
- """
- it = self.orbit(p)
- for _ in range(2*iterations):
- next(it)
- return next(it)
-
- def orbit(self, p):
- r"""
- Return an iterator through the (infinite) orbit of p.
-
- (intermediate steps are yielded as well)
- """
- n = self._triangulation.num_half_edges()
- ep = self._triangulation.edge_permutation(copy=False)
- fp = self._triangulation.face_permutation(copy=False)
- L = self._lengths
- i, x = p
-
- while True:
- assert 0 <= i < n
- assert 0 < x < self._lengths[i]
- yield (i,x)
-
- # 1. cross the tripode (first involution)
- if self._edge_type[i] == 0:
- # big edge
- b = i
- s1 = fp[b]
- s2 = fp[s1]
- if x < L[s2]:
- i = s2
- x = L[s2] - x
- else:
- i = s1
- x = L[s1] + L[s2] - x
- elif self._edge_type[i] == 1:
- # first small edge
- s1 = i
- s2 = fp[s1]
- b = fp[s2]
- i = b
- x = L[s1] + L[s2] - x
- elif self._edge_type[i] == 2:
- # second small edge
- s2 = i
- b = fp[s2]
- s1 = fp[b]
- i = b
- x = L[s2] - x
-
- yield (i,x)
-
- # 2. pass through the edges (second involution)
- i = ep[i]
- x = L[i] - x
diff --git a/veerer/misc.py b/veerer/misc.py
index a44d0d2b..4dae078e 100644
--- a/veerer/misc.py
+++ b/veerer/misc.py
@@ -32,7 +32,7 @@ def flipper_edge(T, e):
r"""
EXAMPLES::
- sage: from veerer.layout import flipper_edge # random output due to deprecation warnings from realalg
+ sage: from veerer.misc import flipper_edge # random output due to deprecation warnings from realalg
sage: import flipper # optional - flipper
sage: T = flipper.create_triangulation([(0r,1r,2r),(-1r,-2r,-3r)]) # optional - flipper
sage: sorted([flipper_edge(T, e) for e in T.edges]) # optional - flipper
diff --git a/veerer/monodromy.py b/veerer/monodromy.py
new file mode 100644
index 00000000..49df371f
--- /dev/null
+++ b/veerer/monodromy.py
@@ -0,0 +1,413 @@
+r"""
+Monodromy in linear subvarieties
+"""
+# ****************************************************************************
+# This file is part of veerer
+#
+# Copyright (C) 2024 Vincent Delecroix
+# 2024 Kai Fu
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# ****************************************************************************
+
+from array import array
+
+from sage.graphs.digraph import DiGraph
+from sage.groups.perm_gps.permgroup_named import SymmetricGroup
+
+from .constants import RED, BLUE, HORIZONTAL, VERTICAL
+from .permutation import perm_preimage, perm_orbit
+from .veering_triangulation import VeeringTriangulation
+from .labelled_digraph import LabelledDiGraph
+
+
+# TODO: make this a proper morphism from the fundamental group of the labelled digraph
+# to some permutation group of the separatrices
+class SeparatrixMonodromy:
+ r"""
+ Monodromy of separatrices at a zero (or simple pole of quadratic differential) in a prime
+ component.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.monodromy import SeparatrixMonodromy
+ sage: vt = VeeringTriangulation("(0,8,~7)(~0,~6,7)(1,11,~10)(~1,~11,4)(2,10,~9)(~2,~4,5)(3,9,~8)(~3,~5,6)", "RRRRBBBBBBBB")
+ sage: ds_graph = vt.delaunay_strebel_graph() # long time
+ sage: monodromy = SeparatrixMonodromy(ds_graph) # long time
+ sage: path = ds_graph.path(0) # long time
+ sage: path.random_append(10, reverse=False) # long time
+ sage: start = ds_graph.vertex_label(path.start()) # long time
+ sage: end = ds_graph.vertex_label(path.end()) # long time
+ sage: separatrices = start.vertex_separatrices() # long time
+ sage: separatrices_image = [monodromy.vertex_separatrix_transport(path, h, a) for (h, a) in separatrices] # long time
+ sage: separatrices_target = end.vertex_separatrices() # long time
+ sage: assert set(separatrices_image) == set(separatrices_target) # long time
+ """
+ def __init__(self, graph):
+ self._graph = graph
+
+ @staticmethod
+ def _relabelling(relabelling, half_edge, angle):
+ return (relabelling[half_edge], angle)
+
+ @staticmethod
+ def _relabelling_back(relabelling, half_edge, angle):
+ return (perm_preimage(relabelling, half_edge), angle)
+
+ # Transport of vertex separatrices (separatrices of a zero of a simple pole of a quadratic differential)
+
+ @staticmethod
+ def _flip(state, e, col, half_edge, angle):
+ assert half_edge != 2 * e and half_edge != (2 * e + 1)
+ a, b, c, d = state.square_about_half_edge(2 * e, check=False)
+ if half_edge == b:
+ assert angle == 0
+ return (2 * e + 1, 0) if col == RED else (half_edge, angle)
+ elif half_edge == d:
+ assert angle == 0
+ return (2 * e, 0) if col == RED else (half_edge, angle)
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _flip_back(state, e, col, half_edge, angle):
+ if half_edge == 2 * e:
+ assert state._colouring[e] == RED
+ assert angle == 0
+ a, b, c, d = state.square_about_half_edge(2 * e, check=False)
+ return (c, 0)
+ elif half_edge == 2 * e + 1:
+ assert state._colouring[e] == RED
+ assert angle == 0
+ a, b, c, d = state.square_about_half_edge(2 * e, check=False)
+ return (a, 0)
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _rotate_vertex(state, half_edge, angle):
+ # blue half-edge: nothing on angle, always fine
+ # red half-edge: do -1 on angle, need to explore previous if angle=0
+ if state._colouring[half_edge // 2] == RED:
+ if angle == 0:
+ half_edge = state.previous_at_vertex(half_edge)
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ while num_seps == 0:
+ half_edge = state.previous_at_vertex(half_edge)
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ angle = num_seps - 1
+ else:
+ angle -= 1
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _rotate_vertex_back(state, half_edge, angle):
+ # blue half-edge: +1 on angle, if next half-edge is blue and angle=max need to explore next
+ # red half-edge: nothing on angle, if next half-edge is blue and angle=max need to explore next
+ if state._colouring[half_edge // 2] == BLUE:
+ angle += 1
+
+ next_half_edge = state.next_at_vertex(half_edge)
+ if state._colouring[next_half_edge // 2] == BLUE:
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ if num_seps == angle:
+ half_edge = next_half_edge
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ while num_seps == 0:
+ half_edge = state.next_at_vertex(half_edge)
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ angle = 0
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _rotate_face(state, half_edge, angle):
+ if state._colouring[half_edge // 2] == RED:
+ if angle == 0:
+ half_edge = state.next_in_face(half_edge)
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ while num_seps == 1:
+ half_edge = state.next_in_face(half_edge)
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ angle = num_seps - 2
+ else:
+ angle -= 1
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _rotate_face_back(state, half_edge, angle):
+ if state._colouring[half_edge // 2] == BLUE:
+ angle += 1
+
+ next_half_edge = state.previous_in_face(half_edge)
+ if state._colouring[next_half_edge // 2] == BLUE:
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ if num_seps == angle:
+ half_edge = next_half_edge
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ while num_seps == 1:
+ half_edge = state.previous_in_face(half_edge)
+ num_seps = state.half_edge_num_separatrices(half_edge, HORIZONTAL)
+ angle = 0
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _strebel(state, mapping, half_edge, angle):
+ while mapping[half_edge] == -1:
+ half_edge = state.previous_at_vertex(half_edge)
+ angle += state._bdry[half_edge] + (state._colouring[half_edge // 2] == RED and state._colouring[state._vp[half_edge] // 2] == BLUE)
+ return (mapping[half_edge], angle)
+
+ @staticmethod
+ def _strebel_back(state, mapping, half_edge, angle):
+ half_edge = next(h for h in range(len(mapping)) if mapping[h] == half_edge)
+ num_seps = state._bdry[half_edge] + (state._colouring[half_edge // 2] == RED and state._colouring[state._vp[half_edge] // 2] == BLUE)
+ while angle >= num_seps:
+ angle -= num_seps
+ half_edge = state.next_at_vertex(half_edge)
+ num_seps = state._bdry[half_edge] + (state._colouring[half_edge // 2] == RED and state._colouring[state._vp[half_edge] // 2] == BLUE)
+
+ return (half_edge, angle)
+
+ @staticmethod
+ def _infinite_cylinder_strebel(mapping_back, half_edge):
+ half_edge = next(k for k, h in enumerate(mapping_back) if h == half_edge)
+ return half_edge
+
+ @staticmethod
+ def _infinite_cylinder_strebel_back(mapping_back, half_edge):
+ return mapping_back[half_edge]
+
+ def vertex_separatrix_transport(self, path, half_edge, angle):
+ r"""
+ Transport the vertex separatrix ``(half_edge, angle)`` along ``path``.
+ """
+ if path._graph is not self._graph:
+ raise ValueError("invalid path for vertex monodromy")
+
+ for i in path:
+ source = self._graph.vertex_label(self._graph.edge_source(i))
+ target = self._graph.vertex_label(self._graph.edge_target(i))
+ transition = self._graph.edge_label(i)
+
+ reverse = i < 0
+
+ # too expensive!!
+ # source._check_vertex_separatrix(half_edge, angle)
+
+ kind = transition[0]
+ if kind == "flip":
+ edges = transition[1]
+ old_col = transition[2]
+ new_col = transition[3]
+ relabelling = transition[4]
+ if reverse:
+ for e in edges:
+ half_edge, angle = self._flip_back(source, relabelling[2 * e] // 2, old_col, half_edge, angle)
+ half_edge = perm_preimage(relabelling, half_edge)
+ else:
+ for e in edges:
+ half_edge, angle = self._flip(source, e, new_col, half_edge, angle)
+ half_edge = relabelling[half_edge]
+
+ elif kind == "rotate":
+ relabelling = transition[1]
+ if reverse:
+ half_edge, angle = self._rotate_vertex_back(source, half_edge, angle)
+ half_edge = perm_preimage(relabelling, half_edge)
+ else:
+ half_edge, angle = self._rotate_vertex(source, half_edge, angle)
+ half_edge = relabelling[half_edge]
+
+ elif kind == "strebel":
+ mapping = transition[1]
+ if reverse:
+ # NOTE: for Strebel operation the argument is always the veering triangulation
+ half_edge, angle = self._strebel_back(target, mapping, half_edge, angle)
+ else:
+ half_edge, angle = self._strebel(source, mapping, half_edge, angle)
+
+ # too expensive!!
+ # target._check_vertex_separatrix(half_edge, angle)
+
+ return (half_edge, angle)
+
+ def face_separatrix_transport(self, path, half_edge, angle):
+ r"""
+ Transport the face separatrix ``(half_edge, angle)`` along this path.
+ """
+ if path._graph is not self._graph:
+ raise ValueError("invalid path for face monodromy")
+
+ for i in path:
+ source = self._graph.vertex_label(self._graph.edge_source(i))
+ target = self._graph.vertex_label(self._graph.edge_target(i))
+ transition = self._graph.edge_label(i)
+
+ reverse = i < 0
+
+ # too expensive!!
+ source._check_face_separatrix(half_edge, angle)
+
+ kind = transition[0]
+ if kind == "flip":
+ relabelling = transition[4]
+ if reverse:
+ half_edge = perm_preimage(relabelling, half_edge)
+ else:
+ half_edge = relabelling[half_edge]
+
+ elif kind == "rotate":
+ relabelling = transition[1]
+ if reverse:
+ half_edge, angle = self._rotate_face_back(source, half_edge, angle)
+ half_edge = perm_preimage(relabelling, half_edge)
+ else:
+ half_edge, angle = self._rotate_face(source, half_edge, angle)
+ half_edge = relabelling[half_edge]
+
+ elif kind == "strebel":
+ mapping = transition[1]
+ if reverse:
+ # NOTE: for Strebel operation the argument is always the veering triangulation
+ half_edge, angle = source._normalization_face_separatrix(half_edge, angle)
+ half_edge, angle = self._strebel_back(target, mapping, half_edge, angle)
+ else:
+ half_edge, angle = self._strebel(source, mapping, half_edge, angle)
+
+ # too expensive!!
+ target._check_face_separatrix(half_edge, angle)
+
+ return self._graph.vertex_label(path.end())._normalization_face_separatrix(half_edge, angle)
+
+ def infinite_cylinder_transport(self, path, half_edge):
+ r"""
+ TESTS::
+
+ sage: from veerer import VeeringTriangulationLinearFamily
+ sage: from veerer.monodromy import SeparatrixMonodromy
+ sage: vt = VeeringTriangulationLinearFamily("(0:1,1:1,2:1)(~0:1,~1:1,~2:1)", "RRR", [(1, 0, 0), (0, 1, 0), (0, 0, 1)])
+ sage: ds_graph = vt.delaunay_strebel_graph() # long time
+ sage: mono = SeparatrixMonodromy(ds_graph) # long time
+ sage: for i in range(ds_graph.num_edges()): # long time
+ ....: p1 = ds_graph.path(ds_graph.edge_source(i), [i])
+ ....: p2 = ds_graph.path(ds_graph.edge_target(i), [-i-1])
+ ....: for path in [p1, p2]:
+ ....: source = ds_graph.vertex_label(path.start())
+ ....: target = ds_graph.vertex_label(path.end())
+ ....: for h1 in source.boundary_half_edges():
+ ....: h2 = mono.infinite_cylinder_transport(path, h1)
+ ....: assert target.face_angle(h2) == 0
+ """
+ if path._graph is not self._graph:
+ raise ValueError("invalid path for infinite cylinder monodromy")
+
+ for i in path:
+ source = self._graph.vertex_label(self._graph.edge_source(i))
+ target = self._graph.vertex_label(self._graph.edge_target(i))
+ transition = self._graph.edge_label(i)
+
+ reverse = i < 0
+
+ # TODO: remove as too expensive!!
+ assert source.face_angle(half_edge) == 0
+
+ kind = transition[0]
+ if kind == "flip" or kind == "rotate":
+ relabelling = transition[4 if kind == "flip" else 1]
+ if reverse:
+ half_edge = perm_preimage(relabelling, half_edge)
+ else:
+ half_edge = relabelling[half_edge]
+
+ elif kind == "strebel":
+ mapping = transition[1]
+ mapping_back = transition[2]
+ if reverse:
+ half_edge = self._infinite_cylinder_strebel_back(mapping_back, half_edge)
+ else:
+ half_edge = self._infinite_cylinder_strebel(mapping_back, half_edge)
+
+ # TODO: remove as too expensive!!
+ assert target.face_angle(half_edge) == 0
+
+ return min(perm_orbit(self._graph.vertex_label(path.end())._fp, half_edge))
+
+
+def framing_group_element(state, G, vseps0, vseps1, fseps0, fseps1, cseps0, cseps1):
+ r"""
+ Return the group element corresponding to the element mapping the separatrices 0
+ onto the separatrices 1 in the coordinates of separatrices 0.
+
+ INPUT:
+
+ - ``state`` - a veering triangulation or a Strebel graph
+
+ - ``G`` - the framing group of state
+
+ - ``vseps0``, ``vseps1`` - batches of vertex separatrices
+
+ - ``fseps0``, ``fseps`` - batches of face separatrices
+
+ - ``cseps0``, ``cseps1`` - batches of infinite cylinder separatrices
+ """
+ # we choose a non-canonical numbering of separatrices and conjugate at the end
+ seps = [(len(sep), sep) for sep in state.vertex_separatrices(flat=False)]
+ seps.sort()
+ seps_indices = {}
+ seps_angles = {}
+ nv = len(seps)
+ for i, (_, sep) in enumerate(seps):
+ for a, s in enumerate(sep):
+ seps_indices[s] = i
+ seps_angles[s] = a
+ p0 = [seps_indices[s] for s in vseps0]
+ r0 = [seps_angles[s] for s in vseps0]
+ p1 = [seps_indices[s] for s in vseps1]
+ r1 = [seps_angles[s] for s in vseps1]
+ assert len(p0) == len(r0) == len(p1) == len(r1) == nv
+
+ seps = [(len(sep), sep) for sep in state.face_separatrices(flat=False)]
+ seps.sort()
+ seps_indices = {}
+ seps_angles = {}
+ nf = len(seps)
+ for i, (_, sep) in enumerate(seps):
+ for a, s in enumerate(sep):
+ seps_indices[s] = nv + i
+ seps_angles[s] = a
+ p0.extend(seps_indices[s] for s in fseps0)
+ r0.extend(seps_angles[s] for s in fseps0)
+ p1.extend(seps_indices[s] for s in fseps1)
+ r1.extend(seps_angles[s] for s in fseps1)
+ assert len(p0) == len(p1) == len(r0) == len(r1) == nv + nf
+
+ seps_indices = {}
+ nc = len(cseps1)
+ for i, s in enumerate(cseps1):
+ seps_indices[s] = nv + nf + i
+ p0.extend(seps_indices[s] for s in cseps0)
+ r0.extend([0] * nc)
+ p1.extend(seps_indices[s] for s in cseps1)
+ r1.extend([0] * nc)
+ assert len(p0) == len(r0) == len(p1) == len(r1) == nv + nf + nc
+
+ g0 = G(array('i', p0), array('i', r0))
+ g1 = G(array('i', p1), array('i', r1))
+ return g1 * ~g0
diff --git a/veerer/multiscale_veering_triangulation.py b/veerer/multiscale_veering_triangulation.py
new file mode 100644
index 00000000..829556cd
--- /dev/null
+++ b/veerer/multiscale_veering_triangulation.py
@@ -0,0 +1,1655 @@
+r"""
+Multi-scale Veering Triangulations
+"""
+
+from array import array
+import itertools
+import numbers
+
+from sage.structure.element import Matrix
+from sage.rings.integer_ring import ZZ
+from sage.matrix.constructor import matrix
+from sage.matrix.special import identity_matrix
+
+from .permutation import perm_check, perm_cycles, perm_cycles_to_string, str_to_cycles, str_to_cycles_and_data, perm_init
+from .triangulation import Triangulation
+from .veering_triangulation import *
+from .constants import *
+from .polyhedron import *
+from .labelled_digraph import *
+from .monodromy import *
+
+
+# TODO: Store the data of horizontal and vertical nodes in terms of LabeleddDiGraph
+
+def str_to_label(h):
+ r"""
+ Turn a string into the label of the half-edge
+ """
+ if h[0] == '~':
+ lh = 2*int(h[1:]) + 1
+ else:
+ lh = 2*int(h[0:])
+ return lh
+
+def in_connected_component(vt, h):
+ r"""
+ Return the index of the component which contains the half-edge h in the list vt.connected_components()
+ """
+ l_comp = vt.connected_components()
+ for comp in l_comp:
+ if (h // 2) in comp:
+ return l_comp.index(comp)
+
+def track_prong(vt, r_up, r_low, prong):
+
+ m, h, ang = prong
+
+ for v in vt.vertices():
+ if h in v:
+ v1 = v
+ vh = [r_up[h] for h in v1]
+
+ if min(vh) >= 0: #The case that vh is in f_up
+ h = r_up[h]
+ return (m, h, ang)
+ else: #The case that vh is in f_low
+ if max(r_up) == -1:
+ return (m, r_low[h],ang)
+ else:
+ a = 0
+ while r_up[h] >= 0:
+ c1 = vt._colouring[h // 2]
+ h = vt.previous_at_vertex(h)
+ c2 = vt._colouring[h // 2]
+ if (c2 == RED) and (c1 == BLUE):
+ a = a + 1
+ ang = ang + a
+ assert r_low[h] >= 0
+ return ((m[0] - 1,0), r_low[h], ang)
+
+def _vaninshing_red_blue_corner(vt, r_up, r_low, h):
+ hh = vt.next_at_vertex(h)
+ if r_up[h] >= 0 and r_low[hh] >= 0 and vt.boundary_vector()[h] == 0 and vt.half_edge_num_separatrices(h) == 1:
+ return True
+ else:
+ return False
+
+def _new_prong_matching(vt, f_low, r_up, r_low, level, comp):
+
+ nh = 2 * vt.num_edges()
+ newpm = []
+ lpoles = [] #the poles at nodes in f_low considered so far
+ for h in range(nh):
+ hh = vt.next_at_vertex(h)
+ if r_up[h] >= 0 and r_low[hh] >= 0:
+ #find the boundary face in f_low containing hh
+ for f in f_low.boundary_faces():
+ if f_low.next_in_edge(r_low[hh]) in f:
+ f_pole = f
+
+ if f_pole not in lpoles:
+ while ((vt.half_edge_num_separatrices(h) == 0) and (r_up[h] >= 0)) or (_vaninshing_red_blue_corner(vt, r_up, r_low, h)):
+ h = vt.previous_at_vertex(h)
+ if (vt.half_edge_num_separatrices(h) > 0) and (r_up[h] >= 0):
+ vert_sep = ((-abs(level), comp), h, 0)
+ prong1 = ((-abs(level),comp), r_up[h], 0)
+ prong2 = track_prong(vt, r_up, r_low, vert_sep)
+ assert prong2[0][0] == -abs(level) - 1
+ pm = [prong1, prong2]
+ newpm.append(pm)
+ lpoles.append(f_pole)
+ return newpm
+
+def tree_with_target(self, root=0, target_vertices=[]):
+ """
+ Return paths to all target vertices, preserving the order of target_vertices.
+ """
+ tree = [None] * len(self)
+ seen = [False] * len(self)
+ seen[root] = True
+ todo = [root]
+
+ path_dic = {}
+ paths = [None] * len(target_vertices)
+ while todo:
+ v = todo.pop()
+ if v in target_vertices and v not in path_dic:
+ path = []
+ current = v
+ while current != root:
+ edge = tree[current]
+ path.append(edge)
+ current = self.edge_target(edge)
+ path = [-(e + 1) for e in path]
+ path.reverse()
+ path_dic[v] = LabelledDiGraphPath(self, 0, path)
+
+ for i in self.incoming_edges(v):
+ u = self.edge_source(i)
+ if not seen[u]:
+ tree[u] = i
+ seen[u] = True
+ todo.append(u)
+
+ for i, target in enumerate(target_vertices):
+ paths[i] = path_dic.get(target)
+ return paths
+
+class MultiscaleVeeringTriangulation:
+ r"""
+ Multi-scale Veering Triangulations.
+
+ A *multi-scale veering triangulation* is a triangulation of a (nodal) surface such that the restriction to each irreducible component forms a veering triangulation. Additionally, it encodes the information at the nodes.
+
+ INPUT:
+
+ veering_triangulation : a list of length N, where the i-th entry is a list of veering triangulations
+
+ horizontal_nodes : a list of length N, where the i-th entry is a list [l0, l1, ...], satisfying that:
+ - lj encodes the horizontal nodes at the j-th component of the level-i veering triangulations,
+ - the horizontal nodes in each lj is of the form
+ "[h1, h2]"
+ , where the half-edges h1 and h2 are contained in the boundary face of simple poles.
+
+ prong_matching : a list consistings of pairs (prong1,prong2) with
+ prong1 = ((level1, label1), h1, angle1)
+ prong2 = ((level2, label2), h2, angle2)
+ where:
+ - level1 > level2
+ - h1 is an half-edge at a zero of the veering triangulation 'vt1' at level1, and h2 is an hal-edge in a boundary face of the veering triangulation 'vt2' at level2
+ - angle1 and angle2 are the indices of the vertical separatrices in the corner of h1 and h2 respectively satisfying that:
+ the angle1 is in [0, vt1.half_edge_num_separatrices(h1)],
+ and the angle2 is in [0, vt2.half_edge_num_separatrices(h2)].
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ An example with non-trivial glabal residue condition::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(4,~2,3)(~3,5,6)(~6,~0,~1)(11, 12,~10)(8,9,10)(~13, 7, ~9)(13,~11,~12)(~5,~7,14)(~14,~4,~8)", "RBBBRRBBBRRBRRB")
+ sage: vt.stratum()
+ H_3(4)
+ sage: vt.is_delaunay()
+ True
+ sage: edges_low = [4,5,7,8,14]
+ sage: mvt = MultiscaleVeeringTriangulation([vt], [[""]], [])
+ sage: mvt1 = mvt.degeneration(0,0,edges_low=edges_low)
+ sage: mvt1
+ MultiscaleVeeringTriangulation(
+ veering_triangulations=[
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)(3,4,5)(~3,~4,~5)", "RBBRBR", [(1, 0, -1, 0, 0, 0), (0, 1, 1, 0, 0, 0), (0, 0, 0, 1, 0, 1), (0, 0, 0, 0, 1, -1)]),
+ VeeringTriangulationLinearFamily("(~0,~3,~4)(~1,~2,4)(0:2,1:2)(2:2,3:2)", "RRBBB", [(1, 1, 0, 0, -1), (0, 0, 1, 1, 1)])
+ ],
+ horizontal_nodes=[[[]], [[]]],
+ prong_matching=[[((0, 0), 0, 0), ((-1, 0), 0, 0)], [((0, 0), 10, 0), ((-1, 0), 4, 0)]]
+ )
+ sage: vt0 = mvt1._veering_triangulations[0][0]
+ sage: vt1 = mvt1._veering_triangulations[1][0]
+ sage: vt0.stratum()
+ (H_1(0), H_1(0))
+ sage: vt1.stratum()
+ H_1(4, -2^2)
+ sage: vt1.residue_constraints()
+ [1 0]
+ [0 1]
+ """
+
+ def __init__(self, veering_triangulations=None, horizontal_nodes=None, prong_matching=None, check=True):
+
+ if isinstance(veering_triangulations, list):
+ self._veering_triangulations = []
+ NN = len(veering_triangulations)
+ for level in range(NN):
+ vts = veering_triangulations[level]
+ if isinstance(vts, VeeringTriangulation):
+ self._veering_triangulations.append([vts])
+ elif isinstance(vts, list):
+ for vt in vts:
+ if not isinstance(vt, VeeringTriangulation):
+ raise TypeError(f"'vt' (value: {vt}) is not an instance of the VeeringTriangulation.")
+ self._veering_triangulations.append(list(vts))
+ else:
+ raise ValueError(f"The input of veering triangulations {vts} at level-{level} is bad.")
+ else:
+ raise ValueError("The 'veering_triangulations' must be a list.")
+
+ if isinstance(horizontal_nodes, list):
+ if len(horizontal_nodes) != len(self._veering_triangulations):
+ raise ValueError("Miss information for horizontal nodes at some levels")
+ self._horizontal_nodes = []
+ for level in range(len(horizontal_nodes)):
+ self._horizontal_nodes.append([])
+ nodes = horizontal_nodes[level]
+ vts = self._veering_triangulations[level]
+ if len(nodes) != len(vts):
+ raise ValueError(f"Miss information for horizontal nodes at some components at level-{level}")
+ for c in range(len(vts)):
+ self._horizontal_nodes[level].append([])
+ nodes_at_c = []
+ vt = vts[c]
+ if isinstance(nodes[c], list):
+ if all(len(n) == 2 for n in nodes[c]):
+ nodes_at_c = nodes[c]
+ else:
+ raise ValueError(f"The input of horizontal nodes {nodes[c]} is bad.")
+ elif isinstance(nodes[c], str):
+ n = str_to_cycles(nodes[c])
+ for i in range(len(n)):
+ node = n[i]
+ for j in range(2):
+ h = node[j]
+ if h < 0:
+ h = -2 * h - 1
+ else:
+ h = 2 * h
+ node[j] = vt._check_half_edge(h)
+ n[i] = node
+ nodes_at_c = n
+ else:
+ raise ValueError(f"The input of horizontal nodes of the {c}-th component at level-{level} is bad")
+
+ # normalization of horizontal nodes
+ fp = vt.face_permutation()
+ for node in nodes_at_c:
+ h1, h2 = node
+ h1 = min(perm_orbit(fp, h1))
+ h2 = min(perm_orbit(fp, h2))
+ node = tuple(sorted((h1, h2)))
+ if check:
+ self._check_horizontal_node(level, c, node)
+ self._horizontal_nodes[level][c].append(node)
+ self._horizontal_nodes[level][c] = sorted(self._horizontal_nodes[level][c], key=lambda x: x[0])
+ else:
+ raise TypeError("The 'horizontal_nodes' must be a list.")
+
+ if isinstance(prong_matching, list):
+ self._prong_matching = []
+ for pm in prong_matching:
+ prong1, prong2 = pm
+
+ if isinstance(prong1[1], str):
+ h = str_to_label(prong1[1])
+ prong1 = (prong1[0],h,prong1[2])
+ if isinstance(prong2[1], str):
+ h = str_to_label(prong2[1])
+ prong2 = (prong2[0],h,prong2[2])
+
+ pm = [prong1, prong2]
+ if check:
+ self._check_local_prong_matching(pm)
+
+ # adjust prong2 according to our convention that we do not consider the last prong in the each corner
+ m2, h2, ang = prong2
+ level2, c2 = m2
+ vt = self._veering_triangulations[abs(level2)][c2]
+ assert vt.face_angle(h2) != 0
+ last_ang = vt.half_edge_num_separatrices(h2) - 1 #the valid range is between 1 and the number of vertical separatrices
+ while ang == last_ang:
+ h2 = vt.previous_in_face(h2)
+ ang = 0
+ prong2 = ((level2, c2), h2, ang)
+ last_ang = vt.half_edge_num_separatrices(h2) - 1
+ #Note that the resulting prong2 is always equivalent to the original prong2
+
+ pm = [prong1, prong2]
+
+ prongs1, prongs2 = self._local_prong_matching(pm)
+ assert prongs1.index(prong1) == prongs2.index(prong2)
+
+ # normalization of prong1
+ filtered = [prong for prong in prongs1 if prong[2] == 0]
+ prong1 = min(filtered, key=lambda x: x[0])
+ prong2 = prongs2[prongs1.index(prong1)]
+
+ self._prong_matching.append([prong1, prong2])
+ self._prong_matching = sorted(self._prong_matching, key=lambda x: (-x[0][0][0], x[0][0][1], x[0][1]))
+ else:
+ raise ValueError("The 'prong_matching' must be a list.")
+
+ if check:
+ self._check_prong_matching()
+ self._check_horizontal_residue_conditions()
+
+ def _check_level(self, level):
+ r"""
+ Return a level as a positive integer
+ """
+ if not isinstance(level, numbers.Integral):
+ raise TypeError("level must be integral")
+ level = int(level)
+ if level < 0:
+ level = -level
+ if not 0 <= level < self.num_levels():
+ raise ValueError("level out of range")
+ return level
+
+ def _check_horizontal_node(self, level, c, node):
+
+ level = self._check_level(level)
+
+ h1, h2 = node
+ vt = self._veering_triangulations[abs(level)][c]
+
+ if (vt.face_angle(h1) != 0):
+ raise ValueError(f"The half-edge {h1} of {c}-th component at level-{level} is not in the face of simple pole")
+ elif (vt.face_angle(h2) != 0):
+ raise ValueError(f"The half-edge {h2} of {c}-th component at level-{level} is not in the face of simple pole")
+
+ f0, f1 = self.horizontal_faces(level, c, node)
+ if f0 == f1:
+ raise ValueError(f"The half-edge {h1} and {h2} are in the same face of simple pole")
+ for hh in f0:
+ for hhh in f1:
+ if vt._colouring[hh // 2] != vt._colouring[hhh // 2]:
+ raise ValueError(f"The boundary edges in the face of {h1} and the face of {h2} have different colors")
+
+ def _check_horizontal_residue_conditions(self):
+ for level, nodes in enumerate(self._horizontal_nodes):
+ for c, nodes_at_c in enumerate(nodes):
+ for h1, h2 in nodes_at_c:
+ vt = self._veering_triangulations[level][c]
+ v1 = vector(vt.base_ring(), vt.num_edges())
+ for h in perm_orbit(vt._fp, h1):
+ v1[h // 2] += 1
+ v2 = vector(vt.base_ring(), vt.num_edges())
+ for h in perm_orbit(vt._fp, h2):
+ v2[h // 2] += 1
+
+ gens = vt.generators_matrix()
+ if gens * v1 != gens * v2:
+ raise ValueError(f"distinct residues at horizontal node {(h1, h2)} at the {c}-th component at level-{level}")
+
+ def _check_local_prong_matching(self, pm):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.multiscale_veering_triangulation import *
+ sage: vt00 = VeeringTriangulation("(~0,~3,4)(~1,~4,~2)(0:3,1:1,2:5,3:1)","RBRBB")
+ sage: vt01 = VeeringTriangulation("(~0,1,2)(~1,~2,3)(~4,~6,~7)(6,7,~5)(0:5)(~3:1)(4:4,5:4)","BBRBBBRR")
+ sage: pm = [((0, 0), 0,4),((-1, 0), 0, 1)]
+ sage: MultiscaleVeeringTriangulation([vt00,vt01],[[""],[""]],[pm])
+ Traceback (most recent call last):
+ ...
+ ValueError: The angle label of ((0, 0), 0, 4) is out of the valid range [0, 3].
+ sage: pm = [((0, 0),7,0),((-1, 0),0,1)]
+ sage: MultiscaleVeeringTriangulation([vt00,vt01],[[""],[""]],[pm])
+ Traceback (most recent call last):
+ ...
+ ValueError: The corner of ((0, 0), 7, 0) is not a red-blue corner
+ sage: pm = [((0,0),1,0),((-1,0),0,1)]
+ sage: MultiscaleVeeringTriangulation([vt00,vt01],[[""],[""]],[pm])
+ Traceback (most recent call last):
+ ...
+ ValueError: The orders of the zero in ((0, 0), 1, 0) and the pole in ((-1, 0), 0, 1) are not matched
+ """
+
+ N = len(self._veering_triangulations)
+ prong1, prong2= pm
+
+ m1, h1, a1 = prong1
+ m2, h2, a2 = prong2
+ l1, c1 = m1
+ l2, c2 = m2
+
+ #check the levels are valid
+ if (l1 > 0) or (l2 > 0) or (-l1 >= N) or (-l2 >= N):
+ raise ValueError(f"The levels in {pm} are invalid")
+ if (l1 <= l2):
+ raise ValueError(f"The level of {prong1} should be greater than the level of {prong2}")
+
+ vt1 = self._veering_triangulations[abs(l1)][c1]
+ vt2 = self._veering_triangulations[abs(l2)][c2]
+
+ #check the orders of the zero and pole are matched
+ if vt2.face_angle(h2) == 0:
+ raise ValueError(f"The prong {prong2} is contained in a face of simple pole")
+ if (vt1.vertex_angle(h1) + vt2.face_angle(h2) != 0):
+ raise ValueError(f"The orders of the zero in {prong1} and the pole in {prong2} are not matched")
+
+ alpha1 = vt1.boundary_vector()
+ alpha2 = vt2.boundary_vector()
+ col1 = vt1._colouring
+
+ hh1 = vt1.vertex_permutation()[h1]
+
+ #check prong1
+ if (alpha1[h1] == 0) and (a1 != 0):
+ raise ValueError(f"The input {prong1} is bad")
+ elif (alpha1[h1] == 0) and (a1 == 0):
+ if not ((col1[h1 // 2] == RED) and (col1[hh1 // 2] == BLUE)):
+ raise ValueError(f"The corner of {prong1} is not a red-blue corner")
+ elif (alpha1[h1] > 0):
+ num_v = vt1.half_edge_num_separatrices(h1)
+ if a1 not in range(num_v):
+ raise ValueError(f"The angle label of {prong1} is out of the valid range [0, {num_v - 1}].")
+
+ #check prong2
+ if alpha2[h2] == 0:
+ raise ValueError(f"The input {prong2} is not in boundary")
+ else:
+ num_p = vt2.half_edge_num_separatrices(h2)
+ if a2 not in range(num_p):
+ raise ValueError(f"The angle label of {prong2} is out of the valid range [0, {num_p}].")
+
+ def _check_prong_matching(self):
+ pms = self._prong_matching
+ NN = self.num_levels()
+ for level in range(NN):
+ l1 = []
+ l2 = []
+ for pm in pms:
+ p1, p2 = pm
+ if p1[0][0] == level:
+ v = self.vertical_node(pm)[0]
+ if v in l1:
+ raise ValueError(f"There are prongs at the same zero as {pm[0]}")
+ else:
+ l1.append((p1[0][1], v))
+ if p2[0][0] == level:
+ f = self.vertical_node(pm)[1]
+ if f in l2:
+ raise ValueError(f"There are prongs at the same pole as {pm[1]}")
+ else:
+ l2.append((p2[0][1], f))
+
+ def __str__(self):
+ vt_strings = ",\n ".join(", ".join(str(vt) for vt in l) for l in self._veering_triangulations)
+ horizontal_nodes_str = "[" + ", ".join(str(hn) for hn in self._horizontal_nodes) + "]"
+ prong_matching_str = "[" + ", ".join(str(pm) for pm in self._prong_matching) + "]"
+ return (
+ f"MultiscaleVeeringTriangulation(\n"
+ f" veering_triangulations=[\n {vt_strings}\n ],\n"
+ f" horizontal_nodes={horizontal_nodes_str},\n"
+ f" prong_matching={prong_matching_str}\n"
+ f")"
+ )
+
+ def __repr__(self):
+ return str(self)
+
+ def __eq__(self, other):
+ if type(self) != type(other):
+ raise TypeError
+ return (self._veering_triangulations == other._veering_triangulations) and (self._horizontal_nodes == other._horizontal_nodes) and (self._prong_matching == other._prong_matching)
+
+ def __ne__(self, other):
+ if type(self) != type(other):
+ raise TypeError
+ return self._veering_triangulations != other._veering_triangulations and self._horizontal_nodes != other._horizontal_nodes and self._prong_matching != other._prong_matching
+
+ def __hash__(self):
+ veering_triangulations_hashable = tuple(
+ tuple(map(hash, level)) for level in self._veering_triangulations
+ )
+ horizontal_nodes_hashable = tuple(
+ tuple(tuple(tuple(node) for node in group) for group in level
+ ) for level in self._horizontal_nodes
+ )
+ prong_matching_hashable = tuple(
+ tuple(tuple(prong) for prong in match) for match in self._prong_matching
+ )
+ return hash((veering_triangulations_hashable, horizontal_nodes_hashable, prong_matching_hashable))
+
+ def num_levels(self):
+ return len(self._veering_triangulations)
+
+ def horizontal_faces(self, level, c, node):
+ r"""
+ Return a pair of faces corresponding to the node
+ """
+ level = self._check_level(level)
+ h1, h2 = node
+
+ vt = self._veering_triangulations[abs(level)][c]
+
+ f0 = None
+ f1 = None
+ for f in vt.boundary_faces():
+ if h1 in f:
+ f0 = f
+ for f in vt.boundary_faces():
+ if h2 in f:
+ f1 = f
+ if f0 is None or f1 is None:
+ raise ValueError(f"The horizontal node {node} at the {c}-th component at level-{level} is not between boundary faces")
+ return (f0, f1)
+
+ def horizontal_nodes_at_level(self, level):
+ r"""
+ Return the list of horizontal nodes at the level of the input 'i'.
+
+ Each node is represented by a triple (level,comp,face0,face1).
+
+ EXAMPLES::
+ """
+ level = self._check_level(level)
+
+ l = self._horizontal_nodes[abs(level)]
+ list_horiz_nodes = []
+ for comp, nodes in enumerate(l):
+ for node in nodes:
+ f0, f1 = self.horizontal_faces(level, comp, node)
+ list_horiz_nodes.append((-level, comp,f0,f1))
+
+ return list_horiz_nodes
+
+ def vertical_node(self, pm):
+ r"""
+ Return the underlying node of the prong matching.
+
+ The vertical node is represented as a pair of zero and face
+ """
+ p1, p2 = pm
+ m1, h1, _ = p1
+ m2, h2, _ = p2
+ vt1 = self._veering_triangulations[abs(m1[0])][m1[1]]
+ vt2 = self._veering_triangulations[abs(m2[0])][m2[1]]
+ for v in vt1.vertices():
+ if h1 in v:
+ v1 = v
+ for f in vt2.boundary_faces():
+ if h2 in f:
+ f1 = f
+ return (v1, f1)
+
+ def _local_prong_matching(self, pm):
+ r"""
+ Returns the local prong matching at a vertical node.
+
+ The input vertical node is represented by a tube (l1,v,l2,f), where v is the vertex of level-l1 veering triangulation, and f is the boundary face of level-l2 veering triangulation
+
+ The output prong matching is represented as a list containing two sublists: [prongs1, prongs2].
+ Each sublist 'prongsi' is a list of prongs, where each prong is represented as a tuple: (level, half_edge, angle_data).
+ The angle_data specifies which prong within the corner of the half_edge is being referred to.
+ Prongs with the same index in the two sublists are matched.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: from veerer.multiscale_veering_triangulation import *
+ sage: vt00 = VeeringTriangulation("(~0,~3,4)(~1,~4,~2)(0:3,1:1,2:5,3:1)","RBRBB")
+ sage: vt01 = VeeringTriangulation("(~0,1,2)(~1,~2,3)(~4,~6,~7)(6,7,~5)(0:5)(~3:1)(4:4,5:4)","BBRBBBRR")
+ sage: mvt0 = MultiscaleVeeringTriangulation([vt00,vt01],[[""],[""]],[[((0,0),"0",0),((-1,0),"0",1)],[((0,0),"2",0),((-1,0),"5",1)]])
+ sage: mvt0._local_prong_matching([((0,0),0,0),((-1,0),0,1)])
+ [[((0, 0), 0, 0), ((0, 0), 0, 1), ((0, 0), 0, 2), ((0, 0), 0, 3)],
+ [((-1, 0), 0, 1), ((-1, 0), 0, 2), ((-1, 0), 0, 3), ((-1, 0), 0, 0)]]
+ """
+
+ p1, p2 = pm
+ m1, h1, _ = p1
+ m2, h2, _ = p2
+ l1, c1 = m1
+ l2, c2 = m2
+
+ vt1 = self._veering_triangulations[abs(l1)][c1]
+ vt2 = self._veering_triangulations[abs(l2)][c2]
+ alpha1 = vt1.boundary_vector()
+
+ for v in vt1.vertices():
+ if h1 in v:
+ zero = v
+ for f in vt2.boundary_faces():
+ if h2 in f:
+ pole = f
+
+ vp1 = vt1.vertex_permutation()
+
+ prongs1 = [] #list of prongs at the zero
+ prongs2 = [] #list of prongs at the pole
+
+ # compute the list of prongs at the zero.
+ for h in zero:
+ if alpha1[h] == 0: #internal edge
+ hh = vp1[h]
+ if (vt1._colouring[h // 2] == RED) and (vt1._colouring[hh // 2] == BLUE): #h corresponds to a red-blue corner
+ prongs1.append((m1,h,0))
+ else:
+ num_p = vt1.half_edge_num_separatrices(h)
+ for j in range(num_p):
+ prongs1.append((m1,h,j))
+
+ #compute the list of prongs at the pole
+ pole.reverse()
+ for h in pole:
+ num_p = vt2.half_edge_num_separatrices(h) - 1
+ for j in range(num_p):
+ prongs2.append((m2,h,j))
+
+ assert len(prongs1) == len(prongs2)
+
+ # match the prongs. Shift the lists so that the corresponding indices are matched.
+ index1 = prongs1.index(p1)
+ shiftprong1 = prongs1[index1:] + prongs1[:index1]
+
+ index2 = prongs2.index(p2)
+ shiftprong2 = prongs2[index2:] + prongs2[:index2]
+
+ return [shiftprong1, shiftprong2]
+
+ def _components_have_horiztal_nodes_with(self, level, label, comp_index):
+ r"""
+ Return the indices of the connected components that have horizontal nodes with the input component at `label`-th component at level-`level`.
+ """
+ l_node = self.horizontal_nodes_at_level(level)
+
+ l = []
+ for level, s, f1, f2 in l_node:
+ #compute the connected component indices of the face f1 and f2
+ vt = self._veering_triangulations[abs(level)][s]
+ c1 = in_connected_component(vt, f1[0])
+ c2 = in_connected_component(vt, f2[0])
+
+ if (s, c1) == (label, comp_index):
+ l.append((level, s, c2))
+ if (s, c2) == (label, comp_index):
+ if (level,s,c1) not in l:
+ l.append((level,s,c1))
+ return l
+
+ def is_abelian(self, certificate=False):
+ r"""
+ Return whether the multi-scale veering triangulation is Abelian.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ A horizontal node between the same components::
+
+ sage: vt = VeeringTriangulationLinearFamily("(0,1,4)(2,3,~4)(~0:1)(~1:1)(~2:1)(~3:1)", "RBBRR", [(1, 0, 0, 1, 1), (0, 1, 1, -2, -1)])
+ sage: mvt = MultiscaleVeeringTriangulation([vt],[["(~1,~2)"]],[])
+ sage: mvt.is_abelian()
+ False
+
+ Mix of horizontal and vertical nodes::
+ sage: vt0 = VeeringTriangulation("(~0,~3,4)(~1,~4,~2)(0:3,1:1,2:3,3:1)","RBRBB")
+ sage: vt1 = VeeringTriangulationLinearFamily("(~0,1,4)(~1,~2,3)(~5,6,9)(~6,~7,8)(0:5)(2:1)(~3:1)(~4:1)(5:5)(7:1)(~8:1)(~9:1)", "BBRBRBBRBR", [(1, 0, 0, 0, 1, 0, 0, 1, 1, 0), (0, 1, 0, 1, -1, 0, 0, -1, -1, 0), (0, 0, 1, 1, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 1, 0, 0, 0, 1), (0, 0, 0, 0, 0, 0, 1, 0, 1, -1)])
+ sage: vt0.is_abelian()
+ True
+ sage: vt1.is_abelian()
+ True
+ sage: mvt0 = MultiscaleVeeringTriangulation([vt0,vt1],[[""],["(~4,7)"]],[[((0,0),"0",0),((-1,0),"0",1)],[((0,0),"2",0),((-1,0),"5",1)]])
+ sage: mvt0.is_abelian()
+ False
+ sage: vt2 = VeeringTriangulationLinearFamily("(~0,1,4)(~1,~2,3)(~5,6,9)(~6,~7,8)(0:5)(2:1)(~3:1)(~4:1)(5:5)(7:1)(~8:1)(~9:1)", "BBRBRBBRBR", [(1, 0, 0, 0, 1, 0, 0, 0, 0, 0), (0, 1, 0, 1, -1, 0, 0, 1, 1, 0), (0, 0, 1, 1, 0, 0, 0, 1, 1, 0), (0, 0, 0, 0, 0, 1, 0, 0, 0, 1), (0, 0, 0, 0, 0, 0, 1, -1, 0, -1)])
+ sage: mvt1 = MultiscaleVeeringTriangulation([vt0,vt2],[[""],["(~3,~8)"]],[[((0,0),"0",0),((-1,0),"0",1)],[((0,0),"2",0),((-1,0),"5",1)]])
+ sage: print(mvt1.is_abelian(certificate=True))
+ (True, [[[True, False, True, False, False, True, False, True, False, True]], [[False, True, False, True, False, True, False, True, False, True, True, False, True, False, True, False, True, False, True, False]]])
+ """
+
+ N = self.num_levels()
+ oris = [[] for _ in range(N)]
+
+ #Step1: propagate level-wise
+ for i in range(N):
+ vts = self._veering_triangulations[i]
+ for vt in vts:
+ abelian, l = vt.is_abelian(certificate=True)
+ if abelian:
+ oris[i].append(l)
+ else:
+ return (False, None) if certificate else False
+
+ #Step2: propagate through horizontal nodes
+ for i in range(N):
+ vts = self._veering_triangulations[i]
+ l_component = []
+ for vt in vts:
+ l_component.append(vt.connected_components())
+
+ l_nodes = self.horizontal_nodes_at_level(-i)
+ lc = [] #the list of components in the level-(-i) adjusted so far
+ for level, s, f1, f2 in l_nodes:
+ # find out the components containing the matched poles
+ vt = self._veering_triangulations[abs(level)][s]
+
+ c1 = in_connected_component(vt, f1[0])
+ c2 = in_connected_component(vt, f2[0])
+
+ lo = oris[i][s]
+ ne = len(lo)
+
+ if lo[f1[0]] == lo[f2[0]]:
+ #check the coherence
+ if c1 == c2:
+ return (False, None) if certificate else False
+
+ if ((s,c1) in lc) and ((s,c2) in lc):
+ return (False, None) if certificate else False
+
+ if (s,c1) not in lc:
+ #rotate the component c1 by pi
+ for h in range(ne):
+ if h // 2 in l_component[s][c1]:
+ lo[h] = not lo[h]
+ #add coherent components
+ lc.append((s,c1))
+ if (s, c2) not in lc:
+ lc.append((s,c2))
+ elif (s,c2) not in lc:
+ #rotate the component c2 by pi
+ for h in range(ne):
+ if h // 2 in l_component[s][c2]:
+ lo[h] = not lo[h]
+ lc.append((s,c2))
+ else:
+ #add coherent components
+ if (s,c1) not in lc:
+ lc.append((s,c1))
+ if (s,c2) not in lc:
+ lc.append((s,c2))
+
+ oris[i][s] = lo
+
+ #Step3: propagate through vertical nodes
+
+ lv = [] #the list of component adjusted so far, where the component is labeled by (level, index of the component in this level)
+
+ for pm in self._prong_matching:
+
+ m1, h1, ang1 = pm[0]
+ m2, h2, ang2 = pm[1]
+ level1, s1 = m1
+ level2, s2 = m2
+
+ vt1 = self._veering_triangulations[abs(level1)][s1]
+ vt2 = self._veering_triangulations[abs(level2)][s2]
+
+ o1 = oris[-level1][s1][h1]
+ o2 = oris[-level2][s2][h2]
+
+ #decide the orientation of the prong pm[0]
+ if (ang1)%2 == 1:
+ o1 = not o1
+
+ #decide the orientation of the prong pm[1]
+ if (ang2)%2 == 1:
+ o2 = not o2
+
+ #find the labels of the components where the prongs are.
+ #the labels are in the form: (level, index of the component in this level)
+ #moreover find out all the components that have horizontal nodes with the components containing the prongs
+ c1 = in_connected_component(vt1, h1)
+ l1 = self._components_have_horiztal_nodes_with(level1, s1, c1)
+ if (level1, s1, c1) not in l1:
+ l1.append((level1, s1, c1))
+
+ c2 = in_connected_component(vt2, h2)
+ l2 = self._components_have_horiztal_nodes_with(level2,s2, c2)
+ if (level2, s2, c2) not in l2:
+ l2.append((level2, s2, c2))
+
+ #adjust the components
+ lo1 = oris[-level1][s1]
+ n1 = len(lo1)
+ lo2 = oris[-level2][s2]
+ n2 = len(lo2)
+ if o1 != o2:#incoherent
+ if ((level1,s1, c1) in lv) and ((level2, s2, c2) in lv):
+ return (False, None) if certificate else False
+
+ if (level1, s1, c1) not in lv:
+ #rotate all the components in l1 by pi
+ for h in range(n1):
+ c = in_connected_component(vt1, h)
+ if (level1, s1, c) in l1:
+ lo1[h] = not lo1[h]
+
+ for label in l1:
+ lv.append(label)
+
+ if (level2, s2, c2) not in lv:
+ for label in l2:
+ lv.append(label)
+
+ elif (level2,s2, c2) not in lv:
+ #rotate all the components in l2 by pi
+ for h in range(n2):
+ c = in_connected_component(vt2, h)
+ if (level2, s2, c) in l2:
+ lo2[h] = not lo2[h]
+
+ for label in l2:
+ lv.append(label)
+ else: #coherent orientation
+ if (level1, s1, c1) not in lv:
+ for label in l1:
+ lv.append(label)
+ if (level2, s2, c2) not in lv:
+ for label in l2:
+ lv.append(label)
+
+ oris[-level1][s1] = lo1
+ oris[-level2][s2] = lo2
+
+ return (True, oris) if certificate else True
+
+ def degeneration(self, level, component, edges_low=None, edges_up=None):
+ r"""
+ Return the multi-scale veering triangulation by blowing-up the given subset of edges.
+
+ This corresponds to an either a horizontal degeneration or a vertical degeneration in the BCGGM compactification.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ Reach all vertical boundary component of H_1(2) from a single veering triangulaiton (TO BE COMPLETED)::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "BRBBRBBRB")
+ sage: vt.stratum() # optional - surface_dynamics
+ H_2(2)
+ sage: mvt = MultiscaleVeeringTriangulation([vt],[[""]],[])
+ sage: vt.vertical_degeneration_low_edges_subsets()
+ [(1,), (4,), (7,), (4, 7), (1, 4)]
+ sage: mvt1 = mvt.degeneration(0,0, edges_low=(1,))
+ sage: mvt2 = mvt.degeneration(0,0, edges_low=(4,))
+ sage: mvt3 = mvt1.degeneration(0,0, edges_low=(4,))
+
+ More examples::
+
+ sage: vt00 = VeeringTriangulation("(~0,~3,4)(~1,~4,~2)(0:3,1:1,2:5,3:1)","RBRBB")
+ sage: vt01 = VeeringTriangulation("(~0,1,2)(~1,~2,3)(~4,~6,~7)(6,7,~5)(0:5)(~3:1)(4:4,5:4)","BBRBBBRR")
+ sage: mvt0 = MultiscaleVeeringTriangulation([vt00,vt01],[[""],[""]],[[((0,0),"0",0),((-1,0),"0",1)],[((0,0),"2",0),((-1,0),"5",1)]])
+ sage: mvt0.degeneration(-1,0, edges_low=[0, 1, 2, 3, 4, 5], edges_up=[6, 7])
+ MultiscaleVeeringTriangulation(
+ veering_triangulations=[
+ VeeringTriangulation("(~0,~3,4)(~1,~4,~2)(0:3,1:1,2:5,3:1)", "RBRBB"),
+ VeeringTriangulationLinearFamily("(~0,1,2)(~1,~2,3)(0:5)(~3:1)(4:4,5:4)(~4:1)(~5:1)", "BBRBBB", [(1, 0, 1, 1, 0, 0), (0, 1, -1, 0, 0, 0), (0, 0, 0, 0, 1, 1)])
+ ],
+ horizontal_nodes=[[[]], [[(9, 11)]]],
+ prong_matching=[[((0, 0), 0, 0), ((-1, 0), 0, 1)], [((0, 0), 4, 0), ((-1, 0), 10, 1)]]
+ )
+ """
+
+ vts = copy.deepcopy(self._veering_triangulations)
+ vt = vts[abs(level)][component]
+ nh = 2 * (vt.num_edges())
+ ep = vt.edge_permutation()
+
+ f_up ,f_low, r_up, r_low = vt.degeneration(edges_low=edges_low, edges_up=edges_up, collapsed_half_edge_relabelling=True)
+
+ #build list of veering triangulations.
+ #TO BE CONFIRMED: there are choices of the component in the original level ``level`` in the new levels. However, since we will consider all possible vertical degenerations, we could consider just one case that their levels remain the same.
+
+ if f_up is None:
+ vts[abs(level)][component] = f_low
+ else:
+ vts[abs(level)][component] = f_up
+ vts.insert(abs(level) + 1, [f_low])
+
+ #build the horizontal nodes.
+ l1 = [] #new horizontal nodes at (level, component)
+ l2 = [] #new horizontal nodes at (level - 1, 0) if the degeneration has two levels
+ l_horiz = copy.deepcopy(self._horizontal_nodes)
+ #existing horizontal nodes
+ for h1, h2 in self._horizontal_nodes[abs(level)][component]:
+ if r_up[h1] >= 0:
+ h1 = r_up[h1]
+ h2 = r_up[h2]
+ assert h2 >= 0
+ l1.append((h1, h2))
+ else:
+ h1 = r_low[h1]
+ h2 = r_low[h2]
+ assert h1 >= 0
+ assert h2 >= 0
+ if f_up is None:
+ l1.append((h1, h2))
+ else:
+ l2.append((h1, h2))
+ #new horizontal nodes
+ if f_up is None: #horizontal degeneration
+ for h in range(nh):
+ if (r_up[h] == r_low[h] == -1):
+
+ def bdry_of_cyl(vt, r_low, h):
+ while r_low[h] == -1:
+ h = vt.previous_at_vertex(h)
+ return r_low[h]
+
+ b1 = bdry_of_cyl(vt, r_low, h)
+ h1 = ep[h]
+ b2 = bdry_of_cyl(vt, r_low, h1)
+
+ #add the corresponding horizontal node
+ assert f_low.face_angle(b1) == f_low.face_angle(b2) == 0
+ for f in f_low.boundary_faces():
+ if b1 in f:
+ b1 = min(f)
+ if b2 in f:
+ b2 = min(f)
+ node = tuple(sorted((b1,b2)))
+ if node not in l1:
+ l1.append(node)
+ l_horiz[abs(level)][component] = l1
+ else:
+ l_horiz.insert(abs(level) + 1, [[]])
+ l_horiz[abs(level)][component] = l1
+ l_horiz[abs(level) + 1][0] = l2
+
+ #build the vertical nodes
+ l_pm = []
+ #existing vertical nodes
+ original_pm = copy.deepcopy(self._prong_matching)
+ for pm in original_pm:
+ prong1, prong2 = pm
+ if prong1[0] == (-abs(level), component):
+ prong1 = track_prong(vt, r_up, r_low, prong1)
+ if f_up is None:
+ l_pm.append([prong1,prong2])
+ else:
+ prong2 = ((prong2[0][0] - 1,prong2[0][1]), prong2[1], prong2[2])
+ l_pm.append([prong1,prong2])
+ elif prong2[0] == (-abs(level), component):
+ prong2 = track_prong(vt, r_up, r_low, prong2)
+ l_pm.append([prong1,prong2])
+ else:
+ l_pm.append(pm)
+ #new vertical nodes
+ if f_up is not None:
+ l_pm = l_pm + _new_prong_matching(vt, f_low, r_up, r_low, -abs(level), component)
+
+ #build the degeneration
+ return MultiscaleVeeringTriangulation(vts,l_horiz,l_pm)
+
+ def codimension_one_horizontal_degenerations(self, level=None, component=None):
+ r"""
+ Iterator through the list of codimension one horizontal degenerations.
+
+ INPUT:
+
+ - ``level`` -- optional integer
+
+ - ``component`` -- optional integer
+ """
+ if level is None:
+ for level in range(self.num_levels()):
+ for component in range(len(self._veering_triangulations[level])):
+ yield from self.codimension_one_horizontal_degenerations(level, component)
+ return
+ if component is None:
+ for component in range(len(self._veering_triangulations[level])):
+ yield from self.codimension_one_horizontal_degenerations(level, component)
+ return
+
+ for edges in self._veering_triangulations[level][component].horizontal_degeneration_up_edges_subsets():
+ yield self.degeneration(level, component, edges_up=edges)
+
+ def codimension_one_vertical_degenerations(self, level=None, component=None):
+ r"""
+ Iterator through the list of codimension one vertical degenerations.
+
+ INPUT:
+
+ - ``level`` -- optional integer
+
+ - ``component`` -- optional integer
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ sage: vt = VeeringTriangulation("(~0,2,3)(~1,4,5)(~2,6,7)(~3,~5,8)(~4,9,10)(~6,11,12)(~7,13,14)(~8,~12,15)(~9,16,~15)(~11,17,18)(~14,~18,19)(~16,~17,20)(0:1)(1:1)(~10:1)(~13:1)(~19:1)(~20:1)", "BBRRRBBBRBBRRBRBRRBBB")
+ sage: mvt = MultiscaleVeeringTriangulation(veering_triangulations=[vt], horizontal_nodes=[[[(0, 2), (21, 27), (39, 41)]]], prong_matching=[])
+ sage: for mvt2 in mvt.codimension_one_vertical_degenerations():
+ ....: print(mvt2)
+ """
+ if level is None:
+ for level in range(self.num_levels()):
+ for component in range(len(self._veering_triangulations[level])):
+ yield from self.codimension_one_vertical_degenerations(level, component)
+ return
+ if component is None:
+ for component in range(len(self._veering_triangulations[level])):
+ yield from self.codimension_one_vertical_degenerations(level, component)
+ return
+
+ for edges in self._veering_triangulations[level][component].vertical_degeneration_low_edges_subsets():
+ yield self.degeneration(level, component, edges_low=edges)
+
+ def transport_along_path(self, level, component, path):
+ r"""
+ Return the multi-scale Veering triangulation obtained by deforming the prime veering triangulation at ``(level, component)`` along the path in the Delaunay-Strebel graph.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,9)(~8,10,11)(~9,~10,~11)", "RRBBRRRRBBBR")
+ sage: edges_up = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ sage: mvt0 = MultiscaleVeeringTriangulation([vt], [[""]], [])
+ sage: mvt = mvt0.degeneration(0,0,edges_up=edges_up)
+ sage: mvt
+ MultiscaleVeeringTriangulation(
+ veering_triangulations=[
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRRRRB", [(1, 0, -1, -1, 0, -1, -1, 0, 1), (0, 1, 1, 1, 0, 1, 1, 0, -1), (0, 0, 0, 0, 1, 1, 1, 0, -1), (0, 0, 0, 0, 0, 0, 0, 1, 1)]),
+ VeeringTriangulationLinearFamily("(0:4,~0:4)", "R", [(1)])
+ ],
+ horizontal_nodes=[[[]], [[]]],
+ prong_matching=[[((0, 0), 11, 0), ((-1, 0), 0, 0)]]
+ )
+ sage: vt0 = mvt._veering_triangulations[0][0]
+ sage: ds_graph = vt0.delaunay_strebel_graph()
+ sage: path = LabelledDiGraphPath(ds_graph, 0, [3, 13, 16])
+ sage: mvt.transport_along_path(0, 0, path)
+ MultiscaleVeeringTriangulation(
+ veering_triangulations=[
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "BRBBRRRBR", [(1, 0, 1, 1, 0, 1, 1, 0, 1), (0, 1, 1, 1, 0, 1, 1, 0, 1), (0, 0, 0, 0, 1, 1, 1, 0, 1), (0, 0, 0, 0, 0, 0, 0, 1, -1)]),
+ VeeringTriangulationLinearFamily("(0:4,~0:4)", "R", [(1)])
+ ],
+ horizontal_nodes=[[[]], [[]]],
+ prong_matching=[[((0, 0), 16, 0), ((-1, 0), 0, 0)]]
+ )
+ """
+ ds_graph = path._graph
+ vt0 = ds_graph.vertex_label(path.start())
+ vt1 = ds_graph.vertex_label(path.end())
+ assert self._veering_triangulations[abs(level)][component] == vt0
+
+ vts = copy.deepcopy(self._veering_triangulations)
+ vts[abs(level)][component] = vt1
+
+ mono = SeparatrixMonodromy(ds_graph)
+
+ #new horizontal nodes
+ l1 = [] #new horizontal nodes at (level, component)
+ l_horiz = copy.deepcopy(self._horizontal_nodes)
+ for h1, h2 in l_horiz[abs(level)][component]:
+ h1 = mono.infinite_cylinder_transport(path, h1)
+ h2 = mono.infinite_cylinder_transport(path, h2)
+ l1.append((h1,h2))
+ l_horiz[abs(level)][component] = l1
+
+ #new vertical nodes
+ l2 = []
+ l_vert = copy.deepcopy(self._prong_matching)
+ for pm in l_vert:
+ p1, p2 = pm
+ m1, h1, ang1 = p1
+ m2, h2, ang2 = p2
+ if m1 == (-abs(level), component):
+ h1, ang1 = mono.vertex_separatrix_transport(path, h1, ang1)
+ if m2 == (-abs(level), component):
+ h2, ang2 = mono.face_separatrix_transport(path, h2, ang2)
+ l2.append([(m1, h1, ang1),(m2, h2, ang2)])
+ l_vert = l2
+
+ return MultiscaleVeeringTriangulation(vts,l_horiz,l_vert)
+
+ def prime_decomposition(self, level, component):
+ r"""
+ Return a prime decomposition of a component at ``(level, component)`` of multi-scale veering triangulation.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ sage: vt = VeeringTriangulation("(0,1,2)(4,~2,3)(~3,5,6)(~6,~0,~1)(11, 12,~10)(8,9,10)(~13, 7, ~9)(13,~11,~12)(~5,~7,14)(~14,~4,~8)", "RBBBRRBBBRRBRRB")
+ sage: edges_low = [4,5,7,8,14]
+ sage: mvt = MultiscaleVeeringTriangulation([vt], [[""]], [])
+ sage: mvt1 = mvt.degeneration(0,0,edges_low=edges_low)
+ sage: mvt1
+ MultiscaleVeeringTriangulation(
+ veering_triangulations=[
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)(3,4,5)(~3,~4,~5)", "RBBRBR", [(1, 0, -1, 0, 0, 0), (0, 1, 1, 0, 0, 0), (0, 0, 0, 1, 0, 1), (0, 0, 0, 0, 1, -1)]),
+ VeeringTriangulationLinearFamily("(~0,~3,~4)(~1,~2,4)(0:2,1:2)(2:2,3:2)", "RRBBB", [(1, 1, 0, 0, -1), (0, 0, 1, 1, 1)])
+ ],
+ horizontal_nodes=[[[]], [[]]],
+ prong_matching=[[((0, 0), 0, 0), ((-1, 0), 0, 0)], [((0, 0), 10, 0), ((-1, 0), 4, 0)]]
+ )
+ sage: mvt1.prime_decomposition(0, 0)
+ MultiscaleVeeringTriangulation(
+ veering_triangulations=[
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RBB", [(1, 0, -1), (0, 1, 1)]), VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)]),
+ VeeringTriangulationLinearFamily("(~0,~3,~4)(~1,~2,4)(0:2,1:2)(2:2,3:2)", "RRBBB", [(1, 1, 0, 0, -1), (0, 0, 1, 1, 1)])
+ ],
+ horizontal_nodes=[[[], []], [[]]],
+ prong_matching=[[((0, 0), 0, 0), ((-1, 0), 0, 0)], [((0, 1), 0, 0), ((-1, 0), 4, 0)]]
+ )
+ """
+ vts = copy.deepcopy(self._veering_triangulations)
+ vt = vts[abs(level)][component]
+
+ l = copy.deepcopy(vt.prime_decomposition())
+
+ #store the prime component and mapping to the canonical labels
+ l_comp = []
+ l_vts_canonical = []
+ l_mapping = []
+
+ for prime in l:
+ l_comp.append(prime[0])
+
+ vt_prime = copy.deepcopy(prime[1])
+ vt_prime._mutable = True
+ mapping = vt_prime.set_canonical_labels(mapping=True)
+ vt_prime.set_immutable()
+
+ l_vts_canonical.append(vt_prime)
+ l_mapping.append(mapping)
+
+ #build veering triangulations
+ vts[abs(level)][component : component + 1] = l_vts_canonical
+
+ def label_in_prime_comp(h, l_comp, l_mapping):
+ for edges in l_comp:
+ if h // 2 in edges:
+ c = l_comp.index(edges)
+ mapping = l_mapping[c]
+ i = edges.index(h // 2)
+ if h%2 == 0:
+ return (c, mapping[2 * i])
+ else:
+ return (c, mapping[2 * i + 1])
+
+ #new horizontal nodes
+ l1 = [[] for i in range(len(l))]
+ l_horiz = copy.deepcopy(self._horizontal_nodes)
+ for h1, h2 in l_horiz[abs(level)][component]:
+ cp1, h1 = label_in_prime_comp(h1, l_comp, l_mapping)
+ cp2, h2 = label_in_prime_comp(h2, l_comp, l_mapping)
+ assert cp1 == cp2
+ l1[cp1].append((h1,h2))
+ l_horiz[abs(level)][component:component + 1] = l1
+
+ #new vertical nodes
+ l2 = []
+ l_vert = copy.deepcopy(self._prong_matching)
+ for pm in l_vert:
+ p1, p2 = pm
+ m1, h1, ang1 = p1
+ m2, h2, ang2 = p2
+ if m1 == (-abs(level), component):
+ cp1, h1 = label_in_prime_comp(h1, l_comp, l_mapping)
+ p1 = ((-abs(level), cp1 + component), h1, ang1)
+ if m2 == (-abs(level), component):
+ cp2, h2 = label_in_prime_comp(h2, l_comp, l_mapping)
+ p2 = ((-abs(level), cp2 + component), h2, ang2)
+ if m1[0] == -abs(level) and m1[1] > component:
+ p1 = ((-abs(level), m1[1] + len(l_vts_canonical) - 1), h1, ang1)
+ if m2[0] == -abs(level) and m2[1] > component:
+ p2 = ((-abs(level), m2[1] + len(l_vts_canonical) - 1), h2, ang2)
+ l2.append([p1,p2])
+ l_vert = l2
+
+ return MultiscaleVeeringTriangulation(vts,l_horiz,l_vert)
+
+ def transport_by_monodromy(self, lc, ld):
+ r"""
+ Return a list of multi-scale veering triangulations that is obtained by the actions of the monodromy of the Delaunay-Strebel graph in the list ``ld`` on the input multi-scale veering triangulation.
+
+ Input:
+ - lc is a list of indicise of components of the multi-scale Veering triangulation, and each index is given by (level, component index).
+ - ld is a list of Delaunay-Strebel graphs corresponding to the components in lc.
+
+ EXAMPLES:
+
+ """
+
+ from itertools import product
+ l_g = []
+ l_info = []
+ for dsg in ld:
+ i = ld.index(dsg)
+ level, c = lc[i]
+ vt = self._veering_triangulations[abs(level)][c]
+
+ vertex_separatrices = vt.vertex_separatrices(flat=True)
+ vertex_separatrix_index = {ha: i for i, ha in enumerate(vertex_separatrices)}
+ nv = len(vertex_separatrices)
+
+ face_separatrices = vt.face_separatrices(flat=True)
+ face_separatrix_index = {ha: nv + i for i, ha in enumerate(face_separatrices)}
+ nf = len(face_separatrices)
+
+ infinite_cylinders = [min(f) for f in vt.boundary_faces() if vt.face_angle(f[0]) == 0]
+ infinite_cylinder_index = {h: nv + nf + i for i, h in enumerate(infinite_cylinders)}
+ nc = len(infinite_cylinders)
+
+ n = nv + nf + nc
+
+ l_info.append((vertex_separatrices, vertex_separatrix_index, face_separatrices, face_separatrix_index, infinite_cylinders, infinite_cylinder_index, nv, nf, nc, n))
+
+ monog0 = monodromy(dsg)
+ monog = [perm_init(f"{p}", n=n) for p in monog0]
+ l_g.append(monog)
+
+ from itertools import product
+ G = product(*l_g)
+ lmvts = set()
+ for g in G:
+ #do not change veering triangulations
+ vts = copy.deepcopy(self._veering_triangulations)
+
+ #new horizontal nodes
+ l_horiz = copy.deepcopy(self._horizontal_nodes)
+ for level, nodes in enumerate(l_horiz):
+ for c, nodes_at_c in enumerate(nodes):
+ if (-abs(level), c) in lc:
+ l1 = []
+ i = lc.index((-abs(level), c))
+ vertex_separatrices, vertex_separatrix_index, face_separatrices, face_separatrix_index, infinite_cylinders, infinite_cylinder_index, nv, nf, nc, n = l_info[i]
+ for h1, h2 in nodes_at_c:
+ a = infinite_cylinder_index[h1]
+ aa = g[i][a]
+ h1 = infinite_cylinders[aa - nv - nf]
+
+ a = infinite_cylinder_index[h2]
+ aa = g[i][a]
+ h2 = infinite_cylinders[aa - nv - nf]
+ l1.append((h1,h2))
+ l_horiz[abs(level)][c] = l1
+
+ #new vertical nodes
+ l2 = []
+ l_vert = copy.deepcopy(self._prong_matching)
+ for pm in l_vert:
+ p1, p2 = pm
+ m1, h1, ang1 = p1
+ m2, h2, ang2 = p2
+
+ if m1 in lc:#the zero of the node is contained in the component
+ i = lc.index(m1)
+ vertex_separatrices, vertex_separatrix_index, face_separatrices, face_separatrix_index, infinite_cylinders, infinite_cylinder_index, nv, nf, nc, n = l_info[i]
+
+ a = vertex_separatrix_index[(h1, ang1)]
+ aa = g[i][a]
+ h1, ang1 = vertex_separatrices[aa]
+ if m2 in lc: #the pole of the node is contained in the component
+ i = lc.index(m2)
+ vertex_separatrices, vertex_separatrix_index, face_separatrices, face_separatrix_index, infinite_cylinders, infinite_cylinder_index, nv, nf, nc, n = l_info[i]
+
+ a = face_separatrix_index[(h2, ang2)]
+ aa = g[i][a]
+ h2, ang2 = face_separatrices[aa - nv]
+ l2.append([(m1, h1, ang1),(m2, h2, ang2)])
+ l_vert = l2
+
+ mvtnew = MultiscaleVeeringTriangulation(vts,l_horiz,l_vert)
+ lmvts.add(mvtnew)
+ return list(lmvts)
+
+ def transport_by_permutation_in_levels(self):
+ r"""
+ Return the list of multi-scale veering triangulations by permutes the same components in each level.
+ """
+ N = self.num_levels()
+
+ #generate the level-wise permutation groups:
+ l_g = []
+ for i in range(N):
+ vts = self._veering_triangulations[i]
+ #find the same components
+ labels = []
+ label_map = {}
+ k = -1
+ for vt in vts:
+ if vt not in label_map:
+ k += 1
+ label_map[vt] = k
+ labels.append(label_map[vt])
+ #generate the symmetry group
+ m = max(labels)
+ generators = []
+ for i in range(m + 1):
+ indices = [idx for idx, label in enumerate(labels) if label == i]
+ generators.extend([(indices[k], indices[k + 1]) for k in range(len(indices) - 1)])
+ n = len(vts)
+ S = SymmetricGroup(range(n))
+ g = S.subgroup([S(a) for a in generators])
+ array_g = [perm_init(f"{p}", n=n) for p in g]
+ l_g.append(array_g)
+
+ from itertools import product
+ G = product(*l_g)
+ mvts = []
+ for p in G:
+ new_vts = copy.deepcopy(self._veering_triangulations)
+
+ #new horizontal nodes
+ orig_horiz_nodes = copy.deepcopy(self._horizontal_nodes)
+ new_horiz_nodes = [None] * N
+ for i in range(N):
+ level_horiz = orig_horiz_nodes[i]
+ new_horiz_nodes[i] = [None] * len(new_vts[i])
+ assert len(level_horiz) == len(new_horiz_nodes[i])
+ g = p[i] #permutation at level-i
+ for j in range(len(new_vts[i])):
+ new_horiz_nodes[i][g[j]] = level_horiz[j]
+
+ #new prong-matchings
+ orig_pms = copy.deepcopy(self._prong_matching)
+ new_pms = []
+ for p1, p2 in orig_pms:
+ m1, h1, ang1 = p1
+ m2, h2, ang2 = p2
+ l1, c1 = m1
+ l2, c2 = m2
+ m1 = (l1, p[l1][c1])
+ m2 = (l2, p[l2][c2])
+ new_pms.append([(m1, h1, ang1), (m2, h2, ang2)])
+
+ mvts.append(MultiscaleVeeringTriangulation(new_vts, new_horiz_nodes, new_pms))
+
+ return mvts
+
+ def codimension_one_vertical_prime_degenerations(self, level, component, ind_ds_graph, D, index=False):
+ r"""
+ Return a list of lists of multi-scale veering triangulations obtained
+ by codimension-one vertical degenerations of the component.
+
+ The returned list is organized as `l = [l0, l1, ...]`, where each Each sublist li satisfies that:
+ - li contains multi-scale veering triangulations with the same enhanced level graph.
+ - The corresponding components of the triangulations in li belong to the same cell of veering triangulations.
+ - The triangulations in li are in the same connected component of the linear subvariety.
+
+ Input:
+ - `ds_graph`: The index of the Delaunay-Strebel graph of the prime component (which is the `component`-th component at the `level`).
+ - `D`: An instance of the `PrimeDegeneration` class, responsible for computing the codimension-one vertical degenerations
+ of the `component` in `ds_graph`.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.linear_subvariety import *
+ sage: from veerer.labelled_digraph import *
+ sage: from veerer.monodromy import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,9)(~8,10,11)(~9,~10,~11)", "RRBBRRRRBBRR")
+ sage: mvt = MultiscaleVeeringTriangulation([vt], [[""]], [])
+ sage: D = PrimeDegenerations()
+ sage: ds_graph = vt.delaunay_strebel_graph()
+ sage: l1 = D.codimension_one_vertical_degenerations(ds_graph)
+ sage: D.find(ds_graph)
+ 0
+ sage: lmvt1 = mvt.codimension_one_vertical_prime_degenerations(0, 0, 0, D)
+ sage: mvt1 = lmvt1[0][0]
+ sage: vt0 = mvt1._veering_triangulations[0][0]
+ sage: ds_graph0 = vt0.delaunay_strebel_graph()
+ sage: l2 = D.codimension_one_vertical_degenerations(ds_graph0)
+ sage: D.find(ds_graph0)
+ 2
+ sage: lmvt2 = mvt1.codimension_one_vertical_prime_degenerations(0, 0, 2, D)
+ sage: mvt2 = lmvt2[0][0]
+ sage: mvt2._veering_triangulations
+ [[VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)])],
+ [VeeringTriangulationLinearFamily("(0:1,1:1,~0:1,~1:1)", "RB", [(1, 0), (0, 1)])],
+ [VeeringTriangulationLinearFamily("(0:4,~0:4)", "R", [(1)])]]
+ sage: l2[0]
+ ((Delaunay-Strebel graph of VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)]) made of
+ 2 veering Delaunay states
+ 0 Strebel states
+ 4 flip transitions
+ 0 rotation transitions
+ 0 Strebel transitions,),
+ (Delaunay-Strebel graph of VeeringTriangulationLinearFamily("(0:1,1:1,~0:1,~1:1)", "RB", [(1, 0), (0, 1)]) made of
+ 9 veering Delaunay states
+ 1 Strebel states
+ 8 flip transitions
+ 5 rotation transitions
+ 5 Strebel transitions,))
+ """
+
+ vt = self._veering_triangulations[abs(level)][component]
+ ds_graph = D._components[ind_ds_graph]
+
+ assert vt.is_prime()
+ assert vt == ds_graph.root() #it is required to be the root of the Delaunay Strebel graph
+
+ l = D._vertical_degenerations[ind_ds_graph]
+
+ list_comps_after_degen = list(l.keys())
+
+ llmvt = []
+ lindex = []
+ for list_comp_up, list_comp_low in list_comps_after_degen:
+ l_vt_edge = l[(list_comp_up, list_comp_low)]#the list of (vt, edges_up, edges_low) such that vt degenerates to prime components in list_com_yp and list_comp_low respectively.
+ l_ds_g_up = [D._components[index] for index in list_comp_up]
+ l_ds_g_low = [D._components[index] for index in list_comp_low]
+ lc0 = [(-abs(level), component + index) for index in range(len(list_comp_up))]
+ lc1 = [(-abs(level) - 1, index) for index in range(len(list_comp_low))]
+ lc = lc0 + lc1
+ ld = l_ds_g_up + l_ds_g_low
+
+ #construct paths
+ target_vertices = [ds_graph.vertex_index(a[0]) for a in l_vt_edge]
+ paths = tree_with_target(ds_graph, root=0, target_vertices=target_vertices)
+ assert len(paths) == len(l_vt_edge)
+
+ #transport mvt
+ lmvt = [self.transport_along_path(-abs(level), component, path) for path in paths]
+ assert len(lmvt) == len(l_vt_edge)
+
+ #make vertical degeneration and compute all transitions under monodromy groups and level-wise permutations
+ l_deg_connected_comps = []
+ all_mvt = []
+ for i, mvt in enumerate(lmvt):
+ _, edges_up, edges_low = l_vt_edge[i]
+ mvt = mvt.degeneration(-abs(level), component, edges_low=edges_low, edges_up=edges_up)
+ mvt = mvt.prime_decomposition(-abs(level), component)
+ mvt = mvt.prime_decomposition(-abs(level) - 1, 0)
+
+ if mvt not in all_mvt:
+ l_mono = mvt.transport_by_monodromy(lc, ld)
+ new_connected_comp = []
+ for mvt1 in l_mono:
+ new_connected_comp = new_connected_comp + mvt1.transport_by_permutation_in_levels()
+ l_deg_connected_comps.append(new_connected_comp)
+ all_mvt = all_mvt + new_connected_comp
+ lindex.append([list(list_comp_up), list(list_comp_low)])
+
+ llmvt = llmvt + l_deg_connected_comps
+
+ return llmvt if not index else (llmvt, lindex)
+
+ def codimension_one_horizontal_prime_degenerations(self, level, component, ind_ds_graph, D, index=False):
+ r"""
+ Return a list of lists of multi-scale veering triangulations obtained
+ by codimension-one horizontal degenerations of the component.
+
+ The returned list is organized as `l = [l0, l1, ...]`, where each Each sublist li satisfies that:
+ - li contains multi-scale veering triangulations with the same enhanced level graph.
+ - The corresponding components of the triangulations in li belong to the same cell of veering triangulations.
+ - The triangulations in li are in the same connected component of the linear subvariety.
+
+ Input:
+ - `ds_graph`: The index of the Delaunay-Strebel graph of the prime component (which is the `component`-th component at the `level`).
+ - `D`: An instance of the `PrimeDegeneration` class, responsible for computing the codimension-one horizontal degenerations
+ of the `component` in `ds_graph`.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.linear_subvariety import *
+ sage: from veerer.labelled_digraph import *
+ sage: from veerer.monodromy import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,9)(~8,10,11)(~9,~10,~11)", "RRBBRRRRBBRR")
+ sage: mvt = MultiscaleVeeringTriangulation([vt], [[""]], [])
+ sage: D = PrimeDegenerations()
+ sage: ds_graph = vt.delaunay_strebel_graph()
+ sage: l1 = D.codimension_one_vertical_degenerations(ds_graph)
+ sage: D.find(ds_graph)
+ 0
+ sage: mvt1 = mvt.codimension_one_vertical_prime_degenerations(0, 0, 0, D)[0][0]
+ sage: vt0 = mvt1._veering_triangulations[0][0]
+ sage: ds_graph0 = vt0.delaunay_strebel_graph()
+ sage: l2 = D.codimension_one_vertical_degenerations(ds_graph0)
+ sage: D.find(ds_graph0)
+ 2
+ sage: mvt2 = mvt1.codimension_one_vertical_prime_degenerations(0, 0, 2, D)[0][0]
+ sage: vt1 = mvt2._veering_triangulations[1][0]
+ sage: ds_graph1 = vt1.delaunay_strebel_graph()
+ sage: l3 = D.codimension_one_horizontal_degenerations(ds_graph1)
+ sage: D.find(ds_graph1)
+ 8
+ sage: lmvts = mvt2.codimension_one_horizontal_prime_degenerations(-1, 0, 8, D)[0][2]
+ """
+
+ vt = self._veering_triangulations[abs(level)][component]
+ ds_graph = D._components[ind_ds_graph]
+ assert vt.is_prime()
+ assert vt == ds_graph.root() #it is required to be the root of the Delaunay Strebel graph
+
+ l = D._horizontal_degenerations[ind_ds_graph]
+
+ list_comps_after_degen = list(l.keys())
+
+ llmvt = []
+ lindex = []
+ for list_comp in list_comps_after_degen:
+ l_vt_edge = l[list_comp]
+ l_ds_g = [D._components[index] for index in list_comp]
+
+ lc = [(-abs(level), component + index) for index in range(len(list_comp))]
+ ld = l_ds_g
+
+ #construct paths
+ target_vertices = [ds_graph.vertex_index(a[0]) for a in l_vt_edge]
+ paths = tree_with_target(ds_graph, root=0, target_vertices=target_vertices)
+ assert len(paths) == len(l_vt_edge)
+
+ #transport mvt
+ lmvt = [self.transport_along_path(-abs(level), component, path) for path in paths]
+ assert len(lmvt) == len(l_vt_edge)
+
+ #make vertical degeneration and compute all transitions under monodromy groups and level-wise permutations
+ l_deg_connected_comps = []
+ all_mvt = []
+
+ for i, mvt in enumerate(lmvt):
+ _, edges_up, edges_low = l_vt_edge[i]
+ mvt = mvt.degeneration(-abs(level), component, edges_low=edges_low, edges_up=edges_up)
+ mvt = mvt.prime_decomposition(-abs(level), component)
+
+ if mvt not in all_mvt:
+ l_mono = mvt.transport_by_monodromy(lc, ld)
+ new_connected_comp = []
+ for mvt1 in l_mono:
+ new_connected_comp = new_connected_comp + mvt1.transport_by_permutation_in_levels()
+ l_deg_connected_comps.append(new_connected_comp)
+ all_mvt = all_mvt + new_connected_comp
+ lindex.append([list(list_comp)])
+
+ llmvt = llmvt + l_deg_connected_comps
+
+ return llmvt if not index else (llmvt, lindex)
+
+
+def multiscale_compactification_representatives(L, D, index=False):
+ r"""
+ Return all connected components of the boundary in the multiscale compoactification of L as a dictionary.
+
+ The value of the key `(i,j)` is a list [l0, l1, ....], where:
+ - li is a list of multi-scale veering triangulations in the same connected components.
+ - A triangulation in a list has `i` levels and `j` horizontal nodes.
+
+ INPUT:
+ - L: a prime linear family
+ - D: An instance of the `PrimeDegeneration` class, responsible for computing all the degenerations of L
+
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: from veerer.linear_subvariety import *
+ sage: from veerer.labelled_digraph import *
+ sage: from veerer.monodromy import *
+ sage: from veerer.multiscale_veering_triangulation import *
+
+ sage: vt = VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,8,~7)(~1,~8,3)(2,7,~6)(~2,~3,4)", "RRRBBBBBB")
+
+ We compare two computations in the following. We first compute by using the class MultiscaleCompactification::
+ sage: L = vt.linear_subvariety()
+ sage: M = L.multiscale_compactification()
+ sage: M
+ MultiscaleCompactification of Irreducible real linear subvariety of projective dimension 3 in [[H_2(2)]] made of
+ 3 components in codimension 1
+ 5 components in codimension 2
+ 3 components in codimension 3
+
+ Then we compute by the method multiscale_compactification_representatives::
+ sage: ds_graph = vt.delaunay_strebel_graph()
+ sage: D = PrimeDegenerations()
+ sage: D.add(ds_graph)
+ 0
+ sage: D.compute_all()
+ sage: dic= multiscale_compactification_representatives(vt, D)
+ sage: print(f"{len(dic[1,0]) + len(dic[0,1])} components in codimension 1\n{len(dic[2,0]) + len(dic[1,1]) + len(dic[0,2])} components in codimension 2\n{len(dic[1,2]) + len(dic[2,1])} components in codimension 3")
+ 3 components in codimension 1
+ 13 components in codimension 2
+ 14 components in codimension 3
+ """
+ assert L.is_prime()
+ #mvt = mvt.prime_decomposition(0,0)
+
+ ds_graph = L.delaunay_strebel_graph()
+ L = ds_graph.root() #move to root
+ mvt = MultiscaleVeeringTriangulation([L], [[""]], [])
+
+ l = collections.defaultdict(list)
+ l[0, 0].append([mvt])
+ lindex = collections.defaultdict(list)
+ ind = D.find(ds_graph) #find the initial index
+ lindex[0, 0].append([[ind,]])
+
+ all_mvt = []
+ d = L.dimension()
+ # vertical degenerations
+ for codim in range(d - 1):
+ for i, connected_comp in enumerate(l[codim, 0]):
+ ind_comp = lindex[codim, 0][i] #the index of the components of the triangulations in connected_comp
+ for mvt in connected_comp:
+ #only degenerate the top level
+ for comp in range(len(mvt._veering_triangulations[0])):
+ ind = ind_comp[0][comp] #the index of the comp
+ if len(D._vertical_degenerations[ind]) > 0:
+ l_mvts, inds = mvt.codimension_one_vertical_prime_degenerations(0, comp, ind, D, index=True)
+ for j, mvts in enumerate(l_mvts):
+ if mvts[0] not in all_mvt:
+ assert all(mvt not in all_mvt for mvt in mvts)
+ l[codim + 1, 0].append(mvts)
+ new = copy.deepcopy(ind_comp)
+ new[1:1] = [inds[j][1]]
+ new[0][comp:comp + 1] = inds[j][0]
+ lindex[codim + 1, 0].append(new)
+ all_mvt = all_mvt + mvts
+
+ #horizontal degenerations
+ for codim in range(d):
+ h = 0
+ has_horiz = True
+ while has_horiz:
+ has_horiz = False
+ for i, connected_comp in enumerate(l[codim, h]):
+ ind_comp = lindex[codim, h][i] #the index of the components of the triangulations in connected_comp
+ for mvt in connected_comp:
+ N = mvt.num_levels()
+ for level in range(N):
+ for comp in range(len(mvt._veering_triangulations[level])):
+ ind = ind_comp[abs(level)][comp] #the index of the comp
+ if len(D._horizontal_degenerations[ind]) >0:
+ l_mvts, inds = mvt.codimension_one_horizontal_prime_degenerations(-abs(level), comp, ind, D, index=True)
+ for j, mvts in enumerate(l_mvts):
+ if mvts[0] not in all_mvt:
+ assert all(mvt not in all_mvt for mvt in mvts)
+ l[codim, h + 1].append(mvts)
+ new = copy.deepcopy(ind_comp)
+ new[abs(level)][comp:comp + 1] = inds[j][0]
+ lindex[codim, h + 1].append(new)
+ all_mvt = all_mvt + mvts
+ has_horiz = True
+ h = h + 1
+
+ return l if not index else (l, lindex)
diff --git a/veerer/permutation.pyx b/veerer/permutation.pyx
index aabe6a72..c78dcf09 100644
--- a/veerer/permutation.pyx
+++ b/veerer/permutation.pyx
@@ -201,7 +201,7 @@ def perm_id(int n):
return array.array('i', range(n))
-def perm_init(data, int n=-1, involution=None):
+def perm_init(data, int n=-1, edge_like=False, partial=False):
"""
Return a permutation from the given data.
@@ -218,7 +218,7 @@ def perm_init(data, int n=-1, involution=None):
sage: perm_init([3,2,1,4])
array('i', [3, 2, 1, 4])
- sage: perm_init([3,1,None,0])
+ sage: perm_init([3,1,-1,0])
array('i', [3, 1, -1, 0])
Cycle notation (not mentioned elements are considered to be fixed
@@ -234,18 +234,17 @@ def perm_init(data, int n=-1, involution=None):
sage: perm_init('(0,1)(3,2)')
array('i', [1, 0, 3, 2])
- Initialize a permutation in the centralizer of an involution::
+ sage: perm_init("(2,4)")
+ array('i', [0, 1, 4, 3, 2])
+ sage: perm_init("(2,4)", partial=True)
+ array('i', [-1, -1, 4, -1, 2])
- sage: perm_init('(0,2)(1,3)', involution=[0,4,2,5,1,3])
- array('i', [2, 3, 0, 1, 5, 4])
- sage: perm_init('(0,~1)', involution=[0,1])
- array('i', [1, 0])
- sage: perm_init('(0,~1)', involution=[2,3,0,1])
- array('i', [3, 2, 1, 0])
- sage: perm_init('(0,3)', involution=[0,4,2,5,1,3])
- Traceback (most recent call last):
- ...
- ValueError: invalid input
+ Edge-like::
+
+ sage: perm_init("(0,~1,1)", edge_like=True)
+ array('i', [3, 1, 0, 2])
+ sage: perm_init("(0,~1,1)", edge_like=True, partial=True)
+ array('i', [3, -1, 0, 2])
Zerology::
@@ -258,8 +257,6 @@ def perm_init(data, int n=-1, involution=None):
sage: perm_init('()')
array('i')
"""
- if n == -1 and involution is not None:
- n = len(involution)
if isinstance(data, (array.array, tuple, list)):
if not data:
if n is not None:
@@ -267,25 +264,25 @@ def perm_init(data, int n=-1, involution=None):
else:
return array.array('i', [])
elif isinstance(data[0], (tuple, list)):
- return perm_from_cycles(data, n=n, involution=involution)
+ return perm_from_cycles(data, n=n, edge_like=edge_like, partial=partial)
+ elif n == -1 or (len(data) == n):
+ return array.array('i', data)
else:
- return array.array('i', (-1 if x is None else x for x in data))
+ raise ValueError("invalid arguments (data={} n={})".format(data, n))
if isinstance(data, str):
c = str_to_cycles(data)
- return perm_from_cycles(c, n=n, involution=involution)
+ return perm_from_cycles(c, n=n, edge_like=edge_like, partial=partial)
# TODO: test flipper conversion
if data.__module__.startswith('flipper'):
- if involution is None:
- raise ValueError("involution must be provided")
from .misc import flipper_isometry_to_perm
- return flipper_isometry_to_perm(data, involution)
+ return flipper_isometry_to_perm(data)
raise TypeError("The input must be list, tuple or string")
-def perm_from_cycles(t, int n=-1, involution=None):
+def perm_from_cycles(t, int n=-1, edge_like=False, partial=False):
r"""
Return a permutation on `[0, n-1]` from a list of cycles on `[0, n-1]`
@@ -295,8 +292,9 @@ def perm_from_cycles(t, int n=-1, involution=None):
- ``n`` - optional domain size
- - ``involution`` (optional) - if provided use it to convert minus
- signs
+ - ``edge_like`` (optional boolean)
+
+ - ``partial`` (optional boolean)
EXAMPLES::
@@ -310,40 +308,57 @@ def perm_from_cycles(t, int n=-1, involution=None):
sage: perm_from_cycles([[],[]])
array('i')
- sage: perm_from_cycles([[1,-2],[0,3]], n=6, involution=[0,4,5,3,1,2])
- array('i', [3, 4, 2, 0, 1, 5])
+ sage: perm_from_cycles([[1,-2], [0,3]], n=8, edge_like=True)
+ array('i', [6, 1, 3, 2, 4, 5, 0, 7])
+ sage: perm_from_cycles([[1,-2], [0,3]], n=8, edge_like=True, partial=True)
+ array('i', [6, -1, 3, 2, -1, -1, 0, -1])
+
+ sage: perm_from_cycles([], n=4)
+ array('i', [0, 1, 2, 3])
+ sage: perm_from_cycles([], n=4, edge_like=True)
+ array('i', [0, 1, 2, 3])
+ sage: perm_from_cycles([], n=4, partial=True)
+ array('i', [-1, -1, -1, -1])
"""
if not any(tt for tt in t):
- return array.array('i', [])
+ if n != -1:
+ if partial:
+ return array.array('i', [-1] * n)
+ else:
+ return array.array('i', range(n))
+ else:
+ return array.array('i', [])
if n == -1:
n = max(map(max, t)) + 1
+ if edge_like:
+ n = max(n, -min(map(min, t)))
+ n *= 2
+ if edge_like and n % 2:
+ raise ValueError("n={} must be even when edge_like=True".format(n))
- res = array.array('i', range(n))
+ res = array.array('i', [-1] * n) if partial else array.array('i', range(n))
for c in t:
a = int(c[0])
- if a < 0:
- a = n+a if involution is None else involution[~a]
- for j in range(1,len(c)):
+ if edge_like:
+ a = 2 * a if a >= 0 else (2 * ~a + 1)
+ if not 0 <= a < n:
+ raise ValueError("invalid input: entry {} is out of range".format(a))
+ for j in range(1, len(c)):
b = int(c[j])
- if b < 0:
- b = n+b if involution is None else involution[~b]
+ if edge_like:
+ b = 2 * b if b >= 0 else (2 * ~b + 1)
+ if not 0 <= b < n:
+ raise ValueError("invalid input: entry {} is out of range".format(b))
res[a] = b
- if involution is not None:
- if (a == involution[a]) != (b == involution[b]):
- raise ValueError("invalid input")
- res[involution[a]] = involution[b]
a = b
b = int(c[0])
- if b < 0:
- b = n+b if involution is None else involution[~b]
-
+ if edge_like:
+ b = 2 * b if b >= 0 else (2 * ~b + 1)
+ if not 0 <= b < n:
+ raise ValueError("invalid input: {} is out of range".format(b))
res[a] = b
- if involution is not None:
- if (a == involution[a]) != (b == involution[b]):
- raise ValueError("invalid input")
- res[involution[a]] = involution[b]
return res
@@ -530,6 +545,8 @@ def uint_base64_str(n, l=None):
'00f'
"""
n = int(n)
+ if n < 0:
+ raise ValueError("negative input")
s = ''
while n:
s = CHARS[n % 64] + s
@@ -583,10 +600,15 @@ def perm_base64_str(p):
sage: perm_from_base64_str('vdh0keigmcjfpxtnrwsouyqba987654321zl', 36)
array('i', [31, 13, 17, 0, 20, 14, 18, 16, 22, 12, 19, 15, 25, 33, 29, 23, 27, 32, 28, 24, 30, 34, 26, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 35, 21])
+
+ Partial permutation::
+
+ sage: perm_base64_str([-1, 3, -1, 1])
+ '~3~1'
"""
n = len(p)
l = int(log(n, 64)) + 1 # number of digits used for each entry
- return ''.join(uint_base64_str(i, l) for i in p)
+ return ''.join('~' * l if i == -1 else uint_base64_str(i, l) for i in p)
def perm_from_base64_str(s, n):
@@ -606,11 +628,16 @@ def perm_from_base64_str(s, n):
sage: p = array('i', r)
sage: perm_from_base64_str(perm_base64_str(p), 3000) == p
True
+
+ Partial permutation::
+
+ sage: perm_from_base64_str('~3~1', 4)
+ array('i', [-1, 3, -1, 1])
"""
l = int(log(n, 64)) + 1 # number of digits used for each entry
if len(s) != n * l:
raise ValueError('wrong length')
- return array.array('i', (uint_from_base64_str(s[i:i+l]) for i in range(0,len(s),l)))
+ return array.array('i', (-1 if s[i:i+l] == '~' * l else uint_from_base64_str(s[i:i+l]) for i in range(0, len(s), l)))
#####################################################################
# Boolean properties
@@ -640,30 +667,26 @@ def perm_dense_cycles(array.array p, int n=-1):
sage: from veerer.permutation import perm_dense_cycles
sage: perm_dense_cycles(array('i', [1,2,0]))
- ([0, 0, 0], [3])
+ array('i', [0, 0, 0])
sage: perm_dense_cycles(array('i', [0,2,1]))
- ([0, 1, 1], [1, 2])
+ array('i', [0, 1, 1])
sage: perm_dense_cycles(array('i', [2,1,0]))
- ([0, 1, 0], [2, 1])
+ array('i', [0, 1, 0])
"""
if n == -1:
n = len(p)
- cdef list deg = []
- cdef list res = [-1] * n
- k = 0
+ cdef array.array res = array.array('i', [-1] * n)
+ cdef int i, k = 0
for i in range(n):
- if res[i] != -1:
+ if p[i] == -1:
continue
- d = 0
while res[i] == -1:
res[i] = k
i = p.data.as_ints[i]
- d += 1
k += 1
- deg.append(d)
- return res, deg
+ return res
def perm_cycles(array.array p, singletons=True, int n=-1):
@@ -715,6 +738,7 @@ def perm_cycles(array.array p, singletons=True, int n=-1):
seen[j] = True
cycle.append(j)
j = p.data.as_ints[j]
+ assert 0 <= j < n, (j, n)
res.append(cycle)
return res
@@ -841,7 +865,56 @@ def perm_cycle_type(array.array p, int n=-1):
return c
-def perm_cycles_to_string(list cycles, involution=None, data=None):
+def perm_order(array.array p, int n=-1):
+ r"""
+ Return the multiplicative order of the permutation ``p``.
+
+ EXAMPLES::
+
+ sage: from veerer.permutation import perm_init, perm_order
+ sage: p = perm_init('(1,3)(2,4,6)(5)')
+ sage: perm_order(p)
+ 6
+ """
+ from sage.arith.functions import lcm
+ return lcm(perm_cycle_type(p))
+
+
+def perm_is_involution(array.array p, int n=-1):
+ r"""
+ Return whether ``p`` is a non-trivial involution.
+
+ EXAMPLES::
+
+ sage: from veerer.permutation import perm_init, perm_is_involution
+ sage: perm_is_involution(perm_init('(0)(1)(2)'))
+ False
+ sage: perm_is_involution(perm_init('(0)(1,2)'))
+ True
+ sage: perm_is_involution(perm_init('(0,2)(1)'))
+ True
+ sage: perm_is_involution(perm_init('(0,2,1)'))
+ False
+
+ sage: perm_is_involution(perm_init('(0,2)', partial=True))
+ True
+ sage: perm_is_involution(perm_init('(0,2,4)', partial=True))
+ False
+ """
+ if n == -1:
+ n = len(p)
+ cdef int i, j, is_identity = 1
+ for i in range(n):
+ if p.data.as_ints[i] == -1:
+ continue
+ j = p.data.as_ints[i]
+ is_identity &= (i == j)
+ if p.data.as_ints[j] != i:
+ return False
+ return not is_identity
+
+
+def perm_cycles_to_string(list cycles, edge_like=False, data=None):
r"""
Return a string representing a list of cycles.
@@ -849,15 +922,15 @@ def perm_cycles_to_string(list cycles, involution=None, data=None):
- ``cycles`` -- list of cycles
- - ``involution`` -- optional involution (possibly with fixed points)
+ - ``edge_like`` -- optional boolean (default ``False``)
- ``data`` -- optional data
"""
- if involution:
+ if edge_like:
if data:
- elt = lambda e: ('%d' % e if e <= involution[e] else '~%d' % involution[e]) + ((':%d' % data[e]) if data[e] else '')
+ elt = lambda e: ('~%d' % (e // 2) if e % 2 else '%d' % (e // 2)) + ((':%d' % data[e]) if data[e] else '')
else:
- elt = lambda e: ('%d' % e if e <= involution[e] else '~%d' % involution[e])
+ elt = lambda e: ('~%d' % (e // 2)) if e % 2 else '%d' % (e // 2)
elif data:
elt = lambda e: ('%d' % e) + ((':%d' % data[e]) if data[e] else '')
else:
@@ -866,7 +939,7 @@ def perm_cycles_to_string(list cycles, involution=None, data=None):
return ''.join(map(lambda x: '(' + ','.join(map(elt, x)) + ')', cycles))
-def perm_cycle_string(array.array p, singletons=True, n=-1, involution=None):
+def perm_cycle_string(array.array p, singletons=True, n=-1, edge_like=False):
r"""
Return a string representing the cycle decomposition of `p`
@@ -875,17 +948,20 @@ def perm_cycle_string(array.array p, singletons=True, n=-1, involution=None):
sage: from array import array
sage: from veerer.permutation import perm_cycle_string
- sage: perm_cycle_string(array('i', [0,2,1]))
+ sage: perm_cycle_string(array('i', [0, 2, 1]))
'(0)(1,2)'
- sage: perm_cycle_string(array('i', [0,2,1]), False)
+ sage: perm_cycle_string(array('i', [0, 2, 1]), False)
'(1,2)'
+ sage: perm_cycle_string(array('i', [0, 1, 2]), False)
+ '()'
"""
- return perm_cycles_to_string(perm_cycles(p, singletons, n), involution)
+ c = perm_cycles(p, singletons, n)
+ return '()' if not c else perm_cycles_to_string(c, edge_like)
def perm_orbit(array.array p, int i):
r"""
- Return the orbit of ``i`` under the permutation ``p``.
+ Return the forward orbit of ``i`` under the permutation ``p``.
EXAMPLES::
@@ -895,10 +971,12 @@ def perm_orbit(array.array p, int i):
sage: perm_orbit(array('i', [0,3,1,2]), 2)
[2, 1, 3]
"""
+ if i < 0 or i >= len(p):
+ raise ValueError("permutation index out of range")
cdef int j
cdef list res = [i]
j = p.data.as_ints[i]
- while j != i:
+ while j != -1 and j != i:
res.append(j)
j = p.data.as_ints[j]
return res
@@ -950,53 +1028,242 @@ def perm_preimage(array.array p, int i):
return j
-def perm_on_list(array.array p, a, int n=-1, swap=None):
+def perm_on_array(array.array dest, array.array src, array.array p, int n=-1):
r"""
Inplace action of permutation on list-like objects.
INPUT:
- - ``p`` - permutation
+ - ``dest``, ``src`` -- destination and source (could be the same array in which
+ case the operation is done inplace)
- - ``a`` - list, array
+ - ``p`` -- permutation
- - ``n`` - (optional) size of permutation
+ - ``n`` -- (optional) size of permutation
- - ``swap`` - (optional) a swap function
+ - ``swap`` -- (optional) a swap function
EXAMPLES::
sage: from array import array
sage: from veerer.permutation import *
- sage: l = [0,1,2,3,4]
- sage: p = array('i', [4,2,3,0,1])
- sage: perm_on_list(p, l)
+ sage: l = array('i', [0, 1, 2, 3, 4])
+ sage: p = array('i', [4, 2, 3, 0, 1])
+ sage: perm_on_array(l, l, p)
sage: l
- [3, 4, 1, 2, 0]
+ array('i', [3, 4, 1, 2, 0])
+
+ sage: src = array('i', [0, 1, 2, 3, 4])
+ sage: dest = array('i', [-1] * 5)
+ sage: perm_on_array(dest, src, p)
+ sage: dest
+ array('i', [3, 4, 1, 2, 0])
+ sage: src
+ array('i', [0, 1, 2, 3, 4])
+ """
+ cdef array.array seen
+ cdef int i, j, tmp
+ if n == -1:
+ n = len(p)
+ if len(dest) < n:
+ raise ValueError("invalid dest")
+ if len(src) < n:
+ raise ValueError("invalid src")
+ if dest is src:
+ seen = array.clone(p, n, True)
+ for i in range(n):
+ if seen.data.as_ints[i]:
+ continue
+ seen.data.as_ints[i] = 1
+ j = p.data.as_ints[i]
+ if j == -1:
+ continue
+ while not seen.data.as_ints[j]:
+ tmp = src.data.as_ints[i]
+ src.data.as_ints[i] = src.data.as_ints[j]
+ src.data.as_ints[j] = tmp
+ seen.data.as_ints[j] = 1
+ j = p.data.as_ints[j]
+ if j == -1:
+ raise ValueError("invalid permutation p")
+ else:
+ for i in range(n):
+ j = p.data.as_ints[i]
+ if not 0 <= j < n:
+ raise ValueError("partial permutation p={}".format(p))
+ dest.data.as_ints[j] = src.data.as_ints[i]
+
+
+def perm_on_list(l, array.array p, int n=-1, swap=None):
+ r"""
+ Inplace permutation action on list like objects.
+
+ EXAMPLES::
Permutation action on matrix rows::
+ sage: from veerer.permutation import perm_init, perm_compose, perm_on_list
+
sage: m1 = matrix(ZZ, 5, 5, 1)
sage: m2 = matrix(ZZ, 5, 5, 1)
sage: m = matrix(ZZ, 5, 5, 1)
sage: p1 = perm_init([4,1,3,2,0])
sage: p2 = perm_init([1,0,3,4,2])
- sage: perm_on_list(p1, m1, swap=sage.matrix.matrix0.Matrix.swap_rows)
- sage: perm_on_list(p2, m2, swap=sage.matrix.matrix0.Matrix.swap_rows)
- sage: perm_on_list(perm_compose(p1, p2), m, swap=sage.matrix.matrix0.Matrix.swap_rows)
+ sage: perm_on_list(m1, p1, swap=sage.matrix.matrix0.Matrix.swap_rows)
+ sage: perm_on_list(m2, p2, swap=sage.matrix.matrix0.Matrix.swap_rows)
+ sage: perm_on_list(m, perm_compose(p1, p2), swap=sage.matrix.matrix0.Matrix.swap_rows)
sage: m == m2 * m1
True
+
+ sage: m1 = matrix(ZZ, 5, range(25))
+ sage: m2 = matrix(ZZ, 5, range(137, 87, -2))
+ sage: m = m1 * m2
+ sage: p = perm_init([4, 1, 3, 2, 0])
+ sage: perm_on_list(m1, p, swap=sage.matrix.matrix0.Matrix.swap_rows)
+ sage: perm_on_list(m1, p, swap=sage.matrix.matrix0.Matrix.swap_columns)
+ sage: perm_on_list(m2, p, swap=sage.matrix.matrix0.Matrix.swap_rows)
+ sage: perm_on_list(m2, p, swap=sage.matrix.matrix0.Matrix.swap_columns)
+ sage: perm_on_list(m, p, swap=sage.matrix.matrix0.Matrix.swap_rows)
+ sage: perm_on_list(m, p, swap=sage.matrix.matrix0.Matrix.swap_columns)
+ sage: m == m1 * m2
+ True
"""
+ cdef int i, j
if n == -1:
n = len(p)
cdef array.array seen = array.clone(p, n, True)
- cdef int i, j
for i in range(n):
if seen.data.as_ints[i]:
continue
seen.data.as_ints[i] = 1
j = p.data.as_ints[i]
+ if j == -1:
+ continue
+ while not seen.data.as_ints[j]:
+ if swap:
+ swap(l, i, j)
+ else:
+ l[i], l[j] = l[j], l[i]
+ seen.data.as_ints[j] = 1
+ j = p.data.as_ints[j]
+ if j == -1:
+ raise ValueError("invalid permutation p")
+
+
+def perm_on_edge_array(array.array dest, array.array src, array.array p, int n=-1):
+ r"""
+ Inplace action of permutation on list-like objects.
+
+ INPUT:
+
+ - ``dest``, ``src`` -- destination and source (could be the same array in which
+ case the operation is done inplace)
+
+ - ``p`` -- permutation
+
+ - ``n`` -- (optional) size of permutation
+
+ - ``swap`` -- (optional) a swap function
+
+ EXAMPLES::
+
+ sage: from array import array
+ sage: from veerer.permutation import *
+
+ sage: p = array('i', [6, 7, 3, 2, 9, 8, 0, 1, 5, 4])
+ sage: l = array('i', [0, 1, 2, 3, 4])
+ sage: perm_on_edge_array(l, l, p)
+ sage: l
+ array('i', [3, 1, 4, 0, 2])
+
+ sage: src = array('i', [0, 1, 2, 3, 4])
+ sage: dest = array('i', [-1] * 5)
+ sage: perm_on_edge_array(dest, src, p)
+ sage: dest
+ array('i', [3, 1, 4, 0, 2])
+ sage: src
+ array('i', [0, 1, 2, 3, 4])
+ """
+ cdef array.array seen
+ cdef int i, j, tmp
+ if n == -1:
+ n = len(p)
+ if n % 2:
+ raise ValueError("n must be even")
+ if len(dest) < n // 2:
+ raise ValueError("invalid dest")
+ if len(src) < n // 2:
+ raise ValueError("invalid src")
+ if len(p) < n:
+ raise ValueError("invalid p")
+ if dest is src:
+ seen = array.clone(p, n // 2, True)
+ for i in range(n // 2):
+ if seen.data.as_ints[i]:
+ continue
+ seen.data.as_ints[i] = 1
+ j = p.data.as_ints[2 * i] // 2
+ assert 0 <= j < n // 2
+ if p.data.as_ints[2 * i + 1] != -1 and p.data.as_ints[2 * i] != p.data.as_ints[2 * i + 1] ^ 1:
+ raise ValueError("inconsistent relabelling: p[2*{}] = {}, p[2*{}+1] = {}".format(i, p.data.as_ints[2 * i],
+ i, p.data.as_ints[2 * i + 1]))
+ while not seen.data.as_ints[j]:
+ tmp = src.data.as_ints[i]
+ src.data.as_ints[i] = src.data.as_ints[j]
+ src.data.as_ints[j] = tmp
+ seen.data.as_ints[j] = 1
+ j = p.data.as_ints[2 * j] // 2
+ assert 0 <= j < n // 2
+ else:
+ for i in range(n // 2):
+ j = p.data.as_ints[2 * i] // 2
+ assert 0 <= j < n
+ dest.data.as_ints[j] = src.data.as_ints[i]
+
+
+def perm_on_edge_list(array.array p, a, int n=-1, swap=None):
+ r"""
+ Inplace action of permutation on list-like objects.
+
+ INPUT:
+
+ - ``p`` - permutation of even length such that {p(2i), p(2i+1)} = {2j, 2j+1}
+
+ - ``a`` - list, array of length half the length of the permutation
+
+ - ``n`` - (optional) size of permutation
+
+ - ``swap`` - (optional) a swap function
+
+ EXAMPLES::
+
+ sage: from array import array
+ sage: from veerer.permutation import *
+
+ sage: l = [0,1,2]
+ sage: p = array('i', [3,2,0,1,5,4])
+ sage: perm_on_edge_list(p, l)
+ sage: l
+ [1, 0, 2]
+ """
+ if n == -1:
+ n = len(p)
+ if n % 2:
+ raise ValueError("n must be even")
+ if len(a) < n // 2:
+ raise ValueError("invalid argument a")
+ if len(p) < n:
+ raise ValueError("invalid argument p")
+ cdef array.array seen = array.clone(p, n // 2, True)
+ cdef int i, j
+ for i in range(n // 2):
+ if seen.data.as_ints[i]:
+ continue
+ seen.data.as_ints[i] = 1
+ j = p.data.as_ints[2 * i] // 2
+ assert 0 <= j < n // 2
+ assert p.data.as_ints[2 * i + 1] == -1 or p.data.as_ints[2 * i] == p.data.as_ints[2 * i + 1] ^ 1
while not seen.data.as_ints[j]:
if swap:
swap(a, i, j)
@@ -1005,7 +1272,10 @@ def perm_on_list(array.array p, a, int n=-1, swap=None):
a[i] = a[j]
a[j] = tmp
seen.data.as_ints[j] = 1
- j = p.data.as_ints[j]
+ assert p.data.as_ints[2 * j + 1] == -1 or p.data.as_ints[2 * j] == p.data.as_ints[2 * j + 1] ^ 1
+ j = p.data.as_ints[2 * j] // 2
+ assert 0 <= j < n // 2
+
#####################################################################
# Group operations
@@ -1202,6 +1472,7 @@ def perm_conjugate(array.array p1, array.array p2, int n=-1):
EXAMPLES::
+ sage: from array import array
sage: from veerer.permutation import perm_conjugate, perm_random
sage: p1 = perm_random(23)
@@ -1209,13 +1480,28 @@ def perm_conjugate(array.array p1, array.array p2, int n=-1):
sage: res = perm_conjugate(p1, p2)
sage: all(res[p2[i]] == p2[p1[i]] for i in range(23))
True
+
+ sage: perm_conjugate(array('i', [8, 7, 2, 3, 4, 5, 6, 1, 0]), array('i', [0, 2, 4, 5, 6, 7, 3, 1]))
+ Traceback (most recent call last):
+ ...
+ ValueError: invalid input
"""
if n == -1:
n = len(p1)
+ if len(p2) < n:
+ raise ValueError("invalid input")
+
cdef array.array res = array.clone(p1, n, False)
cdef int i
for i in range(n):
- res.data.as_ints[p2.data.as_ints[i]] = p2.data.as_ints[p1.data.as_ints[i]]
+ res[i] = -1
+ for i in range(n):
+ if p1.data.as_ints[i] == -1 or p2.data.as_ints[i] == -1:
+ continue
+ if not (0 <= p1.data.as_ints[i] < n) or not (0 <= p2.data.as_ints[i] < n):
+ raise ValueError("invalid input")
+ else:
+ res.data.as_ints[p2.data.as_ints[i]] = p2.data.as_ints[p1.data.as_ints[i]]
return res
#####################################################################
@@ -1270,7 +1556,7 @@ def perms_are_transitive(p, int n=-1):
"""
Test whether the group generated by the permutations in ``p`` is transitive.
- We assume that the list of partial permutations act on
+ The function assumes that the list of partial permutations act on
the same domain (ie the -1 occur at the same positions).
INPUT:
@@ -1294,6 +1580,16 @@ def perms_are_transitive(p, int n=-1):
True
sage: perms_are_transitive([p0,p1,p2,p3])
False
+
+ Example with partial permutations::
+
+ sage: p0 = [2, -1, 0, 3, 4]
+ sage: p1 = [0, -1, 3, 2, 4]
+ sage: p2 = [0, -1, 4, 3, 2]
+ sage: perms_are_transitive([p0, p1, p2])
+ True
+ sage: perms_are_transitive([p0, p1])
+ False
"""
if not p:
raise ValueError("empty list")
@@ -1302,10 +1598,13 @@ def perms_are_transitive(p, int n=-1):
if n == -1:
n = len(p0)
- # compute the connected component of 0
+ # compute the connected component of the first active integer
cc0 = [True if j == -1 else False for j in p0]
- todo = [0]
- cc0[0] = True
+ if all(cc0):
+ return True
+
+ todo = [next(i for i, j in enumerate(p0) if j != -1)]
+ cc0[todo[0]] = True
while todo:
j = todo.pop()
for pp in p:
@@ -1321,6 +1620,9 @@ def perms_orbits(p, int n=-1):
r"""
Return the list of orbits of the permutation group generated by ``p``.
+ The function assumes that the list of partial permutations act on
+ the same domain (ie the -1 occur at the same positions).
+
EXAMPLES::
sage: from veerer.permutation import perm_init, perms_orbits
@@ -1329,6 +1631,13 @@ def perms_orbits(p, int n=-1):
sage: p1 = perm_init("(0,4)(1,5)(2,6)(3,7)")
sage: perms_orbits((p0, p1))
[[0, 1, 4, 5], [2, 3, 6, 7]]
+
+ An example with partial permutations::
+
+ sage: p0 = [-1, 3, -1, 1, -1, 5, 6]
+ sage: p1 = [-1, 1, -1, 3, -1, 6, 5]
+ sage: perms_orbits((p0, p1))
+ [[1, 3], [5, 6]]
"""
if not p:
raise ValueError("empty list")
@@ -1337,7 +1646,7 @@ def perms_orbits(p, int n=-1):
n = len(p[0])
# compute the connected component of 0
- viewed = [False] * n
+ viewed = [True if p[0][i] == -1 else False for i in range(n)]
cc_list = []
for i in range(n):
if viewed[i]:
@@ -1359,6 +1668,60 @@ def perms_orbits(p, int n=-1):
return cc_list
+def perm_edge_orbits(array.array p, int ne=-1):
+ r"""
+ Return the list of orbits of the permutation group generated by ``p``.
+
+ The function assumes that the list of partial permutations act on
+ the same domain (ie the -1 occur at the same positions).
+
+ EXAMPLES::
+
+ sage: from veerer.permutation import perm_init, perm_edge_orbits
+
+ sage: p = perm_init([2, 6, 4, -1, 0, -1, 8, -1, 1, -1])
+ sage: perm_edge_orbits(p)
+ [[0, 1, 2, 3, 4]]
+ """
+ if ne == -1:
+ if len(p) % 2:
+ raise ValueError("invalid permutation")
+ ne = len(p) // 2
+
+ if len(p) < 2 * ne:
+ raise ValueError("inconsistent data")
+
+ # compute the connected component of 0
+ viewed = [False] * ne
+ cc_list = []
+ for i in range(ne):
+ if viewed[i]:
+ continue
+ cc = [i]
+ todo = [i]
+ viewed[i] = True
+ while todo:
+ i = todo.pop()
+ j = p[2 * i] // 2
+ if not viewed[j]:
+ viewed[j] = True
+ cc.append(j)
+ todo.append(j)
+ j = p[2 * i + 1]
+ if j != -1:
+ j //= 2
+ if not viewed[j]:
+ viewed[j] = True
+ cc.append(j)
+ todo.append(j)
+ cc.sort()
+ cc_list.append(cc)
+
+ return cc_list
+
+
+
+
def perms_relabel(p, m):
"""
Relabel the list of permutations ``p`` according to ``m``.
@@ -1489,72 +1852,192 @@ def perms_canonical_labels(p, e=None):
# Triangulation relabellings
#####################################################################
-def triangulation_relabelling_from(array.array vp, array.array ep, int start_edge):
+# NOTE: it would be way more efficient to run through all half-edges as roots and check whether they
+# produce equivalent partial labelling or not by performing a new step of relabelling. If two partial
+# relabelling differ at # a given step, we can determine which one is best.
+def edge_relabelling_from(array.array relabelling, array.array pnew, array.array p, int n, int root, int image=0):
+ r"""
+ We go along cycles of ``p``
+ """
+ if n < 0 or n % 2 or len(relabelling) < n or len(p) < n:
+ raise ValueError("invalid arguments")
+
+ cdef array.array to_process = array.clone(p, n, False) # FIFO stack of half-edges to process
+ cdef int s, t # bottom and top of to_process
+ cdef int e, e1
+
+ to_process.data.as_ints[0] = root
+ s = 0
+ t = 1
+
+ while s < t:
+ # pick the next half-edge and saturate its orbit with p0 and store the
+ # p1 images in to_process
+ e_new = to_process.data.as_ints[s]
+ e_old = -1
+ s += 1
+ while relabelling.data.as_ints[e_new] == -1:
+ if relabelling.data.as_ints[e_new ^ 1] == -1:
+ relabelling.data.as_ints[e_new] = image
+ image += 2
+ if p[e_new ^ 1] != -1:
+ # e_new ^ 1 is indeed active
+ to_process.data.as_ints[t] = e_new ^ 1
+ t += 1
+ else:
+ relabelling.data.as_ints[e_new] = relabelling.data.as_ints[e_new ^ 1] ^ 1
+
+ if e_old != -1:
+ assert relabelling.data.as_ints[e_old] != -1
+ assert relabelling.data.as_ints[e_new] != -1
+ pnew.data.as_ints[relabelling.data.as_ints[e_old]] = relabelling.data.as_ints[e_new]
+
+ e_old, e_new = e_new, p.data.as_ints[e_new]
+
+ if e_old != -1:
+ assert relabelling.data.as_ints[e_old] != -1
+ assert relabelling.data.as_ints[e_new] != -1
+ pnew.data.as_ints[relabelling.data.as_ints[e_old]] = relabelling.data.as_ints[e_new]
+
+ if image > n:
+ raise ValueError("invalid argument")
+
+ # make the relabelling an actual permutation even when p is partial
+ for e in range(n):
+ if relabelling.data.as_ints[e] == -1 and relabelling.data.as_ints[e ^ 1] != -1:
+ relabelling.data.as_ints[e] = relabelling.data.as_ints[e ^ 1] ^ 1
+
+ return image
+
+
+# def perms_relabelling_from(array.array relabelling, array.array p0, array.array p1, int n, int root, int image=0):
+# r"""
+# Set ``relabelling`` to a canonical relabelling of the pair of permutations
+# ``(p0, p1)`` where ``root`` is mapped to ``image``.
+#
+# The canonical exploration is a depth first search done by saturating with
+# ``p0`` first and then ``p1``.
+#
+# INPUT:
+#
+# - ``relabelling`` - a partial relabelling (initialized with ``-1`` at
+# unrelabelled positions). It is assumed that the relabelling is supported on
+# a union of orbits of the group generated by ``p0`` and ``p1``.
+#
+# - ``p0``, ``p1`` - permutations of size ``n``
+#
+# - ``n`` - size of the permutation
+#
+# - ``root`` - (integer) half-edge
+#
+# - ``image`` - (integer) default to 0
+#
+# OUTPUT: The value of ``image`` at the end of the process. If ``image`` is
+# equal to ``n`` if and only if the constellation has been fully relabelled.
+#
+# EXAMPLES::
+#
+# sage: from array import array
+# sage: from veerer.permutation import perms_relabelling_from
+# sage: vp = array('i', [9, 13, 12, 11, 10, 14, 5, 6, 7, 8, 29, 25, 26, 27, 28, 1, 15, 16, 17, 18, 19, 0, 4, 3, 2, 21, 22, 23, 24, 20])
+# sage: ep = array('i', [29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
+# sage: relabelling = array('i', [-1] * 30)
+# sage: permutations_relabelling_from(relabelling, vp, ep, 30, 0, 0)
+# 30
+# sage: relabelling
+# array('i', [0, 22, 9, 26, 13, 5, 4, 3, 2, 1, 14, 27, 10, 23, 6, 21, 20, 19, 18, 17, 16, 29, 12, 25, 8, 28, 11, 24, 7, 15])
+#
+# Note that if the group generated by ``vp`` and ``ep`` is not transitive,
+# then the relabelling is only partial. One needs to call the function twice
+# to get a full relabelling::
+#
+# sage: vp = array('i', [3, 2, 1, 0])
+# sage: ep = array('i', [3, 1, 2, 0])
+#
+# sage: relabelling = array('i', [-1] * 4)
+# sage: permutations_relabelling_from(relabelling, vp, ep, 4, 0, 0)
+# 2
+# sage: relabelling
+#
+# sage: relabelling = array('i', [-1] * 4)
+# sage: permutations_relabelling_from(relabelling, vp, ep, 4, 2, 0)
+# 2
+# sage: relabelling
+#
+# sage: relabelling = array('i', [-1] * 4)
+# sage: permutations_relabelling_from(relabelling, vp, ep, 4, 0, 0)
+# 2
+# sage: permutations_relabelling_from(relabelling, vp, ep, 4, 1, 2)
+# 4
+# sage: relabelling
+# array('i', [0, 2, 3, 1])
+# """
+# if n < 0 or len(relabelling) < n or len(p0) < n or len(p1) < n:
+# raise ValueError("invalid arguments")
+#
+# cdef array.array to_process = array.clone(p0, n, False) # FIFO stack of half-edges to process
+# cdef int s, t # bottom and top of to_process
+# cdef int e, e1
+#
+# to_process.data.as_ints[0] = root
+# s = 0
+# t = 1
+#
+# while s < t:
+# # pick the next half-edge and saturate its orbit with p0 and store the
+# # p1 images in to_process
+# e = to_process.data.as_ints[s]
+# s += 1
+# while relabelling.data.as_ints[e] == -1:
+# relabelling.data.as_ints[e] = image
+# image += 1
+# e1 = p1.data.as_ints[e]
+# if relabelling.data.as_ints[e1] == -1:
+# to_process.data.as_ints[t] = e1
+# t += 1
+# e = p0.data.as_ints[e]
+#
+# if image > n:
+# raise ValueError("invalid argument")
+#
+# return image
+
+
+def perm_relabel_on_edges(array.array r, int ne=-1):
r"""
- Return a canonical relabelling where ``start_edge`` is mapped to ``0``.
+ INPUT:
+
+ - ``r`` - relabelling permutation on half edges (list of length n)
+
+ - ``ne`` - number of edges
+
+ OUTPUT: two lists of length m
EXAMPLES::
sage: from array import array
- sage: from veerer.permutation import triangulation_relabelling_from
- sage: vp = array('i', [9, 13, 12, 11, 10, 14, 5, 6, 7, 8, 29, 25, 26, 27, 28, 1, 15, 16, 17, 18, 19, 0, 4, 3, 2, 21, 22, 23, 24, 20])
- sage: ep = array('i', [29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
- sage: triangulation_relabelling_from(vp, ep, 0)
- array('i', [0, 7, 21, 10, 18, 14, 20, 13, 17, 28, 27, 26, 25, 24, 23, 6, 5, 4, 3, 2, 1, 12, 16, 9, 15, 11, 19, 8, 22, 29])
+ sage: from veerer.permutation import perm_relabel_on_edges
+
+ sage: r = array('i', [3, 2, 4, 5, 1, 0, 7, 6])
+ sage: perm_relabel_on_edges(r)
+ (array('i', [1, 2, 0, 3]), array('i', [-1, 1, -1, -1]))
"""
- # NOTE: the algorithm is as follows
- # 0) we set k=0 and m=n-1 (labelling counter), any time we choose a new
- # label it is k and k is incremented. If it is not a folded edge, its
- # twin gets labelled m and m is decremented.
- # 1) the start_edge is relabelled 0 and set as pending
- # 2) while there is a pending half-edge, we look at its vp-cycle and
- # relabel each edge if needed
- cdef int n = len(vp)
-
- cdef int k = 0 # current available label at the front.
- cdef int m = n - 1 # current available label at the back.
- cdef array.array relabelling = array.clone(vp, n, False)
- cdef int e, e0
- for e in range(n):
- relabelling.data.as_ints[e] = -1
-
- relabelling.data.as_ints[start_edge] = 0
- k += 1
-
- if ep.data.as_ints[start_edge] != start_edge:
- relabelling.data.as_ints[ep.data.as_ints[start_edge]] = m
- m -= 1
-
- cdef array.array to_process = array.clone(vp, n, False)
- cdef int s = 1
- to_process.data.as_ints[0] = start_edge
- if ep.data.as_ints[start_edge] != start_edge:
- to_process.data.as_ints[1] = ep.data.as_ints[start_edge]
- s = 2
-
- while s:
- s -= 1
- e0 = to_process.data.as_ints[s]
- e = vp.data.as_ints[e0]
- while e != e0:
- if relabelling.data.as_ints[e] == -1:
- relabelling.data.as_ints[e] = k
- k += 1
- if ep.data.as_ints[e] != e:
- relabelling.data.as_ints[ep.data.as_ints[e]] = m
- m -= 1
- to_process.data.as_ints[s] = ep.data.as_ints[e]
- s += 1
- e = vp.data.as_ints[e]
-
- if k != m:
- # non transitive case (relabel naively the remaining components)
- for e in range(n):
- if relabelling.data.as_ints[e] == -1:
- relabelling.data.as_ints[e] = k
- k += 1
- if ep.data.as_ints[e] != e:
- relabelling.data.as_ints[ep.data.as_ints[e]] = m
- m -= 1
-
- return relabelling
+ if ne == -1:
+ if len(r) % 2:
+ raise ValueError("invalid input")
+ ne = len(r) // 2
+ if len(r) < 2 * ne:
+ raise ValueError("invalid input")
+
+ cdef array.array rr = array.clone(r, ne, False) # permutation
+ cdef array.array ss = array.clone(r, ne, False) # signs
+ cdef int i, j
+
+ for i in range(ne):
+ j = r.data.as_ints[2 * i]
+ jj = r.data.as_ints[2 * i + 1]
+ if jj != -1 and (j ^ 1 != jj):
+ raise ValueError("invalid relabelling permutation r={}".format(r))
+ rr.data.as_ints[i] = j // 2
+ ss.data.as_ints[i] = -1 if j % 2 else 1
+ return rr, ss
diff --git a/veerer/polyhedron/cone.py b/veerer/polyhedron/cone.py
index ba1f4be0..027b8a15 100644
--- a/veerer/polyhedron/cone.py
+++ b/veerer/polyhedron/cone.py
@@ -126,6 +126,18 @@ def __repr__(self):
return 'Cone of dimension {} in ambient dimension {} made of {} facets (backend={})'.format(
self.affine_dimension(), self.space_dimension(), len(self.ieqs()), self._name)
+ def __eq__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._cone == other._cone
+
+ def __ne__(self, other):
+ if type(self) is not type(other):
+ raise TypeError
+
+ return self._cone != other._cone
+
def space_dimension(self):
raise NotImplementedError
@@ -246,6 +258,18 @@ def add_constraints(self, constraints):
cone.add_constraint(constraint.ppl())
return Cone_ppl(ZZ, cone)
+ def intersection(self, other, inplace=False):
+ if type(self) is not type(other):
+ raise NotImplementedError
+ if inplace:
+ self._cone.intersection_assign(other._cone)
+ return self
+ else:
+ import ppl
+ cone = ppl.C_Polyhedron(self._cone)
+ cone.intersection_assign(other._cone)
+ return Cone_ppl(ZZ, cone)
+
class Cone_sage(Cone):
r"""
@@ -295,6 +319,16 @@ def add_constraints(self, cs):
new_cone = self._cone.intersection(Polyhedron(ieqs=ieqs, eqns=eqns))
return Cone_sage(new_cone.base_ring(), new_cone)
+ def intersection(self, other, inplace=False):
+ if type(self) is not type(other):
+ raise NotImplementedError
+ new_cone = self._cone.intersection(other._cone)
+ if inplace:
+ self._cone = new_cone
+ return self
+ else:
+ return Cone_sage(new_cone.base_ring(), new_cone)
+
class NFElementHandler:
def __init__(self, nf):
@@ -353,6 +387,18 @@ def rays(self):
def lines(self):
raise NotImplementedError
+ def intersection(self, other, inplace=False):
+ if type(self) is not type(other):
+ raise NotImplementedError
+ ieqs = self._nmz_result("SupportHyperplanes") + other._nmz_result("SupportHyperplanes")
+ eqns = self._nmz_result("Equations") + other._nmz_result("Equations")
+ cone = self._new_normaliz(ieqs, eqns)
+ if inplace:
+ self._cone = cone
+ return self
+ else:
+ return self._wrap(cone)
+
class Cone_normalizQQ(Cone_normaliz):
r"""
@@ -365,6 +411,13 @@ def _new(ieqs, eqns):
from PyNormaliz import NmzCone
return Cone_normalizQQ(QQ, NmzCone(inequalities=ieqs, equations=eqns))
+ def _new_normaliz(self, ieqs, eqns):
+ return self._new(ieqs, eqns)
+
+ def _wrap(self, cone):
+ from PyNormaliz import NmzCone
+ return Cone_normalizQQ(QQ, cone)
+
@staticmethod
def vector_space(dim=None):
if dim is None:
@@ -398,6 +451,15 @@ def _new(ieqs, eqns):
ans._nf_data = nf_data
return ans
+ def _new_normaliz(self, ieqs, eqns):
+ from PyNormaliz import NmzCone
+ return NmzCone(number_field=self._nf_data, inequalities=ieqs, equations=eqns)
+
+ def _wrap(self, cone):
+ ans = Cone_normalizNF(self._base_ring, cone)
+ ans._nf_data = self._nf_data
+ return ans
+
@staticmethod
def vector_space(dim=None):
if dim is None:
@@ -412,7 +474,6 @@ def vector_space(dim=None):
ans._nf_data = nf_data
return ans
-
def add_constraints(self, cs):
from PyNormaliz import NmzCone
ieqs = self._nmz_result("SupportHyperplanes")
diff --git a/veerer/polyhedron/linear_algebra.py b/veerer/polyhedron/linear_algebra.py
index c408b4e1..6bc89baf 100644
--- a/veerer/polyhedron/linear_algebra.py
+++ b/veerer/polyhedron/linear_algebra.py
@@ -29,9 +29,11 @@
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
+from sage.sets.disjoint_set import DisjointSet
from sage.rings.integer import GCD_list
from sage.arith.functions import LCM_list
+
def linear_form_project(equations, linear_form):
r"""
Put ``linear_form`` in a canonical way on the subspace defined by ``equations``.
@@ -102,3 +104,97 @@ def linear_form_normalize(base_ring, linear_form):
vector_normalize = linear_form_normalize
+
+
+def prime_decomposition(mat, partition=None):
+ r"""
+ Return the prime decomposition of the matrix ``mat`` together with dimensions.
+
+ EXAMPLES::
+
+ sage: from veerer.polyhedron.linear_algebra import prime_decomposition
+ sage: prime_decomposition(matrix(ZZ, 2, 3, [1, 0, 1, 0, 1, 0]))
+ [([0, 2], [1 1]), ([1], [1])]
+
+ sage: B = (ZZ**8).basis()
+ sage: m = matrix([B[0] + B[1] + B[2], B[1] - B[2], B[3] + B[4] + B[5], B[6] + B[7]])
+ sage: p0 = prime_decomposition(m)
+ sage: p0
+ [(
+ [ 1 0 2]
+ [0, 1, 2], [ 0 1 -1]
+ ),
+ ([3, 4, 5], [1 1 1]),
+ ([6, 7], [1 1])]
+ sage: r = random_matrix(ZZ, 4, algorithm="unimodular")
+ sage: m = r * m
+ sage: p1 = prime_decomposition(m)
+ sage: assert p1 == p0, (p1, p0, r)
+
+ Column permutations (be careful that permutations in sage act on {1, ..., n})::
+
+ sage: s = SymmetricGroup(6).random_element()
+ sage: sinv = ~s
+ sage: m.permute_columns(s)
+ sage: p1 = prime_decomposition(m)
+ sage: p2 = sorted((sorted(sinv(i + 1) - 1 for i in atom), dim) for atom, dim in p0)
+ sage: assert [(atom, subspace.nrows()) for atom, subspace in p1] == [(atom, subspace.nrows()) for atom, subspace in p2]
+ """
+ mat = mat.echelon_form()
+ if partition is None:
+ partition = DisjointSet(mat.ncols())
+ pivots = [None] * mat.ncols()
+ for i, r in enumerate(mat.rows()):
+ z = r.nonzero_positions()
+ if not z:
+ break
+ for j in range(1, len(z)):
+ partition.union(z[0], z[j])
+ pivots[z[0]] = i
+ gens = []
+ for atom in partition:
+ rows = [pivots[j] for j in atom if pivots[j] is not None]
+ gens.append(mat.matrix_from_rows_and_columns(rows, atom))
+
+ return list(zip(partition, gens))
+
+
+def is_rank_one(m, ratios=False):
+ r"""
+ Return whether the given matrix ``m`` has rank one.
+
+ If ``ratios`` is set to ``True`` then return a pair ``(ans,
+ row_or_col_ratios)``
+
+ EXAMPLES::
+
+ sage: from veerer.polyhedron.linear_algebra import is_rank_one
+ sage: assert is_rank_one(matrix(2, 2, [1, 1, 1, 1]))
+ sage: assert is_rank_one(matrix(2, 3, [1, 1, 0, 2, 2, 0]))
+ sage: assert not is_rank_one(matrix(2, 2, [1, 2, 1, 1]))
+ sage: assert not is_rank_one(matrix(2, 2, [1, 1, 1, 2]))
+
+ sage: is_rank_one(matrix(2, 2, [1, 1, 1, 1]), ratios=True)
+ (True, [1, 1])
+ sage: is_rank_one(matrix(2, 3, [1, 1, 0, 2, 2, 0]), ratios=True)
+ (True, [1, 2])
+ """
+ if not m:
+ raise ValueError("zero matrix")
+ coeffs = []
+ i0 = 0
+ while not m[i0]:
+ i0 += 1
+ coeffs.append(0)
+ j0 = 0
+ while not m[i0, j0]:
+ j0 += 1
+ i = i0 + 1
+ coeffs.append(1)
+ for i in range(i0 + 1, m.nrows()):
+ coeff = m[i, j0] / m[i0, j0]
+ for j in range(m.ncols()):
+ if m[i, j] != coeff * m[i0, j]:
+ return (False, None) if ratios else False
+ coeffs.append(coeff)
+ return (True, coeffs) if ratios else True
diff --git a/veerer/polyhedron/linear_expression.py b/veerer/polyhedron/linear_expression.py
index d94f6aef..77684f87 100644
--- a/veerer/polyhedron/linear_expression.py
+++ b/veerer/polyhedron/linear_expression.py
@@ -287,8 +287,10 @@ class LinearConstraint:
sage: 3 * L.variable(0) - 5 * L.variable(2) == 7
3*x0 - 5*x2 - 7 == 0
"""
- def __init__(self, op, left, right):
- self._expression = left - right
+ def __init__(self, op, left, right=None):
+ self._expression = left
+ if right is not None:
+ self._expression -= right
self._op = op
@staticmethod
diff --git a/veerer/strebel_graph.py b/veerer/strebel_graph.py
index 4a5ad282..f1db5503 100644
--- a/veerer/strebel_graph.py
+++ b/veerer/strebel_graph.py
@@ -31,8 +31,8 @@
from sage.matrix.constructor import matrix
from sage.matrix.special import identity_matrix
-from .permutation import perm_check, perm_cycles, perm_cycles_to_string, str_to_cycles_and_data
-from .triangulation import face_edge_perms_init, boundary_init, Triangulation
+from .permutation import perm_check, perm_cycles, perm_cycles_to_string, str_to_cycles_and_data, perm_orbit
+from .triangulation import face_boundary_init, Triangulation
from .constellation import Constellation
from .constants import *
from veerer.polyhedron import *
@@ -57,15 +57,15 @@ def one_edge_completion(t, angle_excess, colouring):
sage: from veerer import Triangulation
sage: from veerer.strebel_graph import one_edge_completion
sage: t = Triangulation("", "(0:1)(1:1)(~0:1,~1:1)")
- sage: colouring = [2,1,1,2]
- sage: angle_excess = array('i', [3, 3, 0, 3])
+ sage: colouring = [2, 1]
+ sage: angle_excess = array('i', [3, 0, 3, 3])
sage: one_edge_completion(t, angle_excess, colouring)
- ((Triangulation("(2,~0,~1)", boundary="(0:1)(1:1)(~2:1)"),
- array('i', [3, 3, 0, 3, 0, 0]),
- array('i', [2, 1, 1, 1, 1, 2])),
- (Triangulation("(2,~0,~1)", boundary="(0:1)(1:1)(~2:1)"),
- array('i', [3, 3, 0, 3, 0, 0]),
- array('i', [2, 1, 2, 2, 1, 2])))
+ ((Triangulation("(~0,2,~1)(0:1)(1:1)(~2:1)"),
+ array('i', [3, 0, 3, 0, 0, 3]),
+ array('i', [2, 1, 1])),
+ (Triangulation("(~0,2,~1)(0:1)(1:1)(~2:1)"),
+ array('i', [3, 0, 3, 0, 0, 3]),
+ array('i', [2, 1, 2])))
"""
# We insert the (m, M)-edge as follows
#
@@ -82,13 +82,17 @@ def one_edge_completion(t, angle_excess, colouring):
# x-----------x-----------x
# E B
- n = t._n
+ n = 2 * t._ne
vp = t._vp
ep = t._ep
fp = t._fp
bdry = t._bdry
- m = t.num_edges()
- M = m + 1
+ m = n # new positive half-edge
+ M = n + 1 # new negative half-edge
+
+ for x in range(1, n, 2):
+ if vp[x] == -1:
+ assert bdry[x] == angle_excess[x] == 0
found = False
for e in range(n):
@@ -98,44 +102,22 @@ def one_edge_completion(t, angle_excess, colouring):
if not found:
raise ValueError('already complete')
+ E = e ^ 1 if vp[e ^ 1] != -1 else e
+ a = vp[e]
+ A = a ^ 1 if vp[a ^ 1] != -1 else a
+ b = fp[e]
+ c = vp[A]
+ C = c ^ 1 if vp[c ^ 1] != -1 else c
- # add an edge (m, M) and possibly shift e
+ # build the new triangle face (e, m, A)
newvp = array('i', vp)
- newep = array('i', ep)
newfp = array('i', fp)
- newvp.insert(m, -1)
- newvp.insert(M, -1)
- newep.insert(m, -1)
- newep.insert(M, -1)
- newfp.insert(m, -1)
- newfp.insert(M, -1)
-
- newep[m] = M
- newep[M] = m
-
- for ee in range(n + 2):
- if (ee != m) and (ee != M):
- vpe = newvp[ee]
- fpe = newfp[ee]
- epe = newep[ee]
- if fpe > m - 1:
- newfp[ee] = fpe + 2
- if vpe > m - 1:
- newvp[ee] = vpe + 2
- if epe > m - 1:
- newep[ee] = epe + 2
-
- if e > m - 1:
- e = e + 2
- E = newep[e]
+ newvp.append(-1)
+ newvp.append(-1)
+ newfp.append(-1)
+ newfp.append(-1)
- # build the new triangle face (e, m, A)
- a = newvp[e]
- A = newep[a]
- b = newfp[e]
- c = newvp[A]
- C = newep[c]
newfp[e] = m
newfp[m] = A
newvp[A] = M
@@ -145,7 +127,6 @@ def one_edge_completion(t, angle_excess, colouring):
assert is_bigon == (b == A)
if is_bigon:
- assert b == A
newfp[M] = M
newvp[M] = m
newvp[b] = M
@@ -157,8 +138,8 @@ def one_edge_completion(t, angle_excess, colouring):
# new bdry: m is internal, M is boundary and e and A becomes internal
newbdry = bdry[:]
- newbdry.insert(m, 0)
- newbdry.insert(M, 1)
+ newbdry.append(0)
+ newbdry.append(1)
assert newbdry[e] == 1
assert newbdry[A] == 1
@@ -166,31 +147,30 @@ def one_edge_completion(t, angle_excess, colouring):
# build new colourings
colouring1 = array('i', colouring)
- colouring1.insert(m, -1)
- colouring1.insert(M, -1)
+ colouring1.append(-1)
# claim: a and e must have different colours
# (so that both RED and BLUE are allowed for the new edge (m, M)
- assert colouring1[a] != colouring1[e]
+ assert colouring1[a // 2] != colouring1[e // 2]
colouring2 = colouring1[:]
- colouring1[m] = colouring1[M] = RED
- colouring2[m] = colouring2[M] = BLUE
+ colouring1[-1] = RED
+ colouring2[-1] = BLUE
# build new angle excesses
angle_excess1 = angle_excess[:]
- angle_excess1.insert(m, -1)
- angle_excess1.insert(M, -1)
+ angle_excess1.append(-1)
+ angle_excess1.append(-1)
angle_excess1[M] = angle_excess1[A]
angle_excess1[A] = angle_excess1[m] = 0
angle_excess2 = angle_excess1[:]
if not is_bigon:
- col_a = colouring1[a]
- col_b = colouring1[b]
- col_c = colouring1[c]
- col_e = colouring1[e]
+ col_a = colouring1[a // 2]
+ col_b = colouring1[b // 2]
+ col_c = colouring1[c // 2]
+ col_e = colouring1[e // 2]
if col_a == BLUE:
assert col_e == RED
@@ -205,7 +185,11 @@ def one_edge_completion(t, angle_excess, colouring):
if col_c == RED:
angle_excess2[M] -= 1
- t = Triangulation.from_permutations(newvp, newep, newfp, (newbdry,), mutable=False, check=False)
+ t = Triangulation.from_permutations(newvp, newfp, (newbdry,), mutable=False, check=False)
+ for e in range(1, n + 2, 2):
+ if t._vp[e] == -1:
+ assert newbdry[e] == angle_excess1[e] == angle_excess2[e] == 0
+
return ((t, angle_excess1, colouring1), (t, angle_excess2, colouring2))
@@ -229,20 +213,32 @@ class StrebelGraph(Constellation):
"""
__slots__ = ['_excess']
- def __init__(self, faces, mutable=False, check=True):
+ def __init__(self, faces, excess=None, mutable=False, check=True):
if isinstance(faces, StrebelGraph):
fp = faces.face_permutation(copy=True)
ep = faces.edge_permutation(copy=True)
- bdry = faces.boundary_vector(copy=True)
+ excess = faces.half_plane_excess(copy=True)
else:
- faces, boundary = str_to_cycles_and_data(faces)
- fp, ep = face_edge_perms_init(faces)
- bdry = boundary_init(fp, ep, boundary)
+ fp, excess = face_boundary_init(faces, excess)
- Constellation.__init__(self, len(fp), None, ep, fp, (bdry,), mutable, check)
+ Constellation.__init__(self, len(fp) // 2, None, fp, (excess,), (), mutable, check)
+
+ def _check_vertex_separatrix(self, half_edge, angle):
+ half_edge = self._check_half_edge(half_edge)
+ if not isinstance(angle, numbers.Integral):
+ raise ValueError("invalid angle for separatrix")
+ angle = int(angle)
+ if angle < 0 or angle > self._excess[half_edge]:
+ raise ValueError("angle (={}) out of range for separatrix; must be >= 0 and <= {}".format(angle, self._excess[half_edge]))
+
+ def boundary_half_edges(self):
+ return self.half_edges()
def _set_data_pointers(self):
- self._excess = self._data[0]
+ self._excess = self._half_edges_data[0]
+
+ def half_plane_excess(self, copy=True):
+ return self._excess[:] if copy else self._excess
boundary_faces = Constellation.faces
num_boundary_faces = Constellation.num_faces
@@ -260,9 +256,9 @@ def __str__(self):
sage: from veerer import StrebelGraph
sage: T = StrebelGraph("(0,1,2)(~0,~1:1,~2:2)")
sage: str(T)
- 'StrebelGraph("(0,1,2)(~2:2,~0,~1:1)")'
+ 'StrebelGraph("(0,1,2)(~0,~1:1,~2:2)")'
"""
- bdry_cycles = perm_cycles_to_string(perm_cycles(self._fp, n=self._n), involution=self._ep, data=self._excess)
+ bdry_cycles = perm_cycles_to_string(perm_cycles(self._fp, 2*self._ne), edge_like=True, data=self._excess)
return 'StrebelGraph("%s")' % bdry_cycles
def __repr__(self):
@@ -283,7 +279,7 @@ def is_abelian(self, certificate=False):
ep = self._ep
vp = self._vp
- if any(e == ep[e] for e in range(self._n)):
+ if self.has_folded_edge():
return (False, None) if certificate else False
# Try to give a coherent holonomy with signs for each half edge. To
@@ -293,10 +289,10 @@ def is_abelian(self, certificate=False):
# The half-edge orientations is propagated walking along edges and
# vertices.
- oris = [None] * self._n # list of orientations decided so far
+ oris = [None] * (2 * self._ne) # list of orientations decided so far
oris[0] = True
- oris[ep[0]] = False
- q = [0, ep[0]] # queue of half-edges to be treated
+ oris[ep(0)] = False
+ q = [0, ep(0)] # queue of half-edges to be treated
while q:
e = q.pop()
@@ -309,10 +305,10 @@ def is_abelian(self, certificate=False):
o = not o
if oris[f] is None:
- assert oris[ep[f]] is None
+ assert oris[ep(f)] is None
oris[f] = o
- oris[ep[f]] = not o
- q.append(ep[f])
+ oris[ep(f)] = not o
+ q.append(ep(f))
elif oris[f] != o:
return (False, None) if certificate else False
else:
@@ -322,6 +318,88 @@ def is_abelian(self, certificate=False):
return (True, oris) if certificate else True
+ def vertex_separatrices(self, flat=True):
+ r"""
+ Return the pairs ``(h, a)`` encoding vertex separatrices on this Strebel graph.
+
+ EXAMPLES::
+
+ sage: from veerer import StrebelGraph
+ sage: sg = StrebelGraph("(0,1,2,3)(~1,~0:1,~3:2,~2:1)")
+ sage: sg.vertex_separatrices()
+ [(0, 0),
+ (7, 0),
+ (7, 1),
+ (7, 2),
+ (1, 0),
+ (1, 1),
+ (2, 0),
+ (3, 0),
+ (4, 0),
+ (5, 0),
+ (5, 1),
+ (6, 0)]
+ sage: seps = sg.vertex_separatrices(flat=False)
+ sage: seps
+ [[(0, 0), (7, 0), (7, 1), (7, 2)],
+ [(1, 0), (1, 1), (2, 0)],
+ [(3, 0), (4, 0)],
+ [(5, 0), (5, 1), (6, 0)]]
+ sage: all(sg.vertex_angle(h) == len(sep) for sep in seps for h, a in sep)
+ True
+ """
+ separatrices = []
+ for cycle in self.vertices():
+ orbit = []
+ for h in cycle:
+ for a in range(self._excess[h] + 1):
+ orbit.append((h, a))
+ if flat:
+ separatrices.extend(orbit)
+ else:
+ separatrices.append(orbit)
+ return separatrices
+
+ # TODO: (for Kai) should we go clockwise or counter-clockwise around the face
+ # TODO: (for Kai) what should we do for angle=0 (ie infinite cylinder) faces
+ # which have no associated separatrices?
+ def face_separatrices(self, flat=True):
+ r"""
+ Return the pairs ``(h, a)`` encoding face separatrices on this Strebel graph.
+
+ EXAMPLES::
+
+ sage: from veerer import StrebelGraph
+ sage: sg = StrebelGraph("(0,1,2)(~0,~1:1,~2:2)")
+ sage: sg.face_separatrices()
+ [(3, 0), (5, 1), (5, 0)]
+ sage: seps = sg.face_separatrices(flat=False)
+ sage: seps
+ [[(3, 0), (5, 1), (5, 0)]]
+ sage: all(sg.face_angle(h) == -len(sep) for sep in seps for h, a in sep)
+ True
+
+ An example in H(1^2, -1^2) where the two faces have no separatrices::
+
+ sage: sg = StrebelGraph("(0,1,2,3)(~0,~1,~2,~3)")
+ sage: sg.face_separatrices(flat=False)
+ []
+ sage: sg.face_separatrices(flat=True)
+ []
+ """
+ separatrices = []
+ for cycle in self.faces():
+ orbit = []
+ for h in cycle:
+ for a in range(self._excess[h] -1, -1, -1):
+ orbit.append((h, a))
+ if orbit:
+ if flat:
+ separatrices.extend(orbit)
+ else:
+ separatrices.append(orbit)
+ return separatrices
+
def stratum(self):
r"""
Return the stratum of Abelian or quadratic differentials of this Strebel graph.
@@ -367,43 +445,7 @@ def stratum(self):
from surface_dynamics import Stratum
return Stratum(hol + [-1] * num_folded_edges + mer, k)
- @staticmethod
- def from_face_edge_perms(vp, ep, fp=None, boundary=None, mutable=False, check=True):
- r"""
- Deprecated methods.
-
- Use the classmethod ``Constellation.from_permutations`` instead.
-
- EXAMPLES::
-
- sage: from veerer import *
- sage: from array import array
- sage: vp = array('i', [1, 3, 0, 2])
- sage: ep = array('i', [3, 2, 1, 0])
- sage: StrebelGraph.from_face_edge_perms(vp, ep, boundary = array('i', [1, 0, 0, 1]))
- doctest:warning
- ...
- UserWarning: the method StrebelGraph.from_face_edge_perms is deprecated; use the classmethod from_permutations instead
- StrebelGraph("(0:1,1,~0:1,~1)")
- """
- import warnings
- warnings.warn('the method StrebelGraph.from_face_edge_perms is deprecated; use the classmethod from_permutations instead')
-
- n = len(vp)
-
- if fp is None:
- fp = array('i', [-1] * n)
- for i in range(n):
- fp[ep[vp[i]]] = i
-
- if boundary is None:
- bdry = array('i', [0] * n)
- else:
- bdry = array('i', boundary)
-
- return StrebelGraph.from_permutations(vp, ep, fp, (bdry,), mutable, check)
-
- def abelian_cover(self, mutable=False):
+ def abelian_cover(self, mutable=False, involution_and_quotient=False):
r"""
Return the orientation double cover of this Strebel graph.
@@ -412,32 +454,55 @@ def abelian_cover(self, mutable=False):
sage: from veerer import StrebelGraph
sage: sg = StrebelGraph("(0:1,1,2)(~2,~3,~4:1)(3,~1:1,5)(~0:1,4,~5)")
sage: sg.abelian_cover()
- StrebelGraph("(0:1,1,2,~11:1,~10,~9)(3,~1:1,~6,~8,10:1,5)(4,6,~0:1,~7,~5,11:1)(7:1,9,8,~4:1,~2,~3)")
+ StrebelGraph("(0:1,1,2,~6:1,~7,~8)(~0:1,~10,~5,6:1,4,11)(~1:1,~11,~9,7:1,5,3)(~2,~3,10:1,8,9,~4:1)")
sage: print(sg.stratum(), sg.abelian_cover().stratum()) # optional - surface_dynamics
Q_0(2^4, -3^4) H_1(1^8, -2^4)
+
+ sage: StrebelGraph("(0)").abelian_cover()
+ StrebelGraph("(0)(~0)")
+ sage: StrebelGraph("(0:1)").abelian_cover()
+ StrebelGraph("(0:1,~0:1)")
+ sage: StrebelGraph("(0,~0)").abelian_cover()
+ StrebelGraph("(0,1)(~0,~1)")
"""
- n = self._n
vp = self._vp
- ep = self._ep
-
- ep_cov = array('i', [-1] * (2 * n))
- vp_cov = array('i', [-1] * (2 * n))
- for e in range(n):
- f = ep[e]
- ep_cov[e] = f + n
- ep_cov[f + n] = e
+ n = (4 * self._ne - 2 * self.num_folded_edges())
+
+ j = 2 * self._ne
+ inv = array('i', [-1] * (2 * self._ne)) # involution on the cover
+ quot = array('i', [-1] * n) # quotient map
+ excess_cov = array('i', [-1] * n)
+ for e in range(self._ne):
+ quot[2 * e] = 2 * e
+ if self._vp[2 * e + 1] == -1:
+ inv[2 * e] = 2 * e + 1
+ excess_cov[2 * e] = excess_cov[2 * e + 1] = self._excess[2 * e]
+ quot[2 * e + 1] = 2 * e
+ else:
+ excess_cov[2 * e] = excess_cov[j + 1] = self._excess[2 * e]
+ excess_cov[2 * e + 1] = excess_cov[j] = self._excess[2 * e + 1]
+ inv[2 * e] = j + 1
+ inv[2 * e + 1] = j
+ quot[2 * e + 1] = 2 * e + 1
+ quot[j] = 2 * e + 1
+ quot[j + 1] = 2 * e
+ j += 2
+
+ vp_cov = array('i', [-1] * n)
+ for e in range(2 * self._ne):
f = vp[e]
- if self._excess[e] % 2 == 0:
- vp_cov[e] = f + n
- vp_cov[e + n] = f
+ if f == -1:
+ continue
+ if ((self._excess[e] % 2 == 0) + (e % 2 != f % 2)) % 2:
+ vp_cov[e] = inv[f]
+ vp_cov[inv[e]] = f
else:
vp_cov[e] = f
- vp_cov[e + n] = f + n
-
- excess_cov = self._excess * 2
+ vp_cov[inv[e]] = inv[f]
- return StrebelGraph.from_permutations(vp_cov, ep_cov, None, (excess_cov,), mutable=mutable, check=False)
+ sg_cov = StrebelGraph.from_permutations(vp_cov, None, (excess_cov,), mutable=mutable, check=False)
+ return (sg_cov, inv, quot) if involution_and_quotient else sg_cov
def as_linear_family(self):
r"""
@@ -448,7 +513,7 @@ def as_linear_family(self):
sage: from veerer import StrebelGraph
sage: G = StrebelGraph("(0,1,2)(~0,~1:1,~2:2)")
sage: G.as_linear_family()
- StrebelGraphLinearFamily("(0,1,2)(~2:2,~0,~1:1)", [(1, 0, 0), (0, 1, 0), (0, 0, 1)])
+ StrebelGraphLinearFamily("(0,1,2)(~0,~1:1,~2:2)", [(1, 0, 0), (0, 1, 0), (0, 0, 1)])
"""
from sage.matrix.special import identity_matrix
from .linear_family import StrebelGraphLinearFamily
@@ -468,28 +533,36 @@ def angle_excess(self, colouring, slope=VERTICAL):
sage: G = StrebelGraph("(0,1,2)(~0,~1:1,~2:2)")
sage: for colouring in G.colourings():
....: print(colouring, G.angle_excess(colouring))
- array('i', [1, 1, 1, 1, 1, 1]) array('i', [1, 1, 1, 3, 2, 1])
- array('i', [1, 1, 2, 2, 1, 1]) array('i', [0, 1, 1, 3, 2, 0])
- array('i', [1, 2, 1, 1, 2, 1]) array('i', [1, 1, 0, 2, 2, 1])
- array('i', [1, 2, 2, 2, 2, 1]) array('i', [0, 1, 1, 3, 2, 0])
- array('i', [2, 1, 1, 1, 1, 2]) array('i', [1, 0, 1, 3, 1, 1])
- array('i', [2, 1, 2, 2, 1, 2]) array('i', [1, 0, 1, 3, 1, 1])
- array('i', [2, 2, 1, 1, 2, 2]) array('i', [1, 1, 0, 2, 2, 1])
- array('i', [2, 2, 2, 2, 2, 2]) array('i', [1, 1, 1, 3, 2, 1])
+ array('i', [1, 1, 1]) array('i', [1, 1, 1, 2, 1, 3])
+ array('i', [1, 1, 2]) array('i', [0, 0, 1, 2, 1, 3])
+ array('i', [1, 2, 1]) array('i', [1, 1, 1, 2, 0, 2])
+ array('i', [1, 2, 2]) array('i', [0, 0, 1, 2, 1, 3])
+ array('i', [2, 1, 1]) array('i', [1, 1, 0, 1, 1, 3])
+ array('i', [2, 1, 2]) array('i', [1, 1, 0, 1, 1, 3])
+ array('i', [2, 2, 1]) array('i', [1, 1, 1, 2, 0, 2])
+ array('i', [2, 2, 2]) array('i', [1, 1, 1, 2, 1, 3])
+
+ sage: G = StrebelGraph("(0,1,~1)")
+ sage: for colouring in G.colourings():
+ ....: print(colouring, G.angle_excess(colouring))
+ array('i', [1, 1]) array('i', [1, 0, 1, 1])
+ array('i', [1, 2]) array('i', [0, 0, 1, 1])
+ array('i', [2, 1]) array('i', [1, 0, 0, 1])
+ array('i', [2, 2]) array('i', [1, 0, 1, 1])
"""
# remark: red-red corners with angle excess 0 and 1 both correspond
# to zero half-plane excess.
# claim: it is impossible to have a red-red corner with 0 angle
# excess in a strebel graph.
- n = self._n
+ n = 2 * self._ne
vp = self._vp
alpha = array('i', self._excess)
for e in range(n):
e1 = vp[e]
- if (slope == VERTICAL and (colouring[e] != RED or colouring[e1] != BLUE)) or \
- (slope == HORIZONTAL and (colouring[e] != BLUE or colouring[e1] != RED)):
+ if (e1 != -1) and ((slope == VERTICAL and (colouring[e // 2] != RED or colouring[e1 // 2] != BLUE)) or \
+ (slope == HORIZONTAL and (colouring[e // 2] != BLUE or colouring[e1 // 2] != RED))):
alpha[e] += 1
return alpha
@@ -506,12 +579,14 @@ def colourings(self):
sage: G = StrebelGraph("(0,1,2)(~0,~1:1,~2:2)")
sage: list(G.colourings())
- [array('i', [1, 1, 1, 1, 1, 1]),
- array('i', [1, 1, 2, 2, 1, 1]),
- ...
- array('i', [2, 2, 1, 1, 2, 2]),
- array('i', [2, 2, 2, 2, 2, 2])]
-
+ [array('i', [1, 1, 1]),
+ array('i', [1, 1, 2]),
+ array('i', [1, 2, 1]),
+ array('i', [1, 2, 2]),
+ array('i', [2, 1, 1]),
+ array('i', [2, 1, 2]),
+ array('i', [2, 2, 1]),
+ array('i', [2, 2, 2])]
sage: G = StrebelGraph("(0,1,2,3)")
sage: list(G.colourings())
@@ -521,14 +596,7 @@ def colourings(self):
array('i', [2, 2, 2, 1]),
array('i', [2, 2, 2, 2])]
"""
- ne = self.num_edges()
- m = self.num_folded_edges()
- ep = self._ep
-
- for colouring in itertools.product([RED, BLUE], repeat=ne):
- colouring = array('i', colouring)
- colouring.extend([colouring[ep[e]] for e in range(ne, self._n)])
- yield colouring
+ return (array('i', x) for x in itertools.product([RED, BLUE], repeat=self._ne))
def _set_strebel_constraints(self, insert, x):
for v in x:
@@ -577,11 +645,13 @@ def veering_triangulations(self, colouring, slope=VERTICAL, mutable=False):
sage: examples.append(StrebelGraph("(~1:1,~0,1:1,0)"))
sage: examples.append(StrebelGraph("(0,~1)(1)(~0)"))
sage: examples.append(StrebelGraph("(0:2)(1:2)(~1,~0:2)"))
+ sage: examples.append(StrebelGraph("(0,1,2,3)"))
sage: for G in examples: # optional - surface_dynamics
....: print(G.stratum())
H_1(2, -2)
H_0(1, -1^3)
H_0(4, -2^3)
+ Q_0(2, -1^4, -2)
sage: for G in examples:
....: print(G)
@@ -591,27 +661,36 @@ def veering_triangulations(self, colouring, slope=VERTICAL, mutable=False):
....: assert all(vt.stratum() == G.stratum() for vt in vts) # optional - surface_dynamics
....: print(colouring, list(G.veering_triangulations(colouring)))
StrebelGraph("(0,~1:1,~0,1:1)")
- array('i', [1, 1, 1, 1]) [VeeringTriangulation("", boundary="(0:1,~1:2,~0:1,1:2)", colouring="RR")]
- array('i', [1, 2, 2, 1]) [VeeringTriangulation("(0,2,1)(3,~1,~0)", boundary="(~3:1,~2:2)", colouring="RBBR"), VeeringTriangulation("(0,2,1)(3,~1,~0)", boundary="(~3:2,~2:2)", colouring="RBBB"), VeeringTriangulation("(0,2,1)(3,~1,~0)", boundary="(~3:2,~2:2)", colouring="RBRR"), VeeringTriangulation("(0,2,1)(3,~1,~0)", boundary="(~3:2,~2:1)", colouring="RBRB")]
- array('i', [2, 1, 1, 2]) [VeeringTriangulation("", boundary="(0:1,~1:1,~0:1,1:1)", colouring="BR")]
- array('i', [2, 2, 2, 2]) [VeeringTriangulation("", boundary="(0:1,~1:2,~0:1,1:2)", colouring="BB")]
- StrebelGraph("(0,~1)(1)(~0)")
- array('i', [1, 1, 1, 1]) [VeeringTriangulation("", boundary="(0:1,~1:1)(1:1)(~0:1)", colouring="RR")]
- array('i', [1, 2, 2, 1]) [VeeringTriangulation("(0,2,~1)", boundary="(1:1)(~2:1)(~0:1)", colouring="RBR"), VeeringTriangulation("(0,2,~1)", boundary="(1:1)(~2:1)(~0:1)", colouring="RBB")]
- array('i', [2, 1, 1, 2]) [VeeringTriangulation("(0,~1,2)", boundary="(1:1)(~2:1)(~0:1)", colouring="BRR"), VeeringTriangulation("(0,~1,2)", boundary="(1:1)(~2:1)(~0:1)", colouring="BRB")]
- array('i', [2, 2, 2, 2]) [VeeringTriangulation("", boundary="(0:1,~1:1)(1:1)(~0:1)", colouring="BB")]
- StrebelGraph("(0:2)(1:2)(~1,~0:2)")
- array('i', [1, 1, 1, 1]) [VeeringTriangulation("", boundary="(0:3)(1:3)(~1:1,~0:3)", colouring="RR")]
- array('i', [1, 2, 2, 1]) [VeeringTriangulation("", boundary="(0:3)(1:3)(~1:1,~0:2)", colouring="RB")]
- array('i', [2, 1, 1, 2]) [VeeringTriangulation("(2,~0,~1)", boundary="(0:3)(1:3)(~2:3)", colouring="BRR"), VeeringTriangulation("(2,~0,~1)", boundary="(0:3)(1:3)(~2:3)", colouring="BRB")]
- array('i', [2, 2, 2, 2]) [VeeringTriangulation("", boundary="(0:3)(1:3)(~1:1,~0:3)", colouring="BB")]
+ array('i', [1, 1]) [VeeringTriangulation("(0:1,~1:2,~0:1,1:2)", "RR")]
+ array('i', [1, 2]) [VeeringTriangulation("(0,2,1)(~0,3,~1)(~2:2,~3:1)", "RBBR"), VeeringTriangulation("(0,2,1)(~0,3,~1)(~2:2,~3:2)", "RBBB"), VeeringTriangulation("(0,2,1)(~0,3,~1)(~2:2,~3:2)", "RBRR"), VeeringTriangulation("(0,2,1)(~0,3,~1)(~2:1,~3:2)", "RBRB")]
+ array('i', [2, 1]) [VeeringTriangulation("(0:1,~1:1,~0:1,1:1)", "BR")]
+ array('i', [2, 2]) [VeeringTriangulation("(0:1,~1:2,~0:1,1:2)", "BB")]
+ StrebelGraph("(0,~1)(~0)(1)")
+ array('i', [1, 1]) [VeeringTriangulation("(0:1,~1:1)(~0:1)(1:1)", "RR")]
+ array('i', [1, 2]) [VeeringTriangulation("(0,2,~1)(~0:1)(1:1)(~2:1)", "RBR"), VeeringTriangulation("(0,2,~1)(~0:1)(1:1)(~2:1)", "RBB")]
+ array('i', [2, 1]) [VeeringTriangulation("(0,~1,2)(~0:1)(1:1)(~2:1)", "BRR"), VeeringTriangulation("(0,~1,2)(~0:1)(1:1)(~2:1)", "BRB")]
+ array('i', [2, 2]) [VeeringTriangulation("(0:1,~1:1)(~0:1)(1:1)", "BB")]
+ StrebelGraph("(0:2)(~0:2,~1)(1:2)")
+ array('i', [1, 1]) [VeeringTriangulation("(0:3)(~0:3,~1:1)(1:3)", "RR")]
+ array('i', [1, 2]) [VeeringTriangulation("(0:3)(~0:2,~1:1)(1:3)", "RB")]
+ array('i', [2, 1]) [VeeringTriangulation("(~0,~1,2)(0:3)(1:3)(~2:3)", "BRR"), VeeringTriangulation("(~0,~1,2)(0:3)(1:3)(~2:3)", "BRB")]
+ array('i', [2, 2]) [VeeringTriangulation("(0:3)(~0:3,~1:1)(1:3)", "BB")]
+ StrebelGraph("(0,1,2,3)")
+ array('i', [1, 1, 1, 1]) [VeeringTriangulation("(0:1,1:1,2:1,3:1)", "RRRR")]
+ array('i', [1, 1, 1, 2]) [VeeringTriangulation("(0,4,3)(1:1,2:1,~4:1)", "RRRBR"), VeeringTriangulation("(0,4,3)(1,5,~4)(2:1,~5:1)", "RRRBBR"), VeeringTriangulation("(0,4,3)(1,5,~4)(2,6,~5)(~6:1)", "RRRBBBR"), VeeringTriangulation("(0,4,3)(1,5,~4)(2,6,~5)(~6:1)", "RRRBBBB")]
+ ...
+ array('i', [2, 2, 2, 2]) [VeeringTriangulation("(0:1,1:1,2:1,3:1)", "BBBB")]
"""
def is_complete(t, angle_excess, colouring):
return not any(b1 and b2 == 0 for (b1, b2) in zip(t._bdry, angle_excess))
- n = self._n
+ n = 2 * self._ne
angle_excess = self.angle_excess(colouring, slope=slope)
- t0 = Triangulation.from_permutations(self._vp[:], self._ep[:], self._fp[:], (array('i', [1] * n),), mutable=False, check=True)
+ bdry = array('i', [1] * n)
+ for e in range(1, n, 2):
+ if self._vp[e] == -1:
+ bdry[e] = 0
+ t0 = Triangulation.from_permutations(self._vp[:], self._fp[:], (bdry,), mutable=False, check=True)
T = (t0, angle_excess, colouring)
complete = []
incomplete = []
@@ -631,10 +710,9 @@ def is_complete(t, angle_excess, colouring):
from .veering_triangulation import VeeringTriangulation
for t, angle_excess, colouring in complete:
vp = t._vp
- ep = t._ep
fp = t._fp
cols = array('i', colouring)
- yield VeeringTriangulation.from_permutations(vp, ep, fp, (angle_excess, cols), mutable=mutable, check=True)
+ yield VeeringTriangulation.from_permutations(vp, fp, (angle_excess,), (cols,), mutable=mutable, check=True)
def delaunay_triangulations(self, colouring, slope=VERTICAL, mutable=False, backend=None):
r"""
@@ -653,28 +731,28 @@ def delaunay_triangulations(self, colouring, slope=VERTICAL, mutable=False, back
....: for colouring in G.colourings():
....: print(colouring, sum(1 for _ in G.veering_triangulations(colouring)), sum(1 for _ in G.delaunay_triangulations(colouring)))
StrebelGraph("(0,~1:1,~0,1:1)")
- array('i', [1, 1, 1, 1]) 1 1
- array('i', [1, 2, 2, 1]) 4 2
- array('i', [2, 1, 1, 2]) 1 1
- array('i', [2, 2, 2, 2]) 1 1
- StrebelGraph("(0,2,~1)(1)(~2,~0)")
- array('i', [1, 1, 1, 1, 1, 1]) 1 1
- array('i', [1, 1, 2, 2, 1, 1]) 6 5
- array('i', [1, 2, 1, 1, 2, 1]) 3 3
- array('i', [1, 2, 2, 2, 2, 1]) 6 5
- array('i', [2, 1, 1, 1, 1, 2]) 6 3
- array('i', [2, 1, 2, 2, 1, 2]) 3 3
- array('i', [2, 2, 1, 1, 2, 2]) 6 3
- array('i', [2, 2, 2, 2, 2, 2]) 1 1
- StrebelGraph("(0:2,2,~1)(1,~0)(~2)")
- array('i', [1, 1, 1, 1, 1, 1]) 1 1
- array('i', [1, 1, 2, 2, 1, 1]) 2 2
- array('i', [1, 2, 1, 1, 2, 1]) 2 2
- array('i', [1, 2, 2, 2, 2, 1]) 2 2
- array('i', [2, 1, 1, 1, 1, 2]) 6 5
- array('i', [2, 1, 2, 2, 1, 2]) 6 5
- array('i', [2, 2, 1, 1, 2, 2]) 2 2
- array('i', [2, 2, 2, 2, 2, 2]) 1 1
+ array('i', [1, 1]) 1 1
+ array('i', [1, 2]) 4 2
+ array('i', [2, 1]) 1 1
+ array('i', [2, 2]) 1 1
+ StrebelGraph("(0,2,~1)(~0,~2)(1)")
+ array('i', [1, 1, 1]) 1 1
+ array('i', [1, 1, 2]) 6 5
+ array('i', [1, 2, 1]) 3 3
+ array('i', [1, 2, 2]) 6 5
+ array('i', [2, 1, 1]) 6 3
+ array('i', [2, 1, 2]) 3 3
+ array('i', [2, 2, 1]) 6 3
+ array('i', [2, 2, 2]) 1 1
+ StrebelGraph("(0:2,2,~1)(~0,1)(~2)")
+ array('i', [1, 1, 1]) 1 1
+ array('i', [1, 1, 2]) 2 2
+ array('i', [1, 2, 1]) 2 2
+ array('i', [1, 2, 2]) 2 2
+ array('i', [2, 1, 1]) 6 5
+ array('i', [2, 1, 2]) 6 5
+ array('i', [2, 2, 1]) 2 2
+ array('i', [2, 2, 2]) 1 1
"""
for vt in self.veering_triangulations(colouring, slope, mutable):
if vt.is_delaunay(backend):
@@ -699,12 +777,12 @@ def residue_matrix(self):
[-1]
sage: StrebelGraph("(0,~1)(1)(~0)").residue_matrix()
[ 1 1]
- [ 0 -1]
[-1 0]
+ [ 0 -1]
sage: StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)").residue_matrix()
[ 1 1 1 1]
- [ 0 -1 0 0]
[-1 0 0 -1]
+ [ 0 -1 0 0]
[ 0 0 -1 0]
sage: StrebelGraph("(0:1,~0:1)").residue_matrix()
@@ -712,31 +790,55 @@ def residue_matrix(self):
sage: StrebelGraph("(0:1,1:1,2)(~2,~3:1,~4:1)(3,~1,5)(~0,4,~5)").residue_matrix()
[ 1 -1 -1 0 0 0]
- [ 0 1 0 1 0 1]
[-1 0 0 0 -1 -1]
+ [ 0 1 0 1 0 1]
[ 0 0 1 -1 1 0]
sage: sg = StrebelGraph("(0:1,1,2)(~2,~3,~4:1)(3,~1:1,5)(~0:1,4,~5)")
sage: sg.abelian_cover().residue_matrix()
- [ 1 1 1 0 0 0 0 0 0 -1 -1 -1]
- [ 0 -1 0 1 0 1 -1 0 -1 0 1 0]
- [-1 0 0 0 1 -1 1 -1 0 0 0 1]
- [ 0 0 -1 -1 -1 0 0 1 1 1 0 0]
- """
- ep = self._ep
- nf = self.num_faces()
+ [ 1 1 1 0 0 0 -1 -1 -1 0 0 0]
+ [-1 0 0 0 1 -1 1 0 0 0 -1 1]
+ [ 0 -1 0 1 0 1 0 1 0 -1 0 -1]
+ [ 0 0 -1 -1 -1 0 0 0 1 1 1 0]
+
+ Examples in the gothic locus::
+
+ sage: StrebelGraph("(0)(1,2,3,4)(~2,~4,5)(~3,~5)").residue_matrix()
+ [1 0 0 0 0 0]
+ [0 1 1 1 1 0]
+ [0 0 1 0 1 1]
+ [0 0 0 1 0 1]
+ """
ne = self.num_edges()
- r = matrix(ZZ, nf, ne)
- ans, orientations = self.is_abelian(certificate=True)
- if not ans:
- raise ValueError('not an Abelian differential')
- orientations = [1 if x else -1 for x in orientations]
- for i, f in enumerate(self.faces()):
- for e in f:
- j = e if e < ep[e] else ep[e]
- r[i, j] += orientations[e]
+ is_abelian, orientations = self.is_abelian(certificate=True)
+ if is_abelian:
+ # Abelian differential: we make a consistent global choice of signs for the residues
+ nf = self.num_faces()
+ r = matrix(ZZ, nf, ne)
+
+ orientations = [1 if x else -1 for x in orientations]
+
+ for i, f in enumerate(self.faces()):
+ for h in f:
+ r[i, h // 2] += orientations[h]
+
+ else:
+ # quadratic differentials: we can only have a local choice of signs
+ # Note that faces whose angle is an odd multiple of pi have residue zero and we
+ # ignore them
+ fp = self._fp
+ even_angle_faces = [f for f in self.boundary_faces() if self.face_angle(f[0]) % 2 == 0]
+ r = matrix(ZZ, len(even_angle_faces), ne)
+ for i, f in enumerate(even_angle_faces):
+ o = 1
+ for h0 in f:
+ r[i, h0 // 2] += o
+
+ h1 = fp[h0]
+ if self.half_edge_num_separatrices(h1) % 2 == 0:
+ o *= -1
return r
@@ -785,23 +887,23 @@ def add_residue_constraints(self, residue_constraints):
EXAMPLES::
sage: from veerer import StrebelGraph
- sage: G = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)")
+ sage: G = StrebelGraph("(0,2,~3,~1)(~0,3)(1)(~2)")
- sage: f1 = G.add_residue_constraints([[1, 2, 0, 0]])
+ sage: f1 = G.add_residue_constraints([[1, 0, 2, 0]])
sage: f1
- StrebelGraphLinearFamily("(0,2,~3,~1)(1)(3,~0)(~2)", [(1, 0, 0, -1), (0, 1, 0, 1), (0, 0, 1, -1)])
+ StrebelGraphLinearFamily("(0,2,~3,~1)(~0,3)(1)(~2)", [(1, 0, 0, -1), (0, 1, 0, 1), (0, 0, 1, -1)])
sage: f1.is_core()
True
- sage: f2 = G.add_residue_constraints([[1, 1, 0, 0]])
+ sage: f2 = G.add_residue_constraints([[1, 0, 1, 0]])
sage: f2
- StrebelGraphLinearFamily("(0,2,~3,~1)(1)(3,~0)(~2)", [(1, 0, 0, -1), (0, 1, 0, 0), (0, 0, 1, -1)])
+ StrebelGraphLinearFamily("(0,2,~3,~1)(~0,3)(1)(~2)", [(1, 0, 0, -1), (0, 1, 0, 0), (0, 0, 1, -1)])
sage: f2.is_core()
False
sage: f3 = G.add_residue_constraints([[0, 1, -1, 0], [0, 1, 0, -1]])
sage: f3
- StrebelGraphLinearFamily("(0,2,~3,~1)(1)(3,~0)(~2)", [(1, 0, 0, -1), (0, 1, 1, 1)])
+ StrebelGraphLinearFamily("(0,2,~3,~1)(~0,3)(1)(~2)", [(1, 0, 0, -1), (0, 1, 1, 1)])
sage: f3.is_core()
True
"""
@@ -831,7 +933,7 @@ def delaunay_strebel_automaton(self, run=True, backend=None):
sage: G = StrebelGraph("(0,1,2,3)")
sage: G.delaunay_strebel_automaton()
- Delaunay-Strebel automaton with 328 vertices
+ Delaunay-Strebel automaton with 327 states
"""
from .automaton import DelaunayStrebelAutomaton
A = DelaunayStrebelAutomaton(backward=True, backend=backend)
@@ -839,3 +941,88 @@ def delaunay_strebel_automaton(self, run=True, backend=None):
if run:
A.run()
return A
+
+ def half_edge_num_separatrices(self, half_edge):
+ r"""
+ Return the number of separatrices in the corner of the half-edge
+
+ EXAMPLES::
+
+ sage: from veerer import StrebelGraph
+ sage: G = StrebelGraph("(0:1, 1:0, ~1:1, ~0:0)")
+ sage: G.half_edge_num_separatrices(0)
+ 2
+ sage: G.half_edge_num_separatrices(1)
+ 1
+ sage: G.half_edge_num_separatrices(2)
+ 1
+ """
+ return self._excess[half_edge] + 1
+
+ def vertex_angle(self, half_edge):
+ r"""
+ Return the angle of the vertex adjacent to ``half_edge``.
+
+ EXAMPLES::
+
+ sage: from veerer import StrebelGraph
+ sage: sg = StrebelGraph("(0:1, 1:0)(~0:3, ~1:1)")
+ sage: sg.vertex_angle(0)
+ 4
+ sage: sg.vertex_angle(1)
+ 5
+ """
+ a = 0
+ for h in perm_orbit(self._vp, half_edge):
+ a = a + self._excess[h] + 1
+ return a
+
+ def face_angle(self, half_edge, check=True):
+ r"""
+ Return the angle of the face adjacent to ``half_edge``.
+
+ EXAMPLES::
+
+ sage: from veerer import StrebelGraph
+ sage: G = StrebelGraph("(0:1, 1:0)(~0:2, ~1:1)")
+ sage: G.face_angle(0)
+ -1
+ sage: G.face_angle(1)
+ -3
+ """
+ if check:
+ h = self._check_half_edge(half_edge)
+
+ a = 0
+ for h in perm_orbit(self._fp, half_edge):
+ a = a - self._excess[h]
+ return a
+
+ def _normalization_face_separatrix(self, half_edge, angle):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: G = StrebelGraph("(0:1, 1:0, ~1:1, ~0:0)")
+ sage: G._normalization_face_separatrix(2, 0)
+ (0, 0)
+ """
+ if self.face_angle(half_edge) == 0:
+ return (min(perm_orbit(self._fp, half_edge)), angle)
+
+ last_angle = self.half_edge_num_separatrices(half_edge) - 1 #the valid range is between 1 and the number of vertical separatrices
+ while angle == last_angle:
+ half_edge = self.previous_in_face(half_edge)
+ angle = 0
+ last_angle = self.half_edge_num_separatrices(half_edge) - 1
+ return (half_edge, angle)
+
+ def _check_face_separatrix(self, half_edge, angle):
+ half_edge = self._check_half_edge(half_edge)
+ if not isinstance(angle, numbers.Integral):
+ raise ValueError("invalid angle for separatrix")
+ angle = int(angle)
+ num_seps = self.half_edge_num_separatrices(half_edge)
+ if angle < 0 or angle >= num_seps:
+ raise ValueError("angle (={}) out of range for separatrix at half_edge={}; must be >= 0 and <= {}".format(angle, half_edge, num_seps))
+ return self._normalization_face_separatrix(half_edge, angle)
diff --git a/veerer/tatami_decomposition.py b/veerer/tatami_decomposition.py
index 97873c0b..b4064cdd 100644
--- a/veerer/tatami_decomposition.py
+++ b/veerer/tatami_decomposition.py
@@ -30,8 +30,11 @@ def tatami_decomposition(rectangles, base_ring=None):
r"""
Return a sage-flatsurf surface built from the given ``rectangles``
- Each point is identified by its position on a separatrix. It is encoded in
- a triple ``(separatrix_label, side, distance_to_singularity)`` where
+ A tatami decomposition is a decomposition of a translation surface such
+ that all sides of all rectangles are on horizontal or vertical
+ separatrices. Each vertex of the decomposition is identified by its
+ position on a separatrix. It is encoded in a triple ``(separatrix_label,
+ side, distance_to_singularity)`` where
- ``separatrix_label``: is a label identifying a separatrix
- ``side``: specifies either left (``LEFT``) or right (``RIGHT``) of the separatrix
- ``distance_to_singularity``: a positive real number
@@ -56,21 +59,21 @@ def tatami_decomposition(rectangles, base_ring=None):
sage: r0 = ((1, RIGHT, 1), (1, RIGHT, 0), (0, LEFT, 0), (0, LEFT, 1), (3, RIGHT, 2), (3, RIGHT, 1), (0, RIGHT, 2), (0, RIGHT, 1))
sage: r1 = ((3, LEFT, 0), (3, LEFT, 2), (0, LEFT, 1), (0, LEFT, 2), (3, RIGHT, 1), (1, LEFT, 1), (0, RIGHT, 1), (0, RIGHT, 0))
sage: tatami_decomposition([r0, r1]) # optional - sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
TESTS::
sage: tatami_decomposition([r0, r1], ZZ) # optional - sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
sage: tatami_decomposition([r0, r1], QQ) # optional - sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
sage: tatami_decomposition([r0, r1], AA) # optional - sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
sage: r0 = ((1, RIGHT, AA(1)), (1, RIGHT, 0), (0, LEFT, 0), (0, LEFT, 1), (3, RIGHT, 2), (3, RIGHT, 1), (0, RIGHT, 2), (0, RIGHT, 1))
sage: r1 = ((3, LEFT, 0), (3, LEFT, 2), (0, LEFT, 1), (0, LEFT, 2), (3, RIGHT, 1), (1, LEFT, 1), (0, RIGHT, 1), (0, RIGHT, 0))
sage: tatami_decomposition([r0, r1]) # optional - sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
"""
if base_ring is None:
coefficients = []
@@ -229,4 +232,5 @@ def tatami_decomposition(rectangles, base_ring=None):
elif side == RIGHT and (sep, LEFT, x, y) not in interval_id:
raise RuntimeError('ERROR: missing (sep={}, side=LEFT, x={}, y={})'.format(sep, x, y))
+ surface.set_immutable()
return surface
diff --git a/veerer/triangulation.py b/veerer/triangulation.py
index 963bc8ce..3da9906f 100644
--- a/veerer/triangulation.py
+++ b/veerer/triangulation.py
@@ -31,43 +31,56 @@
from .permutation import (perm_init, perm_check, perm_cycles, perm_dense_cycles,
perm_invert, perm_conjugate, perm_cycle_string, perm_cycles_lengths,
- perm_cycles_to_string, perm_on_list,
- perm_num_cycles, str_to_cycles, str_to_cycles_and_data, perm_compose, perm_from_base64_str,
- uint_base64_str, uint_from_base64_str, perm_base64_str,
- perms_are_transitive, triangulation_relabelling_from)
-from .constellation import Constellation
+ perm_cycles_to_string, perm_on_list, perm_relabel_on_edges,
+ perm_num_cycles, str_to_cycles, str_to_cycles_and_data)
+from .constellation import Constellation, check_relabelling
-def face_edge_perms_init(faces):
+def face_boundary_init(faces, boundary=None):
r"""
- INPUT:: ``faces`` - a list or a string encoding a permutation
+ Function to simplify initialization of a constellation with boundaries.
- EXAMPLES::
+ INPUT::
+
+ - ``faces`` - a list of cycles or a string encoding a permutation
- sage: from veerer.triangulation import face_edge_perms_init # random output due to deprecation warnings from realalg
+ - ``boundary`` - a dictionary, a list or a string encoding the boundary data
- sage: face_edge_perms_init('(0,1,2)(~0,~1,~2)')
- (array('i', [1, 2, 0, 5, 3, 4]), array('i', [5, 4, 3, 2, 1, 0]))
+ EXAMPLES::
- sage: face_edge_perms_init('(0,1,2)')
- (array('i', [1, 2, 0]), array('i', [0, 1, 2]))
+ sage: from veerer.triangulation import face_boundary_init # random output due to deprecation warnings from realalg
- sage: face_edge_perms_init('(0,1,2)(~0)(~1)(~2)')
- (array('i', [1, 2, 0, 3, 4, 5]), array('i', [5, 4, 3, 2, 1, 0]))
+ sage: face_boundary_init('(0,1,2)(~0,~1,~2)')
+ (array('i', [2, 3, 4, 5, 0, 1]), array('i', [0, 0, 0, 0, 0, 0]))
+ sage: face_boundary_init('(0,1,2)')
+ (array('i', [2, -1, 4, -1, 0, -1]), array('i', [0, 0, 0, 0, 0, 0]))
+ sage: face_boundary_init('(0,1,2)(~0)(~1)(~2)')
+ (array('i', [2, 1, 4, 3, 0, 5]), array('i', [0, 0, 0, 0, 0, 0]))
+ sage: face_boundary_init('(0:1,1:2,2)(~0)(~1)(~2)')
+ (array('i', [2, 1, 4, 3, 0, 5]), array('i', [1, 0, 2, 0, 0, 0]))
TESTS:
- Check that edge permutation do not depend on the details of faces::
+ Check that the edge permutation does not depend on the details of faces::
sage: f1 = "(0,~5,4)(3,5,6)(1,2,~6)"
sage: f2 = "(0,6,5)(1,2,~6)(3,4,~5)"
sage: f3 = "(6,4,3)(~6,~5,1)(5,0,2)"
- sage: assert face_edge_perms_init(f1)[1] == face_edge_perms_init(f2)[1] == face_edge_perms_init(f3)[1]
+ sage: assert face_boundary_init(f1)[1] == face_boundary_init(f2)[1] == face_boundary_init(f3)[1]
"""
- if isinstance(faces, str):
- l = str_to_cycles(faces)
+ if boundary is None:
+ if isinstance(faces, str):
+ l, boundary = str_to_cycles_and_data(faces)
+ else:
+ l = [[int(i) for i in c] for c in faces]
else:
- l = [[int(i) for i in c] for c in faces]
+ if isinstance(faces, str):
+ l = str_to_cycles(faces)
+ else:
+ l = [[int(i) for i in c] for c in faces]
+ if isinstance(boundary, str):
+ ll, boundary = str_to_cycles_and_data(boundary)
+ l.extend(ll)
pos = []
neg = []
@@ -84,91 +97,42 @@ def face_edge_perms_init(faces):
pos.sort()
neg.sort(reverse=True)
+ if pos[0] != 0:
+ raise ValueError("missing half-edge 0")
for i in range(len(pos) - 1):
if pos[i] == pos[i+1]:
- raise ValueError("repeated edge label {}".format(pos[i]))
+ raise ValueError("repeated half-edge {}".format(pos[i]))
elif pos[i + 1] != pos[i] + 1:
- raise ValueError("missing edge label {} (pos={})".format(pos[i] + 1, pos))
+ raise ValueError("missing half-edge {}".format(pos[i] + 1))
for i in range(len(neg) - 1):
if neg[i] == neg[i+1]:
- raise ValueError("repeated edge label ~{}".format(~neg[i]))
+ raise ValueError("repeated half-edge ~{}".format(~neg[i]))
# number of half edges
- n = len(pos) + len(neg)
+ ne = len(pos)
- # build the edge permutation ep
- ep = [-1] * n # edge permutation
- m = n - 1 # label available at the back
- for e in neg:
- E = ~e
- e = m
- if ep[E] != -1:
- raise ValueError("inconsistent permutation data")
- m -= 1
- ep[E] = e
- ep[e] = E
- for i in range(n):
- if ep[i] == -1:
- ep[i] = i
-
- fp = [-1] * n # face permutation
+ def to_half_edge(x):
+ return 2 * x if x >= 0 else 2 * ~x + 1
+
+ # build the face permutation
+ fp = [-1] * (2 * ne)
for c in l:
k = len(c)
for i in range(k):
- e0 = c[i]
- e1 = c[(i + 1) % k]
- if e0 < 0:
- e0 = ep[~e0]
- if e1 < 0:
- e1 = ep[~e1]
+ e0 = to_half_edge(c[i])
+ e1 = to_half_edge(c[(i + 1) % k])
fp[e0] = e1
+ fp = perm_init(fp, partial=True)
- return perm_init(fp), perm_init(ep)
-
-
-def boundary_init(fp, ep, boundary):
- r"""
- Initialize boundary data given face and edge permutation.
-
- EXAMPLES:
-
- A test with folded edges::
-
- sage: from array import array
- sage: from veerer.triangulation import boundary_init
- sage: fp = array('i', [1, 2, 0, 4, 3])
- sage: ep = array('i', [0, 4, 3, 2, 1])
- sage: boundary_init(fp, ep, {0: 1})
- array('i', [1, 0, 0, 0, 0])
- sage: boundary_init(fp, ep, {1: 1})
- array('i', [0, 1, 0, 0, 0])
- sage: boundary_init(fp, ep, {2: 1})
- array('i', [0, 0, 1, 0, 0])
- sage: boundary_init(fp, ep, {-2: 1})
- array('i', [0, 0, 0, 0, 1])
- sage: boundary_init(fp, ep, {-3: 1})
- array('i', [0, 0, 0, 1, 0])
- """
- n = len(ep)
-
+ # construct the boundary
if boundary is None:
- return array('i', [0] * n)
- elif isinstance(boundary, (array, tuple, list)):
- if len(boundary) != n:
+ boundary = array('i', [0] * (2 * ne))
+ elif isinstance(boundary, (tuple, list, array)):
+ if len(boundary) != 2 * ne:
raise ValueError('invalid input argument')
- return array('i', boundary)
+ boundary = array('i', boundary)
elif isinstance(boundary, dict):
- edge_to_half_edge = {}
- for j, c in enumerate(perm_cycles(ep, n)):
- if len(c) == 1:
- edge_to_half_edge[j] = c[0]
- elif len(c) == 2:
- edge_to_half_edge[j] = c[0]
- edge_to_half_edge[~j] = c[1]
- else:
- raise ValueError
-
- output = array('i', [0] * n)
+ output = array('i', [0] * (2 * ne))
for e, v in boundary.items():
if isinstance(e, str):
if not e:
@@ -179,13 +143,14 @@ def boundary_init(fp, ep, boundary):
e = int(e)
elif isinstance(e, numbers.Integral):
e = int(e)
- if e > n:
- raise ValueError('keys must be valid edges, got {!r}'.format(e))
- output[edge_to_half_edge[e]] = v
- return output
+ if e not in pos and e not in neg:
+ raise ValueError('keys in the bdry dictionary must be valid half-edges, got {!r}'.format(e))
+ output[to_half_edge(e)] = v
+ boundary = output
else:
raise TypeError('invalid boundary data')
+ return fp, boundary
# NOTE: we don't really care that we have a triangulation here. When
@@ -265,7 +230,7 @@ class Triangulation(Constellation):
sage: from veerer import *
- sage: T = Triangulation("(~2, 1, ~0)(~1, 0, 2)", mutable=True)
+ sage: T = Triangulation("(0,2,~1)(~0,~2,1)", mutable=True)
sage: T.genus()
1
sage: T.num_triangles()
@@ -274,16 +239,16 @@ class Triangulation(Constellation):
1
sage: T.flip(0)
sage: T
- Triangulation("(0,~1,~2)(1,2,~0)")
+ Triangulation("(0,~1,~2)(~0,1,2)")
sage: T.flip(0)
sage: T
- Triangulation("(0,~2,1)(2,~1,~0)")
+ Triangulation("(0,~2,1)(~0,2,~1)")
sage: T.flip(0)
sage: T
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
sage: T.flip(0)
sage: T
- Triangulation("(0,2,~1)(1,~0,~2)")
+ Triangulation("(0,2,~1)(~0,~2,1)")
sage: T.set_immutable()
sage: T.flip(0)
@@ -299,23 +264,23 @@ class Triangulation(Constellation):
Examples with boundaries::
sage: Triangulation("(0,1,2)(~0)(~1)(~2)", boundary={"~0": 1, "~1": 1, "~2": 1})
- Triangulation("(0,1,2)", boundary="(~2:1)(~1:1)(~0:1)")
- sage: Triangulation("(0,1,2)", boundary="(~0:1)(~1:1,~2:1)")
- Triangulation("(0,1,2)", boundary="(~2:1,~1:1)(~0:1)")
+ Triangulation("(0,1,2)(~0:1)(~1:1)(~2:1)")
+ sage: Triangulation("(0,1,2)(~0:1)(~1:1,~2:1)")
+ Triangulation("(0,1,2)(~0:1)(~1:1,~2:1)")
sage: Triangulation("(0,1,2)(~0,~1,~2)", boundary={"~0": 0})
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
Example with boundary and folded edges::
- sage: Triangulation("(0,1,2)", boundary="(~1:1,~2:1)")
- Triangulation("(0,1,2)", boundary="(~2:1,~1:1)")
+ sage: Triangulation("(0,1,2)(~1:1,~2:1)")
+ Triangulation("(0,1,2)(~1:1,~2:1)")
Examples with invalid boundaries::
sage: Triangulation("(0,1,2)(~0,~1,~2)", boundary={"~0": 1, "~1": 1, "~2": 0})
Traceback (most recent call last):
...
- ValueError: invalid boundary data
+ ValueError: invalid boundary data array('i', [0, 1, 0, 1, 0, 0])
"""
__slots__ = ['_bdry']
@@ -333,52 +298,15 @@ def __init__(self, triangles, boundary=None, mutable=False, check=True):
else:
triangles = list(triangles)
triangles.extend(boundary_cycles)
- fp, ep = face_edge_perms_init(triangles)
- bdry = boundary_init(fp, ep, boundary)
+ fp, bdry = face_boundary_init(triangles, boundary)
- Constellation.__init__(self, len(fp), None, ep, fp, (bdry,), mutable, check)
+ Constellation.__init__(self, len(fp) // 2, None, fp, (bdry,), (), mutable, check)
def _set_data_pointers(self):
- self._bdry = self._data[0]
-
- @staticmethod
- def from_face_edge_perms(fp, ep, vp=None, boundary=None, mutable=False, check=True):
- r"""
- INPUT:
-
- - ``fp``, ``ep``, ``vp`` -- the face, edge and vertex permutation
-
- - ``check`` - boolean (default: ``True``) - if set to ``False`` no
- check are performed
-
- EXAMPLES::
-
- sage: from veerer import Triangulation
- sage: from array import array
-
- sage: fp = array('i', [1, 2, 0, 4, 8, 6, 7, 5, 3])
- sage: ep = array('i', [8, 7, 2, 3, 4, 5, 6, 1, 0])
- sage: vp = array('i', [2, 8, 7, 0, 3, 1, 5, 6, 4])
- sage: Triangulation.from_face_edge_perms(fp, ep, vp)
- doctest:warning
- ...
- UserWarning: the method Triangulation.from_face_edge_perms is deprecated; use the classmethod from_permutations instead
- Triangulation("(0,1,2)(3,4,~0)(5,6,~1)")
- """
- import warnings
- warnings.warn('the method Triangulation.from_face_edge_perms is deprecated; use the classmethod from_permutations instead')
-
- n = len(fp)
- if vp is None:
- vp = array('i', [-1] * n)
- for i in range(n):
- vp[fp[ep[i]]] = i
- if boundary is None:
- bdry = array('i', [0] * n)
- else:
- bdry = array('i', boundary)
+ self._bdry = self._half_edges_data[0]
- return Triangulation.from_permutations(vp, ep, fp, (bdry,), mutable, check)
+ def boundary_vector(self, copy=True):
+ return self._bdry[:] if copy else self._bdry
def _check(self, error=RuntimeError):
r"""
@@ -389,7 +317,7 @@ def _check(self, error=RuntimeError):
sage: Triangulation("(0,1,3)")
Traceback (most recent call last):
...
- ValueError: missing edge label 2 (pos=[0, 1, 3])
+ ValueError: missing half-edge 2
sage: Triangulation("(0,1,~2)")
Traceback (most recent call last):
@@ -399,38 +327,35 @@ def _check(self, error=RuntimeError):
sage: Triangulation("(0)")
Traceback (most recent call last):
...
- ValueError: non-trianglular internal face starting at half-edge i=0
+ ValueError: non-triangular internal face starting at half-edge i=0
sage: from array import array
- sage: fp = array('i', [1,2,0])
- sage: ep = array('i', [0,1,2])
- sage: vp = array('i', [1,2,0])
- sage: Triangulation.from_face_edge_perms(fp, ep, vp)
+ sage: fp = array('i', [2,-1,4,-1,0,-1])
+ sage: vp = array('i', [2,-1,4,-1,0,-1])
+ sage: Triangulation.from_permutations(vp, fp, (array('i', [0]*6),))
Traceback (most recent call last):
...
ValueError: fev relation not satisfied at half-edge i=0
- sage: fp = array('i', [1,2,0])
- sage: ep = array('i', [1,2,0])
- sage: vp = array('i', [1,2,0])
- sage: Triangulation.from_face_edge_perms(fp, ep, vp)
+ sage: fp = array('i', [2,-1,4,-1,0,-1])
+ sage: vp = array('i', [2,-1,4,-1,0,-1])
+ sage: Triangulation.from_permutations(vp, fp, (array('i', [0]*6),))
Traceback (most recent call last):
...
- ValueError: invalid edge permutation at half-edge i=0 (vp=array('i', [1, 2, 0]) ep=array('i', [1, 2, 0]) fp=array('i', [1, 2, 0]))
+ ValueError: fev relation not satisfied at half-edge i=0
"""
Constellation._check(self, error)
- n = self._n
for face in self.faces():
i = face[0]
if self._bdry[i]:
if not all(self._bdry[i] for i in face):
- raise error('invalid boundary data')
+ raise error('invalid boundary data {}'.format(self._bdry))
else:
if any(self._bdry[i] for i in face):
- raise error('invalid boundary data')
+ raise error('invalid boundary data {}'.format(self._bdry))
if len(face) != 3:
- raise error('non-trianglular internal face starting at half-edge i={}'.format(self._half_edge_string(i)))
+ raise error('non-triangular internal face starting at half-edge i={}'.format(self._half_edge_string(i)))
def to_flipper(self):
r"""
@@ -453,7 +378,7 @@ def to_flipper(self):
from .features import flipper_feature
flipper_feature.require()
- if any(self._data[0]):
+ if any(self._bdry):
raise ValueError('triangulation has boundary')
import flipper
@@ -513,7 +438,7 @@ def num_triangles(self):
r"""
Return the number of triangles.
"""
- return sum(self._data[0][c[0]] == 0 for c in perm_cycles(self._fp))
+ return sum(self._bdry[c[0]] == 0 for c in perm_cycles(self._fp))
def triangles(self):
r"""
@@ -524,10 +449,16 @@ def triangles(self):
sage: from veerer import Triangulation
sage: T = Triangulation("(0,1,2)(3,4,5)(~0,~3,6)")
- sage: T.faces()
- [[0, 1, 2], [3, 4, 5], [6, 8, 7]]
+ sage: T.triangles()
+ [[0, 2, 4], [1, 7, 12], [6, 8, 10]]
+ sage: T = Triangulation("(0,1,2)(~0)(~1,~2)", {"~0": 1, "~1": 1, "~2": 1})
+ sage: T.triangles()
+ [[0, 2, 4]]
"""
- return [c for c in perm_cycles(self._fp, True, self._n) if self._data[0][c[0]] == 0]
+ return [c for c in perm_cycles(self._fp, True, 2 * self._ne) if self._bdry[c[0]] == 0]
+
+ def boundary_half_edges(self):
+ return (h for h in self.half_edges() if self._bdry[h])
def boundary_faces(self):
r"""
@@ -542,9 +473,12 @@ def boundary_faces(self):
[]
sage: T = Triangulation("(0,1,2)(~0)(~1,~2)", {"~0": 1, "~1": 1, "~2": 1})
sage: T.boundary_faces()
- [[3, 4], [5]]
+ [[1], [3, 5]]
"""
- return [c for c in perm_cycles(self._fp, True, self._n) if self._data[0][c[0]]]
+ return [c for c in perm_cycles(self._fp, True, 2 * self._ne) if self._bdry[c[0]]]
+
+ def num_internal_faces(self):
+ return sum(not self._bdry[c[0]] for c in perm_cycles(self._fp))
def num_boundary_faces(self):
r"""
@@ -566,47 +500,7 @@ def num_boundary_faces(self):
sage: Triangulation(fp, bdry).num_boundary_faces()
1
"""
- return sum(bool(self._data[0][c[0]]) for c in perm_cycles(self._fp))
-
- def euler_characteristic(self):
- r"""
- Return the Euler characteristic of this triangulation.
-
- EXAMPLES::
-
- sage: from veerer import Triangulation
-
- A sphere::
-
- sage: T = Triangulation("(0,1,2)")
- sage: T.euler_characteristic()
- 2
-
- A torus::
-
- sage: T = Triangulation("(0,1,2)(~0,~1,~2)")
- sage: T.euler_characteristic()
- 0
-
- A genus 2 surface::
-
- sage: T = Triangulation("(0,1,2)(~2,3,4)(~4,5,6)(~6,~0,7)(~7,~1,8)(~8,~3,~5)")
- sage: T.euler_characteristic()
- -2
-
- A cylinder::
-
- sage: T = Triangulation("(0,1,2)(~0,3,4)(~1,~2)(~3,~4)", {"~1": 1, "~2": 1, "~3": 1, "~4": 1})
- sage: T.euler_characteristic()
- 0
-
- A pair of pants::
-
- sage: T = Triangulation("(0,1,2)(~0)(~1)(~2)", {"~0": 1, "~1": 1, "~2": 1})
- sage: T.euler_characteristic()
- -1
- """
- return self.num_triangles() - self.num_edges() + (self.num_vertices() + self.num_folded_edges())
+ return sum(bool(self._bdry[c[0]]) for c in perm_cycles(self._fp))
def genus(self):
r"""
@@ -635,6 +529,15 @@ def genus(self):
sage: T = Triangulation("(0,1,2)(~0)(~1)(~2)", {"~0": 1, "~1": 1, "~2": 1})
sage: T.genus()
0
+
+ Disks::
+
+ sage: T = Triangulation("(0:1,~0:1)")
+ sage: T.genus()
+ 0
+ sage: T = Triangulation("(0:1)")
+ sage: T.genus()
+ 0
"""
if not self.is_connected():
raise NotImplementedError
@@ -649,15 +552,12 @@ def __str__(self):
sage: from veerer import *
sage: T = Triangulation("(0,1,2)(~0,~1,~2)")
sage: str(T)
- 'Triangulation("(0,1,2)(~2,~0,~1)")'
+ 'Triangulation("(0,1,2)(~0,~1,~2)")'
"""
- cycles = perm_cycles(self._fp, n=self._n)
- face_cycles = perm_cycles_to_string([c for c in cycles if not self._data[0][c[0]]], involution=self._ep)
- bdry_cycles = perm_cycles_to_string([c for c in cycles if self._data[0][c[0]]], involution=self._ep, data=self._data[0])
- if bdry_cycles:
- return 'Triangulation("%s", boundary="%s")' % (face_cycles, bdry_cycles)
- else:
- return 'Triangulation("%s")' % face_cycles
+ cycles = perm_cycles(self._fp, n=2 * self._ne)
+ face_cycles = perm_cycles_to_string([c for c in cycles if not self._bdry[c[0]]], edge_like=True)
+ face_cycles += perm_cycles_to_string([c for c in cycles if self._bdry[c[0]]], edge_like=True, data=self._bdry)
+ return 'Triangulation("%s")' % face_cycles
def __repr__(self):
return str(self)
@@ -672,7 +572,7 @@ def _check_homology_matrix(self, m):
sage: T._check_homology_matrix(m)
"""
ne = self.num_edges()
- ep = self._ep
+ fp = self._fp
assert m.nrows() == ne
try:
@@ -683,13 +583,9 @@ def _check_homology_matrix(self, m):
# boundary condition
for F in self.faces():
v = V.zero()
- for e in F:
- E = ep[e]
- if E > e:
- v += m[e]
- elif E < e:
- v -= m[E]
- else:
+ for h in F:
+ e = h // 2
+ if fp[2 * e + 1] == -1:
# folded edge condition
# NOTE: it is stupid to keep all these zeros in
# the matrix! We should label the folded edges
@@ -697,7 +593,11 @@ def _check_homology_matrix(self, m):
# though we can allow non-zero stuff assuming
# that the half edge is oriented toward the pole
assert m[e].is_zero()
- assert v.is_zero()
+ elif h % 2 == 0:
+ v += m[e]
+ else:
+ v -= m[e]
+ assert v.is_zero(), (self, F, v)
# TODO: also compute the intersection pairing!
def homology_matrix(self):
@@ -721,7 +621,7 @@ def homology_matrix(self):
from sage.matrix.constructor import matrix
from sage.rings.integer_ring import ZZ
- ep = self._ep
+ fp = self._fp
nf = self.num_faces()
ne = self.num_edges()
nfe = self.num_folded_edges()
@@ -729,29 +629,31 @@ def homology_matrix(self):
# face equations
for i, f in enumerate(self.faces()):
- for e in f:
- if ep[e] == e:
- continue
- elif ep[e] < e:
- m[i, ep[e]] -= 1
+ for h in f:
+ e = h // 2
+ if fp[2 * e + 1] == -1:
+ pass
+ elif h % 2 == 0:
+ m[i, e] -= 1
else:
m[i, e] += 1
# force the folded edge to have coefficient zero
- for e in range(ne):
- if ep[e] == e:
+ i = nf
+ for e in range(self._ne):
+ if fp[2 * e + 1] == -1:
m[i, e] = 1
i += 1
# compute and check
- h = m.right_kernel_matrix().transpose()
- self._check_homology_matrix(h)
- return h
+ hom = m.right_kernel_matrix().transpose()
+ self._check_homology_matrix(hom)
+ return hom
- def flip_homological_action(self, e, m, twist=False):
+ def flip_homological_action(self, e, m, twist=False, check=True):
r"""
Multiply the matrix ``m`` on the left by the homology action of
- the ``e``-flip.
+ the flip of the edge ``e``.
The matrix ``m`` must have ``ne`` rows and each column represents a
vector in cohomology (possibly twisted for quadratic differentials).
@@ -763,7 +665,7 @@ def flip_homological_action(self, e, m, twist=False):
INPUT:
- - ``e`` - a half edge
+ - ``h`` - a half edge
- ``m`` - matrix
@@ -777,7 +679,7 @@ def flip_homological_action(self, e, m, twist=False):
sage: T = Triangulation("(0,1,2)(~0,~1,~2)", mutable=True)
sage: A = matrix([[1,1],[-1,0],[0,-1]])
sage: B = copy(A)
- sage: for e in [0,1,0,1]:
+ sage: for e in (0, 1, 0, 1):
....: T.flip_homological_action(e, B)
....: T.flip(e)
sage: B
@@ -801,29 +703,26 @@ def flip_homological_action(self, e, m, twist=False):
sage: w = [1,1,1,1,0,0]
sage: A = matrix(ZZ, 3, 6, [u,v,w]).transpose()
sage: B = copy(A)
- sage: for i in (0,2,1,3):
+ sage: for i in (0, 2, 1, 3):
....: T.flip_homological_action(i, B)
....: T.flip(i)
....: T._check_homology_matrix(B)
- sage: p = "(0,2)(~0,~2)(1,3)(~1,~3)"
- sage: T.relabel_homological_action(p, B)
- sage: T.relabel(p)
- sage: T._check_homology_matrix(B)
- sage: A.pseudoinverse() * B
- [1 0 0]
- [0 1 0]
- [1 1 1]
"""
+ if check:
+ e = self._check_edge(e)
+ h = 2 * e
+
ne = self.num_edges()
assert m.nrows() == ne
ep = self._ep
+ fp = self._fp
- if not twist and ep[e] == e:
+ if not twist and ep(h) == h:
return
- elif ep[e] < e:
- e = ep[e]
- a, b, c, d = self.square_about_edge(e)
+ eh = h // 2
+
+ a, b, c, d = self.square_about_half_edge(h)
# v_e use to be v_c + v_d and becomes v_d + v_a
# v<----------u v<----------u
# | a ^^ |^ a ^
@@ -843,22 +742,24 @@ def flip_homological_action(self, e, m, twist=False):
# swap_rows(i, j)
# add_multiple_of_row(i, j, s)
- A = ep[a]
- D = ep[d]
+ A = a if fp[a ^ 1] == -1 else a ^ 1
+ D = d if fp[d ^ 1] == -1 else d ^ 1
+ ea = a // 2
+ ed = d // 2
if twist:
- m[e] = (m[a] if a < A else m[A]) + (m[d] if d < D else m[D])
+ m[eh] = m[ea] + m[ed]
else:
if a == A and d == D:
try:
- m[e] = m.row_ambient_module().zero()
+ m[eh] = m.row_ambient_module().zero()
except AttributeError:
- m[e] = m._row_ambient_module().zero()
+ m[eh] = m._row_ambient_module().zero()
elif a == A:
- m[e] = m[d] if d < D else -m[D]
+ m[eh] = m[ed] if d < D else -m[eD]
elif d == D:
- m[e] = m[a] if a < A else -m[A]
+ m[eh] = m[ea] if a < A else -m[ea]
else:
- m[e] = (m[a] if a < A else -m[A]) + (m[d] if d < D else -m[D])
+ m[eh] = (m[ea] if a < A else -m[ea]) + (m[ed] if d < D else -m[ed])
def relabel_homological_action(self, p, m, twist=False, check=True):
r"""
@@ -891,21 +792,25 @@ def relabel_homological_action(self, p, m, twist=False, check=True):
sage: perms = ["(0,1)(~0,~1)", "(3,~3)", "(3,~5)(~3,5)",
....: "(0,1,2,~0,~1,~2)",
....: "(0,~1,4,~0,1,~4)(2,3)(~2,~3)"]
- sage: for p in perms * 5:
+ sage: for p in perms * 5: # known bug
....: T.relabel_homological_action(p, A)
....: T.relabel(p)
....: T._check_homology_matrix(A)
"""
- n = self._n
- ne = self.num_edges()
+ ne = self._ne
+ n = 2 * ne
if check and not perm_check(p, n):
- p = perm_init(p, self._n, self._ep)
+ p = perm_init(p, 2 * ne, edge_like=True)
if not perm_check(p, n):
raise ValueError('invalid relabeling permutation')
- q = perm_invert(p)
-
- ep = self._ep
+ r, s = perm_relabel_on_edges(p, ne)
+ q = perm_invert(p, 2 * ne)
+ rr, ss = perm_relabel_on_edges(q, ne)
+ for i in range(2 * ne):
+ assert p[q[i]] == i and q[p[i]] == i, (p, q)
+ assert s[i // 2] == ss[r[i // 2]]
+ assert ss[i // 2] == s[rr[i // 2]]
seen = [False] * ne
for e0 in range(ne):
@@ -913,40 +818,25 @@ def relabel_homological_action(self, p, m, twist=False, check=True):
continue
seen[e0] = True
-
- e = q[e0]
- E = ep[e]
- if E < e:
- is_e0_neg = True
- e, E = E, e
- else:
- is_e0_neg = False
+ e = rr[e0]
while not seen[e]:
- assert e < ne
+ assert 0 <= e < ne
seen[e] = True
- ee = q[e]
- EE = ep[ee]
-
- if EE < ee:
- is_neg = True
- ee, EE = EE, ee
- else:
- is_neg = False
+ ee = rr[e]
m.swap_rows(e, ee)
- if is_neg and not twist:
+ if s[e] == -1 and not twist:
m[e] *= -1
-
e = ee
- # one more sign change?
assert e == e0
- if is_e0_neg and not twist:
+ # one more sign change?
+ if s[e] == -1 and not twist:
m[e] *= -1
def is_flippable(self, e, check=True):
r"""
- Check whether the half-edge e is flippable.
+ Check whether the edge ``e`` is flippable.
EXAMPLES::
@@ -955,8 +845,6 @@ def is_flippable(self, e, check=True):
sage: T = Triangulation("(0,1,2)(~0,~2,4)(~1,3,~3)")
sage: T.is_flippable(0)
True
- sage: T.is_flippable(1)
- True
sage: T.is_flippable(3)
False
sage: T.is_flippable(4)
@@ -975,14 +863,18 @@ def is_flippable(self, e, check=True):
False
"""
if check:
- e = self._check_half_edge(e)
- E = self._ep[e]
- a = self._fp[e]
+ h = self._check_half_edge(2 * e)
+ else:
+ h = 2 * e
+ H = self._ep(h)
+ a = self._fp[h]
b = self._fp[a]
- return not self._data[0][e] and not self._data[0][E] and a != E and b != E and self._data[0][e] == 0 and self._data[0][E] == 0
+ return not self._bdry[h] and not self._bdry[H] and a != H and b != H
def flippable_edges(self):
r"""
+ Return the list of flippable edges.
+
EXAMPLES::
sage: from veerer import *
@@ -998,32 +890,30 @@ def flippable_edges(self):
sage: T.flippable_edges()
[0]
"""
- n = self._n
- ep = self._ep
- return [e for e in range(n) if e <= ep[e] and self.is_flippable(e)]
+ return [e for e in range(self._ne) if self.is_flippable(e)]
- def square_about_edge(self, e, check=True):
+ def square_about_half_edge(self, h, check=True):
r"""
- Return the four edges that makes ``e`` the diagonal of a quadrilateral.
+ Return the four half-edges that makes ``h`` the diagonal of a quadrilateral.
EXAMPLES::
sage: from veerer import Triangulation
sage: T = Triangulation("(0,1,2)(~0,~1,~2)")
- sage: T.square_about_edge(0)
- (1, 2, 4, 3)
+ sage: T.square_about_half_edge(0)
+ (2, 4, 3, 5)
sage: T = Triangulation("(0,1,2)")
- sage: T.square_about_edge(0)
- (1, 2, 1, 2)
+ sage: T.square_about_half_edge(0)
+ (2, 4, 2, 4)
"""
# x<----------x
# | a ^^
# | / |
# | / |
# | / |
- # |b e/ d|
+ # |b h/ d|
# | / |
# | / |
# | / |
@@ -1033,15 +923,15 @@ def square_about_edge(self, e, check=True):
# x---------->x
if check:
- e = self._check_half_edge(e)
+ h = self._check_half_edge(h)
- E = self._ep[e]
- if check and (self._data[0][e] or self._data[0][E]):
+ H = self._ep(h)
+ if check and (self._bdry[h] or self._bdry[H]):
raise ValueError('non internal edge')
- a = self._fp[e]
+ a = self._fp[h]
b = self._fp[a]
- c = self._fp[E]
+ c = self._fp[H]
d = self._fp[c]
return a, b, c, d
@@ -1074,7 +964,7 @@ def flip(self, e, check=True):
sage: t = Triangulation("(0,1,2)(~0,3,4)", boundary="(~4:1,~3:1,~2:1,~1:1)", mutable=True)
sage: t.flip(0)
sage: t
- Triangulation("(0,2,3)(1,~0,4)", boundary="(~4:1,~3:1,~2:1,~1:1)")
+ Triangulation("(0,2,3)(~0,4,1)(~1:1,~4:1,~3:1,~2:1)")
sage: t.flip(2)
Traceback (most recent call last):
...
@@ -1098,54 +988,40 @@ def flip(self, e, check=True):
raise ValueError('immutable triangulation; use a mutable copy instead')
if check:
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
+ h = 2 * e
- E = self._ep[e]
- if self._data[0][e] or self._data[0][E]:
- raise ValueError('can not flip non internal edge %s' % self._norm(e))
+ H = self._ep(h)
+ if self._bdry[h] or self._bdry[H]:
+ raise ValueError('can not flip non internal edge %s' % e)
- a = self._fp[e]
+ a = self._fp[h]
b = self._fp[a]
- if a == E or b == E:
- raise ValueError('edge %s is not flippable' % self._norm(e))
- c = self._fp[E]
+ if a == H or b == H:
+ raise ValueError('edge %s is not flippable' % e)
+ c = self._fp[H]
d = self._fp[c]
- A = self._ep[a]
- B = self._ep[b]
- C = self._ep[c]
- D = self._ep[d]
-
- # Disabled for now
- # F = self._fl[e]
- # G = self._fl[E]
-
- # v = self._vl[b]
- # x = self._vl[d]
+ A = self._ep(a)
+ B = self._ep(b)
+ C = self._ep(c)
+ D = self._ep(d)
# fix face perm and cycles
- self._fp[e] = b
+ self._fp[h] = b
self._fp[b] = c
- self._fp[c] = e
- self._fp[a] = E
- self._fp[E] = d
+ self._fp[c] = h
+ self._fp[a] = H
+ self._fp[H] = d
self._fp[d] = a
- # Face labels
- # self._fl[a] = G
- # self._fl[c] = F
-
# fix vertex perm
self._vp[a] = D
- self._vp[b] = E
- self._vp[E] = A
+ self._vp[b] = H
+ self._vp[H] = A
self._vp[c] = B
- self._vp[d] = e
- self._vp[e] = C
-
- # Vertex labels
- # self._vl[e] = x
- # self._vl[E] = v
+ self._vp[d] = h
+ self._vp[h] = C
def flip_back(self, e, check=True):
r"""
@@ -1161,10 +1037,10 @@ def flip_back(self, e, check=True):
sage: T == T0
True
+ sage: T.flip(0)
sage: T.flip(1)
- sage: T.flip(2)
- sage: T.flip_back(2)
sage: T.flip_back(1)
+ sage: T.flip_back(0)
sage: T == T0
True
@@ -1193,57 +1069,41 @@ def flip_back(self, e, check=True):
raise ValueError('immutable triangulation; use a mutable copy instead')
if check:
- e = self._check_half_edge(e)
+ h = self._check_edge(e)
+ h = 2 * e
- E = self._ep[e]
+ H = self._ep(h)
- if self._data[0][e] or self._data[0][E]:
- raise ValueError('can not flip non internal edge %s' % self._norm(e))
+ if self._bdry[h] or self._bdry[H]:
+ raise ValueError('can not flip non-internal half-edge %s' % h)
- a = self._fp[e]
+ a = self._fp[h]
b = self._fp[a]
- if a == E or b == E:
- raise ValueError('edge %s is not flippable' % self._norm(e))
- c = self._fp[E]
+ if a == H or b == H:
+ raise ValueError('edge %s is not flippable' % e)
+ c = self._fp[H]
d = self._fp[c]
- A = self._ep[a]
- B = self._ep[b]
- C = self._ep[c]
- D = self._ep[d]
-
- # Disabled for now
- # F = self._fl[e]
- # G = self._fl[E]
-
- # v = self._vl[b]
- # x = self._vl[d]
+ A = self._ep(a)
+ B = self._ep(b)
+ C = self._ep(c)
+ D = self._ep(d)
# fix face perm and cycles
- self._fp[e] = d
+ self._fp[h] = d
self._fp[d] = a
- self._fp[a] = e
+ self._fp[a] = h
self._fp[b] = c
- self._fp[c] = E
- self._fp[E] = b
-
- # Face labels
- # Disabled for now
- # self._fl[a] = G
- # self._fl[c] = F
+ self._fp[c] = H
+ self._fp[H] = b
# fix vertex perm
self._vp[a] = D
- self._vp[b] = e
- self._vp[e] = A
+ self._vp[b] = h
+ self._vp[h] = A
self._vp[c] = B
- self._vp[d] = E
- self._vp[E] = C
-
- # Vertex labels
- # Disabled for now
- # self._vl[e] = x
- # self._vl[E] = v
+ self._vp[d] = H
+ self._vp[H] = C
def conjugate(self):
r"""
@@ -1260,7 +1120,7 @@ def conjugate(self):
sage: T = Triangulation("(0,1,2)(~0,~4,~2)(3,4,5)(~3,~1,~5)", mutable=True)
sage: T.conjugate()
sage: T
- Triangulation("(0,2,4)(1,3,5)(~5,~4,~3)(~2,~1,~0)")
+ Triangulation("(0,2,4)(~0,~2,~1)(1,3,5)(~3,~5,~4)")
sage: T = Triangulation("(0,1,2)(~0,~4,~2)(3,4,5)(~3,~1,~5)", mutable=False)
sage: T.conjugate()
@@ -1289,27 +1149,9 @@ def conjugate(self):
if not self._mutable:
raise ValueError('immutable triangulation; use a mutable copy instead')
- self._fp = perm_conjugate(perm_invert(self._fp), self._ep)
+ self._fp = perm_conjugate(perm_invert(self._fp), self.edge_permutation())
self._vp = perm_invert(self._vp)
- # TODO: deprecate
- is_isomorphic_to = Constellation.is_isomorphic
-
- def cover(self, c, mutable=False, check=True):
- from .cover import TriangulationCover
- return TriangulationCover(self, c, mutable=mutable, check=check)
-
- def _check_xy(self, x, y):
- if len(x) == self.num_edges():
- x = [x[self._norm(i)] for i in range(self._n)]
- elif len(x) != self._n or any(a < 0 for a in x):
- raise ValueError('invalid argument x')
- if len(y) == self.num_edges():
- y = [y[self._norm(i)] for i in range(self._n)]
- elif len(y) != self._n or any(a < 0 for a in y):
- raise ValueError('invalid argument y')
- return (x, y)
-
def colouring_from_xy(self, x, y, check=True):
r"""
Return the veering colouring associated with the holonomy data ``x`` and ``y``.
@@ -1317,82 +1159,89 @@ def colouring_from_xy(self, x, y, check=True):
EXAMPLES::
sage: from veerer import Triangulation
+
sage: t = "(0,1,2)(~0,~1,~2)"
sage: t = Triangulation(t)
sage: x = [1, 2, 1]
sage: y = [1, 1, 2]
sage: t.colouring_from_xy(x, y)
- array('i', [1, 2, 2, 2, 2, 1])
+ array('i', [1, 2, 2])
+
sage: t = "(0,1,2)(~0,~1,4)(~2,5,3)(~3,~4,~5)"
sage: t = Triangulation(t)
sage: x = [1, 2, 1, 2, 1, 1]
sage: y = [1, 1, 2, 1, 2, 3]
sage: t.colouring_from_xy(x, y)
- array('i', [1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 2, 1])
+ array('i', [1, 2, 2, 1, 2, 1])
"""
from .constants import BLUE, RED, PURPLE, GREEN
- if check:
- x, y = self._check_xy(x, y)
-
- def check_set_colouring(colouring, e, ep, col):
+ def check_set_colouring(colouring, e, col):
if colouring[e] is None:
- colouring[e] = colouring[ep[e]] = col
+ colouring[e] = col
elif colouring[e] != col:
raise ValueError('inconsistent colouring between x and y for edge e={}'.format(e))
- colouring = [None] * self._n
- faces = perm_cycles(self._fp, True, self._n)
+ if len(x) != self._ne or len(y) != self._ne:
+ raise ValueError("x (={}) and y (={}) must have length the number of edges (={})".format(x, y, self._ne))
+
+ colouring = [None] * self._ne
+ faces = perm_cycles(self._fp, True, 2 * self._ne)
ep = self._ep
for face in faces:
a, b, c = face
- x_degenerate = (x[a] == 0) + (x[b] == 0) + (x[c] == 0)
+ ea = a // 2
+ eb = b // 2
+ ec = c // 2
+ x_degenerate = (x[ea] == 0) + (x[eb] == 0) + (x[ec] == 0)
if x_degenerate == 0:
- if x[a] == x[b] + x[c]:
+ if x[ea] == x[eb] + x[ec]:
xlarge = 0
- elif x[b] == x[c] + x[a]:
+ elif x[eb] == x[ec] + x[ea]:
xlarge = 1
- elif x[c] == x[a] + x[b]:
+ elif x[ec] == x[ea] + x[eb]:
xlarge = 2
else:
- raise ValueError('inconsistent x data for triangle {}'.format(face))
- check_set_colouring(colouring, face[(xlarge + 1) % 3], ep, BLUE)
- check_set_colouring(colouring, face[(xlarge + 2) % 3], ep, RED)
+ raise ValueError('inconsistent x data for triangle {} with x[{}]={}, x[{}]={} and x[{}]={}'.format(face,
+ a, x[ea], b, x[eb], c, x[ec]))
+ check_set_colouring(colouring, face[(xlarge + 1) % 3] // 2, BLUE)
+ check_set_colouring(colouring, face[(xlarge + 2) % 3] // 2, RED)
elif x_degenerate == 1:
- if x[a] == 0:
+ if x[ea] == 0:
xvert = 0
- elif x[b] == 0:
+ elif x[eb] == 0:
xvert = 1
- elif x[c] == 0:
+ elif x[ec] == 0:
xvert = 2
- check_set_colouring(colouring, face[xvert], ep, GREEN)
- check_set_colouring(colouring, face[(xvert + 1) % 3], ep, RED)
- check_set_colouring(colouring, face[(xvert + 2) % 3], ep, BLUE)
+ check_set_colouring(colouring, face[xvert] // 2, GREEN)
+ check_set_colouring(colouring, face[(xvert + 1) % 3] // 2, RED)
+ check_set_colouring(colouring, face[(xvert + 2) % 3] // 2, BLUE)
else:
raise ValueError('inconsistent x data for triangle {}'.format(face))
- y_degenerate = (y[a] == 0) + (y[b] == 0) + (y[c] == 0)
+ y_degenerate = (y[ea] == 0) + (y[eb] == 0) + (y[ec] == 0)
if y_degenerate == 0:
- if y[a] == y[b] + y[c]:
+ if y[ea] == y[eb] + y[ec]:
ylarge = 0
- elif y[b] == y[c] + y[a]:
+ elif y[eb] == y[ec] + y[ea]:
ylarge = 1
- elif y[c] == y[a] + y[b]:
+ elif y[ec] == y[ea] + y[eb]:
ylarge = 2
else:
- raise ValueError('inconsistent y data for triangle {}'.format(face))
- check_set_colouring(colouring, face[(ylarge + 1) % 3], ep, RED)
- check_set_colouring(colouring, face[(ylarge + 2) % 3], ep, BLUE)
+ raise ValueError('inconsistent y data for triangle {} with y[{}]={}, y[{}]={} and y[{}]={}'.format(face,
+ a, y[ea], b, y[eb], c, y[ec]))
+ check_set_colouring(colouring, face[(ylarge + 1) % 3] // 2, RED)
+ check_set_colouring(colouring, face[(ylarge + 2) % 3] // 2, BLUE)
elif y_degenerate == 1:
- if y[a] == 0:
+ if y[ea] == 0:
yhor = 0
- elif y[b] == 0:
+ elif y[eb] == 0:
yhor = 1
- elif y[c] == 0:
+ elif y[ec] == 0:
yhor = 2
check_set_colouring(colouring, face[yhor], ep, PURPLE)
- check_set_colouring(colouring, face[(yhor + 1) % 3], ep, BLUE)
- check_set_colouring(colouring, face[(yhor + 2) % 3], ep, RED)
+ check_set_colouring(colouring, face[(yhor + 1) % 3] // 2, BLUE)
+ check_set_colouring(colouring, face[(yhor + 2) % 3] // 2, RED)
else:
raise ValueError('inconsistent y data for triangle {}'.format(face))
diff --git a/veerer/veering_quadrangulation.py b/veerer/veering_quadrangulation.py
index 26ba1fc0..40d1c546 100644
--- a/veerer/veering_quadrangulation.py
+++ b/veerer/veering_quadrangulation.py
@@ -690,8 +690,8 @@ def relabel(self, p):
self._pr = perm_conjugate(self._pr, p)
self._pl = perm_conjugate(self._pl, p)
- perm_on_list(p, self._zr, self._n)
- perm_on_list(p, self._zl, self._n)
+ perm_on_list(self._zr, p, n)
+ perm_on_list(self._zl, p, n)
def well_slanted_r_staircases(self):
return [i[0] for i in perm_cycles(self._pr, self._n) if self.is_r_slanted(i[0])]
@@ -1621,10 +1621,10 @@ def matrix_by_blocks(self):
V.symmetry()
swap = sage.matrix.matrix0.Matrix.swap_rows
- perm_on_list(self._relabelling, P0, n, swap)
- perm_on_list(self._relabelling, P1, n, swap)
- perm_on_list(self._relabelling, Q0, n, swap)
- perm_on_list(self._relabelling, Q1, n, swap)
+ perm_on_list(P0, self._relabelling, n, swap)
+ perm_on_list(P1, self._relabelling, n, swap)
+ perm_on_list(Q0, self._relabelling, n, swap)
+ perm_on_list(Q1, self._relabelling, n, swap)
return Q1, P1, Q0, P0
def matrix(self):
diff --git a/veerer/veering_triangulation.py b/veerer/veering_triangulation.py
index 2f5d15ce..2af64578 100644
--- a/veerer/veering_triangulation.py
+++ b/veerer/veering_triangulation.py
@@ -28,25 +28,30 @@
# ****************************************************************************
import collections
+import copy
import itertools
import numbers
from random import choice, shuffle
from array import array
import ppl
+from sage.sets.disjoint_set import DisjointSet
from sage.structure.element import get_coercion_model, Matrix
from sage.structure.richcmp import op_LT, op_LE, op_EQ, op_NE, op_GT, op_GE, rich_to_bool
from sage.modules.free_module import FreeModule
from sage.matrix.constructor import matrix
+from sage.modules.free_module_element import vector
from sage.rings.integer_ring import ZZ
+from sage.rings.rational import Rational
from .constants import *
from .constellation import Constellation
from .permutation import *
from .misc import det2
-from .triangulation import face_edge_perms_init, boundary_init, Triangulation
+from .triangulation import face_boundary_init, Triangulation
from .polyhedron import LinearExpressions, ConstraintSystem
-from .polyhedron.linear_algebra import linear_form_project, vector_normalize
+from .polyhedron.linear_expression import LinearConstraint
+from .polyhedron.linear_algebra import linear_form_project, vector_normalize, prime_decomposition, is_rank_one
cm = get_coercion_model()
@@ -59,6 +64,10 @@ class VeeringTriangulation(Triangulation):
a colouring of the edges in red or blue so that there is no monochromatic
face and no monochromatic vertex.
+ Boundary faces (which can have any number of edges) are allowed. In which
+ case, each half-edge that belong to a boundary must have a positive angle
+ excess associated to it.
+
EXAMPLES::
sage: from veerer import * # random output due to deprecation warnings from realalg
@@ -66,14 +75,14 @@ class VeeringTriangulation(Triangulation):
Built from an explicit triangulation (in cycle or list form) and a list of colours::
sage: VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
- VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB")
+ VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
From a stratum::
sage: from surface_dynamics import * # optional - surface_dynamics
sage: VeeringTriangulation.from_stratum(Stratum([2], 1)) # optional - surface_dynamics
- VeeringTriangulation("(0,6,~5)(1,8,~7)(2,7,~6)(3,~1,~8)(4,~2,~3)(5,~0,~4)", "RRRBBBBBB")
+ VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,8,~7)(~1,~8,3)(2,7,~6)(~2,~3,4)", "RRRBBBBBB")
sage: vt = VeeringTriangulation.from_stratum(Stratum({1:4}, 2)) # optional - surface_dynamics
sage: vt.stratum() # optional - surface_dynamics
@@ -94,21 +103,21 @@ class VeeringTriangulation(Triangulation):
A triangulation with some purple::
sage: VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBPBRBPRB")
- VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBPBRBPRB")
+ VeeringTriangulation("(0,~6,~3)(~0,2,~1)(1,7,~2)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBPBRBPRB")
Triangulations with boundary::
sage: VeeringTriangulation("(0,1,2)(~0,3,4)", "(~1:1)(~2:1)(~3:1)(~4:1)", "RBRBR")
- VeeringTriangulation("(0,1,2)(3,4,~0)", boundary="(~4:1)(~3:1)(~2:1)(~1:1)", colouring="RBRBR")
- sage: VeeringTriangulation("(0,1,2)(~0,3,4)", boundary="(~1:1)(~2:1)(~3:1)(~4:1)", colouring="RBRBR")
- VeeringTriangulation("(0,1,2)(3,4,~0)", boundary="(~4:1)(~3:1)(~2:1)(~1:1)", colouring="RBRBR")
+ VeeringTriangulation("(0,1,2)(~0,3,4)(~1:1)(~2:1)(~3:1)(~4:1)", "RBRBR")
+ sage: VeeringTriangulation("(0,1,2)(~0,3,4)(~1:1)(~2:1)(~3:1)(~4:1)", colouring="RBRBR")
+ VeeringTriangulation("(0,1,2)(~0,3,4)(~1:1)(~2:1)(~3:1)(~4:1)", "RBRBR")
Triangulations with boundary and folded edges::
- sage: VeeringTriangulation("(0,1,2)", boundary="(~1:1,~2:1)", colouring="RBR")
- VeeringTriangulation("(0,1,2)", boundary="(~2:1,~1:1)", colouring="RBR")
+ sage: VeeringTriangulation("(0,1,2)(~1:1,~2:1)", colouring="RBR")
+ VeeringTriangulation("(0,1,2)(~1:1,~2:1)", "RBR")
"""
- __slots__ = ['_colouring']
+ __slots__ = ['_colouring', '_delaunay_cone']
def __init__(self, *args, triangulation=None, boundary=None, colouring=None, mutable=False, check=True):
if len(args) == 3:
@@ -129,7 +138,6 @@ def __init__(self, *args, triangulation=None, boundary=None, colouring=None, mut
if isinstance(triangulation, Triangulation):
fp = triangulation.face_permutation(copy=True)
- ep = triangulation.edge_permutation(copy=True)
bdry = triangulation.boundary_vector(copy=True)
else:
if boundary is not None and isinstance(boundary, str):
@@ -139,8 +147,7 @@ def __init__(self, *args, triangulation=None, boundary=None, colouring=None, mut
else:
triangulation = list(triangulation)
triangulation.extend(boundary_cycles)
- fp, ep = face_edge_perms_init(triangulation)
- bdry = boundary_init(fp, ep, boundary)
+ fp, bdry = face_boundary_init(triangulation, boundary)
if colouring is None:
if isinstance(triangulation, VeeringTriangulation):
@@ -150,24 +157,83 @@ def __init__(self, *args, triangulation=None, boundary=None, colouring=None, mut
elif isinstance(colouring, str):
colouring = [colour_from_char(c) for c in colouring]
-
# set _colouring: half edge index --> {RED, BLUE}
n = len(fp)
- num_edges = perm_num_cycles(ep, n)
- if len(colouring) == num_edges:
- colouring = [colouring[i] if i <= ep[i] else colouring[ep[i]] for i in range(n)]
- elif len(colouring) == n:
- colouring = colouring
- else:
- raise ValueError("'colouring' argument of invalid length")
-
+ if n % 2:
+ raise ValueError("face permutation must have even length")
+ ne = n // 2
+ if len(colouring) != ne:
+ raise ValueError("wrong colouring argument")
colouring = array('i', colouring)
- Constellation.__init__(self, n, None, ep, fp, (bdry, colouring), mutable, check)
+ Constellation.__init__(self, ne, None, fp, (bdry,), (colouring,), mutable, check)
def _set_data_pointers(self):
Triangulation._set_data_pointers(self)
- self._colouring = self._data[1]
+ self._colouring = self._edges_data[0]
+
+ def _check_vertex_separatrix(self, half_edge, angle):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: vt = VeeringTriangulation("(0,1,2)(~0:1, ~2:1 , ~1:2)", "BRR")
+ sage: vt._check_vertex_separatrix(1, 0)
+ (1, 0)
+ sage: vt._check_vertex_separatrix(2, 0)
+ (2, 0)
+ sage: vt._check_vertex_separatrix(1, 2)
+ Traceback (most recent call last):
+ ...
+ ValueError: angle (=2) out of range for separatrix at half_edge=1; must be >= 0 and <= 1
+ """
+ half_edge = self._check_half_edge(half_edge)
+ if not isinstance(angle, numbers.Integral):
+ raise ValueError("invalid angle for separatrix")
+ angle = int(angle)
+ num_seps = self.half_edge_num_separatrices(half_edge)
+ if angle < 0 or angle >= num_seps:
+ raise ValueError(f"angle (={angle}) out of range for separatrix at half_edge={half_edge}; must be >= 0 and <= {num_seps}")
+ return (half_edge, angle)
+
+ def _normalization_face_separatrix(self, half_edge, angle):
+ if self.face_angle(half_edge) == 0:
+ return (min(perm_orbit(self._fp, half_edge)), angle)
+
+ last_angle = self.half_edge_num_separatrices(half_edge) - 1 #the valid range is between 1 and the number of separatrices
+ while angle == last_angle:
+ half_edge = self.previous_in_face(half_edge)
+ angle = 0
+ last_angle = self.half_edge_num_separatrices(half_edge) - 1
+ return (half_edge, angle)
+
+ def _check_face_separatrix(self, half_edge, angle):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+ sage: vt = VeeringTriangulation("(0,1,2)(~0:1, ~2:1 , ~1:2)", "BRR")
+ sage: vt._check_face_separatrix(1, 0)
+ (3, 0)
+ sage: vt._check_face_separatrix(2, 0)
+ Traceback (most recent call last):
+ ...
+ ValueError: not a face separatrix
+ sage: vt._check_face_separatrix(1, 2)
+ Traceback (most recent call last):
+ ...
+ ValueError: angle (=2) out of range for separatrix at half_edge=1; must be >= 0 and <= 1
+ """
+ half_edge = self._check_half_edge(half_edge)
+ if not isinstance(angle, numbers.Integral):
+ raise ValueError("invalid angle for separatrix")
+ angle = int(angle)
+ num_seps = self.half_edge_num_separatrices(half_edge)
+ if self._bdry[half_edge] == 0:
+ raise ValueError("not a face separatrix")
+ if angle < 0 or angle >= num_seps:
+ raise ValueError(f"angle (={angle}) out of range for separatrix at half_edge={half_edge}; must be >= 0 and <= {num_seps}")
+ return self._normalization_face_separatrix(half_edge, angle)
def _check(self, error=RuntimeError):
"""
@@ -180,8 +246,8 @@ def _check(self, error=RuntimeError):
ValueError: invalid triangle (0, 1, 2) with colours (green, blue, red)
"""
Triangulation._check(self, error)
- n = self.num_half_edges()
- ep = self._ep
+ ne = self._ne
+ n = 2 * ne
ev = self._vp
cols = self._colouring
bdry = self._bdry
@@ -192,13 +258,12 @@ def _check(self, error=RuntimeError):
raise error('bad boundary attribute bdry={}'.format(bdry))
if not isinstance(cols, array) or \
- len(cols) != n or \
- any(col not in COLOURS for col in cols) or \
- any(cols[e] != cols[ep[e]] for e in range(self._n)):
+ len(cols) != ne or \
+ any(col not in COLOURS for col in cols):
raise error('bad colouring attribute cols={}'.format(cols))
- if bdry is not self._data[0] or \
- cols is not self._data[1]:
+ if bdry is not self._half_edges_data[0] or \
+ cols is not self._edges_data[0]:
raise error('wrong pointers: id(bdry)={} id(self._data[0])={} id(cols)={} id(self._data[1])={}'.format(
id(bdry), id(self._data[0]), id(cols), id(self._data[1])))
@@ -208,33 +273,39 @@ def _check(self, error=RuntimeError):
# 1-degenerate: PBR (PURPLE), GRB (GREEN)
# 2-degenerate: BPG (BLUE|PURPLE|GREEN), RGP (RED|GREEN|PURPLE)
for a in range(n):
+ if self._vp[a] == -1:
+ continue
if self._bdry[a]:
continue
- col, a, b, c = self.triangle(a, check=False)
+ col, a, b, c = self.triangle(a, check=True)
+ ea = a // 2
+ eb = b // 2
+ ec = c // 2
good = False
if col == BLUE:
- good = cols[a] == BLUE and cols[b] == BLUE and cols[c] == RED
+ good = cols[ea] == BLUE and cols[eb] == BLUE and cols[ec] == RED
elif col == RED:
- good = cols[a] == RED and cols[b] == RED and cols[c] == BLUE
+ good = cols[ea] == RED and cols[eb] == RED and cols[ec] == BLUE
elif col == PURPLE:
- good = cols[a] == PURPLE and cols[b] == BLUE and cols[c] == RED
+ good = cols[ea] == PURPLE and cols[eb] == BLUE and cols[ec] == RED
elif col == GREEN:
- good = cols[a] == GREEN and cols[b] == RED and cols[c] == BLUE
+ good = cols[ea] == GREEN and cols[eb] == RED and cols[ec] == BLUE
elif col == BLUE | PURPLE | GREEN:
- good = cols[a] == BLUE and cols[b] == GREEN and cols[c] == PURPLE
+ good = cols[ea] == BLUE and cols[eb] == GREEN and cols[ec] == PURPLE
elif col == RED | PURPLE | GREEN:
- good = cols[a] == RED and cols[b] == PURPLE and cols[c] == GREEN
+ good = cols[ea] == RED and cols[eb] == PURPLE and cols[ec] == GREEN
if not good:
- raise error('invalid triangle ({}, {}, {}) with colours ({}, {}, {})'.format(a, b, c,
- colour_to_string(cols[a]), colour_to_string(cols[b]), colour_to_string(cols[c])))
+ raise error('invalid triangle ({}, {}, {}) with colours ({}, {}, {})'.format(
+ self._half_edge_string(a), self._half_edge_string(b), self._half_edge_string(c),
+ colour_to_string(cols[ea]), colour_to_string(cols[eb]), colour_to_string(cols[ec])))
# no monochromatic vertex
for v in self.vertices():
- if self._bdry[v[0]]:
+ if bdry[v[0]]:
continue
- col = cols[v[0]]
+ col = cols[v[0] // 2]
i = 1
- while i < len(v) and cols[v[i]] == col and not self._bdry[v[i]]:
+ while i < len(v) and cols[v[i] // 2] == col and not bdry[v[i]]:
i += 1
if i == len(v):
raise error('monochromatic vertex {} of colour {}'.format(v, colour_to_string(cols[v[0]])))
@@ -242,6 +313,65 @@ def _check(self, error=RuntimeError):
def base_ring(self):
return ZZ
+ def non_degenerate_triangles(self, slope=VERTICAL):
+ r"""
+ Iterate through non-degenerate triangles.
+
+ Triple of half-edges are ordered such that the first is always the large edge.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,3,8)(~0,5,6)(~3,4,2)(~4,1,7)", "BBBRRRRRR")
+ sage: list(vt.non_degenerate_triangles())
+ [(16, 0, 6), (12, 1, 10), (9, 2, 14), (8, 4, 7)]
+ """
+ if slope == VERTICAL:
+ LAR = PURPLE
+ POS = RED
+ NEG = BLUE
+ elif slope == HORIZONTAL:
+ LAR = GREEN
+ POS = BLUE
+ NEG = RED
+
+ for a, b, c in self.triangles():
+ cola = self._colouring[a // 2]
+ colb = self._colouring[b // 2]
+ colc = self._colouring[c // 2]
+ if colb == NEG and colc == POS:
+ yield (a, b, c)
+ elif colc == NEG and cola == POS:
+ yield (b, c, a)
+ elif cola == NEG and colb == POS:
+ yield (c, a, b)
+
+ def degenerate_triangles(self, slope=VERTICAL):
+ r"""
+ Iterate through degenerate triangles.
+
+ Triple of half-edges are ordered such that the first is always the degenerate edge.
+ """
+ if slope == VERTICAL:
+ ZERO = GREEN
+ POS = RED
+ NEG = BLUE
+ elif slope == HORIZONTAL:
+ ZERO = PURPLE
+ POS = BLUE
+ NEG = RED
+
+ for a, b, c in self.triangles():
+ cola = self._colouring[a // 2]
+ colb = self._colouring[b // 2]
+ colc = self._colouring[c // 2]
+ if cola == ZERO:
+ yield (a, b, c)
+ elif colb == ZERO:
+ yield (b, c, a)
+ elif colc == ZERO:
+ yield (c, a, b)
+
def right_wedges(self, slope=VERTICAL):
r"""
Return the (vertical or horizontal) right sides of the wedges in this veering triangulation.
@@ -257,28 +387,12 @@ def right_wedges(self, slope=VERTICAL):
sage: from veerer import VeeringTriangulation, VERTICAL, HORIZONTAL
sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~8,~0,~7)(~6,~1,~5)(~4,~2,~3)", "RRBRRBRRB")
- sage: vt.right_wedges()
- [1, 4, 7, 10, 16, 13]
+ sage: vt.right_wedges(VERTICAL)
+ [0, 1, 13, 7, 6, 12]
sage: vt.right_wedges(HORIZONTAL)
- [2, 5, 8, 9, 12, 15]
+ [4, 17, 11, 5, 10, 16]
"""
- if slope == VERTICAL:
- right_colour = RED
- left_colour = BLUE
- elif slope == HORIZONTAL:
- right_colour = BLUE
- left_colour = RED
- else:
- raise ValueError('invalid slope argument')
- wedges = []
- for face in self.faces():
- for i in range(3):
- if self.edge_colour(face[i]) == right_colour and self.edge_colour(face[(i + 1) % 3]) == left_colour:
- break
- if self.edge_colour(face[i]) != right_colour or self.edge_colour(face[(i + 1) % 3]) != left_colour:
- raise ValueError('invalid colouring')
- wedges.append(face[i])
- return wedges
+ return [c for a, b, c in self.non_degenerate_triangles(slope)]
def as_linear_family(self, mutable=False):
r"""
@@ -289,10 +403,13 @@ def as_linear_family(self, mutable=False):
sage: from veerer import *
sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,~2)", [RED, RED, BLUE])
sage: vt.as_linear_family()
- VeeringTriangulationLinearFamily("(0,1,2)(~2,~0,~1)", "RRB", [(1, 0, -1), (0, 1, 1)])
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)])
+
+ sage: VeeringTriangulation("(0:2,~0:4)", "R").as_linear_family()
+ VeeringTriangulationLinearFamily("(0:2,~0:4)", "R", [(1)])
"""
from .linear_family import VeeringTriangulationLinearFamily
- return VeeringTriangulationLinearFamily(self, self.train_track_linear_space().lines(), mutable=mutable)
+ return VeeringTriangulationLinearFamily(self, self.generators_matrix(), mutable=mutable)
def triangle(self, a, check=True):
r"""
@@ -313,17 +430,17 @@ def triangle(self, a, check=True):
sage: from veerer import VeeringTriangulation, RED, BLUE, PURPLE, GREEN
sage: V = VeeringTriangulation("(0,1,2)", "RRB")
- sage: assert V.triangle(0) == V.triangle(1) == V.triangle(2) == (RED, 0, 1, 2)
+ sage: assert V.triangle(0) == V.triangle(2) == V.triangle(4) == (RED, 0, 2, 4)
sage: V = VeeringTriangulation("(0,1,2)", "BBR")
- sage: assert V.triangle(0) == V.triangle(1) == V.triangle(2) == (BLUE, 0, 1, 2)
+ sage: assert V.triangle(0) == V.triangle(2) == V.triangle(4) == (BLUE, 0, 2, 4)
sage: V = VeeringTriangulation("(0,1,2)", "PBR")
- sage: assert V.triangle(0) == V.triangle(1) == V.triangle(2) == (PURPLE, 0, 1, 2)
+ sage: assert V.triangle(0) == V.triangle(2) == V.triangle(4) == (PURPLE, 0, 2, 4)
sage: V = VeeringTriangulation("(0,1,2)", "GRB")
- sage: assert V.triangle(0) == V.triangle(1) == V.triangle(2) == (GREEN, 0, 1, 2)
+ sage: assert V.triangle(0) == V.triangle(2) == V.triangle(4) == (GREEN, 0, 2, 4)
sage: V = VeeringTriangulation("(0,1,2)", "RPG")
- sage: assert V.triangle(0) == V.triangle(1) == V.triangle(2) == (RED|PURPLE|GREEN, 0, 1, 2)
+ sage: assert V.triangle(0) == V.triangle(2) == V.triangle(4) == (RED|PURPLE|GREEN, 0, 2, 4)
sage: V = VeeringTriangulation("(0,1,2)", "BGP")
- sage: assert V.triangle(0) == V.triangle(1) == V.triangle(2) == (BLUE|GREEN|PURPLE, 0, 1, 2)
+ sage: assert V.triangle(0) == V.triangle(2) == V.triangle(4) == (BLUE|GREEN|PURPLE, 0, 2, 4)
"""
# non-dgenerate: BBR (BLUE), RRB (RED)
# 1-degenerate: PBR (PURPLE), GRB (GREEN)
@@ -334,31 +451,34 @@ def triangle(self, a, check=True):
raise ValueError('boundary edge')
b = self._fp[a]
c = self._fp[b]
+ ea = a // 2
+ eb = b // 2
+ ec = c // 2
cols = self._colouring
degenerate = PURPLE | GREEN
standard = RED | BLUE
- ndegenerate = bool(cols[a] & degenerate) + bool(cols[b] & degenerate) + bool(cols[c] & degenerate)
+ ndegenerate = bool(cols[ea] & degenerate) + bool(cols[eb] & degenerate) + bool(cols[ec] & degenerate)
if ndegenerate == 0:
- if cols[a] == cols[b]:
- return (cols[a], a, b, c)
- elif cols[b] == cols[c]:
- return (cols[b], b, c, a)
- elif cols[c] == cols[a]:
- return (cols[c], c, a, b)
+ if cols[ea] == cols[eb]:
+ return (cols[ea], a, b, c)
+ elif cols[eb] == cols[ec]:
+ return (cols[eb], b, c, a)
+ elif cols[ec] == cols[ea]:
+ return (cols[ec], c, a, b)
elif ndegenerate == 1:
- if cols[a] & degenerate:
- return (cols[a], a, b, c)
- elif cols[b] & degenerate:
- return (cols[b], b, c, a)
- elif cols[c] & degenerate:
- return (cols[c], c, a, b)
+ if cols[ea] & degenerate:
+ return (cols[ea], a, b, c)
+ elif cols[eb] & degenerate:
+ return (cols[eb], b, c, a)
+ elif cols[ec] & degenerate:
+ return (cols[ec], c, a, b)
elif ndegenerate == 2:
- if cols[a] & standard:
- return (cols[a] | degenerate, a, b, c)
- elif cols[b] & standard:
- return (cols[b] | degenerate, b, c, a)
- elif cols[c] & standard:
- return (cols[c] | degenerate, c, a, b)
+ if cols[ea] & standard:
+ return (cols[ea] | degenerate, a, b, c)
+ elif cols[eb] & standard:
+ return (cols[eb] | degenerate, b, c, a)
+ elif cols[ec] & standard:
+ return (cols[ec] | degenerate, c, a, b)
return (None, None, None, None)
@classmethod
@@ -407,7 +527,7 @@ def from_square_tiled(cls, s, col=RED, mutable=False, check=True):
sage: o = Origami('(1,2)', '(1,3)') # optional - surface_dynamics
sage: T = VeeringTriangulation.from_square_tiled(o) # optional - surface_dynamics
sage: T # optional - surface_dynamics
- VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~8,~0,~7)(~6,~1,~5)(~4,~2,~3)", "RRBRRBRRB")
+ VeeringTriangulation("(0,1,2)(~0,~7,~8)(~1,~5,~6)(~2,~3,~4)(3,4,5)(6,7,8)", "RRBRRBRRB")
sage: o.stratum() # optional - surface_dynamics
H_2(2)
sage: T.stratum() # optional - surface_dynamics
@@ -433,23 +553,30 @@ def from_square_tiled(cls, s, col=RED, mutable=False, check=True):
if not isinstance(s, Origami_dense_pyx):
raise ValueError("input must be an origami")
+ # Note: we make all edges point right and up
+ # square i bottom edge = 3*i
+ # square i left edge = 3*i+2
+ # square i diagonal edge = 3*i+1
+
faces = []
n = s.nb_squares() # so 3n edges in the veering triangulation
# (6n half edges)
r = s.r_tuple()
u = s.u_tuple()
- ep = list(range(6*n-1, -1,- 1))
+ # ep used to be i <-> 6n-1-i
fp = [None] * (6*n)
N = 6*n - 1
for i in range(0, n):
- ii = 3*i
- fp[ii] = ii+1
- fp[ii+1] = ii+2
- fp[ii+2] = ii
-
- j = N - (3*r[i] + 2)
- k = N - (3*u[i])
- l = N - (3*i+1)
+ # bottom triangle
+ ii = 6*i
+ fp[ii] = ii + 2
+ fp[ii + 2] = ii + 4
+ fp[ii + 4] = ii
+
+ # top triangle
+ j = 6 * r[i] + 5 # right of i = left of r[i]
+ k = 6 * u[i] + 1 # top of i = bottom of u[i]
+ l = ii + 3 # diagonal
fp[j] = k
fp[k] = l
fp[l] = j
@@ -458,13 +585,11 @@ def from_square_tiled(cls, s, col=RED, mutable=False, check=True):
colouring[::3] = [RED]*n
colouring[2::3] = [BLUE]*n
colouring[1::3] = [col]*n
- colouring.extend(colouring[::-1])
- ep = array('i', ep)
fp = array('i', fp)
colouring = array('i', colouring)
boundary = array('i', [0] * (6 * n))
- return VeeringTriangulation.from_permutations(None, ep, fp, (boundary, colouring), mutable=mutable, check=check)
+ return VeeringTriangulation.from_permutations(None, fp, (boundary,), (colouring,), mutable=mutable, check=check)
@classmethod
def from_stratum(cls, c, folded_edges=False, mutable=False, check=True):
@@ -479,7 +604,7 @@ def from_stratum(cls, c, folded_edges=False, mutable=False, check=True):
sage: T = VeeringTriangulation.from_stratum(Stratum([2], 1)) # optional - surface_dynamics
sage: T # optional - surface_dynamics
- VeeringTriangulation("(0,6,~5)(1,8,~7)(2,7,~6)(3,~1,~8)(4,~2,~3)(5,~0,~4)", "RRRBBBBBB")
+ VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,8,~7)(~1,~8,3)(2,7,~6)(~2,~3,4)", "RRRBBBBBB")
sage: T.stratum() # optional - surface_dynamics
H_2(2)
@@ -497,11 +622,10 @@ def from_stratum(cls, c, folded_edges=False, mutable=False, check=True):
sage: c = QuadraticCylinderDiagram('(0)-(0)') # optional - surface_dynamics
sage: VeeringTriangulation.from_stratum(c) # optional - surface_dynamics
- VeeringTriangulation("(0,2,~1)(1,~0,~2)", "RBB")
-
+ VeeringTriangulation("(0,2,~1)(~0,~2,1)", "RBB")
sage: c = QuadraticCylinderDiagram('(0,0)-(1,1,2,2,3,3)') # optional - surface_dynamics
sage: VeeringTriangulation.from_stratum(c) # optional - surface_dynamics
- VeeringTriangulation("(0,10,~9)(1,~8,9)(2,~6,7)(3,~4,5)(4,~3,~11)(6,~2,~5)(8,~1,~7)(11,~10,~0)", "RRRRBBBBBBBB")
+ VeeringTriangulation("(0,10,~9)(~0,11,~10)(1,~8,9)(~1,~7,8)(2,~6,7)(~2,~5,6)(3,~4,5)(~3,~11,4)", "RRRRBBBBBBBB")
sage: c = CylinderDiagram('(0,6,4,5)-(3,6,5) (1,3,2)-(0,1,4,2)') # optional - surface_dynamics
sage: CT = VeeringTriangulation.from_stratum(c) # optional - surface_dynamics
@@ -588,7 +712,8 @@ def from_face_edge_perms(self, colouring, fp, ep, vp=None, boundary=None, mutabl
if vp is None:
vp = array('i', [-1] * n)
for i in range(n):
- vp[fp[ep[i]]] = i
+ if fp[ep(i)] != -1:
+ vp[fp[ep(i)]] = i
if boundary is None:
bdry = array('i', [0] * n)
@@ -596,7 +721,7 @@ def from_face_edge_perms(self, colouring, fp, ep, vp=None, boundary=None, mutabl
bdry = array('i', boundary)
colouring = array('i', colouring)
- return VeeringTriangulation.from_permutations(vp, ep, fp, (bdry, colouring), mutable, check)
+ return VeeringTriangulation.from_permutations(vp, ep, fp, (bdry, colouring), mutable=mutable, check=check)
def forgot_forward_flippable_colour(self, folded=True):
r"""
@@ -611,8 +736,7 @@ def forgot_forward_flippable_colour(self, folded=True):
sage: t = VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBBBRBBRB", mutable=True)
sage: t.forgot_forward_flippable_colour()
sage: t
- VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBPBRBPRB")
-
+ VeeringTriangulation("(0,~6,~3)(~0,2,~1)(1,7,~2)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBPBRBPRB")
sage: t = VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBBBRBBRB", mutable=False)
sage: t.forgot_forward_flippable_colour()
Traceback (most recent call last):
@@ -624,7 +748,7 @@ def forgot_forward_flippable_colour(self, folded=True):
ep = self._ep
for e in self.forward_flippable_edges(folded=folded):
- self._colouring[e] = self._colouring[ep[e]] = PURPLE
+ self._colouring[e] = PURPLE
def forgot_backward_flippable_colour(self):
r"""
@@ -640,7 +764,7 @@ def forgot_backward_flippable_colour(self):
sage: t = VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RBBBRBBRB", mutable=True)
sage: t.forgot_backward_flippable_colour()
sage: t
- VeeringTriangulation("(0,~6,~3)(1,7,~2)(2,~1,~0)(3,5,~4)(4,8,~5)(6,~8,~7)", "RGBBRGBRB")
+ VeeringTriangulation("(0,~6,~3)(~0,2,~1)(1,7,~2)(3,5,~4)(4,8,~5)(6,~8,~7)", "RGBBRGBRB")
sage: t = VeeringTriangulation("(0,6,~5)(1,8,~7)(2,7,~6)(3,~1,~8)(4,~2,~3)(5,~0,~4)", "RRRBBBBBB", mutable=True)
sage: t.forgot_backward_flippable_colour()
@@ -661,7 +785,7 @@ def forgot_backward_flippable_colour(self):
ep = self._ep
for e in self.backward_flippable_edges():
- self._colouring[e] = self._colouring[ep[e]] = GREEN
+ self._colouring[e] = GREEN
def triangulation(self, mutable=False):
r"""
@@ -672,17 +796,17 @@ def triangulation(self, mutable=False):
sage: from veerer import *
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], "RRB")
sage: T.triangulation()
- Triangulation("(0,1,2)(~2,~0,~1)")
+ Triangulation("(0,1,2)(~0,~1,~2)")
"""
return Triangulation.copy(self, mutable, Triangulation)
- def _colouring_string(self, short=False):
+ def _colouring_string(self, short=None):
+ if short is not None:
+ from warnings import warn
+ warn("short argument in VeeringTriangulation._colouring_string is deprecated")
n = self.num_half_edges()
ep = self._ep
- if short:
- return ''.join(colour_to_char(self._colouring[e]) for e in range(n) if e <= ep[e])
- else:
- return ''.join(colour_to_char(self._colouring[e]) for e in range(n))
+ return ''.join(colour_to_char(x) for x in self._colouring)
def __str__(self):
r"""
@@ -693,31 +817,28 @@ def __str__(self):
sage: VeeringTriangulation("(0,1,2)", [RED, RED, BLUE])
VeeringTriangulation("(0,1,2)", "RRB")
sage: VeeringTriangulation("(0,1,2)(3,4,~0)", "(~4:1,~3:1,~2:1,~1:1)", "RRBRB")
- VeeringTriangulation("(0,1,2)(3,4,~0)", boundary="(~4:1,~3:1,~2:1,~1:1)", colouring="RRBRB")
+ VeeringTriangulation("(0,1,2)(~0,3,4)(~1:1,~4:1,~3:1,~2:1)", "RRBRB")
sage: t = Triangulation("(0,1,2)(3,~0,~1)", "(~3:2,~2:2)")
sage: VeeringTriangulation(t, "RBBR")
- VeeringTriangulation("(0,1,2)(3,~0,~1)", boundary="(~3:2,~2:2)", colouring="RBBR")
+ VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:2,~3:2)", "RBBR")
"""
- cycles = perm_cycles(self._fp, n=self._n)
- face_cycles = perm_cycles_to_string([c for c in cycles if not self._bdry[c[0]]], involution=self._ep)
- bdry_cycles = perm_cycles_to_string([c for c in cycles if self._bdry[c[0]]], involution=self._ep, data=self._bdry)
- colouring = self._colouring_string(short=True)
- if bdry_cycles:
- return "VeeringTriangulation(\"{}\", boundary=\"{}\", colouring=\"{}\")".format(
- face_cycles,
- bdry_cycles,
- colouring)
- else:
- return "VeeringTriangulation(\"{}\", \"{}\")".format(
- face_cycles,
- colouring)
+ cycles = perm_cycles(self._fp, n=2 * self._ne)
+ face_cycles = perm_cycles_to_string([c for c in cycles if not self._bdry[c[0]]], edge_like=True)
+ face_cycles += perm_cycles_to_string([c for c in cycles if self._bdry[c[0]]], edge_like=True, data=self._bdry)
+ colouring = self._colouring_string()
+ return "VeeringTriangulation(\"{}\", \"{}\")".format(face_cycles, colouring)
def __repr__(self):
return str(self)
- # TODO: this duplicates the method edge_colour
- def colour(self, e):
- e = int(e)
+ def half_edge_colour(self, h, check=True):
+ if check:
+ h = self._check_half_edge(h)
+ return self._colouring[h // 2]
+
+ def edge_colour(self, e, check=True):
+ if check:
+ e = self._check_edge(e)
return self._colouring[e]
def is_holomorphic(self):
@@ -726,9 +847,202 @@ def is_holomorphic(self):
def is_meromorphic(self):
return any(self._bdry)
- def vertex_angle(self, e):
+ def is_prime(self):
+ return len(self.prime_components()) == 1
+
+ def prime_components(self):
+ r"""
+ Return the components of the prime_decomposition of this linear family.
+
+ The prime decomposition is coarser than the decomposition into
+ connected components. It is the finest partition so that the
+ constraints is a block-diagonal matrix. The result is a partition
+ of the edges as a list of lists.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+ sage: vt = VeeringTriangulation("(0,1,3)(2,4,5)", "RRRBBB")
+ sage: vt.prime_components()
+ [[0, 1, 3], [2, 4, 5]]
+
+ sage: f = VeeringTriangulationLinearFamily(vt, [[1, 1, 1, 0, 1, 0], [0, 1, 0, 1, 1, 1]])
+ sage: f.prime_components()
+ [[0, 1, 2, 3, 4, 5]]
+ """
+ partition = DisjointSet(self._ne)
+ for comp in self.connected_components():
+ for i in range(1, len(comp)):
+ partition.union(comp[0], comp[i])
+ return [atom for atom, _ in prime_decomposition(self.constraints_matrix(), partition)]
+
+ def prime_decomposition(self, mutable=False, check=True):
+ """
+ Return the prime decomposition of this veering triangulation or family.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+ sage: vt = VeeringTriangulation("(0,1,3)(2,4,5)", "RRRBBB")
+ sage: vt.prime_decomposition()
+ [([0, 1, 3],
+ VeeringTriangulationLinearFamily("(0,1,2)", "RRB", [(1, 0, -1), (0, 1, 1)])),
+ ([2, 4, 5],
+ VeeringTriangulationLinearFamily("(0,1,2)", "RBB", [(1, 0, -1), (0, 1, 1)]))]
+
+ sage: f = VeeringTriangulationLinearFamily(vt, [[1, 1, 1, 0, 1, 0], [0, 1, 0, 1, 1, 1]])
+ sage: f.prime_decomposition()
+ [([0, 1, 2, 3, 4, 5],
+ VeeringTriangulationLinearFamily("(0,1,3)(2,4,5)", "RRRBBB", [(1, 0, 1, -1, 0, -1), (0, 1, 0, 1, 1, 1)]))]
+
+ sage: VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:1,~4:1)(~3:1,4:1)", "BRRRR").prime_decomposition()
+ [([0, 1, 2, 3, 4],
+ VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~2:1,~4:1)(~3:1,4:1)", "BRRRR", [(1, 0, 1, 1, 0), (0, 1, 1, 1, 0), (0, 0, 0, 0, 1)]))]
+
+ """
+ from .linear_family import VeeringTriangulationLinearFamily
+ ans = []
+ gens = self.constraints_matrix().right_kernel_matrix()
+ partition = DisjointSet(self._ne)
+ for comp in self.connected_components():
+ for i in range(1, len(comp)):
+ partition.union(comp[0], comp[i])
+ for atom, subspace in prime_decomposition(gens, partition):
+ graph = self.constellation().subgraph(atom)
+ ans.append((atom, VeeringTriangulationLinearFamily(graph, subspace, mutable=mutable, check=check)))
+ return ans
+
+ def half_edge_num_separatrices(self, h, slope=VERTICAL, check=True):
+ h = self._check_half_edge(h)
+ col0 = self._colouring[h // 2]
+ col1 = self._colouring[self._vp[h] // 2]
+ if slope == VERTICAL:
+ return self._bdry[h] + ((col0 == RED or col0 == PURPLE) and (col1 == BLUE or col1== GREEN))
+ elif slope == HORIZONTAL:
+ return self._bdry[h] + ((col0 == BLUE or col0 == GREEN) and (col1 == RED or col1 == PURPLE))
+ else:
+ raise ValueError("invalid slope argument")
+
+ def vertex_separatrices(self, flat=True, slope=VERTICAL):
+ r"""
+ Return the pairs ``(h, a)`` encoding vertex separatrices on this veering triangulation.
+
+ INPUT:
+
+ - ``flat`` (boolean, default ``False``) -- whether to return the result
+ as a plain list or as a list of cycles corresponding to each vertex
+
+ - ``slope`` -- either ``VERTICAL`` (default) or ``HORIZONTAL``
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,12,~11)(~0,~10,11)(1,16,~15)(~1,~17,6)(2,15,~14)(~2,~6,7)(3,14,~13)(~3,~7,8)(4,17,~16)(~4,~8,9)(5,13,~12)(~5,~9,10)", "RRRRRRBBBBBBBBBBBB")
+ sage: vt.vertex_separatrices()
+ [(0, 0),
+ (9, 0),
+ (10, 0),
+ (1, 0),
+ (2, 0),
+ (5, 0),
+ (6, 0),
+ (11, 0),
+ (3, 0),
+ (4, 0),
+ (7, 0),
+ (8, 0)]
+ sage: vt.vertex_separatrices(flat=False)
+ [[(0, 0), (9, 0), (10, 0), (1, 0), (2, 0), (5, 0), (6, 0), (11, 0)],
+ [(3, 0), (4, 0), (7, 0), (8, 0)]]
+
+ sage: VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:1,~4:2)(~3:3,4:1)", "BRRRR").vertex_separatrices()
+ [(5, 0), (8, 0), (2, 0), (7, 0), (7, 1), (7, 2), (9, 0), (9, 1), (3, 0)]
+ sage: VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:1,~4:2)(~3:3,4:1)", "BRRRR").vertex_separatrices(flat=False)
+ [[(5, 0), (8, 0), (2, 0), (7, 0), (7, 1), (7, 2), (9, 0), (9, 1), (3, 0)]]
+ """
+ if slope == VERTICAL:
+ right = RED
+ left = BLUE
+ else:
+ right = BLUE
+ left = RED
+
+ if any(col == GREEN or col == PURPLE for col in self._colouring):
+ raise NotImplementedError
+
+ separatrices = []
+ for cycle in self.vertices():
+ orbit = []
+ for h in cycle:
+ for a in range(self.half_edge_num_separatrices(h, slope, check=False)):
+ orbit.append((h, a))
+ if flat:
+ separatrices.extend(orbit)
+ else:
+ separatrices.append(orbit)
+ return separatrices
+
+ # TODO: (for Kai) should we go clockwise or counter-clockwise around the face
+ # TODO: (for Kai) what should we do for angle=0 (ie infinite cylinder) faces
+ # which have no associated separatrix?
+ def face_separatrices(self, flat=True, slope=VERTICAL):
+ r"""
+ Return the pairs ``(h, a)`` encoding face separatrices on this veering triangulation.
+
+ INPUT:
+
+ - ``flat`` (boolean, default ``False``) -- whether to return the result
+ as a plain list or as a list of cycles corresponding to each vertex
+
+ - ``slope`` -- either ``VERTICAL`` (default) or ``HORIZONTAL``
+
+ EXAMPLES::
+
+ sage: from veerer import Triangulation, VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~0:1,~4:2)(~3:2,~2:3)", "RBRRB")
+ sage: vt.face_separatrices()
+ [(1, 0), (9, 0), (5, 1), (5, 0), (7, 0)]
+ sage: seps = vt.face_separatrices(flat=False)
+ sage: seps
+ [[(1, 0), (9, 0)], [(5, 1), (5, 0), (7, 0)]]
+ sage: all(vt.face_angle(h) == -len(sep) for sep in seps for h, a in sep)
+ True
+
+ An example in H(1^2, -1^2) where the two faces have no separatrices::
+
+ sage: vt = VeeringTriangulation("(~0,1,2)(~1,3,4)(~2,5,6)(~3,~5,7)(~6,8,9)(~7,~8,~9)(0:1)(~4:1)", "BRRRBBRRRB")
+ sage: vt.face_separatrices(flat=True)
+ []
+ sage: vt.face_separatrices(flat=False)
+ []
+ """
+ if slope == VERTICAL:
+ right = RED
+ left = BLUE
+ else:
+ right = BLUE
+ left = RED
+
+ if any(col == GREEN or col == PURPLE for col in self._colouring):
+ raise NotImplementedError
+
+ separatrices = []
+ for cycle in self.boundary_faces():
+ orbit = []
+ for h in cycle:
+ for a in range(self.half_edge_num_separatrices(h, slope, check=False) - 2, -1, -1):
+ orbit.append((h, a))
+ if orbit:
+ if flat:
+ separatrices.extend(orbit)
+ else:
+ separatrices.append(orbit)
+
+ return separatrices
+
+ def vertex_angle(self, h):
r"""
- Return the angle at the vertex the half-edge ``e`` is adjacent to.
+ Return the angle at the vertex the half-edge ``h`` is adjacent to.
EXAMPLES::
@@ -739,32 +1053,32 @@ def vertex_angle(self, e):
....: [RED, RED, RED, RED, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE])
sage: T.vertex_angle(0)
1
- sage: T.vertex_angle(1)
+ sage: T.vertex_angle(2)
3
"""
- e = self._check_half_edge(e)
+ h = self._check_half_edge(h)
vp = self.vertex_permutation(copy=False)
a = 0
- e0 = e
- col = self._colouring[e]
+ h0 = h
+ col = self._colouring[h // 2]
# NOTE: we count by multiples of pi/2 and then divide by 2
- ee = vp[e]
+ hh = vp[h]
while True:
- ee = vp[e]
- ccol = self._colouring[ee]
+ hh = vp[h]
+ ccol = self._colouring[hh // 2]
switch = bool(col != ccol and (((col & (BLUE|RED)) and (ccol & (BLUE|RED))) or (col & (GREEN|PURPLE))))
- a += switch + 2 * self._bdry[e]
- e = ee
+ a += switch + 2 * self._bdry[h]
+ h = hh
col = ccol
- if e == e0:
+ if h == h0:
break
assert a % 2 == 0, a
return a // 2
- def face_angle(self, e):
+ def face_angle(self, h, check=True):
r"""
- Return the angle associated to the pole in the middle of the face ``e`` is adjacent to.
+ Return the angle associated to the pole in the middle of the face the half-edge ``h`` is adjacent to.
EXAMPLES::
@@ -774,26 +1088,27 @@ def face_angle(self, e):
sage: vt.face_angle(3)
-2
"""
- e = self._check_half_edge(e)
- fp = self.face_permutation(copy=False)
+ if check:
+ h = self._check_half_edge(h)
+ if not self._bdry[h]:
+ raise ValueError('h={} not on a boundary face'.format(self._half_edge_string(h)))
- if not self._bdry[e]:
- raise ValueError('e={} not on a boundary face'.format(self._half_edge_string(e)))
+ fp = self.face_permutation(copy=False)
cum_angle = 0
alternations = 0
num_sides = 0
- col = self._colouring[e]
- e0 = e
+ col = self._colouring[h // 2]
+ h0 = h
while True:
- ee = fp[e]
- ccol = self._colouring[ee]
- cum_angle += self._bdry[e]
+ hh = fp[h]
+ ccol = self._colouring[hh // 2]
+ cum_angle += self._bdry[h]
alternations += (col != ccol)
num_sides += 1
col = ccol
- e = ee
- if e == e0:
+ h = hh
+ if h == h0:
break
return (num_sides - cum_angle - alternations // 2)
@@ -869,7 +1184,7 @@ def angles(self, half_edge_representatives=False):
[2, 2, 2, -2]
"""
# TODO: handle non-connected stratum correctly!
- n = self.num_half_edges()
+ n = 2 * self._ne
angles = []
half_edges = []
seen = [False] * n
@@ -878,16 +1193,16 @@ def angles(self, half_edge_representatives=False):
# zeros (and simple poles) from vertices
for e in range(n):
- if seen[e]:
+ if vp[e] == -1 or seen[e]:
continue
a = 0
- col = self._colouring[e]
+ col = self._colouring[e // 2]
# NOTE: we count by multiples of pi/2 and then divide by 2
half_edges.append(e)
while not seen[e]:
seen[e] = True
ee = vp[e]
- ccol = self._colouring[ee]
+ ccol = self._colouring[ee // 2]
switch = bool(col != ccol and (((col & (BLUE|RED)) and (ccol & (BLUE|RED))) or (col & (GREEN|PURPLE))))
a += switch + 2 * self._bdry[e]
e = ee
@@ -898,18 +1213,18 @@ def angles(self, half_edge_representatives=False):
# angles at infinity from boundary faces (meromorphic differentials)
seen = [False] * n
for e in range(n):
- if seen[e] or not self._bdry[e]:
+ if vp[e] == -1 or seen[e] or not self._bdry[e]:
continue
cum_angle = 0
alternations = 0
num_sides = 0
- col = self._colouring[e]
+ col = self._colouring[e // 2]
half_edges.append(e)
while not seen[e]:
seen[e] = True
ee = fp[e]
- ccol = self._colouring[ee]
+ ccol = self._colouring[ee // 2]
cum_angle += self._bdry[e]
alternations += (col != ccol)
num_sides += 1
@@ -937,7 +1252,7 @@ def is_abelian(self, certificate=False):
sage: T.is_abelian()
True
sage: T.is_abelian(certificate=True)
- (True, [True, True, False, False, False, False, True, True, True, True, False, False])
+ (True, [True, False, True, False, False, True, False, True, False, True, False, True])
sage: T = VeeringTriangulation([(-12, 4, -4), (-11, -1, 11), (-10, 0, 10),
....: (-9, 9, 1), (-8, 8, -2), (-7, 7, 2), (-6, 6, -3), (-5, 5, 3)],
....: [RED, RED, RED, RED, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE, BLUE])
@@ -992,10 +1307,12 @@ def is_abelian(self, certificate=False):
sage: VeeringTriangulation(t, "RBRR").is_abelian()
True
"""
- ep = self._ep
vp = self._vp
cols = self._colouring
+ if self.has_folded_edge():
+ return (False, None) if certificate else False
+
# The code attempt to give a coherent holonomy with signs for each half
# edge. For that purpose, it is enough to store a boolean for each edge:
# True: (+, +) or (+,-)
@@ -1008,39 +1325,45 @@ def is_abelian(self, certificate=False):
#
# The propagation of half-edge orientations is done by walking around
# vertices
- oris = [None] * self._n # list of orientations decided so far
- oris[0] = True
- oris[ep[0]] = False
- q = [0, ep[0]] # queue of half-edges to be treated
-
- while q:
- e = q.pop()
- o = oris[e]
- assert o is not None
- f = vp[e]
- while True:
- # compute the orientation of f from the one of e
- if (((cols[e] == RED or cols[e] == PURPLE) and (cols[f] == BLUE or cols[f] == GREEN)) + self._bdry[e]) % 2:
- o = not o
-
- if oris[f] is None:
- # f is not oriented yet
- assert oris[ep[f]] is None
- oris[f] = o
- oris[ep[f]] = not o
- q.append(ep[f])
- elif oris[f] != o:
- # f is incoherently oriented
- return (False, None) if certificate else False
- else:
- # f is correctly oriented
- break
+ oris = [None] * (2 * self._ne) # list of orientations decided so far
+ q = []
+
+ while any(x is None for x in oris):
+ h0 = 0
+ while oris[h0] is not None:
+ h0 += 1
+ oris[h0] = True
+ oris[h0 ^ 1] = False
+ q = [h0, h0 ^ 1]
+
+ while q:
+ h0 = q.pop()
+ h1 = vp[h0]
+ o = oris[h0]
+ assert o is not None
+ while True:
+ # compute the orientation of f from the one of e
+ if self.half_edge_num_separatrices(h0) % 2:
+ o = not o
+
+ if oris[h1] is None:
+ # f is not oriented yet
+ assert oris[h1 ^ 1] is None
+ oris[h1] = o
+ oris[h1 ^ 1] = not o
+ q.append(h1 ^ 1)
+ elif oris[h1] != o:
+ # f is incoherently oriented
+ return (False, None) if certificate else False
+ else:
+ # f is correctly oriented
+ break
- e, f = f, vp[f]
+ h0, h1 = h1, vp[h1]
return (True, oris) if certificate else True
- def abelian_cover(self, mutable=False):
+ def abelian_cover(self, mutable=False, involution_and_quotient=False):
r"""
Return the orientation double cover of this veering triangulation.
@@ -1088,34 +1411,52 @@ def abelian_cover(self, mutable=False):
# the covering
# {0, ..., n-1} : half-edges positively oriented
# {n, ..., 2n-1} : half-edges negatively oriented
- n = self._n
vp = self._vp
- ep = self._ep
cols = self._colouring
- ep_cov = array('i', [-1] * (2 * n))
- vp_cov = array('i', [-1] * (2 * n))
- for e in range(n):
- f = ep[e]
- ep_cov[e] = f + n
- ep_cov[f + n] = e
-
+ n = (4 * self._ne - 2 * self.num_folded_edges())
+
+ j = 2 * self._ne
+ inv = array('i', [-1] * n) # involution on the cover
+ quot = array('i', [-1] * n) # quotient map
+ bdry_cov = array('i', [-1] * n)
+ cols_cov = array('i', [-1] * (n // 2))
+ for e in range(self._ne):
+ quot[2 * e] = 2 * e
+ if self._vp[2 * e + 1] == -1:
+ inv[2 * e] = 2 * e + 1
+ bdry_cov[2 * e] = bdry_cov[2 * e + 1] = self._bdry[2 * e]
+ cols_cov[e] = cols[e]
+ quot[2 * e + 1] = 2 * e
+ else:
+ bdry_cov[2 * e] = bdry_cov[j + 1] = self._bdry[2 * e]
+ bdry_cov[2 * e + 1] = bdry_cov[j] = self._bdry[2 * e + 1]
+ inv[2 * e] = j + 1
+ inv[2 * e + 1] = j
+ cols_cov[e] = cols_cov[j // 2] = cols[e]
+ quot[2 * e + 1] = 2 * e + 1
+ quot[j] = 2 * e + 1
+ quot[j + 1] = 2 * e
+ j += 2
+
+ vp_cov = array('i', [-1] * n)
+ for e in range(2 * self._ne):
f = vp[e]
- if (((cols[e] == RED or cols[e] == PURPLE) and (cols[f] == BLUE or cols[f] == GREEN)) + self._bdry[e]) % 2:
- vp_cov[e] = f + n
- vp_cov[e + n] = f
+ if f == -1:
+ continue
+ if (((cols[e // 2] == RED or cols[e // 2] == PURPLE) and (cols[f // 2] == BLUE or cols[f // 2] == GREEN)) + self._bdry[e] + (e % 2 != f % 2)) % 2:
+ vp_cov[e] = inv[f]
+ vp_cov[inv[e]] = f
else:
vp_cov[e] = f
- vp_cov[e + n] = f + n
+ vp_cov[inv[e]] = inv[f]
- bdry_cov = self._bdry * 2
- cols_cov = self._colouring * 2
-
- return VeeringTriangulation.from_permutations(vp_cov, ep_cov, None, (bdry_cov, cols_cov), mutable=mutable, check=False)
+ vt_cov = VeeringTriangulation.from_permutations(vp_cov, None, (bdry_cov,), (cols_cov,), mutable=mutable, check=True)
+ return (vt_cov, inv, quot) if involution_and_quotient else vt_cov
def stratum(self):
r"""
- Return the Abelian or quadratic stratum of this coloured triagulation.
+ Return the Abelian or quadratic stratum of this coloured triangulation.
EXAMPLES::
@@ -1198,6 +1539,25 @@ def dimension(self):
sage: vt = VeeringTriangulation(fp, bdry, cols)
sage: assert vt.dimension() == vt.as_linear_family().dimension() == 2
sage: assert vt.stratum().dimension() == 2 # optional - surface_dynamics # not tested (not yet in surface_dynamics)
+
+ An example in Q(-1^2, -2)::
+
+ sage: vt = VeeringTriangulation("(0:1)", "B")
+ sage: vt.dimension()
+ 1
+ sage: vt.stratum().dimension() # optional - surface_dynamics # not tested (not yet in surface_dynamics)
+ 1
+
+ An example with a disconnected linear family::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily, DelaunayStrebelAutomaton
+ sage: vt = VeeringTriangulation("(1,3,~2)(2,6,~3)(4,~6,~5)(7,~11,~8)(8,9,10)(11,~20,~12)(13,~17,~14)(14,15,16)(17,~19,~18)(19,~15,~16)(20,~9,~10)(~4,~1,~0)", boundary="(0:1)(5:1)(12:1,~13:1)(18:1,~7:1)", colouring="RRRBBRRRRBRBRRRBRBRRR")
+ sage: subspace = [(2, 0, 0, 0, 2, 2, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0),
+ ....: (0, 2, 0, 2, -2, 0, 2, 0, 1, 1, 0, -1, 0, 0, 1, 1, 0, -1, 0, 1, 1),
+ ....: (0, 0, 2, -2, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0)]
+ sage: f = VeeringTriangulationLinearFamily(vt, subspace)
+ sage: f.stratum_dimension()
+ 9
"""
# each folded edge gives a simple pole
ans = self.num_boundary_faces() + self.num_vertices() + self.num_folded_edges()
@@ -1205,24 +1565,42 @@ def dimension(self):
ans += 2 * self.genus() - 2 + (self.is_holomorphic() and self.is_abelian())
else:
for comp in self.connected_components():
- G = VeeringTriangulation.subgraph(self, comp)
+ # NOTE: the code below could be called by a VeeringTriangulationLinearFamily
+ # for which the subgraph code is broken
+ # see https://github.com/flatsurf/veerer/issues/54
+ G = VeeringTriangulation.subgraph(self.constellation(), comp)
ans += 2 * G.genus() - 2 + (G.is_holomorphic() and G.is_abelian())
return ans
stratum_dimension = dimension
- def colours_about_edge(self, e, check=True):
+ def colours_about_half_edge(self, h, check=True):
+ r"""
+ Return the list of colours of the quadrilateral around the half-edge ``h``.
+ """
if check:
- e = self._check_half_edge(e)
- return [self._colouring[f] for f in self.square_about_edge(e, check=False)]
+ h = self._check_half_edge(h)
+ return [self._colouring[f // 2] for f in self.square_about_half_edge(h, check=False)]
def alternating_square(self, e, check=True):
r"""
Return whether there is an alternating square around the edge ``e``.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: V = VeeringTriangulation([(0,1,2), (-1,-2,-3)], "RRB")
+ sage: V.alternating_square(0)
+ True
+ sage: V.alternating_square(1)
+ True
+ sage: V.alternating_square(2)
+ False
"""
if check:
- e = self._check_half_edge(e)
- colours = self.colours_about_edge(e, check=False)
+ e = self._check_edge(e)
+ h = 2 * e
+ colours = self.colours_about_half_edge(h, check=False)
if any(colours[f] == GREEN or colours[f] == PURPLE for f in range(4)):
return False
return all(colours[f] != colours[(f+1) % 4] for f in range(4))
@@ -1243,7 +1621,7 @@ def vertex_cycles(self, slope=VERTICAL, backend=None):
sage: sorted(V.vertex_cycles(HORIZONTAL))
[[1, 0, 1], [1, 1, 0]]
"""
- return self.train_track_polytope(slope, backend=backend).rays()
+ return self.cone(slope, backend=backend).rays()
def branches(self, slope=VERTICAL):
r"""
@@ -1266,7 +1644,6 @@ def branches(self, slope=VERTICAL):
n = self.num_half_edges()
ne = self.num_edges()
fp = self.face_permutation(copy=False)
- ep = self.edge_permutation(copy=False)
w = [0] * ne
seen = [False] * n
if slope == VERTICAL:
@@ -1283,18 +1660,18 @@ def branches(self, slope=VERTICAL):
# go to the right transition
a = e
- while self.colour(a) != left or self.colour(fp[a]) != right:
+ while self.half_edge_colour(a) != left or self.half_edge_colour(fp[a]) != right:
a = fp[a]
b = fp[a]
c = fp[b]
- w[self._norm(c)] += 1 # half large
+ w[c // 2] += 1 # half large
seen[a] = seen[b] = seen[c] = True
small = []
mixed = []
large = []
for e in range(ne):
- if ep[e] == e: # folded edge
+ if fp[2 * e + 1] == -1: # folded edge
w[e] *= 2
if w[e] == 0:
small.append(e)
@@ -1307,7 +1684,6 @@ def branches(self, slope=VERTICAL):
return (small, mixed, large)
-
def is_flippable(self, e, check=True):
r"""
Return whether the edge ``e`` can be flipped.
@@ -1325,12 +1701,12 @@ def is_flippable(self, e, check=True):
False
"""
if check:
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
return Triangulation.is_flippable(self, e, check=False) and self.alternating_square(e, check=False)
def is_forward_flippable(self, e, check=True):
r"""
- Return whether one can perform a forward flip to ``e``.
+ Return whether one can perform a forward flip to the edge ``e``.
EXAMPLES::
@@ -1347,19 +1723,29 @@ def is_forward_flippable(self, e, check=True):
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [PURPLE, BLUE, RED])
sage: [T.is_forward_flippable(e) for e in range(3)]
[True, False, False]
+
+ sage: faces = "(0,4,3)(1,~3,5)(2,6,~4)"
+ sage: x = (2, 6, 3, 4, 2, 2, 1)
+ sage: y = (10, 2, 3, 4, 6, 2, 3)
+ sage: cols = "BBRBRRB"
+ sage: fl = FlatVeeringTriangulation(faces, cols, x, y)
+ sage: fl.is_forward_flippable(1)
+ True
+ sage: fl.is_forward_flippable(3)
+ False
"""
if check:
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
if self._colouring[e] == GREEN or not Triangulation.is_flippable(self, e, check=False):
return False
if self._colouring[e] == PURPLE:
return True
- ca, cb, cc, cd = self.colours_about_edge(e, check=False)
+ ca, cb, cc, cd = self.colours_about_half_edge(2 * e, check=False)
return bool(ca & (BLUE | GREEN)) and bool(cb & (RED | GREEN)) and bool(cc & (BLUE | GREEN)) and bool(cd & (RED | GREEN))
def is_backward_flippable(self, e, check=True):
r"""
- Return whether one can perform a backward flip to ``e``.
+ Return whether one can perform a backward flip to the edge ``e``.
EXAMPLES::
@@ -1378,12 +1764,12 @@ def is_backward_flippable(self, e, check=True):
[False, True, True]
"""
if check:
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
if self._colouring[e] == PURPLE or not Triangulation.is_flippable(self, e, check=False):
return False
if self._colouring[e] == GREEN:
return True
- ca, cb, cc, cd = self.colours_about_edge(e, check=False)
+ ca, cb, cc, cd = self.colours_about_half_edge(2 * e, check=False)
return bool(ca & (RED | PURPLE)) and bool(cb & (BLUE | PURPLE)) and bool(cc & (RED | PURPLE)) and bool(cd & (BLUE | PURPLE))
def forward_flippable_edges(self, folded=True):
@@ -1432,19 +1818,18 @@ def forward_flippable_edges(self, folded=True):
TESTS::
- sage: from veerer.permutation import perm_random
+ sage: from veerer.permutation import perm_random_centralizer
sage: from veerer.veering_triangulation import VeeringTriangulation
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], "RRB", mutable=True)
sage: for _ in range(10):
- ....: rel = perm_random(6)
+ ....: rel = perm_random_centralizer(T.edge_permutation())
....: T.relabel(rel)
....: assert len(T.forward_flippable_edges()) == 1
"""
- ep = self._ep
- n = self._n
+ vp = self._vp
if folded:
- return [e for e in range(n) if e <= ep[e] and self.is_forward_flippable(e, check=False)]
- return [e for e in range(n) if e < ep[e] and self.is_forward_flippable(e, check=False)]
+ return [e for e in range(self._ne) if self.is_forward_flippable(e, check=False)]
+ return [e for e in range(self._ne) if vp[2 * e + 1] != -1 and self.is_forward_flippable(e, check=False)]
def backward_flippable_edges(self, folded=True):
r"""
@@ -1486,14 +1871,15 @@ def backward_flippable_edges(self, folded=True):
sage: vtR2.backward_flippable_edges()
[0]
"""
- ep = self._ep
- n = self._n
+ vp = self._vp
if folded:
- return [e for e in range(n) if e <= ep[e] and self.is_backward_flippable(e, check=False)]
- return [e for e in range(n) if e <= ep[e] and self.is_backward_flippable(e, check=False)]
+ return [e for e in range(self._ne) if self.is_backward_flippable(e, check=False)]
+ return [e for e in range(self._ne) if vp[2 * e + 1] != -1 and self.is_backward_flippable(e, check=False)]
def purple_edges(self, folded=True):
r"""
+ Return the list of edges coloured purple.
+
EXAMPLES::
sage: from veerer import VeeringTriangulation
@@ -1501,11 +1887,12 @@ def purple_edges(self, folded=True):
sage: t.purple_edges()
[2, 6]
"""
- ep = self._ep
- n = self._n
+ vp = self._ep
+ colouring = self._colouring
+ ne = self._ne
if folded:
- return [e for e in range(n) if e <= ep[e] and self._colouring[e] == PURPLE]
- return [e for e in range(n) if e < ep[e] and self._colouring[e] == PURPLE]
+ return [e for e in range(ne) if colouring[e] == PURPLE]
+ return [e for e in range(ne) if vp[2 * e + 1] != -1 and self._colouring[e] == PURPLE]
def mostly_sloped_edges(self, slope):
if slope == HORIZONTAL:
@@ -1518,20 +1905,21 @@ def mostly_sloped_edges(self, slope):
Triangulation.relabel(self, p, check=False)
perm_on_list(p, self._colouring)
- # TODO: check the argument!!!
- def edge_colour(self, e):
- return self._colouring[e]
-
- def set_edge_colour(self, e, col):
+ def set_edge_colour(self, e, col, check=True):
+ r"""
+ Set the colour of the edge ``e`` to ``col``.
+ """
if not self._mutable:
raise ValueError('immutable veering triangulation; use a mutable copy instead')
- if self._colouring[e] != PURPLE and self._colouring[e] != GREEN:
- raise ValueError("only PURPLE and GREEN edges could be changed colours")
- if col != BLUE and col != RED:
- raise ValueError("the new colour 'col' must be RED or BLUE")
- E = self._ep[e]
- self._colouring[e] = self._colouring[E] = col
+ if check:
+ e = self._check_edge(e)
+ if self._colouring[e] != PURPLE and self._colouring[e] != GREEN:
+ raise ValueError("only PURPLE and GREEN edges could be changed colours")
+ if col != BLUE and col != RED:
+ raise ValueError("the new colour 'col' must be RED or BLUE")
+
+ self._colouring[e] = col
def set_random_colours(self):
r"""
@@ -1568,7 +1956,7 @@ def set_colour(self, col):
if col != RED and col != BLUE:
raise ValueError("'col' must be RED or BLUE")
- for e in range(self._n):
+ for e in range(self._ne):
if self._colouring[e] == GREEN or self._colouring[e] == PURPLE:
self._colouring[e] = col
@@ -1576,16 +1964,19 @@ def rotate(self):
r"""
Apply the pi/2 rotation.
+ This amount to change the colouring (the combinatorics of the
+ triangulation remains unchanged).
+
EXAMPLES::
sage: from veerer import *
sage: faces = "(0,1,2)(~0,~4,~2)(3,4,5)(~3,~1,~5)"
sage: cols = [BLUE,RED,RED,BLUE,RED,RED]
- sage: T = VeeringTriangulation(faces, cols)
+ sage: T = VeeringTriangulation(faces, cols, mutable=True)
sage: T.rotate()
sage: T
- VeeringTriangulation("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "RBBRBB")
+ VeeringTriangulation("(0,1,2)(~0,~4,~2)(~1,~5,~3)(3,4,5)", "RBBRBB")
sage: T._check()
sage: fp = "(0,12,~11)(1,13,~12)(2,14,~13)(3,15,~14)(4,17,~16)(5,~10,11)(6,~3,~17)(7,~2,~6)(8,~5,~7)(9,~0,~8)(10,~4,~9)(16,~15,~1)"
@@ -1605,11 +1996,38 @@ def rotate(self):
sage: T = VeeringTriangulation("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "BRPBRP", mutable=True)
sage: T.rotate()
sage: T
- VeeringTriangulation("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "RBGRBG")
+ VeeringTriangulation("(0,1,2)(~0,~4,~2)(~1,~5,~3)(3,4,5)", "RBGRBG")
sage: T.rotate()
sage: T
- VeeringTriangulation("(0,1,2)(3,4,5)(~5,~3,~1)(~4,~2,~0)", "BRPBRP")
+ VeeringTriangulation("(0,1,2)(~0,~4,~2)(~1,~5,~3)(3,4,5)", "BRPBRP")
+
+ Forward and backward delaunay flips are interchanged under a rotation::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRBBBR", mutable=True)
+ sage: vt.delaunay_cone()
+ 8-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRBBBR") made of
+ 2 forward-flip facets
+ 2 backward-flip facets
+ 5 x-degeneration facets
+ 4 y-degeneration facets
+ sage: sorted(vt.delaunay_flips())
+ [([1], 1), ([1], 2), ([5, 6], 1), ([5, 6], 2)]
+ sage: sorted(vt.backward_delaunay_flips())
+ [([0], 1), ([0], 2), ([7], 1), ([7], 2)]
+ sage: vt.rotate()
+ sage: vt.delaunay_cone()
+ 8-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "BBRRBRRRB") made of
+ 2 forward-flip facets
+ 2 backward-flip facets
+ 4 x-degeneration facets
+ 5 y-degeneration facets
+ sage: sorted(vt.delaunay_flips())
+ [([0], 1), ([0], 2), ([7], 1), ([7], 2)]
+ sage: sorted(vt.backward_delaunay_flips())
+ [([1], 1), ([1], 2), ([5, 6], 1), ([5, 6], 2)]
"""
+ if not self._mutable:
+ raise ValueError('immutable veering triangulation; use a mutable copy instead')
for i, col in enumerate(self._colouring):
if col == RED:
self._colouring[i] = BLUE
@@ -1634,7 +2052,7 @@ def conjugate(self):
sage: T = VeeringTriangulation(faces, cols, mutable=True)
sage: T.conjugate()
sage: T
- VeeringTriangulation("(0,2,4)(1,3,5)(~5,~4,~3)(~2,~1,~0)", "RBBRBB")
+ VeeringTriangulation("(0,2,4)(~0,~2,~1)(1,3,5)(~3,~5,~4)", "RBBRBB")
sage: T._check()
sage: T = VeeringTriangulation(faces, cols, mutable=False)
@@ -1648,57 +2066,9 @@ def conjugate(self):
Triangulation.conjugate(self)
transp = {RED: BLUE, BLUE: RED, GREEN: GREEN, PURPLE: PURPLE}
- for i in range(self._n):
+ for i in range(self._ne):
self._colouring[i] = transp[self._colouring[i]]
- # TODO: finish this!!
- def automorphism_quotient(self, aut):
- r"""
- Return the canonical triangulation of the quotient.
-
- (vertex fixed, edge fixed, faces fixed)
-
- EXAMPLES::
-
- sage: from veerer import *
-
- sage: p = "(0,~1,2)(~0,1,~3)(4,~5,3)(~4,6,~2)(7,~6,8)(~7,5,~9)(10,~11,9)(~10,11,~8)"
- sage: cols = 'BRBBBRRBBBBR'
- sage: T = VeeringTriangulation(p, cols)
- """
- raise NotImplementedError
- nb_verts = 0 # vertices stabilized
- nb_faces = 0 # faces stabilized
- nb_edges = 0 # edges stabilized
-
- n = self._n # ???
- ep = self._triangulation.edge_permutation()
-
- # edges
- nb_edges = sum(aut[e] == ep[e] or aut[e] == e for e in range(n))
-
- # vertices
- verts, edge_to_vert = perm_cycles(self._vp)
- for i,v in enumerate(verts):
- if edge_to_vert[aut[v[0]]] == i:
- # deg does not change
- nb_edges += 1
- else:
- # deg is wrapped around
- pass
-
- # faces
- faces, edge_to_face = perm_cycles(self._fp)
- for i,f in enumerate(faces):
- nb_faces += edge_to_face[aut[f[0]]] == i
-
- return (nb_verts, nb_edges, nb_faces)
-
- colours = self._colouring_string(short=False)
- fp = perm_base64_str(self._fp)
- ep = perm_base64_str(self._ep)
- return colours + '_' + fp + '_' + ep
-
def flip(self, e, col, Lx=None, Gx=None, reduced=None, check=True):
r"""
Flip an edge inplace.
@@ -1725,16 +2095,16 @@ def flip(self, e, col, Lx=None, Gx=None, reduced=None, check=True):
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB", mutable=True)
sage: T.flip(1, RED)
sage: T
- VeeringTriangulation("(0,~2,1)(2,~1,~0)", "RRB")
+ VeeringTriangulation("(0,~2,1)(~0,2,~1)", "RRB")
sage: T.flip(0, RED)
sage: T
- VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB")
+ VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
sage: T.flip(1, BLUE)
sage: T
- VeeringTriangulation("(0,~2,1)(2,~1,~0)", "RBB")
+ VeeringTriangulation("(0,~2,1)(~0,2,~1)", "RBB")
sage: T.flip(2, BLUE)
sage: T
- VeeringTriangulation("(0,~1,~2)(1,2,~0)", "RBB")
+ VeeringTriangulation("(0,~1,~2)(~0,1,2)", "RBB")
The same flip sequence with reduced veering triangulations (forward flippable
edges in ``PURPLE``)::
@@ -1742,19 +2112,19 @@ def flip(self, e, col, Lx=None, Gx=None, reduced=None, check=True):
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB", mutable=True)
sage: T.forgot_forward_flippable_colour()
sage: T
- VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RPB")
+ VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RPB")
sage: T.flip(1, RED)
sage: T
- VeeringTriangulation("(0,~2,1)(2,~1,~0)", "PRB")
+ VeeringTriangulation("(0,~2,1)(~0,2,~1)", "PRB")
sage: T.flip(0, RED)
sage: T
- VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RPB")
+ VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RPB")
sage: T.flip(1, BLUE)
sage: T
- VeeringTriangulation("(0,~2,1)(2,~1,~0)", "RBP")
+ VeeringTriangulation("(0,~2,1)(~0,2,~1)", "RBP")
sage: T.flip(2, BLUE)
sage: T
- VeeringTriangulation("(0,~1,~2)(1,2,~0)", "RPB")
+ VeeringTriangulation("(0,~1,~2)(~0,1,2)", "RPB")
Some examples involving linear subspaces::
@@ -1764,8 +2134,8 @@ def flip(self, e, col, Lx=None, Gx=None, reduced=None, check=True):
sage: T.flip(3, 2, Gx=Gx)
sage: T.flip(4, 2, Gx=Gx)
sage: T.flip(5, 2, Gx=Gx)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(0), VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(1), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(0), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(1), VERTICAL)
sage: T, s, t = VeeringTriangulations.L_shaped_surface(2, 3, 4, 5, 1, 2)
sage: T = T.copy(mutable=True)
@@ -1773,8 +2143,8 @@ def flip(self, e, col, Lx=None, Gx=None, reduced=None, check=True):
sage: flip_sequence = [(3, 2), (4, 1), (5, 2), (6 , 2), (5, 1), (1, 1), (5, 1)]
sage: for e, col in flip_sequence:
....: T.flip(e, col, Gx=Gx)
- ....: T._set_switch_conditions(T._tt_check, Gx.row(0), VERTICAL)
- ....: T._set_switch_conditions(T._tt_check, Gx.row(1), VERTICAL)
+ ....: T._set_subspace_constraints(T._constraint_check, Gx.row(0), VERTICAL)
+ ....: T._set_subspace_constraints(T._constraint_check, Gx.row(1), VERTICAL)
"""
if not self._mutable:
raise ValueError('immutable veering triangulation; use a mutable copy instead')
@@ -1783,72 +2153,66 @@ def flip(self, e, col, Lx=None, Gx=None, reduced=None, check=True):
if col != BLUE and col != RED and col != GREEN:
raise ValueError("'col' must be BLUE, RED or GREEN")
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
if not self.is_forward_flippable(e, check=False):
raise ValueError("half-edge e={} is not forward flippable".format(e))
+ h = 2 * e
+
if reduced is None:
reduced = self._colouring[e] == PURPLE
if Lx is not None:
raise NotImplementedError("not implemented for linear equations")
if Gx is not None:
- a, b, c, d = self.square_about_edge(e, check=False)
- e = self._norm(e)
- a = self._norm(a)
- d = self._norm(d)
-
- b = self._norm(b)
- c = self._norm(c)
- assert Gx.column(e) == Gx.column(a) + Gx.column(b) == Gx.column(c) + Gx.column(d)
+ a, b, c, d = self.square_about_half_edge(h, check=False)
+ ea = a // 2
+ ed = d // 2
+
+ eb = b // 2
+ ec = c // 2
+ assert Gx.column(e) == Gx.column(ea) + Gx.column(eb) == Gx.column(ec) + Gx.column(ed)
if col == RED:
# ve <- vd - va
# e-th column becomes d-th column minus a-th column
Gx.add_multiple_of_column(e, e, -1)
- Gx.add_multiple_of_column(e, d, +1)
- Gx.add_multiple_of_column(e, a, -1)
+ Gx.add_multiple_of_column(e, ed, +1)
+ Gx.add_multiple_of_column(e, ea, -1)
elif col == BLUE:
# ve <- va - vd
Gx.add_multiple_of_column(e, e, -1)
- Gx.add_multiple_of_column(e, d, -1)
- Gx.add_multiple_of_column(e, a, +1)
+ Gx.add_multiple_of_column(e, ed, -1)
+ Gx.add_multiple_of_column(e, ea, +1)
else:
raise NotImplementedError('GREEN not implemented with linear subspace')
# flip and set colour
- E = self._ep[e]
+ ep = self._ep
Triangulation.flip(self, e, check=False)
- self._colouring[e] = self._colouring[E] = col
+ self._colouring[e] = col
if reduced:
- ep = self._ep
- a, b, c, d = self.square_about_edge(e, check=False)
- assert self._colouring[a] & (RED | GREEN), (a, colour_to_string(self._colouring[a]))
- assert self._colouring[b] & (BLUE | GREEN), (b, colour_to_string(self._colouring[b]))
- assert self._colouring[c] & (RED | GREEN), (c, colour_to_string(self._colouring[c]))
- assert self._colouring[d] & (BLUE | GREEN), (d, colour_to_string(self._colouring[d]))
-
- # assertions to be removed
+ a, b, c, d = self.square_about_half_edge(h, check=False)
+ assert self._colouring[a // 2] & (RED | GREEN)
+ assert self._colouring[b // 2] & (BLUE | GREEN)
+ assert self._colouring[c // 2] & (RED | GREEN)
+ assert self._colouring[d // 2] & (BLUE | GREEN)
assert not self.is_forward_flippable(e)
if col == BLUE:
- if self.is_forward_flippable(b, check=False):
- self._colouring[b] = PURPLE
- self._colouring[ep[b]] = PURPLE
- if d != ep[b] and self.is_forward_flippable(d, check=False):
- self._colouring[d] = PURPLE
- self._colouring[ep[d]] = PURPLE
- assert not self.is_forward_flippable(a)
- assert not self.is_forward_flippable(c)
+ if self.is_forward_flippable(b // 2, check=False):
+ self._colouring[b // 2] = PURPLE
+ if d != ep(b) and self.is_forward_flippable(d // 2, check=False):
+ self._colouring[d // 2] = PURPLE
+ assert not self.is_forward_flippable(a // 2)
+ assert not self.is_forward_flippable(c // 2)
elif col == RED:
- if self.is_forward_flippable(a, check=False):
- self._colouring[a] = PURPLE
- self._colouring[ep[a]] = PURPLE
- if c != ep[a] and self.is_forward_flippable(c, check=False):
- self._colouring[c] = PURPLE
- self._colouring[ep[c]] = PURPLE
- assert not self.is_forward_flippable(b)
- assert not self.is_forward_flippable(d)
+ if self.is_forward_flippable(a // 2, check=False):
+ self._colouring[a // 2] = PURPLE
+ if c != ep(a) and self.is_forward_flippable(c // 2, check=False):
+ self._colouring[c // 2] = PURPLE
+ assert not self.is_forward_flippable(b // 2)
+ assert not self.is_forward_flippable(d // 2)
else:
assert col == GREEN
# should we put all edges in a cylinder PURPLE?
@@ -1857,14 +2221,18 @@ def cylinders(self, col):
r"""
Return the list of cylinders of colour ``col``.
- Each cylinder is given as a quadruple ``(edges, rbdry, lbdry, half)`` where
+ Each cylinder is given as a quadruple ``(middle, rbdry, lbdry, pocket)`` where
+
+ - ``middle`` are the half-edges crossed by the core curve of the annulus
- - ``edges`` are the edges crossed by the core curve of the annulus
+ - ``rbdry`` and ``lbdry`` are either two lists of half-edge boundaries or,
+ in the case of "pocket cylinder" the list of half-edge boundaries cut by
+ the folded edges.
- - ``rbdry`` and ``lbdry`` are respectively the list of edges on the right
- and left boundaries
+ - ``pocket`` (boolean) whether this is a pocket cylinder
- - ``half`` is a boolean that is ``True`` when it is cut by two folded edges
+ In the case of "pocket cylinder", the first and last elements of
+ ``middle`` are the folded edges.
EXAMPLES::
@@ -1874,13 +2242,13 @@ def cylinders(self, col):
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
sage: T.cylinders(RED)
- [([0, 4], [2], [3], False)]
+ [([0, 3], [4], [5], False)]
sage: T.cylinders(BLUE)
[]
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "BRB")
sage: T.cylinders(BLUE)
- [([2, 5], [1], [4], False)]
+ [([4, 1], [2], [3], False)]
sage: T.cylinders(RED)
[]
@@ -1888,7 +2256,7 @@ def cylinders(self, col):
sage: T = VeeringTriangulation("(0,1,2)", "BBR")
sage: T.cylinders(BLUE)
- [([0, 1], [], [2], True)]
+ [([0, 2], [], [4], True)]
sage: T.cylinders(RED)
[]
@@ -1896,51 +2264,50 @@ def cylinders(self, col):
sage: cols = "BBBBBBBRRRRRR"
sage: T = VeeringTriangulation(fp, cols, mutable=True)
sage: T.cylinders(BLUE)
- [([6, 2, 0, 1, 14, 5, 3], [9, 8, 7], [12, 11, 10], True)]
+ [([12, 4, 0, 2, 9, 10, 6], [18, 16, 14], [24, 22, 20], True)]
sage: T.cylinders(RED)
[]
sage: T.forgot_forward_flippable_colour()
sage: T.cylinders(RED)
- [([12, 15, 9], [6], [0], True),
- ([8, 1, 11], [14], [17], True),
- ([10, 13, 7], [4], [3], True)]
+ [([24, 5, 18], [12], [0], True),
+ ([16, 2, 22], [9], [1], True),
+ ([20, 11, 14], [8], [6], True)]
sage: fp = "(0,12,3)(1,2,11)(4,6,7)(5,~2,10)(8,~5,~4)(9,~1,~0)"
sage: cols = "BBRRRBBBBRRRB"
sage: T = VeeringTriangulation(fp, cols)
sage: T.cylinders(BLUE)
- [([6, 7], [], [4], True)]
+ [([12, 14], [], [8], True)]
sage: T.cylinders(RED)
- [([10, 15, 11], [5], [1], True)]
+ [([20, 5, 22], [10], [2], True)]
sage: fp = '(0,~5,4)(1,~3,2)(3,5,~4)(6,~1,~0)'
sage: cols = 'RBBRBRB'
sage: T = VeeringTriangulation(fp, cols)
sage: T.cylinders(BLUE)
- [([2, 1, 6], [11], [9], True)]
+ [([12, 3, 4], [7], [1], True)]
sage: T.cylinders(RED)
[]
sage: T = VeeringTriangulation("(0,~2,1)(2,~4,3)(4,~6,5)(6,~1,~0)", "RBRBBBR")
sage: T.cylinders(BLUE)
- [([5, 4, 3], [], [7, 2], True)]
+ [([10, 8, 6], [], [13, 4], True)]
Torus with PURPLE edge::
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "PBR", mutable=True)
sage: T.cylinders(RED)
- [([2, 5], [1], [4], False)]
+ [([4, 1], [2], [3], False)]
sage: T.cylinders(BLUE)
- [([0, 4], [2], [3], False)]
-
+ [([0, 3], [4], [5], False)]
sage: R = T.copy()
sage: R.set_colour(RED)
sage: R.cylinders(RED)
- [([2, 5], [1], [4], False)]
+ [([4, 1], [2], [3], False)]
sage: B = T.copy()
sage: B.set_colour(BLUE)
sage: B.cylinders(BLUE)
- [([0, 4], [2], [3], False)]
+ [([0, 3], [4], [5], False)]
Longer examples::
@@ -1948,31 +2315,43 @@ def cylinders(self, col):
sage: cols = "BBBBBRRRRR"
sage: T = VeeringTriangulation(fp, cols, mutable=True)
sage: T.cylinders(BLUE)
- [([0, 1, 2, 3, 4], [7, 5, 6], [9, 8], False)]
+ [([0, 2, 4, 6, 8], [14, 10, 12], [18, 16], False)]
sage: T.forgot_forward_flippable_colour()
sage: T.cylinders(BLUE)
- [([0, 1, 2, 3, 4], [7, 5, 6], [9, 8], False)]
+ [([0, 2, 4, 6, 8], [14, 10, 12], [18, 16], False)]
sage: T.set_colour(BLUE)
sage: T.rotate()
sage: T.cylinders(RED)
- [([0, 1, 2, 3, 4], [7, 5, 6], [9, 8], False)]
+ [([0, 2, 4, 6, 8], [14, 10, 12], [18, 16], False)]
sage: V = VeeringTriangulation("(0,3,8)(1,~4,2)(4,5,~11)(6,~5,7)(9,~8,11)(10,~2,~1)(~10,~7,~9)(~6,~0,~3)", "PBPBRPRBRBRB")
sage: V.cylinders(BLUE)
- [([0, 20], [8], [17], False),
- ([2, 22], [19], [10], False),
- ([5, 7, 14, 11], [4, 15], [6, 13], False)]
+ [([0, 7], [16], [13], False),
+ ([4, 3], [9], [20], False),
+ ([10, 14, 19, 22], [8, 17], [12, 21], False)]
+
sage: V = VeeringTriangulation("(0,12,~11)(1,13,~12)(2,3,14)(4,10,9)(5,~10,11)(6,~5,~17)(7,~0,~6)(8,~4,~7)(15,~13,~14)(16,17,~2)(~16,~3,~15)(~9,~8,~1)", "RRRBRRBBBBPBBBRRPB")
sage: V.cylinders(BLUE)
[]
+
+ Some examples with boundaries::
+
+ sage: fp = "(0,2,1)(~0,3,~1)"
+ sage: bdry = "(~2:2,~3:2)"
+ sage: for cols in ["BRRR", "BBRR", "RBRR"]:
+ ....: vt = VeeringTriangulation(fp, bdry, cols)
+ ....: print(cols, vt.cylinders(RED), vt.cylinders(BLUE))
+ BRRR [] []
+ BBRR [] [([2, 1], [4], [6], False)]
+ RBRR [] []
"""
if col != RED and col != BLUE:
raise ValueError("'col' must be RED or BLUE")
opcol = RED if col == BLUE else BLUE
- n = self._n
+ n = 2 * self._ne
fp = self._fp
ep = self._ep
cols = self._colouring
@@ -1981,7 +2360,7 @@ def cylinders(self, col):
seen = [False] * n
for a in range(n):
- if seen[a]:
+ if fp[a] == -1 or seen[a] or self._bdry[a]:
continue
# triangle (a,b,c)
@@ -1990,7 +2369,7 @@ def cylinders(self, col):
if seen[b] or seen[c]:
continue
- if (cols[a] == opcol) + (cols[b] == opcol) + (cols[c] == opcol) > 1:
+ if (cols[a // 2] == opcol) + (cols[b // 2] == opcol) + (cols[c // 2] == opcol) > 1:
seen[a] = seen[b] = seen[c] = True
continue
@@ -2009,112 +2388,240 @@ def cylinders(self, col):
# x---------x x
#
# We make it so we start with a right triangle.
- if cols[a] == opcol:
+ if cols[a // 2] == opcol:
a, b, c = b, c, a
- elif cols[b] == opcol:
+ elif cols[b // 2] == opcol:
a, b, c = c, a, b
- assert cols[a] == col or cols[a] == PURPLE
- assert cols[b] == col or cols[b] == PURPLE
- assert cols[c] == opcol
+ assert cols[a // 2] == col or cols[a // 2] == PURPLE
+ assert cols[b // 2] == col or cols[b // 2] == PURPLE
+ assert cols[c // 2] == opcol
a0, b0, c0 = a, b, c
RIGHT = 0
LEFT = 1
typ = RIGHT
- cc = [] # cycle of half-edges inside the cylinder
+ cc = [] # cycle of edges inside the cylinder
rbdry = [] # right boundary
lbdry = [] # left boundary
half_turn = False # whether the cylinder is a folded cylinder
cyl = True
- while not seen[a]:
- assert cols[a] == col or cols[a] == PURPLE, (typ, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
- seen[a] = seen[ep[a]] = True
+ while not seen[a] and not self._bdry[a]:
+ assert cols[a // 2] == col or cols[a // 2] == PURPLE, (typ, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
+ seen[a] = seen[ep(a)] = True
cc.append(a)
if typ == RIGHT:
- assert cols[c] == opcol, (typ, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
- assert cols[b] == col or cols[b] == PURPLE, (typ, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
+ assert cols[c // 2] == opcol, (typ, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
+ assert cols[b // 2] == col or cols[b // 2] == PURPLE, (typ, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
rbdry.append(c)
else:
- assert cols[b] == opcol, (typ, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
- assert cols[c] == col or cols[c] == PURPLE, (typ, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
+ assert cols[b // 2] == opcol, (typ, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
+ assert cols[c // 2] == col or cols[c // 2] == PURPLE, (typ, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
lbdry.append(b)
- if a == ep[a]:
+ if a == ep(a):
# found a folded edge... we can not continue in this
# direction. Either stop or start again from the other door.
if half_turn:
cyl = True
break
half_turn = True
- cc = [ep[x] for x in reversed(cc)]
+ cc = [ep(x) for x in reversed(cc)]
lbdry.reverse()
rbdry.reverse()
rbdry, lbdry = lbdry, rbdry
lbdry.pop() # a0 will be added again at the beginning of next loop
- assert cols[c0] == opcol
+ assert cols[c0 // 2] == opcol
a, b, c = b0, c0, a0
typ = LEFT
continue
# go to triangle across the door
- a = ep[a]
- b = fp[a]
- c = fp[b]
- assert cols[a] == col or cols[a] == PURPLE, (self, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
- if cols[b] == col or cols[b] == PURPLE:
- assert cols[c] == opcol, (self, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
- # LEFT type, next door is b
- a, b, c = b, c, a
- typ = LEFT
- elif cols[c] == col or cols[c] == PURPLE:
- assert cols[b] == opcol, (self, a, colour_to_string(cols[a]), b, colour_to_string(cols[b]), c, colour_to_string(cols[c]))
- # RIGHT type, next door is c
- a, b, c = c, a, b
- typ = RIGHT
- else:
+ a = ep(a)
+ if self._bdry[a]:
cyl = False
break
+ else:
+ b = fp[a]
+ c = fp[b]
+ assert cols[a // 2] == col or cols[a // 2] == PURPLE, (self, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
+ if cols[b // 2] == col or cols[b // 2] == PURPLE:
+ assert cols[c // 2] == opcol, (self, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
+ # LEFT type, next door is b
+ a, b, c = b, c, a
+ typ = LEFT
+ elif cols[c // 2] == col or cols[c // 2] == PURPLE:
+ assert cols[b // 2] == opcol, (self, a, colour_to_string(cols[a // 2]), b, colour_to_string(cols[b // 2]), c, colour_to_string(cols[c // 2]))
+ # RIGHT type, next door is c
+ a, b, c = c, a, b
+ typ = RIGHT
+ else:
+ cyl = False
+ break
if cyl and (a == a0 or half_turn):
cylinders.append((cc, rbdry, lbdry, half_turn))
return cylinders
- def dehn_twists(self, col):
+ def cylinder_is_minimal(self, cyl):
r"""
- Return the list of Dehn twists along the cylinders of colour ``col``.
+ Return whether ``cyl`` is minimal, that is whether all saddle connection
+ appearing in its boundary are colinear.
EXAMPLES::
- sage: from veerer import VeeringTriangulation, BLUE, RED
+ sage: from veerer import VeeringTriangulation, RED
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRRRRB")
+ sage: cyl0, cyl1 = vt.cylinders(RED)
+ sage: cyl0
+ ([0, 3], [4], [6], False)
+ sage: cyl1
+ ([8, 12, 15, 11], [5, 16], [7, 17], False)
+ sage: vt.cylinder_is_minimal(cyl0)
+ True
+ sage: vt.cylinder_is_minimal(cyl1)
+ False
+ """
+ gens = self.generators_matrix()
+ mid_half_edges, rbdry, lbdry, pocket = cyl
- sage: T = VeeringTriangulation("(0,~4,5)(1,~0,6)(2,8,~1)(3,~7,~2)(4,~3,7)(~8,9,~11)(10,~5,~9)(11,~6,~10)", "BBBBBRRRRBBB")
- sage: b1, b2 = T.dehn_twists(BLUE)
- sage: b1
- VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(11,~6,~10)", "BBBBBRRRRBBB"), "1B 0B 4B 2B 1B 0B", "(0,3,1,4,~2)(2,~0,~3,~1,~4)(5)(6)(7)(8)(9)(10)(11)(~11)(~10)(~9)(~8)(~7)(~6)(~5)")
- sage: b2
- VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(11,~6,~10)", "BBBBBRRRRBBB"), "9B 10B", "(0)(1)(2)(3)(4)(5)(6)(7)(8)(9,~10,11)(10,~11,~9)(~8)(~7)(~6)(~5)(~4)(~3)(~2)(~1)(~0)")
+ rbdry = gens.matrix_from_columns([i // 2 for i in rbdry])
+ lbdry = gens.matrix_from_columns([i // 2 for i in lbdry])
+ return is_rank_one(rbdry) and is_rank_one(lbdry)
- sage: T.rotate()
- sage: r1, r2 = T.dehn_twists(RED)
- sage: r1
- VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(11,~6,~10)", "RRRRRBBBBRRR"), "3R 4R 0R 2R 3R 4R", "(0,2,4,1,3)(5)(6)(7)(8)(9)(10)(11)(~11)(~10)(~9)(~8)(~7)(~6)(~5)(~4,~1,~3,~0,~2)")
- sage: r2
- VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(11,~6,~10)", "RRRRRBBBBRRR"), "11R 10R", "(0)(1)(2)(3)(4)(5)(6)(7)(8)(9,11,10)(~11,~10,~9)(~8)(~7)(~6)(~5)(~4)(~3)(~2)(~1)(~0)")
+ def cylinder_circumference(self, cyl, x, check=True):
+ r"""
+ Return the circumference of the cylinder ``x`` in coordinates ``x``.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, RED
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRRRRB")
+ sage: cyl0, cyl1 = vt.cylinders(RED)
+ sage: gens = vt.generators_matrix()
+ sage: x = (5, 9, 4, 4, 1, 5, 5, 6, 1)
+ sage: vt.cylinder_circumference(cyl0, x)
+ 4
+ sage: vt.cylinder_circumference(cyl1, x)
+ 5
+
+ sage: x = (12, 9, 4, 4, 1, 5, 5, 6, 1)
+ sage: print(vt.cylinder_circumference(cyl0, x))
+ Traceback (most recent call last):
+ ...
+ AssertionError: does not satisfy train-track constraints
+ """
+ if check:
+ if len(x) != self._ne:
+ raise ValueError("x must be a list or a vector of length the number of edges")
+ self._set_subspace_constraints(self._constraint_check, x, VERTICAL)
+
+ middle, rbdry, lbdry, pocket = cyl
+ if pocket:
+ raise "NotImplementedError"
+ circ = 0
+ for h in middle:
+ e = h // 2
+ if self.is_forward_flippable(e):
+ circ += x[e]
+ elif self.is_backward_flippable(e):
+ circ -= x[e]
+ return circ
+
+ def cylinder_area(self, cyl, x, y, check=True):
+ r"""
+ Return the area of the cylinder ``cyl`` for the coordinates ``x`` and ``y``.
+
+ Warning: this does not return the area of the flat cylinder but rather
+ the sum of areas of the triangles in the combinatorial cylinder. The
+ former might be smaller. They coincide when the cylinder is minimal
+ (see :meth:`cylinder_is_minimal`).
+
+ The area is a quadratic form and ``x`` and ``y`` are not required to be
+ non-negative. Simply to satisfy the triangle inequalities (and possibly
+ additional linear constraints).
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, RED
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "RRBBRRRRB")
+ sage: cyl0, cyl1 = vt.cylinders(RED)
+ sage: gens = vt.generators_matrix()
+ sage: x = (5, 9, 4, 4, 1, 5, 5, 6, 1)
+ sage: y = (5, 1, 4, 4, 9, 5, 5, 4, 1)
+ sage: vt.cylinder_area(cyl0, x, y)
+ 40
+ sage: vt.cylinder_area(cyl1, x, y)
+ 50
+
+ sage: x = (12, 9, 4, 4, 1, 5, 5, 6, 1)
+ sage: y = (5, 1, 4, 4, 9, 5, 5, 4, 1)
+ sage: print(vt.cylinder_area(cyl0, x, y))
+ Traceback (most recent call last):
+ ...
+ AssertionError: does not satisfy train-track constraints
+ """
+ if check:
+ if len(x) != self._ne or len(y) != self._ne:
+ raise ValueError("x and y must be lists or vectors of length the number of edges")
+ self._set_subspace_constraints(self._constraint_check, x, VERTICAL)
+ self._set_subspace_constraints(self._constraint_check, y, HORIZONTAL)
+
+ middle, rbdry, lbdry, pocket = cyl
+ area = 0
+ for a in middle:
+ b = self._fp[a]
+ c = self._fp[b]
+ if self.half_edge_colour(a) == BLUE and self.half_edge_colour(b) == RED:
+ a, b, c = c, a, b
+ elif self.half_edge_colour(c) == BLUE and self.half_edge_colour(a) == RED:
+ a, b, c = b, c, a
+ assert self.half_edge_colour(b) == BLUE and self.half_edge_colour(c) == RED
+ xl = x[b // 2]
+ yl = y[b // 2]
+ xr = x[c // 2]
+ yr = y[c // 2]
+ area += xr * yl + xl * yr
+ return area /2
+
+ def dehn_twists(self, col):
+ r"""
+ Return the list of Dehn twists along the cylinders of colour ``col``.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, BLUE, RED
+
+ sage: T = VeeringTriangulation("(0,~4,5)(1,~0,6)(2,8,~1)(3,~7,~2)(4,~3,7)(~8,9,~11)(10,~5,~9)(11,~6,~10)", "BBBBBRRRRBBB", mutable=True)
+ sage: b1, b2 = T.dehn_twists(BLUE)
+ sage: b1
+ VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(~8,9,~11)", "BBBBBRRRRBBB"), "1B 0B 4B 2B 1B 0B", "(0,3,1,4,~2)(~0,~3,~1,~4,2)")
+ sage: b2
+ VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(~8,9,~11)", "BBBBBRRRRBBB"), "9B 10B", "(9,~10,11)(~9,10,~11)")
+
+ sage: T.rotate()
+ sage: r1, r2 = T.dehn_twists(RED)
+ sage: r1
+ VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(~8,9,~11)", "RRRRRBBBBRRR"), "3R 4R 0R 2R 3R 4R", "(0,2,4,1,3)(~0,~2,~4,~1,~3)")
+ sage: r2
+ VeeringFlipSequence(VeeringTriangulation("(0,~4,5)...(~8,9,~11)", "RRRRRBBBBRRR"), "11R 10R", "(9,11,10)(~9,~11,~10)")
A (purple) square-tiled surface corresponds to a Penner system. A
product associated to the Dehn twists is of pseudo-Anosov type if and
only if all twists appear at least once::
sage: T = VeeringTriangulation("(0,~2,1)(2,~11,~3)(3,10,~4)(4,~15,~5)(5,14,~6)(6,~10,~7)(7,9,~8)(8,17,~9)(11,15,~12)(12,~17,~13)(13,~16,~14)(16,~1,~0)", "PRBPRBPRBPBRBPRPBR")
- sage: b1,b2 = T.dehn_twists(BLUE)
- sage: r1,r2,r3 = T.dehn_twists(RED)
+ sage: b1, b2 = T.dehn_twists(BLUE)
+ sage: r1, r2, r3 = T.dehn_twists(RED)
sage: (r1 * r3 * b1 * b2).is_pseudo_anosov()
False
sage: (r1 * r3 * b1 * b2 * r2).is_pseudo_anosov()
True
"""
+ if self.has_folded_edge():
+ raise NotImplementedError
+
if col != RED and col != BLUE:
raise ValueError("'col' must be RED or BLUE")
@@ -2126,25 +2633,23 @@ def dehn_twists(self, col):
ep = self._ep
cols = self._colouring
for mid, rbdry, lbdry, half in self.cylinders(col):
- if half:
- raise NotImplementedError
+ assert not half
# count packets
edges = []
packets = []
p = []
- for r in lbdry:
- assert cols[r] == opcol
- s = vp[r]
- del p[:]
- while cols[s] != opcol:
- p.append(s)
+ for h in lbdry:
+ assert cols[h // 2] == opcol
+ s = vp[h]
+ p.clear()
+ while cols[s // 2] != opcol:
+ p.append(s // 2)
s = vp[s]
edges.extend(p)
packets.append(len(p))
- # for as many times as there are saddle connection
- # in the bdry twist
+ # build the flip sequence
F = VeeringFlipSequence(self)
m = len(lbdry)
n = len(edges)
@@ -2158,21 +2663,38 @@ def dehn_twists(self, col):
F.append_flip(edges[l], col)
flipsmod2[l] = 1 - flipsmod2[l]
j += packets[i]
- r = perm_id(self._n)
+
+ # build the relabelling
+ r = perm_id(2 * self._ne)
+ vp1 = F._start._vp
+ cols1 = F._start._colouring
+ vp2 = F._end._vp
+ cols2 = F._end._colouring
+ for h in lbdry:
+ assert cols1[h // 2] == cols2[h // 2] == opcol
+ s1 = vp1[h]
+ s2 = vp2[h]
+ assert cols1[s1 // 2] == cols2[s2 // 2]
+ assert cols1[s1 // 2] != opcol
+ while cols1[s1 // 2] != opcol:
+ r[s2] = s1
+ r[s2 ^ 1] = s1 ^ 1
+ s1 = vp1[s1]
+ s2 = vp2[s2]
+ assert cols1[s1 // 2] == cols2[s2 // 2]
+ assert s1 == s2
+
+ # check
for i in range(n):
- j = (i-m)%n if col == BLUE else (i+m)%n
+ j = (i - m) % n if col == BLUE else (i + m) % n
e = edges[i]
f = edges[j]
- if col == BLUE and flipsmod2[i]:
- r[e] = ep[f]
- r[ep[e]] = f
- else:
- r[e] = f
- r[ep[e]] = ep[f]
+ assert r[2 * e] // 2 == f and r[2 * e + 1] // 2 == f
+
F.append_relabelling(r)
# TODO: remove assertion check
- assert F.start() == F.end()
+ assert F.start() == F.end(), (F.start(), F.end(), F.start().is_isomorphic_to(F.end(), certificate=True))
twists.append(F)
@@ -2215,34 +2737,33 @@ def is_cylindrical(self, col=None):
elif col != BLUE and col != RED and col != PURPLE:
raise ValueError("'col' must be one of BLUE, RED or PURPLE")
- n = self._n
+ n = 2 * self._ne
fp = self._fp
cols = self._colouring
seen = [False] * n
for a in range(n):
- if seen[a]:
+ if fp[a] == -1 or seen[a]:
continue
b = fp[a]
c = fp[b]
seen[a] = seen[b] = seen[c] = True
if col == PURPLE:
- if cols[a] != PURPLE and cols[b] != PURPLE and cols[c] != PURPLE:
+ if cols[a // 2] != PURPLE and cols[b // 2] != PURPLE and cols[c // 2] != PURPLE:
return False
else:
- if (cols[a] == PURPLE) + (cols[b] == PURPLE) + (cols[c] == PURPLE) + \
- (cols[a] == col) + (cols[b] == col) + (cols[c] == col) != 2:
+ if (cols[a // 2] == PURPLE) + (cols[b // 2] == PURPLE) + (cols[c // 2] == PURPLE) + \
+ (cols[a // 2] == col) + (cols[b // 2] == col) + (cols[c // 2] == col) != 2:
return False
return True
def is_quadrangulable(self):
k = 0
- n = self.num_edges()
- ep = self._ep
- for e in range(n):
+ fp = self._fp
+ for e in range(self._ne):
if not self.is_forward_flippable(e, check=False):
continue
- k += 1 if ep[e] == e else 2
+ k += 1 + (self._fp[2 * e + 1] != -1)
return k == self.num_faces()
@@ -2291,15 +2812,14 @@ def is_square_tiled(self, col=PURPLE):
True
"""
k = 0
- n = self.num_edges()
- ep = self._ep
- for e in range(n):
- if not self.is_forward_flippable(e, check=False):
+ fp = self._fp
+ for e in range(self._ne):
+ if not self.is_forward_flippable(e):
continue
- if self._colouring[e] != col:
+ if self.edge_colour(e) != col:
return False
- k += 1 if ep[e] == e else 2
+ k += 1 + (fp[2 * e + 1] != -1)
return k == self.num_faces()
@@ -2335,11 +2855,11 @@ def is_strebel(self, slope=VERTICAL):
False
"""
ep = self._ep
- n = self._n
+ n = 2 * self._ne
if slope == VERTICAL:
- return not any(self.is_forward_flippable(e, check=False) for e in range(n) if not self._bdry[e] and not self._bdry[ep[e]] and e <= ep[e])
+ return not any(self.is_forward_flippable(e, check=False) for e in range(self._ne) if not self._bdry[2 * e] and not self._bdry[ep(2 * e)])
elif slope == HORIZONTAL:
- return not any(self.is_backward_flippable(e, check=False) for e in range(n) if not self._bdry[e] and not self._bdry[ep[e]] and e <= ep[e])
+ return not any(self.is_backward_flippable(e, check=False) for e in range(self._ne) if not self._bdry[2 * e] and not self._bdry[ep(2 * e)])
else:
raise ValueError('slope must either be HORIZONTAL or VERTICAL')
@@ -2388,6 +2908,8 @@ def properties_code(self):
def residue_matrix(self, slope=VERTICAL):
r"""
+ Return the matrix of residues.
+
EXAMPLES::
sage: from veerer import VeeringTriangulation, StrebelGraph, VERTICAL, HORIZONTAL
@@ -2395,8 +2917,8 @@ def residue_matrix(self, slope=VERTICAL):
sage: vt = VeeringTriangulation("(0,1,2)(~1,~2,3)", "(~0:1)(~3:1)", "RBRR")
sage: r = vt.residue_matrix()
sage: r
- [ 0 0 0 1]
[-1 0 0 0]
+ [ 0 0 0 1]
sage: G = StrebelGraph("(0,2,~3,~1)(1)(3,~0)(~2)")
sage: for colouring in G.colourings():
@@ -2405,27 +2927,52 @@ def residue_matrix(self, slope=VERTICAL):
....: r = vt.residue_matrix(slope)
....: for x in vt.generators_matrix(slope).rows():
....: assert sum(r * x) == 0, (vt, slope, x)
- """
- ep = self._ep
- nf = self.num_boundary_faces()
- ne = self.num_edges()
- r = matrix(ZZ, nf, ne)
- ans, orientations = self.is_abelian(certificate=True)
- if not ans:
- raise ValueError('not an Abelian differential')
+ Some quadratic differentials with double poles::
+
+ sage: VeeringTriangulation("(~2,4,5)(~3,6,~4)(0:1)(1:1,2:1,3:1)(~5:1,7:1)(~6:1,~7:1)", "BBBBRBBB").residue_matrix()
+ [1 0 0 0 0 0 0 0]
+ [0 1 1 1 0 0 0 0]
+ [0 0 0 0 0 1 0 1]
+ [0 0 0 0 0 0 1 1]
+ """
+ ne = self._ne
+ colouring = self._colouring
+
+ if any(col == PURPLE or col == GREEN for col in colouring):
+ raise NotImplementedError
+
+ is_abelian, orientations = self.is_abelian(certificate=True)
+ if is_abelian:
+ # Abelian differential: we make a consistent global choice of signs for the residues
+ nf = self.num_boundary_faces()
+ r = matrix(ZZ, nf, ne)
+ if slope == VERTICAL:
+ orientations = [1 if x else -1 for x in orientations]
+ elif slope == HORIZONTAL:
+ orientations = [1 if (orientations[i] == (colouring[i // 2] == RED)) else -1 for i in range(2 * ne)]
+ else:
+ raise ValueError('invalid slope argument; must be VERTICAL or HORIZONTAL')
+
+ for i, f in enumerate(self.boundary_faces()):
+ for h in f:
+ r[i, h // 2] += orientations[h]
- if slope == VERTICAL:
- orientations = [1 if x else -1 for x in orientations]
- elif slope == HORIZONTAL:
- orientations = [1 if (x == (col == RED)) else -1 for x, col in zip(orientations, self._colouring)]
else:
- raise ValueError('invalid slope argument; must be VERTICAL or HORIZONTAL')
+ # quadratic differentials: we can only have a local choice of signs
+ # Note that faces whose angle is an odd multiple of pi have residue zero and we
+ # ignore them
+ fp = self._fp
+ even_angle_faces = [f for f in self.boundary_faces() if self.face_angle(f[0]) % 2 == 0]
+ r = matrix(ZZ, len(even_angle_faces), ne)
+ for i, f in enumerate(even_angle_faces):
+ o = 1
+ for h0 in f:
+ r[i, h0 // 2] += o
- for i, f in enumerate(self.boundary_faces()):
- for e in f:
- j = e if e < ep[e] else ep[e]
- r[i, j] += orientations[e]
+ h1 = fp[h0]
+ if self.half_edge_num_separatrices(h1, slope) % 2 == 0:
+ o *= -1
return r
@@ -2442,10 +2989,10 @@ def add_residue_constraints(self, residue_constraints):
EXAMPLES::
sage: from veerer import VeeringTriangulation
- sage: vt = VeeringTriangulation("(0,5,~4)(2,6,~5)(3,~0,7)(4,~3,~1)", boundary="(1:1)(~7:1)(~6:1)(~2:1)", colouring="RRRBBBBB")
- sage: f1 = vt.add_residue_constraints([[1, 1, 1, 0]])
+ sage: vt = VeeringTriangulation("(0,5,~4)(2,6,~5)(3,~0,7)(4,~3,~1)(1:1)(~7:1)(~6:1)(~2:1)", colouring="RRRBBBBB")
+ sage: f1 = vt.add_residue_constraints([[1, 0, 1, 1]])
sage: f1
- VeeringTriangulationLinearFamily("(0,5,~4)(2,6,~5)(3,~0,7)(4,~3,~1)", boundary="(1:1)(~7:1)(~6:1)(~2:1)", colouring="RRRBBBBB", [(1, 0, 0, 0, 0, 1, 1, 1), (0, 1, 0, 0, 1, 1, 1, 0), (0, 0, 0, 1, 1, 1, 1, 1)])
+ VeeringTriangulationLinearFamily("(0,5,~4)(~0,7,3)(~1,4,~3)(2,6,~5)(1:1)(~2:1)(~6:1)(~7:1)", "RRRBBBBB", [(1, 0, 0, 0, 0, 1, 1, 1), (0, 1, 0, 0, 1, 1, 1, 0), (0, 0, 0, 1, 1, 1, 1, 1)])
sage: f1.is_core()
True
sage: f1.is_delaunay()
@@ -2453,16 +3000,16 @@ def add_residue_constraints(self, residue_constraints):
sage: f2 = vt.add_residue_constraints([[1, 1, 0, 1]])
sage: f2
- VeeringTriangulationLinearFamily("(0,5,~4)(2,6,~5)(3,~0,7)(4,~3,~1)", boundary="(1:1)(~7:1)(~6:1)(~2:1)", colouring="RRRBBBBB", [(1, 0, 0, -1, -1, 0, 0, 0), (0, 1, 0, -1, 0, 0, 0, -1), (0, 0, 1, -1, -1, -1, 0, -1)])
+ VeeringTriangulationLinearFamily("(0,5,~4)(~0,7,3)(~1,4,~3)(2,6,~5)(1:1)(~2:1)(~6:1)(~7:1)", "RRRBBBBB", [(1, 0, 0, -1, -1, 0, 0, 0), (0, 1, 0, -1, 0, 0, 0, -1), (0, 0, 1, -1, -1, -1, 0, -1)])
sage: f2.is_core()
False
- Adding residue constraints commute with taking the Strebel graph::
+ Adding residue constraints commute with taking the Strebel graph
+ (modulo a possible relabelling of the residue)::
- sage: G1 = f1.strebel_graph().add_residue_constraints([[1, 1, 1, 0]])
- sage: G2 = f1.add_residue_constraints([[1, 1, 1, 0]]).strebel_graph()
- sage: G1 == G2
- True
+ sage: G1 = f1.strebel_graph().add_residue_constraints([[1, 1, 0, 1]])
+ sage: G2 = f1.add_residue_constraints([[0, 1, 1, 1]]).strebel_graph()
+ sage: assert G1 == G2, (G1, G2)
"""
if not isinstance(residue_constraints, Matrix):
residue_constraints = matrix(residue_constraints)
@@ -2513,8 +3060,8 @@ def flip_back(self, e, col, Lx=None, Gx=None, check=True):
sage: T.flip(3, 2, Gx=Gx)
sage: T.flip(4, 2, Gx=Gx)
sage: T.flip(5, 2, Gx=Gx)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(0), VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(1), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(0), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(1), VERTICAL)
sage: Gx.echelon_form()
[ 1 0 0 1 1 1 1]
[ 0 1 1 -1 -1 -1 0]
@@ -2532,100 +3079,184 @@ def flip_back(self, e, col, Lx=None, Gx=None, check=True):
if col != BLUE and col != RED and col != PURPLE:
raise ValueError("'col' must be BLUE, RED or PURPLE")
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
if not self.is_backward_flippable(e, check=False):
raise ValueError('half-edge e={} is not backward flippable'.format(e))
+ h = 2 * e
if Lx is not None:
raise NotImplementedError("not implemented for linear equations")
- E = self._ep[e]
+ H = self._ep(h)
Triangulation.flip_back(self, e, check=False)
old_col = self._colouring[e]
- self._colouring[e] = self._colouring[E] = col
+ self._colouring[e] = col
if Gx is not None:
- a, b, c, d = self.square_about_edge(e, check=False)
- e = self._norm(e)
- a = self._norm(a)
- b = self._norm(b)
- c = self._norm(c)
- d = self._norm(d)
+ a, b, c, d = self.square_about_half_edge(h, check=False)
+ a //= 2
+ b //= 2
+ c //= 2
+ d //= 2
Gx.add_multiple_of_column(e, e, -1)
Gx.add_multiple_of_column(e, a, +1)
Gx.add_multiple_of_column(e, b, +1)
+ def intersection_form(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,3,8)(~0,5,6)(~3,4,2)(~4,1,7)", "BBBRRRRRR")
+ sage: I = vt.intersection_form()
+ sage: I
+ [ 0 0 0 1 0 1 0 0 0]
+ [ 0 0 0 0 0 0 0 1 0]
+ [ 0 0 0 1 0 0 0 0 0]
+ [-1 0 -1 0 0 0 0 0 0]
+ [ 0 0 0 0 0 0 0 0 0]
+ [-1 0 0 0 0 0 0 0 0]
+ [ 0 0 0 0 0 0 0 0 0]
+ [ 0 -1 0 0 0 0 0 0 0]
+ [ 0 0 0 0 0 0 0 0 0]
+ """
+ if any(c == PURPLE or c == GREEN for c in self._colouring):
+ raise NotImplementedError
+
+ if any(self._bdry):
+ raise NotImplementedError
+
+ m = matrix(ZZ, self._ne)
+ for a, b, c in self.non_degenerate_triangles():
+ b //= 2
+ c //= 2
+ m[b, c] = 1
+ m[c, b] = -1
+ return m
+
+ def relative_generators_matrix(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+
+ sage: VeeringTriangulation("(0,~5,4)(~0,~2,3)(1,2,5)(~1,~3,~4)", "RRBBRB").relative_generators_matrix().echelon_form()
+ [ 1 -1 0 -1 0 1]
+ sage: VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,3,8)(~1,~6,~7)(2,~8,7)(~2,~3,4)", "RRRBBBBRB").relative_generators_matrix().echelon_form()
+ []
+ sage: VeeringTriangulation("(0,~5,~4)(~0,7,9)(1,5,~7)(~1,10,~9)(2,11,~6)(~2,~11,8)(3,~8,~10)(~3,6,4)", "RRBBBRRBRRBR").relative_generators_matrix().echelon_form()
+ [ 1 -1 2 -1 -1 0 0 1 0 0 -1 -2]
+ """
+ if any(self._bdry):
+ raise NotImplementedError
+
+ G = self.generators_matrix()
+ I = G * self.intersection_form() * G.transpose()
+ return I.right_kernel_matrix() * G
+
+ def relative_dimension(self):
+ return self.relative_generators_matrix().nrows()
+
+ def rank(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import *
+
+ An example in Q_0(2, -1^6)::
+
+ sage: VeeringTriangulation("(0,3,8)(~0,5,6)(~3,4,2)(~4,1,7)", "BBBRRRRRR").rank()
+ 2
+
+ Abelian principal stratum in genus 3::
+
+ sage: vt = VeeringTriangulation("(0,16,~15)(~0,~14,15)(1,19,~18)(~1,~23,8)(2,22,~21)(~2,~8,9)(3,21,~20)(~3,~9,10)(4,20,~19)(~4,~10,11)(5,23,~22)(~5,~11,12)(6,18,~17)(~6,~12,13)(7,17,~16)(~7,~13,14)", "RRRRRRRRBBBBBBBBBBBBBBBB")
+ sage: vt.rank()
+ 3
+
+ A Teichmueller curve in Q_0(1, -1^5)::
+
+ sage: T, s, t = VeeringTriangulations.L_shaped_surface(1, 1, 1, 1)
+ sage: f = VeeringTriangulationLinearFamily(T, [s, t])
+ sage: f.rank()
+ 1
+
+ An eigenform locus in Q_0(2, -1^6)::
+
+ sage: X9 = VeeringTriangulationLinearFamilies.prototype_H1_1(0, 2, 1, -1)
+ sage: X9.rank()
+ 1
+
+ Gothic locus::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,~0)(5,6,~1)(7,~2,8)(9,~3,~6)(10,~7,~4)(11,~5,12)(13,14,~8)(15,~9,16)(17,18,~10)(19,~17,~11)(20,~13,~12)(21,~14,~18)(22,~21,~15)(23,24,~16)(25,~23,~19)(26,~20,~25)(~26,~24,~22)", "RBBRBRBRRBRBBRBBRRBRRRBBRRB")
+ sage: subspace = [(1, 0, -1, 0, -1, 0, 0, 0, 1, 0, 1, -1, -1, 0, -1, 0, 0, 0, -1, 1, 1, 0, 0, -1, 1, 0, -1),
+ ....: (0, 1, 1, 0, 0, 1, 2, 0, -1, 2, 0, 0, 1, 0, 1, 2, 0, 0, 0, 0, -1, 1, 1, 0, 0, 0, 1),
+ ....: (0, 0, 0, 1, 1, 1, 1, 0, 0, 0, -1, 1, 2, 2, 2, 1, 1, 0, 1, -1, 0, 1, 0, 1, 0, 0, 0),
+ ....: (0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 2, 1, 0, 0, 2, 1, 1, 1, 0, 0, 0, 0, 1, 0)]
+ sage: f = VeeringTriangulationLinearFamily(vt, subspace)
+ sage: f.rank()
+ 2
+
+ An eigenform locus in Q_1(2, 1, -1^3)::
+
+ sage: f = VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,3)(~2,4,5)(~3,6,7)(~4,8,9)(~5,~7,10)(~9,~10,11)", "RRBBBRRBRRBR", [(1, 0, -1, -1, 0, -1, -2, -3, 0, 0, -2, 2), (0, 1, 1, 1, 0, 1, 2, 3, 2, 2, 2, 0), (0, 0, 0, 0, 1, -1, -2, -2, -2, -1, -1, 0)])
+ sage: f.rank()
+ 1
+
+ TESTS:
+
+ These Abelian examples used to be wrong::
+
+ sage: VeeringTriangulation("(0,~5,4)(~0,~2,3)(1,2,5)(~1,~3,~4)", "RRBBRB").rank()
+ 1
+ sage: VeeringTriangulation("(0,6,~5)(~0,~4,5)(1,3,8)(~1,~6,~7)(2,~8,7)(~2,~3,4)", "RRRBBBBRB").rank()
+ 2
+ sage: VeeringTriangulation("(0,3,2)(~0,4,7)(1,~2,~7)(~1,~3,~6)(~4,~5,~8)(5,8,6)", "BBRRRBRRR").rank()
+ 2
+ sage: VeeringTriangulation("(0,~5,~4)(~0,7,9)(1,5,~7)(~1,10,~9)(2,11,~6)(~2,~11,8)(3,~8,~10)(~3,6,4)", "RRBBBRRBRRBR").rank()
+ 2
+ sage: VeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,7,8)(~3,10,9)(~4,11,~8)(~5,12,13)(~6,14,15)(~7,~9,~14)(~10,16,~11)(~12,17,18)(~13,19,20)(~15,~18,~20)(~16,~17,~19)", "BRRRRRBRBBBRBRRRRRBBR").rank()
+ 4
+ """
+ absolute_dimension = self.dimension() - self.relative_dimension()
+ if absolute_dimension % 2:
+ raise ValueError("odd-dimensional intersection with absolute cohomology")
+ return absolute_dimension // 2
+
def _set_train_track_constraints_fast(self, cs, L, slope):
zero = L.base_ring().zero()
one = L.base_ring().one()
m_one = -one
ne = self.num_edges()
- ep = self._ep
if slope == VERTICAL:
- LAR = PURPLE
- POS = BLUE
- NEG = RED
- ZERO = GREEN
shift = 0
elif slope == HORIZONTAL:
- LAR = GREEN
- POS = RED
- NEG = BLUE
- ZERO = PURPLE
shift = ne
else:
- raise ValueError('bad slope parameter')
+ raise ValueError("invalid slope parameter; must be VERTICAL or HORIZONTAL")
# switch
- for (i, j, k) in self.triangles():
- i = self._norm(i)
- ci = self._colouring[i]
- j = self._norm(j)
- cj = self._colouring[j]
- k = self._norm(k)
- ck = self._colouring[k]
-
- if ci == ZERO and cj == NEG and ck == POS:
- # i is degenerate
- cs.insert(L.element_class(L, {shift + i: one}, zero) == zero, check=False)
- if i != k:
- cs.insert(L.element_class(L, {shift + i: one, shift + k: m_one}, zero) == zero, check=False)
- elif cj == ZERO and ck == NEG and ci == POS:
- # j is degenerate
- cs.insert(L.element_class(L, {shift + j: one}, zero) == zero, check=False)
- if i != k:
- cs.insert(L.element_class(L, {shift + k: one, shift + i: m_one}, zero) == zero, check=False)
- elif ck == ZERO and ci == NEG and cj == POS:
- # k is degenerate
- cs.insert(L.element_class(L, {shift + k: one}, zero) == zero, check=False)
- if i != j:
- cs.insert(L.element_class(L, {shift + i: one, shift + j: m_one}, zero) == zero, check=False)
- elif ck == LAR or (ci == POS and cj == NEG):
- # k is large
- cs.insert(L.element_class(L, {shift + k: one, shift + i: m_one, shift + j: m_one}, zero) == zero, check=False)
- elif ci == LAR or (cj == POS and ck == NEG):
- # i is large
- cs.insert(L.element_class(L, {shift + i: one, shift + j: m_one, shift + k: m_one}, zero) == zero, check=False)
- elif cj == LAR or (ck == POS and ci == NEG):
- # j is large
- cs.insert(L.element_class(L, {shift + j: one, shift + k: m_one, shift + i: m_one}, zero) == zero, check=False)
- else:
- raise ValueError('can not determine the nature of triangle (%s, %s, %s) with colors (%s, %s, %s) in %s direction' %
- (self._edge_rep(i), self._edge_rep(j), self._edge_rep(k),
- colour_to_string(ci), colour_to_string(cj), colour_to_string(ck),
- 'horizontal' if slope == HORIZONTAL else 'vertical'))
+ for (i, j, k) in self.degenerate_triangles(slope):
+ # i is degenerate
+ # x[i] = 0
+ # x[j] - x[k] = 0
+ cs.insert(L.element_class(L, {shift + i // 2: one}, zero) == zero, check=False)
+ cs.insert(L.element_class(L, {shift + j // 2: one, shift + k // 2: m_one}, zero) == zero, check=False)
+
+ for (i, j, k) in self.non_degenerate_triangles(slope):
+ # i is large
+ # x[i] - x[j] - x[k] == 0
+ cs.insert(L.element_class(L, {shift + i // 2: one, shift + j // 2: m_one, shift + k // 2: m_one}, zero) == zero, check=False)
# non-negativity
for e in range(ne):
- if ep[e] != e and ep[e] < ne:
- raise ValueError('edge permutation not in standard form')
- if self._colouring[e] != ZERO:
- cs.insert(L.element_class(L, {shift + e: one}, zero) >= zero, check=False)
+ cs.insert(L.element_class(L, {shift + e: one}, zero) >= zero, check=False)
- def _set_switch_conditions(self, insert, x, slope=VERTICAL):
+ def _set_subspace_constraints(self, insert, x, slope=VERTICAL):
r"""
- These are the linear parts of the train-track equations
+ Set the linear parts of the train-track equations
INPUT:
@@ -2644,68 +3275,42 @@ def _set_switch_conditions(self, insert, x, slope=VERTICAL):
sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~8,~0,~7)(~6,~1,~5)(~4,~2,~3)", "RRBRRBRRB")
sage: cs = ppl.Constraint_System()
sage: x = [ppl.Variable(e) for e in range(vt.num_edges())]
- sage: vt._set_switch_conditions(cs.insert, x)
+ sage: vt._set_subspace_constraints(cs.insert, x)
sage: for g in cs:
....: print(vector(ZZ, g.coefficients()))
(1, -1, 1, 0, 0, 0, 0, 0, 0)
- (0, 0, 0, 1, -1, 1, 0, 0, 0)
- (0, 0, 0, 0, 0, 0, 1, -1, 1)
(1, 0, 0, 0, 0, 0, 0, -1, 1)
(0, 1, 0, 0, 0, -1, -1, 0, 0)
(0, 0, 1, 1, -1, 0, 0, 0, 0)
+ (0, 0, 0, 1, -1, 1, 0, 0, 0)
+ (0, 0, 0, 0, 0, 0, 1, -1, 1)
"""
- if slope == VERTICAL:
- LAR = PURPLE
- POS = BLUE
- NEG = RED
- ZERO = GREEN
- elif slope == HORIZONTAL:
- LAR = GREEN
- POS = RED
- NEG = BLUE
- ZERO = PURPLE
- else:
- raise ValueError('bad slope parameter')
+ for (i, j, k) in self.degenerate_triangles(slope):
+ # i is degenerate
+ insert(x[i // 2] == 0)
+ insert(x[j // 2] == x[k // 2])
- for (i,j,k) in self.triangles():
- i = self._norm(i)
- ci = self._colouring[i]
- j = self._norm(j)
- cj = self._colouring[j]
- k = self._norm(k)
- ck = self._colouring[k]
+ for (i, j, k) in self.non_degenerate_triangles(slope):
+ # i is large
+ insert(x[i // 2] == x[j // 2] + x[k // 2])
- if ci == ZERO and cj == NEG and ck == POS:
- # i is degenerate
- insert(x[i] == 0)
- insert(x[j] == x[k])
- elif cj == ZERO and ck == NEG and ci == POS:
- # j is degenerate
- insert(x[j] == 0)
- insert(x[k] == x[i])
- elif ck == ZERO and ci == NEG and cj == POS:
- # k is degenerate
- insert(x[k] == 0)
- insert(x[i] == x[j])
- elif ck == LAR or (ci == POS and cj == NEG):
- # k is large
- insert(x[k] == x[i] + x[j])
- elif ci == LAR or (cj == POS and ck == NEG):
- # i is large
- insert(x[i] == x[j] + x[k])
- elif cj == LAR or (ck == POS and ci == NEG):
- # j is large
- insert(x[j] == x[k] + x[i])
- else:
- raise ValueError('can not determine the nature of triangle (%s, %s, %s) with colors (%s, %s, %s) in %s direction' %
- (self._edge_rep(i), self._edge_rep(j), self._edge_rep(k),
- colour_to_string(ci), colour_to_string(cj), colour_to_string(ck),
- 'horizontal' if slope == HORIZONTAL else 'vertical'))
+ def _set_subspace_constraints_fast(self, cs, L, slope, shift):
+ zero = L.base_ring().zero()
+ one = L.base_ring().one()
+ m_one = -one
+ for (i, j, k) in self.degenerate_triangles(slope):
+ # is is degenerate
+ cs.insert(LinearConstraint(op_EQ, L.element_class(L, {shift + i // 2: one}, zero)), check=False)
+ cs.insert(LinearConstraint(op_EQ, L.element_class(L, {shift + j // 2: one, k // 2: m_one}, zero)), check=False)
+
+ for (i, j, k) in self.non_degenerate_triangles(slope):
+ # is is large
+ cs.insert(LinearConstraint(op_EQ, L.element_class(L, {shift + i // 2: one, shift + j // 2: m_one, shift + k // 2: m_one}, zero)), check=False)
@staticmethod
- def _tt_check(x):
+ def _constraint_check(x, error=AssertionError):
if not x:
- raise AssertionError("does not satisfy train-track constraints")
+ raise error("does not satisfy train-track constraints")
def train_track_switch_constraints(self, slope=VERTICAL):
r"""
@@ -2719,7 +3324,7 @@ def train_track_switch_constraints(self, slope=VERTICAL):
sage: vt = VeeringTriangulation(fp, cols)
sage: cs = vt.train_track_switch_constraints()
sage: cs
- {-1*x0 - x3 + x8 == 0, -1*x1 + x4 - x7 == 0, -1*x2 - x3 + x4 == 0, -1*x0 - x5 + x6 == 0}
+ {-1*x0 - x3 + x8 == 0, -1*x0 - x5 + x6 == 0, -1*x1 + x4 - x7 == 0, -1*x2 - x3 + x4 == 0}
"""
from sage.rings.integer_ring import ZZ
from .polyhedron.linear_expression import LinearExpressions
@@ -2727,7 +3332,7 @@ def train_track_switch_constraints(self, slope=VERTICAL):
L = LinearExpressions(ZZ)
cs = ConstraintSystem(ne)
variables = [L.variable(e) for e in range(ne)]
- self._set_switch_conditions(cs.insert, variables, slope)
+ self._set_subspace_constraints(cs.insert, variables, slope)
return cs
def _set_train_track_constraints(self, insert, x, slope, low_bound, allow_degenerations):
@@ -2781,8 +3386,8 @@ def _set_train_track_constraints(self, insert, x, slope, low_bound, allow_degene
This can also be used to check that a given vector satisfies the train-track
equations::
- sage: T._set_train_track_constraints(T._tt_check, [2,1,1], HORIZONTAL, False, False)
- sage: T._set_train_track_constraints(T._tt_check, [1,1,1], HORIZONTAL, False, False)
+ sage: T._set_train_track_constraints(T._constraint_check, [2,1,1], HORIZONTAL, False, False)
+ sage: T._set_train_track_constraints(T._constraint_check, [1,1,1], HORIZONTAL, False, False)
Traceback (most recent call last):
...
AssertionError: does not satisfy train-track constraints
@@ -2790,8 +3395,8 @@ def _set_train_track_constraints(self, insert, x, slope, low_bound, allow_degene
Check equations with folded edges (that are "counted twice")::
sage: T = VeeringTriangulation("(0,2,3)(~0,1,4)(~1,5,6)", [BLUE, RED, RED, BLUE, BLUE, BLUE, BLUE])
- sage: T._set_train_track_constraints(T._tt_check, [0,1,1,1,1,1,0], VERTICAL, False, False)
- sage: T._set_train_track_constraints(T._tt_check, [1,2,3,4,3,7,5], VERTICAL, False, False)
+ sage: T._set_train_track_constraints(T._constraint_check, [0,1,1,1,1,1,0], VERTICAL, False, False)
+ sage: T._set_train_track_constraints(T._constraint_check, [1,2,3,4,3,7,5], VERTICAL, False, False)
"""
if slope == VERTICAL:
POS = BLUE
@@ -2809,12 +3414,10 @@ def _set_train_track_constraints(self, insert, x, slope, low_bound, allow_degene
ep = self._ep
# switch
- self._set_switch_conditions(insert, x, slope)
+ self._set_subspace_constraints(insert, x, slope)
# non-negativity
for e in range(ne):
- if ep[e] != e and ep[e] < ne:
- raise ValueError('edge permutation not in standard form')
if self._colouring[e] == ZERO:
# already done in switch conditions: insert(x[e] == 0)
pass
@@ -2828,23 +3431,23 @@ def _set_train_track_constraints(self, insert, x, slope, low_bound, allow_degene
def _set_delaunay_constraints_fast(self, cs, L):
zero = L.base_ring().zero()
+ one = L.base_ring().one()
+ minus_one = -one
ne = self.num_edges()
for e in self.forward_flippable_edges():
- a, _, _, d = self.square_about_edge(e, check=False)
- a = self._norm(a)
- d = self._norm(d)
- e = self._norm(e)
+ a, _, _, d = self.square_about_half_edge(2 * e, check=False)
+ a = a // 2
+ d = d // 2
# y[a] + y[d] - x[e] >= 0
- l = L.element_class(L, {ne + a: 1, ne + d: 1, e: -1}, zero)
- cs.insert(l >= 0, check=False)
+ l = L.element_class(L, {ne + a: one, ne + d: one, e: minus_one}, zero)
+ cs.insert(LinearConstraint(op_GE, l), check=False)
for e in self.backward_flippable_edges():
- a, _, _, d = self.square_about_edge(e, check=False)
- a = self._norm(a)
- d = self._norm(d)
- e = self._norm(e)
+ a, _, _, d = self.square_about_half_edge(2 * e, check=False)
+ a = a // 2
+ d = d // 2
# x[a] + x[d] - y[e] >= 0
- l = L.element_class(L, {a: 1, d: 1, ne + e: -1}, zero)
- cs.insert(l >= 0, check=False)
+ l = L.element_class(L, {a: one, d: one, ne + e: minus_one}, zero)
+ cs.insert(LinearConstraint(op_GE, l), check=False)
def _set_delaunay_constraints(self, insert, x, y, hw_bound=0):
r"""
@@ -2873,11 +3476,15 @@ def _set_delaunay_constraints(self, insert, x, y, hw_bound=0):
"""
hw_bound = max(0, int(hw_bound))
for e in self.forward_flippable_edges():
- a, b, c, d = self.square_about_edge(e, check=False)
- insert(x[self._norm(e)] <= y[self._norm(a)] + y[self._norm(d)] - hw_bound)
+ a, _, _, d = self.square_about_half_edge(2 * e, check=False)
+ a //= 2
+ d //= 2
+ insert(x[e] <= y[a] + y[d] - hw_bound)
for e in self.backward_flippable_edges():
- a, b, c, d = self.square_about_edge(e, check=False)
- insert(y[self._norm(e)] <= x[self._norm(a)] + x[self._norm(d)] - hw_bound)
+ a, _, _, d = self.square_about_half_edge(2 * e, check=False)
+ a //= 2
+ d //= 2
+ insert(y[e] <= x[a] + x[d] - hw_bound)
def _set_balance_constraints(self, insert, x, slope, homogeneous):
r"""
@@ -2886,150 +3493,217 @@ def _set_balance_constraints(self, insert, x, slope, homogeneous):
if homogeneous:
if slope == VERTICAL:
for eff, ebf in itertools.product(self.forward_flippable_edges(), self.backward_flippable_edges()):
- a, b, c, d = self.square_about_edge(ebf, check=False)
- insert(x[self._norm(b)] + x[self._norm(c)] >= x[self._norm(eff)])
+ a, b, c, d = self.square_about_half_edge(2 * ebf, check=False)
+ insert(x[b // 2] + x[c // 2] >= x[eff])
elif slope == HORIZONTAL:
for eff, ebf in itertools.product(self.forward_flippable_edges(), self.backward_flippable_edges()):
- a, b, c, d = self.square_about_edge(eff, check=False)
- insert(x[self._norm(b)] + x[self._norm(c)] >= x[self._norm(ebf)])
+ a, b, c, d = self.square_about_half_edge(2 * eff, check=False)
+ insert(x[b // 2] + x[c // 2] >= x[ebf])
else:
raise ValueError("slope must be HORIZONTAL or VERTICAL")
else:
if slope == VERTICAL:
for e in self.forward_flippable_edges():
- insert(x[self._norm(e)] <= 1)
+ insert(x[e] <= 1)
for e in self.backward_flippable_edges():
- a, b, c, d = self.square_about_edge(e, check=False)
- insert(x[self._norm(b)] + x[self._norm(c)] >= 1)
+ a, b, c, d = self.square_about_half_edge(2 * e, check=False)
+ insert(x[b // 2] + x[c // 2] >= 1)
elif slope == HORIZONTAL:
for e in self.forward_flippable_edges():
- a, b, c, d = self.square_about_edge(e, check=False)
- insert(x[self._norm(b)] + x[self._norm(c)] >= 1)
+ a, b, c, d = self.square_about_half_edge(2 * e, check=False)
+ insert(x[b // 2] + x[c // 2] >= 1)
for e in self.backward_flippable_edges():
- insert(x[self._norm(e)] <= 1)
+ insert(x[e] <= 1)
else:
raise ValueError("slope must be HORIZONTAL or VERTICAL")
def train_track_linear_space(self, slope=VERTICAL, backend=None):
r"""
- Return the polytope determined by the switch equations (a linear subspace)
-
- INPUT:
-
- - ``slope`` - the slope for the train track (``HORIZONTAL`` or ``VERTICAL``)
+ Deprecated method
EXAMPLES::
sage: from veerer import *
sage: T = VeeringTriangulation("(0,1,2)(~2,~0,~1)", "RRB")
- sage: T.train_track_linear_space()
- Cone of dimension 2 in ambient dimension 3 made of 0 facets (backend=ppl)
- sage: T.train_track_linear_space(backend='sage')
- Cone of dimension 2 in ambient dimension 3 made of 0 facets (backend=sage)
+ sage: T.train_track_linear_space().lines()
+ doctest:warning
+ ...
+ UserWarning: train_track_linear_space is deprecated; use generators_matrix() instead
+ [[1, 1, 0], [1, 0, -1]]
+ sage: T.generators_matrix().rows()
+ [(1, 1, 0), (-1, 0, 1)]
+
+ sage: T.train_track_linear_space(HORIZONTAL).lines()
+ [[1, 1, 0], [1, 0, 1]]
+ sage: T.generators_matrix(HORIZONTAL).rows()
+ [(-1, -1, 0), (-1, 0, -1)]
"""
- from sage.rings.integer_ring import ZZ
- cs = ConstraintSystem()
- L = LinearExpressions(ZZ)
+ from warnings import warn
+ warn('train_track_linear_space is deprecated; use generators_matrix() instead')
+
ne = self.num_edges()
+ L = LinearExpressions(ZZ)
+ cs = ConstraintSystem(ne)
x = [L.variable(e) for e in range(ne)]
- self._set_switch_conditions(cs.insert, x, slope)
+ self._set_subspace_constraints(cs.insert, x, slope)
return cs.cone(backend)
def train_track_polytope(self, slope=VERTICAL, low_bound=0, backend=None):
r"""
- Return the polytope determined by the constraints.
+ Deprecated method.
+ """
+ if low_bound:
+ raise NotImplementedError
+
+ from warnings import warn
+ warn('train_track_polytope is deprecated; use cone instead')
+ return self.cone(slope=slope, backend=backend)
+
+ def cone_min(self, slope=VERTICAL, allow_degenerations=False):
+ r"""
+ Return the minimal integral point satisfying the constraints.
INPUT:
- - ``slope`` - the slope for the train track
+ - ``slope`` - the slope of the cone constraints
- - ``low_bound`` - integer - optional lower bound for the lengths
- (default to 0)
+ - ``allow_degenerations`` - boolean - if ``True`` then allow certain
+ degenerations to occur.
+
+ OUTPUT: a point from ppl
EXAMPLES::
sage: from veerer import *
sage: T = VeeringTriangulation([(0,1,2),(-1,-2,-3)], [RED, RED, BLUE])
- sage: P = T.train_track_polytope(VERTICAL)
+ sage: T.cone_min(VERTICAL)
+ (1, 2, 1)
+ sage: T.cone_min(VERTICAL, True)
+ (0, 1, 1)
+
+ sage: T.cone_min(HORIZONTAL)
+ (2, 1, 1)
+ sage: T.cone_min(HORIZONTAL, True)
+ (1, 0, 1)
+ """
+ R = self.base_ring()
+ if R is not ZZ and R is not QQ:
+ raise NotImplementedError
+
+ ne = self.num_edges()
+ M = ppl.MIP_Problem(ne)
+
+ x = [ppl.Variable(e) for e in range(ne)]
+ M.set_objective_function(-sum(x))
+ self._set_subspace_constraints(M.add_constraint, x, slope)
+ if allow_degenerations:
+ M.add_constraint(sum(x[e] for e in range(ne)) >= 1)
+ for e in range(ne):
+ M.add_constraint(x[e] >= 0)
+ else:
+ for e in range(ne):
+ M.add_constraint(x[e] >= 1)
+ return vector(R, M.optimizing_point().coefficients())
+
+ def train_track_min_solution(self, *args, **kwds):
+ r"""
+ Deprecated method.
+
+ TESTS::
+
+ sage: from veerer import *
+
+ sage: T = VeeringTriangulation([(0,1,2),(-1,-2,-3)], [RED, RED, BLUE])
+ sage: T.train_track_min_solution(VERTICAL)
+ doctest:warning
+ ...
+ UserWarning: train_track_min_solution is deprecated; use cone_min instead
+ (1, 2, 1)
+ sage: T.train_track_min_solution(VERTICAL, allow_degenerations=True)
+ (0, 1, 1)
+
+ sage: T.train_track_min_solution(HORIZONTAL)
+ (2, 1, 1)
+ sage: T.train_track_min_solution(HORIZONTAL, allow_degenerations=True)
+ (1, 0, 1)
+ """
+ from warnings import warn
+ warn('train_track_min_solution is deprecated; use cone_min instead')
+ return self.cone_min(*args, **kwds)
+
+ def cone(self, slope=VERTICAL, backend=None):
+ r"""
+ Return the cone of coordinates for the given ``slope``.
+
+ EXAMPLES::
+
+ sage: from veerer import *
+
+ sage: T = VeeringTriangulation([(0,1,2),(-1,-2,-3)], [RED, RED, BLUE])
+ sage: P = T.cone(VERTICAL)
sage: P
Cone of dimension 2 in ambient dimension 3 made of 2 facets (backend=ppl)
sage: sorted(P.rays())
[[0, 1, 1], [1, 1, 0]]
- sage: P = T.train_track_polytope(VERTICAL, low_bound=3) # not tested
+ sage: P = T.cone(VERTICAL, low_bound=3) # not tested
sage: P.generators() # not tested
Generator_System {ray(1, 1, 0), ray(0, 1, 1), point(3/1, 6/1, 3/1)}
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [GREEN, RED, BLUE])
- sage: sorted(T.train_track_polytope(VERTICAL).rays())
+ sage: sorted(T.cone(VERTICAL).rays())
[[0, 1, 1]]
- sage: sorted(T.train_track_polytope(HORIZONTAL).rays())
+ sage: sorted(T.cone(HORIZONTAL).rays())
[[1, 0, 1], [1, 1, 0]]
sage: T = VeeringTriangulation([(0,1,2), (-1,-2,-3)], [PURPLE, BLUE, RED])
- sage: sorted(T.train_track_polytope(VERTICAL).rays())
+ sage: sorted(T.cone(VERTICAL).rays())
[[1, 0, 1], [1, 1, 0]]
- sage: sorted(T.train_track_polytope(HORIZONTAL).rays())
+ sage: sorted(T.cone(HORIZONTAL).rays())
[[0, 1, 1]]
One can also use other backends::
- sage: sorted(T.train_track_polytope(VERTICAL, backend='sage').rays())
+ sage: sorted(T.cone(VERTICAL, backend='sage').rays())
[[1, 0, 1], [1, 1, 0]]
- sage: sorted(T.train_track_polytope(HORIZONTAL, backend='sage').rays())
+ sage: sorted(T.cone(HORIZONTAL, backend='sage').rays())
[[0, 1, 1]]
Examples with boundaries (meromorphic differentials)::
- sage: VeeringTriangulation("", "(0:1)(~0:1)", "R").train_track_polytope()
+ sage: VeeringTriangulation("", "(0:1)(~0:1)", "R").cone()
Cone of dimension 1 in ambient dimension 1 made of 1 facets (backend=ppl)
- sage: VeeringTriangulation("(0,1,2)(~1,~2,3)", "(~0:1)(~3:1)", "RBRR").train_track_polytope()
+ sage: VeeringTriangulation("(0,1,2)(~1,~2,3)", "(~0:1)(~3:1)", "RBRR").cone()
Cone of dimension 2 in ambient dimension 4 made of 2 facets (backend=ppl)
- """
- from sage.rings.integer_ring import ZZ
- L = LinearExpressions(ZZ)
- cs = ConstraintSystem()
- ne = self.num_edges()
- variables = [L.variable(e) for e in range(ne)]
- self._set_train_track_constraints(cs.insert, variables, slope, low_bound, False)
- return cs.cone(backend)
-
- def train_track_min_solution(self, slope=VERTICAL, allow_degenerations=False):
- r"""
- Return the minimal integral point satisfying the constraints.
-
- INPUT:
-
- - ``slope`` - the slope of the train track
-
- - ``allow_degenerations`` - boolean - if ``True`` then allow certain
- degenerations to occur.
-
- OUTPUT: a point from ppl
-
- EXAMPLES::
- sage: from veerer import *
+ Linear families::
- sage: T = VeeringTriangulation([(0,1,2),(-1,-2,-3)], [RED, RED, BLUE])
- sage: T.train_track_min_solution(VERTICAL)
- point(1/1, 2/1, 1/1)
- sage: T.train_track_min_solution(VERTICAL, allow_degenerations=True)
- point(0/1, 1/1, 1/1)
+ sage: vt, s, t = VeeringTriangulations.L_shaped_surface(1, 3, 1, 1)
+ sage: f = VeeringTriangulationLinearFamily(vt, [s, t])
+ sage: f.cone(VERTICAL)
+ Cone of dimension 2 in ambient dimension 7 made of 2 facets (backend=ppl)
+ sage: f.cone(HORIZONTAL)
+ Cone of dimension 2 in ambient dimension 7 made of 2 facets (backend=ppl)
- sage: T.train_track_min_solution(HORIZONTAL)
- point(2/1, 1/1, 1/1)
- sage: T.train_track_min_solution(HORIZONTAL, allow_degenerations=True)
- point(1/1, 0/1, 1/1)
+ sage: sorted(f.cone(VERTICAL).rays())
+ [[0, 1, 3, 3, 1, 1, 0], [1, 0, 0, 1, 1, 1, 1]]
+ sage: sorted(f.cone(HORIZONTAL).rays())
+ [[1, 0, 0, 1, 1, 1, 1], [3, 1, 3, 0, 2, 2, 3]]
"""
- n = self.num_edges()
- M = ppl.MIP_Problem(n)
+ R = self.base_ring()
+ L = LinearExpressions(R)
+ zero = R.zero()
+ one = R.one()
+ ne = self.num_edges()
+ cs = ConstraintSystem(ne)
- x = [ppl.Variable(e) for e in range(n)]
- M.set_objective_function(-sum(x))
- self._set_train_track_constraints(M.add_constraint, x, slope, 1, allow_degenerations)
- return M.optimizing_point()
+ # non-negativity
+ for e in range(ne):
+ cs.insert(LinearConstraint(op_GE, L.element_class(L, {e : one}, zero)), check=False)
+
+ self._set_subspace_constraints_fast(cs, L, slope, 0)
+ return cs.cone(backend)
def delaunay_cone(self, x_low_bound=0, y_low_bound=0, hw_bound=0, backend=None):
r"""
@@ -3044,28 +3718,108 @@ def delaunay_cone(self, x_low_bound=0, y_low_bound=0, hw_bound=0, backend=None):
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
sage: T.delaunay_cone()
- Cone of dimension 4 in ambient dimension 6 made of 6 facets (backend=ppl)
+ 4-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB") made of
+ 1 forward-flip facets
+ 1 backward-flip facets
+ 2 x-degeneration facets
+ 2 y-degeneration facets
sage: T.delaunay_cone(x_low_bound=1, y_low_bound=1, hw_bound=1) # not tested
sage: T.delaunay_cone(backend='sage')
- Cone of dimension 4 in ambient dimension 6 made of 6 facets (backend=sage)
+ 4-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB") made of
+ 1 forward-flip facets
+ 1 backward-flip facets
+ 2 x-degeneration facets
+ 2 y-degeneration facets
+
+ sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
+ sage: T.delaunay_cone()
+ 4-dimensional Delaunay cone of VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB") made of
+ 1 forward-flip facets
+ 1 backward-flip facets
+ 2 x-degeneration facets
+ 2 y-degeneration facets
+ sage: T.as_linear_family().delaunay_cone(backend='ppl')
+ 4-dimensional Delaunay cone of VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)]) made of
+ 1 forward-flip facets
+ 1 backward-flip facets
+ 2 x-degeneration facets
+ 2 y-degeneration facets
+ sage: T.as_linear_family().delaunay_cone(backend='sage')
+ 4-dimensional Delaunay cone of VeeringTriangulationLinearFamily("(0,1,2)(~0,~1,~2)", "RRB", [(1, 0, -1), (0, 1, 1)]) made of
+ 1 forward-flip facets
+ 1 backward-flip facets
+ 2 x-degeneration facets
+ 2 y-degeneration facets
+
+ An example in genus 2 involving a linear constraint::
+
+ sage: vt, s, t = VeeringTriangulations.L_shaped_surface(1, 1, 1, 1)
+ sage: f = VeeringTriangulationLinearFamily(vt, [s, t])
+ sage: PG = f.delaunay_cone(backend='ppl')
+ sage: PG
+ 4-dimensional Delaunay cone of VeeringTriangulationLinearFamily("(0,2,3)(~0,1,4)(~1,5,6)", "BRRBBBB", [(1, 0, 0, 1, 1, 1, 1), (0, 1, 1, 1, 1, 1, 0)]) made of
+ 1 forward-flip facets
+ 1 backward-flip facets
+ 2 x-degeneration facets
+ 2 y-degeneration facets
+ sage: sorted(PG.rays())
+ [(0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1),
+ (0, 1, 1, 1, 1, 1, 0, 2, 0, 0, 2, 2, 2, 2),
+ (0, 1, 1, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 2),
+ (0, 2, 2, 2, 2, 2, 0, 1, 1, 1, 0, 0, 0, 1),
+ (1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1),
+ (1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1),
+ (2, 0, 0, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 1)]
"""
if x_low_bound or y_low_bound or hw_bound:
raise NotImplementedError
- L = LinearExpressions(ZZ)
+ try:
+ return self._delaunay_cone[backend]
+ except (AttributeError, KeyError):
+ pass
+
+ R = self.base_ring()
+ L = LinearExpressions(R)
+ zero = R.zero()
+ one = R.one()
ne = self.num_edges()
- cs = ConstraintSystem()
- self._set_train_track_constraints_fast(cs, L, VERTICAL)
- self._set_train_track_constraints_fast(cs, L, HORIZONTAL)
+ cs = ConstraintSystem(2 * ne)
+
+ # non-negativity
+ for i in range(2 * ne):
+ cs.insert(LinearConstraint(op_GE, L.element_class(L, {i : one}, zero)), check=False)
+
self._set_delaunay_constraints_fast(cs, L)
- return cs.cone(backend)
+ self._set_subspace_constraints_fast(cs, L, VERTICAL, 0)
+ self._set_subspace_constraints_fast(cs, L, HORIZONTAL, ne)
+ from .delaunay_cone import DelaunayCone
+ delaunay_cone = DelaunayCone(self.copy(mutable=False), cs.cone(backend))
+ if not self._mutable:
+ try:
+ cache = self._delaunay_cone
+ except AttributeError:
+ cache = self._delaunay_cone = {}
+ self._delaunay_cone[backend] = delaunay_cone
+ return delaunay_cone
def geometric_polytope(self, *args, **kwds):
from warnings import warn
warn('geometric_polytope is deprecated; use delaunay_cone instead')
return self.delaunay_cone(*args, **kwds)
+ def linear_subvariety(self):
+ r"""
+ Return the linear subvariety generated by this family.
+
+ Note that if the family is not prime, its decomposition into prime
+ components is computed.
+ """
+ from .linear_subvariety import IrreducibleRealLinearSubvariety
+ DS = [component.delaunay_strebel_automaton()._graph for atom, component in self.prime_decomposition()]
+ return IrreducibleRealLinearSubvariety(DS)
+
def delaunay_automaton(self, run=True, backward=None, backend=None):
r"""
Return the Delaunay automaton containing this veering triangulation or family.
@@ -3087,12 +3841,11 @@ def delaunay_automaton(self, run=True, backward=None, backend=None):
sage: from veerer import VeeringTriangulation
-
sage: fp = "(0,~7,6)(1,~8,~2)(2,~6,~3)(3,5,~4)(4,8,~5)(7,~1,~0)"
sage: cols = "RBRBRBBBB"
sage: vt = VeeringTriangulation(fp, cols)
sage: vt.delaunay_automaton()
- Delaunay automaton with 54 vertices
+ Delaunay automaton with 54 states
Meromorphic example (with a non strongly connected automaton)::
@@ -3101,9 +3854,9 @@ def delaunay_automaton(self, run=True, backward=None, backend=None):
sage: cols = "RBRR"
sage: vt = VeeringTriangulation(fp, bdry, cols)
sage: vt.delaunay_automaton()
- Delaunay automaton with 3 vertices
+ Delaunay automaton with 3 states
sage: vt.delaunay_automaton(backward=False)
- Delaunay automaton with 1 vertex
+ Delaunay automaton with 1 state
"""
from .automaton import DelaunayAutomaton
if backward is None:
@@ -3122,7 +3875,7 @@ def geometric_automaton(self, *args, **kwds):
warn('geometric_automaton is deprecated; use delaunay_automaton instead')
return self.delaunay_automaton(self, *args, **kwds)
- def delaunay_strebel_automaton(self, run=True, backward=None, backend=None):
+ def delaunay_strebel_automaton(self, run=True, backward=None, verbosity=0, backend=None):
r"""
Return the Delaunay-Strebel automaton containing this veering triangulation.
@@ -3148,17 +3901,34 @@ def delaunay_strebel_automaton(self, run=True, backward=None, backend=None):
sage: cols = "RBRR"
sage: vt = VeeringTriangulation(fp, bdry, cols)
sage: vt.delaunay_strebel_automaton()
- Delaunay-Strebel automaton with 11 vertices
+ Delaunay-Strebel automaton with 10 states
"""
from .automaton import DelaunayStrebelAutomaton
if backward is None:
backward = any(self._bdry)
- A = DelaunayStrebelAutomaton(backward=backward, backend=backend)
+ A = DelaunayStrebelAutomaton(backward=backward, verbosity=verbosity, backend=backend)
A.add_seed(self)
if run:
A.run()
return A
+ def delaunay_strebel_graph(self):
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,2,1)(~0,3,~1)(~2:2,~3:2)", "RBRR")
+ sage: vt.delaunay_strebel_graph()
+ Delaunay-Strebel graph of VeeringTriangulation("(0:1,1:1,~0:1,~1:1)", "RB") made of
+ 9 veering Delaunay states
+ 1 Strebel states
+ 8 flip transitions
+ 5 rotation transitions
+ 5 Strebel transitions
+ """
+ from .delaunay_strebel_graph import DelaunayStrebelGraph
+ return DelaunayStrebelGraph(self.delaunay_strebel_automaton()._graph)
+
def _complexify_generators(self, Gx):
r"""
Given ``Gx`` a matrix whose rows are admissible lengths return the
@@ -3171,27 +3941,23 @@ def _complexify_generators(self, Gx):
sage: T, s, t = VeeringTriangulations.L_shaped_surface(1, 1, 1, 1)
sage: Gx = matrix(QQ, 2, [s, t])
sage: Gy = T._complexify_generators(Gx)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(0), VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(1), VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, Gy.row(0), HORIZONTAL)
- sage: T._set_switch_conditions(T._tt_check, Gy.row(1), HORIZONTAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(0), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(1), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gy.row(0), HORIZONTAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gy.row(1), HORIZONTAL)
sage: T, s, t = VeeringTriangulations.L_shaped_surface(2, 3, 4, 5, 1, 2)
sage: Gx = matrix(QQ, 2, [s, t])
sage: Gy = T._complexify_generators(Gx)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(0), VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, Gx.row(1), VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, Gy.row(0), HORIZONTAL)
- sage: T._set_switch_conditions(T._tt_check, Gy.row(1), HORIZONTAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(0), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gx.row(1), VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gy.row(0), HORIZONTAL)
+ sage: T._set_subspace_constraints(T._constraint_check, Gy.row(1), HORIZONTAL)
"""
- ne = self.num_edges()
- ep = self._ep
- if Gx.ncols() != ne:
+ if Gx.ncols() != self._ne:
raise ValueError
Gy = Gx.__copy__()
- for j in range(ne):
- if ep[j] < j:
- raise ValueError("triangulation not in standard form")
+ for j in range(self._ne):
if self._colouring[j] == BLUE:
for i in range(Gy.nrows()):
Gy[i, j] *= -1
@@ -3224,60 +3990,25 @@ def _complexify_equations(self, Lx):
sage: V2 = V.subspace(Gy.right_kernel_matrix())
sage: assert V1 == V2
"""
- ne = self.num_edges()
- ep = self._ep
- if Lx.ncols() != ne:
+ if Lx.ncols() != self._ne:
raise ValueError
Ly = Lx.__copy__()
- for j in range(ne):
- if ep[j] < j:
- raise ValueError("triangulation not in standard form")
+ for j in range(self._ne):
if self._colouring[j] == BLUE:
for i in range(Ly.nrows()):
Ly[i, j] *= -1
return Ly
- def _flat_structure_from_train_track_lengths(self, VH, VV, base_ring=None, mutable=False, check=True):
+ def flat_structure(self, x, y, mutable=False, check=True):
r"""
- Return a flat structure from two vectors ``VH`` and ``VV``
- satisfying the train track equations.
+ Return a flat structure from two coordinates ``x`` and ``y``.
"""
- from sage.modules.free_module import FreeModule
-
- if base_ring is None:
- base_ring = self.base_ring()
-
- assert len(VH) == len(VV) == self.num_edges()
- assert all(x >=0 for x in VH)
- assert all(x >= 0 for x in VV)
-
- self._set_train_track_constraints(self._tt_check, VH, HORIZONTAL, False, False)
- self._set_train_track_constraints(self._tt_check, VV, VERTICAL, False, False)
-
- V = FreeModule(base_ring, 2)
- vectors = [V((x, y if self._colouring[i] == RED else -y)) for \
- i,(x,y) in enumerate(zip(VV, VH))]
- m = self.num_edges()
- n = self.num_half_edges()
- ep = self._ep
- vectors.extend(vectors[ep[e]] for e in range(m,n))
-
- # get correct signs for each triangle
- for i,j,k in self.faces():
- if det2(vectors[i], vectors[j]) < 0:
- vectors[j] = -vectors[j]
- if det2(vectors[j], vectors[k]) < 0:
- vectors[k] = -vectors[k]
- if vectors[i] + vectors[j] + vectors[k]:
- raise RuntimeError('bad vectors for %s:\n vec[%s] = %s\n vec[%s] = %s\n vec[%s] = %s' \
- % (self.to_string(), self._edge_rep(i), vectors[i], self._edge_rep(j), \
- vectors[j], self._edge_rep(k), vectors[k]))
-
- if det2(vectors[k], vectors[i]) < 0:
- raise RuntimeError
+ if check:
+ self._set_train_track_constraints(self._constraint_check, x, VERTICAL, False, False)
+ self._set_train_track_constraints(self._constraint_check, y, HORIZONTAL, False, False)
from .flat_structure import FlatVeeringTriangulation
- return FlatVeeringTriangulation(self, vectors, mutable=mutable, check=check)
+ return FlatVeeringTriangulation(self, x, y, mutable=mutable, check=check)
def flat_structure_middle(self, backend=None):
r"""
@@ -3293,7 +4024,7 @@ def flat_structure_middle(self, backend=None):
sage: T = VeeringTriangulation("(0,1,2)", "RRB")
sage: T.flat_structure_middle()
- FlatVeeringTriangulation(Triangulation("(0,1,2)"), [(1, 2), (-2, -1), (1, -1)])
+ FlatVeeringTriangulation("(0,1,2)", "RRB", (1, 2, 1), (2, 1, 1))
sage: x = polygen(QQ)
sage: K = NumberField(x^2 - x - 1, 'c0', embedding=(1+AA(5).sqrt())/2)
@@ -3305,13 +4036,13 @@ def flat_structure_middle(self, backend=None):
....: (0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, c0 - 1, c0 - 1, c0 - 1, -c0 + 1)]
sage: F = VeeringTriangulationLinearFamily(T, deformations)
sage: F.flat_structure_middle()
- FlatVeeringTriangulation(Triangulation("(0,1,2)(3,4,~0)(5,6,~1)(7,8,~2)(9,~3,10)(11,~8,~4)(12,13,~5)(14,15,~6)(16,~11,~10)(17,18,~12)(19,20,~13)(~20,~15,~18)(~19,~16,~17)(~14,~7,~9)"), ...)
+ FlatVeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,7,8)(~3,10,9)(~4,11,~8)(~5,12,13)(~6,14,15)(~7,~9,~14)(~10,16,~11)(~12,17,18)(~13,19,20)(~15,~18,~20)(~16,~17,~19)", ...)
sage: from surface_dynamics import * # optional - surface_dynamics
sage: Q = Stratum({1:4, -1:4}, 2) # optional - surface_dynamics
sage: CT = VeeringTriangulation.from_stratum(Q) # optional - surface_dynamics
sage: CT.flat_structure_middle() # optional - surface_dynamics
- FlatVeeringTriangulation(Triangulation("(0,18,~17)(1,20,~19)...(19,~18,~0)"), [(3, 3), (1, 1), ..., (-3, -3)])
+ FlatVeeringTriangulation("(0,18,~17)(~0,19,~18)...(~7,~23,8)", ... 5, 4, 3, 2, 1))
TESTS::
@@ -3320,8 +4051,8 @@ def flat_structure_middle(self, backend=None):
"""
n = self.num_edges()
- PH = self.train_track_polytope(HORIZONTAL, backend=backend)
- PV = self.train_track_polytope(VERTICAL, backend=backend)
+ PH = self.cone(HORIZONTAL, backend=backend)
+ PV = self.cone(VERTICAL, backend=backend)
# pick sum of rays
VH = PH.rays()
@@ -3329,7 +4060,7 @@ def flat_structure_middle(self, backend=None):
VV = PV.rays()
VV = [sum(v[i] for v in VV) for i in range(n)]
- return self._flat_structure_from_train_track_lengths(VH, VV)
+ return self.flat_structure(VV, VH)
def flat_structure_min(self, allow_degenerations=False):
r"""
@@ -3346,31 +4077,50 @@ def flat_structure_min(self, allow_degenerations=False):
sage: Q = Stratum({1:4, -1:4}, 2) # optional - surface_dynamics
sage: CT = VeeringTriangulation.from_stratum(Q) # optional - surface_dynamics
sage: CT.flat_structure_min() # optional - surface_dynamics
- FlatVeeringTriangulation(Triangulation("(0,18,~17)(1,20,~19)...(19,~18,~0)"), [(3, 3), (1, 1), ..., (-3, -3)])
+ FlatVeeringTriangulation("(0,18,~17)(~0,19,~18)...(~7,~23,8)", ... 5, 4, 3, 2, 1))
By allowing degenerations you can get a simpler solution but
with some of the edges horizontal or vertical::
sage: F = CT.flat_structure_min(True) # optional - surface_dynamics
sage: F # optional - surface_dynamics
- FlatVeeringTriangulation(Triangulation("(0,18,~17)(1,20,~19)...(19,~18,~0)"), [(3, 3), (1, 1), ..., (-3, -3)])
- sage: F.to_veering_triangulation() # optional - surface_dynamics
- VeeringTriangulation("(0,18,~17)(1,20,~19)...(19,~18,~0)", "RRRRRRRRBBBBBBBBBGBBBBBP")
+ FlatVeeringTriangulation("(0,18,~17)(~0,19,~18)...(~7,~23,8)", ... 7, 4, 3, 2, 1, 0))
+ sage: F.constellation() # optional - surface_dynamics
+ VeeringTriangulation("(0,18,~17)(~0,19,~18)...(~7,~23,8)", "RRRRRRRRBBBBBBBBBBBBBBBB")
"""
- VH = self.train_track_min_solution(HORIZONTAL, allow_degenerations=allow_degenerations)
- VV = self.train_track_min_solution(VERTICAL, allow_degenerations=allow_degenerations)
+ x = self.cone_min(VERTICAL, allow_degenerations=allow_degenerations)
+ y = self.cone_min(HORIZONTAL, allow_degenerations=allow_degenerations)
+ return self.flat_structure(x, y)
- assert VH.is_point()
- assert VV.is_point()
-
- from sage.rings.rational import Rational
- assert VH.divisor() == 1
- VH = [Rational(c) for c in VH.coefficients()]
+ # TODO: examples
+ def flat_structure_cylinder(self):
+ r"""
+ Return a flat structure which makes all topological cylinders flat.
- assert VV.divisor() == 1
- VV = [Rational(c) for c in VV.coefficients()]
+ EXAMPLES::
- return self._flat_structure_from_train_track_lengths(VH, VV)
+ sage: from veerer import *
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "BBRRRBBRR")
+ sage: vt.flat_structure_cylinder()
+ FlatVeeringTriangulation("(0,1,2)(~0,~1,3)(~2,4,5)(~3,~4,6)(~5,7,8)(~6,~7,~8)", "BBRRRBBRR", (3, 1, 2, 2, 3, 1, 1, 1, 2), (1, 3, 2, 2, 1, 1, 1, 2, 1))
+ """
+ ne = self.num_edges()
+ M = ppl.MIP_Problem(2 * ne)
+
+ x = [ppl.Variable(e) for e in range(ne)]
+ y = [ppl.Variable(ne + e) for e in range(ne)]
+ # TODO: add linear subvariety constraints
+ self._set_subspace_constraints(M.add_constraint, x, VERTICAL)
+ self._set_subspace_constraints(M.add_constraint, y, HORIZONTAL)
+ self._set_train_track_constraints(M.add_constraint, x, VERTICAL, 1, False)
+ self._set_train_track_constraints(M.add_constraint, y, HORIZONTAL, 1, False)
+ for col in [BLUE, RED]:
+ for mid, lbdry, rbdry, folded in self.cylinders(col):
+ for h in lbdry + rbdry:
+ M.add_constraint(x[h // 2] == y[h // 2])
+
+ xy = [Rational(c) for c in M.optimizing_point().coefficients()]
+ return self.flat_structure(xy[:ne], xy[ne:])
def flat_structure_geometric_middle(self, backend=None):
r"""
@@ -3383,14 +4133,14 @@ def flat_structure_geometric_middle(self, backend=None):
sage: T = VeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB")
sage: T.flat_structure_geometric_middle()
- FlatVeeringTriangulation(Triangulation("(0,1,2)(~2,~0,~1)"), [(4, 9), (-9, -4), (5, -5), (-5, 5), (9, 4), (-4, -9)])
+ FlatVeeringTriangulation("(0,1,2)(~0,~1,~2)", "RRB", (4, 9, 5), (9, 4, 5))
"""
ne = self.num_edges()
r = self.delaunay_cone(backend=backend).rays()
VV = [sum(v[i] for v in r) for i in range(ne)]
VH = [sum(v[ne + i] for v in r) for i in range(ne)]
- return self._flat_structure_from_train_track_lengths(VH, VV)
+ return self.flat_structure(VV, VH)
def zippered_rectangles(self, x, y, base_ring=None, check=True):
r"""
@@ -3420,13 +4170,13 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
sage: x = [1, 2, 1]
sage: y = [1, 1, 2]
sage: vt.zippered_rectangles(x, y) # optional: sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,4)(~2,5,3)(~3,~4,~5)", "RBBRBR")
sage: x = [1,2,1,2,1,1]
sage: y = [1,1,2,1,2,3]
sage: vt.zippered_rectangles(x, y) # optional: sage_flatsurf
- Translation Surface built from 3 squares and a rectangle
+ Translation Surface in H_1(0^7) built from 3 squares and a rectangle
sage: vt = VeeringTriangulation("(0,~2,1)(2,~8,~3)(3,~7,~4)(4,6,~5)(5,8,~6)(7,~1,~0)", "PRBPRBPBR")
sage: R0, R1 = vt.dehn_twists(RED)
@@ -3435,7 +4185,7 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
sage: a, x, y = f.self_similar_widths_and_heights()
sage: S = vt.zippered_rectangles(x, y) # optional: sage_flatsurf
sage: S # optional: sage_flatsurf
- Translation Surface built from 6 rectangles
+ Translation Surface in H_2(2, 0^9) built from 6 rectangles
We now check that labelling of the rectangles in ``S`` coincide with
the order of faces in the veering triangulation::
@@ -3457,14 +4207,15 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
sage: x = [1, 2, 1]
sage: y = [1, 1, 2]
sage: vt.zippered_rectangles(x, y, base_ring=AA) # optional: sage_flatsurf
- Translation Surface built from a square and a rectangle
+ Translation Surface in H_1(0^3) built from a square and a rectangle
"""
ans, edge_orientations = self.is_abelian(certificate=True)
if not ans:
raise ValueError('the construction is only valid for Abelian differentials')
if check:
- x, y = self._check_xy(x, y)
+ if len(x) != self._ne or len(y) != self._ne:
+ raise ValueError("x and y must have the same length as the number of edges (got {} and {} instead of {})".format(len(x), len(y), self._ne))
if base_ring is None:
from sage.structure.sequence import Sequence
base_ring = Sequence(list(x) + list(y)).universe()
@@ -3499,8 +4250,8 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
# compute for each half-edge the label of the next separatrix
left_wedges = []
right_wedges = []
- next_separatrix = [-1] * self._n
- previous_separatrix = [-1] * self._n
+ next_separatrix = [-1] * (2 * self._ne)
+ previous_separatrix = [-1] * (2 * self._ne)
i = 0
for vertex in self.vertices():
# find a blue half-edge after a down separatrix, that is
@@ -3508,15 +4259,19 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
# edge_orientations[a] == False and edge_orientations[b] == True
a = vertex[0]
b = vertex[1]
+ cola = colouring[a // 2]
+ colb = colouring[b // 2]
while not edge_orientations[a] or edge_orientations[b]:
a = b
b = self.next_at_vertex(a)
- assert colouring[a] == RED and colouring[b] == BLUE, (a, colouring[a], colouring[b])
+ cola = colouring[a // 2]
+ colb = colouring[b // 2]
+ assert cola == RED and colb == BLUE, (a, cola, colb)
while previous_separatrix[b] == -1:
if i % 2 == 0:
- assert colouring[a] == RED and colouring[b] == BLUE, (i, a, colouring[a], b, colouring[b])
+ assert cola == RED and colb == BLUE, (i, a, cola, b, colb)
else:
- assert colouring[a] == BLUE and colouring[b] == RED, (i, a, colouring[a], b, colouring[b])
+ assert cola == BLUE and colb == RED, (i, a, cola, b, colb)
right_wedges.append(a)
left_wedges.append(b)
@@ -3524,20 +4279,27 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
b = self.next_at_vertex(a)
previous_separatrix[a] = i
next_separatrix[a] = i + 1
- while colouring[a] == colouring[b]:
+ cola = colouring[a // 2]
+ colb = colouring[b // 2]
+ while cola == colb:
previous_separatrix[b] = i
next_separatrix[b] = i + 1
a = b
b = self.next_at_vertex(a)
+ cola = colouring[a // 2]
+ colb = colouring[b // 2]
# colour change
i += 1
# correct the last quadrant
- assert colouring[a] == RED and colouring[b] == BLUE
+ cola = colouring[a // 2]
+ colb = colouring[b // 2]
+ assert cola == RED and colb == BLUE
j = previous_separatrix[b]
- while colouring[a] == RED:
+ while cola == RED:
next_separatrix[a] = j
a = self.previous_at_vertex(a)
+ cola = colouring[a // 2]
# check that we did a multiple of 2pi
assert i % 4 == 0
@@ -3545,15 +4307,15 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
assert all(x != -1 for x in next_separatrix)
assert all(x != -1 for x in previous_separatrix)
assert len(right_wedges) == len(left_wedges) == 2 * self.num_faces()
- assert all(colouring[right_wedges[i]] == RED for i in range(0, 2 * self.num_faces(), 2))
- assert all(colouring[right_wedges[i]] == BLUE for i in range(1, 2 * self.num_faces(), 2))
- assert all(colouring[left_wedges[i]] == BLUE for i in range(0, 2 * self.num_faces(), 2))
- assert all(colouring[left_wedges[i]] == RED for i in range(1, 2 * self.num_faces(), 2))
+ assert all(colouring[right_wedges[i] // 2] == RED for i in range(0, 2 * self.num_faces(), 2))
+ assert all(colouring[right_wedges[i] // 2] == BLUE for i in range(1, 2 * self.num_faces(), 2))
+ assert all(colouring[left_wedges[i] // 2] == BLUE for i in range(0, 2 * self.num_faces(), 2))
+ assert all(colouring[left_wedges[i] // 2] == RED for i in range(1, 2 * self.num_faces(), 2))
# In order to have a consistent labelling between the triangles as provided by self.faces()
# and the rectangles we compute the face index associated to each half-edge and use it
# later to order the rectangles
- half_edge_face_index = [-1] * self._n
+ half_edge_face_index = [-1] * (2 * self._ne)
for i, face in enumerate(self.faces()):
for e in face:
half_edge_face_index[e] = i
@@ -3565,11 +4327,11 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
rectangles = [None] * self.num_faces()
for i in range(1, 2 * self.num_faces(), 2):
l = left_wedges[i]
- L = ep[l]
+ L = ep(l)
r = right_wedges[i]
- R = ep[r]
+ R = ep(r)
e = self.next_in_face(r)
- E = ep[e]
+ E = ep(e)
assert self.next_in_face(e) == L
# The rectangles are always built starting from the bottom left corner
@@ -3585,41 +4347,41 @@ def zippered_rectangles(self, x, y, base_ring=None, check=True):
if i % 4 == 1:
# right rectangle
# bottom side
- p0 = (next_separatrix[R], RIGHT, x[r])
- if x[l] < x[r]:
+ p0 = (next_separatrix[R], RIGHT, x[r // 2])
+ if x[l // 2] < x[r // 2]:
# small case: x[l] = x[r] - x[e]
- p1 = (next_separatrix[R], RIGHT, x[e])
+ p1 = (next_separatrix[R], RIGHT, x[e // 2])
else:
# big case: x[l] = x[r] + x[e]
- p1 = (previous_separatrix[e], LEFT, x[e])
+ p1 = (previous_separatrix[e], LEFT, x[e // 2])
# right side
- p2 = (next_separatrix[L], RIGHT, y[e])
- p3 = (next_separatrix[L], RIGHT, y[l])
+ p2 = (next_separatrix[L], RIGHT, y[e // 2])
+ p3 = (next_separatrix[L], RIGHT, y[l // 2])
# top side
- p4 = (next_separatrix[r], RIGHT, x[l])
+ p4 = (next_separatrix[r], RIGHT, x[l // 2])
p5 = (next_separatrix[r], RIGHT, 0)
# left side
p6 = (previous_separatrix[r], LEFT, 0)
- p7 = (previous_separatrix[r], LEFT, y[r])
+ p7 = (previous_separatrix[r], LEFT, y[r // 2])
else:
# bottom side
- if x[r] < x[l]:
+ if x[r // 2] < x[l // 2]:
# small case: x[r] = x[l] - x[e]
- p0 = (previous_separatrix[L], LEFT, x[e])
+ p0 = (previous_separatrix[L], LEFT, x[e // 2])
else:
# big case: x[r] = x[l] + x[e]
- p0 = (next_separatrix[E], RIGHT, x[e])
- p1 = (previous_separatrix[L], LEFT, x[l])
+ p0 = (next_separatrix[E], RIGHT, x[e // 2])
+ p1 = (previous_separatrix[L], LEFT, x[l // 2])
# right side
- p2 = (next_separatrix[l], RIGHT, y[l])
+ p2 = (next_separatrix[l], RIGHT, y[l // 2])
p3 = (next_separatrix[l], RIGHT, 0)
# top side
p4 = (next_separatrix[r], LEFT, 0)
- p5 = (next_separatrix[r], LEFT, x[r])
+ p5 = (next_separatrix[r], LEFT, x[r // 2])
# left side
- p6 = (previous_separatrix[R], LEFT, y[r])
- p7 = (previous_separatrix[R], LEFT, y[e])
+ p6 = (previous_separatrix[R], LEFT, y[r // 2])
+ p7 = (previous_separatrix[R], LEFT, y[e // 2])
j = half_edge_face_index[r]
rectangles[j] = (p0, p1, p2, p3, p4, p5, p6, p7)
@@ -3685,8 +4447,8 @@ def is_core(self, backend=None):
# In theory LP should be much faster but in practice (in small dimensions)
# polytope is much better
d = self.dimension()
- return self.train_track_polytope(HORIZONTAL, backend=backend).affine_dimension() == d and \
- self.train_track_polytope(VERTICAL, backend=backend).affine_dimension() == d
+ return self.cone(VERTICAL, backend=backend).affine_dimension() == d and \
+ self.cone(HORIZONTAL, backend=backend).affine_dimension() == d
def is_delaunay(self, backend=None):
r"""
@@ -3739,7 +4501,7 @@ def is_geometric(self, *args, **kwds):
def balanced_polytope(self, slope=VERTICAL, homogeneous=False, backend=None):
r"""
- Return the set of balanced coordinates for this veering triangulation
+ Return the set of balanced coordinates for this veering triangulation<<
INPUT:
@@ -3772,7 +4534,7 @@ def is_balanced(self, slope=VERTICAL, backend=None):
sage: T.is_balanced() # not tested
False
- sage: T = VeeringTriangulation("(0,1,8)(2,~7,~1)(3,~0,~2)(4,~5,~3)(5,6,~4)(7,~8,~6)", "BRRRRBRBR")
+ sage: T = VeeringTriangulation("(0,1,8)(2,~7,~1)(3,~0,~2)(4,~5,~3)(5,6,~4)(7,~8,~6)", "BRRRRBRBR", mutable=True)
sage: T.is_balanced() # not tested
False
sage: T.rotate()
@@ -3783,12 +4545,8 @@ def is_balanced(self, slope=VERTICAL, backend=None):
def edge_has_curve(self, e, check=True, verbose=False):
r"""
- INPUT:
-
- - e - edge label
-
- OUTPUT: boolean - whether there is a curve which crosses the associated
- (dual) train-track in the correct direction.
+ Return whether there is a curve which crosses ``e`` in the associated
+ (dual) train-track.
EXAMPLES::
@@ -3818,17 +4576,17 @@ def edge_has_curve(self, e, check=True, verbose=False):
sage: T2.is_core()
False
- Equivantly, the train track polytope is degenerate::
+ Equivalently, the cone is degenerate::
- sage: P1 = T1.train_track_polytope(VERTICAL)
+ sage: P1 = T1.cone(VERTICAL)
sage: P1.affine_dimension()
3
- sage: P2 = T2.train_track_polytope(VERTICAL)
+ sage: P2 = T2.cone(VERTICAL)
sage: P2.affine_dimension()
2
"""
if check:
- e = self._check_half_edge(e)
+ e = self._check_edge(e)
# TODO: we should only search for vertex cycles; i.e. not allow more
# than two pairs (i, ~i) to be both seen (barbell are fine but not more)
@@ -3842,33 +4600,38 @@ def edge_has_curve(self, e, check=True, verbose=False):
if verbose:
print('[edge_has_curve] checking edge %s with colour %s' % (edge_rep(e), colouring[e]))
- a, b, c, d = self.square_about_edge(e, check=False)
- if colouring[a] == BLUE or colouring[b] == RED:
- assert colouring[c] == BLUE or colouring[d] == RED
+ a, b, c, d = self.square_about_half_edge(2 * e, check=False)
+ cola = colouring[a // 2]
+ colb = colouring[b // 2]
+ colc = colouring[c // 2]
+ cold = colouring[d // 2]
+ cole = colouring[e]
+ if cola == BLUE or colb == RED:
+ assert colc == BLUE or cold == RED
POS, NEG = BLUE, RED
if verbose:
print('[edge_has_curve] checking HORIZONTAL track')
else:
- assert colouring[a] == RED or colouring[b] == BLUE
- assert colouring[c] == RED or colouring[d] == BLUE
+ assert cola == RED or colb == BLUE
+ assert colc == RED or cold == BLUE
POS, NEG = RED, BLUE
if verbose:
print('[edge_has_curve] checking VERTICAL track')
# check alternating condition
- assert colouring[e] == BLUE or colouring[e] == RED
- assert colouring[a] != colouring[b], (a, b, colouring[a], colouring[b])
- assert colouring[c] != colouring[d], (c, d, colouring[c], colouring[d])
+ assert cole == BLUE or colouring[e] == RED
+ assert cola != colb
+ assert colc != cold
- if colouring[e] == NEG:
+ if cole == NEG:
start = b
- end = ep[d]
+ end = ep(d)
if verbose:
print('[edge_has_curve] try to find path from b=%s to ~d=%s' %
(edge_rep(start), edge_rep(end)))
else:
start = a
- end = ep[c]
+ end = ep(c)
if verbose:
print('[edge_has_curve] try to find path from a=%s to ~c=%s' %
(edge_rep(start), edge_rep(end)))
@@ -3876,7 +4639,7 @@ def edge_has_curve(self, e, check=True, verbose=False):
if start == end:
return True
- n = self._n
+ n = 2 * self._ne
fp = self._fp
seen = [False] * n
seen[start] = True
@@ -3888,11 +4651,11 @@ def edge_has_curve(self, e, check=True, verbose=False):
print('[edge_has_curve] crossing %s' % edge_rep(f))
# here we set r and s so that the triangle is (r, s, ~f)
- r = fp[ep[f]]
+ r = fp[ep(f)]
s = fp[r]
if verbose:
print('[edge_has_curve] switch with r=%s s=%s' % (edge_rep(r), edge_rep(s)))
- if not seen[r] and not (colouring[r] == POS and colouring[ep[f]] == NEG):
+ if not seen[r] and not (colouring[r // 2] == POS and colouring[ep(f) // 2] == NEG):
if r == end:
if verbose:
print('[edge_has_curve] done at %s' % edge_rep(r))
@@ -3901,7 +4664,7 @@ def edge_has_curve(self, e, check=True, verbose=False):
q.append(r)
if verbose:
print('[edge_has_curve] adding %s on top of the queue' % edge_rep(r))
- if not seen[s] and not (colouring[s] == NEG and colouring[ep[f]] == POS):
+ if not seen[s] and not (colouring[s // 2] == NEG and colouring[ep(f) // 2] == POS):
if s == end:
if verbose:
print('[edge_has_curve] done at %s' % edge_rep(s))
@@ -3954,7 +4717,6 @@ def delaunay_flips(self, backend=None):
sage: sorted(T.as_linear_family().delaunay_flips())
[([3], 1), ([3], 2), ([4], 1), ([4], 2), ([5], 1), ([5], 2)]
-
A more complicated example in which edge 4 have a forced colour after
flip and where the flippable edges 0 and 3 are not part of any geometric
flips::
@@ -3981,58 +4743,40 @@ def delaunay_flips(self, backend=None):
sage: sorted(vt.as_linear_family().delaunay_flips())
[([2], 1), ([2], 2), ([4, 8], 1), ([4, 8], 2)]
"""
- from sage.matrix.constructor import matrix
-
- dim = self.dimension()
- base_ring = self.base_ring()
- ne = ambient_dim = self.num_edges()
- L = LinearExpressions(base_ring)
- x = [L.variable(e) for e in range(ne)]
- y = [L.variable(ne + e) for e in range(ne)]
- P = self.delaunay_cone(backend=backend)
- if P.affine_dimension() != 2 * dim:
- raise ValueError('not geometric P.dimension() = {} while 2 * dim = {}'.format(P.affine_dimension(), 2 * dim))
-
- # compute the Delaunay flip facets and the associated
- # subset of edges
- eqns = matrix(base_ring, P.eqns())
- delaunay_facets = {}
- for e in self.forward_flippable_edges():
- a, b, c, d = self.square_about_edge(e, check=False)
- constraint = x[self._norm(e)] == y[self._norm(a)] + y[self._norm(d)]
- constraint = constraint.coefficients(dim=2*ne, homogeneous=True)
- linear_form_project(eqns, constraint)
- vector_normalize(base_ring, constraint)
- constraint = tuple(constraint)
- if constraint in delaunay_facets:
- delaunay_facets[constraint].append(e)
- else:
- delaunay_facets[constraint] = [e]
-
- # determine the possible colours
- neighbours = []
- for ieq, edges in delaunay_facets.items():
- # build the facet
- F = P.add_constraint(L(ieq) == 0)
- if not F.affine_dimension() == 2 * dim - 1:
- continue
-
- # test each edge colour conditions
- # NOTE: all simultaneous flips must be of the same colour
- assert all(self._colouring[e] == self._colouring[edges[0]] for e in edges)
- # NOTE: the equations for the different edges are all equivalent, it
- # is hence enough to use the first edge
- a, b, c, d = self.square_about_edge(edges[0], check=False)
- Fred = F.add_constraint(x[self._norm(a)] <= x[self._norm(d)])
- if Fred.affine_dimension() == 2 * dim - 1:
- neighbours.append((edges, RED))
- Fblue = F.add_constraint(x[self._norm(a)] >= x[self._norm(d)])
- if Fblue.affine_dimension() == 2 * dim - 1:
- neighbours.append((edges, BLUE))
- else:
- neighbours.append((edges, BLUE))
-
- return neighbours
+ ne = self._ne
+ delaunay_cone = self.delaunay_cone()
+ rays = delaunay_cone.rays()
+ ans = []
+ for facet, edges in delaunay_cone.forward_delaunay_facets():
+ e = edges[0]
+ a, b, c, d = self.square_about_half_edge(2 * e)
+ a //= 2
+ b //= 2
+ c //= 2
+ d //= 2
+ colours = 0
+ # TODO: here we do not need to run through all rays. It is actually
+ # sufficient to run through a basis of the support of the facet.
+ for i in facet.ambient_V_indices():
+ r = rays[i]
+ if r[ne + b] > r[ne + a]:
+ assert self._colouring[e] == RED
+ elif r[ne + b] < r[ne + a]:
+ assert self._colouring[e] == BLUE
+ if r[a] < r[d]:
+ colours |= RED
+ if colours & BLUE:
+ break
+ if r[a] > r[d]:
+ colours |= BLUE
+ if colours & RED:
+ break
+ assert colours
+ if colours & RED:
+ ans.append((edges, RED))
+ if colours & BLUE:
+ ans.append((edges, BLUE))
+ return ans
def geometric_flips(self, *args, **kwds):
import warnings
@@ -4065,12 +4809,11 @@ def backward_delaunay_flips(self, backend=None):
sage: cols2 = "RBRR"
sage: VeeringTriangulation(fp, bdry, cols0).backward_delaunay_flips()
[]
- sage: VeeringTriangulation(fp, bdry, cols1).backward_delaunay_flips()
- [([0], 2), ([0], 1)]
+ sage: sorted(VeeringTriangulation(fp, bdry, cols1).backward_delaunay_flips())
+ [([0], 1), ([0], 2)]
sage: sorted(VeeringTriangulation(fp, bdry, cols2).backward_delaunay_flips())
[([0], 1), ([0], 2)]
-
An example in H_0(1, 0, -1^3)::
sage: from veerer import VeeringTriangulation
@@ -4082,61 +4825,51 @@ def backward_delaunay_flips(self, backend=None):
....: vt2.flip_back(e, col)
....: assert vt2.is_delaunay()
"""
- from sage.matrix.constructor import matrix
-
- dim = self.dimension()
- base_ring = self.base_ring()
- ne = ambient_dim = self.num_edges()
- L = LinearExpressions(base_ring)
- x = [L.variable(e) for e in range(ne)]
- y = [L.variable(ne + e) for e in range(ne)]
- P = self.delaunay_cone(backend=backend)
- if P.affine_dimension() != 2 * dim:
- raise ValueError('not geometric P.dimension() = {} while 2 * dim = {}'.format(P.affine_dimension(), 2 * dim))
-
- # compute the Delaunay flip facets and the associated
- # subset of edges
- eqns = matrix(base_ring, P.eqns())
- delaunay_facets = {}
- for e in self.backward_flippable_edges():
- a, b, c, d = self.square_about_edge(e, check=False)
- constraint = y[self._norm(e)] == x[self._norm(a)] + x[self._norm(d)]
- constraint = constraint.coefficients(dim=2*ne, homogeneous=True)
- linear_form_project(eqns, constraint)
- vector_normalize(base_ring, constraint)
- constraint = tuple(constraint)
- if constraint in delaunay_facets:
- delaunay_facets[constraint].append(e)
- else:
- delaunay_facets[constraint] = [e]
-
- # determine the possible colours
- neighbours = []
- for ieq, edges in delaunay_facets.items():
- # build the facet
- F = P.add_constraint(L(ieq) == 0)
- if not F.affine_dimension() == 2 * dim - 1:
- continue
-
- # test each edge colour conditions
- # NOTE: all simultaneous flips must be of the same colour
- assert all(self._colouring[e] == self._colouring[edges[0]] for e in edges)
- # NOTE: the equations for the different edges are all equivalent, it
- # is hence enough to use the first edge
- a, b, c, d = self.square_about_edge(edges[0], check=False)
- Fred = F.add_constraint(y[self._norm(a)] <= y[self._norm(d)])
- if Fred.affine_dimension() == 2 * dim - 1:
- neighbours.append((edges, BLUE))
- Fblue = F.add_constraint(y[self._norm(a)] >= y[self._norm(d)])
- if Fblue.affine_dimension() == 2 * dim - 1:
- neighbours.append((edges, RED))
- else:
- neighbours.append((edges, RED))
-
- return neighbours
+ ne = self._ne
+ delaunay_cone = self.delaunay_cone()
+ rays = delaunay_cone.rays()
+ ans = []
+ for facet, edges in delaunay_cone.backward_delaunay_facets():
+ e = edges[0]
+ a, b, c, d = self.square_about_half_edge(2 * e)
+ a //= 2
+ b //= 2
+ c //= 2
+ d //= 2
+ colours = 0
+ for i in facet.ambient_V_indices():
+ r = rays[i]
+ if r[a] > r[b]:
+ assert self._colouring[e] == RED
+ if r[a] < r[b]:
+ assert self._colouring[e] == BLUE
+ if r[ne + a] > r[ne + d]:
+ colours |= RED
+ if colours & BLUE:
+ break
+ if r[ne + a] < r[ne + d]:
+ colours |= BLUE
+ if colours & RED:
+ break
+ assert colours
+ if colours & RED:
+ ans.append((edges, RED))
+ if colours & BLUE:
+ ans.append((edges, BLUE))
+ return ans
def random_forward_flip_sequence(self, length=1, relabel=False):
- V = self.copy()
+ r"""
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,2,3)(1,4,~0)(5,6,~1)", "BRRBBBB")
+ sage: vt.random_forward_flip_sequence(5) # random
+ VeeringFlipSequence(VeeringTriangulation("(0,2,3)(~0,1,4)(~1,5,6)", "BRRBBBB"), "3B 4B 5B 6B 0R", "()")
+ sage: vt.random_forward_flip_sequence(5, relabel=True) # random
+ VeeringFlipSequence(VeeringTriangulation("(0,2,3)(~0,1,4)(~1,5,6)", "BRRBBBB"), "4B 5B 3R 2B 0B", "(0,~1)(~0,1)(2,6,3,5,4)(~2,~6,~3,~5,~4)")
+ """
+ V = self.copy(mutable=True)
cols = [RED, BLUE]
flips = []
for _ in range(length):
@@ -4153,9 +4886,9 @@ def random_forward_flip_sequence(self, length=1, relabel=False):
flips.append((e, col))
if relabel:
- relabelling = perm_random_centralizer(self._ep)
+ relabelling = perm_random_centralizer(self.edge_permutation())
else:
- relabelling = perm_id(self._n)
+ relabelling = perm_id(2 * self._ne)
from .flip_sequence import VeeringFlipSequence
return VeeringFlipSequence(self, flips, relabelling)
@@ -4184,7 +4917,10 @@ def random_forward_flip(self, repeat=1):
else:
self.flip_back(e, old_col)
- def switch_constraints_matrix(self, slope=VERTICAL):
+ def constraints_matrix(self, slope=VERTICAL):
+ r"""
+ Return a matrix of constraints on x or y coordinates.
+ """
if slope == VERTICAL:
LAR = PURPLE
POS = BLUE
@@ -4200,11 +4936,11 @@ def switch_constraints_matrix(self, slope=VERTICAL):
ans = matrix(ZZ, self.num_triangles(), self.num_edges())
for s, (i,j,k) in enumerate(self.triangles()):
- i = self._norm(i)
+ i //= 2
ci = self._colouring[i]
- j = self._norm(j)
+ j //= 2
cj = self._colouring[j]
- k = self._norm(k)
+ k //= 2
ck = self._colouring[k]
if ci == ZERO and cj == NEG and ck == POS:
@@ -4239,15 +4975,13 @@ def switch_constraints_matrix(self, slope=VERTICAL):
return ans
- constraints_matrix = switch_constraints_matrix
-
- def switch_generators_matrix(self, slope=VERTICAL, mutable=True):
+ def generators_matrix(self, slope=VERTICAL, mutable=True):
r"""
EXAMPLES::
sage: from veerer import VeeringTriangulation
sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~0,~7,~5)(~3,~4,~2)(~6,~1,~8)", "RRBRRBRRB")
- sage: m = vt.switch_generators_matrix()
+ sage: m = vt.generators_matrix()
sage: m # random
sage: m.echelon_form()
[ 1 0 -1 0 -1 -1 0 0 0]
@@ -4255,7 +4989,7 @@ def switch_generators_matrix(self, slope=VERTICAL, mutable=True):
[ 0 0 0 1 1 0 0 0 0]
[ 0 0 0 0 0 0 1 0 -1]
sage: vt = VeeringTriangulation("", boundary="(0:1,1:1,2:1)(~2:3,~0:1,~1:2)", colouring="RRR")
- sage: m = vt.switch_generators_matrix() # random
+ sage: m = vt.generators_matrix() # random
sage: m # random
[1 0 0]
[0 1 0]
@@ -4265,42 +4999,35 @@ def switch_generators_matrix(self, slope=VERTICAL, mutable=True):
[0 1 0]
[0 0 1]
"""
- subspace = self.switch_constraints_matrix(slope).right_kernel_matrix()
+ subspace = self.constraints_matrix(slope).right_kernel_matrix()
if not mutable:
return subspace
return subspace.__copy__()
- generators_matrix = switch_generators_matrix
-
def parallel_cylinders(self, col=RED):
r"""
Return the L-parallel cylinders.
+ The output is a list of lists ``[l0, l1, ...]`` where each ``li``
+ represents a L-parallel family of cylinders, together with circumference.
+
EXAMPLES::
sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
sage: X9 = VeeringTriangulationLinearFamilies.prototype_H1_1(0, 2, 1, -1)
sage: X9.parallel_cylinders()
- [([(0, 0, 0, 1, 1, 0, 0, 1, 1), (0, 0, 0, 0, 0, 1, 1, 0, 0)], [1, 1])]
+ [([([14, 9, 7, 16], [4], [2, 0], True), ([10, 12], [], [1], True)], [1, 1])]
"""
cylinders = list(self.cylinders(col))
if not cylinders:
[]
- B = [] # boundary edges
C = [] # middle edges
for cyl in cylinders:
- b = [0] * self.num_edges() # indicatrix of bottom edges
- t = [0] * self.num_edges() # indicatrix of top edges
c = [0] * self.num_edges() # indicatrix of the middle edges
for e in cyl[0]:
- c[self._norm(e)] = 1
- for e in cyl[1]:
- b[self._norm(e)] = 1
- for e in cyl[2]:
- t[self._norm(e)] = 1
+ c[e // 2] = 1
C.append(c)
- B.append((b, t))
# take intersection of the cylinder twists in the tangent space
F = FreeModule(self.base_ring(), self.num_edges())
@@ -4324,11 +5051,652 @@ def parallel_cylinders(self, col=RED):
pos = u.nonzero_positions()
assert not any(i in seen for i in pos)
seen.update(pos)
- parallel_cylinders = [C[i] for i in pos]
+ parallel_cylinders = [cylinders[i] for i in pos]
parallel_families.append((parallel_cylinders, [u[i] for i in pos]))
return parallel_families
+ # TODO: change edges_low/edges_up to low_edges/up_edges
+ def degeneration(self, edges_low=None, edges_up=None, mutable=False, collapsed_half_edge_relabelling=False, check=True):
+ r"""
+ Return the veering triangulation obtained by blowing-up the given subset of ``edges``.
+
+ This corresponds to a a two levels degeneration in the BCGGM compactification.
+
+ The output is a 4-tuple ``(f_up, f_low, relabelling_up, relabelling_low)`` where
+ - ``f_up`` and ``f_low`` are ``VeeringTriangulationLinearFamily``s
+ - ``relabelling_up``, ``relabelling_low`` are partial maps from the half-edges of this
+ veering triangulation to the half-edges in respectively ``f_up`` and ``f_low``
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+
+ Horizontal degeneration (cylinder blowup)::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~2,~3,~7)(~1,~8,~6)(~5,~0,~4)", "RBRRBBRBR")
+ sage: f_up, f_low, r_up, r_low = vt.degeneration(edges_low=[0, 1, 2, 3, 4, 5, 7], edges_up=[6, 8])
+ sage: f_up is None
+ True
+ sage: f_low.stratum() # optional - surface_dynamics
+ H_1(2, -1^2)
+
+ sage: f_up, f_low, r_up, r_low = vt.degeneration(edges_low=[0, 1, 2, 3, 6, 7, 8], edges_up=[4, 5])
+ sage: f_up is None
+ True
+ sage: f_low.stratum() # optional - surface_dynamics
+ H_1(2, -1^2)
+
+ sage: f_up, f_low, r_up, r_low = vt.degeneration(edges_low=[0, 1, 2, 3, 7], edges_up=[4, 5, 6, 8])
+ sage: f_up is None
+ True
+ sage: f_low.stratum() # optional - surface_dynamics
+ H_0(2, -1^4)
+ sage: r_low
+ array('i', [0, 1, 2, 3, 4, 5, 6, 7, -1, -1, -1, -1, -1, -1, 8, 9, -1, -1])
+
+ Note that when we degenerate two cylinders, the residue condition becomes codimension one
+ in the associated stratum::
+
+ sage: f_low.dimension() == f_low.stratum().dimension() - 1 # optional - surface_dynamics # not tested
+ True
+ sage: f_low.residue_constraints().echelon_form()
+ [1 0 1 0]
+ [0 1 0 1]
+
+ Collapsing the two marked point of a torus in H(0,0)::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,~0)(~3,4,5)(~5,~2,~4)", "BRRRRB")
+ sage: f_up, f_low, r_up, r_low = vt.degeneration(edges_low=[5], edges_up=[0, 1, 2, 3, 4])
+ sage: f_up.stratum() # optional - surface_dynamics
+ H_1(0)
+ sage: f_low.stratum() # optional - surface_dynamics
+ H_0(0^2, -2)
+
+ Examples related to parallel cylinder degeneration in the gothic locus::
+
+ sage: vt = VeeringTriangulation("(0,13,~21)(1,3,~2)(2,9,~3)(4,20,~5)(5,~26,~6)(6,~19,~7)(7,~16,~8)(8,~10,~9)(10,15,~11)(11,24,~12)(12,~14,~13)(14,~25,~15)(16,18,~17)(17,26,~18)(19,25,~20)(21,23,~22)(22,~24,~23)(~4,~1,~0)", "RBBRBRBBRBBRBBRBBBRRBBBRBBB")
+ sage: _, f_low, _, _ = vt.degeneration(edges_up=[1, 2, 4, 6, 7, 9, 10, 12, 13, 15, 16, 17, 20, 21, 22, 24, 25, 26], mutable=True)
+ sage: f_low.set_canonical_labels()
+
+ An example with folded edges::
+
+ sage: vt = VeeringTriangulation("(0,2,7)(1,4,9)(3,12,14)(5,~7,8)(6,~9,~10)(~8,13,~11)(10,~12,11)", "BRBRRBRRBBBBRRB")
+ sage: vt.degeneration(edges_low=(14,))
+ (VeeringTriangulationLinearFamily("(0,2,6)(1,3,8)(4,~6,7)(5,~8,~9)(~7,12,~10)(9,11,10)", "BRBRBRRBBBBRR", [(1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1), (0, 1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 1, 0), (0, 0, 1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 1), (0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, -1, 0), (0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, -1), (0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 1, 0), (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1)]),
+ VeeringTriangulationLinearFamily("(0:7)", "B", [(1)]),
+ array('i', [0, -1, 2, -1, 4, -1, -1, -1, 6, -1, 8, -1, 10, -1, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, -1, 22, 24, -1, -1, -1]),
+ array('i', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1]))
+
+ sage: vt.degeneration(edges_low=(0, 2, 6, 7, 14))
+ (VeeringTriangulationLinearFamily("(0,1,3)(2,6,~4)(~3,5,4)", "RRBBBRR", [(1, 0, 0, -1, 0, 1, 0), (0, 1, 0, 1, 0, -1, 0), (0, 0, 1, 0, 0, 0, -1), (0, 0, 0, 0, 1, 1, 1)]),
+ VeeringTriangulationLinearFamily("(0,1,3)(2:2,4:2,~3:1)", "BBRRB", [(1, 0, 0, 1, 0), (0, 1, 0, -1, 0), (0, 0, 1, 0, 0), (0, 0, 0, 0, 1)]),
+ array('i', [-1, -1, 0, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, 4, 6, -1, 7, -1, 8, 9, -1, 10, 12, -1, -1, -1]),
+ array('i', [0, -1, -1, -1, 2, -1, -1, -1, -1, -1, -1, -1, 4, -1, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 8, -1]))
+
+ TESTS:
+
+ These examples used to not work::
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,~1,3)(~2:2,~3:2)", "BRRR")
+ sage: vt.degeneration([0], [1, 2, 3])
+ (VeeringTriangulationLinearFamily("(0:2,~0:2)", "R", [(1)]),
+ VeeringTriangulationLinearFamily("(0:3)(~0:3)", "B", [(1)]),
+ array('i', [-1, -1, -1, -1, -1, 0, -1, 1]),
+ array('i', [0, 1, -1, -1, -1, -1, -1, -1]))
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~2,3,4)(~4,5,6)", "BRBRBRB")
+ sage: vt.degeneration(edges_low=[1], edges_up=[0, 2, 3, 4, 5, 6])
+ (VeeringTriangulationLinearFamily("(0,1,2)(~2,3,4)", "BRBRB", [(1, 0, 1, 0, 1), (0, 1, 1, 0, 1), (0, 0, 0, 1, 1)]),
+ VeeringTriangulationLinearFamily("(0:3)", "R", [(1)]),
+ array('i', [-1, -1, -1, -1, -1, 0, 2, -1, 4, 5, 6, -1, 8, -1]),
+ array('i', [-1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]))
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~2,3,4)(~4,5,6)", "BRBRBRB")
+ sage: vt.degeneration(edges_low=[1, 3, 5], edges_up=[0, 2, 4, 6])
+ (None,
+ VeeringTriangulationLinearFamily("(0:1,1:1,2:1)(3:1)", "RRRR", [(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)]),
+ array('i', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]),
+ array('i', [-1, -1, 0, -1, -1, -1, 2, -1, -1, -1, 4, -1, -1, -1]))
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~2,3,4)(~4,5,6)", "BRBRBRR")
+ sage: vt.degeneration(edges_low=[0, 1, 2, 3, 4], edges_up=[5, 6])
+ (None,
+ VeeringTriangulationLinearFamily("(0,1,2)(~2,3,4)(~4:1)(5:1)", "BRBRBB", [(1, 0, 1, 0, 1, 1), (0, 1, 1, 0, 1, 1), (0, 0, 0, 1, 1, 1)]),
+ array('i', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]),
+ array('i', [0, -1, 2, -1, 4, 5, 6, -1, 8, 9, -1, -1, -1, -1]))
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~2,3,4)(~3,5,6)(~6,7,8)", "RBBRRRBBR")
+ sage: vt.degeneration(edges_up=[3, 4, 5, 6, 7, 8], edges_low=[0, 1, 2])
+ (VeeringTriangulationLinearFamily("(0,~3,4)(1,2,3)", "RRRBB", [(1, 0, 0, 0, -1), (0, 1, 0, -1, -1), (0, 0, 1, 1, 1)]),
+ VeeringTriangulationLinearFamily("(0,1,2)(~2:3)", "RBB", [(1, 0, -1), (0, 1, 1)]),
+ array('i', [-1, -1, -1, -1, -1, -1, -1, 2, -1, -1, 4, -1, 6, 7, 8, -1, 0, -1]),
+ array('i', [0, -1, 2, -1, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]))
+ sage: vt.degeneration(edges_up=[3, 4, 5, 6, 7, 8], edges_low=[0, 1, 2], collapsed_half_edge_relabelling=True)
+ (VeeringTriangulationLinearFamily("(0,~3,4)(1,2,3)", "RRRBB", [(1, 0, 0, 0, -1), (0, 1, 0, -1, -1), (0, 0, 1, 1, 1)]),
+ VeeringTriangulationLinearFamily("(0,1,2)(~2:3)", "RBB", [(1, 0, -1), (0, 1, 1)]),
+ array('i', [-1, -1, -1, -1, -1, -1, 2, 2, 3, -1, 4, -1, 6, 7, 8, -1, 0, -1]),
+ array('i', [0, -1, 2, -1, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]))
+ """
+ # We distinguish three kinds of triangles
+ # - up triangles: when the three edges are in the up partition
+ # - down triangles: when the three edges are in the down partition
+ # - mixed triangles: when the triangle has one down edge and two up edges
+ n = 2 * self._ne
+ m = self._ne
+ ep = self._ep
+ vp = self._vp
+ fp = self._fp
+ bdry = self._bdry
+ colouring = self._colouring
+
+ if edges_low is None and edges_up is None:
+ raise ValueError('must specify at least one of edges_low and edges_up')
+
+ if edges_low is None:
+ edges_up = set(edges_up)
+ edges_low = set(range(m)).difference(edges_up)
+ if edges_up is None:
+ edges_low = set(edges_low)
+ edges_up = set(range(m)).difference(edges_low)
+ else:
+ edges_low = set(edges_low)
+ edges_up = set(edges_up)
+
+ if check:
+ edges_low = set(self._check_edge(e) for e in edges_low)
+ edges_up = set(self._check_edge(e) for e in edges_up)
+ if not edges_low or not edges_up or edges_low.intersection(edges_up) or edges_low.union(edges_up) != set(range(m)):
+ raise ValueError('invalid arguments: edges_low={} edges_up={}'.format(edges_low, edges_up))
+
+ half_edges_up = set(2 * e for e in edges_up).union(2 * e + 1 for e in edges_up if vp[2 * e + 1] != -1)
+ half_edges_low = set(2 * e for e in edges_low).union(2 * e + 1 for e in edges_low if vp[2 * e + 1] != -1)
+
+ nt_mix = 0 # number of mixed triangles (ie triangle with two up edges and one low edge)
+ n_mix_folded = 0
+ mix_p = array('i', [-1] * n) # involution on mixed edges (crossing triangles)
+ for (a, b, c) in self.triangles():
+ x = (a in half_edges_up) + (b in half_edges_up) + (c in half_edges_up)
+ if x == 1:
+ raise ValueError('invalid set of edges: {} is a up-down-down triangle'.format((a, b, c)))
+ elif x == 2:
+ nt_mix += 1
+ if a not in half_edges_up:
+ down = a
+ mix1, mix2 = b, c
+ if b not in half_edges_up:
+ down = b
+ mix1, mix2 = c, a
+ if c not in half_edges_up:
+ down = c
+ mix1, mix2 = a, b
+ if colouring[mix1 // 2] != colouring[mix2 // 2]:
+ raise ValueError('invalid mixed triangle ({}, {}, {}) with colouring ({}, {}, {})'.format(
+ down, mix1, mix2,
+ colour_to_string(colouring[down // 2]),
+ colour_to_string(colouring[mix1 // 2]),
+ colour_to_string(colouring[mix2 // 2])))
+ mix_p[mix1] = mix2
+ mix_p[mix2] = mix1
+ n_mix_folded += (vp[mix1 ^ 1] == -1) + (vp[mix2 ^ 1] == -1)
+
+ n_up = len(half_edges_up) - 2 * nt_mix # actual number of half-edges in upper level
+
+ # build the upper level (only the face permutation fp_up)
+ relabelling_up = array('i', [-1] * n)
+ vertical_nodes = []
+ n_folded_up = 0
+ if n_up:
+ # If we only blow-up cylinders (horizontal degenerations) there is no
+ # upper level at all
+ ne_up = 0
+ for a in half_edges_up:
+ if mix_p[a] != -1 or relabelling_up[a] != -1:
+ continue
+
+ # build ep
+ relabelling_up[a] = 2 * ne_up
+ A = ep(a)
+ while mix_p[A] != -1:
+ if collapsed_half_edge_relabelling:
+ relabelling_up[A] = 2 * ne_up + 1
+ relabelling_up[mix_p[A]] = 2 * ne_up
+ A = ep(mix_p[A])
+ if A != a:
+ relabelling_up[A] = 2 * ne_up + 1
+ else:
+ n_folded_up += 1
+ ne_up += 1
+
+ assert ne_up <= n_up <= 2 * ne_up
+
+ fp_up = array('i', [-1] * (2 * ne_up))
+ angle_excess_up = array('i', [0] * (2 * ne_up))
+ colouring_up = array('i', [0] * ne_up)
+
+ for e in half_edges_up:
+ if mix_p[e] != -1:
+ continue
+
+ colouring_up[relabelling_up[e] // 2] = colouring[e // 2]
+
+ # Compute the angle excess of the former edge e (named relabelling_up[e]
+ # in the upper veering triangulation). We simply apply the formula
+ # new_angle_excess = sum(angle excesses of contracted edges) - length + alternations / 2
+ col = colouring[e // 2]
+ excess = 0 # excess collected
+ alternations = 0 # number of alternations (including start and end)
+ length = 0 # number of edges that degenerate along the face
+ ee = fp[e]
+ while ee not in half_edges_up:
+ excess += bdry[ee]
+ alternations += colouring[ee // 2] != col
+ length += 1
+ col = colouring[ee // 2]
+ ee = fp[ee]
+ alternations += colouring[ee // 2] != col
+ alternations -= colouring[ee // 2] != colouring[e // 2] # alternation after degeneration
+ excess += bdry[ee]
+ fp_up[relabelling_up[e]] = relabelling_up[ee]
+ assert alternations % 2 == 0
+ angle_excess_up[relabelling_up[ee]] = excess - length + alternations // 2
+
+ vt_up = VeeringTriangulation.from_permutations(None, fp_up, (angle_excess_up,), (colouring_up,), mutable=mutable, check=check)
+
+ else:
+ vt_up = None
+
+ # build the lower level
+ # In the case of folded cylinder, we need to introduce special edges as
+ # circumference of cylinders (in the stratum Q_0(-1^2, -2))
+ folded_cylinders = []
+ has_cylinder = False
+ seen = [False] * 2 * self._ne
+ for h in range(0, 2 * self._ne, 2):
+ if seen[h] or mix_p[h] == -1:
+ continue
+ orbit = []
+ while not seen[h] and mix_p[h] != -1:
+ orbit.append(h)
+ seen[h] = True
+ h = ep(mix_p[h])
+ if mix_p[orbit[-1]] != -1 and ep(mix_p[orbit[-1]]) == orbit[0]:
+ has_cylinder = True
+ folded = [h for h in orbit if vp[2 * (h // 2) + 1] == -1]
+ if len(folded) == 2:
+ assert len(orbit) % 2 == 0
+ i = orbit.index(folded[0])
+ orbit = orbit[i:] + orbit[:i]
+ assert orbit[0] == folded[0]
+ assert orbit[len(orbit) // 2] == folded[1]
+ folded_cylinders.append(orbit)
+ else:
+ assert len(folded) == 0
+
+ if vt_up is not None and has_cylinder:
+ raise ValueError("mixed horizontal and vertical degeneration")
+
+ relabelling_low = array('i', [-1] * n)
+ j = 0
+ n_folded_low = 0
+ for e in sorted(edges_low):
+ relabelling_low[2 * e] = j
+ if vp[2 * e + 1] != -1:
+ relabelling_low[2 * e + 1] = j + 1
+ else:
+ n_folded_low += 1
+ j += 2
+
+ assert j == len(half_edges_low) + n_folded_low, (j, len(half_edges_low), n_folded_low)
+ ne_low = j // 2 + len(folded_cylinders)
+
+ vp_low = array('i', [-1] * (2 * ne_low))
+ angle_excess_low = array('i', [0] * (2 * ne_low))
+ colouring_low = array('i', [0] * ne_low)
+ for e in half_edges_low:
+ colouring_low[relabelling_low[e] // 2] = colouring[e // 2]
+
+ excess = bdry[e]
+ col = colouring[e // 2]
+ alternations = 0
+ ee = vp[e]
+ while relabelling_low[ee] == -1:
+ excess += bdry[ee]
+ alternations += colouring[ee // 2] != col
+ col = colouring[ee // 2]
+ assert col == RED or col == BLUE
+ ee = vp[ee]
+ vp_low[relabelling_low[e]] = relabelling_low[ee]
+ alternations += colouring[ee // 2] != col
+ assert (alternations % 2 == 0) == (colouring[e // 2] == colouring[ee // 2])
+ angle_excess_low[relabelling_low[e]] = excess + alternations // 2
+
+ # build Q_0(-1^2, -2) components for folded cylinders
+ for i, cylinder in enumerate(folded_cylinders):
+ j = 2 * (ne_low - i - 1)
+ vp_low[j] = j
+ angle_excess_low[j] = 1
+ col = colouring[cylinder[0] // 2]
+ assert all(colouring[h // 2] == col for h in cylinder)
+ colouring_low[ne_low - i - 1] = BLUE if col == RED else RED
+
+ vt_low = VeeringTriangulation.from_permutations(vp_low, None, (angle_excess_low,), (colouring_low,), mutable=mutable, check=check)
+
+ edges_up = sorted(edges_up)
+ edges_low = sorted(edges_low)
+
+ # compute constraints in order to produce linear families
+ constraints = self.constraints_matrix().matrix_from_columns(edges_up + edges_low)
+ constraints.echelonize()
+ i = 0
+ constraints_up = constraints[:, :len(edges_up)]
+ while i < constraints_up.nrows() and constraints_up[i]:
+ i += 1
+ constraints_up = constraints_up[:i]
+
+ constraints_low = constraints[i:, len(edges_up):]
+ j = 0
+ while j < constraints_low.nrows() and constraints_low[j]:
+ j += 1
+ constraints_low = constraints_low[:j]
+
+ if folded_cylinders:
+ constraints_low_extended = matrix(constraints_low.base_ring(), constraints_low.nrows() + len(folded_cylinders), constraints_low.ncols() + len(folded_cylinders))
+ constraints_low_extended[:constraints_low.nrows(), :constraints_low.ncols()] = constraints_low
+ for i, cylinder in enumerate(folded_cylinders):
+ j = 2 * (ne_low - i - 1)
+ col = colouring_low[ne_low - i - 1]
+ for h in cylinder:
+ assert colouring[h // 2] != col
+ if colouring[fp[h] // 2] == col:
+ assert fp[h] in half_edges_low
+ constraints_low_extended[constraints_low.nrows() + i, relabelling_low[fp[h]] // 2] += 1
+ constraints_low_extended[constraints_low.nrows() + i, ne_low - i - 1] = -1
+ constraints_low = constraints_low_extended
+
+ from .linear_family import VeeringTriangulationLinearFamily
+ if vt_up is not None:
+ generators_up = constraints_up.right_kernel_matrix()
+ for e in range(n):
+ if mix_p[e] == -1:
+ continue
+ a = edges_up.index(e // 2)
+ b = edges_up.index(mix_p[e] // 2)
+ if generators_up.column(a) != generators_up.column(b):
+ raise RuntimeError('a={} b={}\ngenerators_matrix={}'.format(a, b, generators_up))
+
+
+ edges_up_to_index = {e: i for i, e in enumerate(edges_up)}
+ representatives = [None] * ne_up
+ for h, h_up in enumerate(relabelling_up):
+ if mix_p[h] != -1 or h_up == -1:
+ # collapsed half edge or edge in lower level
+ continue
+ if representatives[h_up // 2] is None:
+ representatives[h_up // 2] = edges_up_to_index[h // 2]
+
+ f_up = VeeringTriangulationLinearFamily(vt_up, generators_up.matrix_from_columns(representatives), mutable=mutable, check=check)
+ else:
+ # horizontal degeneration
+ f_up = None
+
+ f_low = VeeringTriangulationLinearFamily(vt_low, constraints_low.right_kernel_matrix().__copy__(), mutable=mutable, check=check)
+
+ if f_up is not None:
+ # some additional checks for vertical degenerations
+ assert not self.is_abelian() or (f_up.is_abelian() and f_low.is_abelian())
+
+ angles = self.angles()
+ angles_deg = list(f_up.angles()) + list(f_low.angles())
+
+ for a in angles:
+ assert a in angles_deg
+ del angles_deg[angles_deg.index(a)]
+ assert all(a != 0 for a in angles_deg)
+ assert sorted(a for a in angles_deg if a > 0) == sorted(-a for a in angles_deg if a < 0)
+ else:
+ # some additional checks for horizontal degenerations
+ assert not self.is_abelian() or f_low.is_abelian()
+
+ angles = self.angles()
+ angles_deg = list(f_low.angles())
+
+ for a in angles:
+ assert a in angles_deg
+ del angles_deg[angles_deg.index(a)]
+ assert len(angles_deg) % 2 == 0 and all(a == 0 for a in angles_deg)
+
+ return (f_up, f_low, relabelling_up, relabelling_low)
+
+ def horizontal_degeneration_up_edges_subsets(self):
+ for col in [RED, BLUE]:
+ for cyl_family, circumference_ratio in self.parallel_cylinders(col):
+ edges = set()
+ for c, rbdry, lbdry, half in cyl_family:
+ edges.update(i // 2 for i in c)
+ yield tuple(sorted(edges))
+
+ def codimension_one_horizontal_degenerations(self, mutable=False, mapping=False, check=True):
+ r"""
+ Return codimension one horizontal degenerations.
+
+ INPUT:
+
+ - ``mapping`` -- whether to also return the mapping used to relabel edges
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+
+ sage: vt = VeeringTriangulation("(0,1,2)(~1,3,4)(~3,5,6)(~6,~2,~5)(~4,7,8)(~8,~0,~7)", "RBBBRRBBR")
+ sage: [degeneration.stratum() for _, degeneration, _, _ in vt.codimension_one_horizontal_degenerations()] # optional - surface_dynamics
+ [H_1(2, -1^2)]
+
+ sage: from veerer.linear_family import VeeringTriangulationLinearFamilies
+ sage: X9 = VeeringTriangulationLinearFamilies.prototype_H1_1(0, 2, 1, -1)
+ sage: [degeneration.stratum() for _, degeneration, _, _ in vt.codimension_one_horizontal_degenerations()] # optional - surface_dynamics
+ [H_1(2, -1^2)]
+ """
+ if mapping:
+ raise NotImplementedError
+ for edges in self.horizontal_degeneration_up_edges_subsets():
+ yield self.degeneration(edges_up=edges, mutable=mutable, check=check)
+
+ def vertical_degeneration_low_edges_subsets(self):
+ r"""
+ Iterate through subsets of admissible edge degenerations of given complex codimension ``codim``.
+
+ Warning: we do not check for the "boundary of cylinder" condition.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+
+ sage: vt = VeeringTriangulation("(0,8,~7)(1,3,~2)(2,10,~3)(4,6,~5)(5,11,~6)(7,~9,~8)(9,~11,~10)(~4,~1,~0)", "RRBRBBRRBRRB")
+ sage: sorted(vt.vertical_degeneration_low_edges_subsets(), key=lambda x: (len(x), x))
+ [(2,), (6,), (8,), (2, 6), (6, 8), (4, 5, 6, 11)]
+
+ sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)(~0,~7,~5)(~3,~4,~2)(~6,~1,~8)", "RRBRRBRRB")
+ sage: sorted(vt.vertical_degeneration_low_edges_subsets(), key=lambda x: (len(x), x))
+ [(8,), (2, 3, 4, 5)]
+
+ sage: vt = VeeringTriangulation("(0,~7,6)(1,~5,~2)(2,4,~3)(3,11,~4)(5,10,~6)(7,9,~8)(8,~10,~9)(~11,~1,~0)", "RBRBRRBRBRRR")
+ sage: sorted(vt.vertical_degeneration_low_edges_subsets(), key=lambda x: (len(x), x))
+ [(1,), (3,), (6,), (8,), (1, 3), (1, 6), (3, 8), (6, 8)]
+
+ TESTS::
+
+ An example which used to be wrong::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,3,4)(~1,~3,5)(~2,6,7)(~4,~6,8)(~5,9,10)(~7,11,12)(~8,13,~12)(~9,14,15)(~10,16,17)(~11,18,19)(~13,~14,~16)(~15,20,21)(~17,~18,22)(~19,23,24)(~20,~23,25)(~21,26,~25)(~22,~26,~24)", "RRBBBBRRRRRBRBRBBBRRRBRBRRB")
+ sage: sorted(vt.vertical_degeneration_low_edges_subsets(), key=lambda x: (len(x), x))
+ [(2,),
+ (23,),
+ (2, 23),
+ (0, 1, 2),
+ (20, 23, 25),
+ (0, 1, 2, 23),
+ (2, 20, 23, 25),
+ (0, 1, 2, 3, 4, 5),
+ (0, 1, 2, 20, 23, 25),
+ (15, 20, 21, 23, 25, 26),
+ (0, 1, 2, 3, 4, 5, 23),
+ (2, 15, 20, 21, 23, 25, 26),
+ (0, 1, 2, 3, 4, 5, 20, 23, 25),
+ (0, 1, 2, 15, 20, 21, 23, 25, 26),
+ (0, 1, 2, 3, 4, 5, 15, 20, 21, 23, 25, 26),
+ (0, 1, 2, 3, 4, 5, 9, 10, 14, 15, 20, 21, 23, 25, 26)]
+ sage: for low_edges in vt.vertical_degeneration_low_edges_subsets():
+ ....: fup, flow, _, _ = vt.degeneration(edges_low=low_edges)
+ ....: assert fup is not None and flow is not None
+ ....: assert fup.is_delaunay() and flow.is_delaunay()
+ """
+ # For each edge e compute the face corresponding to x[e] == 0 and the face to y[e] == 0 (in H-rep)
+ delaunay_cone = self.delaunay_cone()
+ dim = delaunay_cone.affine_dimension()
+ base_ring = self.base_ring()
+ ne = self._ne
+ space_dim = 2 * ne
+ facets = delaunay_cone.facets()
+ rays = delaunay_cone.rays()
+ CP = delaunay_cone.combinatorial_polyhedron().face_generator()
+ vanishing_faces = [delaunay_cone.vanishing_face(e) for e in range(ne)]
+ vanishing_H_indices = [frozenset(face.ambient_H_indices()) for face in vanishing_faces]
+ facets_kind, facets_data = delaunay_cone.facets_kind_and_data()
+
+ cylinders = collections.defaultdict(set)
+ for middle, bot, top, _ in itertools.chain(self.cylinders(RED), self.cylinders(BLUE)):
+ # NOTE: each cylinder is a quadruple (middle, bottom, top, folded)
+ middle = [h // 2 for h in middle]
+ bot = frozenset(h // 2 for h in bot)
+ top = frozenset(h // 2 for h in top)
+ cylinders[bot].update(top)
+ cylinders[bot].update(middle)
+ cylinders[top].update(bot)
+ cylinders[top].update(middle)
+
+ def complete(face):
+ # if x-degenerationn or y-degeneration facets -> complete with its friend
+ # if Delaunay -> add x-degeneration and y-degeneration correspond to a,b,c,d
+ done = False
+ while not done:
+ done = True
+ ambient_H_indices = set(face.ambient_H_indices())
+
+ # force compl
+ for i in list(ambient_H_indices):
+ kind = facets_kind[i]
+ if kind == "x" or kind == "y":
+ # x or y degeneration
+ for e in facets_data[i]:
+ new_indices = vanishing_H_indices[e]
+ if not new_indices.issubset(ambient_H_indices):
+ done = False
+ ambient_H_indices.update(new_indices)
+ elif kind == "f" or kind == "b":
+ # forward or backward Delaunay
+ for e in facets_data[i]:
+ a, b, c, d = self.square_about_half_edge(2 * e)
+ new_indices = set().union(vanishing_H_indices[a // 2], vanishing_H_indices[b // 2], vanishing_H_indices[c // 2], vanishing_H_indices[d // 2])
+ if not new_indices.issubset(ambient_H_indices):
+ done = False
+ ambient_H_indices.update(new_indices)
+ if not done:
+ face = CP.meet_of_Hrep(*ambient_H_indices)
+
+ return face
+
+ ans = [set() for _ in range(dim + 1)]
+ for e in range(ne):
+ e_face = complete(vanishing_faces[e])
+ assert e_face.dimension() % 2 == 1 # WARNING: there is a shift in dimension
+ codim = dim - (1 + e_face.dimension()) // 2
+ ans[codim].add(e_face.ambient_H_indices())
+
+ for codim1 in range(1, dim):
+ for Hindices1 in ans[codim1]:
+ assert CP.meet_of_Hrep(*Hindices1).dimension() == 2 * dim - 2 * codim1 - 1
+ for codim2 in range(1, codim1 + 1):
+ for Hindices2 in ans[codim2]:
+ assert CP.meet_of_Hrep(*Hindices2).dimension() == 2 * dim - 2 * codim2 - 1
+ if Hindices1 != Hindices2:
+ new_face = CP.meet_of_Hrep(*Hindices1, *Hindices2)
+ new_face = complete(new_face)
+ assert new_face.dimension() % 2 == 1
+ codim = dim - (1 + new_face.dimension()) // 2
+ if codim == codim1:
+ assert new_face.ambient_H_indices() == Hindices1, (face.ambient_H_indices(), Hindices1)
+ elif codim == codim2:
+ assert new_face.ambient_H_indices() == Hindices2, (face.ambient_H_indices(), Hindices2)
+ else:
+ assert codim > min(codim1, codim2), (codim, codim1, codim2)
+ ans[codim].add(new_face.ambient_H_indices())
+
+ output = []
+ for codim in range(1, dim):
+ for Hindices in ans[codim]:
+ face = CP.meet_of_Hrep(*Hindices)
+ frays = [rays[i] for i in face.ambient_V_indices()]
+ vanishing_edges1 = [e for e in range(ne) if all(r[e] == 0 for r in frays)]
+ vanishing_edges2 = [e for e in range(ne) if all(r[ne + e] == 0 for r in frays)]
+ assert vanishing_edges1 == vanishing_edges2
+ output.append(tuple(vanishing_edges1))
+ return output
+
+ def codimension_one_vertical_degenerations(self, mutable=False, mapping=False, check=True):
+ r"""
+ Return codimension one vertical degenerations.
+
+ EXAMPLES::
+
+ sage: from veerer import VeeringTriangulation
+ sage: vt = VeeringTriangulation("(0,8,~7)(1,3,~2)(2,10,~3)(4,6,~5)(5,11,~6)(7,~9,~8)(9,~11,~10)(~4,~1,~0)", "RRBRBBRRBRRB")
+ sage: [(f_up.stratum(), f_low.stratum()) for (f_up, f_low, _, _) in vt.codimension_one_vertical_degenerations()] # optional - surface_dynamics
+ [(H_2(2), H_0(1^2, -4)),
+ (H_2(2), H_0(1^2, -4)),
+ (H_2(2), H_0(1^2, -4)),
+ (H_1(0^2), H_0(1^2, -2^2)),
+ (H_1(0^2), H_0(1^2, -2^2)),
+ (H_1(0^2), H_0(1^2, -2^2))]
+
+ TESTS:
+
+ This example used to be wrong::
+
+ sage: from veerer import VeeringTriangulation, VeeringTriangulationLinearFamily
+ sage: vt = VeeringTriangulation("(0,1,2)(~0,3,4)(~1,5,6)(~2,7,8)(~3,~5,9)(~4,10,~8)(~6,11,12)(~7,13,14)(~9,15,16)(~10,~12,17)(~11,18,19)(~13,~15,20)(~14,~16,21)(~17,22,23)(~18,24,~20)(~19,25,26)(~21,~26,~23)(~22,~24,~25)", "BRRRRBRRBRRRBBRRBRBRRRRBRBR")
+ sage: subspace = [(1, 0, 1, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, -2, 2, 2, 2, 0, -1, 1, 0, 0, 0, 0, 1, 1, 0),
+ ....: (0, 1, 1, 0, 0, 0, 1, 2, 1, 0, 1, 1, 0, 2, 0, 0, 0, 1, 1, 0, 2, 0, 1, 0, 1, 0, 0),
+ ....: (0, 0, 0, 1, 1, 0, 0, -1, -1, 1, 0, 0, 0, -2, 1, 1, 0, 0, -1, 1, -1, 1, 0, 0, 0, 0, 1),
+ ....: (0, 0, 0, 0, 0, 1, -1, 1, 1, -1, 1, 1, 2, 2, -1, -1, 0, -1, 1, 0, 1, -1, 0, 1, 0, 0, 0)]
+ sage: f = VeeringTriangulationLinearFamily(vt, subspace)
+ sage: assert all(f_up is not None for f_up, _, _, _ in f.codimension_one_vertical_degenerations())
+ sage: half_edges = set(f.half_edges())
+ sage: for edges_low in sorted(f.vertical_degeneration_low_edges_subsets(), key=lambda x: (len(x), x)):
+ ....: print(edges_low, tuple(sorted(half_edges.difference(edges_low))))
+ (0, 16, 25) (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53)
+ (5, 12, 23) (0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53)
+ (8, 13, 18) (0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53)
+ (0, 8, 13, 16, 18, 25) (1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 17, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53)
+ sage: for edges_up in f.horizontal_degeneration_up_edges_subsets():
+ ....: print(tuple(sorted(half_edges.difference(edges_up))), edges_up)
+ (0, 5, 8, 12, 13, 16, 18, 23, 25, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53) (1, 2, 3, 4, 6, 7, 9, 10, 11, 14, 15, 17, 19, 20, 21, 22, 24, 26)
+
+ Another example that used to be wrong::
+
+ sage: vt = VeeringTriangulation("(0:1,1:1,~0:1,2:1,3:3)(~1:1,4:1,~3:1,~2:3,~4:1)", "BRRRB")
+ sage: f_up, f_low, _, _ = vt.degeneration(edges_low=(1,))
+ sage: f_up
+ VeeringTriangulationLinearFamily("(0:1,~0:2,1:1,2:3)(~1:3,~3:1,3:2,~2:1)", "BRRB", [(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)])
+ sage: f_low
+ VeeringTriangulationLinearFamily("(0:3)(~0:3)", "R", [(1)])
+ sage: (f_up.stratum(), f_low.stratum()) # optional - surface_dynamics
+ (H_0(2^2, 0^2, -3^2), H_0(2, -2^2))
+ """
+ for edges in self.vertical_degeneration_low_edges_subsets():
+ yield self.degeneration(edges_low=edges, mutable=mutable, check=check)
+
def is_half_edge_strebel(self, e, slope=VERTICAL, check=True):
r"""
Return whether ``e`` is a Strebel half-edge.
@@ -4345,10 +5713,10 @@ def is_half_edge_strebel(self, e, slope=VERTICAL, check=True):
sage: vt = VeeringTriangulation("(0,~3,2)(1,3,~2)", boundary="(~1:2,~0:2)", colouring="BBRR")
sage: for (e0, e1) in vt.edges():
....: print(e0, vt.is_half_edge_strebel(e0), e1, vt.is_half_edge_strebel(e1))
- 0 True 7 True
- 1 True 6 True
- 2 False 5 False
- 3 True 4 True
+ 0 True 1 True
+ 2 True 3 True
+ 4 False 5 False
+ 6 True 7 True
"""
if check:
e = self._check_half_edge(e)
@@ -4363,8 +5731,8 @@ def is_half_edge_strebel(self, e, slope=VERTICAL, check=True):
b = fp[a]
assert e == fp[b]
- ca = self._colouring[a]
- cb = self._colouring[b]
+ ca = self._colouring[a // 2]
+ cb = self._colouring[b // 2]
if slope == VERTICAL:
return (ca != BLUE or cb != RED)
@@ -4373,7 +5741,7 @@ def is_half_edge_strebel(self, e, slope=VERTICAL, check=True):
else:
raise ValueError('invalid slope parameter')
- def strebel_graph(self, slope=VERTICAL, mutable=False):
+ def strebel_graph(self, slope=VERTICAL, mapping=False, mutable=False):
r"""
Return the Strebel graph associated to this veering triangulation.
@@ -4383,6 +5751,9 @@ def strebel_graph(self, slope=VERTICAL, mutable=False):
- ``mutable`` (optional, default ``False``) -- whether the output Strebel graph is mutable
+ - ``mapping`` -- whether to return the mapping of edges (the Strebel
+ graph is a subgraph of the triangulation) and the mapping of boundary faces (the output list of boundary faces of the Strebel graph corresponds positionally to the list of boundary faces of the input veering triangulation)
+
EXAMPLES::
sage: from veerer import *
@@ -4419,8 +5790,12 @@ def strebel_graph(self, slope=VERTICAL, mutable=False):
sage: vt = VeeringTriangulation(triangles, boundary, colours)
sage: sg = vt.strebel_graph()
sage: sg
- StrebelGraph("(0:1,1,~1:1,~0,2)(3,~2,~3:2,4,~4:1)")
+ StrebelGraph("(0:1,1,~1:1,~0,2)(~2,~3:2,4,~4:1,3)")
sage: assert sg.stratum() == vt.stratum() == Stratum((5, 0, 0, 0, 0, -4, -5), 2) # optional - surface_dynamics
+ sage: vt.strebel_graph(mapping=True)
+ (StrebelGraph("(0:1,1,~1:1,~0,2)(~2,~3:2,4,~4:1,3)"),
+ array('i', [-1, -1, 0, 1, 2, 3, 4, 5, -1, -1, -1, -1, 6, 7, 8, 9, -1, -1]),
+ array('i', [1, 9, 1, 5, 9, 11, 11, 17, 17, 15]))
sage: vt = VeeringTriangulation("(0,1,2)(3,4,5)(6,7,8)", "(~8:2,~7:1,~6:1,~5:2,~4:1,~3:1,~2:2,~1:1,~0:1)", "RBRRBBRRB")
sage: sg = vt.strebel_graph()
@@ -4441,44 +5816,69 @@ def strebel_graph(self, slope=VERTICAL, mutable=False):
sage: for cols in G.colourings():
....: for vt in G.veering_triangulations(cols):
....: assert vt.is_strebel() and vt.strebel_graph() == G
+
+ Example where the output mapping of boundary faces differs in order from the list of boundary faces in the output Strebel graph::
+
+ sage: vt = VeeringTriangulation("(1,4,2)(~2,3,~0)(~1:1, ~4:1)(0:1,~3:1)","BRBRB")
+ sage: sg = vt.strebel_graph()
+ sage: sg.boundary_faces()
+ [[0, 1, 2], [3, 4, 5]]
+ sage: sg, r1, r2 = vt.strebel_graph(mapping=True)
+ sage: sg
+ StrebelGraph("(0,~0:1,1)(~1,2,~2:1)")
+ sage: r1
+ array('i', [-1, -1, 0, 1, 2, 3, 4, 5, -1, -1])
+ sage: r2
+ array('i', [9, 3, 9, 0, 0, 7])
+ sage: assert all(vt.face_angle(r2[h]) == sg.face_angle(h) for h in sg.half_edges())
+
+ TESTS:
+
+ An example with folded edges that used not to work::
+
+ sage: vt = VeeringTriangulation("(0:1)(1:1,2:1,3:1,4:1)(~2:1,~4:1,5:1)(~3:1,~5:1)", "BBBBBB")
+ sage: sg, r1, r2 = vt.strebel_graph(mapping=True)
+ sage: sg
+ StrebelGraph("(0)(1,2,3,4)(~2,~4,5)(~3,~5)")
+ sage: assert all(vt.face_angle(r2[h]) == sg.face_angle(h) for h in sg.half_edges())
"""
if not self.is_strebel(slope=slope):
raise ValueError('triangulation is not Strebel')
- n = self._n
+ n = 2 * self._ne
ep = self._ep
fp = self._fp
vp = self._vp
bdry = self._bdry
colouring = self._colouring
- dim = VeeringTriangulation.dimension(self)
- m = 2 * dim - self.num_folded_edges() # number of half-edges
+ dim = self.stratum_dimension()
+ m = 2 * dim # number of half-edges
# index of half-edges that remain in the strebel graph
- index_strebel = [-1] * n
- j = k = 0
- for e in range(n):
- E = ep[e]
+ index_strebel = array('i', [-1] * n)
+ j = 0
+ for e in range(0, n, 2):
+ E = ep(e)
if e == E:
if self.is_half_edge_strebel(e, slope=slope):
- index_strebel[e] = j + k
- k += 1
- if e < ep[e]:
+ index_strebel[e] = j
+ j += 2
+ else:
if self.is_half_edge_strebel(e, slope=slope) and self.is_half_edge_strebel(E, slope=slope):
- index_strebel[e] = j + k
- index_strebel[E] = m - j - 1
- j += 1
+ index_strebel[e] = j
+ index_strebel[E] = j + 1
+ j += 2
+
+ assert j == m, (j, m)
- # build vertex permutation, edge permutation, and boundary array
+ # build vertex permutation, edge permutation, angle excess and nodes
vertex_permutation = array('i', [-1] * m)
- edge_permutation = array('i', [-1] * m)
- beta = array('i', [-1] * m)
+ beta = array('i', [0] * m)
for e0, i in enumerate(index_strebel):
if i == -1:
continue
- assert index_strebel[ep[e0]] != -1
- edge_permutation[i] = index_strebel[ep[e0]]
+ assert index_strebel[ep(e0)] != -1
e = e0
j = -1
sum_alpha_e = 0
@@ -4495,19 +5895,42 @@ def strebel_graph(self, slope=VERTICAL, mutable=False):
beta[i] = 0
else:
if slope == VERTICAL:
- if colouring[e_mark_0] == RED and colouring[vp[e_mark_0]] == BLUE:
+ if colouring[e_mark_0 // 2] == RED and colouring[vp[e_mark_0] // 2] == BLUE:
beta[i] = sum_alpha_e
else:
beta[i] = sum_alpha_e - 1
elif slope == HORIZONTAL:
- if colouring[e_mark_0] == BLUE and colouring[vp[e_mark_0]] == RED:
+ if colouring[e_mark_0 // 2] == BLUE and colouring[vp[e_mark_0] // 2] == RED:
beta[i] = sum_alpha_e
else:
beta[i] = sum_alpha_e - 1
- #STEP3: build the Strebel graph
from .strebel_graph import StrebelGraph
- return StrebelGraph.from_permutations(vertex_permutation, edge_permutation, None, (beta,), mutable=mutable, check=True)
+ sg = StrebelGraph.from_permutations(vertex_permutation, None, (beta,), (), mutable=mutable, check=True)
+
+ if mapping:
+ # Each edge in the Strebel graph is mapped to a sub-edge of
+ # the veering triangulation at the boundary. We compute
+ # this map below.
+ strebel_to_veering_boundary = array('i', [-1] * m)
+ for h in range(2 * self._ne):
+ if self._bdry[h]:
+ k = h
+ while index_strebel[k] == -1:
+ k = self.previous_at_vertex(k)
+ strebel_to_veering_boundary[index_strebel[k]] = h
+
+ for k, h in enumerate(strebel_to_veering_boundary):
+ if h == -1:
+ continue
+ k = sg._fp[k]
+ while strebel_to_veering_boundary[k] == -1:
+ strebel_to_veering_boundary[k] = h
+ k = sg._fp[k]
+
+ return (sg, index_strebel, strebel_to_veering_boundary)
+
+ return sg
class VeeringTriangulations:
@@ -4524,17 +5947,17 @@ def L_shaped_surface(a1, a2, b1, b2, t1=0, t2=0):
sage: from veerer import *
sage: T, s, t = VeeringTriangulations.L_shaped_surface(1, 1, 1, 1)
sage: T
- VeeringTriangulation("(0,2,3)(1,4,~0)(5,6,~1)", "BRRBBBB")
+ VeeringTriangulation("(0,2,3)(~0,1,4)(~1,5,6)", "BRRBBBB")
sage: s
(0, 1, 1, 1, 1, 1, 0)
sage: t
(1, 0, 0, 1, 1, 1, 1)
- sage: T._set_switch_conditions(T._tt_check, s, VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, t, VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, s, VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, t, VERTICAL)
sage: T, s, t = VeeringTriangulations.L_shaped_surface(2, 3, 4, 5, 1, 2)
- sage: T._set_switch_conditions(T._tt_check, s, VERTICAL)
- sage: T._set_switch_conditions(T._tt_check, t, VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, s, VERTICAL)
+ sage: T._set_subspace_constraints(T._constraint_check, t, VERTICAL)
"""
# Return the (quotient by the hyperelliptic involution of the) L-shaped surface
# together with the equations of the GL2R deformation