From 4da232ddc0555349b7225efe25f44159e2a07608 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 21 Jun 2024 08:49:12 +0200 Subject: [PATCH 01/13] get_knotinfo_non_prime initial --- src/sage/knots/free_knotinfo_monoid.py | 451 +++++++++++++++++++++++++ src/sage/knots/knotinfo.py | 121 ++++++- src/sage/knots/link.py | 222 ++++++------ 3 files changed, 674 insertions(+), 120 deletions(-) create mode 100644 src/sage/knots/free_knotinfo_monoid.py diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py new file mode 100644 index 00000000000..1a4549bf6f8 --- /dev/null +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -0,0 +1,451 @@ +# 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 + + +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:`KontInfoBase` instance ``ki`` and + :class:`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 + """ + return super().__classcall__(cls, max_crossing_number, prefix=prefix, **kwds) + + def __init__(self, max_crossing_number, category=None, prefix=None, **kwds): + r""" + Init this monoid 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.set import Set + indices = Set(self._index_dict.keys()) + if not prefix or prefix == 'F': + prefix = 'KnotInfo' + 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: F = DatabaseKnotInfo() + sage: F.hide() + sage: FreeKnotInfoMonoid(7) + 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.set import Set + self._indices = Set(self._index_dict.keys()) + + 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`. + """ + for e in elems: + k = e.as_knot() + if knot._markov_move_cmp(k.braid()): + return e + return None + + @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 :method:`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'],) + """ + from sage.knots.knotinfo import KnotInfo + + def search_composition(max_cr, 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 + - ``hpoly`` -- Homfly polynomial + """ + 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 = search_composition(max_cr - c, h) + if res_rec: + res += [Kgen * k for k in res_rec] + res_t = tuple(res) + if c > former_cr and res_t: + k = self._check_elements(knot, res_t) + if k: + # matching item found + return tuple([k]) + former_cr = c + + return tuple(sorted(set(res_t))) + + hp = knot.homfly_polynomial(normalization='vz') + return search_composition(13, 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: K = Knot(KnotInfo.K9_12.braid()) # optional - database_knotinfo + sage: FKIM.from_knot(K) # optional - database_knotinfo, 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) # optional - database_knotinfo, 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: + return res[0] + k = self._check_elements(knot, res) + if k: + return k + + if res and not unique: + return 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) + else: + 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 611d502a3d0..1f224745da1 100644 --- a/src/sage/knots/knotinfo.py +++ b/src/sage/knots/knotinfo.py @@ -203,7 +203,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: @@ -280,6 +280,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. @@ -305,6 +306,7 @@ def knotinfo_int(string): else: return int(string) + def knotinfo_bool(string): r""" Preparse a string from the KnotInfo database representing a boolean. @@ -349,9 +351,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): @@ -365,14 +367,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 @@ -530,7 +621,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 @@ -871,7 +962,7 @@ def three_genus(self): EXAMPLES:: - sage: KnotInfo.K5_2.three_genus() # optional - databsase_knotinfo + sage: KnotInfo.K5_2.three_genus() # optional - database_knotinfo 1 Note that this differs from the corresponding result in Sage @@ -899,8 +990,8 @@ def signature(self): EXAMPLES:: - sage: KnotInfo.K5_2.signature() # optional - databsase_knotinfo - 1 + sage: KnotInfo.K5_2.signature() # optional - database_knotinfo + -2 """ return knotinfo_int(self[self.items.signature]) @@ -970,7 +1061,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 @@ -1322,7 +1413,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 @@ -2195,9 +2286,13 @@ def check_result(L, m): return m is SymmetryMutant.itself try: - L, m = l.get_knotinfo() + ki = l.get_knotinfo() + if type(ki) == tuple: + L, m = ki + else: + L, m = l.get_knotinfo().to_knotinfo()[0] if isinstance(L, KnotInfoBase): - return check_result(L,m) + return check_result(L, m) elif unique: return False except NotImplementedError: @@ -2526,7 +2621,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 7470d8c2837..9f9e972b412 100644 --- a/src/sage/knots/link.py +++ b/src/sage/knots/link.py @@ -2987,8 +2987,8 @@ def homfly_polynomial(self, var1=None, var2=None, normalization='lm'): Comparison with KnotInfo:: - 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 @@ -4025,7 +4025,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. @@ -4033,7 +4033,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. @@ -4045,18 +4045,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()} @@ -4079,11 +4079,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 @@ -4092,8 +4096,7 @@ def get_knotinfo(self, mirror_version=True, unique=True): If ``mirror_version`` is set to ``False`` then the result is just ``K`` (that is: ``m`` is suppressed). - If it is not possible to determine a unique result - a :class:`NotImplementedError` + If it is not possible to determine a unique result a :class:`NotImplementedError` will be raised. To avoid this you can set ``unique`` to ``False``. You will get a list of matching candidates instead. @@ -4117,35 +4120,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): ... @@ -4164,7 +4167,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``:: @@ -4181,10 +4184,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() @@ -4204,11 +4204,7 @@ def get_knotinfo(self, mirror_version=True, unique=True): use keyword argument `unique` to obtain more details sage: l12.get_knotinfo(unique=False) [(, ), - (, ), - (, ), - (, - ), - (, ), + (, ), (, ), (, )] @@ -4220,10 +4216,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 @@ -4236,21 +4230,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 `__:: @@ -4268,16 +4260,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 `__):: @@ -4286,11 +4278,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() # - database_knotinfo, long time + KnotInfo['K4_1']*KnotInfo['K9_2m'] TESTS:: @@ -4298,18 +4302,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): ... @@ -4327,9 +4323,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 @@ -4341,37 +4334,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 @@ -4382,6 +4374,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): @@ -4411,7 +4408,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(set(ansl)) if len(set(l)) == 1: return answer(l[0]) @@ -4423,6 +4425,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 @@ -4460,11 +4472,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: From 173f9e58cf358c5d7a2279986bc1a26c89f4287e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 22 Jun 2024 15:44:07 +0200 Subject: [PATCH 02/13] 38254: linter and doctest fixes --- src/doc/en/reference/knots/index.rst | 1 + src/sage/knots/free_knotinfo_monoid.py | 9 +++++---- src/sage/knots/knotinfo.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) 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/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index 1a4549bf6f8..1d4ee79952f 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -259,7 +259,7 @@ 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 :method:`from_knot`. + part of :meth:`from_knot`. INPUT: @@ -356,13 +356,14 @@ def from_knot(self, knot, unique=True): sage: FKIM.from_knot(K) KnotInfo['K5_1m'] - sage: K = Knot(KnotInfo.K9_12.braid()) # optional - database_knotinfo - sage: FKIM.from_knot(K) # optional - database_knotinfo, long time + 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) # optional - database_knotinfo, long time + sage: FKIM.from_knot(K, unique=False) # long time (KnotInfo['K4_1']*KnotInfo['K5_2'], KnotInfo['K9_12']) """ hp = knot.homfly_polynomial(normalization='vz') diff --git a/src/sage/knots/knotinfo.py b/src/sage/knots/knotinfo.py index 1f224745da1..e1081e688eb 100644 --- a/src/sage/knots/knotinfo.py +++ b/src/sage/knots/knotinfo.py @@ -2287,7 +2287,7 @@ def check_result(L, m): try: ki = l.get_knotinfo() - if type(ki) == tuple: + if type(ki) is tuple: L, m = ki else: L, m = l.get_knotinfo().to_knotinfo()[0] From 803c1e4fbc564162f4806e0f371a326845e7ebb3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 24 Jun 2024 18:55:30 +0200 Subject: [PATCH 03/13] 38254: fix links in documentation --- src/sage/knots/free_knotinfo_monoid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index 1d4ee79952f..c4136221ba3 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -67,8 +67,8 @@ def as_knot(self): def to_knotinfo(self): r""" Return a word representing ``self`` as a list of pairs where each pair - ``(ki, sym)`` consists of a :class:`KontInfoBase` instance ``ki`` and - :class:`SymmetryMutant` instance ``sym``. + ``(ki, sym)`` consists of a :class:`~sage.knots.knotinfo.KontInfoBase` instance ``ki`` and + :class:`~sage.knots.knotinfo.SymmetryMutant` instance ``sym``. EXAMPLES:: From 2e1e10d00c4aa038e351d3a3f84d18fa89666d71 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 25 Jun 2024 23:57:08 +0200 Subject: [PATCH 04/13] 38254: fix wrong doctest tag --- src/sage/knots/link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/knots/link.py b/src/sage/knots/link.py index 9f9e972b412..79f272fc6b5 100644 --- a/src/sage/knots/link.py +++ b/src/sage/knots/link.py @@ -4293,7 +4293,7 @@ def get_knotinfo(self, mirror_version=True, unique=True): ....: [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() # - database_knotinfo, long time + sage: K.get_knotinfo() # optional - database_knotinfo, long time KnotInfo['K4_1']*KnotInfo['K9_2m'] TESTS:: From 29849eb44c3bce90f780eaa98b5333ef2f06a2dc Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 26 Jun 2024 08:10:50 +0200 Subject: [PATCH 05/13] 38254: one more (unrelated) wrong doctest tag --- src/sage/databases/knotinfo_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/databases/knotinfo_db.py b/src/sage/databases/knotinfo_db.py index dc21575d779..55fa4f85560 100644 --- a/src/sage/databases/knotinfo_db.py +++ b/src/sage/databases/knotinfo_db.py @@ -800,7 +800,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 From a936a13c994d7876168c27feb26407eb82614d16 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 27 Jun 2024 08:18:49 +0200 Subject: [PATCH 06/13] 38254: have is_recoverable check all symmetry mutants --- src/sage/knots/free_knotinfo_monoid.py | 7 ++-- src/sage/knots/knotinfo.py | 49 +++++++++++++------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index c4136221ba3..a98193a68cf 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -318,15 +318,14 @@ def hp_mirr(hp): res_rec = search_composition(max_cr - c, h) if res_rec: res += [Kgen * k for k in res_rec] - res_t = tuple(res) - if c > former_cr and res_t: - k = self._check_elements(knot, res_t) + 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_t))) + return tuple(sorted(set(res))) hp = knot.homfly_polynomial(normalization='vz') return search_composition(13, hp) diff --git a/src/sage/knots/knotinfo.py b/src/sage/knots/knotinfo.py index e1081e688eb..e2ffa6b9d11 100644 --- a/src/sage/knots/knotinfo.py +++ b/src/sage/knots/knotinfo.py @@ -2259,7 +2259,7 @@ def is_recoverable(self, unique=True): sage: L5a1_0.is_recoverable(unique=False) True """ - def recover(mirror, braid): + def recover(sym_mut, braid): r""" Check if ``self`` can be recovered form its associated Sage link. @@ -2268,41 +2268,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: - ki = l.get_knotinfo() - if type(ki) is tuple: - L, m = ki - else: - L, m = l.get_knotinfo().to_knotinfo()[0] - 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): """ From 0334dbc9e71394e91b70786eae6bc0757f51333f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 28 Jun 2024 19:41:30 +0200 Subject: [PATCH 07/13] 38254: fix inconsist output --- src/sage/knots/free_knotinfo_monoid.py | 14 ++++++++++---- src/sage/knots/knotinfo.py | 5 +++++ src/sage/knots/link.py | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index a98193a68cf..bf88a791ada 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -363,7 +363,7 @@ def from_knot(self, knot, unique=True): 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']) + [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()) @@ -373,13 +373,19 @@ def from_knot(self, knot, unique=True): res = self._from_knot(knot) if res: if len(res) == 1: - return res[0] + if unique: + return res[0] + else: + return [res[0]] # to be consistent with get_knotinfo k = self._check_elements(knot, res) if k: - return k + if unique: + return k + else: + return[k] # to be consistent with get_knotinfo if res and not unique: - return res + 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) diff --git a/src/sage/knots/knotinfo.py b/src/sage/knots/knotinfo.py index e2ffa6b9d11..95461484392 100644 --- a/src/sage/knots/knotinfo.py +++ b/src/sage/knots/knotinfo.py @@ -2258,6 +2258,11 @@ 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(sym_mut, braid): r""" diff --git a/src/sage/knots/link.py b/src/sage/knots/link.py index 79f272fc6b5..5589ad35684 100644 --- a/src/sage/knots/link.py +++ b/src/sage/knots/link.py @@ -4413,7 +4413,7 @@ def answer_list(l): a = answer(L) if a: ansl.append(a) - return sorted(set(ansl)) + return sorted(list(set(ansl))) if len(set(l)) == 1: return answer(l[0]) From 530249ab998b6992c5bdfee7dd28c282f08b1e79 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 3 Jul 2024 23:06:20 +0200 Subject: [PATCH 08/13] 38254: Have _check_elements use pd_code, too --- src/sage/knots/free_knotinfo_monoid.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index bf88a791ada..259522664bc 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -162,7 +162,7 @@ def _set_index_dictionary(self, max_crossing_number=6): sage: F = DatabaseKnotInfo() sage: F.hide() - sage: FreeKnotInfoMonoid(7) + sage: FreeKnotInfoMonoid(7) # indirect doctest Traceback (most recent call last): ... sage.features.FeatureNotPresentError: database_knotinfo is not available. @@ -247,9 +247,31 @@ 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 From a2cfdeb8446713e10ad8b30d74e647563a8517ea Mon Sep 17 00:00:00 2001 From: Sebastian Oehms <47305845+soehms@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:17:45 +0200 Subject: [PATCH 09/13] Update src/sage/knots/free_knotinfo_monoid.py Remove unnecessary `else` Co-authored-by: Travis Scrimshaw --- src/sage/knots/free_knotinfo_monoid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index 259522664bc..5b86ee488ea 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -411,8 +411,7 @@ def from_knot(self, knot, unique=True): 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) - else: - raise NotImplementedError('this (possibly non prime) knot cannot be identified by KnotInfo') + raise NotImplementedError('this (possibly non prime) knot cannot be identified by KnotInfo') def inject_variables(self, select=None, verbose=True): """ From 631a112c182e1dcac318c0616537d6a10c9c722c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 25 Jul 2024 10:20:11 +0200 Subject: [PATCH 10/13] 38254: local search_composition changed to a method _search_composition --- src/sage/knots/free_knotinfo_monoid.py | 132 ++++++++++++++----------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index 5b86ee488ea..33f852211bf 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -27,6 +27,7 @@ from sage.knots.knotinfo import SymmetryMutant from sage.monoids.indexed_free_monoid import IndexedFreeAbelianMonoid, IndexedFreeAbelianMonoidElement from sage.misc.cachefunc import cached_method +from sage.misc.fast_methods import Singleton class FreeKnotInfoMonoidElement(IndexedFreeAbelianMonoidElement): @@ -88,7 +89,7 @@ def to_knotinfo(self): return [P._index_dict[w] for w in wl] -class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid): +class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid, Singleton): Element = FreeKnotInfoMonoidElement @@ -276,6 +277,79 @@ def _check_elements(self, knot, elems): 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): """ @@ -295,62 +369,8 @@ def _from_knot(self, knot): sage: FKIM._from_knot(K) (KnotInfo['K5_1m'],) """ - from sage.knots.knotinfo import KnotInfo - - def search_composition(max_cr, 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 - - ``hpoly`` -- Homfly polynomial - """ - 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 = search_composition(max_cr - c, 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))) - hp = knot.homfly_polynomial(normalization='vz') - return search_composition(13, hp) + return self._search_composition(13, knot, hp) def from_knot(self, knot, unique=True): """ From 0f03b9ed484e2da421b062954e0ee6eb7d33c480 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 25 Jul 2024 20:04:36 +0200 Subject: [PATCH 11/13] 38254: use FiniteEnumeratedSet instead of Set --- src/sage/knots/all.py | 1 - src/sage/knots/free_knotinfo_monoid.py | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) 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 index 33f852211bf..750a07e1852 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -123,8 +123,8 @@ def __init__(self, max_crossing_number, category=None, prefix=None, **kwds): """ self._max_crossing_number = None self._set_index_dictionary(max_crossing_number=max_crossing_number) - from sage.sets.set import Set - indices = Set(self._index_dict.keys()) + from sage.sets.finite_enumerated_set import FiniteEnumeratedSet + indices = FiniteEnumeratedSet(self._index_dict.keys()) if not prefix or prefix == 'F': prefix = 'KnotInfo' super().__init__(indices, prefix) @@ -161,6 +161,7 @@ def _set_index_dictionary(self, max_crossing_number=6): TESTS:: + sage: from sage.features.databases import DatabaseKnotInfo sage: F = DatabaseKnotInfo() sage: F.hide() sage: FreeKnotInfoMonoid(7) # indirect doctest @@ -195,8 +196,8 @@ def add_index(ki, sym): if sym.is_minimal(K): add_index(K, sym) if current_max_crossing_number > 0: - from sage.sets.set import Set - self._indices = Set(self._index_dict.keys()) + from sage.sets.finite_enumerated_set import FiniteEnumeratedSet + self._indices = FiniteEnumeratedSet(self._index_dict.keys()) def _repr_(self): """ @@ -308,6 +309,7 @@ def _search_composition(self, max_cr, knot, hpoly): (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}) From 0b0248a72b90a78bb1f777269e63e0087d701c23 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 28 Jul 2024 21:11:00 +0200 Subject: [PATCH 12/13] 38254: remove dict.keys() --- src/sage/knots/free_knotinfo_monoid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index 750a07e1852..acf286fba0b 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -124,7 +124,7 @@ def __init__(self, max_crossing_number, category=None, prefix=None, **kwds): 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.keys()) + indices = FiniteEnumeratedSet(self._index_dict) if not prefix or prefix == 'F': prefix = 'KnotInfo' super().__init__(indices, prefix) @@ -197,7 +197,7 @@ def add_index(ki, sym): add_index(K, sym) if current_max_crossing_number > 0: from sage.sets.finite_enumerated_set import FiniteEnumeratedSet - self._indices = FiniteEnumeratedSet(self._index_dict.keys()) + self._indices = FiniteEnumeratedSet(self._index_dict) def _repr_(self): """ From cfe19f91386df22090b3e43de98597f225475625 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 25 Sep 2024 20:18:31 +0200 Subject: [PATCH 13/13] 38254: corrections according to review --- src/sage/knots/free_knotinfo_monoid.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py index acf286fba0b..c8301a4d138 100644 --- a/src/sage/knots/free_knotinfo_monoid.py +++ b/src/sage/knots/free_knotinfo_monoid.py @@ -27,7 +27,7 @@ from sage.knots.knotinfo import SymmetryMutant from sage.monoids.indexed_free_monoid import IndexedFreeAbelianMonoid, IndexedFreeAbelianMonoidElement from sage.misc.cachefunc import cached_method -from sage.misc.fast_methods import Singleton +from sage.structure.unique_representation import UniqueRepresentation class FreeKnotInfoMonoidElement(IndexedFreeAbelianMonoidElement): @@ -89,7 +89,7 @@ def to_knotinfo(self): return [P._index_dict[w] for w in wl] -class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid, Singleton): +class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid): Element = FreeKnotInfoMonoidElement @@ -106,12 +106,15 @@ def __classcall_private__(cls, max_crossing_number=6, prefix=None, **kwds): sage: FreeKnotInfoMonoid(5) Free abelian monoid of knots with at most 5 crossings """ - return super().__classcall__(cls, max_crossing_number, prefix=prefix, **kwds) + 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""" - Init this monoid with generators belonging to prime knots with at most - ``max_crossing_number`` crossings. + Initialize ``self`` with generators belonging to prime knots with + at most ``max_crossing_number`` crossings. TESTS: @@ -125,8 +128,6 @@ def __init__(self, max_crossing_number, category=None, prefix=None, **kwds): self._set_index_dictionary(max_crossing_number=max_crossing_number) from sage.sets.finite_enumerated_set import FiniteEnumeratedSet indices = FiniteEnumeratedSet(self._index_dict) - if not prefix or prefix == 'F': - prefix = 'KnotInfo' super().__init__(indices, prefix) def _from_knotinfo(self, knotinfo, symmetry_mutant):