aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAaron LI <aly@aaronly.me>2017-06-13 08:47:08 +0800
committerAaron LI <aly@aaronly.me>2017-06-13 08:47:08 +0800
commit3148e665ea336c0b4b97b3ebe52546f552720073 (patch)
treedbf01cf221d6cede1c2f40597341f454ac74b6b6
parent14c32fbbe8276153395beeac67776254e145357c (diff)
downloadatoolbox-3148e665ea336c0b4b97b3ebe52546f552720073.tar.bz2
Add astro/oskar/fits2skymodel.py
-rwxr-xr-xastro/oskar/fits2skymodel.py248
1 files changed, 248 insertions, 0 deletions
diff --git a/astro/oskar/fits2skymodel.py b/astro/oskar/fits2skymodel.py
new file mode 100755
index 0000000..781b907
--- /dev/null
+++ b/astro/oskar/fits2skymodel.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2017 Weitian LI <weitian@aaronly.me>
+# MIT License
+#
+
+"""
+Convert a FITS image to OSKAR sky model for simulation usage.
+
+NOTE
+----
+The OSKAR sky model consists of all the valid (>threshold) pixels
+from the given image (slice), and fluxes are given in unit [Jy],
+therefore, the input image should be converted from brightness
+temperature [K] to unit [Jy/pixel].
+
+References
+----------
+[1] GitHub: OxfordSKA/OSKAR
+ https://github.com/OxfordSKA/OSKAR
+[2] OSKAR - Sky Model
+ http://www.oerc.ox.ac.uk/~ska/oskar2/OSKAR-Sky-Model.pdf
+[3] OSKAR - Settings
+ http://www.oerc.ox.ac.uk/~ska/oskar2/OSKAR-Settings.pdf
+"""
+
+import os
+import sys
+import argparse
+import logging
+from datetime import datetime
+
+import numpy as np
+import astropy.io.fits as fits
+import astropy.constants as ac
+from astropy.wcs import WCS
+
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(os.path.basename(sys.argv[0]))
+
+
+class SkyModel:
+ """
+ OSKAR sky model.
+
+ Parameters
+ ----------
+ image : 2D float `~numpy.ndarray`
+ Input image array; unit [K] (brightness temperature)
+ freq : float
+ Frequency of the input image slice; unit [MHz]
+ pixsize : float
+ Pixel size of the input image; unit [arcmin]
+ ra0, dec0 : float
+ The coordinate of the image center; unit [deg]
+ minvalue : float, optional
+ The minimum threshold for the image values
+ projection : str, optional
+ The WCS projection for the image; default "TAN"
+ TODO: support "SIN" etc.
+ """
+ def __init__(self, image, freq, pixsize, ra0, dec0,
+ minvalue=1e-4, projection="TAN"):
+ self.image = image # K (brightness temperature)
+ self.freq = freq # MHz
+ self.pixsize = pixsize # arcmin
+ self.ra0 = ra0 # deg
+ self.dec0 = dec0 # deg
+ self.minvalue = minvalue
+ self.projection = projection
+ logger.info("SkyModel: Loaded image @ %.2f [MHz]" % freq)
+
+ @property
+ def wcs(self):
+ """
+ WCS for the given image slice.
+ """
+ shape = self.image.shape
+ delta = self.pixsize / 60.0 # deg
+ wcs_ = WCS(naxis=2)
+ wcs_.wcs.ctype = ["RA---"+self.projection, "DEC--"+self.projection]
+ wcs_.wcs.crval = np.array([self.ra0, self.dec0])
+ wcs_.wcs.crpix = np.array([shape[1], shape[0]]) / 2.0 + 1
+ wcs_.wcs.cdelt = np.array([delta, delta])
+ return wcs_
+
+ @property
+ def fits_header(self):
+ header = self.wcs.to_header()
+ header["BUNIT"] = ("Jy/pixel", "Brightness unit")
+ header["FREQ"] = (self.freq, "Frequency [MHz]")
+ header["RA0"] = (self.ra0, "Center R.A. [deg]")
+ header["DEC0"] = (self.dec0, "Center Dec. [deg]")
+ header["PIXSIZE"] = (self.pixsize, "Pixel size [arcmin]")
+ return header
+
+ @property
+ def factor_K2JyPixel(self):
+ """
+ Conversion factor to convert brightness unit from 'K' to 'Jy/pixel'
+
+ http://www.iram.fr/IRAMFR/IS/IS2002/html_1/node187.html
+ """
+ pixarea = np.deg2rad(self.pixsize/60.0) ** 2 # [sr]
+ kB = ac.k_B.si.value # Boltzmann constant [J/K]
+ c0 = ac.c.si.value # speed of light in vacuum [m/s]
+ freqHz = self.freq * 1e6 # [Hz]
+ factor = 2*kB * 1.0e26 * pixarea * (freqHz/c0)**2
+ return factor
+
+ @property
+ def ra_dec(self):
+ """
+ Calculate the (ra, dec) of each image pixel using the above WCS.
+
+ NOTE: axis ordering difference between numpy array and FITS
+ """
+ shape = self.image.shape
+ wcs = self.wcs
+ x, y = np.meshgrid(np.arange(shape[1]), np.arange(shape[0]))
+ pix = np.column_stack([x.flatten(), y.flatten()])
+ world = wcs.wcs_pix2world(pix, 0)
+ ra = world[:, 0].reshape(shape)
+ dec = world[:, 1].reshape(shape)
+ return (ra, dec)
+
+ @property
+ def sky(self):
+ """
+ OSKAR sky model array converted from the input image.
+
+ Columns
+ -------
+ ra : (J2000) right ascension (deg)
+ dec : (J2000) declination (deg)
+ flux : source (Stokes I) flux density (Jy)
+ """
+ idx = self.image.flatten() >= self.minvalue
+ ra, dec = self.ra_dec
+ ra = ra.flatten()[idx]
+ dec = dec.flatten()[idx]
+ flux = self.image.flatten()[idx] * self.factor_K2JyPixel
+ sky_ = np.column_stack([ra, dec, flux])
+ return sky_
+
+ def write_sky_model(self, outfile, clobber=False):
+ """
+ Write the converted sky model for simulation.
+ """
+ if os.path.exists(outfile) and (not clobber):
+ raise OSError("OSKAR sky model file already exists: " % outfile)
+ sky = self.sky
+ nsources = sky.shape[0]
+ logger.info("Number of sources: %d" % nsources)
+ header = ("Frequency = %.3f [MHz]\n" % self.freq +
+ "Pixel size = %.2f [arcmin]\n" % self.pixsize +
+ "RA0 = %.4f [deg]\n" % self.ra0 +
+ "Dec0 = %.4f [deg]\n" % self.dec0 +
+ "Number of sources = %d\n\n" % len(sky) +
+ "R.A.[deg] Dec.[deg] flux[Jy]")
+ np.savetxt(outfile, sky, fmt='%.10e, %.10e, %.10e', header=header)
+ logger.info("Wrote OSKAR sky model to file: %s" % outfile)
+
+ def write_fits(self, outfile, oldheader=None, clobber=False):
+ if os.path.exists(outfile) and (not clobber):
+ raise OSError("Sky FITS already exists: " % outfile)
+ if oldheader is not None:
+ header = oldheader
+ header.extend(self.fits_header, update=True)
+ else:
+ header = self.fits_header
+ header.add_history(datetime.now().isoformat())
+ header.add_history(" ".join(sys.argv))
+ image = self.image
+ image[image < self.minvalue] = np.nan
+ image *= self.factor_K2JyPixel
+ hdu = fits.PrimaryHDU(data=image, header=header)
+ try:
+ hdu.writeto(outfile, overwrite=True)
+ except TypeError:
+ hdu.writeto(outfile, clobber=True) # old astropy versions
+ logger.info("Wrote FITS image of sky model to file: %s" % outfile)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Convert FITS image to OSKAR sky model")
+ parser.add_argument("-C", "--clobber", dest="clobber",
+ action="store_true",
+ help="overwrite existing file")
+ parser.add_argument("-r", "--ra0", dest="ra0", type=float, required=True,
+ help="R.A. of the image center")
+ parser.add_argument("-d", "--dec0", dest="dec0", type=float, required=True,
+ help="Dec. of the image center")
+ parser.add_argument("-p", "--pix-size", dest="pixsize", type=float,
+ help="image pixel size [arcmin]; " +
+ "(default: obtain from the FITS header 'PIXSIZE')")
+ parser.add_argument("-f", "--freq", dest="freq", type=float,
+ help="frequency [MHz] the image measured; " +
+ "(default: obtain from the FITS header 'FREQ')")
+ exgrp = parser.add_mutually_exclusive_group()
+ exgrp.add_argument("-m", "--min-value", dest="minvalue", type=float,
+ help="minimum threshold to the output sky model " +
+ "(default: 1e-4, i.e., 0.1 mK)")
+ exgrp.add_argument("-M", "--min-peak-fraction", dest="minpfrac",
+ type=float,
+ help="minimum threshold determined as the fraction " +
+ "the peak value to the output sky model")
+ #
+ parser.add_argument("-F", "--osm-fits", dest="osmfits",
+ action="store_true",
+ help="save a FITS version of the converted sky model")
+ parser.add_argument("infile", help="input FITS image")
+ parser.add_argument("outfile", nargs="?",
+ help="output OSKAR sky model (default: " +
+ "save basename as the input FITS image)")
+ args = parser.parse_args()
+
+ if args.outfile:
+ outfile = args.outfile
+ else:
+ outfile = os.path.splitext(args.infile)[0] + ".osm"
+
+ with fits.open(args.infile) as f:
+ image = f[0].data
+ header = f[0].header
+ logger.info("Read image slice: %s" % args.infile)
+ freq = args.freq if args.freq else header["FREQ"] # [MHz]
+ pixsize = args.pixsize if args.pixsize else header["PIXSIZE"] # [arcmin]
+ logger.info("Frequency: %.2f [MHz]" % freq)
+ logger.info("Pixel size: %.2f [arcmin]" % pixsize)
+ minvalue = 1e-4 # i.e., 0.1 [mK]
+ if args.minvalue:
+ minvalue = args.minvalue
+ if args.minpfrac:
+ minvalue = args.minpfrac * image.max()
+ logger.info("Minimum threshold: %g [K]" % minvalue)
+ skymodel = SkyModel(image=image, freq=freq, ra0=args.ra0, dec0=args.dec0,
+ pixsize=pixsize, minvalue=minvalue)
+ skymodel.write_sky_model(outfile, clobber=args.clobber)
+ if args.osmfits:
+ outfits = outfile + ".fits"
+ skymodel.write_fits(outfits, oldheader=header, clobber=args.clobber)
+
+
+if __name__ == "__main__":
+ main()