From 61d71b1b5aab404927e4685ba6a2baf944030153 Mon Sep 17 00:00:00 2001 From: alex-he Date: Fri, 14 Nov 2025 01:32:59 +1000 Subject: [PATCH 01/11] Preparation for braid closures. --- engine/link/braid.cpp | 48 ++++++++++++++++++++++++++++++++++ engine/link/link.cpp | 2 ++ engine/link/link.h | 11 ++++++++ engine/testsuite/link/link.cpp | 4 +++ python/link/link.cpp | 1 + 5 files changed, 66 insertions(+) create mode 100644 engine/link/braid.cpp diff --git a/engine/link/braid.cpp b/engine/link/braid.cpp new file mode 100644 index 000000000..65bb59937 --- /dev/null +++ b/engine/link/braid.cpp @@ -0,0 +1,48 @@ + +/************************************************************************** + * * + * Regina - A Normal Surface Theory Calculator * + * Computational Engine * + * * + * Copyright (c) 2025, Alex He * + * For further details contact Ben Burton (bab@debian.org). * + * * + * 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. * + * * + * As an exception, when this program is distributed through (i) the * + * App Store by Apple Inc.; (ii) the Mac App Store by Apple Inc.; or * + * (iii) Google Play by Google Inc., then that store may impose any * + * digital rights management, device limits and/or redistribution * + * restrictions that are required by its terms of service. * + * * + * 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, see . * + * * + **************************************************************************/ + +#include "link/link.h" +#include + +namespace regina { + +Link Link::fromBraid(const std::string& s) { + // Work with the largest integer type that we could possibly need. + using Int = std::make_signed_t; + //TODO +} + +template +Link Link::fromBraid(Iterator begin, Iterator end) { + //TODO +} + +} // namespace regina + diff --git a/engine/link/link.cpp b/engine/link/link.cpp index 0613694a3..65cb64696 100644 --- a/engine/link/link.cpp +++ b/engine/link/link.cpp @@ -224,6 +224,8 @@ Link::Link(const std::string& description) { } catch (const InvalidArgument&) { } + //TODO Maybe parse braid words too. + throw InvalidArgument("The given string could not be interpreted " "as representing a link"); } diff --git a/engine/link/link.h b/engine/link/link.h index 895777124..96b42341f 100644 --- a/engine/link/link.h +++ b/engine/link/link.h @@ -6731,6 +6731,17 @@ class Link : template static Link fromPD(Iterator begin, Iterator end); + /** + * TODO + */ + static Link fromBraid(const std::string& str); + + /** + * TODO + */ + template + static Link fromBraid(Iterator begin, Iterator end); + /*@}*/ private: diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp index 5a02ceffb..921131fc6 100644 --- a/engine/testsuite/link/link.cpp +++ b/engine/testsuite/link/link.cpp @@ -4269,6 +4269,8 @@ TEST_F(LinkTest, pdCode) { testManualCases(verifyPDCode); } +//TODO Test fromBraid(). + TEST_F(LinkTest, invalidCode) { static const char* code = "INVALID"; @@ -4279,6 +4281,8 @@ TEST_F(LinkTest, invalidCode) { EXPECT_THROW({ Link::fromJenkins(code); }, InvalidArgument); EXPECT_THROW({ Link::fromPD(code); }, InvalidArgument); + //TODO Test fromBraid(). + // Finally, the "magic" constructor: EXPECT_THROW({ Link l(code); }, InvalidArgument); } diff --git a/python/link/link.cpp b/python/link/link.cpp index b3a7b42f8..2815bbbc1 100644 --- a/python/link/link.cpp +++ b/python/link/link.cpp @@ -233,6 +233,7 @@ void addLink(pybind11::module_& m, pybind11::module_& internal) { }, pybind11::arg("signs"), pybind11::arg("component"), rdoc::fromData) .def_static("fromKnotSig", &Link::fromKnotSig, rdoc::fromKnotSig) .def_static("fromSig", &Link::fromSig, rdoc::fromSig) + //TODO python bindings for fromBraid(). .def("swap", &Link::swap, rdoc::swap) .def("insertLink", overload_cast(&Link::insertLink), rdoc::insertLink) From 6d6c899519d8b090e22154146226ce2fa0126fd9 Mon Sep 17 00:00:00 2001 From: alex-he Date: Fri, 14 Nov 2025 15:17:59 +1000 Subject: [PATCH 02/11] Document fromBraid(). --- engine/link/link.h | 115 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/engine/link/link.h b/engine/link/link.h index 96b42341f..71e8d75f4 100644 --- a/engine/link/link.h +++ b/engine/link/link.h @@ -5616,6 +5616,9 @@ class Link : */ bool pdAmbiguous() const; + //TODO Use Vogel's algorithm to implement braid(), ie convert a link + // diagram into a braid word. + /** * Outputs the underlying 4-valent multigraph for this link diagram * using the PACE text format. This format is described in detail at @@ -6732,12 +6735,120 @@ class Link : static Link fromPD(Iterator begin, Iterator end); /** - * TODO + * Creates a new classical link from a braid word, presented as a + * string. + * + * For a braid on n strands (not to be confused with strands of + * a link diagram), orient the strands from left to right, and label + * the strands in order from bottom to top by 0 to n - 1 + * (inclusive). A braid word for such an n-strand braid is + * given by a (nonempty) sequence of nonzero integers between + * 1 - n and n - 1 (inclusive), in which either + * 1 - n or n - 1 appears at least once; in other words, + * we assume that there is at least one crossing involving the + * uppermost strand (we make no such assumption for the lowermost + * strand). + * + * - A positive integer s in the braid word indicates an + * exchange of strands s - 1 and s via a positive + * crossing. + \verbatim + ___ ___ + \ / + \ + ___/ \___ + \endverbatim + * + * - A negative integer -s in the braid word indicates an + * exchange of strands s - 1 and s via a negative + * crossing. + \verbatim + ___ ___ + \ / + / + ___/ \___ + \endverbatim + * + * As an example, the braid word + \verbatim + 1 -3 -3 2 1 + \endverbatim + * describes the following 4-strand braid: + * + \verbatim + _________ ___ _______________ + \ / \ / + / / + _________/ \___/ \___ _________ + \ / + \ + ___ _______________/ \___ ___ + \ / \ / + \ \ + ___/ \_____________________/ \___ + \endverbatim + * + * The corresponding link is constructed by taking the closure of the + * braid; that is, by taking strand s on the right and joining + * it with strand s on the left, for each s between 0 and + * n - 1 (inclusive). Depending on how the braid word permutes + * the strands as we go from left to right, this produces a link with + * up to n components. For example, in the 4-strand example + * above, the closure will be a 3-component link. + * + * The conventions for braids described above are chosen to be + * consistent with those used in SnapPy 3.0/Spherogram 2.0 and newer. + * + * There are two variants of this routine. This variant takes a single + * string, where the integers have been combined together and separated + * by whitespace. The other variant takes a sequence of integers, + * defined by a pair of iterators. + * + * In this variant (the string variant), the exact form of the + * whitespace does not matter, and additional whitespace at the + * beginning or end of the string is allowed. + * + * \exception InvalidArgument The given string was not a valid braid + * word for a classical link. + * + * \author Alex He + * + * \param str a braid word for a classical link, as described above. + * \return the reconstructed link. */ static Link fromBraid(const std::string& str); /** - * TODO + * Creates a new classical link from a braid word, presented as an + * integer sequence. + * + * See fromBraid(const std::string&) for a full description of the + * notation for braid words, as well as a detailed discussion of how + * Regina constructs classical links from such notation. + * + * This routine is a variant of fromBraid(const std::string&) which, + * instead of taking a human-readable string, takes a machine-readable + * sequence of integers. This sequence is given by passing a pair of + * begin/end iterators. + * + * \pre \a Iterator is a random access iterator type, and + * dereferencing such an iterator produces a native C++ integer. + * (The specific native C++ integer type being used will be deduced + * from the type \a Iterator.) + * + * \exception InvalidArgument The given sequence was not a valid braid + * word for a classical link. + * + * \python Instead of a pair of begin and past-the-end + * iterators, this routine takes a Python list of integers. + * + * \author Alex He + * + * \param begin an iterator that points to the beginning of the + * sequence of integers for the braid word for a classical link. + * \param end an iterator that points past the end of the + * sequence of integers for the braid word for a classical link. + * \return the reconstructed link. */ template static Link fromBraid(Iterator begin, Iterator end); From f711241d5377a08eb13b2372c40351a356b072f3 Mon Sep 17 00:00:00 2001 From: alex-he Date: Fri, 14 Nov 2025 17:52:56 +1000 Subject: [PATCH 03/11] Implement iterator version of fromBraid() (part 1). --- engine/link/braid-impl.h | 127 +++++++++++++++++++++++++++++++++++++++ engine/link/braid.cpp | 6 -- engine/link/link.h | 4 ++ 3 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 engine/link/braid-impl.h diff --git a/engine/link/braid-impl.h b/engine/link/braid-impl.h new file mode 100644 index 000000000..ec36233c2 --- /dev/null +++ b/engine/link/braid-impl.h @@ -0,0 +1,127 @@ + +/************************************************************************** + * * + * Regina - A Normal Surface Theory Calculator * + * Computational Engine * + * * + * Copyright (c) 2025, Alex He * + * For further details contact Ben Burton (bab@debian.org). * + * * + * 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. * + * * + * As an exception, when this program is distributed through (i) the * + * App Store by Apple Inc.; (ii) the Mac App Store by Apple Inc.; or * + * (iii) Google Play by Google Inc., then that store may impose any * + * digital rights management, device limits and/or redistribution * + * restrictions that are required by its terms of service. * + * * + * 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, see . * + * * + **************************************************************************/ + +/*! \file link/braid-impl.h + * \brief Contains implementation details for parsing braid words. + * + * This file is automatically included from link.h; there is no need for end + * users to include it explicitly. + */ + +#ifndef __REGINA_BRAID_IMPL_H +#ifndef __DOXYGEN +#define __REGINA_BRAID_IMPL_H +#endif + +#include // For std::abs +#include + +namespace regina { + +template +Link Link::fromBraid(Iterator begin, Iterator end) { + using InputInt = std::remove_cv_t>; + static_assert(std::is_integral_v && + ! std::is_unsigned_v, "fromBraid(): the iterator type " + "needs to dereference to give a native signed C++ integer type."); + + size_t numCross = end - begin; + if (numCross == 0) { + return { 1 }; // Zero-crossing unknot. + } + + // TODO It should be possible to do everything in just a single pass + // through the braid. But is this worth the effort? + + // Make a first pass through the braid in order to: + // (a) Work out the number of "rows" in the braid (we use "rows" to avoid + // a class of terminology with "strands" of the link diagram). + // (b) Work out the signs of all the crossings of the link diagram. + size_t numRows = 0; + size_t upperRow; + size_t iCross; + InputInt s; + Link ans; + for (iCross = 0; iCross < numCross; ++iCross) { + s = begin[iCross]; + if (s == 0) { + throw InvalidArgument("fromBraid(): braid word contains 0"); + } else { + // Have we found a new uppermost row in the braid? + upperRow = static_cast( std::abs(s) ); + if (upperRow > numRows) { + numRows = upperRow; + } + + // What's the sign of crossing number iCross? + if (s > 0) { + ans.crossings_.push_back( new Crossing(1) ); + } else { + ans.crossings_.push_back( new Crossing(-1) ); + } + } + } + numRows += 1; + + // In a moment, we will make a second pass through the braid in order to + // join all the crossings together. As part of the book-keeping, we will: + // --> search for the leftmost strand in each row, and + // --> keep track of the latest strand that we have encountered in each + // row. + std::vector leftmostStrand; + std::vector latestStrand; + size_t row; + for (row = 0; row < numRows; ++row) { + leftmostStrand.emplace_back( StrandRef() ); + latestStrand.emplace_back( StrandRef() ); + } + + // As promised, go through the braid again to work out how to join all the + // crossings together. + for (iCross = 0; iCross < numCross; ++iCross) { + s = begin[iCross]; + //TODO + } + + // Close up the braid. + //TODO + + // Find all the components. + std::unordered_set unvisitedRows; + for (InputInt i = 0; i < numCross; ++i) { + unvisitedRows.insert(i); + } + //TODO +} + +} // namespace regina + +#endif + diff --git a/engine/link/braid.cpp b/engine/link/braid.cpp index 65bb59937..f8fdeb762 100644 --- a/engine/link/braid.cpp +++ b/engine/link/braid.cpp @@ -29,7 +29,6 @@ **************************************************************************/ #include "link/link.h" -#include namespace regina { @@ -39,10 +38,5 @@ Link Link::fromBraid(const std::string& s) { //TODO } -template -Link Link::fromBraid(Iterator begin, Iterator end) { - //TODO -} - } // namespace regina diff --git a/engine/link/link.h b/engine/link/link.h index 71e8d75f4..34947f158 100644 --- a/engine/link/link.h +++ b/engine/link/link.h @@ -6799,6 +6799,9 @@ class Link : * The conventions for braids described above are chosen to be * consistent with those used in SnapPy 3.0/Spherogram 2.0 and newer. * + * For the special case where the braid word is empty, this routine + * returns a zero-crossing unknot. + * * There are two variants of this routine. This variant takes a single * string, where the integers have been combined together and separated * by whitespace. The other variant takes a sequence of integers, @@ -8110,6 +8113,7 @@ inline void swap(Link& lhs, Link& rhs) { #include "link/gauss-impl.h" #include "link/jenkins-impl.h" #include "link/pd-impl.h" +#include "link/braid-impl.h" #endif From 8e73b2d73299b79fed1746cda372b1ee357f80c6 Mon Sep 17 00:00:00 2001 From: alex-he Date: Sat, 15 Nov 2025 01:26:47 +1000 Subject: [PATCH 04/11] Implement iterator version of fromBraid() (part 2). --- engine/link/braid-impl.h | 168 +++++++++++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 44 deletions(-) diff --git a/engine/link/braid-impl.h b/engine/link/braid-impl.h index ec36233c2..277aec7fb 100644 --- a/engine/link/braid-impl.h +++ b/engine/link/braid-impl.h @@ -57,68 +57,148 @@ Link Link::fromBraid(Iterator begin, Iterator end) { return { 1 }; // Zero-crossing unknot. } - // TODO It should be possible to do everything in just a single pass - // through the braid. But is this worth the effort? - - // Make a first pass through the braid in order to: - // (a) Work out the number of "rows" in the braid (we use "rows" to avoid - // a class of terminology with "strands" of the link diagram). - // (b) Work out the signs of all the crossings of the link diagram. - size_t numRows = 0; + // The braid must have at least 2 "rows" (we use "rows" to avoid a clash + // of terminology with "strands" of the link diagram), so we can at least + // make a start on the book-keeping for the first 2 rows (ie, the rows + // numbered either 0 or 1). + std::vector leftmostStrand; + std::vector previousStrand; + std::vector rowPerm; + size_t row; + for (row : {0,1}) { + leftmostStrand.emplace_back(); + previousStrand.emplace_back(); + rowPerm.push_back(row); + } + + // Iterate through the braid word and build the link. + Link ans; + size_t uppermostRow = 1; size_t upperRow; size_t iCross; InputInt s; - Link ans; for (iCross = 0; iCross < numCross; ++iCross) { s = begin[iCross]; if (s == 0) { throw InvalidArgument("fromBraid(): braid word contains 0"); + } + + // Have we found a new uppermost row in the braid? + upperRow = static_cast( std::abs(s) ); + for (++uppermostRow; uppermostRow <= upperRow; ++uppermostRow) { + leftmostStrand.emplace_back(); + previousStrand.emplace_back(); + rowPerm.push_back(uppermostRow); + } + + // We have a new crossing that exchanges upperRow and upperRow - 1. + std::swap( rowPerm[upperRow], rowPerm[upperRow - 1] ); + Crossing* crossing = new Crossing; + ans.crossings_.push_back(crossing); + if (s > 0) { + // Positive crossing. + // ___ ___ + // \ / + // \ + // ___/ \___ + // + crossing->sign_ = 1; + + // The overstrand either: + // --> joins up with the previous strand in upperRow; or + // --> there is no previous strand, which means that the + // overstrand is the leftmost strand in upperRow. + if (previousStrand[upperRow]) { + ans.join( previousStrand[upperRow], crossing->over() ); + } else { + leftmostStrand[upperRow] = crossing->over(); + } + + // The understrand either: + // --> joins up with the previous strand in upperRow - 1; or + // --> there is no previous strand, which means that the + // understrand is the leftmost strand in upperRow - 1. + if (previousStrand[upperRow - 1]) { + ans.join( previousStrand[upperRow - 1], crossing->under() ); + } else { + leftmostStrand[upperRow - 1] = crossing->under(); + } + + // Update the previous strands. + previousStrand[upperRow - 1] = crossing->over(); + previousStrand[upperRow] = crossing->under(); } else { - // Have we found a new uppermost row in the braid? - upperRow = static_cast( std::abs(s) ); - if (upperRow > numRows) { - numRows = upperRow; + // Negative crossing. + // ___ ___ + // \ / + // / + // ___/ \___ + // + crossing->sign_ = -1; + + // The understrand either: + // --> joins up with the previous strand in upperRow; or + // --> there is no previous strand, which means that the + // understrand is the leftmost strand in upperRow. + if (previousStrand[upperRow]) { + ans.join( previousStrand[upperRow], crossing->under() ); + } else { + leftmostStrand[upperRow] = crossing->under(); } - // What's the sign of crossing number iCross? - if (s > 0) { - ans.crossings_.push_back( new Crossing(1) ); + // The overstrand either: + // --> joins up with the previous strand in upperRow - 1; or + // --> there is no previous strand, which means that the + // overstrand is the leftmost strand in upperRow - 1. + if (previousStrand[upperRow - 1]) { + ans.join( previousStrand[upperRow - 1], crossing->over() ); } else { - ans.crossings_.push_back( new Crossing(-1) ); + leftmostStrand[upperRow - 1] = crossing->over(); } - } - } - numRows += 1; - // In a moment, we will make a second pass through the braid in order to - // join all the crossings together. As part of the book-keeping, we will: - // --> search for the leftmost strand in each row, and - // --> keep track of the latest strand that we have encountered in each - // row. - std::vector leftmostStrand; - std::vector latestStrand; - size_t row; - for (row = 0; row < numRows; ++row) { - leftmostStrand.emplace_back( StrandRef() ); - latestStrand.emplace_back( StrandRef() ); + // Update the previous strands. + previousStrand[upperRow - 1] = crossing->under(); + previousStrand[upperRow] = crossing->over(); + } } - // As promised, go through the braid again to work out how to join all the - // crossings together. - for (iCross = 0; iCross < numCross; ++iCross) { - s = begin[iCross]; - //TODO + // At this point, we have effectively built the braid, but haven't done + // the closure yet. + std::unordered_set untraversedRows; + for (row = 0; row <= uppermostRow; ++row) { + if (previousStrand[row]) { + // Close up this row. + ans.join( previousStrand[row], leftmostStrand[row] ); + + // In a moment, we will need to traverse the leftmost strand of + // this row to find all the components of the link with at least + // one crossing. + untraversedRows.insert(row); + } else { + // This row isn't involved in any crossings at all, so it simply + // forms a zero-crossing unknotted component of the link. + ans.components_.emplace_back(); + } } - // Close up the braid. - //TODO - - // Find all the components. - std::unordered_set unvisitedRows; - for (InputInt i = 0; i < numCross; ++i) { - unvisitedRows.insert(i); + // All that remains is to find all the components (with at least one + // crossing). + size_t firstRow; + size_t currentRow; + while (not untraversedRows.empty()) { + firstRow = *untraversedRows.begin(); + untraversedRows.erase( untraversedRows.begin() ); + ans.components_.push_back( leftmostStrand[firstRow] ); + + // Traverse and erase all the other leftmost strands that belong to + // this component. + currentRow = rowPerm[firstRow]; + while (currentRow != firstRow) { + untraversedRows.erase(currentRow); + currentRow = rowPerm[firstRow]; + } } - //TODO + return ans; } } // namespace regina From 3c32255494469ba51aae873c65a8bac3c46da2f3 Mon Sep 17 00:00:00 2001 From: alex-he Date: Sat, 15 Nov 2025 13:07:30 +1000 Subject: [PATCH 05/11] Implement string version of fromBraid(). --- engine/link/braid-impl.h | 2 +- engine/link/braid.cpp | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/engine/link/braid-impl.h b/engine/link/braid-impl.h index 277aec7fb..b188969fc 100644 --- a/engine/link/braid-impl.h +++ b/engine/link/braid-impl.h @@ -65,7 +65,7 @@ Link Link::fromBraid(Iterator begin, Iterator end) { std::vector previousStrand; std::vector rowPerm; size_t row; - for (row : {0,1}) { + for (row = 0; row <= 1; ++row) { leftmostStrand.emplace_back(); previousStrand.emplace_back(); rowPerm.push_back(row); diff --git a/engine/link/braid.cpp b/engine/link/braid.cpp index f8fdeb762..51d472c37 100644 --- a/engine/link/braid.cpp +++ b/engine/link/braid.cpp @@ -35,7 +35,22 @@ namespace regina { Link Link::fromBraid(const std::string& s) { // Work with the largest integer type that we could possibly need. using Int = std::make_signed_t; - //TODO + std::istringstream in(s); + std::vector terms; + + Int i; + while (true) { + in >> i; + if (not in) { + if (in.eof()) { + break; + } + throw InvalidArgument( + "fromBraid(): invalid integer in braid word"); + } + terms.push_back(i); + } + return fromBraid( terms.begin(), terms.end() ); } } // namespace regina From fa5dae76079a2311cac26a7e6d575a5cf5c2d8ff Mon Sep 17 00:00:00 2001 From: alex-he Date: Sat, 15 Nov 2025 16:12:42 +1000 Subject: [PATCH 06/11] Attempt at Python bindings for fromBraid(). --- engine/link/link.h | 3 +- python/docstrings/link/link.h | 131 ++++++++++++++++++ python/docstrings/triangulation/detail/face.h | 7 +- python/link/link.cpp | 13 +- 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/engine/link/link.h b/engine/link/link.h index 34947f158..9f3ea99b1 100644 --- a/engine/link/link.h +++ b/engine/link/link.h @@ -6843,7 +6843,8 @@ class Link : * word for a classical link. * * \python Instead of a pair of begin and past-the-end - * iterators, this routine takes a Python list of integers. + * iterators, this routine takes a sequence (such as a Python list) of + * integers. * * \author Alex He * diff --git a/python/docstrings/link/link.h b/python/docstrings/link/link.h index a4a23f963..f57dce58b 100644 --- a/python/docstrings/link/link.h +++ b/python/docstrings/link/link.h @@ -1338,6 +1338,137 @@ Parameter ``simplify``: the groups of this link obtained by the "native" and "reflected" Silver-Williams presentations, as described above.)doc"; +// Docstring regina::python::doc::Link_::fromBraid +static const char *fromBraid = +R"doc(Creates a new classical link from a braid word, presented as a string. + +For a braid on *n* strands (not to be confused with strands of a link +diagram), orient the strands from left to right, and label the strands +in order from bottom to top by 0 to *n* - 1 (inclusive). A braid word +for such an *n*-strand braid is given by a (nonempty) sequence of +nonzero integers between 1 - *n* and *n* - 1 (inclusive), in which +either 1 - *n* or *n* - 1 appears at least once; in other words, we +assume that there is at least one crossing involving the uppermost +strand (we make no such assumption for the lowermost strand). + +* A positive integer *s* in the braid word indicates an exchange of + strands *s* - 1 and *s* via a positive crossing. + +``` + ___ ___ + \ / + \ + ___/ \___ +``` + +* A negative integer -*s* in the braid word indicates an exchange of + strands *s* - 1 and *s* via a negative crossing. + +``` + ___ ___ + \ / + / + ___/ \___ +``` + +As an example, the braid word + +``` +1 -3 -3 2 1 +``` + +describes the following 4-strand braid: + +``` +_________ ___ _______________ + \ / \ / + / / +_________/ \___/ \___ _________ + \ / + \ +___ _______________/ \___ ___ + \ / \ / + \ \ +___/ \_____________________/ \___ +``` + +The corresponding link is constructed by taking the closure of the +braid; that is, by taking strand *s* on the right and joining it with +strand *s* on the left, for each *s* between 0 and *n* - 1 +(inclusive). Depending on how the braid word permutes the strands as +we go from left to right, this produces a link with up to *n* +components. For example, in the 4-strand example above, the closure +will be a 3-component link. + +The conventions for braids described above are chosen to be consistent +with those used in SnapPy 3.0/Spherogram 2.0 and newer. + +For the special case where the braid word is empty, this routine +returns a zero-crossing unknot. + +There are two variants of this routine. This variant takes a single +string, where the integers have been combined together and separated +by whitespace. The other variant takes a sequence of integers, defined +by a pair of iterators. + +In this variant (the string variant), the exact form of the whitespace +does not matter, and additional whitespace at the beginning or end of +the string is allowed. + +Exception ``InvalidArgument``: + The given string was not a valid braid word for a classical link. + +Author: + Alex He + +Parameter ``str``: + a braid word for a classical link, as described above. + +Returns: + the reconstructed link.)doc"; + +// Docstring regina::python::doc::Link_::fromBraid_2 +static const char *fromBraid_2 = +R"doc(Creates a new classical link from a braid word, presented as an +integer sequence. + +See fromBraid(const std::string&) for a full description of the +notation for braid words, as well as a detailed discussion of how +Regina constructs classical links from such notation. + +This routine is a variant of fromBraid(const std::string&) which, +instead of taking a human-readable string, takes a machine-readable +sequence of integers. This sequence is given by passing a pair of +begin/end iterators. + +Precondition: + *Iterator* is a random access iterator type, and dereferencing + such an iterator produces a native C++ integer. (The specific + native C++ integer type being used will be deduced from the type + *Iterator*.) + +Exception ``InvalidArgument``: + The given sequence was not a valid braid word for a classical + link. + +Python: + Instead of a pair of begin and past-the-end iterators, this + routine takes a sequence (such as a Python list) of integers. + +Author: + Alex He + +Parameter ``begin``: + an iterator that points to the beginning of the sequence of + integers for the braid word for a classical link. + +Parameter ``end``: + an iterator that points past the end of the sequence of integers + for the braid word for a classical link. + +Returns: + the reconstructed link.)doc"; + // Docstring regina::python::doc::Link_::fromDT static const char *fromDT = R"doc(Creates a new classical knot from either alphabetical or numerical diff --git a/python/docstrings/triangulation/detail/face.h b/python/docstrings/triangulation/detail/face.h index 17f62301c..c9f407d6c 100644 --- a/python/docstrings/triangulation/detail/face.h +++ b/python/docstrings/triangulation/detail/face.h @@ -565,10 +565,9 @@ R"doc(Determines if the link of this face is orientable. This routine is fast: it uses pre-computed information, and does not need to build a full triangulation of the link. -.. warning:: - If this face is identified with itself under a non-identity - permutation (which makes the face invalid), then the return value - of this routine is undefined. +As of Regina 7.4.1, the orientability of the link will be calculated +correctly even if the face is invalid due to a non-trivial self- +identification. Returns: ``True`` if and only if the link is orientable.)doc"; diff --git a/python/link/link.cpp b/python/link/link.cpp index 2815bbbc1..118f408f2 100644 --- a/python/link/link.cpp +++ b/python/link/link.cpp @@ -187,6 +187,18 @@ void addLink(pybind11::module_& m, pybind11::module_& internal) { .def_static("fromDT", [](const std::vector& v) { return Link::fromDT(v.begin(), v.end()); }, pybind11::arg("integers"), rdoc::fromDT_2) + //TODO Don't know why this breaks all of the following tests: + // --> python-test + // --> python-basic + // --> python-sub + // --> python-callback + //TODO + //.def_static("fromBraid", [](const std::string& s) { + // return Link::fromBraid(s); + //}, pybind11::arg("word"), rdoc::fromBraid) + //.def_static("fromBraid", [](const std::vector& v) { + // return Link::fromBraid(v.begin(), v.end()); + //}, pybind11::arg("integers"), rdoc::fromBraid_2) .def_static("fromPD", [](const std::string& s) { return Link::fromPD(s); }, rdoc::fromPD) @@ -233,7 +245,6 @@ void addLink(pybind11::module_& m, pybind11::module_& internal) { }, pybind11::arg("signs"), pybind11::arg("component"), rdoc::fromData) .def_static("fromKnotSig", &Link::fromKnotSig, rdoc::fromKnotSig) .def_static("fromSig", &Link::fromSig, rdoc::fromSig) - //TODO python bindings for fromBraid(). .def("swap", &Link::swap, rdoc::swap) .def("insertLink", overload_cast(&Link::insertLink), rdoc::insertLink) From 87b3d6d7a2b4d20c3cab352b237aca26b5dd06c7 Mon Sep 17 00:00:00 2001 From: alex-he Date: Sat, 15 Nov 2025 20:09:28 +1000 Subject: [PATCH 07/11] Fix it. --- engine/link/CMakeLists.txt | 2 ++ python/link/link.cpp | 18 ++++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/engine/link/CMakeLists.txt b/engine/link/CMakeLists.txt index 1d8bddfee..607b630e6 100644 --- a/engine/link/CMakeLists.txt +++ b/engine/link/CMakeLists.txt @@ -6,6 +6,7 @@ SET ( FILES alexander.cpp algebra.cpp arrow.cpp + braid.cpp complement.cpp dt.cpp examplelink.cpp @@ -41,6 +42,7 @@ SET( SOURCES ${SOURCES} PARENT_SCOPE) if (${REGINA_INSTALL_DEV}) INSTALL(FILES + braid-impl.h data-impl.h dt-impl.h examplelink.h diff --git a/python/link/link.cpp b/python/link/link.cpp index 118f408f2..b15be8ade 100644 --- a/python/link/link.cpp +++ b/python/link/link.cpp @@ -187,18 +187,12 @@ void addLink(pybind11::module_& m, pybind11::module_& internal) { .def_static("fromDT", [](const std::vector& v) { return Link::fromDT(v.begin(), v.end()); }, pybind11::arg("integers"), rdoc::fromDT_2) - //TODO Don't know why this breaks all of the following tests: - // --> python-test - // --> python-basic - // --> python-sub - // --> python-callback - //TODO - //.def_static("fromBraid", [](const std::string& s) { - // return Link::fromBraid(s); - //}, pybind11::arg("word"), rdoc::fromBraid) - //.def_static("fromBraid", [](const std::vector& v) { - // return Link::fromBraid(v.begin(), v.end()); - //}, pybind11::arg("integers"), rdoc::fromBraid_2) + .def_static("fromBraid", [](const std::string& s) { + return Link::fromBraid(s); + }, pybind11::arg("word"), rdoc::fromBraid) + .def_static("fromBraid", [](const std::vector& v) { + return Link::fromBraid(v.begin(), v.end()); + }, pybind11::arg("integers"), rdoc::fromBraid_2) .def_static("fromPD", [](const std::string& s) { return Link::fromPD(s); }, rdoc::fromPD) From 7fd47a0862314329c722ad5ab82553f36efae984 Mon Sep 17 00:00:00 2001 From: alex-he Date: Tue, 18 Nov 2025 01:00:02 +1000 Subject: [PATCH 08/11] Preparation for testing fromBraid(). --- engine/testsuite/link/link.cpp | 42 +++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp index 921131fc6..e2b5d7c5c 100644 --- a/engine/testsuite/link/link.cpp +++ b/engine/testsuite/link/link.cpp @@ -4269,7 +4269,44 @@ TEST_F(LinkTest, pdCode) { testManualCases(verifyPDCode); } -//TODO Test fromBraid(). +static void verifyBraid( + const Link& link, const std::string& word, const char* name) { + //TODO Test fromBraid(). + // --> No throw + // --> Size + // --> Components + // --> Magic constructor + // --> Simple invariants? + // + // Also look through the documentation and implementation for + // specific things that we might want to test. +} + +TEST_F(LinkTest, braid) { + //TODO We don't currently have Vogel's algorithm, so we will need to + // resort to manually coming up with some braid words. + // + // Since we're supposed to be consistent with SnapPy, it probably + // makes sense to use SnapPy to help generate test cases. + + // Invalid braid words. + //TODO + + // Braid words for the unknot. + //TODO + + // Braid words for the trefoil knot. + //TODO + + // Braid words for the figure-eight knot. + //TODO + + // Braid words for multi-component links. + //TODO + + // Some more specialised braid word tests. + //TODO ?????? +} TEST_F(LinkTest, invalidCode) { static const char* code = "INVALID"; @@ -4280,8 +4317,7 @@ TEST_F(LinkTest, invalidCode) { EXPECT_THROW({ Link::fromOrientedGauss(code); }, InvalidArgument); EXPECT_THROW({ Link::fromJenkins(code); }, InvalidArgument); EXPECT_THROW({ Link::fromPD(code); }, InvalidArgument); - - //TODO Test fromBraid(). + EXPECT_THROW({ Link::fromBraid(code); }, InvalidArgument); // Finally, the "magic" constructor: EXPECT_THROW({ Link l(code); }, InvalidArgument); From d995e103d7086eaedd7f19415ef3a926a20192ae Mon Sep 17 00:00:00 2001 From: alex-he Date: Wed, 19 Nov 2025 15:44:13 +1000 Subject: [PATCH 09/11] Test fromBraid(), part 1. --- engine/testsuite/link/link.cpp | 147 +++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 7 deletions(-) diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp index e2b5d7c5c..a10d3cfc8 100644 --- a/engine/testsuite/link/link.cpp +++ b/engine/testsuite/link/link.cpp @@ -4269,6 +4269,12 @@ TEST_F(LinkTest, pdCode) { testManualCases(verifyPDCode); } +struct BraidTestCase { + Link link; + std::string word; + const char* name; +}; + static void verifyBraid( const Link& link, const std::string& word, const char* name) { //TODO Test fromBraid(). @@ -4283,25 +4289,152 @@ static void verifyBraid( } TEST_F(LinkTest, braid) { - //TODO We don't currently have Vogel's algorithm, so we will need to - // resort to manually coming up with some braid words. + // Regina doesn't (yet) have an implementation of Vogel's algorithm, so + // for the braid closure tests we resort to using some bespoke test cases. // - // Since we're supposed to be consistent with SnapPy, it probably - // makes sense to use SnapPy to help generate test cases. + // Here are some notes on how these test cases were constructed: + // --> For links built from PD codes, such codes were computed from the + // SnapPy link given by the closure of the braid word. This ensures + // that the fromBraid() construction is indeed consistent with + // SnapPy 3.0, as promised in the documentation of fromBraid(). + // --> Many of the braid words were constructed using some Python code + // (currently not publicly available) that randomises braid words + // using Markov moves. // Invalid braid words. //TODO // Braid words for the unknot. - //TODO + // 3-, 4- and 6-strand braids generated using random Markov moves. + BraidTestCase braidEmpty {Link(1), "", "Empty"}; + BraidTestCase braidUnknot1Pos { + Link::fromPD("[[2, 2, 1, 1]]"), + "1", + "Unknot (1 +ve crossing)" }; + BraidTestCase braidUnknot1Neg { + Link::fromPD("[[2, 1, 1, 2]]"), + "-1", + "Unknot (1 -ve crossing)" }; + BraidTestCase braidUnknot3Strand { + Link::fromPD("[[4, 8, 5, 7], [2, 5, 3, 6], [3, 8, 4, 1], " + "[6, 1, 7, 2]]"), + "1 -2 -1 -2", + "Unknot (3 strands)" }; + BraidTestCase braidUnknot4Strand { + Link::fromPD("[[9, 7, 10, 6], [7, 2, 8, 3], [10, 3, 11, 4], " + "[4, 14, 5, 13], [11, 1, 12, 14], [12, 6, 13, 5], " + "[8, 2, 9, 1]]"), + "2 -1 -2 3 2 3 1", + "Unknot (4 strands)" }; + BraidTestCase braidUnknot6Strand { + Link::fromPD("[[14, 17, 15, 18], [6, 9, 7, 10], [11, 3, 12, 2], " + "[10, 4, 11, 3], [7, 5, 8, 4], [15, 12, 16, 13], " + "[5, 9, 6, 8], [13, 1, 14, 18], [16, 2, 17, 1]]"), + "-5 -1 3 2 1 -4 1 5 4", + "Unknot (6 strands)" }; + // Run the tests. + verifyBraid(braidEmpty.link, braidEmpty.word, braidEmpty.name); + verifyBraid(braidUnknot1Pos.link, braidUnknot1Pos.word, + braidUnknot1Pos.name); + verifyBraid(braidUnknot1Neg.link, braidUnknot1Neg.word, + braidUnknot1Neg.name); + verifyBraid(braidUnknot3Strand.link, braidUnknot3Strand.word, + braidUnknot3Strand.name); + verifyBraid(braidUnknot4Strand.link, braidUnknot4Strand.word, + braidUnknot4Strand.name); + verifyBraid(braidUnknot6Strand.link, braidUnknot6Strand.word, + braidUnknot6Strand.name); + // We could check that these are all unknots, but this shouldn't be + // necessary since verifyBraid() already checks for combinatorial + // isomorphism (which is stronger). // Braid words for the trefoil knot. - //TODO + // 3-, 4- and 6-strand braids generated using random Markov moves. + BraidTestCase braidTrefoil3Pos { + Link::fromPD("[[2, 6, 3, 5], [6, 4, 1, 3], [4, 2, 5, 1]]"), + "1 1 1", + "Trefoil (3 +ve crossings)" }; + BraidTestCase braidTrefoil3Neg { + Link::fromPD("[[2, 5, 3, 6], [6, 3, 1, 4], [4, 1, 5, 2]]"), + "-1 -1 -1", + "Trefoil (3 -ve crossings)" }; + BraidTestCase braidTrefoil3Strand { + Link::fromPD("[[7, 5, 8, 4], [2, 6, 3, 5], [3, 1, 4, 8], " + "[6, 2, 7, 1]]"), + "2 1 2 1", + "Trefoil (3 strands)" }; + BraidTestCase braidTrefoil4Strand { + Link::fromPD("[[13, 8, 14, 9], [2, 6, 3, 5], [9, 7, 10, 6], " + "[10, 4, 11, 3], [4, 12, 5, 11], [14, 8, 1, 7], " + "[12, 1, 13, 2]]"), + "-1 3 2 3 3 1 -2", + "Trefoil (4 strands)" }; + BraidTestCase braidTrefoil6Strand { + Link::fromPD("[[7, 24, 8, 25], [25, 3, 26, 2], [8, 4, 9, 3], " + "[16, 20, 17, 19], [9, 27, 10, 26], [13, 21, 14, 20], " + "[17, 14, 18, 15], [27, 11, 28, 10], [11, 29, 12, 28], " + "[4, 30, 5, 29], [21, 12, 22, 13], [15, 18, 16, 19], " + "[5, 23, 6, 22], [23, 30, 24, 1], [6, 1, 7, 2]]"), + "-1 2 1 5 2 4 -5 2 2 1 -3 -5 2 -1 -2", + "Trefoil (6 strands)" }; + // Run the tests. + verifyBraid(braidTrefoil3Pos.link, braidTrefoil3Pos.word, + braidTrefoil3Pos.name); + verifyBraid(braidTrefoil3Neg.link, braidTrefoil3Neg.word, + braidTrefoil3Neg.name); + verifyBraid(braidTrefoil3Strand.link, braidTrefoil3Strand.word, + braidTrefoil3Strand.name); + verifyBraid(braidTrefoil4Strand.link, braidTrefoil4Strand.word, + braidTrefoil4Strand.name); + verifyBraid(braidTrefoil6Strand.link, braidTrefoil6Strand.word, + braidTrefoil6Strand.name); + // Again we could check that these are all trefoil knots, but this + // shouldn't be necessary. // Braid words for the figure-eight knot. - //TODO + // 4- and 5-strand braids generated using random Markov moves. + BraidTestCase braidFig8_4Cross { + Link::fromPD("[[4, 8, 5, 7], [2, 5, 3, 6], [8, 4, 1, 3], " + "[6, 1, 7, 2]]"), + "1 -2 1 -2", + "Figure eight (4 crossings)"}; + BraidTestCase braidFig8_4Strand { + Link::fromPD("[[2, 9, 3, 10], [12, 4, 13, 3], [15, 5, 16, 4], " + "[13, 16, 14, 17], [5, 15, 6, 14], [17, 6, 18, 7], " + "[7, 11, 8, 10], [18, 12, 1, 11], [8, 1, 9, 2]]"), + "-3 2 1 -2 1 -2 3 2 -3", + "Figure eight (4 strands)"}; + BraidTestCase braidFig8_5Strand { + Link::fromPD("[[4, 12, 5, 11], [5, 3, 6, 2], [12, 4, 13, 3], " + "[8, 15, 9, 16], [9, 6, 10, 7], [16, 7, 1, 8], " + "[10, 13, 11, 14], [14, 2, 15, 1]]"), + "1 2 1 -4 -3 -4 -2 3", + "Figure eight (5 strands)"}; + // Run the tests. + verifyBraid(braidFig8_4Cross.link, braidFig8_4Cross.word, + braidFig8_4Cross.name); + verifyBraid(braidFig8_4Strand.link, braidFig8_4Strand.word, + braidFig8_4Strand.name); + verifyBraid(braidFig8_5Strand.link, braidFig8_5Strand.word, + braidFig8_5Strand.name); + // Again we could check that these are all figure eight knots, but this + // shouldn't be necessary. // Braid words for multi-component links. + BraidTestCase braidHopf2Pos { + Link::fromPD("[[2, 4, 1, 3], [4, 2, 3, 1]]"), + "1 1", + "Hopf link (2 +ve crossings)"}; + BraidTestCase braidHopf2Neg { + Link::fromPD("[[2, 3, 1, 4], [4, 1, 3, 2]]"), + "-1 -1", + "Hopf link (2 -ve crossings)"}; + //TODO + // Run the tests. + verifyBraid(braidHopf2Pos.link, braidHopf2Pos.word, + braidHopf2Pos.name); + verifyBraid(braidHopf2Neg.link, braidHopf2Neg.word, + braidHopf2Neg.name); //TODO // Some more specialised braid word tests. From 961cf4299cce071917acc338fd0b45292c1f508e Mon Sep 17 00:00:00 2001 From: alex-he Date: Fri, 21 Nov 2025 10:40:39 +1000 Subject: [PATCH 10/11] Test fromBraid(), part 2. --- engine/testsuite/link/link.cpp | 56 +++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp index a10d3cfc8..3f0d738cc 100644 --- a/engine/testsuite/link/link.cpp +++ b/engine/testsuite/link/link.cpp @@ -4277,15 +4277,18 @@ struct BraidTestCase { static void verifyBraid( const Link& link, const std::string& word, const char* name) { + SCOPED_TRACE_CSTRING(name); + + // The closure of the given braid should be the same as the given link + // diagram, possibly up to relabelling (but not reflection, rotation or + // reversal). //TODO Test fromBraid(). // --> No throw - // --> Size - // --> Components + // --> Knot/link signature, which should subsume following tests: + // --> Size + // --> Components + // --> Simple invariants? // --> Magic constructor - // --> Simple invariants? - // - // Also look through the documentation and implementation for - // specific things that we might want to test. } TEST_F(LinkTest, braid) { @@ -4302,7 +4305,8 @@ TEST_F(LinkTest, braid) { // using Markov moves. // Invalid braid words. - //TODO + EXPECT_THROW({ Link::fromBraid("3 -2 a 2"); }, regina::InvalidArgument); + EXPECT_THROW({ Link::fromBraid("3 -2 0 2"); }, regina::InvalidArgument); // Braid words for the unknot. // 3-, 4- and 6-strand braids generated using random Markov moves. @@ -4393,9 +4397,10 @@ TEST_F(LinkTest, braid) { // Braid words for the figure-eight knot. // 4- and 5-strand braids generated using random Markov moves. + Link fig8_4Cross = Link::fromPD("[[4, 8, 5, 7], [2, 5, 3, 6], " + "[8, 4, 1, 3], [6, 1, 7, 2]]"); // We reuse this later. BraidTestCase braidFig8_4Cross { - Link::fromPD("[[4, 8, 5, 7], [2, 5, 3, 6], [8, 4, 1, 3], " - "[6, 1, 7, 2]]"), + fig8_4Cross, "1 -2 1 -2", "Figure eight (4 crossings)"}; BraidTestCase braidFig8_4Strand { @@ -4429,16 +4434,39 @@ TEST_F(LinkTest, braid) { Link::fromPD("[[2, 3, 1, 4], [4, 1, 3, 2]]"), "-1 -1", "Hopf link (2 -ve crossings)"}; - //TODO + BraidTestCase braidFig8_unknot1 { + addTrivialComponents(fig8_4Cross,1), + "2 -3 2 -3", + "Figure-eight U unknot"}; + BraidTestCase braidFig8_unknot2 { + addTrivialComponents(fig8_4Cross,2), + "3 -4 3 -4", + "Figure-eight U unknot U unknot"}; + BraidTestCase braid2Comp { + Link::fromPD("[[2, 8, 3, 7], [18, 22, 19, 21], [3, 11, 4, 22], " + "[14, 19, 15, 20], [11, 5, 12, 4], [8, 6, 9, 5], " + "[12, 16, 13, 15], [9, 17, 10, 16], [20, 13, 21, 14], " + "[17, 1, 18, 10], [6, 2, 7, 1]]"), + "1 3 2 -4 2 1 3 2 -4 2 1", + "2-component link"}; + BraidTestCase braid3Comp { + Link::fromPD("[[2, 8, 3, 7], [9, 4, 10, 5], [5, 10, 6, 9], " + "[3, 1, 4, 6], [8, 2, 7, 1]]"), + "1 -3 -3 2 1", + "3-component link"}; // Run the tests. verifyBraid(braidHopf2Pos.link, braidHopf2Pos.word, braidHopf2Pos.name); verifyBraid(braidHopf2Neg.link, braidHopf2Neg.word, braidHopf2Neg.name); - //TODO - - // Some more specialised braid word tests. - //TODO ?????? + verifyBraid(braidFig8_unknot1.link, braidFig8_unknot1.word, + braidFig8_unknot1.name); + verifyBraid(braidFig8_unknot2.link, braidFig8_unknot2.word, + braidFig8_unknot2.name); + verifyBraid(braid2Comp.link, braid2Comp.word, + braid2Comp.name); + verifyBraid(braid3Comp.link, braid3Comp.word, + braid3Comp.name); } TEST_F(LinkTest, invalidCode) { From 6b215318c4a653c8ea70e0f235806324e11d653b Mon Sep 17 00:00:00 2001 From: alex-he Date: Sun, 23 Nov 2025 18:57:01 +1000 Subject: [PATCH 11/11] Test fromBraid(), part 3. --- engine/link/braid-impl.h | 5 +++-- engine/testsuite/link/link.cpp | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/engine/link/braid-impl.h b/engine/link/braid-impl.h index b188969fc..38d681f2f 100644 --- a/engine/link/braid-impl.h +++ b/engine/link/braid-impl.h @@ -85,7 +85,8 @@ Link Link::fromBraid(Iterator begin, Iterator end) { // Have we found a new uppermost row in the braid? upperRow = static_cast( std::abs(s) ); - for (++uppermostRow; uppermostRow <= upperRow; ++uppermostRow) { + while (upperRow > uppermostRow) { + ++uppermostRow; leftmostStrand.emplace_back(); previousStrand.emplace_back(); rowPerm.push_back(uppermostRow); @@ -195,7 +196,7 @@ Link Link::fromBraid(Iterator begin, Iterator end) { currentRow = rowPerm[firstRow]; while (currentRow != firstRow) { untraversedRows.erase(currentRow); - currentRow = rowPerm[firstRow]; + currentRow = rowPerm[currentRow]; } } return ans; diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp index 3f0d738cc..62a5685a1 100644 --- a/engine/testsuite/link/link.cpp +++ b/engine/testsuite/link/link.cpp @@ -4282,13 +4282,14 @@ static void verifyBraid( // The closure of the given braid should be the same as the given link // diagram, possibly up to relabelling (but not reflection, rotation or // reversal). - //TODO Test fromBraid(). - // --> No throw - // --> Knot/link signature, which should subsume following tests: - // --> Size - // --> Components - // --> Simple invariants? - // --> Magic constructor + Link recon; + ASSERT_NO_THROW({ recon = Link::fromBraid(word); }); + EXPECT_EQ( recon.sig(false, false, false), + link.sig(false, false, false) ); + + // Verify the "magic" string constructor. + //TODO Need to add braid words to the "magic" constructor first. + //EXPECT_NO_THROW({ EXPECT_EQ( Link(word), recon ); }); } TEST_F(LinkTest, braid) { @@ -4305,8 +4306,8 @@ TEST_F(LinkTest, braid) { // using Markov moves. // Invalid braid words. - EXPECT_THROW({ Link::fromBraid("3 -2 a 2"); }, regina::InvalidArgument); - EXPECT_THROW({ Link::fromBraid("3 -2 0 2"); }, regina::InvalidArgument); + EXPECT_THROW({ Link::fromBraid("3 -2 2 a"); }, regina::InvalidArgument); + EXPECT_THROW({ Link::fromBraid("3 -2 2 0"); }, regina::InvalidArgument); // Braid words for the unknot. // 3-, 4- and 6-strand braids generated using random Markov moves.