def _(expr: ArrayTensorProduct, x: Expr): args = expr.args addend_list = [] for i, arg in enumerate(expr.args): darg = array_derive(arg, x) if darg == 0: continue args_prev = args[:i] args_succ = args[i + 1:] shape_prev = reduce(operator.add, map(get_shape, args_prev), ()) shape_succ = reduce(operator.add, map(get_shape, args_succ), ()) addend = _array_tensor_product(*args_prev, darg, *args_succ) tot1 = len(get_shape(x)) tot2 = tot1 + len(shape_prev) tot3 = tot2 + len(get_shape(arg)) tot4 = tot3 + len(shape_succ) perm = [i for i in range(tot1, tot2)] + \ [i for i in range(tot1)] + [i for i in range(tot2, tot3)] + \ [i for i in range(tot3, tot4)] addend = _permute_dims(addend, _af_invert(perm)) addend_list.append(addend) if len(addend_list) == 1: return addend_list[0] elif len(addend_list) == 0: return S.Zero else: return _array_add(*addend_list)
def _strip_af(h, base, orbits, transversals, j, slp=[], slps={}): """ optimized _strip, with h, transversals and result in array form if the stripped elements is the identity, it returns False, base_len + 1 j h[base[i]] == base[i] for i <= j """ base_len = len(base) for i in range(j+1, base_len): beta = h[base[i]] if beta == base[i]: continue if beta not in orbits[i]: if not slp: return h, i + 1 return h, i + 1, slp u = transversals[i][beta] if h == u: if not slp: return False, base_len + 1 return False, base_len + 1, slp h = _af_rmul(_af_invert(u), h) if slp: u_slp = slps[i][beta][:] u_slp.reverse() u_slp = [(i, (g,)) for g in u_slp] slp = u_slp + slp if not slp: return h, base_len + 1 return h, base_len + 1, slp
def _strip_af(h, base, orbits, transversals, j, slp=[], slps={}): """ optimized _strip, with h, transversals and result in array form if the stripped elements is the identity, it returns False, base_len + 1 j h[base[i]] == base[i] for i <= j """ base_len = len(base) for i in range(j + 1, base_len): beta = h[base[i]] if beta == base[i]: continue if beta not in orbits[i]: if not slp: return h, i + 1 return h, i + 1, slp u = transversals[i][beta] if h == u: if not slp: return False, base_len + 1 return False, base_len + 1, slp h = _af_rmul(_af_invert(u), h) if slp: u_slp = slps[i][beta][:] u_slp.reverse() u_slp = [(i, (g, )) for g in u_slp] slp = u_slp + slp if not slp: return h, base_len + 1 return h, base_len + 1, slp
def _handle_nested_contraction(cls, expr, permutation): if not isinstance(expr, ArrayContraction): return expr, permutation if not isinstance(expr.expr, ArrayTensorProduct): return expr, permutation args = expr.expr.args subranks = [get_rank(arg) for arg in expr.expr.args] contraction_indices = expr.contraction_indices contraction_indices_flat = [j for i in contraction_indices for j in i] cumul = list(accumulate([0] + subranks)) # Spread the permutation in its array form across the args in the corresponding # tensor-product arguments with free indices: permutation_array_blocks_up = [] image_form = _af_invert(permutation.array_form) counter = 0 for i, e in enumerate(subranks): current = [] for j in range(cumul[i], cumul[i+1]): if j in contraction_indices_flat: continue current.append(image_form[counter]) counter += 1 permutation_array_blocks_up.append(current) # Get the map of axis repositioning for every argument of tensor-product: index_blocks = [[j for j in range(cumul[i], cumul[i+1])] for i, e in enumerate(expr.subranks)] index_blocks_up = expr._push_indices_up(expr.contraction_indices, index_blocks) inverse_permutation = permutation**(-1) index_blocks_up_permuted = [[inverse_permutation(j) for j in i if j is not None] for i in index_blocks_up] # Sorting key is a list of tuple, first element is the index of `args`, second element of # the tuple is the sorting key to sort `args` of the tensor product: sorting_keys = list(enumerate(index_blocks_up_permuted)) sorting_keys.sort(key=lambda x: x[1]) # Now we can get the permutation acting on the args in its image-form: new_perm_image_form = [i[0] for i in sorting_keys] # Apply the args-level permutation to various elements: new_index_blocks = [index_blocks[i] for i in new_perm_image_form] new_index_perm_array_form = _af_invert([j for i in new_index_blocks for j in i]) new_args = [args[i] for i in new_perm_image_form] new_contraction_indices = [tuple(new_index_perm_array_form[j] for j in i) for i in contraction_indices] new_expr = ArrayContraction(ArrayTensorProduct(*new_args), *new_contraction_indices) new_permutation = Permutation(_af_invert([j for i in [permutation_array_blocks_up[k] for k in new_perm_image_form] for j in i])) return new_expr, new_permutation
def _(expr: PermuteDims): if expr.permutation.array_form == [1, 0]: return _a2m_transpose(_array2matrix(expr.expr)) elif isinstance(expr.expr, ArrayTensorProduct): ranks = expr.expr.subranks inv_permutation = expr.permutation**(-1) newrange = [inv_permutation(i) for i in range(sum(ranks))] newpos = [] counter = 0 for rank in ranks: newpos.append(newrange[counter:counter + rank]) counter += rank newargs = [] newperm = [] scalars = [] for pos, arg in zip(newpos, expr.expr.args): if len(pos) == 0: scalars.append(_array2matrix(arg)) elif pos == sorted(pos): newargs.append((_array2matrix(arg), pos[0])) newperm.extend(pos) elif len(pos) == 2: newargs.append((_a2m_transpose(_array2matrix(arg)), pos[0])) newperm.extend(reversed(pos)) else: raise NotImplementedError() newargs = [i[0] for i in newargs] return _permute_dims(_a2m_tensor_product(*scalars, *newargs), _af_invert(newperm)) elif isinstance(expr.expr, ArrayContraction): mat_mul_lines = _array2matrix(expr.expr) if not isinstance(mat_mul_lines, ArrayTensorProduct): flat_cyclic_form = [ j for i in expr.permutation.cyclic_form for j in i ] expr_shape = get_shape(expr) if all(expr_shape[i] == 1 for i in flat_cyclic_form): return mat_mul_lines return mat_mul_lines # TODO: this assumes that all arguments are matrices, it may not be the case: permutation = Permutation(2 * len(mat_mul_lines.args) - 1) * expr.permutation permuted = [permutation(i) for i in range(2 * len(mat_mul_lines.args))] args_array = [None for i in mat_mul_lines.args] for i in range(len(mat_mul_lines.args)): p1 = permuted[2 * i] p2 = permuted[2 * i + 1] if p1 // 2 != p2 // 2: return _permute_dims(mat_mul_lines, permutation) pos = p1 // 2 if p1 > p2: args_array[i] = _a2m_transpose( mat_mul_lines.args[pos]) # type: ignore else: args_array[i] = mat_mul_lines.args[pos] # type: ignore return _a2m_tensor_product(*args_array) else: return expr
def _(expr: PermuteDims): subexpr, subremoved = _remove_trivial_dims(expr.expr) p = expr.permutation.array_form pinv = _af_invert(expr.permutation.array_form) shift = list( accumulate([1 if i in subremoved else 0 for i in range(len(p))])) premoved = [pinv[i] for i in subremoved] p2 = [e - shift[e] for i, e in enumerate(p) if e not in subremoved] # TODO: check if subremoved should be permuted as well... newexpr = PermuteDims(subexpr, p2) if newexpr != expr: newexpr = _array2matrix(newexpr) return newexpr, sorted(premoved)
def _sort_components(cls, expr, permutation): # Get the permutation in its image-form: perm_image_form = _af_invert(permutation.array_form) args = list(expr.args) # Starting index global position for every arg: cumul = list(accumulate([0] + expr.subranks)) # Split `perm_image_form` into a list of list corresponding to the indices # of every argument: perm_image_form_in_components = [perm_image_form[cumul[i]:cumul[i+1]] for i in range(len(args))] # Create an index, target-position-key array: ps = [(i, sorted(comp)) for i, comp in enumerate(perm_image_form_in_components)] # Sort the array according to the target-position-key: # In this way, we define a canonical way to sort the arguments according # to the permutation. ps.sort(key=lambda x: x[1]) # Read the inverse-permutation (i.e. image-form) of the args: perm_args_image_form = [i[0] for i in ps] # Apply the args-permutation to the `args`: args_sorted = [args[i] for i in perm_args_image_form] # Apply the args-permutation to the array-form of the permutation of the axes (of `expr`): perm_image_form_sorted_args = [perm_image_form_in_components[i] for i in perm_args_image_form] new_permutation = Permutation(_af_invert([j for i in perm_image_form_sorted_args for j in i])) return ArrayTensorProduct(*args_sorted), new_permutation
def _sort_fully_contracted_args(cls, expr, contraction_indices): if expr.shape is None: return expr, contraction_indices cumul = list(accumulate([0] + expr.subranks)) index_blocks = [list(range(cumul[i], cumul[i+1])) for i in range(len(expr.args))] contraction_indices_flat = {j for i in contraction_indices for j in i} fully_contracted = [all(j in contraction_indices_flat for j in range(cumul[i], cumul[i+1])) for i, arg in enumerate(expr.args)] new_pos = sorted(range(len(expr.args)), key=lambda x: (0, default_sort_key(expr.args[x])) if fully_contracted[x] else (1,)) new_args = [expr.args[i] for i in new_pos] new_index_blocks_flat = [j for i in new_pos for j in index_blocks[i]] index_permutation_array_form = _af_invert(new_index_blocks_flat) new_contraction_indices = [tuple(index_permutation_array_form[j] for j in i) for i in contraction_indices] new_contraction_indices = _sort_contraction_indices(new_contraction_indices) return ArrayTensorProduct(*new_args), new_contraction_indices
def _strip_af(h, base, orbits, transversals, j): """ optimized _strip, with h, transversals and result in array form if the stripped elements is the identity, it returns False, base_len + 1 j h[base[i]] == base[i] for i <= j """ base_len = len(base) for i in range(j + 1, base_len): beta = h[base[i]] if beta == base[i]: continue if beta not in orbits[i]: return h, i + 1 u = transversals[i][beta] if h == u: return False, base_len + 1 h = _af_rmul(_af_invert(u), h) return h, base_len + 1
def _strip_af(h, base, orbits, transversals, j): """ optimized _strip, with h, transversals and result in array form if the stripped elements is the identity, it returns False, base_len + 1 j h[base[i]] == base[i] for i <= j """ base_len = len(base) for i in range(j+1, base_len): beta = h[base[i]] if beta == base[i]: continue if beta not in orbits[i]: return h, i + 1 u = transversals[i][beta] if h == u: return False, base_len + 1 h = _af_rmul(_af_invert(u), h) return h, base_len + 1
def _strip(g, base, orbits, transversals): """ Attempt to decompose a permutation using a (possibly partial) BSGS structure. This is done by treating the sequence ``base`` as an actual base, and the orbits ``orbits`` and transversals ``transversals`` as basic orbits and transversals relative to it. This process is called "sifting". A sift is unsuccessful when a certain orbit element is not found or when after the sift the decomposition doesn't end with the identity element. The argument ``transversals`` is a list of dictionaries that provides transversal elements for the orbits ``orbits``. Parameters ========== ``g`` - permutation to be decomposed ``base`` - sequence of points ``orbits`` - a list in which the ``i``-th entry is an orbit of ``base[i]`` under some subgroup of the pointwise stabilizer of ` `base[0], base[1], ..., base[i - 1]``. The groups themselves are implicit in this function since the only infromation we need is encoded in the orbits and transversals ``transversals`` - a list of orbit transversals associated with the orbits ``orbits``. Examples ======== >>> from sympy.combinatorics import Permutation >>> Permutation.print_cyclic = True >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics.permutations import Permutation >>> from sympy.combinatorics.util import _strip >>> S = SymmetricGroup(5) >>> S.schreier_sims() >>> g = Permutation([0, 2, 3, 1, 4]) >>> _strip(g, S.base, S.basic_orbits, S.basic_transversals) (Permutation(4), 5) Notes ===== The algorithm is described in [1],pp.89-90. The reason for returning both the current state of the element being decomposed and the level at which the sifting ends is that they provide important information for the randomized version of the Schreier-Sims algorithm. References ========== [1] Holt, D., Eick, B., O'Brien, E. "Handbook of computational group theory" See Also ======== sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims_random """ h = g.array_form base_len = len(base) for i in range(base_len): beta = h[base[i]] if beta == base[i]: continue if beta not in orbits[i]: return _af_new(h), i + 1 u = transversals[i][beta].array_form h = _af_rmul(_af_invert(u), h) return _af_new(h), base_len + 1
def double_coset_can_rep(dummies, sym, b_S, sgens, S_transversals, g): """ Butler-Portugal algorithm for tensor canonicalization with dummy indices dummies list of lists of dummy indices, one list for each type of index; the dummy indices are put in order contravariant, covariant [d0, -d0, d1, -d1, ...]. sym list of the symmetries of the index metric for each type. possible symmetries of the metrics * 0 symmetric * 1 antisymmetric * None no symmetry b_S base of a minimal slot symmetry BSGS. sgens generators of the slot symmetry BSGS. S_transversals transversals for the slot BSGS. g permutation representing the tensor. Return 0 if the tensor is zero, else return the array form of the permutation representing the canonical form of the tensor. A tensor with dummy indices can be represented in a number of equivalent ways which typically grows exponentially with the number of indices. To be able to establish if two tensors with many indices are equal becomes computationally very slow in absence of an efficient algorithm. The Butler-Portugal algorithm [3] is an efficient algorithm to put tensors in canonical form, solving the above problem. Portugal observed that a tensor can be represented by a permutation, and that the class of tensors equivalent to it under slot and dummy symmetries is equivalent to the double coset `D*g*S` (Note: in this documentation we use the conventions for multiplication of permutations p, q with (p*q)(i) = p[q[i]] which is opposite to the one used in the Permutation class) Using the algorithm by Butler to find a representative of the double coset one can find a canonical form for the tensor. To see this correspondence, let `g` be a permutation in array form; a tensor with indices `ind` (the indices including both the contravariant and the covariant ones) can be written as `t = T(ind[g[0],..., ind[g[n-1]])`, where `n= len(ind)`; `g` has size `n + 2`, the last two indices for the sign of the tensor (trick introduced in [4]). A slot symmetry transformation `s` is a permutation acting on the slots `t -> T(ind[(g*s)[0]],..., ind[(g*s)[n-1]])` A dummy symmetry transformation acts on `ind` `t -> T(ind[(d*g)[0]],..., ind[(d*g)[n-1]])` Being interested only in the transformations of the tensor under these symmetries, one can represent the tensor by `g`, which transforms as `g -> d*g*s`, so it belongs to the coset `D*g*S`. Let us explain the conventions by an example. Given a tensor `T^{d3 d2 d1}{}_{d1 d2 d3}` with the slot symmetries `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}` `T^{a0 a1 a2 a3 a4 a5} = -T^{a4 a1 a2 a3 a0 a5}` and symmetric metric, find the tensor equivalent to it which is the lowest under the ordering of indices: lexicographic ordering `d1, d2, d3` then and contravariant index before covariant index; that is the canonical form of the tensor. The canonical form is `-T^{d1 d2 d3}{}_{d1 d2 d3}` obtained using `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}`. To convert this problem in the input for this function, use the following labelling of the index names (- for covariant for short) `d1, -d1, d2, -d2, d3, -d3` `T^{d3 d2 d1}{}_{d1 d2 d3}` corresponds to `g = [4,2,0,1,3,5,6,7]` where the last two indices are for the sign `sgens = [Permutation(0,2)(6,7), Permutation(0,4)(6,7)]` sgens[0] is the slot symmetry `-(0,2)` `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}` sgens[1] is the slot symmetry `-(0,4)` `T^{a0 a1 a2 a3 a4 a5} = -T^{a4 a1 a2 a3 a0 a5}` The dummy symmetry group D is generated by the strong base generators `[(0,1),(2,3),(4,5),(0,1)(2,3),(2,3)(4,5)]` The dummy symmetry acts from the left `d = [1,0,2,3,4,5,6,7]` exchange `d1 -> -d1` `T^{d3 d2 d1}{}_{d1 d2 d3} == T^{d3 d2}{}_{d1}{}^{d1}{}_{d2 d3}` `g=[4,2,0,1,3,5,6,7] -> [4,2,1,0,3,5,6,7] = _af_rmul(d, g)` which differs from `_af_rmul(g, d)`. The slot symmetry acts from the right `s = [2,1,0,3,4,5,7,6]` exchanges slots 0 and 2 and changes sign `T^{d3 d2 d1}{}_{d1 d2 d3} == -T^{d1 d2 d3}{}_{d1 d2 d3}` `g=[4,2,0,1,3,5,6,7] -> [0,2,4,1,3,5,7,6] = _af_rmul(g, s)` Example in which the tensor is zero, same slot symmetries as above: `T^{d3}{}_{d1,d2}{}^{d1}{}_{d3}{}^{d2}` `= -T^{d3}{}_{d1,d3}{}^{d1}{}_{d2}{}^{d2}` under slot symmetry `-(2,4)`; `= T_{d3 d1}{}^{d3}{}^{d1}{}_{d2}{}^{d2}` under slot symmetry `-(0,2)`; `= T^{d3}{}_{d1 d3}{}^{d1}{}_{d2}{}^{d2}` symmetric metric; `= 0` since two of these lines have tensors differ only for the sign. The double coset D*g*S consists of permutations `h = d*g*s` corresponding to equivalent tensors; if there are two `h` which are the same apart from the sign, return zero; otherwise choose as representative the tensor with indices ordered lexicographically according to `[d1, -d1, d2, -d2, d3, -d3]` that is `rep = min(D*g*S) = min([d*g*s for d in D for s in S])` The indices are fixed one by one; first choose the lowest index for slot 0, then the lowest remaining index for slot 1, etc. Doing this one obtains a chain of stabilizers `S -> S_{b0} -> S_{b0,b1} -> ...` and `D -> D_{p0} -> D_{p0,p1} -> ...` where `[b0, b1, ...] = range(b)` is a base of the symmetric group; the strong base `b_S` of S is an ordered sublist of it; therefore it is sufficient to compute once the strong base generators of S using the Schreier-Sims algorithm; the stabilizers of the strong base generators are the strong base generators of the stabilizer subgroup. `dbase = [p0,p1,...]` is not in general in lexicographic order, so that one must recompute the strong base generators each time; however this is trivial, there is no need to use the Schreier-Sims algorithm for D. The algorithm keeps a TAB of elements `(s_i, d_i, h_i)` where `h_i = d_i*g*s_i` satisfying `h_i[j] = p_j` for `0 <= j < i` starting from `s_0 = id, d_0 = id, h_0 = g`. The equations `h_0[0] = p_0, h_1[1] = p_1,...` are solved in this order, choosing each time the lowest possible value of p_i For `j < i` `d_i*g*s_i*S_{b_0,...,b_{i-1}}*b_j = D_{p_0,...,p_{i-1}}*p_j` so that for dx in `D_{p_0,...,p_{i-1}}` and sx in `S_{base[0],...,base[i-1]}` one has `dx*d_i*g*s_i*sx*b_j = p_j` Search for dx, sx such that this equation holds for `j = i`; it can be written as `s_i*sx*b_j = J, dx*d_i*g*J = p_j` `sx*b_j = s_i**-1*J; sx = trace(s_i**-1, S_{b_0,...,b_{i-1}})` `dx**-1*p_j = d_i*g*J; dx = trace(d_i*g*J, D_{p_0,...,p_{i-1}})` `s_{i+1} = s_i*trace(s_i**-1*J, S_{b_0,...,b_{i-1}})` `d_{i+1} = trace(d_i*g*J, D_{p_0,...,p_{i-1}})**-1*d_i` `h_{i+1}*b_i = d_{i+1}*g*s_{i+1}*b_i = p_i` `h_n*b_j = p_j` for all j, so that `h_n` is the solution. Add the found `(s, d, h)` to TAB1. At the end of the iteration sort TAB1 with respect to the `h`; if there are two consecutive `h` in TAB1 which differ only for the sign, the tensor is zero, so return 0; if there are two consecutive `h` which are equal, keep only one. Then stabilize the slot generators under `i` and the dummy generators under `p_i`. Assign `TAB = TAB1` at the end of the iteration step. At the end `TAB` contains a unique `(s, d, h)`, since all the slots of the tensor `h` have been fixed to have the minimum value according to the symmetries. The algorithm returns `h`. It is important that the slot BSGS has lexicographic minimal base, otherwise there is an `i` which does not belong to the slot base for which `p_i` is fixed by the dummy symmetry only, while `i` is not invariant from the slot stabilizer, so `p_i` is not in general the minimal value. This algorithm differs slightly from the original algorithm [3]: the canonical form is minimal lexicographically, and the BSGS has minimal base under lexicographic order. Equal tensors `h` are eliminated from TAB. Examples ======== >>> from sympy.combinatorics.permutations import Permutation >>> from sympy.combinatorics.perm_groups import PermutationGroup >>> from sympy.combinatorics.tensor_can import double_coset_can_rep, get_transversals >>> gens = [Permutation(x) for x in [[2,1,0,3,4,5,7,6], [4,1,2,3,0,5,7,6]]] >>> base = [0, 2] >>> g = Permutation([4,2,0,1,3,5,6,7]) >>> transversals = get_transversals(base, gens) >>> double_coset_can_rep([list(range(6))], [0], base, gens, transversals, g) [0, 1, 2, 3, 4, 5, 7, 6] >>> g = Permutation([4,1,3,0,5,2,6,7]) >>> double_coset_can_rep([list(range(6))], [0], base, gens, transversals, g) 0 """ size = g.size g = g.array_form num_dummies = size - 2 indices = list(range(num_dummies)) all_metrics_with_sym = all([_ is not None for _ in sym]) num_types = len(sym) dumx = dummies[:] dumx_flat = [] for dx in dumx: dumx_flat.extend(dx) b_S = b_S[:] sgensx = [h._array_form for h in sgens] if b_S: S_transversals = transversal2coset(size, b_S, S_transversals) # strong generating set for D dsgsx = [] for i in range(num_types): dsgsx.extend(dummy_sgs(dumx[i], sym[i], num_dummies)) ginv = _af_invert(g) idn = list(range(size)) # TAB = list of entries (s, d, h) where h = _af_rmuln(d,g,s) # for short, in the following d*g*s means _af_rmuln(d,g,s) TAB = [(idn, idn, g)] for i in range(size - 2): b = i testb = b in b_S and sgensx if testb: sgensx1 = [_af_new(_) for _ in sgensx] deltab = _orbit(size, sgensx1, b) else: deltab = set([b]) # p1 = min(IMAGES) = min(Union D_p*h*deltab for h in TAB) if all_metrics_with_sym: md = _min_dummies(dumx, sym, indices) else: md = [min(_orbit(size, [_af_new( ddx) for ddx in dsgsx], ii)) for ii in range(size - 2)] p_i = min([min([md[h[x]] for x in deltab]) for s, d, h in TAB]) dsgsx1 = [_af_new(_) for _ in dsgsx] Dxtrav = _orbit_transversal(size, dsgsx1, p_i, False, af=True) \ if dsgsx else None if Dxtrav: Dxtrav = [_af_invert(x) for x in Dxtrav] # compute the orbit of p_i for ii in range(num_types): if p_i in dumx[ii]: # the orbit is made by all the indices in dum[ii] if sym[ii] is not None: deltap = dumx[ii] else: # the orbit is made by all the even indices if p_i # is even, by all the odd indices if p_i is odd p_i_index = dumx[ii].index(p_i) % 2 deltap = dumx[ii][p_i_index::2] break else: deltap = [p_i] TAB1 = [] nTAB = len(TAB) while TAB: s, d, h = TAB.pop() if min([md[h[x]] for x in deltab]) != p_i: continue deltab1 = [x for x in deltab if md[h[x]] == p_i] # NEXT = s*deltab1 intersection (d*g)**-1*deltap dg = _af_rmul(d, g) dginv = _af_invert(dg) sdeltab = [s[x] for x in deltab1] gdeltap = [dginv[x] for x in deltap] NEXT = [x for x in sdeltab if x in gdeltap] # d, s satisfy # d*g*s*base[i-1] = p_{i-1}; using the stabilizers # d*g*s*S_{base[0],...,base[i-1]}*base[i-1] = # D_{p_0,...,p_{i-1}}*p_{i-1} # so that to find d1, s1 satisfying d1*g*s1*b = p_i # one can look for dx in D_{p_0,...,p_{i-1}} and # sx in S_{base[0],...,base[i-1]} # d1 = dx*d; s1 = s*sx # d1*g*s1*b = dx*d*g*s*sx*b = p_i for j in NEXT: if testb: # solve s1*b = j with s1 = s*sx for some element sx # of the stabilizer of ..., base[i-1] # sx*b = s**-1*j; sx = _trace_S(s, j,...) # s1 = s*trace_S(s**-1*j,...) s1 = _trace_S(s, j, b, S_transversals) if not s1: continue else: s1 = [s[ix] for ix in s1] else: s1 = s #assert s1[b] == j # invariant # solve d1*g*j = p_i with d1 = dx*d for some element dg # of the stabilizer of ..., p_{i-1} # dx**-1*p_i = d*g*j; dx**-1 = trace_D(d*g*j,...) # d1 = trace_D(d*g*j,...)**-1*d # to save an inversion in the inner loop; notice we did # Dxtrav = [perm_af_invert(x) for x in Dxtrav] out of the loop if Dxtrav: d1 = _trace_D(dg[j], p_i, Dxtrav) if not d1: continue else: if p_i != dg[j]: continue d1 = idn assert d1[dg[j]] == p_i # invariant d1 = [d1[ix] for ix in d] h1 = [d1[g[ix]] for ix in s1] #assert h1[b] == p_i # invariant TAB1.append((s1, d1, h1)) # if TAB contains equal permutations, keep only one of them; # if TAB contains equal permutations up to the sign, return 0 TAB1.sort(key=lambda x: x[-1]) nTAB1 = len(TAB1) prev = [0] * size while TAB1: s, d, h = TAB1.pop() if h[:-2] == prev[:-2]: if h[-1] != prev[-1]: return 0 else: TAB.append((s, d, h)) prev = h # stabilize the SGS sgensx = [h for h in sgensx if h[b] == b] if b in b_S: b_S.remove(b) _dumx_remove(dumx, dumx_flat, p_i) dsgsx = [] for i in range(num_types): dsgsx.extend(dummy_sgs(dumx[i], sym[i], num_dummies)) return TAB[0][-1]
def test_array_permutedims(): sa = symbols('a0:144') m1 = Array(sa[:6], (2, 3)) assert permutedims(m1, (1, 0)) == transpose(m1) assert m1.tomatrix().T == permutedims(m1, (1, 0)).tomatrix() assert m1.tomatrix().T == transpose(m1).tomatrix() assert m1.tomatrix().C == conjugate(m1).tomatrix() assert m1.tomatrix().H == adjoint(m1).tomatrix() assert m1.tomatrix().T == m1.transpose().tomatrix() assert m1.tomatrix().C == m1.conjugate().tomatrix() assert m1.tomatrix().H == m1.adjoint().tomatrix() raises(ValueError, lambda: permutedims(m1, (0,))) raises(ValueError, lambda: permutedims(m1, (0, 0))) raises(ValueError, lambda: permutedims(m1, (1, 2, 0))) # Some tests with random arrays: dims = 6 shape = [random.randint(1,5) for i in range(dims)] elems = [random.random() for i in range(tensorproduct(*shape))] ra = Array(elems, shape) perm = list(range(dims)) # Randomize the permutation: random.shuffle(perm) # Test inverse permutation: assert permutedims(permutedims(ra, perm), _af_invert(perm)) == ra # Test that permuted shape corresponds to action by `Permutation`: assert permutedims(ra, perm).shape == tuple(Permutation(perm)(shape)) z = Array.zeros(4,5,6,7) assert permutedims(z, (2, 3, 1, 0)).shape == (6, 7, 5, 4) assert permutedims(z, [2, 3, 1, 0]).shape == (6, 7, 5, 4) assert permutedims(z, Permutation([2, 3, 1, 0])).shape == (6, 7, 5, 4) po = Array(sa, [2, 2, 3, 3, 2, 2]) raises(ValueError, lambda: permutedims(po, (1, 1))) raises(ValueError, lambda: po.transpose()) raises(ValueError, lambda: po.adjoint()) assert permutedims(po, reversed(range(po.rank()))) == Array( [[[[[[sa[0], sa[72]], [sa[36], sa[108]]], [[sa[12], sa[84]], [sa[48], sa[120]]], [[sa[24], sa[96]], [sa[60], sa[132]]]], [[[sa[4], sa[76]], [sa[40], sa[112]]], [[sa[16], sa[88]], [sa[52], sa[124]]], [[sa[28], sa[100]], [sa[64], sa[136]]]], [[[sa[8], sa[80]], [sa[44], sa[116]]], [[sa[20], sa[92]], [sa[56], sa[128]]], [[sa[32], sa[104]], [sa[68], sa[140]]]]], [[[[sa[2], sa[74]], [sa[38], sa[110]]], [[sa[14], sa[86]], [sa[50], sa[122]]], [[sa[26], sa[98]], [sa[62], sa[134]]]], [[[sa[6], sa[78]], [sa[42], sa[114]]], [[sa[18], sa[90]], [sa[54], sa[126]]], [[sa[30], sa[102]], [sa[66], sa[138]]]], [[[sa[10], sa[82]], [sa[46], sa[118]]], [[sa[22], sa[94]], [sa[58], sa[130]]], [[sa[34], sa[106]], [sa[70], sa[142]]]]]], [[[[[sa[1], sa[73]], [sa[37], sa[109]]], [[sa[13], sa[85]], [sa[49], sa[121]]], [[sa[25], sa[97]], [sa[61], sa[133]]]], [[[sa[5], sa[77]], [sa[41], sa[113]]], [[sa[17], sa[89]], [sa[53], sa[125]]], [[sa[29], sa[101]], [sa[65], sa[137]]]], [[[sa[9], sa[81]], [sa[45], sa[117]]], [[sa[21], sa[93]], [sa[57], sa[129]]], [[sa[33], sa[105]], [sa[69], sa[141]]]]], [[[[sa[3], sa[75]], [sa[39], sa[111]]], [[sa[15], sa[87]], [sa[51], sa[123]]], [[sa[27], sa[99]], [sa[63], sa[135]]]], [[[sa[7], sa[79]], [sa[43], sa[115]]], [[sa[19], sa[91]], [sa[55], sa[127]]], [[sa[31], sa[103]], [sa[67], sa[139]]]], [[[sa[11], sa[83]], [sa[47], sa[119]]], [[sa[23], sa[95]], [sa[59], sa[131]]], [[sa[35], sa[107]], [sa[71], sa[143]]]]]]]) assert permutedims(po, (1, 0, 2, 3, 4, 5)) == Array( [[[[[[sa[0], sa[1]], [sa[2], sa[3]]], [[sa[4], sa[5]], [sa[6], sa[7]]], [[sa[8], sa[9]], [sa[10], sa[11]]]], [[[sa[12], sa[13]], [sa[14], sa[15]]], [[sa[16], sa[17]], [sa[18], sa[19]]], [[sa[20], sa[21]], [sa[22], sa[23]]]], [[[sa[24], sa[25]], [sa[26], sa[27]]], [[sa[28], sa[29]], [sa[30], sa[31]]], [[sa[32], sa[33]], [sa[34], sa[35]]]]], [[[[sa[72], sa[73]], [sa[74], sa[75]]], [[sa[76], sa[77]], [sa[78], sa[79]]], [[sa[80], sa[81]], [sa[82], sa[83]]]], [[[sa[84], sa[85]], [sa[86], sa[87]]], [[sa[88], sa[89]], [sa[90], sa[91]]], [[sa[92], sa[93]], [sa[94], sa[95]]]], [[[sa[96], sa[97]], [sa[98], sa[99]]], [[sa[100], sa[101]], [sa[102], sa[103]]], [[sa[104], sa[105]], [sa[106], sa[107]]]]]], [[[[[sa[36], sa[37]], [sa[38], sa[39]]], [[sa[40], sa[41]], [sa[42], sa[43]]], [[sa[44], sa[45]], [sa[46], sa[47]]]], [[[sa[48], sa[49]], [sa[50], sa[51]]], [[sa[52], sa[53]], [sa[54], sa[55]]], [[sa[56], sa[57]], [sa[58], sa[59]]]], [[[sa[60], sa[61]], [sa[62], sa[63]]], [[sa[64], sa[65]], [sa[66], sa[67]]], [[sa[68], sa[69]], [sa[70], sa[71]]]]], [ [[[sa[108], sa[109]], [sa[110], sa[111]]], [[sa[112], sa[113]], [sa[114], sa[115]]], [[sa[116], sa[117]], [sa[118], sa[119]]]], [[[sa[120], sa[121]], [sa[122], sa[123]]], [[sa[124], sa[125]], [sa[126], sa[127]]], [[sa[128], sa[129]], [sa[130], sa[131]]]], [[[sa[132], sa[133]], [sa[134], sa[135]]], [[sa[136], sa[137]], [sa[138], sa[139]]], [[sa[140], sa[141]], [sa[142], sa[143]]]]]]]) assert permutedims(po, (0, 2, 1, 4, 3, 5)) == Array( [[[[[[sa[0], sa[1]], [sa[4], sa[5]], [sa[8], sa[9]]], [[sa[2], sa[3]], [sa[6], sa[7]], [sa[10], sa[11]]]], [[[sa[36], sa[37]], [sa[40], sa[41]], [sa[44], sa[45]]], [[sa[38], sa[39]], [sa[42], sa[43]], [sa[46], sa[47]]]]], [[[[sa[12], sa[13]], [sa[16], sa[17]], [sa[20], sa[21]]], [[sa[14], sa[15]], [sa[18], sa[19]], [sa[22], sa[23]]]], [[[sa[48], sa[49]], [sa[52], sa[53]], [sa[56], sa[57]]], [[sa[50], sa[51]], [sa[54], sa[55]], [sa[58], sa[59]]]]], [[[[sa[24], sa[25]], [sa[28], sa[29]], [sa[32], sa[33]]], [[sa[26], sa[27]], [sa[30], sa[31]], [sa[34], sa[35]]]], [[[sa[60], sa[61]], [sa[64], sa[65]], [sa[68], sa[69]]], [[sa[62], sa[63]], [sa[66], sa[67]], [sa[70], sa[71]]]]]], [[[[[sa[72], sa[73]], [sa[76], sa[77]], [sa[80], sa[81]]], [[sa[74], sa[75]], [sa[78], sa[79]], [sa[82], sa[83]]]], [[[sa[108], sa[109]], [sa[112], sa[113]], [sa[116], sa[117]]], [[sa[110], sa[111]], [sa[114], sa[115]], [sa[118], sa[119]]]]], [[[[sa[84], sa[85]], [sa[88], sa[89]], [sa[92], sa[93]]], [[sa[86], sa[87]], [sa[90], sa[91]], [sa[94], sa[95]]]], [[[sa[120], sa[121]], [sa[124], sa[125]], [sa[128], sa[129]]], [[sa[122], sa[123]], [sa[126], sa[127]], [sa[130], sa[131]]]]], [[[[sa[96], sa[97]], [sa[100], sa[101]], [sa[104], sa[105]]], [[sa[98], sa[99]], [sa[102], sa[103]], [sa[106], sa[107]]]], [[[sa[132], sa[133]], [sa[136], sa[137]], [sa[140], sa[141]]], [[sa[134], sa[135]], [sa[138], sa[139]], [sa[142], sa[143]]]]]]]) po2 = po.reshape(4, 9, 2, 2) assert po2 == Array([[[[sa[0], sa[1]], [sa[2], sa[3]]], [[sa[4], sa[5]], [sa[6], sa[7]]], [[sa[8], sa[9]], [sa[10], sa[11]]], [[sa[12], sa[13]], [sa[14], sa[15]]], [[sa[16], sa[17]], [sa[18], sa[19]]], [[sa[20], sa[21]], [sa[22], sa[23]]], [[sa[24], sa[25]], [sa[26], sa[27]]], [[sa[28], sa[29]], [sa[30], sa[31]]], [[sa[32], sa[33]], [sa[34], sa[35]]]], [[[sa[36], sa[37]], [sa[38], sa[39]]], [[sa[40], sa[41]], [sa[42], sa[43]]], [[sa[44], sa[45]], [sa[46], sa[47]]], [[sa[48], sa[49]], [sa[50], sa[51]]], [[sa[52], sa[53]], [sa[54], sa[55]]], [[sa[56], sa[57]], [sa[58], sa[59]]], [[sa[60], sa[61]], [sa[62], sa[63]]], [[sa[64], sa[65]], [sa[66], sa[67]]], [[sa[68], sa[69]], [sa[70], sa[71]]]], [[[sa[72], sa[73]], [sa[74], sa[75]]], [[sa[76], sa[77]], [sa[78], sa[79]]], [[sa[80], sa[81]], [sa[82], sa[83]]], [[sa[84], sa[85]], [sa[86], sa[87]]], [[sa[88], sa[89]], [sa[90], sa[91]]], [[sa[92], sa[93]], [sa[94], sa[95]]], [[sa[96], sa[97]], [sa[98], sa[99]]], [[sa[100], sa[101]], [sa[102], sa[103]]], [[sa[104], sa[105]], [sa[106], sa[107]]]], [[[sa[108], sa[109]], [sa[110], sa[111]]], [[sa[112], sa[113]], [sa[114], sa[115]]], [[sa[116], sa[117]], [sa[118], sa[119]]], [[sa[120], sa[121]], [sa[122], sa[123]]], [[sa[124], sa[125]], [sa[126], sa[127]]], [[sa[128], sa[129]], [sa[130], sa[131]]], [[sa[132], sa[133]], [sa[134], sa[135]]], [[sa[136], sa[137]], [sa[138], sa[139]]], [[sa[140], sa[141]], [sa[142], sa[143]]]]]) assert permutedims(po2, (3, 2, 0, 1)) == Array([[[[sa[0], sa[4], sa[8], sa[12], sa[16], sa[20], sa[24], sa[28], sa[32]], [sa[36], sa[40], sa[44], sa[48], sa[52], sa[56], sa[60], sa[64], sa[68]], [sa[72], sa[76], sa[80], sa[84], sa[88], sa[92], sa[96], sa[100], sa[104]], [sa[108], sa[112], sa[116], sa[120], sa[124], sa[128], sa[132], sa[136], sa[140]]], [[sa[2], sa[6], sa[10], sa[14], sa[18], sa[22], sa[26], sa[30], sa[34]], [sa[38], sa[42], sa[46], sa[50], sa[54], sa[58], sa[62], sa[66], sa[70]], [sa[74], sa[78], sa[82], sa[86], sa[90], sa[94], sa[98], sa[102], sa[106]], [sa[110], sa[114], sa[118], sa[122], sa[126], sa[130], sa[134], sa[138], sa[142]]]], [[[sa[1], sa[5], sa[9], sa[13], sa[17], sa[21], sa[25], sa[29], sa[33]], [sa[37], sa[41], sa[45], sa[49], sa[53], sa[57], sa[61], sa[65], sa[69]], [sa[73], sa[77], sa[81], sa[85], sa[89], sa[93], sa[97], sa[101], sa[105]], [sa[109], sa[113], sa[117], sa[121], sa[125], sa[129], sa[133], sa[137], sa[141]]], [[sa[3], sa[7], sa[11], sa[15], sa[19], sa[23], sa[27], sa[31], sa[35]], [sa[39], sa[43], sa[47], sa[51], sa[55], sa[59], sa[63], sa[67], sa[71]], [sa[75], sa[79], sa[83], sa[87], sa[91], sa[95], sa[99], sa[103], sa[107]], [sa[111], sa[115], sa[119], sa[123], sa[127], sa[131], sa[135], sa[139], sa[143]]]]])
def double_coset_can_rep(dummies, sym, b_S, sgens, S_transversals, g): """ Butler-Portugal algorithm for tensor canonicalization with dummy indices Parameters ========== dummies list of lists of dummy indices, one list for each type of index; the dummy indices are put in order contravariant, covariant [d0, -d0, d1, -d1, ...]. sym list of the symmetries of the index metric for each type. possible symmetries of the metrics * 0 symmetric * 1 antisymmetric * None no symmetry b_S base of a minimal slot symmetry BSGS. sgens generators of the slot symmetry BSGS. S_transversals transversals for the slot BSGS. g permutation representing the tensor. Returns ======= Return 0 if the tensor is zero, else return the array form of the permutation representing the canonical form of the tensor. Notes ===== A tensor with dummy indices can be represented in a number of equivalent ways which typically grows exponentially with the number of indices. To be able to establish if two tensors with many indices are equal becomes computationally very slow in absence of an efficient algorithm. The Butler-Portugal algorithm [3] is an efficient algorithm to put tensors in canonical form, solving the above problem. Portugal observed that a tensor can be represented by a permutation, and that the class of tensors equivalent to it under slot and dummy symmetries is equivalent to the double coset `D*g*S` (Note: in this documentation we use the conventions for multiplication of permutations p, q with (p*q)(i) = p[q[i]] which is opposite to the one used in the Permutation class) Using the algorithm by Butler to find a representative of the double coset one can find a canonical form for the tensor. To see this correspondence, let `g` be a permutation in array form; a tensor with indices `ind` (the indices including both the contravariant and the covariant ones) can be written as `t = T(ind[g[0]],..., ind[g[n-1]])`, where `n= len(ind)`; `g` has size `n + 2`, the last two indices for the sign of the tensor (trick introduced in [4]). A slot symmetry transformation `s` is a permutation acting on the slots `t -> T(ind[(g*s)[0]],..., ind[(g*s)[n-1]])` A dummy symmetry transformation acts on `ind` `t -> T(ind[(d*g)[0]],..., ind[(d*g)[n-1]])` Being interested only in the transformations of the tensor under these symmetries, one can represent the tensor by `g`, which transforms as `g -> d*g*s`, so it belongs to the coset `D*g*S`, or in other words to the set of all permutations allowed by the slot and dummy symmetries. Let us explain the conventions by an example. Given a tensor `T^{d3 d2 d1}{}_{d1 d2 d3}` with the slot symmetries `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}` `T^{a0 a1 a2 a3 a4 a5} = -T^{a4 a1 a2 a3 a0 a5}` and symmetric metric, find the tensor equivalent to it which is the lowest under the ordering of indices: lexicographic ordering `d1, d2, d3` and then contravariant before covariant index; that is the canonical form of the tensor. The canonical form is `-T^{d1 d2 d3}{}_{d1 d2 d3}` obtained using `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}`. To convert this problem in the input for this function, use the following ordering of the index names (- for covariant for short) `d1, -d1, d2, -d2, d3, -d3` `T^{d3 d2 d1}{}_{d1 d2 d3}` corresponds to `g = [4, 2, 0, 1, 3, 5, 6, 7]` where the last two indices are for the sign `sgens = [Permutation(0, 2)(6, 7), Permutation(0, 4)(6, 7)]` sgens[0] is the slot symmetry `-(0, 2)` `T^{a0 a1 a2 a3 a4 a5} = -T^{a2 a1 a0 a3 a4 a5}` sgens[1] is the slot symmetry `-(0, 4)` `T^{a0 a1 a2 a3 a4 a5} = -T^{a4 a1 a2 a3 a0 a5}` The dummy symmetry group D is generated by the strong base generators `[(0, 1), (2, 3), (4, 5), (0, 2)(1, 3), (0, 4)(1, 5)]` where the first three interchange covariant and contravariant positions of the same index (d1 <-> -d1) and the last two interchange the dummy indices themselves (d1 <-> d2). The dummy symmetry acts from the left `d = [1, 0, 2, 3, 4, 5, 6, 7]` exchange `d1 <-> -d1` `T^{d3 d2 d1}{}_{d1 d2 d3} == T^{d3 d2}{}_{d1}{}^{d1}{}_{d2 d3}` `g=[4, 2, 0, 1, 3, 5, 6, 7] -> [4, 2, 1, 0, 3, 5, 6, 7] = _af_rmul(d, g)` which differs from `_af_rmul(g, d)`. The slot symmetry acts from the right `s = [2, 1, 0, 3, 4, 5, 7, 6]` exchanges slots 0 and 2 and changes sign `T^{d3 d2 d1}{}_{d1 d2 d3} == -T^{d1 d2 d3}{}_{d1 d2 d3}` `g=[4,2,0,1,3,5,6,7] -> [0, 2, 4, 1, 3, 5, 7, 6] = _af_rmul(g, s)` Example in which the tensor is zero, same slot symmetries as above: `T^{d2}{}_{d1 d3}{}^{d1 d3}{}_{d2}` `= -T^{d3}{}_{d1 d3}{}^{d1 d2}{}_{d2}` under slot symmetry `-(0,4)`; `= T_{d3 d1}{}^{d3}{}^{d1 d2}{}_{d2}` under slot symmetry `-(0,2)`; `= T^{d3}{}_{d1 d3}{}^{d1 d2}{}_{d2}` symmetric metric; `= 0` since two of these lines have tensors differ only for the sign. The double coset D*g*S consists of permutations `h = d*g*s` corresponding to equivalent tensors; if there are two `h` which are the same apart from the sign, return zero; otherwise choose as representative the tensor with indices ordered lexicographically according to `[d1, -d1, d2, -d2, d3, -d3]` that is `rep = min(D*g*S) = min([d*g*s for d in D for s in S])` The indices are fixed one by one; first choose the lowest index for slot 0, then the lowest remaining index for slot 1, etc. Doing this one obtains a chain of stabilizers `S -> S_{b0} -> S_{b0,b1} -> ...` and `D -> D_{p0} -> D_{p0,p1} -> ...` where `[b0, b1, ...] = range(b)` is a base of the symmetric group; the strong base `b_S` of S is an ordered sublist of it; therefore it is sufficient to compute once the strong base generators of S using the Schreier-Sims algorithm; the stabilizers of the strong base generators are the strong base generators of the stabilizer subgroup. `dbase = [p0, p1, ...]` is not in general in lexicographic order, so that one must recompute the strong base generators each time; however this is trivial, there is no need to use the Schreier-Sims algorithm for D. The algorithm keeps a TAB of elements `(s_i, d_i, h_i)` where `h_i = d_i*g*s_i` satisfying `h_i[j] = p_j` for `0 <= j < i` starting from `s_0 = id, d_0 = id, h_0 = g`. The equations `h_0[0] = p_0, h_1[1] = p_1,...` are solved in this order, choosing each time the lowest possible value of p_i For `j < i` `d_i*g*s_i*S_{b_0,...,b_{i-1}}*b_j = D_{p_0,...,p_{i-1}}*p_j` so that for dx in `D_{p_0,...,p_{i-1}}` and sx in `S_{base[0],...,base[i-1]}` one has `dx*d_i*g*s_i*sx*b_j = p_j` Search for dx, sx such that this equation holds for `j = i`; it can be written as `s_i*sx*b_j = J, dx*d_i*g*J = p_j` `sx*b_j = s_i**-1*J; sx = trace(s_i**-1, S_{b_0,...,b_{i-1}})` `dx**-1*p_j = d_i*g*J; dx = trace(d_i*g*J, D_{p_0,...,p_{i-1}})` `s_{i+1} = s_i*trace(s_i**-1*J, S_{b_0,...,b_{i-1}})` `d_{i+1} = trace(d_i*g*J, D_{p_0,...,p_{i-1}})**-1*d_i` `h_{i+1}*b_i = d_{i+1}*g*s_{i+1}*b_i = p_i` `h_n*b_j = p_j` for all j, so that `h_n` is the solution. Add the found `(s, d, h)` to TAB1. At the end of the iteration sort TAB1 with respect to the `h`; if there are two consecutive `h` in TAB1 which differ only for the sign, the tensor is zero, so return 0; if there are two consecutive `h` which are equal, keep only one. Then stabilize the slot generators under `i` and the dummy generators under `p_i`. Assign `TAB = TAB1` at the end of the iteration step. At the end `TAB` contains a unique `(s, d, h)`, since all the slots of the tensor `h` have been fixed to have the minimum value according to the symmetries. The algorithm returns `h`. It is important that the slot BSGS has lexicographic minimal base, otherwise there is an `i` which does not belong to the slot base for which `p_i` is fixed by the dummy symmetry only, while `i` is not invariant from the slot stabilizer, so `p_i` is not in general the minimal value. This algorithm differs slightly from the original algorithm [3]: the canonical form is minimal lexicographically, and the BSGS has minimal base under lexicographic order. Equal tensors `h` are eliminated from TAB. Examples ======== >>> from sympy.combinatorics.permutations import Permutation >>> from sympy.combinatorics.perm_groups import PermutationGroup >>> from sympy.combinatorics.tensor_can import double_coset_can_rep, get_transversals >>> gens = [Permutation(x) for x in [[2, 1, 0, 3, 4, 5, 7, 6], [4, 1, 2, 3, 0, 5, 7, 6]]] >>> base = [0, 2] >>> g = Permutation([4, 2, 0, 1, 3, 5, 6, 7]) >>> transversals = get_transversals(base, gens) >>> double_coset_can_rep([list(range(6))], [0], base, gens, transversals, g) [0, 1, 2, 3, 4, 5, 7, 6] >>> g = Permutation([4, 1, 3, 0, 5, 2, 6, 7]) >>> double_coset_can_rep([list(range(6))], [0], base, gens, transversals, g) 0 """ size = g.size g = g.array_form num_dummies = size - 2 indices = list(range(num_dummies)) all_metrics_with_sym = all([_ is not None for _ in sym]) num_types = len(sym) dumx = dummies[:] dumx_flat = [] for dx in dumx: dumx_flat.extend(dx) b_S = b_S[:] sgensx = [h._array_form for h in sgens] if b_S: S_transversals = transversal2coset(size, b_S, S_transversals) # strong generating set for D dsgsx = [] for i in range(num_types): dsgsx.extend(dummy_sgs(dumx[i], sym[i], num_dummies)) idn = list(range(size)) # TAB = list of entries (s, d, h) where h = _af_rmuln(d,g,s) # for short, in the following d*g*s means _af_rmuln(d,g,s) TAB = [(idn, idn, g)] for i in range(size - 2): b = i testb = b in b_S and sgensx if testb: sgensx1 = [_af_new(_) for _ in sgensx] deltab = _orbit(size, sgensx1, b) else: deltab = {b} # p1 = min(IMAGES) = min(Union D_p*h*deltab for h in TAB) if all_metrics_with_sym: md = _min_dummies(dumx, sym, indices) else: md = [ min(_orbit(size, [_af_new(ddx) for ddx in dsgsx], ii)) for ii in range(size - 2) ] p_i = min([min([md[h[x]] for x in deltab]) for s, d, h in TAB]) dsgsx1 = [_af_new(_) for _ in dsgsx] Dxtrav = _orbit_transversal(size, dsgsx1, p_i, False, af=True) \ if dsgsx else None if Dxtrav: Dxtrav = [_af_invert(x) for x in Dxtrav] # compute the orbit of p_i for ii in range(num_types): if p_i in dumx[ii]: # the orbit is made by all the indices in dum[ii] if sym[ii] is not None: deltap = dumx[ii] else: # the orbit is made by all the even indices if p_i # is even, by all the odd indices if p_i is odd p_i_index = dumx[ii].index(p_i) % 2 deltap = dumx[ii][p_i_index::2] break else: deltap = [p_i] TAB1 = [] while TAB: s, d, h = TAB.pop() if min([md[h[x]] for x in deltab]) != p_i: continue deltab1 = [x for x in deltab if md[h[x]] == p_i] # NEXT = s*deltab1 intersection (d*g)**-1*deltap dg = _af_rmul(d, g) dginv = _af_invert(dg) sdeltab = [s[x] for x in deltab1] gdeltap = [dginv[x] for x in deltap] NEXT = [x for x in sdeltab if x in gdeltap] # d, s satisfy # d*g*s*base[i-1] = p_{i-1}; using the stabilizers # d*g*s*S_{base[0],...,base[i-1]}*base[i-1] = # D_{p_0,...,p_{i-1}}*p_{i-1} # so that to find d1, s1 satisfying d1*g*s1*b = p_i # one can look for dx in D_{p_0,...,p_{i-1}} and # sx in S_{base[0],...,base[i-1]} # d1 = dx*d; s1 = s*sx # d1*g*s1*b = dx*d*g*s*sx*b = p_i for j in NEXT: if testb: # solve s1*b = j with s1 = s*sx for some element sx # of the stabilizer of ..., base[i-1] # sx*b = s**-1*j; sx = _trace_S(s, j,...) # s1 = s*trace_S(s**-1*j,...) s1 = _trace_S(s, j, b, S_transversals) if not s1: continue else: s1 = [s[ix] for ix in s1] else: s1 = s # assert s1[b] == j # invariant # solve d1*g*j = p_i with d1 = dx*d for some element dg # of the stabilizer of ..., p_{i-1} # dx**-1*p_i = d*g*j; dx**-1 = trace_D(d*g*j,...) # d1 = trace_D(d*g*j,...)**-1*d # to save an inversion in the inner loop; notice we did # Dxtrav = [perm_af_invert(x) for x in Dxtrav] out of the loop if Dxtrav: d1 = _trace_D(dg[j], p_i, Dxtrav) if not d1: continue else: if p_i != dg[j]: continue d1 = idn assert d1[dg[j]] == p_i # invariant d1 = [d1[ix] for ix in d] h1 = [d1[g[ix]] for ix in s1] # assert h1[b] == p_i # invariant TAB1.append((s1, d1, h1)) # if TAB contains equal permutations, keep only one of them; # if TAB contains equal permutations up to the sign, return 0 TAB1.sort(key=lambda x: x[-1]) prev = [0] * size while TAB1: s, d, h = TAB1.pop() if h[:-2] == prev[:-2]: if h[-1] != prev[-1]: return 0 else: TAB.append((s, d, h)) prev = h # stabilize the SGS sgensx = [h for h in sgensx if h[b] == b] if b in b_S: b_S.remove(b) _dumx_remove(dumx, dumx_flat, p_i) dsgsx = [] for i in range(num_types): dsgsx.extend(dummy_sgs(dumx[i], sym[i], num_dummies)) return TAB[0][-1]
def _strip(g, base, orbits, transversals): """ Attempt to decompose a permutation using a (possibly partial) BSGS structure. This is done by treating the sequence ``base`` as an actual base, and the orbits ``orbits`` and transversals ``transversals`` as basic orbits and transversals relative to it. This process is called "sifting". A sift is unsuccessful when a certain orbit element is not found or when after the sift the decomposition doesn't end with the identity element. The argument ``transversals`` is a list of dictionaries that provides transversal elements for the orbits ``orbits``. Parameters ========== ``g`` - permutation to be decomposed ``base`` - sequence of points ``orbits`` - a list in which the ``i``-th entry is an orbit of ``base[i]`` under some subgroup of the pointwise stabilizer of ` `base[0], base[1], ..., base[i - 1]``. The groups themselves are implicit in this function since the only information we need is encoded in the orbits and transversals ``transversals`` - a list of orbit transversals associated with the orbits ``orbits``. Examples ======== >>> from sympy.combinatorics import Permutation >>> Permutation.print_cyclic = True >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics.permutations import Permutation >>> from sympy.combinatorics.util import _strip >>> S = SymmetricGroup(5) >>> S.schreier_sims() >>> g = Permutation([0, 2, 3, 1, 4]) >>> _strip(g, S.base, S.basic_orbits, S.basic_transversals) ((4), 5) Notes ===== The algorithm is described in [1],pp.89-90. The reason for returning both the current state of the element being decomposed and the level at which the sifting ends is that they provide important information for the randomized version of the Schreier-Sims algorithm. References ========== [1] Holt, D., Eick, B., O'Brien, E. "Handbook of computational group theory" See Also ======== sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims_random """ h = g._array_form base_len = len(base) for i in range(base_len): beta = h[base[i]] if beta == base[i]: continue if beta not in orbits[i]: return _af_new(h), i + 1 u = transversals[i][beta]._array_form h = _af_rmul(_af_invert(u), h) return _af_new(h), base_len + 1
def graph_certificate(gr): """ Return a certificate for the graph gr adjacency list The graph is assumed to be unoriented and without external lines. Associate to each vertex of the graph a symmetric tensor with number of indices equal to the degree of the vertex; indices are contracted when they correspond to the same line of the graph. The canonical form of the tensor gives a certificate for the graph. This is not an efficient algorithm to get the certificate of a graph. Examples ======== >>> from sympy.combinatorics.testutil import graph_certificate >>> gr1 = {0:[1,2,3,5], 1:[0,2,4], 2:[0,1,3,4], 3:[0,2,4], 4:[1,2,3,5], 5:[0,4]} >>> gr2 = {0:[1,5], 1:[0,2,3,4], 2:[1,3,5], 3:[1,2,4,5], 4:[1,3,5], 5:[0,2,3,4]} >>> c1 = graph_certificate(gr1) >>> c2 = graph_certificate(gr2) >>> c1 [0, 2, 4, 6, 1, 8, 10, 12, 3, 14, 16, 18, 5, 9, 15, 7, 11, 17, 13, 19, 20, 21] >>> c1 == c2 True """ from sympy.combinatorics.permutations import _af_invert from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, canonicalize items = list(gr.items()) items.sort(key=lambda x: len(x[1]), reverse=True) pvert = [x[0] for x in items] pvert = _af_invert(pvert) # the indices of the tensor are twice the number of lines of the graph num_indices = 0 for v, neigh in items: num_indices += len(neigh) # associate to each vertex its indices; for each line # between two vertices assign the # even index to the vertex which comes first in items, # the odd index to the other vertex vertices = [[] for i in items] i = 0 for v, neigh in items: for v2 in neigh: if pvert[v] < pvert[v2]: vertices[pvert[v]].append(i) vertices[pvert[v2]].append(i+1) i += 2 g = [] for v in vertices: g.extend(v) assert len(g) == num_indices g += [num_indices, num_indices + 1] size = num_indices + 2 assert sorted(g) == list(range(size)) g = Permutation(g) vlen = [0]*(len(vertices[0])+1) for neigh in vertices: vlen[len(neigh)] += 1 v = [] for i in range(len(vlen)): n = vlen[i] if n: base, gens = get_symmetric_group_sgs(i) v.append((base, gens, n, 0)) v.reverse() dummies = list(range(num_indices)) can = canonicalize(g, dummies, 0, *v) return can
def remove_identity_matrices(expr: ArrayContraction): editor = _EditArrayContraction(expr) removed: List[int] = [] permutation_map = {} free_indices = list( accumulate([0] + [ sum([i is None for i in arg.indices]) for arg in editor.args_with_ind ])) free_map = {k: v for k, v in zip(editor.args_with_ind, free_indices[:-1])} update_pairs = {} for ind in range(editor.number_of_contraction_indices): args = editor.get_args_with_index(ind) identity_matrices = [ i for i in args if isinstance(i.element, Identity) ] number_identity_matrices = len(identity_matrices) # If the contraction involves a non-identity matrix and multiple identity matrices: if number_identity_matrices != len( args) - 1 or number_identity_matrices == 0: continue # Get the non-identity element: non_identity = [ i for i in args if not isinstance(i.element, Identity) ][0] # Check that all identity matrices have at least one free index # (otherwise they would be contractions to some other elements) if any([None not in i.indices for i in identity_matrices]): continue # Mark the identity matrices for removal: for i in identity_matrices: i.element = None removed.extend( range(free_map[i], free_map[i] + len([j for j in i.indices if j is None]))) last_removed = removed.pop(-1) update_pairs[last_removed, ind] = non_identity.indices[:] # Remove the indices from the non-identity matrix, as the contraction # no longer exists: non_identity.indices = [ None if i == ind else i for i in non_identity.indices ] removed.sort() shifts = list( accumulate([1 if i in removed else 0 for i in range(get_rank(expr))])) for (last_removed, ind), non_identity_indices in update_pairs.items(): pos = [ free_map[non_identity] + i for i, e in enumerate(non_identity_indices) if e == ind ] assert len(pos) == 1 for j in pos: permutation_map[j] = last_removed editor.args_with_ind = [ i for i in editor.args_with_ind if i.element is not None ] ret_expr = editor.to_array_contraction() permutation = [] counter = 0 counter2 = 0 for j in range(get_rank(expr)): if j in removed: continue if counter2 in permutation_map: target = permutation_map[counter2] permutation.append(target - shifts[target]) counter2 += 1 else: while counter in permutation_map.values(): counter += 1 permutation.append(counter) counter += 1 counter2 += 1 ret_expr2 = _permute_dims(ret_expr, _af_invert(permutation)) return ret_expr2, removed
def identify_hadamard_products(expr: Union[ArrayContraction, ArrayDiagonal]): mapping = _get_mapping_from_subranks(expr.subranks) editor: _EditArrayContraction if isinstance(expr, ArrayContraction): editor = _EditArrayContraction(expr) elif isinstance(expr, ArrayDiagonal): if isinstance(expr.expr, ArrayContraction): editor = _EditArrayContraction(expr.expr) diagonalized = ArrayContraction._push_indices_down( expr.expr.contraction_indices, expr.diagonal_indices) elif isinstance(expr.expr, ArrayTensorProduct): editor = _EditArrayContraction(None) editor.args_with_ind = [ _ArgE(arg) for i, arg in enumerate(expr.expr.args) ] diagonalized = expr.diagonal_indices else: raise NotImplementedError("not implemented") # Trick: add diagonalized indices as negative indices into the editor object: for i, e in enumerate(diagonalized): for j in e: arg_pos, rel_pos = mapping[j] editor.args_with_ind[arg_pos].indices[rel_pos] = -1 - i map_contr_to_args: Dict[FrozenSet, List[_ArgE]] = defaultdict(list) map_ind_to_inds = defaultdict(int) for arg_with_ind in editor.args_with_ind: for ind in arg_with_ind.indices: map_ind_to_inds[ind] += 1 if None in arg_with_ind.indices: continue map_contr_to_args[frozenset(arg_with_ind.indices)].append(arg_with_ind) k: FrozenSet[int] v: List[_ArgE] for k, v in map_contr_to_args.items(): if len(k) != 2: # Hadamard product only defined for matrices: continue if len(v) == 1: # Hadamard product with a single argument makes no sense: continue for ind in k: if map_ind_to_inds[ind] <= 2: # There is no other contraction, skip: continue # Check if expression is a trace: if all([map_ind_to_inds[j] == len(v) and j >= 0 for j in k]): # This is a trace continue # This is a Hadamard product: def check_transpose(x): x = [i if i >= 0 else -1 - i for i in x] return x == sorted(x) hp = hadamard_product(*[ i.element if check_transpose(i.indices) else Transpose(i.element) for i in v ]) hp_indices = v[0].indices if not check_transpose(v[0].indices): hp_indices = list(reversed(hp_indices)) editor.insert_after(v[0], _ArgE(hp, hp_indices)) for i in v: editor.args_with_ind.remove(i) # Count the ranks of the arguments: counter = 0 # Create a collector for the new diagonal indices: diag_indices = defaultdict(list) count_index_freq = Counter() for arg_with_ind in editor.args_with_ind: count_index_freq.update(Counter(arg_with_ind.indices)) free_index_count = count_index_freq[None] # Construct the inverse permutation: inv_perm1 = [] inv_perm2 = [] # Keep track of which diagonal indices have already been processed: done = set([]) # Counter for the diagonal indices: counter4 = 0 for arg_with_ind in editor.args_with_ind: # If some diagonalization axes have been removed, they should be # permuted in order to keep the permutation. # Add permutation here counter2 = 0 # counter for the indices for i in arg_with_ind.indices: if i is None: inv_perm1.append(counter4) counter2 += 1 counter4 += 1 continue if i >= 0: continue # Reconstruct the diagonal indices: diag_indices[-1 - i].append(counter + counter2) if count_index_freq[i] == 1 and i not in done: inv_perm1.append(free_index_count - 1 - i) done.add(i) elif i not in done: inv_perm2.append(free_index_count - 1 - i) done.add(i) counter2 += 1 # Remove negative indices to restore a proper editor object: arg_with_ind.indices = [ i if i is not None and i >= 0 else None for i in arg_with_ind.indices ] counter += len([i for i in arg_with_ind.indices if i is None or i < 0]) inverse_permutation = inv_perm1 + inv_perm2 permutation = _af_invert(inverse_permutation) if isinstance(expr, ArrayContraction): return editor.to_array_contraction() else: # Get the diagonal indices after the detection of HadamardProduct in the expression: diag_indices_filtered = [ tuple(v) for v in diag_indices.values() if len(v) > 1 ] expr1 = editor.to_array_contraction() expr2 = ArrayDiagonal(expr1, *diag_indices_filtered) expr3 = PermuteDims(expr2, permutation) return expr3
def convert_indexed_to_array(expr, first_indices=None): r""" Parse indexed expression into a form useful for code generation. Examples ======== >>> from sympy.tensor.array.expressions.conv_indexed_to_array import convert_indexed_to_array >>> from sympy import MatrixSymbol, Sum, symbols >>> i, j, k, d = symbols("i j k d") >>> M = MatrixSymbol("M", d, d) >>> N = MatrixSymbol("N", d, d) Recognize the trace in summation form: >>> expr = Sum(M[i, i], (i, 0, d-1)) >>> convert_indexed_to_array(expr) ArrayContraction(M, (0, 1)) Recognize the extraction of the diagonal by using the same index `i` on both axes of the matrix: >>> expr = M[i, i] >>> convert_indexed_to_array(expr) ArrayDiagonal(M, (0, 1)) This function can help perform the transformation expressed in two different mathematical notations as: `\sum_{j=0}^{N-1} A_{i,j} B_{j,k} \Longrightarrow \mathbf{A}\cdot \mathbf{B}` Recognize the matrix multiplication in summation form: >>> expr = Sum(M[i, j]*N[j, k], (j, 0, d-1)) >>> convert_indexed_to_array(expr) ArrayContraction(ArrayTensorProduct(M, N), (1, 2)) Specify that ``k`` has to be the starting index: >>> convert_indexed_to_array(expr, first_indices=[k]) ArrayContraction(ArrayTensorProduct(N, M), (0, 3)) """ result, indices = _convert_indexed_to_array(expr) if any(isinstance(i, (int, Integer)) for i in indices): result = ArrayElement(result, indices) indices = [] if not first_indices: return result def _check_is_in(elem, indices): if elem in indices: return True if any(elem in i for i in indices if isinstance(i, frozenset)): return True return False repl = {j: i for i in indices if isinstance(i, frozenset) for j in i} first_indices = [repl.get(i, i) for i in first_indices] for i in first_indices: if not _check_is_in(i, indices): first_indices.remove(i) first_indices.extend( [i for i in indices if not _check_is_in(i, first_indices)]) def _get_pos(elem, indices): if elem in indices: return indices.index(elem) for i, e in enumerate(indices): if not isinstance(e, frozenset): continue if elem in e: return i raise ValueError("not found") permutation = _af_invert([_get_pos(i, first_indices) for i in indices]) if isinstance(result, ArrayAdd): return _array_add( *[_permute_dims(arg, permutation) for arg in result.args]) else: return _permute_dims(result, permutation)
def graph_certificate(gr): """ Return a certificate for the graph Parameters ========== gr : adjacency list Explanation =========== The graph is assumed to be unoriented and without external lines. Associate to each vertex of the graph a symmetric tensor with number of indices equal to the degree of the vertex; indices are contracted when they correspond to the same line of the graph. The canonical form of the tensor gives a certificate for the graph. This is not an efficient algorithm to get the certificate of a graph. Examples ======== >>> from sympy.combinatorics.testutil import graph_certificate >>> gr1 = {0:[1, 2, 3, 5], 1:[0, 2, 4], 2:[0, 1, 3, 4], 3:[0, 2, 4], 4:[1, 2, 3, 5], 5:[0, 4]} >>> gr2 = {0:[1, 5], 1:[0, 2, 3, 4], 2:[1, 3, 5], 3:[1, 2, 4, 5], 4:[1, 3, 5], 5:[0, 2, 3, 4]} >>> c1 = graph_certificate(gr1) >>> c2 = graph_certificate(gr2) >>> c1 [0, 2, 4, 6, 1, 8, 10, 12, 3, 14, 16, 18, 5, 9, 15, 7, 11, 17, 13, 19, 20, 21] >>> c1 == c2 True """ from sympy.combinatorics.permutations import _af_invert from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, canonicalize items = list(gr.items()) items.sort(key=lambda x: len(x[1]), reverse=True) pvert = [x[0] for x in items] pvert = _af_invert(pvert) # the indices of the tensor are twice the number of lines of the graph num_indices = 0 for v, neigh in items: num_indices += len(neigh) # associate to each vertex its indices; for each line # between two vertices assign the # even index to the vertex which comes first in items, # the odd index to the other vertex vertices = [[] for i in items] i = 0 for v, neigh in items: for v2 in neigh: if pvert[v] < pvert[v2]: vertices[pvert[v]].append(i) vertices[pvert[v2]].append(i + 1) i += 2 g = [] for v in vertices: g.extend(v) assert len(g) == num_indices g += [num_indices, num_indices + 1] size = num_indices + 2 assert sorted(g) == list(range(size)) g = Permutation(g) vlen = [0] * (len(vertices[0]) + 1) for neigh in vertices: vlen[len(neigh)] += 1 v = [] for i in range(len(vlen)): n = vlen[i] if n: base, gens = get_symmetric_group_sgs(i) v.append((base, gens, n, 0)) v.reverse() dummies = list(range(num_indices)) can = canonicalize(g, dummies, 0, *v) return can
def _support_function_tp1_recognize(contraction_indices, args): subranks = [get_rank(i) for i in args] coeff = reduce(lambda x, y: x * y, [arg for arg, srank in zip(args, subranks) if srank == 0], S.One) mapping = _get_mapping_from_subranks(subranks) new_contraction_indices = list(contraction_indices) newargs = args[:] # make a copy of the list removed = [None for i in newargs] cumul = list(accumulate([0] + [get_rank(arg) for arg in args])) new_perms = [ list(range(cumul[i], cumul[i + 1])) for i, arg in enumerate(args) ] for pi, contraction_pair in enumerate(contraction_indices): if len(contraction_pair) != 2: continue i1, i2 = contraction_pair a1, e1 = mapping[i1] a2, e2 = mapping[i2] while removed[a1] is not None: a1, e1 = removed[a1] while removed[a2] is not None: a2, e2 = removed[a2] if a1 == a2: trace_arg = newargs[a1] newargs[a1] = Trace(trace_arg)._normalize() new_contraction_indices[pi] = None continue if not isinstance(newargs[a1], MatrixExpr) or not isinstance( newargs[a2], MatrixExpr): continue arg1 = newargs[a1] arg2 = newargs[a2] if (e1 == 1 and e2 == 1) or (e1 == 0 and e2 == 0): arg2 = Transpose(arg2) if e1 == 1: argnew = arg1 * arg2 else: argnew = arg2 * arg1 removed[a2] = a1, e1 new_perms[a1][e1] = new_perms[a2][1 - e2] new_perms[a2] = None newargs[a1] = argnew newargs[a2] = None new_contraction_indices[pi] = None new_contraction_indices = [ i for i in new_contraction_indices if i is not None ] newargs2 = [arg for arg in newargs if arg is not None] if len(newargs2) == 0: return coeff tp = _a2m_tensor_product(*newargs2) tc = ArrayContraction(tp, *new_contraction_indices) new_perms2 = ArrayContraction._push_indices_up( contraction_indices, [i for i in new_perms if i is not None]) permutation = _af_invert( [j for i in new_perms2 for j in i if j is not None]) if permutation == [1, 0] and len(newargs2) == 1: return Transpose(newargs2[0]).doit() tperm = PermuteDims(tc, permutation) return tperm