diff --git a/src/doc/en/reference/knots/index.rst b/src/doc/en/reference/knots/index.rst index ed42964e5a5..999a75a97dc 100644 --- a/src/doc/en/reference/knots/index.rst +++ b/src/doc/en/reference/knots/index.rst @@ -7,5 +7,6 @@ Knot Theory sage/knots/knot sage/knots/link sage/knots/knotinfo + sage/knots/free_knotinfo_monoid .. include:: ../footer.txt diff --git a/src/sage/databases/knotinfo_db.py b/src/sage/databases/knotinfo_db.py index 8ffc5d5f32a..aae6f46d980 100644 --- a/src/sage/databases/knotinfo_db.py +++ b/src/sage/databases/knotinfo_db.py @@ -793,7 +793,7 @@ def _test_database(self, **options): sage: from sage.databases.knotinfo_db import KnotInfoDataBase sage: ki_db = KnotInfoDataBase() - sage: TestSuite(ki_db).run() # long time indirect doctest + sage: TestSuite(ki_db).run() # optional - database_knotinfo, long time, indirect doctest """ from sage.knots.knotinfo import KnotInfo from sage.misc.misc import some_tuples diff --git a/src/sage/knots/all.py b/src/sage/knots/all.py index d25acbda1a3..5e456b752b3 100644 --- a/src/sage/knots/all.py +++ b/src/sage/knots/all.py @@ -1,5 +1,4 @@ from sage.misc.lazy_import import lazy_import -from sage.features.databases import DatabaseKnotInfo lazy_import('sage.knots.knot', ['Knot', 'Knots']) lazy_import('sage.knots.link', 'Link') diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py new file mode 100644 index 00000000000..c8301a4d138 --- /dev/null +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -0,0 +1,501 @@ +# sage_setup: distribution = sagemath-graphs +r""" +Free monoid genereated by prime knots available via the :class:`~sage.knots.knotinfo.KnotInfoBase` class. + +A generator of this free abelian monoid is a prime knot according to the list at +`KnotInfo `__. A fully amphicheiral prime +knot is represented by exactly one generator with the corresponding name. For +non-chiral prime knots, there are additionally one or three generators with the +suffixes ``m``, ``r`` and ``c`` which specify the mirror and reverse images +according to their symmetry type. + +AUTHORS: + +- Sebastian Oehms June 2024: initial version +""" + +############################################################################## +# Copyright (C) 2024 Sebastian Oehms +# +# 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. +# http://www.gnu.org/licenses/ +############################################################################## + +from sage.knots.knotinfo import SymmetryMutant +from sage.monoids.indexed_free_monoid import IndexedFreeAbelianMonoid, IndexedFreeAbelianMonoidElement +from sage.misc.cachefunc import cached_method +from sage.structure.unique_representation import UniqueRepresentation + + +class FreeKnotInfoMonoidElement(IndexedFreeAbelianMonoidElement): + """ + An element of an indexed free abelian monoid. + """ + def as_knot(self): + r""" + Return the knot represented by ``self``. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: FKIM.inject_variables(select=3) + Defining K3_1 + Defining K3_1m + sage: K = K3_1^2 * K3_1m + sage: K.as_knot() + Knot represented by 9 crossings + """ + wl = self.to_word_list() + P = self.parent() + if len(wl) == 1: + name = wl[0] + L = P._index_dict[name][0].link() + if name.endswith(SymmetryMutant.mirror_image.value): + return L.mirror_image() + if name.endswith(SymmetryMutant.reverse.value): + return L.reverse() + if name.endswith(SymmetryMutant.concordance_inverse.value): + return L.mirror_image().reverse() + return L + else: + from sage.misc.misc_c import prod + return prod(P.gen(wl[i]).as_knot() for i in range(len(wl))) + + def to_knotinfo(self): + r""" + Return a word representing ``self`` as a list of pairs where each pair + ``(ki, sym)`` consists of a :class:`~sage.knots.knotinfo.KontInfoBase` instance ``ki`` and + :class:`~sage.knots.knotinfo.SymmetryMutant` instance ``sym``. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: FKIM.inject_variables(select=3) + Defining K3_1 + Defining K3_1m + sage: K = K3_1^2 * K3_1m + sage: K.to_knotinfo() + [(, ), + (, ), + (, )] + """ + wl = self.to_word_list() + P = self.parent() + return [P._index_dict[w] for w in wl] + + +class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid): + + Element = FreeKnotInfoMonoidElement + + @staticmethod + def __classcall_private__(cls, max_crossing_number=6, prefix=None, **kwds): + r""" + Normalize input to ensure a unique representation. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FreeKnotInfoMonoid() + Free abelian monoid of knots with at most 6 crossings + sage: FreeKnotInfoMonoid(5) + Free abelian monoid of knots with at most 5 crossings + """ + if not prefix: + prefix = 'KnotInfo' + # We skip the IndexedMonoid__classcall__ + return UniqueRepresentation.__classcall__(cls, max_crossing_number, prefix=prefix, **kwds) + + def __init__(self, max_crossing_number, category=None, prefix=None, **kwds): + r""" + Initialize ``self`` with generators belonging to prime knots with + at most ``max_crossing_number`` crossings. + + TESTS: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: FKIM4 = FreeKnotInfoMonoid(4) + sage: TestSuite(FKIM).run() + sage: TestSuite(FKIM4).run() + """ + self._max_crossing_number = None + self._set_index_dictionary(max_crossing_number=max_crossing_number) + from sage.sets.finite_enumerated_set import FiniteEnumeratedSet + indices = FiniteEnumeratedSet(self._index_dict) + super().__init__(indices, prefix) + + def _from_knotinfo(self, knotinfo, symmetry_mutant): + r""" + Return the name on the generator for the given ``symmetry_mutant`` + of the given entry ``knotinfo`` if the KnotInfo database. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: from sage.knots.knotinfo import SymmetryMutant + sage: FKIM = FreeKnotInfoMonoid() + sage: ki = KnotInfo.K5_2 + sage: FKIM._from_knotinfo(ki, SymmetryMutant.itself) + 'K5_2' + sage: FKIM._from_knotinfo(ki, SymmetryMutant.concordance_inverse) + 'K5_2c' + """ + if symmetry_mutant == SymmetryMutant.itself: + return knotinfo.name + else: + return '%s%s' % (knotinfo.name, symmetry_mutant.value) + + def _set_index_dictionary(self, max_crossing_number=6): + r""" + Set or expand the set of generators. + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FreeKnotInfoMonoid() + Free abelian monoid of knots with at most 6 crossings + + TESTS:: + + sage: from sage.features.databases import DatabaseKnotInfo + sage: F = DatabaseKnotInfo() + sage: F.hide() + sage: FreeKnotInfoMonoid(7) # indirect doctest + Traceback (most recent call last): + ... + sage.features.FeatureNotPresentError: database_knotinfo is not available. + Feature `database_knotinfo` is hidden. + Use method `unhide` to make it available again. + sage: F.unhide() + """ + if max_crossing_number > 6: + from sage.features.databases import DatabaseKnotInfo + DatabaseKnotInfo().require() + + current_max_crossing_number = self._max_crossing_number + if not current_max_crossing_number: + current_max_crossing_number = - 1 + self._index_dict = {} + self._max_crossing_number = max_crossing_number + + def add_index(ki, sym): + self._index_dict[self._from_knotinfo(ki, sym)] = (ki, sym) + + from sage.knots.knotinfo import KnotInfo + for K in KnotInfo: + ncr = K.crossing_number() + if ncr <= current_max_crossing_number: + continue + if ncr > self._max_crossing_number: + break + for sym in SymmetryMutant: + if sym.is_minimal(K): + add_index(K, sym) + if current_max_crossing_number > 0: + from sage.sets.finite_enumerated_set import FiniteEnumeratedSet + self._indices = FiniteEnumeratedSet(self._index_dict) + + def _repr_(self): + """ + Return a string representation of ``self``. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FreeKnotInfoMonoid(4) + Free abelian monoid of knots with at most 4 crossings + """ + return "Free abelian monoid of knots with at most %s crossings" % self._max_crossing_number + + def _element_constructor_(self, x=None): + """ + Create an element of this abelian monoid from ``x``. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: K = KnotInfo.K5_1.link().mirror_image() + sage: FKIM(K) + KnotInfo['K5_1m'] + """ + if isinstance(x, tuple): + if len(x) == 2: + ki, sym = x + from sage.knots.knotinfo import KnotInfoBase + if isinstance(ki, KnotInfoBase) and isinstance(sym, SymmetryMutant): + mcr = ki.crossing_number() + if mcr > self._max_crossing_number: + self._set_index_dictionary(max_crossing_number=mcr) + + sym_min = min([sym] + sym.matches(ki)) + return self.gen(self._from_knotinfo(ki, sym_min)) + + from sage.knots.knot import Knot + from sage.knots.link import Link + if not isinstance(x, Knot): + if isinstance(x, Link): + x = Knot(x.pd_code()) + if isinstance(x, Knot): + return self.from_knot(x) + return self.element_class(self, x) + + @cached_method + def _check_elements(self, knot, elems): + r""" + Return a matching item from the list in ``elems`` if it exists. + Elsewise return ``None``. This is a helper method for .meth:`from_knot`. + + INPUT: + + - ``knot`` -- an instance of :class:`~sage.knots.knot.Knot` + - ``elems`` -- a tuple of elements of ``self`` + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: FKIM.inject_variables(select=3) + Defining K3_1 + Defining K3_1m + sage: elems = (K3_1, K3_1m) + sage: K = Knots().from_table(3, 1) + sage: FKIM._check_elements(K, elems) + KnotInfo['K3_1m'] + sage: K = Knots().from_table(4, 1) + sage: FKIM._check_elements(K, elems) is None + True + """ + for e in elems: + k = e.as_knot() + if knot.pd_code() == k.pd_code(): + return e + if knot._markov_move_cmp(k.braid()): + return e + return None + + @cached_method + def _search_composition(self, max_cr, knot, hpoly): + r""" + Add KnotInfo items to the list of candidates that have + matching Homfly polynomial. + + INPUT: + + - ``max_cr`` -- max number of crorssing to stop searching + - ``knot`` -- instance of :class:`~sage.knots.knot.Knot` + - ``hpoly`` -- Homfly polynomial to search for a component + + OUTPUT: + + A tuple of elements of ``self`` that match a (not necessarily prime or + proper) component of the given knot having the given Homfly polynomial. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: FKIM.inject_variables(select=3) + Defining K3_1 + Defining K3_1m + sage: KI = K3_1 * K3_1m + sage: K = KI.as_knot() + sage: h = K3_1.to_knotinfo()[0][0].homfly_polynomial() + sage: FKIM._search_composition(3, K, h) + (KnotInfo['K3_1'],) + """ + from sage.knots.knotinfo import KnotInfo + + def hp_mirr(hp): + v, z = hp.parent().gens() + return hp.subs({v: ~v, z: z}) + + former_cr = 3 + res = [] + for K in KnotInfo: + if not K.is_knot(): + break + c = K.crossing_number() + if c < 3: + continue + if c > max_cr: + break + hp = K.homfly_polynomial() + hp_sym = {s: hp for s in SymmetryMutant if s.is_minimal(K)} + hpm = hp_mirr(hp) + if hp != hpm: + hp_sym[SymmetryMutant.mirror_image] = hpm + if SymmetryMutant.concordance_inverse in hp_sym.keys(): + hp_sym[SymmetryMutant.concordance_inverse] = hpm + + for sym_mut in hp_sym.keys(): + hps = hp_sym[sym_mut] + if hps.divides(hpoly): + Kgen = self((K, sym_mut)) + h = hpoly // hps + if h.is_unit(): + res += [Kgen] + else: + res_rec = self._search_composition(max_cr - c, knot, h) + if res_rec: + res += [Kgen * k for k in res_rec] + if c > former_cr and res: + k = self._check_elements(knot, tuple(res)) + if k: + # matching item found + return tuple([k]) + former_cr = c + + return tuple(sorted(set(res))) + + @cached_method + def _from_knot(self, knot): + """ + Create a tuple of element of this abelian monoid which possibly + represent ``knot``. This method caches the performance relevant + part of :meth:`from_knot`. + + INPUT: + + - ``knot`` -- an instance of :class:`~sage.knots.knot.Knot` + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: K = KnotInfo.K5_1.link().mirror_image() + sage: FKIM._from_knot(K) + (KnotInfo['K5_1m'],) + """ + hp = knot.homfly_polynomial(normalization='vz') + return self._search_composition(13, knot, hp) + + def from_knot(self, knot, unique=True): + """ + Create an element of this abelian monoid from ``knot``. + + INPUT: + + - ``knot`` -- an instance of :class:`~sage.knots.knot.Knot` + + - ``unique`` -- boolean (default is ``True``). This only affects the case + where a unique identification is not possible. If set to ``False`` you + can obtain a matching list (see explanation of the output below) + + OUTPUT: + + An instance of the element class of ``self`` per default. If the keyword + argument ``unique`` then a list of such instances is returned. + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid() + sage: K = KnotInfo.K5_1.link().mirror_image() + sage: FKIM.from_knot(K) + KnotInfo['K5_1m'] + + sage: # optional - database_knotinfo + sage: K = Knot(KnotInfo.K9_12.braid()) + sage: FKIM.from_knot(K) # long time + Traceback (most recent call last): + ... + NotImplementedError: this (possibly non prime) knot cannot be identified uniquely by KnotInfo + use keyword argument `unique` to obtain more details + sage: FKIM.from_knot(K, unique=False) # long time + [KnotInfo['K4_1']*KnotInfo['K5_2'], KnotInfo['K9_12']] + """ + hp = knot.homfly_polynomial(normalization='vz') + num_summands = sum(e for f, e in hp.factor()) + if num_summands == 1: + return knot.get_knotinfo() + + res = self._from_knot(knot) + if res: + if len(res) == 1: + if unique: + return res[0] + else: + return [res[0]] # to be consistent with get_knotinfo + k = self._check_elements(knot, res) + if k: + if unique: + return k + else: + return[k] # to be consistent with get_knotinfo + + if res and not unique: + return sorted(list(set(res))) + if unique and len(res) > 1: + non_unique_hint = '\nuse keyword argument `unique` to obtain more details' + raise NotImplementedError('this (possibly non prime) knot cannot be identified uniquely by KnotInfo%s' % non_unique_hint) + raise NotImplementedError('this (possibly non prime) knot cannot be identified by KnotInfo') + + def inject_variables(self, select=None, verbose=True): + """ + Inject ``self`` with its name into the namespace of the + Python code from which this function is called. + + INPUT: + + - ``select`` -- instance of :class:`~sage.knots.knotinfo.KnotInfoBase`, + :class:`~sage.knots.knotinfo.KnotInfoSeries` or an integer. In all + cases the input is used to restrict the injected generators to the + according subset (number of crossings in the case of integer) + - ``verbose`` -- boolean (optional, default ``True``) to suppress + the message printed on the invocation + + EXAMPLES:: + + sage: from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + sage: FKIM = FreeKnotInfoMonoid(5) + sage: FKIM.inject_variables(select=3) + Defining K3_1 + Defining K3_1m + sage: FKIM.inject_variables(select=KnotInfo.K5_2) + Defining K5_2 + Defining K5_2m + sage: FKIM.inject_variables(select=KnotInfo.K5_2.series()) + Defining K5_1 + Defining K5_1m + sage: FKIM.inject_variables() + Defining K0_1 + Defining K4_1 + """ + from sage.knots.knotinfo import KnotInfoBase, KnotInfoSeries + from sage.rings.integer import Integer + gen_list = [] + idx_dict = self._index_dict + max_crn = self._max_crossing_number + gens = self.gens() + if select: + if isinstance(select, KnotInfoBase): + crn = select.crossing_number() + if crn > max_crn: + self._set_index_dictionary(max_crossing_number=crn) + gen_list += [k for k, v in idx_dict.items() if v[0] == select] + elif isinstance(select, KnotInfoSeries): + for v in select: + self.inject_variables(select=v) + return + elif type(select) is int or isinstance(select, Integer): + crn = select + if crn > max_crn: + self._set_index_dictionary(max_crossing_number=crn) + gen_list += [k for k, v in idx_dict.items() if v[0].crossing_number() == crn] + else: + raise TypeError('cannot select generators by %s' % select) + else: + gen_list = list(idx_dict.keys()) + + from sage.repl.user_globals import set_global, get_globals + for name in gen_list: + if name not in get_globals().keys(): + set_global(name, gens[name]) + if verbose: + print("Defining %s" % (name)) diff --git a/src/sage/knots/knotinfo.py b/src/sage/knots/knotinfo.py index e6b3cbafa6b..6001f7abeff 100644 --- a/src/sage/knots/knotinfo.py +++ b/src/sage/knots/knotinfo.py @@ -204,7 +204,7 @@ ....: [17,19,8,18], [9,10,11,14], [10,12,13,11], ....: [12,19,15,13], [20,16,14,15], [16,20,17,2]]) sage: L.get_knotinfo() - (, ) + KnotInfo['K0_1'] REFERENCES: @@ -281,6 +281,7 @@ def eval_knotinfo(string, locals={}, to_tuple=True): new_string = new_string.replace(';', ',') return sage_eval(new_string, locals=locals) + def knotinfo_int(string): r""" Preparse a string from the KnotInfo database representing an integer. @@ -306,6 +307,7 @@ def knotinfo_int(string): else: return int(string) + def knotinfo_bool(string): r""" Preparse a string from the KnotInfo database representing a boolean. @@ -350,9 +352,9 @@ class of an oriented pair, `K = (S_3, S_1)`, with `S_i` """ itself = 's' reverse = 'r' - concordance_inverse = 'mr' + concordance_inverse = 'c' mirror_image = 'm' - mixed = 'x' # to be used in connection with KnotInfoSeries + mixed = 'x' # to be used in connection with KnotInfoSeries unknown = '?' def __gt__(self, other): @@ -366,14 +368,103 @@ def __gt__(self, other): [, , , - , , + , ] """ # We use the reversal of the alphabetical order of the values so that # `itself` occurs before the mirrored cases return self.value < other.value + def rev(self): + r""" + Return the reverse of ``self``. + + EXAMPLES:: + + sage: from sage.knots.knotinfo import SymmetryMutant + sage: all( sym.rev().rev() == sym for sym in SymmetryMutant) + True + """ + if self is SymmetryMutant.itself: + return SymmetryMutant.reverse + elif self is SymmetryMutant.reverse: + return SymmetryMutant.itself + elif self is SymmetryMutant.mirror_image: + return SymmetryMutant.concordance_inverse + elif self is SymmetryMutant.concordance_inverse: + return SymmetryMutant.mirror_image + return self + + def mir(self): + r""" + Return the mirror image of ``self``. + + EXAMPLES:: + + sage: from sage.knots.knotinfo import SymmetryMutant + sage: all( sym.mir().mir() == sym for sym in SymmetryMutant) + True + """ + if self is SymmetryMutant.itself: + return SymmetryMutant.mirror_image + elif self is SymmetryMutant.reverse: + return SymmetryMutant.concordance_inverse + elif self is SymmetryMutant.mirror_image: + return SymmetryMutant.itself + elif self is SymmetryMutant.concordance_inverse: + return SymmetryMutant.reverse + return self + + def matches(self, link): + r""" + Return the list of other symmetry mutants that give isotopic links + with respect to ``link`` and ``self``. For ``self`` is + ``SymmetryMutant.unknown`` a boolean is returned which is ``True`` + if the chirality of ``link`` is unknown. + + EXAMPLES:: + + sage: from sage.knots.knotinfo import SymmetryMutant + sage: SymmetryMutant.itself.matches(KnotInfo.K6_1) + [] + sage: SymmetryMutant.mirror_image.matches(KnotInfo.K6_1) + [] + """ + rev = link.is_reversible() + achp = link.is_amphicheiral(positive=True) + ach = link.is_amphicheiral() + if self is SymmetryMutant.unknown: + if rev is None or ach is None or achp is None: + return True + else: + return False + res = [] + if rev: + res.append(self.rev()) + if achp: + res.append(self.mir()) + if ach: + res.append(self.rev().mir()) + return res + + def is_minimal(self, link): + r""" + Return whether ``self`` is minimal among its matching mutants. + + EXAMPLES:: + + sage: from sage.knots.knotinfo import SymmetryMutant + sage: SymmetryMutant.itself.is_minimal(KnotInfo.K6_1) + True + sage: SymmetryMutant.concordance_inverse.is_minimal(KnotInfo.K6_1) + False + """ + if self in [SymmetryMutant.unknown, SymmetryMutant.mixed]: + return False + matches = self.matches(link) + return all(self < other for other in matches) + # --------------------------------------------------------------------------------- # KnotInfoBase @@ -531,7 +622,7 @@ def _homfly_pol_ring(self, var1, var2): sage: L._homfly_pol_ring('u', 'v') Multivariate Laurent Polynomial Ring in u, v over Integer Ring """ - K3_1 = Knots().from_table(3,1) + K3_1 = Knots().from_table(3, 1) return K3_1.homfly_polynomial(var1=var1, var2=var2).parent() @cached_method @@ -896,7 +987,7 @@ def signature(self): EXAMPLES:: sage: KnotInfo.K5_2.signature() # optional - database_knotinfo - 1 + -2 """ return knotinfo_int(self[self.items.signature]) @@ -966,7 +1057,7 @@ class of an oriented pair, `K = (S_3, S_1)`, with `S_i` if not self.is_knot(): raise NotImplementedError('this is only available for knots') - symmetry_type = self[self.items.symmetry_type].strip() # for example K10_88 is a case with trailing whitespaces + symmetry_type = self[self.items.symmetry_type].strip() # for example K10_88 is a case with trailing whitespaces if not symmetry_type and self.crossing_number() == 0: return 'fully amphicheiral' return symmetry_type @@ -1316,7 +1407,7 @@ def homfly_polynomial(self, var1='v', var2='z', original=False): homfly_polynomial = homfly_polynomial.strip('}') L, M = R.gens() - lc = {'v': L, 'z':M} + lc = {'v': L, 'z': M} return eval_knotinfo(homfly_polynomial, locals=lc) @cached_method @@ -2163,8 +2254,13 @@ def is_recoverable(self, unique=True): False sage: L5a1_0.is_recoverable(unique=False) True + + TESTS: + + sage: KnotInfo.K12a_165.is_recoverable(unique=False) # optional - database_knotinfo, long time + True """ - def recover(mirror, braid): + def recover(sym_mut, braid): r""" Check if ``self`` can be recovered form its associated Sage link. @@ -2173,37 +2269,42 @@ def recover(mirror, braid): l = self.link(self.items.braid_notation) else: l = self.link() - if mirror: - if self.is_amphicheiral(): - # no need to test again - return True + if sym_mut is SymmetryMutant.mirror_image: l = l.mirror_image() + elif sym_mut is SymmetryMutant.reverse: + l = l.reverse() + elif sym_mut is SymmetryMutant.concordance_inverse: + l = l.mirror_image().reservse() - def check_result(L, m): + def check_result(res): r""" Check a single result from ``get_knotinfo``. """ + if type(res) is tuple: + L, s = res + else: + L, s = res.to_knotinfo()[0] + if not isinstance(L, KnotInfoBase): + return False if L != self: return False - if mirror: - return m is SymmetryMutant.mirror_image - else: - return m is SymmetryMutant.itself + return s == sym_mut try: - L, m = l.get_knotinfo() - if isinstance(L, KnotInfoBase): - return check_result(L,m) - elif unique: - return False + res = l.get_knotinfo(unique=unique) except NotImplementedError: - if unique: - return False - Llist = l.get_knotinfo(unique=False) - return any(check_result(L, m) for (L, m) in Llist) + return False + if unique: + return check_result(res) + else: + return any(check_result(r) for r in res) from sage.misc.misc import some_tuples - return all(recover(mirror, braid) for mirror, braid in some_tuples([True, False], 2, 4)) + if SymmetryMutant.unknown.matches(self): + sym_muts = [SymmetryMutant.unknown] + else: + sym_muts = [s for s in SymmetryMutant if s.is_minimal(self)] + return all(recover(sym, braid) for sym, braid in some_tuples(sym_muts, 2, 8)) def inject(self, verbose=True): """ @@ -2522,7 +2623,7 @@ def lower_list(self, oriented=False, comp=None, det=None, homfly=None): l = [] cr = self._crossing_number if cr > 0: - LS = type(self)(cr - 1, self._is_knot, self._is_alternating, self._name_unoriented ) + LS = type(self)(cr - 1, self._is_knot, self._is_alternating, self._name_unoriented) l = LS.lower_list(oriented=oriented, comp=comp, det=det, homfly=homfly) return l + self.list(oriented=oriented, comp=comp, det=det, homfly=homfly) diff --git a/src/sage/knots/link.py b/src/sage/knots/link.py index 0b0dfd6dd47..dbaca5662c8 100644 --- a/src/sage/knots/link.py +++ b/src/sage/knots/link.py @@ -3019,8 +3019,8 @@ def homfly_polynomial(self, var1=None, var2=None, normalization='lm'): Comparison with KnotInfo:: sage: # needs sage.libs.homfly - sage: KI, m = K.get_knotinfo(); KI, m - (, ) + sage: KI = K.get_knotinfo(mirror_version=False); KI + sage: K.homfly_polynomial(normalization='vz') == KI.homfly_polynomial() True @@ -4056,7 +4056,7 @@ def _knotinfo_matching_list(self): def _knotinfo_matching_dict(self): r""" - Return a dictionary mapping items of the enum :class:`~sage.knots.knotinfo.SymmetryType` + Return a dictionary mapping items of the enum :class:`~sage.knots.knotinfo.SymmetryMutant` to list of links from the KnotInfo and LinkInfo databases which match the properties of the according symmetry mutant of ``self`` as much as possible. @@ -4064,7 +4064,7 @@ def _knotinfo_matching_dict(self): OUTPUT: A pair (``match_lists, proves``) of dictionaries with keys from the - enum :class:`~sage.knots.knotinfo.SymmetryType`. The first dictionary maps these keys to + enum :class:`~sage.knots.knotinfo.SymmetryMutant`. The first dictionary maps these keys to the corresponding matching list and ``proves`` maps them to booleans telling if the entries of the corresponding ``match_lists`` are checked to be isotopic to the symmetry mutant of ``self`` or not. @@ -4076,18 +4076,18 @@ def _knotinfo_matching_dict(self): sage: L4a1_0.link()._knotinfo_matching_dict() ({: [], : [], - : [], - : []}, + : [], + : []}, {: True, : True, - : False, - : False}) + : False, + : False}) """ from sage.knots.knotinfo import SymmetryMutant mutant = {} mutant[SymmetryMutant.itself] = self - mutant[SymmetryMutant.mirror_image] = self.mirror_image() mutant[SymmetryMutant.reverse] = self.reverse() + mutant[SymmetryMutant.mirror_image] = self.mirror_image() mutant[SymmetryMutant.concordance_inverse] = mutant[SymmetryMutant.mirror_image].reverse() match_lists = {k: list(mutant[k]._knotinfo_matching_list()[0]) for k in mutant.keys()} proves = {k: mutant[k]._knotinfo_matching_list()[1] for k in mutant.keys()} @@ -4110,11 +4110,15 @@ def get_knotinfo(self, mirror_version=True, unique=True): OUTPUT: - A tuple ``(K, m)`` where ``K`` is an instance of :class:`~sage.knots.knotinfo.KnotInfoBase` - and ``m`` an instance of :class:`~sage.knots.knotinfo.SymmetryMutant` - (for chiral links) specifying the symmetry mutant of ``K`` to which - ``self`` is isotopic. The value of ``m`` is ``unknown`` if it cannot - be determined uniquely and the keyword option ``unique=False`` is given. + If ``self`` is a knot, then an element of the free monoid over prime + knots constructed from the KnotInfo database is returned. More explicitly + this is an element of :class:`~sage.knots.free_knotinfo_monoid.FreeKnotInfoMonoidElement`. + Else a tuple ``(K, m)`` is returned where ``K`` is an instance of + :class:`~sage.knots.knotinfo.KnotInfoBase` and ``m`` an instance of + :class:`~sage.knots.knotinfo.SymmetryMutant` (for chiral links) specifying + the symmetry mutant of ``K`` to which ``self`` is isotopic. The value of + ``m`` is ``unknown`` if it cannot be determined uniquely and the keyword + option ``unique=False`` is given. For proper links, if the orientation mutant cannot be uniquely determined, K will be a series of links gathering all links having the same unoriented @@ -4148,35 +4152,35 @@ def get_knotinfo(self, mirror_version=True, unique=True): EXAMPLES:: sage: # optional - database_knotinfo - sage: from sage.knots.knotinfo import KnotInfo sage: L = Link([[4,1,5,2], [10,4,11,3], [5,17,6,16], [7,13,8,12], ....: [18,10,19,9], [2,12,3,11], [13,21,14,20], [15,7,16,6], ....: [22,17,1,18], [8,20,9,19], [21,15,22,14]]) sage: L.get_knotinfo() - (, ) + KnotInfo['K11n_121m'] sage: K = KnotInfo.K10_25 sage: l = K.link() sage: l.get_knotinfo() - (, ) + KnotInfo['K10_25'] sage: k11 = KnotInfo.K11n_82.link() sage: k11m = k11.mirror_image() sage: k11mr = k11m.reverse() sage: k11mr.get_knotinfo() - (, ) + KnotInfo['K11n_82m'] sage: k11r = k11.reverse() sage: k11r.get_knotinfo() - (, ) + KnotInfo['K11n_82'] sage: k11rm = k11r.mirror_image() sage: k11rm.get_knotinfo() - (, ) + KnotInfo['K11n_82m'] - Knots with more than 13 and proper links having more than 11 crossings - cannot be identified. In addition non prime links or even links whose - HOMFLY-PT polynomial is not irreducible cannot be identified:: + Knots with more than 13 and multi-component links having more than 11 + crossings cannot be identified. In addition non prime multi-component + links or even links whose HOMFLY-PT polynomial is not irreducible cannot + be identified:: sage: b, = BraidGroup(2).gens() sage: Link(b**13).get_knotinfo() # optional - database_knotinfo - (, ) + KnotInfo['K13a_4878'] sage: Link(b**14).get_knotinfo() Traceback (most recent call last): ... @@ -4196,7 +4200,7 @@ def get_knotinfo(self, mirror_version=True, unique=True): ....: [17,19,8,18], [9,10,11,14], [10,12,13,11], ....: [12,19,15,13], [20,16,14,15], [16,20,17,2]]) sage: L.get_knotinfo() - (, ) + KnotInfo['K0_1'] Usage of option ``mirror_version``:: @@ -4213,10 +4217,7 @@ def get_knotinfo(self, mirror_version=True, unique=True): NotImplementedError: this link cannot be uniquely determined use keyword argument `unique` to obtain more details sage: l.get_knotinfo(unique=False) - [(, ), - (, ), - (, ), - (, )] + [KnotInfo['K10_25'], KnotInfo['K10_56']] sage: t = (1, -2, 1, 1, -2, 1, -2, -2) sage: l8 = Link(BraidGroup(3)(t)) sage: l8.get_knotinfo() @@ -4237,11 +4238,7 @@ def get_knotinfo(self, mirror_version=True, unique=True): use keyword argument `unique` to obtain more details sage: l12.get_knotinfo(unique=False) [(, ), - (, ), - (, ), - (, - ), - (, ), + (, ), (, ), (, )] @@ -4253,10 +4250,8 @@ def get_knotinfo(self, mirror_version=True, unique=True): sage: L2a1.get_knotinfo() (Series of links L2a1, ) sage: L2a1.get_knotinfo(unique=False) - [(, ), - (, ), - (, ), - (, )] + [(, ), + (, )] sage: KnotInfo.L5a1_0.inject() Defining L5a1_0 @@ -4269,21 +4264,19 @@ def get_knotinfo(self, mirror_version=True, unique=True): [, ] sage: l5.get_knotinfo(unique=False) [(, ), - (, ), - (, ), - (, )] + (, )] Clarifying the series around the Perko pair (:wikipedia:`Perko_pair`):: sage: for i in range(160, 166): # optional - database_knotinfo ....: K = Knots().from_table(10, i) ....: print('%s_%s' %(10, i), '--->', K.get_knotinfo()) - 10_160 ---> (, ) - 10_161 ---> (, ) - 10_162 ---> (, ) - 10_163 ---> (, ) - 10_164 ---> (, ) - 10_165 ---> (, ) + 10_160 ---> KnotInfo['K10_160'] + 10_161 ---> KnotInfo['K10_161m'] + 10_162 ---> KnotInfo['K10_162'] + 10_163 ---> KnotInfo['K10_163'] + 10_164 ---> KnotInfo['K10_164'] + 10_165 ---> KnotInfo['K10_165m'] Clarifying ther Perko series against `SnapPy `__:: @@ -4301,16 +4294,16 @@ def get_knotinfo(self, mirror_version=True, unique=True): ....: K = K10(i) ....: k = K.link(K.items.name, snappy=True) ....: print(k, '--->', k.sage_link().get_knotinfo()) - ---> (, ) - ---> (, ) - ---> (, ) - ---> (, ) - ---> (, ) - ---> (, ) + ---> KnotInfo['K10_160'] + ---> KnotInfo['K10_161m'] + ---> KnotInfo['K10_161'] + ---> KnotInfo['K10_162'] + ---> KnotInfo['K10_163'] + ---> KnotInfo['K10_164'] sage: snappy.Link('10_166') sage: _.sage_link().get_knotinfo() - (, ) + KnotInfo['K10_165m'] Another pair of confusion (see the corresponding `Warning `__):: @@ -4319,11 +4312,23 @@ def get_knotinfo(self, mirror_version=True, unique=True): sage: Ks10_86 = snappy.Link('10_86') sage: Ks10_83 = snappy.Link('10_83') sage: Ks10_86.sage_link().get_knotinfo(unique=False) - [(, ), - (, )] + [KnotInfo['K10_83c'], KnotInfo['K10_83m']] sage: Ks10_83.sage_link().get_knotinfo(unique=False) - [(, ), - (, )] + [KnotInfo['K10_86'], KnotInfo['K10_86r']] + + Non prime knots can be detected, as well:: + + sage: b = BraidGroup(4)((1, 2, 2, 2, -1, 2, 2, 2, -3, -3, -3)) + sage: Kb = Knot(b) + sage: Kb.get_knotinfo() + KnotInfo['K3_1']^2*KnotInfo['K3_1m'] + + sage: K = Link([[4, 2, 5, 1], [8, 6, 9, 5], [6, 3, 7, 4], [2, 7, 3, 8], + ....: [10, 15, 11, 16], [12, 21, 13, 22], [14, 11, 15, 12], [16, 9, 17, 10], + ....: [18, 25, 19, 26], [20, 23, 21, 24], [22, 13, 23, 14], [24, 19, 25, 20], + ....: [26, 17, 1, 18]]) + sage: K.get_knotinfo() # optional - database_knotinfo, long time + KnotInfo['K4_1']*KnotInfo['K9_2m'] TESTS:: @@ -4331,18 +4336,10 @@ def get_knotinfo(self, mirror_version=True, unique=True): sage: L = KnotInfo.L10a171_1_1_0 sage: l = L.link(L.items.braid_notation) sage: l.get_knotinfo(unique=False) - [(, - ), - (, - ), - (, - ), - (, - ), - (, ), - (, ), - (, ), - (, )] + [(, ), + (, ), + (, ), + (, )] sage: KnotInfo.L10a151_0_0.link().get_knotinfo() Traceback (most recent call last): ... @@ -4360,9 +4357,6 @@ def get_knotinfo(self, mirror_version=True, unique=True): sage: L1.get_knotinfo() == L2.get_knotinfo() True """ - # ToDo: extension to non prime links in which case an element of the monoid - # over :class:`KnotInfo` should be returned - non_unique_hint = '\nuse keyword argument `unique` to obtain more details' from sage.knots.knotinfo import SymmetryMutant @@ -4374,37 +4368,36 @@ def answer(L): if not mirror_version: return L - chiral = True - ach = L.is_amphicheiral() - achp = L.is_amphicheiral(positive=True) - rev = L.is_reversible() - if ach is None and achp is None and rev is None: - if unique: - raise NotImplementedError('this link cannot be uniquely determined (unknown chirality)%s' % non_unique_hint) - chiral = None - elif ach and achp: - chiral = False + def find_mutant(proved=True): + r""" + Return the according symmetry mutant from the matching list + and removes the entry from the list. + """ + for k in match_lists: + if proved: + prove = proves[k] or any(proves[m] for m in k.matches(L)) + if not prove: + continue + if k.is_minimal(L): + lk = match_lists[k] + if L in lk: + lk.remove(L) + return k sym_mut = None - if chiral is None: + if SymmetryMutant.unknown.matches(L): + if unique: + raise NotImplementedError('this link cannot be uniquely determined (unknown chirality)%s' % non_unique_hint) sym_mut = SymmetryMutant.unknown - elif not chiral: - sym_mut = SymmetryMutant.itself - else: - for k in match_lists: - lk = match_lists[k] - if proves[k] and L in lk: - lk.remove(L) - sym_mut = k - break if not sym_mut: - for k in match_lists: - lk = match_lists[k] - if L in lk: - lk.remove(L) - sym_mut = k - break + sym_mut = find_mutant() + + if not sym_mut: + sym_mut = find_mutant(proved=False) + + if not unique and not sym_mut: + return None if not sym_mut: # In case of a chiral link this means that the HOMFLY-PT @@ -4415,6 +4408,11 @@ def answer(L): if unique and sym_mut is SymmetryMutant.unknown: raise NotImplementedError('symmetry mutant of this link cannot be uniquely determined%s' % non_unique_hint) + if L.is_knot(): + from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + FKIM = FreeKnotInfoMonoid() + return FKIM((L, sym_mut)) + return L, sym_mut def answer_unori(S): @@ -4444,7 +4442,12 @@ def answer_list(l): argument ``unique``. """ if not unique: - return sorted(set([answer(L) for L in l])) + ansl = [] + for L in l: + a = answer(L) + if a: + ansl.append(a) + return sorted(list(set(ansl))) if len(set(l)) == 1: return answer(l[0]) @@ -4456,6 +4459,16 @@ def answer_list(l): raise NotImplementedError('this link cannot be uniquely determined%s' % non_unique_hint) + H = self.homfly_polynomial(normalization='vz') + num_fac = sum(exp for f, exp in H.factor()) + if num_fac > 1 and self.is_knot(): + # we cannot be sure if this is a prime knot (see the example for the connected + # sum of K4_1 and K5_2 in the doctest of :meth:`_knotinfo_matching_list`) + # Therefor we calculate it directly in the free KnotInfo monoid + from sage.knots.free_knotinfo_monoid import FreeKnotInfoMonoid + FKIM = FreeKnotInfoMonoid() + return FKIM.from_knot(self, unique=unique) + match_lists, proves = self._knotinfo_matching_dict() # first add only proved matching lists @@ -4493,11 +4506,7 @@ def answer_list(l): # we cannot not be sure if this link is recorded in the KnotInfo database raise NotImplementedError('this link having more than 11 crossings cannot be%s determined%s' % uniq_txt) - H = self.homfly_polynomial(normalization='vz') - - if sum(exp for f, exp in H.factor()) > 1: - # we cannot be sure if this is a prime link (see the example for the connected - # sum of K4_1 and K5_2 in the doctest of :meth:`_knotinfo_matching_list`) + if num_fac > 1: raise NotImplementedError('this (possibly non prime) link cannot be%s determined%s' % uniq_txt) if not l: