Skip to content

Box

Generate a 3D box in real space with some power spectrum. (Phil Bull, 2012, 2017, 2021)

CosmoBox

Source code in fastbox/box.py
class CosmoBox(object):

    def __init__(self, cosmo, box_scale=1e3, nsamp=32, redshift=0., 
                 line_freq=1420.405752, realise_now=True):
        """
        Initialise a box containing a matter distribution with a given power 
        spectrum.

        Parameters:
            cosmo (CCL.Cosmology object or dict):
                Cosmology object. If passed as a dictionary, this will be used to 
                create a new CCL Cosmology object.

            box_scale (float or tuple, optional):
                The side length (in Mpc) of the cubic box. If passed as a tuple, 
                this will specify the scales in the x, y, and z Cartesian 
                directions separately.

            nsamp (int, optional):
                The number of grid points per dimension.

            redshift (float, optional):
                The redshift to place the box at. This affects the redshift at 
                which the power spectrum is evaluated when generating the fields.

            line_freq (float, optional):
                Frequency of the emission line used as the redshift reference, in 
                MHz.

            realise_now (bool, optional):
                If True, generate realisations of the density, velocity, and 
                potential immediately on initialisation.

        Note:
            The `self.delta_k` field is not in proper cosmological units; to 
            get the power spectrum in the right units, a factor of `self.boxfactor` 
            is needed for example.
        """
        if isinstance(cosmo, dict):
            cosmo = ccl.Cosmology(**cosmo)
        if not isinstance(cosmo, ccl.Cosmology):
            raise TypeError("`cosmo` must be a CCL Cosmology object or dict.")
        self.cosmo = cosmo # Cosmological parameters, required by Cosmolopy fns.

        # Number of sample points per dimension
        self.N = nsamp

        # Box redshift and emission line reference
        self.redshift = redshift
        self.scale_factor = 1. / (1. + redshift)
        self.line_freq = line_freq

        # Define grid coordinates along each dimension
        if isinstance(box_scale, tuple):
            assert len(box_scale) == 3, "Must specify scale of x, y, z dimensions"
            scale_x, scale_y, scale_z = box_scale
            self.x = np.linspace(-0.5*scale_x, 0.5*scale_x, nsamp) # in Mpc
            self.y = np.linspace(-0.5*scale_y, 0.5*scale_y, nsamp) # in Mpc
            self.z = np.linspace(-0.5*scale_z, 0.5*scale_z, nsamp) # in Mpc
            self.Lx = self.x[-1] - self.x[0] # Linear size of box
            self.Ly = self.y[-1] - self.y[0] # Linear size of box
            self.Lz = self.z[-1] - self.z[0] # Linear size of box
        else:
            self.x = self.y = self.z = np.linspace(-0.5*box_scale, 
                                                    0.5*box_scale, 
                                                    nsamp) # in Mpc
            self.Lx = self.Ly = self.Lz = self.x[-1] - self.x[0] # Linear size of box

        # Conversion factor for FFT of power spectrum
        # For an example, see in liteMap.py:fillWithGaussianRandomField() in 
        # Flipper, by Sudeep Das. http://www.astro.princeton.edu/~act/flipper
        self.boxfactor = (self.N**6.) / (self.Lx * self.Ly * self.Lz)

        # Fourier mode array
        self.set_fft_sample_spacing() # 3D array, arranged in correct order

        # Min./max. k modes in 3D (excl. zero mode)
        self.kmin = 2.*np.pi/np.max([self.Lx, self.Ly, self.Lz])
        self.kmax = 2.*np.pi*np.sqrt(3.)*self.N/np.min([self.Lx, self.Ly, self.Lz])

        # Create a realisation of density/velocity perturbations in the box
        if realise_now:
            self.realise_density()
            self.realise_velocity()
            self.realise_potential()


    def set_fft_sample_spacing(self):
        """
        Calculate the sample spacing in Fourier space, given some symmetric 3D 
        box in real space, with 1D grid point coordinates `x`.
        """
        # These are related to comoving k by factor of 2 pi / L
        self.Kx = np.zeros((self.N,self.N,self.N))
        self.Ky = np.zeros((self.N,self.N,self.N))
        self.Kz = np.zeros((self.N,self.N,self.N))
        NN = ( self.N*fft.fftfreq(self.N, 1.) ).astype("i")
        for i in NN:
            self.Kx[i,:,:] = i
            self.Ky[:,i,:] = i
            self.Kz[:,:,i] = i

        self.k = 2.*np.pi * np.sqrt(  (self.Kx/self.Lx)**2. 
                                    + (self.Ky/self.Ly)**2. 
                                    + (self.Kz/self.Lz)**2.)


    def realise_density(self, linear=False, redshift=None, inplace=True):
        """
        Create realisation of the matter power spectrum by randomly sampling 
        from Gaussian distributions of variance P(k) for each k mode.

        Parameters:
            linear (bool, optional):
                If True, use the linear matter power spectrum to do the Gaussian 
                random realisation instead of the non-linear power spectrum set in 
                `self.cosmo`.

            redshift (float, optional):
                If specified, use this redshift to calculate the matter power 
                spectrum for the Gaussian random realisation. Otherwise, the value 
                of `self.redshift` is used. Default: None.

            inplace (bool, optional):
                If True, store the resulting density field and its Fourier transform 
                into the `self.delta_x` and `self.delta_k` variables as well as 
                returning `delta_x`.

        Returns:
            delta_x (array_like):
                3D array of delta_x values.
        """
        # Get redshift
        if redshift is None:
            redshift = self.redshift
        scale_factor = 1. / (1. + redshift)

        # Calculate matter power spectrum
        k = self.k.flatten()
        if linear:
            pk = ccl.linear_matter_power(self.cosmo, k=k, a=scale_factor)
        else:
            pk = ccl.nonlin_matter_power(self.cosmo, k=k, a=scale_factor)
        pk = np.reshape(pk, np.shape(self.k))
        pk = np.nan_to_num(pk) # Remove NaN at k=0 (and any others...)

        # Normalise the power spectrum properly (factor of volume, and norm. 
        # factor of 3D DFT)
        pk *= self.boxfactor

        # Generate Gaussian random field with given power spectrum
        re = np.random.normal(0.0, 1.0, np.shape(self.k))
        im = np.random.normal(0.0, 1.0, np.shape(self.k))
        delta_k = ( re + 1j*im ) * np.sqrt(pk) # (makes variance too high by 2x)
        if inplace:
            if redshift != self.redshift:
                print("Warning: Storing density field into self.delta_x with a "
                      "different redshift than self.redshift.")
            self.delta_k = delta_k

        # Transform to real space. Here, we are discarding the imaginary part 
        # of the inverse FT! But we can recover the correct (statistical) 
        # result by multiplying by a factor of sqrt(2) [see above, where this 
        # factor was already omitted when defining delta_k].
        delta_x = fft.ifftn(self.delta_k).real # This ensures the field is real
        if inplace:
            self.delta_x = delta_x

        # Finally, get the Fourier transform back
        if inplace:
            self.delta_k = fft.fftn(self.delta_x)
        return delta_x


    def realise_velocity(self, delta_x=None, delta_k=None, redshift=None, 
                         inplace=True):
        """
        Realise the (unscaled) velocity field in Fourier space (e.g. see 
        Dodelson Eq. 9.18):

            v(k) = i [f(a) H(a) a] delta_k vec{k} / k^2

        The DFT prefactor has not been applied; to get the real-space velocity, 
        simply do ifftn(velocity_k[i]), where i=0..2 for the x,y,z Cartesian 
        directions.

        Parameters:
            delta_x (array_like, optional):
                Density fluctuation field. If this and `delta_k` are None, will use 
                `self.delta_k` as the field. Default: None.

            delta_k (array_like, optional):
                Fourier-space density fluctuation field. If this and `delta_x` are 
                None, will use `self.delta_k` as the field. Default: None.

            redshift (float, optional):
                If specified, use this redshift to calculate the matter power 
                spectrum for the Gaussian random realisation. Otherwise, the value 
                of `self.redshift` is used. Default: None.

            inplace (bool, optional):
                If True, store the Fourier transform, `velocity_k`, of the 
                resulting velocity field and its Fourier transform 
                into the `self.velocity_k` variable. Default: True.

        Returns:
            velocity_k (tuple of array_like):
                3-tuple of the x,y,z velocity field components in Fourier space, 
                v_x(k), v_y(k), v_z(k). Apply ifftn() directly to any of the 
                components to get the real-space velocity component with the 
                correct normalisation.
        """
        # Get redshift
        if redshift is None:
            redshift = self.redshift
        scale_factor = 1. / (1. + redshift)

        # Check inputs
        if delta_x is not None and delta_k is not None:
            raise ValueError("delta_x and delta_k specified; can only specify one")

        # Do FFT of delta_x if specified; otherwise use delta_k or self.delta_k
        if delta_x is not None:
            delta_k = fft.fftn(delta_x)
        if delta_k is None:
            delta_k = self.delta_k

        # Get squared k-vector in k-space (and factor in scaling from kx, ky, kz)
        k2 = self.k**2.

        # Calculate components of A (the unscaled velocity)
        Ax = 1.j * delta_k * self.Kx * (2.*np.pi/self.Lx) / k2
        Ay = 1.j * delta_k * self.Ky * (2.*np.pi/self.Ly) / k2
        Az = 1.j * delta_k * self.Kz * (2.*np.pi/self.Lz) / k2
        Ax = np.nan_to_num(Ax)
        Ay = np.nan_to_num(Ay)
        Az = np.nan_to_num(Az)

        # If the FFT has an even number of samples, the most negative frequency 
        # mode must have the same value as the most positive frequency mode. 
        # However, when multiplying by 'i', allowing this mode to have a 
        # non-zero real part makes it impossible to satisfy the reality 
        # conditions. As such, we can set the whole mode to be zero, make sure 
        # that it's pure imaginary, or use an odd number of samples. Different 
        # ways of dealing with this could change the answer!
        if self.N % 2 == 0: # Even no. samples
            # Set highest (negative) freq. to zero
            mx = np.where(self.Kx == np.min(self.Kx))
            my = np.where(self.Ky == np.min(self.Ky))
            mz = np.where(self.Kz == np.min(self.Kz))
        #    self.Kx[mx] = 0.0; self.Ky[my] = 0.0; self.Kz[mz] = 0.0
        Ax[mx] = Ay[my] = Az[mz] = 0.

        # Apply prefactor, v(k) = i [f(a) H(a) a] delta_k vec{k} / k^2
        # N.B. velocity_k is missing a prefactor of 1/sqrt(self.box_factor).
        # If you do ifftn(velocity_k), you will get the real-space velocity 
        # field back with the correct scaling however
        fac = 100.*self.cosmo['h'] * ccl.h_over_h0(self.cosmo, a=scale_factor) \
            * ccl.growth_rate(self.cosmo, a=scale_factor) * scale_factor
        Ax *= fac
        Ay *= fac
        Az *= fac
        velocity_k = (Ax, Ay, Az)

        # Store result in object if requested
        if inplace:
            self.velocity_k = velocity_k
        return velocity_k


    def realise_potential(self, delta_x=None, delta_k=None, redshift=None, 
                         inplace=True):
        """
        Realise the potential in Fourier space.

            Phi = 3/2 (Omega_m H_0^2) (D(a) / a) delta(k) / k^2

        The DFT prefactor has not been applied; to get the real-space potential, 
        simply do ifftn(potential_k).

        Parameters:
            delta_x (array_like, optional):
                Density fluctuation field. If this and `delta_k` are None, will use 
                `self.delta_k` as the field. Default: None.

            delta_k (array_like, optional):
                Fourier-space density fluctuation field. If this and `delta_x` are 
                None, will use `self.delta_k` as the field. Default: None.

            redshift (float, optional):
                If specified, use this redshift to calculate the matter power 
                spectrum for the Gaussian random realisation. Otherwise, the value 
                of `self.redshift` is used. Default: None.

            inplace (bool, optional):
                If True, store the Fourier transform, `velocity_k`, of the 
                resulting velocity field and its Fourier transform 
                into the `self.velocity_k` variable. Default: True.

        Returns:
            phi_k (array_like):
                Potential field, in Fourier space.
        """
        # Get redshift
        if redshift is None:
            redshift = self.redshift
        scale_factor = 1. / (1. + redshift)

        # Check inputs
        if delta_x is not None and delta_k is not None:
            raise ValueError("delta_x and delta_k specified; can only specify one")

        # Do FFT of delta_x if specified; otherwise use delta_k or self.delta_k
        if delta_x is not None:
            delta_k = fft.fftn(delta_x)
        if delta_k is None:
            delta_k = self.delta_k

        # Calculate pre-factor (FIXME: Make sure D isn't double-counted)
        # Phi = 3/2 (Omega_m H_0^2) (D(a) / a) delta(k) / k^2
        Omega_m = self.cosmo['Omega_c'] + self.cosmo['Omega_b']
        fac = (3./2.) * Omega_m * (100.*self.cosmo['h'])**2. \
            * ccl.growth_factor(self.cosmo, self.scale_factor)/self.scale_factor

        phi_k = delta_k / self.k**2.
        phi_k[0,0,0] = 0. # Fix monopole

        # Store result in object if requested
        if inplace:
            self.phi_k = phi_k
        return phi_k


    def apply_transfer_fn(self, field_k, transfer_fn):
        """
        Apply a Fourier-space transfer function and transform back to real space.

        Parameters:
            field_k (ndarray):
                Field in Fourier space (3D array), with Fourier modes self.kx,ky,kz.

            transfer_fn (callable):
                Function that modulates the Fourier-space field. Must have 
                call signature `transfer_fn(k_perp, k_par)`.

        Returns:
            field_x (ndarray):
                Real-space field that has had the transfer function applied.
        """
        # Get 2D Fourier modes
        #fac = (2.*np.pi/self.L)
        k_perp = 2.*np.pi * np.sqrt((self.Kx/self.Lx)**2. + (self.Ky/self.Ly)**2.)
        k_par = 2.*np.pi * self.Kz / self.Lz

        # Apply transfer function, perform inverse FFT, and return
        dk = field_k * transfer_fn(k_perp, k_par)
        dk = np.nan_to_num(dk)
        dx = fft.ifftn(dk)
        return dx


    def redshift_space_density(self, delta_x=None, velocity_z=None, sigma_nl=0., 
                               method='linear'):
        """
        Remap the real-space density field to redshift-space using the line-of-
        sight velocity field.

        Parameters:
            delta_x (array_like, optional):
                Real-space density field.

            velocity_z (array_like, optional):
                Velocity in the z (line-of-sight) direction (km/s).

            sigma_nl (float, optional):
                Optionally, add random small-scale incoherent velocities along the 
                LOS (uncorrelated Gaussian; km/s).

            method (str, optional):
                Interpolation method to use when performing remapping, using the 
                `scipy.interpolate.griddata` function. Default: 'linear'.
        """
        # Expansion rate (km/s/Mpc)
        Hz = 100.*self.cosmo['h'] * ccl.h_over_h0(self.cosmo, self.scale_factor)

        # Empty redshift-space array
        delta_s = np.zeros_like(delta_x) - 1. # Default value is -1 (void)

        # Loop over x and y pixels
        for i in range(delta_x.shape[0]):
            for j in range(delta_x.shape[1]):

                # Realisation of uncorrelated non-linear velocities
                vel_nl = 0.
                if sigma_nl > 0.:
                    vel_nl = sigma_nl * np.random.normal(0., 1., self.z.size)

                # Redshift-space z coordinate (negative sign as we will map 
                # from real coord to redshift-space coord)
                s = self.z - (velocity_z[i,j,:] + vel_nl) / Hz

                # Apply periodic boundary conditions
                length_z = np.max(self.z) - np.min(self.z)
                s = (s - np.min(self.z)) % (length_z) + np.min(self.z)

                # Use average value of endpoints as fill value
                fill_value = 0.5 * (delta_x[i,j,0] + delta_x[i,j,-1])

                # Remap to redshift-space (on regular grid in redshift-space 
                # with same grid points as in 'z' array)
                delta_s[i,j,:] = griddata(points=(s,), 
                                          values=delta_x[i,j,:], 
                                          xi=(self.z), 
                                          method=method,
                                          fill_value=fill_value)
        return delta_s


    def lognormal(self, delta_x):
        """
        Return a log-normal transform of the input field (see Eq. 3.1 of 
        arXiv:1706.09195; also c.f. Eq. 7 of Alonso et al., arXiv:1405.1751, 
        which differs by a factor of 1/2).

        Parameters:
            delta_x (array_like):
                Density field (shoudl be Gaussian, generated using a linear matter 
                power spectrum).

        Returns:
            delta_ln (array_like):
                Log-normal transform of input density field.
        """
        # Use similar normalisation strategy to nbodykit lognormal_transform()
        delta_ln = np.exp(delta_x)
        delta_ln /= np.mean(delta_ln)
        delta_ln -= 1.
        return delta_ln


    def realise_density_cola(self, redshift=None, redshift_init=15., 
                             keep_velocities=True, seed=None, inplace=True):
        """
        Create realisation of the matter power spectrum by running a COLA 
        (Comoving-Lagrangian) approximate N-body simulation.

        N.B: The ``pycola3`` package must be installed to use this method.

        This type of density realisation code assumes 

        Parameters:
            redshift (float, optional):
                If specified, use this redshift as the final redshift of the COLA 
                evolution. Otherwise, `self.redshift` is used.

            redshift_init (float, optional):
                The initial redshift to evolve the box from using COLA.

            keep_velocities (bool, optional):
                If True, deposit the velocities output by COLA onto a grid and use 
                those as the velocity fields. Otherwise, velocity data are not 
                stored.

            seed (int, optional):
                Random seed to use when generating initial conditions. If None, a 
                random integer will be used as the random seed instead. 

            inplace (bool, optional):
                If True, store the resulting density field and its Fourier transform 
                into the `self.delta_x` and `self.delta_k` variables as well as 
                returning `delta_x`.

        Returns:
            delta_x (array_like):
                3D array of delta_x values.
        """
        import pycola3
        assert self.Lx == self.Ly == self.Lz, \
            "realise_density_cola() requires a cubic box with Lx=Ly=Lz"

        # Get redshift
        if redshift is None:
            redshift = self.redshift
        assert redshift_init > redshift, "Must have redshift_init > redshift"

        # Fractional matter density
        Omega_m = self.cosmo['Omega_c'] + self.cosmo['Omega_b']

        # Power spectrum function at z=0, in h/Mpc units
        h = self.cosmo['h']
        pspec = lambda k: h**3. * ccl.linear_matter_power(self.cosmo, k * h, a=1.)

        # Initialise COLABox object
        # (note that the input matter power spectrum should be evaluated at z=0)
        box = pycola3.COLABox(
            ngrid=self.N,
            nparticles=self.N,
            box_size=self.Lx * h, # Mpc/h
            z_init=redshift_init,
            z_final=redshift,
            omega_m=Omega_m,
            h=h,
            pspec=pspec
        )

        # Initialise initial particle displacements
        if seed is None:
            seed = np.random.randint(0, 10000000)
        box.generate_initial_conditions(seed=seed)

        # Evolve the initial displacements until the final redshift
        px, py, pz, vx, vy, vz = box.evolve(n_steps=int(1 + box.z_init))

        # Deposit final particle disp. on regular grid to make density field
        density = box.cic_deposit()
        delta_x = density - 1.0  # this is valid if nparticles == ngrid
        if inplace:
            self.delta_x = delta_x

        # Deposit velocities onto grid
        method = 'linear'
        if keep_velocities:
            # Output velocity grids
            vel_x = np.zeros((self.N, self.N, self.N), dtype='float32')
            vel_y = np.zeros((self.N, self.N, self.N), dtype='float32')
            vel_z = np.zeros((self.N, self.N, self.N), dtype='float32')

            # Particle positions in COLA box
            part_vec = np.column_stack((px.flatten(), py.flatten(), pz.flatten()))
            part_vec /= self.cosmo['h'] # convert Mpc/h -> Mpc

            # Positions of box grid points
            box_vec = np.column_stack((self.x.flatten() - self.x.min(), 
                                       self.y.flatten() - self.y.min(), 
                                       self.z.flatten() - self.z.min()))

            # Interpolate velocities onto regular grid
            # FIXME: Beware float32 vs float64
            vel_x[:,:,:] = griddata(points=part_vec, 
                                    values=vx.flatten(), 
                                    xi=box_vec, 
                                    method=method,
                                    fill_value=0.).reshape(vel_x.shape)
            vel_y[:,:,:] = griddata(points=part_vec, 
                                    values=vy.flatten(), 
                                    xi=box_vec, 
                                    method=method,
                                    fill_value=0.).reshape(vel_x.shape)
            vel_z[:,:,:] = griddata(points=part_vec, 
                                    values=vz.flatten(), 
                                    xi=box_vec, 
                                    method=method,
                                    fill_value=0.).reshape(vel_x.shape)

            # Rescale units of velocity (see evolve.py)
            # v_{rsd}\equiv (ds/d\eta)/(a H(a))
            a_final = 1. / (1. + redshift)
            rsd_fac = a_final / pycola3.growth._q_factor(a_final, Omega_m, 1.-Omega_m)
            H0 = 100. * self.cosmo['h'] # H_0 in km/s
            vel_x *= H0 / rsd_fac # FIXME: Check factor!
            vel_y *= H0 / rsd_fac
            vel_z *= H0 / rsd_fac

            return delta_x, vel_x, vel_y, vel_z

        # Return density field only
        return delta_x

    ############################################################################
    # Output quantities related to the realisation
    ############################################################################

    def window(self, k, R):
        """
        Fourier transform of tophat window function *squared*, used to calculate 
        sigmaR etc. See "Cosmology", S. Weinberg, Eq.8.1.46.

        Parameters:
            k (array_like):
                Wavenumber, in Mpc^-1.

            R (array_like): 
                Top-hat radius, in Mpc.

        Returns:
            W (array_like):
                Window function squared.
        """
        x = k*R
        f = (3. / x**3.) * ( np.sin(x) - x*np.cos(x) )
        return f**2.

    def window1(self, k, R):
        """
        Fourier transform of tophat window function, used to calculate 
        sigmaR etc. See "Cosmology", S. Weinberg, Eq.8.1.46.

        Parameters:
            k (array_like):
                Wavenumber, in Mpc^-1.

            R (array_like): 
                Top-hat radius, in Mpc.

        Returns:
            W (array_like):
                Window function.
        """
        x = k*R
        f = (3. / x**3.) * ( np.sin(x) - x*np.cos(x) )
        return f

    def smooth_field(self, field_k, R):
        """
        Smooth a given (Fourier-space) field using a tophat filter with scale 
        R h^-1 Mpc, and then return real-space copy of smoothed field.

        Parameters:
            field_k (array_like):
                Fourier-space field to be smoothed (complex-valued).

            R (array_like): 
                Top-hat radius, in Mpc/h (note the units!).

        Returns:
            field_x (array_like):
                Smoothed real-space field.
        """
        dk = field_k #np.reshape(field_k, np.shape(self.k))
        dk = dk * self.window1(self.k, R/self.cosmo['h'])
        dk = np.nan_to_num(dk)
        dx = fft.ifftn(dk)
        return dx

    def sigmaR(self, R):
        """
        Get variance of matter perturbations, smoothed with a tophat filter 
        of radius R h^-1 Mpc.

        Parameters:
            R (array_like): 
                Top-hat radius, in Mpc/h (note the units!).

        Returns:
            sigmaR (array_like):
                RMS of the smoothed field.
        """
        # Need binned power spectrum, with k flat, monotonic for integration.
        k, pk, stddev = self.binned_power_spectrum()

        # Only use non-NaN values
        good_idxs = ~np.isnan(pk)
        pk = pk[good_idxs]
        k = k[good_idxs]

        # Discretely-sampled integrand, y
        y = k**2. * pk * self.window(k, R/self.cosmo['h'])
        I = scipy.integrate.simps(y, k)

        # Return sigma_R (note factor of 4pi / (2pi)^3 from integration)
        return np.sqrt( I / (2. * np.pi**2.) )

    def sigma8(self):
        """
        Get variance of matter perturbations on smoothing scale 
        of 8 h^-1 Mpc.

        Returns:
            sigma8 (array_like):
                RMS of the smoothed field for R = 8 Mpc/h.
        """
        return self.sigmaR(8.0)

    def binned_power_spectrum(self, delta_x=None, delta_k=None, nbins=20, kbins=None):
        """
        Return a binned power spectrum, calculated from the realisation.

        Parameters:
            delta_x (array_like, optional):
                Density fluctuation field. If this and `delta_k` are None, will use 
                `self.delta_k` as the field.

            delta_k (array_like, optional):
                Fourier-space density fluctuation field. If this and `delta_x` are 
                None, will use `self.delta_k` as the field.

            nbins (int, optional):
                Number of k bins to use, spanning [self.kmin, self.kmax]. Will be 
                ignored if `kbins` is set.

            kbins (array_like, optional):
                If specified, use this array as the k bin edges.

        Returns:
            kc, pk, sigma_pk (array_like):
                Measured 1D power spectrum and statistical error bars at 
                particular wavenumbers.

                - ``kc (array_like)``: Centroids of k bins.

                - ``pk (array_like)``: Power spectrum values (with correct 
                  DFT/volume normalisation).

                - ``sigma_pk (array_like)``: Estimate of the empirical error on 
                  ``pk`` in each bin (calculated as 
                  ``sigma_pk = stddev(pk) / sqrt(N_pk)`` in each bin).
        """
        # Check inputs
        if delta_x is not None and delta_k is not None:
            raise ValueError("delta_x and delta_k specified; can only specify one")

        # Do FFT of delta_x if specified; otherwise use delta_k or self.delta_k
        if delta_x is not None:
            delta_k = fft.fftn(delta_x)
        if delta_k is None:
            delta_k = self.delta_k

        # Calculate the (noisy, unbinned) power spectrum and normalise
        pk = delta_k * np.conj(delta_k)
        pk = pk.real / self.boxfactor

        # Bin edges/centroids
        if kbins is not None:
            bins = kbins
        else:
            # Logarithmically-distributed bins in k-space.
            bins = np.logspace(np.log10(self.kmin), np.log10(self.kmax), nbins)
        _bins = [0.0] + list(bins) # Add zero to the beginning
        cent = [0.5*(_bins[j+1] + _bins[j]) for j in range(bins.size)]

        # Initialise arrays
        vals = np.zeros(bins.size)
        stddev = np.zeros(bins.size)

        # Identify which bin each element of 'pk' should go into
        idxs = np.digitize(self.k.flatten(), bins)

        # For each bin, get the average pk value in that bin
        for i in range(bins.size):
            ii = np.where(idxs==i, True, False)
            vals[i] = np.mean(pk.flatten()[ii])
            stddev[i] = np.std(pk.flatten()[ii]) / np.sqrt(pk.flatten()[ii].size)
            # ^ This is a crude estimate of the error on the mean

        # First value is garbage, so throw it away
        return np.array(cent[1:]), np.array(vals[1:]), np.array(stddev[1:])

    def theoretical_power_spectrum(self):
        """
        Calculate the theoretical nonlinear power spectrum for the given 
        cosmological parameters, using CCL. Does not depend on the realisation.

        Returns:
            k, pk (array_like):
                Wavenumbers, from 10^-3.5 to 10^1, in Mpc^-1, and the 
                theoretical nonlinear power spectrum, in (Mpc)^3.
        """
        k = np.logspace(-3.5, 1., int(1e3))
        pk = ccl.nonlin_matter_power(self.cosmo, k=k, a=self.scale_factor)
        return k, pk


    ############################################################################
    # Information about the box
    ############################################################################

    def freq_array(self, redshift=None):
        """
        Return frequency array coordinates (in the z direction of the box).

        This approximates the frequency channel width to be constant across the 
        box, which is only a good approximation in the distant observer 
        approximation.

        Parameters:
            redshift (float, optional):
                Redshift to evaluate the centre of the box at. Default: Same value 
                as ``self.redshift``.

        Returns:
            freqs (array_like):
                Frequencies, in MHz. Frequency decreases as z coordinate 
                increases.
        """
        # Check redshift
        if redshift is None:
            redshift = self.redshift
        a = 1. / (1. + redshift)

        # Calculate central frequency of box
        freq_centre = a * self.line_freq

        # Comoving voxel size
        dx = self.Lz / self.N

        # Convert comoving voxel size to frequency channel size
        # df / dr = df / da * (dr / da)^-1 = f0 * (a^2 H) / c
        Hz = 100. * self.cosmo['h'] * ccl.h_over_h0(self.cosmo, a) # km/s/Mpc
        df = dx * self.line_freq * (a**2. * Hz) / (C / 1e3) # Same units as line_freq

        # Comoving units in z direction: place origin in centre of box
        freqs = freq_centre \
              + df * (np.arange(self.N) - 0.5*(self.N - 1.))

        # Frequency is decreasing with increasing z coordinate
        return freqs[::-1]


    def pixel_array(self, redshift=None):
        """
        Return angular pixel coordinate array in degrees.

        Parameters:
            redshift (float, optional):
                Redshift to evaluate the centre of the box at. Default: Same value 
                as ``self.redshift``.

        Returns:
            pix_x, pix_y (array_like):
                Coordinates of pixel centres in the x and y directions, in degrees. 
                The origin is the centre of the box.
        """
        # Check redshift
        if redshift is None:
            redshift = self.redshift
        scale_factor = 1. / (1. + redshift)

        # Calculate comoving distance to box redshift
        r = ccl.comoving_angular_distance(self.cosmo, scale_factor)

        # Comoving pixel size
        x_px = self.x[1] - self.x[0]
        y_px = self.y[1] - self.y[0]

        # Angular pixel size
        ang_x = (180. / np.pi) * (x_px / r)
        ang_y = (180. / np.pi) * (y_px / r)

        # Pixel index grid; place origin in centre of box
        grid = np.arange(self.N) - 0.5*(self.N - 1.)

        return ang_x*grid, ang_y*grid


    ############################################################################
    # Tests for consistency and accuracy
    ############################################################################

    def test_sampling_error(self):
        """
        P(k) is sampled within some finite window in the interval 
        `[kmin, kmax]`, where `kmin=2pi/L` and `kmax=2pi*sqrt(3)*(N/2)*(1/L)` 
        (for 3D FT). The lack of sampling in some regions of k-space means that 
        sigma8 can't be perfectly reconstructed (see U-L. Pen, 
        arXiv:astro-ph/9709261 for a discussion).

        This function calculates sigma8 from the realised box, and compares 
        this with the theoretical calculation for sigma8 over a large 
        k-window, and over a k-window of the same size as for the box.
        """

        # Calc. sigma8 from the realisation
        s8_real = self.sigma8()

        # Calc. theoretical sigma8 in same k-window as realisation
        _k = np.linspace(self.kmin, self.kmax, int(5e3))
        _pk = ccl.nonlin_matter_power(self.cosmo, k=_k, a=self.scale_factor)
        _y = _k**2. * _pk * self.window(_k, 8.0/self.cosmo['h'])
        _y = np.nan_to_num(_y)
        s8_th_win = np.sqrt( scipy.integrate.simps(_y, _k) / (2. * np.pi**2.) )

        # Calc. full sigma8 (in window that is wide enough)
        _k2 = np.logspace(-5, 2, int(5e4))
        _pk2 = ccl.nonlin_matter_power(self.cosmo, k=_k2, a=self.scale_factor)
        _y2 = _k2**2. * _pk2 * self.window(_k2, 8.0/self.cosmo['h'])
        _y2 = np.nan_to_num(_y2)
        s8_th_full = np.sqrt( scipy.integrate.simps(_y2, _k2) / (2. * np.pi**2.) )

        # Calculate sigma8 in real space
        dk = np.reshape(self.delta_k, np.shape(self.k))
        dk = dk * self.window1(self.k, 8.0/self.cosmo['h'])
        dk = np.nan_to_num(dk)
        dx = fft.ifftn(dk)
        s8_realspace = np.std(dx)

        # sigma20
        dk = np.reshape(self.delta_k, np.shape(self.k))
        dk = dk * self.window1(self.k, 20.0/self.cosmo['h'])
        dk = np.nan_to_num(dk)
        dx = fft.ifftn(dk)
        s20_realspace = np.std(dx)

        s20_real = self.sigmaR(20.)

        # Print report
        print("")
        print("sigma8 (real.): \t", s8_real)
        print("sigma8 (th.win.):\t", s8_th_win)
        print("sigma8 (th.full):\t", s8_th_full)
        print("sigma8 (realsp.):\t", s8_realspace)
        print("ratio =", 1. / (s8_real / s8_realspace))
        print("")
        print("sigma20 (real.): \t", s20_real)
        print("sigma20 (realsp.):\t", s20_realspace)
        print("ratio =", 1. / (s20_real / s20_realspace))
        print("var(delta) =", np.std(self.delta_x))


    def test_parseval(self):
        """
        Ensure that Parseval's theorem is satisfied for delta_x and delta_k, 
        i.e. <delta_x^2> = Sum_k[P(k)]. Important consistency check for FT;
        should return unity if everything is OK.

        Returns:
            s1, s2 (float):
                `s1` is the sum (~integral) over the `delta_x` field, 
                ``sum(delta_x^2) * N^3``, and `s2` is the sum (~integral) over 
                the `delta_k` field, ``Re[sum(delta_k delta_k^*)]. 
                Should find that ``s1 == s2``.
        """
        s1 = np.sum(self.delta_x**2.) * self.N**3.
        # ^ Factor of N^3 missing due to averaging
        s2 = np.sum(self.delta_k*np.conj(self.delta_k)).real
        print("Parseval test:", s1/s2, "(should be 1.0)")
        return s1, s2

__init__(self, cosmo, box_scale=1000.0, nsamp=32, redshift=0.0, line_freq=1420.405752, realise_now=True) special

Initialise a box containing a matter distribution with a given power spectrum.

Parameters:

Name Type Description Default
cosmo CCL.Cosmology object or dict

Cosmology object. If passed as a dictionary, this will be used to create a new CCL Cosmology object.

required
box_scale float or tuple

The side length (in Mpc) of the cubic box. If passed as a tuple, this will specify the scales in the x, y, and z Cartesian directions separately.

1000.0
nsamp int

The number of grid points per dimension.

32
redshift float

The redshift to place the box at. This affects the redshift at which the power spectrum is evaluated when generating the fields.

0.0
line_freq float

Frequency of the emission line used as the redshift reference, in MHz.

1420.405752
realise_now bool

If True, generate realisations of the density, velocity, and potential immediately on initialisation.

True

!!! note The self.delta_k field is not in proper cosmological units; to get the power spectrum in the right units, a factor of self.boxfactor is needed for example.

Source code in fastbox/box.py
def __init__(self, cosmo, box_scale=1e3, nsamp=32, redshift=0., 
             line_freq=1420.405752, realise_now=True):
    """
    Initialise a box containing a matter distribution with a given power 
    spectrum.

    Parameters:
        cosmo (CCL.Cosmology object or dict):
            Cosmology object. If passed as a dictionary, this will be used to 
            create a new CCL Cosmology object.

        box_scale (float or tuple, optional):
            The side length (in Mpc) of the cubic box. If passed as a tuple, 
            this will specify the scales in the x, y, and z Cartesian 
            directions separately.

        nsamp (int, optional):
            The number of grid points per dimension.

        redshift (float, optional):
            The redshift to place the box at. This affects the redshift at 
            which the power spectrum is evaluated when generating the fields.

        line_freq (float, optional):
            Frequency of the emission line used as the redshift reference, in 
            MHz.

        realise_now (bool, optional):
            If True, generate realisations of the density, velocity, and 
            potential immediately on initialisation.

    Note:
        The `self.delta_k` field is not in proper cosmological units; to 
        get the power spectrum in the right units, a factor of `self.boxfactor` 
        is needed for example.
    """
    if isinstance(cosmo, dict):
        cosmo = ccl.Cosmology(**cosmo)
    if not isinstance(cosmo, ccl.Cosmology):
        raise TypeError("`cosmo` must be a CCL Cosmology object or dict.")
    self.cosmo = cosmo # Cosmological parameters, required by Cosmolopy fns.

    # Number of sample points per dimension
    self.N = nsamp

    # Box redshift and emission line reference
    self.redshift = redshift
    self.scale_factor = 1. / (1. + redshift)
    self.line_freq = line_freq

    # Define grid coordinates along each dimension
    if isinstance(box_scale, tuple):
        assert len(box_scale) == 3, "Must specify scale of x, y, z dimensions"
        scale_x, scale_y, scale_z = box_scale
        self.x = np.linspace(-0.5*scale_x, 0.5*scale_x, nsamp) # in Mpc
        self.y = np.linspace(-0.5*scale_y, 0.5*scale_y, nsamp) # in Mpc
        self.z = np.linspace(-0.5*scale_z, 0.5*scale_z, nsamp) # in Mpc
        self.Lx = self.x[-1] - self.x[0] # Linear size of box
        self.Ly = self.y[-1] - self.y[0] # Linear size of box
        self.Lz = self.z[-1] - self.z[0] # Linear size of box
    else:
        self.x = self.y = self.z = np.linspace(-0.5*box_scale, 
                                                0.5*box_scale, 
                                                nsamp) # in Mpc
        self.Lx = self.Ly = self.Lz = self.x[-1] - self.x[0] # Linear size of box

    # Conversion factor for FFT of power spectrum
    # For an example, see in liteMap.py:fillWithGaussianRandomField() in 
    # Flipper, by Sudeep Das. http://www.astro.princeton.edu/~act/flipper
    self.boxfactor = (self.N**6.) / (self.Lx * self.Ly * self.Lz)

    # Fourier mode array
    self.set_fft_sample_spacing() # 3D array, arranged in correct order

    # Min./max. k modes in 3D (excl. zero mode)
    self.kmin = 2.*np.pi/np.max([self.Lx, self.Ly, self.Lz])
    self.kmax = 2.*np.pi*np.sqrt(3.)*self.N/np.min([self.Lx, self.Ly, self.Lz])

    # Create a realisation of density/velocity perturbations in the box
    if realise_now:
        self.realise_density()
        self.realise_velocity()
        self.realise_potential()

apply_transfer_fn(self, field_k, transfer_fn)

Apply a Fourier-space transfer function and transform back to real space.

Parameters:

Name Type Description Default
field_k ndarray

Field in Fourier space (3D array), with Fourier modes self.kx,ky,kz.

required
transfer_fn callable

Function that modulates the Fourier-space field. Must have call signature transfer_fn(k_perp, k_par).

required

Returns:

Type Description
field_x (ndarray)

Real-space field that has had the transfer function applied.

Source code in fastbox/box.py
def apply_transfer_fn(self, field_k, transfer_fn):
    """
    Apply a Fourier-space transfer function and transform back to real space.

    Parameters:
        field_k (ndarray):
            Field in Fourier space (3D array), with Fourier modes self.kx,ky,kz.

        transfer_fn (callable):
            Function that modulates the Fourier-space field. Must have 
            call signature `transfer_fn(k_perp, k_par)`.

    Returns:
        field_x (ndarray):
            Real-space field that has had the transfer function applied.
    """
    # Get 2D Fourier modes
    #fac = (2.*np.pi/self.L)
    k_perp = 2.*np.pi * np.sqrt((self.Kx/self.Lx)**2. + (self.Ky/self.Ly)**2.)
    k_par = 2.*np.pi * self.Kz / self.Lz

    # Apply transfer function, perform inverse FFT, and return
    dk = field_k * transfer_fn(k_perp, k_par)
    dk = np.nan_to_num(dk)
    dx = fft.ifftn(dk)
    return dx

binned_power_spectrum(self, delta_x=None, delta_k=None, nbins=20, kbins=None)

Return a binned power spectrum, calculated from the realisation.

Parameters:

Name Type Description Default
delta_x array_like

Density fluctuation field. If this and delta_k are None, will use self.delta_k as the field.

None
delta_k array_like

Fourier-space density fluctuation field. If this and delta_x are None, will use self.delta_k as the field.

None
nbins int

Number of k bins to use, spanning [self.kmin, self.kmax]. Will be ignored if kbins is set.

20
kbins array_like

If specified, use this array as the k bin edges.

None

Returns:

Type Description
kc, pk, sigma_pk (array_like)

Measured 1D power spectrum and statistical error bars at particular wavenumbers.

- ``kc (array_like)``: Centroids of k bins.

- ``pk (array_like)``: Power spectrum values (with correct 
  DFT/volume normalisation).

- ``sigma_pk (array_like)``: Estimate of the empirical error on 
  ``pk`` in each bin (calculated as 
  ``sigma_pk = stddev(pk) / sqrt(N_pk)`` in each bin).
Source code in fastbox/box.py
def binned_power_spectrum(self, delta_x=None, delta_k=None, nbins=20, kbins=None):
    """
    Return a binned power spectrum, calculated from the realisation.

    Parameters:
        delta_x (array_like, optional):
            Density fluctuation field. If this and `delta_k` are None, will use 
            `self.delta_k` as the field.

        delta_k (array_like, optional):
            Fourier-space density fluctuation field. If this and `delta_x` are 
            None, will use `self.delta_k` as the field.

        nbins (int, optional):
            Number of k bins to use, spanning [self.kmin, self.kmax]. Will be 
            ignored if `kbins` is set.

        kbins (array_like, optional):
            If specified, use this array as the k bin edges.

    Returns:
        kc, pk, sigma_pk (array_like):
            Measured 1D power spectrum and statistical error bars at 
            particular wavenumbers.

            - ``kc (array_like)``: Centroids of k bins.

            - ``pk (array_like)``: Power spectrum values (with correct 
              DFT/volume normalisation).

            - ``sigma_pk (array_like)``: Estimate of the empirical error on 
              ``pk`` in each bin (calculated as 
              ``sigma_pk = stddev(pk) / sqrt(N_pk)`` in each bin).
    """
    # Check inputs
    if delta_x is not None and delta_k is not None:
        raise ValueError("delta_x and delta_k specified; can only specify one")

    # Do FFT of delta_x if specified; otherwise use delta_k or self.delta_k
    if delta_x is not None:
        delta_k = fft.fftn(delta_x)
    if delta_k is None:
        delta_k = self.delta_k

    # Calculate the (noisy, unbinned) power spectrum and normalise
    pk = delta_k * np.conj(delta_k)
    pk = pk.real / self.boxfactor

    # Bin edges/centroids
    if kbins is not None:
        bins = kbins
    else:
        # Logarithmically-distributed bins in k-space.
        bins = np.logspace(np.log10(self.kmin), np.log10(self.kmax), nbins)
    _bins = [0.0] + list(bins) # Add zero to the beginning
    cent = [0.5*(_bins[j+1] + _bins[j]) for j in range(bins.size)]

    # Initialise arrays
    vals = np.zeros(bins.size)
    stddev = np.zeros(bins.size)

    # Identify which bin each element of 'pk' should go into
    idxs = np.digitize(self.k.flatten(), bins)

    # For each bin, get the average pk value in that bin
    for i in range(bins.size):
        ii = np.where(idxs==i, True, False)
        vals[i] = np.mean(pk.flatten()[ii])
        stddev[i] = np.std(pk.flatten()[ii]) / np.sqrt(pk.flatten()[ii].size)
        # ^ This is a crude estimate of the error on the mean

    # First value is garbage, so throw it away
    return np.array(cent[1:]), np.array(vals[1:]), np.array(stddev[1:])

freq_array(self, redshift=None)

Return frequency array coordinates (in the z direction of the box).

This approximates the frequency channel width to be constant across the box, which is only a good approximation in the distant observer approximation.

Parameters:

Name Type Description Default
redshift float

Redshift to evaluate the centre of the box at. Default: Same value as self.redshift.

None

Returns:

Type Description
freqs (array_like)

Frequencies, in MHz. Frequency decreases as z coordinate increases.

Source code in fastbox/box.py
def freq_array(self, redshift=None):
    """
    Return frequency array coordinates (in the z direction of the box).

    This approximates the frequency channel width to be constant across the 
    box, which is only a good approximation in the distant observer 
    approximation.

    Parameters:
        redshift (float, optional):
            Redshift to evaluate the centre of the box at. Default: Same value 
            as ``self.redshift``.

    Returns:
        freqs (array_like):
            Frequencies, in MHz. Frequency decreases as z coordinate 
            increases.
    """
    # Check redshift
    if redshift is None:
        redshift = self.redshift
    a = 1. / (1. + redshift)

    # Calculate central frequency of box
    freq_centre = a * self.line_freq

    # Comoving voxel size
    dx = self.Lz / self.N

    # Convert comoving voxel size to frequency channel size
    # df / dr = df / da * (dr / da)^-1 = f0 * (a^2 H) / c
    Hz = 100. * self.cosmo['h'] * ccl.h_over_h0(self.cosmo, a) # km/s/Mpc
    df = dx * self.line_freq * (a**2. * Hz) / (C / 1e3) # Same units as line_freq

    # Comoving units in z direction: place origin in centre of box
    freqs = freq_centre \
          + df * (np.arange(self.N) - 0.5*(self.N - 1.))

    # Frequency is decreasing with increasing z coordinate
    return freqs[::-1]

lognormal(self, delta_x)

Return a log-normal transform of the input field (see Eq. 3.1 of arXiv:1706.09195; also c.f. Eq. 7 of Alonso et al., arXiv:1405.1751, which differs by a factor of 1/2).

Parameters:

Name Type Description Default
delta_x array_like

Density field (shoudl be Gaussian, generated using a linear matter power spectrum).

required

Returns:

Type Description
delta_ln (array_like)

Log-normal transform of input density field.

Source code in fastbox/box.py
def lognormal(self, delta_x):
    """
    Return a log-normal transform of the input field (see Eq. 3.1 of 
    arXiv:1706.09195; also c.f. Eq. 7 of Alonso et al., arXiv:1405.1751, 
    which differs by a factor of 1/2).

    Parameters:
        delta_x (array_like):
            Density field (shoudl be Gaussian, generated using a linear matter 
            power spectrum).

    Returns:
        delta_ln (array_like):
            Log-normal transform of input density field.
    """
    # Use similar normalisation strategy to nbodykit lognormal_transform()
    delta_ln = np.exp(delta_x)
    delta_ln /= np.mean(delta_ln)
    delta_ln -= 1.
    return delta_ln

pixel_array(self, redshift=None)

Return angular pixel coordinate array in degrees.

Parameters:

Name Type Description Default
redshift float

Redshift to evaluate the centre of the box at. Default: Same value as self.redshift.

None

Returns:

Type Description
pix_x, pix_y (array_like)

Coordinates of pixel centres in the x and y directions, in degrees. The origin is the centre of the box.

Source code in fastbox/box.py
def pixel_array(self, redshift=None):
    """
    Return angular pixel coordinate array in degrees.

    Parameters:
        redshift (float, optional):
            Redshift to evaluate the centre of the box at. Default: Same value 
            as ``self.redshift``.

    Returns:
        pix_x, pix_y (array_like):
            Coordinates of pixel centres in the x and y directions, in degrees. 
            The origin is the centre of the box.
    """
    # Check redshift
    if redshift is None:
        redshift = self.redshift
    scale_factor = 1. / (1. + redshift)

    # Calculate comoving distance to box redshift
    r = ccl.comoving_angular_distance(self.cosmo, scale_factor)

    # Comoving pixel size
    x_px = self.x[1] - self.x[0]
    y_px = self.y[1] - self.y[0]

    # Angular pixel size
    ang_x = (180. / np.pi) * (x_px / r)
    ang_y = (180. / np.pi) * (y_px / r)

    # Pixel index grid; place origin in centre of box
    grid = np.arange(self.N) - 0.5*(self.N - 1.)

    return ang_x*grid, ang_y*grid

realise_density(self, linear=False, redshift=None, inplace=True)

Create realisation of the matter power spectrum by randomly sampling from Gaussian distributions of variance P(k) for each k mode.

Parameters:

Name Type Description Default
linear bool

If True, use the linear matter power spectrum to do the Gaussian random realisation instead of the non-linear power spectrum set in self.cosmo.

False
redshift float

If specified, use this redshift to calculate the matter power spectrum for the Gaussian random realisation. Otherwise, the value of self.redshift is used. Default: None.

None
inplace bool

If True, store the resulting density field and its Fourier transform into the self.delta_x and self.delta_k variables as well as returning delta_x.

True

Returns:

Type Description
delta_x (array_like)

3D array of delta_x values.

Source code in fastbox/box.py
def realise_density(self, linear=False, redshift=None, inplace=True):
    """
    Create realisation of the matter power spectrum by randomly sampling 
    from Gaussian distributions of variance P(k) for each k mode.

    Parameters:
        linear (bool, optional):
            If True, use the linear matter power spectrum to do the Gaussian 
            random realisation instead of the non-linear power spectrum set in 
            `self.cosmo`.

        redshift (float, optional):
            If specified, use this redshift to calculate the matter power 
            spectrum for the Gaussian random realisation. Otherwise, the value 
            of `self.redshift` is used. Default: None.

        inplace (bool, optional):
            If True, store the resulting density field and its Fourier transform 
            into the `self.delta_x` and `self.delta_k` variables as well as 
            returning `delta_x`.

    Returns:
        delta_x (array_like):
            3D array of delta_x values.
    """
    # Get redshift
    if redshift is None:
        redshift = self.redshift
    scale_factor = 1. / (1. + redshift)

    # Calculate matter power spectrum
    k = self.k.flatten()
    if linear:
        pk = ccl.linear_matter_power(self.cosmo, k=k, a=scale_factor)
    else:
        pk = ccl.nonlin_matter_power(self.cosmo, k=k, a=scale_factor)
    pk = np.reshape(pk, np.shape(self.k))
    pk = np.nan_to_num(pk) # Remove NaN at k=0 (and any others...)

    # Normalise the power spectrum properly (factor of volume, and norm. 
    # factor of 3D DFT)
    pk *= self.boxfactor

    # Generate Gaussian random field with given power spectrum
    re = np.random.normal(0.0, 1.0, np.shape(self.k))
    im = np.random.normal(0.0, 1.0, np.shape(self.k))
    delta_k = ( re + 1j*im ) * np.sqrt(pk) # (makes variance too high by 2x)
    if inplace:
        if redshift != self.redshift:
            print("Warning: Storing density field into self.delta_x with a "
                  "different redshift than self.redshift.")
        self.delta_k = delta_k

    # Transform to real space. Here, we are discarding the imaginary part 
    # of the inverse FT! But we can recover the correct (statistical) 
    # result by multiplying by a factor of sqrt(2) [see above, where this 
    # factor was already omitted when defining delta_k].
    delta_x = fft.ifftn(self.delta_k).real # This ensures the field is real
    if inplace:
        self.delta_x = delta_x

    # Finally, get the Fourier transform back
    if inplace:
        self.delta_k = fft.fftn(self.delta_x)
    return delta_x

realise_density_cola(self, redshift=None, redshift_init=15.0, keep_velocities=True, seed=None, inplace=True)

Create realisation of the matter power spectrum by running a COLA (Comoving-Lagrangian) approximate N-body simulation.

N.B: The pycola3 package must be installed to use this method.

This type of density realisation code assumes

Parameters:

Name Type Description Default
redshift float

If specified, use this redshift as the final redshift of the COLA evolution. Otherwise, self.redshift is used.

None
redshift_init float

The initial redshift to evolve the box from using COLA.

15.0
keep_velocities bool

If True, deposit the velocities output by COLA onto a grid and use those as the velocity fields. Otherwise, velocity data are not stored.

True
seed int

Random seed to use when generating initial conditions. If None, a random integer will be used as the random seed instead.

None
inplace bool

If True, store the resulting density field and its Fourier transform into the self.delta_x and self.delta_k variables as well as returning delta_x.

True

Returns:

Type Description
delta_x (array_like)

3D array of delta_x values.

Source code in fastbox/box.py
def realise_density_cola(self, redshift=None, redshift_init=15., 
                         keep_velocities=True, seed=None, inplace=True):
    """
    Create realisation of the matter power spectrum by running a COLA 
    (Comoving-Lagrangian) approximate N-body simulation.

    N.B: The ``pycola3`` package must be installed to use this method.

    This type of density realisation code assumes 

    Parameters:
        redshift (float, optional):
            If specified, use this redshift as the final redshift of the COLA 
            evolution. Otherwise, `self.redshift` is used.

        redshift_init (float, optional):
            The initial redshift to evolve the box from using COLA.

        keep_velocities (bool, optional):
            If True, deposit the velocities output by COLA onto a grid and use 
            those as the velocity fields. Otherwise, velocity data are not 
            stored.

        seed (int, optional):
            Random seed to use when generating initial conditions. If None, a 
            random integer will be used as the random seed instead. 

        inplace (bool, optional):
            If True, store the resulting density field and its Fourier transform 
            into the `self.delta_x` and `self.delta_k` variables as well as 
            returning `delta_x`.

    Returns:
        delta_x (array_like):
            3D array of delta_x values.
    """
    import pycola3
    assert self.Lx == self.Ly == self.Lz, \
        "realise_density_cola() requires a cubic box with Lx=Ly=Lz"

    # Get redshift
    if redshift is None:
        redshift = self.redshift
    assert redshift_init > redshift, "Must have redshift_init > redshift"

    # Fractional matter density
    Omega_m = self.cosmo['Omega_c'] + self.cosmo['Omega_b']

    # Power spectrum function at z=0, in h/Mpc units
    h = self.cosmo['h']
    pspec = lambda k: h**3. * ccl.linear_matter_power(self.cosmo, k * h, a=1.)

    # Initialise COLABox object
    # (note that the input matter power spectrum should be evaluated at z=0)
    box = pycola3.COLABox(
        ngrid=self.N,
        nparticles=self.N,
        box_size=self.Lx * h, # Mpc/h
        z_init=redshift_init,
        z_final=redshift,
        omega_m=Omega_m,
        h=h,
        pspec=pspec
    )

    # Initialise initial particle displacements
    if seed is None:
        seed = np.random.randint(0, 10000000)
    box.generate_initial_conditions(seed=seed)

    # Evolve the initial displacements until the final redshift
    px, py, pz, vx, vy, vz = box.evolve(n_steps=int(1 + box.z_init))

    # Deposit final particle disp. on regular grid to make density field
    density = box.cic_deposit()
    delta_x = density - 1.0  # this is valid if nparticles == ngrid
    if inplace:
        self.delta_x = delta_x

    # Deposit velocities onto grid
    method = 'linear'
    if keep_velocities:
        # Output velocity grids
        vel_x = np.zeros((self.N, self.N, self.N), dtype='float32')
        vel_y = np.zeros((self.N, self.N, self.N), dtype='float32')
        vel_z = np.zeros((self.N, self.N, self.N), dtype='float32')

        # Particle positions in COLA box
        part_vec = np.column_stack((px.flatten(), py.flatten(), pz.flatten()))
        part_vec /= self.cosmo['h'] # convert Mpc/h -> Mpc

        # Positions of box grid points
        box_vec = np.column_stack((self.x.flatten() - self.x.min(), 
                                   self.y.flatten() - self.y.min(), 
                                   self.z.flatten() - self.z.min()))

        # Interpolate velocities onto regular grid
        # FIXME: Beware float32 vs float64
        vel_x[:,:,:] = griddata(points=part_vec, 
                                values=vx.flatten(), 
                                xi=box_vec, 
                                method=method,
                                fill_value=0.).reshape(vel_x.shape)
        vel_y[:,:,:] = griddata(points=part_vec, 
                                values=vy.flatten(), 
                                xi=box_vec, 
                                method=method,
                                fill_value=0.).reshape(vel_x.shape)
        vel_z[:,:,:] = griddata(points=part_vec, 
                                values=vz.flatten(), 
                                xi=box_vec, 
                                method=method,
                                fill_value=0.).reshape(vel_x.shape)

        # Rescale units of velocity (see evolve.py)
        # v_{rsd}\equiv (ds/d\eta)/(a H(a))
        a_final = 1. / (1. + redshift)
        rsd_fac = a_final / pycola3.growth._q_factor(a_final, Omega_m, 1.-Omega_m)
        H0 = 100. * self.cosmo['h'] # H_0 in km/s
        vel_x *= H0 / rsd_fac # FIXME: Check factor!
        vel_y *= H0 / rsd_fac
        vel_z *= H0 / rsd_fac

        return delta_x, vel_x, vel_y, vel_z

    # Return density field only
    return delta_x

realise_potential(self, delta_x=None, delta_k=None, redshift=None, inplace=True)

Realise the potential in Fourier space.

Phi = 3/2 (Omega_m H_0^2) (D(a) / a) delta(k) / k^2

The DFT prefactor has not been applied; to get the real-space potential, simply do ifftn(potential_k).

Parameters:

Name Type Description Default
delta_x array_like

Density fluctuation field. If this and delta_k are None, will use self.delta_k as the field. Default: None.

None
delta_k array_like

Fourier-space density fluctuation field. If this and delta_x are None, will use self.delta_k as the field. Default: None.

None
redshift float

If specified, use this redshift to calculate the matter power spectrum for the Gaussian random realisation. Otherwise, the value of self.redshift is used. Default: None.

None
inplace bool

If True, store the Fourier transform, velocity_k, of the resulting velocity field and its Fourier transform into the self.velocity_k variable. Default: True.

True

Returns:

Type Description
phi_k (array_like)

Potential field, in Fourier space.

Source code in fastbox/box.py
def realise_potential(self, delta_x=None, delta_k=None, redshift=None, 
                     inplace=True):
    """
    Realise the potential in Fourier space.

        Phi = 3/2 (Omega_m H_0^2) (D(a) / a) delta(k) / k^2

    The DFT prefactor has not been applied; to get the real-space potential, 
    simply do ifftn(potential_k).

    Parameters:
        delta_x (array_like, optional):
            Density fluctuation field. If this and `delta_k` are None, will use 
            `self.delta_k` as the field. Default: None.

        delta_k (array_like, optional):
            Fourier-space density fluctuation field. If this and `delta_x` are 
            None, will use `self.delta_k` as the field. Default: None.

        redshift (float, optional):
            If specified, use this redshift to calculate the matter power 
            spectrum for the Gaussian random realisation. Otherwise, the value 
            of `self.redshift` is used. Default: None.

        inplace (bool, optional):
            If True, store the Fourier transform, `velocity_k`, of the 
            resulting velocity field and its Fourier transform 
            into the `self.velocity_k` variable. Default: True.

    Returns:
        phi_k (array_like):
            Potential field, in Fourier space.
    """
    # Get redshift
    if redshift is None:
        redshift = self.redshift
    scale_factor = 1. / (1. + redshift)

    # Check inputs
    if delta_x is not None and delta_k is not None:
        raise ValueError("delta_x and delta_k specified; can only specify one")

    # Do FFT of delta_x if specified; otherwise use delta_k or self.delta_k
    if delta_x is not None:
        delta_k = fft.fftn(delta_x)
    if delta_k is None:
        delta_k = self.delta_k

    # Calculate pre-factor (FIXME: Make sure D isn't double-counted)
    # Phi = 3/2 (Omega_m H_0^2) (D(a) / a) delta(k) / k^2
    Omega_m = self.cosmo['Omega_c'] + self.cosmo['Omega_b']
    fac = (3./2.) * Omega_m * (100.*self.cosmo['h'])**2. \
        * ccl.growth_factor(self.cosmo, self.scale_factor)/self.scale_factor

    phi_k = delta_k / self.k**2.
    phi_k[0,0,0] = 0. # Fix monopole

    # Store result in object if requested
    if inplace:
        self.phi_k = phi_k
    return phi_k

realise_velocity(self, delta_x=None, delta_k=None, redshift=None, inplace=True)

Realise the (unscaled) velocity field in Fourier space (e.g. see Dodelson Eq. 9.18):

v(k) = i [f(a) H(a) a] delta_k vec{k} / k^2

The DFT prefactor has not been applied; to get the real-space velocity, simply do ifftn(velocity_k[i]), where i=0..2 for the x,y,z Cartesian directions.

Parameters:

Name Type Description Default
delta_x array_like

Density fluctuation field. If this and delta_k are None, will use self.delta_k as the field. Default: None.

None
delta_k array_like

Fourier-space density fluctuation field. If this and delta_x are None, will use self.delta_k as the field. Default: None.

None
redshift float

If specified, use this redshift to calculate the matter power spectrum for the Gaussian random realisation. Otherwise, the value of self.redshift is used. Default: None.

None
inplace bool

If True, store the Fourier transform, velocity_k, of the resulting velocity field and its Fourier transform into the self.velocity_k variable. Default: True.

True

Returns:

Type Description
velocity_k (tuple of array_like)

3-tuple of the x,y,z velocity field components in Fourier space, v_x(k), v_y(k), v_z(k). Apply ifftn() directly to any of the components to get the real-space velocity component with the correct normalisation.

Source code in fastbox/box.py
def realise_velocity(self, delta_x=None, delta_k=None, redshift=None, 
                     inplace=True):
    """
    Realise the (unscaled) velocity field in Fourier space (e.g. see 
    Dodelson Eq. 9.18):

        v(k) = i [f(a) H(a) a] delta_k vec{k} / k^2

    The DFT prefactor has not been applied; to get the real-space velocity, 
    simply do ifftn(velocity_k[i]), where i=0..2 for the x,y,z Cartesian 
    directions.

    Parameters:
        delta_x (array_like, optional):
            Density fluctuation field. If this and `delta_k` are None, will use 
            `self.delta_k` as the field. Default: None.

        delta_k (array_like, optional):
            Fourier-space density fluctuation field. If this and `delta_x` are 
            None, will use `self.delta_k` as the field. Default: None.

        redshift (float, optional):
            If specified, use this redshift to calculate the matter power 
            spectrum for the Gaussian random realisation. Otherwise, the value 
            of `self.redshift` is used. Default: None.

        inplace (bool, optional):
            If True, store the Fourier transform, `velocity_k`, of the 
            resulting velocity field and its Fourier transform 
            into the `self.velocity_k` variable. Default: True.

    Returns:
        velocity_k (tuple of array_like):
            3-tuple of the x,y,z velocity field components in Fourier space, 
            v_x(k), v_y(k), v_z(k). Apply ifftn() directly to any of the 
            components to get the real-space velocity component with the 
            correct normalisation.
    """
    # Get redshift
    if redshift is None:
        redshift = self.redshift
    scale_factor = 1. / (1. + redshift)

    # Check inputs
    if delta_x is not None and delta_k is not None:
        raise ValueError("delta_x and delta_k specified; can only specify one")

    # Do FFT of delta_x if specified; otherwise use delta_k or self.delta_k
    if delta_x is not None:
        delta_k = fft.fftn(delta_x)
    if delta_k is None:
        delta_k = self.delta_k

    # Get squared k-vector in k-space (and factor in scaling from kx, ky, kz)
    k2 = self.k**2.

    # Calculate components of A (the unscaled velocity)
    Ax = 1.j * delta_k * self.Kx * (2.*np.pi/self.Lx) / k2
    Ay = 1.j * delta_k * self.Ky * (2.*np.pi/self.Ly) / k2
    Az = 1.j * delta_k * self.Kz * (2.*np.pi/self.Lz) / k2
    Ax = np.nan_to_num(Ax)
    Ay = np.nan_to_num(Ay)
    Az = np.nan_to_num(Az)

    # If the FFT has an even number of samples, the most negative frequency 
    # mode must have the same value as the most positive frequency mode. 
    # However, when multiplying by 'i', allowing this mode to have a 
    # non-zero real part makes it impossible to satisfy the reality 
    # conditions. As such, we can set the whole mode to be zero, make sure 
    # that it's pure imaginary, or use an odd number of samples. Different 
    # ways of dealing with this could change the answer!
    if self.N % 2 == 0: # Even no. samples
        # Set highest (negative) freq. to zero
        mx = np.where(self.Kx == np.min(self.Kx))
        my = np.where(self.Ky == np.min(self.Ky))
        mz = np.where(self.Kz == np.min(self.Kz))
    #    self.Kx[mx] = 0.0; self.Ky[my] = 0.0; self.Kz[mz] = 0.0
    Ax[mx] = Ay[my] = Az[mz] = 0.

    # Apply prefactor, v(k) = i [f(a) H(a) a] delta_k vec{k} / k^2
    # N.B. velocity_k is missing a prefactor of 1/sqrt(self.box_factor).
    # If you do ifftn(velocity_k), you will get the real-space velocity 
    # field back with the correct scaling however
    fac = 100.*self.cosmo['h'] * ccl.h_over_h0(self.cosmo, a=scale_factor) \
        * ccl.growth_rate(self.cosmo, a=scale_factor) * scale_factor
    Ax *= fac
    Ay *= fac
    Az *= fac
    velocity_k = (Ax, Ay, Az)

    # Store result in object if requested
    if inplace:
        self.velocity_k = velocity_k
    return velocity_k

redshift_space_density(self, delta_x=None, velocity_z=None, sigma_nl=0.0, method='linear')

Remap the real-space density field to redshift-space using the line-of- sight velocity field.

Parameters:

Name Type Description Default
delta_x array_like

Real-space density field.

None
velocity_z array_like

Velocity in the z (line-of-sight) direction (km/s).

None
sigma_nl float

Optionally, add random small-scale incoherent velocities along the LOS (uncorrelated Gaussian; km/s).

0.0
method str

Interpolation method to use when performing remapping, using the scipy.interpolate.griddata function. Default: 'linear'.

'linear'
Source code in fastbox/box.py
def redshift_space_density(self, delta_x=None, velocity_z=None, sigma_nl=0., 
                           method='linear'):
    """
    Remap the real-space density field to redshift-space using the line-of-
    sight velocity field.

    Parameters:
        delta_x (array_like, optional):
            Real-space density field.

        velocity_z (array_like, optional):
            Velocity in the z (line-of-sight) direction (km/s).

        sigma_nl (float, optional):
            Optionally, add random small-scale incoherent velocities along the 
            LOS (uncorrelated Gaussian; km/s).

        method (str, optional):
            Interpolation method to use when performing remapping, using the 
            `scipy.interpolate.griddata` function. Default: 'linear'.
    """
    # Expansion rate (km/s/Mpc)
    Hz = 100.*self.cosmo['h'] * ccl.h_over_h0(self.cosmo, self.scale_factor)

    # Empty redshift-space array
    delta_s = np.zeros_like(delta_x) - 1. # Default value is -1 (void)

    # Loop over x and y pixels
    for i in range(delta_x.shape[0]):
        for j in range(delta_x.shape[1]):

            # Realisation of uncorrelated non-linear velocities
            vel_nl = 0.
            if sigma_nl > 0.:
                vel_nl = sigma_nl * np.random.normal(0., 1., self.z.size)

            # Redshift-space z coordinate (negative sign as we will map 
            # from real coord to redshift-space coord)
            s = self.z - (velocity_z[i,j,:] + vel_nl) / Hz

            # Apply periodic boundary conditions
            length_z = np.max(self.z) - np.min(self.z)
            s = (s - np.min(self.z)) % (length_z) + np.min(self.z)

            # Use average value of endpoints as fill value
            fill_value = 0.5 * (delta_x[i,j,0] + delta_x[i,j,-1])

            # Remap to redshift-space (on regular grid in redshift-space 
            # with same grid points as in 'z' array)
            delta_s[i,j,:] = griddata(points=(s,), 
                                      values=delta_x[i,j,:], 
                                      xi=(self.z), 
                                      method=method,
                                      fill_value=fill_value)
    return delta_s

set_fft_sample_spacing(self)

Calculate the sample spacing in Fourier space, given some symmetric 3D box in real space, with 1D grid point coordinates x.

Source code in fastbox/box.py
def set_fft_sample_spacing(self):
    """
    Calculate the sample spacing in Fourier space, given some symmetric 3D 
    box in real space, with 1D grid point coordinates `x`.
    """
    # These are related to comoving k by factor of 2 pi / L
    self.Kx = np.zeros((self.N,self.N,self.N))
    self.Ky = np.zeros((self.N,self.N,self.N))
    self.Kz = np.zeros((self.N,self.N,self.N))
    NN = ( self.N*fft.fftfreq(self.N, 1.) ).astype("i")
    for i in NN:
        self.Kx[i,:,:] = i
        self.Ky[:,i,:] = i
        self.Kz[:,:,i] = i

    self.k = 2.*np.pi * np.sqrt(  (self.Kx/self.Lx)**2. 
                                + (self.Ky/self.Ly)**2. 
                                + (self.Kz/self.Lz)**2.)

sigma8(self)

Get variance of matter perturbations on smoothing scale of 8 h^-1 Mpc.

Returns:

Type Description
sigma8 (array_like)

RMS of the smoothed field for R = 8 Mpc/h.

Source code in fastbox/box.py
def sigma8(self):
    """
    Get variance of matter perturbations on smoothing scale 
    of 8 h^-1 Mpc.

    Returns:
        sigma8 (array_like):
            RMS of the smoothed field for R = 8 Mpc/h.
    """
    return self.sigmaR(8.0)

sigmaR(self, R)

Get variance of matter perturbations, smoothed with a tophat filter of radius R h^-1 Mpc.

Parameters:

Name Type Description Default
R array_like

Top-hat radius, in Mpc/h (note the units!).

required

Returns:

Type Description
sigmaR (array_like)

RMS of the smoothed field.

Source code in fastbox/box.py
def sigmaR(self, R):
    """
    Get variance of matter perturbations, smoothed with a tophat filter 
    of radius R h^-1 Mpc.

    Parameters:
        R (array_like): 
            Top-hat radius, in Mpc/h (note the units!).

    Returns:
        sigmaR (array_like):
            RMS of the smoothed field.
    """
    # Need binned power spectrum, with k flat, monotonic for integration.
    k, pk, stddev = self.binned_power_spectrum()

    # Only use non-NaN values
    good_idxs = ~np.isnan(pk)
    pk = pk[good_idxs]
    k = k[good_idxs]

    # Discretely-sampled integrand, y
    y = k**2. * pk * self.window(k, R/self.cosmo['h'])
    I = scipy.integrate.simps(y, k)

    # Return sigma_R (note factor of 4pi / (2pi)^3 from integration)
    return np.sqrt( I / (2. * np.pi**2.) )

smooth_field(self, field_k, R)

Smooth a given (Fourier-space) field using a tophat filter with scale R h^-1 Mpc, and then return real-space copy of smoothed field.

Parameters:

Name Type Description Default
field_k array_like

Fourier-space field to be smoothed (complex-valued).

required
R array_like

Top-hat radius, in Mpc/h (note the units!).

required

Returns:

Type Description
field_x (array_like)

Smoothed real-space field.

Source code in fastbox/box.py
def smooth_field(self, field_k, R):
    """
    Smooth a given (Fourier-space) field using a tophat filter with scale 
    R h^-1 Mpc, and then return real-space copy of smoothed field.

    Parameters:
        field_k (array_like):
            Fourier-space field to be smoothed (complex-valued).

        R (array_like): 
            Top-hat radius, in Mpc/h (note the units!).

    Returns:
        field_x (array_like):
            Smoothed real-space field.
    """
    dk = field_k #np.reshape(field_k, np.shape(self.k))
    dk = dk * self.window1(self.k, R/self.cosmo['h'])
    dk = np.nan_to_num(dk)
    dx = fft.ifftn(dk)
    return dx

test_parseval(self)

Ensure that Parseval's theorem is satisfied for delta_x and delta_k, i.e. = Sum_k[P(k)]. Important consistency check for FT; should return unity if everything is OK.

Returns:

Type Description
s1, s2 (float)

s1 is the sum (~integral) over the delta_x field, sum(delta_x^2) * N^3, and s2 is the sum (~integral) over the delta_k field, Re[sum(delta_k delta_k^*)]. Should find thats1 == s2``.

Source code in fastbox/box.py
def test_parseval(self):
    """
    Ensure that Parseval's theorem is satisfied for delta_x and delta_k, 
    i.e. <delta_x^2> = Sum_k[P(k)]. Important consistency check for FT;
    should return unity if everything is OK.

    Returns:
        s1, s2 (float):
            `s1` is the sum (~integral) over the `delta_x` field, 
            ``sum(delta_x^2) * N^3``, and `s2` is the sum (~integral) over 
            the `delta_k` field, ``Re[sum(delta_k delta_k^*)]. 
            Should find that ``s1 == s2``.
    """
    s1 = np.sum(self.delta_x**2.) * self.N**3.
    # ^ Factor of N^3 missing due to averaging
    s2 = np.sum(self.delta_k*np.conj(self.delta_k)).real
    print("Parseval test:", s1/s2, "(should be 1.0)")
    return s1, s2

test_sampling_error(self)

P(k) is sampled within some finite window in the interval [kmin, kmax], where kmin=2pi/L and kmax=2pi*sqrt(3)*(N/2)*(1/L) (for 3D FT). The lack of sampling in some regions of k-space means that sigma8 can't be perfectly reconstructed (see U-L. Pen, arXiv:astro-ph/9709261 for a discussion).

This function calculates sigma8 from the realised box, and compares this with the theoretical calculation for sigma8 over a large k-window, and over a k-window of the same size as for the box.

Source code in fastbox/box.py
def test_sampling_error(self):
    """
    P(k) is sampled within some finite window in the interval 
    `[kmin, kmax]`, where `kmin=2pi/L` and `kmax=2pi*sqrt(3)*(N/2)*(1/L)` 
    (for 3D FT). The lack of sampling in some regions of k-space means that 
    sigma8 can't be perfectly reconstructed (see U-L. Pen, 
    arXiv:astro-ph/9709261 for a discussion).

    This function calculates sigma8 from the realised box, and compares 
    this with the theoretical calculation for sigma8 over a large 
    k-window, and over a k-window of the same size as for the box.
    """

    # Calc. sigma8 from the realisation
    s8_real = self.sigma8()

    # Calc. theoretical sigma8 in same k-window as realisation
    _k = np.linspace(self.kmin, self.kmax, int(5e3))
    _pk = ccl.nonlin_matter_power(self.cosmo, k=_k, a=self.scale_factor)
    _y = _k**2. * _pk * self.window(_k, 8.0/self.cosmo['h'])
    _y = np.nan_to_num(_y)
    s8_th_win = np.sqrt( scipy.integrate.simps(_y, _k) / (2. * np.pi**2.) )

    # Calc. full sigma8 (in window that is wide enough)
    _k2 = np.logspace(-5, 2, int(5e4))
    _pk2 = ccl.nonlin_matter_power(self.cosmo, k=_k2, a=self.scale_factor)
    _y2 = _k2**2. * _pk2 * self.window(_k2, 8.0/self.cosmo['h'])
    _y2 = np.nan_to_num(_y2)
    s8_th_full = np.sqrt( scipy.integrate.simps(_y2, _k2) / (2. * np.pi**2.) )

    # Calculate sigma8 in real space
    dk = np.reshape(self.delta_k, np.shape(self.k))
    dk = dk * self.window1(self.k, 8.0/self.cosmo['h'])
    dk = np.nan_to_num(dk)
    dx = fft.ifftn(dk)
    s8_realspace = np.std(dx)

    # sigma20
    dk = np.reshape(self.delta_k, np.shape(self.k))
    dk = dk * self.window1(self.k, 20.0/self.cosmo['h'])
    dk = np.nan_to_num(dk)
    dx = fft.ifftn(dk)
    s20_realspace = np.std(dx)

    s20_real = self.sigmaR(20.)

    # Print report
    print("")
    print("sigma8 (real.): \t", s8_real)
    print("sigma8 (th.win.):\t", s8_th_win)
    print("sigma8 (th.full):\t", s8_th_full)
    print("sigma8 (realsp.):\t", s8_realspace)
    print("ratio =", 1. / (s8_real / s8_realspace))
    print("")
    print("sigma20 (real.): \t", s20_real)
    print("sigma20 (realsp.):\t", s20_realspace)
    print("ratio =", 1. / (s20_real / s20_realspace))
    print("var(delta) =", np.std(self.delta_x))

theoretical_power_spectrum(self)

Calculate the theoretical nonlinear power spectrum for the given cosmological parameters, using CCL. Does not depend on the realisation.

Returns:

Type Description
k, pk (array_like)

Wavenumbers, from 10^-3.5 to 10^1, in Mpc^-1, and the theoretical nonlinear power spectrum, in (Mpc)^3.

Source code in fastbox/box.py
def theoretical_power_spectrum(self):
    """
    Calculate the theoretical nonlinear power spectrum for the given 
    cosmological parameters, using CCL. Does not depend on the realisation.

    Returns:
        k, pk (array_like):
            Wavenumbers, from 10^-3.5 to 10^1, in Mpc^-1, and the 
            theoretical nonlinear power spectrum, in (Mpc)^3.
    """
    k = np.logspace(-3.5, 1., int(1e3))
    pk = ccl.nonlin_matter_power(self.cosmo, k=k, a=self.scale_factor)
    return k, pk

window(self, k, R)

Fourier transform of tophat window function squared, used to calculate sigmaR etc. See "Cosmology", S. Weinberg, Eq.8.1.46.

Parameters:

Name Type Description Default
k array_like

Wavenumber, in Mpc^-1.

required
R array_like

Top-hat radius, in Mpc.

required

Returns:

Type Description
W (array_like)

Window function squared.

Source code in fastbox/box.py
def window(self, k, R):
    """
    Fourier transform of tophat window function *squared*, used to calculate 
    sigmaR etc. See "Cosmology", S. Weinberg, Eq.8.1.46.

    Parameters:
        k (array_like):
            Wavenumber, in Mpc^-1.

        R (array_like): 
            Top-hat radius, in Mpc.

    Returns:
        W (array_like):
            Window function squared.
    """
    x = k*R
    f = (3. / x**3.) * ( np.sin(x) - x*np.cos(x) )
    return f**2.

window1(self, k, R)

Fourier transform of tophat window function, used to calculate sigmaR etc. See "Cosmology", S. Weinberg, Eq.8.1.46.

Parameters:

Name Type Description Default
k array_like

Wavenumber, in Mpc^-1.

required
R array_like

Top-hat radius, in Mpc.

required

Returns:

Type Description
W (array_like)

Window function.

Source code in fastbox/box.py
def window1(self, k, R):
    """
    Fourier transform of tophat window function, used to calculate 
    sigmaR etc. See "Cosmology", S. Weinberg, Eq.8.1.46.

    Parameters:
        k (array_like):
            Wavenumber, in Mpc^-1.

        R (array_like): 
            Top-hat radius, in Mpc.

    Returns:
        W (array_like):
            Window function.
    """
    x = k*R
    f = (3. / x**3.) * ( np.sin(x) - x*np.cos(x) )
    return f