def colour_singlets(operators: List[Operator], overcomplete=False): """Contracts colour indices into singlets.""" result = [] for op in operators: # collect colour indices colour_indices = [] for i in op.free_indices: if i.index_type == Index.get_index_types()["c"]: colour_indices.append(i) # separate up and down indices ups, downs = [], [] for i in colour_indices: if i.is_up: ups.append(i) else: downs.append(i) # can only contract into singlet with deltas if equal number of raised # and lowered colour indices. Otherwise need to contract with epsilons ΔL # = 2 EFT only contains 2 and 4 quark operators up to dim 11, so no need # to implement contraction with epsilons yet. Make exception for simple # contraction with epsilon if len(ups) == 3 and not downs: result.append(op * eps(" ".join(str(-i) for i in ups))) elif len(downs) == 3 and not ups: result.append(op * eps(" ".join(str(-i) for i in downs))) elif len(downs) == 3 and len(ups) == 3: result.append( op * eps(" ".join(str(-i) for i in ups)) * eps(" ".join(str(-i) for i in downs)) ) elif len(ups) != len(downs): raise ValueError("Cannot contract colour indices into a singlet.") delta_index_combos = [tuple(zip(perm, downs)) for perm in permutations(ups)] for combo in delta_index_combos: deltas = [delta(" ".join([(-j).label, (-i).label])) for i, j in combo] # multiply deltas into operator prod = op for d in deltas: prod *= d result.append(prod) if len(deltas) == 2 and overcomplete: a, b = deltas upa, downa = a.indices upb, downb = b.indices dummy = Index.fresh("c") up_index_str = " ".join(str(i) for i in (upa, upb, dummy)) down_index_str = " ".join(str(i) for i in (downa, downb, -dummy)) up_epsilon = eps(up_index_str) down_epsilon = eps(down_index_str) # append additional structure to results result.append(op * up_epsilon * down_epsilon) return result
def contract_su2_helper( left: Indexed, right: Indexed, out: Union[int, str], index_type: str ): """Returns epsilon structures so that `left * right * epsilon` for each structure transforms as `out`. Example: >>> contract_su2_helper(left=L("u0 i0"), right=L("u1 i1"), out=0, index_type="i") [eps(-i0, -i1)] """ # create an index dict mapping index type to position in dynkin str index_dict = {} for pos, (_, label) in enumerate(Index.get_dynkin_labels()): index_dict[label] = pos # if out is a string, take to be dynkin string and extract appropriate # dynkin label for index type if isinstance(out, int): n_target_indices = out else: n_target_indices = int(out[index_dict[index_type]]) n_input_indices = ( left.dynkin_ints[index_dict[index_type]] + right.dynkin_ints[index_dict[index_type]] ) assert n_target_indices <= n_input_indices assert (n_target_indices - n_input_indices) % 2 == 0 # if target indices == input indices, no need for epsilons if n_target_indices == n_input_indices: return [1] # get indices of index_type from tensors left_indices = left.indices_by_type[Index.get_index_types()[index_type]] right_indices = right.indices_by_type[Index.get_index_types()[index_type]] # There is in general more than one way to contract the indices. # Construct every possible way and return a list of results. # get different epsilon combinations shortest, longest = sorted([left_indices, right_indices], key=len) eps_index_combos = [tuple(zip(perm, shortest)) for perm in permutations(longest)] results = [] for combo in eps_index_combos: prod = 1 counter = n_input_indices for i, j in combo: prod *= eps(" ".join([(-i).label, (-j).label])) counter -= 2 if counter == n_target_indices: results.append(prod) return results
def test_extract_relabellings(): a = [eps("-i0 -i2"), eps("-i3 -i1")] b = [eps("-i3 -i2"), eps("-i2 -i1")] # relabellings action on a # 0 -> 2, 2 -> 3, 3 -> 2 # 1 -> 2, 0 -> 1 relabellings1 = [ (Index("-i0"), Index("-i2")), (Index("-i2"), Index("-i3")), (Index("-i3"), Index("-i2")), ] relabellings2 = [(Index("-i1"), Index("-i2")), (Index("-i0"), Index("-i1"))] assert extract_relabellings(a, b) == [relabellings1, relabellings2]
def majorana_partner(self): """New copy of fermion with colour indices flipped""" undotted, dotted, colour, isospin, _ = Index.indices_by_type( self.indices).values() colour = tuple(i.conj for i in colour) indices = undotted + dotted + colour + isospin return MajoranaFermion( self.label, indices, latex=self.latex, is_conj=self.is_conj, symmetry=self.symmetry, comm=FERMI, charges=None, )
def swap_colour_indices(self): """New copy of field with colour indices flipped. # TODO Refactor this out with majorana_partner to FieldType """ undotted, dotted, colour, isospin, _ = Index.indices_by_type( self.indices).values() colour = tuple(i.conj for i in colour) indices = undotted + dotted + colour + isospin return RealScalar( self.label, indices, latex=self.latex, is_conj=self.is_conj, symmetry=self.symmetry, comm=BOSE, charges=None, )
def su2_singlets_type(op: Operator, index_type: str) -> List[Operator]: """Contracts su2 indices into singlets by type.""" result = [] # collect undotted indices indices = [] for i in op.free_indices: if i.index_type == Index.get_index_types()[index_type]: indices.append(i) # can only contract into singlet if even number of indices if len(indices) % 2: raise ValueError("Cannot contract odd number of indices into a singlet.") for combo in epsilon_combos(indices): epsilons = [eps(" ".join([(-i).label, (-j).label])) for i, j in combo] # multiply epsilons into operator prod = op for e in epsilons: prod *= e result.append(prod) return result
def dirac_partner(self): undotted, dotted, colour, isospin, _ = Index.indices_by_type( self.indices).values() colour = tuple(i.conj for i in colour) indices = undotted + dotted + colour + isospin charges = {k: -v for k, v in self.charges.items()} is_unbarred = self.is_unbarred symb = "~" # indicate bar with circumflex if is_unbarred: label = self.label + symb else: label = self.label.replace(symb, "") return VectorLikeDiracFermion( label, indices, charges=charges, latex=self.latex, is_unbarred=(not self.is_unbarred), is_conj=self.is_conj, symmetry=self.symmetry, comm=FERMI, )
def exotic_field_and_term( op: Operator, symbols: Dict[str, List[str]], field_dict: Dict[tuple, str] ) -> Tuple[IndexedField, IndexedField, Union[Operator, str]]: """Returns exotic field, partner (that couples in Lagrangian) and Lagrangian term. Mutates field_dict with exotic field's symbol. The last item returned may be a string explaining why the contraction failed. """ fields = [f for f in op.tensors if isinstance(f, IndexedField)] exotic_charges = {} pairs = list(map(lambda x: x.charges.items(), fields)) # ensure no fields have missing or extra charges fst, *rst = pairs for pair in rst: assert len(pair) == len(fst) for n_pairs in zip(*pairs): # fish out key (k, _), *rst = n_pairs # sum over values exotic_charges[k] = sum(map(lambda x: x[1], n_pairs)) indices_by_type = op.indices_by_type.values() exotic_undotted, exotic_dotted, exotic_colour, exotic_isospin, _ = map( sorted, indices_by_type ) exotic_indices = " ".join( str(i) for i in [*exotic_undotted, *exotic_dotted, *exotic_colour, *exotic_isospin] ) # establish fermion or boson for symbols lorentz_dynkin = get_dynkin(exotic_indices)[:2] if lorentz_dynkin in ("10", "01"): symbols_to_use = symbols["fermion"] else: symbols_to_use = symbols["boson"] fs = sorted([f.field for f in fields], key=lambda x: x.label_with_dagger) fs = tuple(fs) if fs in field_dict.keys(): symbol = field_dict[fs] else: symbol = symbols_to_use.pop(0) # field_dict mutated here! field_dict[fs] = symbol # for Dirac and Majorana fermions, always keep plain symbol left handed to_conj = False if exotic_indices and exotic_indices[0] == "d": exotic_indices = Index.conj_index_string(exotic_indices) to_conj = True exotic_indexed_field = IndexedField( label=symbol, indices=exotic_indices, charges=exotic_charges ) # construct MajoranaFermion, VectorLikeDiracFermion, ... # `partner` is in the Lagrangian, `exotic_field` transforms like contracted pair exotic_field = cons_completion_field(exotic_indexed_field) exotic_field = exotic_field.conj_indices if to_conj else exotic_field partner = exotic_field if isinstance(exotic_field, ComplexScalar): partner = exotic_field.conj elif isinstance(exotic_field, RealScalar): partner = exotic_field.swap_colour_indices() elif isinstance(exotic_field, VectorLikeDiracFermion): partner = exotic_field.dirac_partner() elif isinstance(exotic_field, MajoranaFermion): partner = exotic_field.majorana_partner() # Need additional su2 epsilons to fix su2 indices (since not working with # lowered indices at all). Won't need to do this if removing a derivative in # the process partner, fix_su2_epsilons = partner.lower_su2() term = reduce(lambda x, y: x * y, fix_su2_epsilons, op * partner) # construct term and check to see if vanishes. This is a very costly step, # check first whether there are any doubled up fields in the term and only # run on those set_fields = set([f.label for f in term.fields]) if len(set_fields) < len(term.fields) and term.safe_simplify() == 0: return exotic_field, partner, f"Vanishing coupling at {term}" # need to construct term again because sympy is annoying term = reduce(lambda x, y: x * y, fix_su2_epsilons, op * partner) check_singlet(term) return exotic_field, partner, term
def make_coupling(symbol: str, indices: str, sym=None): indices_list = indices.split() if sym is None: sym = [[1]] * len(indices_list) return tensorhead(symbol, [Index(i) for i in indices_list], sym)