From 2e3ea5cf2373c8753887a2cb398baa48cb313bc1 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 02:47:03 +1000 Subject: [PATCH 01/13] Preparation for new SFS constructions. --- engine/manifold/sfs.cpp | 25 ++++++++++++++++++++++++ engine/testsuite/dim2/triangulation2.cpp | 2 ++ engine/testsuite/manifold/sfs.cpp | 2 ++ engine/triangulation/example2.h | 23 +++++++++++++++++----- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/engine/manifold/sfs.cpp b/engine/manifold/sfs.cpp index 40998bbdd6..5c500e9de9 100644 --- a/engine/manifold/sfs.cpp +++ b/engine/manifold/sfs.cpp @@ -39,6 +39,7 @@ #include "maths/numbertheory.h" #include "subcomplex/satannulus.h" #include "triangulation/dim3.h" +#include "triangulation/example2.h" // for Example<2>::disc() #include "utilities/exception.h" namespace regina { @@ -706,6 +707,28 @@ std::strong_ordering SFSpace::operator <=> (const SFSpace& rhs) const { return std::strong_ordering::equal; } +// ------------------------------------------------------------------------ +// Supporting classes for construct() +// ------------------------------------------------------------------------ +namespace { + + //TODO + + /** + */ + class OrientableCircleBundle { + }; // class OrientableCircleBundle + + /** + */ + class TriPrism { + }; // class TriPrism + +} // anonymous namespace + +// ------------------------------------------------------------------------ +// Implementation of construct() +// ------------------------------------------------------------------------ Triangulation<3> SFSpace::construct() const { // Things that we don't deal with just yet. if (punctures_ || puncturesTwisted_ || reflectors_ || reflectorsTwisted_) @@ -717,6 +740,8 @@ Triangulation<3> SFSpace::construct() const { if (auto lens = isLensSpace()) return lens->construct(); + //TODO The plan is to construct orientable SFS over any base. + // Currently we work over the 2-sphere only. if (genus_ != 0 || class_ != Class::o1) throw NotImplemented("SFSpace::construct() is currently not " diff --git a/engine/testsuite/dim2/triangulation2.cpp b/engine/testsuite/dim2/triangulation2.cpp index 5d60c53a29..7c5a6dd685 100644 --- a/engine/testsuite/dim2/triangulation2.cpp +++ b/engine/testsuite/dim2/triangulation2.cpp @@ -37,6 +37,8 @@ using regina::Triangulation; using regina::Example; +//TODO Test new Example<2> constructions. + class Dim2Test : public TriangulationTest<2> { protected: // Closed orientable triangulations: diff --git a/engine/testsuite/manifold/sfs.cpp b/engine/testsuite/manifold/sfs.cpp index 82ec61a9bc..670ecccbfe 100644 --- a/engine/testsuite/manifold/sfs.cpp +++ b/engine/testsuite/manifold/sfs.cpp @@ -35,6 +35,8 @@ using regina::SFSFibre; using regina::SFSpace; +//TODO Test new SFS constructions. + void verifyName(SFSpace::Class c, size_t genus, size_t punctures, size_t puncturesTwisted, size_t reflectors, size_t reflectorsTwisted, diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index 6a3dbbc483..08ab19bd6f 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -61,6 +61,9 @@ namespace regina { template <> class Example<2> : public detail::ExampleBase<2> { public: + + //TODO The plan is for orientable() to always return minimal. + /** * Returns a triangulation of the given orientable surface. * @@ -77,6 +80,8 @@ class Example<2> : public detail::ExampleBase<2> { static Triangulation<2> orientable( unsigned genus, unsigned punctures); + //TODO The plan is for nonOrientable() to always return minimal. + /** * Returns a triangulation of the given non-orientable surface. * @@ -114,13 +119,19 @@ class Example<2> : public detail::ExampleBase<2> { static Triangulation<2> sphereOctahedron(); /** - * Returns a one-triangle disc. - * This is identical to the triangulation returned by the generic - * routine ball(). + * Returns an n-triangle disc (n = 1 by default). + * + * The default 1-triangle disc is identical to the triangulation + * returned by the generic routine ball(). + * + * This routine always returns an oriented triangulation of an + * (n + 2)-sided polygon. In particular, the returned + * triangulation always uses the minimum number of edge + * identifications for an n-triangle disc. * * \return the disc. */ - static Triangulation<2> disc(); + static Triangulation<2> disc(unsigned n = 1); /** * Returns a two-triangle annulus. @@ -176,7 +187,9 @@ inline Triangulation<2> Example<2>::kb() { return twistedSphereBundle(); } -inline Triangulation<2> Example<2>::disc() { + +inline Triangulation<2> Example<2>::disc(unsigned n) { + //TODO Construct polygonal discs with any number of triangles. return ball(); } From 95177359ab37ac418857a3642077dd3184d62c99 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 10:59:36 +1000 Subject: [PATCH 02/13] More preparation. --- engine/manifold/sfs.cpp | 2 +- engine/triangulation/example2.cpp | 4 ++++ engine/triangulation/example2.h | 35 +++++++++++++++++++------------ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/engine/manifold/sfs.cpp b/engine/manifold/sfs.cpp index 5c500e9de9..520041107d 100644 --- a/engine/manifold/sfs.cpp +++ b/engine/manifold/sfs.cpp @@ -39,7 +39,7 @@ #include "maths/numbertheory.h" #include "subcomplex/satannulus.h" #include "triangulation/dim3.h" -#include "triangulation/example2.h" // for Example<2>::disc() +#include "triangulation/example2.h" // for Example<2>::polygon() #include "utilities/exception.h" namespace regina { diff --git a/engine/triangulation/example2.cpp b/engine/triangulation/example2.cpp index baf814f1ee..cd988e272e 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -127,6 +127,10 @@ Triangulation<2> Example<2>::nonOrientable(unsigned genus, unsigned punctures) { return ans; } +Triangulation<2> Example<2>::polygon(unsigned n) { + //TODO +} + Triangulation<2> Example<2>::sphereOctahedron() { Triangulation<2> ans; diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index 08ab19bd6f..83b159e44b 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -119,19 +119,13 @@ class Example<2> : public detail::ExampleBase<2> { static Triangulation<2> sphereOctahedron(); /** - * Returns an n-triangle disc (n = 1 by default). - * - * The default 1-triangle disc is identical to the triangulation - * returned by the generic routine ball(). - * - * This routine always returns an oriented triangulation of an - * (n + 2)-sided polygon. In particular, the returned - * triangulation always uses the minimum number of edge - * identifications for an n-triangle disc. + * Returns a one-triangle disc. + * This is identical to the triangulation returned by the generic + * routine ball(). * * \return the disc. */ - static Triangulation<2> disc(unsigned n = 1); + static Triangulation<2> disc(); /** * Returns a two-triangle annulus. @@ -173,6 +167,23 @@ 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. + */ + static Triangulation<2> polygon(unsigned n); + + friend class regina::SFSpace; }; inline Triangulation<2> Example<2>::sphereTetrahedron() { @@ -187,9 +198,7 @@ inline Triangulation<2> Example<2>::kb() { return twistedSphereBundle(); } - -inline Triangulation<2> Example<2>::disc(unsigned n) { - //TODO Construct polygonal discs with any number of triangles. +inline Triangulation<2> Example<2>::disc() { return ball(); } From 089f6baa62bad1529f3d83d378283ece7282c5cf Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 11:34:47 +1000 Subject: [PATCH 03/13] Private polygon() routine. --- engine/triangulation/example2.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/engine/triangulation/example2.cpp b/engine/triangulation/example2.cpp index cd988e272e..7ad41a110b 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -128,7 +128,16 @@ Triangulation<2> Example<2>::nonOrientable(unsigned genus, unsigned punctures) { } Triangulation<2> Example<2>::polygon(unsigned n) { - //TODO + // 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; } Triangulation<2> Example<2>::sphereOctahedron() { From 7c451beaa4a5f59bcb7af05417fc3b2fb5fa0106 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 11:54:30 +1000 Subject: [PATCH 04/13] Make orientable() minimal. --- engine/triangulation/example2.cpp | 99 ++++++++++++++++++++++++------- engine/triangulation/example2.h | 35 ++++++----- 2 files changed, 97 insertions(+), 37 deletions(-) diff --git a/engine/triangulation/example2.cpp b/engine/triangulation/example2.cpp index 7ad41a110b..cb90d42b29 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -35,39 +35,82 @@ 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)); + //TODO Merge the punctures == 0 and punctures == 1 cases? + 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 ( handle == genus - 1 && 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; @@ -140,6 +183,16 @@ Triangulation<2> Example<2>::polygon(unsigned n) { return ans; } +void Example<2>::addPunctures(Triangulation<2> surf, unsigned punctures) { + 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 83b159e44b..fc357a306d 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 { @@ -62,14 +63,8 @@ template <> class Example<2> : public detail::ExampleBase<2> { public: - //TODO The plan is for orientable() to always return minimal. - /** - * 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. @@ -80,14 +75,8 @@ class Example<2> : public detail::ExampleBase<2> { static Triangulation<2> orientable( unsigned genus, unsigned punctures); - //TODO The plan is for nonOrientable() to always return minimal. - /** - * 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 @@ -183,6 +172,24 @@ class Example<2> : public detail::ExampleBase<2> { */ 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. + */ + static void addPunctures( + Triangulation<2> surf, unsigned punctures); + friend class regina::SFSpace; }; From c1571bef0c18ad87def2f3c52c05574c16b3f375 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 17:17:19 +1000 Subject: [PATCH 05/13] Test and fix new version of orientable(). --- engine/testsuite/dim2/triangulation2.cpp | 57 ++++++++++++++++++++++++ engine/triangulation/example2.cpp | 8 ++-- engine/triangulation/example2.h | 2 +- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/engine/testsuite/dim2/triangulation2.cpp b/engine/testsuite/dim2/triangulation2.cpp index 7c5a6dd685..0d9f62b3c1 100644 --- a/engine/testsuite/dim2/triangulation2.cpp +++ b/engine/testsuite/dim2/triangulation2.cpp @@ -49,6 +49,13 @@ class Dim2Test : public TriangulationTest<2> { // Closed non-orientable triangulations: TestCase rp2 { Example<2>::rp2(), "RP^2" }; + // 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" }; + // Disconnected triangulations (we build these in the constructor): TestCase disjoint2 { {}, "Torus U Mobius" }; TestCase disjoint3 { {}, "KB U Annulus U S^2" }; @@ -74,6 +81,11 @@ class Dim2Test : public TriangulationTest<2> { f(rp2.tri, rp2.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); } }; @@ -97,6 +109,11 @@ TEST_F(Dim2Test, validity) { verifyValid(rp2); verifyValid(disjoint2); verifyValid(disjoint3); + + // Minimal bounded triangulations. + verifyValid(pants); + verifyValid(orPunc1); + verifyValid(orPunc3); } TEST_F(Dim2Test, connectivity) { TriangulationTest<2>::connectivityGenericCases(); @@ -106,6 +123,11 @@ TEST_F(Dim2Test, connectivity) { EXPECT_TRUE(rp2.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()); } TEST_F(Dim2Test, orientability) { TriangulationTest<2>::orientabilityGenericCases(); @@ -115,6 +137,11 @@ TEST_F(Dim2Test, orientability) { EXPECT_FALSE(rp2.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()); } TEST_F(Dim2Test, orientedExamples) { // Ensure that the orientable Example<2> constructions are oriented. @@ -130,6 +157,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) { @@ -140,6 +172,11 @@ TEST_F(Dim2Test, eulerChar) { EXPECT_EQ(rp2.tri.eulerCharTri(), 1); 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); } TEST_F(Dim2Test, boundaryBasic) { TriangulationTest<2>::boundaryBasicGenericCases(); @@ -149,6 +186,11 @@ TEST_F(Dim2Test, boundaryBasic) { verifyBoundaryBasic(rp2, {}, {}, {}); verifyBoundaryBasic(disjoint2, {0}, {}, {}); verifyBoundaryBasic(disjoint3, {0, 0}, {}, {}); + + // Minimal bounded triangulations. + verifyBoundaryBasic(pants, {0, 0, 0}, {}, {}); + verifyBoundaryBasic(orPunc1, {0}, {}, {}); + verifyBoundaryBasic(orPunc3, {0, 0, 0}, {}, {}); } TEST_F(Dim2Test, vertexLinksBasic) { TriangulationTest<2>::vertexLinksBasicGenericCases(); @@ -158,6 +200,11 @@ TEST_F(Dim2Test, vertexLinksBasic) { verifyVertexLinksBasic(rp2, 2, 0); verifyVertexLinksBasic(disjoint2, 1, 1); verifyVertexLinksBasic(disjoint3, 4, 2); + + // Minimal bounded triangulations. + verifyVertexLinksBasic(pants, 0, 3); + verifyVertexLinksBasic(orPunc1, 0, 1); + verifyVertexLinksBasic(orPunc3, 0, 3); } TEST_F(Dim2Test, orient) { testManualCases(TriangulationTest<2>::verifyOrient); @@ -219,6 +266,11 @@ TEST_F(Dim2Test, homologyH1) { EXPECT_EQ(rp2.tri.homology<1>(), regina::AbelianGroup(0, {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)); } TEST_F(Dim2Test, fundGroup) { TriangulationTest<2>::fundGroupGenericCases(); @@ -228,6 +280,11 @@ TEST_F(Dim2Test, fundGroup) { EXPECT_EQ(rp2.tri.group().recogniseGroup(), "Z_2"); // 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)"); } TEST_F(Dim2Test, chainComplex) { testManualCases(TriangulationTest<2>::verifyChainComplex); diff --git a/engine/triangulation/example2.cpp b/engine/triangulation/example2.cpp index cb90d42b29..85e33f6e6c 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -49,7 +49,6 @@ Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { Triangulation<2> ans; - //TODO Merge the punctures == 0 and punctures == 1 cases? if (punctures == 0) { // Already handled the sphere, so genus >= 1. // @@ -74,7 +73,7 @@ Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { ans = polygon( 4*genus - 1 ); for (unsigned handle = 0; handle < genus; ++handle) { for (unsigned faceIndex : {4*handle, 4*handle + 1}) { - if ( handle == genus - 1 && faceIndex == 4*genus - 3 ) { + if ( faceIndex == 4*genus - 3 ) { // The very last gluing needs to be handled differently // from the others. ans.triangle(faceIndex)->join( @@ -183,7 +182,10 @@ Triangulation<2> Example<2>::polygon(unsigned n) { return ans; } -void Example<2>::addPunctures(Triangulation<2> surf, unsigned punctures) { +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) { diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index fc357a306d..910a1af960 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -188,7 +188,7 @@ class Example<2> : public detail::ExampleBase<2> { * up with. */ static void addPunctures( - Triangulation<2> surf, unsigned punctures); + Triangulation<2>& surf, unsigned punctures); friend class regina::SFSpace; }; From 8895bd609884956e4624802f087289babcf7445e Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 21:14:54 +1000 Subject: [PATCH 06/13] Make LinkTest recognise another trefoil complement iso sig. --- engine/testsuite/link/link.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp index bf619c3213..1e0741b5a7 100644 --- a/engine/testsuite/link/link.cpp +++ b/engine/testsuite/link/link.cpp @@ -66,7 +66,8 @@ static bool isTrefoilComplement(const Triangulation<3>& tri) { for (const char* s : { "cPcbbbadh", "cPcbbbadu", "dLQbcbcdlcj", "dLQbcbcdlcn", "dLQabccbrwj", "dLQabccbrwn", - "eLAkbbcddaikhc", "eLAkbbcddainqv", "eLAkbcbddducqn", "eLAkbcbdddmcxj" }) + "eLAkbbcddaikhc", "eLAkbbcddainqv", "eLAkbcbddducqn", + "eLAkbcbdddmcxj", "gLLMQaeefedfbaapgjr" }) if (sig == s) return true; From 72f68cc3c88ecac9ecd10ca549a5a22abab5b832 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 21:49:25 +1000 Subject: [PATCH 07/13] Test that orientable() actually returns minimal triangulations. --- engine/testsuite/dim2/triangulation2.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/engine/testsuite/dim2/triangulation2.cpp b/engine/testsuite/dim2/triangulation2.cpp index 0d9f62b3c1..fbd37e9421 100644 --- a/engine/testsuite/dim2/triangulation2.cpp +++ b/engine/testsuite/dim2/triangulation2.cpp @@ -298,3 +298,12 @@ 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(pants.tri.size(), 5); + EXPECT_EQ(orPunc1.tri.size(), 7); + EXPECT_EQ(orPunc3.tri.size(), 13); +} From b54ddaaa11b543673e103336c86688e53e0cc4b6 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 22:24:06 +1000 Subject: [PATCH 08/13] Make nonOrientable() minimal. --- engine/testsuite/dim2/triangulation2.cpp | 41 ++++++++++++- engine/triangulation/example2.cpp | 77 ++++++++++++------------ engine/triangulation/example2.h | 6 ++ 3 files changed, 83 insertions(+), 41 deletions(-) diff --git a/engine/testsuite/dim2/triangulation2.cpp b/engine/testsuite/dim2/triangulation2.cpp index fbd37e9421..013910cfcb 100644 --- a/engine/testsuite/dim2/triangulation2.cpp +++ b/engine/testsuite/dim2/triangulation2.cpp @@ -37,8 +37,6 @@ using regina::Triangulation; using regina::Example; -//TODO Test new Example<2> constructions. - class Dim2Test : public TriangulationTest<2> { protected: // Closed orientable triangulations: @@ -48,6 +46,7 @@ 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" }; @@ -56,6 +55,12 @@ class Dim2Test : public TriangulationTest<2> { 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" }; TestCase disjoint3 { {}, "KB U Annulus U S^2" }; @@ -79,6 +84,7 @@ 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); @@ -86,6 +92,8 @@ class Dim2Test : public TriangulationTest<2> { 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); } }; @@ -107,6 +115,7 @@ TEST_F(Dim2Test, validity) { verifyValid(s2Oct); verifyValid(torus2); verifyValid(rp2); + verifyValid(kb); verifyValid(disjoint2); verifyValid(disjoint3); @@ -114,6 +123,8 @@ TEST_F(Dim2Test, validity) { verifyValid(pants); verifyValid(orPunc1); verifyValid(orPunc3); + verifyValid(norPunc1); + verifyValid(norPunc3); } TEST_F(Dim2Test, connectivity) { TriangulationTest<2>::connectivityGenericCases(); @@ -121,6 +132,7 @@ 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()); @@ -128,6 +140,8 @@ TEST_F(Dim2Test, connectivity) { 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(); @@ -135,6 +149,7 @@ 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()); @@ -142,6 +157,8 @@ TEST_F(Dim2Test, orientability) { 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. @@ -170,6 +187,7 @@ 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); @@ -177,6 +195,8 @@ TEST_F(Dim2Test, eulerChar) { 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(); @@ -184,6 +204,7 @@ TEST_F(Dim2Test, boundaryBasic) { verifyBoundaryBasic(s2Oct, {}, {}, {}); verifyBoundaryBasic(torus2, {}, {}, {}); verifyBoundaryBasic(rp2, {}, {}, {}); + verifyBoundaryBasic(kb, {}, {}, {}); verifyBoundaryBasic(disjoint2, {0}, {}, {}); verifyBoundaryBasic(disjoint3, {0, 0}, {}, {}); @@ -191,6 +212,8 @@ TEST_F(Dim2Test, boundaryBasic) { 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(); @@ -198,6 +221,7 @@ 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); @@ -205,6 +229,8 @@ TEST_F(Dim2Test, vertexLinksBasic) { 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); @@ -264,6 +290,7 @@ 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})); @@ -271,6 +298,8 @@ TEST_F(Dim2Test, homologyH1) { 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(); @@ -278,6 +307,8 @@ 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. @@ -285,6 +316,8 @@ TEST_F(Dim2Test, fundGroup) { 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); @@ -303,7 +336,11 @@ TEST_F(Dim2Test, minimalSize) { 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 85e33f6e6c..f6298e3e43 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -123,47 +123,46 @@ 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; diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index 910a1af960..cb2ad8f8c0 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -71,6 +71,8 @@ class Example<2> : public detail::ExampleBase<2> { * \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); @@ -169,6 +171,8 @@ class Example<2> : public detail::ExampleBase<2> { * \param n the number of triangles used to construct the polygon. * * \return the polygon. + * + * \author Alex He */ static Triangulation<2> polygon(unsigned n); @@ -186,6 +190,8 @@ class Example<2> : public detail::ExampleBase<2> { * 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); From f75036e13694517a03ef01a82c343105472dcab8 Mon Sep 17 00:00:00 2001 From: alex-he Date: Mon, 10 Nov 2025 23:09:59 +1000 Subject: [PATCH 09/13] Update python skeleton test that uses nonOrientable(). --- python/testsuite/skeleton.out | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/python/testsuite/skeleton.out b/python/testsuite/skeleton.out index cce22f3857..b1014a8a6a 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) From 3f02fa24200d311a76f0b34aca13f1bff2b0a97a Mon Sep 17 00:00:00 2001 From: alex-he Date: Sun, 8 Mar 2026 14:06:10 +1100 Subject: [PATCH 10/13] Remove code from separate unfinished feature. --- engine/manifold/sfs.cpp | 25 ------------------------- engine/testsuite/manifold/sfs.cpp | 2 -- 2 files changed, 27 deletions(-) diff --git a/engine/manifold/sfs.cpp b/engine/manifold/sfs.cpp index 520041107d..40998bbdd6 100644 --- a/engine/manifold/sfs.cpp +++ b/engine/manifold/sfs.cpp @@ -39,7 +39,6 @@ #include "maths/numbertheory.h" #include "subcomplex/satannulus.h" #include "triangulation/dim3.h" -#include "triangulation/example2.h" // for Example<2>::polygon() #include "utilities/exception.h" namespace regina { @@ -707,28 +706,6 @@ std::strong_ordering SFSpace::operator <=> (const SFSpace& rhs) const { return std::strong_ordering::equal; } -// ------------------------------------------------------------------------ -// Supporting classes for construct() -// ------------------------------------------------------------------------ -namespace { - - //TODO - - /** - */ - class OrientableCircleBundle { - }; // class OrientableCircleBundle - - /** - */ - class TriPrism { - }; // class TriPrism - -} // anonymous namespace - -// ------------------------------------------------------------------------ -// Implementation of construct() -// ------------------------------------------------------------------------ Triangulation<3> SFSpace::construct() const { // Things that we don't deal with just yet. if (punctures_ || puncturesTwisted_ || reflectors_ || reflectorsTwisted_) @@ -740,8 +717,6 @@ Triangulation<3> SFSpace::construct() const { if (auto lens = isLensSpace()) return lens->construct(); - //TODO The plan is to construct orientable SFS over any base. - // Currently we work over the 2-sphere only. if (genus_ != 0 || class_ != Class::o1) throw NotImplemented("SFSpace::construct() is currently not " diff --git a/engine/testsuite/manifold/sfs.cpp b/engine/testsuite/manifold/sfs.cpp index 670ecccbfe..82ec61a9bc 100644 --- a/engine/testsuite/manifold/sfs.cpp +++ b/engine/testsuite/manifold/sfs.cpp @@ -35,8 +35,6 @@ using regina::SFSFibre; using regina::SFSpace; -//TODO Test new SFS constructions. - void verifyName(SFSpace::Class c, size_t genus, size_t punctures, size_t puncturesTwisted, size_t reflectors, size_t reflectorsTwisted, From 44092f839114987c1fe01faabebd11a8876c2263 Mon Sep 17 00:00:00 2001 From: alex-he Date: Wed, 17 Jun 2026 15:27:43 +1000 Subject: [PATCH 11/13] Cleaning up. --- engine/triangulation/example2.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index 4803758c4b..a6eedbc7bb 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -41,7 +41,6 @@ #include "regina-core.h" #include "triangulation/dim2.h" #include "triangulation/detail/example.h" -#include "manifold/sfs.h" // To make regina::SFSpace a friend. ENSURE_ESSENTIAL_REGINA_HEADERS @@ -199,8 +198,6 @@ class Example<2> : public detail::ExampleBase<2> { */ static void addPunctures( Triangulation<2>& surf, unsigned punctures); - - friend class regina::SFSpace; }; inline Triangulation<2> Example<2>::sphereTetrahedron() { From 64c3894fbd17aca9df36687e7b59c19cba192925 Mon Sep 17 00:00:00 2001 From: alex-he Date: Wed, 17 Jun 2026 15:50:47 +1000 Subject: [PATCH 12/13] Pull in changes from branch 'construct-sfs' that belong here. --- .../triangulation/dim2/triangulation2.cpp | 66 ++++++++++++++ engine/triangulation/example2.cpp | 86 ++++++++++++------- engine/triangulation/example2.h | 24 +++--- 3 files changed, 137 insertions(+), 39 deletions(-) diff --git a/engine/testsuite/triangulation/dim2/triangulation2.cpp b/engine/testsuite/triangulation/dim2/triangulation2.cpp index fb6849ae44..ad1b4163e6 100644 --- a/engine/testsuite/triangulation/dim2/triangulation2.cpp +++ b/engine/testsuite/triangulation/dim2/triangulation2.cpp @@ -61,6 +61,12 @@ class Dim2Test : public TriangulationTest<2> { TestCase norPunc3 { Example<2>::nonOrientable(2, 3), "Non-or, g=2 + 3 punctures" }; + // Polygons. + TestCase poly1 { Example<2>::polygon(1), "Polygon, 1 side" }; + TestCase poly2 { Example<2>::polygon(2), "Polygon, 2 sides" }; + TestCase poly3 { Example<2>::polygon(3), "Polygon, 3 sides" }; + TestCase poly4 { Example<2>::polygon(4), "Polygon, 4 sides" }; + // Disconnected triangulations (we build these in the constructor): TestCase disjoint2 { {}, "Torus U Mobius" }; TestCase disjoint3 { {}, "KB U Annulus U S^2" }; @@ -94,6 +100,12 @@ class Dim2Test : public TriangulationTest<2> { f(orPunc3.tri, orPunc3.name); f(norPunc1.tri, norPunc1.name); f(norPunc3.tri, norPunc3.name); + + // Polygons. + f(poly1.tri, poly1.name); + f(poly2.tri, poly2.name); + f(poly3.tri, poly3.name); + f(poly4.tri, poly4.name); } }; @@ -125,6 +137,12 @@ TEST_F(Dim2Test, validity) { verifyValid(orPunc3); verifyValid(norPunc1); verifyValid(norPunc3); + + // Polygons. + verifyValid(poly1); + verifyValid(poly2); + verifyValid(poly3); + verifyValid(poly4); } TEST_F(Dim2Test, connectivity) { TriangulationTest<2>::connectivityGenericCases(); @@ -142,6 +160,12 @@ TEST_F(Dim2Test, connectivity) { EXPECT_TRUE(orPunc3.tri.isConnected()); EXPECT_TRUE(norPunc1.tri.isConnected()); EXPECT_TRUE(norPunc3.tri.isConnected()); + + // Polygons. + EXPECT_TRUE(poly1.tri.isConnected()); + EXPECT_TRUE(poly2.tri.isConnected()); + EXPECT_TRUE(poly3.tri.isConnected()); + EXPECT_TRUE(poly4.tri.isConnected()); } TEST_F(Dim2Test, orientability) { TriangulationTest<2>::orientabilityGenericCases(); @@ -159,6 +183,12 @@ TEST_F(Dim2Test, orientability) { EXPECT_TRUE(orPunc3.tri.isOrientable()); EXPECT_FALSE(norPunc1.tri.isOrientable()); EXPECT_FALSE(norPunc3.tri.isOrientable()); + + // Polygons. + EXPECT_TRUE(poly1.tri.isOrientable()); + EXPECT_TRUE(poly2.tri.isOrientable()); + EXPECT_TRUE(poly3.tri.isOrientable()); + EXPECT_TRUE(poly4.tri.isOrientable()); } TEST_F(Dim2Test, orientedExamples) { // Ensure that the orientable Example<2> constructions are oriented. @@ -179,6 +209,12 @@ TEST_F(Dim2Test, orientedExamples) { EXPECT_TRUE(pants.tri.isOriented()); EXPECT_TRUE(orPunc1.tri.isOriented()); EXPECT_TRUE(orPunc3.tri.isOriented()); + + // Polygons. + EXPECT_TRUE(poly1.tri.isOriented()); + EXPECT_TRUE(poly2.tri.isOriented()); + EXPECT_TRUE(poly3.tri.isOriented()); + EXPECT_TRUE(poly4.tri.isOriented()); } TEST_F(Dim2Test, eulerChar) { @@ -197,6 +233,12 @@ TEST_F(Dim2Test, eulerChar) { EXPECT_EQ(orPunc3.tri.eulerCharTri(), -5); EXPECT_EQ(norPunc1.tri.eulerCharTri(), -1); EXPECT_EQ(norPunc3.tri.eulerCharTri(), -3); + + // Polygons. + EXPECT_EQ(poly1.tri.eulerCharTri(), 1); + EXPECT_EQ(poly2.tri.eulerCharTri(), 1); + EXPECT_EQ(poly3.tri.eulerCharTri(), 1); + EXPECT_EQ(poly4.tri.eulerCharTri(), 1); } TEST_F(Dim2Test, boundaryBasic) { TriangulationTest<2>::boundaryBasicGenericCases(); @@ -214,6 +256,12 @@ TEST_F(Dim2Test, boundaryBasic) { verifyBoundaryBasic(orPunc3, {0, 0, 0}, {}, {}); verifyBoundaryBasic(norPunc1, {0}, {}, {}); verifyBoundaryBasic(norPunc3, {0, 0, 0}, {}, {}); + + // Polygons. + verifyBoundaryBasic(poly1, {0}, {}, {}); + verifyBoundaryBasic(poly2, {0}, {}, {}); + verifyBoundaryBasic(poly3, {0}, {}, {}); + verifyBoundaryBasic(poly4, {0}, {}, {}); } TEST_F(Dim2Test, vertexLinksBasic) { TriangulationTest<2>::vertexLinksBasicGenericCases(); @@ -231,6 +279,12 @@ TEST_F(Dim2Test, vertexLinksBasic) { verifyVertexLinksBasic(orPunc3, 0, 3); verifyVertexLinksBasic(norPunc1, 0, 1); verifyVertexLinksBasic(norPunc3, 0, 3); + + // Polygons. + verifyVertexLinksBasic(poly1, 1, 1); + verifyVertexLinksBasic(poly2, 1, 2); + verifyVertexLinksBasic(poly3, 0, 3); + verifyVertexLinksBasic(poly4, 0, 4); } TEST_F(Dim2Test, orient) { testManualCases(TriangulationTest<2>::verifyOrient); @@ -303,6 +357,12 @@ TEST_F(Dim2Test, homologyH1) { 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)); + + // Polygons. + EXPECT_EQ(poly1.tri.homology<1>(), regina::AbelianGroup()); + EXPECT_EQ(poly2.tri.homology<1>(), regina::AbelianGroup()); + EXPECT_EQ(poly3.tri.homology<1>(), regina::AbelianGroup()); + EXPECT_EQ(poly4.tri.homology<1>(), regina::AbelianGroup()); } TEST_F(Dim2Test, fundGroup) { TriangulationTest<2>::fundGroupGenericCases(); @@ -321,6 +381,12 @@ TEST_F(Dim2Test, fundGroup) { EXPECT_EQ(orPunc3.tri.group().recogniseGroup(), "Free(6)"); EXPECT_EQ(norPunc1.tri.group().recogniseGroup(), "Free(2)"); EXPECT_EQ(norPunc3.tri.group().recogniseGroup(), "Free(4)"); + + // Polygons. + EXPECT_EQ(poly1.tri.group().recogniseGroup(), "0"); + EXPECT_EQ(poly2.tri.group().recogniseGroup(), "0"); + EXPECT_EQ(poly3.tri.group().recogniseGroup(), "0"); + EXPECT_EQ(poly4.tri.group().recogniseGroup(), "0"); } TEST_F(Dim2Test, chainComplex) { testManualCases(TriangulationTest<2>::verifyChainComplex); diff --git a/engine/triangulation/example2.cpp b/engine/triangulation/example2.cpp index 38ee525c91..314e4bf090 100644 --- a/engine/triangulation/example2.cpp +++ b/engine/triangulation/example2.cpp @@ -34,7 +34,7 @@ namespace regina { -Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { +Triangulation<2> Example<2>::orientable(size_t genus, size_t punctures) { // Small cases that don't fit into the general constructions below. if (genus == 0) { if (punctures == 0) { @@ -52,15 +52,17 @@ Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { if (punctures == 0) { // Already handled the sphere, so genus >= 1. // - // The size of a minimal triangulation is 4*genus - 2. + // The size of a minimal triangulation is n := 4*genus - 2, and we can + // build such a triangulation by gluing pairs of boundary edges of an + // (n + 2)-sided polygon. // // 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); + size_t n = 4 * genus - 2; + ans = polygon(n + 2); 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 (unsigned i = 1; i < genus; ++i) { + for (size_t 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), @@ -69,10 +71,12 @@ Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { } 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}) { + // The size of a minimal triangulation is n := 4*genus - 1, and we can + // build such a triangulation by gluing pairs of boundary edges of an + // (n + 2)-sided polygon. + ans = polygon( 4*genus + 1 ); + for (size_t handle = 0; handle < genus; ++handle) { + for (size_t faceIndex : {4*handle, 4*handle + 1}) { if ( faceIndex == 4*genus - 3 ) { // The very last gluing needs to be handled differently // from the others. @@ -87,9 +91,11 @@ Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { } 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) { + // The size of a minimal triangulation is n := 3*punctures - 4, and we + // can build such a triangulation by gluing pairs of boundary edges of + // an (n + 2)-sided polygon. + ans = polygon( 3*punctures - 2 ); + for (size_t i = 0; i < punctures - 1; ++i) { if (i == punctures - 2) { // The very last gluing needs to be handled differently // from the others. @@ -115,7 +121,7 @@ Triangulation<2> Example<2>::orientable(unsigned genus, unsigned punctures) { return ans; } -Triangulation<2> Example<2>::nonOrientable(unsigned genus, unsigned punctures) { +Triangulation<2> Example<2>::nonOrientable(size_t genus, size_t punctures) { if (genus == 0) return orientable(0, punctures); // Just in case. *shrug* if (genus == 1 && punctures == 0) @@ -126,22 +132,26 @@ Triangulation<2> Example<2>::nonOrientable(unsigned genus, unsigned punctures) { if (punctures == 0) { // Already handled RP^2, so genus >= 2. // - // The size of a minimal triangulation is 2*genus - 2. + // The size of a minimal triangulation is n := 2*genus - 2, and we can + // build such a triangulation by gluing pairs of boundary edges of an + // (n + 2)-sided polygon. // // 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); + size_t n = 2 * genus - 2; + ans = polygon(n + 2); ans.triangle(0)->join(2, ans.triangle(n - 1), Perm<3>(2, 0, 1)); - for (unsigned i = 1; i < genus; ++i) + for (size_t 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) { + // The size of a minimal triangulation is n := 2*genus - 1, and we can + // build such a triangulation by gluing pairs of boundary edges of an + // (n + 2)-sided polygon. + ans = polygon(2*genus + 1); + for (size_t crosscap = 0; crosscap < genus; ++crosscap) { if (crosscap == genus - 1) { // The very last gluing needs to be handled differently from // the others. @@ -168,26 +178,44 @@ Triangulation<2> Example<2>::nonOrientable(unsigned genus, unsigned punctures) { return ans; } -Triangulation<2> Example<2>::polygon(unsigned n) { - // NOTE: The following routines all rely on this specific construction +Triangulation<2> Example<2>::polygon(size_t n) { + Triangulation<2> ans; + switch (n) { + case 0: + return ans; + case 1: + ans.newTriangle(); + ans.triangle(0)->join( + 2, ans.triangle(0), Perm<3>(1, 2) ); + return ans; + case 2: + ans.newTriangles(2); + for (size_t i : {0, 1}) { + ans.triangle(i)->join( + 2, ans.triangle(1 - i), Perm<3>(1, 2) ); + } + return ans; + } + + // NOTE: The following routines rely on the specific construction below. // --> 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) ); + ans.newTriangles(n - 2); + for (size_t i = 1; i < n - 2; ++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) { +void Example<2>::addPunctures(Triangulation<2>& surf, size_t 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.insertTriangulation( polygon(3*punctures - 1) ); + for (size_t i = 0; i < punctures - 1; ++i) { surf.triangle( initSize + 3*i )->join( 0, surf.triangle( initSize + 3*i + 2 ), Perm<3>(1, 2) ); } diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index a6eedbc7bb..3151e87cfa 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -78,7 +78,7 @@ class Example<2> : public detail::ExampleBase<2> { * \author Alex He, B.B. */ static Triangulation<2> orientable( - unsigned genus, unsigned punctures); + size_t genus, size_t punctures); /** * Returns a minimal triangulation of the given non-orientable surface. @@ -93,7 +93,7 @@ class Example<2> : public detail::ExampleBase<2> { * \author Alex He, B.B. */ static Triangulation<2> nonOrientable( - unsigned genus, unsigned punctures); + size_t genus, size_t punctures); /** * Returns the four-triangle 2-sphere formed from the boundary @@ -162,22 +162,26 @@ class Example<2> : public detail::ExampleBase<2> { */ static Triangulation<2> kb(); - private: - /** - * Returns an oriented n-triangle polygon with (n + 2) boundary edges. + * Returns an oriented triangulation of an `n`-sided polygon. + * + * For `n <= 2`, this routine returns a triangulation built from `n` + * triangles. * - * 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) + * For `n >= 3`, this routine returns a triangulation built from + * `n - 2` triangles 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. + * \param n the number of boundary edges of the constructed polygon. * * \return the polygon. * * \author Alex He */ - static Triangulation<2> polygon(unsigned n); + static Triangulation<2> polygon(size_t n); + + private: /** * Adds punctures to the given once-punctured surface until it has @@ -197,7 +201,7 @@ class Example<2> : public detail::ExampleBase<2> { * \author Alex He */ static void addPunctures( - Triangulation<2>& surf, unsigned punctures); + Triangulation<2>& surf, size_t punctures); }; inline Triangulation<2> Example<2>::sphereTetrahedron() { From a7546d9c7bdf4196df6a5a253ef82686de32bbea Mon Sep 17 00:00:00 2001 From: alex-he Date: Sat, 27 Jun 2026 16:20:51 +1000 Subject: [PATCH 13/13] Fix documentation for Example<2>::polygon(), and verify the combinatorial promises that it makes. --- .../triangulation/dim2/triangulation2.cpp | 36 +++++++++++++++++++ engine/triangulation/example2.h | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/engine/testsuite/triangulation/dim2/triangulation2.cpp b/engine/testsuite/triangulation/dim2/triangulation2.cpp index ad1b4163e6..564bcac101 100644 --- a/engine/testsuite/triangulation/dim2/triangulation2.cpp +++ b/engine/testsuite/triangulation/dim2/triangulation2.cpp @@ -413,3 +413,39 @@ TEST_F(Dim2Test, minimalSize) { EXPECT_EQ(norPunc1.tri.size(), 3); EXPECT_EQ(norPunc3.tri.size(), 9); } + +static void verifyPolygon( + const TriangulationTest<2>::TestCase& test, size_t n) { + // Other tests already verify that the constructed polygons are oriented + // triangulations of topological discs. Here we just check that the + // construction fulfils its combinatorial promises. + SCOPED_TRACE_CSTRING(test.name); + + // Need to have the correct number of boundary edges. + size_t numBdryEdges = 0; + for (auto e : test.tri.edges()) { + if (e->isBoundary()) { + ++numBdryEdges; + } + } + EXPECT_EQ( numBdryEdges, n ); + + // For n <= 2, the only other promise is that size() == n. + if (n <= 2) { + EXPECT_EQ( test.tri.size(), n ); + return; + } + + // For n >= 3, polygon() promises to use a specific construction. + EXPECT_EQ( test.tri.size(), n - 2 ); + for (size_t i = 1; i < n - 2; ++i) { + EXPECT_EQ( test.tri.triangle(i)->adjacentTriangle(2)->index(), i - 1 ); + EXPECT_EQ( test.tri.triangle(i)->adjacentGluing(2), Perm<3>(1, 2) ); + } +} +TEST_F(Dim2Test, polygon) { + verifyPolygon( poly1, 1 ); + verifyPolygon( poly2, 2 ); + verifyPolygon( poly3, 3 ); + verifyPolygon( poly4, 4 ); +} diff --git a/engine/triangulation/example2.h b/engine/triangulation/example2.h index 3151e87cfa..823190e9a8 100644 --- a/engine/triangulation/example2.h +++ b/engine/triangulation/example2.h @@ -170,7 +170,7 @@ class Example<2> : public detail::ExampleBase<2> { * * For `n >= 3`, this routine returns a triangulation built from * `n - 2` triangles by gluing edge `(01)` of triangle `i` to edge - * `(02)` of triangle `i - 1`, for each `i` from 1 to `n - 1` + * `(02)` of triangle `i - 1`, for each `i` from 1 to `n - 3` * (inclusive). * * \param n the number of boundary edges of the constructed polygon.