예제 #1
0
    def testRegularizeModelIter(self):
        """Test that large amplitude changes between iterations are restricted.

        This also tests that noise-like pixels are not regularized.
        """
        modelClampFactor = 2.
        regularizationWidth = 2
        subfilter = 0
        dcrModels = DcrModel(modelImages=self.makeTestImages(),
                             effectiveWavelength=self.effectiveWavelength,
                             bandwidth=self.bandwidth)
        oldModel = dcrModels[0]
        xSize, ySize = self.bbox.getDimensions()
        newModel = oldModel.clone()
        newModel.array[:] += self.rng.rand(ySize, xSize) * np.max(
            oldModel.array)
        newModelRef = newModel.clone()

        dcrModels.regularizeModelIter(subfilter, newModel, self.bbox,
                                      modelClampFactor, regularizationWidth)

        # Make sure the test parameters do reduce the outliers
        self.assertGreater(np.max(newModelRef.array),
                           np.max(newModel.array - oldModel.array))
        # Check that all of the outliers are clipped
        highThreshold = oldModel.array * modelClampFactor
        highPix = newModel.array > highThreshold
        highPix = ndimage.morphology.binary_opening(
            highPix, iterations=regularizationWidth)
        self.assertFalse(np.all(highPix))
        lowThreshold = oldModel.array / modelClampFactor
        lowPix = newModel.array < lowThreshold
        lowPix = ndimage.morphology.binary_opening(
            lowPix, iterations=regularizationWidth)
        self.assertFalse(np.all(lowPix))
예제 #2
0
    def testRegularizationSidelobes(self):
        """Test that artificial chromatic sidelobes are suppressed.
        """
        clampFrequency = 2.
        regularizationWidth = 2
        noiseLevel = 0.1
        sourceAmplitude = 100.
        modelImages = self.makeTestImages(seed=5, nSrc=5, psfSize=3., noiseLevel=noiseLevel,
                                          detectionSigma=5., sourceSigma=sourceAmplitude, fluxRange=2.)
        templateImage = np.mean([model.array for model in modelImages], axis=0)
        sidelobeImages = self.makeTestImages(seed=5, nSrc=5, psfSize=1.5, noiseLevel=noiseLevel/10.,
                                             detectionSigma=5., sourceSigma=sourceAmplitude*5., fluxRange=2.)
        statsCtrl = self.prepareStats()
        signList = [-1., 0., 1.]
        sidelobeShift = (0., 4.)
        for model, sidelobe, sign in zip(modelImages, sidelobeImages, signList):
            sidelobe.array *= sign
            model.array += applyDcr(sidelobe.array, sidelobeShift, useInverse=False)
            model.array += applyDcr(sidelobe.array, sidelobeShift, useInverse=True)

        dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
        refModels = [dcrModels[subfilter].clone() for subfilter in range(self.dcrNumSubfilters)]

        dcrModels.regularizeModelFreq(modelImages, self.bbox, statsCtrl, clampFrequency,
                                      regularizationWidth=regularizationWidth)
        for model, refModel, sign in zip(modelImages, refModels, signList):
            # Make sure the test parameters do reduce the outliers
            self.assertGreater(np.sum(np.abs(refModel.array - templateImage)),
                               np.sum(np.abs(model.array - templateImage)))
예제 #3
0
    def testRegularizationSmallClamp(self):
        """Test that large variations between model planes are reduced.

        This also tests that noise-like pixels are not regularized.
        """
        clampFrequency = 2
        regularizationWidth = 2
        fluxRange = 10.
        modelImages = self.makeTestImages(fluxRange=fluxRange)
        dcrModels = DcrModel(modelImages=modelImages,
                             mask=self.mask,
                             effectiveWavelength=self.effectiveWavelength,
                             bandwidth=self.bandwidth)
        newModels = [model.clone() for model in dcrModels]
        templateImage = dcrModels.getReferenceImage(self.bbox)

        statsCtrl = self.prepareStats()
        dcrModels.regularizeModelFreq(newModels, self.bbox, statsCtrl,
                                      clampFrequency, regularizationWidth)
        for model, refModel in zip(newModels, dcrModels):
            # Make sure the test parameters do reduce the outliers
            self.assertGreater(np.max(refModel.array - templateImage),
                               np.max(model.array - templateImage))
            highThreshold = templateImage * clampFrequency
            highPix = model.array > highThreshold
            highPix = ndimage.morphology.binary_opening(
                highPix, iterations=regularizationWidth)
            self.assertFalse(np.all(highPix))
            lowThreshold = templateImage / clampFrequency
            lowPix = model.array < lowThreshold
            lowPix = ndimage.morphology.binary_opening(
                lowPix, iterations=regularizationWidth)
            self.assertFalse(np.all(lowPix))
예제 #4
0
    def testRegularizeModelIter(self):
        """Test that large amplitude changes between iterations are restricted.

        This also tests that noise-like pixels are not regularized.
        """
        modelClampFactor = 2.
        regularizationWidth = 2
        subfilter = 0
        dcrModels = DcrModel(modelImages=self.makeTestImages())
        oldModel = dcrModels[0]
        xSize, ySize = self.bbox.getDimensions()
        newModel = oldModel.clone()
        newModel.array[:] += self.rng.rand(ySize, xSize)*np.max(oldModel.array)
        newModelRef = newModel.clone()

        dcrModels.regularizeModelIter(subfilter, newModel, self.bbox, modelClampFactor, regularizationWidth)

        # Make sure the test parameters do reduce the outliers
        self.assertGreater(np.max(newModelRef.array),
                           np.max(newModel.array - oldModel.array))
        # Check that all of the outliers are clipped
        highThreshold = oldModel.array*modelClampFactor
        highPix = newModel.array > highThreshold
        highPix = ndimage.morphology.binary_opening(highPix, iterations=regularizationWidth)
        self.assertFalse(np.all(highPix))
        lowThreshold = oldModel.array/modelClampFactor
        lowPix = newModel.array < lowThreshold
        lowPix = ndimage.morphology.binary_opening(lowPix, iterations=regularizationWidth)
        self.assertFalse(np.all(lowPix))
예제 #5
0
    def testRegularizationSidelobes(self):
        """Test that artificial chromatic sidelobes are suppressed.
        """
        clampFrequency = 2.
        regularizationWidth = 2
        noiseLevel = 0.1
        sourceAmplitude = 100.
        modelImages = self.makeTestImages(seed=5, nSrc=5, psfSize=3., noiseLevel=noiseLevel,
                                          detectionSigma=5., sourceSigma=sourceAmplitude, fluxRange=2.)
        templateImage = np.mean([model.array for model in modelImages], axis=0)
        sidelobeImages = self.makeTestImages(seed=5, nSrc=5, psfSize=1.5, noiseLevel=noiseLevel/10.,
                                             detectionSigma=5., sourceSigma=sourceAmplitude*5., fluxRange=2.)
        statsCtrl = self.prepareStats()
        signList = [-1., 0., 1.]
        sidelobeShift = (0., 4.)
        for model, sidelobe, sign in zip(modelImages, sidelobeImages, signList):
            sidelobe.array *= sign
            model.array += applyDcr(sidelobe.array, sidelobeShift, useInverse=False)
            model.array += applyDcr(sidelobe.array, sidelobeShift, useInverse=True)

        dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
        refModels = [dcrModels[subfilter].clone() for subfilter in range(self.dcrNumSubfilters)]

        dcrModels.regularizeModelFreq(modelImages, self.bbox, statsCtrl, clampFrequency,
                                      regularizationWidth=regularizationWidth)
        for model, refModel, sign in zip(modelImages, refModels, signList):
            # Make sure the test parameters do reduce the outliers
            self.assertGreater(np.sum(np.abs(refModel.array - templateImage)),
                               np.sum(np.abs(model.array - templateImage)))
예제 #6
0
    def testRegularizationSmallClamp(self):
        """Test that large variations between model planes are reduced.

        This also tests that noise-like pixels are not regularized.
        """
        clampFrequency = 2
        regularizationWidth = 2
        fluxRange = 10.
        modelImages = self.makeTestImages(fluxRange=fluxRange)
        dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
        newModels = [model.clone() for model in dcrModels]
        templateImage = dcrModels.getReferenceImage(self.bbox)

        statsCtrl = self.prepareStats()
        dcrModels.regularizeModelFreq(newModels, self.bbox, statsCtrl, clampFrequency, regularizationWidth)
        for model, refModel in zip(newModels, dcrModels):
            # Make sure the test parameters do reduce the outliers
            self.assertGreater(np.max(refModel.array - templateImage),
                               np.max(model.array - templateImage))
            highThreshold = templateImage*clampFrequency
            highPix = model.array > highThreshold
            highPix = ndimage.morphology.binary_opening(highPix, iterations=regularizationWidth)
            self.assertFalse(np.all(highPix))
            lowThreshold = templateImage/clampFrequency
            lowPix = model.array < lowThreshold
            lowPix = ndimage.morphology.binary_opening(lowPix, iterations=regularizationWidth)
            self.assertFalse(np.all(lowPix))
예제 #7
0
 def testConditionDcrModelNoChangeHighGain(self):
     """Conditioning should not change the model if it equals the reference.
     """
     modelImages = self.makeTestImages()
     dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
     newModels = [model.clone() for model in dcrModels]
     dcrModels.conditionDcrModel(newModels, self.bbox, gain=3.)
     for refModel, newModel in zip(dcrModels, newModels):
         self.assertFloatsAlmostEqual(refModel.array, newModel.array)
예제 #8
0
 def testConditionDcrModelNoChangeHighGain(self):
     """Conditioning should not change the model if it equals the reference.
     """
     modelImages = self.makeTestImages()
     dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
     newModels = [model.clone() for model in dcrModels]
     dcrModels.conditionDcrModel(newModels, self.bbox, gain=3.)
     for refModel, newModel in zip(dcrModels, newModels):
         self.assertFloatsAlmostEqual(refModel.array, newModel.array)
예제 #9
0
 def testConditionDcrModelNoChange(self):
     """Conditioning should not change the model if it equals the reference.
     """
     modelImages = self.makeTestImages()
     dcrModels = DcrModel(modelImages=modelImages,
                          mask=self.mask,
                          effectiveWavelength=self.effectiveWavelength,
                          bandwidth=self.bandwidth)
     newModels = [model.clone() for model in dcrModels]
     dcrModels.conditionDcrModel(newModels, self.bbox, gain=1.)
     for refModel, newModel in zip(dcrModels, newModels):
         self.assertFloatsAlmostEqual(refModel.array, newModel.array)
예제 #10
0
 def testConditionDcrModelWithChange(self):
     """Verify conditioning when the model changes by a known amount.
     """
     modelImages = self.makeTestImages()
     dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
     newModels = [model.clone() for model in dcrModels]
     for model in newModels:
         model.array[:] *= 3.
     dcrModels.conditionDcrModel(newModels, self.bbox, gain=1.)
     for refModel, newModel in zip(dcrModels, newModels):
         refModel.array[:] *= 2.
         self.assertFloatsAlmostEqual(refModel.array, newModel.array)
예제 #11
0
 def testConditionDcrModelWithChange(self):
     """Verify conditioning when the model changes by a known amount.
     """
     modelImages = self.makeTestImages()
     dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
     newModels = [model.clone() for model in dcrModels]
     for model in newModels:
         model.array[:] *= 3.
     dcrModels.conditionDcrModel(newModels, self.bbox, gain=1.)
     for refModel, newModel in zip(dcrModels, newModels):
         refModel.array[:] *= 2.
         self.assertFloatsAlmostEqual(refModel.array, newModel.array)
예제 #12
0
    def getDcrModel(self, patchList, coaddRefs, visitInfo):
        """Build DCR-matched coadds from a list of exposure references.

        Parameters
        ----------
        patchList : `dict`
            Dict of the patches containing valid data for each tract
        coaddRefs : `list` of elements of type
                    `lsst.daf.butler.DeferredDatasetHandle` of
                    `lsst.afw.image.Exposure`
            Data references to DcrModels that overlap the detector.
        visitInfo : `lsst.afw.image.VisitInfo`
            Metadata for the science image.

        Returns
        -------
        `list` of elements of type `lsst.afw.image.Exposure`
                Coadd exposures that overlap the detector.
        """
        coaddExposureList = []
        for tract in patchList:
            for patch in set(patchList[tract]):
                coaddRefList = [
                    coaddRef for coaddRef in coaddRefs
                    if _selectDataRef(coaddRef, tract, patch)
                ]

                dcrModel = DcrModel.fromQuantum(
                    coaddRefList, self.config.effectiveWavelength,
                    self.config.bandwidth, self.config.numSubfilters)
                coaddExposureList.append(
                    dcrModel.buildMatchedExposure(visitInfo=visitInfo))
        return coaddExposureList
예제 #13
0
 def testIterateModel(self):
     """Test that the DcrModel is iterable, and has the right values.
     """
     testModels = self.makeTestImages()
     refVals = [np.sum(model.array) for model in testModels]
     dcrModels = DcrModel(modelImages=testModels)
     for refVal, model in zip(refVals, dcrModels):
         self.assertFloatsEqual(refVal, np.sum(model.array))
     # Negative indices are allowed, so check that those return models from the end.
     self.assertFloatsEqual(refVals[-1], np.sum(dcrModels[-1].array))
예제 #14
0
    def run(self, exposure, sensorRef, templateIdList=None):
        """Retrieve and mosaic a template coadd exposure that overlaps the exposure

        Parameters
        ----------
        exposure: `lsst.afw.image.Exposure`
            an exposure for which to generate an overlapping template
        sensorRef : TYPE
            a Butler data reference that can be used to obtain coadd data
        templateIdList : TYPE, optional
            list of data ids (unused)

        Returns
        -------
        result : `struct`
            return a pipeBase.Struct:

            - ``exposure`` : a template coadd exposure assembled out of patches
            - ``sources`` :  None for this subtask
        """
        skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap")
        expWcs = exposure.getWcs()
        expBoxD = afwGeom.Box2D(exposure.getBBox())
        expBoxD.grow(self.config.templateBorderSize)
        ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
        tractInfo = skyMap.findTract(ctrSkyPos)
        self.log.info("Using skyMap tract %s" % (tractInfo.getId(),))
        skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
        patchList = tractInfo.findPatchList(skyCorners)

        if not patchList:
            raise RuntimeError("No suitable tract found")
        self.log.info("Assembling %s coadd patches" % (len(patchList),))

        # compute coadd bbox
        coaddWcs = tractInfo.getWcs()
        coaddBBox = afwGeom.Box2D()
        for skyPos in skyCorners:
            coaddBBox.include(coaddWcs.skyToPixel(skyPos))
        coaddBBox = afwGeom.Box2I(coaddBBox)
        self.log.info("exposure dimensions=%s; coadd dimensions=%s" %
                      (exposure.getDimensions(), coaddBBox.getDimensions()))

        # assemble coadd exposure from subregions of patches
        coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
        coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
        nPatchesFound = 0
        coaddFilter = None
        coaddPsf = None
        for patchInfo in patchList:
            patchSubBBox = patchInfo.getOuterBBox()
            patchSubBBox.clip(coaddBBox)
            patchArgDict = dict(
                datasetType=self.getCoaddDatasetName() + "_sub",
                bbox=patchSubBBox,
                tract=tractInfo.getId(),
                patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
                numSubfilters=self.config.numSubfilters,
            )
            if patchSubBBox.isEmpty():
                self.log.info("skip tract=%(tract)s, patch=%(patch)s; no overlapping pixels" % patchArgDict)
                continue

            if self.config.coaddName == 'dcr':
                if not sensorRef.datasetExists(subfilter=0, **patchArgDict):
                    self.log.warn("%(datasetType)s, tract=%(tract)s, patch=%(patch)s,"
                                  " numSubfilters=%(numSubfilters)s, subfilter=0 does not exist"
                                  % patchArgDict)
                    continue
                self.log.info("Constructing DCR-matched template for patch %s" % patchArgDict)
                dcrModel = DcrModel.fromDataRef(sensorRef, **patchArgDict)
                coaddPatch = dcrModel.buildMatchedExposure(bbox=patchSubBBox,
                                                           wcs=coaddWcs,
                                                           visitInfo=exposure.getInfo().getVisitInfo())
            else:
                if not sensorRef.datasetExists(**patchArgDict):
                    self.log.warn("%(datasetType)s, tract=%(tract)s, patch=%(patch)s does not exist"
                                  % patchArgDict)
                    continue
                self.log.info("Reading patch %s" % patchArgDict)
                coaddPatch = sensorRef.get(**patchArgDict)
            nPatchesFound += 1
            coaddExposure.maskedImage.assign(coaddPatch.maskedImage, coaddPatch.getBBox())
            if coaddFilter is None:
                coaddFilter = coaddPatch.getFilter()

            # Retrieve the PSF for this coadd tract, if not already retrieved
            if coaddPsf is None and coaddPatch.hasPsf():
                coaddPsf = coaddPatch.getPsf()

        if nPatchesFound == 0:
            raise RuntimeError("No patches found!")

        if coaddPsf is None:
            raise RuntimeError("No coadd Psf found!")

        coaddExposure.setPsf(coaddPsf)
        coaddExposure.setFilter(coaddFilter)
        return pipeBase.Struct(exposure=coaddExposure,
                               sources=None)
예제 #15
0
    def run(self,
            tractInfo,
            patchList,
            skyCorners,
            availableCoaddRefs,
            sensorRef=None,
            visitInfo=None):
        """Gen2 and gen3 shared code: determination of exposure dimensions and
        copying of pixels from overlapping patch regions.

        Parameters
        ----------
        skyMap : `lsst.skymap.BaseSkyMap`
            SkyMap object that corresponds to the template coadd.
        tractInfo : `lsst.skymap.TractInfo`
            The selected tract.
        patchList : iterable of `lsst.skymap.patchInfo.PatchInfo`
            Patches to consider for making the template exposure.
        skyCorners : list of `lsst.geom.SpherePoint`
            Sky corner coordinates to be covered by the template exposure.
        availableCoaddRefs : `dict` [`int`]
            Dictionary of spatially relevant retrieved coadd patches,
            indexed by their sequential patch number. In Gen3 mode, values are
            `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called,
            in Gen2 mode, ``sensorRef.get(**coaddef)`` is called to retrieve the coadd.
        sensorRef : `lsst.daf.persistence.ButlerDataRef`, Gen2 only
            Butler data reference to get coadd data.
            Must be `None` for Gen3.
        visitInfo : `lsst.afw.image.VisitInfo`, Gen2 only
            VisitInfo to make dcr model.

        Returns
        -------
        templateExposure: `lsst.afw.image.ExposureF`
            The created template exposure.
        """
        coaddWcs = tractInfo.getWcs()

        # compute coadd bbox
        coaddBBox = geom.Box2D()
        for skyPos in skyCorners:
            coaddBBox.include(coaddWcs.skyToPixel(skyPos))
        coaddBBox = geom.Box2I(coaddBBox)
        self.log.info("coadd dimensions=%s", coaddBBox.getDimensions())

        coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
        coaddExposure.maskedImage.set(np.nan,
                                      afwImage.Mask.getPlaneBitMask("NO_DATA"),
                                      np.nan)
        nPatchesFound = 0
        coaddFilterLabel = None
        coaddPsf = None
        coaddPhotoCalib = None
        for patchInfo in patchList:
            patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
            patchSubBBox = patchInfo.getOuterBBox()
            patchSubBBox.clip(coaddBBox)
            if patchNumber not in availableCoaddRefs:
                self.log.warning(
                    "skip patch=%d; patch does not exist for this coadd",
                    patchNumber)
                continue
            if patchSubBBox.isEmpty():
                if isinstance(availableCoaddRefs[patchNumber],
                              DeferredDatasetHandle):
                    tract = availableCoaddRefs[patchNumber].dataId['tract']
                else:
                    tract = availableCoaddRefs[patchNumber]['tract']
                self.log.info("skip tract=%d patch=%d; no overlapping pixels",
                              tract, patchNumber)
                continue

            if self.config.coaddName == 'dcr':
                patchInnerBBox = patchInfo.getInnerBBox()
                patchInnerBBox.clip(coaddBBox)
                if np.min(patchInnerBBox.getDimensions()
                          ) <= 2 * self.config.templateBorderSize:
                    self.log.info(
                        "skip tract=%(tract)s, patch=%(patch)s; too few pixels.",
                        availableCoaddRefs[patchNumber])
                    continue
                self.log.info("Constructing DCR-matched template for patch %s",
                              availableCoaddRefs[patchNumber])

                dcrModel = DcrModel.fromQuantum(
                    availableCoaddRefs[patchNumber],
                    self.config.effectiveWavelength, self.config.bandwidth)
                # The edge pixels of the DcrCoadd may contain artifacts due to missing data.
                # Each patch has significant overlap, and the contaminated edge pixels in
                # a new patch will overwrite good pixels in the overlap region from
                # previous patches.
                # Shrink the BBox to remove the contaminated pixels,
                # but make sure it is only the overlap region that is reduced.
                dcrBBox = geom.Box2I(patchSubBBox)
                dcrBBox.grow(-self.config.templateBorderSize)
                dcrBBox.include(patchInnerBBox)
                coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
                                                           visitInfo=visitInfo)
            else:
                if sensorRef is None:
                    # Gen3
                    coaddPatch = availableCoaddRefs[patchNumber].get()
                else:
                    # Gen2
                    coaddPatch = sensorRef.get(
                        **availableCoaddRefs[patchNumber])
            nPatchesFound += 1

            # Gen2 get() seems to clip based on bbox kwarg but we removed bbox
            # calculation from caller code. Gen3 also does not do this.
            overlapBox = coaddPatch.getBBox()
            overlapBox.clip(coaddBBox)
            coaddExposure.maskedImage.assign(
                coaddPatch.maskedImage[overlapBox], overlapBox)

            if coaddFilterLabel is None:
                coaddFilterLabel = coaddPatch.getFilter()

            # Retrieve the PSF for this coadd tract, if not already retrieved
            if coaddPsf is None and coaddPatch.hasPsf():
                coaddPsf = coaddPatch.getPsf()

            # Retrieve the calibration for this coadd tract, if not already retrieved
            if coaddPhotoCalib is None:
                coaddPhotoCalib = coaddPatch.getPhotoCalib()

        if coaddPhotoCalib is None:
            raise RuntimeError("No coadd PhotoCalib found!")
        if nPatchesFound == 0:
            raise RuntimeError("No patches found!")
        if coaddPsf is None:
            raise RuntimeError("No coadd Psf found!")

        coaddExposure.setPhotoCalib(coaddPhotoCalib)
        coaddExposure.setPsf(coaddPsf)
        coaddExposure.setFilter(coaddFilterLabel)
        return coaddExposure
예제 #16
0
    def run(self, exposure, sensorRef, templateIdList=None):
        """Retrieve and mosaic a template coadd exposure that overlaps the exposure

        Parameters
        ----------
        exposure: `lsst.afw.image.Exposure`
            an exposure for which to generate an overlapping template
        sensorRef : TYPE
            a Butler data reference that can be used to obtain coadd data
        templateIdList : TYPE, optional
            list of data ids (unused)

        Returns
        -------
        result : `struct`
            return a pipeBase.Struct:

            - ``exposure`` : a template coadd exposure assembled out of patches
            - ``sources`` :  None for this subtask
        """
        skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap")
        expWcs = exposure.getWcs()
        expBoxD = afwGeom.Box2D(exposure.getBBox())
        expBoxD.grow(self.config.templateBorderSize)
        ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
        tractInfo = skyMap.findTract(ctrSkyPos)
        self.log.info("Using skyMap tract %s" % (tractInfo.getId(),))
        skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
        patchList = tractInfo.findPatchList(skyCorners)

        if not patchList:
            raise RuntimeError("No suitable tract found")
        self.log.info("Assembling %s coadd patches" % (len(patchList),))

        # compute coadd bbox
        coaddWcs = tractInfo.getWcs()
        coaddBBox = afwGeom.Box2D()
        for skyPos in skyCorners:
            coaddBBox.include(coaddWcs.skyToPixel(skyPos))
        coaddBBox = afwGeom.Box2I(coaddBBox)
        self.log.info("exposure dimensions=%s; coadd dimensions=%s" %
                      (exposure.getDimensions(), coaddBBox.getDimensions()))

        # assemble coadd exposure from subregions of patches
        coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
        coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
        nPatchesFound = 0
        coaddFilter = None
        coaddPsf = None
        for patchInfo in patchList:
            patchSubBBox = patchInfo.getOuterBBox()
            patchSubBBox.clip(coaddBBox)
            patchArgDict = dict(
                datasetType=self.getCoaddDatasetName() + "_sub",
                bbox=patchSubBBox,
                tract=tractInfo.getId(),
                patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
                numSubfilters=self.config.numSubfilters,
            )
            if patchSubBBox.isEmpty():
                self.log.info("skip tract=%(tract)s, patch=%(patch)s; no overlapping pixels" % patchArgDict)
                continue

            if self.config.coaddName == 'dcr':
                if not sensorRef.datasetExists(subfilter=0, **patchArgDict):
                    self.log.warn("%(datasetType)s, tract=%(tract)s, patch=%(patch)s,"
                                  " numSubfilters=%(numSubfilters)s, subfilter=0 does not exist"
                                  % patchArgDict)
                    continue
                self.log.info("Constructing DCR-matched template for patch %s" % patchArgDict)
                dcrModel = DcrModel.fromDataRef(sensorRef, **patchArgDict)
                # The edge pixels of the DcrCoadd may contain artifacts due to missing data.
                # Each patch has significant overlap, and the contaminated edge pixels in
                # a new patch will overwrite good pixels in the overlap region from
                # previous patches.
                # Shrink the BBox to remove the contaminated pixels,
                # but make sure it is only the overlap region that is reduced.
                patchInnerBBox = patchInfo.getInnerBBox()
                patchInnerBBox.clip(coaddBBox)
                dcrBBox = afwGeom.Box2I(patchSubBBox)
                dcrBBox.grow(-self.config.templateBorderSize)
                dcrBBox.include(patchInnerBBox)
                coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
                                                           wcs=coaddWcs,
                                                           visitInfo=exposure.getInfo().getVisitInfo())
            else:
                if not sensorRef.datasetExists(**patchArgDict):
                    self.log.warn("%(datasetType)s, tract=%(tract)s, patch=%(patch)s does not exist"
                                  % patchArgDict)
                    continue
                self.log.info("Reading patch %s" % patchArgDict)
                coaddPatch = sensorRef.get(**patchArgDict)
            nPatchesFound += 1
            coaddExposure.maskedImage.assign(coaddPatch.maskedImage, coaddPatch.getBBox())
            if coaddFilter is None:
                coaddFilter = coaddPatch.getFilter()

            # Retrieve the PSF for this coadd tract, if not already retrieved
            if coaddPsf is None and coaddPatch.hasPsf():
                coaddPsf = coaddPatch.getPsf()

        if nPatchesFound == 0:
            raise RuntimeError("No patches found!")

        if coaddPsf is None:
            raise RuntimeError("No coadd Psf found!")

        coaddExposure.setPsf(coaddPsf)
        coaddExposure.setFilter(coaddFilter)
        return pipeBase.Struct(exposure=coaddExposure,
                               sources=None)