# -*- coding: utf-8 -*-
import copy
import itertools
from collections import namedtuple, Counter
from common.utils import PicklableObject
from interactions.four_particle import FourParticleMHelper class that takes care of creating all necessary Integrals for the actual interaction.
class Interaction(PicklableObject):Human-friendly interaction identifier
    name = "Particle interaction"
    integrals = None
    particles = None  # All particles involvedTemperature when the typical interaction time exceeds the Hubble expansion time
    decoupling_temperature = 0.Create an Integral object for all particle species involved in the interaction.
Precise expressions for all integrals can be derived by permuting all particle-related functions in the distribution functions, matrix elements, momenta, energy and mass arrays.
To avoid double-counting, one should create an integral for each particle specie only once.
Additionally, one has to generate the integrals for all the crossing processes by\
    def __init__(self, **kwargs):
        for key in kwargs:
            setattr(self, key, kwargs[key])
        self.integrals = []
        for reaction in self.reactions_map():
            lhs = [item for item in reaction if item.side == -1]
            item = lhs[0]Skip collision integrals for anti-particles
            if item.antiparticle:
                continue
            self.init_integrals(reaction, item)String-like representation of the interaction and all its integral
    def __str__(self):
        return self.name + "\n\t" + "\n\t".join([str(integral) for integral in self.integrals])    @classmethod
    def cross_particle(cls, item):
        return item._replace(
            crossed=not item.crossed,
            antiparticle=not item.antiparticle if not item.specie.majorana else False,
            side=-item.side
        )    def permutations(self, particles, antiparticles):
        return itertools.permutations(
            [
                IntegralItem(side=-1,
                             specie=particle,
                             antiparticle=antiparticle,
                             index=i,
                             crossed=False)
                for i, (particle, antiparticle)
                in enumerate(zip(particles[0], antiparticles[0]))
            ] + [
                IntegralItem(side=1,
                             specie=particle,
                             antiparticle=antiparticle,
                             index=i,
                             crossed=False)
                for i, (particle, antiparticle)
                in enumerate(zip(particles[1], antiparticles[1]), len(particles[0]))
            ]
        )Return all relevant collision integrals involving count particles 
    def reactions_map(self):
        reactions = {}
        perms = self.permutations(self.particles, self.antiparticles)perms = itertools.chain( self.permutations(self.particles, self.antiparticles), self.permutations(tuple(reversed(self.particles)), tuple(reversed(self.antiparticles))) )
if any(specie.majorana for specie in itertools.chain.from_iterable(self.particles)): print "There is Majorana in da house!" conjugated_antiparticles = ( tuple(not antip if not specie.majorana else False for specie, antip in zip(self.particles[0], self.antiparticles[0])), tuple(not antip if not specie.majorana else False for specie, antip in zip(self.particles[1], self.antiparticles[1])) ) perms = itertools.chain(perms, self.permutations(self.particles, conjugated_antiparticles))
Generate possible reactions by inserting an arrow at every position
            for i in range(sum(map(len, self.particles)) - 1):Cross particles according to their position relative to the reaction arrow
                reaction = tuple(
                    Interaction.cross_particle(item)
                    if (item.side > 0 and j <= i) or (item.side < 0 and j > i)
                    else item
                    for j, item in enumerate(perm)
                )
                lhs = [item for item in reaction if item.side == -1]
                rhs = [item for item in reaction if item.side == 1]
                lhs = lhs[0:1] + sorted(lhs[1:])
                rhs = sorted(rhs)
                if lhs[0].antiparticle:
                    continueSkip kinematically impossible reactions
                if (
                    (len(lhs) == 1 and lhs[0].specie.mass <= sum(item.specie.mass for item in rhs))
                    or
                    (len(rhs) == 1 and rhs[0].specie.mass <= sum(item.specie.mass for item in lhs))
                ):
                    continue
                k = tuple(map(tuple, lhs + rhs))
                if k not in reactions:
                    reactions[k] = tuple(lhs + rhs)Starting from the single representation of the interaction, create integrals objects to be computed for every particle specie involved
    def init_integrals(self, reaction, item):
        particle = item.specie
        particle_Ms = copy.deepcopy(self.Ms)
        for M in particle_Ms:
            if isinstance(M, FourParticleM):
                map = {item.index: i for i, item in enumerate(reaction)}
                M.apply_order(tuple(map[val] for val in M.order), reaction)Add interaction integrals by putting each incoming particle as the first one
        self.integrals.append(self.integral(
            particle=particle,
            reaction=reaction,
            decoupling_temperature=self.decoupling_temperature,
            Ms=particle_Ms,
        ))class IntegralSet(PicklableObject):
    integrals = None
    def __init__(self, new_integrals):
        self.integrals = []
        for new_integral in new_integrals:
            self.append(new_integral)    def species(self, integral):
        return (
            Counter(item.specie for item in integral.reaction if item.side == -1),
            Counter(item.specie for item in integral.reaction if item.side == 1)
        )Check if there is already a similar integral
        for integral in self.integrals:
            if new_integral.particle == integral.particle and new_species == self.species(integral):
                old_integral = integral
                breakIf there is no, just append a new one
        if not old_integral:
            self.integrals.append(new_integral)
            return