-
Notifications
You must be signed in to change notification settings - Fork 0
/
eor_h21cm.py
332 lines (280 loc) · 11.9 KB
/
eor_h21cm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
#!/usr/bin/env python3
"""
Pipeline for the creation of a synthetic dataset, and its corresponding
deconvolved image, containing the epoch of reionisation's hydrogen 21cm signal
"""
import os
import logging
import pathlib
from datetime import datetime
from typing import Optional, Union
import matplotlib.pylab as plt
import numpy as np
import numpy.typing as npt
import py21cmfast as p21c
import toml
import tools21cm as t2c
from astropy.cosmology import Planck18
from astropy.io import fits
LOG_FMT = "%(asctime)s:: %(levelname)s:: %(module)s.%(funcName)s:: %(message)s"
LOG_DATE_FMT = "%Y-%m-%d %H:%M:%S"
PAGE_WIDTH_INCHES = 6.97522
def plot_lightcone(lightcone: npt.NDArray, loc_axis: npt.NDArray, fov: float,
xlabel: str = 'z', ylabel: str = 'L (cMpc)',
fig: Optional[plt.Figure] = None,
ax: Optional[plt.Axes] = None,
title: Optional[str] = None,
savefig: Union[bool, pathlib.Path] = False):
"""
Plot the Epoch of Reionisation's Hydrogen 21cm lightcone
Parameters
----------
lightcone
Lightcone data
loc_axis
Line of sight axis (e.g. redshift) data
fov
Field of view [Mpc]
xlabel
Axis x-label
ylabel
Axis y-label
fig
matplotlib.pylab.Figure instance to plot to. If None, will create new
figure instance
ax
matplotlib.pylab.Axes instance to plot to. If None, will create new
axes instance
title
Plot title
savefig
Whether to save the plot. If False (default), will not save. Otherwise
must be the full path to the save file
Returns
-------
2-Tuple of (matplotlib.pylab.Figure instance,
matplotlib.pylab.Axes instance) plotted to
"""
data = {'lc': lightcone, 'z': loc_axis}
xi = np.array([data['z'] for _ in range(data['lc'].shape[1])])
yi = np.array(
[np.linspace(0, fov, data['lc'].shape[1]) for _ in range(xi.shape[1])]
).T
zj = (data['lc'][100, 1:, 1:] +
data['lc'][100, 1:, :-1] +
data['lc'][100, :-1, 1:] +
data['lc'][100, :-1, :-1]) / 4
if fig is None or ax is None:
fig, ax = plt.subplots(1, 1, figsize=(PAGE_WIDTH_INCHES,
PAGE_WIDTH_INCHES / 3.))
if title is not None:
ax.set_title(title)
im = ax.pcolor(xi, yi, zj, cmap='jet')
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
if loc_axis[0] > loc_axis[-1]:
ax.invert_xaxis()
ax.tick_params(axis='both', which='major')
fig.subplots_adjust(bottom=0.11, right=0.91, top=0.95, left=0.06)
cax = plt.axes([0.92, 0.15, 0.02, 0.75])
fig.colorbar(im, cax=cax)
if savefig:
fig.savefig(savefig, dpi=300, bbox_inches='tight')
return fig, ax
def z_from_freq(nu):
"""Observed frequency [Hz] to redshift for Hydrogen 21cm line"""
nu0 = 1420.40575e6
return (nu0 / nu) - 1.
def create_eor_h21cm_fits(params_file: Union[str, pathlib.Path]):
"""
Create the .fits cube of the Epoch of Reionisation's hydrogen-21cm signal
Parameters
----------
params_file
Full path to .toml configuration file for producing the .fits cube
"""
with open(params_file, 'rt') as f:
params = toml.load(f)
output_dir = params['output']['output_dcy'].rstrip('/')
pipeline_start = datetime.now()
# ######################################################################## #
# ######################## Set up the logger ############################# #
# ######################################################################## #
date_str = pipeline_start.strftime("%Y%b%d_%H%M%S").upper()
logfile_name = f'EoR_H21cm_{date_str}.log'
logfile = f"{output_dir}{os.sep}{logfile_name}"
logger = logging.getLogger('eor_h21cm')
logger.setLevel(LOG_LEVEL)
# Set up handler for writing log messages to log file
file_handler = logging.FileHandler(
str(logfile), mode="w", encoding=sys.stdout.encoding
)
log_formatter = logging.Formatter(LOG_FMT, datefmt=LOG_DATE_FMT)
file_handler.setFormatter(log_formatter)
file_handler.setLevel(LOG_LEVEL)
logger.addHandler(file_handler)
# Set up handler to print to console
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(log_formatter)
console_handler.setLevel(LOG_LEVEL)
logger.addHandler(console_handler)
# ######################################################################## #
# ###################### Parse configuration from file ################### #
# ######################################################################## #
output_fits = f"{output_dir}{os.sep}{params['output']['root_name']}.fits"
random_seed = params['user_params']['seed']
ra_deg = params['field']['ra0'] # [deg]
dec_deg = params['field']['dec0'] # [deg]
fov_deg = params['field']['fov'] # [deg]
freqs = np.linspace(params['correlator']['freq_min'],
params['correlator']['freq_max'],
params['correlator']['nchan'])
n_output_cell = params['field']['n_cell']
plot_light_cone = params['user_params']['plot_lc']
# Number of cells per side for the low res box (output cube)
HII_DIM = n_output_cell
cdelt = fov_deg / HII_DIM
# Number of cells for the high res box (sampling initial conditions).
# Should be at least 3 times HII_DIM.
DIM = 3 * HII_DIM
astro_params = params['astro_params']
dfreq = np.ptp(freqs[:2])
redshift = z_from_freq(freqs).tolist()
zmin = np.amin(redshift)
zmax = np.amax(redshift)
# Desired field of view at highest redshift
fov_mpc = (Planck18.comoving_transverse_distance(zmax).value *
np.deg2rad(fov_deg))
# Length of the box in Mpc (simulation size) i.e. the comoving size
BOX_LEN = fov_mpc
flag_options = params['flags']
user_params: dict = params["user_params"]
user_params['N_THREADS'] = params['user_params']['n_cpu']
user_params["HII_DIM"] = HII_DIM
user_params["BOX_LEN"] = BOX_LEN
user_params["DIM"] = DIM
user_params.pop('n_cpu')
user_params.pop('seed')
user_params.pop('plot_lc')
if flag_options['USE_MINI_HALOS']:
user_params['USE_RELATIVE_VELOCITIES'] = True
# ######################################################################## #
# ###################### Create relevant directories ##################### #
# ######################################################################## #
if not os.path.exists(output_dir):
os.mkdir(output_dir)
cache_dir = f"{output_dir}{os.sep}_cache"
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
p21c.config['direc'] = cache_dir
# clear the cache so that we get the same result each time
logger.debug("Clearing the cache")
p21c.cache_tools.clear_cache(direc=cache_dir)
# ######################################################################## #
# ###################### Parse configuration from file ################### #
# ######################################################################## #
logger.info("Computing initial conditions")
initial_conditions = p21c.initial_conditions(
user_params=user_params, random_seed=random_seed, direc=output_dir,
)
lightcone_quantities = ('brightness_temp', )
logger.info("Generating lightcone")
lightcone = p21c.run_lightcone(
redshift=zmin,
# max_redshift = 10.0,
init_box=initial_conditions,
flag_options=flag_options,
astro_params=astro_params,
lightcone_quantities=lightcone_quantities,
global_quantities=lightcone_quantities,
random_seed=random_seed,
direc=output_dir,
)
lc0 = getattr(lightcone, 'brightness_temp')
zs0 = lightcone.lightcone_redshifts
# so cut a lightcone in a redshift (=freq) range I want
z_start_index = min(range(len(zs0)), key=lambda i: abs(zs0[i] - zmin))
z_end_index = min(range(len(zs0)), key=lambda i: abs(zs0[i] - zmax))
zs = zs0[z_start_index:z_end_index]
lc = lc0[:, :, z_start_index:z_end_index]
# plotting physical lightcone
if plot_light_cone:
output_png = output_fits.replace('.fits', '.png')
logger.info(f"Plotting lightcone and saving to {output_png}")
try:
plot_lightcone(lc, zs, fov=BOX_LEN, title='Physical lightcone',
xlabel='z', ylabel='L (cMpc)', savefig=output_png)
except Exception as e:
logger.error(f"{e.__class__.__name__}: {e}")
logger.error("Could not plot lightcone")
# converting physical to observational coordinates - given cosmology is
# different here
angular_size_deg = t2c.angular_size_comoving(BOX_LEN, zs)
logger.info(f'Minimum angular size: {angular_size_deg.min():.2f} degrees')
logger.info(f'Maximum angular size: {angular_size_deg.max():.2f} degrees')
physical_freq = t2c.z_to_nu(zs) # redshift to frequencies in MHz
logger.info(
'Minimum frequency gap in the physical light-cone data: '
'{:.2f} MHz'.format(np.abs(np.gradient(physical_freq)).min())
)
logger.info(
'Maximum frequency gap in the physical light-cone data: '
'{:.2f} MHz'.format(np.abs(np.gradient(physical_freq)).max())
)
zmin = zs.min()
zmax = zs.max()
output_dtheta = (fov_deg / (n_output_cell + 1)) * 60 # [arcmin]
# Original physical_lightcone_to_observational padded highest redshifts by
# repeating flux distribution. AB + EL didn't like that, and changed it to
# trim whilst starting with a bigger cube.
logger.info("Converting physical lightcone to observational")
obs_lc, obs_freq = t2c.physical_lightcone_to_observational(
lc, zmin, zmax, dfreq / 1e6, output_dtheta, input_box_size_mpc=BOX_LEN
) # Mpc * Mpc * Mpc to deg * deg * Hz
# save to a fits file
lc_out = np.float32(obs_lc.transpose()[::-1])
lc_out /= 1000. # mK to K
hdu = fits.PrimaryHDU(lc_out)
hdul = fits.HDUList([hdu])
hdul[0].header.set('CTYPE1', 'RA---SIN')
hdul[0].header.set('CTYPE2', 'DEC--SIN')
hdul[0].header.set('CTYPE3', 'FREQ ')
hdul[0].header.set('CRVAL1', ra_deg)
hdul[0].header.set('CRVAL2', dec_deg)
hdul[0].header.set('CRVAL3', np.min(freqs))
hdul[0].header.set('CRPIX1', HII_DIM / 2.)
hdul[0].header.set('CRPIX2', HII_DIM / 2.)
hdul[0].header.set('CRPIX3', 1)
hdul[0].header.set('CDELT1', -cdelt)
hdul[0].header.set('CDELT2', cdelt)
hdul[0].header.set('CDELT3', dfreq)
hdul[0].header.set('CUNIT1', 'deg ')
hdul[0].header.set('CUNIT2', 'deg ')
hdul[0].header.set('CUNIT3', 'Hz ')
hdul[0].header.set('BUNIT', 'K ')
logger.info(f"Writing output lightcone to {output_fits}")
hdul.writeto(output_fits, overwrite=True)
if __name__ == '__main__':
import sys
import argparse
import pathlib
# ######################################################################## #
# ########## Parse configuration from file or from command-line ########## #
# ######################################################################## #
if len(sys.argv) != 1:
parser = argparse.ArgumentParser()
parser.add_argument("param_file",
help="Full path to EoR H21cm configuration .toml "
"file",
type=str)
parser.add_argument("-d", "--debug",
help="Set terminal log output to verbose levels",
action="store_true")
args = parser.parse_args()
param_file = pathlib.Path(args.param_file)
if not param_file.exists():
raise FileNotFoundError(f"{param_file} does not exist")
LOG_LEVEL = logging.DEBUG if args.debug else logging.INFO
else:
raise RuntimeError("No parameter file specified")
create_eor_h21cm_fits(param_file)