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
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
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
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)]
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)
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
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
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
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
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