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)