From c9c7761319a046c877aaade02faa1e04fa34f86e Mon Sep 17 00:00:00 2001 From: Willie Aboumrad Date: Wed, 12 Apr 2023 03:04:42 -0400 Subject: [PATCH 1/3] first stab at re-factoring fusion ring code. doc tests pass except for a couple issues with get_order and FusionRingFromWCR.__classcall__. Need some documentation/explanation in FusionRing constructor --- src/sage/algebras/fusion_rings/all.py | 5 +- src/sage/algebras/fusion_rings/f_matrix.py | 2 +- .../algebras/fusion_rings/fusion_double.py | 580 +---- .../fusion_rings/fusion_ring_from_wcr.py | 628 ++++++ ...{fusion_ring.py => generic_fusion_ring.py} | 1997 +++++++---------- 5 files changed, 1511 insertions(+), 1701 deletions(-) create mode 100644 src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py rename src/sage/algebras/fusion_rings/{fusion_ring.py => generic_fusion_ring.py} (56%) diff --git a/src/sage/algebras/fusion_rings/all.py b/src/sage/algebras/fusion_rings/all.py index bf9c016510f..4c682532927 100644 --- a/src/sage/algebras/fusion_rings/all.py +++ b/src/sage/algebras/fusion_rings/all.py @@ -2,7 +2,7 @@ Fusion Rings """ # **************************************************************************** -# Copyright (C) 2022 Guillermo Aboumrad +# Copyright (C) 2023 Guillermo Aboumrad # # 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 @@ -13,5 +13,4 @@ from sage.misc.lazy_import import lazy_import -lazy_import('sage.algebras.fusion_rings.fusion_ring', ['FusionRing']) -lazy_import('sage.algebras.fusion_rings.fusion_double', ['FusionDouble']) +lazy_import('sage.algebras.fusion_rings.generic_fusion_ring', ['FusionRing']) diff --git a/src/sage/algebras/fusion_rings/f_matrix.py b/src/sage/algebras/fusion_rings/f_matrix.py index 734d9cffa24..3d155a24a6e 100644 --- a/src/sage/algebras/fusion_rings/f_matrix.py +++ b/src/sage/algebras/fusion_rings/f_matrix.py @@ -1865,7 +1865,7 @@ def attempt_number_field_computation(self): undertake a :func:`NumberField` computation. """ ct = self._FR.cartan_type() - k = self._FR._k + k = self._FR.fusion_level() # Don't try when k is large and odd for SU(2)_k if ct.letter == 'A': if ct.n == 1 and k >= 9 and k % 2: diff --git a/src/sage/algebras/fusion_rings/fusion_double.py b/src/sage/algebras/fusion_rings/fusion_double.py index 36361a2352b..308b0006937 100644 --- a/src/sage/algebras/fusion_rings/fusion_double.py +++ b/src/sage/algebras/fusion_rings/fusion_double.py @@ -8,18 +8,12 @@ # Distributed under the terms of the GNU General Public License (GPL) # https://www.gnu.org/licenses/ # **************************************************************************** -from sage.categories.algebras_with_basis import AlgebrasWithBasis -from sage.combinat.free_module import CombinatorialFreeModule -from sage.rings.integer_ring import ZZ -from sage.misc.misc import inject_variable +from sage.algebras.fusion_rings.generic_fusion_ring import FusionRing from sage.misc.cachefunc import cached_method +from sage.rings.integer_ring import ZZ from sage.sets.set import Set -from sage.rings.number_field.number_field import CyclotomicField -from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing -from sage.rings.ideal import Ideal -from sage.matrix.constructor import matrix -class FusionDouble(CombinatorialFreeModule): +class FusionRingFromQuantumDouble(FusionRing): r""" This constructs the Fusion Ring of the modular tensor category of modules over the Drinfeld Double of a finite group. Usage is similar to :class:`FusionRing`::. @@ -42,7 +36,7 @@ class FusionDouble(CombinatorialFreeModule): EXAMPLES:: sage: G = DihedralGroup(5) - sage: H = FusionDouble(G, inject_variables=True) + sage: H = FusionRing(G, inject_variables=True) sage: H.basis() Finite family {0: s0, 1: s1, 2: s2, 3: s3, 4: s4, 5: s5, 6: s6, 7: s7, 8: s8, 9: s9, 10: s10, 11: s11, 12: s12, 13: s13, 14: s14, 15: s15} sage: for x in H.basis(): @@ -84,7 +78,7 @@ class FusionDouble(CombinatorialFreeModule): EXAMPLES:: sage: G1 = SymmetricGroup(3) - sage: H1 = FusionDouble(G1,prefix="u",inject_variables=True) + sage: H1 = FusionRing(G1, prefix="u", inject_variables=True) sage: F = H1.get_fmatrix() The above commands create the F-matrix factory. You can compute the F-matrices @@ -99,7 +93,7 @@ class FusionDouble(CombinatorialFreeModule): see :class:`FMatrix` for more information. Unfortunately beyond `S_3` the number of simple objects is larger. - Although the :class:`FusionDouble` class and its methods work well + Although the :class:`FusionRing` class and its methods work well for groups of moderate size, the FMatrix may not be available. For the dihedral group of order 8, there are already 22 simple objects, and the F-matrix seems out of reach. @@ -115,88 +109,71 @@ class FusionDouble(CombinatorialFreeModule): sage: G1 = GL(2,3) sage: G2 = G1.as_permutation_group() - sage: H2 = FusionDouble(G2,prefix="b",inject_variables=True) + sage: H2 = FusionRing(G2, prefix="b", inject_variables=True) sage: b13^2 # long time (43s) b0 + b1 + b5 + b6 + b13 + b26 + b30 + b31 + b32 + b33 + b38 + b39 sage: b13.ribbon() zeta3 In this example, implementing the simple group of order 168 as - the matrix group ``G1`` will not work with the ``FusionDouble``, so we + the matrix group ``G1`` will not work with the ``FusionRing``, so we recreate it as the permutation group ``G2``. Although the test of squaring `b2` takes a long time, the fusion coefficients are cached and this FusionRing is not too slow to work with. (Of course the F-matrix factory is not available for this group.) """ - def __init__(self, G, prefix="s",inject_variables=False): + def __init__(self, G, base_ring=ZZ, prefix="s", cyclotomic_order=None, fusion_labels=None, inject_variables=False): """ EXAMPLES:: - sage: H = FusionDouble(DihedralGroup(7)) - sage: TestSuite(H).run() + sage: H = FusionRing(DihedralGroup(7)) """ self._G = G - self._prefix = prefix - self._names = {} self._elt = {} self._chi = {} count = 0 + names = {} for g in G.conjugacy_classes_representatives(): for chi in G.centralizer(g).irreducible_characters(): - self._names[count] = "%s%s"%(prefix, count) + names[count] = "%s%s"%(prefix, count) self._elt[count] = g self._chi[count] = chi count += 1 - self._rank = count - self._cyclotomic_order = G.exponent() - self._basecoer = None - self._fusion_labels = None - self._field = None - cat = AlgebrasWithBasis(ZZ) - CombinatorialFreeModule.__init__(self, ZZ, [k for k in self._names], category=cat) - if inject_variables: - self.inject_variables() + cyclotomic_order = G.exponent() + super().__init__(names=names, base_ring=ZZ, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) def _repr_(self): """ EXAMPLES:: - sage: FusionDouble(SymmetricGroup(3)) + sage: FusionRing(SymmetricGroup(3)) The Fusion Ring of the Drinfeld Double of Symmetric group of order 3! as a permutation group """ return "The Fusion Ring of the Drinfeld Double of %s"%self._G - def _element_constructor(self, k): + def group(self): """ - Construct a monomial (basis element) from a key. - - INPUT: - - - ``key`` -- a key for the dictionary `self._names` + Returns the underlying group. EXAMPLES:: - sage: F=FusionDouble(SymmetricGroup(3),prefix="n",inject_variables=True) - sage: F._names - {0: 'n0', 1: 'n1', 2: 'n2', 3: 'n3', 4: 'n4', 5: 'n5', 6: 'n6', 7: 'n7'} - sage: [F._element_constructor(x) for x in F._names] - [n0, n1, n2, n3, n4, n5, n6, n7] + sage: FusionRing(DiCyclicGroup(4)).group() + Diyclic group of order 16 as a permutation group """ - return self.monomial(k) + return self._G - def inject_variables(self): + def IdGroup(self): """ - Create variables for the simple objects in the global name space. + Returns the GAP Small Group identifier. This is a pair ``[n,k]`` where ``n`` is + the order of the group, and ``k`` is an integer characterizing the + isomorphism class of the group, available for very many groups. EXAMPLES:: - sage: F = FusionDouble(DiCyclicGroup(3), prefix="d") - sage: F.inject_variables() - sage: d4^2 - d0 + d1 + d5 + sage: FusionRing(DiCyclicGroup(4)).IdGroup() + [ 16, 9 ] """ - for i in range(self._rank): - inject_variable(self._names[i],self.monomial(i)) + return self._G._libgap_().IdGroup() @cached_method def s_ij(self, i, j, unitary=False, base_coercion=True): @@ -214,107 +191,27 @@ def s_ij(self, i, j, unitary=False, base_coercion=True): EXAMPLES:: - sage: D = FusionDouble(SymmetricGroup(3),prefix="t",inject_variables=True) - sage: [D.s_ij(t2,x) for x in D.basis()] + sage: D = FusionRing(SymmetricGroup(3), prefix="t", inject_variables=True) + sage: [D.s_ij(t2, x) for x in D.basis()] [2, 2, 4, 0, 0, -2, -2, -2] - sage: [D.s_ij(t2,x,unitary=True) for x in D.basis()] + sage: [D.s_ij(t2, x, unitary=True) for x in D.basis()] [1/3, 1/3, 2/3, 0, 0, -1/3, -1/3, -1/3] """ - sum = 0 + ret = 0 G = self._G [i, j] = [x.support_of_term() for x in [i,j]] [a, chi_1] = [self._elt[i], self._chi[i]] [b, chi_2] = [self._elt[j], self._chi[j]] for g in G: if a*g*b*g.inverse() == g*b*g.inverse()*a: - sum += chi_1(g*b*g.inverse()) * chi_2(g.inverse()*a*g) + ret += chi_1(g*b*g.inverse()) * chi_2(g.inverse()*a*g) + ret *= G.order() / (G.centralizer(a).order() * G.centralizer(b).order()) if unitary: - coef = 1 / (G.centralizer(a).order() * G.centralizer(b).order()) - else: - coef = G.order() / (G.centralizer(a).order() * G.centralizer(b).order()) - ret = coef * sum + ret /= self.total_q_order(base_coercion=False) if (not base_coercion) or (self._basecoer is None): return ret return self._basecoer(ret) - def s_ijconj(self, i, j, unitary=False, base_coercion=True): - """ - Return the conjugate of the element of the S-matrix given by - ``self.s_ij(elt_i, elt_j, base_coercion=base_coercion)``. - - See :meth:`s_ij`. - - EXAMPLES:: - - sage: P=FusionDouble(CyclicPermutationGroup(3),prefix="p",inject_variables=True) - sage: P.s_ij(p1,p3) - zeta3 - sage: P.s_ijconj(p1,p3) - -zeta3 - 1 - """ - return self.s_ij(i, j, unitary=unitary, base_coercion=base_coercion).conjugate() - - def s_matrix(self, unitary=False, base_coercion=True): - r""" - Return the S-matrix of this fusion ring. - - OPTIONAL: - - - ``unitary`` -- (default: ``False``) set to ``True`` to obtain - the unitary S-matrix - - Without the ``unitary`` parameter, this is the matrix denoted - `\widetilde{s}` in [BaKi2001]_. - - EXAMPLES:: - - sage: FusionDouble(SymmetricGroup(3)).s_matrix() - [ 1 1 2 3 3 2 2 2] - [ 1 1 2 -3 -3 2 2 2] - [ 2 2 4 0 0 -2 -2 -2] - [ 3 -3 0 3 -3 0 0 0] - [ 3 -3 0 -3 3 0 0 0] - [ 2 2 -2 0 0 4 -2 -2] - [ 2 2 -2 0 0 -2 -2 4] - [ 2 2 -2 0 0 -2 4 -2] - sage: FusionDouble(SymmetricGroup(3)).s_matrix(unitary=True) - [ 1/6 1/6 1/3 1/2 1/2 1/3 1/3 1/3] - [ 1/6 1/6 1/3 -1/2 -1/2 1/3 1/3 1/3] - [ 1/3 1/3 2/3 0 0 -1/3 -1/3 -1/3] - [ 1/2 -1/2 0 1/2 -1/2 0 0 0] - [ 1/2 -1/2 0 -1/2 1/2 0 0 0] - [ 1/3 1/3 -1/3 0 0 2/3 -1/3 -1/3] - [ 1/3 1/3 -1/3 0 0 -1/3 -1/3 2/3] - [ 1/3 1/3 -1/3 0 0 -1/3 2/3 -1/3] - - """ - b = self.basis() - S = matrix([[self.s_ij(b[x], b[y], unitary=unitary, base_coercion=base_coercion) - for x in self.get_order()] for y in self.get_order()]) - return S - - @cached_method - def N_ijk(self, i, j, k): - """ - The symmetric invariant of three simple objects, - this returns the dimension of - - .. MATH:: - Hom(i \\otimes j\\otimes k, s_0) - - where `s_0` is the unit element (assuming prefix='s'). - Method of computation is through the Verlinde formula, - deducing the values from the known values of the S-matrix. - - EXAMPLES:: - - sage: A = FusionDouble(AlternatingGroup(4),prefix="a",inject_variables=True) - sage: [A.N_ijk(a10,a11,x) for x in A.basis()] - [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0] - """ - sz = self.one() - return ZZ(sum(self.s_ij(i, r, unitary=True) * self.s_ij(j, r, unitary=True) * self.s_ij(k, r, unitary=True)/self.s_ij(sz, r, unitary=True) for r in self.basis())) - @cached_method def Nk_ij(self, i, j, k): r""" @@ -325,11 +222,12 @@ def Nk_ij(self, i, j, k): EXAMPLES:: - sage: A = FusionDouble(AlternatingGroup(4),prefix="aa",inject_variables=True) - sage: [A.Nk_ij(aa8,aa10,x) for x in A.basis()] + sage: A = FusionRing(AlternatingGroup(4), prefix="aa", inject_variables=True) + sage: [A.Nk_ij(aa8, aa10, x) for x in A.basis()] [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1] """ - return self.N_ijk(i, j, self.dual(k)) + sz = self.one() + return ZZ(sum(self.s_ij(i, r, unitary=True) * self.s_ij(j, r, unitary=True) * self.s_ijconj(k, r, unitary=True)/self.s_ij(sz, r, unitary=True) for r in self.basis())) def char_Nk_ij(self, i, j, k): r""" @@ -360,7 +258,7 @@ def char_Nk_ij(self, i, j, k): EXAMPLES:: - sage: B = FusionDouble(CyclicPermutationGroup(2)) + sage: B = FusionRing(CyclicPermutationGroup(2)) sage: all(B.char_Nk_ij(x,y,z)==B.Nk_ij(x,y,z) for x in B.basis() for y in B.basis() for z in B.basis()) True """ @@ -393,342 +291,18 @@ def char_Nk_ij(self, i, j, k): res += i.char()(i_twist * x * i_twist.inverse()) * j.char()(j_twist * x * j_twist.inverse()) * k.char()(x).conjugate() return c * res - @cached_method - def field(self): - """ - Returns a cyclotomic field large enough to contain the values - of R-matrices and twists that can arise for this fusion ring. - - EXAMPLES:: - - sage: FusionDouble(SymmetricGroup(3)).field() - Cyclotomic Field of order 24 and degree 8 - """ - return CyclotomicField(4 * self._cyclotomic_order) - - def fvars_field(self): - r""" - Return a field containing the ``CyclotomicField`` computed by - :meth:`field` as well as all the F-symbols of the associated - ``FMatrix`` factory object. - - This method is only available if ``self`` is multiplicity-free. - - EXAMPLES:: - - sage: FusionDouble(SymmetricGroup(3)).fvars_field() - Cyclotomic Field of order 24 and degree 8 - """ - if self.is_multiplicity_free(verbose=False): - return self.get_fmatrix().field() - raise NotImplementedError("method is only available for multiplicity free fusion rings") - - def root_of_unity(self, r, base_coercion=True): - r""" - Return `e^{i\pi r}` as an element of ``self.field()`` if possible. - - INPUT: - - - ``r`` -- a rational number - - EXAMPLES:: - - sage: H = FusionDouble(DihedralGroup(6)) - sage: H.field() - Cyclotomic Field of order 24 and degree 8 - sage: for n in [1..7]: - ....: try: - ....: print (n,H.root_of_unity(2/n)) - ....: except ValueError as err: - ....: print (n,err) - ....: - 1 1 - 2 -1 - 3 zeta24^4 - 1 - 4 zeta24^6 - 5 not a root of unity in the field - 6 zeta24^4 - 7 not a root of unity in the field - """ - n = 2 * r * self._cyclotomic_order - if n not in ZZ: - raise ValueError("not a root of unity in the field") - ret = self.field().gen() ** n - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - @cached_method - def r_matrix(self, i, j, k, base_coercion=True): - r""" - Return the R-matrix entry corresponding to the subobject ``k`` - in the tensor product of ``i`` with ``j``. This method is only - correct if the fusion coefficient ``N_{ij}^k\leq 1``. See the - :class:`FusionRing` method for more information, including - the reason for this caveat, and the algorithm. - - EXAMPLES:: - - sage: C = FusionDouble(SymmetricGroup(3),prefix="c",inject_variables=True) - sage: c4*c5 - c3 + c4 - sage: [C.r_matrix(c4,c5,k) for k in [c3,c4]] - [-zeta24^6, 1] - sage: c6^2 - c0 + c1 + c6 - sage: [C.r_matrix(c6,c6,k) for k in [c0,c1,c6]] - [zeta3, -zeta3, -zeta3 - 1] - """ - if self.Nk_ij(i, j, k) == 0: - return self.field().zero() if (not base_coercion) or (self._basecoer is None) else self.fvars_field().zero() - if i != j: - ret = self.root_of_unity((k.twist() - i.twist() - j.twist()) / 2) - else: - i0 = self.one() - B = self.basis() - ret = sum(y.ribbon()**2 / (i.ribbon() * x.ribbon()**2) - * self.s_ij(i0, y) * self.s_ij(i, z) * self.s_ijconj(x, z) - * self.s_ijconj(k, x) * self.s_ijconj(y, z) / self.s_ij(i0, z) - for x in B for y in B for z in B) / (self.total_q_order()**4) - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def global_q_dimension(self, base_coercion=True): - r""" - Return the global quantum dimension, which is the sum of the squares of the - quantum dimensions of the simple objects. - For the Drinfeld double, it is the square of the order of the underlying quantum group. - - EXAMPLES:: - - sage: G = SymmetricGroup(4) - sage: H = FusionDouble(G) - sage: H.global_q_dimension() - 576 - sage: sum(x.q_dimension()^2 for x in H.basis()) - 576 - """ - ret = self._G.order()**2 - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - - def total_q_order(self, base_coercion=True): - r""" - Return the positive square root of :meth:`self.global_q_dimension() - ` as an element of :meth:`self.field() `. - - For the Drinfeld double of a finite group `G`, this equals the - cardinality of `G`. - - EXAMPLES:: - - sage: FusionDouble(DihedralGroup(7)).total_q_order() - 14 - """ - ret = self._G.order() - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def D_plus(self, base_coercion=True): - r""" - Return `\sum d_i^2\theta_i` where `i` runs through the simple objects, - `d_i` is the quantum dimension and `\theta_i` is the twist. - - This is denoted `p_+` in [BaKi2001]_ Chapter 3. For the Drinfeld - double, it equals the order of the group. - - EXAMPLES:: - - sage: FusionDouble(DihedralGroup(8)).D_plus() - 16 - """ - ret = self._G.order() - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def D_minus(self, base_coercion=True): - r""" - Return `\sum d_i^2\theta_i^{-1}` where `i` runs through the simple - objects, `d_i` is the quantum dimension and `\theta_i` is the twist. - - This is denoted `p_-` in [BaKi2001]_ Chapter 3. For the Drinfeld - double, it equals the order of the group. - - EXAMPLES:: - - sage: FusionDouble(DihedralGroup(9)).D_minus() - 18 - """ - ret = self._G.order() - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def is_multiplicity_free(self, verbose=False): - """ - Returns True if all fusion coefficients are at most 1. - - EXAMPLES:: - - sage: FusionDouble(SymmetricGroup(3)).is_multiplicity_free() - True - sage: FusionDouble(SymmetricGroup(4)).is_multiplicity_free() - False - """ - if verbose: - print("Checking multiplicity free-ness") - for i in self.basis(): - for j in self.basis(): - for k in self.basis(): - if self.N_ijk(i,j,k) > 1: - if verbose: - print("N(%s,%s,%s)=%s"%(i,j,k,self.N_ijk(i,j,k))) - return False - return True - def one(self): """ The unit element of the ring, which is the first basis element. EXAMPLES:: - sage: FusionDouble(CyclicPermutationGroup(2),prefix="h").one() + sage: FusionRing(CyclicPermutationGroup(2), prefix="h").one() h0 """ return self.basis()[0] - @cached_method - def dual(self,i): - r""" - Return the dual object ``i^\ast`` to ``i``. The dual is also - available as an element method of ``i``. - - EXAMPLES:: - - sage: K = FusionDouble(CyclicPermutationGroup(3),prefix="k") - sage: [(x,K.dual(x)) for x in K.basis()] - [(k0, k0), - (k1, k2), - (k2, k1), - (k3, k6), - (k4, k8), - (k5, k7), - (k6, k3), - (k7, k5), - (k8, k4)] - sage: all(K.dual(x)==x.dual() for x in K.basis()) - True - """ - sz = self.one() - for j in self.basis(): - if self.N_ijk(i,j,sz) > 0: - return j - - def product_on_basis(self, a, b): - """ - Return the product of two basis elements corresponding to keys `a` and `b`. - - INPUT: - - - ``a`, ``b`` -- keys for the dictionary ``self._names`` representing simple objects - - EXAMPLES:: - - sage: Q=FusionDouble(SymmetricGroup(3),prefix="q",inject_variables=True) - sage: q3*q4 - q1 + q2 + q5 + q6 + q7 - sage: Q._names - {0: 'q0', 1: 'q1', 2: 'q2', 3: 'q3', 4: 'q4', 5: 'q5', 6: 'q6', 7: 'q7'} - sage: Q.product_on_basis(3,4) - q1 + q2 + q5 + q6 + q7 - """ - d = {k.support_of_term() : self.N_ijk(self.monomial(a),self.monomial(b),self.dual(k)) for k in self.basis()} - return self._from_dict(d) - - def _repr_term(self, t): - """ - EXAMPLES:: - - sage: F = FusionDouble(CyclicPermutationGroup(2)) - sage: [F._repr_term(t) for t in F._names] - ['s0', 's1', 's2', 's3'] - """ - return self._names[t] - - def group(self): - """ - Returns the underlying group. - - EXAMPLES:: - - sage: FusionDouble(DiCyclicGroup(4)).group() - Diyclic group of order 16 as a permutation group - """ - return self._G - - def IdGroup(self): - """ - Returns the GAP Small Group identifier. This is a pair ``[n,k]`` where ``n`` is - the order of the group, and ``k`` is an integer characterizing the - isomorphism class of the group, available for very many groups. - - EXAMPLES:: - - sage: FusionDouble(DiCyclicGroup(4)).IdGroup() - [ 16, 9 ] - - """ - return self._G._libgap_().IdGroup() - - def get_fmatrix(self, *args, **kwargs): - """ - Construct an :class:`FMatrix` factory to solve the pentagon and hexagon relations - and organize the resulting F-symbols. - - EXAMPLES:: - - sage: f = FusionDouble(SymmetricGroup(3)).get_fmatrix(); f - F-Matrix factory for The Fusion Ring of the Drinfeld Double of Symmetric group of order 3! as a permutation group - """ - if not hasattr(self, 'fmats') or kwargs.get('new', False): - kwargs.pop('new', None) - from sage.algebras.fusion_rings.f_matrix import FMatrix - self.fmats = FMatrix(self, *args, **kwargs) - return self.fmats - - def get_order(self): - r""" - Return the keys of the basis vectors in a fixed order, needed - for the F-matrix code. - - EXAMPLES:: - - sage: FusionDouble(SymmetricGroup(4)).get_order() - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] - - """ - if self._order is None: - self.set_order(self.basis().keys().list()) - return self._order - - class Element(CombinatorialFreeModule.Element): - def is_simple_object(self): - r""" - Determine whether ``self`` is a simple object (basis element) of the fusion ring. - - EXAMPLES:: - - sage: H=FusionDouble(CyclicPermutationGroup(2),prefix="g",inject_variables=True) - sage: [x.is_simple_object() for x in [g0,g1,g0+g1]] - [True, True, False] - """ - return self in self.parent().basis() - + class Element(FusionRing.Element): def g(self): r""" The data determining a simple object consists of a conjugacy @@ -741,10 +315,10 @@ class representative `g` and an irreducible character `\chi` of EXAMPLES:: sage: G = QuaternionGroup() - sage: H = FusionDouble(G,prefix="e",inject_variables=True) - sage: e10.g() + sage: H = FusionRing(G, prefix="q", inject_variables=True) + sage: q10.g() (1,3)(2,4)(5,7)(6,8) - sage: e10.char() + sage: q10.char() Character of Subgroup generated by [(1,2,3,4)(5,6,7,8), (1,5,3,7)(2,8,4,6)] of (Quaternion group of order 8 as a permutation group) """ @@ -761,21 +335,22 @@ class representative `g` and an irreducible character `\chi` of EXAMPLES:: sage: G = DihedralGroup(5) - sage: H = FusionDouble(G,prefix="f",inject_variables=True) - sage: f10.g() + sage: H = FusionRing(G, prefix="d", inject_variables=True) + sage: d10.g() (1,2,3,4,5) - sage: f10.char() + sage: d10.char() Character of Subgroup generated by [(1,2,3,4,5)] of (Dihedral group of order 10 as a permutation group) """ return self.parent()._chi[self.support_of_term()] + @cached_method def ribbon(self, base_coercion=True): """ The twist or ribbon of the simple object. EXAMPLES:: - sage: H = FusionDouble(CyclicPermutationGroup(3)) + sage: H = FusionRing(CyclicPermutationGroup(3)) sage: [i.ribbon() for i in H.basis()] [1, 1, 1, 1, zeta3, -zeta3 - 1, 1, -zeta3 - 1, zeta3] """ @@ -783,60 +358,3 @@ def ribbon(self, base_coercion=True): if (not base_coercion) or (self.parent()._basecoer is None): return ret return self.parent()._basecoer(ret) - - def twist(self, reduced=True): - r""" - Return a rational number `h` such that `\theta = e^{i \pi h}` - is the twist of ``self``. The quantity `e^{i \pi h}` is - also available using :meth:`ribbon`. - - This method is only available for simple objects. - - EXAMPLES:: - - sage: Q=FusionDouble(CyclicPermutationGroup(3)) - sage: [x.twist() for x in Q.basis()] - [0, 0, 0, 0, 2/3, 4/3, 0, 4/3, 2/3] - sage: [x.ribbon() for x in Q.basis()] - [1, 1, 1, 1, zeta3, -zeta3 - 1, 1, -zeta3 - 1, zeta3] - """ - if not self.is_simple_object(): - raise ValueError("quantum twist is only available for simple objects of a FusionRing") - zeta = self.parent().field().gen() - rib = self.ribbon() - for k in range(4*self.parent()._cyclotomic_order): - if zeta**k == rib: - return k/(2*self.parent()._cyclotomic_order) - - def dual(self): - """ - Return the dual of self. - - EXAMPLES:: - - sage: G = CyclicPermutationGroup(4) - sage: H = FusionDouble(G, prefix="j") - sage: [x for x in H.basis() if x==x.dual()] - [j0, j1, j8, j9] - """ - if not self.is_simple_object(): - raise ValueError("dual is only available for simple objects of a FusionRing") - return self.parent().dual(self) - - @cached_method - def q_dimension(self, base_coercion=True): - """ - Return the q-dimension of self. - - EXAMPLES:: - - sage: G = AlternatingGroup(4) - sage: H = FusionDouble(G) - sage: [x.q_dimension() for x in H.basis()] - [1, 1, 1, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4] - sage: sum(x.q_dimension()^2 for x in H.basis()) == G.order()^2 - True - """ - if not self.is_simple_object(): - raise ValueError("quantum dimension is only available for simple objects of a FusionRing") - return self.parent().s_ij(self,self.parent().one()) diff --git a/src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py b/src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py new file mode 100644 index 00000000000..dd5af46f00b --- /dev/null +++ b/src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py @@ -0,0 +1,628 @@ +""" +Fusion Rings defined by Weyl Character Rings +""" +# **************************************************************************** +# Copyright (C) 2019 Daniel Bump +# Guillermo Aboumrad +# Travis Scrimshaw +# Nicolas Thiery +# 2022 Guillermo Aboumrad +# +# Distributed under the terms of the GNU General Public License (GPL) +# https://www.gnu.org/licenses/ +# **************************************************************************** +from sage.algebras.fusion_rings.generic_fusion_ring import FusionRing +from sage.combinat.q_analogues import q_int +from sage.combinat.root_system.weyl_characters import WeylCharacterRing +from sage.misc.cachefunc import cached_method +from sage.rings.integer_ring import ZZ + +class FusionRingFromWCR(FusionRing): + r""" + Return the Fusion Ring (Verlinde Algebra) of level ``k``. + + INPUT: + + - ``ct`` -- the Cartan type of a simple (finite-dimensional) Lie algebra + - ``k`` -- a nonnegative integer + - ``conjugate`` -- (default ``False``) set ``True`` to obtain + the complex conjugate ring + - ``cyclotomic_order`` -- (default computed depending on ``ct`` and ``k``) + - ``fusion_labels`` -- (default None) either a tuple of strings to use as labels of the + basis of simple objects, or a string from which the labels will be + constructed + - ``inject_variables`` -- (default ``False``): use with ``fusion_labels``. + If ``inject_variables`` is ``True``, the fusion labels will be variables + that can be accessed from the command line + + The cyclotomic order is an integer `N` such that all computations + will return elements of the cyclotomic field of `N`-th roots of unity. + Normally you will never need to change this but consider changing it + if :meth:`root_of_unity` raises a ``ValueError``. + + This algebra has a basis (sometimes called *primary fields* but here + called *simple objects*) indexed by the weights of level `\leq k`. + These arise as the fusion algebras of Wess-Zumino-Witten (WZW) conformal + field theories, or as Grothendieck groups of tilting modules for quantum + groups at roots of unity. The :class:`FusionRing` class is implemented as + a variant of the :class:`WeylCharacterRing`. + + REFERENCES: + + - [BaKi2001]_ Chapter 3 + - [DFMS1996]_ Chapter 16 + - [EGNO2015]_ Chapter 8 + - [Feingold2004]_ + - [Fuchs1994]_ + - [Row2006]_ + - [Walton1990]_ + - [Wan2010]_ + + EXAMPLES:: + + sage: A22 = FusionRing(("A2", 2)) + sage: [f1, f2] = A22.fundamental_weights() + sage: M = [A22(x) for x in [0*f1, 2*f1, 2*f2, f1+f2, f2, f1]] + sage: [M[3] * x for x in M] + [A22(1,1), + A22(0,1), + A22(1,0), + A22(0,0) + A22(1,1), + A22(0,1) + A22(2,0), + A22(1,0) + A22(0,2)] + + You may assign your own labels to the basis elements. In the next + example, we create the `SO(5)` fusion ring of level `2`, check the + weights of the basis elements, then assign new labels to them while + injecting them into the global namespace:: + + sage: B22 = FusionRing(("B2", 2)) + sage: b = [B22(x) for x in B22.get_order()]; b + [B22(0,0), B22(1,0), B22(0,1), B22(2,0), B22(1,1), B22(0,2)] + sage: [x.weight() for x in b] + [(0, 0), (1, 0), (1/2, 1/2), (2, 0), (3/2, 1/2), (1, 1)] + sage: B22.fusion_labels(['I0', 'Y1', 'X', 'Z', 'Xp', 'Y2'], inject_variables=True) + sage: b = [B22(x) for x in B22.get_order()]; b + [I0, Y1, X, Z, Xp, Y2] + sage: [(x, x.weight()) for x in b] + [(I0, (0, 0)), + (Y1, (1, 0)), + (X, (1/2, 1/2)), + (Z, (2, 0)), + (Xp, (3/2, 1/2)), + (Y2, (1, 1))] + sage: X * Y1 + X + Xp + sage: Z * Z + I0 + + A fixed order of the basis keys is available with :meth:`get_order`. + This is the order used by methods such as :meth:`s_matrix`. You may + use :meth:`CombinatorialFreeModule.set_order` to reorder the basis:: + + sage: B22.set_order([x.weight() for x in [I0, Y1, Y2, X, Xp, Z]]) + sage: [B22(x) for x in B22.get_order()] + [I0, Y1, Y2, X, Xp, Z] + + To reset the labels, you may run :meth:`fusion_labels` with no parameter:: + + sage: B22.fusion_labels() + sage: [B22(x) for x in B22.get_order()] + [B22(0,0), B22(1,0), B22(0,2), B22(0,1), B22(1,1), B22(2,0)] + + To reset the order to the default, simply set it to the list of basis + element keys:: + + sage: B22.set_order(B22.basis().keys().list()) + sage: [B22(x) for x in B22.get_order()] + [B22(0,0), B22(1,0), B22(0,1), B22(2,0), B22(1,1), B22(0,2)] + + The fusion ring has a number of methods that reflect its role + as the Grothendieck ring of a *modular tensor category* (MTC). These + include twist methods :meth:`Element.twist` and :meth:`Element.ribbon` + for its elements related to the ribbon structure, and the + S-matrix :meth:`s_ij`. + + There are two natural normalizations of the S-matrix. Both + are explained in Chapter 3 of [BaKi2001]_. The one that is computed + by the method :meth:`s_matrix`, or whose individual entries + are computed by :meth:`s_ij` is denoted `\tilde{s}` in + [BaKi2001]_. It is not unitary. + + The unitary S-matrix is `s=D^{-1/2}\tilde{s}` where + + .. MATH:: + + D = \sum_V d_i(V)^2. + + The sum is over all simple objects `V` with + `d_i(V)` the *quantum dimension*. We will call quantity `D` + the *global quantum dimension* and `\sqrt{D}` the + *total quantum order*. They are computed by :meth:`global_q_dimension` + and :meth:`total_q_order`. The unitary S-matrix `s` may be obtained + using :meth:`s_matrix` with the option ``unitary=True``. + + Let us check the Verlinde formula, which is [DFMS1996]_ (16.3). This + famous identity states that + + .. MATH:: + + N^k_{ij} = \sum_l \frac{s(i, \ell)\, s(j, \ell)\, \overline{s(k, \ell)}}{s(I, \ell)}, + + where `N^k_{ij}` are the fusion coefficients, i.e. the structure + constants of the fusion ring, and ``I`` is the unit object. + The S-matrix has the property that if `i*` denotes the dual + object of `i`, implemented in Sage as ``i.dual()``, then + + .. MATH:: + + s(i*, j) = s(i, j*) = \overline{s(i, j)}. + + This is equation (16.5) in [DFMS1996]_. Thus with `N_{ijk}=N^{k*}_{ij}` + the Verlinde formula is equivalent to + + .. MATH:: + + N_{ijk} = \sum_l \frac{s(i, \ell)\, s(j, \ell)\, s(k, \ell)}{s(I, \ell)}, + + In this formula `s` is the normalized unitary S-matrix + denoted `s` in [BaKi2001]_. We may define a function that + corresponds to the right-hand side, except using + `\tilde{s}` instead of `s`:: + + sage: def V(i, j, k): + ....: R = i.parent() + ....: return sum(R.s_ij(i, l) * R.s_ij(j, l) * R.s_ij(k, l) / R.s_ij(R.one(), l) + ....: for l in R.basis()) + + This does not produce ``self.N_ijk(i, j, k)`` exactly, because of the + missing normalization factor. The following code to check the + Verlinde formula takes this into account:: + + sage: def test_verlinde(R): + ....: b0 = R.one() + ....: c = R.global_q_dimension() + ....: return all(V(i, j, k) == c * R.N_ijk(i, j, k) for i in R.basis() + ....: for j in R.basis() for k in R.basis()) + + Every fusion ring should pass this test:: + + sage: test_verlinde(FusionRing(("A2", 1))) + True + sage: test_verlinde(FusionRing(("B4", 2))) # long time (.56s) + True + + As an exercise, the reader may verify the examples in + Section 5.3 of [RoStWa2009]_. Here we check the example + of the Ising modular tensor category, which is related + to the BPZ minimal model `M(4, 3)` or to an `E_8` coset + model. See [DFMS1996]_ Sections 7.4.2 and 18.4.1. + [RoStWa2009]_ Example 5.3.4 tells us how to + construct it as the conjugate of the `E_8` level 2 + :class:`FusionRing`:: + + sage: I = FusionRing(("E8", 2), conjugate=True) + sage: I.fusion_labels(["i0", "p", "s"], inject_variables=True) + sage: b = I.basis().list(); b + [i0, p, s] + sage: Matrix([[x*y for x in b] for y in b]) # long time (.93s) + [ i0 p s] + [ p i0 s] + [ s s i0 + p] + sage: [x.twist() for x in b] + [0, 1, 1/8] + sage: [x.ribbon() for x in b] + [1, -1, zeta128^8] + sage: [I.r_matrix(i, j, k) for (i, j, k) in [(s, s, i0), (p, p, i0), (p, s, s), (s, p, s), (s, s, p)]] + [-zeta128^56, -1, -zeta128^32, -zeta128^32, zeta128^24] + sage: I.r_matrix(s, s, i0) == I.root_of_unity(-1/8) + True + sage: I.global_q_dimension() + 4 + sage: I.total_q_order() + 2 + sage: [x.q_dimension()^2 for x in b] + [1, 1, 2] + sage: I.s_matrix() + [ 1 1 -zeta128^48 + zeta128^16] + [ 1 1 zeta128^48 - zeta128^16] + [-zeta128^48 + zeta128^16 zeta128^48 - zeta128^16 0] + sage: I.s_matrix().apply_map(lambda x:x^2) + [1 1 2] + [1 1 2] + [2 2 0] + + The term *modular tensor category* refers to the fact that associated + with the category there is a projective representation of the modular + group `SL(2, \ZZ)`. We recall that this group is generated by + + .. MATH:: + + S = \begin{pmatrix} & -1\\1\end{pmatrix}, \qquad + T = \begin{pmatrix} 1 & 1\\ &1 \end{pmatrix} + + subject to the relations `(ST)^3 = S^2`, `S^2T = TS^2`, and `S^4 = I`. + Let `s` be the normalized S-matrix, and + `t` the diagonal matrix whose entries are the twists of the simple + objects. Let `s` the unitary S-matrix and `t` the matrix of twists, + and `C` the conjugation matrix :meth:`conj_matrix`. Let + + .. MATH:: + + D_+ = \sum_i d_i^2 \theta_i, \qquad D_- = d_i^2 \theta_i^{-1}, + + where `d_i` and `\theta_i` are the quantum dimensions and twists of the + simple objects. Let `c` be the Virasoro central charge, a rational number + that is computed in :meth:`virasoro_central_charge`. It is known that + + .. MATH:: + + \sqrt{\frac{D_+}{D_-}} = e^{i\pi c/4}. + + It is proved in [BaKi2001]_ Equation (3.1.17) that + + .. MATH:: + + (st)^3 = e^{i\pi c/4} s^2, \qquad + s^2 = C, \qquad C^2 = 1, \qquad Ct = tC. + + Therefore `S \mapsto s, T \mapsto t` is a projective representation + of `SL(2, \ZZ)`. Let us confirm these identities for the Fibonacci MTC + ``FusionRing("G2", 1)``:: + + sage: R = FusionRing(("G2", 1)) + sage: S = R.s_matrix(unitary=True) + sage: T = R.twists_matrix() + sage: C = R.conj_matrix() + sage: c = R.virasoro_central_charge(); c + 14/5 + sage: (S*T)^3 == R.root_of_unity(c/4) * S^2 + True + sage: S^2 == C + True + sage: C*T == T*C + True + """ + def __init__(self, ct, k, conjugate=False, base_ring=ZZ, prefix=None, cyclotomic_order=None, fusion_labels=None, inject_variables=False): + self._WCR = WeylCharacterRing(ct, base_ring, prefix, + k=k, conjugate=conjugate, + cyclotomic_order=cyclotomic_order, + style="coroots") + names = dict(self._WCR.basis()) + super().__init__(names=names, base_ring=base_ring, + prefix=prefix, conjugate=conjugate, + cyclotomic_order=cyclotomic_order, + fusion_labels=fusion_labels, + inject_variables=inject_variables) + self._cyclotomic_order = self._WCR._cyclotomic_order + + def _repr_(self): + """ + EXAMPLES:: + + sage: FusionRing(("A1", 3)) + The Fusion Ring of Type A1 and level 3 with Integer Ring coefficients + """ + return self._WCR._repr_() + + def _test_verlinde(self, **options): + """ + Check the Verlinde formula for this :class:`FusionRing` instance. + + EXAMPLES:: + + sage: G22 = FusionRing(("G2", 2)) + sage: G22._test_verlinde() + """ + tester = self._tester(**options) + c = self.global_q_dimension() + i0 = self.one() + from sage.misc.misc import some_tuples + B = self.basis() + for x, y, z in some_tuples(B, 3, tester._max_runs): + v = sum(self.s_ij(x, w) * self.s_ij(y, w) * self.s_ij(z, w) / self.s_ij(i0, w) for w in B) + tester.assertEqual(v, c * self.N_ijk(x, y, z)) + + def _test_total_q_order(self, **options): + r""" + Check that the total quantum order is real and positive. + + The total quantum order is the positive square root + of the global quantum dimension. This indirectly test the + Virasoro central charge. + + EXAMPLES:: + + sage: G22 = FusionRing(("G2", 2)) + sage: G22._test_total_q_order() + """ + tester = self._tester(**options) + tqo = self.total_q_order(base_coercion=False) + tester.assertTrue(tqo.is_real_positive()) + tester.assertEqual(tqo**2, self.global_q_dimension(base_coercion=False)) + + def cartan_type(self): + """ + Return the Cartan type of ``self``. + + EXAMPLES:: + + sage: G22 = FusionRing(("G2", 2)) + sage: G22.cartan_type() + ['G', 2] + """ + return self._WCR._cartan_type + + def fundamental_weights(self): + """ + Return the fundamental weights of the :class:`WeylCharacterRing` + associated to ``self``. + + EXAMPLES:: + + sage: G22 = FusionRing(("G2", 2)) + sage: G22.fundamental_weights() + Finite family {1: (1, 0, -1), 2: (2, -1, -1)} + """ + return self._WCR.fundamental_weights() + + def fusion_level(self): + r""" + Return the level `k` of ``self``. + + EXAMPLES:: + + sage: B22 = FusionRing(('B2', 2)) + sage: B22.fusion_level() + 2 + """ + return self._WCR._k + + def fusion_l(self): + r""" + Return the product `\ell = m_g(k + h^\vee)`, where `m_g` denotes the + square of the ratio of the lengths of long to short roots of + the underlying Lie algebra, `k` denotes the level of the FusionRing, + and `h^\vee` denotes the dual Coxeter number of the underlying Lie + algebra. + + This value is used to define the associated root `2\ell`-th + of unity `q = e^{i\pi/\ell}`. + + EXAMPLES:: + + sage: B22 = FusionRing(('B2', 2)) + sage: B22.fusion_l() + 10 + sage: D52 = FusionRing(('D5', 2)) + sage: D52.fusion_l() + 10 + """ + return self._WCR._l + + @cached_method + def Nk_ij(self, elt_i, elt_j, elt_k): + r""" + Return the fusion coefficient `N^k_{ij}`. + + These are the structure coefficients of the fusion ring, so + + .. MATH:: + + i * j = \sum_{k} N_{ij}^k k. + + EXAMPLES:: + + sage: A22 = FusionRing(("A2", 2)) + sage: b = A22.basis().list() + sage: all(x*y == sum(A22.Nk_ij(x, y, k)*k for k in b) for x in b for y in b) + True + """ + mc = (self._WCR(elt_i.weight()) * self._WCR(elt_j.weight()))._monomial_coefficients + return mc.get(elt_k.weight(), 0) + + def one(self): + return self.basis()[self._WCR.one_basis()] + + @cached_method + def s_ij(self, elt_i, elt_j, unitary=False, base_coercion=True): + r""" + Return the element of the S-matrix of this fusion ring corresponding to + the given elements. + + This is computed using the formula + + .. MATH:: + + s_{i, j} = \frac{1}{\theta_i\theta_j} \sum_k N_{ik}^j d_k \theta_k, + + where `\theta_k` is the twist and `d_k` is the quantum + dimension. See [Row2006]_ Equation (2.2) or [EGNO2015]_ + Proposition 8.13.8. + + INPUT: + + - ``elt_i``, ``elt_j`` -- elements of the fusion basis + + EXAMPLES:: + + sage: G21 = FusionRing(("G2", 1)) + sage: b = G21.basis() + sage: [G21.s_ij(x, y) for x in b for y in b] + [1, -zeta60^14 + zeta60^6 + zeta60^4, -zeta60^14 + zeta60^6 + zeta60^4, -1] + """ + ijtwist = elt_i.twist() + elt_j.twist() + ret = sum(k.q_dimension(base_coercion=False) * self.Nk_ij(elt_i, k, elt_j) + * self.root_of_unity(k.twist() - ijtwist, base_coercion=False) + for k in self.basis()) + if unitary: + ret /= self.total_q_order(base_coercion=False) + if (not base_coercion) or (self._basecoer is None): + return ret + return self._basecoer(ret) + + def virasoro_central_charge(self): + r""" + Return the Virasoro central charge of the WZW conformal + field theory associated with the Fusion Ring. + + If `\mathfrak{g}` is the corresponding semisimple Lie algebra, this is + + .. MATH:: + + \frac{k\dim\mathfrak{g}}{k+h^\vee}, + + where `k` is the level and `h^\vee` is the dual Coxeter number. + See [DFMS1996]_ Equation (15.61). + + Let `d_i` and `\theta_i` be the quantum dimensions and + twists of the simple objects. By Proposition 2.3 in [RoStWa2009]_, + there exists a rational number `c` such that + `D_+ / \sqrt{D} = e^{i\pi c/4}`, where `D_+ = \sum d_i^2 \theta_i` + is computed in :meth:`D_plus` and `D = \sum d_i^2 > 0` is computed + by :meth:`global_q_dimension`. Squaring this identity and + remembering that `D_+ D_- = D` gives + + .. MATH:: + + D_+ / D_- = e^{i\pi c/2}. + + EXAMPLES:: + + sage: R = FusionRing(("A1", 2)) + sage: c = R.virasoro_central_charge(); c + 3/2 + sage: Dp = R.D_plus(); Dp + 2*zeta32^6 + sage: Dm = R.D_minus(); Dm + -2*zeta32^10 + sage: Dp / Dm == R.root_of_unity(c/2) + True + """ + dim_g = len(self._WCR.space().roots()) + self._WCR.cartan_type().rank() + return self._conj * self.fusion_level() * dim_g / (self.fusion_level() + self._WCR._h_check) + + class Element(FusionRing.Element): + + @cached_method + def q_dimension(self, base_coercion=True): + r""" + Return the quantum dimension as an element of the cyclotomic + field of the `2\ell`-th roots of unity, where `l = m (k+h^\vee)` + with `m=1, 2, 3` depending on whether type is simply, doubly or + triply laced, `k` is the level and `h^\vee` is the dual + Coxeter number. + """ + if not self.is_simple_object(): + raise ValueError("quantum dimension is only available for simple objects of a FusionRing") + P = self.parent()._WCR + lam = self.weight() + space = P.space() + rho = space.rho() + powers = {} + for alpha in space.positive_roots(): + val = alpha.inner_product(lam + rho) + if val in powers: + powers[val] += 1 + else: + powers[val] = 1 + val = alpha.inner_product(rho) + if val in powers: + powers[val] -= 1 + else: + powers[val] = -1 + R = ZZ['q'] + q = R.gen() + expr = R.fraction_field().one() + for val in powers: + exp = powers[val] + if exp > 0: + expr *= q_int(P._nf * val, q)**exp + elif exp < 0: + expr /= q_int(P._nf * val, q)**(-exp) + expr = R(expr) + expr = expr.substitute(q=q**4) / (q**(2*expr.degree())) + zet = self.parent().field().gen() ** (self.parent()._cyclotomic_order/P._l) + ret = expr.substitute(q=zet) + + if (not base_coercion) or (self.parent()._basecoer is None): + return ret + return self.parent()._basecoer(ret) + + @cached_method + def ribbon(self, base_coercion=True): + r""" + Return the twist or ribbon element of ``self``. + + If `h` is the rational number modulo 2 produced by + ``self.twist()``, this method produces `e^{i\pi h}`. + + .. SEEALSO:: + + An additive version of this is available as :meth:`twist`. + + EXAMPLES:: + + sage: F = FusionRing(("A1", 3)) + sage: [x.twist() for x in F.basis()] + [0, 3/10, 4/5, 3/2] + sage: [x.ribbon(base_coercion=False) for x in F.basis()] + [1, zeta40^6, zeta40^12 - zeta40^8 + zeta40^4 - 1, -zeta40^10] + sage: [F.root_of_unity(x, base_coercion=False) for x in [0, 3/10, 4/5, 3/2]] + [1, zeta40^6, zeta40^12 - zeta40^8 + zeta40^4 - 1, -zeta40^10] + """ + ret = self.parent().root_of_unity(self.twist(), base_coercion=False) + if (not base_coercion) or (self.parent()._basecoer is None): + return ret + return self.parent()._basecoer(ret) + + def twist(self, reduced=True): + r""" + Return a rational number `h` such that `\theta = e^{i \pi h}` + is the twist of ``self``. The quantity `e^{i \pi h}` is + also available using :meth:`ribbon`. + + This method is only available for simple objects. If + `\lambda` is the weight of the object, then + `h = \langle \lambda, \lambda+2\rho \rangle`, where + `\rho` is half the sum of the positive roots. + As in [Row2006]_, this requires normalizing + the invariant bilinear form so that + `\langle \alpha, \alpha \rangle = 2` for short roots. + + INPUT: + + - ``reduced`` -- (default: ``True``) boolean; if ``True`` + then return the twist reduced modulo 2 + """ + if not self.is_simple_object(): + raise ValueError("Quantum twist is only available for simple objects of a FusionRing") + P = self.parent()._WCR + rho = P.space().rho() + # We copy self.weight() to skip the test (which was already done + # by self.is_simple_object()). + lam = next(iter(self._monomial_coefficients)) + inner = lam.inner_product(lam + 2*rho) + twist = P._conj * P._nf * inner / self.parent().fusion_l() + # Reduce modulo 2 + if reduced: + f = twist.floor() + twist -= f + return twist + (f % 2) + return twist + + def weight(self): + r""" + Return the parametrizing dominant weight in the level `k` alcove. + + This method is only available for basis elements. + + EXAMPLES:: + + sage: A21 = FusionRing(("A2", 1)) + sage: [x.weight() for x in A21.basis().list()] + [(0, 0, 0), (2/3, -1/3, -1/3), (1/3, 1/3, -2/3)] + """ + if len(self._monomial_coefficients) != 1: + raise ValueError("Fusion weight is valid for basis elements only") + return next(iter(self._monomial_coefficients)) diff --git a/src/sage/algebras/fusion_rings/fusion_ring.py b/src/sage/algebras/fusion_rings/generic_fusion_ring.py similarity index 56% rename from src/sage/algebras/fusion_rings/fusion_ring.py rename to src/sage/algebras/fusion_rings/generic_fusion_ring.py index 6aee34112e8..f57ff024324 100644 --- a/src/sage/algebras/fusion_rings/fusion_ring.py +++ b/src/sage/algebras/fusion_rings/generic_fusion_ring.py @@ -2,11 +2,8 @@ Fusion Rings """ # **************************************************************************** -# Copyright (C) 2019 Daniel Bump -# Guillermo Aboumrad -# Travis Scrimshaw -# Nicolas Thiery -# 2022 Guillermo Aboumrad +# Copyright (C) 2023 Guillermo Aboumrad +# Daniel Bump # # Distributed under the terms of the GNU General Public License (GPL) # https://www.gnu.org/licenses/ @@ -19,418 +16,226 @@ executor, _unflatten_entries ) +from sage.categories.all import Algebras, AlgebrasWithBasis, Groups +from sage.combinat.free_module import CombinatorialFreeModule +from sage.combinat.root_system.cartan_type import CartanType from sage.combinat.root_system.weyl_characters import WeylCharacterRing from sage.matrix.constructor import matrix from sage.matrix.special import diagonal_matrix +from sage.misc.abstract_method import abstract_method from sage.misc.cachefunc import cached_method from sage.misc.misc import inject_variable from sage.rings.integer_ring import ZZ from sage.rings.number_field.number_field import CyclotomicField from sage.rings.qqbar import QQbar - -class FusionRing(WeylCharacterRing): - r""" - Return the Fusion Ring (Verlinde Algebra) of level ``k``. - - INPUT: - - - ``ct`` -- the Cartan type of a simple (finite-dimensional) Lie algebra - - ``k`` -- a nonnegative integer - - ``conjugate`` -- (default ``False``) set ``True`` to obtain - the complex conjugate ring - - ``cyclotomic_order`` -- (default computed depending on ``ct`` and ``k``) - - ``fusion_labels`` -- (default None) either a tuple of strings to use as labels of the - basis of simple objects, or a string from which the labels will be - constructed - - ``inject_variables`` -- (default ``False``): use with ``fusion_labels``. - If ``inject_variables`` is ``True``, the fusion labels will be variables - that can be accessed from the command line - - The cyclotomic order is an integer `N` such that all computations - will return elements of the cyclotomic field of `N`-th roots of unity. - Normally you will never need to change this but consider changing it - if :meth:`root_of_unity` raises a ``ValueError``. - - This algebra has a basis (sometimes called *primary fields* but here - called *simple objects*) indexed by the weights of level `\leq k`. - These arise as the fusion algebras of Wess-Zumino-Witten (WZW) conformal - field theories, or as Grothendieck groups of tilting modules for quantum - groups at roots of unity. The :class:`FusionRing` class is implemented as - a variant of the :class:`WeylCharacterRing`. - - REFERENCES: - - - [BaKi2001]_ Chapter 3 - - [DFMS1996]_ Chapter 16 - - [EGNO2015]_ Chapter 8 - - [Feingold2004]_ - - [Fuchs1994]_ - - [Row2006]_ - - [Walton1990]_ - - [Wan2010]_ - - EXAMPLES:: - - sage: A22 = FusionRing("A2", 2) - sage: [f1, f2] = A22.fundamental_weights() - sage: M = [A22(x) for x in [0*f1, 2*f1, 2*f2, f1+f2, f2, f1]] - sage: [M[3] * x for x in M] - [A22(1,1), - A22(0,1), - A22(1,0), - A22(0,0) + A22(1,1), - A22(0,1) + A22(2,0), - A22(1,0) + A22(0,2)] - - You may assign your own labels to the basis elements. In the next - example, we create the `SO(5)` fusion ring of level `2`, check the - weights of the basis elements, then assign new labels to them while - injecting them into the global namespace:: - - sage: B22 = FusionRing("B2", 2) - sage: b = [B22(x) for x in B22.get_order()]; b - [B22(0,0), B22(1,0), B22(0,1), B22(2,0), B22(1,1), B22(0,2)] - sage: [x.weight() for x in b] - [(0, 0), (1, 0), (1/2, 1/2), (2, 0), (3/2, 1/2), (1, 1)] - sage: B22.fusion_labels(['I0', 'Y1', 'X', 'Z', 'Xp', 'Y2'], inject_variables=True) - sage: b = [B22(x) for x in B22.get_order()]; b - [I0, Y1, X, Z, Xp, Y2] - sage: [(x, x.weight()) for x in b] - [(I0, (0, 0)), - (Y1, (1, 0)), - (X, (1/2, 1/2)), - (Z, (2, 0)), - (Xp, (3/2, 1/2)), - (Y2, (1, 1))] - sage: X * Y1 - X + Xp - sage: Z * Z - I0 - - A fixed order of the basis keys is available with :meth:`get_order`. - This is the order used by methods such as :meth:`s_matrix`. You may - use :meth:`CombinatorialFreeModule.set_order` to reorder the basis:: - - sage: B22.set_order([x.weight() for x in [I0, Y1, Y2, X, Xp, Z]]) - sage: [B22(x) for x in B22.get_order()] - [I0, Y1, Y2, X, Xp, Z] - - To reset the labels, you may run :meth:`fusion_labels` with no parameter:: - - sage: B22.fusion_labels() - sage: [B22(x) for x in B22.get_order()] - [B22(0,0), B22(1,0), B22(0,2), B22(0,1), B22(1,1), B22(2,0)] - - To reset the order to the default, simply set it to the list of basis - element keys:: - - sage: B22.set_order(B22.basis().keys().list()) - sage: [B22(x) for x in B22.get_order()] - [B22(0,0), B22(1,0), B22(0,1), B22(2,0), B22(1,1), B22(0,2)] - - The fusion ring has a number of methods that reflect its role - as the Grothendieck ring of a *modular tensor category* (MTC). These - include twist methods :meth:`Element.twist` and :meth:`Element.ribbon` - for its elements related to the ribbon structure, and the - S-matrix :meth:`s_ij`. - - There are two natural normalizations of the S-matrix. Both - are explained in Chapter 3 of [BaKi2001]_. The one that is computed - by the method :meth:`s_matrix`, or whose individual entries - are computed by :meth:`s_ij` is denoted `\tilde{s}` in - [BaKi2001]_. It is not unitary. - - The unitary S-matrix is `s=D^{-1/2}\tilde{s}` where - - .. MATH:: - - D = \sum_V d_i(V)^2. - - The sum is over all simple objects `V` with - `d_i(V)` the *quantum dimension*. We will call quantity `D` - the *global quantum dimension* and `\sqrt{D}` the - *total quantum order*. They are computed by :meth:`global_q_dimension` - and :meth:`total_q_order`. The unitary S-matrix `s` may be obtained - using :meth:`s_matrix` with the option ``unitary=True``. - - Let us check the Verlinde formula, which is [DFMS1996]_ (16.3). This - famous identity states that - - .. MATH:: - - N^k_{ij} = \sum_l \frac{s(i, \ell)\, s(j, \ell)\, \overline{s(k, \ell)}}{s(I, \ell)}, - - where `N^k_{ij}` are the fusion coefficients, i.e. the structure - constants of the fusion ring, and ``I`` is the unit object. - The S-matrix has the property that if `i*` denotes the dual - object of `i`, implemented in Sage as ``i.dual()``, then - - .. MATH:: - - s(i*, j) = s(i, j*) = \overline{s(i, j)}. - - This is equation (16.5) in [DFMS1996]_. Thus with `N_{ijk}=N^{k*}_{ij}` - the Verlinde formula is equivalent to - - .. MATH:: - - N_{ijk} = \sum_l \frac{s(i, \ell)\, s(j, \ell)\, s(k, \ell)}{s(I, \ell)}, - - In this formula `s` is the normalized unitary S-matrix - denoted `s` in [BaKi2001]_. We may define a function that - corresponds to the right-hand side, except using - `\tilde{s}` instead of `s`:: - - sage: def V(i, j, k): - ....: R = i.parent() - ....: return sum(R.s_ij(i, l) * R.s_ij(j, l) * R.s_ij(k, l) / R.s_ij(R.one(), l) - ....: for l in R.basis()) - - This does not produce ``self.N_ijk(i, j, k)`` exactly, because of the - missing normalization factor. The following code to check the - Verlinde formula takes this into account:: - - sage: def test_verlinde(R): - ....: b0 = R.one() - ....: c = R.global_q_dimension() - ....: return all(V(i, j, k) == c * R.N_ijk(i, j, k) for i in R.basis() - ....: for j in R.basis() for k in R.basis()) - - Every fusion ring should pass this test:: - - sage: test_verlinde(FusionRing("A2", 1)) - True - sage: test_verlinde(FusionRing("B4", 2)) # long time (.56s) - True - - As an exercise, the reader may verify the examples in - Section 5.3 of [RoStWa2009]_. Here we check the example - of the Ising modular tensor category, which is related - to the BPZ minimal model `M(4, 3)` or to an `E_8` coset - model. See [DFMS1996]_ Sections 7.4.2 and 18.4.1. - [RoStWa2009]_ Example 5.3.4 tells us how to - construct it as the conjugate of the `E_8` level 2 - :class:`FusionRing`:: - - sage: I = FusionRing("E8", 2, conjugate=True) - sage: I.fusion_labels(["i0", "p", "s"], inject_variables=True) - sage: b = I.basis().list(); b - [i0, p, s] - sage: Matrix([[x*y for x in b] for y in b]) # long time (.93s) - [ i0 p s] - [ p i0 s] - [ s s i0 + p] - sage: [x.twist() for x in b] - [0, 1, 1/8] - sage: [x.ribbon() for x in b] - [1, -1, zeta128^8] - sage: [I.r_matrix(i, j, k) for (i, j, k) in [(s, s, i0), (p, p, i0), (p, s, s), (s, p, s), (s, s, p)]] - [-zeta128^56, -1, -zeta128^32, -zeta128^32, zeta128^24] - sage: I.r_matrix(s, s, i0) == I.root_of_unity(-1/8) - True - sage: I.global_q_dimension() - 4 - sage: I.total_q_order() - 2 - sage: [x.q_dimension()^2 for x in b] - [1, 1, 2] - sage: I.s_matrix() - [ 1 1 -zeta128^48 + zeta128^16] - [ 1 1 zeta128^48 - zeta128^16] - [-zeta128^48 + zeta128^16 zeta128^48 - zeta128^16 0] - sage: I.s_matrix().apply_map(lambda x:x^2) - [1 1 2] - [1 1 2] - [2 2 0] - - The term *modular tensor category* refers to the fact that associated - with the category there is a projective representation of the modular - group `SL(2, \ZZ)`. We recall that this group is generated by - - .. MATH:: - - S = \begin{pmatrix} & -1\\1\end{pmatrix}, \qquad - T = \begin{pmatrix} 1 & 1\\ &1 \end{pmatrix} - - subject to the relations `(ST)^3 = S^2`, `S^2T = TS^2`, and `S^4 = I`. - Let `s` be the normalized S-matrix, and - `t` the diagonal matrix whose entries are the twists of the simple - objects. Let `s` the unitary S-matrix and `t` the matrix of twists, - and `C` the conjugation matrix :meth:`conj_matrix`. Let - - .. MATH:: - - D_+ = \sum_i d_i^2 \theta_i, \qquad D_- = d_i^2 \theta_i^{-1}, - - where `d_i` and `\theta_i` are the quantum dimensions and twists of the - simple objects. Let `c` be the Virasoro central charge, a rational number - that is computed in :meth:`virasoro_central_charge`. It is known that - - .. MATH:: - - \sqrt{\frac{D_+}{D_-}} = e^{i\pi c/4}. - - It is proved in [BaKi2001]_ Equation (3.1.17) that - - .. MATH:: - - (st)^3 = e^{i\pi c/4} s^2, \qquad - s^2 = C, \qquad C^2 = 1, \qquad Ct = tC. - - Therefore `S \mapsto s, T \mapsto t` is a projective representation - of `SL(2, \ZZ)`. Let us confirm these identities for the Fibonacci MTC - ``FusionRing("G2", 1)``:: - - sage: R = FusionRing("G2", 1) - sage: S = R.s_matrix(unitary=True) - sage: T = R.twists_matrix() - sage: C = R.conj_matrix() - sage: c = R.virasoro_central_charge(); c - 14/5 - sage: (S*T)^3 == R.root_of_unity(c/4) * S^2 - True - sage: S^2 == C - True - sage: C*T == T*C - True - """ +class FusionRing(CombinatorialFreeModule): @staticmethod - def __classcall__(cls, ct, k, base_ring=ZZ, prefix=None, style="coroots", conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False): + def __classcall_private__(cls, input_data, base_ring=ZZ, prefix=None, conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False, **kwds): + """ + Select the correct parent depending on the given input. + """ + if input_data in Groups: + if prefix is None: + prefix = "s" + from sage.algebras.fusion_rings.fusion_double import FusionRingFromQuantumDouble + return FusionRingFromQuantumDouble(input_data, base_ring=base_ring, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) + elif isinstance(input_data, tuple): + try: + ct = CartanType(input_data[:-1]) + except ValueError: + raise ValueError("Input data cannot be parsed as a valid CartanType") + from sage.algebras.fusion_rings.fusion_ring_from_wcr import FusionRingFromWCR + return FusionRingFromWCR(ct, input_data[-1], conjugate=conjugate, base_ring=base_ring, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) + + def __init__(self, names, base_ring=ZZ, prefix=None, conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False): + self._names = names + self._rank = len(names) + self._conj = -1 if conjugate else 1 + self._cyclotomic_order = cyclotomic_order + self._field = None + self._basecoer = None + cat = AlgebrasWithBasis(base_ring).Subobjects() + CombinatorialFreeModule.__init__(self, base_ring, [k for k in self._names], category=cat) + self._fusion_labels = fusion_labels + self.fusion_labels(labels=fusion_labels, inject_variables=inject_variables) + + @abstract_method + def _repr_(self): + pass + + @abstract_method + def Nk_ij(self, i, j, k): + r""" + Returns the fusion coefficient `N^k_{ij}` """ - Normalize input to ensure a unique representation. + pass - TESTS:: + @abstract_method + def one(self): + """ + Get the multiplicative identity of ``self``. + """ + pass - sage: F1 = FusionRing('B3', 2) - sage: F2 = FusionRing(CartanType('B3'), QQ(2), ZZ) - sage: F3 = FusionRing(CartanType('B3'), int(2), style="coroots") - sage: F1 is F2 and F2 is F3 - True + @abstract_method + def s_ij(self, elt_i, elt_j, unitary=False, base_coercion=True): + """ + Get the `(i, j)`-entry of the S-matrix of ``self``. + """ + pass + + def _element_constructor(self, k): + """ + Construct a monomial (basis element) from a key. - sage: A23 = FusionRing('A2', 3) - sage: TestSuite(A23).run() + INPUT: - sage: B22 = FusionRing('B2', 2) - sage: TestSuite(B22).run() + - ``key`` -- a key for the dictionary `self._names` - sage: C31 = FusionRing('C3', 1) - sage: TestSuite(C31).run() + EXAMPLES:: - sage: D41 = FusionRing('D4', 1) - sage: TestSuite(D41).run() + sage: F = FusionRing(SymmetricGroup(3), prefix="n") + sage: F._names + {0: 'n0', 1: 'n1', 2: 'n2', 3: 'n3', 4: 'n4', 5: 'n5', 6: 'n6', 7: 'n7'} + sage: [F._element_constructor(x) for x in F._names] + [n0, n1, n2, n3, n4, n5, n6, n7] + """ + return self.monomial(k) - sage: G22 = FusionRing('G2', 2) - sage: TestSuite(G22).run() + def _repr_term(self, t): + """ + EXAMPLES:: - sage: F41 = FusionRing('F4', 1) - sage: TestSuite(F41).run() + sage: F = FusionRing(CyclicPermutationGroup(2)) + sage: [F._repr_term(t) for t in F._names] + ['s0', 's1', 's2', 's3'] + """ + if self._fusion_labels is not None: + idx = self.get_order().index(t) + return self._fusion_labels[idx] + return self._names[t] - sage: E61 = FusionRing('E6', 1) - sage: TestSuite(E61).run() + def conj_matrix(self): + r""" + Return the conjugation matrix, which is the permutation matrix + for the conjugation (dual) operation on basis elements. - sage: E71 = FusionRing('E7', 1) - sage: TestSuite(E71).run() + EXAMPLES:: - sage: E81 = FusionRing('E8', 1) - sage: TestSuite(E81).run() - """ - return super().__classcall__(cls, ct, base_ring=base_ring, - prefix=prefix, style=style, k=k, - conjugate=conjugate, - cyclotomic_order=cyclotomic_order, - fusion_labels=fusion_labels, - inject_variables=inject_variables) - - def _test_verlinde(self, **options): + sage: FusionRing(("A2", 1)).conj_matrix() + [1 0 0] + [0 0 1] + [0 1 0] """ - Check the Verlinde formula for this :class:`FusionRing` instance. + b = self.basis().list() + return matrix(ZZ, [[i == j.dual() for i in b] for j in b]) + + def D_plus(self, base_coercion=True): + r""" + Return `\sum d_i^2\theta_i` where `i` runs through the simple objects, + `d_i` is the quantum dimension and `\theta_i` is the twist. + + This is denoted `p_+` in [BaKi2001]_ Chapter 3. + + EXAMPLES:: + + sage: B31 = FusionRing(("B3", 1)) + sage: Dp = B31.D_plus(); Dp + 2*zeta48^13 - 2*zeta48^5 + sage: Dm = B31.D_minus(); Dm + -2*zeta48^3 + sage: Dp*Dm == B31.global_q_dimension() + True + sage: c = B31.virasoro_central_charge(); c + 7/2 + sage: Dp/Dm == B31.root_of_unity(c/2) + True + + For the Drinfeld double, it equals the order of the group. EXAMPLES:: - sage: G22 = FusionRing("G2", 2) - sage: G22._test_verlinde() + sage: FusionRing(DihedralGroup(8)).D_plus() + 16 """ - tester = self._tester(**options) - c = self.global_q_dimension() - i0 = self.one() - from sage.misc.misc import some_tuples - B = self.basis() - for x, y, z in some_tuples(B, 3, tester._max_runs): - v = sum(self.s_ij(x, w) * self.s_ij(y, w) * self.s_ij(z, w) / self.s_ij(i0, w) for w in B) - tester.assertEqual(v, c * self.N_ijk(x, y, z)) + ret = sum((x.q_dimension(base_coercion=False))**2 * x.ribbon(base_coercion=False) for x in self.basis()) + if (not base_coercion) or (self._basecoer is None): + return ret + return self._basecoer(ret) - def _test_total_q_order(self, **options): + def D_minus(self, base_coercion=True): r""" - Check that the total quantum order is real and positive. + Return `\sum d_i^2\theta_i^{-1}` where `i` runs through the simple + objects, `d_i` is the quantum dimension and `\theta_i` is the twist. - The total quantum order is the positive square root - of the global quantum dimension. This indirectly test the - Virasoro central charge. + This is denoted `p_-` in [BaKi2001]_ Chapter 3. EXAMPLES:: - sage: G22 = FusionRing("G2", 2) - sage: G22._test_total_q_order() - """ - tester = self._tester(**options) - tqo = self.total_q_order(base_coercion=False) - tester.assertTrue(tqo.is_real_positive()) - tester.assertEqual(tqo**2, self.global_q_dimension(base_coercion=False)) + sage: E83 = FusionRing(("E8", 3), conjugate=True) + sage: [Dp, Dm] = [E83.D_plus(), E83.D_minus()] + sage: Dp*Dm == E83.global_q_dimension() + True + sage: c = E83.virasoro_central_charge(); c + -248/11 + sage: Dp*Dm == E83.global_q_dimension() + True - def test_braid_representation(self, max_strands=6, anyon=None): - """ - Check that we can compute valid braid group representations. + For the Drinfeld double, it equals the order of the group. - INPUT: + EXAMPLES:: - - ``max_strands`` -- (default: 6): maximum number of braid group strands - - ``anyon`` -- (optional) run this test on this particular simple object + sage: FusionRing(DihedralGroup(9)).D_minus() + 18 + """ + ret = sum((x.q_dimension(base_coercion=False))**2 / x.ribbon(base_coercion=False) for x in self.basis()) + if (not base_coercion) or (self._basecoer is None): + return ret + return self._basecoer(ret) - Create a braid group representation using :meth:`get_braid_generators` - and confirms the braid relations. This test indirectly partially - verifies the correctness of the orthogonal F-matrix solver. If the - code were incorrect the method would not be deterministic because the - fusing anyon is chosen randomly. (A different choice is made for each - number of strands tested.) However the doctest is deterministic since - it will always return ``True``. If the anyon parameter is omitted, - a random anyon is tested for each number of strands up to ``max_strands``. + @cached_method + def dual(self, i): + r""" + Return the dual object ``i^\ast`` to ``i``. The dual is also + available as an element method of ``i``. EXAMPLES:: - sage: A21 = FusionRing("A2", 1) - sage: A21.test_braid_representation(max_strands=4) - True - sage: F41 = FusionRing("F4", 1) # long time - sage: F41.test_braid_representation() # long time + sage: K = FusionRing(CyclicPermutationGroup(3),prefix="k") + sage: [(x,K.dual(x)) for x in K.basis()] + [(k0, k0), + (k1, k2), + (k2, k1), + (k3, k6), + (k4, k8), + (k5, k7), + (k6, k3), + (k7, k5), + (k8, k4)] + sage: all(K.dual(x)==x.dual() for x in K.basis()) True """ - if not self.is_multiplicity_free(): # Braid group representation is not available if self is not multiplicity free - raise NotImplementedError("only implemented for multiplicity free fusion rings") - b = self.basis() - results = [] - # Test with different numbers of strands - for n_strands in range(3, max_strands+1): - # Randomly select a fusing anyon. Skip the identity element, since - # its braiding matrices are trivial - if anyon is not None: - a = anyon - else: - while True: - a = b.random_element() - if a != self.one(): - break - pow = a ** n_strands - d = pow.monomials()[0] - # Try to find 'interesting' braid group reps i.e. skip 1-d reps - for k, v in pow.monomial_coefficients().items(): - if v > 1: - d = self(k) - break - comp_basis, sig = self.get_braid_generators(a, d, n_strands, verbose=False) - results.append(len(comp_basis) > 0) - results.append(self.gens_satisfy_braid_gp_rels(sig)) - return all(results) + sz = self.one() + for j in self.basis(): + if self.Nk_ij(i,j,sz) > 0: + return j + + @cached_method + def field(self): + """ + Return a cyclotomic field large enough to contain the S- and R-matrices. + + EXAMPLES:: + + sage: FusionRing(("A2", 2)).field() + Cyclotomic Field of order 60 and degree 16 + sage: FusionRing(("B2", 2)).field() + Cyclotomic Field of order 40 and degree 16 + sage: FusionRing(SymmetricGroup(3)).field() + Cyclotomic Field of order 24 and degree 8 + """ + return CyclotomicField(4 * self._cyclotomic_order) def fusion_labels(self, labels=None, inject_variables=False): r""" @@ -454,7 +259,7 @@ def fusion_labels(self, labels=None, inject_variables=False): EXAMPLES:: - sage: A13 = FusionRing("A1", 3) + sage: A13 = FusionRing(("A1", 3)) sage: A13.fusion_labels("x") sage: fb = list(A13.basis()); fb [x0, x1, x2, x3] @@ -481,44 +286,21 @@ def fusion_labels(self, labels=None, inject_variables=False): sage: y0 A13(0) """ + # Maintain a tuple of names sorted according to (ordered) CFM basis if labels is None: - # Remove the fusion labels - self._fusion_labels = None - return + # Reset the fusion labels + labels = tuple(str(self._names[b]) for b in self.get_order()) B = self.basis() if isinstance(labels, str): - labels = [labels + str(k) for k in range(len(B))] + labels = tuple(labels + str(k) for k in range(len(B))) elif len(labels) != len(B): raise ValueError('invalid data') - d = {} - ac = self.simple_coroots() for j, b in enumerate(self.get_order()): - t = tuple([b.inner_product(x) for x in ac]) - d[t] = labels[j] if inject_variables: inject_variable(labels[j], B[b]) - self._fusion_labels = d - - @cached_method - def field(self): - r""" - Return a cyclotomic field large enough to - contain the `2 \ell`-th roots of unity, as well as - all the S-matrix entries. - - EXAMPLES:: - - sage: FusionRing("A2", 2).field() - Cyclotomic Field of order 60 and degree 16 - sage: FusionRing("B2", 2).field() - Cyclotomic Field of order 40 and degree 16 - """ - # if self._field is None: - # self._field = CyclotomicField(4 * self._cyclotomic_order) - # return self._field - return CyclotomicField(4 * self._cyclotomic_order) + self._fusion_labels = labels def fvars_field(self): r""" @@ -551,7 +333,7 @@ def fvars_field(self): EXAMPLES:: - sage: A13 = FusionRing("A1", 3, fusion_labels="a", inject_variables=True) + sage: A13 = FusionRing(("A1", 3), fusion_labels="a", inject_variables=True) sage: A13.fvars_field() Cyclotomic Field of order 40 and degree 16 sage: A13.field() @@ -559,9 +341,9 @@ def fvars_field(self): sage: a2**4 2*a0 + 3*a2 sage: comp_basis, sig = A13.get_braid_generators(a2, a2, 3, verbose=False) # long time (<3s) - sage: A13.fvars_field() # long time + sage: A13.fvars_field() # long time Number Field in a with defining polynomial y^32 - ... - 500*y^2 + 25 - sage: a2.q_dimension().parent() # long time + sage: a2.q_dimension().parent() # long time Number Field in a with defining polynomial y^32 - ... - 500*y^2 + 25 sage: A13.field() Cyclotomic Field of order 40 and degree 16 @@ -575,356 +357,438 @@ def fvars_field(self): return self.get_fmatrix().field() raise NotImplementedError("method is only available for multiplicity free fusion rings") - def root_of_unity(self, r, base_coercion=True): + def gens_satisfy_braid_gp_rels(self, sig): r""" - Return `e^{i\pi r}` as an element of ``self.field()`` if possible. - - INPUT: + Return ``True`` if the matrices in the list ``sig`` satisfy + the braid relations. - - ``r`` -- a rational number + This if `n` is the cardinality of ``sig``, this + confirms that these matrices define a representation of + the Artin braid group on `n+1` strands. Tests correctness of + :meth:`get_braid_generators`. EXAMPLES:: - sage: A11 = FusionRing("A1", 1) - sage: A11.field() - Cyclotomic Field of order 24 and degree 8 - sage: for n in [1..7]: - ....: try: - ....: print(n, A11.root_of_unity(2/n)) - ....: except ValueError as err: - ....: print(n, err) - 1 1 - 2 -1 - 3 zeta24^4 - 1 - 4 zeta24^6 - 5 not a root of unity in the field - 6 zeta24^4 - 7 not a root of unity in the field + sage: F41 = FusionRing(("F4", 1), fusion_labels="f", inject_variables=True) + sage: f1*f1 + f0 + f1 + sage: comp, sig = F41.get_braid_generators(f1, f0, 4, verbose=False) + sage: F41.gens_satisfy_braid_gp_rels(sig) + True """ - n = 2 * r * self._cyclotomic_order - if n not in ZZ: - raise ValueError("not a root of unity in the field") - ret = self.field().gen() ** n - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) + n = len(sig) + braid_rels = all(sig[i] * sig[i+1] * sig[i] == sig[i+1] * sig[i] * sig[i+1] for i in range(n-1)) + far_comm = all(sig[i] * sig[j] == sig[j] * sig[i] for i, j in product(range(n), repeat=2) if abs(i-j) > 1 and i > j) + singular = any(s.is_singular() for s in sig) + return braid_rels and far_comm and not singular - def get_order(self): + def get_braid_generators(self, + fusing_anyon, + total_charge_anyon, + n_strands, + checkpoint=False, + save_results="", + warm_start="", + use_mp=True, + verbose=True): r""" - Return the weights of the basis vectors in a fixed order. + Compute generators of the Artin braid group on ``n_strands`` strands. - You may change the order of the basis using :meth:`CombinatorialFreeModule.set_order` + If `a = ` ``fusing_anyon`` and `b = ` ``total_charge_anyon`` + the generators are endomorphisms of `\text{Hom}(b, a^n)`. - EXAMPLES:: + INPUT: - sage: A15 = FusionRing("A1", 5) - sage: w = A15.get_order(); w - [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2), (5/2, -5/2)] - sage: A15.set_order([w[k] for k in [0, 4, 1, 3, 5, 2]]) - sage: [A15(x) for x in A15.get_order()] - [A15(0), A15(4), A15(1), A15(3), A15(5), A15(2)] + - ``fusing_anyon`` -- a basis element of ``self`` + - ``total_charge_anyon`` -- a basis element of ``self`` + - ``n_strands`` -- a positive integer greater than 2 + - ``checkpoint`` -- (default: ``False``) a boolean indicating + whether the F-matrix solver should pickle checkpoints + - ``save_results`` -- (optional) a string indicating the name of + a file in which to pickle computed F-symbols for later use + - ``warm_start`` -- (optional) a string indicating the name of a + pickled checkpoint file to "warm" start the F-matrix solver. + The pickle may be a checkpoint generated by the solver, or + a file containing solver results. If all F-symbols are known, + we don't run the solver again. + - ``use_mp`` -- (default: ``True``) a boolean indicating whether + to use multiprocessing to speed up the computation; this is + highly recommended. Python 3.8+ is required. + - ``verbose`` -- (default: ``True``) boolean indicating whether + to be verbose with the computation - .. WARNING:: + For more information on the optional parameters, see + :meth:`FMatrix.find_orthogonal_solution`. - This duplicates :meth:`get_order` from - :class:`CombinatorialFreeModule` except the result - is *not* cached. Caching of - :meth:`CombinatorialFreeModule.get_order` causes inconsistent - results after calling :meth:`CombinatorialFreeModule.set_order`. + Given a simple object in the fusion category, here called + ``fusing_anyon`` allowing the universal R-matrix to act on adjacent + pairs in the fusion of ``n_strands`` copies of ``fusing_anyon`` + produces an action of the braid group. This representation can + be decomposed over another anyon, here called ``total_charge_anyon``. + See [CHW2015]_. - """ - if self._order is None: - self.set_order(self.basis().keys().list()) - return self._order + OUTPUT: - def some_elements(self): - """ - Return some elements of ``self``. + The method outputs a pair of data ``(comp_basis, sig)`` where + ``comp_basis`` is a list of basis elements of the braid group + module, parametrized by a list of fusion ring elements describing + a fusion tree. For example with 5 strands the fusion tree + is as follows. See :meth:`get_computational_basis` + for more information. - EXAMPLES:: + .. IMAGE:: ../../../media/fusiontree.png + :scale: 45 + :align: center - sage: D41 = FusionRing('D4', 1) - sage: D41.some_elements() - [D41(1,0,0,0), D41(0,0,1,0), D41(0,0,0,1)] - """ - return [self.monomial(x) for x in self.fundamental_weights() - if self.level(x) <= self._k] + ``sig`` is a list of braid group generators as matrices. In + some cases these will be represented as sparse matrices. - def fusion_level(self): - r""" - Return the level `k` of ``self``. + In the following example we compute a 5-dimensional braid group + representation on 5 strands associated to the spin representation + in the modular tensor category `SU(2)_4 \cong SO(3)_2`. EXAMPLES:: - sage: B22 = FusionRing('B2', 2) - sage: B22.fusion_level() - 2 + sage: A14 = FusionRing(("A1", 4)) + sage: A14.get_order() + [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2)] + sage: A14.fusion_labels(["one", "two", "three", "four", "five"], inject_variables=True) + sage: [A14(x) for x in A14.get_order()] + [one, two, three, four, five] + sage: two ** 5 + 5*two + 4*four + sage: comp_basis, sig = A14.get_braid_generators(two, two, 5, verbose=False) # long time + sage: A14.gens_satisfy_braid_gp_rels(sig) # long time + True + sage: len(comp_basis) == 5 # long time + True """ - return self._k + if n_strands < 3: + raise ValueError("the number of strands must be an integer at least 3") + # Construct associated FMatrix object and solve for F-symbols + self.get_fmatrix() + if self.fmats._chkpt_status < 7: + self.fmats.find_orthogonal_solution(checkpoint=checkpoint, + save_results=save_results, + warm_start=warm_start, + use_mp=use_mp, + verbose=verbose) + + # Set multiprocessing parameters. Context can only be set once, so we try to set it + try: + set_start_method('fork') + except RuntimeError: + pass + # Turn off multiprocessing when field is QQbar due to pickling issues introduced by PARI upgrade in trac ticket #30537 + pool = Pool() if use_mp and self.fvars_field() != QQbar else None + + # Set up computational basis and compute generators one at a time + a, b = fusing_anyon, total_charge_anyon + comp_basis = self.get_computational_basis(a, b, n_strands) + d = len(comp_basis) + if verbose: + print("Computing an {}-dimensional representation of the Artin braid group on {} strands...".format(d, n_strands)) + + # Compute diagonal odd-indexed generators using the 3j-symbols + gens = {2*i+1: diagonal_matrix(self.r_matrix(a, a, c[i]) for c in comp_basis) for i in range(n_strands//2)} + + # Compute even-indexed generators using F-matrices + for k in range(1, n_strands//2): + entries = self._emap('sig_2k', (k, a, b, n_strands), pool) + + # Build cyclotomic field element objects from tuple of rationals repn + _unflatten_entries(self, entries) + gens[2*k] = matrix(dict(entries)) + + # If n_strands is odd, we compute the final generator + if n_strands % 2: + entries = self._emap('odd_one_out', (a, b, n_strands), pool) + + # Build cyclotomic field element objects from tuple of rationals repn + _unflatten_entries(self, entries) + gens[n_strands-1] = matrix(dict(entries)) + + return comp_basis, [gens[k] for k in sorted(gens)] - def fusion_l(self): + def _emap(self, mapper, input_args, worker_pool=None): r""" - Return the product `\ell = m_g(k + h^\vee)`, where `m_g` denotes the - square of the ratio of the lengths of long to short roots of - the underlying Lie algebra, `k` denotes the level of the FusionRing, - and `h^\vee` denotes the dual Coxeter number of the underlying Lie - algebra. + Apply the given mapper to each element of the given input iterable + and return the results (with no duplicates) in a list. - This value is used to define the associated root `2\ell`-th - of unity `q = e^{i\pi/\ell}`. + INPUT: + + - ``mapper`` -- a string specifying the name of a function defined + in the ``fast_parallel_fusion_ring_braid_repn`` module + - ``input_args`` -- a tuple of arguments to be passed to mapper + + This method applies the mapper in parallel if a ``worker_pool`` + is provided. + + .. NOTE:: + + If ``worker_pool`` is not provided, function maps and reduces on + a single process. If ``worker_pool`` is provided, the function + attempts to determine whether it should use multiprocessing + based on the length of the input iterable. If it cannot determine + the length of the input iterable then it uses multiprocessing + with the default chunksize of `1` if chunksize is not + explicitly provided. EXAMPLES:: - sage: B22 = FusionRing('B2', 2) - sage: B22.fusion_l() - 10 - sage: D52 = FusionRing('D5', 2) - sage: D52.fusion_l() - 10 + sage: FR = FusionRing(("A1", 4)) + sage: FR.fusion_labels(['idd', 'one', 'two', 'three', 'four'], inject_variables=True) + sage: fmats = FR.get_fmatrix() + sage: fmats.find_orthogonal_solution(verbose=False) # long time + sage: len(FR._emap('sig_2k', (1, one, one, 5))) # long time + 13 + sage: FR = FusionRing(("A1", 2)) + sage: FR.fusion_labels("a", inject_variables=True) + sage: fmats = FR.get_fmatrix() + sage: fmats.find_orthogonal_solution(verbose=False) + sage: len(FR._emap('odd_one_out', (a1, a1, 7))) + 16 """ - return self._l + n_proc = worker_pool._processes if worker_pool is not None else 1 + input_iter = [(child_id, n_proc, input_args) for child_id in range(n_proc)] + no_mp = worker_pool is None + # Map phase + input_iter = zip_longest([], input_iter, fillvalue=(mapper, id(self))) + results = list() + if no_mp: + mapped = map(executor, input_iter) + else: + mapped = worker_pool.imap_unordered(executor, input_iter, chunksize=1) + # Reduce phase + for worker_results in mapped: + results.extend(worker_results) + return results - def virasoro_central_charge(self): + def get_computational_basis(self, a, b, n_strands): r""" - Return the Virasoro central charge of the WZW conformal - field theory associated with the Fusion Ring. + Return the so-called computational basis for `\text{Hom}(b, a^n)`. - If `\mathfrak{g}` is the corresponding semisimple Lie algebra, this is + INPUT: - .. MATH:: + - ``a`` -- a basis element + - ``b`` -- another basis element + - ``n_strands`` -- the number of strands for a braid group + + Let `n=` ``n_strands`` and let `k` be the greatest integer `\leq n/2`. + The braid group acts on `\text{Hom}(b, a^n)`. This action + is computed in :meth:`get_braid_generators`. This method + returns the computational basis in the form of a list of + fusion trees. Each tree is represented by an `(n-2)`-tuple - \frac{k\dim\mathfrak{g}}{k+h^\vee}, + .. MATH:: - where `k` is the level and `h^\vee` is the dual Coxeter number. - See [DFMS1996]_ Equation (15.61). + (m_1, \ldots, m_k, l_1, \ldots, l_{k-2}) - Let `d_i` and `\theta_i` be the quantum dimensions and - twists of the simple objects. By Proposition 2.3 in [RoStWa2009]_, - there exists a rational number `c` such that - `D_+ / \sqrt{D} = e^{i\pi c/4}`, where `D_+ = \sum d_i^2 \theta_i` - is computed in :meth:`D_plus` and `D = \sum d_i^2 > 0` is computed - by :meth:`global_q_dimension`. Squaring this identity and - remembering that `D_+ D_- = D` gives + such that each `m_j` is an irreducible constituent in `a \otimes a` + and .. MATH:: - D_+ / D_- = e^{i\pi c/2}. + \begin{array}{l} + b \in l_{k-2} \otimes m_{k}, \\ + l_{k-2} \in l_{k-3} \otimes m_{k-1}, \\ + \cdots, \\ + l_2 \in l_1 \otimes m_3, \\ + l_1 \in m_1 \otimes m_2, + \end{array} + + where `z \in x \otimes y` means `N_{xy}^z \neq 0`. + + As a computational device when ``n_strands`` is odd, we pad the + vector `(m_1, \ldots, m_k)` with an additional `m_{k+1}` equal to `a`. + However, this `m_{k+1}` does *not* appear in the output of this method. + + The following example appears in Section 3.1 of [CW2015]_. EXAMPLES:: - sage: R = FusionRing("A1", 2) - sage: c = R.virasoro_central_charge(); c - 3/2 - sage: Dp = R.D_plus(); Dp - 2*zeta32^6 - sage: Dm = R.D_minus(); Dm - -2*zeta32^10 - sage: Dp / Dm == R.root_of_unity(c/2) - True + sage: A14 = FusionRing(("A1", 4)) + sage: A14.get_order() + [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2)] + sage: A14.fusion_labels(["zero", "one", "two", "three", "four"], inject_variables=True) + sage: [A14(x) for x in A14.get_order()] + [zero, one, two, three, four] + sage: A14.get_computational_basis(one, two, 4) + [(two, two), (two, zero), (zero, two)] """ - dim_g = len(self.space().roots()) + self.cartan_type().rank() - return self._conj * self._k * dim_g / (self._k + self._h_check) + def _get_trees(fr, top_row, root): + if len(top_row) == 2: + m1, m2 = top_row + return [[]] if fr.Nk_ij(m1, m2, root) else [] + else: + m1, m2 = top_row[:2] + return [tuple([l, *b]) for l in fr.basis() for b in _get_trees(fr, [l]+top_row[2:], root) if fr.Nk_ij(m1, m2, l)] - def conj_matrix(self): + comp_basis = list() + for top in product((a*a).monomials(), repeat=n_strands//2): + # If the n_strands is odd, we must extend the top row by a fusing anyon + top_row = list(top)+[a]*(n_strands%2) + comp_basis.extend(tuple([*top, *levels]) for levels in _get_trees(self, top_row, b)) + return comp_basis + + def get_fmatrix(self, *args, **kwargs): r""" - Return the conjugation matrix, which is the permutation matrix - for the conjugation (dual) operation on basis elements. + Construct an :class:`FMatrix` factory to solve the pentagon relations + and organize the resulting F-symbols. + + We only need this attribute to compute braid group representations. EXAMPLES:: - sage: FusionRing("A2", 1).conj_matrix() - [1 0 0] - [0 0 1] - [0 1 0] + sage: A15 = FusionRing(("A1", 5)) + sage: A15.get_fmatrix() + F-Matrix factory for The Fusion Ring of Type A1 and level 5 with Integer Ring coefficients + + EXAMPLES:: + + sage: f = FusionRing(SymmetricGroup(3)).get_fmatrix(); f + F-Matrix factory for The Fusion Ring of the Drinfeld Double of Symmetric group of order 3! as a permutation group """ - b = self.basis().list() - return matrix(ZZ, [[i == j.dual() for i in b] for j in b]) + # Initialize fresh FMatrix object. Useful if you need to reset + # FMatrix properties and there are various FusionRing objects (unique) + # associated to same level and algebra. + if not hasattr(self, 'fmats') or kwargs.get('new', False): + kwargs.pop('new', None) + from sage.algebras.fusion_rings.f_matrix import FMatrix + self.fmats = FMatrix(self, *args, **kwargs) + return self.fmats - def twists_matrix(self): + def get_order(self): r""" - Return a diagonal matrix describing the twist corresponding to - each simple object in the ``FusionRing``. + Return the weights of the basis vectors in a fixed order. + + You may change the order of the basis using :meth:`CombinatorialFreeModule.set_order` EXAMPLES:: - sage: B21=FusionRing("B2", 1) - sage: [x.twist() for x in B21.basis().list()] - [0, 1, 5/8] - sage: [B21.root_of_unity(x.twist()) for x in B21.basis().list()] - [1, -1, zeta32^10] - sage: B21.twists_matrix() - [ 1 0 0] - [ 0 -1 0] - [ 0 0 zeta32^10] + sage: A15 = FusionRing(("A1", 5)) + sage: w = A15.get_order(); w + [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2), (5/2, -5/2)] + sage: A15.set_order([w[k] for k in [0, 4, 1, 3, 5, 2]]) + sage: [A15(x) for x in A15.get_order()] + [A15(0), A15(4), A15(1), A15(3), A15(5), A15(2)] + sage: FusionRing(SymmetricGroup(4)).get_order() + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + + .. WARNING:: + + This duplicates :meth:`get_order` from + :class:`CombinatorialFreeModule` except the result + is *not* cached. Caching of + :meth:`CombinatorialFreeModule.get_order` causes inconsistent + results after calling :meth:`CombinatorialFreeModule.set_order`. + """ - B = self.basis() - return diagonal_matrix(B[x].ribbon() for x in self.get_order()) + if self._order is None: + self.set_order(self.basis().keys().list()) + return self._order - @cached_method - def N_ijk(self, elt_i, elt_j, elt_k): + def global_q_dimension(self, base_coercion=True): r""" - Return the symmetric fusion coefficient `N_{ijk}`. + Return `\sum d_i^2`, where the sum is over all simple objects + and `d_i` is the quantum dimension. - INPUT: + The global `q`-dimension is a positive real number. + + EXAMPLES:: - - ``elt_i``, ``elt_j``, ``elt_k`` -- elements of the fusion basis + sage: FusionRing(("E6", 1)).global_q_dimension() + 3 - This is the same as `N_{ij}^{k\ast}`, where `N_{ij}^k` are - the structure coefficients of the ring (see :meth:`Nk_ij`), - and `k\ast`` denotes the dual element. The coefficient `N_{ijk}` - is unchanged under permutations of the three basis vectors. + For the Drinfeld double, it is the square of the order of the underlying quantum group. EXAMPLES:: - sage: G23 = FusionRing("G2", 3) - sage: G23.fusion_labels("g") - sage: b = G23.basis().list(); b - [g0, g1, g2, g3, g4, g5] - sage: [(x, y, z) for x in b for y in b for z in b if G23.N_ijk(x, y, z) > 1] - [(g3, g3, g3), (g3, g3, g4), (g3, g4, g3), (g4, g3, g3)] - sage: all(G23.N_ijk(x, y, z)==G23.N_ijk(y, z, x) for x in b for y in b for z in b) - True - sage: all(G23.N_ijk(x, y, z)==G23.N_ijk(y, x, z) for x in b for y in b for z in b) - True + sage: G = SymmetricGroup(4) + sage: H = FusionRing(G) + sage: H.global_q_dimension() + 576 + sage: sum(x.q_dimension()^2 for x in H.basis()) + 576 """ - return (elt_i * elt_j).monomial_coefficients().get(elt_k.dual().weight(), 0) + ret = sum(x.q_dimension(base_coercion=False) ** 2 for x in self.basis()) + if (not base_coercion) or (self._basecoer is None): + return ret + return self._basecoer(ret) - @cached_method - def Nk_ij(self, elt_i, elt_j, elt_k): + def is_multiplicity_free(self): r""" - Return the fusion coefficient `N^k_{ij}`. + Return ``True`` if the fusion multiplicities + :meth:`Nk_ij` are bounded by 1. - These are the structure coefficients of the fusion ring, so + The :class:`FMatrix` is available only for multiplicity free + instances of :class:`FusionRing`. - .. MATH:: + EXAMPLES:: - i * j = \sum_{k} N_{ij}^k k. + sage: [FusionRing((ct, k)).is_multiplicity_free() for ct in ("A1", "A2", "B2", "C3") for k in (1, 2, 3)] + [True, True, True, True, True, False, True, True, False, True, False, False] EXAMPLES:: - sage: A22 = FusionRing("A2", 2) - sage: b = A22.basis().list() - sage: all(x*y == sum(A22.Nk_ij(x, y, k)*k for k in b) for x in b for y in b) + sage: FusionRing(SymmetricGroup(3)).is_multiplicity_free() True + sage: FusionRing(SymmetricGroup(4)).is_multiplicity_free() + False """ - return (elt_i * elt_j).monomial_coefficients(copy=False).get(elt_k.weight(), 0) - - @cached_method - def s_ij(self, elt_i, elt_j, base_coercion=True): - r""" - Return the element of the S-matrix of this fusion ring corresponding to - the given elements. This is the unnormalized S-matrix, denoted - `\tilde{s}_{ij}` in [BaKi2001]_ . To obtain the normalized S-matrix, - divide by ``self.global_q_dimension()`` or use ``self.S_matrix()`` with - the option ``unitary=True``. + return all(self.N_ijk(i, j, k) <= 1 for i in self.basis() for j in self.basis() for k in self.basis()) - This is computed using the formula + def N_ijk(self, i, j, k): + """ + The symmetric invariant of three simple objects, + this returns the dimension of .. MATH:: + Hom(i \\otimes j\\otimes k, s0) - s_{i, j} = \frac{1}{\theta_i\theta_j} \sum_k N_{ik}^j d_k \theta_k, + where `s_0` is the unit element (assuming prefix='s'). - where `\theta_k` is the twist and `d_k` is the quantum - dimension. See [Row2006]_ Equation (2.2) or [EGNO2015]_ - Proposition 8.13.8. + EXAMPLES:: - INPUT: - - - ``elt_i``, ``elt_j`` -- elements of the fusion basis + sage: G23 = FusionRing(("G2", 3)) + sage: G23.fusion_labels("g") + sage: b = G23.basis().list(); b + [g0, g1, g2, g3, g4, g5] + sage: [(x, y, z) for x in b for y in b for z in b if G23.N_ijk(x, y, z) > 1] + [(g3, g3, g3), (g3, g3, g4), (g3, g4, g3), (g4, g3, g3)] + sage: all(G23.N_ijk(x, y, z)==G23.N_ijk(y, z, x) for x in b for y in b for z in b) + True + sage: all(G23.N_ijk(x, y, z)==G23.N_ijk(y, x, z) for x in b for y in b for z in b) + True EXAMPLES:: - sage: G21 = FusionRing("G2", 1) - sage: b = G21.basis() - sage: [G21.s_ij(x, y) for x in b for y in b] - [1, -zeta60^14 + zeta60^6 + zeta60^4, -zeta60^14 + zeta60^6 + zeta60^4, -1] + sage: A = FusionRing(AlternatingGroup(4), prefix="a", inject_variables=True) + sage: [A.N_ijk(a10, a11, x) for x in A.basis()] + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0] """ - ijtwist = elt_i.twist() + elt_j.twist() - ret = sum(k.q_dimension(base_coercion=False) * self.Nk_ij(elt_i, k, elt_j) - * self.root_of_unity(k.twist() - ijtwist, base_coercion=False) - for k in self.basis()) - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) + return self.Nk_ij(i, j, self.dual(k)) - def s_ijconj(self, elt_i, elt_j, base_coercion=True): + def product_on_basis(self, a, b): """ - Return the conjugate of the element of the S-matrix given by - ``self.s_ij(elt_i, elt_j, base_coercion=base_coercion)``. - - See :meth:`s_ij`. - - EXAMPLES:: - - sage: G21 = FusionRing("G2", 1) - sage: b = G21.basis() - sage: [G21.s_ijconj(x, y) for x in b for y in b] - [1, -zeta60^14 + zeta60^6 + zeta60^4, -zeta60^14 + zeta60^6 + zeta60^4, -1] + Return the product of two basis elements corresponding to keys `a` and `b`. - This method works with all possible types of fields returned by - ``self.fmats.field()``. - - TESTS:: - - sage: E62 = FusionRing("E6", 2) - sage: E62.fusion_labels("e", inject_variables=True) - sage: E62.s_ij(e8, e1).conjugate() == E62.s_ijconj(e8, e1) - True - sage: F41 = FusionRing("F4", 1) - sage: fmats = F41.get_fmatrix() - sage: fmats.find_orthogonal_solution(verbose=False) - sage: b = F41.basis() - sage: all(F41.s_ijconj(x, y) == F41._basecoer(F41.s_ij(x, y, base_coercion=False).conjugate()) for x in b for y in b) - True - sage: G22 = FusionRing("G2", 2) - sage: fmats = G22.get_fmatrix() - sage: fmats.find_orthogonal_solution(verbose=False) # long time (~11 s) - sage: b = G22.basis() # long time - sage: all(G22.s_ijconj(x, y) == fmats.field()(G22.s_ij(x, y, base_coercion=False).conjugate()) for x in b for y in b) # long time - True - """ - ret = self.s_ij(elt_i, elt_j, base_coercion=False).conjugate() - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def s_matrix(self, unitary=False, base_coercion=True): - r""" - Return the S-matrix of this fusion ring. - - OPTIONAL: - - - ``unitary`` -- (default: ``False``) set to ``True`` to obtain - the unitary S-matrix + INPUT: - Without the ``unitary`` parameter, this is the matrix denoted - `\widetilde{s}` in [BaKi2001]_. + - ``a`, ``b`` -- keys for the dictionary ``self._names`` representing simple objects EXAMPLES:: - sage: D91 = FusionRing("D9", 1) - sage: D91.s_matrix() - [ 1 1 1 1] - [ 1 1 -1 -1] - [ 1 -1 -zeta136^34 zeta136^34] - [ 1 -1 zeta136^34 -zeta136^34] - sage: S = D91.s_matrix(unitary=True); S - [ 1/2 1/2 1/2 1/2] - [ 1/2 1/2 -1/2 -1/2] - [ 1/2 -1/2 -1/2*zeta136^34 1/2*zeta136^34] - [ 1/2 -1/2 1/2*zeta136^34 -1/2*zeta136^34] - sage: S*S.conjugate() - [1 0 0 0] - [0 1 0 0] - [0 0 1 0] - [0 0 0 1] + sage: S = FusionRing(SymmetricGroup(3), prefix="s", inject_variables=True) + sage: s3*s4 + s1 + s2 + s5 + s6 + s7 + sage: S._names + {0: 's0', 1: 's1', 2: 's2', 3: 's3', 4: 's4', 5: 's5', 6: 's6', 7: 's7'} + sage: S.product_on_basis(3,4) + s1 + s2 + s5 + s6 + s7 """ - b = self.basis() - S = matrix([[self.s_ij(b[x], b[y], base_coercion=base_coercion) - for x in self.get_order()] for y in self.get_order()]) - if unitary: - return S / self.total_q_order(base_coercion=base_coercion) - return S + d = {k.support_of_term() : self.Nk_ij(self.monomial(a),self.monomial(b),k) for k in self.basis()} + return self._from_dict(d) @cached_method def r_matrix(self, i, j, k, base_coercion=True): @@ -959,7 +823,7 @@ def r_matrix(self, i, j, k, base_coercion=True): EXAMPLES:: - sage: I = FusionRing("E8", 2, conjugate=True) # Ising MTC + sage: I = FusionRing(("E8", 2), conjugate=True) # Ising MTC sage: I.fusion_labels(["i0", "p", "s"], inject_variables=True) sage: I.r_matrix(s, s, i0) == I.root_of_unity(-1/8) True @@ -971,11 +835,23 @@ def r_matrix(self, i, j, k, base_coercion=True): True sage: I.r_matrix(s, s, p) == I.root_of_unity(3/8) True + + EXAMPLES:: + + sage: C = FusionRing(SymmetricGroup(3), prefix="c", inject_variables=True) + sage: c4*c5 + c3 + c4 + sage: [C.r_matrix(c4,c5,k) for k in [c3,c4]] + [-zeta24^6, 1] + sage: c6^2 + c0 + c1 + c6 + sage: [C.r_matrix(c6,c6,k) for k in [c0,c1,c6]] + [zeta3, -zeta3, -zeta3 - 1] """ if self.Nk_ij(i, j, k) == 0: return self.field().zero() if (not base_coercion) or (self._basecoer is None) else self.fvars_field().zero() if i != j: - ret = self.root_of_unity((k.twist(reduced=False) - i.twist(reduced=False) - j.twist(reduced=False)) / 2, base_coercion=False) + ret = self.root_of_unity((k.twist() - i.twist() - j.twist()) / 2) else: i0 = self.one() B = self.basis() @@ -987,436 +863,299 @@ def r_matrix(self, i, j, k, base_coercion=True): return ret return self._basecoer(ret) - def global_q_dimension(self, base_coercion=True): - r""" - Return `\sum d_i^2`, where the sum is over all simple objects - and `d_i` is the quantum dimension. - - The global `q`-dimension is a positive real number. - - EXAMPLES:: - - sage: FusionRing("E6", 1).global_q_dimension() - 3 - """ - ret = sum(x.q_dimension(base_coercion=False) ** 2 for x in self.basis()) - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def total_q_order(self, base_coercion=True): + def root_of_unity(self, r, base_coercion=True): r""" - Return the positive square root of :meth:`self.global_q_dimension() - ` as an element of :meth:`self.field() `. - - This is implemented as `D_{+}e^{-i\pi c/4}`, where `D_+` is - :meth:`D_plus()` and `c` is :meth:`virasoro_central_charge()`. - - EXAMPLES:: - - sage: F = FusionRing("G2", 1) - sage: tqo=F.total_q_order(); tqo - zeta60^15 - zeta60^11 - zeta60^9 + 2*zeta60^3 + zeta60 - sage: tqo.is_real_positive() - True - sage: tqo^2 == F.global_q_dimension() - True - """ - c = self.virasoro_central_charge() - ret = self.D_plus(base_coercion=False) * self.root_of_unity(-c/4, base_coercion=False) - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) + Return `e^{i\pi r}` as an element of ``self.field()`` if possible. - def D_plus(self, base_coercion=True): - r""" - Return `\sum d_i^2\theta_i` where `i` runs through the simple objects, - `d_i` is the quantum dimension and `\theta_i` is the twist. + INPUT: - This is denoted `p_+` in [BaKi2001]_ Chapter 3. + - ``r`` -- a rational number EXAMPLES:: - sage: B31 = FusionRing("B3", 1) - sage: Dp = B31.D_plus(); Dp - 2*zeta48^13 - 2*zeta48^5 - sage: Dm = B31.D_minus(); Dm - -2*zeta48^3 - sage: Dp*Dm == B31.global_q_dimension() - True - sage: c = B31.virasoro_central_charge(); c - 7/2 - sage: Dp/Dm == B31.root_of_unity(c/2) - True - """ - ret = sum((x.q_dimension(base_coercion=False))**2 * x.ribbon(base_coercion=False) for x in self.basis()) - if (not base_coercion) or (self._basecoer is None): - return ret - return self._basecoer(ret) - - def D_minus(self, base_coercion=True): - r""" - Return `\sum d_i^2\theta_i^{-1}` where `i` runs through the simple - objects, `d_i` is the quantum dimension and `\theta_i` is the twist. - - This is denoted `p_-` in [BaKi2001]_ Chapter 3. + sage: A11 = FusionRing(("A1", 1)) + sage: A11.field() + Cyclotomic Field of order 24 and degree 8 + sage: for n in [1..7]: + ....: try: + ....: print(n, A11.root_of_unity(2/n)) + ....: except ValueError as err: + ....: print(n, err) + 1 1 + 2 -1 + 3 zeta24^4 - 1 + 4 zeta24^6 + 5 not a root of unity in the field + 6 zeta24^4 + 7 not a root of unity in the field EXAMPLES:: - sage: E83 = FusionRing("E8", 3, conjugate=True) - sage: [Dp, Dm] = [E83.D_plus(), E83.D_minus()] - sage: Dp*Dm == E83.global_q_dimension() - True - sage: c = E83.virasoro_central_charge(); c - -248/11 - sage: Dp*Dm == E83.global_q_dimension() - True + sage: H = FusionRing(DihedralGroup(6)) + sage: H.field() + Cyclotomic Field of order 24 and degree 8 + sage: for n in [1..7]: + ....: try: + ....: print (n,H.root_of_unity(2/n)) + ....: except ValueError as err: + ....: print (n,err) + ....: + 1 1 + 2 -1 + 3 zeta24^4 - 1 + 4 zeta24^6 + 5 not a root of unity in the field + 6 zeta24^4 + 7 not a root of unity in the field """ - ret = sum((x.q_dimension(base_coercion=False))**2 / x.ribbon(base_coercion=False) for x in self.basis()) + n = 2 * r * self._cyclotomic_order + if n not in ZZ: + raise ValueError("not a root of unity in the field") + ret = self.field().gen() ** n if (not base_coercion) or (self._basecoer is None): return ret return self._basecoer(ret) - def is_multiplicity_free(self): - r""" - Return ``True`` if the fusion multiplicities - :meth:`Nk_ij` are bounded by 1. - - The :class:`FMatrix` is available only for multiplicity free - instances of :class:`FusionRing`. - - EXAMPLES:: - - sage: [FusionRing(ct, k).is_multiplicity_free() for ct in ("A1", "A2", "B2", "C3") for k in (1, 2, 3)] - [True, True, True, True, True, False, True, True, False, True, False, False] + def s_ijconj(self, elt_i, elt_j, unitary=False, base_coercion=True): """ - ct = self.cartan_type() - k = self.fusion_level() - if ct.letter == 'A': - if ct.n == 1: - return True - return k <= 2 - # if ct.letter in ['B', 'D', 'G', 'F', 'E']: - if ct.letter in ['B', 'D', 'F', 'G']: - return k <= 2 - if ct.letter == 'C': - if ct.n == 2: - return k <= 2 - return k == 1 - if ct.letter == 'E': - if ct.n == 8: - return k <= 3 - return k <= 2 - - ################################### - ### Braid group representations ### - ################################### - - def get_computational_basis(self, a, b, n_strands): - r""" - Return the so-called computational basis for `\text{Hom}(b, a^n)`. - - INPUT: - - - ``a`` -- a basis element - - ``b`` -- another basis element - - ``n_strands`` -- the number of strands for a braid group - - Let `n=` ``n_strands`` and let `k` be the greatest integer `\leq n/2`. - The braid group acts on `\text{Hom}(b, a^n)`. This action - is computed in :meth:`get_braid_generators`. This method - returns the computational basis in the form of a list of - fusion trees. Each tree is represented by an `(n-2)`-tuple - - .. MATH:: - - (m_1, \ldots, m_k, l_1, \ldots, l_{k-2}) - - such that each `m_j` is an irreducible constituent in `a \otimes a` - and - - .. MATH:: - - \begin{array}{l} - b \in l_{k-2} \otimes m_{k}, \\ - l_{k-2} \in l_{k-3} \otimes m_{k-1}, \\ - \cdots, \\ - l_2 \in l_1 \otimes m_3, \\ - l_1 \in m_1 \otimes m_2, - \end{array} - - where `z \in x \otimes y` means `N_{xy}^z \neq 0`. - - As a computational device when ``n_strands`` is odd, we pad the - vector `(m_1, \ldots, m_k)` with an additional `m_{k+1}` equal to `a`. - However, this `m_{k+1}` does *not* appear in the output of this method. - - The following example appears in Section 3.1 of [CW2015]_. - - EXAMPLES:: - - sage: A14 = FusionRing("A1", 4) - sage: A14.get_order() - [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2)] - sage: A14.fusion_labels(["zero", "one", "two", "three", "four"], inject_variables=True) - sage: [A14(x) for x in A14.get_order()] - [zero, one, two, three, four] - sage: A14.get_computational_basis(one, two, 4) - [(two, two), (two, zero), (zero, two)] - """ - def _get_trees(fr, top_row, root): - if len(top_row) == 2: - m1, m2 = top_row - return [[]] if fr.Nk_ij(m1, m2, root) else [] - else: - m1, m2 = top_row[:2] - return [tuple([l, *b]) for l in fr.basis() for b in _get_trees(fr, [l]+top_row[2:], root) if fr.Nk_ij(m1, m2, l)] - - comp_basis = list() - for top in product((a*a).monomials(), repeat=n_strands//2): - # If the n_strands is odd, we must extend the top row by a fusing anyon - top_row = list(top)+[a]*(n_strands%2) - comp_basis.extend(tuple([*top, *levels]) for levels in _get_trees(self, top_row, b)) - return comp_basis - - def get_fmatrix(self, *args, **kwargs): - r""" - Construct an :class:`FMatrix` factory to solve the pentagon relations - and organize the resulting F-symbols. - - EXAMPLES:: - - sage: A15 = FusionRing("A1", 5) - sage: A15.get_fmatrix() - F-Matrix factory for The Fusion Ring of Type A1 and level 5 with Integer Ring coefficients - """ - # Initialize fresh FMatrix object. Useful if you need to reset - # FMatrix properties and there are various FusionRing objects (unique) - # associated to same level and algebra. - if not hasattr(self, 'fmats') or kwargs.get('new', False): - kwargs.pop('new', None) - from sage.algebras.fusion_rings.f_matrix import FMatrix - self.fmats = FMatrix(self, *args, **kwargs) - return self.fmats - - def _emap(self, mapper, input_args, worker_pool=None): - r""" - Apply the given mapper to each element of the given input iterable - and return the results (with no duplicates) in a list. - - INPUT: - - - ``mapper`` -- a string specifying the name of a function defined - in the ``fast_parallel_fusion_ring_braid_repn`` module - - ``input_args`` -- a tuple of arguments to be passed to mapper - - This method applies the mapper in parallel if a ``worker_pool`` - is provided. - - .. NOTE:: - - If ``worker_pool`` is not provided, function maps and reduces on - a single process. If ``worker_pool`` is provided, the function - attempts to determine whether it should use multiprocessing - based on the length of the input iterable. If it cannot determine - the length of the input iterable then it uses multiprocessing - with the default chunksize of `1` if chunksize is not - explicitly provided. - - EXAMPLES:: - - sage: FR = FusionRing("A1", 4) - sage: FR.fusion_labels(['idd', 'one', 'two', 'three', 'four'], inject_variables=True) - sage: fmats = FR.get_fmatrix() - sage: fmats.find_orthogonal_solution(verbose=False) # long time - sage: len(FR._emap('sig_2k', (1, one, one, 5))) # long time - 13 - sage: FR = FusionRing("A1", 2) - sage: FR.fusion_labels("a", inject_variables=True) - sage: fmats = FR.get_fmatrix() - sage: fmats.find_orthogonal_solution(verbose=False) - sage: len(FR._emap('odd_one_out', (a1, a1, 7))) - 16 - """ - n_proc = worker_pool._processes if worker_pool is not None else 1 - input_iter = [(child_id, n_proc, input_args) for child_id in range(n_proc)] - no_mp = worker_pool is None - # Map phase - input_iter = zip_longest([], input_iter, fillvalue=(mapper, id(self))) - results = list() - if no_mp: - mapped = map(executor, input_iter) - else: - mapped = worker_pool.imap_unordered(executor, input_iter, chunksize=1) - # Reduce phase - for worker_results in mapped: - results.extend(worker_results) - return results - - def get_braid_generators(self, - fusing_anyon, - total_charge_anyon, - n_strands, - checkpoint=False, - save_results="", - warm_start="", - use_mp=True, - verbose=True): - r""" - Compute generators of the Artin braid group on ``n_strands`` strands. - - If `a = ` ``fusing_anyon`` and `b = ` ``total_charge_anyon`` - the generators are endomorphisms of `\text{Hom}(b, a^n)`. - - INPUT: - - - ``fusing_anyon`` -- a basis element of ``self`` - - ``total_charge_anyon`` -- a basis element of ``self`` - - ``n_strands`` -- a positive integer greater than 2 - - ``checkpoint`` -- (default: ``False``) a boolean indicating - whether the F-matrix solver should pickle checkpoints - - ``save_results`` -- (optional) a string indicating the name of - a file in which to pickle computed F-symbols for later use - - ``warm_start`` -- (optional) a string indicating the name of a - pickled checkpoint file to "warm" start the F-matrix solver. - The pickle may be a checkpoint generated by the solver, or - a file containing solver results. If all F-symbols are known, - we don't run the solver again. - - ``use_mp`` -- (default: ``True``) a boolean indicating whether - to use multiprocessing to speed up the computation; this is - highly recommended. Python 3.8+ is required. - - ``verbose`` -- (default: ``True``) boolean indicating whether - to be verbose with the computation - - For more information on the optional parameters, see - :meth:`FMatrix.find_orthogonal_solution`. - - Given a simple object in the fusion category, here called - ``fusing_anyon`` allowing the universal R-matrix to act on adjacent - pairs in the fusion of ``n_strands`` copies of ``fusing_anyon`` - produces an action of the braid group. This representation can - be decomposed over another anyon, here called ``total_charge_anyon``. - See [CHW2015]_. - - OUTPUT: - - The method outputs a pair of data ``(comp_basis, sig)`` where - ``comp_basis`` is a list of basis elements of the braid group - module, parametrized by a list of fusion ring elements describing - a fusion tree. For example with 5 strands the fusion tree - is as follows. See :meth:`get_computational_basis` - for more information. + Return the conjugate of the element of the S-matrix given by + ``self.s_ij(elt_i, elt_j, base_coercion=base_coercion)``. - .. IMAGE:: ../../../media/fusiontree.png - :scale: 45 - :align: center + See :meth:`s_ij`. - ``sig`` is a list of braid group generators as matrices. In - some cases these will be represented as sparse matrices. + EXAMPLES:: - In the following example we compute a 5-dimensional braid group - representation on 5 strands associated to the spin representation - in the modular tensor category `SU(2)_4 \cong SO(3)_2`. + sage: G21 = FusionRing(("G2", 1)) + sage: b = G21.basis() + sage: [G21.s_ijconj(x, y) for x in b for y in b] + [1, -zeta60^14 + zeta60^6 + zeta60^4, -zeta60^14 + zeta60^6 + zeta60^4, -1] EXAMPLES:: - sage: A14 = FusionRing("A1", 4) - sage: A14.get_order() - [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2)] - sage: A14.fusion_labels(["one", "two", "three", "four", "five"], inject_variables=True) - sage: [A14(x) for x in A14.get_order()] - [one, two, three, four, five] - sage: two ** 5 - 5*two + 4*four - sage: comp_basis, sig = A14.get_braid_generators(two, two, 5, verbose=False) # long time - sage: A14.gens_satisfy_braid_gp_rels(sig) # long time + sage: P = FusionRing(CyclicPermutationGroup(3), prefix="p", inject_variables=True) + sage: P.s_ij(p1,p3) + zeta3 + sage: P.s_ijconj(p1,p3) + -zeta3 - 1 + + TESTS:: + + sage: E62 = FusionRing(("E6", 2)) + sage: E62.fusion_labels("e", inject_variables=True) + sage: E62.s_ij(e8, e1).conjugate() == E62.s_ijconj(e8, e1) True - sage: len(comp_basis) == 5 # long time + sage: F41 = FusionRing(("F4", 1)) + sage: fmats = F41.get_fmatrix() + sage: fmats.find_orthogonal_solution(verbose=False) + sage: b = F41.basis() + sage: all(F41.s_ijconj(x, y) == F41._basecoer(F41.s_ij(x, y, base_coercion=False).conjugate()) for x in b for y in b) + True + sage: G22 = FusionRing(("G2", 2)) + sage: fmats = G22.get_fmatrix() + sage: fmats.find_orthogonal_solution(verbose=False) # long time (~11 s) + sage: b = G22.basis() # long time + sage: all(G22.s_ijconj(x, y) == fmats.field()(G22.s_ij(x, y, base_coercion=False).conjugate()) for x in b for y in b) # long time True """ - if n_strands < 3: - raise ValueError("the number of strands must be an integer at least 3") - # Construct associated FMatrix object and solve for F-symbols - self.get_fmatrix() - if self.fmats._chkpt_status < 7: - self.fmats.find_orthogonal_solution(checkpoint=checkpoint, - save_results=save_results, - warm_start=warm_start, - use_mp=use_mp, - verbose=verbose) + ret = self.s_ij(elt_i, elt_j, unitary=unitary, base_coercion=False).conjugate() + if (not base_coercion) or (self._basecoer is None): + return ret + return self._basecoer(ret) - # Set multiprocessing parameters. Context can only be set once, so we try to set it - try: - set_start_method('fork') - except RuntimeError: - pass - # Turn off multiprocessing when field is QQbar due to pickling issues introduced by PARI upgrade in github issue #30537 - pool = Pool() if use_mp and self.fvars_field() != QQbar else None + def s_matrix(self, unitary=False, base_coercion=True): + r""" + Return the S-matrix of this fusion ring. - # Set up computational basis and compute generators one at a time - a, b = fusing_anyon, total_charge_anyon - comp_basis = self.get_computational_basis(a, b, n_strands) - d = len(comp_basis) - if verbose: - print("Computing an {}-dimensional representation of the Artin braid group on {} strands...".format(d, n_strands)) + OPTIONAL: - # Compute diagonal odd-indexed generators using the 3j-symbols - gens = {2*i+1: diagonal_matrix(self.r_matrix(a, a, c[i]) for c in comp_basis) for i in range(n_strands//2)} + - ``unitary`` -- (default: ``False``) set to ``True`` to obtain + the unitary S-matrix - # Compute even-indexed generators using F-matrices - for k in range(1, n_strands//2): - entries = self._emap('sig_2k', (k, a, b, n_strands), pool) + Without the ``unitary`` parameter, this is the matrix denoted + `\widetilde{s}` in [BaKi2001]_. - # Build cyclotomic field element objects from tuple of rationals repn - _unflatten_entries(self, entries) - gens[2*k] = matrix(dict(entries)) + EXAMPLES:: - # If n_strands is odd, we compute the final generator - if n_strands % 2: - entries = self._emap('odd_one_out', (a, b, n_strands), pool) + sage: D91 = FusionRing(("D9", 1)) + sage: D91.s_matrix() + [ 1 1 1 1] + [ 1 1 -1 -1] + [ 1 -1 -zeta136^34 zeta136^34] + [ 1 -1 zeta136^34 -zeta136^34] + sage: S = D91.s_matrix(unitary=True); S + [ 1/2 1/2 1/2 1/2] + [ 1/2 1/2 -1/2 -1/2] + [ 1/2 -1/2 -1/2*zeta136^34 1/2*zeta136^34] + [ 1/2 -1/2 1/2*zeta136^34 -1/2*zeta136^34] + sage: S*S.conjugate() + [1 0 0 0] + [0 1 0 0] + [0 0 1 0] + [0 0 0 1] - # Build cyclotomic field element objects from tuple of rationals repn - _unflatten_entries(self, entries) - gens[n_strands-1] = matrix(dict(entries)) + EXAMPLES:: - return comp_basis, [gens[k] for k in sorted(gens)] + sage: FusionRing(SymmetricGroup(3)).s_matrix() + [ 1 1 2 3 3 2 2 2] + [ 1 1 2 -3 -3 2 2 2] + [ 2 2 4 0 0 -2 -2 -2] + [ 3 -3 0 3 -3 0 0 0] + [ 3 -3 0 -3 3 0 0 0] + [ 2 2 -2 0 0 4 -2 -2] + [ 2 2 -2 0 0 -2 -2 4] + [ 2 2 -2 0 0 -2 4 -2] + sage: FusionRing(SymmetricGroup(3)).s_matrix(unitary=True) + [ 1/6 1/6 1/3 1/2 1/2 1/3 1/3 1/3] + [ 1/6 1/6 1/3 -1/2 -1/2 1/3 1/3 1/3] + [ 1/3 1/3 2/3 0 0 -1/3 -1/3 -1/3] + [ 1/2 -1/2 0 1/2 -1/2 0 0 0] + [ 1/2 -1/2 0 -1/2 1/2 0 0 0] + [ 1/3 1/3 -1/3 0 0 2/3 -1/3 -1/3] + [ 1/3 1/3 -1/3 0 0 -1/3 -1/3 2/3] + [ 1/3 1/3 -1/3 0 0 -1/3 2/3 -1/3] + """ + b = self.basis() + S = matrix([[self.s_ij(b[x], b[y], unitary=unitary, base_coercion=base_coercion) + for x in self.get_order()] for y in self.get_order()]) + return S - def gens_satisfy_braid_gp_rels(self, sig): + def total_q_order(self, base_coercion=True): r""" - Return ``True`` if the matrices in the list ``sig`` satisfy - the braid relations. + Return the positive square root of :meth:`self.global_q_dimension() + ` as an element of :meth:`self.field() `. - This if `n` is the cardinality of ``sig``, this - confirms that these matrices define a representation of - the Artin braid group on `n+1` strands. Tests correctness of - :meth:`get_braid_generators`. + This is implemented as `D_{+}e^{-i\pi c/4}`, where `D_+` is + :meth:`D_plus()` and `c` is :meth:`virasoro_central_charge()`. EXAMPLES:: - sage: F41 = FusionRing("F4", 1, fusion_labels="f", inject_variables=True) - sage: f1*f1 - f0 + f1 - sage: comp, sig = F41.get_braid_generators(f1, f0, 4, verbose=False) - sage: F41.gens_satisfy_braid_gp_rels(sig) + sage: F = FusionRing(("G2", 1)) + sage: tqo=F.total_q_order(); tqo + zeta60^15 - zeta60^11 - zeta60^9 + 2*zeta60^3 + zeta60 + sage: tqo.is_real_positive() + True + sage: tqo^2 == F.global_q_dimension() True + + For the Drinfeld double of a finite group `G`, this equals the + cardinality of `G`. + + EXAMPLES:: + + sage: FusionRing(DihedralGroup(7)).total_q_order() + 14 """ - n = len(sig) - braid_rels = all(sig[i] * sig[i+1] * sig[i] == sig[i+1] * sig[i] * sig[i+1] for i in range(n-1)) - far_comm = all(sig[i] * sig[j] == sig[j] * sig[i] for i, j in product(range(n), repeat=2) if abs(i-j) > 1 and i > j) - singular = any(s.is_singular() for s in sig) - return braid_rels and far_comm and not singular + roots = self.global_q_dimension(base_coercion=False).sqrt(extend=False, all=True) + ret = [root for root in roots if root.is_real_positive()][0] + if (not base_coercion) or (self._basecoer is None): + return ret + return self._basecoer(ret) + + def test_braid_representation(self, max_strands=6, anyon=None): + """ + Check that we can compute valid braid group representations. + + INPUT: + + - ``max_strands`` -- (default: 6): maximum number of braid group strands + - ``anyon`` -- (optional) run this test on this particular simple object + + Create a braid group representation using :meth:`get_braid_generators` + and confirms the braid relations. This test indirectly partially + verifies the correctness of the orthogonal F-matrix solver. If the + code were incorrect the method would not be deterministic because the + fusing anyon is chosen randomly. (A different choice is made for each + number of strands tested.) However the doctest is deterministic since + it will always return ``True``. If the anyon parameter is omitted, + a random anyon is tested for each number of strands up to ``max_strands``. + + EXAMPLES:: - class Element(WeylCharacterRing.Element): + sage: A21 = FusionRing(("A2", 1)) + sage: A21.test_braid_representation(max_strands=4) + True + sage: F41 = FusionRing(("F4", 1)) # long time + sage: F41.test_braid_representation() # long time + True """ - A class for FusionRing elements. + if not self.is_multiplicity_free(): # Braid group representation is not available if self is not multiplicity free + raise NotImplementedError("only implemented for multiplicity free fusion rings") + b = self.basis() + results = [] + # Test with different numbers of strands + for n_strands in range(3, max_strands+1): + # Randomly select a fusing anyon. Skip the identity element, since + # its braiding matrices are trivial + if anyon is not None: + a = anyon + else: + while True: + a = b.random_element() + if a != self.one(): + break + pow = a ** n_strands + d = pow.monomials()[0] + # Try to find 'interesting' braid group reps i.e. skip 1-d reps + for k, v in pow.monomial_coefficients().items(): + if v > 1: + d = self(k) + break + comp_basis, sig = self.get_braid_generators(a, d, n_strands, verbose=False) + results.append(len(comp_basis) > 0) + results.append(self.gens_satisfy_braid_gp_rels(sig)) + return all(results) + + def twists_matrix(self): + r""" + Return a diagonal matrix describing the twist corresponding to + each simple object in the ``FusionRing``. + + EXAMPLES:: + + sage: B21 = FusionRing(("B2", 1)) + sage: [x.twist() for x in B21.basis().list()] + [0, 1, 5/8] + sage: [B21.root_of_unity(x.twist()) for x in B21.basis().list()] + [1, -1, zeta32^10] + sage: B21.twists_matrix() + [ 1 0 0] + [ 0 -1 0] + [ 0 0 zeta32^10] """ + B = self.basis() + return diagonal_matrix(B[x].ribbon() for x in self.get_order()) + + class Element(CombinatorialFreeModule.Element): + + @abstract_method + def ribbon(self, base_coercion=True): + """ + The twist or ribbon of the simple object. + """ + pass + + def dual(self): + """ + The dual element under the conjugation involution. + + EXAMPLES:: + + sage: G = CyclicPermutationGroup(4) + sage: H = FusionRing(G, prefix="j") + sage: [x for x in H.basis() if x==x.dual()] + [j0, j1, j8, j9] + """ + if not self.is_simple_object(): + raise ValueError("Dual is only available for simple objects of a FusionRing") + return self.parent().dual(self) + def is_simple_object(self): - r""" - Determine whether ``self`` is a simple object of the fusion ring. + """ + Determine whether ``self`` is a simple object (basis element) of the fusion ring. EXAMPLES:: - sage: A22 = FusionRing("A2", 2) + sage: A22 = FusionRing(("A2", 2)) sage: x = A22(1, 0); x A22(1,0) sage: x.is_simple_object() @@ -1425,47 +1164,50 @@ def is_simple_object(self): A22(0,1) + A22(2,0) sage: (x^2).is_simple_object() False + + EXAMPLES:: + + sage: H = FusionRing(CyclicPermutationGroup(2), prefix="g", inject_variables=True) + sage: [x.is_simple_object() for x in [g0, g1, g0+g1]] + [True, True, False] """ - return self.parent()._k is not None and len(self._monomial_coefficients) == 1 + return self in self.parent().basis() - def weight(self): - r""" - Return the parametrizing dominant weight in the level `k` alcove. + @cached_method + def q_dimension(self, base_coercion=True): + """ + Compute the quantum dimension as an element of the parent's base + cyclotomic field. + + EXAMPLES:: - This method is only available for basis elements. + sage: B22 = FusionRing(("B2", 2)) + sage: [(b.q_dimension())^2 for b in B22.basis()] + [1, 4, 5, 1, 5, 4] EXAMPLES:: - sage: A21 = FusionRing("A2", 1) - sage: [x.weight() for x in A21.basis().list()] - [(0, 0, 0), (2/3, -1/3, -1/3), (1/3, 1/3, -2/3)] + sage: G = AlternatingGroup(4) + sage: H = FusionRing(G) + sage: [x.q_dimension() for x in H.basis()] + [1, 1, 1, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4] + sage: sum(x.q_dimension()^2 for x in H.basis()) == G.order()^2 + True """ - if len(self._monomial_coefficients) != 1: - raise ValueError("fusion weight is valid for basis elements only") - return next(iter(self._monomial_coefficients)) + if not self.is_simple_object(): + raise ValueError("Quantum dimension is only available for simple objects of a FusionRing") + return self.parent().s_ij(self, self.parent().one(), base_coercion=base_coercion) - def twist(self, reduced=True): + def twist(self, reduced=True, base_coercion=True): r""" Return a rational number `h` such that `\theta = e^{i \pi h}` is the twist of ``self``. The quantity `e^{i \pi h}` is also available using :meth:`ribbon`. - - This method is only available for simple objects. If - `\lambda` is the weight of the object, then - `h = \langle \lambda, \lambda+2\rho \rangle`, where - `\rho` is half the sum of the positive roots. - As in [Row2006]_, this requires normalizing - the invariant bilinear form so that - `\langle \alpha, \alpha \rangle = 2` for short roots. - - INPUT: - - - ``reduced`` -- (default: ``True``) boolean; if ``True`` - then return the twist reduced modulo 2 + This method is only available for simple objects. EXAMPLES:: - sage: G21 = FusionRing("G2", 1) + sage: G21 = FusionRing(("G2", 1)) sage: [x.twist() for x in G21.basis()] [0, 4/5] sage: [G21.root_of_unity(x.twist()) for x in G21.basis()] @@ -1474,103 +1216,26 @@ def twist(self, reduced=True): sage: zeta60^((4/5)*(60/2)) zeta60^14 - zeta60^4 - sage: F42 = FusionRing("F4", 2) + sage: F42 = FusionRing(("F4", 2)) sage: [x.twist() for x in F42.basis()] [0, 18/11, 2/11, 12/11, 4/11] - sage: E62 = FusionRing("E6", 2) + sage: E62 = FusionRing(("E6", 2)) sage: [x.twist() for x in E62.basis()] [0, 26/21, 12/7, 8/21, 8/21, 26/21, 2/3, 4/7, 2/3] - """ - if not self.is_simple_object(): - raise ValueError("quantum twist is only available for simple objects of a FusionRing") - P = self.parent() - rho = P.space().rho() - # We copy self.weight() to skip the test (which was already done - # by self.is_simple_object()). - lam = next(iter(self._monomial_coefficients)) - inner = lam.inner_product(lam + 2*rho) - twist = P._conj * P._nf * inner / P.fusion_l() - # Reduce modulo 2 - if reduced: - f = twist.floor() - twist -= f - return twist + (f % 2) - return twist - - def ribbon(self, base_coercion=True): - r""" - Return the twist or ribbon element of ``self``. - - If `h` is the rational number modulo 2 produced by - ``self.twist()``, this method produces `e^{i\pi h}`. - - .. SEEALSO:: - - An additive version of this is available as :meth:`twist`. - - EXAMPLES:: - - sage: F = FusionRing("A1", 3) - sage: [x.twist() for x in F.basis()] - [0, 3/10, 4/5, 3/2] - sage: [x.ribbon(base_coercion=False) for x in F.basis()] - [1, zeta40^6, zeta40^12 - zeta40^8 + zeta40^4 - 1, -zeta40^10] - sage: [F.root_of_unity(x, base_coercion=False) for x in [0, 3/10, 4/5, 3/2]] - [1, zeta40^6, zeta40^12 - zeta40^8 + zeta40^4 - 1, -zeta40^10] - """ - ret = self.parent().root_of_unity(self.twist(), base_coercion=False) - if (not base_coercion) or (self.parent()._basecoer is None): - return ret - return self.parent()._basecoer(ret) - - @cached_method - def q_dimension(self, base_coercion=True): - r""" - Return the quantum dimension as an element of the cyclotomic - field of the `2\ell`-th roots of unity, where `l = m (k+h^\vee)` - with `m=1, 2, 3` depending on whether type is simply, doubly or - triply laced, `k` is the level and `h^\vee` is the dual - Coxeter number. EXAMPLES:: - sage: B22 = FusionRing("B2", 2) - sage: [(b.q_dimension())^2 for b in B22.basis()] - [1, 4, 5, 1, 5, 4] + sage: Q = FusionRing(CyclicPermutationGroup(3)) + sage: [x.twist() for x in Q.basis()] + [0, 0, 0, 0, 2/3, 4/3, 0, 4/3, 2/3] + sage: [x.ribbon() for x in Q.basis()] + [1, 1, 1, 1, zeta3, -zeta3 - 1, 1, -zeta3 - 1, zeta3] """ if not self.is_simple_object(): - raise ValueError("quantum dimension is only available for simple objects of a FusionRing") - P = self.parent() - lam = self.weight() - space = P.space() - rho = space.rho() - powers = {} - for alpha in space.positive_roots(): - val = alpha.inner_product(lam + rho) - if val in powers: - powers[val] += 1 - else: - powers[val] = 1 - val = alpha.inner_product(rho) - if val in powers: - powers[val] -= 1 - else: - powers[val] = -1 - R = ZZ['q'] - q = R.gen() - expr = R.fraction_field().one() - for val in powers: - exp = powers[val] - if exp > 0: - expr *= q_int(P._nf * val, q)**exp - elif exp < 0: - expr /= q_int(P._nf * val, q)**(-exp) - expr = R(expr) - expr = expr.substitute(q=q**4) / (q**(2*expr.degree())) - zet = P.field().gen() ** (P._cyclotomic_order/P._l) - ret = expr.substitute(q=zet) - - if (not base_coercion) or (self.parent()._basecoer is None): - return ret - return self.parent()._basecoer(ret) + raise ValueError("Quantum twist is only available for simple objects of a FusionRing") + zeta = self.parent().field().gen() + rib = self.ribbon() + for k in range(4*self.parent()._cyclotomic_order): + if zeta**k == rib: + return k/(2*self.parent()._cyclotomic_order) From a0ed4c35d6fac81da9ae64c1c6fd47526733281e Mon Sep 17 00:00:00 2001 From: Willie Aboumrad Date: Wed, 12 Apr 2023 14:37:27 -0400 Subject: [PATCH 2/3] updating concrete class names and FusionRing usage to be consistent with original FusionRing. 10 get_order /__classcall__ doctests still failing... --- .../algebras/fusion_rings/fusion_double.py | 2 +- .../fusion_rings/generic_fusion_ring.py | 87 ++++++++++--------- ...n_ring_from_wcr.py => verlinde_algebra.py} | 40 ++++----- 3 files changed, 65 insertions(+), 64 deletions(-) rename src/sage/algebras/fusion_rings/{fusion_ring_from_wcr.py => verlinde_algebra.py} (96%) diff --git a/src/sage/algebras/fusion_rings/fusion_double.py b/src/sage/algebras/fusion_rings/fusion_double.py index 308b0006937..e0135572ddd 100644 --- a/src/sage/algebras/fusion_rings/fusion_double.py +++ b/src/sage/algebras/fusion_rings/fusion_double.py @@ -13,7 +13,7 @@ from sage.rings.integer_ring import ZZ from sage.sets.set import Set -class FusionRingFromQuantumDouble(FusionRing): +class FusionDouble(FusionRing): r""" This constructs the Fusion Ring of the modular tensor category of modules over the Drinfeld Double of a finite group. Usage is similar to :class:`FusionRing`::. diff --git a/src/sage/algebras/fusion_rings/generic_fusion_ring.py b/src/sage/algebras/fusion_rings/generic_fusion_ring.py index f57ff024324..3bd3f579e65 100644 --- a/src/sage/algebras/fusion_rings/generic_fusion_ring.py +++ b/src/sage/algebras/fusion_rings/generic_fusion_ring.py @@ -31,22 +31,23 @@ class FusionRing(CombinatorialFreeModule): @staticmethod - def __classcall_private__(cls, input_data, base_ring=ZZ, prefix=None, conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False, **kwds): + def __classcall_private__(cls, input_data, level=None, base_ring=ZZ, prefix=None, conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False, **kwds): """ Select the correct parent depending on the given input. """ if input_data in Groups: if prefix is None: prefix = "s" - from sage.algebras.fusion_rings.fusion_double import FusionRingFromQuantumDouble - return FusionRingFromQuantumDouble(input_data, base_ring=base_ring, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) - elif isinstance(input_data, tuple): + from sage.algebras.fusion_rings.fusion_double import FusionDouble + return FusionDouble(input_data, base_ring=base_ring, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) + else: try: - ct = CartanType(input_data[:-1]) + ct = CartanType(input_data) except ValueError: - raise ValueError("Input data cannot be parsed as a valid CartanType") - from sage.algebras.fusion_rings.fusion_ring_from_wcr import FusionRingFromWCR - return FusionRingFromWCR(ct, input_data[-1], conjugate=conjugate, base_ring=base_ring, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) + raise ValueError("Input data must be a group or an object that can be coerced into a CartanType") + assert level is not None, "Fusion level must be supplied when input data is a CartanType" + from sage.algebras.fusion_rings.verlinde_algebra import VerlindeAlgebra + return VerlindeAlgebra(ct, level, conjugate=conjugate, base_ring=base_ring, prefix=prefix, cyclotomic_order=cyclotomic_order, fusion_labels=fusion_labels, inject_variables=inject_variables) def __init__(self, names, base_ring=ZZ, prefix=None, conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False): self._names = names @@ -123,7 +124,7 @@ def conj_matrix(self): EXAMPLES:: - sage: FusionRing(("A2", 1)).conj_matrix() + sage: FusionRing("A2", 1).conj_matrix() [1 0 0] [0 0 1] [0 1 0] @@ -140,7 +141,7 @@ def D_plus(self, base_coercion=True): EXAMPLES:: - sage: B31 = FusionRing(("B3", 1)) + sage: B31 = FusionRing("B3", 1) sage: Dp = B31.D_plus(); Dp 2*zeta48^13 - 2*zeta48^5 sage: Dm = B31.D_minus(); Dm @@ -173,7 +174,7 @@ def D_minus(self, base_coercion=True): EXAMPLES:: - sage: E83 = FusionRing(("E8", 3), conjugate=True) + sage: E83 = FusionRing("E8", 3, conjugate=True) sage: [Dp, Dm] = [E83.D_plus(), E83.D_minus()] sage: Dp*Dm == E83.global_q_dimension() True @@ -228,9 +229,9 @@ def field(self): EXAMPLES:: - sage: FusionRing(("A2", 2)).field() + sage: FusionRing("A2", 2).field() Cyclotomic Field of order 60 and degree 16 - sage: FusionRing(("B2", 2)).field() + sage: FusionRing("B2", 2).field() Cyclotomic Field of order 40 and degree 16 sage: FusionRing(SymmetricGroup(3)).field() Cyclotomic Field of order 24 and degree 8 @@ -259,7 +260,7 @@ def fusion_labels(self, labels=None, inject_variables=False): EXAMPLES:: - sage: A13 = FusionRing(("A1", 3)) + sage: A13 = FusionRing("A1", 3) sage: A13.fusion_labels("x") sage: fb = list(A13.basis()); fb [x0, x1, x2, x3] @@ -333,7 +334,7 @@ def fvars_field(self): EXAMPLES:: - sage: A13 = FusionRing(("A1", 3), fusion_labels="a", inject_variables=True) + sage: A13 = FusionRing("A1", 3, fusion_labels="a", inject_variables=True) sage: A13.fvars_field() Cyclotomic Field of order 40 and degree 16 sage: A13.field() @@ -369,7 +370,7 @@ def gens_satisfy_braid_gp_rels(self, sig): EXAMPLES:: - sage: F41 = FusionRing(("F4", 1), fusion_labels="f", inject_variables=True) + sage: F41 = FusionRing("F4", 1, fusion_labels="f", inject_variables=True) sage: f1*f1 f0 + f1 sage: comp, sig = F41.get_braid_generators(f1, f0, 4, verbose=False) @@ -449,7 +450,7 @@ def get_braid_generators(self, EXAMPLES:: - sage: A14 = FusionRing(("A1", 4)) + sage: A14 = FusionRing("A1", 4) sage: A14.get_order() [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2)] sage: A14.fusion_labels(["one", "two", "three", "four", "five"], inject_variables=True) @@ -536,13 +537,13 @@ def _emap(self, mapper, input_args, worker_pool=None): EXAMPLES:: - sage: FR = FusionRing(("A1", 4)) + sage: FR = FusionRing("A1", 4) sage: FR.fusion_labels(['idd', 'one', 'two', 'three', 'four'], inject_variables=True) sage: fmats = FR.get_fmatrix() sage: fmats.find_orthogonal_solution(verbose=False) # long time sage: len(FR._emap('sig_2k', (1, one, one, 5))) # long time 13 - sage: FR = FusionRing(("A1", 2)) + sage: FR = FusionRing("A1", 2) sage: FR.fusion_labels("a", inject_variables=True) sage: fmats = FR.get_fmatrix() sage: fmats.find_orthogonal_solution(verbose=False) @@ -607,7 +608,7 @@ def get_computational_basis(self, a, b, n_strands): EXAMPLES:: - sage: A14 = FusionRing(("A1", 4)) + sage: A14 = FusionRing("A1", 4) sage: A14.get_order() [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2)] sage: A14.fusion_labels(["zero", "one", "two", "three", "four"], inject_variables=True) @@ -640,7 +641,7 @@ def get_fmatrix(self, *args, **kwargs): EXAMPLES:: - sage: A15 = FusionRing(("A1", 5)) + sage: A15 = FusionRing("A1", 5) sage: A15.get_fmatrix() F-Matrix factory for The Fusion Ring of Type A1 and level 5 with Integer Ring coefficients @@ -666,7 +667,7 @@ def get_order(self): EXAMPLES:: - sage: A15 = FusionRing(("A1", 5)) + sage: A15 = FusionRing("A1", 5) sage: w = A15.get_order(); w [(0, 0), (1/2, -1/2), (1, -1), (3/2, -3/2), (2, -2), (5/2, -5/2)] sage: A15.set_order([w[k] for k in [0, 4, 1, 3, 5, 2]]) @@ -697,7 +698,7 @@ def global_q_dimension(self, base_coercion=True): EXAMPLES:: - sage: FusionRing(("E6", 1)).global_q_dimension() + sage: FusionRing("E6", 1).global_q_dimension() 3 For the Drinfeld double, it is the square of the order of the underlying quantum group. @@ -726,7 +727,7 @@ def is_multiplicity_free(self): EXAMPLES:: - sage: [FusionRing((ct, k)).is_multiplicity_free() for ct in ("A1", "A2", "B2", "C3") for k in (1, 2, 3)] + sage: [FusionRing(ct, k).is_multiplicity_free() for ct in ("A1", "A2", "B2", "C3") for k in (1, 2, 3)] [True, True, True, True, True, False, True, True, False, True, False, False] EXAMPLES:: @@ -750,15 +751,15 @@ def N_ijk(self, i, j, k): EXAMPLES:: - sage: G23 = FusionRing(("G2", 3)) + sage: G23 = FusionRing("G2", 3) sage: G23.fusion_labels("g") sage: b = G23.basis().list(); b [g0, g1, g2, g3, g4, g5] sage: [(x, y, z) for x in b for y in b for z in b if G23.N_ijk(x, y, z) > 1] [(g3, g3, g3), (g3, g3, g4), (g3, g4, g3), (g4, g3, g3)] - sage: all(G23.N_ijk(x, y, z)==G23.N_ijk(y, z, x) for x in b for y in b for z in b) + sage: all(G23.N_ijk(x, y, z) == G23.N_ijk(y, z, x) for x in b for y in b for z in b) True - sage: all(G23.N_ijk(x, y, z)==G23.N_ijk(y, x, z) for x in b for y in b for z in b) + sage: all(G23.N_ijk(x, y, z) == G23.N_ijk(y, x, z) for x in b for y in b for z in b) True EXAMPLES:: @@ -823,7 +824,7 @@ def r_matrix(self, i, j, k, base_coercion=True): EXAMPLES:: - sage: I = FusionRing(("E8", 2), conjugate=True) # Ising MTC + sage: I = FusionRing("E8", 2, conjugate=True) # Ising MTC sage: I.fusion_labels(["i0", "p", "s"], inject_variables=True) sage: I.r_matrix(s, s, i0) == I.root_of_unity(-1/8) True @@ -873,7 +874,7 @@ def root_of_unity(self, r, base_coercion=True): EXAMPLES:: - sage: A11 = FusionRing(("A1", 1)) + sage: A11 = FusionRing("A1", 1) sage: A11.field() Cyclotomic Field of order 24 and degree 8 sage: for n in [1..7]: @@ -925,7 +926,7 @@ def s_ijconj(self, elt_i, elt_j, unitary=False, base_coercion=True): EXAMPLES:: - sage: G21 = FusionRing(("G2", 1)) + sage: G21 = FusionRing("G2", 1) sage: b = G21.basis() sage: [G21.s_ijconj(x, y) for x in b for y in b] [1, -zeta60^14 + zeta60^6 + zeta60^4, -zeta60^14 + zeta60^6 + zeta60^4, -1] @@ -940,17 +941,17 @@ def s_ijconj(self, elt_i, elt_j, unitary=False, base_coercion=True): TESTS:: - sage: E62 = FusionRing(("E6", 2)) + sage: E62 = FusionRing("E6", 2) sage: E62.fusion_labels("e", inject_variables=True) sage: E62.s_ij(e8, e1).conjugate() == E62.s_ijconj(e8, e1) True - sage: F41 = FusionRing(("F4", 1)) + sage: F41 = FusionRing("F4", 1) sage: fmats = F41.get_fmatrix() sage: fmats.find_orthogonal_solution(verbose=False) sage: b = F41.basis() sage: all(F41.s_ijconj(x, y) == F41._basecoer(F41.s_ij(x, y, base_coercion=False).conjugate()) for x in b for y in b) True - sage: G22 = FusionRing(("G2", 2)) + sage: G22 = FusionRing("G2", 2) sage: fmats = G22.get_fmatrix() sage: fmats.find_orthogonal_solution(verbose=False) # long time (~11 s) sage: b = G22.basis() # long time @@ -976,7 +977,7 @@ def s_matrix(self, unitary=False, base_coercion=True): EXAMPLES:: - sage: D91 = FusionRing(("D9", 1)) + sage: D91 = FusionRing("D9", 1) sage: D91.s_matrix() [ 1 1 1 1] [ 1 1 -1 -1] @@ -1029,7 +1030,7 @@ def total_q_order(self, base_coercion=True): EXAMPLES:: - sage: F = FusionRing(("G2", 1)) + sage: F = FusionRing("G2", 1) sage: tqo=F.total_q_order(); tqo zeta60^15 - zeta60^11 - zeta60^9 + 2*zeta60^3 + zeta60 sage: tqo.is_real_positive() @@ -1071,10 +1072,10 @@ def test_braid_representation(self, max_strands=6, anyon=None): EXAMPLES:: - sage: A21 = FusionRing(("A2", 1)) + sage: A21 = FusionRing("A2", 1) sage: A21.test_braid_representation(max_strands=4) True - sage: F41 = FusionRing(("F4", 1)) # long time + sage: F41 = FusionRing("F4", 1) # long time sage: F41.test_braid_representation() # long time True """ @@ -1112,7 +1113,7 @@ def twists_matrix(self): EXAMPLES:: - sage: B21 = FusionRing(("B2", 1)) + sage: B21 = FusionRing("B2", 1) sage: [x.twist() for x in B21.basis().list()] [0, 1, 5/8] sage: [B21.root_of_unity(x.twist()) for x in B21.basis().list()] @@ -1155,7 +1156,7 @@ def is_simple_object(self): EXAMPLES:: - sage: A22 = FusionRing(("A2", 2)) + sage: A22 = FusionRing("A2", 2) sage: x = A22(1, 0); x A22(1,0) sage: x.is_simple_object() @@ -1181,7 +1182,7 @@ def q_dimension(self, base_coercion=True): EXAMPLES:: - sage: B22 = FusionRing(("B2", 2)) + sage: B22 = FusionRing("B2", 2) sage: [(b.q_dimension())^2 for b in B22.basis()] [1, 4, 5, 1, 5, 4] @@ -1207,7 +1208,7 @@ def twist(self, reduced=True, base_coercion=True): EXAMPLES:: - sage: G21 = FusionRing(("G2", 1)) + sage: G21 = FusionRing("G2", 1) sage: [x.twist() for x in G21.basis()] [0, 4/5] sage: [G21.root_of_unity(x.twist()) for x in G21.basis()] @@ -1216,11 +1217,11 @@ def twist(self, reduced=True, base_coercion=True): sage: zeta60^((4/5)*(60/2)) zeta60^14 - zeta60^4 - sage: F42 = FusionRing(("F4", 2)) + sage: F42 = FusionRing("F4", 2) sage: [x.twist() for x in F42.basis()] [0, 18/11, 2/11, 12/11, 4/11] - sage: E62 = FusionRing(("E6", 2)) + sage: E62 = FusionRing("E6", 2) sage: [x.twist() for x in E62.basis()] [0, 26/21, 12/7, 8/21, 8/21, 26/21, 2/3, 4/7, 2/3] diff --git a/src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py b/src/sage/algebras/fusion_rings/verlinde_algebra.py similarity index 96% rename from src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py rename to src/sage/algebras/fusion_rings/verlinde_algebra.py index dd5af46f00b..6e32b90dac1 100644 --- a/src/sage/algebras/fusion_rings/fusion_ring_from_wcr.py +++ b/src/sage/algebras/fusion_rings/verlinde_algebra.py @@ -17,7 +17,7 @@ from sage.misc.cachefunc import cached_method from sage.rings.integer_ring import ZZ -class FusionRingFromWCR(FusionRing): +class VerlindeAlgebra(FusionRing): r""" Return the Fusion Ring (Verlinde Algebra) of level ``k``. @@ -60,7 +60,7 @@ class FusionRingFromWCR(FusionRing): EXAMPLES:: - sage: A22 = FusionRing(("A2", 2)) + sage: A22 = FusionRing("A2", 2) sage: [f1, f2] = A22.fundamental_weights() sage: M = [A22(x) for x in [0*f1, 2*f1, 2*f2, f1+f2, f2, f1]] sage: [M[3] * x for x in M] @@ -76,7 +76,7 @@ class FusionRingFromWCR(FusionRing): weights of the basis elements, then assign new labels to them while injecting them into the global namespace:: - sage: B22 = FusionRing(("B2", 2)) + sage: B22 = FusionRing("B2", 2) sage: b = [B22(x) for x in B22.get_order()]; b [B22(0,0), B22(1,0), B22(0,1), B22(2,0), B22(1,1), B22(0,2)] sage: [x.weight() for x in b] @@ -187,9 +187,9 @@ class FusionRingFromWCR(FusionRing): Every fusion ring should pass this test:: - sage: test_verlinde(FusionRing(("A2", 1))) + sage: test_verlinde(FusionRing("A2", 1)) True - sage: test_verlinde(FusionRing(("B4", 2))) # long time (.56s) + sage: test_verlinde(FusionRing("B4", 2)) # long time (.56s) True As an exercise, the reader may verify the examples in @@ -201,7 +201,7 @@ class FusionRingFromWCR(FusionRing): construct it as the conjugate of the `E_8` level 2 :class:`FusionRing`:: - sage: I = FusionRing(("E8", 2), conjugate=True) + sage: I = FusionRing("E8", 2, conjugate=True) sage: I.fusion_labels(["i0", "p", "s"], inject_variables=True) sage: b = I.basis().list(); b [i0, p, s] @@ -270,7 +270,7 @@ class FusionRingFromWCR(FusionRing): of `SL(2, \ZZ)`. Let us confirm these identities for the Fibonacci MTC ``FusionRing("G2", 1)``:: - sage: R = FusionRing(("G2", 1)) + sage: R = FusionRing("G2", 1) sage: S = R.s_matrix(unitary=True) sage: T = R.twists_matrix() sage: C = R.conj_matrix() @@ -300,7 +300,7 @@ def _repr_(self): """ EXAMPLES:: - sage: FusionRing(("A1", 3)) + sage: FusionRing("A1", 3) The Fusion Ring of Type A1 and level 3 with Integer Ring coefficients """ return self._WCR._repr_() @@ -311,7 +311,7 @@ def _test_verlinde(self, **options): EXAMPLES:: - sage: G22 = FusionRing(("G2", 2)) + sage: G22 = FusionRing("G2", 2) sage: G22._test_verlinde() """ tester = self._tester(**options) @@ -333,7 +333,7 @@ def _test_total_q_order(self, **options): EXAMPLES:: - sage: G22 = FusionRing(("G2", 2)) + sage: G22 = FusionRing("G2", 2) sage: G22._test_total_q_order() """ tester = self._tester(**options) @@ -347,7 +347,7 @@ def cartan_type(self): EXAMPLES:: - sage: G22 = FusionRing(("G2", 2)) + sage: G22 = FusionRing("G2", 2) sage: G22.cartan_type() ['G', 2] """ @@ -360,7 +360,7 @@ def fundamental_weights(self): EXAMPLES:: - sage: G22 = FusionRing(("G2", 2)) + sage: G22 = FusionRing("G2", 2) sage: G22.fundamental_weights() Finite family {1: (1, 0, -1), 2: (2, -1, -1)} """ @@ -372,7 +372,7 @@ def fusion_level(self): EXAMPLES:: - sage: B22 = FusionRing(('B2', 2)) + sage: B22 = FusionRing('B2', 2) sage: B22.fusion_level() 2 """ @@ -391,10 +391,10 @@ def fusion_l(self): EXAMPLES:: - sage: B22 = FusionRing(('B2', 2)) + sage: B22 = FusionRing('B2', 2) sage: B22.fusion_l() 10 - sage: D52 = FusionRing(('D5', 2)) + sage: D52 = FusionRing('D5', 2) sage: D52.fusion_l() 10 """ @@ -413,7 +413,7 @@ def Nk_ij(self, elt_i, elt_j, elt_k): EXAMPLES:: - sage: A22 = FusionRing(("A2", 2)) + sage: A22 = FusionRing("A2", 2) sage: b = A22.basis().list() sage: all(x*y == sum(A22.Nk_ij(x, y, k)*k for k in b) for x in b for y in b) True @@ -446,7 +446,7 @@ def s_ij(self, elt_i, elt_j, unitary=False, base_coercion=True): EXAMPLES:: - sage: G21 = FusionRing(("G2", 1)) + sage: G21 = FusionRing("G2", 1) sage: b = G21.basis() sage: [G21.s_ij(x, y) for x in b for y in b] [1, -zeta60^14 + zeta60^6 + zeta60^4, -zeta60^14 + zeta60^6 + zeta60^4, -1] @@ -489,7 +489,7 @@ def virasoro_central_charge(self): EXAMPLES:: - sage: R = FusionRing(("A1", 2)) + sage: R = FusionRing("A1", 2) sage: c = R.virasoro_central_charge(); c 3/2 sage: Dp = R.D_plus(); Dp @@ -563,7 +563,7 @@ def ribbon(self, base_coercion=True): EXAMPLES:: - sage: F = FusionRing(("A1", 3)) + sage: F = FusionRing("A1", 3) sage: [x.twist() for x in F.basis()] [0, 3/10, 4/5, 3/2] sage: [x.ribbon(base_coercion=False) for x in F.basis()] @@ -619,7 +619,7 @@ def weight(self): EXAMPLES:: - sage: A21 = FusionRing(("A2", 1)) + sage: A21 = FusionRing("A2", 1) sage: [x.weight() for x in A21.basis().list()] [(0, 0, 0), (2/3, -1/3, -1/3), (1/3, 1/3, -2/3)] """ From a0b7d6ada31ec6f7724d757eb5d963d25aa67fea Mon Sep 17 00:00:00 2001 From: Daniel Bump <75940445+dwbmscz@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:46:19 -0700 Subject: [PATCH 3/3] edit docstring to FusionRing --- .../fusion_rings/generic_fusion_ring.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/sage/algebras/fusion_rings/generic_fusion_ring.py b/src/sage/algebras/fusion_rings/generic_fusion_ring.py index 3bd3f579e65..26a7c1dd0d6 100644 --- a/src/sage/algebras/fusion_rings/generic_fusion_ring.py +++ b/src/sage/algebras/fusion_rings/generic_fusion_ring.py @@ -30,6 +30,82 @@ from sage.rings.qqbar import QQbar class FusionRing(CombinatorialFreeModule): + """ + This class constructs the FusionRing of a modular tensor category. + There are two classes currently implemented, Verlinde algebras, + and the Fusion Ring of the Drinfeld Double of a finite group. + Such a category is a semisimple braided tensor category, with + a finite number of simple objects, and its Grothendieck group + is the fusion ring. It also has a twist or ribbon method, + which makes it a ribbon category. It has an S-matrix, which is + invertible, and so it is a *modular tensor category* (MTC). + + The Verlinde algebra, which is the first class of fusion + ring we can construct with this class, is the Grothendieck group + of a modular tensor category (MTC) `\mathcal{C}(\mathfrak{g},k)`, + where `\mathfrak{g}` is a semisimple Lie algebra, and `k`, the + *level* is a positive integer. The objects of this category + are in bijection with the weights in the level `k` fundamental + alcove, and the fusion coefficients, which are the structure + constants of the ring, are computed by the Kac-Walton formula. + The category `\mathcal{C}(\mathfrak{g},k)` has various realizations + as the category of fields in a Wess-Zumino-Witten conformal field + theory, the (semisimplified) category tilting modules of a quantum group at a + root of unity, the Drinfeld category related to monodromy of the KZ equation, + or as a category based on the integrable highest weight + modules of an untwisted affine Lie algebra of level `k`. + + REFERENCES: + + - [BaKi2001]_ Chapter 3 + - [DFMS1996]_ Chapter 16 + - [EGNO2015]_ Chapter 8 + - [Feingold2004]_ + - [Fuchs1994]_ + - [Row2006]_ + - [Walton1990]_ + - [Wan2010]_ + + On the other hand, the Drinfeld double of a finite group is a + braided Hopf whose modules form a MTC. + + REFERENCES: + + - [BaKi2001]_ Chapter 3 + - [Mas1995]_ + - [CHW2015]_ + - [Goff1999]_ + + INPUT either: + + - ``ct`` -- the Cartan type of a simple (finite-dimensional) Lie algebra + - ``k`` -- the level + + or: + + - ``G`` -- a finite group. + + OPTIONAL: + + - ``conjugate`` -- (default ``False``) set ``True`` to obtain the complex conjugate ring + - ``cyclotomic_order`` -- (default computed depending on ``ct`` and ``k``) + - ``fusion_labels`` -- (default None) either a tuple of strings to use as labels of the + basis of simple objects, or a string from which the labels will be + constructed + - ``inject_variables`` -- (default ``False``): use with ``fusion_labels``. + If ``inject_variables`` is ``True``, the fusion labels will be variables + that can be accessed from the command line + + The cyclotomic order is an integer `N` such that all computations + will return elements of the cyclotomic field of `N`-th roots of unity. + Normally you will never need to change this but consider changing it + if :meth:`root_of_unity` raises a ``ValueError``. + + For further information, see + :class:`~sage.algebras.fusion_rings.verlinde_algebra.VerlindeAlgebra` and + :class:`~sage.algebras.fusion_rings.fusion_double.FusionDouble` + for the two classes of Fusion Rings that are implemented. + """ @staticmethod def __classcall_private__(cls, input_data, level=None, base_ring=ZZ, prefix=None, conjugate=False, cyclotomic_order=None, fusion_labels=None, inject_variables=False, **kwds): """