Esempio n. 1
0
class BGGComplex:
    """A class encoding all the things we need of the BGG complex.
    
    Parameters
    ----------
    root_system : str
        String encoding the Dynkin diagram of the root system (e.g. `'A3'`)
    pickle_directory : str (optional)
            Directory where to store `.pkl` files to save computations
            regarding maps in the BGG complex. If `None`, maps are not saved. (default: `None`)


    Attributes
    -------
    root_system : str
        String encoding Dynkin diagram.
    W : WeylGroup
        Object encoding the Weyl group.
    LA : LieAlgebraChevalleyBasis
        Object encoding the Lie algebra in the Chevalley basis over Q
    PBW : PoincareBirkhoffWittBasis
        Poincaré-Birkhoff-Witt basis of the universal enveloping 
        algebra. The maps in the BGG complex are elements of this basis. This package uses
        a slight modification of the PBW class as implemented in Sagemath proper.
    lattice : RootSpace
        The space of roots
    S : FiniteFamily
        Set of simple reflections in the Weyl group
    T : FIniteFamily
        Set of reflections in the Weyl group
    cycles : List 
        List of 4-tuples encoding the length-4 cycles in the Bruhat graph
    simple_roots : List
        (Ordered) list of the simple roots as elements of `lattice`
    rank : int
        Rank of the root system
    neg_roots : list
        List of the negative roots in the root system, encoded as `numpy.ndarray`.
    alpha_to_index : dict
        Dictionary mapping negative roots (as elements of `lattice`) to
        an ordered index encoding the negative root as basis element of the lie algebra $n$
        spanned by the negative roots.
    zero_root : element of `lattice`
        The zero root
    rho : element of `lattice`
        Half the sum of all positive roots
    """
    def __init__(self, root_system, pickle_directory=None):
        self.root_system = root_system
        self.W = WeylGroup(root_system)
        self.domain = self.W.domain()
        self.LA = LieAlgebra(QQ, cartan_type=root_system)
        self.PBW = PoincareBirkhoffWittBasis(self.LA,
                                             None,
                                             "PBW",
                                             cache_degree=5)
        # self.PBW = self.LA.pbw_basis()
        self.PBW_alg_gens = self.PBW.algebra_generators()
        self.lattice = self.domain.root_system.root_lattice()
        self.S = self.W.simple_reflections()
        self.T = self.W.reflections()
        self.signs = None
        self.cycles = None

        self._compute_weyl_dictionary()
        self._construct_BGG_graph()

        self.find_cycles()

        self.simple_roots = self.domain.simple_roots().values()
        self.rank = len(self.simple_roots)

        # for PBW computations we need to put the right order on the negative roots.
        # This order coincides with that of the sagemath source code.
        lie_alg_order = {k: i for i, k in enumerate(self.LA.basis().keys())}
        ordered_roots = sorted(
            [
                self._weight_to_alpha_sum(r)
                for r in self.domain.negative_roots()
            ],
            key=lambda rr: lie_alg_order[rr],
        )
        self.neg_roots = [-self._alpha_sum_to_array(r) for r in ordered_roots]

        self.alpha_to_index = {
            self._weight_to_alpha_sum(-self._tuple_to_weight(r)): i
            for i, r in enumerate(self.neg_roots)
        }
        self.zero_root = self.domain.zero()

        self.pickle_directory = pickle_directory
        if pickle_directory is None:
            self.pickle_maps = False
        else:
            self.pickle_maps = True

        if self.pickle_maps:
            self._maps = self._read_maps()
        else:
            self._maps = dict()

        self.rho = self.domain.rho()

        self._action_dic = dict()
        for s, w in self.reduced_word_dic.items():
            self._action_dic[s] = {
                i: self._weight_to_alpha_sum(w.action(mu))
                for i, mu in dict(self.domain.simple_roots()).items()
            }
        self._rho_action_dic = dict()
        for s, w in self.reduced_word_dic.items():
            self._rho_action_dic[s] = self._weight_to_alpha_sum(
                w.action(self.rho) - self.rho)

    def _compute_weyl_dictionary(self):
        """Construct a dictionary enumerating all of the elements of the Weyl group."""
        self.reduced_word_dic = {
            "".join([str(s) for s in g.reduced_word()]): g
            for g in self.W
        }
        self.reduced_word_dic_reversed = dict(
            [[v, k] for k, v in self.reduced_word_dic.items()])
        self.reduced_words = sorted(
            self.reduced_word_dic.keys(),
            key=len)  # sort the reduced words by their length
        long_element = self.W.long_element()
        self.dual_words = {
            s: self.reduced_word_dic_reversed[long_element * w]
            for s, w in self.reduced_word_dic.items()
        }  # the dual word is the word times the longest element

        self.column = defaultdict(list)
        max_len = 0
        for red_word in self.reduced_words:
            length = len(red_word)
            max_len = max(max_len, length)
            self.column[length] += [red_word]
        self.max_word_length = max_len

    def _construct_BGG_graph(self):
        """Find all the arrows in the BGG Graph.

        There is an arrow w->w' if len(w')=len(w)+1 and w' = t.w for some t in T.
        """
        self.arrows = []
        for w in self.reduced_words:
            for t in self.T:
                product_word = self.reduced_word_dic_reversed[
                    t * self.reduced_word_dic[w]]
                if len(product_word) == len(w) + 1:
                    self.arrows += [(w, product_word)]
        self.arrows = sorted(
            self.arrows,
            key=lambda t: len(t[0]))  # sort the arrows by the word length
        self.graph = DiGraph(self.arrows)

    def plot_graph(self):
        """Create a pretty plot of the BGG graph, with vertices colored by word lenght."""
        BGGVertices = sorted(self.reduced_words, key=len)
        BGGPartition = [list(v) for k, v in groupby(BGGVertices, len)]

        BGGGraphPlot = self.graph.to_undirected().graphplot(
            partition=BGGPartition, vertex_labels=None, vertex_size=30)
        display(BGGGraphPlot.plot())

    def find_cycles(self):
        """Find all the admitted cycles in the BGG graph.
        
        An admitted cycle consists of two paths a->b->c and a->b'->c, 
        where the word length increases by 1 each step. 
        The cycles are returned as tuples (a,b,c,b',a).
        """
        # only compute cycles if we haven't yet done so already
        if self.cycles is None:
            # for faster searching, make a dictionary of pairs (v,[u_1,...,u_k]) where v is a vertex and u_i
            # are vertices such that there is an arrow v->u_i
            first = lambda x: x[0]
            second = lambda x: x[1]
            outgoing = {
                k: list(map(second, v))
                for k, v in groupby(sorted(self.arrows, key=first), first)
            }
            # outgoing[max(self.reduced_words,key=lambda x: len(x))]=[]
            outgoing[self.reduced_word_dic_reversed[
                self.W.long_element()]] = []

            # make a dictionary of pairs (v,[u_1,...,u_k]) where v is a vertex and u_i are vertices such that
            # there is an arrow u_i->v
            incoming = {
                k: list(map(first, v))
                for k, v in groupby(sorted(self.arrows, key=second), second)
            }
            incoming[""] = []

            # enumerate all paths of length 2, a->b->c, where length goes +1,+1
            self.cycles = chain.from_iterable(
                [[a + (v, ) for v in outgoing[a[-1]]] for a in self.arrows])

            # enumerate all paths of length 3, a->b->c->b' such that b' != b,
            # b<b' in lexicographic order (to avoid duplicates) and length goes +1,+1,-1
            self.cycles = chain.from_iterable(
                [[a + (v, ) for v in incoming[a[-1]] if v > a[1]]
                 for a in self.cycles])

            # enumerate all cycles of length 4, a->b->c->b'->a such that b'!=b and length goes +1,+1,-1,-1
            self.cycles = [
                a + (a[0], ) for a in self.cycles if a[0] in incoming[a[-1]]
            ]

        return self.cycles

    def compute_signs(self, force_recompute=False):
        """Compute signs making making product of signs around all squares equal to -1.

        Returns
        -------
        dict[tuple(str,str), int]
            Dictionary mapping edges in the Bruhat graph to {+1,-1}
        """
        if not force_recompute:
            if self.signs is not None:
                return self.signs

        self.signs = compute_signs(self)
        return self.signs

    def compute_maps(self, root, column=None, check=False, pbar=None):
        """Compute the (unsigned) maps of the BGG complex for a given weight.

        Parameters
        ----------
        root : tuple(int)
            root for which to compute the maps.
        column : int or `None` (default: `None`)
            Try to only compute the maps up to this particular column if not `None`. This
            is faster in particular for small or large values of `column`. 
        check : bool (default: False)
            After computing all the maps, perform a check whether they are correct for 
            debugging purposes.
        pbar : tqdm (default: None)
            tqdm progress bar to give status updates about the progress. If `None` this 
            feature is disabled.

        Returns
        -------
        dict mapping edges (in form ('w1', 'w2')) to elements of `self.PBW`.
        """
        # Convert to tuple to make sure root is hasheable
        root = tuple(root)

        # If the maps are not in the cache, compute them and cache the result
        if root in self._maps:
            cached_result = self._maps[root]
        else:
            cached_result = None
        MapSolver = BGGMapSolver(self,
                                 root,
                                 pbar=pbar,
                                 cached_results=cached_result)
        self._maps[root] = MapSolver.solve(column=column)
        if check:
            maps_OK = MapSolver.check_maps()
            if not maps_OK:
                raise ValueError(
                    "For root %s the map solver produced something wrong" %
                    root)

        if self.pickle_maps:
            self._store_maps()

        return self._maps[root]

    def _read_maps(self):
        target_path = os.path.join(self.pickle_directory,
                                   self.root_system + r"_maps.pkl")
        try:
            with open(target_path, "rb") as file:
                maps = pickle.load(file)
                return maps
        except IOError:
            return dict()

    def _store_maps(self):
        target_path = os.path.join(self.pickle_directory,
                                   self.root_system + r"_maps.pkl")
        try:
            with open(target_path, "wb") as file:
                pickle.dump(self._maps, file, pickle.HIGHEST_PROTOCOL)
        except IOError:
            pass

    def _weight_to_tuple(self, weight):
        """Convert rootspace element to tuple."""
        b = weight.to_vector()
        b = matrix(b).transpose()
        A = [list(a.to_vector()) for a in self.simple_roots]
        A = matrix(A).transpose()

        return tuple(A.solve_right(b).transpose().list())

    def _weight_to_alpha_sum(self, weight):
        """Express a weight in the lattice as a linear combination of alpha[i]'s.
        
        These objects form the keys for elements of the Lie algebra, 
        and for factors in the universal enveloping algebra.
        """
        if type(weight) is not tuple and type(weight) is not list:
            tup = self._weight_to_tuple(weight)
        else:
            tup = tuple(weight)
        alpha = self.lattice.alpha()
        zero = self.lattice.zero()
        return sum((int(c) * alpha[i + 1] for i, c in enumerate(tup)), zero)

    def _alpha_sum_to_array(self, weight):
        output = array(vector(ZZ, self.rank))
        for i, c in weight.monomial_coefficients().items():
            output[i - 1] = c
        return output

    def _tuple_to_weight(self, t):
        """Turn a tuple encoding a linear combination of simple roots back into a weight."""
        return sum(int(a) * b for a, b in zip(t, self.simple_roots))

    def _is_dot_regular(self, mu):
        """Check if a weight is dot-regular by checking that it has trivial stabilizer under dot action.
        
        Parameters
        ----------
        mu : element of `self.lattice`
            Weight encoded as linear combination of `alpha[i]`, use `self._weight_to_alpha_sum()`
            to convert to this format
        """
        for w in self.reduced_words[1:]:
            if (self._dot_action(w, mu) == mu
                ):  # Stabilizer is non-empty, mu is not dot regular
                return False

        # No nontrivial stabilizer found
        return True

    def _make_dominant(self, mu):
        """Given a dot-regular weight, find the associated dominant weight.

        Parameters
        ----------
        mu : element of `self.lattice`
            Weight encoded as linear combination of `alpha[i]`, use `self._weight_to_alpha_sum()`
            to convert to this format

        Returns
        -------
        dominant weight encoded in same format as input
        """
        for w in self.reduced_words:
            new_mu = self._dot_action(w, mu)
            if new_mu.is_dominant():
                return new_mu, w

        # Nothing found
        raise Exception(
            "The weight %s can not be made dominant. Probably it is not dot-regular."
            % mu)

    # def compute_weights(self, weight_module):
    #     all_weights = weight_module.weight_dic.keys()

    #     regular_weights = []
    #     for mu in all_weights:
    #         if self._is_dot_regular(mu):
    #             mu_prime, w = self._make_dominant(mu)
    #             # mu_prime = self._weight_to_alpha_sum(mu_prime)
    #             # w = self.reduced_word_dic_reversed[w]
    #             regular_weights.append((mu, mu_prime, len(w)))
    #     return all_weights, regular_weights

    def _dot_action(self, w, mu):
        """Dot action of Weyl group on weights.

        Parameters
        ----------
        w : element of `self.W`
        mu : RootSpace.element_class
            Weight encoded as linear combination of `alpha[i]`, use `self._weight_to_alpha_sum()`
            to convert to this format

        """
        action = self._action_dic[w]
        mu_action = sum(
            [
                action[i] * int(c)
                for i, c in mu.monomial_coefficients().items()
            ],
            self.lattice.zero(),
        )
        return mu_action + self._rho_action_dic[w]

    def display_pbw(self, f, notebook=True):
        """Typesets an element of PBW of the universal enveloping algebra with LaTeX.

        Parameters
        ----------
        f : PoincareBirkhoffWittBasis.element_class
            The element to display
        notebook : bool (optional, default: `True`)
            Uses IPython display with math if `True`, otherwise just returns the LaTeX code as string.
        """
        map_string = []
        first_term = True
        for monomial, coefficient in f.monomial_coefficients().items():
            alphas = monomial.dict().items()

            if first_term:
                if coefficient < 0:
                    sign_string = "-"
                else:
                    sign_string = ""
                first_term = False
            else:
                if coefficient < 0:
                    sign_string = "-"
                else:
                    sign_string = "+"
            if abs(coefficient) == 1:
                coeff_string = ""
            else:
                coeff_string = str(abs(coefficient))
            term_strings = [sign_string + coeff_string]
            for alpha, power in alphas:
                if power > 1:
                    power_string = r"^{" + str(power) + r"}"
                else:
                    power_string = ""
                alpha_string = "".join(
                    str(k) * int(-v)
                    for k, v in alpha.monomial_coefficients().items())
                term_strings.append(r"f_{" + alpha_string + r"}" +
                                    power_string)

            map_string.append(r"\,".join(term_strings))
        if notebook:
            display(Math(" ".join(map_string)))
        else:
            return " ".join(map_string)

    def _display_map(self, arrow, f):
        """Display a single arrow plus map of the BGG complex."""
        f_string = self.display_pbw(f, notebook=False)
        display(Math(r"\to".join(arrow) + r",\,\," + f_string))

    def display_maps(self, mu):
        """Display all the maps of the BGG complex for a given mu, in appropriate order.
        
        Parameters
        ----------
        mu : tuple
            tuple encoding the weight as linear combination of simple roots
        """
        maps = self.compute_maps(mu)
        maps = sorted(maps.items(), key=lambda s: (len(s[0][0]), s[0]))
        for arrow, f in maps:
            self._display_map(arrow, f)
Esempio n. 2
0
class WeightSet:
    """Class to do simple computations with the weights of a weight module.

    Parameters
    ----------
    root_system : str
        String representing the root system (e.g. 'A2')

    Attributes
    ----------
    root_system : str
        String representing the root system (e.g. 'A2')
    W : WeylGroup
        Object encoding the Weyl group.
    weyl_dic : dict(str, WeylGroup.element_class)
        Dictionary mapping strings representing a Weyl group element as reduced word
        in simple reflections, to the Weyl group element.
    reduced_words : list[str]
        Sorted list of all strings representing Weyl group elements as reduced word in
        simple reflections.
    simple_roots : list[RootSpace.element_class]
        List of the simple roots
    rank : int
        Rank of root system
    rho : RootSpace.element_class
        Half the sum of all positive roots
    pos_roots : List[RootSpace.element_class]
        List of all the positive roots
    action_dic : Dict[str, np.array(np.int32, np.int32)]
        dictionary mapping each string representing an
        element of the Weyl group to a matrix expressing the action on the simple roots.
    rho_action_dic : Dict[str, np.array(np.int32)]
        dictionary mapping each string representing an
        element of the Weyl group to a vector representing the image
        of the dot action on rho.
    """
    @classmethod
    def from_bgg(cls, BGG):
        """Initialize from an instance of BGGComplex.

        Some data can be reused, and this gives roughly 3x faster initialization.
        
        Parameters
        ----------
        BGG : BGGComplex
            The BGGComplex to initialize from.
        """
        hot_start = {"W": BGG.W, "weyl_dic": BGG.reduced_word_dic}
        return cls(BGG.root_system, hot_start=hot_start)

    def __init__(self, root_system, hot_start=None):
        self.root_system = root_system

        if hot_start is None:
            self.W = WeylGroup(root_system)
            self.weyl_dic = self._compute_weyl_dictionary()
        else:
            self.W = hot_start["W"]
            self.weyl_dic = hot_start["weyl_dic"]

        self.domain = self.W.domain()

        self.reduced_words = sorted(self.weyl_dic.keys(), key=len)

        self.simple_roots = self.domain.simple_roots().values()
        self.rank = len(self.simple_roots)
        self.rho = self.domain.rho()

        self.pos_roots = self.domain.positive_roots()

        # Matrix of all simple roots, for faster matrix solving
        self.simple_root_matrix = matrix(
            [list(s.to_vector()) for s in self.simple_roots]).transpose()

        self.action_dic, self.rho_action_dic = self.get_action_dic()

    def _compute_weyl_dictionary(self):
        """Construct a dictionary enumerating all of the elements of the Weyl group.

        The keys are reduced words of the elements
        """
        reduced_word_dic = {
            "".join([str(s) for s in g.reduced_word()]): g
            for g in self.W
        }
        return reduced_word_dic

    def weight_to_tuple(self, weight):
        """Convert element of weight lattice to a sum of simple roots.

        Parameters
        ----------
        weight : RootSpace.element_class

        Returns
        -------
        tuple[int]
            tuple representing root as linear combination of simple roots
        """
        b = weight.to_vector()
        b = matrix(b).transpose()
        return tuple(self.simple_root_matrix.solve_right(b).transpose().list())

    def tuple_to_weight(self, t):
        """Inverse of `weight_to_tuple`.
        
        Parameters
        ----------
        t : tuple[int]

        Returns
        -------
        RootSpace.element_class
        """
        return sum(int(a) * b for a, b in zip(t, self.simple_roots))

    def get_action_dic(self):
        """Compute weyl group action as well as action on rho.

        Returns
        -------
        Dict[str, np.array(np.int32, np.int32)] : dictionary mapping each string representing an
            element of the Weyl group to a matrix expressing the action on the simple roots.
        Dict[str, np.array(np.int32)] : dictionary mapping each string representing an
            element of the Weyl group to a vector representing the image
            of the dot action on rho.
        """
        action_dic = dict()
        rho_action_dic = dict()
        for s, w in self.weyl_dic.items():  # s is a string, w is a matrix
            # Compute action of w on every simple root, decompose result in simple roots, encode result as matrix.
            action_mat = []
            for mu in self.simple_roots:
                action_mat.append(self.weight_to_tuple(w.action(mu)))
            action_dic[s] = np.array(action_mat, dtype=np.int32)

            # Encode the dot action of w on rho.
            rho_action_dic[s] = np.array(
                self.weight_to_tuple(w.action(self.rho) - self.rho),
                dtype=np.int32)
        return action_dic, rho_action_dic

    def dot_action(self, w, mu):
        r"""Compute the dot action of w on mu.

        The dot action :math:`w\cdot\mu = w(\mu+\rho)-\rho`, with :math:`\rho` half
        the sum of all the positive roots.
        
        Parameters
        ----------
        w : str
            string representing the weyl group element
        mu : iterable(int)
            the weight

        Returns
        -------
        np.array[np.int32]
            vector encoding the new weight
        """
        # The dot action w.mu = w(mu+rho)-rho = w*mu + (w*rho-rho).
        # The former term is given by action_dic, the latter by rho_action_dic
        return (np.matmul(self.action_dic[w].T, np.array(mu, dtype=np.int32)) +
                self.rho_action_dic[w])

    def dot_orbit(self, mu):
        """Compute the orbit of the Weyl group action on a weight.

        Parameters
        ----------
        mu : iterable(int)
            A weight

        Returns
        -------
        dict(str, np.array[np.int32])
            Dictionary mapping Weyl group elements to weights encoded as numpy vectors.
        """
        return {w: self.dot_action(w, mu) for w in self.reduced_words}

    def is_dot_regular(self, mu):
        """Check if mu has a non-trivial stabilizer under the dot action.
        
        Parameters
        ----------
        mu : iterable(int)
            The weight

        Returns
        -------
        bool
            `True` if the weight is dot-regular
        """
        for s in self.reduced_words[1:]:
            if np.all(self.dot_action(s, mu) == mu):
                return False
        # no stabilizer found
        return True

    def compute_weights(self, weights):
        """Find dot-regular weights and associated dominant weights of a set of weights.

        Parameters
        ----------
        weights : iterable(iterable(int))
            Iterable of weights
        
        returns
        -------
        list(tuple[tuple(int), tuple(int), int])
            list of triples consisting of
            dot-regular weight, associated dominant, and the length of the Weyl group
            element making the weight dominant under the dot action.
        """
        regular_weights = []
        for mu in weights:
            if self.is_dot_regular(mu):
                mu_prime, w = self.make_dominant(mu)
                regular_weights.append((mu, tuple(mu_prime), len(w)))
        return regular_weights

    def is_dominant(self, mu):
        """Use sagemath built-in function to check if weight is dominant.
        
        Parameters
        ----------
        mu : iterable(int)
            the weight

        Returns
        -------
        bool
            `True` if weight is dominant
        """
        return self.tuple_to_weight(mu).is_dominant()

    def make_dominant(self, mu):
        """For a dot-regular weight mu, w such that if w.mu is dominant.
        
         Such a w exists iff mu is dot-regular, in which case it is also unique.

        Parameters
        ----------
        mu : iterable(int)
            the dot-regular weight
        
        Returns
        -------
        tuple(int)
            The dominant weight w.mu
        str
            the string representing the Weyl group element w.
        """
        for w in self.reduced_words:
            new_mu = self.dot_action(w, mu)
            if self.is_dominant(new_mu):
                return new_mu, w
        else:
            raise ValueError(
                "Could not make weight %s dominant, probably it is not dot-regular."
            )

    def get_vertex_weights(self, mu):
        """For a given dot-regular mu, return its orbit under the dot-action.
        
        Parameters
        ----------
        mu : iterable(int)

        Returns
        -------
        list[tuple[int]]
            list of weights
        """
        vertex_weights = dict()
        for w in self.reduced_words:
            vertex_weights[w] = tuple(self.dot_action(w, mu))
        return vertex_weights

    def highest_weight_rep_dim(self, mu):
        """Give dimension of highest weight representation of integral dominant weight.

        Parameters
        ----------
        mu : tuple(int)
            A integral dominant weight

        Returns
        -------
        int
            dimension of highest weight representation.
        """
        mu_weight = self.tuple_to_weight(mu)
        numerator = 1
        denominator = 1
        for alpha in self.pos_roots:
            numerator *= (mu_weight + self.rho).dot_product(alpha)
            denominator *= self.rho.dot_product(alpha)
        return numerator // denominator