Example #1
0
def subtract_sky(fframe, skymodel):
    """
    skymodel: skymodel object. 
    fframe: frame object to do the sky subtraction, should be already fiber flat fielded
    need same number of fibers and same wavelength grid
    """
    #- Check number of specs
    assert fframe.nspec == skymodel.nspec
    assert fframe.nwave == skymodel.nwave

    #- check same wavelength grid, die if not
    if not np.allclose(fframe.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        raise ValueError(message)

    sflux = fframe.flux - skymodel.flux
    sivar = util.combine_ivar(fframe.ivar.clip(0), skymodel.ivar.clip(0))
    smask = fframe.mask | skymodel.mask
    #- create a frame object now
    sframe = fr.Frame(fframe.wave,
                      sflux,
                      sivar,
                      smask,
                      fframe.resolution_data,
                      meta=fframe.meta,
                      fibermap=fframe.fibermap)
    return sframe
Example #2
0
def subtract_sky(frame, skymodel):
    """Subtract skymodel from frame, altering frame.flux, .ivar, and .mask
    """
    assert frame.nspec == skymodel.nspec
    assert frame.nwave == skymodel.nwave

    log = get_logger()
    log.info("starting")

    # check same wavelength, die if not the case
    if not np.allclose(frame.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        log.error(message)
        raise ValueError(message)

    frame.flux -= skymodel.flux
    frame.ivar = util.combine_ivar(frame.ivar, skymodel.ivar)
    frame.mask |= skymodel.mask

    log.info("done")
Example #3
0
def subtract_sky(frame, skymodel) :
    """Subtract skymodel from frame, altering frame.flux, .ivar, and .mask
    """
    assert frame.nspec == skymodel.nspec
    assert frame.nwave == skymodel.nwave

    log=get_logger()
    log.info("starting")

    # check same wavelength, die if not the case
    if not np.allclose(frame.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        log.error(message)
        raise ValueError(message)

    frame.flux -= skymodel.flux
    frame.ivar = util.combine_ivar(frame.ivar, skymodel.ivar)
    frame.mask |= skymodel.mask

    log.info("done")
Example #4
0
def subtract_sky(fframe,skymodel):
    """
    skymodel: skymodel object. 
    fframe: frame object to do the sky subtraction, should be already fiber flat fielded
    need same number of fibers and same wavelength grid
    """
    #- Check number of specs
    assert fframe.nspec == skymodel.nspec
    assert fframe.nwave == skymodel.nwave

    #- check same wavelength grid, die if not
    if not np.allclose(fframe.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        raise ValueError(message)

    sflux = fframe.flux-skymodel.flux
    sivar = util.combine_ivar(fframe.ivar.clip(0), skymodel.ivar.clip(0))
    smask = fframe.mask | skymodel.mask
    #- create a frame object now
    sframe=fr.Frame(fframe.wave,sflux,sivar,smask,fframe.resolution_data,meta=fframe.meta,fibermap=fframe.fibermap)
    return sframe
Example #5
0
def subtract_sky(fframe, skymodel):
    """
    skymodel: skymodel object. 
    fframe: frame object to do the sky subtraction, should be already fiber flat fielded
    need same number of fibers and same wavelength grid
    """
    #- Check number of specs
    assert fframe.nspec == skymodel.nspec
    assert fframe.nwave == skymodel.nwave

    #- check same wavelength grid, die if not
    if not np.allclose(fframe.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        raise ValueError(message)

    #SK. This wouldn't work since not all properties of the input
    #frame is modified. Just modify input frame directly instead!

    fframe.flux = fframe.flux - skymodel.flux
    fframe.ivar = util.combine_ivar(fframe.ivar.clip(0), skymodel.ivar.clip(0))
    fframe.mask = fframe.mask | skymodel.mask
    #- create a frame object now
    #sframe=fr.Frame(fframe.wave,sflux,sivar,smask,fframe.resolution_data,meta=fframe.meta,fibermap=fframe.fibermap)
    return fframe
Example #6
0
def subtract_sky(fframe,skymodel):
    """
    skymodel: skymodel object. 
    fframe: frame object to do the sky subtraction, should be already fiber flat fielded
    need same number of fibers and same wavelength grid
    """
    #- Check number of specs
    assert fframe.nspec == skymodel.nspec
    assert fframe.nwave == skymodel.nwave

    #- check same wavelength grid, die if not
    if not np.allclose(fframe.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        raise ValueError(message)

    #SK. This wouldn't work since not all properties of the input
    #frame is modified. Just modify input frame directly instead!

    fframe.flux= fframe.flux-skymodel.flux
    fframe.ivar = util.combine_ivar(fframe.ivar.clip(1e-8), skymodel.ivar.clip(1e-8))
    fframe.mask = fframe.mask | skymodel.mask
    #- create a frame object now
    #sframe=fr.Frame(fframe.wave,sflux,sivar,smask,fframe.resolution_data,meta=fframe.meta,fibermap=fframe.fibermap)
    return fframe
Example #7
0
    def test_combine_ivar(self):
        #- input inverse variances with some zeros (1D)
        ivar1 = np.random.uniform(-1, 10, size=200).clip(0)
        ivar2 = np.random.uniform(-1, 10, size=200).clip(0)
        ivar = util.combine_ivar(ivar1, ivar2)
        izero = np.where(ivar1 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        izero = np.where(ivar2 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        self.assertTrue(ivar.dtype == np.float64)

        #- input inverse variances with some zeros (2D)
        np.random.seed(0)
        ivar1 = np.random.uniform(-1, 10, size=(10, 20)).clip(0)
        ivar2 = np.random.uniform(-1, 10, size=(10, 20)).clip(0)
        ivar = util.combine_ivar(ivar1, ivar2)
        izero = np.where(ivar1 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        izero = np.where(ivar2 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        self.assertTrue(ivar.dtype == np.float64)

        #- Dimensionality
        self.assertRaises(AssertionError, util.combine_ivar, ivar1, ivar2[0])

        #- ivar must be positive
        self.assertRaises(AssertionError, util.combine_ivar, -ivar1, ivar2)
        self.assertRaises(AssertionError, util.combine_ivar, ivar1, -ivar2)

        #- does it actually combine them correctly?
        ivar = util.combine_ivar(1, 2)
        self.assertEqual(ivar, 1.0 / (1.0 + 0.5))

        #- float -> float, int -> float, 0-dim ndarray -> 0-dim ndarray
        ivar = util.combine_ivar(1, 2)
        self.assertTrue(isinstance(ivar, float))
        ivar = util.combine_ivar(1.0, 2.0)
        self.assertTrue(isinstance(ivar, float))
        ivar = util.combine_ivar(np.asarray(1.0), np.asarray(2.0))
        self.assertTrue(isinstance(ivar, np.ndarray))
        self.assertEqual(ivar.ndim, 0)
Example #8
0
    def test_combine_ivar(self):
        #- input inverse variances with some zeros (1D)
        ivar1 = np.random.uniform(-1, 10, size=200).clip(0)
        ivar2 = np.random.uniform(-1, 10, size=200).clip(0)
        ivar = util.combine_ivar(ivar1, ivar2)
        izero = np.where(ivar1 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        izero = np.where(ivar2 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        self.assertTrue(ivar.dtype == np.float64)
        
        #- input inverse variances with some zeros (2D)
        np.random.seed(0)
        ivar1 = np.random.uniform(-1, 10, size=(10,20)).clip(0)
        ivar2 = np.random.uniform(-1, 10, size=(10,20)).clip(0)
        ivar = util.combine_ivar(ivar1, ivar2)
        izero = np.where(ivar1 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        izero = np.where(ivar2 == 0)
        self.assertTrue(np.all(ivar[izero] == 0))
        self.assertTrue(ivar.dtype == np.float64)

        #- Dimensionality
        self.assertRaises(AssertionError, util.combine_ivar, ivar1, ivar2[0])

        #- ivar must be positive
        self.assertRaises(AssertionError, util.combine_ivar, -ivar1, ivar2)
        self.assertRaises(AssertionError, util.combine_ivar, ivar1, -ivar2)
        
        #- does it actually combine them correctly?
        ivar = util.combine_ivar(1, 2)
        self.assertEqual(ivar, 1.0/(1.0 + 0.5))
        
        #- float -> float, int -> float, 0-dim ndarray -> 0-dim ndarray
        ivar = util.combine_ivar(1, 2)
        self.assertTrue(isinstance(ivar, float))
        ivar = util.combine_ivar(1.0, 2.0)
        self.assertTrue(isinstance(ivar, float))
        ivar = util.combine_ivar(np.asarray(1.0), np.asarray(2.0))
        self.assertTrue(isinstance(ivar, np.ndarray))
        self.assertEqual(ivar.ndim, 0)
Example #9
0
def _model_variance(frame, cskyflux, cskyivar, skyfibers):
    """look at chi2 per wavelength and increase sky variance to reach chi2/ndf=1
    """

    log = get_logger()

    tivar = util.combine_ivar(frame.ivar[skyfibers], cskyivar[skyfibers])

    # the chi2 at a given wavelength can be large because on a cosmic
    # and not a psf error or sky non uniformity
    # so we need to consider only waves for which
    # a reasonable sky model error can be computed

    # mean sky
    msky = np.mean(cskyflux, axis=0)
    dwave = np.mean(np.gradient(frame.wave))
    dskydw = np.zeros(msky.shape)
    dskydw[1:-1] = (msky[2:] - msky[:-2]) / (frame.wave[2:] - frame.wave[:-2])
    dskydw = np.abs(dskydw)

    # now we consider a worst possible sky model error (20% error on flat, 0.5A )
    max_possible_var = 1. / (tivar +
                             (tivar == 0)) + (0.2 * msky)**2 + (0.5 *
                                                                dskydw)**2

    # exclude residuals inconsistent with this max possible variance (at 3 sigma)
    bad = (frame.flux[skyfibers] -
           cskyflux[skyfibers])**2 > 3**2 * max_possible_var
    tivar[bad] = 0
    ndata = np.sum(tivar > 0, axis=0)
    ok = np.where(ndata > 1)[0]

    chi2 = np.zeros(frame.wave.size)
    chi2[ok] = np.sum(tivar * (frame.flux[skyfibers] - cskyflux[skyfibers])**2,
                      axis=0)[ok] / (ndata[ok] - 1)
    chi2[ndata <= 1] = 1.  # default

    # now we are going to evaluate a sky model error based on this chi2,
    # but only around sky flux peaks (>0.1*max)
    tmp = np.zeros(frame.wave.size)
    tmp = (msky[1:-1] > msky[2:]) * (msky[1:-1] > msky[:-2]) * (
        msky[1:-1] > 0.1 * np.max(msky))
    peaks = np.where(tmp)[0] + 1
    dpix = int(np.ceil(3 / dwave))  # +- n Angstrom around each peak

    skyvar = 1. / (cskyivar + (cskyivar == 0))

    # loop on peaks
    for peak in peaks:
        b = peak - dpix
        e = peak + dpix + 1
        mchi2 = np.mean(chi2[b:e])  # mean reduced chi2 around peak
        mndata = np.mean(ndata[b:e])  # mean number of fibers contributing

        # sky model variance = sigma_flat * msky  + sigma_wave * dmskydw
        sigma_flat = 0.000  # the fiber flat error is already included in the flux ivar
        sigma_wave = 0.005  # A, minimum value
        res2 = (frame.flux[skyfibers, b:e] - cskyflux[skyfibers, b:e])**2
        var = 1. / (tivar[:, b:e] + (tivar[:, b:e] == 0))
        nd = np.sum(tivar[:, b:e] > 0)
        while (sigma_wave < 2):
            pivar = 1. / (var + (sigma_flat * msky[b:e])**2 +
                          (sigma_wave * dskydw[b:e])**2)
            pchi2 = np.sum(pivar * res2) / nd
            if pchi2 <= 1:
                log.info("peak at {}A : sigma_wave={}".format(
                    int(frame.wave[peak]), sigma_wave))
                skyvar[:, b:e] += ((sigma_flat * msky[b:e])**2 +
                                   (sigma_wave * dskydw[b:e])**2)
                break
            sigma_wave += 0.005
    return (cskyivar > 0) / (skyvar + (skyvar == 0))
Example #10
0
def subtract_sky(frame,
                 skymodel,
                 throughput_correction=False,
                 default_throughput_correction=1.):
    """Subtract skymodel from frame, altering frame.flux, .ivar, and .mask

    Args:
        frame : desispec.Frame object
        skymodel : desispec.SkyModel object

    Option:
        throughput_correction : if True, fit for an achromatic throughput correction. This is to absorb variations of Focal Ratio Degradation with fiber flexure.
        default_throughput_correction : float, default value of correction if the fit on sky lines failed.
    """
    assert frame.nspec == skymodel.nspec
    assert frame.nwave == skymodel.nwave

    log = get_logger()
    log.info("starting")

    # check same wavelength, die if not the case
    if not np.allclose(frame.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        log.error(message)
        raise ValueError(message)

    if throughput_correction:
        # need to fit for a multiplicative factor of the sky model
        # before subtraction
        # we are going to use a set of bright sky lines,
        # and fit a multiplicative factor + background around
        # each of them individually, and then combine the results
        # with outlier rejection in case a source emission line
        # coincides with one of the sky lines.

        # it's more robust to have a hardcoded set of sky lines here
        # these are all the sky lines with a flux >5% of the max flux
        # except in b where we add an extra weaker line at 5199.4A
        skyline = np.array([
            5199.4, 5578.4, 5656.4, 5891.4, 5897.4, 6302.4, 6308.4, 6365.4,
            6500.4, 6546.4, 6555.4, 6618.4, 6663.4, 6679.4, 6690.4, 6765.4,
            6831.4, 6836.4, 6865.4, 6925.4, 6951.4, 6980.4, 7242.4, 7247.4,
            7278.4, 7286.4, 7305.4, 7318.4, 7331.4, 7343.4, 7360.4, 7371.4,
            7394.4, 7404.4, 7440.4, 7526.4, 7714.4, 7719.4, 7752.4, 7762.4,
            7782.4, 7796.4, 7810.4, 7823.4, 7843.4, 7855.4, 7862.4, 7873.4,
            7881.4, 7892.4, 7915.4, 7923.4, 7933.4, 7951.4, 7966.4, 7982.4,
            7995.4, 8016.4, 8028.4, 8064.4, 8280.4, 8284.4, 8290.4, 8298.4,
            8301.4, 8313.4, 8346.4, 8355.4, 8367.4, 8384.4, 8401.4, 8417.4,
            8432.4, 8454.4, 8467.4, 8495.4, 8507.4, 8627.4, 8630.4, 8634.4,
            8638.4, 8652.4, 8657.4, 8662.4, 8667.4, 8672.4, 8677.4, 8683.4,
            8763.4, 8770.4, 8780.4, 8793.4, 8829.4, 8835.4, 8838.4, 8852.4,
            8870.4, 8888.4, 8905.4, 8922.4, 8945.4, 8960.4, 8990.4, 9003.4,
            9040.4, 9052.4, 9105.4, 9227.4, 9309.4, 9315.4, 9320.4, 9326.4,
            9340.4, 9378.4, 9389.4, 9404.4, 9422.4, 9442.4, 9461.4, 9479.4,
            9505.4, 9521.4, 9555.4, 9570.4, 9610.4, 9623.4, 9671.4, 9684.4,
            9693.4, 9702.4, 9714.4, 9722.4, 9740.4, 9748.4, 9793.4, 9802.4,
            9814.4, 9820.4
        ])

        sw = []
        swf = []
        sws = []
        sws2 = []
        swsf = []

        # half width of wavelength region around each sky line
        # larger values give a better statistical precision
        # but also a larger sensitivity to source features
        # best solution on one dark night exposure obtained with
        # a half width of 4A.
        hw = 4  #A
        tivar = frame.ivar
        if frame.mask is not None:
            tivar *= (frame.mask == 0)
            tivar *= (skymodel.ivar > 0)

        # we precompute the quantities needed to fit each sky line + continuum
        # the sky "line profile" is the actual sky model
        # and we consider an additive constant
        for line in skyline:
            if line <= frame.wave[0] or line >= frame.wave[-1]: continue
            ii = np.where((frame.wave >= line - hw)
                          & (frame.wave <= line + hw))[0]
            if ii.size < 2: continue
            sw.append(np.sum(tivar[:, ii], axis=1))
            swf.append(np.sum(tivar[:, ii] * frame.flux[:, ii], axis=1))
            swsf.append(
                np.sum(tivar[:, ii] * frame.flux[:, ii] * skymodel.flux[:, ii],
                       axis=1))
            sws.append(np.sum(tivar[:, ii] * skymodel.flux[:, ii], axis=1))
            sws2.append(np.sum(tivar[:, ii] * skymodel.flux[:, ii]**2, axis=1))

        nlines = len(sw)

        for fiber in range(frame.flux.shape[0]):

            # we solve the 2x2 linear system for each fiber and sky line
            # and save the results for each fiber

            coef = []  # list of scale values
            var = []  # list of variance on scale values
            for line in range(nlines):
                if sw[line][fiber] <= 0: continue
                A = np.array([[sw[line][fiber], sws[line][fiber]],
                              [sws[line][fiber], sws2[line][fiber]]])
                B = np.array([swf[line][fiber], swsf[line][fiber]])
                try:
                    Ai = np.linalg.inv(A)
                    X = Ai.dot(B)
                    coef.append(
                        X[1]
                    )  # the scale coef (marginalized over cst background)
                    var.append(Ai[1, 1])
                except:
                    pass

            if len(coef) == 0:
                log.warning("cannot corr. throughput. for fiber %d" % fiber)
                continue

            coef = np.array(coef)
            var = np.array(var)
            ivar = (var > 0) / (var + (var == 0) + 0.005**2)
            ivar_for_outliers = (var > 0) / (var + (var == 0) + 0.02**2)

            # loop for outlier rejection
            failed = False
            for loop in range(50):
                a = np.sum(ivar)
                if a <= 0:
                    log.warning(
                        "cannot corr. throughput. ivar=0 everywhere on sky lines for fiber %d"
                        % fiber)
                    failed = True
                    break

                mcoef = np.sum(ivar * coef) / a
                mcoeferr = 1 / np.sqrt(a)

                nsig = 3.
                chi2 = ivar_for_outliers * (coef - mcoef)**2
                worst = np.argmax(chi2)
                if chi2[worst] > nsig**2 * np.median(
                        chi2[chi2 > 0]):  # with rough scaling of errors
                    #log.debug("discard a bad measurement for fiber %d"%(fiber))
                    ivar[worst] = 0
                    ivar_for_outliers[worst] = 0
                else:
                    break

            if failed:
                continue

            log.info(
                "fiber #%03d throughput corr = %5.4f +- %5.4f (mean fiber flux=%f)"
                % (fiber, mcoef, mcoeferr, np.median(frame.flux[fiber])))
            '''
            if np.abs(mcoef)>0.01 :
                
                print(fiber,"mean coef=",mcoef,"all coef=",coef)
                print(fiber,"all err=",np.sqrt(var))
                print(fiber,"mean coef=",mcoef,"selected coef=",coef[ivar>0])
                print(fiber,"select err=",np.sqrt(var[ivar>0]))
                import matplotlib.pyplot as plt
                x=np.arange(coef.size)
                plt.errorbar(x,coef,np.sqrt(var),fmt="o")
                plt.errorbar(x[ivar>0],coef[ivar>0],np.sqrt(var[ivar>0]),fmt="o")
                plt.axhline(0.)
                plt.axhline(mcoef)
                plt.ylim(-0.11,0.11)
                plt.grid()
                plt.show()
            '''

            if mcoeferr > 0.01:
                log.warning(
                    "throughput corr error = %5.4f is too large for fiber #%03d, do not apply correction"
                    % (mcoeferr, fiber))
                throughput_correction_value = default_throughput_correction
            else:
                throughput_correction_value = mcoef

            # apply this correction to the sky model even if we have not fit it (default can be 1 or 0)
            skymodel.flux[fiber] *= throughput_correction_value

    frame.flux -= skymodel.flux
    frame.ivar = util.combine_ivar(frame.ivar, skymodel.ivar)
    frame.mask |= skymodel.mask

    log.info("done")
Example #11
0
def qa_skysub(param, frame, skymodel, quick_look=False):
    """Calculate QA on SkySubtraction

    Note: Pixels rejected in generating the SkyModel (as above), are
    not rejected in the stats calculated here.  Would need to carry
    along current_ivar to do so.

    Args:
        param : dict of QA parameters
        frame : desispec.Frame object
        skymodel : desispec.SkyModel object
        quick_look : bool, optional
          If True, do QuickLook specific QA (or avoid some)
    Returns:
        qadict: dict of QA outputs
          Need to record simple Python objects for yaml (str, float, int)
    """
    log = get_logger()

    # Output dict
    qadict = {}
    qadict['NREJ'] = int(skymodel.nrej)

    # Grab sky fibers on this frame
    skyfibers = np.where(frame.fibermap['OBJTYPE'] == 'SKY')[0]
    assert np.max(skyfibers) < 500  #- indices, not fiber numbers
    nfibers = len(skyfibers)
    qadict['NSKY_FIB'] = int(nfibers)

    current_ivar = frame.ivar[skyfibers].copy()
    flux = frame.flux[skyfibers]

    # Subtract
    res = flux - skymodel.flux[skyfibers]  # Residuals
    res_ivar = util.combine_ivar(current_ivar, skymodel.ivar[skyfibers])

    # Chi^2 and Probability
    chi2_fiber = np.sum(res_ivar * (res**2), 1)
    chi2_prob = np.zeros(nfibers)
    for ii in range(nfibers):
        # Stats
        dof = np.sum(res_ivar[ii, :] > 0.)
        chi2_prob[ii] = scipy.stats.chisqprob(chi2_fiber[ii], dof)
    # Bad models
    qadict['NBAD_PCHI'] = int(np.sum(chi2_prob < param['PCHI_RESID']))
    if qadict['NBAD_PCHI'] > 0:
        log.warn("Bad Sky Subtraction in {:d} fibers".format(
            qadict['NBAD_PCHI']))

    # Median residual
    qadict['MED_RESID'] = float(np.median(res))  # Median residual (counts)
    log.info("Median residual for sky fibers = {:g}".format(
        qadict['MED_RESID']))

    # Residual percentiles
    perc = dustat.perc(res, per=param['PER_RESID'])
    qadict['RESID_PER'] = [float(iperc) for iperc in perc]

    # Mean Sky Continuum from all skyfibers
    # need to limit in wavelength?

    if quick_look:
        continuum = scipy.ndimage.filters.median_filter(
            flux, 200)  # taking 200 bins (somewhat arbitrarily)
        mean_continuum = np.zeros(flux.shape[1])
        for ii in range(flux.shape[1]):
            mean_continuum[ii] = np.mean(continuum[:, ii])
        qadict['MEAN_CONTIN'] = mean_continuum

    # Median Signal to Noise on sky subtracted spectra
    # first do the subtraction:
    if quick_look:
        fframe = frame  # make a copy
        sskymodel = skymodel  # make a copy
        subtract_sky(fframe, sskymodel)
        medsnr = np.zeros(fframe.flux.shape[0])
        totsnr = np.zeros(fframe.flux.shape[0])
        for ii in range(fframe.flux.shape[0]):
            signalmask = fframe.flux[ii, :] > 0
            # total snr considering bin by bin uncorrelated S/N
            snr = fframe.flux[ii, signalmask] * np.sqrt(
                fframe.ivar[ii, signalmask])
            medsnr[ii] = np.median(snr)
            totsnr[ii] = np.sqrt(np.sum(snr**2))
        qadict['MED_SNR'] = medsnr  # for each fiber
        qadict['TOT_SNR'] = totsnr  # for each fiber

    # Return
    return qadict
Example #12
0
def frame_skyres(outfil, frame, skymodel, qaframe):
    """
    Generate QA plots and files for sky residuals of a given frame

    Parameters
    ----------
    outfil: str
        Name of output file
    frame: Frame object
    skymodel: SkyModel object
    qaframe: QAFrame object
    """

    # Sky fibers
    skyfibers = np.where(frame.fibermap['OBJTYPE'] == 'SKY')[0]
    assert np.max(skyfibers) < 500  #- indices, not fiber numbers

    # Residuals
    res = frame.flux[skyfibers] - skymodel.flux[skyfibers] # Residuals
    res_ivar = util.combine_ivar(frame.ivar[skyfibers], skymodel.ivar[skyfibers])
    med_res = np.median(res,0)

    # Deviates
    gd_res = res_ivar > 0.
    devs = res[gd_res] * np.sqrt(res_ivar[gd_res])

    # Calculations
    wavg_res = np.sum(res*res_ivar,0) / np.sum(res_ivar,0)
    '''
    wavg_ivar = np.sum(res_ivar,0)
    chi2_wavg = np.sum(wavg_res**2 * wavg_ivar)
    dof_wavg = np.sum(wavg_ivar > 0.)
    pchi2_wavg = scipy.stats.chisqprob(chi2_wavg, dof_wavg)
    chi2_med = np.sum(med_res**2 * wavg_ivar)
    pchi2_med = scipy.stats.chisqprob(chi2_med, dof_wavg)
    '''

    # Plot
    fig = plt.figure(figsize=(8, 5.0))
    gs = gridspec.GridSpec(2,2)

    xmin,xmax = np.min(frame.wave), np.max(frame.wave)

    # Simple residual plot
    ax0 = plt.subplot(gs[0,:])
    ax0.plot(frame.wave, med_res, label='Median Res')
    ax0.plot(frame.wave, signal.medfilt(med_res,51), color='black', label='Median**2 Res')
    ax0.plot(frame.wave, signal.medfilt(wavg_res,51), color='red', label='Med WAvgRes')
    #ax_flux.plot(wave, sky_sig, label='Model Error')
    #ax_flux.plot(wave,true_flux*scl, label='Truth')
    #ax_flux.get_xaxis().set_ticks([]) # Suppress labeling

    #
    ax0.plot([xmin,xmax], [0., 0], '--', color='gray')
    ax0.plot([xmin,xmax], [0., 0], '--', color='gray')
    ax0.set_xlabel('Wavelength')
    ax0.set_ylabel('Sky Residuals (Counts)')
    ax0.set_xlim(xmin,xmax)
    ax0.set_xlabel('Wavelength')
    ax0.set_ylabel('Sky Residuals (Counts)')
    ax0.set_xlim(xmin,xmax)
    med0 = np.maximum(np.abs(np.median(med_res)), 1.)
    ax0.set_ylim(-5.*med0, 5.*med0)
    #ax0.text(0.5, 0.85, 'Sky Meanspec',
    #    transform=ax_flux.transAxes, ha='center')

    # Legend
    legend = ax0.legend(loc='upper right', borderpad=0.3,
                        handletextpad=0.3, fontsize='small')

    # Histogram of all residuals
    ax1 = plt.subplot(gs[1,0])
    binsz = 0.1
    xmin,xmax = -5., 5.
    i0, i1 = int( np.min(devs) / binsz) - 1, int( np.max(devs) / binsz) + 1
    rng = tuple( binsz*np.array([i0,i1]) )
    nbin = i1-i0
    # Histogram
    hist, edges = np.histogram(devs, range=rng, bins=nbin)
    xhist = (edges[1:] + edges[:-1])/2.
    #ax.hist(xhist, color='black', bins=edges, weights=hist)#, histtype='step')
    ax1.hist(xhist, color='blue', bins=edges, weights=hist)#, histtype='step')
    # PDF for Gaussian
    area = len(devs) * binsz
    xppf = np.linspace(scipy.stats.norm.ppf(0.0001), scipy.stats.norm.ppf(0.9999), 100)
    ax1.plot(xppf, area*scipy.stats.norm.pdf(xppf), 'r-', alpha=1.0)

    ax1.set_xlabel(r'Res/$\sigma$')
    ax1.set_ylabel('N')
    ax1.set_xlim(xmin,xmax)


    # Meta text
    ax2 = plt.subplot(gs[1,1])
    ax2.set_axis_off()
    show_meta(ax2, qaframe, 'SKYSUB', outfil)
    """
    # Meta
    xlbl = 0.1
    ylbl = 0.85
    i0 = outfil.rfind('/')
    ax2.text(xlbl, ylbl, outfil[i0+1:], color='black', transform=ax2.transAxes, ha='left')
    yoff=0.15
    for key in sorted(qaframe.data['SKYSUB']['QA'].keys()):
        if key in ['QA_FIG']:
            continue
        # Show
        ylbl -= yoff
        ax2.text(xlbl+0.1, ylbl, key+': '+str(qaframe.data['SKYSUB']['QA'][key]),
            transform=ax2.transAxes, ha='left', fontsize='small')
    """


    '''
    # Residuals
    scatt_sz = 0.5
    ax_res = plt.subplot(gs[1])
    ax_res.get_xaxis().set_ticks([]) # Suppress labeling
    res = (sky_model - (true_flux*scl))/(true_flux*scl)
    rms = np.sqrt(np.sum(res**2)/len(res))
    #ax_res.set_ylim(-3.*rms, 3.*rms)
    ax_res.set_ylim(-2, 2)
    ax_res.set_ylabel('Frac Res')
    # Error
    #ax_res.plot(true_wave, 2.*ms_sig/sky_model, color='red')
    ax_res.scatter(wave,res, marker='o',s=scatt_sz)
    ax_res.plot([xmin,xmax], [0.,0], 'g-')
    ax_res.set_xlim(xmin,xmax)

    # Relative to error
    ax_sig = plt.subplot(gs[2])
    ax_sig.set_xlabel('Wavelength')
    sig_res = (sky_model - (true_flux*scl))/sky_sig
    ax_sig.scatter(wave, sig_res, marker='o',s=scatt_sz)
    ax_sig.set_ylabel(r'Res $\delta/\sigma$')
    ax_sig.set_ylim(-5., 5.)
    ax_sig.plot([xmin,xmax], [0.,0], 'g-')
    ax_sig.set_xlim(xmin,xmax)
    '''

    # Finish
    plt.tight_layout(pad=0.1,h_pad=0.0,w_pad=0.0)
    plt.savefig(outfil)
    plt.close()
    print('Wrote QA SkyRes file: {:s}'.format(outfil))
Example #13
0
def subtract_sky(frame, skymodel, throughput_correction = False, default_throughput_correction = 1.) :
    """Subtract skymodel from frame, altering frame.flux, .ivar, and .mask

    Args:
        frame : desispec.Frame object
        skymodel : desispec.SkyModel object

    Option:
        throughput_correction : if True, fit for an achromatic throughput correction. This is to absorb variations of Focal Ratio Degradation with fiber flexure.
        default_throughput_correction : float, default value of correction if the fit on sky lines failed.
    """
    assert frame.nspec == skymodel.nspec
    assert frame.nwave == skymodel.nwave

    log=get_logger()
    log.info("starting")

    # check same wavelength, die if not the case
    if not np.allclose(frame.wave, skymodel.wave):
        message = "frame and sky not on same wavelength grid"
        log.error(message)
        raise ValueError(message)

    if throughput_correction :
        # need to fit for a multiplicative factor of the sky model
        # before subtraction
        # we are going to use a set of bright sky lines,
        # and fit a multiplicative factor + background around
        # each of them individually, and then combine the results
        # with outlier rejection in case a source emission line
        # coincides with one of the sky lines.
        
        # it's more robust to have a hardcoded set of sky lines here
        # these are all the sky lines with a flux >5% of the max flux
        # except in b where we add an extra weaker line at 5199.4A
        skyline=np.array([5199.4,5578.4,5656.4,5891.4,5897.4,6302.4,6308.4,6365.4,6500.4,6546.4,6555.4,6618.4,6663.4,6679.4,6690.4,6765.4,6831.4,6836.4,6865.4,6925.4,6951.4,6980.4,7242.4,7247.4,7278.4,7286.4,7305.4,7318.4,7331.4,7343.4,7360.4,7371.4,7394.4,7404.4,7440.4,7526.4,7714.4,7719.4,7752.4,7762.4,7782.4,7796.4,7810.4,7823.4,7843.4,7855.4,7862.4,7873.4,7881.4,7892.4,7915.4,7923.4,7933.4,7951.4,7966.4,7982.4,7995.4,8016.4,8028.4,8064.4,8280.4,8284.4,8290.4,8298.4,8301.4,8313.4,8346.4,8355.4,8367.4,8384.4,8401.4,8417.4,8432.4,8454.4,8467.4,8495.4,8507.4,8627.4,8630.4,8634.4,8638.4,8652.4,8657.4,8662.4,8667.4,8672.4,8677.4,8683.4,8763.4,8770.4,8780.4,8793.4,8829.4,8835.4,8838.4,8852.4,8870.4,8888.4,8905.4,8922.4,8945.4,8960.4,8990.4,9003.4,9040.4,9052.4,9105.4,9227.4,9309.4,9315.4,9320.4,9326.4,9340.4,9378.4,9389.4,9404.4,9422.4,9442.4,9461.4,9479.4,9505.4,9521.4,9555.4,9570.4,9610.4,9623.4,9671.4,9684.4,9693.4,9702.4,9714.4,9722.4,9740.4,9748.4,9793.4,9802.4,9814.4,9820.4])
        
        
        
        sw=[]
        swf=[]
        sws=[]
        sws2=[]
        swsf=[]
        
        # half width of wavelength region around each sky line
        # larger values give a better statistical precision
        # but also a larger sensitivity to source features
        # best solution on one dark night exposure obtained with
        # a half width of 4A.
        hw=4#A 
        tivar=frame.ivar
        if frame.mask is not None :
            tivar *= (frame.mask==0)
            tivar *= (skymodel.ivar>0)
        
        # we precompute the quantities needed to fit each sky line + continuum
        # the sky "line profile" is the actual sky model
        # and we consider an additive constant
        for line in skyline :
            if line<=frame.wave[0] or line>=frame.wave[-1] : continue            
            ii=np.where((frame.wave>=line-hw)&(frame.wave<=line+hw))[0]
            if ii.size<2 : continue
            sw.append(np.sum(tivar[:,ii],axis=1))
            swf.append(np.sum(tivar[:,ii]*frame.flux[:,ii],axis=1))
            swsf.append(np.sum(tivar[:,ii]*frame.flux[:,ii]*skymodel.flux[:,ii],axis=1))            
            sws.append(np.sum(tivar[:,ii]*skymodel.flux[:,ii],axis=1))
            sws2.append(np.sum(tivar[:,ii]*skymodel.flux[:,ii]**2,axis=1))
        
        nlines=len(sw)
        
        for fiber in range(frame.flux.shape[0]) :

            # we solve the 2x2 linear system for each fiber and sky line
            # and save the results for each fiber

            coef=[] # list of scale values
            var=[] # list of variance on scale values
            for line in range(nlines) :
                if sw[line][fiber]<=0 : continue
                A=np.array([[sw[line][fiber],sws[line][fiber]],[sws[line][fiber],sws2[line][fiber]]])
                B=np.array([swf[line][fiber],swsf[line][fiber]])
                try :
                    Ai=np.linalg.inv(A)
                    X=Ai.dot(B)
                    coef.append(X[1]) # the scale coef (marginalized over cst background)
                    var.append(Ai[1,1])
                except :
                    pass
            
            if len(coef)==0 :
                log.warning("cannot corr. throughput. for fiber %d"%fiber)
                continue
            
            coef=np.array(coef)
            var=np.array(var)
            ivar=(var>0)/(var+(var==0)+0.005**2)
            ivar_for_outliers=(var>0)/(var+(var==0)+0.02**2)
            
            # loop for outlier rejection
            failed=False
            for loop in range(50) :
                a=np.sum(ivar)
                if a <= 0 :
                    log.warning("cannot corr. throughput. ivar=0 everywhere on sky lines for fiber %d"%fiber)
                    failed=True
                    break
                
                mcoef=np.sum(ivar*coef)/a                    
                mcoeferr=1/np.sqrt(a)                
                
                nsig=3.
                chi2=ivar_for_outliers*(coef-mcoef)**2                
                worst=np.argmax(chi2)
                if chi2[worst]>nsig**2*np.median(chi2[chi2>0]) : # with rough scaling of errors
                    #log.debug("discard a bad measurement for fiber %d"%(fiber))
                    ivar[worst]=0
                    ivar_for_outliers[worst]=0
                else :                    
                    break

            if failed :
                continue
                
            log.info("fiber #%03d throughput corr = %5.4f +- %5.4f (mean fiber flux=%f)"%(fiber,mcoef,mcoeferr,np.median(frame.flux[fiber])))
            
            '''
            if np.abs(mcoef)>0.01 :
                
                print(fiber,"mean coef=",mcoef,"all coef=",coef)
                print(fiber,"all err=",np.sqrt(var))
                print(fiber,"mean coef=",mcoef,"selected coef=",coef[ivar>0])
                print(fiber,"select err=",np.sqrt(var[ivar>0]))
                import matplotlib.pyplot as plt
                x=np.arange(coef.size)
                plt.errorbar(x,coef,np.sqrt(var),fmt="o")
                plt.errorbar(x[ivar>0],coef[ivar>0],np.sqrt(var[ivar>0]),fmt="o")
                plt.axhline(0.)
                plt.axhline(mcoef)
                plt.ylim(-0.11,0.11)
                plt.grid()
                plt.show()
            '''
            
            if mcoeferr>0.01 :
                log.warning("throughput corr error = %5.4f is too large for fiber #%03d, do not apply correction"%(mcoeferr,fiber))
                throughput_correction_value = default_throughput_correction
            else :
                throughput_correction_value = mcoef
        
            # apply this correction to the sky model even if we have not fit it (default can be 1 or 0)
            skymodel.flux[fiber] *= throughput_correction_value
    
    frame.flux -= skymodel.flux
    frame.ivar = util.combine_ivar(frame.ivar, skymodel.ivar)
    frame.mask |= skymodel.mask

    log.info("done")
Example #14
0
def _model_variance(frame,cskyflux,cskyivar,skyfibers) :
    """look at chi2 per wavelength and increase sky variance to reach chi2/ndf=1
    """

    log = get_logger()


    tivar = util.combine_ivar(frame.ivar[skyfibers], cskyivar[skyfibers])
        
    # the chi2 at a given wavelength can be large because on a cosmic
    # and not a psf error or sky non uniformity
    # so we need to consider only waves for which 
    # a reasonable sky model error can be computed

    # mean sky
    msky = np.mean(cskyflux,axis=0)
    dwave = np.mean(np.gradient(frame.wave))
    dskydw = np.zeros(msky.shape)
    dskydw[1:-1]=(msky[2:]-msky[:-2])/(frame.wave[2:]-frame.wave[:-2])
    dskydw = np.abs(dskydw)

    # now we consider a worst possible sky model error (20% error on flat, 0.5A )
    max_possible_var = 1./(tivar+(tivar==0)) + (0.2*msky)**2 + (0.5*dskydw)**2

    # exclude residuals inconsistent with this max possible variance (at 3 sigma)
    bad = (frame.flux[skyfibers]-cskyflux[skyfibers])**2 > 3**2*max_possible_var
    tivar[bad]=0
    ndata = np.sum(tivar>0,axis=0)
    ok=np.where(ndata>1)[0]

    chi2  = np.zeros(frame.wave.size)
    chi2[ok] = np.sum(tivar*(frame.flux[skyfibers]-cskyflux[skyfibers])**2,axis=0)[ok]/(ndata[ok]-1)
    chi2[ndata<=1] = 1. # default

    # now we are going to evaluate a sky model error based on this chi2, 
    # but only around sky flux peaks (>0.1*max)
    tmp   = np.zeros(frame.wave.size)
    tmp   = (msky[1:-1]>msky[2:])*(msky[1:-1]>msky[:-2])*(msky[1:-1]>0.1*np.max(msky))
    peaks = np.where(tmp)[0]+1
    dpix  = int(np.ceil(3/dwave)) # +- n Angstrom around each peak

    skyvar = 1./(cskyivar+(cskyivar==0))

    # loop on peaks
    for peak in peaks :
        b=peak-dpix
        e=peak+dpix+1
        mchi2  = np.mean(chi2[b:e]) # mean reduced chi2 around peak
        mndata = np.mean(ndata[b:e]) # mean number of fibers contributing

        # sky model variance = sigma_flat * msky  + sigma_wave * dmskydw
        sigma_flat=0.000 # the fiber flat error is already included in the flux ivar
        sigma_wave=0.005 # A, minimum value                
        res2=(frame.flux[skyfibers,b:e]-cskyflux[skyfibers,b:e])**2
        var=1./(tivar[:,b:e]+(tivar[:,b:e]==0))
        nd=np.sum(tivar[:,b:e]>0)
        while(sigma_wave<2) :
            pivar=1./(var+(sigma_flat*msky[b:e])**2+(sigma_wave*dskydw[b:e])**2)
            pchi2=np.sum(pivar*res2)/nd
            if pchi2<=1 :
                log.info("peak at {}A : sigma_wave={}".format(int(frame.wave[peak]),sigma_wave))
                skyvar[:,b:e] += ( (sigma_flat*msky[b:e])**2 + (sigma_wave*dskydw[b:e])**2 )
                break
            sigma_wave += 0.005
    return (cskyivar>0)/(skyvar+(skyvar==0))
Example #15
0
def qa_skysub(param, frame, skymodel, quick_look=False):
    """Calculate QA on SkySubtraction

    Note: Pixels rejected in generating the SkyModel (as above), are
    not rejected in the stats calculated here.  Would need to carry
    along current_ivar to do so.

    Args:
        param : dict of QA parameters
        frame : desispec.Frame object
        skymodel : desispec.SkyModel object
        quick_look : bool, optional
          If True, do QuickLook specific QA (or avoid some)
    Returns:
        qadict: dict of QA outputs
          Need to record simple Python objects for yaml (str, float, int)
    """
    log=get_logger()

    # Output dict
    qadict = {}
    qadict['NREJ'] = int(skymodel.nrej)

    # Grab sky fibers on this frame
    skyfibers = np.where(frame.fibermap['OBJTYPE'] == 'SKY')[0]
    assert np.max(skyfibers) < 500  #- indices, not fiber numbers
    nfibers=len(skyfibers)
    qadict['NSKY_FIB'] = int(nfibers)

    current_ivar=frame.ivar[skyfibers].copy()
    flux = frame.flux[skyfibers]

    # Subtract
    res = flux - skymodel.flux[skyfibers] # Residuals
    res_ivar = util.combine_ivar(current_ivar, skymodel.ivar[skyfibers])

    # Chi^2 and Probability
    chi2_fiber = np.sum(res_ivar*(res**2),1)
    chi2_prob = np.zeros(nfibers)
    for ii in range(nfibers):
        # Stats
        dof = np.sum(res_ivar[ii,:] > 0.)
        chi2_prob[ii] = scipy.stats.chisqprob(chi2_fiber[ii], dof)
    # Bad models
    qadict['NBAD_PCHI'] = int(np.sum(chi2_prob < param['PCHI_RESID']))
    if qadict['NBAD_PCHI'] > 0:
        log.warn("Bad Sky Subtraction in {:d} fibers".format(
                qadict['NBAD_PCHI']))

    # Median residual
    qadict['MED_RESID'] = float(np.median(res)) # Median residual (counts)
    log.info("Median residual for sky fibers = {:g}".format(
        qadict['MED_RESID']))

    # Residual percentiles
    perc = dustat.perc(res, per=param['PER_RESID'])
    qadict['RESID_PER'] = [float(iperc) for iperc in perc]

    # Mean Sky Continuum from all skyfibers
    # need to limit in wavelength?

    if quick_look:
        continuum=scipy.ndimage.filters.median_filter(flux,200) # taking 200 bins (somewhat arbitrarily)
        mean_continuum=np.zeros(flux.shape[1])
        for ii in range(flux.shape[1]):
            mean_continuum[ii]=np.mean(continuum[:,ii])
        qadict['MEAN_CONTIN'] = mean_continuum

    # Median Signal to Noise on sky subtracted spectra
    # first do the subtraction:
    if quick_look:
        fframe=frame # make a copy
        sskymodel=skymodel # make a copy
        subtract_sky(fframe,sskymodel)
        medsnr=np.zeros(fframe.flux.shape[0])
        totsnr=np.zeros(fframe.flux.shape[0])
        for ii in range(fframe.flux.shape[0]):
            signalmask=fframe.flux[ii,:]>0
            # total snr considering bin by bin uncorrelated S/N
            snr=fframe.flux[ii,signalmask]*np.sqrt(fframe.ivar[ii,signalmask])
            medsnr[ii]=np.median(snr)
            totsnr[ii]=np.sqrt(np.sum(snr**2))
        qadict['MED_SNR']=medsnr  # for each fiber
        qadict['TOT_SNR']=totsnr  # for each fiber

    # Return
    return qadict
Example #16
0
def qa_skysub(param, frame, skymodel, quick_look=False):
    """Calculate QA on SkySubtraction

    Note: Pixels rejected in generating the SkyModel (as above), are
    not rejected in the stats calculated here.  Would need to carry
    along current_ivar to do so.

    Args:
        param : dict of QA parameters
        frame : desispec.Frame object
        skymodel : desispec.SkyModel object
        quick_look : bool, optional
          If True, do QuickLook specific QA (or avoid some)
    Returns:
        qadict: dict of QA outputs
          Need to record simple Python objects for yaml (str, float, int)
    """
    log = get_logger()

    # Output dict
    qadict = {}
    qadict['NREJ'] = int(skymodel.nrej)

    # Grab sky fibers on this frame
    skyfibers = np.where(frame.fibermap['OBJTYPE'] == 'SKY')[0]
    assert np.max(skyfibers) < 500  #- indices, not fiber numbers
    nfibers = len(skyfibers)
    qadict['NSKY_FIB'] = int(nfibers)

    current_ivar = frame.ivar[skyfibers].copy()
    flux = frame.flux[skyfibers]

    # Subtract
    res = flux - skymodel.flux[skyfibers]  # Residuals
    res_ivar = util.combine_ivar(current_ivar, skymodel.ivar[skyfibers])

    # Chi^2 and Probability
    chi2_fiber = np.sum(res_ivar * (res**2), 1)
    chi2_prob = np.zeros(nfibers)
    for ii in range(nfibers):
        # Stats
        dof = np.sum(res_ivar[ii, :] > 0.)
        chi2_prob[ii] = scipy.stats.chisqprob(chi2_fiber[ii], dof)
    # Bad models
    qadict['NBAD_PCHI'] = int(np.sum(chi2_prob < param['PCHI_RESID']))
    if qadict['NBAD_PCHI'] > 0:
        log.warning("Bad Sky Subtraction in {:d} fibers".format(
            qadict['NBAD_PCHI']))

    # Median residual
    qadict['MED_RESID'] = float(np.median(res))  # Median residual (counts)
    log.info("Median residual for sky fibers = {:g}".format(
        qadict['MED_RESID']))

    # Residual percentiles
    perc = dustat.perc(res, per=param['PER_RESID'])
    qadict['RESID_PER'] = [float(iperc) for iperc in perc]

    #- Add per fiber median residuals
    qadict["MED_RESID_FIBER"] = np.median(res, axis=1)

    #- Evaluate residuals in wave axis for quicklook
    if quick_look:

        qadict["MED_RESID_WAVE"] = np.median(res, axis=0)
        qadict["WAVELENGTH"] = frame.wave

    # Return
    return qadict