def A_photometry(image_data, bg_err, factor=1, ape_sum=[], ape_sum_err=[], cx=15, cy=15, r=2.5, a=5, b=5, w_r=5, h_r=5, theta=0, shape='Circular', method='center'): ''' Performs aperture photometry, first by creating the aperture (Circular, Rectangular or Elliptical), then it sums up the flux that falls into the aperture. Parameters ---------- image_data: 3D array Data cube of images (2D arrays of pixel values). bg_err : 1D array Array of uncertainties on pixel value. factor : float (optional) Electron count to photon count factor. Default is 1 if none given. ape_sum : 1D array (optional) Array of flux to append new flux values to. If 'None', the new values will be appended to an empty array ape_sum_err: 1D array (optional) Array of flux uncertainty to append new flux uncertainty values to. If 'None', the new values will be appended to an empty array. cx : int (optional) x-coordinate of the center of the aperture. Default is 15. cy : int (optional) y-coordinate of the center of the aperture. Default is 15. r : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Circular', c_radius is the radius for the circular aperture. Default is 2.5. a : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Elliptical', e_semix is the semi-major axis for elliptical aperture (x-axis). Default is 5. b : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Elliptical', e_semiy is the semi-major axis for elliptical aperture (y-axis). Default is 5. w_r : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Rectangular', r_widthx is the full width for rectangular aperture (x-axis). Default is 5. h_r : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Rectangular', r_widthy is the full height for rectangular aperture (y-axis). Default is 5. theta : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Elliptical' or 'Rectangular', theta is the angle of the rotation angle in radians of the semimajor axis from the positive x axis. The rotation angle increases counterclockwise. Default is 0. shape : string object (optional) If phot_meth is 'Aperture', ap_shape is the shape of the aperture. Possible aperture shapes are 'Circular', 'Elliptical', 'Rectangular'. Default is 'Circular'. method : string object (optional) If phot_meth is 'Aperture', apemethod is the method used to determine the overlap of the aperture on the pixel grid. Possible methods are 'exact', 'subpixel', 'center'. Default is 'center'. Returns ------- ape_sum : 1D array Array of flux with new flux appended. ape_sum_err: 1D array Array of flux uncertainties with new flux uncertainties appended. ''' l, h, w = image_data.shape position = [cx, cy] if (shape == 'Circular'): aperture = CircularAperture(position, r=r) elif (shape == 'Elliptical'): aperture = EllipticalAperture(position, a=a, b=b, theta=theta) elif (shape == 'Rectangular'): aperture = RectangularAperture(position, w=w_r, h=h_r, theta=theta) for i in range(l): data_error = calc_total_error(image_data[i, :, :], bg_err[i], effective_gain=1) phot_table = aperture_photometry(image_data[i, :, :], aperture, error=data_error, pixelwise_error=False) if (phot_table['aperture_sum_err'] > 0.000001): ape_sum.extend(phot_table['aperture_sum'] * factor) ape_sum_err.extend(phot_table['aperture_sum_err'] * factor) else: ape_sum.extend([np.nan]) ape_sum_err.extend([np.nan]) return ape_sum, ape_sum_err
def find_target_phot(stack, fil, fwhm, zp, zp_err, show_phot=False, log=None, log2=None, ra=None, dec=None, x=None, y=None): ''' Function to perform photometry on a target at a specific RA and Dec. Usually used after `find_zero_point` (this finds the ``zp``, ``zp_err``, and ``fwhm``). During this process, the user has the options to change the target's location and radii used in aperture photometry (if ``show_plots`` is ``True``, user can see cutouts of target and its radial profile). Attempts to find the magnitude of the target using aperture photometry. If magnitude cannot be found at a 3 sigma confidence interval, limiting magnitude is found at the same position. Limiting magnitude is found by adding fake sources of increasing magnitude at the target's position until it is more than 3 sigma above the background. :param stack: str Name of the stack to use (include path to the stack). :param fil: str Name of the filter of the stack. :param fwhm: float FWHM of image (most likely found through `find_zero_point`). :param zp: float Zero point of image (most likely found through `find_zero_point`). :param zp_err: float Error on the zero point of image (most likely found through `find_zero_point`). :param show_phot: boolean, optional Option to see the cutout of the source with apertures along with its radial profile. Default is ``False``. :param log: log, optional In-depth log. If no log is inputted, information is printed out instead of being written to ``log``. Default is ``None``. :param log2: log, optional Overview log. If no log is inputted, information is printed out instead of being written to ``log2``. Default is ``None``. :param ra: float, optional RA coordinate of target. Default is ``None``. :param dec: float, optional Dec coordinate of target. Default is ``None``. :param x: float, optional X pixel coordinate (not RA). Default is ``None``. :param y: float, optional Y pixel coordiante (not Dec). Default is ``None``. Returns ------- :return: float, float Three options: * Returns the magnitude of the target (in AB) and its error. * Returns tne limiting magnitude at the target's location (in AB) and its error. * Returns ``None``, ``None`` if a fake source of amplitude 5 (adu / sec / pixel) is not three sigma above the background. ''' with fits.open(stack) as hdr: header, data = hdr[0].header, hdr[0].data w = wcs.WCS(header) # Parse the WCS keywords in the primary HDU if ra is not None: coords = np.array([[ra, dec]], np.float) # Array of coordinates: [[RA, Dec]] in [deg, deg] pixel_coords = w.wcs_world2pix(coords, 1)[0] # Find the pixel coordinates in the image x = pixel_coords[0] y = pixel_coords[1] elif x is not None: coords = np.array([[x, y]], np.float) # Array of coordinates: [[RA, Dec]] in [deg, deg] pixel_coords = w.wcs_pix2world(coords, 1)[0] # Find the pixel coordinates in the image ra = pixel_coords[0] dec = pixel_coords[1] else: print("No coordinates inputted") d_x_y = 125 # Pixels above and below the target when showing image...125 corresponds to a 50" x 50" cutout if log is not None: log.info("Pixel coordinates: (%.3f, %.3f)" % (x, y)) else: print("Pixel coordinates: (%.3f, %.3f)" % (x, y)) if input('Would you like to choose the cutout size? Default is 125x125 pixels (50"x50"): ') == "yes": try: d_x_y = int(input("Choose the radius, in arcsec: "))*2.5 except TypeError: pass sigma_clip = SigmaClip(sigma=3) # This section is to find the error on the background bkg_estimator = MedianBackground() bkg = Background2D(data, (120, 120), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) error = calc_total_error(data, bkg.background_rms, 1) # Used later as well for limiting magnitude if log is not None: log.info("You will now get to pick the position you would like. " "Decide what pixel coordinates you would like to use") log.info("Blue circle in image is the distance the radial profile extends to") else: print("You will now get to pick the position you would like. " "Decide what pixel coordinates you would like to use") print("Blue circle in image is the distance the radial profile extends to") correct_position = "no" while correct_position == "no": if show_phot: radial_profile(data, x, y, 0.7, fwhm, rad=6) # Radial profile plt.axvline(6*fwhm) print("Double click to set the new center. Do nothing if you are ok with current coordinates.") fig = plt.figure(2) new_coords = [] fig.canvas.mpl_connect('button_press_event', lambda event: onclick(event, new_coords=new_coords)) ap_in = CircularAperture((x, y), 1) # Aperture to perform photometry ap_out = CircularAperture((x, y), 6*fwhm) # Aperture to perform photometry ap_in.plot(color='r', lw=1) # Plot of target with apertures and annulus ap_out.plot(color='b', lw=2) norm = simple_norm(data, 'sqrt', percent=99) plt.imshow(data, norm=norm) plt.xlim(int(x) - d_x_y, int(x) + d_x_y) plt.ylim(int(y) - d_x_y, int(y) + d_x_y) plt.colorbar() plt.show() if len(new_coords) != 0: x, y = new_coords[len(new_coords) - 2], new_coords[len(new_coords) - 1] real_coords = w.wcs_pix2world(np.array([[x, y]], np.float), 1)[0] # Find the pixel coords in the image ra, dec = real_coords[0], real_coords[1] if input("Are you ok with current coordinates? RA = %.5f Dec = %.5f Type 'yes' or 'no': " % (ra, dec)) != "yes": pass else: correct_position = "yes" if log is not None: log.info("Coordinates chosen: (%.3f, %.3f) at RA = %.5f and Dec = %.5f" % (x, y, ra, dec)) else: print("Coordinates chosen: (%.3f, %.3f) at RA = %.5f and Dec = %.5f" % (x, y, ra, dec)) if log2 is not None: log2.info("Final coordinates: (%.3f, %.3f) at RA = %.5f and Dec = %.5f" % (x, y, ra, dec)) if log is not None: log.info("You will now get to choose the radii for the circular aperture and the r_in and r_out of the annulus") else: print("You will now get to choose the radii for the circular aperture and the r_in and r_out of the annulus") correct_radii = "no" while correct_radii == "no": if log is not None: log.info("Automatic radii picked by comparing to FWHM of field: rad = %.3f, r_in = %.3f, r_out = %.3f" % (2.5 * fwhm, 3.5 * fwhm, 4.5 * fwhm)) else: print("Automatic radii picked by comparing to FWHM of field: rad = %.3f, r_in = %.3f, r_out = %.3f" % (2.5 * fwhm, 3.5 * fwhm, 4.5 * fwhm)) if input("Would you like to use these radii? Type 'yes or 'no': ") == "yes": rad, r_in, r_out = 2.5*fwhm, 3.5*fwhm, 4.5*fwhm else: log.info("FWHM = %.3f" % fwhm) rad = float(input("Pick a radius (in pixels) for the circular aperture: ")) r_in = float(input("Pick an inner radius (in pixels) for the background annulus: ")) r_out = float(input("Pick an outer radius (in pixels) for the background annulus: ")) if r_in >= r_out: r_out = r_in + 1 if log is not None: log.info("You selected an invalid r_out value. Automatically set to %s" % r_out) else: print("You selected an invalid r_out value. Automatically set to %s" % r_out) aperture = CircularAperture((x, y), rad) # Aperture to perform photometry annulus_aperture = CircularAnnulus((x, y), r_in=r_in, r_out=r_out) # Annulus of target annulus_mask = annulus_aperture.to_mask(method='center') # Create masks to highlight pixels in annuli annulus_data = annulus_mask.multiply(data) annulus_data_1d = annulus_data[annulus_mask.data > 0] _, median_sigclip, stddev = sigma_clipped_stats(annulus_data_1d) phot_table = aperture_photometry(data, aperture, error=error) # Photometry, error accounted for bkg_aper = median_sigclip * aperture.area if show_phot: # Radial profile out to 6 * fwhm (large just to be safe) radial_profile(data, x, y, 0.7, fwhm, rad=6) plt.axvline(rad, c='r') plt.axvline(r_in) plt.axvline(r_out) plt.figure(2) aperture.plot(color='white', lw=2) # Plot of target with apertures and annulus annulus_aperture.plot(color='b', lw=2) norm = simple_norm(data, 'sqrt', percent=99) plt.imshow(data, norm=norm) plt.xlim(int(x) - d_x_y, int(x) + d_x_y) plt.ylim(int(y) - d_x_y, int(y) + d_x_y) plt.colorbar() plt.show() correct_radii = input("Are you ok with the previously selected radii? Type 'yes' or 'no': ") if log is not None: log.info("Radii chosen: %.3f, %.3f, %.3f" % (rad, r_in, r_out)) else: print("Radii chosen: %.3f, %.3f, %.3f" % (rad, r_in, r_out)) if log2 is not None: log2.info("Final radii: %.3f, %.3f, %.3f" % (rad, r_in, r_out)) three_sigma = 3 # Change according to what sigma you'd like to use if (phot_table['aperture_sum']) - bkg_aper > 0: mag = -2.5 * np.log10((phot_table['aperture_sum']) - bkg_aper) # Instrumental magnitude mag_err = 2.5 / np.log(10.) * (phot_table['aperture_sum_err'])/(phot_table['aperture_sum']) mag += zp # Apparent magnitude (Vega) mag_err = np.sqrt(mag_err ** 2 + zp_err ** 2) if log is not None: log.info("Magnitude of target = %.3f +/- %.3f AB mag" % (mag + AB_conversion(fil), mag_err)) # Final mag if log2 is not None: log2.info("Magnitude of target = %.3f +/- %.3f AB mag" % (mag + AB_conversion(fil), mag_err)) # Final mag else: print("Magnitude of target = %.3f +/- %.3f AB mag" % (mag + AB_conversion(fil), mag_err)) # Final magnitude if np.abs(mag_err) < 1/three_sigma: # Good! No limiting magnitude needed return float(mag + AB_conversion(fil)), float(mag_err) if log is not None: log.info("Target was not %s sigma above the background." % three_sigma) else: print("Target was not %s sigma above the background." % three_sigma) if log2 is not None: log2.info("Target not found") else: if log is not None: log.info("Target's aperture sum was less than background aperture sum. Target was not found") else: print("Target's aperture sum was less than background aperture sum. Target was not found") if log2 is not None: log2.info("Target not found") # Finding the limiting magnitude where the target was supposed to be located if log is not None: log.info("Now finding the limiting magnitude to %s sigma." % three_sigma) else: print("Now finding the limiting magnitude to %s sigma." % three_sigma) d_x_y = 50 d = data[int(y)-d_x_y:int(y)+d_x_y, int(x)-d_x_y:int(x)+d_x_y] # Cutout of source; upside down from normal x_mesh, y_mesh = np.meshgrid(np.linspace(0, np.shape(d)[1] - 1, np.shape(d)[1]), np.linspace(0, np.shape(d)[0] - 1, np.shape(d)[0])) amplitudes = np.arange(0, 5, 0.1) # Amplitudes to consider for i, j in enumerate(amplitudes): gauss_data = twoD_Gaussian((x_mesh, y_mesh), j, d_x_y, d_x_y, fwhm/2.35482, fwhm/2.35482, 0, 0) # Fake source norm = simple_norm(data, 'sqrt', percent=99) # Find this before changing data d += gauss_data.reshape(np.shape(d)) # Adding the Gaussian to the data # Photometry on source with resulting limiting magnitude aperture = CircularAperture((x, y), rad) # Aperture to perform photometry annulus_aperture = CircularAnnulus((x, y), r_in=r_in, r_out=r_out) # Annulus of target annulus_mask = annulus_aperture.to_mask(method='center') # Create masks to highlight pixels in annuli annulus_data = annulus_mask.multiply(data) annulus_data_1d = annulus_data[annulus_mask.data > 0] _, median_sigclip, stddev = sigma_clipped_stats(annulus_data_1d) phot_table = aperture_photometry(data, aperture, error=error) # Photometry, error accounted for (found earlier) bkg_aper = median_sigclip * aperture.area if (phot_table['aperture_sum']) - bkg_aper > 0: mag_err = 2.5 / np.log(10.) * (phot_table['aperture_sum_err']) / (phot_table['aperture_sum']) if np.abs(mag_err) < 1/three_sigma: if log is not None: log.info("Amplitude: %.3f Magnitude Error: %.3f" % (j, mag_err)) else: print("Amplitude: %.3f Magnitude Error: %.3f" % (j, mag_err)) if show_phot: plt.figure(2) d -= gauss_data.reshape(np.shape(d)) aperture.plot(color='white', lw=2) # Old data plot annulus_aperture.plot(color='b', lw=2) plt.imshow(data, norm=norm) plt.xlim(int(x) - d_x_y, int(x) + d_x_y) plt.ylim(int(y) - d_x_y, int(y) + d_x_y) plt.colorbar() plt.title("Target without fake source added in") plt.figure(3) d += gauss_data.reshape(np.shape(d)) aperture.plot(color='white', lw=2) # New data plot annulus_aperture.plot(color='b', lw=2) plt.imshow(data, norm=norm) plt.xlim(int(x) - d_x_y, int(x) + d_x_y) plt.ylim(int(y) - d_x_y, int(y) + d_x_y) plt.colorbar() plt.title("Target after fake source has been added in") # Radial profile...only need for good fake source (same process as used to find the FWHM) radial_profile(data, x, y, 0.7, fwhm, rad=r_out/fwhm) plt.axvline(rad, c='r') plt.axvline(r_in) plt.axvline(r_out) plt.show() mag = -2.5 * np.log10((phot_table['aperture_sum']) - bkg_aper) # Instrumental magnitude mag += zp # Apparent magnitude (Vega) mag_err = np.sqrt(mag_err ** 2 + zp_err ** 2) # Limiting magnitude at the position of the target if log is not None: log.info("Limiting magnitude = %.3f +/- %.3f AB mag" % (mag + AB_conversion(fil), mag_err)) else: print("Limiting magnitude = %.3f +/- %.3f AB mag" % (mag + AB_conversion(fil), mag_err)) if log2 is not None: log2.info("Limiting magnitude = %.3f +/- %.3f AB mag" % (mag + AB_conversion(fil), mag_err)) return float(mag + AB_conversion(fil)), float(mag_err) d -= gauss_data.reshape(np.shape(d)) # 'Reset' data for next run if limiting magnitude not found if log is not None: log.info("False source with amplitude of 5 (adu/sec/pixel) was not %s sigma above the background." % three_sigma) log.info("Limiting magnitude not found") else: print("False source with amplitude of 5 (adu/sec/pixel) was not %s sigma above the background." % three_sigma) print("Limiting magnitude not found") if log2 is not None: log2.info("Limiting magnitude not found") return None, None
def find_zero_point(stack, fil, ext, data_path=None, show_fwhm=False, show_bkg=False, show_contours=False, r_annulus_in=3.5, r_annulus_out=4.5, log=None): ''' Function used to find the FWHM, zero point, and zero point error for a specific stack and filter. Initially finds all sources in 2MASS (HJK) or UKIDSS (Y) within the image and matches them to a 10+ sigma source in the image. Then makes a series of cuts to determine the standard stars to use when finding the zero point: * Only considers sources with a center count (in adu / pix / sec) between 100 and 300 - Plots radial profiles of these sources - Finds the median FWHM of the image from these sources (returns this value) * Removes sources too close to the edge * Removes sources that are too close to each other * Removes oblong sources (most likely galaxies or two very close stars) Checks isophotes of remaining sources to determine if the stack is aligned correctly. * Shows the user a sample cutout of a source that reflects the isophotes of the stack as a whole If not, then allows the user the option of checking them individual images and possibly remaking the stack with `revisit_stack` before continuing. If ``show_bkg`` is True, shows the remaining sources to be used when finding the zero point, color coded by the median background level. Finds the instrumental magnitudes of the remaining sources and compares to the magnitudes from the catalog (2MASS or UKIDSS) and determines the zero point and zero point error. Returns these values along with the FWHM of the image. Parameters ---------- :param stack: str Name of the stack to use (include path to the stack). :param fil: str Name of the filter of the stack. :param ext: int Extension to use (only used if `revisit_stack` is called). :param data_path: str, optional Path to the original data (only used if `revisit_stack` is called). Default is ``None``. If ``None`` and `revisit_stack` is called, error will occur. :param show_fwhm: boolean, optional Set to ``True`` to view the radial profiles when finding the FWHM of the image. Default is ``False``. :param show_bkg: boolean, optional Set to ``True`` to view the final sources before finding the zero point, color coded by the median background level. Default is ``False``. :param show_contours: boolean, optional Set to ``True`` to view the contours when examining the isophotes. Also shows isophotes in `revisit_stack`. Default is ``False``. :param r_annulus_in: int, optional Radius for the inner annulus when performing photometry on the final sources to determine their instrumental magnitude. Default is ``3.5``. :param r_annulus_out: int, optional Radius for the oter annulus when performing photometry on the final sources to determine their instrumental magnitude. Default is ``4.5``. :param log: In-depth log. If no log is inputted, information is printed out instead of being written to ``log``. Default is ``None``. Returns ------- :return: float, float, float First float is the calculated zero point of the image. Second float is the calculated zero point error of the image. Third float is the calculated FWHM of the image. ''' if log is not None: log.info("Computing FWHM of sources in the stack %s" % stack) else: print("Computing FWHM of sources in the stack %s" % stack) with fits.open(stack) as hdr: header, data = hdr[0].header, hdr[0].data w = wcs.WCS(header) # Center in pixel coordinates, change to real coordinates real_coords = w.wcs_pix2world( np.array([[header['NAXIS1'] / 2, header['NAXIS2'] / 2]], np.float), 1) coords = SkyCoord(real_coords[0][0] * u.deg, real_coords[0][1] * u.deg, frame='fk5') Vizier.ROW_LIMIT = -1 # So that you get all the rows, not just the first 50 # Information about the sources from 2MASS found in the extension (full area, not radially) if (fil != 'y') or (fil != 'Y'): two_mass = Vizier.query_region(coords, width=13.65 * u.arcmin, catalog='II/246')[0] else: two_mass = Vizier.query_region( coords, width=13.65 * u.arcmin, catalog='II/319')[0] # Todo UKIDSS not 2MASS # Find the sources above 10 std in the image _, median, std = sigma_clipped_stats(data, sigma=3.0) daofind = DAOStarFinder( fwhm=7, threshold=10. * std) # Looking for best sources, so raise threshold way up dao_sources = daofind( np.asarray(data)) # DAOStarFinder sources == dao_sources if log is not None: log.info( "%s objects found in 2MASS and %s sources above 10 std found in the stack" % (len(two_mass), len(dao_sources))) log.info( "DAOStarFinder found %s sources that are 10 std above the background" % len(dao_sources)) else: print( "%s objects found in 2MASS and %s sources above 10 std found in the stack" % (len(two_mass), len(dao_sources))) print( "DAOStarFinder found %s sources that are 10 std above the background" % len(dao_sources)) ra_dao = [ ] # Change pixel coordinates to real coordinates for DAOStarFinder sources dec_dao = [] for i, j in enumerate(dao_sources): pixel_coords = np.array([[j['xcentroid'], j['ycentroid']]], np.float) dao_coords = w.wcs_pix2world(pixel_coords, 1) ra_dao.append(dao_coords[0][0]) dec_dao.append(dao_coords[0][1]) # Coordinates need to be in real, not pixel coords_dao = SkyCoord(ra_dao * u.deg, dec_dao * u.deg, frame='fk5') # Coordinates from sources # coords_two_mass = SkyCoord(two_mass['ra'], two_mass['dec'], frame='fk5') # Coordinates from 2MASS catalog coords_two_mass = SkyCoord(two_mass['RAJ2000'], two_mass['DEJ2000'], frame='fk5') # Coordinates from 2MASS catalog # Match each 2MASS sources to the closest dao_source idx, d2, d3 = coords_two_mass.match_to_catalog_sky( coords_dao) # Match smaller to larger if log is not None: log.info("%s objects have been matched" % len(idx)) else: print("%s objects have been matched" % len(idx)) # Next step: find the radial profiles from intermediate-brightness sources and then find the fwhm of the field step_size = 0.7 fwhm_orig = 5 coords = [ w.wcs_world2pix(np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0] for j in idx ] # Positions of DAO radii = np.arange(step_size, 2.5 * fwhm_orig, step_size) # Radii of apertures x_data = np.arange( 0, 2.5 * fwhm_orig, step_size) # Steps (starting at zero, to 2.5 * fwhm, by step_size) apers_area = [np.pi * (step_size**2)] # Circle area = pi*(r**2) for r in radii: apers_area.append(np.pi * ((r + step_size)**2 - r**2)) # Annulus area = pi*(r_out**2 - r_in**2) apertures = [CircularAperture(coords, r=step_size) ] # For circle aperture around center for r in radii: apertures.append(CircularAnnulus(coords, r_in=r, r_out=r + step_size)) # Annuli apertures phot_table = aperture_photometry( data, apertures ) # Each row is a source, with len(x_data) photometry measurements if show_fwhm: fig, ax = plt.subplots() sigmas = [] new_idx = [] for i, source in enumerate( phot_table ): # Find the sums per area (adu/pix/sec) for each of the sources sums = [source[i] / apers_area[i - 3] for i in range(3, len(source))] # Only consider moderately bright sources; too bright are saturated and too dim can have larger error values if 100 <= sums[0] <= 300 and sums[len(sums) - 1] < 15: try: # Try unless it takes too much time...if that happens, it's probably a bad fit and we don't want it w_fit = x_data f_fit = sums g, _ = curve_fit(fix_x0, w_fit, f_fit) if show_fwhm: # Add two plots: radial profile and Gaussian fit ax.plot(x_data, sums, 'o-', alpha=0.5, lw=2, markersize=4) # Plot the radial profiles ax.plot(x_data, fix_x0(x_data, *g), '^-', alpha=0.5, lw=2, markersize=4) sigmas.append( np.abs(g[1]) ) # Sigma can be either positive or negative; fix_x0 allows both new_idx.append( idx[i] ) # Append to a list that will be used later for future sources except RuntimeError: pass fwhm_values = [i * 2.35482 for i in sigmas] sigmas_post_clipping, _, _ = sigmaclip(sigmas, low=3, high=3) # Clip outlier sigma values med = np.median(sigmas_post_clipping) # Median sigma value fwhm = med * 2.35482 # FWHM to be used to find the zero point and make cuts if log is not None: log.info( "Median sigma: %.3f Median FWHM: %.3f Number of sources counted/total found in field: %s/%s" % (med, fwhm, len(sigmas_post_clipping), len(dao_sources))) else: print( "Median sigma: %.3f Median FWHM: %.3f Number of sources counted/total found in field: %s/%s" % (med, fwhm, len(sigmas_post_clipping), len(dao_sources))) clipped_fwhm_values = [i * 2.35482 for i in sigmas_post_clipping] if show_fwhm: ax.set_title("Radial profiles of sources 10 std above background" ) # Plot of radial profiles and Gaussian fits ax.set_xlabel("Radial distance from centroid of source (pixels)") ax.set_ylabel("Count (adu per second per pixel)") plt.show() fig, ax = plt.subplots( ) # Histogram of fwhm_values after sigma clipping plt.hist(clipped_fwhm_values, bins=30) ax.set_title("FWHM values for Gaussian-fit radial profiles") ax.set_xlabel("FWHM value for best-fit Gaussian (pixels)") plt.show() # Calculate the zero point if log is not None: log.info( "Computing the zero point of the stack %s with filter %s and FWHM of %.3f" % (stack, fil, fwhm)) else: print( "Computing the zero point of the stack %s with filter %s and FWHM of %.3f" % (stack, fil, fwhm)) orig_coords = [ w.wcs_world2pix(np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0] for j in new_idx ] # DAO pos # First cut: removing sources near the edge edge_error = 100 first_cut_coords, first_cut_idx, first_fwhm_values = [], [], [] for i, j in enumerate(orig_coords): if not (j[0] < edge_error or data.shape[0] - j[0] < edge_error or j[1] < edge_error or data.shape[0] - j[1] < edge_error): first_cut_coords.append(orig_coords[i]) first_cut_idx.append(new_idx[i]) first_fwhm_values.append(fwhm_values[i]) # Second cut: removing sources that are not isolated # 1: Make sure no two sources are within (r_annulus_out + r_annulus_in) * fwhm of each other # 2: If a second source is present in the annulus, the sigma clipping for the mask will cut out the additional # counts from these pixels so that the second source will be cut out and not interfere with the zero point # If the sources are close enough together, they will either be cut out here or will look oblong and be cut out next overlap = [] for i, j in enumerate(first_cut_coords): for k in range(i + 1, len(first_cut_coords)): # (r_ann_out + r_ann_in)*fwhm so that the annulus of one doesn't overlap the circular aperture of the other if np.sqrt((j[0] - first_cut_coords[k][0])**2 + (j[1] - first_cut_coords[k][1])**2) < \ (r_annulus_out + r_annulus_in) * fwhm: overlap.append(i) overlap.append(k) second_cut_coords, second_cut_idx, second_fwhm_values = [], [], [] for i, j in enumerate(first_cut_coords): if i not in overlap: second_cut_coords.append(j) second_cut_idx.append(first_cut_idx[i]) second_fwhm_values.append(first_fwhm_values[i]) # Third cut: removing sources that are not circular # Part 1: remove sources with a fwhm 1.2 * median fwhm of the field (1.2 determined by considering 5 stacks) third_cut_coords, third_cut_idx, third_fwhm_values = [], [], [] for i, j in enumerate(second_fwhm_values): if not (j > 1.2 * fwhm): third_cut_coords.append(second_cut_coords[i]) third_cut_idx.append(second_cut_idx[i]) third_fwhm_values.append(j) if log is not None: log.info( "Checking if the stack was performed correctly with %s sources" % len(third_cut_coords)) # Final check else: print("Checking if the stack was performed correctly with %s sources" % len(third_cut_coords)) # Final check d_x_y = 40 field_sigmas = cutouts_2d_gaussian(stack, third_cut_coords, d_x_y, fwhm, show_cutouts=show_contours) field_ratio = np.median(field_sigmas) if log is not None: log.info("Median field ratio (y_sigma/x_sigma): %.3f" % field_ratio) else: print("Median field ratio (y_sigma/x_sigma): %.3f" % field_ratio) if not ((1 / 1.2) > field_ratio or 1.2 < field_ratio): if log is not None: log.info( "Good to go! Now calculating the zero point with %s sources" % len(third_cut_coords)) else: print( "Good to go! Now calculating the zero point with %s sources" % len(third_cut_coords)) else: # Stack is slightly off. Check to see what the issue is if log is not None: log.info("Stack seems to be off. Sample cutout shown") else: print("Stack seems to be off. Sample cutout shown") if show_contours: for i, j in enumerate(third_cut_coords): d = data[int(j[1]) - d_x_y:int(j[1]) + d_x_y, int(j[0]) - d_x_y:int(j[0]) + d_x_y] # Cutout of source x, y = np.meshgrid( np.linspace(0, np.shape(d)[1] - 1, np.shape(d)[1]), np.linspace(0, np.shape(d)[0] - 1, np.shape(d)[0])) initial_guess = (100, d_x_y, d_x_y, fwhm / 2.35482, fwhm / 2.35482, 0, 0 ) # Initial guess is IMPORTANT popt, pcov = curve_fit(twoD_Gaussian, (x, y), d.ravel(), p0=initial_guess) # Fit of 2D Gaussian if np.abs((popt[4] / popt[3]) - field_ratio) < 0.01: if log is not None: log.info('x sigma = %.3f y sigma = %.3f ratio = %.3f' % (popt[3], popt[4], popt[4] / popt[3])) else: print('x sigma = %.3f y sigma = %.3f ratio = %.3f' % (popt[3], popt[4], popt[4] / popt[3])) fitted_data = twoD_Gaussian((x, y), *popt) contours = np.arange(np.min(d), np.max(d), 30.) norm = simple_norm(data, 'sqrt', percent=99) plt.imshow(d, norm=norm) plt.colorbar() plt.contour(x, y, fitted_data.reshape(np.shape(d)), contours, colors='w') plt.show() break # Breaks out of loop if input( "Would you like to revisit the stack and look at isophotes within it? Type 'yes' or 'no': " ) == "yes": try: zp, zp_err, fwhm = revisit_stack(stack, fil, ext, third_cut_coords, fwhm, show_fwhm, show_bkg, show_contours, data_path, log) return zp, zp_err, fwhm except TypeError: pass elif log is not None: log.info( "User decided not to remake the stack even though isophotes are not circular" ) final_coords, final_idx = third_cut_coords, third_cut_idx # Final coordinates and idx to use # Annuli of DAO sources annulus_apertures = CircularAnnulus(final_coords, r_in=fwhm * r_annulus_in, r_out=fwhm * r_annulus_out) annulus_masks = annulus_apertures.to_mask( method='center') # Create masks to highlight pixels in annuli bkg_median = [] for mask in annulus_masks: annulus_data = mask.multiply(data) annulus_data_1d = annulus_data[mask.data > 0] _, median_sigclip, stddev = sigma_clipped_stats(annulus_data_1d) bkg_median.append(median_sigclip) bkg_median = np.array(bkg_median) if show_bkg: # Finds the sources for each upper median background limit and plots them over the image groupings = [[], [], [], [], [], [], [], []] for i, j in enumerate(bkg_median): if j < 0: groupings[0].append(i) elif j < 0.1: groupings[1].append(i) elif j < 0.25: groupings[2].append(i) elif j < 0.5: groupings[3].append(i) elif j < 0.75: groupings[4].append(i) elif j < 1: groupings[5].append(i) elif j < 2.5: groupings[6].append(i) else: groupings[7].append(i) positions = [[], [], [], [], [], [], [], []] for i, j in enumerate(final_idx): if i in groupings[0]: # Positions of DAO positions[0].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[1]: positions[1].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[2]: positions[2].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[3]: positions[3].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[4]: positions[4].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[5]: positions[5].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[6]: positions[6].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) elif i in groupings[7]: positions[7].append( w.wcs_world2pix( np.array([[ra_dao[j], dec_dao[j]]], np.float), 1)[0]) colors = [ 'w', 'r', 'orange', 'lime', 'deepskyblue', 'b', 'purple', 'k' ] bkg_med_list = [] for i, j in enumerate(positions): if len(positions[i]) != 0: apertures = CircularAperture( positions[i], 2.5 * fwhm) # Aperture to perform photometry # Annuli of DAO sources annulus_apertures = CircularAnnulus(positions[i], r_in=fwhm * r_annulus_in, r_out=fwhm * r_annulus_out) annulus_masks = annulus_apertures.to_mask( method='center' ) # Create masks to highlight pixels in annuli bkg_median_1 = [] for mask in annulus_masks: annulus_data = mask.multiply(data) annulus_data_1d = annulus_data[mask.data > 0] _, median_sigclip, stddev = sigma_clipped_stats( annulus_data_1d) bkg_median_1.append(median_sigclip) bkg_median_1 = np.array(bkg_median_1) bkg_med_list.append(bkg_median_1) apertures.plot(color='white', lw=2) annulus_apertures.plot(color=colors[i], lw=2) if log is not None: log.info("Plotting the sources by median background value") log.info( "Number of sources per list (<0, 0.1, 0.25, 0.5, 0.75, 1, 2.5, >2.5)" ) else: print("Plotting the sources by median background value") print( "Number of sources per list (<0, 0.1, 0.25, 0.5, 0.75, 1, 2.5, >2.5)" ) for i in range(len(bkg_med_list)): if log is not None: log.info(len(bkg_med_list[i])) else: print(len(bkg_med_list[i])) norm = simple_norm(data, 'sqrt', percent=99) plt.suptitle( 'w < 0 | r < 0.1 | o < 0.25 | g < 0.5 | lb < 0.75 | db < 1 | p < 2.5 | b > 2.5' ) plt.imshow(data, norm=norm) plt.colorbar() plt.show( ) # Plot of matched sources with annuli, color coded by median background value in annulus mag_two_mass = [] # Moving past show_bkg for i, j in enumerate(final_idx): # mag_two_mass.append(two_mass[i][fil.lower() + '_m']) # Reference magnitudes of 2MASS objects mag_two_mass.append( two_mass[i][fil.upper() + 'mag']) # Reference magnitudes of 2MASS objects final_apertures = CircularAperture(final_coords, 2.5 * fwhm) # Aperture to perform photometry # Annuli of DAO sources final_ann_aps = CircularAnnulus(final_coords, r_in=fwhm * r_annulus_in, r_out=fwhm * r_annulus_out) final_ann_masks = final_ann_aps.to_mask( method='center') # Create masks to highlight pixels in annuli final_bkg_median = [] for mask in final_ann_masks: # For each masked annulus, find the median background value and add to list annulus_data = mask.multiply(data) annulus_data_1d = annulus_data[mask.data > 0] _, median_sigclip, stddev = sigma_clipped_stats(annulus_data_1d) final_bkg_median.append(median_sigclip) final_bkg_median = np.array(final_bkg_median) sigma_clip = SigmaClip( sigma=3) # This section is to find the error on the background bkg_estimator = MedianBackground() bkg = Background2D(data, (120, 120), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) error = calc_total_error(data, bkg.background_rms, 1) phot_table = aperture_photometry( data, final_apertures, error=error) # Photometry, error accounted for bkg_aper = final_bkg_median * final_apertures.area mag_inst = -2.5 * np.log10( (np.asarray(phot_table['aperture_sum']) - bkg_aper)) # Instrumental magnitudes (and error) # Find zero point if log is not None: log.info( 'Calculating zero point from %i stars with sigma clipping and a maximum std error of 0.1.' % len(mag_inst)) else: print( 'Calculating zero point from %i stars with sigma clipping and a maximum std error of 0.1.' % len(mag_inst)) for i in np.flip(np.linspace(1, 3, num=10)): _, zp, zp_err = sigma_clipped_stats(mag_two_mass - mag_inst, sigma=i) if zp_err < 0.1: break if log is not None: log.info('zp = %.3f +/- %.3f.' % (zp, zp_err)) else: print('zp = %.3f +/- %.3f.' % (zp, zp_err)) # Online zp (vega): https://www.ukirt.hawaii.edu/instruments/wfcam/user_guide/performance.html return zp, zp_err, fwhm
def do_aperture_photometry(filename,count, fwhm,date): #fwhm,files=iraf_fwhm() #xpix,ypix=source_list(files) #ast=AstrometryNet() #ast.api_key= 'iqmqwvazpvolmjmn' ''' choice=input("Enter the mode. Please use the keywords\n 'single' for single image, 'multiple' for multiple images: \n\n") if (choice=='single'): print('single mode') elif (choice ==' multiple'): print(' multiple image mode') else: print('Enter valid choice!!!') ''' data,header=fits.getdata(filename,header=True) #exposure=header['EXPOSURE'] exposure=300 #print('Exposure is',exposure) sigma_clip = SigmaClip(sigma=3, maxiters=10) bkg_estimator = SExtractorBackground() bkg = Background2D(data, (10,10), filter_size=(3, 3),sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) back=bkg.background # this is the background we need for the background subtraction. back2=np.median(bkg.background) #print('median background is',back2) mask = data == 0 unit = u.electron / u.s xdf_image = CCDData(data, unit=unit, meta=header, mask=mask) norm_image = ImageNormalize(vmin=1e-4, vmax=5e-2, stretch=LogStretch(), clip=False) xdf_image_clipped = np.clip(xdf_image, 1e-4, None) mean, median, std = sigma_clipped_stats(xdf_image.data, sigma=3.0, maxiters=20, mask=xdf_image.mask) #print('Finding the sources') #daofind = DAOStarFinder(fwhm=fwhm, threshold=5*std) # 3 sigma above the background. #sources = daofind(data-back) #sources_findpeaks = find_peaks(xdf_image.data, mask=xdf_image.mask, threshold=30.*std, box_size=30, centroid_func=centroid_2dg) #print('We have found:',len(sources),' sources') #print(sources) #print(sources['xcentroid'], sources['ycentroid'],sources['fwhm']) #positions=sources['xcentroid'], sources['ycentroid'] positions=np.genfromtxt('co_ordinates_list.txt',unpack=True,usecols=(0,1)) #print(positions) radii=[ fwhm,2*fwhm, 3*fwhm,4*fwhm,5*fwhm] #positions=(sources['xcentroid'], sources['ycentroid']) apertures = [CircularAperture(positions, r=r) for r in radii] an_ap = CircularAnnulus(positions, r_in=6*fwhm, r_out=6.2*fwhm) #apers = [apertures, annulus_apertures] #bkg_sigma=mad_std(data) effective_gain=exposure error=calc_total_error(data,back,effective_gain) #error=0.1*data phot_table = aperture_photometry(data-back, apertures,error=error) phot_table2=aperture_photometry(data-back,an_ap) bkg_mean = phot_table2['aperture_sum'] / an_ap.area bkg_sum = bkg_mean * an_ap.area final_sum0=phot_table['aperture_sum_0']-bkg_sum final_sum1=phot_table['aperture_sum_1']-bkg_sum final_sum2=phot_table['aperture_sum_2']-bkg_sum final_sum3=phot_table['aperture_sum_3']-bkg_sum final_sum4=phot_table['aperture_sum_4']-bkg_sum mag_back=-2.5*np.log10(bkg_mean/exposure)+22 mag_0=-2.5*np.log10(final_sum0/exposure)+22 mag_1=-2.5*np.log10(final_sum1/exposure)+22 mag_2=-2.5*np.log10(final_sum2/exposure)+22 mag_3=-2.5*np.log10(final_sum3/exposure)+22 mag_4=-2.5*np.log10(final_sum4/exposure)+22 #print(mag_back,mag_0,mag_1,mag_2,mag_3,mag_4) flux_err_0=phot_table['aperture_sum_err_0'] mag_err_0=1.09*flux_err_0/final_sum0 flux_err_1=phot_table['aperture_sum_err_1'] mag_err_1=1.09*flux_err_1/final_sum1 flux_err_2=phot_table['aperture_sum_err_2'] mag_err_2=1.09*flux_err_2/final_sum2 flux_err_3=phot_table['aperture_sum_err_3'] mag_err_3=1.09*flux_err_3/final_sum3 flux_err_4=phot_table['aperture_sum_err_4'] mag_err_4=1.09*flux_err_4/final_sum4 ''' fig=plt.figure() plt.imshow(data,cmap='gray',origin='lower',vmin=mean-4*std,vmax=mean+4*std) colors=['red','salmon','yellow','blue','cyan'] for i in range(len(apertures)): apertures[i].plot(color=colors[i], alpha=0.7) an_ap.plot(color='green', alpha=0.7) plt.show() ''' with open ('{}_r.dat'.format(date),'w') as r: for i in range (len(phot_table)): #print(final_sum0[i],final_sum1[i],final_sum2[i],final_sum3[i],final_sum4[i],final_sum5[i],file=r) print(mag_back[i],mag_0[i],mag_err_0[i],mag_1[i],mag_err_1[i],mag_2[i],mag_err_2[i],mag_3[i],mag_err_3[i],mag_4[i],mag_err_4[i],file=r) print('No. {} file has been processed'.format(count+1)) ''' mag=-2.5*np.log10(final_sum/30)+25 flux=final_sum flux_err=phot_table['aperture_sum_err_0'] mag_err=1.09*flux_err/flux x=[phot.value for phot in phot_table['xcenter']] y=[phot.value for phot in phot_table['ycenter']] #with open('result.dat', 'w') as f: #with open('out.txt', 'w') as f: for i in range(len(x)): print(x[i],y[i],'\t',mag[i],mag_err[i]) outfile=' ' for i in range (len(phot_table)): outfile+=x[i]+ " "+ y[i]+" "+ mag[i]+" " +mag_err[i] outfile+='\n' out=open('result.txt','w') out.write(outfile,overwrite=True) out.close() ''' '''
def readimageanddatared(image_name, fits_data_slice, output_name, plot_title, fits_cut, fileloc=0): ''' Function: Read images and create simple plots ''' print('>>> Running readimageanddatared') if fileloc == 0: image_file = fits.open(image_name + '.fits') else: image_file = fits.open('./All_Files/' + image_name + '.fits') image_file.info() image_org_wcs = wcs.WCS(image_file[fits_data_slice].header) # Create cut out if fits_cut[0] > 0: image_sci_full = image_file[fits_data_slice].data image_sci_cutout = Cutout2D(image_sci_full, (fits_cut[0], fits_cut[1]), (fits_cut[2] * 2, fits_cut[3] * 2), wcs=image_org_wcs) image_sci = image_sci_cutout.data image_wcs = image_sci_cutout.wcs else: image_sci_full = image_file[fits_data_slice].data image_sci = image_sci_full image_wcs = image_org_wcs # Create rms normalized results (based on region chosen) image_sigma = mad_std(image_sci) image_sci_rmsnorm = image_sci / image_sigma image_sci_full_rmsnorm = image_sci_full / image_sigma # Create arrays for plotting (setting negatives to zero) image_sci_rmsnorm_plot = image_sci_rmsnorm image_sci_full_rmsnorm_plot = image_sci_full_rmsnorm image_sci_rmsnorm_plot[image_sci_rmsnorm_plot < 0] = 0 image_sci_full_rmsnorm_plot[image_sci_full_rmsnorm_plot < 0] = 0 # Create background image bkg = Background2D(image_sci, (7, 7)) # Calculate Error Array image_exptime = float(image_file[0].header['EXPTIME']) print('From Header: Exposure Time = {}'.format(image_exptime)) image_error = calc_total_error(image_sci, bkg.background_rms, image_exptime) # Plot Full File + Rectangle fig, ax = plt.subplots(1, figsize=(20, 20)) plt.imshow(np.sqrt(image_sci_full_rmsnorm), origin='lower', cmap='Greys_r', vmin=sn_vmin, vmax=sn_vmax) plt.plot(fits_cut[0], fits_cut[1], 'rs') rect = patches.Rectangle( (fits_cut[0] - fits_cut[2], fits_cut[1] - fits_cut[3]), fits_cut[2] * 2, fits_cut[3] * 2, linewidth=1, edgecolor='r', facecolor='none') ax.add_patch(rect) plt.title(plot_title) plt.axis([ fits_cut[0] - 1000, fits_cut[0] + 1000, fits_cut[1] - 1000, fits_cut[1] + 1000 ]) plt.savefig(output_name + '01_rms_full.jpg') plt.close() # Plot cut region fig, ax = plt.subplots(1, figsize=(20, 20)) plt.imshow(np.sqrt(image_sci_rmsnorm_plot), origin='lower', cmap='Greys_r', vmin=sn_vmin, vmax=sn_vmax) plt.title(plot_title) plt.savefig(output_name + '02_rms_cut.jpg') plt.close() # Plot cut region (background image) fig, ax = plt.subplots(1, figsize=(20, 20)) plt.imshow(bkg.background, origin='lower', cmap='Greys_r') plt.title(plot_title) plt.savefig(output_name + '03_background.jpg') plt.close() print('Size of Output Array: {}'.format(len(image_sci))) print('{} Pixel Value Range: {:.2e} to {:.2e}'.format( image_name, np.nanmin(image_sci), np.nanmax(image_sci))) print('{} S/N Value Range: {:.2e} to {:.2e}'.format( image_name, np.nanmin(image_sci_rmsnorm), np.nanmax(image_sci_rmsnorm))) return image_sci, image_sigma, image_wcs, image_error, image_exptime
def photometry(header, data, name, RA_bound, DEC_bound, thresh_factor, results_file, im=True): """ Input: the header of a reduced object's .fits file, the image data of the file, the name to be used when creating the segmented image and a csv containing all detected sources, a threshold factor to be used in image segmentation, 2 arrays giving the RA and DEC (in degrees) boundaries on the desired source, the name of the results textfile to which the photometry will be appended, and a boolean indicating whether or not to save the image of the segmentation test (optional; defaultTrue) Output: None Obtains a stack of images in the form of a header and data from a .fits file. Estimates the background photon count of this image. Sets a threshold above which we declare a cluster of pixels to be a source: this threshold is defined as the background + the input thresh_factor*the RMS deviation of the background. Scans the input image and looks for sources which are at least 7 pixels in area and above this threshold. Saves an image of the sources found in the original field to the working directory. Uses a pixel coordinate to WCS coordinate transformation, via a previously known reference pixel (see PESTO_lib.WCS_merge()) to obtain WCS coords of all detected sources. Outputs a .csv containing the properties of all detected sources. A tab-delimited results file is appended to. The number of images used in the stack, the total exposure time of the stack and its error, the timestamp (averaged across all images) and its error are already included. IF a source is found, this script appends the x and y minima of the source's centroid, the pixel area of the source, the photon count, and the error on the photon count. If not, the flag NO SOURCE FOUND is appended to the image. """ import numpy as np import numpy.ma as ma import os from astropy.stats import (SigmaClip, gaussian_fwhm_to_sigma, sigma_clipped_stats) from astropy.convolution import Gaussian2DKernel from astropy.table import Table, Column from astroquery.vizier import Vizier from astropy.coordinates import SkyCoord import astropy.units as u from photutils import Background2D, MedianBackground from photutils.utils import calc_total_error from photutils import detect_sources # perform 20 iterations of sigma clipping where needed sigma_clip = SigmaClip(sigma=3.0, iters=20) # background is estimated as the median of the entire image bkg_estimator = MedianBackground() mask = (data == 0) # mask all pixels where the ADU is 0 bkg = Background2D(data, (50,50), filter_size=(3,3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator, mask=mask) ### find sources using image segmentation # set the threshold for source detection threshold = bkg.background + (thresh_factor*bkg.background_rms) sigma = 3.0*gaussian_fwhm_to_sigma kernel = Gaussian2DKernel(sigma, x_size=3.0, y_size=3.0) kernel.normalize() segm = detect_sources(data, threshold, npixels=7, filter_kernel=kernel) segm.remove_masked_labels(mask) try: segm.remove_border_labels(10, partial_overlap=True, relabel=True) except: print("The background threshold factor is too large; sources are "+ "being ignored during image segmentation.\nPlease try a smaller"+ " value.\n") return # pictures to see what's going on if(im): import matplotlib.pyplot as plt plt.switch_backend('agg') # stop matplotlib from trying to show image from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize norm = ImageNormalize(stretch=SqrtStretch()) # normalize the image fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(90, 90)) # 2 plots # show the (data-background) of the image with a greyscale colourmap # using the above normalization ax1.imshow(data-bkg.background, origin='lower', cmap='Greys_r', norm=norm) ax2.imshow(segm, origin='lower', cmap=segm.cmap(random_state=12345)) plt.savefig('segmentationtest_'+name+'.png') # find source properties (centroid, source pixel area, etc.) from photutils import source_properties from astropy.wcs import WCS # calculate the error on the photon counts # tf = open('/data/irulan/omm_transients/'+results_file,'r') # contents = tf.readlines() # tf.close() # tf_last = contents[len(contents)-1] # tf_data = tf_last.split("\t") # # gain*exposure*stack: # effective_gain = 13.522*(float(tf_data[1])/1000.0)*float(tf_data[0]) effective_gain = 13.522 # compute photon count error : error = calc_total_error(data, bkg.background_rms, effective_gain) cat = source_properties(data-bkg.background, segm, wcs='all_pix2world', error=error) segm_tbl = cat.to_table() # contruct a table of source properties # WCS object w = WCS(header) # get WCS of all sources, add to segm_tbl, and write the table ra, dec = w.all_pix2world(segm_tbl['xcentroid'], segm_tbl['ycentroid'],1) segm_tbl["ra"] = ra segm_tbl["dec"] = dec segm_tbl.write('segmentation_table_'+name+'.csv', format = 'csv', overwrite=True) # build a new table with only the parameters we care about tbl = Table() tbl["id"] = segm_tbl["id"] # id tbl["xcentroid"] = segm_tbl["xcentroid"] # x coord tbl["ycentroid"] = segm_tbl["ycentroid"] # y coord tbl["area"] = segm_tbl["area"] # area in pixels tbl["ra"] = ra # ra tbl["dec"] = dec # dec tbl["pc"] = segm_tbl["source_sum"] # flux tbl["pc_err"] = segm_tbl["source_sum_err"] # error on flux tbl["mag_fit"] = -2.5*np.log10(tbl["pc"]) # instrumental magnitude tbl["mag_fit_unc"] = 2.5/(tbl["pc"]*np.log(10)) # error on magnitude ### query Vizier to match sources and do aperture photometry # set the catalogue and filter of the image ref_catalog = "II/349/ps1" ref_catalog_name = "PS1" # PanStarrs 1 filt = header["filtre"][0] # get the centre of the image and its RA, Dec x_size = data.shape[1] y_size = data.shape[0] ra_centre, dec_centre = np.array(w.all_pix2world(x_size/2.0, y_size/2.0, 1)) # set radius to search in, minimum, maximum magnitudes minmag = 10.0 maxmag = 22.0 max_emag = 0.3 # maximum error on magnitude pixscale = np.mean(np.abs([header["CDELT1"], header["CDELT2"]])) # in deg pixscale = pixscale*3600.0 # in arcsec radius = pixscale*y_size/60.0 # radius in arcmin # query print statement #print('Querying Vizier %s around RA %.4f, Dec %.4f with a radius of %.4f arcmin\n'%( # ref_catalog, ra_centre, dec_centre, radius)) # querying v = Vizier(columns=["*"], column_filters={ filt+"mag":str(minmag)+".."+str(maxmag), "e_"+filt+"mag":"<"+str(max_emag)}, row_limit=-1) # no row limit Q = v.query_region(SkyCoord(ra=ra_centre, dec=dec_centre, unit = (u.deg, u.deg)), radius = str(radius)+'m', catalog=ref_catalog, cache=False) cat_coords = w.all_world2pix(Q[0]['RAJ2000'], Q[0]['DEJ2000'], 1) # mask out edge sources x_lims = [int(0.05*x_size), int(0.95*x_size)] y_lims = [int(0.05*y_size), int(0.95*y_size)] mask = (cat_coords[0] > x_lims[0]) & ( cat_coords[0] < x_lims[1]) & ( cat_coords[1] > y_lims[0]) & ( cat_coords[1] < y_lims[1]) good_cat_sources = Q[0][mask] # sources in catalogue # cross-matching coords of sources found by astrometry source_coords = SkyCoord(ra=tbl['ra'], dec=tbl['dec'], frame='fk5', unit='degree') # and coords of valid sources in the queried catalog cat_source_coords = SkyCoord(ra=good_cat_sources['RAJ2000'], dec=good_cat_sources['DEJ2000'], frame='fk5', unit='degree') # indices of matching sources (within 5.0 pix of each other) idx_image, idx_cat, d2d, d3d = cat_source_coords.search_around_sky( source_coords, 5.0*pixscale*u.arcsec) # compute magnitude offsets and zero point mag_offsets = ma.array(good_cat_sources[filt+'mag'][idx_cat] - tbl['mag_fit'][idx_image]) zp_mean, zp_med, zp_std = sigma_clipped_stats(mag_offsets) # zero point mag_calib = tbl['mag_fit'] + zp_mean # compute magnitudes mag_calib.name = 'mag_calib' mag_calib_unc = np.sqrt(tbl['mag_fit_unc']**2 + zp_std**2) # propagate errs mag_calib_unc.name = 'mag_calib_unc' tbl['mag_calib'] = mag_calib tbl['mag_calib_unc'] = mag_calib_unc #print(zp_mean) #print(zp_std) # add flag indicating if source is in catalog #in_cat = [] #for i in range(len(tbl)): # if i in idx_image: # in_cat.append(True) # else: # in_cat.append(False) #in_cat_col = Column(data=in_cat, name="in "+ref_catalog_name) #tbl["in "+ref_catalog_name] = in_cat_col #return tbl # boundaries on the desired source RA_min, RA_max = RA_bound DEC_min, DEC_max = DEC_bound # parse a list of all sources for a source within the RA, Dec bounds cwd = os.getcwd() # current working dir for i in range(len(tbl['id'])): # if source is found: if (RA_min <= tbl[i]['ra'] <= RA_max) and ( DEC_min <= tbl[i]['dec'] <= DEC_max): print("\nFound a source.\n") # Write xcentroid and ycentroid, the pixel area of the source, # the photon count, photon count error, calibrated magnitude, # calibrated magnitude error, and filter used to the file. # If xcentroid and ycentroid change drastically from one stack to # another, the sources are not the same or astrometric calibration # may have failed. xcentroid = tbl["xcentroid"].data[i] ycentroid = tbl["ycentroid"].data[i] area = tbl["area"].data[i] pc = tbl["pc"].data[i] pc_err = tbl["pc_err"].data[i] mag = tbl["mag_calib"].data[i] mag_err = tbl["mag_calib_unc"][i] # line to write to the file: line = str(xcentroid)+"\t"+str(ycentroid)+"\t"+str(area) line += "\t"+str(pc)+"\t"+str(pc_err) line += "\t"+str(mag)+"\t"+str(mag_err)+"\t"+filt+"\n" tf = open(cwd+"/"+results_file,'a') tf.write(line) tf.close() return tbl # if no source is found: line = "NO SOURCE FOUND.\n" print("No source found.\n") tf = open(cwd+"/"+results_file,'a') tf.write(line) tf.close() return tbl
def phot(images, positions=None, aperture_radius=6.0, annulus=8.0, dannulus=10.0, method='exact', bg_method='median', bg_box_size=50, gain=1.1, output=None): """ Basic circular aperture photometry of given images. Parameters ---------- images : generator or list of 'ccdproc.CCDData' Images to be combined. positions : list The positions should be either a single tuple of (x, y), a list of (x, y) tuples, or an array with shape Nx2, where N is the number of positions. Default is 'None'. aperture_radius : list or float The aperture radius of a source. Default is '3.0'. annulus : float The circular inner annulus aperture radius of a source. Default is '6.0'. dannulus : float The circular outer annulus aperture radius of a source. Default is '8.0'. method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. bg_method: {'mean', 'median'}, optional The statistic used to calculate the background. All measurements are sigma clipped. bg_box_size: int Selecting the box size requires some care by the user. The box size should generally be larger than the typical size of sources in the image, but small enough to encapsulate any background variations. For best results, the box size should also be chosen so that the data are covered by an integer number of boxes in both dimensions. More information: https://github.com/Gabriel-p/photpy/blob/master/IRAF_compare/aperphot.py gain: float CCD gain value. output : None or str If it is None, function returns just Table. If it is 'str', function creates photometry result file. Returns ------- 'A table object or file' Examples -------- >>> from tuglib.io import FitsCollection >>> from tuglib.analysis import phot >>> location = '/Users/ykilic/Desktop/data/' >>> images = FitsCollection(location, file_extension="fit") >>> fits_images = images.ccds(IMAGETYP='object') >>> phot(fits_images, positions=[(1400.65, 1545.78)], aperture_radius=10.0, annulus=12.0, dannulus=15) """ if not isinstance(images, (list, types.GeneratorType)): raise TypeError("'images' should be a 'ccdproc.CCDData' object.") if positions is not None: if not isinstance(positions, (list, type(None))): raise TypeError("'positions' should be 'list' or 'None' object.") if not isinstance(aperture_radius, (list, float, int)): raise TypeError("'radius' should be a 'list' or 'float' object.") if not isinstance(annulus, (float, int)): raise TypeError("'annulus' should be a 'int' or 'float' object.") if not isinstance(dannulus, (float, int)): raise TypeError("'dannulus' should be a 'int' or 'float' object.") if not isinstance(method, str): raise TypeError("'method' should be a 'str' object.") if not isinstance(bg_method, str): raise TypeError("'bg_method' should be a 'str' object.") if not isinstance(bg_box_size, int): raise TypeError("'bg_box_size' should be a 'int' or 'float' object.") if not isinstance(gain, (float, int)): raise TypeError("'gain' should be a 'float' object.") if not isinstance(output, (type(None), type(str))): raise TypeError("'output' should be 'None' or 'str' objects.") if isinstance(images, types.GeneratorType): try: ccds = list(images) except IndexError: return None else: ccds = images ccds_phot_list = list() for ccd in ccds: exptime = ccd.meta['EXPTIME'] box_xy = (bg_box_size, bg_box_size) sigma_clip = SigmaClip(sigma=3.) bkg_estimator = MedianBackground() bkg = Background2D(ccd, box_xy, filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) ccd.uncertainty = StdDevUncertainty( calc_total_error(ccd.data, bkg.background, gain)) aperture = CircularAperture(positions, r=aperture_radius) annulus_aperture = CircularAnnulus(positions, r_in=annulus, r_out=dannulus) annulus_masks = annulus_aperture.to_mask(method=method) bkg_phot = [] for mask in annulus_masks: annulus_data = mask.multiply(ccd) annulus_data_1d = annulus_data[mask.data > 0] mean_sigclip, median_sigclip, stddev_sigclip = sigma_clipped_stats( annulus_data_1d) if bg_method == "mean": sigclip = mean_sigclip elif bg_method == "median": sigclip = median_sigclip else: sigclip = median_sigclip bkg_phot.append(sigclip) bkg_phot = np.array(sigclip) phot_table = aperture_photometry(ccd, aperture) phot_table['annulus_{}'.format(bg_method)] = bkg_phot * u.adu phot_table['aper_bkg'] = bkg_phot * aperture.area * u.adu phot_table['residual_aperture_sum'] = phot_table[ 'aperture_sum'] - phot_table['aper_bkg'] phot_table['flux'] = phot_table['residual_aperture_sum'] / float( exptime) phot_table['flux'].info.format = '%.3f' phot_table['JD'] = float(ccd.meta['JD']) phot_table['JD'].info.format = '%.8f' phot_table['JD'].unit = 'd' phot_table['aperture_sum'].info.format = '%.3f' phot_table['aperture_sum_err'].info.format = '%.3f' phot_table['annulus_{}'.format(bg_method)].info.format = '%.3f' phot_table['aper_bkg'].info.format = '%.3f' phot_table['residual_aperture_sum'].info.format = '%.3f' phot_table = flux2mag(phot_table) phot_table['mag_err'] = 1.0857 * ( phot_table['aperture_sum_err'] / phot_table['residual_aperture_sum']) * u.mag phot_table['mag_err'].info.format = '%.3f' ccds_phot_list.append(phot_table) return vstack(ccds_phot_list)
def A_photometry(bg_err, cx=15, cx_med=15, cy=15, cy_med=15, r=[2.5], a=[5], b=[5], w_r=[5], h_r=[5], theta=[0], scale=1, shape='Circular', methods=['center', 'exact'], moveCentroids=[True], i=0, img_data=None): """Performs aperture photometry, first by creating the aperture then summing the flux within the aperture. Note that this will implicitly use the global variable image_stack (3D array) to allow for parallel computing with large (many GB) datasets. Args: bg_err (1D array): Array of uncertainties on pixel value. cx (int/array, optional): x-coordinate(s) of the center of the aperture. Default is 15. cy (int/array, optional): y-coordinate(s) of the center of the aperture. Default is 15. r (int/array, optional): If shape is 'Circular', the radii to try for aperture photometry in units of pixels. Default is just 2.5 pixels. a (int/array, optional): If shape is 'Elliptical', the semi-major axes to try for elliptical aperture in units of pixels. Default is 5. b (int/array, optional): If shape is 'Elliptical', the semi-minor axes to try for elliptical aperture in units of pixels. Default is 5. w_r (int/array, optional): If shape is 'Rectangular', the full widths to try for rectangular aperture (x-axis). Default is 5. h_r (int/array, optional): If shape is 'Rectangular', the full heights to try for rectangular aperture (y-axis). Default is 5. theta (int/array, optional): If shape is 'Elliptical' or 'Rectangular', the rotation angles in radians of the semimajor axis from the positive x axis. The rotation angle increases counterclockwise. Default is 0. scale (int, optional): If the image is oversampled, scaling factor for centroid and bounds, i.e, give centroid in terms of the pixel value of the initial image. shape (string, optional): shape is the shape of the aperture. Possible aperture shapes are 'Circular', 'Elliptical', 'Rectangular'. Default is 'Circular'. methods (iterable, optional): The methods used to determine the overlap of the aperture on the pixel grid. Possible methods are 'exact', 'subpixel', 'center'. Default is ['center', 'exact']. i (int, optional): The current frame number being examined. img_data (3D array): The image stack being analyzed if not using the global variable to allow for parallel computing. Returns: tuple: results (2D array) Array of flux and flux errors, of shape (nMethods*nSizes, 2), where the nSizes loop is nested inside the nMethods loop which is itself nested inside the moveCentoids loop. """ # Access the global variable if img_data is None: global image_stack else: image_stack = img_data if not isinstance(r, Iterable): r = [r] if not isinstance(cx, Iterable): cx = [cx] if not isinstance(cy, Iterable): cy = [cy] if not isinstance(a, Iterable): a = [a] if not isinstance(b, Iterable): b = [b] if not isinstance(w_r, Iterable): w_r = [w_r] if not isinstance(h_r, Iterable): h_r = [h_r] if not isinstance(theta, Iterable): theta = [theta] if not isinstance(methods, Iterable): methods = [methods] data_error = calc_total_error(image_stack[i, :, :], bg_err[i], effective_gain=1) results = [] for moveCentroid in moveCentroids: # Set up aperture(s) apertures = [] if not moveCentroid: position = [cx_med * scale, cy_med * scale] else: position = [cx[i] * scale, cy[i] * scale] if (shape == 'Circular'): for j in range(len(r)): apertures.append(CircularAperture(position, r=r[j] * scale)) elif (shape == 'Elliptical'): for j in range(len(a)): apertures.append( EllipticalAperture(position, a=a[j] * scale, b=b[j] * scale, theta=theta[j])) elif (shape == 'Rectangular'): for j in range(len(w_r)): apertures.append( RectangularAperture(position, w=w_r[j] * scale, h=h_r[j] * scale, theta=theta[j])) for method in methods: phot_table = aperture_photometry(image_stack[i, :, :], apertures, error=data_error, method=method) results.extend([ float(phot_table[f'aperture_sum_{j}']) for j in range(len(apertures)) ]) return np.array(results)
else: record_phot = False skip_phot = False else: record_phot = False skip_phot = False if skip_phot: logging.shutdown() sys.exit(-1) sigma_clip = SigmaClip(sigma=3) bkg_estimator = MedianBackground() bkg = Background2D(sci_med, (120, 120), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) error = calc_total_error(sci_med, bkg.background_rms, 1) fwhm = np.float(input('FWHM of stars? ')) log.info('Using FWHM of ' + str(fwhm)) apertures = CircularAperture(positions, r=fwhm * 2.5) annulus_apertures = CircularAnnulus(positions, r_in=fwhm * 3, r_out=fwhm * 4) annulus_masks = annulus_apertures.to_mask(method='center') bkg_median = [] for mask in annulus_masks: annulus_data = mask.multiply(sci_med) annulus_data_1d = annulus_data[mask.data > 0] _, median_sigclip, stddev = sigma_clipped_stats(annulus_data_1d) bkg_median.append(median_sigclip) bkg_median = np.array(bkg_median) phot = aperture_photometry(sci_med, apertures, error=error)
def extract_photometry(filename, approx_location, centering_width=80, ap_rad=7.0, in_rad=8.0, out_rad=15.0): """ Does aperture photometry on the reduced image in filename, at the location specified by approx_location Parameters ---------- filename : str Name of the reduced image. approx_location : `~astropy.coordinates.SkyCoord` `astropy.coordinates.SkyCoord` with the RA and Dec of the object you want to extract. Passed to generate_regions. centering_width : int, optional Size of box around source region to find the centroid of in pixels. Passed to generate_regions. ap_rad : float, optional Radius of source region in arcseconds. Passed to generate_regions. in_rad : float, optional Inner radius of background annulus in arcseconds. Passed to generate_regions. out_rad : float, optional Outer radius of background annulus in arcseconds. Passed to generate_regions. Returns ------- phot_table : `~astropy.table.Table` A table containing the photometry information. This is the original filename, the time of the start of the observation, the exposure time, the time associated with the data point, the filter, the center of the extraction region, the radius of the source region, the area of the source region in pixels, the source counts, the source count error, the center of the background region, the inner radius of the background region, the outer radius of the background region, the area of the background region, the background counts, the background count error, then the net counts, net count error, and the instrumental magnitude and instrumental magnitude error. """ #Open up the file hdu = fits.open(filename)[0] #Let's do some bookkeeping #Get the TAI time from the header, convert to an astropy.time.Time object time_header = hdu.header['DATE-OBS'] obs_time_obj = Time(time_header, scale='tai') #Get the actual value of the MJD out of the astropy object obs_time = obs_time_obj.mjd exp_time = hdu.header['EXPTIME'] * u.second time_obj = obs_time_obj + (exp_time / 2.0) #Get the value of the MJD out of the astropy object again. time = time_obj.mjd #Get the name of the filter filt = hdu.header['FILTER'] #Now for some actual photometry #Give me the data! data = hdu.data #Major source of error in the image assumed to be readout noise and photon shot noise. #The latter is handled by calc_total_error. The former is just an array the same size #as the data, filled with the read out noise. ron = hdu.header['GTRON11'] error_arr = np.full(data.shape, ron) #Calculate the total error array from the data + gain (Poisson noise) and readout noise. gain = hdu.header['GTGAIN11'] error = calc_total_error(data, error_arr, gain) #Generate source regions src, bkg = generate_regions(hdu, approx_location, centering_width, ap_rad, in_rad, out_rad) #grab centers of source and background regions src_center = (src.positions.ra.value, src.positions.dec.value) bkg_center = (bkg.positions.ra.value, bkg.positions.dec.value) apers = [src, bkg] #Do some aperture photometry! phot_table = aperture_photometry(hdu, apers, error=error) #Calculate grab counts src_cts = phot_table['aperture_sum_0'].data[0] src_cts_err = phot_table['aperture_sum_err_0'].data[0] bkg_cts = phot_table['aperture_sum_1'].data[0] bkg_cts_err = phot_table['aperture_sum_err_1'].data[0] #We need source and background region areas, convert sky regions to pix regions wcs = WCS(hdu) src_pix = src.to_pixel(wcs) bkg_pix = bkg.to_pixel(wcs) #Calculate region areas src_area = src_pix.area() bkg_area = bkg_pix.area() #Scale the background counts bkg_scaled = bkg_cts * (src_area / bkg_area) bkg_scaled_err = bkg_cts_err * (src_area / bkg_area) #Net flux = Source - Bkg net_cts = src_cts - bkg_scaled net_cts_err = np.sqrt(src_cts_err**2.0 + bkg_scaled_err**2.0) inst_mag = -2.5 * np.log10(net_cts / exp_time.value) inst_mag_err = 2.5 * net_cts_err / (net_cts * np.log(10.0)) if type(bkg_area[0]) == float: print(type(bkg_area)) out_table = Table( [[filename], [obs_time], [exp_time.value], [time], [filt], [src_center], [ap_rad], [src_area], [src_cts], [src_cts_err], [bkg_center], [in_rad], [out_rad], [bkg_area], [bkg_cts], [bkg_cts_err], [net_cts[0]], [net_cts_err[0]], [inst_mag[0]], [inst_mag_err[0]]], names=[ 'Filename', 'Obs_start', 'Exptime', 'Time', 'Filter', 'Src_center', 'Src_rad', 'Src_area', 'Src_cts', 'Src_cts_err', 'Bkg_center', 'Bkg_in_rad', 'Bkg_out_rad', 'Bkg_area', 'Bkg_cts', 'Bkg_cts_err', 'Net_cts', 'Net_cts_err', 'Inst_mag', 'Inst_mag_err' ]) return out_table out_table = Table([[filename], [obs_time], [exp_time.value], [time], [filt], [src_center], [ap_rad], [src_area], [src_cts], [src_cts_err], [bkg_center], [in_rad], [out_rad], [bkg_area[0]], [bkg_cts], [bkg_cts_err], [net_cts[0]], [net_cts_err[0]], [inst_mag[0]], [inst_mag_err[0]]], names=[ 'Filename', 'Obs_start', 'Exptime', 'Time', 'Filter', 'Src_center', 'Src_rad', 'Src_area', 'Src_cts', 'Src_cts_err', 'Bkg_center', 'Bkg_in_rad', 'Bkg_out_rad', 'Bkg_area', 'Bkg_cts', 'Bkg_cts_err', 'Net_cts', 'Net_cts_err', 'Inst_mag', 'Inst_mag_err' ]) print(type(bkg_area)) return out_table
def A_photometry(image_data, bg_err, factor=1, ape_sum=None, ape_sum_err=None, cx=15, cy=15, r=2.5, a=5, b=5, w_r=5, h_r=5, theta=0, shape='Circular', method='center'): """ Performs aperture photometry, first by creating the aperture (Circular, Rectangular or Elliptical), then it sums up the flux that falls into the aperture. Args: image_data (3D array): Data cube of images (2D arrays of pixel values). bg_err (1D array): Array of uncertainties on pixel value. factor (float, optional): Electron count to photon count factor. Default is 1 if none given. ape_sum (1D array, optional): Array of flux to append new flux values to. If None, the new values will be appended to an empty array ape_sum_err (1D array, optional): Array of flux uncertainty to append new flux uncertainty values to. If None, the new values will be appended to an empty array. cx (int, optional): x-coordinate of the center of the aperture. Default is 15. cy (int, optional): y-coordinate of the center of the aperture. Default is 15. r (int, optional): If shape is 'Circular', r is the radius for the circular aperture. Default is 2.5. a (int, optional): If shape is 'Elliptical', a is the semi-major axis for elliptical aperture (x-axis). Default is 5. b (int, optional): If shape is 'Elliptical', b is the semi-major axis for elliptical aperture (y-axis). Default is 5. w_r (int, optional): If shape is 'Rectangular', w_r is the full width for rectangular aperture (x-axis). Default is 5. h_r (int, optional): If shape is 'Rectangular', h_r is the full height for rectangular aperture (y-axis). Default is 5. theta (int, optional): If shape is 'Elliptical' or 'Rectangular', theta is the angle of the rotation angle in radians of the semimajor axis from the positive x axis. The rotation angle increases counterclockwise. Default is 0. shape (string, optional): shape is the shape of the aperture. Possible aperture shapes are 'Circular', 'Elliptical', 'Rectangular'. Default is 'Circular'. method (string, optional): The method used to determine the overlap of the aperture on the pixel grid. Possible methods are 'exact', 'subpixel', 'center'. Default is 'center'. Returns: tuple: ape_sum (1D array) Array of flux with new flux appended. ape_sum_err (1D array) Array of flux uncertainties with new flux uncertainties appended. """ if ape_sum is None: ape_sum = [] if ape_sum_err is None: ape_sum_err = [] l, h, w = image_data.shape tmp_sum = [] tmp_err = [] movingCentroid = (isinstance(cx, Iterable) or isinstance(cy, Iterable)) if not movingCentroid: position = [cx, cy] if (shape == 'Circular'): aperture = CircularAperture(position, r=r) elif (shape == 'Elliptical'): aperture = EllipticalAperture(position, a=a, b=b, theta=theta) elif (shape == 'Rectangular'): aperture = RectangularAperture(position, w=w_r, h=h_r, theta=theta) for i in range(l): if movingCentroid: position = [cx[i], cy[i]] if (shape == 'Circular'): aperture = CircularAperture(position, r=r) elif (shape == 'Elliptical'): aperture = EllipticalAperture(position, a=a, b=b, theta=theta) elif (shape == 'Rectangular'): aperture = RectangularAperture(position, w=w_r, h=h_r, theta=theta) data_error = calc_total_error(image_data[i, :, :], bg_err[i], effective_gain=1) phot_table = aperture_photometry( image_data[i, :, :], aperture, error=data_error, method=method) #, pixelwise_error=False) tmp_sum.extend(phot_table['aperture_sum'] * factor) tmp_err.extend(phot_table['aperture_sum_err'] * factor) # removing outliers tmp_sum = sigma_clip(tmp_sum, sigma=4, maxiters=2, cenfunc=np.ma.median) tmp_err = sigma_clip(tmp_err, sigma=4, maxiters=2, cenfunc=np.ma.median) ape_sum.extend(tmp_sum) ape_sum_err.extend(tmp_err) return ape_sum, ape_sum_err
def A_photometry(image_data, bg_err, factor=1, ape_sum=[], ape_sum_err=[], cx=15, cy=15, r=2.5, a=5, b=5, w_r=5, h_r=5, theta=0, shape='Circular', method='center'): ''' Performs aperture photometry, first by creating the aperture (Circular, Rectangular or Elliptical), then it sums up the flux that falls into the aperture. Parameters ========== image_data: 3D array Data cube of images (2D arrays of pixel values). bg_err : 1D array Array of uncertainties on pixel value. factor : float (optional) Electron count to photon count factor. Default is 1 if none given. ape_sum : 1D array (optional) Array of flux to append new flux values to. If 'None', the new values will be appended to an empty array ape_sum_err: 1D array (optional) Array of flux uncertainty to append new flux uncertainty values to. If 'None', the new values will be appended to an empty array. cx : float or 1D array (optional) x-coordinate of the center of the aperture. Dimension must be equal to dimension of cy. Default is 15. cy : float or 1D array (optional) y-coordinate of the center of the aperture. Default is 15. r : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Circular', c_radius is the radius for the circular aperture. Default is 2.5. a : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Elliptical', e_semix is the semi-major axis for elliptical aperture (x-axis). Default is 5. b : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Elliptical', e_semiy is the semi-major axis for elliptical aperture (y-axis). Default is 5. w_r : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Rectangular', r_widthx is the full width for rectangular aperture (x-axis). Default is 5. h_r : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Rectangular', r_widthy is the full height for rectangular aperture (y-axis). Default is 5. theta : int (optional) If phot_meth is 'Aperture' and ap_shape is 'Elliptical' or 'Rectangular', theta is the angle of the rotation angle in radians of the semimajor axis from the positive x axis. The rotation angle increases counterclockwise. Default is 0. shape : string object (optional) If phot_meth is 'Aperture', ap_shape is the shape of the aperture. Possible aperture shapes are 'Circular', 'Elliptical', 'Rectangular'. Default is 'Circular'. method : string object (optional) If phot_meth is 'Aperture', apemethod is the method used to determine the overlap of the aperture on the pixel grid. Possible methods are 'exact', 'subpixel', 'center'. Default is 'exact'. Returns ------- ape_sum : 1D array Array of flux with new flux appended. ape_sum_err: 1D array Array of flux uncertainties with new flux uncertainties appended. ''' l, h, w = image_data.shape # central position of aperture #position = np.c_[cx, cy] # remove when uncommenting below if (type(cx) is list): position = np.c_[cx, cy] else: position = np.c_[(cx * np.ones(l)), (cy * np.ones(l))] tmp_sum = [] tmp_err = [] # performing aperture photometry for i in range(l): #aperture = CircularAperture(position[i], r=r) # remove when uncommenting below if (shape == 'Circular'): aperture = CircularAperture(position[i], r=r) elif (shape == 'Elliptical'): aperture = EllipticalAperture(position[i], a=a, b=b, theta=theta) elif (shape == 'Rectangular'): aperture = RectangularAperture(position[i], w=w_r, h=h_r, theta=theta) data_error = calc_total_error(image_data[i, :, :], bg_err[i], effective_gain=1) phot_table = aperture_photometry(image_data[i, :, :], aperture, error=data_error, pixelwise_error=False, method=method) tmp_sum.extend(phot_table['aperture_sum'] * factor) tmp_err.extend(phot_table['aperture_sum_err'] * factor) # removing outliers tmp_sum = sigma_clip(tmp_sum, sigma=4, iters=2, cenfunc=np.ma.median) tmp_err = sigma_clip(tmp_err, sigma=4, iters=2, cenfunc=np.ma.median) ape_sum.extend(tmp_sum) ape_sum_err.extend(tmp_err) return ape_sum, ape_sum_err
def single_star_photometry(img, xc, yc, bkg, filename, images_info, upper_limit=False): shortfile = filename.split('.fits')[0] ttexp = images_info.loc[0, 'EXPTIME'] print('exptime = ' + str(ttexp)) nanmask = pd.isnull(img) ap = 0.4 / images_info.loc[0, 'PIXSCALE'] filt = images_info.loc[images_info['FILENAME'] == filename, 'FILTER'].values[0] vegazpt = config.VEGA_ZEROPTS[filt] pos = [float(xc), float(yc)] apertures = CircularAperture(pos, r=ap) sigma_clip = SigmaClip(sigma=3.) bkg_estimator = MedianBackground() bkgimg = Background2D(img, (20, 20), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) error = calc_total_error( img, bkgimg.background, effective_gain=images_info.loc[images_info['FILENAME'] == filename, 'CCDGAIN'].values[0]) # error = calc_total_error(img, bkgimg.background, effective_gain=config.GAIN[instrument]) #For sky annuli background #sky_inner = 0.8/images_info.loc[0,'PIXSCALE'] #sky_outer = 0.9/images_info.loc[0,'PIXSCALE'] #skyannuli = CircularAnnulus(pos, r_in=sky_inner, r_out=sky_outer) #phot_apers = [apertures, skyannuli] #phot_table2 = Table(aperture_photometry(img, phot_apers, method='exact', error=error, mask=nanmask)).to_pandas() #For multiapertures background phot = aperture_photometry(img, apertures, method='exact', mask=nanmask) #error=error, phot = Table(phot).to_pandas() phot = phot.rename(columns={'aperture_sum': 'APERTURE_FLUX' }) #,'aperture_sum_err':'APERTURE_FLUX_ERR' phot['BKG_FLUX'] = bkg[0] phot['BKG_FLUX_ERR'] = bkg[1] phot['APERTURE_FLUX_ERR'] = np.sqrt(phot['APERTURE_FLUX'] / ttexp + 2 * phot['BKG_FLUX_ERR']**2) phot = phot.apply(pd.to_numeric, errors='ignore') if upper_limit == True: print('this is true') print('background flux error = ' + str(bkg[1])) phot['STAR_FLUX'] = 3 * bkg[1] #3 sigma = 3 * 1.4826 * MAD since distribution is Gaussian - No more MAD, just STD phot['STAR_FLUX_ERR'] = np.nan else: #no need to divide over area because aperture areas for both star flux and backgroun flux are the same phot['STAR_FLUX'] = phot['APERTURE_FLUX'] - phot['BKG_FLUX'] phot['STAR_FLUX_ERR'] = np.sqrt(phot['APERTURE_FLUX_ERR']**2 + phot['BKG_FLUX_ERR']**2) #bkg_mean = phot_table2['aperture_sum_1'] / skyannuli.area #bkg_starap_sum = bkg_mean * apertures.area #final_sum = phot_table2['aperture_sum_0']-bkg_starap_sum #phot_table2['bg_subtracted_star_counts'] = final_sum #bkg_mean_err = phot_table2['aperture_sum_err_1'] / skyannuli.area #bkg_sum_err = bkg_mean_err * apertures.area #phot_table2['bg_sub_star_cts_err'] = np.sqrt((phot_table2['aperture_sum_err_0']**2)+(bkg_sum_err**2)) #phot['STAR_FLUX_ANNULI'] = final_sum #phot['STAR_FLUX_ERR_ANNULI'] = phot_table2['bg_sub_star_cts_err'] phot['VEGAMAG'] = vegazpt - 2.5 * np.log10(phot['STAR_FLUX']) phot['VEGAMAG_UNC'] = 1.0857 * phot['STAR_FLUX_ERR'] / phot['STAR_FLUX'] #phot['VEGAMAG_UNC_APPHOT_ONLY'] = 1.0857 * phot['APERTURE_FLUX_ERR'] / phot['STAR_FLUX'] #phot['VEGAMAG_ANNULI'] = vegazpt - 2.5 * np.log10(phot['STAR_FLUX_ANNULI']) #phot['VEGAMAG_UNC_ANNULI'] = 1.0857 * phot['STAR_FLUX_ERR_ANNULI'] / phot['STAR_FLUX_ANNULI'] phot.to_csv('phot_table_' + str(shortfile) + '.csv') return phot
def reduce(fitsfile, outfile, makeplots=False): from photutils import (Background2D, DAOStarFinder, CircularAperture, aperture_photometry, MedianBackground, make_source_mask, SigmaClip) from photutils.utils import calc_total_error from astropy.stats import sigma_clipped_stats # background variables bkg_boxsize = 100 bkg_filter_size = (11, 11) bkg_edge_method = 'pad' bkg_estimator = MedianBackground() # sigma clipped stats variables sig = 5. iters = 5 # star finder variables fwhm = 1.5 # threshold = 5. * std # aperture radius (in pixels) # plate scale ~0.034 × 0.030"/pixel # infinate aperture is 4" # 15 pixels ~ 0.45" apr_rad = 5 # load data and header hdu = fits.open(fitsfile) data = hdu[0].data w = wcs.WCS(hdu[0].header) nanmask = np.isnan(data) # subtract background bkg = Background2D(data, bkg_boxsize, filter_size=bkg_filter_size, edge_method=bkg_edge_method, bkg_estimator=bkg_estimator, mask=nanmask) data_sub = data - bkg.background # calculate background stats mean, median, std = sigma_clipped_stats(data_sub, sigma=sig, iters=iters) threshold = 5. * std daofind = DAOStarFinder(fwhm=fwhm, threshold=threshold, exclude_border=True) sources = daofind(data_sub) positions = (sources['xcentroid'], sources['ycentroid']) # apertures = CircularAperture(positions, r=apr_rad) effective_gain = hdu[0].header['EXPTIME'] error = calc_total_error(data_sub, bkg.background, effective_gain) phot_table = aperture_photometry(data_sub, apertures, error=error, mask=nanmask) vegamag, vegamagerr = calcmag(phot_table['aperture_sum'], phot_table['aperture_sum_err']) ra, dec = w.all_pix2world(phot_table['xcenter'], phot_table['ycenter'], 1) phot_table['ra'] = ra phot_table['dec'] = dec phot_table['vegamag'] = vegamag phot_table['vegamagerr'] = vegamagerr fmts = {c: '%.6f' for c in ['xcenter', 'ycenter', 'aperture_sum', 'aperture_sum_err', 'ra', 'dec', 'vegamag', 'vegamagerr']} phot_table.write(outfile, format='ascii.commented_header', formats=fmts, overwrite=True) if makeplots: outfig = outfile + '.pdf' axs = photplots(data, bkg.background, bkg.background_rms, data_sub, fitsfile, outfig) apertures.plot(color='r', lw=1.5, alpha=0.5, ax=axs[0, 1]) plt.savefig(outfig) return vegamag, vegamagerr, ra, dec
def photometry(fileid): """ Run photometry. .. codeauthor:: Rasmus Handberg <*****@*****.**> """ # Settings: ref_mag_limit = 17 # Lower limit on reference target brightness ref_target_dist_limit = 30 # Reference star must be further than this away to be included logger = logging.getLogger(__name__) tic = default_timer() # Use local copy of archive if configured to do so: config = load_config() # Get datafile dict from API: datafile = api.get_datafile(fileid) logger.debug("Datafile: %s", datafile) targetid = datafile['targetid'] photfilter = datafile['photfilter'] archive_local = config.get('photometry', 'archive_local', fallback=None) if archive_local is not None: datafile['archive_path'] = archive_local if not os.path.isdir(datafile['archive_path']): raise FileNotFoundError("ARCHIVE is not available") # Get the catalog containing the target and reference stars: # TODO: Include proper-motion to the time of observation catalog = api.get_catalog(targetid, output='table') target = catalog['target'][0] # Extract information about target: target_name = str(target['target_name']) target_coord = coords.SkyCoord(ra=target['ra'], dec=target['decl'], unit='deg', frame='icrs') # Folder to save output: # TODO: Change this! output_folder_root = config.get('photometry', 'output', fallback='.') output_folder = os.path.join(output_folder_root, target_name, '%04d' % fileid) os.makedirs(output_folder, exist_ok=True) # Also write any logging output to the formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') _filehandler = logging.FileHandler(os.path.join(output_folder, 'photometry.log'), mode='w') _filehandler.setFormatter(formatter) _filehandler.setLevel(logging.INFO) logger.addHandler(_filehandler) # The paths to the science image: filepath = os.path.join(datafile['archive_path'], datafile['path']) # TODO: Download datafile using API to local drive: # TODO: Is this a security concern? #if archive_local: # api.download_datafile(datafile, archive_local) # Translate photometric filter into table column: if photfilter == 'gp': ref_filter = 'g_mag' elif photfilter == 'rp': ref_filter = 'r_mag' elif photfilter == 'ip': ref_filter = 'i_mag' elif photfilter == 'zp': ref_filter = 'z_mag' elif photfilter == 'B': ref_filter = 'B_mag' elif photfilter == 'V': ref_filter = 'V_mag' else: logger.warning( "Could not find filter '%s' in catalogs. Using default gp filter.", photfilter) ref_filter = 'g_mag' references = catalog['references'] references.sort(ref_filter) # Load the image from the FITS file: image = load_image(filepath) # Calculate pixel-coordinates of references: row_col_coords = image.wcs.all_world2pix( np.array([[ref['ra'], ref['decl']] for ref in references]), 0) references['pixel_column'] = row_col_coords[:, 0] references['pixel_row'] = row_col_coords[:, 1] # Calculate the targets position in the image: target_pixel_pos = image.wcs.all_world2pix( [[target['ra'], target['decl']]], 0)[0] # Clean out the references: hsize = 10 x = references['pixel_column'] y = references['pixel_row'] references = references[ (np.sqrt((x - target_pixel_pos[0])**2 + (y - target_pixel_pos[1])**2) > ref_target_dist_limit) & (references[ref_filter] < ref_mag_limit) & (x > hsize) & (x < (image.shape[1] - 1 - hsize)) & (y > hsize) & (y < (image.shape[0] - 1 - hsize))] #============================================================================================== # BARYCENTRIC CORRECTION OF TIME #============================================================================================== ltt_bary = image.obstime.light_travel_time(target_coord, ephemeris='jpl') image.obstime = image.obstime.tdb + ltt_bary #============================================================================================== # BACKGROUND ESITMATION #============================================================================================== fig, ax = plt.subplots(1, 2, figsize=(20, 18)) plot_image(image.clean, ax=ax[0], scale='log', cbar='right', title='Image') plot_image(image.mask, ax=ax[1], scale='linear', cbar='right', title='Mask') fig.savefig(os.path.join(output_folder, 'original.png'), bbox_inches='tight') plt.close(fig) # Estimate image background: # Not using image.clean here, since we are redefining the mask anyway bkg = Background2D( image.clean, (128, 128), filter_size=(5, 5), #mask=image.mask | (image.clean > background_cutoff), sigma_clip=SigmaClip(sigma=3.0), bkg_estimator=SExtractorBackground(), exclude_percentile=50.0) image.background = bkg.background # Create background-subtracted image: image.subclean = image.clean - image.background # Plot background estimation: fig, ax = plt.subplots(1, 3, figsize=(20, 6)) plot_image(image.clean, ax=ax[0], scale='log', title='Original') plot_image(image.background, ax=ax[1], scale='log', title='Background') plot_image(image.subclean, ax=ax[2], scale='log', title='Background subtracted') fig.savefig(os.path.join(output_folder, 'background.png'), bbox_inches='tight') plt.close(fig) # TODO: Is this correct?! image.error = calc_total_error(image.clean, bkg.background_rms, 1.0) #============================================================================================== # DETECTION OF STARS AND MATCHING WITH CATALOG #============================================================================================== logger.info("References:\n%s", references) radius = 10 fwhm_guess = 6.0 fwhm_min = 3.5 fwhm_max = 13.5 # Extract stars sub-images: #stars = extract_stars( # NDData(data=image.subclean, mask=image.mask), # stars_for_epsf, # size=size #) # Set up 2D Gaussian model for fitting to reference stars: g2d = models.Gaussian2D(amplitude=1.0, x_mean=radius, y_mean=radius, x_stddev=fwhm_guess * gaussian_fwhm_to_sigma) g2d.amplitude.bounds = (0.1, 2.0) g2d.x_mean.bounds = (0.5 * radius, 1.5 * radius) g2d.y_mean.bounds = (0.5 * radius, 1.5 * radius) g2d.x_stddev.bounds = (fwhm_min * gaussian_fwhm_to_sigma, fwhm_max * gaussian_fwhm_to_sigma) g2d.y_stddev.tied = lambda model: model.x_stddev g2d.theta.fixed = True gfitter = fitting.LevMarLSQFitter() fwhms = np.full(len(references), np.NaN) for i, (x, y) in enumerate( zip(references['pixel_column'], references['pixel_row'])): x = int(np.round(x)) y = int(np.round(y)) x0, y0, width, height = x - radius, y - radius, 2 * radius, 2 * radius cutout = slice(y0 - 1, y0 + height), slice(x0 - 1, x0 + width) curr_star = image.subclean[cutout] / np.max(image.subclean[cutout]) npix = len(curr_star) ypos, xpos = np.mgrid[:npix, :npix] gfit = gfitter(g2d, x=xpos, y=ypos, z=curr_star) fwhms[i] = gfit.x_fwhm mask = ~np.isfinite(fwhms) | (fwhms <= fwhm_min) | (fwhms >= fwhm_max) masked_fwhms = np.ma.masked_array(fwhms, mask) fwhm = np.mean(sigma_clip(masked_fwhms, maxiters=20, sigma=2.0)) logger.info("FWHM: %f", fwhm) # Use DAOStarFinder to search the image for stars, and only use reference-stars where a # star was actually detected close to the references-star coordinate: cleanout_references = (len(references) > 50) if cleanout_references: daofind_tbl = DAOStarFinder(100, fwhm=fwhm, roundlo=-0.5, roundhi=0.5).find_stars(image.subclean, mask=image.mask) indx_good = np.zeros(len(references), dtype='bool') for k, ref in enumerate(references): dist = np.sqrt((daofind_tbl['xcentroid'] - ref['pixel_column'])**2 + (daofind_tbl['ycentroid'] - ref['pixel_row'])**2) if np.any(dist <= fwhm / 4): # Cutoff set somewhat arbitrary indx_good[k] = True references = references[indx_good] fig, ax = plt.subplots(1, 1, figsize=(20, 18)) plot_image(image.subclean, ax=ax, scale='log', cbar='right', title=target_name) ax.scatter(references['pixel_column'], references['pixel_row'], c='r', alpha=0.3) if cleanout_references: ax.scatter(daofind_tbl['xcentroid'], daofind_tbl['ycentroid'], c='g', alpha=0.3) ax.scatter(target_pixel_pos[0], target_pixel_pos[1], marker='+', c='r') fig.savefig(os.path.join(output_folder, 'positions.png'), bbox_inches='tight') plt.close(fig) #============================================================================================== # CREATE EFFECTIVE PSF MODEL #============================================================================================== # Make cutouts of stars using extract_stars: # Scales with FWHM size = int(np.round(29 * fwhm / 6)) if size % 2 == 0: size += 1 # Make sure it's a uneven number size = max(size, 15) # Never go below 15 pixels hsize = (size - 1) / 2 x = references['pixel_column'] y = references['pixel_row'] mask_near_edge = ((x > hsize) & (x < (image.shape[1] - 1 - hsize)) & (y > hsize) & (y < (image.shape[0] - 1 - hsize))) stars_for_epsf = Table() stars_for_epsf['x'] = x[mask_near_edge] stars_for_epsf['y'] = y[mask_near_edge] # Store which stars were used in ePSF in the table: logger.info("Number of stars used for ePSF: %d", len(stars_for_epsf)) references['used_for_epsf'] = mask_near_edge # Extract stars sub-images: stars = extract_stars(NDData(data=image.subclean, mask=image.mask), stars_for_epsf, size=size) # Plot the stars being used for ePSF: nrows = 5 ncols = 5 imgnr = 0 for k in range(int(np.ceil(len(stars_for_epsf) / (nrows * ncols)))): fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(20, 20), squeeze=True) ax = ax.ravel() for i in range(nrows * ncols): if imgnr > len(stars_for_epsf) - 1: ax[i].axis('off') else: plot_image(stars[imgnr], ax=ax[i], scale='log', cmap='viridis') imgnr += 1 fig.savefig(os.path.join(output_folder, 'epsf_stars%02d.png' % (k + 1)), bbox_inches='tight') plt.close(fig) # Build the ePSF: epsf = EPSFBuilder(oversampling=1.0, maxiters=500, fitter=EPSFFitter(fit_boxsize=2 * fwhm), progress_bar=True)(stars)[0] logger.info('Successfully built PSF model') fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 15)) plot_image(epsf.data, ax=ax1, cmap='viridis') fwhms = [] for a, ax in ((0, ax3), (1, ax2)): # Collapse the PDF along this axis: profile = epsf.data.sum(axis=a) itop = profile.argmax() poffset = profile[itop] / 2 # Run a spline through the points, but subtract half of the peak value, and find the roots: # We have to use a cubic spline, since roots() is not supported for other splines # for some reason profile_intp = UnivariateSpline(np.arange(0, len(profile)), profile - poffset, k=3, s=0, ext=3) lr = profile_intp.roots() axis_fwhm = lr[1] - lr[0] fwhms.append(axis_fwhm) x_fine = np.linspace(-0.5, len(profile) - 0.5, 500) ax.plot(profile, 'k.-') ax.plot(x_fine, profile_intp(x_fine) + poffset, 'g-') ax.axvline(itop) ax.axvspan(lr[0], lr[1], facecolor='g', alpha=0.2) ax.set_xlim(-0.5, len(profile) - 0.5) # Let's make the final FWHM the largest one we found: fwhm = np.max(fwhms) logger.info("Final FWHM based on ePSF: %f", fwhm) #ax2.axvspan(itop - fwhm/2, itop + fwhm/2, facecolor='b', alpha=0.2) #ax3.axvspan(itop - fwhm/2, itop + fwhm/2, facecolor='b', alpha=0.2) ax4.axis('off') fig.savefig(os.path.join(output_folder, 'epsf.png'), bbox_inches='tight') plt.close(fig) #============================================================================================== # COORDINATES TO DO PHOTOMETRY AT #============================================================================================== coordinates = np.array([[ref['pixel_column'], ref['pixel_row']] for ref in references]) # Add the main target position as the first entry: if datafile.get('template') is None: coordinates = np.concatenate(([target_pixel_pos], coordinates), axis=0) #============================================================================================== # APERTURE PHOTOMETRY #============================================================================================== # Define apertures for aperture photometry: apertures = CircularAperture(coordinates, r=fwhm) annuli = CircularAnnulus(coordinates, r_in=1.5 * fwhm, r_out=2.5 * fwhm) apphot_tbl = aperture_photometry(image.subclean, [apertures, annuli], mask=image.mask, error=image.error) logger.debug("Aperture Photometry Table:\n%s", apphot_tbl) logger.info('Apperature Photometry Success') #============================================================================================== # PSF PHOTOMETRY #============================================================================================== # Are we fixing the postions? epsf.fixed.update({'x_0': False, 'y_0': False}) # Create photometry object: photometry = BasicPSFPhotometry(group_maker=DAOGroup(fwhm), bkg_estimator=SExtractorBackground(), psf_model=epsf, fitter=fitting.LevMarLSQFitter(), fitshape=size, aperture_radius=fwhm) psfphot_tbl = photometry(image=image.subclean, init_guesses=Table(coordinates, names=['x_0', 'y_0'])) logger.debug("PSF Photometry Table:\n%s", psfphot_tbl) logger.info('PSF Photometry Success') #============================================================================================== # TEMPLATE SUBTRACTION AND TARGET PHOTOMETRY #============================================================================================== if datafile.get('template') is not None: # Find the pixel-scale of the science image: pixel_area = proj_plane_pixel_area(image.wcs.celestial) pixel_scale = np.sqrt(pixel_area) * 3600 # arcsec/pixel #print(image.wcs.celestial.cunit) % Doesn't work? logger.info("Science image pixel scale: %f", pixel_scale) # Run the template subtraction, and get back # the science image where the template has been subtracted: diffimage = run_imagematch(datafile, target, star_coord=coordinates, fwhm=fwhm, pixel_scale=pixel_scale) # Include mask from original image: diffimage = np.ma.masked_array(diffimage, image.mask) # Create apertures around the target: apertures = CircularAperture(target_pixel_pos, r=fwhm) annuli = CircularAnnulus(target_pixel_pos, r_in=1.5 * fwhm, r_out=2.5 * fwhm) # Create two plots of the difference image: fig, ax = plt.subplots(1, 1, squeeze=True, figsize=(20, 20)) plot_image(diffimage, ax=ax, cbar='right', title=target_name) ax.plot(target_pixel_pos[0], target_pixel_pos[1], marker='+', color='r') fig.savefig(os.path.join(output_folder, 'diffimg.png'), bbox_inches='tight') apertures.plot(color='r') annuli.plot(color='k') ax.set_xlim(target_pixel_pos[0] - 50, target_pixel_pos[0] + 50) ax.set_ylim(target_pixel_pos[1] - 50, target_pixel_pos[1] + 50) fig.savefig(os.path.join(output_folder, 'diffimg_zoom.png'), bbox_inches='tight') plt.close(fig) # Run aperture photometry on subtracted image: target_apphot_tbl = aperture_photometry(diffimage, [apertures, annuli], mask=image.mask, error=image.error) # Run PSF photometry on template subtracted image: target_psfphot_tbl = photometry(diffimage, init_guesses=Table( target_pixel_pos, names=['x_0', 'y_0'])) # Combine the output tables from the target and the reference stars into one: apphot_tbl = vstack([target_apphot_tbl, apphot_tbl], join_type='exact') psfphot_tbl = vstack([target_psfphot_tbl, psfphot_tbl], join_type='exact') # Build results table: tab = references.copy() tab.insert_row( 0, { 'starid': 0, 'ra': target['ra'], 'decl': target['decl'], 'pixel_column': target_pixel_pos[0], 'pixel_row': target_pixel_pos[1] }) for key in ('pm_ra', 'pm_dec', 'gaia_mag', 'gaia_bp_mag', 'gaia_rp_mag', 'H_mag', 'J_mag', 'K_mag', 'g_mag', 'r_mag', 'i_mag', 'z_mag'): tab[0][key] = np.NaN # Subtract background estimated from annuli: flux_aperture = apphot_tbl['aperture_sum_0'] - ( apphot_tbl['aperture_sum_1'] / annuli.area()) * apertures.area() flux_aperture_error = np.sqrt(apphot_tbl['aperture_sum_err_0']**2 + (apphot_tbl['aperture_sum_err_1'] / annuli.area() * apertures.area())**2) # Add table columns with results: tab['flux_aperture'] = flux_aperture / image.exptime tab['flux_aperture_error'] = flux_aperture_error / image.exptime tab['flux_psf'] = psfphot_tbl['flux_fit'] / image.exptime tab['flux_psf_error'] = psfphot_tbl['flux_unc'] / image.exptime tab['pixel_column_psf_fit'] = psfphot_tbl['x_fit'] tab['pixel_row_psf_fit'] = psfphot_tbl['y_fit'] tab['pixel_column_psf_fit_error'] = psfphot_tbl['x_0_unc'] tab['pixel_row_psf_fit_error'] = psfphot_tbl['y_0_unc'] # Theck that we got valid photometry: if not np.isfinite(tab[0]['flux_psf']) or not np.isfinite( tab[0]['flux_psf_error']): raise Exception("Target magnitude is undefined.") #============================================================================================== # CALIBRATE #============================================================================================== # Convert PSF fluxes to magnitudes: mag_inst = -2.5 * np.log10(tab['flux_psf']) mag_inst_err = (2.5 / np.log(10)) * (tab['flux_psf_error'] / tab['flux_psf']) # Corresponding magnitudes in catalog: mag_catalog = tab[ref_filter] # Mask out things that should not be used in calibration: use_for_calibration = np.ones_like(mag_catalog, dtype='bool') use_for_calibration[0] = False # Do not use target for calibration use_for_calibration[~np.isfinite(mag_inst) | ~np.isfinite(mag_catalog)] = False # Just creating some short-hands: x = mag_catalog[use_for_calibration] y = mag_inst[use_for_calibration] yerr = mag_inst_err[use_for_calibration] # Fit linear function with fixed slope, using sigma-clipping: model = models.Linear1D(slope=1, fixed={'slope': True}) fitter = fitting.FittingWithOutlierRemoval(fitting.LinearLSQFitter(), sigma_clip, sigma=3.0) best_fit, sigma_clipped = fitter(model, x, y, weights=1.0 / yerr**2) # Extract zero-point and estimate its error: # I don't know why there is not an error-estimate attached directly to the Parameter? zp = -1 * best_fit.intercept.value # Negative, because that is the way zeropoints are usually defined zp_error = nanstd(y[~sigma_clipped] - best_fit(x[~sigma_clipped])) # Add calibrated magnitudes to the photometry table: tab['mag'] = mag_inst + zp tab['mag_error'] = np.sqrt(mag_inst_err**2 + zp_error**2) fig, ax = plt.subplots(1, 1) ax.errorbar(x, y, yerr=yerr, fmt='k.') ax.scatter(x[sigma_clipped], y[sigma_clipped], marker='x', c='r') ax.plot(x, best_fit(x), color='g', linewidth=3) ax.set_xlabel('Catalog magnitude') ax.set_ylabel('Instrumental magnitude') fig.savefig(os.path.join(output_folder, 'calibration.png'), bbox_inches='tight') plt.close(fig) #============================================================================================== # SAVE PHOTOMETRY #============================================================================================== # Descriptions of columns: tab['flux_aperture'].unit = u.count / u.second tab['flux_aperture_error'].unit = u.count / u.second tab['flux_psf'].unit = u.count / u.second tab['flux_psf_error'].unit = u.count / u.second tab['pixel_column'].unit = u.pixel tab['pixel_row'].unit = u.pixel tab['pixel_column_psf_fit'].unit = u.pixel tab['pixel_row_psf_fit'].unit = u.pixel tab['pixel_column_psf_fit_error'].unit = u.pixel tab['pixel_row_psf_fit_error'].unit = u.pixel # Meta-data: tab.meta['version'] = __version__ tab.meta['fileid'] = fileid tab.meta['template'] = None if datafile.get( 'template') is None else datafile['template']['fileid'] tab.meta['photfilter'] = photfilter tab.meta['fwhm'] = fwhm tab.meta['obstime-bmjd'] = float(image.obstime.mjd) tab.meta['zp'] = zp tab.meta['zp_error'] = zp_error # Filepath where to save photometry: photometry_output = os.path.join(output_folder, 'photometry.ecsv') # Write the final table to file: tab.write(photometry_output, format='ascii.ecsv', delimiter=',', overwrite=True) toc = default_timer() logger.info("Photometry took: %f seconds", toc - tic) return photometry_output
def apr_red(fitsfile, outfile, makeplots=False): """ aperture photometry on a fitsfild """ from photutils import (Background2D, DAOStarFinder, CircularAperture, aperture_photometry, MeanBackground) from photutils.utils import calc_total_error from astropy.stats import sigma_clipped_stats # Open the fits file hdu = fits.open(fitsfile) # Take out the data -- this is often not the 0 extention. If this breaks, # try this instead: # data = hdu[1].data data = hdu[0].data # the header is almost always the 0th ext. w = wcs.WCS(hdu[0].header) nanmask = np.isnan(data) data[nanmask] = -99 newdata = data.copy() # you might not need to make the image square -- I did it for my mosaic. # square-ify: add -99 values to fill a square nx, ny = np.shape(data) if nx != ny: if ny > nx: filler = np.zeros((ny - nx, ny)) - 99 newdata = np.vstack((data, filler)) else: filler = np.zeros((nx, nx - ny)) - 99 newdata = np.hstack((data, filler)) else: newdata = data.copy() # See photutils documentation, box dimension is the nearest factor to 150px boxdim = nearest_factor(ny, 150) mask = (newdata == -99) # sigma_clip = SigmaClip(sigma=3., iters=10) bkg = Background2D(newdata, (boxdim, boxdim), filter_size=(9, 9), mask=mask, bkg_estimator=MeanBackground()) # data - bg data_sub = newdata - bkg.background mean, median, std = sigma_clipped_stats(data_sub, sigma=8.0, iters=10, mask=mask) daofind = DAOStarFinder(fwhm=4.0, threshold=10. * std + median) sources = daofind(data_sub) positions = (sources['xcentroid'], sources['ycentroid']) # ACS handbook says 4. is inf. apertures = CircularAperture(positions, r=4.) effective_gain = hdu[0].header['EXPTIME'] error = calc_total_error(data_sub, bkg.background, effective_gain) phot_table = aperture_photometry(data_sub, apertures, error=error, mask=mask) vegamag = -2.5 * np.log10(phot_table['aperture_sum'] * PHOTFLAM) - VEGAmag0 # vegamagerr = \ # -2.5 * np.log10(phot_table['aperture_sum_err'] * PHOTFLAM) - VEGAmag0 vegamagerr = verrtest(phot_table['aperture_sum'], phot_table['aperture_sum_err']) # see astropy.wcs for info: ra, dec = w.all_pix2world(phot_table['xcenter'], phot_table['ycenter'], 1) phot_table['ra'] = ra phot_table['dec'] = dec phot_table['vegamag'] = vegamag phot_table['vegamagerr'] = vegamagerr fmts = { c: '%.6f' for c in [ 'xcenter', 'ycenter', 'aperture_sum', 'aperture_sum_err', 'ra', 'dec', 'vegamag', 'vegamagerr' ] } phot_table.write(outfile, format='ascii.commented_header', formats=fmts) if makeplots: outfig = outfile + '.pdf' axs = photplots(newdata, bkg.background, bkg.background_rms, data_sub, fitsfile, outfig) apertures.plot(color='red', lw=1.5, alpha=0.5, ax=axs[0, 1]) plt.savefig(outfig) return vegamag, vegamagerr, ra, dec