diff --git a/engine/testsuite/dim2/triangulation2.cpp b/engine/testsuite/dim2/triangulation2.cpp index 5d60c53a2..013910cfc 100644 --- a/engine/testsuite/dim2/triangulation2.cpp +++ b/engine/testsuite/dim2/triangulation2.cpp @@ -46,6 +46,20 @@ class Dim2Test : public TriangulationTest<2> { // Closed non-orientable triangulations: TestCase rp2 { Example<2>::rp2(), "RP^2" }; + TestCase kb { Example<2>::nonOrientable(2, 0), "KB" }; + + // Minimal bounded orientable triangulations: + TestCase pants { Example<2>::orientable(0, 3), "Pair of pants" }; + TestCase orPunc1 { Example<2>::orientable(2, 1), + "Or, g=2 + 1 puncture" }; + TestCase orPunc3 { Example<2>::orientable(2, 3), + "Or, g=2 + 3 punctures" }; + + // Minimal bounded non-orientable triangulations: + TestCase norPunc1 { Example<2>::nonOrientable(2, 1), + "Non-or, g=2 + 1 puncture" }; + TestCase norPunc3 { Example<2>::nonOrientable(2, 3), + "Non-or, g=2 + 3 punctures" }; // Disconnected triangulations (we build these in the constructor): TestCase disjoint2 { {}, "Torus U Mobius" }; @@ -70,8 +84,16 @@ class Dim2Test : public TriangulationTest<2> { f(s2Oct.tri, s2Oct.name); f(torus2.tri, torus2.name); f(rp2.tri, rp2.name); + f(kb.tri, kb.name); f(disjoint2.tri, disjoint2.name); f(disjoint3.tri, disjoint3.name); + + // Minimal bounded triangulations. + f(pants.tri, pants.name); + f(orPunc1.tri, orPunc1.name); + f(orPunc3.tri, orPunc3.name); + f(norPunc1.tri, norPunc1.name); + f(norPunc3.tri, norPunc3.name); } }; @@ -93,8 +115,16 @@ TEST_F(Dim2Test, validity) { verifyValid(s2Oct); verifyValid(torus2); verifyValid(rp2); + verifyValid(kb); verifyValid(disjoint2); verifyValid(disjoint3); + + // Minimal bounded triangulations. + verifyValid(pants); + verifyValid(orPunc1); + verifyValid(orPunc3); + verifyValid(norPunc1); + verifyValid(norPunc3); } TEST_F(Dim2Test, connectivity) { TriangulationTest<2>::connectivityGenericCases(); @@ -102,8 +132,16 @@ TEST_F(Dim2Test, connectivity) { EXPECT_TRUE(s2Oct.tri.isConnected()); EXPECT_TRUE(torus2.tri.isConnected()); EXPECT_TRUE(rp2.tri.isConnected()); + EXPECT_TRUE(kb.tri.isConnected()); EXPECT_FALSE(disjoint2.tri.isConnected()); EXPECT_FALSE(disjoint3.tri.isConnected()); + + // Minimal bounded triangulations. + EXPECT_TRUE(pants.tri.isConnected()); + EXPECT_TRUE(orPunc1.tri.isConnected()); + EXPECT_TRUE(orPunc3.tri.isConnected()); + EXPECT_TRUE(norPunc1.tri.isConnected()); + EXPECT_TRUE(norPunc3.tri.isConnected()); } TEST_F(Dim2Test, orientability) { TriangulationTest<2>::orientabilityGenericCases(); @@ -111,8 +149,16 @@ TEST_F(Dim2Test, orientability) { EXPECT_TRUE(s2Oct.tri.isOrientable()); EXPECT_TRUE(torus2.tri.isOrientable()); EXPECT_FALSE(rp2.tri.isOrientable()); + EXPECT_FALSE(kb.tri.isOrientable()); EXPECT_FALSE(disjoint2.tri.isOrientable()); EXPECT_FALSE(disjoint3.tri.isOrientable()); + + // Minimal bounded triangulations. + EXPECT_TRUE(pants.tri.isOrientable()); + EXPECT_TRUE(orPunc1.tri.isOrientable()); + EXPECT_TRUE(orPunc3.tri.isOrientable()); + EXPECT_FALSE(norPunc1.tri.isOrientable()); + EXPECT_FALSE(norPunc3.tri.isOrientable()); } TEST_F(Dim2Test, orientedExamples) { // Ensure that the orientable Example<2> constructions are oriented. @@ -128,6 +174,11 @@ TEST_F(Dim2Test, orientedExamples) { EXPECT_TRUE(Example<2>::orientable(5, 3).isOriented()); EXPECT_TRUE(Example<2>::sphereOctahedron().isOriented()); EXPECT_TRUE(Example<2>::disc().isOriented()); + + // Minimal bounded triangulations. + EXPECT_TRUE(pants.tri.isOriented()); + EXPECT_TRUE(orPunc1.tri.isOriented()); + EXPECT_TRUE(orPunc3.tri.isOriented()); } TEST_F(Dim2Test, eulerChar) { @@ -136,8 +187,16 @@ TEST_F(Dim2Test, eulerChar) { EXPECT_EQ(s2Oct.tri.eulerCharTri(), 2); EXPECT_EQ(torus2.tri.eulerCharTri(), -2); EXPECT_EQ(rp2.tri.eulerCharTri(), 1); + EXPECT_EQ(kb.tri.eulerCharTri(), 0); EXPECT_EQ(disjoint2.tri.eulerCharTri(), 0); EXPECT_EQ(disjoint3.tri.eulerCharTri(), 2); + + // Minimal bounded triangulations. + EXPECT_EQ(pants.tri.eulerCharTri(), -1); + EXPECT_EQ(orPunc1.tri.eulerCharTri(), -3); + EXPECT_EQ(orPunc3.tri.eulerCharTri(), -5); + EXPECT_EQ(norPunc1.tri.eulerCharTri(), -1); + EXPECT_EQ(norPunc3.tri.eulerCharTri(), -3); } TEST_F(Dim2Test, boundaryBasic) { TriangulationTest<2>::boundaryBasicGenericCases(); @@ -145,8 +204,16 @@ TEST_F(Dim2Test, boundaryBasic) { verifyBoundaryBasic(s2Oct, {}, {}, {}); verifyBoundaryBasic(torus2, {}, {}, {}); verifyBoundaryBasic(rp2, {}, {}, {}); + verifyBoundaryBasic(kb, {}, {}, {}); verifyBoundaryBasic(disjoint2, {0}, {}, {}); verifyBoundaryBasic(disjoint3, {0, 0}, {}, {}); + + // Minimal bounded triangulations. + verifyBoundaryBasic(pants, {0, 0, 0}, {}, {}); + verifyBoundaryBasic(orPunc1, {0}, {}, {}); + verifyBoundaryBasic(orPunc3, {0, 0, 0}, {}, {}); + verifyBoundaryBasic(norPunc1, {0}, {}, {}); + verifyBoundaryBasic(norPunc3, {0, 0, 0}, {}, {}); } TEST_F(Dim2Test, vertexLinksBasic) { TriangulationTest<2>::vertexLinksBasicGenericCases(); @@ -154,8 +221,16 @@ TEST_F(Dim2Test, vertexLinksBasic) { verifyVertexLinksBasic(s2Oct, 6, 0); verifyVertexLinksBasic(torus2, 1, 0); verifyVertexLinksBasic(rp2, 2, 0); + verifyVertexLinksBasic(kb, 1, 0); verifyVertexLinksBasic(disjoint2, 1, 1); verifyVertexLinksBasic(disjoint3, 4, 2); + + // Minimal bounded triangulations. + verifyVertexLinksBasic(pants, 0, 3); + verifyVertexLinksBasic(orPunc1, 0, 1); + verifyVertexLinksBasic(orPunc3, 0, 3); + verifyVertexLinksBasic(norPunc1, 0, 1); + verifyVertexLinksBasic(norPunc3, 0, 3); } TEST_F(Dim2Test, orient) { testManualCases(TriangulationTest<2>::verifyOrient); @@ -215,8 +290,16 @@ TEST_F(Dim2Test, homologyH1) { EXPECT_EQ(s2Oct.tri.homology<1>(), regina::AbelianGroup()); EXPECT_EQ(torus2.tri.homology<1>(), regina::AbelianGroup(4)); EXPECT_EQ(rp2.tri.homology<1>(), regina::AbelianGroup(0, {2})); + EXPECT_EQ(kb.tri.homology<1>(), regina::AbelianGroup(1, {2})); EXPECT_EQ(disjoint2.tri.homology<1>(), regina::AbelianGroup(3)); EXPECT_EQ(disjoint3.tri.homology<1>(), regina::AbelianGroup(2, {2})); + + // Minimal bounded triangulations. + EXPECT_EQ(pants.tri.homology<1>(), regina::AbelianGroup(2)); + EXPECT_EQ(orPunc1.tri.homology<1>(), regina::AbelianGroup(4)); + EXPECT_EQ(orPunc3.tri.homology<1>(), regina::AbelianGroup(6)); + EXPECT_EQ(norPunc1.tri.homology<1>(), regina::AbelianGroup(2)); + EXPECT_EQ(norPunc3.tri.homology<1>(), regina::AbelianGroup(4)); } TEST_F(Dim2Test, fundGroup) { TriangulationTest<2>::fundGroupGenericCases(); @@ -224,8 +307,17 @@ TEST_F(Dim2Test, fundGroup) { EXPECT_EQ(s2Oct.tri.group().recogniseGroup(), "0"); EXPECT_EQ(torus2.tri.group().recogniseGroup(), ""); EXPECT_EQ(rp2.tri.group().recogniseGroup(), "Z_2"); + EXPECT_EQ(kb.tri.group().recogniseGroup(), + "Z~Z w/monodromy a ↦ a^-1"); // We cannot call group() on disjoint triangulations. + + // Minimal bounded triangulations. + EXPECT_EQ(pants.tri.group().recogniseGroup(), "Free(2)"); + EXPECT_EQ(orPunc1.tri.group().recogniseGroup(), "Free(4)"); + EXPECT_EQ(orPunc3.tri.group().recogniseGroup(), "Free(6)"); + EXPECT_EQ(norPunc1.tri.group().recogniseGroup(), "Free(2)"); + EXPECT_EQ(norPunc3.tri.group().recogniseGroup(), "Free(4)"); } TEST_F(Dim2Test, chainComplex) { testManualCases(TriangulationTest<2>::verifyChainComplex); @@ -239,3 +331,16 @@ TEST_F(Dim2Test, dualToPrimal) { TEST_F(Dim2Test, copyMove) { testManualCases(TriangulationTest<2>::verifyCopyMove); } +TEST_F(Dim2Test, minimalSize) { + // Check that promises of minimality are fulfilled. + EXPECT_EQ(Example<2>::orientable(0, 0).size(), 2); + EXPECT_EQ(Example<2>::orientable(0, 1).size(), 1); + EXPECT_EQ(torus2.tri.size(), 6); + EXPECT_EQ(rp2.tri.size(), 2); + EXPECT_EQ(kb.tri.size(), 2); + EXPECT_EQ(pants.tri.size(), 5); + EXPECT_EQ(orPunc1.tri.size(), 7); + EXPECT_EQ(orPunc3.tri.size(), 13); + EXPECT_EQ(norPunc1.tri.size(), 3); + EXPECT_EQ(norPunc3.tri.size(), 9); +} diff --git a/engine/triangulation/example2.cpp b/engine/triangulation/example2.cpp index baf814f1e..f6298e3e4 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -35,39 +35,81 @@ namespace regina { Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { - if (genus == 0 && punctures == 0) - return sphere(); + // Small cases that don't fit into the general constructions below. + if (genus == 0) { + if (punctures == 0) { + //TODO If sphere() were oriented(), then we could promise + // oriented for orientable(), since all the other constructions + // below are already oriented. + return sphere(); + } else if (punctures == 1) { + return disc(); + } + } Triangulation<2> ans; - if (genus == 0) { - // Fact: punctures >= 1. - unsigned n = 3 * punctures - 2; - unsigned i; - ans.newTriangles(n); - for (i = 0; i < n - 1; ++i) - ans.triangle(i)->join(1, ans.triangle(i + 1), Perm<3>(1, 2)); - ans.triangle(0)->join(0, ans.triangle(n - 1), Perm<3>(0, 1)); - for (i = 1; i < punctures; ++i) - ans.triangle(3 * i - 2)->join(0, ans.triangle(3 * i), - Perm<3>(1, 2)); - } else { - unsigned n = 4 * genus + 3 * punctures - 2; - unsigned i; - ans.newTriangles(n); - for (i = 0; i < n - 1; ++i) - ans.triangle(i)->join(1, ans.triangle(i + 1), Perm<3>(1, 2)); + if (punctures == 0) { + // Already handled the sphere, so genus >= 1. + // + // The size of a minimal triangulation is 4*genus - 2. + // + // This is essentially the same as the old implementation, but without + // the punctures part of the construction (which was non-minimal). + unsigned n = 4 * genus - 2; + ans = polygon(n); ans.triangle(0)->join(2, ans.triangle(n - 1), Perm<3>(0, 2)); ans.triangle(0)->join(0, ans.triangle(n - 1), Perm<3>(0, 1)); - for (i = 1; i < genus; ++i) { + for (unsigned i = 1; i < genus; ++i) { ans.triangle(4 * i - 3)->join(0, ans.triangle(4 * i - 1), Perm<3>(1, 2)); ans.triangle(4 * i - 2)->join(0, ans.triangle(4 * i), Perm<3>(1, 2)); } - for (i = 0; i < punctures; ++i) - ans.triangle(4 * genus + 3 * i - 3)->join( - 0, ans.triangle(4 * genus + 3 * i - 1), Perm<3>(1, 2)); + } else if (punctures == 1) { + // Already handled the disc, so genus >= 1. + // + // The size of a minimal triangulation is 4*genus - 1. + ans = polygon( 4*genus - 1 ); + for (unsigned handle = 0; handle < genus; ++handle) { + for (unsigned faceIndex : {4*handle, 4*handle + 1}) { + if ( faceIndex == 4*genus - 3 ) { + // The very last gluing needs to be handled differently + // from the others. + ans.triangle(faceIndex)->join( + 0, ans.triangle(4*genus - 2), Perm<3>(0, 1) ); + } else { + ans.triangle(faceIndex)->join( + 0, ans.triangle(faceIndex + 2), Perm<3>(1, 2) ); + } + } + } + } else if (genus == 0) { + // Already handled the sphere and the disc, so punctures >= 2. + // + // The size of a minimal triangulation is 3*punctures - 4. + ans = polygon( 3*punctures - 4 ); + for (unsigned i = 0; i < punctures - 1; ++i) { + if (i == punctures - 2) { + // The very last gluing needs to be handled differently + // from the others. + ans.triangle(3*i)->join( + 0, ans.triangle( 3*punctures - 5 ), Perm<3>(0, 1) ); + } else { + ans.triangle(3*i)->join( + 0, ans.triangle( 3*i + 2 ), Perm<3>(1, 2) ); + } + } + } else { + // All that remains are the cases where genus >= 1 and punctures >= 2. + // + // The size of a minimal triangulation is 4*genus + 3*punctures - 4. + // + // We construct this by starting with one puncture (which has + // 4*genus - 1 triangles), and then adding all the extra punctures by + // attaching a gadget with 3*punctures - 3 triangles. + ans = orientable( genus, 1 ); + addPunctures( ans, punctures ); } return ans; @@ -81,52 +123,77 @@ Triangulation<2> Example<2>::nonOrientable(unsigned genus, unsigned punctures) { Triangulation<2> ans; - // The generic code below will create one internal vertex, and one for - // each puncture. This is minimal for zero punctures, but non-minimal - // otherwise. For now, we use a different triangulation for the - // once-punctured case so at least that gets to be minimal also; - // ideally these should be minimal for all values of punctures. - - if (punctures == 1) { - // Thanks to Alex He for this code. - - // Let g denote the given genus. We use g-1 "inner" triangles and g - // "outer" triangles, for a total of 2*g-1 triangles. We start by using - // the g-1 "inner" triangles to build a (g+1)-sided polygon P. We then - // form each of the g "outer" triangles into a one-triangle Mobius band, - // and attach the boundary of each of these Mobius bands to one of the - // sides of P. It is clear that the resulting surface is once-punctured - // and one-vertex, and has non-orientable genus g. - unsigned n = 2*genus - 1; - unsigned i; - ans.newTriangles(n); - // Form "outer" triangles into Mobius bands. - for ( i = genus - 1; i < n; ++i ) { - ans.triangle(i)->join( - 0, ans.triangle(i), Perm<3>(1, 2, 0) ); - } - // Glue everything together. - for ( i = 1; i < n; ++i ) { - ans.triangle(i)->join( - 2, ans.triangle( (i-1)/2 ), Perm<3>( 2, i%2 ) ); + if (punctures == 0) { + // Already handled RP^2, so genus >= 2. + // + // The size of a minimal triangulation is 2*genus - 2. + // + // This is essentially the same as the old implementation, but without + // the punctures part of the construction (which was non-minimal). + unsigned n = 2 * genus - 2; + ans = polygon(n); + ans.triangle(0)->join(2, ans.triangle(n - 1), Perm<3>(2, 0, 1)); + for (unsigned i = 1; i < genus; ++i) + ans.triangle(2 * i - 2)->join( + 0, ans.triangle(2 * i - 1), Perm<3>()); + } else if (punctures == 1) { + // Trivially, we have genus >= 1. + // + // The size of a minimal triangulation is 2*genus - 1. + ans = polygon(2*genus - 1); + for (unsigned crosscap = 0; crosscap < genus; ++crosscap) { + if (crosscap == genus - 1) { + // The very last gluing needs to be handled differently from + // the others. + ans.triangle(2*crosscap)->join( + 0, ans.triangle(2*crosscap), Perm<3>(1, 2, 0) ); + } else { + ans.triangle(2*crosscap)->join( + 0, ans.triangle(2*crosscap + 1), Perm<3>() ); + } } } else { - unsigned n = 2 * genus + 3 * punctures - 2; - unsigned i; - ans.newTriangles(n); - for (i = 0; i < n - 1; ++i) - ans.triangle(i)->join(1, ans.triangle(i + 1), Perm<3>(1, 2)); - ans.triangle(0)->join(2, ans.triangle(n - 1), Perm<3>(2, 0, 1)); - for (i = 1; i < genus; ++i) - ans.triangle(2 * i - 2)->join(0, ans.triangle(2 * i - 1), Perm<3>()); - for (i = 0; i < punctures; ++i) - ans.triangle(2 * genus + 3 * i - 2)->join( - 0, ans.triangle(2 * genus + 3 * i), Perm<3>(1, 2)); + // All that remains are the cases where punctures >= 2. Again, + // trivially, we have genus >= 1. + // + // The size of a minimal triangulation is 2*genus + 3*punctures - 4. + // + // We construct this by starting with one puncture (which has + // 2*genus - 1 triangles), and then adding all the extra punctures by + // attaching a gadget with 3*punctures - 3 triangles. + ans = nonOrientable( genus, 1 ); + addPunctures( ans, punctures ); } return ans; } +Triangulation<2> Example<2>::polygon(unsigned n) { + // NOTE: The following routines all rely on this specific construction + // --> Example<2>::orientable() + // --> Example<2>::nonOrientable() + // --> SFSpace::construct() + Triangulation<2> ans; + ans.newTriangles(n); + for (unsigned i = 1; i < n; ++i) { + ans.triangle(i)->join( 2, ans.triangle(i - 1), Perm<3>(1, 2) ); + } + return ans; +} + +void Example<2>::addPunctures(Triangulation<2>& surf, unsigned punctures) { + // NOTE: The following routines rely on this specific construction + // --> Example<2>::orientable() + // --> Example<2>::nonOrientable() + size_t initSize = surf.size(); + surf.insertTriangulation( polygon(3*punctures - 3) ); + for (unsigned i = 0; i < punctures - 1; ++i) { + surf.triangle( initSize + 3*i )->join( + 0, surf.triangle( initSize + 3*i + 2 ), Perm<3>(1, 2) ); + } + surf.triangle(0)->join( 2, surf.triangle(initSize), Perm<3>(0, 1) ); +} + Triangulation<2> Example<2>::sphereOctahedron() { Triangulation<2> ans; diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index 6a3dbbc48..cb2ad8f8c 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -41,6 +41,7 @@ #include "regina-core.h" #include "triangulation/dim2.h" #include "triangulation/detail/example.h" +#include "manifold/sfs.h" // To make regina::SFSpace a friend. namespace regina { @@ -61,28 +62,23 @@ namespace regina { template <> class Example<2> : public detail::ExampleBase<2> { public: + /** - * Returns a triangulation of the given orientable surface. - * - * If the number of punctures is 0, then the resulting triangulation - * will be minimal (which, for positive genus, means there is exactly - * one vertex). + * Returns a minimal triangulation of the given orientable surface. * * \param genus the genus of the surface; this must be greater * than or equal to zero. * \param punctures the number of punctures in the surface; * this must be greater than or equal to zero. * \return the requested orientable surface. + * + * \author Alex He, B.B. */ static Triangulation<2> orientable( unsigned genus, unsigned punctures); /** - * Returns a triangulation of the given non-orientable surface. - * - * If the number of punctures is 0 or 1, then the resulting - * triangulation will be minimal (which, with the exception of - * the projective plane, means there is exactly one vertex). + * Returns a minimal triangulation of the given non-orientable surface. * * \param genus the non-orientable genus of the surface, i.e., * the number of crosscaps that it contains; this must be greater @@ -162,6 +158,45 @@ class Example<2> : public detail::ExampleBase<2> { * \return the Klein bottle. */ static Triangulation<2> kb(); + + private: + + /** + * Returns an oriented n-triangle polygon with (n + 2) boundary edges. + * + * The triangulation is constructed by gluing edge (01) of triangle + * i to edge (02) of triangle (i - 1), for each i from 1 to (n - 1) + * (inclusive). + * + * \param n the number of triangles used to construct the polygon. + * + * \return the polygon. + * + * \author Alex He + */ + static Triangulation<2> polygon(unsigned n); + + /** + * Adds punctures to the given once-punctured surface until it has + * the given number of punctures. + * + * This routine modifies \a surf directly. Adding the punctures + * increases the size of \a surf by `3*punctures - 3`. + * + * \pre \a surf has exactly one boundary edge, and this boundary edge + * is given by edge (01) of triangle 0. + * + * \param surf the once-punctured surface to which we should add + * extra punctures. + * \param punctures the total number of punctures that we should end + * up with. + * + * \author Alex He + */ + static void addPunctures( + Triangulation<2>& surf, unsigned punctures); + + friend class regina::SFSpace; }; inline Triangulation<2> Example<2>::sphereTetrahedron() { diff --git a/python/testsuite/skeleton.out b/python/testsuite/skeleton.out index cce22f385..b1014a8a6 100644 --- a/python/testsuite/skeleton.out +++ b/python/testsuite/skeleton.out @@ -263,11 +263,11 @@ Size of the skeleton: Triangle gluing: Triangle | gluing: (01) (02) (12) ----------+--------------------------------------- - 0 | boundary 1 (01) 2 (10) - 1 | 0 (02) 3 (01) 4 (10) - 2 | 0 (21) 2 (21) 2 (20) - 3 | 1 (02) 3 (21) 3 (20) - 4 | 1 (21) 4 (21) 4 (20) + 0 | boundary 1 (01) 1 (12) + 1 | 0 (02) 2 (01) 0 (12) + 2 | 1 (02) 3 (01) 3 (12) + 3 | 2 (02) 4 (01) 2 (12) + 4 | 3 (02) 4 (21) 4 (20) Vertices: Triangle | vertex: 0 1 2 @@ -282,27 +282,27 @@ Edges: Triangle | edge: 01 02 12 ----------+-------------------- 0 | 0 1 2 - 1 | 1 3 4 - 2 | 2 5 5 - 3 | 3 6 6 - 4 | 4 7 7 + 1 | 1 3 2 + 2 | 3 4 5 + 3 | 4 6 5 + 4 | 6 7 7 -Vertex 0, boundary, degree 15: 0 (0), 1 (0), 3 (0), 3 (2), 3 (1), 1 (2), 4 (0), 4 (2), 4 (1), 1 (1), 0 (2), 2 (0), 2 (2), 2 (1), 0 (1) +Vertex 0, boundary, degree 15: 0 (0), 1 (0), 2 (0), 3 (0), 4 (0), 4 (2), 4 (1), 3 (2), 2 (2), 3 (1), 2 (1), 1 (2), 0 (2), 1 (1), 0 (1) Edge 0, boundary: 0 (01) Edge 1, internal: 1 (01), 0 (02) -Edge 2, internal: 0 (12), 2 (10) +Edge 2, internal: 0 (12), 1 (12) -Edge 3, internal: 3 (01), 1 (02) +Edge 3, internal: 1 (02), 2 (01) -Edge 4, internal: 1 (12), 4 (10) +Edge 4, internal: 2 (02), 3 (01) -Edge 5, internal: 2 (21), 2 (02) +Edge 5, internal: 3 (12), 2 (12) -Edge 6, internal: 3 (21), 3 (02) +Edge 6, internal: 4 (01), 3 (02) Edge 7, internal: 4 (21), 4 (02)