コード例 #1
0
ファイル: Psf.py プロジェクト: RobertLuptonTheGood/afw
    def testKernelPsf(self):
        """Test creating a Psf from a Kernel"""

        x,y = 10.4999, 10.4999
        ksize = 15
        sigma1 = 1
        #
        # Make a PSF from that kernel
        #
        kPsf = afwDetect.createPsf("Kernel",
                                   afwMath.AnalyticKernel(ksize, ksize,
                                                          afwMath.GaussianFunction2D(sigma1, sigma1)))

        kIm = kPsf.computeImage(afwGeom.Point2D(x, y))
        #
        # And now via the dgPsf model
        #
        dgPsf = afwDetect.createPsf("DoubleGaussian", ksize, ksize, sigma1)
        dgIm = dgPsf.computeImage(afwGeom.Point2D(x, y))
        #
        # Check that they're the same
        #
        diff = type(kIm)(kIm, True); diff -= dgIm
        stats = afwMath.makeStatistics(diff, afwMath.MAX | afwMath.MIN)
        self.assertAlmostEqual(stats.getValue(afwMath.MAX), 0.0, places=16)
        self.assertAlmostEqual(stats.getValue(afwMath.MIN), 0.0, places=16)

        if display:
            mos = displayUtils.Mosaic()
            mos.setBackground(-0.1)
            ds9.mtv(mos.makeMosaic([kIm, dgIm, diff], mode="x"), frame=1)
コード例 #2
0
ファイル: Psf.py プロジェクト: RobertLuptonTheGood/afw
    def testInvalidDgPsf(self):
        """Test parameters of dgPsfs, both valid and not"""
        sigma1, sigma2, b = 1, 0, 0                     # sigma2 may be 0 iff b == 0
        afwDetect.createPsf("DoubleGaussian", self.ksize, self.ksize, sigma1, sigma2, b)

        def badSigma1():
            sigma1 = 0
            afwDetect.createPsf("DoubleGaussian", self.ksize, self.ksize, sigma1, sigma2, b)

        utilsTests.assertRaisesLsstCpp(self, pexExceptions.DomainErrorException, badSigma1)

        def badSigma2():
            sigma2, b = 0, 1
            afwDetect.createPsf("DoubleGaussian", self.ksize, self.ksize, sigma1, sigma2, b)

        utilsTests.assertRaisesLsstCpp(self, pexExceptions.DomainErrorException, badSigma2)
コード例 #3
0
 def run(self, exposure, wcs, maxBBox=None, destBBox=None):
     """PSF-match exposure (if self.config.desiredFwhm is not None) and warp
     
     @param[in,out] exposure: exposure to preprocess; PSF matching is done in place
     @param[in] wcs: desired WCS of temporary images
     @param maxBBox: maximum allowed parent bbox of warped exposure (an afwGeom.Box2I or None);
         if None then the warped exposure will be just big enough to contain all warped pixels;
         if provided then the warped exposure may be smaller, and so missing some warped pixels;
         ignored if destBBox is not None
     @param destBBox: exact parent bbox of warped exposure (an afwGeom.Box2I or None);
         if None then maxBBox is used to determine the bbox, otherwise maxBBox is ignored
     
     @return a pipe_base Struct containing:
     - exposure: processed exposure
     """
     if self.config.desiredFwhm is not None:
         self.log.info("PSF-match exposure")
         fwhmPixels = self.config.desiredFwhm / wcs.pixelScale().asArcseconds()
         kernelDim = exposure.getPsf().getKernel().getDimensions()
         coreSigma = fwhmPixels / FwhmPerSigma
         modelPsf = afwDetection.createPsf("DoubleGaussian", kernelDim[0], kernelDim[1],
             coreSigma, coreSigma * 2.5, 0.1)
         exposure = self.psfMatch.run(exposure, modelPsf).psfMatchedExposure
     self.log.info("Warp exposure")
     with self.timer("warp"):
         exposure = self.warper.warpExposure(wcs, exposure, maxBBox=maxBBox, destBBox=destBBox)
     
     return pipeBase.Struct(
         exposure = exposure,
     )
コード例 #4
0
ファイル: PsfIo.py プロジェクト: RayPlante/DMS.afw
 def setUp(self):
     self.ksize = 25                      # size of desired kernel
     FWHM = 5
     self.sigma1 = FWHM/(2*sqrt(2*log(2)))
     self.sigma2 = 2*self.sigma1
     self.b = 0.1
     self.psf = roundTripPsf(1,
                             afwDetect.createPsf("DoubleGaussian",
                                                 self.ksize, self.ksize, self.sigma1, self.sigma2, self.b))
コード例 #5
0
ファイル: run-lsst.py プロジェクト: barentsen/tractor
def getFakePsf(pixscale):
	#fwhmarcsec = 0.7 #1.0 #0.5
	fwhmarcsec = 1.0
	fwhm = fwhmarcsec / pixscale
	print 'fwhm', fwhm
	psfsize = 25
	model = 'DoubleGaussian'
	sig = fwhm/(2.*math.sqrt(2.*math.log(2.)))
	print 'sigma', sig
	psf = afwDet.createPsf(model, psfsize, psfsize, sig, 0., 0.)
	print 'psf', psf
	return psf
コード例 #6
0
def getFakePsf(pixscale):
    #fwhmarcsec = 0.7 #1.0 #0.5
    fwhmarcsec = 1.0
    fwhm = fwhmarcsec / pixscale
    print 'fwhm', fwhm
    psfsize = 25
    model = 'DoubleGaussian'
    sig = fwhm / (2. * math.sqrt(2. * math.log(2.)))
    print 'sigma', sig
    psf = afwDet.createPsf(model, psfsize, psfsize, sig, 0., 0.)
    print 'psf', psf
    return psf
コード例 #7
0
ファイル: Psf.py プロジェクト: RobertLuptonTheGood/afw
    def testComputeImage3(self):
        """Test the computation of the PSF's image at a point for non-native sizes"""
        #
        # First an analytic Kernel
        #
        ksize = 15
        aPsf = afwDetect.createPsf("Kernel",
                                   afwMath.AnalyticKernel(ksize, ksize, afwMath.GaussianFunction2D(1, 1)))
        #
        # Then an image-based Kernel
        #
        iPsf = afwDetect.createPsf("Kernel", afwMath.FixedKernel(aPsf.computeImage()))

        for dy in range(-1, 2):
            for dx in range(-1, 2):
                dimen = afwGeom.Extent2I(ksize + 2*dx, ksize + 2*dy)

                aIm = aPsf.computeImage(dimen)
                self.assertTrue(aIm.getDimensions() == dimen)

                iIm = iPsf.computeImage(dimen)
                self.assertTrue(iIm.getDimensions() == dimen)
コード例 #8
0
ファイル: coaddBase.py プロジェクト: mjuric/lsst-pipe_tasks
 def makeModelPsf(self, fwhmPixels, kernelDim):
     """Construct a model PSF, or reuse the prior model, if possible
     
     The model PSF is a double Gaussian with core FWHM = fwhmPixels
     and wings of amplitude 1/10 of core and FWHM = 2.5 * core.
     
     @param fwhmPixels: desired FWHM of core Gaussian, in pixels
     @param kernelDim: desired dimensions of PSF kernel, in pixels
     @return model PSF
     """
     self.log.logdebug("Create double Gaussian PSF model with core fwhm %0.1f pixels and size %dx%d" % \
         (fwhmPixels, kernelDim[0], kernelDim[1]))
     coreSigma = fwhmPixels / FwhmPerSigma
     return afwDetection.createPsf("DoubleGaussian", kernelDim[0], kernelDim[1],
         coreSigma, coreSigma * 2.5, 0.1)
コード例 #9
0
ファイル: exposure.py プロジェクト: RayPlante/DMS.afw
    def testCopyExposure(self):
        """Copy an Exposure (maybe changing type)"""

        exposureU = afwImage.ExposureU(inFilePathSmall)
        exposureU.setWcs(self.wcs)
        exposureU.setDetector(cameraGeom.Detector(cameraGeom.Id(666)))
        exposureU.setFilter(afwImage.Filter("g"))
        exposureU.getCalib().setExptime(666)
        exposureU.setPsf(afwDetection.createPsf("DoubleGaussian", 11, 11, 1))

        exposureF = exposureU.convertF()
        self.cmpExposure(exposureF, exposureU)

        nexp = exposureF.Factory(exposureF, False)
        self.cmpExposure(exposureF, nexp)
コード例 #10
0
ファイル: calibrate.py プロジェクト: mjuric/lsst-pipe_tasks
    def installInitialPsf(self, exposure):
        """Initialise the calibration procedure by setting the PSF to a configuration-defined guess.

        @param[in,out] exposure Exposure to process; fake PSF will be installed here.
        """
        assert exposure, "No exposure provided"
        
        wcs = exposure.getWcs()
        assert wcs, "No wcs in exposure"

        model = self.config.initialPsf.model
        fwhm = self.config.initialPsf.fwhm / wcs.pixelScale().asArcseconds()
        size = self.config.initialPsf.size
        self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels" % (fwhm, size))
        psf = afwDet.createPsf(model, size, size, fwhm/(2*math.sqrt(2*math.log(2))))
        exposure.setPsf(psf)
コード例 #11
0
ファイル: interpImage.py プロジェクト: mjuric/lsst-pipe_tasks
    def interpolateOnePlane(self, maskedImage, planeName, fwhmPixels):
        """Interpolate over one mask plane, in place

        @param[in,out] maskedImage: MaskedImage over which to interpolate over edge pixels
        @param[in] fwhmPixels: FWHM of double Gaussian model to use for interpolation (pixels)
        @param[in] planeName: mask plane over which to interpolate
        @param[in] PSF to use to detect NaNs
        """
        self.log.info("Interpolate over %s pixels" % (planeName,))
        kernelSize = int(round(fwhmPixels * self.config.interpKernelSizeFactor))
        kernelDim = afwGeom.Point2I(kernelSize, kernelSize)
        coreSigma = fwhmPixels / FwhmPerSigma
        psfModel = afwDetection.createPsf("DoubleGaussian", kernelDim[0], kernelDim[1],
            coreSigma, coreSigma * 2.5, 0.1)

        nanDefectList = ipIsr.getDefectListFromMask(maskedImage, planeName, growFootprints=0)
        measAlg.interpolateOverDefects(maskedImage, psfModel, nanDefectList, 0.0)
コード例 #12
0
def findCosmicRays(exposure, crRejectPolicy, defaultFwhm, keepCRs):
    """defaultFwhm is in arcsec"""

    mi = exposure.getMaskedImage()
    wcs = exposure.getWcs()

    scale = wcs.pixelScale().asArcseconds()
    defaultFwhm /= scale  # convert to pixels
    ksize = 4 * int(defaultFwhm) + 1

    psf = afwDetection.createPsf(
        'DoubleGaussian', ksize, ksize,
        defaultFwhm / (2 * math.sqrt(2 * math.log(2))))

    bg = afwMath.makeStatistics(mi, afwMath.MEDIAN).getValue()
    crs = measAlg.findCosmicRays(mi, psf, bg, crRejectPolicy, keepCRs)

    return crs
コード例 #13
0
ファイル: calibrate.py プロジェクト: lsst-dm/legacy-pipette
    def fakePsf(self, exposure):
        """Initialise the calibration procedure by setting the PSF and WCS

        @param exposure Exposure to process
        @return PSF, WCS
        """
        assert exposure, "No exposure provided"

        wcs = exposure.getWcs()
        assert wcs, "No wcs in exposure"

        calibrate = self.config['calibrate']
        model = calibrate['model']
        fwhm = calibrate['fwhm'] / wcs.pixelScale().asArcseconds()
        size = calibrate['size']
        psf = afwDet.createPsf(model, size, size,
                               fwhm / (2 * math.sqrt(2 * math.log(2))))
        return psf, wcs
コード例 #14
0
ファイル: snapCombine.py プロジェクト: mjuric/lsst-pipe_tasks
    def makeInitialPsf(self, exposure, fwhmPix=None):
        """Initialise the detection procedure by setting the PSF and WCS

        @param exposure Exposure to process
        @return PSF, WCS
        """
        assert exposure, "No exposure provided"
        wcs = exposure.getWcs()
        assert wcs, "No wcs in exposure"
        
        if fwhmPix is None:
            fwhmPix = self.config.initialPsf.fwhm / wcs.pixelScale().asArcseconds()
            
        size = self.config.initialPsf.size
        model = self.config.initialPsf.model
        self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels" % (fwhmPix, size))
        psf = afwDet.createPsf(model, size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
        return psf
コード例 #15
0
ファイル: PsfIo.py プロジェクト: RayPlante/DMS.afw
    def testKernel(self):
        """Test the creation of the Psf's kernel"""

        kIm = self.psf.computeImage()

        if False:
            ds9.mtv(kIm)        

        self.assertTrue(kIm.getWidth() == self.ksize)
        #
        # Check that the image is as expected
        #
        dgPsf = afwDetect.createPsf("DoubleGaussian",
                                    self.ksize, self.ksize, self.sigma1, self.sigma2, self.b)
        dgIm = dgPsf.computeImage()
        #
        # Check that they're the same
        #
        diff = type(kIm)(kIm, True); diff -= dgIm
        stats = afwMath.makeStatistics(diff, afwMath.MAX | afwMath.MIN)
        self.assertEqual(stats.getValue(afwMath.MAX), 0.0)
        self.assertEqual(stats.getValue(afwMath.MIN), 0.0)
コード例 #16
0
    def testPsfDistortion(self):

	#distorter = cameraGeom.NullDistortion() #self.sCamDistorter #Exag
	distorter = self.sCamDistorter

	# set the psf
	kwid = 55
	psfSigma = 4.5
	psf = afwDet.createPsf("DoubleGaussian", kwid, kwid, psfSigma, psfSigma, 0.0)

        
	# create a detector which is offset from the boresight
        pixelSize = 0.01 # mm
        allPixels = afwGeom.BoxI(afwGeom.PointI(0, 0), afwGeom.ExtentI(self.nx, self.ny))
        detector = cameraUtils.makeDefaultCcd(allPixels, pixelSize=pixelSize)
        detector.setCenterPixel(afwGeom.Point2D(self.nx/2, self.ny/2))
        # try the upper right corner of chip 0 on suprimecam
        cenPixX, cenPixY = 5000.0, 4000.0
        detector.setCenter(cameraGeom.FpPoint(cenPixX*pixelSize, cenPixY*pixelSize))
        
        detector.setDistortion(distorter)
        psf.setDetector(detector)

        
	settings = {'scale': 'minmax', 'zoom':"to fit", 'mask':'transparency 80'}

        # use a point in the middle of the test image
        x = self.nx//2
        y = self.ny//2
        p = afwGeom.Point2D(x,y) # this is our **measured** coordinate
        pOrig = distorter.undistort(p, detector)  # this is where p would be without optical distortion

        ########################################################
        # check that the camera behaves as expected
        pos = detector.getPositionFromPixel(p)
        pix = detector.getPixelFromPosition(pos)
        print "posmm, pospix, pix", pos.getMm(), pos.getPixels(detector.getPixelSize()), pix
        posPix = pos.getPixels(detector.getPixelSize())
        # note that p is in the center of the ccd
        self.assertEqual(posPix.getX(), cenPixX)
        self.assertEqual(posPix.getY(), cenPixY)
        self.assertEqual(pix.getX(), x)
        self.assertEqual(pix.getY(), y)


        ########################################################
        # compare the measured shear in a psf image to the expected value
        
        # get the expected shear at p
        q = distorter.distort(pOrig, geomEllip.Quadrupole(), detector)
        ax = geomEllip.Axes(q)
        aKnown = ax.getA()
        bKnown = ax.getB()
        thetaKnown = ax.getTheta()*180.0/math.pi
        print "Shear at p: ", ax, thetaKnown


        # make a plain PSF
        doDistort = False # the default is True
        psfImg               = psf.computeImage(p, True, doDistort)

        # compute a PSF at p
        psfImgDistInternally = psf.computeImage(p)

        # make a plain one and distort it ourselves
        # --> note that we use the undistorted pOrig ... that's where p was before the optics
        #psfImgOrig = psf.computeImage(pOrig, True, doDistort)
        #psfImgDistByUs       = distorter.distort(pOrig, psfImgOrig, detector, 0.0)
        #shift = p - afwGeom.Extent2D(pOrig)
        #afwMath.offsetImage(psfImgDistByUs, shift.getX(), shift.getY(), "lanczos5", 5) 
        psfImgDistByUs       = distorter.distort(p, psfImg, detector, 0.0)

        # to display, we'll trim off the edge of the original so it's the same size as the distorted.
        wid2 = psfImgDistInternally.getWidth()
        edge = (psfImg.getWidth() - wid2)/2
        box = afwGeom.Box2I(afwGeom.Point2I(edge, edge), afwGeom.Extent2I(wid2,wid2))
        if display:
            ds9.mtv(afwImage.ImageD(psfImg, box), frame=1, title="psf", settings=settings)
            ds9.mtv(psfImgDistInternally, frame=2, title="psfDist", settings=settings)
            ds9.mtv(afwImage.ImageD(psfImgDistByUs, box), frame=3, title="psfDist2", settings=settings)

        # first make sure we can plant a known quantity and measure it
        # quickAndDirtyShape() must be tested to be used itself as a tester
        sigma = 1.0
        img = plantEllipse(kwid, kwid, sigma, sigma, 0.0)
        a, b, theta, ixx, iyy, ixy = quickAndDirtyShape(img, afwGeom.Point2D(kwid/2,kwid/2))
        print "planted:", a/sigma, b/sigma, theta, ixx/sigma**2, iyy/sigma**2, ixy/sigma**2
        prec = 6
        self.assertAlmostEqual(a, sigma, prec)
        self.assertAlmostEqual(b, sigma, prec)
        self.assertAlmostEqual(ixx, sigma**2, prec)
        self.assertAlmostEqual(iyy, sigma**2, prec)
        
        # try 4% shear along theta=45
        shear = 1.04
        q = geomEllip.Quadrupole(geomEllip.Axes(shear*sigma, sigma, math.pi/4.0))
        img = plantEllipse(kwid, kwid, q.getIxx(), q.getIyy(), q.getIxy())
        a, b, theta, ixx, iyy, ixy = quickAndDirtyShape(img, afwGeom.Point2D(kwid/2,kwid/2))
        print "sheared 4%:", a/sigma, b/sigma, theta, ixx/sigma**2, iyy/sigma**2, ixy/sigma**2
        self.assertAlmostEqual(a, shear*sigma, prec)
        self.assertAlmostEqual(b, sigma, prec)
        self.assertAlmostEqual(theta, 45.0, prec)
        

        # now use quickAndDirty to measure the PSFs we created
        a, b, theta, ixx, iyy, ixy = quickAndDirtyShape(psfImg, p)
        print "psfImg:", a/psfSigma, b/psfSigma, theta, ixx/psfSigma**2, iyy/psfSigma**2, ixy/psfSigma**2
        self.assertAlmostEqual(a, psfSigma, prec)
        self.assertAlmostEqual(b, psfSigma, prec)

        
        print "known Theta = ", thetaKnown
        a, b, theta, ixx, iyy, ixy = quickAndDirtyShape(psfImgDistInternally, p)
        print "warpIntern:", a/psfSigma, b/psfSigma, theta, ixx/psfSigma**2, iyy/psfSigma**2, ixy/psfSigma**2
        self.assertTrue(abs(a/psfSigma - aKnown) < 0.01)
        self.assertTrue(abs(b/psfSigma - bKnown) < 0.01)
        self.assertTrue(abs(theta - thetaKnown) < 0.5) # half a degree

        a, b, theta, ixx, iyy, ixy = quickAndDirtyShape(psfImgDistByUs, p)
        print "warpExtern:", a/psfSigma, b/psfSigma, theta, ixx/psfSigma**2, iyy/psfSigma**2, ixy/psfSigma**2
        self.assertTrue(abs(a/psfSigma - aKnown) < 0.01)
        self.assertTrue(abs(b/psfSigma - bKnown) < 0.01)
        self.assertTrue(abs(theta - thetaKnown) < 0.5)
コード例 #17
0
ファイル: Psf.py プロジェクト: RobertLuptonTheGood/afw
 def setUp(self):
     FWHM = 5
     self.ksize = 25                      # size of desired kernel
     self.psf = afwDetect.createPsf("DoubleGaussian", self.ksize, self.ksize,
                                    FWHM/(2*sqrt(2*log(2))), 1, 0.1)
コード例 #18
0
ファイル: coadd.py プロジェクト: lsst-dm/legacy-pipette
def coadd(idList, butler, desFwhm, coaddWcs, coaddBBox, policy):
    """PSF-match (if desFwhm is specified), warp and coadd images
    
    PSF matching is to a double gaussian model with core FWHM = desFwhm
    and wings of amplitude 1/10 of core and FWHM = 2.5 * core.
    The size of the PSF matching kernel is the same as the size of the kernel
    found in the first calibrated science exposure, since there is no benefit
    to making it any other size.
    
    PSF-matching is performed before warping so the code can use the PSF models
    associated with the calibrated science exposures (without having to warp those models).

    @param[in] idList: list of data identity dictionaries
    @param[in] butler: data butler for input images
    @param[in] desFwhm: desired PSF of coadd, but in science exposure pixels
                (the coadd usually has a different scale!);
                if 0 then no PSF matching is performed.
    @param[in] coaddWcs: WCS for coadd
    @param[in] coaddBBox: bounding box for coadd
    @param[in] policy: a Policy object that must contain these policies:
        psfMatchPolicy: see ip_diffim/policy/PsfMatchingDictionary.paf (may omit if desFwhm <= 0)
        warpPolicy: see afw/policy/WarpDictionary.paf
        coaddPolicy: see coadd_utils/policy/CoaddDictionary.paf
    @output:
    - coaddExposure: coadd exposure
    - weightMap: a float Image of the same dimensions as the coadd; the value of each pixel
        is the sum of the weights of all the images that contributed to that pixel.
    """
    numExp = len(idList)
    if numExp < 1:
        print "Warning: no exposures to coadd!"
        sys.exit(1)
    print "Coadd %s calexp" % (numExp, )

    warpPolicy = policy.getPolicy("warpPolicy")
    coaddPolicy = policy.getPolicy("coaddPolicy")

    if desFwhm > 0:
        psfMatchPolicy = policy.getPolicy("psfMatchPolicy")
        psfMatchPolicy = ipDiffIm.modifyForModelPsfMatch(psfMatchPolicy)
        psfMatcher = ipDiffIm.ModelPsfMatch(psfMatchPolicy)
    else:
        print "No PSF matching will be done (desFwhm <= 0)"

    warper = afwMath.Warper.fromPolicy(warpPolicy)
    coadd = coaddUtils.Coadd.fromPolicy(coaddBBox, coaddWcs, coaddPolicy)
    prevKernelDim = afwGeom.Extent2I(
        0, 0)  # use this because the test Extent2I == None is an error
    for ind, id in enumerate(idList):
        print "Processing exposure %d of %d: id=%s" % (ind + 1, numExp, id)
        exposure = butler.get("calexp", id)
        psf = butler.get("psf", id)
        exposure.setPsf(psf)
        if desFwhm > 0:
            psfKernel = psf.getKernel()
            kernelDim = psfKernel.getDimensions()
            if kernelDim != prevKernelDim:
                print "Create double Gaussian PSF model with core fwhm %0.1f and size %dx%d" % \
                    (desFwhm, kernelDim[0], kernelDim[1])
                coreSigma = desFwhm / FWHMPerSigma
                modelPsf = afwDetection.createPsf("DoubleGaussian",
                                                  kernelDim[0], kernelDim[1],
                                                  coreSigma, coreSigma * 2.5,
                                                  0.1)
                prevKernelDim = kernelDim

            print "PSF-match exposure"
            exposure, psfMatchingKernel, kernelCellSet = psfMatcher.matchExposure(
                exposure, modelPsf)
        print "Warp exposure"
        exposure = warper.warpExposure(coaddWcs, exposure, maxBBox=coaddBBox)
        coadd.addExposure(exposure)

    coaddExposure = coadd.getCoadd()
    weightMap = coadd.getWeightMap()

    return coaddExposure, weightMap
コード例 #19
0
ファイル: Psf.py プロジェクト: RobertLuptonTheGood/afw
 def badSigma2():
     sigma2, b = 0, 1
     afwDetect.createPsf("DoubleGaussian", self.ksize, self.ksize, sigma1, sigma2, b)
コード例 #20
0
def psfMatchAndWarp(idList, butler, desFwhm, coaddWcs, coaddBBox, policy):
    """Normalize, PSF-match (if desFWhm > 0) and warp exposures; save the resulting exposures as FITS files
    
    @param[in] idList: a list of IDs of calexp (and associated PSFs) to coadd
    @param[in] butler: data butler for retrieving input calexp and associated PSFs
    @param[in] desFwhm: desired FWHM (pixels)
    @param[in] coaddWcs: desired WCS of coadd
    @param[in] coaddBBox: bounding box for coadd
    @param[in] policy: policy: see policy/outlierRejectedCoaddDictionary.paf
    
    @return
    - coaddCalib: Calib object for coadd
    - exposureMetadataList: a list of ExposureMetadata objects
        describing the saved psf-matched and warped exposures
    """
    numExp = len(idList)

    if numExp < 1:
        return []

    warpPolicy = policy.getPolicy("warpPolicy")
    coaddPolicy = policy.getPolicy("coaddPolicy")
    badPixelMask = afwImage.MaskU.getPlaneBitMask(
        coaddPolicy.getArray("badMaskPlanes"))
    coaddZeroPoint = coaddPolicy.get("coaddZeroPoint")
    coddFluxMag0 = 10**(0.4 * coaddZeroPoint)
    coaddCalib = afwImage.Calib()
    coaddCalib.setFluxMag0(coddFluxMag0)

    if desFwhm > 0:
        psfMatchPolicy = policy.getPolicy("psfMatchPolicy")
        psfMatchPolicy = ipDiffIm.modifyForModelPsfMatch(psfMatchPolicy)
        psfMatcher = ipDiffIm.ModelPsfMatch(psfMatchPolicy)
    else:
        print "No PSF matching will be done (desFwhm <= 0)"

    warper = afwMath.Warper.fromPolicy(warpPolicy)
    exposureMetadataList = []
    prevKernelDim = afwGeom.Extent2I(
        0, 0)  # use this because the test Extent2I == None is an error
    for ind, id in enumerate(idList):
        outPath = "_".join(["%s_%s" % (k, id[k]) for k in sorted(id.keys())])
        outPath = outPath.replace(",", "_")
        outPath = outPath + ".fits"
        if True:
            print "Processing exposure %d of %d: id=%s" % (ind + 1, numExp, id)
            print "Saving intermediate exposure as %s" % (outPath, )
            exposure = butler.get("calexp", id)
            psf = butler.get("psf", id)
            exposure.setPsf(psf)

            srcCalib = exposure.getCalib()
            scaleFac = 1.0 / srcCalib.getFlux(coaddZeroPoint)
            maskedImage = exposure.getMaskedImage()
            maskedImage *= scaleFac
            print "Normalized using scaleFac=%0.3g" % (scaleFac, )

            if desFwhm > 0:
                psfKernel = psf.getKernel()

                kernelDim = psfKernel.getDimensions()
                if kernelDim != prevKernelDim:
                    print "Create double Gaussian PSF model with core fwhm %0.1f and size %dx%d" % \
                        (desFwhm, kernelDim[0], kernelDim[1])
                    coreSigma = desFwhm / FWHMPerSigma
                    modelPsf = afwDetection.createPsf("DoubleGaussian",
                                                      kernelDim[0],
                                                      kernelDim[1], coreSigma,
                                                      coreSigma * 2.5, 0.1)
                    prevKernelDim = kernelDim

                print "PSF-match exposure"
                exposure, psfMatchingKernel, kernelCellSet = psfMatcher.matchExposure(
                    exposure, modelPsf)

            print "Warp exposure"
            exposure = warper.warpExposure(coaddWcs,
                                           exposure,
                                           maxBBox=coaddBBox)
            exposure.setCalib(coaddCalib)

            print "Saving intermediate exposure %s" % (outPath, )
            exposure.writeFits(outPath)
        else:
            # debug mode; exposures already exist
            print "WARNING: DEBUG MODE; Processing id=%s; retrieving from %s" % (
                id, outPath)
            exposure = afwImage.ExposureF(outPath)

        expMetadata = ExposureMetadata(
            path=outPath,
            exposure=exposure,
            badPixelMask=badPixelMask,
        )
        exposureMetadataList.append(expMetadata)

    return coaddCalib, exposureMetadataList