示例#1
0
 def count_badpixels(self, thresh=10000, prescan=False):
     amps = common.valid_amps_list()
     
     if prescan:
         return dict([(amp, int(np.sum(self.prescan_cutouts[amp] > thresh))) for amp in amps])
     else:
         return dict([(amp, int(np.sum(self.overscan_cutouts[amp] > thresh))) for amp in amps])
示例#2
0
def fit_dark_scaling(im, dark_guess_scaled, extname):
    # im needs to be bias-subtracted !!!!!!!!!

    # im should have overscan stripped out
    # dark should have overscan stripped out
  
    # dark should be scaled according to exptime and best guess of
    # T dependence so that the additional scaling fit out by this routine
    # is a perturbation

    sh_im = im.shape
    assert((sh_im[0] == 1032) and (sh_im[1] == 2048))

    sh_dark = dark_guess_scaled.shape
    assert((sh_dark[0] == 1032) and (sh_dark[1] == 2048))

    amps = common.valid_amps_list()

    rescale_factors = np.zeros(4)
    ncalls = np.zeros(4, dtype=int)
    # did optimizer exit successfully or not
    success = np.zeros(4, dtype=bool)

    for i, amp in enumerate(amps):
        res = fit_dark_scaling_1amp(im, dark_guess_scaled, amp, extname)
        x = res.x
        rescale_factors[i] = x[0]
        ncalls[i] = res.nfev
        success[i] = res.success

    return rescale_factors, ncalls, success
示例#3
0
    def __init__(self, image):
        # image should be a 2D numpy array with dimensions
        # 2248 x 1032 in the case of DESI GFA cameras

        par = common.gfa_misc_params()

        sh = image.shape
        assert(sh[0] == par['height_with_prescan_overscan'])
        assert(sh[1] == par['width_with_prescan_overscan'])

        amps = common.valid_amps_list()

        self.overscan_cutouts = {}
        self.prescan_cutouts = {}

        for amp in amps:
            bdy = common.overscan_bdy_coords(amp)
            self.overscan_cutouts[amp] = image[bdy['y_l']:bdy['y_u'], bdy['x_l']:bdy['x_u']]
            bdy = common.prescan_bdy_coords(amp)
            self.prescan_cutouts[amp] = image[bdy['y_l']:bdy['y_u'], bdy['x_l']:bdy['x_u']]

        self.n_badpix_overscan = self.count_badpixels()
        self.n_badpix_prescan = self.count_badpixels(prescan=True)

        # still per-amp but summing prescan and overscan counts together
        self.n_badpix = dict([(amp, self.n_badpix_overscan[amp] + self.n_badpix_prescan[amp]) for amp in amps])

        # including all amps and lumping together prescan and overscan
        self.n_badpix_all = np.sum([n for n in self.n_badpix.values()])

        # units are raw ADU
        self.overscan_medians = dict([(amp, np.median(self.overscan_cutouts[amp])) for amp in amps])

        # units are raw ADU
        self.prescan_medians = dict([(amp, np.median(self.prescan_cutouts[amp])) for amp in amps])
示例#4
0
    def estimate_sky_mag(self, careful_sky=False, flatfielding_on=True):
        # calculate sky brightness in mag per sq asec
        # this is meant to be run on the reduced image in ADU

        assert(self.are_pixels_calibrated(flatfielding_on=flatfielding_on))

        sky_adu_per_pixel = self.estimate_sky_level(careful_sky=careful_sky,
                                                    flatfielding_on=flatfielding_on)

        acttime = self.time_s_for_dark
        
        sky_mag = sky.adu_to_surface_brightness(sky_adu_per_pixel, 
                                                acttime, self.extname)

        if self.sky_level_adu_per_amp is not None:
            amps = common.valid_amps_list()
            self.sky_mag_per_amp = [sky.adu_to_surface_brightness(self.sky_level_adu_per_amp[amp], acttime, self.extname) for amp in amps]

        print(self.extname + ' sky mag per square asec AB : ' +  
              '{:.3f}'.format(sky_mag))

        self.sky_mag = sky_mag

        # calculate sky mag for just upper 8 rows (lumping together amps G and H)
        self.sky_mag_upper()

        return sky_mag
示例#5
0
def prescan_overscan_ccds_table(tab, exp):
    # add information about bad pixels in overscan/prescan to
    # CCDs table, should be useful in identifying reasons for problematic
    # reductions...

    tab['npix_bad_total'] = [
        exp.images[extname].overscan.n_badpix_all for extname in tab['extname']
    ]

    ampnames = common.valid_amps_list()
    npix_bad_per_amp = np.zeros((len(tab), len(ampnames)), dtype=int)
    overscan_medians = np.zeros((len(tab), len(ampnames)), dtype='float32')
    prescan_medians = np.zeros((len(tab), len(ampnames)), dtype='float32')

    for i, t in enumerate(tab):
        npix_bad_per_amp[i, :] = np.array([
            exp.images[t['extname']].overscan.n_badpix[amp] for amp in ampnames
        ])
        overscan_medians[i, :] = np.array([
            exp.images[t['extname']].overscan.overscan_medians[amp]
            for amp in ampnames
        ])
        prescan_medians[i, :] = np.array([
            exp.images[t['extname']].overscan.prescan_medians[amp]
            for amp in ampnames
        ])

    tab['npix_bad_per_amp'] = npix_bad_per_amp
    tab['overscan_medians_adu'] = overscan_medians
    tab['prescan_medians_adu'] = prescan_medians
示例#6
0
def median_zenith_camera_zeropoint(extname):
    # wraps zenith_zeropoint_photometric_1amp
    # eventually want to do a better job of dealing with amp-to-amp
    # zeropoint variation so this is hopefully a temporary hack

    amps = common.valid_amps_list()

    zps = [zenith_zeropoint_photometric_1amp(extname, amp) for amp in amps]

    return np.median(zps)
示例#7
0
    def subtract_bias(self):
        print('Attempting to subtract bias...')
        for extname in self.images.keys():
            if self.images[extname] is not None:
                self.images[extname].image = self.images[extname].image - \
                    load_calibs.read_bias_image(extname)

                amps = common.valid_amps_list()
                for amp in amps:
                    bdy = common.amp_bdy_coords(amp)
                    self.images[extname].image[bdy['y_l']:bdy['y_u'],
                                               bdy['x_l']:bdy['x_u']] -= \
                        self.images[extname].overscan.overscan_medians[amp]

                self.images[extname].bias_subtracted = True
                self.images[extname].calc_variance_e_squared()
示例#8
0
    def estimate_sky_level(self, careful_sky=False, flatfielding_on=True):
        # do something dumb for now, return to this later with something
        # more sophisticated, possibly involving segmentation
        # and/or finding the mode

        assert(self.are_pixels_calibrated(flatfielding_on=flatfielding_on))

        if careful_sky:
            if self.segmap is None:
                self.set_segmap()

            self.sky_level_adu = np.median(self.image[self.segmap.array == 0])
        else:
            self.sky_level_adu = np.median(self.image)
            self.sky_level_adu_per_amp = {}
            for amp in common.valid_amps_list():
                bdy = common.amp_bdy_coords(amp)
                self.sky_level_adu_per_amp[amp] = np.median(self.image[bdy['y_l']:bdy['y_u'], bdy['x_l']:bdy['x_u']])
                

        return self.sky_level_adu
示例#9
0
文件: io.py 项目: desihub/gfa_reduce
def assemble_ccds_table(tab, catalog, exp, outdir, proc_obj, cube_index=None,
                        ps1=None, det_sn_thresh=5.0, sky_mags=True,
                        minimal=False, mjdrange=None):

    nrows = len(tab)

    tab['extname'] = tab['camera']

    tab['contrast'] = [exp.images[extname].header['CONTRAST'] for extname in tab['camera']]

    if minimal:
        return tab

    if sky_mags:
        tab['sky_mag_ab'] = [exp.images[extname].sky_mag for extname in tab['camera']]
        tab['sky_mag_ab_subregion'] = [exp.images[extname].sky_mag_upper for extname in tab['camera']]

    amps = common.valid_amps_list()

    if sky_mags:
        sky_mag_ab_per_amp = np.zeros((nrows, len(amps)), dtype='float32') # 4 amps
        for i, extname in enumerate(tab['camera']):
            for j in range(len(amps)):
                _mag = np.nan if exp.images[extname].sky_mag_per_amp is None else exp.images[extname].sky_mag_per_amp[j]
                sky_mag_ab_per_amp[i, j] = _mag

        tab['sky_mag_ab_per_amp'] = sky_mag_ab_per_amp
    
    tab['petal_loc'] = np.array([common.gfa_extname_to_gfa_number(extname) for extname in tab['camera']], dtype='uint8')

    tab['expid'] = [exp.images[extname].header['EXPID'] for extname in tab['camera']]

    # should work except if early versions of guide cubes lacked
    # MJD information...
    tab['mjd'] = [exp.images[extname].try_retrieve_meta_keyword('MJD-OBS', placeholder=0.0) for extname in tab['camera']]

    eph = util.load_lst()
    tab['lst_deg'] = [util.interp_ephemeris(t['mjd'], eph=eph) for t in tab]
    tab['moon_illumination'] = [util.interp_ephemeris(t['mjd'], eph=eph, colname='MPHASE') for t in tab]

    tab['program'] = [str(exp.images[extname].try_retrieve_meta_keyword('PROGRAM', placeholder='')) for extname in tab['camera']]

    tab['skyra'] = [exp.images[extname].try_retrieve_meta_keyword('SKYRA', placeholder=np.nan) for extname in tab['camera']]

    tab['skydec'] = [exp.images[extname].try_retrieve_meta_keyword('SKYDEC', placeholder=np.nan) for extname in tab['camera']]
    
    # zenith distance using approximate center of the field given by SKYRA, SKYDEC

    tab['zenith_dist_deg'] = [util._zenith_distance(t['skyra'], t['skydec'], t['lst_deg']) for t in tab]
    
    tab['domshutl'] = np.array(nrows*[exp.try_retrieve_header_card('DOMSHUTL', placeholder='')], dtype='U8')
    tab['domshutu'] = np.array(nrows*[exp.try_retrieve_header_card('DOMSHUTU', placeholder='')], dtype='U8')
    tab['pmcover'] = exp.try_retrieve_header_card('PMCOVER', placeholder='')
    tab['moonra'] = exp.try_retrieve_header_card('MOONRA', placeholder=np.nan)
    tab['moondec'] = exp.try_retrieve_header_card('MOONDEC', placeholder=np.nan)

    if np.isnan(tab['moonra'][0]):
        tab['moonra'] = util.interp_ephemeris(tab['mjd'][0], eph=eph,
                                              colname='MOONRA')
    if np.isnan(tab['moondec'][0]):
        tab['moondec'] = util.interp_ephemeris(tab['mjd'][0], eph=eph,
                                               colname='MOONDEC')

    tab['moon_zd_deg'] = util._zenith_distance(tab['moonra'][0],
                                               tab['moondec'][0],
                                               tab['lst_deg'][0])
    
    tab['t_c_for_dark'] = [exp.images[extname].t_c_for_dark for extname in tab['camera']]
    tab['t_c_for_dark_is_guess'] = [int(exp.images[extname].t_c_for_dark_is_guess) for extname in tab['camera']]
    tab['time_s_for_dark'] = [exp.images[extname].time_s_for_dark for extname in tab['camera']]

    tab['night'] = exp.try_retrieve_header_card('NIGHT', placeholder='')

    tab['focus'] = exp.try_retrieve_header_card('FOCUS', placeholder='')

    tab['exptime'] = [exp.images[extname].try_retrieve_meta_keyword('EXPTIME', placeholder=np.nan) for extname in tab['camera']]

    # hack for acquisition images like guide-00074954-0000.fits.fz
    # to make sure their _ccds table ends up listing cube_index 0
    # rather than NaN

    is_0000_acq_file = proc_obj.fname_in.find('-0000.fits.fz') != -1

    if is_0000_acq_file:
        tab['cube_index'] = 0
    else:
        tab['cube_index'] = np.nan if cube_index is None else int(cube_index)

    if cube_index == -1:
        tab['coadd_index_start'] = [exp.images[extname].coadd_index_range[0] for extname in tab['camera']]
        tab['coadd_index_end'] = [exp.images[extname].coadd_index_range[1] for extname in tab['camera']]
        tab['coadd_mjdobs_min'] = [exp.bintables[extname]['MJD-OBS'][ind] for extname, ind in zip(tab['camera'], tab['coadd_index_start'])]
        tab['coadd_mjdobs_max'] =  [exp.bintables[extname]['MJD-OBS'][ind] for extname, ind in zip(tab['camera'], tab['coadd_index_end'])]
    
    tab['racen'] = np.zeros(len(tab), dtype=float)
    tab['deccen'] = np.zeros(len(tab), dtype=float)

    tab['fname_raw'] = proc_obj.fname_in
    tab['gitrev']  = proc_obj.gitrev

    tab['fiber_fracflux'] = [(exp.images[extname].psf.fiber_fracflux if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['fiber_fracflux_elg'] = [(exp.images[extname].psf.fiber_fracflux_elg if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['fiber_fracflux_bgs'] = [(exp.images[extname].psf.fiber_fracflux_bgs if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['n_sources_for_psf'] = [(exp.images[extname].psf.nstars if exp.images[extname].psf is not None else 0) for extname in tab['camera']]
    
    # this pertains to aperture _3 which is 1.5 asec radius
    tab['aper_corr_fac'] = [(exp.images[extname].psf.aper_corr_fac if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['xcentroid_psf'] = [(exp.images[extname].psf.xcen_flux_weighted if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]
    tab['ycentroid_psf'] = [(exp.images[extname].psf.ycen_flux_weighted if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['psf_fwhm_pix'] =  [(exp.images[extname].psf.moffat_fwhm_pix if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['psf_fwhm_asec'] = [(exp.images[extname].psf.moffat_fwhm_asec if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['psf_centroid_cbox'] = [(float(exp.images[extname].psf.cbox) if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    tab['psf_centroid_failed'] =  [(exp.images[extname].psf.psf_centroiding_failed if exp.images[extname].psf is not None else 0) for extname in tab['camera']]

    tab['radprof_fwhm_asec'] =  [(exp.images[extname].psf.radprof_fwhm_asec if exp.images[extname].psf is not None else np.nan) for extname in tab['camera']]

    # is 0 the best placeholder value here when no PSF exists?
    tab['psf_centroiding_flag'] =  [(exp.images[extname].psf.psf_centroiding_flag if exp.images[extname].psf is not None else 0) for extname in tab['camera']]

    tab['psf_asymmetry_ratio'] = [(exp.images[extname].psf.psf_asymmetry_ratio if exp.images[extname].psf is not None else np.float32(np.nan)) for extname in tab['camera']]

    tab['psf_asymmetry_numerator'] = [(exp.images[extname].psf.psf_asymmetry_numerator if exp.images[extname].psf is not None else np.float32(np.nan)) for extname in tab['camera']]

    tab['psf_asymmetry_denominator'] =  [(exp.images[extname].psf.psf_asymmetry_denominator if exp.images[extname].psf is not None else np.float32(np.nan)) for extname in tab['camera']]

    tab['psf_total_flux'] =  [(exp.images[extname].psf.psf_total_flux if exp.images[extname].psf is not None else np.float32(np.nan)) for extname in tab['camera']]

    radprof_ccds_table(tab, exp)

    for i, extname in enumerate(tab['camera']):
        racen, deccen = ccd_center_radec(exp.images[extname].wcs)
        tab['racen'][i] = racen
        tab['deccen'][i] = deccen

    tab['mountha_header'] = exp.try_retrieve_header_card('MOUNTHA', placeholder=np.nan)
    tab['mountdec_header'] = exp.try_retrieve_header_card('MOUNTDEC', placeholder=np.nan)

    tab['ha_deg'] = [util._get_ha(t['skyra'], t['lst_deg'], t['mountdec_header']) for t in tab]

    tab['ha_deg_per_gfa'] = [util._get_ha(t['racen'], t['lst_deg'], t['mountdec_header']) for t in tab]
    
    tab['moon_sep_deg'] = util.moon_separation(tab['moonra'], tab['moondec'],
                                               tab['racen'], tab['deccen'])

    # per-camera zenith distance -- will only be accurate to the extent that
    # each camera's WCS recalibration succeeded
    tab['zd_deg_per_gfa'] = [util._zenith_distance(t['racen'], t['deccen'], t['lst_deg']) for t in tab]

    tab['header_airmass'] = exp.try_retrieve_header_card('AIRMASS', placeholder=np.nan)

    # this should make the airmass properly evolve
    # with time for guide cube outputs
    tab['airmass'] = 1.0/np.cos(tab['zenith_dist_deg']/(180.0/np.pi))

    # this per-camera version of airmass should also evolve
    # properly with time for guide cube outputs
    tab['airmass_per_gfa'] = 1.0/np.cos(tab['zd_deg_per_gfa']/(180.0/np.pi))
    
    tab['zp_adu_per_s'] = [exp.images[extname].compute_zeropoint(ps1) for extname in tab['camera']]

    tab['n_stars_for_zp'] = [(np.sum(ps1['use_for_zp'] & (ps1['extname'] == extname)).astype(int) if 'use_for_zp' in ps1.colnames else 0) for extname in tab['camera']]

    tab['transparency'] = [util.transparency_from_zeropoint(tab[i]['zp_adu_per_s'], tab[i]['airmass_per_gfa'], tab[i]['camera']) for i in range(len(tab))]

    par = common.gfa_misc_params()
    tab['kterm'] = np.float32(par['kterm'])
    tab['fracflux_nominal_pointsource'] = np.float32(par['fracflux_nominal_pointsource'])
    tab['fracflux_nominal_elg'] = np.float32(par['fracflux_nominal_elg'])
    tab['fracflux_nominal_bgs'] = np.float32(par['fracflux_nominal_bgs'])

    tab['det_sn_thresh'] = det_sn_thresh
    
    prescan_overscan_ccds_table(tab, exp)
    high_level_ccds_metrics(tab, catalog, exp)
    astrom_ccds_table(tab, exp)
    dark_current_ccds_table(tab, exp)

    if mjdrange is not None:
        tab['req_mjd_min'] = mjdrange[0]
        tab['req_mjd_max'] = mjdrange[1]

    return tab
示例#10
0
def assemble_ccds_table(tab,
                        catalog,
                        exp,
                        outdir,
                        proc_obj,
                        cube_index=None,
                        ps1=None,
                        det_sn_thresh=5.0,
                        sky_mags=True,
                        minimal=False):

    nrows = len(tab)

    tab['extname'] = tab['camera']

    tab['contrast'] = [
        exp.images[extname].header['CONTRAST'] for extname in tab['camera']
    ]

    if minimal:
        return tab

    if sky_mags:
        tab['sky_mag_ab'] = [
            exp.images[extname].sky_mag for extname in tab['camera']
        ]

    amps = common.valid_amps_list()

    if sky_mags:
        sky_mag_ab_per_amp = np.zeros((nrows, len(amps)),
                                      dtype='float32')  # 4 amps
        for i, extname in enumerate(tab['camera']):
            for j in range(len(amps)):
                _mag = np.nan if exp.images[
                    extname].sky_mag_per_amp is None else exp.images[
                        extname].sky_mag_per_amp[j]
                sky_mag_ab_per_amp[i, j] = _mag

        tab['sky_mag_ab_per_amp'] = sky_mag_ab_per_amp

    tab['petal_loc'] = np.array([
        common.gfa_extname_to_gfa_number(extname) for extname in tab['camera']
    ],
                                dtype='uint8')

    tab['expid'] = [
        exp.images[extname].header['EXPID'] for extname in tab['camera']
    ]

    # should work except if early versions of guide cubes lacked
    # MJD information...
    tab['mjd'] = [
        exp.images[extname].try_retrieve_meta_keyword('MJD-OBS',
                                                      placeholder=0.0)
        for extname in tab['camera']
    ]

    eph = util.load_lst()
    tab['lst_deg'] = [util.interp_lst(t['mjd'], eph=eph) for t in tab]

    tab['program'] = [
        str(exp.images[extname].try_retrieve_meta_keyword('PROGRAM',
                                                          placeholder=''))
        for extname in tab['camera']
    ]

    tab['skyra'] = [
        exp.images[extname].try_retrieve_meta_keyword('SKYRA',
                                                      placeholder=np.nan)
        for extname in tab['camera']
    ]

    tab['skydec'] = [
        exp.images[extname].try_retrieve_meta_keyword('SKYDEC',
                                                      placeholder=np.nan)
        for extname in tab['camera']
    ]

    # zenith distance using approximate center of the field given by SKYRA, SKYDEC

    tab['zenith_dist_deg'] = [
        util._zenith_distance(t['skyra'], t['skydec'], t['lst_deg'])
        for t in tab
    ]

    tab['domshutl'] = np.array(
        nrows * [exp.try_retrieve_header_card('DOMSHUTL', placeholder='')],
        dtype='U8')
    tab['domshutu'] = np.array(
        nrows * [exp.try_retrieve_header_card('DOMSHUTU', placeholder='')],
        dtype='U8')
    tab['pmcover'] = exp.try_retrieve_header_card('PMCOVER', placeholder='')
    tab['moonra'] = exp.try_retrieve_header_card('MOONRA', placeholder=np.nan)
    tab['moondec'] = exp.try_retrieve_header_card('MOONDEC',
                                                  placeholder=np.nan)

    tab['t_c_for_dark'] = [
        exp.images[extname].t_c_for_dark for extname in tab['camera']
    ]
    tab['t_c_for_dark_is_guess'] = [
        int(exp.images[extname].t_c_for_dark_is_guess)
        for extname in tab['camera']
    ]
    tab['time_s_for_dark'] = [
        exp.images[extname].time_s_for_dark for extname in tab['camera']
    ]

    tab['night'] = exp.try_retrieve_header_card('NIGHT', placeholder='')

    tab['focus'] = exp.try_retrieve_header_card('FOCUS', placeholder='')

    tab['exptime'] = [
        exp.images[extname].try_retrieve_meta_keyword('EXPTIME',
                                                      placeholder=np.nan)
        for extname in tab['camera']
    ]

    tab['cube_index'] = np.nan if cube_index is None else int(cube_index)

    if cube_index == -1:
        tab['coadd_index_start'] = [
            exp.images[extname].coadd_index_range[0]
            for extname in tab['camera']
        ]
        tab['coadd_index_end'] = [
            exp.images[extname].coadd_index_range[1]
            for extname in tab['camera']
        ]

    tab['racen'] = np.zeros(len(tab), dtype=float)
    tab['deccen'] = np.zeros(len(tab), dtype=float)

    tab['fname_raw'] = proc_obj.fname_in
    tab['gitrev'] = proc_obj.gitrev

    tab['fiber_fracflux'] = [(exp.images[extname].psf.fiber_fracflux if
                              exp.images[extname].psf is not None else np.nan)
                             for extname in tab['camera']]

    tab['n_sources_for_psf'] = [(exp.images[extname].psf.nstars
                                 if exp.images[extname].psf is not None else 0)
                                for extname in tab['camera']]

    # this pertains to aperture _3 which is 1.5 asec radius
    tab['aper_corr_fac'] = [(exp.images[extname].psf.aper_corr_fac if
                             exp.images[extname].psf is not None else np.nan)
                            for extname in tab['camera']]

    tab['xcentroid_psf'] = [(exp.images[extname].psf.xcen_flux_weighted if
                             exp.images[extname].psf is not None else np.nan)
                            for extname in tab['camera']]
    tab['ycentroid_psf'] = [(exp.images[extname].psf.ycen_flux_weighted if
                             exp.images[extname].psf is not None else np.nan)
                            for extname in tab['camera']]

    tab['psf_fwhm_pix'] = [(exp.images[extname].psf.moffat_fwhm_pix
                            if exp.images[extname].psf is not None else np.nan)
                           for extname in tab['camera']]

    tab['psf_fwhm_asec'] = [(exp.images[extname].psf.moffat_fwhm_asec if
                             exp.images[extname].psf is not None else np.nan)
                            for extname in tab['camera']]

    tab['psf_centroid_cbox'] = [
        (float(exp.images[extname].psf.cbox)
         if exp.images[extname].psf is not None else np.nan)
        for extname in tab['camera']
    ]

    tab['psf_centroid_failed'] = [
        (exp.images[extname].psf.psf_centroiding_failed
         if exp.images[extname].psf is not None else 0)
        for extname in tab['camera']
    ]

    for i, extname in enumerate(tab['camera']):
        racen, deccen = ccd_center_radec(exp.images[extname].wcs)
        tab['racen'][i] = racen
        tab['deccen'][i] = deccen

    tab['mountha_header'] = exp.try_retrieve_header_card('MOUNTHA',
                                                         placeholder=np.nan)
    tab['mountdec_header'] = exp.try_retrieve_header_card('MOUNTDEC',
                                                          placeholder=np.nan)

    tab['ha_deg'] = [
        util._get_ha(t['skyra'], t['lst_deg'], t['mountdec_header'])
        for t in tab
    ]

    tab['ha_deg_per_gfa'] = [
        util._get_ha(t['racen'], t['lst_deg'], t['mountdec_header'])
        for t in tab
    ]

    tab['moon_sep_deg'] = util.moon_separation(tab['moonra'], tab['moondec'],
                                               tab['racen'], tab['deccen'])

    # per-camera zenith distance -- will only be accurate to the extent that
    # each camera's WCS recalibration succeeded
    tab['zd_deg_per_gfa'] = [
        util._zenith_distance(t['racen'], t['deccen'], t['lst_deg'])
        for t in tab
    ]

    tab['header_airmass'] = exp.try_retrieve_header_card('AIRMASS',
                                                         placeholder=np.nan)

    # this should make the airmass properly evolve
    # with time for guide cube outputs
    tab['airmass'] = 1.0 / np.cos(tab['zenith_dist_deg'] / (180.0 / np.pi))

    # this per-camera version of airmass should also evolve
    # properly with time for guide cube outputs
    tab['airmass_per_gfa'] = 1.0 / np.cos(tab['zd_deg_per_gfa'] /
                                          (180.0 / np.pi))

    tab['zp_adu_per_s'] = [
        exp.images[extname].compute_zeropoint(ps1) for extname in tab['camera']
    ]

    tab['transparency'] = [
        util.transparency_from_zeropoint(tab[i]['zp_adu_per_s'],
                                         tab[i]['airmass_per_gfa'],
                                         tab[i]['camera'])
        for i in range(len(tab))
    ]

    tab['det_sn_thresh'] = det_sn_thresh

    prescan_overscan_ccds_table(tab, exp)
    high_level_ccds_metrics(tab, catalog, exp)
    astrom_ccds_table(tab, exp)
    dark_current_ccds_table(tab, exp)

    return tab