diff --git a/build/pkgs/khoca/SPKG.rst b/build/pkgs/khoca/SPKG.rst new file mode 100644 index 00000000000..38743225d44 --- /dev/null +++ b/build/pkgs/khoca/SPKG.rst @@ -0,0 +1,18 @@ +khoca: Khoca as pip installable package +======================================= + +Description +----------- + +Khoca as pip installable package + +License +------- + +GPLv2+ + +Upstream Contact +---------------- + +https://pypi.org/project/khoca/ + diff --git a/build/pkgs/khoca/dependencies b/build/pkgs/khoca/dependencies new file mode 100644 index 00000000000..8032e705951 --- /dev/null +++ b/build/pkgs/khoca/dependencies @@ -0,0 +1,5 @@ + ipython cypari | $(PYTHON_TOOLCHAIN) sagelib $(PYTHON) + + +---------- +All lines of this file are ignored except the first. diff --git a/build/pkgs/khoca/package-version.txt b/build/pkgs/khoca/package-version.txt new file mode 100644 index 00000000000..c068b2447cc --- /dev/null +++ b/build/pkgs/khoca/package-version.txt @@ -0,0 +1 @@ +1.4 diff --git a/build/pkgs/khoca/requirements.txt b/build/pkgs/khoca/requirements.txt new file mode 100644 index 00000000000..639d854c8ff --- /dev/null +++ b/build/pkgs/khoca/requirements.txt @@ -0,0 +1,2 @@ +# See https://github.com/sagemath/sage/pull/40081 +khoca diff --git a/build/pkgs/khoca/spkg-configure.m4 b/build/pkgs/khoca/spkg-configure.m4 new file mode 100644 index 00000000000..a2334eb68f5 --- /dev/null +++ b/build/pkgs/khoca/spkg-configure.m4 @@ -0,0 +1 @@ +SAGE_SPKG_CONFIGURE([khoca], [SAGE_PYTHON_PACKAGE_CHECK([khoca])]) diff --git a/build/pkgs/khoca/spkg-install.in b/build/pkgs/khoca/spkg-install.in new file mode 100644 index 00000000000..37ac1a53437 --- /dev/null +++ b/build/pkgs/khoca/spkg-install.in @@ -0,0 +1,2 @@ +cd src +sdh_pip_install . diff --git a/build/pkgs/khoca/type b/build/pkgs/khoca/type new file mode 100644 index 00000000000..134d9bc32d5 --- /dev/null +++ b/build/pkgs/khoca/type @@ -0,0 +1 @@ +optional diff --git a/build/pkgs/khoca/version_requirements.txt b/build/pkgs/khoca/version_requirements.txt new file mode 100644 index 00000000000..d7bd7163365 --- /dev/null +++ b/build/pkgs/khoca/version_requirements.txt @@ -0,0 +1 @@ +khoca diff --git a/src/sage/features/khoca.py b/src/sage/features/khoca.py new file mode 100644 index 00000000000..c4bfffe545f --- /dev/null +++ b/src/sage/features/khoca.py @@ -0,0 +1,31 @@ +r""" +Check for Khoca +""" +from . import PythonModule + + +class Khoca(PythonModule): + r""" + A :class:`sage.features.Feature` describing the presence of Khoca. + + Khoca is provided by an optional package in the Sage distribution. + + EXAMPLES:: + + sage: from sage.features.khoca import Khoca + sage: Khoca().is_present() # optional - khoca + FeatureTestResult('khoca', True) + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.khoca import Khoca + sage: isinstance(Khoca(), Khoca) + True + """ + PythonModule.__init__(self, 'khoca', spkg='khoca') + + +def all_features(): + return [Khoca()] diff --git a/src/sage/interfaces/all.py b/src/sage/interfaces/all.py index 0ef74630304..352c4c525a8 100644 --- a/src/sage/interfaces/all.py +++ b/src/sage/interfaces/all.py @@ -23,6 +23,7 @@ lazy_import('sage.interfaces.gnuplot', 'gnuplot') lazy_import('sage.interfaces.gp', ['gp', 'gp_version', 'Gp']) lazy_import('sage.interfaces.kash', ['kash', 'kash_version', 'Kash']) +lazy_import('sage.interfaces.khoca', ['khoca', 'Khoca']) lazy_import('sage.interfaces.lie', ['lie', 'LiE']) lazy_import('sage.interfaces.lisp', ['lisp', 'Lisp']) lazy_import('sage.interfaces.macaulay2', ['macaulay2', 'Macaulay2']) diff --git a/src/sage/interfaces/khoca.py b/src/sage/interfaces/khoca.py new file mode 100644 index 00000000000..05517b9fc77 --- /dev/null +++ b/src/sage/interfaces/khoca.py @@ -0,0 +1,215 @@ +r""" +Interface to Khoca + +Khoca is computer program writen by Lukas Lewark to calculate sl(N)-homology +of knots and links. It calculates the following: + +* Khovanov sl(2)-homology of arbitrary links, given as a braid or in PD code. +* Khovanov-Rozansky `sl(N)`-homology with `N > 2` of bipartite knots, given by a + certain encoding of a matched diagram of the knot. +* Homology over the integers, the rationals or a prime field. +* Either equivariant homology, or homology with an arbitrary fixed potential. +* All pages of the spectral sequence of filtered homology over a field. +* Reduced and unreduced homology. +* Homology of sums and mirror images of knots. + +For more details please have a look at the `Khoca repository `__. +If you are using khoca for a project or publication, please cite the web page or the literature +given there. Furthermore, note that not all functionality listed above is available through +this interface. Especially this is true for `sl(N)` homology for `N > 2` since we don't have +a method to obtain a *matched diagram* for bipartite knots. + +The Khoca interface will only work if the optional Sage package Khoca is installed. + +AUTHORS: + +- Sebastian Oehms (2025): +""" + +############################################################################## +# Copyright (C) 2025 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. +# https://www.gnu.org/licenses/ +############################################################################## + +from sage.misc.cachefunc import cached_function +from enum import Enum + + +class KnownKeywords(Enum): + r""" + Enum class to specify if a keyword belongs to the interface. + + EXAMPLES:: + + sage: from sage.interfaces.khoca import KnownKeywords + sage: [kwd for kwd in KnownKeywords] + [, + , + , + , + ] + """ + frobenius_algebra = 'frobenius_algebra' + root = 'root' + equivariant = 'equivariant' + reduced = 'reduced' + code = 'code' + + +@cached_function +def check_kwds(**kwds): + r""" + Return the keys of ``kwds`` as a list of elements of ``KnownKeywords``. + If ``kwds`` contains a key that is not in ``KnownKeywords`` a ``KeyError`` + is raised. + + EXAMPLES:: + + sage: from sage.interfaces.khoca import check_kwds + sage: check_kwds(frobenius_algebra=(0,0), root=0) + [, + ] + sage: check_kwds(frobenius=(0,0), root=0) + Traceback (most recent call last): + ... + ValueError: 'frobenius' is not a valid KnownKeywords + """ + keylist = [] + for k in kwds: + keylist.append(KnownKeywords(k)) + return keylist + + +@cached_function +def khoca_interface(ring, **kwds): + r""" + Return an instance of ``InteractiveCalculator` of the ``Khoca``. + This is a calculator for Khovanov homology written by Lukas + Lewark. For more information see :class:`~sage.features.khoca.Khoca`. + + EXAMPLES:: + + sage: # optional khoca + sage: from sage.interfaces.khoca import khoca_interface + sage: khoca_interface(ZZ) + Khovanov homology calculator for Frobenius algebra: Z[X] / (1*X^2). + sage: khoca_interface(QQ) + Khovanov homology calculator for Frobenius algebra: Q[X] / (1*X^2). + sage: khoca_interface(GF(3)) + Khovanov homology calculator for Frobenius algebra: F_3[X] / (1*X^2). + sage: khoca_interface(ZZ, frobenius_algebra=(1,-2), root=1) + Khovanov homology calculator for Frobenius algebra: Z[X] / (1*X^2 + -2*X + 1). + sage: khoca_interface(QQ, equivariant=3) + Traceback (most recent call last): + ... + NotImplementedError: keyword equivariant is not implemented yet + """ + from sage.features.khoca import Khoca + Khoca().require() + keys = check_kwds(**kwds) + ch = ring.characteristic() + rg = ch + if rg == 0 and ring.is_field(): + rg = 1 + from khoca import InteractiveCalculator + frobenius_algebra = (0, 0) + if KnownKeywords.frobenius_algebra in keys: + frobenius_algebra = kwds[KnownKeywords.frobenius_algebra.value] + root = 0 + if KnownKeywords.root in keys: + root = kwds[KnownKeywords.root.value] + equivariant = None + if KnownKeywords.equivariant in keys: + raise NotImplementedError('keyword %s is not implemented yet' % KnownKeywords.equivariant.value) + return InteractiveCalculator(coefficient_ring=rg, + frobenius_algebra=frobenius_algebra, + root=root, + equivariant=equivariant) + + +@cached_function +def khoca_raw_data(link, ring, red_typ=True, **kwds): + r""" + Return the raw data for the Khovanov homology from the ``Khoca`` + calculator. This needs the optional feature ``khoca`` be present. + + INPUT: + + - ``link`` -- :class:`~sage.knots.link.Link` + - ``ring`` -- the coefficient ring + - ``kwds`` -- dictionary of options to be passes to ``Khoca`` + + OUTPUT: + + A list of quadruples ``[degree, height, torsion, rank]`` each of which + represents a summand of a homology group for the given degree and height. + + EXAMPLES:: + + sage: # optional - khoca + sage: from sage.interfaces.khoca import khoca_raw_data + sage: B2 = BraidGroup(2) + sage: b2 = B2((1,1)) + sage: L2 = Link(b2) + sage: khoca_raw_data(L2, ZZ) + {(0, 0, 0): 1, (2, 0, 0): 1, (4, 2, 0): 1, (6, 2, 0): 1} + sage: khoca_raw_data(L2, ZZ, reduced=True) + {(1, 0, 0): 1, (5, 2, 0): 1} + sage: b3 = B2((1,1,1)) + sage: K3 = Link(b3) + sage: khoca_raw_data(K3, ZZ, code='pd') == khoca_raw_data(K3, ZZ, code='braid') + True + sage: khoca_raw_data(L2, ZZ, equivariant=3) + Traceback (most recent call last): + ... + NotImplementedError: keyword equivariant is not implemented yet + sage: khoca_raw_data(K3, QQ, code='gauss') + Traceback (most recent call last): + ... + ValueError: unknown code gauss, must be one of (pd, braid) + """ + def prepare_data(data): + r""" + compress and adapt data to Sage + """ + data_as_dict = {} + + for i in data: + # i[0]: degree, i[1]: height, i[2]: torsion i[3]: rank + d, h, t, r = i + # make d compatibil with Sage + d = int(t/2) - d + if (h, d, t) in data_as_dict: + data_as_dict[(h, d, t)] += r + else: + data_as_dict[(h, d, t)] = r + return data_as_dict + + if not red_typ: + keys = check_kwds(**kwds) + arg = link.braid().Tietze() + if KnownKeywords.code in keys: + code = kwds[KnownKeywords.code.value] + if code == 'pd': + arg = link.pd_code() + elif code != 'braid': + raise ValueError('unknown code %s, must be one of (pd, braid)' % code) + + KH = khoca_interface(ring, **kwds) + khres = KH(arg) + return {'red': prepare_data(khres[0]), 'unred': prepare_data(khres[1])} + + raw_data = khoca_raw_data(link, ring, red_typ=False, **kwds) + if 'reduced' in kwds: + red = kwds['reduced'] + if type(red) == bool: + if red: + return raw_data['red'] + else: + raise TypeError('reduced must be a boolean') + return raw_data['unred'] diff --git a/src/sage/interfaces/meson.build b/src/sage/interfaces/meson.build index 6a71a2e641e..c3408cf17fc 100644 --- a/src/sage/interfaces/meson.build +++ b/src/sage/interfaces/meson.build @@ -21,6 +21,7 @@ py.install_sources( 'jmoldata.py', 'kash.py', 'kenzo.py', + 'khoca.py', 'latte.py', 'lie.py', 'lisp.py', diff --git a/src/sage/knots/link.py b/src/sage/knots/link.py index 3366c1f02a0..6f80db8f310 100644 --- a/src/sage/knots/link.py +++ b/src/sage/knots/link.py @@ -1141,14 +1141,22 @@ def _enhanced_states(self): return tuple(states) @cached_method - def _khovanov_homology_cached(self, height, ring=ZZ): + def _khovanov_homology_cached(self, height, implementation, ring=ZZ, **kwds): r""" Return the Khovanov homology of the link. INPUT: - ``height`` -- the height of the homology to compute + - ``implementation=`` -- can be one of the + following: + + * ``'native'`` -- uses the original Sage implementation + * ``'Khoca'`` -- uses the implementation of the optional package + ``khoca`` package is present + - ``ring`` -- (default: ``ZZ``) the coefficient ring + - ``kwds`` -- dictionary of options to be passes to ``Khoca`` OUTPUT: @@ -1163,16 +1171,39 @@ def _khovanov_homology_cached(self, height, ring=ZZ): EXAMPLES:: sage: K = Link([[[1, -2, 3, -1, 2, -3]],[-1, -1, -1]]) - sage: K._khovanov_homology_cached(-5) # needs sage.modules + sage: K._khovanov_homology_cached(-5, 'native') # needs sage.modules ((-3, 0), (-2, Z), (-1, 0), (0, 0)) The figure eight knot:: sage: L = Link([[1, 6, 2, 7], [5, 2, 6, 3], [3, 1, 4, 8], [7, 5, 8, 4]]) - sage: L._khovanov_homology_cached(-1) # needs sage.modules + sage: L._khovanov_homology_cached(-1, 'native') # needs sage.modules ((-2, 0), (-1, Z), (0, Z), (1, 0), (2, 0)) """ crossings = self.pd_code() + + if implementation == 'Khoca': + from sage.interfaces.khoca import khoca_raw_data + raw_data = khoca_raw_data(self, ring, **kwds) + data = {(d, t): raw_data[(h, d, t)] for (h, d, t) in raw_data if h == height} + + from sage.homology.homology_group import HomologyGroup + if not data: + return [(0, HomologyGroup(0, ring))] + + torsion = set([k[1] for k in data]) + invfac = {} + for d in [k[0] for k in data]: + invfac[d] = [] + for t in torsion: + if (d, t) in data: + invfac[d] += [t]*data[(d, t)] + res = [] + for d in invfac: + ifac = sorted(invfac[d]) + res += [(d, HomologyGroup(len(ifac), ring, ifac))] + return tuple(sorted(res)) + ncross = len(crossings) states = [(_0, set(_1), set(_2), _3, _4) for (_0, _1, _2, _3, _4) in self._enhanced_states()] @@ -1206,7 +1237,7 @@ def _khovanov_homology_cached(self, height, ring=ZZ): homologies = ChainComplex(complexes).homology() return tuple(sorted(homologies.items())) - def khovanov_homology(self, ring=ZZ, height=None, degree=None): + def khovanov_homology(self, ring=ZZ, height=None, degree=None, implementation='native', **kwds): r""" Return the Khovanov homology of the link. @@ -1220,6 +1251,31 @@ def khovanov_homology(self, ring=ZZ, height=None, degree=None): - ``degree`` -- the degree of the homology to compute, if not specified, all the degrees are computed + - ``implementation=`` -- string (default 'native') can be one of the + following: + + * ``'native'`` -- uses the original Sage implementation + + * ``'Khoca'`` -- uses the implementation of the optional package + ``khoca`` + + - ``kwds`` -- dictionary of options to be passed to ``Khoca`` + + * ``reduced`` -- boolean (default ``False``); if + ``True``, then returns the reduced homology + + * ``equivariant`` -- positive integer (default ``2``); if this is + `n`, then it returns the Khovanov-Rozansky `sl(n)`-homology + with `n > 2` of bipartite knots + + * ``frobenius_algebra`` -- tuple of integers (default ``(0, 0)``); the + elements of the tuple are interpreted as modulus coefficients of + the underlying Frobenius algebra + + * ``root`` -- integer specifying a root of the modulus of the + Frobenius algeba + + OUTPUT: The Khovanov homology of the Link. It is given as a dictionary @@ -1235,6 +1291,8 @@ def khovanov_homology(self, ring=ZZ, height=None, degree=None): -5: {-3: 0, -2: Z, -1: 0, 0: 0}, -3: {-3: 0, -2: 0, -1: 0, 0: Z}, -1: {0: Z}} + sage: K.khovanov_homology(implementation='Khoca') # optional khoca, needs sage.modules + {-9: {-3: Z}, -7: {-3: 0, -2: C2}, -5: {-3: 0, -2: Z}, -3: {0: Z}, -1: {0: Z}} The figure eight knot:: @@ -1250,6 +1308,15 @@ def khovanov_homology(self, ring=ZZ, height=None, degree=None): sage: K = Link(b) sage: K.khovanov_homology(degree=2) {2: {2: 0}, 4: {2: Z}, 6: {2: Z}} + sage: K.khovanov_homology(degree=2, implementation='Khoca') # optional - khoca + {4: {2: Z}, 6: {2: Z}} + + Caution:: + + sage: K.khovanov_homology(base_ring=QQ) + Traceback (most recent call last): + ... + ValueError: invalid keyword(s): ['base_ring'] TESTS: @@ -1275,6 +1342,16 @@ def khovanov_homology(self, ring=ZZ, height=None, degree=None): sage: L.khovanov_homology(degree=1, height=1) {} """ + khoca = False + if implementation == 'Khoca': + khoca = True + from sage.interfaces.khoca import check_kwds + check_kwds(**kwds) + elif implementation != 'native': + raise ValueError('%s is not a recognized implementation') + elif kwds: + raise ValueError(f"invalid keyword(s): {list(kwds)}") + if not self.pd_code(): # special case for the unknot with no crossings from sage.homology.homology_group import HomologyGroup homs = {-1: {0: HomologyGroup(1, ring, [0])}, @@ -1291,11 +1368,15 @@ def khovanov_homology(self, ring=ZZ, height=None, degree=None): heights = [height] else: heights = sorted(set(state[-1] for state in self._enhanced_states())) + if khoca: + from sage.interfaces.khoca import khoca_raw_data + raw_data = khoca_raw_data(self, ring, **kwds) + heights = sorted(set(k[0] for k in raw_data)) if degree is not None: - homs = {j: dict(self._khovanov_homology_cached(j, ring)) for j in heights} + homs = {j: dict(self._khovanov_homology_cached(j, implementation, ring, **kwds)) for j in heights} homologies = {j: {degree: homs[j][degree]} for j in homs if degree in homs[j]} else: - homologies = {j: dict(self._khovanov_homology_cached(j, ring)) for j in heights} + homologies = {j: dict(self._khovanov_homology_cached(j, implementation, ring, **kwds)) for j in heights} return homologies def oriented_gauss_code(self): @@ -2050,7 +2131,8 @@ def conway_polynomial(self): conway += coeff * t_poly**M return conway - def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, base_ring=None): + def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, + base_ring=None, implementation='native', **kwds): r""" Return the Khovanov polynomial of ``self``. @@ -2069,6 +2151,16 @@ def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, base_rin the torsion - ``ring`` -- (default: ``ZZ``) the ring of the homology. This will be transferred to :meth:`khovanov_homology` + - ``implementation=`` -- string (default 'native') can be one of the + following: + + * ``'native'`` -- uses the original Sage implementation + + * ``'Khoca'`` -- uses the implementation of the optional package + ``khoca`` + + - ``kwds`` -- dictionary of options to be passes to ``Khoca`` + for details see :meth:`khovanov_homology` Here we follow the conventions used in `KnotInfo `__ @@ -2084,11 +2176,16 @@ def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, base_rin sage: K = Link([[[1, -2, 3, -1, 2, -3]],[-1, -1, -1]]) sage: K.khovanov_polynomial() # needs sage.modules q^-1 + q^-3 + q^-5*t^-2 + q^-7*t^-2*T^2 + q^-9*t^-3 + sage: K.khovanov_polynomial(implementation='Khoca') == _ # optional khoca, needs sage.modules + True sage: K.khovanov_polynomial(ring=GF(2)) # needs sage.modules q^-1 + q^-3 + q^-5*t^-2 + q^-7*t^-2 + q^-7*t^-3 + q^-9*t^-3 + sage: K.khovanov_polynomial(ring=GF(2), implementation='Khoca') == _ # optional khoca, needs sage.modules + True The figure eight knot:: + sage: # needs sage.modules sage: L = Link([[1, 6, 2, 7], [5, 2, 6, 3], [3, 1, 4, 8], [7, 5, 8, 4]]) sage: L.khovanov_polynomial(var1='p') # needs sage.modules p^5*t^2 + p^3*t^2*T^2 + p*t + p + p^-1 + p^-1*t^-1 @@ -2096,6 +2193,9 @@ def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, base_rin sage: L.khovanov_polynomial(var1='p', var2='s', ring=GF(4)) # needs sage.modules sage.rings.finite_rings p^5*s^2 + p^3*s^2 + p^3*s + p*s + p + p^-1 + p^-1*s^-1 + p^-3*s^-1 + p^-3*s^-2 + p^-5*s^-2 + sage: L.khovanov_polynomial(var1='p', var2='s', ring=GF(4), # optional khoca, sage.rings.finite_rings + ....: implementation='Khoca') == _ # optional khoca, sage.rings.finite_rings + True The Hopf link:: @@ -2104,6 +2204,8 @@ def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, base_rin sage: K = Link(b) sage: K.khovanov_polynomial() # needs sage.modules q^6*t^2 + q^4*t^2 + q^2 + 1 + sage: K.khovanov_polynomial(implementation='Khoca') == _ # optional khoca, needs sage.modules + True .. SEEALSO:: :meth:`khovanov_homology` """ @@ -2120,7 +2222,7 @@ def khovanov_polynomial(self, var1='q', var2='t', torsion='T', ring=ZZ, base_rin else: L = LaurentPolynomialRing(ZZ, [var1, var2]) coeff = {} - kh = self.khovanov_homology(ring=ring) + kh = self.khovanov_homology(ring=ring, implementation=implementation, **kwds) from sage.rings.infinity import infinity for h in kh: for d in kh[h]: