diff --git a/spherogram_src/__init__.py b/spherogram_src/__init__.py index fd4644b..fc98bef 100644 --- a/spherogram_src/__init__.py +++ b/spherogram_src/__init__.py @@ -23,4 +23,4 @@ def version(): # from spherogram.links.tangles: 'Tangle', 'CapTangle', 'CupTangle', 'RationalTangle', 'ZeroTangle', 'InfinityTangle', 'MinusOneTangle', 'OneTangle', 'IntegerTangle', - 'IdentityBraid', 'ComponentTangle', 'join_strands'] + 'IdentityBraid', 'BraidTangle', 'ComponentTangle', 'join_strands'] diff --git a/spherogram_src/links/__init__.py b/spherogram_src/links/__init__.py index 91a5c36..5fa6bd3 100644 --- a/spherogram_src/links/__init__.py +++ b/spherogram_src/links/__init__.py @@ -2,7 +2,7 @@ import sys from .links import Crossing, Strand, Link, ClosedBraid -from .tangles import Tangle, CapTangle, CupTangle, RationalTangle, ZeroTangle, InfinityTangle, MinusOneTangle, OneTangle, IntegerTangle, IdentityBraid, ComponentTangle, join_strands +from .tangles import Tangle, CapTangle, CupTangle, RationalTangle, ZeroTangle, InfinityTangle, MinusOneTangle, OneTangle, IntegerTangle, IdentityBraid, BraidTangle, ComponentTangle, join_strands from . import orthogonal from .random_links import random_link from . import bands @@ -25,5 +25,5 @@ def pdf_docs(): __all__ = ['Crossing', 'Strand', 'Link', 'ClosedBraid', 'Tangle', 'CapTangle', 'CupTangle', 'RationalTangle', 'ZeroTangle', 'InfinityTangle', 'MinusOneTangle', 'OneTangle', 'IntegerTangle', - 'IdentityBraid', 'join_strands', + 'IdentityBraid', 'BraidTangle', 'ComponentTangle','join_strands', 'pdf_docs', 'random_link', 'bands'] diff --git a/spherogram_src/links/links_base.py b/spherogram_src/links/links_base.py index 61c5473..c14acb7 100644 --- a/spherogram_src/links/links_base.py +++ b/spherogram_src/links/links_base.py @@ -124,6 +124,16 @@ def make_tail(self, a): raise ValueError("Can only orient a strand once.") self.directions.add(b) + def make_head(self, a): + """ + Orients the strand joining input "a" to input" a+2" to start at "a" and end at + "a+2". + """ + b = ((a + 2) % 4, a) + if (b[1], b[0]) in self.directions: + raise ValueError("Can only orient a strand once.") + self.directions.add(b) + def rotate(self, s): """ Rotate the incoming connections by 90*s degrees anticlockwise. @@ -251,7 +261,10 @@ def previous_corner(self): return self.opposite().rotate(-1) def strand_label(self): - return self.crossing.strand_labels[self.strand_index] + if isinstance(self.crossing, Strand): + return self.crossing.strand_label + else: + return self.crossing.strand_labels[self.strand_index] def oriented(self): """ @@ -266,25 +279,35 @@ def oriented(self): def __repr__(self): return "" % (self.crossing, self.strand_index) - class CrossingEntryPoint(CrossingStrand): """ One of the two entry points of an oriented crossing """ def next(self): - c, e = self.crossing, self.strand_index - s = c._adjacent_len // 2 - return CrossingEntryPoint(*c.adjacent[(e + s) % (2 * s)]) + if isinstance(self.crossing, (Crossing, Strand)): + c, e = self.crossing, self.strand_index + s = c._adjacent_len // 2 + return CrossingEntryPoint(*c.adjacent[(e + s) % (2 * s)]) + else: + raise RuntimeError('This should not be reached') def previous(self): - s = self.crossing._adjacent_len // 2 - return CrossingEntryPoint(*self.opposite().rotate(s)) + d, j = self.opposite() + + if isinstance(d, (Crossing, Strand)): + s = d._adjacent_len // 2 + return CrossingEntryPoint(*self.opposite().rotate(s)) + else: + return CrossingEntryPoint(d, j) def other(self): - nonzero_entry_point = 1 if self.crossing.sign == -1 else 3 - other = nonzero_entry_point if self.strand_index == 0 else 0 - return CrossingEntryPoint(self.crossing, other) + if isinstance(self.crossing, Crossing): + nonzero_entry_point = 1 if self.crossing.sign == -1 else 3 + other = nonzero_entry_point if self.strand_index == 0 else 0 + return CrossingEntryPoint(self.crossing, other) + else: + return None def is_under_crossing(self): return self.strand_index == 0 @@ -294,12 +317,27 @@ def is_over_crossing(self): def component(self): ans = [self] + + is_reversed = False while True: - next = ans[-1].next() - if next == self: + if is_reversed: + d = ans[0].previous() + else: + d = ans[-1].next() + + if d == self: break else: - ans.append(next) + if is_reversed: + ans.insert(0, d) + else: + ans.append(d) + + if not isinstance(d.crossing, (Crossing, Strand)): + if is_reversed: + break + else: + is_reversed = True return ans @@ -308,9 +346,15 @@ def component_label(self): def label_crossing(self, comp, labels): c, e = self.crossing, self.strand_index - f = (e + 2) % 4 - c.strand_labels[e], c.strand_components[e] = labels[self], comp - c.strand_labels[f], c.strand_components[f] = labels[self.next()], comp + + if isinstance(c, Crossing): + f = (e + 2) % 4 + c.strand_labels[e], c.strand_components[e] = labels[self], comp + c.strand_labels[f], c.strand_components[f] = labels[self.next()], comp + elif isinstance(c, Strand): + c.strand_label, c.strand_component = labels[self], comp + else: + c.strand_labels[e], c.strand_components[e] = labels[self], comp def __repr__(self): return "" % (self.crossing, self.strand_index) @@ -338,6 +382,7 @@ class Strand: def __init__(self, label=None, component_idx=None): self.label = label self.adjacent = [None, None] + self._clear() self._adjacent_len = 2 self.component_idx = component_idx @@ -368,8 +413,63 @@ def format_adjacent(a): (self.label, [format_adjacent(a) for a in self.adjacent])) def is_loop(self): - return self == self.adjacent[0][0] + return self.adjacent[0] is not None and self == self.adjacent[0][0] + + def _clear(self): + self.sign, self.direction = 0, None + self._clear_strand_info() + + def _clear_strand_info(self): + self.strand_label = None + self.strand_component = None + + def make_tail(self, a): + """ + Orients the strand joining input "a" to input" a+1" to start at "a" and end at + "a+1". + """ + b = (a, (a + 1) % 2) + if self.direction is not None and self.direction != b: + raise ValueError("Can only orient a strand once.") + self.direction = b + + def make_head(self, a): + """ + Orients the strand joining input "a" to input" a+1" to start at "a" and end at + "a+1". + """ + b = ((a + 1) % 2, a) + if self.direction is not None and self.direction != b: + raise ValueError("Can only orient a strand once.") + self.direction = b + + def rotate(self, s): + """ + Rotate the incoming connections by 180*s degrees anticlockwise. + """ + def rotate(v): + return (v + s) % 2 + new_adjacent = [self.adjacent[rotate(i)] for i in range(2)] + for i, (o, j) in enumerate(new_adjacent): + if o != self: + o.adjacent[j] = (self, i) + self.adjacent[i] = (o, j) + else: + self.adjacent[i] = (self, (j - s) % 2) + + a,b = self.direction + self.direction = (rotate(a), rotate(b)) + + def orient(self): + if self.direction == (1, 0): + self.rotate(1) + self.direction = (0,1) + + self.sign = 1 + def entry_points(self): + assert self.sign == 1 + return [CrossingEntryPoint(self, 0)] def enumerate_lists(lists, n=0, filter=lambda x: True): ans = [] @@ -515,7 +615,7 @@ def __init__(self, crossings=None, braid_closure=None, check_planarity=True, bui if not all(isinstance(c, Crossing) for c, _ in s.adjacent): raise ValueError("Strands with a component index must be in the same" " component as a crossing") - # Go through the component strands to construct component_starts + # Go through the component strands to construct component_spec if component_strands: component_spec = [] for s in component_strands: @@ -777,7 +877,8 @@ def _rebuild(self, same_components_and_orientations=False): for comp in self.link_components: for cs in comp: if cs.crossing in self.crossings: - start_css.append(cs) + s = cs.crossing._adjacent_len // 2 + start_css.append(cs.rotate(s)) break self.link_components = None for c in self.crossings: @@ -788,6 +889,39 @@ def _rebuild(self, same_components_and_orientations=False): else: self._build() + def reverse_orientation(self, component_index): + """ + Reverse the orientation of components specified by component_index. + + component_index: either a single index of component or a list of indices of components + + """ + if not isinstance(component_index, (set, list, tuple)): + component_index = [component_index] + + org_entries = [] + for comp in self.components: + for cs in comp: + if cs.crossing in self.crossings: + org_entries.append(cs) + break + + new_starts = [] + for i, cs in enumerate(org_entries): + if i not in component_index: + c, e = cs.crossing, cs.strand_index + s = c._adjacent_len // 2 + reversed_cs = CrossingStrand(c, (e + s) % (2 * s)) + new_starts.append(reversed_cs) + else: + new_starts.append(cs) + + self.link_components = None + for c in self.crossings: + c._clear() + self._build(start_orientations = new_starts, + component_starts = new_starts) + def _check_crossing_orientations(self): for C in self.crossings: if C.sign == 1: @@ -884,8 +1018,7 @@ def _build_components(self, component_starts=None): # and turn them into CrossingEntryPoints component_starts = [cs.crossing.entry_points()[cs.strand_index % 2] for cs in component_starts] - remaining, components = OrderedSet( - self.crossing_entries()), LinkComponents() + remaining, components = OrderedSet(self.crossing_entries()), LinkComponents() other_crossing_entries = [] self.labels = labels = Labels() for c in self.crossings: @@ -940,6 +1073,17 @@ def _build_components(self, component_starts=None): self.link_components = components + @property + def components(self): + """ + Synonym for link_components + """ + return self.link_components + + @components.setter + def components(self, value): + self.link_components = value + def digraph(self): """ The underlying directed graph for the link diagram. diff --git a/spherogram_src/links/old_tangles.py b/spherogram_src/links/old_tangles.py new file mode 100644 index 0000000..6688579 --- /dev/null +++ b/spherogram_src/links/old_tangles.py @@ -0,0 +1,394 @@ +""" +A tangle is piece of a knot diagram in a disk where some of the +strands meet the boundary. Tangles can be composed by gluing them +along arcs in each boundary that have the same number of incident +strands. + +This module gives a version of tangles where there are four distinguished +boundary arcs used for gluing: the bottom and top, which can have incident +strands, and the left and right, which cannot. Tangles can be glued +vertically using ``*`` and horizontally using ``|``. There is also a second +kind of horizontal composition using ``+`` where the rightmost strands of the top +and bottom of the first tangle are glued to the leftmost strands of the top and +bottom of the second tangle. + +Rational tangles (created using ``RationalTangle``) are following the paper + +Classifying and Applying Rational Knots and Rational Tangles +http://homepages.math.uic.edu/~kauffman/VegasAMS.pdf + +See doc.pdf for conventions. +""" +import pickle + +from .links import Crossing, Strand, Link +from . import planar_isotopy + + +def join_strands(x, y): + """ + Input: two (c, i) pairs where c is a Crossing, Strand, or Tangle object and i is an index into + c.adjacent. Joins the objects by having them refer to each other at those positions. + + When c is a Tangle it is conceptually a special case since its c.adjacent is being + used to record the boundary strands. + + This function equivalent to creating a Strand s with s.adjacent = [x, y] and then + doing s.fuse() + """ + (a, i), (b, j) = x, y + a.adjacent[i] = (b, j) + b.adjacent[j] = (a, i) + + +def rotate_list(L, s): + """Rotate the list, putting L[s] into index 0.""" + n = len(L) + return [L[(i + s) % n] for i in range(n)] + + +def decode_boundary(boundary): + """The boundary is either a nonnegative integer or a pair of non-negative integers. + + * When the input is an integer n, this returns (n, n). + * When the input is a pair (m, n), then it returns (m, n). + + >>> decode_boundary(2) + (2, 2) + >>> decode_boundary((3,4)) + (3, 4) + >>> decode_boundary(-2) + Traceback (most recent call last): + ... + ValueError: Number of bottom boundary strands cannot be negative + >>> decode_boundary((3,-2)) + Traceback (most recent call last): + ... + ValueError: Number of top boundary strands cannot be negative + """ + if isinstance(boundary, tuple): + m, n = boundary + else: + m = n = boundary + if m < 0: + raise ValueError("Number of bottom boundary strands cannot be negative") + if n < 0: + raise ValueError("Number of top boundary strands cannot be negative") + return (m, n) + + +class Tangle: + def __init__(self, boundary=2, crossings=None, entry_points=None, label=None): + """ + A tangle is a fragment of a Link with some number of boundary + strands. Tangles can be composed in various ways along their boundary strands, + including the horizontal and vertical compositions of the tangle category. + + Inputs: + + * When boundary is an integer, then the tangle has n strands coming into both + the top and the bottom of the tangle. When boundary is a pair of integers + (m, n), then the tangle has m strands coming into the bottom and n coming + into the top. + + The strands are numbered 0 to m-1 on the bottom and m to m+n-1 on the + top, both from left to right. + + * crossings is a list of Crossing or Strand objects that comprise the tangle. + * entry_points is a list of pairs (c, i) where c is a Crossing or Strand + and i indexes into c.adjacent. These pairs describe the boundary strands + in order of the strand numbering. + * label is an arbitrary label for the tangle for informational purposes, which + appears in the ``repr`` form of the tangle. + + Usually tangles should not be created directly using this constructor since the + tangle operations and various primitive tangles are sufficient to create any tangle. + """ + + m, n = decode_boundary(boundary) + + if crossings is None: + crossings = [] + for c in crossings: + if not isinstance(c, (Crossing, Strand)): + raise ValueError("Every element of crossings must be a Crossing or a Strand") + self.crossings = crossings + + # the pair for the number of lower strands and the number of upper strands + self.boundary = (m, n) + + # a list of (c, i) pairs for the boundary strands. Each c will reciprocally + # contain (self, j) where j is the strand number. The fact this is called + # 'adjacent' means that the Tangle can take part in the joining protocol + # implemented in join_strands. + self.adjacent = (m + n) * [None] + entry_points = entry_points or [] + if len(entry_points) != m + n: + raise ValueError("The number of boundary strands is not equal to the length" + " of entry_points") + for i, e in enumerate(entry_points): + join_strands((self, i), e) + + self.label = label + + def __add__(self, other): + """Put self to left of other and fuse the top-right strand of self to the top-left + strand of other and the bottom-right strand of self to the bottom-left strand of other. + + >>> (IdentityBraid(2) + BraidTangle([1])).describe() + 'Tangle[{1,2}, {3,4}, P[1,3], X[2,4,5,5]]' + """ + A, B = self.copy(), other.copy() + (mA, nA), (mB, nB) = A.boundary, B.boundary + if mA == 0 or mB == 0 or nA == 0 or nB == 0: + raise ValueError("Tangles must have at least one top and bottom strand each.") + a, b = A.adjacent, B.adjacent + join_strands(a[mA - 1], b[0]) + join_strands(a[mA + nA - 1], b[mB]) + entry_points = a[:mA - 1] + b[1:mB] + a[mA:mA + nA - 1] + b[mB + 1:] + return Tangle((mA + mB - 2, nA + nB - 2), A.crossings + B.crossings, entry_points) + + def __mul__(self, other): + """Join with self *above* other, as with braid multiplication. + (See doc.pdf) + + >>> BraidTangle([1,1]).describe() + 'Tangle[{1,2}, {3,4}, X[5,4,3,6], X[2,5,6,1]]' + >>> (BraidTangle([1])*BraidTangle([1])).describe() + 'Tangle[{1,2}, {3,4}, X[5,4,3,6], X[2,5,6,1]]' + """ + A, B = self.copy(), other.copy() + (mA, nA), (mB, nB) = A.boundary, B.boundary + if mA != nB: + raise ValueError("Tangles must have a compatible number of strands to multiply them") + + a, b = A.adjacent, B.adjacent + for i in range(mA): + join_strands(a[i], b[mB + i]) + return Tangle((mB, nA), A.crossings + B.crossings, b[:mB] + a[mA:]) + + def __neg__(self): + """Mirror image of self. + + >>> (-BraidTangle([1])).describe() + 'Tangle[{1,2}, {3,4}, X[4,3,1,2]]'""" + T = self.copy() + for c in T.crossings: + if not isinstance(c, Strand): + c.rotate_by_90() + return T + + def __or__(self, other): + """Put self to left of other. This is like tangle addition but without the fusing of strands. + + >>> (IdentityBraid(1) | CupTangle()).describe() + 'Tangle[{1}, {2,3,4}, P[1,2], P[3,4]]' + """ + A, B = self.copy(), other.copy() + (mA, nA), (mB, nB) = A.boundary, B.boundary + a, b = A.adjacent, B.adjacent + entry_points = a[:mA] + b[:mB] + a[mA:] + b[mB:] + return Tangle((mA + mB, nA + nB), A.crossings + B.crossings, entry_points) + + def copy(self): + return pickle.loads(pickle.dumps(self)) + + def rotate(self, s): + """Rotate anticlockwise by s*90 degrees. This is only for (2,2) tangles. + + See ``Tangle.reshape()`` for a generalization to all tangle shapes.""" + if self.boundary != (2, 2): + raise ValueError("Only boundary=(2,2) tangles can be rotated") + anticlockwise = [0, 1, 3, 2] + rotate = dict(zip(anticlockwise, rotate_list(anticlockwise, s))) + T = self.copy() + T.adjacent = [T.adjacent[rotate[i]] for i in range(4)] + for i, (o, j) in enumerate(T.adjacent): + o.adjacent[j] = (T, i) + return T + + def invert(self): + """Rotate anticlockwise by 90 and take the mirror image. This is only for (2,2) tangles.""" + if self.boundary != (2, 2): + raise ValueError("Only boundary=(2,2) tangles can be inverted") + return -self.rotate(1) + + def numerator_closure(self): + """The bridge closure, where consecutive pairs of strands at both the top and + at the bottom are respectively joined by caps and cups. The numbers of + strands at both the top and the bottom must be even. Returns a Link. + + A synonym for this is ``Tangle.bridge_closure()``. + + sage: BraidTangle([2,-1,2],4).numerator_closure().alexander_polynomial() + t^2 - t + 1 + sage: BraidTangle([1,1,1]).rotate(1).numerator_closure().alexander_polynomial() + t^2 - t + 1 + """ + m, n = self.boundary + if m % 2 or n % 2: + raise ValueError("To do bridge closure, both the top and bottom must have an even number of strands") + T = self.copy() + for i in range(0, m, 2): + join_strands(T.adjacent[i], T.adjacent[i + 1]) + for i in range(0, n, 2): + join_strands(T.adjacent[m + i], T.adjacent[m + i + 1]) + return Link(T.crossings, check_planarity=False) + + def denominator_closure(self): + """The braid closure, where corresponding strands between the top and bottom + are joined. The number of strands at the top must equal the number of strands at + the bottom. Returns a Link. + + A synonym for this is ``Tangle.braid_closure()``. + + sage: BraidTangle([1,1,1]).braid_closure().alexander_polynomial() + t^2 - t + 1 + sage: BraidTangle([1,-2,1,-2]).braid_closure().alexander_polynomial() + t^2 - 3*t + 1 + >>> BraidTangle([1,-2,1,-2]).braid_closure().exterior().identify() # doctest: +SNAPPY + [m004(0,0), 4_1(0,0), K2_1(0,0), K4a1(0,0), otet02_00001(0,0)] + """ + m, n = self.boundary + if m != n: + raise ValueError("To do braid closure, both the top and bottom numbers of strands must be equal") + T = self.copy() + for i in range(n): + join_strands(T.adjacent[i], T.adjacent[m + i]) + return Link(T.crossings, check_planarity=False) + + def link(self): + """If its boundary is (0, 0), return this Tangle as a Link.""" + if self.boundary != (0, 0): + raise ValueError("The boundary must be (0, 0)") + return Link(self.copy().crossings, check_planarity=False) + + def reshape(self, boundary, displace=0): + """Renumber the boundary strands so that the tangle has the new boundary + shape. This is performed by either repeatedly moving the last strands from the + bottom right to the top right or vice versa. Simultaneously, displace controls + a rotation of the tangle where the tangle is rotated clockwise by ``displace`` steps + (so, for example, if 0 <= displace < m then the strand numbered ``displace`` + becomes the new lower-left strand). + + This is a generalization of ``Tangle.rotate()``. + """ + m, n = self.boundary + Tm, Tn = decode_boundary(boundary) + if m + n != Tm + Tn: + raise ValueError("Reshaping requires the tangle have the same number of boundary" + " strands as in the new boundary.") + T = self.copy() + # The 'adjacent' array but in total counterclockwise order + adj_ccw = T.adjacent[:m] + list(reversed(T.adjacent[m:])) + adj_ccw = rotate_list(adj_ccw, displace) + + return Tangle((Tm, Tn), T.crossings, + adj_ccw[:Tm] + list(reversed(adj_ccw[Tm:]))) + + def circular_rotate(self, n): + """ + Rotate a tangle in a circular fashion clockwise, keeping the same boundary. + + This generalizes ``Tangle.rotate()``, and it is a mild specialization of ``Tangle.reshape()``. + """ + return self.reshape(self.boundary, n) + + def circular_sum(self, other, n=0): + """ + Glue two tangles together to form a link by gluing them vertically and then taking + the braid closure (the ``Tangle.denominator_closure()``). + The second tangle is rotated clockwise by n strands using ``Tangle.circular_rotate()``. + """ + Am, An = self.boundary + Bm, Bn = self.boundary + if (Am, An) != (Bn, Bm): + raise ValueError("Tangles must have compatible boundary shapes") + return (self * (other.circular_rotate(n))).denominator_closure() + + def isosig(self, root=None, over_or_under=False): + """ + Return a bunch of data which encodes the planar isotopy class of the + tangle. Of course, this is just up to isotopy of the plane + (no Reidemeister moves). A root can be specified with a CrossingStrand + and ``over_or_under`` toggles whether only the underlying + shadow (4-valent planar map) is considered or the tangle with the + over/under data at each crossing. + + >>> BraidTangle([1]).isosig() == BraidTangle([1]).circular_rotate(1).isosig() + True + >>> BraidTangle([1]).isosig() == BraidTangle([-1]).isosig() + True + """ + copy = self.copy() + copy._fuse_strands() + return planar_isotopy.min_isosig(copy, root, over_or_under) + + def is_planar_isotopic(self, other, root=None, over_or_under=False) -> bool: + return self.isosig() == other.isosig() + + def _fuse_strands(self, preserve_boundary=False, preserve_components=False): + """Fuse all strands and delete them, even ones incident to only the boundary (unless + ``preserve_boundary`` is True). This will eliminate Strands that are loops as well. + + If ``preserve_components`` is True, then do not fuse strands that have the + ``component_idx`` attribute.""" + for s in reversed(self.crossings): + if isinstance(s, Strand): + # check that the strand is not only incident to the boundary + if preserve_boundary and all(a[0] == self for a in s.adjacent): + continue + if preserve_components and s.component_idx is not None: + continue + s.fuse() + self.crossings.remove(s) + + def __repr__(self): + return "" % self.label + + def describe(self, fuse_strands=True): + """Give a PD-like description of the tangle in the form + Tangle[{lower arcs}, {upper arcs}, P and X codes]. + + If fuse_strands is True, then fuse all internal Strand nodes first. + + >>> BraidTangle([1]).describe() + 'Tangle[{1,2}, {3,4}, X[2,4,3,1]]' + """ + T = self.copy() + if fuse_strands: + T._fuse_strands(preserve_boundary=True, preserve_components=True) + T.label = 0 + # give each crossing/strand a unique identifier, which + # is used for calculating ids for arcs + for i, c in enumerate(T.crossings): + c.label = i + 1 + arc_ids = {} + + def arc_key(c, i): + """For the given entity c and index into c.adjacent, + create a name for the incident arc. This gives something + that's suitable for use as a dictionary key.""" + d, j = c.adjacent[i] + return tuple(sorted([(c.label, i), (d.label, j)])) + + def arc_id(c, i): + """Get the unique integer id associated to the arc, generating + a fresh one if needed.""" + return arc_ids.setdefault(arc_key(c, i), len(arc_ids) + 1) + m, n = T.boundary + lower = "{" + ",".join(str(arc_id(T, i)) for i in range(m)) + "}" + upper = "{" + ",".join(str(arc_id(T, i)) for i in range(m, m + n)) + "}" + parts = [] + for c in T.crossings: + arcs = [arc_id(c, i) for i in range(len(c.adjacent))] + if isinstance(c, Crossing): + parts.append("X[%s,%s,%s,%s]" % tuple(arcs)) + elif isinstance(c, Strand): + if c.component_idx is not None: + parts.append(f"P[{arcs[0]},{arcs[1]}, component->{c.component_idx}]") + else: + parts.append(f"P[{arcs[0]},{arcs[1]}]") + else: + raise TypeError("Unexpected entity") + return f"Tangle[{lower}, {upper}{''.join(', ' + p for p in parts)}]" \ No newline at end of file diff --git a/spherogram_src/links/simplify.py b/spherogram_src/links/simplify.py index e6f91c5..bb2c122 100644 --- a/spherogram_src/links/simplify.py +++ b/spherogram_src/links/simplify.py @@ -74,7 +74,7 @@ def remove_crossings(link, eliminate): for C in eliminate: link.crossings.remove(C) new_components = [] - for component in link.link_components: + for component in link.components: for C in eliminate: for cep in C.entry_points(): try: @@ -83,9 +83,10 @@ def remove_crossings(link, eliminate): pass if len(component): new_components.append(component) - components_removed = len(link.link_components) - len(new_components) + components_removed = len(link.components) - len(new_components) link.unlinked_unknot_components += components_removed - link.link_components = new_components + + link.components = new_components def reidemeister_I(link, C): @@ -95,15 +96,18 @@ def reidemeister_I(link, C): Returns the pair: {crossings eliminated}, {crossings changed} """ elim, changed = set(), set() - for i in range(4): - if C.adjacent[i] == (C, (i + 1) % 4): - (A, a), (B, b) = C.adjacent[i + 2], C.adjacent[i + 3] - elim = {C} - if C != A: - A[a] = B[b] - changed = {A, B} - - remove_crossings(link, elim) + + if isinstance(C, Crossing): + for i in range(4): + if C.adjacent[i] == (C, (i + 1) % 4): + (A, a), (B, b) = C.adjacent[i + 2], C.adjacent[i + 3] + elim = {C} + if C != A: + A[a] = B[b] + changed = {A, B} + + remove_crossings(link, elim) + return elim, changed @@ -118,25 +122,28 @@ def reidemeister_I_and_II(link, A): if not eliminated: for a in range(4): (B, b), (C, c) = A.adjacent[a], A.adjacent[a + 1] - if B == C and (b - 1) % 4 == c and (a + b) % 2 == 0: - eliminated, changed = reidemeister_I(link, B) - if eliminated: - break - else: - W, w = A.adjacent[a + 2] - X, x = A.adjacent[a + 3] - Y, y = B.adjacent[b + 1] - Z, z = B.adjacent[b + 2] - eliminated = {A, B} - if W != B: - W[w] = Z[z] - changed.update({W, Z}) - if X != B: - X[x] = Y[y] - changed.update({X, Y}) - remove_crossings(link, eliminated) - break - + if all(isinstance(x, Crossing) for x in (B,C)): + if B == C and (b - 1) % 4 == c and (a + b) % 2 == 0: + eliminated, changed = reidemeister_I(link, B) + if eliminated: + break + else: + W, w = A.adjacent[a + 2] + X, x = A.adjacent[a + 3] + Y, y = B.adjacent[b + 1] + Z, z = B.adjacent[b + 2] + eliminated = {A, B} + if W != B: + W[w] = Z[z] + changed.update({W, Z}) + if X != B: + X[x] = Y[y] + changed.update({X, Y}) + remove_crossings(link, eliminated) + break + + + changed &= {x for x in changed if isinstance(x, Crossing)} return eliminated, changed @@ -177,10 +184,15 @@ def basic_simplify(link, build_components=True, to_visit=None, # Redo the strand labels (used for DT codes) if (success and build_components) or force_build_components: component_starts = [] - for component in link.link_components: + + for component in link.components: assert len(component) > 0 if len(component) > 1: - a, b = component[:2] + if isinstance(component[0].crossing, (Strand, Crossing)): + a, b = component[:2] + else: + assert len(component) > 3 + a, b = component[1:3] else: a = component[0] b = a.next() @@ -820,7 +832,8 @@ def clear_orientations(link): """ Resets the orientations on the crossings of a link to default values """ - link.link_components = None + link.components = None + for i in link.crossings: i.sign = 0 i.directions.clear() diff --git a/spherogram_src/links/tangles.py b/spherogram_src/links/tangles.py index 0ade8b9..44c5dcf 100644 --- a/spherogram_src/links/tangles.py +++ b/spherogram_src/links/tangles.py @@ -21,9 +21,18 @@ """ import pickle -from .links import Crossing, Strand, Link -from . import planar_isotopy +from collections import OrderedDict, Counter +from .ordered_set import OrderedSet +from .links import Crossing, Strand, Link, CrossingStrand, CrossingEntryPoint +class CyclicList(list): + def __init__(self, iterable): + super().__init__(iterable) + + def __getitem__(self, i): + if isinstance(i, slice): + return super().__getitem__(i) + return super().__getitem__(i % len(self)) def join_strands(x, y): """ @@ -76,9 +85,28 @@ def decode_boundary(boundary): raise ValueError("Number of top boundary strands cannot be negative") return (m, n) +class ArcLabels(OrderedDict): + def __init__(self, iterable = []): + super().__init__(iterable) + + self.counter = 0 + + def add(self, c, advance): + if c not in self: + self[c] = self.counter + if advance: + self.counter += 1 + else: + raise ValueError(f"Each CEP should only be labeled once, but {c} is already labeled with {self[c]}") + +class TangleComponents(list): + def add(self, c): + component = c.component() + self.append(component) + return component class Tangle: - def __init__(self, boundary=2, crossings=None, entry_points=None, label=None): + def __init__(self, boundary=2, crossings=None, entry_points=None, build = True, label=None, start_orientations = None, component_starts = None): """ A tangle is a fragment of a Link with some number of boundary strands. Tangles can be composed in various ways along their boundary strands, @@ -101,42 +129,528 @@ def __init__(self, boundary=2, crossings=None, entry_points=None, label=None): * label is an arbitrary label for the tangle for informational purposes, which appears in the ``repr`` form of the tangle. - Usually tangles should not be created directly using this constructor since the - tangle operations and various primitive tangles are sufficient to create any tangle. + Tangles now support creation from PD_code, for example: + + >>> Tangle(3, [[0,4,1,5],[1,8,2,9],[2,7,3,6],[5,9,6,10]], [0,4,8,10,3,7], label = 'RIII') + + + see doc of ``PD_code`` for more details. """ + if label is None: + self.label = id(self) + else: + self.label = label - m, n = decode_boundary(boundary) + m, n = decode_boundary(boundary) + component_starts = component_starts + start_orientations = start_orientations + self.strand_labels = CyclicList(m * [None] + n * [None]) + self.strand_components = CyclicList(m * [None] + n * [None]) if crossings is None: crossings = [] - for c in crossings: - if not isinstance(c, (Crossing, Strand)): - raise ValueError("Every element of crossings must be a Crossing or a Strand") - self.crossings = crossings + else: + if isinstance(crossings, str) or isinstance(entry_points, str): + raise NotImplementedError("Not Implemented. If you are trying to create a tangle from a PD code, input the PD code as a list instead.") + + if (len(crossings) > 0 and not isinstance(crossings[0], (Strand, Crossing)))\ + or (entry_points is not None and len(entry_points) > 0 and not isinstance(entry_points[0], (CrossingStrand, list, tuple))): + assert component_starts is None and start_orientations is None, "Specifying components_starts and start_orientations is not compatible with creating from PD codes" + + crossings, component_starts, entry_points = self._crossings_from_PD_code(crossings, entry_points) + start_orientations = component_starts[:] # the pair for the number of lower strands and the number of upper strands self.boundary = (m, n) + self.boundary_strands = CyclicList([]) + # -1 if entering Tangle, 1 if exiting Tangle, 0 if not yet oriented. + self.boundary_signs = CyclicList(m * [0] + n * [0]) # a list of (c, i) pairs for the boundary strands. Each c will reciprocally # contain (self, j) where j is the strand number. The fact this is called # 'adjacent' means that the Tangle can take part in the joining protocol # implemented in join_strands. - self.adjacent = (m + n) * [None] + self.adjacent = CyclicList((m + n) * [None]) entry_points = entry_points or [] if len(entry_points) != m + n: raise ValueError("The number of boundary strands is not equal to the length" " of entry_points") + for i, e in enumerate(entry_points): - join_strands((self, i), e) + this_strand = Strand(label = f'TSE({str(self)}, {i})') + self.boundary_strands.append(this_strand) + join_strands(e, (this_strand, 1)) + join_strands((self, i), (this_strand, 0)) + + if not all(isinstance(c, (Crossing, Strand)) for c in crossings): + raise ValueError("Every element of crossings must be a Crossing or a Strand") + + self.unlinked_unknot_components = 0 + # Note that crossings in Tangle can contain Strands for now + # which will be removed after build + self.crossings = crossings + + if build: + self._build(start_orientations, component_starts) + assert self.is_oriented(), 'Tangle is not oriented after build' + + # Remove all Strands from crossings and components. + # Note that this will not affect strands in boundary_strands + for s in reversed(crossings): + if isinstance(s, Strand): + comp = self.components[s.strand_component] + + if isinstance(comp[0].crossing, Tangle): + for cep in reversed(comp): + if cep.crossing == s: + comp.remove(cep) + break + else: + raise RuntimeError(f"Component strand {s} not found in component {comp}") + + # Note that the components are always built following the orientation + # hence below always insists that the comp_id is labeled on the entrance strand + if s.component_idx is not None: + comp_id = s.component_idx + if comp[1].crossing.component_idx is not None: + assert comp[1].crossing.component_idx == comp_id + else: + comp[1].crossing.component_idx = comp_id + if s.is_loop(): + self.unlinked_unknot_components += 1 + else: + s.fuse() + self.crossings.remove(s) + + def __getitem__(self, i): + return (self, i % (self.boundary[0] + self.boundary[1])) + + def __setitem__(self, i, other): + o, j = other + self.adjacent[i % (self.boundary[0] + self.boundary[1])] = other + o.adjacent[j] = (self, i) + + def is_upward(self): + return self.boundary_signs == CyclicList([-1] * self.boundary[0] + [1] * self.boundary[1]) + + def is_downward(self): + return self.boundary_signs == CyclicList([1] * self.boundary[0] + [-1] * self.boundary[1]) + + def is_oriented(self): + return all(s != 0 for s in self.boundary_signs) + + def make_upward(self): + """ + Change the orientation of the tangle, trying to make it upwardly oriented. + The order of components is preserved. + + >>> T = BraidTangle([1,2,1]) + >>> T.reverse_orientation([1,2]) + >>> T.is_upward() + False + >>> T.make_upward() + >>> T.is_upward() + True + Like alternating() for Links, this may fail silently if there is no orientation + which makes the tangle upwardly oriented. + + >>> T = RationalTangle(-2,3) + >>> T.make_upward() + >>> T.is_upward() + False + """ + if self.is_upward(): + return + + assert self.is_oriented(), 'Tangle should be oriented to tell if it is upward' + + to_reverse = set() + for i in range(self.boundary[0]): + if self.boundary_signs[i] == 1: + to_reverse.add(self.boundary_strands[i].strand_component) + + self.reverse_orientation(to_reverse) + + def entry_points(self): + assert self.is_oriented(), 'Tangle should be oriented to tell the entry points' + return [CrossingEntryPoint(self, i) for i in range(self.boundary[0] + self.boundary[1]) + if self.boundary_signs[i] == -1] + + def update_label(self, label): self.label = label + for i, s in enumerate(self.boundary_strands): + s.label = f'TSE({self}, {i})' + + def _build(self, start_orientations=None, component_starts=None): + self._orient_crossings(start_orientations=start_orientations) + self._build_components(component_starts=component_starts) + + def _clear(self): + self.components = None + for c in self.crossings: + c._clear() + for s in self.boundary_strands: + s._clear() + + self.boundary_signs = CyclicList(self.boundary[0] * [0] + self.boundary[1] * [0]) + self.strand_labels = CyclicList(self.boundary[0] * [None] + self.boundary[1] * [None]) + self.strand_components = CyclicList(self.boundary[0] * [None] + self.boundary[1] * [None]) + + def _rebuild(self, same_components_and_orientations = False): + if same_components_and_orientations: + # Hopefully we have enough of the original components left + # to figure out what this is. Otherwise, new choices will + # be made as in the default algorithm. + start_css = self._start_orientations() + + self._clear() + if same_components_and_orientations: + self._build(start_orientations=start_css, + component_starts=start_css) + else: + self._build() + + def all_crossings_oriented(self): + return all(c.sign != 0 for c in self.crossings + self.boundary_strands) + + def _orient_crossings(self, start_orientations=None): + if self.all_crossings_oriented(): + return + if start_orientations is None: + start_orientations = list() + else: # copy as algorithm modifies this list + start_orientations = list(start_orientations) + + remaining = OrderedSet( + [(c, i) for c in self.crossings + list(reversed(self.boundary_strands)) + for i in range(c._adjacent_len) if c.sign == 0]) + + while len(remaining): + if len(start_orientations) > 0: + c, i = start = start_orientations.pop() + else: + c, i = start = remaining.pop() + + is_reversed = False + finished = False + while not finished: + if is_reversed: + c.make_tail(i) + else: + c.make_head(i) + remaining.discard((c, i)) + + if not c.adjacent[i][0] == self: + d, j = c.adjacent[i] + remaining.discard((d, j)) + s = d._adjacent_len // 2 + c, i = d, (j + s) % (2 * s) + + finished = (c, i) == start + else: + boundary_index = c.adjacent[i][1] + assert self.boundary_signs[boundary_index] == 0, f'Boundary {boundary_index} is unexpectedly signed, something is wrong with the gluings' + + if is_reversed: + # Hit the boundary of the tangle from both sides, + # done with this component + self.boundary_signs[boundary_index] = -1 + finished = True + else: + # Hit the boundary of the tangle, + # now go back and orient reversely + self.boundary_signs[boundary_index] = 1 + is_reversed = True + + c, i = start + s = c._adjacent_len // 2 + c, i = c, (i + s) % (2 * s) + + for c in self.crossings: + c.orient() + + for s in self.boundary_strands: + s.orient() + + def _build_components(self, component_starts=None): + """ + Each component is stored as a list of *entry points* to crossings. + If the component starts and ends at the boundary of tangles, + the corresponding CrossingEntryPoint(self, boundary_index) + will be put at the tail and the head of the list. + + If provided, the component_starts must consist of one + CrossingEntryPoint per component. + + >>> len(RationalTangle(-2, 3).components) + 2 + >>> len(Tangle(3, [[0,4,1,5],[1,8,2,9],[2,7,3,6],[5,9,6,10]], + ... [0,4,8,10,3,7], label = 'RIII').components) + 3 + >>> len(((RationalTangle(2,3)+IdentityBraid(1))|(RationalTangle(2,5)+ComponentTangle(-1))).components) + 2 + """ + if component_starts is not None: + # Take all CrossingStrand and CrossingEntryPoint objects + # and turn them into CrossingEntryPoints + component_starts = [cs.crossing.entry_points()[cs.strand_index % 2 if isinstance(cs.crossing, Crossing) else 0] + for cs in component_starts] + remaining, components = OrderedSet(self.crossing_entries()), TangleComponents() + other_crossing_entries = [] + self.labels = labels = ArcLabels() + for c in self.crossings: + c._clear_strand_info() + for s in self.boundary_strands: + s._clear_strand_info() + + while len(remaining): + if component_starts: + d = component_starts[len(components)] + elif len(components) == 0: + d = remaining.pop() + else: # prioritize labeling crossing strands that are adjacent to already labeled ones + found, comp_index = False, 0 + while not found and comp_index < len(components): + others = other_crossing_entries[comp_index] + if others: + for j, d in enumerate(others): + if d.component_label() is None: + if labels[d.other()] % 2 == 0: + d = d.next() + found = True + break + other_crossing_entries[comp_index] = others[j:] + comp_index += 1 + + if not found: + d = remaining.pop() + + component = components.add(d) + + # Label arcs along the component + for i, c in enumerate(component): + if isinstance(c.crossing, Tangle) and self.boundary_signs[c.strand_index] == 0: + assert len(component) > 2 + + if i == 0: + self.boundary_signs[c.strand_index] = -1 + else: + assert i == len(component) - 1 + self.boundary_signs[c.strand_index] = 1 + # if is a Crossing or at the end of the component, advance the label + # otherwise, don't advance the label since we will still be on the same arc + if isinstance(c.crossing, Crossing) or i == len(component) - 1: + advance = True + else: + advance = False + + labels.add(c, advance) + + others = [] + for c in component: + c.label_crossing(len(components) - 1, labels) + o = c.other() + if o is not None and o.component_label() is None: + others.append(o) + other_crossing_entries.append(others) + remaining.difference_update(component) + + self.components = components + + def crossing_entries(self): + ans = [] + for C in self.crossings: + ans += C.entry_points() + for s in reversed(self.boundary_strands): + ans += s.entry_points() + + return ans + + def _crossings_from_PD_code(self, code, entry_points): + """ + entry_points as INPUT: a list of labels of arcs left open in the tangle + as OUTPUT: a list of CrossingStrands + """ + assert Counter(entry_points).most_common(1)[0][1] <= 2, "Each entry point label should appear at most twice" + + labels = set() + for X in code: + for i in X: + labels.add(i) + for x in entry_points: + labels.add(x) + + gluings = OrderedDict() + + for c, X in enumerate(code): + for i, x in enumerate(X): + if x in gluings: + gluings[x].append((c, i)) + else: + gluings[x] = [(c, i)] + + if any(len(v) > 2 for v in gluings.values()): + raise ValueError("PD code isn't consistent") + + crossings = [Crossing(i) for i, d in enumerate(code)] + + for item in gluings.values(): + if len(item) > 1: + (c, i), (d, j) = item + crossings[c][i] = crossings[d][j] + + entry_strands = [] + entry_dict = dict() + + for i, x in enumerate(entry_points): + if x in gluings: + entry_strands.append(crossings[gluings[x][0][0]].crossing_strands()[gluings[x][0][1]]) + else: + this_strand = Strand(label = f'PDSE({str(self)}, {i})') + crossings.append(this_strand) + if x not in entry_dict: + entry_strands.append((this_strand, 0)) + entry_dict[x] = (this_strand, 1) + else: + entry_strands.append((this_strand, 1)) + join_strands(entry_dict[x], (this_strand, 0)) + + component_starts = self._component_starts_from_PD( + code, labels, gluings, entry_dict) + + component_starts = [crossings[c].crossing_strands()[i] + if not isinstance(c, Strand) else CrossingStrand(c, i) + for (c, i) in component_starts] + + return crossings, component_starts, entry_strands + + def PD_code(self, KnotTheory=False, min_strand_index = 0): + """ + The planar diagram code for the tangle. Unlike for links, it returns two extra fields, + boundary and entry_info in addition to the PD code of crossings, in order to specify + how the boundary and entries of the tangle is arranged. The fields are ordered as follows: + + boundary, PD, entry_info + + so that they can be unpacked immediately for creating Tangles. + + >>> RationalTangle(-1,2).PD_code() + ((2, 2), [(1, 5, 2, 4), (3, 1, 4, 0)], [0, 3, 2, 5]) + >>> BraidTangle([1,2,1]).PD_code() + ((3, 3), [(7, 5, 8, 4), (6, 2, 7, 1), (3, 1, 4, 0)], [0, 3, 6, 8, 5, 2]) + >>> Tangle(*RationalTangle(-1,2).PD_code()).PD_code() + ((2, 2), [(1, 5, 2, 4), (3, 1, 4, 0)], [0, 3, 2, 5]) + """ + PD = [] + entry_info = [s + min_strand_index for s in self.strand_labels] + + for c in self.crossings: + if isinstance(c, Crossing): + PD.append([s + min_strand_index for s in c.strand_labels]) + + if KnotTheory: + PD = "PD" + repr(PD).replace('[', 'X[')[1:] + entry_info = "EP" + repr(entry_info) + else: + PD = [tuple(x) for x in PD] + + return self.boundary, PD, entry_info + + def _component_starts_from_PD(self, code, labels, gluings, entry_dict): + """ + A PD code determines an order and orientation on the tangle + components as follows, where we view the code as labels on the + strands at the point where two crossings are stuck together. + + 1. The minimum label on each component is used to order the + components. + + 2. Each component is oriented by finding its minimal label, + looking at the labels of its two neighbors, and then + orienting the component towards the smaller of those two. + + This is designed so that a PLink-generated PD_code results in a + link with the same component order and orientation. + """ + starts = [] + while labels: + m = min(labels) + labels.remove(m) + + if m not in gluings: + next_label = m + starts.append(entry_dict[m]) + elif len(gluings[m]) == 1: + # entrance strand of the tangle + [(c, index)] = gluings[m] + + j = (index + 2) % 4 + next_label = code[c][j] + direction = (c, j) + + starts.append(direction) + else: + (c1, index1), (c2, index2) = gluings[m] + if c1 == c2: + # loop at strand, take next strand to be next smallest label + # on crossing + next_label = min(set(code[c1]) - {m}) + direction = (c1, code[c1].index(next_label)) + starts.append(direction) + else: + # strand connects two different crossings, take next strand to + # be next smallest label on two 'opposite' strands + j1, j2 = (index1 + 2) % 4, (index2 + 2) % 4 + l1, l2 = code[c1][j1], code[c2][j2] + if l1 < l2: + next_label = l1 + direction = (c1, j1) + elif l2 < l1: + next_label = l2 + direction = (c2, j2) + else: + # We have a component of length 2, so now rely on + # the convention that the first position at a PD + # crossing is a directed entry point. (If both + # crossings are over or both under, the + # orientation is arbitrary anyway.) + next_label = l1 + + # The strand labeled m is oriented c2 --> c1 if + # and only if either l1 = l2 is the incoming + # understrand of c2 or m is incoming understrand + # at c1. + if code[c2][0] == l1 or code[c1][0] == m: + direction = (c1, j1) + else: + direction = (c2, j2) + + starts.append(direction) + + # Component start recorded. Erase the rest of the component + # by traversing along it and remove the labels + while next_label != m: + labels.remove(next_label) + g = gluings[next_label] + if len(g) == 1: + break + other_direction = g[1 - g.index(direction)] + direction = (other_direction[0], (other_direction[1] + 2) % 4) + next_label = code[direction[0]][direction[1]] + + return starts + + + # The following operators always clear the current orientations on both tangles + # and recreate an orientation with default behaviour. def __add__(self, other): """Put self to left of other and fuse the top-right strand of self to the top-left strand of other and the bottom-right strand of self to the bottom-left strand of other. >>> (IdentityBraid(2) + BraidTangle([1])).describe() - 'Tangle[{1,2}, {3,4}, P[1,3], X[2,4,5,5]]' + 'Tangle[{1,2}, {3,4}, X[2,4,5,5], P[1,3]]' """ A, B = self.copy(), other.copy() (mA, nA), (mB, nB) = A.boundary, B.boundary @@ -146,7 +660,15 @@ def __add__(self, other): join_strands(a[mA - 1], b[0]) join_strands(a[mA + nA - 1], b[mB]) entry_points = a[:mA - 1] + b[1:mB] + a[mA:mA + nA - 1] + b[mB + 1:] - return Tangle((mA + mB - 2, nA + nB - 2), A.crossings + B.crossings, entry_points) + + crossings = A.crossings + A.boundary_strands + B.crossings + B.boundary_strands + + for c in crossings: + c._clear() + + return Tangle((mA + mB - 2, nA + nB - 2), + crossings, + entry_points) def __mul__(self, other): """Join with self *above* other, as with braid multiplication. @@ -165,7 +687,15 @@ def __mul__(self, other): a, b = A.adjacent, B.adjacent for i in range(mA): join_strands(a[i], b[mB + i]) - return Tangle((mB, nA), A.crossings + B.crossings, b[:mB] + a[mA:]) + + crossings = A.crossings + A.boundary_strands + B.crossings + B.boundary_strands + + for c in crossings: + c._clear() + + return Tangle((mB, nA), + crossings, + b[:mB] + a[mA:]) def __neg__(self): """Mirror image of self. @@ -176,19 +706,57 @@ def __neg__(self): for c in T.crossings: if not isinstance(c, Strand): c.rotate_by_90() + c.orient() return T + def _start_orientations(self): + """ + Obtain the start orientations according to the current orientation + and components (the latter may be outdated) + """ + start_css = [] + for comp in self.components: + for cs in comp: + if cs.crossing in self.crossings + self.boundary_strands: + s = cs.crossing._adjacent_len // 2 + start_css.append(cs.rotate(s)) + break + + return start_css + def __or__(self, other): - """Put self to left of other. This is like tangle addition but without the fusing of strands. + """ + Put self to left of other. This is like tangle addition but without the fusing of strands. + Preserves the orientations of both tangles, since no gluing happens. >>> (IdentityBraid(1) | CupTangle()).describe() 'Tangle[{1}, {2,3,4}, P[1,2], P[3,4]]' + + >>> T = BraidTangle([1,2,1]) + >>> T.reverse_orientation([1,2]) + >>> T.is_upward() + False + >>> T.boundary_signs + [-1, 1, 1, -1, -1, 1] + >>> TT = T | RationalTangle(1,2) + >>> TT.is_upward() + False + >>> TT.boundary_signs + [-1, 1, 1, -1, -1, -1, -1, 1, 1, 1] """ A, B = self.copy(), other.copy() (mA, nA), (mB, nB) = A.boundary, B.boundary a, b = A.adjacent, B.adjacent entry_points = a[:mA] + b[:mB] + a[mA:] + b[mB:] - return Tangle((mA + mB, nA + nB), A.crossings + B.crossings, entry_points) + crossings = A.crossings + A.boundary_strands + B.crossings + B.boundary_strands + + start_css = A._start_orientations() + B._start_orientations() + + return Tangle((mA + mB, nA + nB), + crossings, + entry_points, + start_orientations=start_css, + component_starts=start_css) def copy(self): return pickle.loads(pickle.dumps(self)) @@ -202,9 +770,14 @@ def rotate(self, s): anticlockwise = [0, 1, 3, 2] rotate = dict(zip(anticlockwise, rotate_list(anticlockwise, s))) T = self.copy() + T.adjacent = [T.adjacent[rotate[i]] for i in range(4)] for i, (o, j) in enumerate(T.adjacent): o.adjacent[j] = (T, i) + + T.boundary_strands = [T.boundary_strands[rotate[i]] for i in range(4)] + T._rebuild(True) + return T def invert(self): @@ -233,7 +806,12 @@ def numerator_closure(self): join_strands(T.adjacent[i], T.adjacent[i + 1]) for i in range(0, n, 2): join_strands(T.adjacent[m + i], T.adjacent[m + i + 1]) - return Link(T.crossings, check_planarity=False) + + crossings = T.crossings + T.boundary_strands + for c in crossings: + c._clear() + + return Link(crossings, check_planarity=False) def denominator_closure(self): """The braid closure, where corresponding strands between the top and bottom @@ -255,13 +833,23 @@ def denominator_closure(self): T = self.copy() for i in range(n): join_strands(T.adjacent[i], T.adjacent[m + i]) - return Link(T.crossings, check_planarity=False) + + crossings = T.crossings + T.boundary_strands + for c in crossings: + c._clear() + + return Link(crossings, check_planarity=False) def link(self): """If its boundary is (0, 0), return this Tangle as a Link.""" if self.boundary != (0, 0): raise ValueError("The boundary must be (0, 0)") - return Link(self.copy().crossings, check_planarity=False) + + crossings = self.copy().crossings + for c in crossings: + c._clear() + + return Link(crossings, check_planarity=False) def reshape(self, boundary, displace=0): """Renumber the boundary strands so that the tangle has the new boundary @@ -272,19 +860,42 @@ def reshape(self, boundary, displace=0): becomes the new lower-left strand). This is a generalization of ``Tangle.rotate()``. + + >>> T = BraidTangle([1,2,1]) + >>> T.PD_code() + ((3, 3), [(7, 5, 8, 4), (6, 2, 7, 1), (3, 1, 4, 0)], [0, 3, 6, 8, 5, 2]) + >>> T.reshape((4,2)).PD_code() + ((4, 2), [(7, 5, 8, 4), (6, 2, 7, 1), (3, 1, 4, 0)], [0, 3, 6, 2, 8, 5]) + >>> T.reshape((4,2), displace = 1).PD_code() + ((4, 2), [(7, 5, 8, 4), (6, 2, 7, 1), (3, 1, 4, 0)], [3, 6, 2, 5, 0, 8]) """ m, n = self.boundary Tm, Tn = decode_boundary(boundary) + if (m, n) == (Tm, Tn) and displace == 0: + return self if m + n != Tm + Tn: raise ValueError("Reshaping requires the tangle have the same number of boundary" " strands as in the new boundary.") + anticlockwise = [i for i in range(m)] + list(reversed([m + i for i in range(n)])) + rotate = dict(zip(anticlockwise, rotate_list(anticlockwise, displace))) + T = self.copy() + + displaced_adj = [T.adjacent[rotate[i]] for i in range(m + n)] # The 'adjacent' array but in total counterclockwise order - adj_ccw = T.adjacent[:m] + list(reversed(T.adjacent[m:])) - adj_ccw = rotate_list(adj_ccw, displace) + adj_ccw = displaced_adj[:m] + list(reversed(displaced_adj[m:])) + T.adjacent = adj_ccw[:Tm] + list(reversed(adj_ccw[Tm:])) + T.boundary = (Tm, Tn) + for i, (o, j) in enumerate(T.adjacent): + o.adjacent[j] = (T, i) + + displaced_bd_strands = [T.boundary_strands[rotate[i]] for i in range(m + n)] + bd_strands_ccw = displaced_bd_strands[:m] + list(reversed(displaced_bd_strands[m:])) + T.boundary_strands = bd_strands_ccw[:Tm] + list(reversed(bd_strands_ccw[Tm:])) + + T._rebuild(True) - return Tangle((Tm, Tn), T.crossings, - adj_ccw[:Tm] + list(reversed(adj_ccw[Tm:]))) + return T def circular_rotate(self, n): """ @@ -306,6 +917,15 @@ def circular_sum(self, other, n=0): raise ValueError("Tangles must have compatible boundary shapes") return (self * (other.circular_rotate(n))).denominator_closure() + def _to_old_tangle(self): + from . import old_tangles + copy = self.copy() + + return old_tangles.Tangle(copy.boundary, + copy.crossings + copy.boundary_strands, + copy.adjacent, + copy.label) + def isosig(self, root=None, over_or_under=False): """ Return a bunch of data which encodes the planar isotopy class of the @@ -319,31 +939,175 @@ def isosig(self, root=None, over_or_under=False): True >>> BraidTangle([1]).isosig() == BraidTangle([-1]).isosig() True + >>> BraidTangle([1,1]).isosig() == BraidTangle([-1,-1]).isosig() + True + >>> BraidTangle([1,1]).isosig(over_or_under=True) == BraidTangle([-1,-1]).isosig(over_or_under=True) + False """ - copy = self.copy() - copy._fuse_strands() - return planar_isotopy.min_isosig(copy, root, over_or_under) + + return self._to_old_tangle().isosig(root = root, + over_or_under=over_or_under) + + def reverse_orientation(self, component_index): + """ + Reverse the orientation of components specified by component_index, + changing the current tangle and the signs of crossings. + + component_index: either a single index of component or a list of indices of components + + >>> T = BraidTangle([1,2,1]) + >>> T + + >>> T.PD_code() + ((3, 3), [(7, 5, 8, 4), (6, 2, 7, 1), (3, 1, 4, 0)], [0, 3, 6, 8, 5, 2]) + >>> T.reverse_orientation(1) + >>> T.PD_code() + ((3, 3), [(7, 3, 8, 4), (6, 2, 7, 1), (4, 0, 5, 1)], [0, 5, 6, 8, 3, 2]) + >>> T.reverse_orientation([1,2]) + >>> T.PD_code() + ((3, 3), [(6, 4, 7, 5), (7, 1, 8, 2), (3, 1, 4, 0)], [0, 3, 8, 6, 5, 2]) + >>> T.reverse_orientation([0,2]) + >>> T.PD_code() + ((3, 3), [(7, 5, 8, 4), (6, 0, 7, 1), (3, 1, 4, 2)], [2, 3, 6, 8, 5, 0]) + >>> T.reverse_orientation([0]) + >>> T.PD_code() + ((3, 3), [(7, 5, 8, 4), (6, 2, 7, 1), (3, 1, 4, 0)], [0, 3, 6, 8, 5, 2]) + """ + if not isinstance(component_index, (set, list, tuple)): + component_index = [component_index] + + org_entries = [] + for comp in self.components: + for cs in comp: + if cs.crossing in self.crossings + self.boundary_strands: + org_entries.append(cs) + break + + new_starts = [] + for i, cs in enumerate(org_entries): + if i not in component_index: + c, e = cs.crossing, cs.strand_index + s = c._adjacent_len // 2 + reversed_cs = CrossingStrand(c, (e + s) % (2 * s)) + new_starts.append(reversed_cs) + else: + new_starts.append(cs) + + self._clear() + self._build(start_orientations = new_starts, + component_starts = new_starts) + + def faces(self): + """ + The faces are the complementary regions of the tangle diagram in the disk, + where the boundary of the disk is thought of as the cusp. + + Each face is given as a list of corners of crossings as one + goes around *clockwise*. These corners are recorded as + CrossingStrands, where CrossingStrand(c, j) denotes the corner + of the face abutting crossing c between strand j and j + 1; + similarly, if c is the tangle itself, it denots the corner + as one stands at the j-th boundary entry and look *counterclockwisely*. + + Alternatively, the sequence of CrossingStrands can be regarded + as the *heads* of the oriented edges of the face. + + >>> len(IdentityBraid(2).faces()) + 3 + >>> len(BraidTangle([1,2,1]).faces()) + 7 + """ + corners = OrderedSet([CrossingStrand(c, i) + for c in self.crossings + self.boundary_strands for i in range(c._adjacent_len)]) + faces = [] + while len(corners): + cs0 = corners.pop() + face = [cs0] + next = cs0 + while True: + # Next two lines equiv to: next = next.next_corner() + c, e = next.crossing, next.strand_index + if isinstance(c, Tangle): + if e == 0: + next = CrossingStrand(*c.adjacent[c.boundary[0]]) + elif e < c.boundary[0]: + next = CrossingStrand(*c.adjacent[e-1]) + elif e < c.boundary[0] + c.boundary[1] - 1: + next = CrossingStrand(*c.adjacent[e+1]) + else: + assert e == c.boundary[0] + c.boundary[1] - 1 + next = CrossingStrand(*c.adjacent[c.boundary[0]-1]) + else: + next = next.next_corner() + + if next == cs0: + faces.append(face) + break + else: + corners.discard(next) + face.append(next) + + return faces + + def simplify(self, mode = 'basic', type_III_limit = 100): + """ + Tries to simplify the tangle diagram. Returns whether it succeeded + in reducing the number of crossings. Modifies the tangle in place, + and unknot components which are also unlinked may be silently discarded. + The ordering of ``components`` is not always preserved. + + The following strategies can be employed. + + 1. In the default ``basic`` mode, it does Reidemeister I and II moves + until none are possible. + + 2. In ``level`` mode, it does random Reidemeister III moves, reducing + the number of crossings via type I and II moves whenever possible. + The process stops when it has done ``type_III_limit`` *consecutive* + type III moves without any simplification. + + The ``pickup`` and ``global`` modes are currently not available for tangles. + + Some examples: + + >>> T = Tangle(2, [[0,3,1,4],[1,5,2,4]], [0,3,2,5], label = 'RII') + >>> T + + >>> T.simplify('basic') + True + >>> T + + >>> T.simplify('basic') # Already done all it can + False + + >>> T = Tangle(3, [[0,4,1,5],[1,8,2,9],[2,7,3,6],[5,9,6,10]], + ... [0,4,8,10,3,7], label = 'RIII') + >>> T + + >>> T.simplify('basic') + False + >>> T # No change happens + + >>> T.simplify('level') + True + >>> T + + """ + from . import simplify + if mode == 'basic': + return simplify.basic_simplify(self) + elif mode == 'level': + return simplify.simplify_via_level_type_III(self, type_III_limit) + else: + raise NotImplementedError() def is_planar_isotopic(self, other, root=None, over_or_under=False) -> bool: - return self.isosig() == other.isosig() - - def _fuse_strands(self, preserve_boundary=False, preserve_components=False): - """Fuse all strands and delete them, even ones incident to only the boundary (unless - ``preserve_boundary`` is True). This will eliminate Strands that are loops as well. - - If ``preserve_components`` is True, then do not fuse strands that have the - ``component_idx`` attribute.""" - for s in reversed(self.crossings): - if isinstance(s, Strand): - # check that the strand is not only incident to the boundary - if preserve_boundary and all(a[0] == self for a in s.adjacent): - continue - if preserve_components and s.component_idx is not None: - continue - s.fuse() - self.crossings.remove(s) + return self.isosig(root = root, over_or_under=over_or_under) == other.isosig(root = root, over_or_under = over_or_under) def __repr__(self): + return "" % (self.label, len(self.components), len(self.crossings), self.boundary[0], self.boundary[1]) + + def __str__(self): return "" % self.label def describe(self, fuse_strands=True): @@ -355,46 +1119,12 @@ def describe(self, fuse_strands=True): >>> BraidTangle([1]).describe() 'Tangle[{1,2}, {3,4}, X[2,4,3,1]]' """ - T = self.copy() - if fuse_strands: - T._fuse_strands(preserve_boundary=True, preserve_components=True) - T.label = 0 - # give each crossing/strand a unique identifier, which - # is used for calculating ids for arcs - for i, c in enumerate(T.crossings): - c.label = i + 1 - arc_ids = {} - - def arc_key(c, i): - """For the given entity c and index into c.adjacent, - create a name for the incident arc. This gives something - that's suitable for use as a dictionary key.""" - d, j = c.adjacent[i] - return tuple(sorted([(c.label, i), (d.label, j)])) - - def arc_id(c, i): - """Get the unique integer id associated to the arc, generating - a fresh one if needed.""" - return arc_ids.setdefault(arc_key(c, i), len(arc_ids) + 1) - m, n = T.boundary - lower = "{" + ",".join(str(arc_id(T, i)) for i in range(m)) + "}" - upper = "{" + ",".join(str(arc_id(T, i)) for i in range(m, m + n)) + "}" - parts = [] - for c in T.crossings: - arcs = [arc_id(c, i) for i in range(len(c.adjacent))] - if isinstance(c, Crossing): - parts.append("X[%s,%s,%s,%s]" % tuple(arcs)) - elif isinstance(c, Strand): - if c.component_idx is not None: - parts.append(f"P[{arcs[0]},{arcs[1]}, component->{c.component_idx}]") - else: - parts.append(f"P[{arcs[0]},{arcs[1]}]") - else: - raise TypeError("Unexpected entity") - return f"Tangle[{lower}, {upper}{''.join(', ' + p for p in parts)}]" + + return self._to_old_tangle().describe(fuse_strands=fuse_strands) Tangle.bridge_closure = Tangle.numerator_closure + Tangle.braid_closure = Tangle.denominator_closure @@ -405,9 +1135,12 @@ def ComponentTangle(component_idx): this tangle should be the last component when it is turned into a Link. + >>> ComponentTangle(2) + + >>> T=(RationalTangle(2,3)+IdentityBraid(1))|(RationalTangle(2,5)+ComponentTangle(-1)) >>> T.describe() - 'Tangle[{1,2}, {3,4}, X[5,6,7,3], X[8,7,6,9], X[1,8,9,5], X[10,11,12,4], X[13,14,11,10], X[15,16,14,17], X[2,15,17,13], P[16,12, component->-1]]' + 'Tangle[{1,2}, {3,4}, X[5,3,6,7], X[8,5,7,9], X[1,8,9,6], X[10,4,11,12], X[13,14,12,11], X[14,15,16,10], X[17,16,15,13], P[2,17, component->-1]]' >>> M=T.braid_closure().exterior() # doctest: +SNAPPY >>> M.dehn_fill([(1,0),(0,0)]) # doctest: +SNAPPY @@ -416,7 +1149,7 @@ def ComponentTangle(component_idx): >>> T=(RationalTangle(2,3)+IdentityBraid(1))|(RationalTangle(2,5)+ComponentTangle(0)) >>> T.describe() - 'Tangle[{1,2}, {3,4}, X[5,6,7,3], X[8,7,6,9], X[1,8,9,5], X[10,11,12,4], X[13,14,11,10], X[15,16,14,17], X[2,15,17,13], P[16,12, component->0]]' + 'Tangle[{1,2}, {3,4}, X[5,3,6,7], X[8,5,7,9], X[1,8,9,6], X[10,4,11,12], X[13,14,12,11], X[14,15,16,10], X[17,16,15,13], P[2,17, component->0]]' >>> M=T.braid_closure().exterior() # doctest: +SNAPPY >>> M.dehn_fill([(0,0),(1,0)]) # doctest: +SNAPPY @@ -428,10 +1161,9 @@ def ComponentTangle(component_idx): Traceback (most recent call last): ... ValueError: Two Strand objects in different components have the same component_idx values - """ s = Strand(component_idx=component_idx) - return Tangle((1, 1), [s], [(s, 0), (s, 1)]) + return Tangle((1, 1), [s], [(s, 0), (s, 1)], label = f'ComponentTangle({component_idx})') def CapTangle(): @@ -488,11 +1220,11 @@ def IntegerTangle(n): T = OneTangle() for i in range(n - 1): T += OneTangle() - T.label = f"IntegerTangle({n})" + T.update_label(f"IntegerTangle({n})") return T elif n < 0: T = -IntegerTangle(-n) - T.label = f"IntegerTangle({n})" + T.update_label(f"IntegerTangle({n})") return T else: raise ValueError("Expecting int") @@ -527,6 +1259,9 @@ class RationalTangle(Tangle): attributes: ``fraction`` gives (a, b) and ``partial_quotients`` gives the continued fraction expansion of ``abs(a)/b``. + >>> RationalTangle(-2,3) + + >>> RationalTangle(2,5).braid_closure().exterior().identify() # doctest: +SNAPPY [m004(0,0), 4_1(0,0), K2_1(0,0), K4a1(0,0), otet02_00001(0,0)] """ @@ -542,8 +1277,16 @@ def __init__(self, a, b=1): T = IntegerTangle(p) + T.invert() if a < 0: T = -T - Tangle.__init__(self, 2, T.crossings, T.adjacent, - f"RationalTangle({a}, {b})") + + crossings = T.crossings + T.boundary_strands + + for c in crossings: + c._clear() + + Tangle.__init__(self, 2, + crossings, + T.adjacent, + label = f"RationalTangle({a}, {b})") # --------------------------------------------------- # @@ -562,6 +1305,8 @@ def IdentityBraid(n): 'Tangle[{1}, {2}, P[1,2]]' >>> IdentityBraid(2).describe() 'Tangle[{1,2}, {3,4}, P[1,3], P[2,4]]' + >>> IdentityBraid(5) + >>> IdentityBraid(-1) Traceback (most recent call last): ... @@ -569,10 +1314,9 @@ def IdentityBraid(n): """ if n < 0: raise ValueError("Expecting non-negative int") - strands = [Strand() for i in range(n)] - entry_points = [(s, 0) for s in strands] + [(s, 1) for s in strands] - return Tangle(n, strands, entry_points, - f"IdentityBraid({n})") + entry_points = 2* [i for i in range(n)] + return Tangle(n, [], entry_points, + label = f"IdentityBraid({n})") def BraidTangle(gens, n=None): @@ -587,19 +1331,21 @@ def BraidTangle(gens, n=None): number of strands that works for the given list of generators >>> BraidTangle([], 1) - + >>> BraidTangle([1]).describe() 'Tangle[{1,2}, {3,4}, X[2,4,3,1]]' >>> BraidTangle([-1]).describe() 'Tangle[{1,2}, {3,4}, X[1,2,4,3]]' >>> BraidTangle([1],3).describe() - 'Tangle[{1,2,3}, {4,5,6}, P[3,6], X[2,5,4,1]]' + 'Tangle[{1,2,3}, {4,5,6}, X[2,5,4,1], P[3,6]]' >>> BraidTangle([2],3).describe() - 'Tangle[{1,2,3}, {4,5,6}, P[1,4], X[3,6,5,2]]' + 'Tangle[{1,2,3}, {4,5,6}, X[3,6,5,2], P[1,4]]' >>> BraidTangle([1,2]).describe() 'Tangle[{1,2,3}, {4,5,6}, X[7,5,4,1], X[3,6,7,2]]' >>> BraidTangle([1,2,1]).describe() 'Tangle[{1,2,3}, {4,5,6}, X[7,5,4,8], X[3,6,7,9], X[2,9,8,1]]' + >>> BraidTangle([1,2,1]) + """ if n is None: n = max(-min(gens), max(gens)) + 1 @@ -616,4 +1362,8 @@ def gen(i): if abs(i) >= n: raise ValueError("Generators must have magnitude less than n") b = b * gen(i) + + b.make_upward() + b.update_label(f'BraidTangle({gens}, {n})') + return b diff --git a/spherogram_src/version.py b/spherogram_src/version.py index b5525e1..dc4633d 100644 --- a/spherogram_src/version.py +++ b/spherogram_src/version.py @@ -1 +1 @@ -version = '2.4.2a1' +version = '2.4.2b'