Beispiel #1
0
    def do_stage(self, image):
        lab_lines = find_nearest(image.features['wavelength'],
                                 np.sort(image.line_list))
        delta_lambda = image.features['wavelength'] - lab_lines

        sigma_delta_lambda = robust_standard_deviation(delta_lambda)
        low_scatter_lines = delta_lambda < 3. * sigma_delta_lambda

        matched_sigma_delta_lambda = robust_standard_deviation(
            delta_lambda[low_scatter_lines])
        num_detected_lines = len(image.features['wavelength'])
        num_matched_lines = np.count_nonzero(low_scatter_lines)

        feature_centroid_uncertainty = image.features['centroid_err']

        reduced_chi2 = get_reduced_chi_squared(
            delta_lambda[low_scatter_lines],
            feature_centroid_uncertainty[low_scatter_lines])
        velocity_precision = get_velocity_precision(
            image.features['wavelength'][low_scatter_lines],
            lab_lines[low_scatter_lines], num_matched_lines)

        if num_matched_lines == 0:  # get rid of nans in the matched statistics if we have zero matched lines.
            matched_sigma_delta_lambda, reduced_chi2, velocity_precision = 0, 0, 0 * units.meter / units.second

        # opensearch keys don't have to be the same as the fits headers
        qc_results = {
            'SIGLAM':
            np.round(matched_sigma_delta_lambda, 4),
            'RVPRECSN':
            np.round(
                velocity_precision.to(units.meter / units.second).value, 4),
            'WAVRCHI2':
            np.round(reduced_chi2, 4),
            'NLINEDET':
            num_detected_lines,
            'NLINES':
            num_matched_lines
        }
        qc_description = {
            'SIGLAM': 'wavecal residuals [Angstroms]',
            'RVPRECSN': 'wavecal precision [m/s]',
            'WAVRCHI2': 'reduced chisquared goodness of wavecal fit',
            'NLINEDET': 'Number of lines found on detector',
            'NLINES': 'Number of matched lines'
        }
        qc.save_qc_results(self.runtime_context, qc_results, image)
        # saving the results to the image header
        for key in qc_results.keys():
            image.meta[key] = (qc_results[key], qc_description[key])

        logger.info(f'wavecal precision (m/s) = {qc_results["RVPRECSN"]}',
                    image=image)
        if qc_results['RVPRECSN'] > 10 or qc_results['RVPRECSN'] < 3:
            logger.warning(
                f' Final calibration precision is outside the expected range '
                f'wavecal precision (m/s) = '
                f'{qc_results["RVPRECSN"]}',
                image=image)
        return image
Beispiel #2
0
def compute_snr(power_2d, fractional_window_size=0.05):
    """
    Extract the central region of the 2D Fourier transform

    Parameters
    ----------
    power_2d : numpy array
        The 2D Fourier transform of the data
    fractional_window_size : float
        Median filter window size as a fraction of the 1D power array

    Returns
    -------
    snr : numpy array
        The 1D SNR
    """
    power = np.median(power_2d, axis=0)
    p2p_scatter = abs(power[1:] - power[:-1])
    power = power[1:]  # Throw away DC term

    # Median filter
    window_size = get_odd_integer(fractional_window_size * len(power))
    continuum = median_filter(power, size=window_size)
    pixel_to_pixel_scatter = median_filter(p2p_scatter, size=window_size)
    snr = (power - continuum) / pixel_to_pixel_scatter

    # Also divide out the global scatter for any residual structure that was not removed with the median filter
    global_scatter = robust_standard_deviation(snr)
    snr /= global_scatter

    return snr
Beispiel #3
0
def compute_snr(power_2d, fractional_window_size=0.05):
    """
    Extract the central region of the 2D Fourier transform

    Parameters
    ----------
    power_2d : numpy array
        The 2D Fourier transform of the data
    fractional_window_size : float
        Median filter window size as a fraction of the 1D power array

    Returns
    -------
    snr : numpy array
        The 1D SNR
    """
    power = np.median(power_2d, axis=0)
    p2p_scatter = abs(power[1:] - power[:-1])
    power = power[1:]  # Throw away DC term

    # Median filter
    window_size = get_odd_integer(fractional_window_size * len(power))
    continuum = median_filter(power, size=window_size)
    pixel_to_pixel_scatter = median_filter(p2p_scatter, size=window_size)
    snr = (power - continuum) / pixel_to_pixel_scatter

    # Also divide out the global scatter for any residual structure that was not removed with the median filter
    global_scatter = robust_standard_deviation(snr)
    snr /= global_scatter

    return snr
Beispiel #4
0
def get_principle_order_number(m0_values, features):
    """
    Finds the principle order number m0. Selects the m0 such that the function y(i) = (m0 + i) * central_wavelengths
    has the smallest slope. I.e. this selects the m0 that allows constant/(m0+i) to best fit central_wavelengths.
    This is exactly what CERES does. See equation 3 of Brahm et al. 2016.

    :param m0_values: ndarray of integers. 1d.
    :param features: dict.
           dictionary of ndarrays of pixel and order positions of measured spectral features (e.g. emission lines)
           and their wavelengths.
           Example:
               measured_lines = {'pixel': np.array([1, 2.5, 6.1]), 'order': np.array([1, 1, 2]),
                                 'wavelength': np.array([4000, 5001, 5005)}
               If the principle order number is 52, then these measured_lines represents 3 spectral features,
               with (pixel, diffraction order) coordinates of (1, 53), (2.5, 53), (6.1, 54), and wavelengths
               4000, 5001, and 5005 Angstroms, respectively.
               respectively. The wavelength solution will calibrate each fiber separately.
    :return: m0: int.
    The principle order number for the fiber from which central_wavelengths were taken.
    This is the true order index of the the trace that corresponds to ref_id[0].
    """
    center_wavelengths = get_center_wavelengths(features)
    ref_ids = np.sort(np.array(list(set(features['order']))))
    slopes = []
    for m0 in m0_values:
        slopes.append(
            robust_standard_deviation(center_wavelengths * (m0 + ref_ids)))

    if np.count_nonzero(np.isclose(slopes, np.min(slopes), rtol=0.01)) > 1:
        logger.error(
            'Two or more viable principle order numbers for this fiber! The m0 recovered from the '
            'wavelength solution could be wrong. A wrong m0 would mess up fiber identification as'
            ' well. Aborting wavelength solution!')
        return None
    return m0_values[np.argmin(slopes)]
Beispiel #5
0
 def test_line_matching(self):
     nlines, wavelength_scatter = 100, 0.1
     mock_lines = np.linspace(4000, 5000, nlines)
     line_list = np.random.permutation(mock_lines)
     features = mock_lines + np.random.randn(nlines) * wavelength_scatter
     lab_lines = find_nearest(features, np.sort(line_list))
     sigma_delta_lambda = robust_standard_deviation(features - lab_lines)
     assert np.isclose(sigma_delta_lambda, wavelength_scatter, rtol=5.e-2)
Beispiel #6
0
def get_velocity_precision(image_lines, lab_lines, num_matched_lines):
    """
    calculating metrics in velocity space (easily understood by users) del lambda/ lambda * c = delta v.
    then divide delta v by square root of the number of lines, giving the error on the mean of the residuals.
    """
    delta_lambda = image_lines - lab_lines
    dlam_overlam = delta_lambda / lab_lines
    velocity_precision = robust_standard_deviation(dlam_overlam) / np.sqrt(
        num_matched_lines) * constants.c
    return velocity_precision
Beispiel #7
0
def mark_features(flux,
                  sigma=3,
                  continuum_formal_error=None,
                  detector_resolution=4):
    """
    :param flux:
    :param sigma:
        :param continuum_formal_error:
    :param min_feature_width:
    :param detector_resolution:
    :return:
    """
    # start with a mask that marks every pixel as "ignore"
    mask = np.ones_like(flux, dtype=bool)
    if continuum_formal_error is None:
        # get the noisy scatter in the continuum-only portion of the spectrum by high-pass filtering the spectrum and
        # then taking the mad times 1.4826 (the robust standard deviation)
        smoothed_flux = gaussian_filter1d(flux, sigma=detector_resolution / 2)
        continuum_formal_error = robust_standard_deviation(flux -
                                                           smoothed_flux)
    # take the brightest 10% of pixels to trace above the tops of the continuum
    continuum_estimate = percentile_filter(flux,
                                           percentile=-10,
                                           size=3 * detector_resolution,
                                           mode='nearest')
    # keep the pixels that are close to the continuum estimate (mark as "keep")
    mask[np.isclose(continuum_estimate,
                    flux,
                    atol=sigma * continuum_formal_error)] = 0
    # binary dilate to cover the wings of lines which were clipped.
    mask = binary_dilation(mask, iterations=detector_resolution)
    if np.count_nonzero(mask) == len(mask):
        logger.warning(
            'Masking unsuccessful. Entire spectral region was masked. Aborting and returning the spectrum'
            'as it was input, i.e. without masking any elements.')
        # if all the elements are masked, raise a warning then return a all zeros mask (no masked elements)
        # so that later stages do not crash.
        mask = np.zeros_like(mask)
    return mask
Beispiel #8
0
    def do_stage(self, images):
        for i, image in enumerate(images):
            try:
                # Set the number of source pixels to be 5% of the total. This keeps us safe from
                # satellites and airplanes.
                sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

                data = image.data.copy()
                error = (np.abs(data) + image.readnoise ** 2.0) ** 0.5
                mask = image.bpm > 0

                # Fits can be backwards byte order, so fix that if need be and subtract
                # the background
                try:
                    bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
                except ValueError:
                    data = data.byteswap(True).newbyteorder()
                    bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
                bkg.subfrom(data)

                # Do an initial source detection
                # TODO: Add back in masking after we are sure SEP works
                sources = sep.extract(data, self.threshold, minarea=self.min_area,
                                      err=error, deblend_cont=0.005)

                # Convert the detections into a table
                sources = Table(sources)

                # Calculate the ellipticity
                sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

                # Fix any value of theta that are invalid due to floating point rounding
                # -pi / 2 < theta < pi / 2
                sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
                sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

                # Calculate the kron radius
                kronrad, krflag = sep.kron_radius(data, sources['x'], sources['y'],
                                                  sources['a'], sources['b'],
                                                  sources['theta'], 6.0)
                sources['flag'] |= krflag
                sources['kronrad'] = kronrad

                # Calcuate the equivilent of flux_auto
                flux, fluxerr, flag = sep.sum_ellipse(data, sources['x'], sources['y'],
                                                      sources['a'], sources['b'],
                                                      np.pi / 2.0, 2.5 * kronrad,
                                                      subpix=1, err=error)
                sources['flux'] = flux
                sources['fluxerr'] = fluxerr
                sources['flag'] |= flag

                # Calculate the FWHMs of the stars:
                fwhm = 2.0 * (np.log(2) * (sources['a'] ** 2.0 + sources['b'] ** 2.0)) ** 0.5
                sources['fwhm'] = fwhm

                # Cut individual bright pixels. Often cosmic rays
                sources = sources[fwhm > 1.0]

                # Measure the flux profile
                flux_radii, flag = sep.flux_radius(data, sources['x'], sources['y'],
                                                   6.0 * sources['a'], [0.25, 0.5, 0.75],
                                                   normflux=sources['flux'], subpix=5)
                sources['flag'] |= flag
                sources['fluxrad25'] = flux_radii[:, 0]
                sources['fluxrad50'] = flux_radii[:, 1]
                sources['fluxrad75'] = flux_radii[:, 2]

                # Calculate the windowed positions
                sig = 2.0 / 2.35 * sources['fluxrad50']
                xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'], sig)
                sources['flag'] |= flag
                sources['xwin'] = xwin
                sources['ywin'] = ywin

                # Calculate the average background at each source
                bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(), sources['x'], sources['y'],
                                                         sources['a'], sources['b'], np.pi / 2.0,
                                                         2.5 * sources['kronrad'], subpix=1)
                #masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
                #                                         sources['a'], sources['b'], np.pi / 2.0,
                #                                         2.5 * kronrad, subpix=1)

                background_area = (2.5 * sources['kronrad']) ** 2.0 * sources['a'] * sources['b'] * np.pi # - masksum
                sources['background'] = bkgflux
                sources['background'][background_area > 0] /= background_area[background_area > 0]
                # Update the catalog to match fits convention instead of python array convention
                sources['x'] += 1.0
                sources['y'] += 1.0

                sources['xpeak'] += 1
                sources['ypeak'] += 1

                sources['xwin'] += 1.0
                sources['ywin'] += 1.0

                sources['theta'] = np.degrees(sources['theta'])

                image.catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak', 'ypeak',
                                        'flux', 'fluxerr', 'background', 'fwhm',
                                        'a', 'b', 'theta', 'kronrad', 'ellipticity',
                                        'fluxrad25', 'fluxrad50', 'fluxrad75',
                                        'x2', 'y2', 'xy', 'flag']

                # Add the units and description to the catalogs
                image.catalog['x'].unit = 'pixel'
                image.catalog['x'].description = 'X coordinate of the object'
                image.catalog['y'].unit = 'pixel'
                image.catalog['y'].description = 'Y coordinate of the object'
                image.catalog['xwin'].unit = 'pixel'
                image.catalog['xwin'].description = 'Windowed X coordinate of the object'
                image.catalog['ywin'].unit = 'pixel'
                image.catalog['ywin'].description = 'Windowed Y coordinate of the object'
                image.catalog['xpeak'].unit = 'pixel'
                image.catalog['xpeak'].description = 'X coordinate of the peak'
                image.catalog['ypeak'].unit = 'pixel'
                image.catalog['ypeak'].description = 'Windowed Y coordinate of the peak'
                image.catalog['flux'].unit = 'counts'
                image.catalog['flux'].description = 'Flux within a Kron-like elliptical aperture'
                image.catalog['fluxerr'].unit = 'counts'
                image.catalog['fluxerr'].description = 'Erronr on the flux within a Kron-like elliptical aperture'
                image.catalog['background'].unit = 'counts'
                image.catalog['background'].description = 'Average background value in the aperture'
                image.catalog['fwhm'].unit = 'pixel'
                image.catalog['fwhm'].description = 'FWHM of the object'
                image.catalog['a'].unit = 'pixel'
                image.catalog['a'].description = 'Semi-major axis of the object'
                image.catalog['b'].unit = 'pixel'
                image.catalog['b'].description = 'Semi-minor axis of the object'
                image.catalog['theta'].unit = 'degrees'
                image.catalog['theta'].description = 'Position angle of the object'
                image.catalog['kronrad'].unit = 'pixel'
                image.catalog['kronrad'].description = 'Kron radius used for extraction'
                image.catalog['ellipticity'].description = 'Ellipticity'
                image.catalog['fluxrad25'].unit = 'pixel'
                image.catalog['fluxrad25'].description = 'Radius containing 25% of the flux'
                image.catalog['fluxrad50'].unit = 'pixel'
                image.catalog['fluxrad50'].description = 'Radius containing 50% of the flux'
                image.catalog['fluxrad75'].unit = 'pixel'
                image.catalog['fluxrad75'].description = 'Radius containing 75% of the flux'
                image.catalog['x2'].unit = 'pixel^2'
                image.catalog['x2'].description = 'Variance on X coordinate of the object'
                image.catalog['y2'].unit = 'pixel^2'
                image.catalog['y2'].description = 'Variance on Y coordinate of the object'
                image.catalog['xy'].unit = 'pixel^2'
                image.catalog['xy'].description = 'XY covariance of the object'
                image.catalog['flag'].description = 'Bit mask combination of extraction and photometry flags'

                image.catalog.sort('flux')
                image.catalog.reverse()

                logging_tags = logs.image_config_to_tags(image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename', os.path.basename(image.filename))

                # Save some background statistics in the header
                mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
                image.header['L1MEAN'] = (mean_background,
                                          '[counts] Sigma clipped mean of frame background')
                logs.add_tag(logging_tags, 'L1MEAN', float(mean_background))

                median_background = np.median(bkg.back())
                image.header['L1MEDIAN'] = (median_background,
                                            '[counts] Median of frame background')
                logs.add_tag(logging_tags, 'L1MEDIAN', float(median_background))

                std_background = stats.robust_standard_deviation(bkg.back())
                image.header['L1SIGMA'] = (std_background,
                                           '[counts] Robust std dev of frame background')
                logs.add_tag(logging_tags, 'L1SIGMA', float(std_background))

                # Save some image statistics to the header
                good_objects = image.catalog['flag'] == 0

                seeing = np.median(image.catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing, '[arcsec] Frame FWHM in arcsec')
                logs.add_tag(logging_tags, 'L1FWHM', float(seeing))

                mean_ellipticity = stats.sigma_clipped_mean(sources['ellipticity'][good_objects],
                                                            3.0)
                image.header['L1ELLIP'] = (mean_ellipticity, 'Mean image ellipticity (1-B/A)')
                logs.add_tag(logging_tags, 'L1ELLIP', float(mean_ellipticity))

                mean_position_angle = stats.sigma_clipped_mean(sources['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (mean_position_angle,
                                            '[deg] PA of mean image ellipticity')
                logs.add_tag(logging_tags, 'L1ELLIPA', float(mean_position_angle))

                self.logger.info('Extracted sources', extra=logging_tags)

            except Exception as e:
                logging_tags = logs.image_config_to_tags(image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename', os.path.basename(image.filename))
                self.logger.error(e, extra=logging_tags)
        return images
Beispiel #9
0
    def do_stage(self, image):
        try:
            # Set the number of source pixels to be 5% of the total. This keeps us safe from
            # satellites and airplanes.
            sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

            data = image.data.copy()
            error = (np.abs(data) + image.readnoise**2.0)**0.5
            mask = image.bpm > 0

            # Fits can be backwards byte order, so fix that if need be and subtract
            # the background
            try:
                bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
            except ValueError:
                data = data.byteswap(True).newbyteorder()
                bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
            bkg.subfrom(data)

            # Do an initial source detection
            # TODO: Add back in masking after we are sure SEP works
            sources = sep.extract(data,
                                  self.threshold,
                                  minarea=self.min_area,
                                  err=error,
                                  deblend_cont=0.005)

            # Convert the detections into a table
            sources = Table(sources)

            # We remove anything with a detection flag >= 8
            # This includes memory overflows and objects that are too close the edge
            sources = sources[sources['flag'] < 8]

            sources = array_utils.prune_nans_from_table(sources)

            # Calculate the ellipticity
            sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

            # Fix any value of theta that are invalid due to floating point rounding
            # -pi / 2 < theta < pi / 2
            sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
            sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

            # Calculate the kron radius
            kronrad, krflag = sep.kron_radius(data, sources['x'], sources['y'],
                                              sources['a'], sources['b'],
                                              sources['theta'], 6.0)
            sources['flag'] |= krflag
            sources['kronrad'] = kronrad

            # Calcuate the equivilent of flux_auto
            flux, fluxerr, flag = sep.sum_ellipse(data,
                                                  sources['x'],
                                                  sources['y'],
                                                  sources['a'],
                                                  sources['b'],
                                                  np.pi / 2.0,
                                                  2.5 * kronrad,
                                                  subpix=1,
                                                  err=error)
            sources['flux'] = flux
            sources['fluxerr'] = fluxerr
            sources['flag'] |= flag

            # Do circular aperture photometry for diameters of 1" to 6"
            for diameter in [1, 2, 3, 4, 5, 6]:
                flux, fluxerr, flag = sep.sum_circle(data,
                                                     sources['x'],
                                                     sources['y'],
                                                     diameter / 2.0 /
                                                     image.pixel_scale,
                                                     gain=1.0,
                                                     err=error)
                sources['fluxaper{0}'.format(diameter)] = flux
                sources['fluxerr{0}'.format(diameter)] = fluxerr
                sources['flag'] |= flag

            # Calculate the FWHMs of the stars:
            fwhm = 2.0 * (np.log(2) *
                          (sources['a']**2.0 + sources['b']**2.0))**0.5
            sources['fwhm'] = fwhm

            # Cut individual bright pixels. Often cosmic rays
            sources = sources[fwhm > 1.0]

            # Measure the flux profile
            flux_radii, flag = sep.flux_radius(data,
                                               sources['x'],
                                               sources['y'],
                                               6.0 * sources['a'],
                                               [0.25, 0.5, 0.75],
                                               normflux=sources['flux'],
                                               subpix=5)
            sources['flag'] |= flag
            sources['fluxrad25'] = flux_radii[:, 0]
            sources['fluxrad50'] = flux_radii[:, 1]
            sources['fluxrad75'] = flux_radii[:, 2]

            # Calculate the windowed positions
            sig = 2.0 / 2.35 * sources['fluxrad50']
            xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'],
                                          sig)
            sources['flag'] |= flag
            sources['xwin'] = xwin
            sources['ywin'] = ywin

            # Calculate the average background at each source
            bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(),
                                                     sources['x'],
                                                     sources['y'],
                                                     sources['a'],
                                                     sources['b'],
                                                     np.pi / 2.0,
                                                     2.5 * sources['kronrad'],
                                                     subpix=1)
            # masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
            #                                         sources['a'], sources['b'], np.pi / 2.0,
            #                                         2.5 * kronrad, subpix=1)

            background_area = (
                2.5 * sources['kronrad']
            )**2.0 * sources['a'] * sources['b'] * np.pi  # - masksum
            sources['background'] = bkgflux
            sources['background'][background_area > 0] /= background_area[
                background_area > 0]
            # Update the catalog to match fits convention instead of python array convention
            sources['x'] += 1.0
            sources['y'] += 1.0

            sources['xpeak'] += 1
            sources['ypeak'] += 1

            sources['xwin'] += 1.0
            sources['ywin'] += 1.0

            sources['theta'] = np.degrees(sources['theta'])

            catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak', 'ypeak',
                              'flux', 'fluxerr', 'peak', 'fluxaper1',
                              'fluxerr1', 'fluxaper2', 'fluxerr2', 'fluxaper3',
                              'fluxerr3', 'fluxaper4', 'fluxerr4', 'fluxaper5',
                              'fluxerr5', 'fluxaper6', 'fluxerr6',
                              'background', 'fwhm', 'a', 'b', 'theta',
                              'kronrad', 'ellipticity', 'fluxrad25',
                              'fluxrad50', 'fluxrad75', 'x2', 'y2', 'xy',
                              'flag']

            # Add the units and description to the catalogs
            catalog['x'].unit = 'pixel'
            catalog['x'].description = 'X coordinate of the object'
            catalog['y'].unit = 'pixel'
            catalog['y'].description = 'Y coordinate of the object'
            catalog['xwin'].unit = 'pixel'
            catalog['xwin'].description = 'Windowed X coordinate of the object'
            catalog['ywin'].unit = 'pixel'
            catalog['ywin'].description = 'Windowed Y coordinate of the object'
            catalog['xpeak'].unit = 'pixel'
            catalog['xpeak'].description = 'X coordinate of the peak'
            catalog['ypeak'].unit = 'pixel'
            catalog['ypeak'].description = 'Windowed Y coordinate of the peak'
            catalog['flux'].unit = 'count'
            catalog[
                'flux'].description = 'Flux within a Kron-like elliptical aperture'
            catalog['fluxerr'].unit = 'count'
            catalog[
                'fluxerr'].description = 'Error on the flux within Kron aperture'
            catalog['peak'].unit = 'count'
            catalog['peak'].description = 'Peak flux (flux at xpeak, ypeak)'
            for diameter in [1, 2, 3, 4, 5, 6]:
                catalog['fluxaper{0}'.format(diameter)].unit = 'count'
                catalog['fluxaper{0}'.format(
                    diameter
                )].description = 'Flux from fixed circular aperture: {0}" diameter'.format(
                    diameter)
                catalog['fluxerr{0}'.format(diameter)].unit = 'count'
                catalog['fluxerr{0}'.format(
                    diameter
                )].description = 'Error on Flux from circular aperture: {0}"'.format(
                    diameter)

            catalog['background'].unit = 'count'
            catalog[
                'background'].description = 'Average background value in the aperture'
            catalog['fwhm'].unit = 'pixel'
            catalog['fwhm'].description = 'FWHM of the object'
            catalog['a'].unit = 'pixel'
            catalog['a'].description = 'Semi-major axis of the object'
            catalog['b'].unit = 'pixel'
            catalog['b'].description = 'Semi-minor axis of the object'
            catalog['theta'].unit = 'degree'
            catalog['theta'].description = 'Position angle of the object'
            catalog['kronrad'].unit = 'pixel'
            catalog['kronrad'].description = 'Kron radius used for extraction'
            catalog['ellipticity'].description = 'Ellipticity'
            catalog['fluxrad25'].unit = 'pixel'
            catalog[
                'fluxrad25'].description = 'Radius containing 25% of the flux'
            catalog['fluxrad50'].unit = 'pixel'
            catalog[
                'fluxrad50'].description = 'Radius containing 50% of the flux'
            catalog['fluxrad75'].unit = 'pixel'
            catalog[
                'fluxrad75'].description = 'Radius containing 75% of the flux'
            catalog['x2'].unit = 'pixel^2'
            catalog[
                'x2'].description = 'Variance on X coordinate of the object'
            catalog['y2'].unit = 'pixel^2'
            catalog[
                'y2'].description = 'Variance on Y coordinate of the object'
            catalog['xy'].unit = 'pixel^2'
            catalog['xy'].description = 'XY covariance of the object'
            catalog[
                'flag'].description = 'Bit mask of extraction/photometry flags'

            catalog.sort('flux')
            catalog.reverse()

            # Save some background statistics in the header
            mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
            image.header['L1MEAN'] = (
                mean_background,
                '[counts] Sigma clipped mean of frame background')

            median_background = np.median(bkg.back())
            image.header['L1MEDIAN'] = (median_background,
                                        '[counts] Median of frame background')

            std_background = stats.robust_standard_deviation(bkg.back())
            image.header['L1SIGMA'] = (
                std_background, '[counts] Robust std dev of frame background')

            # Save some image statistics to the header
            good_objects = catalog['flag'] == 0
            for quantity in ['fwhm', 'ellipticity', 'theta']:
                good_objects = np.logical_and(
                    good_objects, np.logical_not(np.isnan(catalog[quantity])))
            if good_objects.sum() == 0:
                image.header['L1FWHM'] = ('NaN',
                                          '[arcsec] Frame FWHM in arcsec')
                image.header['L1ELLIP'] = ('NaN',
                                           'Mean image ellipticity (1-B/A)')
                image.header['L1ELLIPA'] = (
                    'NaN', '[deg] PA of mean image ellipticity')
            else:
                seeing = np.median(
                    catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing,
                                          '[arcsec] Frame FWHM in arcsec')

                mean_ellipticity = stats.sigma_clipped_mean(
                    catalog['ellipticity'][good_objects], 3.0)
                image.header['L1ELLIP'] = (mean_ellipticity,
                                           'Mean image ellipticity (1-B/A)')

                mean_position_angle = stats.sigma_clipped_mean(
                    catalog['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (
                    mean_position_angle, '[deg] PA of mean image ellipticity')

            logging_tags = {
                key: float(image.header[key])
                for key in [
                    'L1MEAN', 'L1MEDIAN', 'L1SIGMA', 'L1FWHM', 'L1ELLIP',
                    'L1ELLIPA'
                ]
            }

            logger.info('Extracted sources',
                        image=image,
                        extra_tags=logging_tags)
            # adding catalog (a data table) to the appropriate images attribute.
            image.data_tables['catalog'] = DataTable(data_table=catalog,
                                                     name='CAT')
        except Exception:
            logger.error(logs.format_exception(), image=image)
        return image
Beispiel #10
0
    def do_stage(self, images):
        for i, image in enumerate(images):
            try:
                # Set the number of source pixels to be 5% of the total. This keeps us safe from
                # satellites and airplanes.
                sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

                data = image.data.copy()
                error = (np.abs(data) + image.readnoise**2.0)**0.5
                mask = image.bpm > 0

                # Fits can be backwards byte order, so fix that if need be and subtract
                # the background
                try:
                    bkg = sep.Background(data,
                                         mask=mask,
                                         bw=32,
                                         bh=32,
                                         fw=3,
                                         fh=3)
                except ValueError:
                    data = data.byteswap(True).newbyteorder()
                    bkg = sep.Background(data,
                                         mask=mask,
                                         bw=32,
                                         bh=32,
                                         fw=3,
                                         fh=3)
                bkg.subfrom(data)

                # Do an initial source detection
                # TODO: Add back in masking after we are sure SEP works
                sources = sep.extract(data,
                                      self.threshold,
                                      minarea=self.min_area,
                                      err=error,
                                      deblend_cont=0.005)

                # Convert the detections into a table
                sources = Table(sources)

                # Calculate the ellipticity
                sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

                # Fix any value of theta that are invalid due to floating point rounding
                # -pi / 2 < theta < pi / 2
                sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
                sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

                # Calculate the kron radius
                kronrad, krflag = sep.kron_radius(data, sources['x'],
                                                  sources['y'], sources['a'],
                                                  sources['b'],
                                                  sources['theta'], 6.0)
                sources['flag'] |= krflag
                sources['kronrad'] = kronrad

                # Calcuate the equivilent of flux_auto
                flux, fluxerr, flag = sep.sum_ellipse(data,
                                                      sources['x'],
                                                      sources['y'],
                                                      sources['a'],
                                                      sources['b'],
                                                      np.pi / 2.0,
                                                      2.5 * kronrad,
                                                      subpix=1,
                                                      err=error)
                sources['flux'] = flux
                sources['fluxerr'] = fluxerr
                sources['flag'] |= flag

                # Calculate the FWHMs of the stars:
                fwhm = 2.0 * (np.log(2) *
                              (sources['a']**2.0 + sources['b']**2.0))**0.5
                sources['fwhm'] = fwhm

                # Cut individual bright pixels. Often cosmic rays
                sources = sources[fwhm > 1.0]

                # Measure the flux profile
                flux_radii, flag = sep.flux_radius(data,
                                                   sources['x'],
                                                   sources['y'],
                                                   6.0 * sources['a'],
                                                   [0.25, 0.5, 0.75],
                                                   normflux=sources['flux'],
                                                   subpix=5)
                sources['flag'] |= flag
                sources['fluxrad25'] = flux_radii[:, 0]
                sources['fluxrad50'] = flux_radii[:, 1]
                sources['fluxrad75'] = flux_radii[:, 2]

                # Calculate the windowed positions
                sig = 2.0 / 2.35 * sources['fluxrad50']
                xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'],
                                              sig)
                sources['flag'] |= flag
                sources['xwin'] = xwin
                sources['ywin'] = ywin

                # Calculate the average background at each source
                bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(),
                                                         sources['x'],
                                                         sources['y'],
                                                         sources['a'],
                                                         sources['b'],
                                                         np.pi / 2.0,
                                                         2.5 *
                                                         sources['kronrad'],
                                                         subpix=1)
                #masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
                #                                         sources['a'], sources['b'], np.pi / 2.0,
                #                                         2.5 * kronrad, subpix=1)

                background_area = (
                    2.5 * sources['kronrad']
                )**2.0 * sources['a'] * sources['b'] * np.pi  # - masksum
                sources['background'] = bkgflux
                sources['background'][background_area > 0] /= background_area[
                    background_area > 0]
                # Update the catalog to match fits convention instead of python array convention
                sources['x'] += 1.0
                sources['y'] += 1.0

                sources['xpeak'] += 1
                sources['ypeak'] += 1

                sources['xwin'] += 1.0
                sources['ywin'] += 1.0

                sources['theta'] = np.degrees(sources['theta'])

                image.catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak',
                                        'ypeak', 'flux', 'fluxerr',
                                        'background', 'fwhm', 'a', 'b',
                                        'theta', 'kronrad', 'ellipticity',
                                        'fluxrad25', 'fluxrad50', 'fluxrad75',
                                        'x2', 'y2', 'xy', 'flag']

                # Add the units and description to the catalogs
                image.catalog['x'].unit = 'pixel'
                image.catalog['x'].description = 'X coordinate of the object'
                image.catalog['y'].unit = 'pixel'
                image.catalog['y'].description = 'Y coordinate of the object'
                image.catalog['xwin'].unit = 'pixel'
                image.catalog[
                    'xwin'].description = 'Windowed X coordinate of the object'
                image.catalog['ywin'].unit = 'pixel'
                image.catalog[
                    'ywin'].description = 'Windowed Y coordinate of the object'
                image.catalog['xpeak'].unit = 'pixel'
                image.catalog['xpeak'].description = 'X coordinate of the peak'
                image.catalog['ypeak'].unit = 'pixel'
                image.catalog[
                    'ypeak'].description = 'Windowed Y coordinate of the peak'
                image.catalog['flux'].unit = 'counts'
                image.catalog[
                    'flux'].description = 'Flux within a Kron-like elliptical aperture'
                image.catalog['fluxerr'].unit = 'counts'
                image.catalog[
                    'fluxerr'].description = 'Erronr on the flux within a Kron-like elliptical aperture'
                image.catalog['background'].unit = 'counts'
                image.catalog[
                    'background'].description = 'Average background value in the aperture'
                image.catalog['fwhm'].unit = 'pixel'
                image.catalog['fwhm'].description = 'FWHM of the object'
                image.catalog['a'].unit = 'pixel'
                image.catalog[
                    'a'].description = 'Semi-major axis of the object'
                image.catalog['b'].unit = 'pixel'
                image.catalog[
                    'b'].description = 'Semi-minor axis of the object'
                image.catalog['theta'].unit = 'degrees'
                image.catalog[
                    'theta'].description = 'Position angle of the object'
                image.catalog['kronrad'].unit = 'pixel'
                image.catalog[
                    'kronrad'].description = 'Kron radius used for extraction'
                image.catalog['ellipticity'].description = 'Ellipticity'
                image.catalog['fluxrad25'].unit = 'pixel'
                image.catalog[
                    'fluxrad25'].description = 'Radius containing 25% of the flux'
                image.catalog['fluxrad50'].unit = 'pixel'
                image.catalog[
                    'fluxrad50'].description = 'Radius containing 50% of the flux'
                image.catalog['fluxrad75'].unit = 'pixel'
                image.catalog[
                    'fluxrad75'].description = 'Radius containing 75% of the flux'
                image.catalog['x2'].unit = 'pixel^2'
                image.catalog[
                    'x2'].description = 'Variance on X coordinate of the object'
                image.catalog['y2'].unit = 'pixel^2'
                image.catalog[
                    'y2'].description = 'Variance on Y coordinate of the object'
                image.catalog['xy'].unit = 'pixel^2'
                image.catalog['xy'].description = 'XY covariance of the object'
                image.catalog[
                    'flag'].description = 'Bit mask combination of extraction and photometry flags'

                image.catalog.sort('flux')
                image.catalog.reverse()

                logging_tags = logs.image_config_to_tags(
                    image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename',
                             os.path.basename(image.filename))

                # Save some background statistics in the header
                mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
                image.header['L1MEAN'] = (
                    mean_background,
                    '[counts] Sigma clipped mean of frame background')
                logs.add_tag(logging_tags, 'L1MEAN', float(mean_background))

                median_background = np.median(bkg.back())
                image.header['L1MEDIAN'] = (
                    median_background, '[counts] Median of frame background')
                logs.add_tag(logging_tags, 'L1MEDIAN',
                             float(median_background))

                std_background = stats.robust_standard_deviation(bkg.back())
                image.header['L1SIGMA'] = (
                    std_background,
                    '[counts] Robust std dev of frame background')
                logs.add_tag(logging_tags, 'L1SIGMA', float(std_background))

                # Save some image statistics to the header
                good_objects = image.catalog['flag'] == 0

                seeing = np.median(
                    image.catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing,
                                          '[arcsec] Frame FWHM in arcsec')
                logs.add_tag(logging_tags, 'L1FWHM', float(seeing))

                mean_ellipticity = stats.sigma_clipped_mean(
                    sources['ellipticity'][good_objects], 3.0)
                image.header['L1ELLIP'] = (mean_ellipticity,
                                           'Mean image ellipticity (1-B/A)')
                logs.add_tag(logging_tags, 'L1ELLIP', float(mean_ellipticity))

                mean_position_angle = stats.sigma_clipped_mean(
                    sources['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (
                    mean_position_angle, '[deg] PA of mean image ellipticity')
                logs.add_tag(logging_tags, 'L1ELLIPA',
                             float(mean_position_angle))

                self.logger.info('Extracted sources', extra=logging_tags)

            except Exception as e:
                logging_tags = logs.image_config_to_tags(
                    image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename',
                             os.path.basename(image.filename))
                self.logger.error(e, extra=logging_tags)
        return images