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/engine/link/braid-impl.h b/engine/link/braid-impl.h
new file mode 100644
index 000000000..38d681f2f
--- /dev/null
+++ b/engine/link/braid-impl.h
@@ -0,0 +1,208 @@
+
+/**************************************************************************
+ * *
+ * 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.
+ }
+
+ // 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; row <= 1; ++row) {
+ 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;
+ 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) );
+ while (upperRow > uppermostRow) {
+ ++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 {
+ // 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();
+ }
+
+ // 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 {
+ leftmostStrand[upperRow - 1] = crossing->over();
+ }
+
+ // Update the previous strands.
+ previousStrand[upperRow - 1] = crossing->under();
+ previousStrand[upperRow] = crossing->over();
+ }
+ }
+
+ // 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();
+ }
+ }
+
+ // 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[currentRow];
+ }
+ }
+ return ans;
+}
+
+} // namespace regina
+
+#endif
+
diff --git a/engine/link/braid.cpp b/engine/link/braid.cpp
new file mode 100644
index 000000000..51d472c37
--- /dev/null
+++ b/engine/link/braid.cpp
@@ -0,0 +1,57 @@
+
+/**************************************************************************
+ * *
+ * 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"
+
+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;
+ 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
+
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 96ada3b24..a9a38534b 100644
--- a/engine/link/link.h
+++ b/engine/link/link.h
@@ -5618,6 +5618,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
@@ -6723,6 +6726,129 @@ class Link :
template
static Link fromPD(Iterator begin, Iterator end);
+ /**
+ * 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.
+ *
+ * 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
+ *
+ * \param str a braid word for a classical link, as described above.
+ * \return the reconstructed link.
+ */
+ static Link fromBraid(const std::string& str);
+
+ /**
+ * 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 sequence (such as 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);
+
/*@}*/
private:
@@ -7979,6 +8105,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
diff --git a/engine/testsuite/link/link.cpp b/engine/testsuite/link/link.cpp
index 5a02ceffb..62a5685a1 100644
--- a/engine/testsuite/link/link.cpp
+++ b/engine/testsuite/link/link.cpp
@@ -4269,6 +4269,207 @@ 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) {
+ 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).
+ 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) {
+ // 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.
+ //
+ // 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.
+ 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.
+ 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.
+ // 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.
+ // 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 {
+ fig8_4Cross,
+ "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)"};
+ 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);
+ 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) {
static const char* code = "INVALID";
@@ -4278,6 +4479,7 @@ TEST_F(LinkTest, invalidCode) {
EXPECT_THROW({ Link::fromOrientedGauss(code); }, InvalidArgument);
EXPECT_THROW({ Link::fromJenkins(code); }, InvalidArgument);
EXPECT_THROW({ Link::fromPD(code); }, InvalidArgument);
+ EXPECT_THROW({ Link::fromBraid(code); }, InvalidArgument);
// Finally, the "magic" constructor:
EXPECT_THROW({ Link l(code); }, InvalidArgument);
diff --git a/python/docstrings/link/link.h b/python/docstrings/link/link.h
index c38bd502c..b9a8658e8 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/link/link.cpp b/python/link/link.cpp
index b3a7b42f8..b15be8ade 100644
--- a/python/link/link.cpp
+++ b/python/link/link.cpp
@@ -187,6 +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)
+ .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)