Module cgc.Collision

Expand source code
from .Wavefunction import Wavefunction

import numpy as np
from scipy.fft import ifft2, fft2

import numba

CACHE_OPTIMIZATIONS = True

class Collision():

    targetWavefunction = None # Implements wilson line
    incidentWavefunction = None # Doesn't (have to) implement wilson line

    _omega = None
    _omegaFFT = None
    _particlesProduced = None
    _particlesProducedDeriv = None
    _momentaMagSquared = None
    _momentaComponents = None
    _thetaInFourierSpace = None
    _momentaBins = None
    _fourierHarmonics = None # This will be initialized as an empty dict to store harmonics (see __init__)

    _omegaExists = False
    _omegaFFTExists = False
    _momentaComponentsExist = False
    _particlesProducedExists = False
    _particlesProducedDerivExists = False
    _momentaBinsExists = False

    def __init__(self, wavefunction1: Wavefunction, wavefunction2: Wavefunction):
        r"""
        Initialize a collision with two wavefunctions, presumably a nucleus and a proton. One must implement
        the wilson line, though the order of the arguments does not matter.

        In the case that both wavefunctions implement the wilson line, the first (wavefunction1) will be used as such.

        In the case that neither implement the wilson line, an exception will be raised.

        Parameters
        ----------
        wavefunction1 : Wavefunction (or child)
            The first wavefunction

        wavefunction2 : Wavefunction (or child)
            The second wavefunction
        """

        # Make sure that at least one has a wilson line
        wilsonLineExists1 = callable(getattr(wavefunction1, "wilsonLine", None))
        wilsonLineExists2 = callable(getattr(wavefunction2, "wilsonLine", None))

        if not wilsonLineExists1 and not wilsonLineExists2:
            raise Exception("Neither of the wavefunctions passed to Collision(Wavefunction, Wavefunction) implement the wilsonLine() method; at least one is required to.")

        if wilsonLineExists1 and not wilsonLineExists2:
            self.targetWavefunction = wavefunction1
            self.incidentWavefunction = wavefunction2
        elif wilsonLineExists2 and not wilsonLineExists1:
            self.targetWavefunction = wavefunction2
            self.incidentWavefunction = wavefunction1
        else:
            self.targetWavefunction = wavefunction1
            self.incidentWavefunction = wavefunction2

        # Make sure that both use the same number of colors
        if self.targetWavefunction.gluonDOF != self.incidentWavefunction.gluonDOF:
            raise Exception(f"Wavefunctions implement different gluon degrees of freedom (number of color charges): {self.incidentWavefunction.gluonDOF} vs. {self.targetWavefunction.gluonDOF}")

        # Probably some other checks that need to be done to make sure the two wavefunctions are compatable, but this is fine for now

        # Carry over some variables so we don't have to call through the wavefunctions so much
        self.N = self.targetWavefunction.N
        self.length = self.targetWavefunction.length
        self.gluonDOF = self.targetWavefunction.gluonDOF
        self.delta = self.targetWavefunction.delta
        self.delta2 = self.targetWavefunction.delta2
        #print(self.targetWavefunction)
        #print(self.incidentWavefunction)

        
        # Variables to do with binning the momenta later on
        self.binSize = 4*np.pi/self.length
        self.kMax = 2/self.delta
        self.numBins = int(self.kMax/self.binSize)

        # This has to be initialized as an empty dict within the constructor
        # because otherwise it can retain information across separate objects
        # (no idea how, but this fixes it)
        self._fourierHarmonics = {}

    def omega(self, forceCalculate=False):
        r"""
        Calculate the field omega at each point on the lattice.

        If the field already exists, it is simply returned and no calculation is done.

        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        omega : array(2, 2, `colorCharges`**2 - 1, N, N)

        """

        if self._omegaExists and not forceCalculate:
            return self._omega

        self._omega = _calculateOmegaOpt(self.N, self.gluonDOF, self.delta, self.incidentWavefunction.gaugeField(), self.targetWavefunction.adjointWilsonLine())

        self._omegaExists = True
        return self._omega


    def omegaFFT(self, forceCalculate=False):
        r"""
        Compute the fourier transform of the field omega on the lattice.

        If the fft of the field already exists, it is simply returned and no calculation is done.

        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        omegaFFT : array(2, 2, `colorCharges`**2 - 1, N, N)

        """
        if self._omegaFFTExists and not forceCalculate:
            return self._omegaFFT

        # Make sure omega exists
        self.omega()

        self._omegaFFT = fft2(self._omega, axes=(-2, -1), norm='backward')
        self._omegaFFTExists = True

        return self._omegaFFT


    def momentaBins(self, forceCalculate=False):
        r"""
        Compute the range of momenta at which particles will be created based on the dimensions of the lattice.

        The exact values are:

        - \( k_{max} = 2 / \Delta\)
        - \( w_k = 4 \pi / L \)

        If the bins already exist, they are simply returned and no calculation is done.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        momentaBins : array(numBins = L / (delta 2 pi))
        """

        if self._momentaBinsExists and not forceCalculate:
            return self._momentaBins

        self._momentaBins = [i*self.binSize for i in range(self.numBins)]
        self._momentaBinsExists = True

        return self._momentaBins


    def momentaComponents(self, forceCalculate=False):
        r"""
        Compute the components of the momentum at each point on the lattice, according to:

        $$ (k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right) $$

        where \(i\) and \(j\) index the \(x\) and \(y\) directions in real space, respectively.

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        momentaComponents : array(N, N, 2)
        """

        if self._momentaComponentsExist and not forceCalculate:
            return self._momentaComponents

        self._momentaComponents, self._thetaInFourierSpace = _calculateMomentaOpt(self.N, self.delta)
        self._momentaMagSquared = np.linalg.norm(self._momentaComponents, axis=2)**2

        self._momentaComponentsExist = True

        return self._momentaComponents


    def momentaMagnitudeSquared(self, forceCalculate=False):
        r"""
        Compute the magnitude of the momentum at each point on the lattice, according to:

        $$ |k| = \sqrt{k_x^2 + k_y^2} $$

        $$ (k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right) $$

        where \(i\) and \(j\) index the \(x\) and \(y\) directions in real space, respectively.

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        momentaComponents : array(N, N)
        """

        if self._momentaComponentsExist and not forceCalculate:
            return self._momentaMagSquared

        self._momentaComponents, self._thetaInFourierSpace = _calculateMomentaOpt(self.N, self.delta)
        self._momentaMagSquared = np.linalg.norm(self._momentaComponents, axis=2)**2

        self._momentaComponentsExist = True

        return self._momentaMagSquared


    def particlesProducedDeriv(self, forceCalculate=False):
        r"""
        Compute the derivative of particles produced (\( \frac{d^2 N}{d^2 k} \)) at each point on the lattice
        
        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        particlesProducedDeriv : array(N, N)
        """
        if self._particlesProducedDerivExists and not forceCalculate:
            return self._particlesProducedDeriv

        # Make sure these quantities exist
        self.omegaFFT()
        self.momentaMagnitudeSquared() # This also calculates thetaInFourierSpace and momentaComponents

        self._particlesProducedDeriv = _calculateParticlesProducedDerivOpt(self.N, self.gluonDOF, self._momentaMagSquared, self._omegaFFT)

        self._particlesProducedDerivExists = True

        return self._particlesProducedDeriv
 

    def particlesProduced(self, forceCalculate=False):
        r"""
        Compute the number of particles produced \(N(|k|)\) as a function of momentum. Note that this
        is technically the zeroth fourier harmonic, so this actually just calls the
        cgc.Collision.fourierHarmonic() function.

        The particles are binned according to cgc.Collision.momentaBins().
       
        Most likely will be plotted against cgc.Collision.momentaBins().

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        particlesProduced : array(numBins = L / (delta 2 pi))
        """
        # This one is strictly real, so we should make sure that is updated        
        self._fourierHarmonics[0] = np.real(self.fourierHarmonic(0, forceCalculate))
        return self._fourierHarmonics[0]


    def fourierHarmonic(self, harmonic: int, forceCalculate=False):
        r"""
        Calculate the fourier harmonic of the particle production as:

        $$ v_n = \frac{ \sum_{(i,j)\in [k, k+ \Delta k]} |k| \frac{d^2 N}{d^2 k} e^{i n \theta }} { \sum_{(i,j)\in [k, k+ \Delta k]} |k| } $$

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        particlesProduced : array(numBins = L / (delta 2 pi))
        """
        # First, see if we have already calculated this harmonic
        if harmonic in self._fourierHarmonics.keys() and not forceCalculate:
            return self._fourierHarmonics[harmonic]

        # For actually calculating the harmonic, we first have to make sure we've calculated
        # the derivative, dN/d^2k
        # This makes sure that _momentaMagSquared, _thetaInFourierSpace and _particlesProducedDeriv
        # all exist
        self.particlesProducedDeriv()

        # Drop all of our arrays into long 1D structure, since we will want to bin them
        vectorizedParticleDerivs = np.reshape(self._particlesProducedDeriv, [self.N*self.N])
        vectorizedTheta = np.reshape(self._thetaInFourierSpace, [self.N*self.N])
        vectorizedMomentaMag = np.reshape(np.sqrt(self._momentaMagSquared), [self.N*self.N])
       
        # The number of particles that are produced in each bin
        # These bins are actually just thin rings in momentum space
        self._fourierHarmonics[harmonic] = np.zeros(self.numBins, dtype='complex')
        # The bin sizes/bounds are calculated for elsewhere
        self.momentaBins()

        momentaNormalization = np.zeros(self.numBins)

        for i in range(self.N):
            for j in range(self.N):
                k = np.sqrt(self._momentaMagSquared[i,j])
                indexK = int(np.floor(k / self.binSize)) 
                # Anything greater than the highest bin just gets added to the last bin
                # @TODO: Check if this is what we should be doing
                indexK = min(indexK, self.numBins-1)

                self._fourierHarmonics[harmonic][indexK] += self._particlesProducedDeriv[i,j] * k * np.exp(1.j * self._thetaInFourierSpace[i,j] * harmonic)
                momentaNormalization[indexK] += k
                

        for i in range(self.numBins):
            self._fourierHarmonics[harmonic][i] /= momentaNormalization[i]

        # Ideally, these rings should be only have a thickness dk (infinitesimal)
        # but since we have a discrete lattice, we weight the particles by their momentum
        # (which may slightly vary) and then properly normalize

        # Go through each bin and calculate (for all points in that bin):
        # 1. Sum over |k| * dN/d^2k * exp(i * harmonic * theta)
        # 2. Sum over |k|
        # 3. Divide 1./2.
        #for i in range(self.numBins):
        #    # Find which places on the lattice fall into this particular momentum bin
        #    # Note the use of element-wise (or bitwise) and, "&"
        #    particleDerivsInRing = vectorizedParticleDerivs[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]
        #    momentaMagInRing = vectorizedMomentaMag[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]
        #    thetaInRing = vectorizedTheta[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]

        #    # @TODO: make sure this is correct
        #    # Note that multiplication is done element-wise by default
        #    numeratorSum = np.sum(particleDerivsInRing * momentaMagInRing * np.exp(1.j * harmonic * thetaInRing))
           
        #    denominatorSum = np.sum(momentaMagInRing)

        #    self._fourierHarmonics[harmonic][i] = numeratorSum / denominatorSum


        return self._fourierHarmonics[harmonic]


@numba.jit(nopython=True)
def _x_deriv(matrix, i, j, N, delta):
    return (matrix[i,(j+1)%N] - matrix[i,j-1]) / (2 * delta)

@numba.jit(nopython=True)
def _y_deriv(matrix, i, j, N, delta):
    return (matrix[(i+1)%N,j] - matrix[i-1,j]) / (2 * delta)

# This doesn't work because of the above two derivative functions;
# need to figure out a way for it to compile
#@numba.jit(nopython=True, cache=CACHE_OPTIMIZATIONS)
def _calculateOmegaOpt(N, gluonDOF, delta, incidentGaugeField, targetAdjointWilsonLine):
    """
    Calculate the field omega at each point on the lattice.

    If the field already exists, it is simply returned and no calculation is done.

    Returns
    -------
    numpy.array : shape=(2, 2, `colorCharges`**2 - 1, N, N)

    """

    # 2,2 is for the 2 dimensions, x and y
    omega = np.zeros((2, 2, gluonDOF, N, N), dtype='complex') # 2 is for two dimensions, x and y

    derivs = [_x_deriv, _y_deriv]

    for i in range(N):
        for j in range(N):
            for k in range(gluonDOF):
                for l in range(2): # 2 is number of dimensions
                    for n in range(2): # 2 is number of dimensions
                        omega[l,n,k,i,j] = np.sum([derivs[l](incidentGaugeField[m], i, j, N, delta) * derivs[n](targetAdjointWilsonLine[k, m], i, j, N, delta) for m in range(gluonDOF)])

    return omega


@numba.jit(nopython=True, cache=CACHE_OPTIMIZATIONS)
def _calculateMomentaOpt(N, delta):
    """
    Optimized (via numba) function to calculated the position (momentum) in Fourier space of each point

    Parameters
    ----------

    N : int
        Size of the lattice

    delta : double
        Spacing between each point

    Returns
    -------
    (momentaComponents, theta)

    momentaComponents : array(N, N, 2)
        x and y components of the momentum at each point

    theta : array(N, N)
        Relationship between x and y components at each point, or atan2(k_y, k_x)

    """
    momentaMagSquared = np.zeros((N, N))
    momentaComponents = np.zeros((N, N, 2))
    theta = np.zeros((N, N))

    for i in range(N):
        for j in range(N):
            # Note that these components are of the form:
            # k_x = 2/a sin(k_x' a / 2)
            # Though the argument of the sin is simplified a bit
            # @TODO: make sure this is correct prefactor/argument
            momentaComponents[i,j] = [2/delta * np.sin(np.pi*i/N), 2/delta * np.sin(np.pi*j/N)]
            theta[i,j] = np.arctan2(momentaComponents[i,j,1], momentaComponents[i,j,0])

    return momentaComponents, theta

@numba.jit(nopython=True, cache=CACHE_OPTIMIZATIONS)
def _calculateParticlesProducedDerivOpt(N, gluonDOF, momentaMagSquared, omegaFFT):
    """
    Optimized (via numba) function to calculate dN/d^2k

    Parameters
    ----------

    N : int
        The system size

    gluonDOF : int
        The number of gluon degrees of freedom ((possible color charges)^2 - 1)

    momentaMagSquared : array(N, N)
        The magnitude of the momentum at each point, likely calculated (in part) with _calculateMomentaOpt()

    omegaFFT : array(2, 2, gluonDOF, N, N)
        Previously calculated omega array

    Returns
    -------
    particleProduction : array(N, N)
        The number of particles produced at each point on the momentum lattice

    """
    # Where we will calculate dN/d^2k 
    particleProduction = np.zeros((N,N))

    # # 2D Levi-Cevita symbol
    LCS = np.array([[0,1],[-1,0]])

    # # 2D Delta function
    KDF = np.array([[1,0],[0,1]])

    for y in range(N):
        for x in range(N):
            # To prevent any divide by zero errors
            if momentaMagSquared[y,x] == 0:
                continue
            
            # All of these 2s are for our two dimensions, x and y
            for i in range(2):
                for j in range(2):
                    for l in range(2):
                        for m in range(2):

                            for a in range(gluonDOF):
                                particleProduction[y,x] += np.real(2/(2*np.pi)**3 / momentaMagSquared[y,x] * (
                                    (KDF[i,j]*KDF[l,m] + LCS[i,j]*LCS[l,m])) * (
                                        omegaFFT[i,j,a,y,x] * np.conj(omegaFFT[l,m,a,y,x])))

    return particleProduction

Classes

class Collision (wavefunction1: Wavefunction, wavefunction2: Wavefunction)

Initialize a collision with two wavefunctions, presumably a nucleus and a proton. One must implement the wilson line, though the order of the arguments does not matter.

In the case that both wavefunctions implement the wilson line, the first (wavefunction1) will be used as such.

In the case that neither implement the wilson line, an exception will be raised.

Parameters

wavefunction1 : Wavefunction (or child)
The first wavefunction
wavefunction2 : Wavefunction (or child)
The second wavefunction
Expand source code
class Collision():

    targetWavefunction = None # Implements wilson line
    incidentWavefunction = None # Doesn't (have to) implement wilson line

    _omega = None
    _omegaFFT = None
    _particlesProduced = None
    _particlesProducedDeriv = None
    _momentaMagSquared = None
    _momentaComponents = None
    _thetaInFourierSpace = None
    _momentaBins = None
    _fourierHarmonics = None # This will be initialized as an empty dict to store harmonics (see __init__)

    _omegaExists = False
    _omegaFFTExists = False
    _momentaComponentsExist = False
    _particlesProducedExists = False
    _particlesProducedDerivExists = False
    _momentaBinsExists = False

    def __init__(self, wavefunction1: Wavefunction, wavefunction2: Wavefunction):
        r"""
        Initialize a collision with two wavefunctions, presumably a nucleus and a proton. One must implement
        the wilson line, though the order of the arguments does not matter.

        In the case that both wavefunctions implement the wilson line, the first (wavefunction1) will be used as such.

        In the case that neither implement the wilson line, an exception will be raised.

        Parameters
        ----------
        wavefunction1 : Wavefunction (or child)
            The first wavefunction

        wavefunction2 : Wavefunction (or child)
            The second wavefunction
        """

        # Make sure that at least one has a wilson line
        wilsonLineExists1 = callable(getattr(wavefunction1, "wilsonLine", None))
        wilsonLineExists2 = callable(getattr(wavefunction2, "wilsonLine", None))

        if not wilsonLineExists1 and not wilsonLineExists2:
            raise Exception("Neither of the wavefunctions passed to Collision(Wavefunction, Wavefunction) implement the wilsonLine() method; at least one is required to.")

        if wilsonLineExists1 and not wilsonLineExists2:
            self.targetWavefunction = wavefunction1
            self.incidentWavefunction = wavefunction2
        elif wilsonLineExists2 and not wilsonLineExists1:
            self.targetWavefunction = wavefunction2
            self.incidentWavefunction = wavefunction1
        else:
            self.targetWavefunction = wavefunction1
            self.incidentWavefunction = wavefunction2

        # Make sure that both use the same number of colors
        if self.targetWavefunction.gluonDOF != self.incidentWavefunction.gluonDOF:
            raise Exception(f"Wavefunctions implement different gluon degrees of freedom (number of color charges): {self.incidentWavefunction.gluonDOF} vs. {self.targetWavefunction.gluonDOF}")

        # Probably some other checks that need to be done to make sure the two wavefunctions are compatable, but this is fine for now

        # Carry over some variables so we don't have to call through the wavefunctions so much
        self.N = self.targetWavefunction.N
        self.length = self.targetWavefunction.length
        self.gluonDOF = self.targetWavefunction.gluonDOF
        self.delta = self.targetWavefunction.delta
        self.delta2 = self.targetWavefunction.delta2
        #print(self.targetWavefunction)
        #print(self.incidentWavefunction)

        
        # Variables to do with binning the momenta later on
        self.binSize = 4*np.pi/self.length
        self.kMax = 2/self.delta
        self.numBins = int(self.kMax/self.binSize)

        # This has to be initialized as an empty dict within the constructor
        # because otherwise it can retain information across separate objects
        # (no idea how, but this fixes it)
        self._fourierHarmonics = {}

    def omega(self, forceCalculate=False):
        r"""
        Calculate the field omega at each point on the lattice.

        If the field already exists, it is simply returned and no calculation is done.

        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        omega : array(2, 2, `colorCharges`**2 - 1, N, N)

        """

        if self._omegaExists and not forceCalculate:
            return self._omega

        self._omega = _calculateOmegaOpt(self.N, self.gluonDOF, self.delta, self.incidentWavefunction.gaugeField(), self.targetWavefunction.adjointWilsonLine())

        self._omegaExists = True
        return self._omega


    def omegaFFT(self, forceCalculate=False):
        r"""
        Compute the fourier transform of the field omega on the lattice.

        If the fft of the field already exists, it is simply returned and no calculation is done.

        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        omegaFFT : array(2, 2, `colorCharges`**2 - 1, N, N)

        """
        if self._omegaFFTExists and not forceCalculate:
            return self._omegaFFT

        # Make sure omega exists
        self.omega()

        self._omegaFFT = fft2(self._omega, axes=(-2, -1), norm='backward')
        self._omegaFFTExists = True

        return self._omegaFFT


    def momentaBins(self, forceCalculate=False):
        r"""
        Compute the range of momenta at which particles will be created based on the dimensions of the lattice.

        The exact values are:

        - \( k_{max} = 2 / \Delta\)
        - \( w_k = 4 \pi / L \)

        If the bins already exist, they are simply returned and no calculation is done.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        momentaBins : array(numBins = L / (delta 2 pi))
        """

        if self._momentaBinsExists and not forceCalculate:
            return self._momentaBins

        self._momentaBins = [i*self.binSize for i in range(self.numBins)]
        self._momentaBinsExists = True

        return self._momentaBins


    def momentaComponents(self, forceCalculate=False):
        r"""
        Compute the components of the momentum at each point on the lattice, according to:

        $$ (k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right) $$

        where \(i\) and \(j\) index the \(x\) and \(y\) directions in real space, respectively.

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        momentaComponents : array(N, N, 2)
        """

        if self._momentaComponentsExist and not forceCalculate:
            return self._momentaComponents

        self._momentaComponents, self._thetaInFourierSpace = _calculateMomentaOpt(self.N, self.delta)
        self._momentaMagSquared = np.linalg.norm(self._momentaComponents, axis=2)**2

        self._momentaComponentsExist = True

        return self._momentaComponents


    def momentaMagnitudeSquared(self, forceCalculate=False):
        r"""
        Compute the magnitude of the momentum at each point on the lattice, according to:

        $$ |k| = \sqrt{k_x^2 + k_y^2} $$

        $$ (k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right) $$

        where \(i\) and \(j\) index the \(x\) and \(y\) directions in real space, respectively.

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        momentaComponents : array(N, N)
        """

        if self._momentaComponentsExist and not forceCalculate:
            return self._momentaMagSquared

        self._momentaComponents, self._thetaInFourierSpace = _calculateMomentaOpt(self.N, self.delta)
        self._momentaMagSquared = np.linalg.norm(self._momentaComponents, axis=2)**2

        self._momentaComponentsExist = True

        return self._momentaMagSquared


    def particlesProducedDeriv(self, forceCalculate=False):
        r"""
        Compute the derivative of particles produced (\( \frac{d^2 N}{d^2 k} \)) at each point on the lattice
        
        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        particlesProducedDeriv : array(N, N)
        """
        if self._particlesProducedDerivExists and not forceCalculate:
            return self._particlesProducedDeriv

        # Make sure these quantities exist
        self.omegaFFT()
        self.momentaMagnitudeSquared() # This also calculates thetaInFourierSpace and momentaComponents

        self._particlesProducedDeriv = _calculateParticlesProducedDerivOpt(self.N, self.gluonDOF, self._momentaMagSquared, self._omegaFFT)

        self._particlesProducedDerivExists = True

        return self._particlesProducedDeriv
 

    def particlesProduced(self, forceCalculate=False):
        r"""
        Compute the number of particles produced \(N(|k|)\) as a function of momentum. Note that this
        is technically the zeroth fourier harmonic, so this actually just calls the
        cgc.Collision.fourierHarmonic() function.

        The particles are binned according to cgc.Collision.momentaBins().
       
        Most likely will be plotted against cgc.Collision.momentaBins().

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        particlesProduced : array(numBins = L / (delta 2 pi))
        """
        # This one is strictly real, so we should make sure that is updated        
        self._fourierHarmonics[0] = np.real(self.fourierHarmonic(0, forceCalculate))
        return self._fourierHarmonics[0]


    def fourierHarmonic(self, harmonic: int, forceCalculate=False):
        r"""
        Calculate the fourier harmonic of the particle production as:

        $$ v_n = \frac{ \sum_{(i,j)\in [k, k+ \Delta k]} |k| \frac{d^2 N}{d^2 k} e^{i n \theta }} { \sum_{(i,j)\in [k, k+ \Delta k]} |k| } $$

        If the calculation has already been done, the result is simply returned and is not repeated.


        Parameters
        ----------

        forceCalculate : bool (default=False)
            If the quantity has previously been calculated, the calculation will not be done
            again unless this argument is set to True.

        Returns
        -------

        particlesProduced : array(numBins = L / (delta 2 pi))
        """
        # First, see if we have already calculated this harmonic
        if harmonic in self._fourierHarmonics.keys() and not forceCalculate:
            return self._fourierHarmonics[harmonic]

        # For actually calculating the harmonic, we first have to make sure we've calculated
        # the derivative, dN/d^2k
        # This makes sure that _momentaMagSquared, _thetaInFourierSpace and _particlesProducedDeriv
        # all exist
        self.particlesProducedDeriv()

        # Drop all of our arrays into long 1D structure, since we will want to bin them
        vectorizedParticleDerivs = np.reshape(self._particlesProducedDeriv, [self.N*self.N])
        vectorizedTheta = np.reshape(self._thetaInFourierSpace, [self.N*self.N])
        vectorizedMomentaMag = np.reshape(np.sqrt(self._momentaMagSquared), [self.N*self.N])
       
        # The number of particles that are produced in each bin
        # These bins are actually just thin rings in momentum space
        self._fourierHarmonics[harmonic] = np.zeros(self.numBins, dtype='complex')
        # The bin sizes/bounds are calculated for elsewhere
        self.momentaBins()

        momentaNormalization = np.zeros(self.numBins)

        for i in range(self.N):
            for j in range(self.N):
                k = np.sqrt(self._momentaMagSquared[i,j])
                indexK = int(np.floor(k / self.binSize)) 
                # Anything greater than the highest bin just gets added to the last bin
                # @TODO: Check if this is what we should be doing
                indexK = min(indexK, self.numBins-1)

                self._fourierHarmonics[harmonic][indexK] += self._particlesProducedDeriv[i,j] * k * np.exp(1.j * self._thetaInFourierSpace[i,j] * harmonic)
                momentaNormalization[indexK] += k
                

        for i in range(self.numBins):
            self._fourierHarmonics[harmonic][i] /= momentaNormalization[i]

        # Ideally, these rings should be only have a thickness dk (infinitesimal)
        # but since we have a discrete lattice, we weight the particles by their momentum
        # (which may slightly vary) and then properly normalize

        # Go through each bin and calculate (for all points in that bin):
        # 1. Sum over |k| * dN/d^2k * exp(i * harmonic * theta)
        # 2. Sum over |k|
        # 3. Divide 1./2.
        #for i in range(self.numBins):
        #    # Find which places on the lattice fall into this particular momentum bin
        #    # Note the use of element-wise (or bitwise) and, "&"
        #    particleDerivsInRing = vectorizedParticleDerivs[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]
        #    momentaMagInRing = vectorizedMomentaMag[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]
        #    thetaInRing = vectorizedTheta[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]

        #    # @TODO: make sure this is correct
        #    # Note that multiplication is done element-wise by default
        #    numeratorSum = np.sum(particleDerivsInRing * momentaMagInRing * np.exp(1.j * harmonic * thetaInRing))
           
        #    denominatorSum = np.sum(momentaMagInRing)

        #    self._fourierHarmonics[harmonic][i] = numeratorSum / denominatorSum


        return self._fourierHarmonics[harmonic]

Class variables

var incidentWavefunction
var targetWavefunction

Methods

def fourierHarmonic(self, harmonic: int, forceCalculate=False)

Calculate the fourier harmonic of the particle production as:

v_n = \frac{ \sum_{(i,j)\in [k, k+ \Delta k]} |k| \frac{d^2 N}{d^2 k} e^{i n \theta }} { \sum_{(i,j)\in [k, k+ \Delta k]} |k| }

If the calculation has already been done, the result is simply returned and is not repeated.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

particlesProduced : array(numBins = L / (delta 2 pi))
 
Expand source code
def fourierHarmonic(self, harmonic: int, forceCalculate=False):
    r"""
    Calculate the fourier harmonic of the particle production as:

    $$ v_n = \frac{ \sum_{(i,j)\in [k, k+ \Delta k]} |k| \frac{d^2 N}{d^2 k} e^{i n \theta }} { \sum_{(i,j)\in [k, k+ \Delta k]} |k| } $$

    If the calculation has already been done, the result is simply returned and is not repeated.


    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    particlesProduced : array(numBins = L / (delta 2 pi))
    """
    # First, see if we have already calculated this harmonic
    if harmonic in self._fourierHarmonics.keys() and not forceCalculate:
        return self._fourierHarmonics[harmonic]

    # For actually calculating the harmonic, we first have to make sure we've calculated
    # the derivative, dN/d^2k
    # This makes sure that _momentaMagSquared, _thetaInFourierSpace and _particlesProducedDeriv
    # all exist
    self.particlesProducedDeriv()

    # Drop all of our arrays into long 1D structure, since we will want to bin them
    vectorizedParticleDerivs = np.reshape(self._particlesProducedDeriv, [self.N*self.N])
    vectorizedTheta = np.reshape(self._thetaInFourierSpace, [self.N*self.N])
    vectorizedMomentaMag = np.reshape(np.sqrt(self._momentaMagSquared), [self.N*self.N])
   
    # The number of particles that are produced in each bin
    # These bins are actually just thin rings in momentum space
    self._fourierHarmonics[harmonic] = np.zeros(self.numBins, dtype='complex')
    # The bin sizes/bounds are calculated for elsewhere
    self.momentaBins()

    momentaNormalization = np.zeros(self.numBins)

    for i in range(self.N):
        for j in range(self.N):
            k = np.sqrt(self._momentaMagSquared[i,j])
            indexK = int(np.floor(k / self.binSize)) 
            # Anything greater than the highest bin just gets added to the last bin
            # @TODO: Check if this is what we should be doing
            indexK = min(indexK, self.numBins-1)

            self._fourierHarmonics[harmonic][indexK] += self._particlesProducedDeriv[i,j] * k * np.exp(1.j * self._thetaInFourierSpace[i,j] * harmonic)
            momentaNormalization[indexK] += k
            

    for i in range(self.numBins):
        self._fourierHarmonics[harmonic][i] /= momentaNormalization[i]

    # Ideally, these rings should be only have a thickness dk (infinitesimal)
    # but since we have a discrete lattice, we weight the particles by their momentum
    # (which may slightly vary) and then properly normalize

    # Go through each bin and calculate (for all points in that bin):
    # 1. Sum over |k| * dN/d^2k * exp(i * harmonic * theta)
    # 2. Sum over |k|
    # 3. Divide 1./2.
    #for i in range(self.numBins):
    #    # Find which places on the lattice fall into this particular momentum bin
    #    # Note the use of element-wise (or bitwise) and, "&"
    #    particleDerivsInRing = vectorizedParticleDerivs[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]
    #    momentaMagInRing = vectorizedMomentaMag[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]
    #    thetaInRing = vectorizedTheta[(vectorizedMomentaMag < self.binSize*(i+1)) & (vectorizedMomentaMag > self.binSize*i)]

    #    # @TODO: make sure this is correct
    #    # Note that multiplication is done element-wise by default
    #    numeratorSum = np.sum(particleDerivsInRing * momentaMagInRing * np.exp(1.j * harmonic * thetaInRing))
       
    #    denominatorSum = np.sum(momentaMagInRing)

    #    self._fourierHarmonics[harmonic][i] = numeratorSum / denominatorSum


    return self._fourierHarmonics[harmonic]
def momentaBins(self, forceCalculate=False)

Compute the range of momenta at which particles will be created based on the dimensions of the lattice.

The exact values are:

  • k_{max} = 2 / \Delta
  • w_k = 4 \pi / L

If the bins already exist, they are simply returned and no calculation is done.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

momentaBins : array(numBins = L / (delta 2 pi))
 
Expand source code
def momentaBins(self, forceCalculate=False):
    r"""
    Compute the range of momenta at which particles will be created based on the dimensions of the lattice.

    The exact values are:

    - \( k_{max} = 2 / \Delta\)
    - \( w_k = 4 \pi / L \)

    If the bins already exist, they are simply returned and no calculation is done.


    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    momentaBins : array(numBins = L / (delta 2 pi))
    """

    if self._momentaBinsExists and not forceCalculate:
        return self._momentaBins

    self._momentaBins = [i*self.binSize for i in range(self.numBins)]
    self._momentaBinsExists = True

    return self._momentaBins
def momentaComponents(self, forceCalculate=False)

Compute the components of the momentum at each point on the lattice, according to:

(k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right)

where i and j index the x and y directions in real space, respectively.

If the calculation has already been done, the result is simply returned and is not repeated.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

momentaComponents : array(N, N, 2)
 
Expand source code
def momentaComponents(self, forceCalculate=False):
    r"""
    Compute the components of the momentum at each point on the lattice, according to:

    $$ (k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right) $$

    where \(i\) and \(j\) index the \(x\) and \(y\) directions in real space, respectively.

    If the calculation has already been done, the result is simply returned and is not repeated.


    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    momentaComponents : array(N, N, 2)
    """

    if self._momentaComponentsExist and not forceCalculate:
        return self._momentaComponents

    self._momentaComponents, self._thetaInFourierSpace = _calculateMomentaOpt(self.N, self.delta)
    self._momentaMagSquared = np.linalg.norm(self._momentaComponents, axis=2)**2

    self._momentaComponentsExist = True

    return self._momentaComponents
def momentaMagnitudeSquared(self, forceCalculate=False)

Compute the magnitude of the momentum at each point on the lattice, according to:

|k| = \sqrt{k_x^2 + k_y^2}

(k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right)

where i and j index the x and y directions in real space, respectively.

If the calculation has already been done, the result is simply returned and is not repeated.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

momentaComponents : array(N, N)
 
Expand source code
def momentaMagnitudeSquared(self, forceCalculate=False):
    r"""
    Compute the magnitude of the momentum at each point on the lattice, according to:

    $$ |k| = \sqrt{k_x^2 + k_y^2} $$

    $$ (k_x, k_y) = \frac{2}{\Delta} \left( \sin\left( \frac{\pi i}{N} \right), \sin\left( \frac{\pi j}{N} \right) \right) $$

    where \(i\) and \(j\) index the \(x\) and \(y\) directions in real space, respectively.

    If the calculation has already been done, the result is simply returned and is not repeated.


    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    momentaComponents : array(N, N)
    """

    if self._momentaComponentsExist and not forceCalculate:
        return self._momentaMagSquared

    self._momentaComponents, self._thetaInFourierSpace = _calculateMomentaOpt(self.N, self.delta)
    self._momentaMagSquared = np.linalg.norm(self._momentaComponents, axis=2)**2

    self._momentaComponentsExist = True

    return self._momentaMagSquared
def omega(self, forceCalculate=False)

Calculate the field omega at each point on the lattice.

If the field already exists, it is simply returned and no calculation is done.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

omega : array(2, 2, colorCharges**2 - 1, N, N)
 
Expand source code
def omega(self, forceCalculate=False):
    r"""
    Calculate the field omega at each point on the lattice.

    If the field already exists, it is simply returned and no calculation is done.

    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    omega : array(2, 2, `colorCharges`**2 - 1, N, N)

    """

    if self._omegaExists and not forceCalculate:
        return self._omega

    self._omega = _calculateOmegaOpt(self.N, self.gluonDOF, self.delta, self.incidentWavefunction.gaugeField(), self.targetWavefunction.adjointWilsonLine())

    self._omegaExists = True
    return self._omega
def omegaFFT(self, forceCalculate=False)

Compute the fourier transform of the field omega on the lattice.

If the fft of the field already exists, it is simply returned and no calculation is done.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

omegaFFT : array(2, 2, colorCharges**2 - 1, N, N)
 
Expand source code
def omegaFFT(self, forceCalculate=False):
    r"""
    Compute the fourier transform of the field omega on the lattice.

    If the fft of the field already exists, it is simply returned and no calculation is done.

    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    omegaFFT : array(2, 2, `colorCharges`**2 - 1, N, N)

    """
    if self._omegaFFTExists and not forceCalculate:
        return self._omegaFFT

    # Make sure omega exists
    self.omega()

    self._omegaFFT = fft2(self._omega, axes=(-2, -1), norm='backward')
    self._omegaFFTExists = True

    return self._omegaFFT
def particlesProduced(self, forceCalculate=False)

Compute the number of particles produced N(|k|) as a function of momentum. Note that this is technically the zeroth fourier harmonic, so this actually just calls the cgc.Collision.fourierHarmonic() function.

The particles are binned according to cgc.Collision.momentaBins().

Most likely will be plotted against cgc.Collision.momentaBins().

If the calculation has already been done, the result is simply returned and is not repeated.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

particlesProduced : array(numBins = L / (delta 2 pi))
 
Expand source code
def particlesProduced(self, forceCalculate=False):
    r"""
    Compute the number of particles produced \(N(|k|)\) as a function of momentum. Note that this
    is technically the zeroth fourier harmonic, so this actually just calls the
    cgc.Collision.fourierHarmonic() function.

    The particles are binned according to cgc.Collision.momentaBins().
   
    Most likely will be plotted against cgc.Collision.momentaBins().

    If the calculation has already been done, the result is simply returned and is not repeated.


    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    particlesProduced : array(numBins = L / (delta 2 pi))
    """
    # This one is strictly real, so we should make sure that is updated        
    self._fourierHarmonics[0] = np.real(self.fourierHarmonic(0, forceCalculate))
    return self._fourierHarmonics[0]
def particlesProducedDeriv(self, forceCalculate=False)

Compute the derivative of particles produced (( \frac{d^2 N}{d^2 k} )) at each point on the lattice

If the calculation has already been done, the result is simply returned and is not repeated.

Parameters

forceCalculate : bool (default=False)
If the quantity has previously been calculated, the calculation will not be done again unless this argument is set to True.

Returns

particlesProducedDeriv : array(N, N)
 
Expand source code
def particlesProducedDeriv(self, forceCalculate=False):
    r"""
    Compute the derivative of particles produced (\( \frac{d^2 N}{d^2 k} \)) at each point on the lattice
    
    If the calculation has already been done, the result is simply returned and is not repeated.


    Parameters
    ----------

    forceCalculate : bool (default=False)
        If the quantity has previously been calculated, the calculation will not be done
        again unless this argument is set to True.

    Returns
    -------

    particlesProducedDeriv : array(N, N)
    """
    if self._particlesProducedDerivExists and not forceCalculate:
        return self._particlesProducedDeriv

    # Make sure these quantities exist
    self.omegaFFT()
    self.momentaMagnitudeSquared() # This also calculates thetaInFourierSpace and momentaComponents

    self._particlesProducedDeriv = _calculateParticlesProducedDerivOpt(self.N, self.gluonDOF, self._momentaMagSquared, self._omegaFFT)

    self._particlesProducedDerivExists = True

    return self._particlesProducedDeriv