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: