class PhotALPs_ICM(object):
    """
    Class for photon ALP conversion in galaxy clusters and the intra cluster medium (ICM) 

    Attributes
    ----------
    Lcoh:	coherence length / domain size of turbulent B-field in the cluster in kpc
    B:		field strength of transverse component of the cluster B-field, in muG
    r_abell:	size of cluster filled with the constant B-field in kpc
    g:		Photon ALP coupling in 10^{-11} GeV^-1
    m:		ALP mass in neV
    n:		thermal electron density in the cluster, in 10^{-3} cm^-3
    Nd:		number of domains, Lcoh/r_abell
    Psin:	random angle in domain n between transverse B field and propagation direction
    T1:		Transfer matrix 1 (3x3xNd)-matrix
    T2:		Transfer matrix 2 (3x3xNd)-matrix		
    T3:		Transfer matrix 3 (3x3xNd)-matrix
    Un:		Total transfer matrix in all domains (3x3xNd)-matrix
    Dperp:	Mixing matrix parameter Delta_perpedicular in n-th domain
    Dpar:	Mixing matrix parameter Delta_{||} in n-th domain
    Dag:	Mixing matrix parameter Delta_{a\gamma} in n-th domain
    Da:		Mixing matrix parameter Delta_{a} in n-th domain
    alph:	Mixing angle
    Dosc:	Oscillation Delta

    EW1: 	Eigenvalue 1 of mixing matrix
    EW2:	Eigenvalue 2 of mixing matrix
    EW3:	Eigenvalue 3 of mixing matrix

    Notes
    -----
    For Photon - ALP mixing theory see e.g. De Angelis et al. 2011 and also Horns et al. 2012
    http://adsabs.harvard.edu/abs/2011PhRvD..84j5030D
    http://adsabs.harvard.edu/abs/2012PhRvD..86g5024H
    """
    def __init__(self, **kwargs):
        """
	init photon axion conversion in intracluster medium

	Parameters
	----------
	Lcoh:		coherence length / domain size of turbulent B-field in the cluster in kpc, default: 10 kpc
	B:		field strength of transverse component of the cluster B-field, in muG, default: 1 muG
	r_abell:	size of cluster filled with the constant B-field in kpc. default: 1500 * h
	g:		Photon ALP coupling in 10^{-11} GeV^-1, default: 1.
	m:		ALP mass in neV, default: 1.
	n:		thermal electron density in the cluster, in 10^{-3} cm^-3, default: 1.

	Bn_const:	boolean, if True n and B are constant all over the cluster
			if False than B and n are modeled, see notes
	Bgauss:		boolean, if True, B field calculated from gaussian turbulence spectrum,
			if False then domain-like structure is assumed.

	kH:		float, upper wave number cutoff, should be at at least > 1. / osc. wavelength (default = 200 / (1 kpc))
	kL:		float, lower wave number cutoff, should be of same size as the system (default = 1 / (r_abell kpc))
	q:  		float, power-law turbulence spectrum (default: q = 11/3 is Kolmogorov type spectrum)
	dkType:		string, either linear, log, or random. Determine the spacing of the dk intervals 	
	dkSteps: 	int, number of dkSteps. For log spacing, number of steps per decade / number of decades ~ 10
			should be chosen.

	r_core:		Core radius for n and B modeling in kpc, default: 200 kpc
	beta:		power of n dependence, default: 2/3
	eta:		power with what B follows n, see Notes. Typical values: 0.5 <= eta <= 1. default: 1.

	Returns
	-------
	Nothing.

	Notes
	-----

	If Bn_const = False then electron density is modeled according to Carilli & Taylor (2002) Eq. 2:
	    n_e(r)  = n * (1 - (r/r_core)**2.)**(-3/2*beta)
	with typical values of r_core = 200 kpc and beta = 2/3.

	The magnetic field is supposed to follow n_e(r) with (Feretti et al. 2012, p. 41, section 7.1)
	    B(r) = B * (n_e(r)/n) ** eta
	with typical values 1 muG <= B <= 15muG and 0.5 <= eta <= 1
	"""
        # --- Set the defaults
        kwargs.setdefault('g', 1.)
        kwargs.setdefault('m', 1.)
        kwargs.setdefault('B', 1.)
        kwargs.setdefault('n', 1.)
        kwargs.setdefault('Lcoh', 10.)
        kwargs.setdefault('r_abell', 100.)
        kwargs.setdefault('r_core', 200.)
        kwargs.setdefault('E_GeV', 1.)

        kwargs.setdefault('B_gauss', False)
        kwargs.setdefault('kL', 0.)
        kwargs.setdefault('kH', 15.)
        kwargs.setdefault('q', -11. / 3.)
        kwargs.setdefault('dkType', 'log')
        kwargs.setdefault('dkSteps', 0)

        kwargs.setdefault('Bn_const', True)
        kwargs.setdefault('beta', 2. / 3.)
        kwargs.setdefault('eta', 1.)
        # --------------------
        self.update_params(**kwargs)

        super(PhotALPs_ICM, self).__init__()

        return

    def update_params(self, new_Bn=True, **kwargs):
        """Update all parameters with new values and initialize all matrices
	
	kwargs
	------
	new_B_n:	boolean, if True, recalculate B field and electron density
	
	"""
        self.__dict__.update(kwargs)

        if self.B_gauss:
            if not self.kL:
                self.kL = 1. / self.r_abell
                kwargs['kL'] = self.kL
            self.Lcoh = 1. / self.kH
            kwargs['Lcoh'] = self.Lcoh
            self.bfield = Bgaus(**kwargs)  # init gaussian turbulent field

        self.Nd = int(self.r_abell /
                      self.Lcoh)  # number of domains, no expansion assumed
        self.r = np.linspace(self.Lcoh, self.r_abell + self.Lcoh, int(self.Nd))

        if new_Bn:
            self.new_B_n()

        self.T1 = np.zeros((3, 3, self.Nd), np.complex)  # Transfer matrices
        self.T2 = np.zeros((3, 3, self.Nd), np.complex)
        self.T3 = np.zeros((3, 3, self.Nd), np.complex)
        self.Un = np.zeros((3, 3, self.Nd), np.complex)

        return

    def new_B_n(self):
        """
	Recalculate Bfield and density, if Kolmogorov turbulence is set to true, new random values for B and Psi are calculated.
	"""

        if self.B_gauss:
            Bt = self.bfield.Bgaus(
                self.r)  # calculate first transverse component
            self.bfield.new_random_numbers()  # new random numbers
            Bu = self.bfield.Bgaus(
                self.r)  # calculate second transverse component
            self.B = np.sqrt(Bt**2. +
                             Bu**2.)  # calculate total transverse component
            self.Psin = np.arctan2(
                Bt, Bu
            )  # and angle to x2 (t) axis -- use atan2 to get the quadrants right

        if self.Bn_const:
            self.n = self.n * np.ones(
                int(self.Nd
                    ))  # assuming a constant electron density over all domains
            if not self.B_gauss:
                self.B = self.B * np.ones(int(
                    self.Nd))  # assuming a constant B-field over all domains
        else:
            if np.isscalar(self.n):
                n0 = self.n
            else:
                n0 = self.n[0]
# check for double beta profile
            try:
                if np.isscalar(self.n2):
                    n2 = self.n2
                else:
                    n2 = self.n2[0]
                try:  # check for two different beta values
                    self.beta2
                    self.n =  n0 * (np.ones(int(self.Nd)) + self.r**2./self.r_core**2.)**(-1.5 * self.beta) +\
                     n2 * (np.ones(int(self.Nd)) + self.r**2./self.r_core2**2.)**(-1.5 * self.beta2)
                    self.B = self.B * (self.n / (n0 + n2))**self.eta
                except NameError:
                    self.n =  np.sqrt(n0**2. * (np.ones(int(self.Nd)) + self.r**2./self.r_core**2.)**(-3. * self.beta) +\
                     n2**2. * (np.ones(int(self.Nd)) + self.r**2./self.r_core2**2.)**(-3. * self.beta) )
                    self.B = self.B * (self.n /
                                       np.sqrt(n0**2. + n2**2.))**self.eta
            except AttributeError:
                self.n = n0 * (np.ones(int(self.Nd)) + self.r**2. /
                               self.r_core**2.)**(-1.5 * self.beta)
                self.B = self.B * (self.n / n0)**self.eta
        return

    def new_random_psi(self):
        """
	Calculate new random psi values

	Parameters:
	-----------
	None

	Returns:
	--------
	Nothing
	"""
        self.Psin = 2. * np.pi * rand(1, int(self.Nd))[
            0]  # angle between photon propagation on B-field in i-th domain
        return

    def __setDeltas(self):
        """
	Set Deltas of mixing matrix for each domain
	
	Parameters
	----------
	None (self only)

	Returns
	-------
	Nothing
	"""

        self.Dperp = Delta_pl_kpc(self.n, self.E) + 2. * Delta_QED_kpc(
            self.B, self.E)  # np.arrays , self.Nd-dim
        self.Dpar = Delta_pl_kpc(self.n, self.E) + 3.5 * Delta_QED_kpc(
            self.B, self.E)  # np.arrays , self.Nd-dim
        self.Dag = Delta_ag_kpc(self.g, self.B)  # np.array, self.Nd-dim
        self.Da = Delta_a_kpc(self.m, self.E) * np.ones(int(
            self.Nd))  # np.ones, so that it is np.array, self.Nd-dim
        self.alph = 0.5 * np.arctan2(2. * self.Dag, (self.Dpar - self.Da))
        self.Dosc = np.sqrt((self.Dpar - self.Da)**2. + 4. * self.Dag**2.)

        return

    def __setEW(self):
        """
	Set Eigenvalues
	
	Parameters
	----------
	None (self only)

	Returns
	-------
	Nothing
	"""
        # Eigen values are all self.Nd-dimensional
        self.__setDeltas()
        self.EW1 = self.Dperp
        self.EW2 = 0.5 * (self.Dpar + self.Da - self.Dosc)
        self.EW3 = 0.5 * (self.Dpar + self.Da + self.Dosc)
        return

    def __setT1n(self):
        """
	Set T1 in all domains
	
	Parameters
	----------
	None (self only)

	Returns
	-------
	Nothing
	"""
        c = np.cos(self.Psin)
        s = np.sin(self.Psin)
        self.T1[0, 0, :] = c * c
        self.T1[0, 1, :] = -1. * c * s
        self.T1[1, 0, :] = self.T1[0, 1]
        self.T1[1, 1, :] = s * s
        return

    def __setT2n(self):
        """
	Set T2 in all domains
	
	Parameters
	----------
	None (self only)

	Returns
	-------
	Nothing
	"""
        c = np.cos(self.Psin)
        s = np.sin(self.Psin)
        ca = np.cos(self.alph)
        sa = np.sin(self.alph)
        self.T2[0, 0, :] = s * s * sa * sa
        self.T2[0, 1, :] = s * c * sa * sa
        self.T2[0, 2, :] = -1. * s * sa * ca

        self.T2[1, 0, :] = self.T2[0, 1]
        self.T2[1, 1, :] = c * c * sa * sa
        self.T2[1, 2, :] = -1. * c * ca * sa

        self.T2[2, 0, :] = self.T2[0, 2]
        self.T2[2, 1, :] = self.T2[1, 2]
        self.T2[2, 2, :] = ca * ca
        return

    def __setT3n(self):
        """
	Set T3 in all domains
	
	Parameters
	----------
	None (self only)

	Returns
	-------
	Nothing
	"""
        c = np.cos(self.Psin)
        s = np.sin(self.Psin)
        ca = np.cos(self.alph)
        sa = np.sin(self.alph)
        self.T3[0, 0, :] = s * s * ca * ca
        self.T3[0, 1, :] = s * c * ca * ca
        self.T3[0, 2, :] = s * sa * ca

        self.T3[1, 0, :] = self.T3[0, 1]
        self.T3[1, 1, :] = c * c * ca * ca
        self.T3[1, 2, :] = c * sa * ca

        self.T3[2, 0, :] = self.T3[0, 2]
        self.T3[2, 1, :] = self.T3[1, 2]
        self.T3[2, 2, :] = sa * sa
        return

    def __setUn(self):
        """
	Set Transfer Matrix Un in n-th domain
	
	Parameters
	----------
	None (self only)

	Returns
	-------
	Nothing
	"""
        self.Un = np.exp(1.j * self.EW1 * self.Lcoh) * self.T1 + \
        np.exp(1.j * self.EW2 * self.Lcoh) * self.T2 + \
        np.exp(1.j * self.EW3 * self.Lcoh) * self.T3
        return

    def SetDomainN(self):
        """
	Set Transfer matrix in all domains and multiply it

	Parameters
	----------
	None (self only)

	Returns
	-------
	Transfer matrix as 3x3 complex numpy array
	"""
        if not self.Nd == self.Psin.shape[0]:
            raise TypeError(
                "Number of domains (={0:n}) is not equal to number of angles (={1:n})!"
                .format(self.Nd, self.Psin.shape[0]))
        self.__setEW()
        self.__setT1n()
        self.__setT2n()
        self.__setT3n()
        self.__setUn(
        )  # self.Un contains now all 3x3 matrices in all self.Nd domains
        # do the martix multiplication
        for i in range(self.Un.shape[2]):
            if not i:
                U = self.Un[:, :, i]
            else:
                U = np.dot(U, self.Un[:, :, i])  # first matrix on the left
        return U