diff options
Diffstat (limited to 'astro')
-rwxr-xr-x | astro/21cm/make_lightcone.py | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/astro/21cm/make_lightcone.py b/astro/21cm/make_lightcone.py new file mode 100755 index 0000000..93b29df --- /dev/null +++ b/astro/21cm/make_lightcone.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2017 Weitian LI <weitian@aaronly.me> +# MIT License +# + +""" +Make the light-cone cube of HI 21cm signals along the line-of-sight (LoS) +direction from the simulated deltaTb coeval cubes (brightness temperature, +in units of [K]), e.g., by SimFast21. + +The HI reionization simulation gives so-called *coeval cubes*, 3D volumes +of density and HI fraction at the same cosmological redshift. However, an +observer cannot observe these coeval cubes, but they can be used to create +the observable *light-cone* cubes, utilizing the method outlined in +[datta2012],Sec.(2.3). + +NOTE +---- +The created light-cone cubes constructed in this way *differ* from the +observational ones in that the field of view (FoV) has a constant comoving +size and not a constant angular size. + +References +---------- +.. [datta2012] + Datta et al. 2012, MNRAS, 424, 1877 + "Light-cone effect on the reionization 21-cm power spectrum"; + http://adsabs.harvard.edu/abs/2012MNRAS.424.1877D + +.. SimFast21: https://github.com/mariogrs/Simfast21 + + +Sample Configuration (YAML format) +--------------------------------------------------------------------------- +# Parameters for the flat ΛCDM cosmology +H0: 71.0 +Om0: 0.27 + +# Whether overwrite existing output file? (default: False) +clobber: False +# Data type of the input deltaTb cubes. (default: float32) +dtype: float32 +# Unit of the deltaTb cubes. (default: K) +unit: K + +# Redshifts of the input cubes (required) +# (zmin, zmax, dz from SimFast21 configuration file) +zmin: +zmax: +dz: +# Simulation cube side length [Mpc] (required) +# (sim_length from SimFast21 configuration file) +Lside: +# Number of cells of the input cubes (required) +# (N_smoothed from SimFast21 configuration file) +Nside: +# Filename pattern of the input coeval cubes +infiles_pattern: "deltaTb_z{z:05.3f}_N{Nside:d}_L{Lside:.1f}.dat" +# Filename of output light-cone cube (required) +outfile: deltaTb_lightcone.fits +--------------------------------------------------------------------------- +""" + +import os +import sys +import argparse +import logging +from datetime import datetime, timezone + +import yaml +import numpy as np +from scipy import interpolate +import astropy.io.fits as fits +from astropy.cosmology import FlatLambdaCDM + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger() + + +class Configs: + def __init__(self, configfile): + self.filepath = os.path.abspath(configfile) + self.configs = yaml.load(open(configfile)) + logger.info("Loaded configurations from file: %s" % configfile) + + for item in [("H0", 71.0), + ("Om0", 0.27), + ("clobber", False), + ("dtype", "float32"), + ("unit", "K"), + "zmin", + "zmax", + "dz", + "Lside", + "Nside", + "infiles_pattern", + "outfile"]: + if isinstance(item, tuple): + option, default = item + setattr(self, option, self.configs.get(option, default)) + else: + setattr(self, item, self.configs[item]) + logger.info("Set configurations") + + self.cosmo = FlatLambdaCDM(H0=self.H0, Om0=self.Om0) + + @property + def redshifts(self): + return np.arange(self.zmin, self.zmax+self.dz/2, step=self.dz) + + def get_infile(self, z): + data = {"z": z, "Nside": self.Nside, "Lside": self.Lside} + filename = self.infiles_pattern.format(**data) + if os.path.exists(filename): + return filename + else: + raise OSError("requested file does not exists: %s" % filename) + + def get_cubepair(self, z): + """ + Get the consecutive two cubes enclosing the given redshift. + """ + redshifts = self.redshifts + i2 = np.sum(redshifts <= z) + i1 = i2 - 1 + z1 = redshifts[i1] + cube1 = self.get_infile(z1) + try: + z2 = redshifts[i2] + cube2 = self.get_infile(z2) + except IndexError: + z2 = None + cube2 = None + return [(z1, cube1), (z2, cube2)] + + @property + def Dc_limit(self): + Dc_min, Dc_max = self.cosmo.comoving_distance( + [self.zmin, self.zmax]).value # [Mpc] + return (Dc_min, Dc_max) + + def Dc_to_redshift(self, Dc): + """ + Calculate the redshift corresponding to the given comoving distance + (along LoS) by using interpolation. + """ + if not hasattr(self, "_Dc_interp"): + Dc_min, Dc_max = self.Dc_limit + dDc = self.Lside / self.Nside + N = int((Dc_max - Dc_min) / dDc) + z_ = np.linspace(self.zmin, self.zmax, num=N) + Dc_ = self.cosmo.comoving_distance(z_).value # [Mpc] + self._Dc_interp = interpolate.interp1d(Dc_, z_, kind="linear") + + return self._Dc_interp(Dc) + + +class CubePair: + """ + A pair of (redshift) consecutive coeval cubes. + """ + z1 = None + z2 = None + infile1 = None + infile2 = None + cube1 = None + cube2 = None + + def __init__(self, Nside, dtype="float32"): + self.Nside = Nside + self.dtype = np.dtype(dtype) + + def set(self, cubepair): + (z1, infile1), (z2, infile2) = cubepair + if self.infile1 != infile1: + self.infile1 = infile1 + self.z1 = z1 + self.cube1 = self.load(infile1) + logger.info("Loaded cube1 [z=%.3f] from: %s" % (z1, infile1)) + if self.infile2 != infile2: + self.infile2 = infile2 + self.z2 = z2 + self.cube2 = self.load(infile2) + logger.info("Loaded cube2 [z=%.3f] from: %s" % (z2, infile2)) + + def load(self, infile): + cube = np.fromfile(open(infile, "rb"), dtype=self.dtype) + cube = cube.reshape((self.Nside, self.Nside, self.Nside)) + return cube + + def get_slice(self, idx, z): + q = idx % self.Nside + if abs(z-self.z1) < 1e-5: + # The last slice, and z2/infile2/cube2 is None + return self.cube1[:, :, q] + else: + if self.z2 is None: + raise RuntimeError("z2/infile2/cube2 missing!") + # linear interpolation + s1 = self.cube1[:, :, q] + s2 = self.cube2[:, :, q] + slope = (s2 - s1) / (self.z2 - self.z1) + return s1 + slope * (z - self.z1) + + +class LightCone: + """ + Light-cone cube mimic the observation of HI signal. + """ + def __init__(self, configs): + self.configs = configs + + Nside = self.configs.Nside + Nslice = self.Nslice + self.cube = np.zeros(shape=(Nslice, Nside, Nside), + dtype=np.dtype(self.configs.dtype)) + logger.info("Light-cone cube shape: %dx%d (cells) * %d (slices)" % + (Nside, Nside, Nslice)) + + def set_slice(self, idx, data): + self.cube[idx, :, :] = data + + @property + def slices_Dc(self): + """ + The slices evenly distributed along the LoS representing with + comoving distances. [Mpc] + """ + dDc = self.configs.Lside / self.configs.Nside + Dc_min, Dc_max = self.configs.Dc_limit + Dc = np.arange(Dc_min, Dc_max, step=dDc) + return Dc + + @property + def Nslice(self): + return len(self.slices_Dc) + + @property + def header(self): + dDc = self.configs.Lside / self.configs.Nside + Dc_min, Dc_max = self.configs.Dc_limit + header = fits.Header() + header["BUNIT"] = (self.configs.unit, "Data unit") + header["zmin"] = (self.configs.zmin, "HI simulation minimum redshift") + header["zmax"] = (self.configs.zmax, "HI simulation maximum redshift") + header["dz"] = (self.configs.dz, "HI simulation redshift step size") + header["Dc_min"] = (Dc_min, "[cMpc] comoving distance at zmin") + header["Dc_max"] = (Dc_max, "[cMpc] comoving distance at zmax") + header["Dc_step"] = (dDc, "[cMpc] comoving distance between slices") + header["Lside"] = (self.configs.Lside, "[cMpc] Simulation side length") + header["Nside"] = (self.configs.Nside, "Side number of cells") + header["DATE"] = (datetime.now(timezone.utc).astimezone().isoformat(), + "File creation date") + header.add_history(" ".join(sys.argv)) + return header + + def write(self, outfile=None, clobber=None): + if outfile is None: + outfile = self.configs.outfile + if clobber is None: + clobber = self.configs.clobber + + hdu = fits.PrimaryHDU(data=self.cube, header=self.header) + try: + hdu.writeto(outfile, overwrite=clobber) + except TypeError: + hdu.writeto(outfile, clobber=clobber) + logger.info("Wrote light-cone cube to: %s" % outfile) + + +def main(): + parser = argparse.ArgumentParser( + description="Create light-cone cube from coeval cubes") + parser.add_argument("config", help="configuration file") + args = parser.parse_args() + configs = Configs(args.config) + cubepair = CubePair(Nside=configs.Nside, dtype=configs.dtype) + lightcone = LightCone(configs) + for idx, Dc in enumerate(lightcone.slices_Dc): + z = configs.Dc_to_redshift(Dc) + logger.info("Slice #%d @ z=%.3f / Dc=%.1f[cMpc] ..." % (idx+1, z, Dc)) + pair = configs.get_cubepair(z) + cubepair.set(pair) + data = cubepair.get_slice(idx, z) + lightcone.set_slice(idx, data) + lightcone.write() + + +if __name__ == "__main__": + main() |