def main(): """File modification I/O""" parser = argparse.ArgumentParser(description=("Given an image" " in tangent-projected sky coordinates and" " an ASC-REGION-FITS list of point source exclusions in" " celestial RA/dec coordinates," " fill in the image gaps by drawing counts from a" " Poisson distribution with mean count rate derived from" " an annulus surrounding each point source")) parser.add_argument('image', metavar='FITS', help=("FITS image in tangent-projected sky coords")) parser.add_argument('exposure', metavar='FITS', help=("FITS exposure image in tangent-projected sky coords")) parser.add_argument('--annulus-width', metavar='pixels', type=float, help=("Width (pixels) of sampling annulus around each" " point source.")) parser.add_argument('--mask', metavar='FITS', help=("ASC-REGION-FITS sources in un-projected RA/DEC," " radii in arcmin.")) parser.add_argument('--debug', action='store_true', help=("Create debugging output w/ sources and annuli" " highlighted by artificially high/low values")) parser.add_argument('--clobber', action='store_true', help=("Clobber any existing output file")) parser.add_argument('--out', metavar='output.fits', help=("Output filename, image w/ filled holes")) args = parser.parse_args() F_INPUT = args.image F_EXPOSURE = args.exposure F_MASK = args.mask F_OUTPUT = args.out ANNULUS_WIDTH = args.annulus_width OPT_DEBUG = args.debug OPT_CLOBBER = args.clobber R_EPSILON = 0.5 # Radius error (pixels) for each point source unit_tests() assert F_INPUT != F_OUTPUT fits_mask = fits.open(F_MASK) mask = fits_mask[1] assert mask.header['HDUCLASS'] == 'ASC' assert mask.header['HDUCLAS1'] == 'REGION' assert mask.header['HDUCLAS2'] == 'STANDARD' assert mask.header['MTYPE1'] == 'pos' assert mask.header['MFORM1'] == 'RA,DEC' assert all(map(lambda x: x == '!CIRCLE', mask.data['SHAPE'])) fits_image = fits.open(F_INPUT) phdu = fits_image[0] assert phdu.is_image assert phdu.header['CTYPE1'] == 'RA---TAN' assert phdu.header['CUNIT1'] == 'deg' assert phdu.header['CTYPE2'] == 'DEC--TAN' assert phdu.header['CUNIT2'] == 'deg' assert phdu.header['CDELT1'] == -1 * phdu.header['CDELT2'] assert phdu.header['CDELT2'] > 0 # XMM ESAS image convention: X scales inversely w/ RA, Y increases w/ dec. # X,Y pixel sizes are equal in deg.; the projection transformation accounts # for RA "compression" at high declination dat = phdu.data x0 = phdu.header['CRPIX1'] y0 = phdu.header['CRPIX2'] ra0 = phdu.header['CRVAL1'] dec0 = phdu.header['CRVAL2'] scale = phdu.header['CDELT2'] # pixel size == deg./pixel fits_exposure = fits.open(F_EXPOSURE) exposure = fits_exposure[0] for key in ['CTYPE1', 'CUNIT1', 'CTYPE2', 'CUNIT2', 'CDELT1', 'CDELT2']: assert exposure.header[key] == phdu.header[key] dat_exp = exposure.data # Convert point sources to current image projection x_srcs, y_srcs = radec2tanproj(mask.data['RA'][:,0], mask.data['DEC'][:,0], ra0, dec0, x0, y0, 1/scale) r_srcs = mask.data['R'][:,0] / (abs(scale) * 60) # scale*60 = arcmin/px n_overlaps = any_sources_overlap(x_srcs, y_srcs, r_srcs) if n_overlaps: print "\nWARNING: {} pairwise overlap(s), source filling may be inconsistent!\n".format(n_overlaps) dat_filled = np.copy(dat) max_abs_dat = np.max(np.abs(dat)) # Get all pixels in annulus of radii (r_pt + 0.5, r_pt + 0.5 + ANN_WIDTH) # centered on each point source. for x_pt, y_pt, r_pt in zip(x_srcs, y_srcs, r_srcs): ann_r1 = r_pt + R_EPSILON # Excise extra edge pixels ann_r2 = r_pt + R_EPSILON + ANNULUS_WIDTH # Inspect all pixels in a box around source + annulus # Bounds for x, y search: either annulus edge or image boundary search_x = (max(1, np.floor(x_pt - ann_r2)), min(dat.shape[0], np.ceil(x_pt + ann_r2))) search_y = (max(1, np.floor(y_pt - ann_r2)), min(dat.shape[1], np.ceil(y_pt + ann_r2))) search_x = map(int, search_x) search_y = map(int, search_y) ann_count_rate = 0 ann_px = 0 mean_exp = 0 for x in range(search_x[0], search_x[1] + 1): for y in range(search_y[0], search_y[1] + 1): # Order of conditionals matters for short-circuiting: # distance check is O(1) # vs. overlapping source check is O(n_sources) d = distance((x, y), (x_pt, y_pt)) if d > ann_r1 and d < ann_r2 and (not point_overlaps_sources(x, y, x_srcs, y_srcs, r_srcs + R_EPSILON)): # Prevent division by zero. Counts should be zero in these pixels anyways. if dat_exp[y-1, x-1] <= 0: continue ann_count_rate += (dat[y-1, x-1] / dat_exp[y-1, x-1]) # shift 1- to 0-based indices ann_px += 1 mean_exp += dat_exp[y-1, x-1] mean_exp = mean_exp / ann_px # Average exposure for a given pixel print "Source at ({:g},{:g}), r = {:g}. Search X in {}, Y in {}.".format( x_pt, y_pt, r_pt, search_x, search_y) print " Annulus count rate: {:g} cts/s".format(ann_count_rate) print " usable pixels: {:g} px".format(ann_px) print " count flux: {:g} cts/px/s".format(ann_count_rate/ann_px) print " Total cts mean exp: {:g}".format(ann_count_rate * mean_exp) if ann_px < 100: print "\n ***WARNING: <100 pixels sampled; adjust --annulus-width***\n" if ann_count_rate * mean_exp < 1: print "\n ***WARNING: <1 count in annulus for mean exposure***\n" flux = ann_count_rate/ann_px # counts / sec / pixel if flux < 0: print "\n ***WARNING: negative count rate, forcing to zero!***\n" flux = 0 # Insert new values into image for x in range(search_x[0], search_x[1] + 1): for y in range(search_y[0], search_y[1] + 1): d = distance((x, y), (x_pt, y_pt)) if OPT_DEBUG: if d <= ann_r1: dat_filled[y-1, x-1] = 10 * max_abs_dat elif d > ann_r1 and d < ann_r2: dat_filled[y-1, x-1] = -10 * max_abs_dat else: if d <= ann_r1: # Scale flux to counts/px for current pixel's exposure dat_filled[y-1, x-1] = np.random.poisson(flux * dat_exp[y-1, x-1]) phdu.data = dat_filled fits_image.writeto(F_OUTPUT, clobber=OPT_CLOBBER) print "\nWrote output to {:s}. Check image for reasonable annuli".format(F_OUTPUT) print "(e.g., overlapping sources OK; annuli not sampling beyond sky FOV)."
def main(): """File conversion I/O""" parser = argparse.ArgumentParser(description=("Convert an" " ASC-REGION-FITS point source exclusion (!CIRCLE) file" " from RA/dec celestial coordinates with radii in arcmin." " to tangent-projected sky coordinates (xy)")) parser.add_argument('input', metavar='input-radec.fits', help=("ASC-REGION-FITS sources in un-projected RA/dec," " radii in arcmin.")) parser.add_argument('--template', metavar='reference.fits', help=("Reference ASC-REGION-FITS file (with desired" " projection and header keywords")) parser.add_argument('--out', metavar='output-sky.fits', help=("Output filename, converted ASC-REGION-FITS in" " tangent-projected sky coordinates")) args = parser.parse_args() F_INPUT = args.input F_TEMPLATE = args.template F_OUTPUT = args.out # Parse and validate input if not re.match(".*bkg_region-sky.*\.fits", F_TEMPLATE): warn(("--template {:s} doesn't follow ESAS naming").format(F_TEMPLATE)) if not re.match(".*bkg_region-sky.*\.fits", F_OUTPUT): warn(("--out {:s} doesn't follow ESAS naming").format(F_OUTPUT)) #shutil.copy(F_TEMPLATE, F_OUT) fits_input = fits.open(F_INPUT) fits_template = fits.open(F_TEMPLATE) t_in = fits_input[1] # first BinTable t_template = fits_template[1] for t in [t_in, t_template]: assert t.header['HDUCLASS'] == 'ASC' assert t.header['HDUCLAS1'] == 'REGION' assert t.header['HDUCLAS2'] == 'STANDARD' assert t_in.header['MTYPE1'] == 'pos' assert t_in.header['MFORM1'] == 'RA,DEC' assert all(map(lambda x: x == '!CIRCLE', t_in.data['SHAPE'])) # Set up projection center assert t_template.header['TCTYP2'] == "RA---TAN" assert t_template.header['TCUNI2'] == "deg" x0 = t_template.header['TCRPX2'] ra0 = t_template.header['TCRVL2'] assert t_template.header['TCTYP3'] == "DEC--TAN" assert t_template.header['TCUNI3'] == "deg" y0 = t_template.header['TCRPX3'] dec0 = t_template.header['TCRVL3'] # Square coordinates assert t_template.header['TCDLT2'] == -1 * t_template.header['TCDLT3'] assert t_template.header['TCDLT3'] > 0 scale = t_template.header['TCDLT3'] # Perform the coordinate conversion (vectorized) x, y = radec2tanproj(t_in.data['RA'][:,0], t_in.data['DEC'][:,0], ra0, dec0, x0, y0, 1/scale) # 1/scale needed to get pixel/deg., as FITS sky coords records deg./pixel radius = t_in.data['R'][:,0] / (abs(scale) * 60) # scale*60 = arcmin/px # Construct output FITS file in same format as output by SAS task region # (except use 'E' instead of '4E' for X, Y, R, ROTANG) x_pad = pad_E_to_4E(x) y_pad = pad_E_to_4E(y) radius_pad = pad_E_to_4E(radius) bhdu = fits.BinTableHDU.from_columns( [fits.Column(name='SHAPE', format=t_in.columns['SHAPE'].format, array=t_in.data['SHAPE']), fits.Column(name='X', format='4E', array=x_pad), fits.Column(name='Y', format='4E', array=y_pad), fits.Column(name='R', format='4E', array=radius_pad), fits.Column(name='ROTANG', format='4E', array=t_in.data['ROTANG']), fits.Column(name='COMPONENT', format='J', array=t_in.data['COMPONENT']) ] ) bhdu.name = 'REGION' # Loosely following ASC-REGION-FITS spec bhdu.header['HDUVERS'] = '1.2.0' bhdu.header['HDUCLASS'] = 'ASC' bhdu.header['HDUCLAS1'] = 'REGION' bhdu.header['HDUCLAS2'] = 'STANDARD' bhdu.header['HDUDOC'] = 'ASC-FITS-REGION-1.2: Rots, McDowell' bhdu.header['MTYPE1'] = 'pos' bhdu.header['MFORM1'] = 'X,Y' for kw in ['TCTYP2', 'TCRPX2', 'TCRVL2', 'TCUNI2', 'TCDLT2', 'TCTYP3', 'TCRPX3', 'TCRVL3', 'TCUNI3', 'TCDLT3']: # Coordinate transformation parameters already validated above if ("TCRPX" in kw) or ("TCRVL" in kw) or ("TCDLT" in kw): bhdu.header[kw] = "{:.14e}".format(t_template.header[kw]).upper() else: bhdu.header[kw] = t_template.header[kw] phdu = fits_template[0] # Work off of template header phdu.header['HISTORY'] = ('Rewritten by {} (atran@cfa)' ' using RA/dec list {} on date {}').format( os.path.basename(__file__), os.path.basename(F_INPUT), datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%Sz')) # RFC3339/ISO8601 date as used by XMM tools; add 'Z' to indicate timezone f_out = fits.HDUList([phdu, bhdu]) f_out.writeto(F_OUTPUT, clobber=True)