def run(
        self,
        exposures: typing.List[afwImage.Exposure],
        donutCatalogs: typing.List[pd.DataFrame],
        camera: lsst.afw.cameraGeom.Camera,
    ) -> pipeBase.Struct:

        cameraName = camera.getName()
        extraCatalog, intraCatalog = donutCatalogs

        # Get the donut stamps from extra and intra focal images
        donutStampsExtra = DonutStamps([])
        donutStampsIntra = DonutStamps([])

        for exposure in exposures:
            detectorName = exposure.getDetector().getName()
            if detectorName in self.extraFocalNames:
                donutStampsExtraExp = self.cutOutStamps(
                    exposure, extraCatalog, DefocalType.Extra, cameraName)
                donutStampsExtra.extend(
                    [stamp for stamp in donutStampsExtraExp])
            elif detectorName in self.intraFocalNames:
                donutStampsIntraExp = self.cutOutStamps(
                    exposure, intraCatalog, DefocalType.Intra, cameraName)
                donutStampsIntra.extend(
                    [stamp for stamp in donutStampsIntraExp])
            else:
                continue

        # If no donuts are in the donutCatalog for a set of exposures
        # then return the Zernike coefficients as nan.
        if (len(donutStampsExtra) == 0) or (len(donutStampsIntra) == 0):
            return pipeBase.Struct(
                outputZernikesRaw=np.ones(19) * np.nan,
                outputZernikesAvg=np.ones(19) * np.nan,
                donutStampsExtra=DonutStamps([]),
                donutStampsIntra=DonutStamps([]),
            )

        # Estimate Zernikes from collection of stamps
        zernikeCoeffsRaw = self.estimateZernikes(donutStampsExtra,
                                                 donutStampsIntra)
        zernikeCoeffsCombined = self.getCombinedZernikes(zernikeCoeffsRaw)

        # Return extra-focal DonutStamps, intra-focal DonutStamps and
        # Zernike coefficient numpy array as Struct that can be saved to
        # Gen 3 repository all with the same dataId.
        return pipeBase.Struct(
            outputZernikesAvg=np.array(zernikeCoeffsCombined.combinedZernikes),
            outputZernikesRaw=np.array(zernikeCoeffsRaw),
            donutStampsExtra=donutStampsExtra,
            donutStampsIntra=donutStampsIntra,
        )
Exemple #2
0
    def run(
        self,
        exposures: typing.List[afwImage.Exposure],
        donutCatalog: typing.List[pd.DataFrame],
        camera: lsst.afw.cameraGeom.Camera,
    ) -> pipeBase.Struct:

        # Get exposure metadata to find which is extra and intra
        focusZ0 = exposures[0].getMetadata()["FOCUSZ"]
        focusZ1 = exposures[1].getMetadata()["FOCUSZ"]

        extraExpIdx, intraExpIdx = self.assignExtraIntraIdx(focusZ0, focusZ1)

        # Get the donut stamps from extra and intra focal images
        # The donut catalogs for each exposure should be the same
        # so we just pick the one for the first exposure
        cameraName = camera.getName()
        donutStampsExtra = self.cutOutStamps(exposures[extraExpIdx],
                                             donutCatalog[0],
                                             DefocalType.Extra, cameraName)
        donutStampsIntra = self.cutOutStamps(exposures[intraExpIdx],
                                             donutCatalog[0],
                                             DefocalType.Intra, cameraName)

        # If no donuts are in the donutCatalog for a set of exposures
        # then return the Zernike coefficients as nan.
        if len(donutStampsExtra) == 0:
            return pipeBase.Struct(
                outputZernikesRaw=[np.ones(19) * np.nan] * 2,
                outputZernikesAvg=[np.ones(19) * np.nan] * 2,
                donutStampsExtra=[DonutStamps([])] * 2,
                donutStampsIntra=[DonutStamps([])] * 2,
            )

        # Estimate Zernikes from collection of stamps
        zernikeCoeffsRaw = self.estimateZernikes(donutStampsExtra,
                                                 donutStampsIntra)
        zernikeCoeffsCombined = self.getCombinedZernikes(zernikeCoeffsRaw)

        # Return extra-focal DonutStamps, intra-focal DonutStamps and
        # Zernike coefficient numpy array as Struct that can be saved to
        # Gen 3 repository all with the same dataId.
        return pipeBase.Struct(
            outputZernikesAvg=[
                np.array(zernikeCoeffsCombined.combinedZernikes)
            ] * 2,
            outputZernikesRaw=[np.array(zernikeCoeffsRaw)] * 2,
            donutStampsExtra=[donutStampsExtra] * 2,
            donutStampsIntra=[donutStampsIntra] * 2,
        )
Exemple #3
0
 def testIOsub(self):
     """
     Test the class' write and readFits when passing on a bounding box.
     """
     bbox = lsst.geom.Box2I(lsst.geom.Point2I(25, 25), lsst.geom.Extent2I(4, 4))
     with tempfile.NamedTemporaryFile() as f:
         self.donutStamps.writeFits(f.name)
         options = {"bbox": bbox}
         subStamps = DonutStamps.readFitsWithOptions(f.name, options)
         for stamp1, stamp2 in zip(self.donutStamps, subStamps):
             self.assertEqual(bbox.getDimensions(), stamp2.stamp_im.getDimensions())
             self.assertMaskedImagesAlmostEqual(
                 stamp1.stamp_im[bbox], stamp2.stamp_im
             )
Exemple #4
0
    def _makeDonutStamps(self, nStamps, stampSize):

        randState = np.random.RandomState(42)
        stampList = []

        for idx in range(nStamps):
            stamp = afwImage.maskedImage.MaskedImageF(stampSize, stampSize)
            stamp.image.array += randState.rand(stampSize, stampSize)
            stamp.mask.array += 10
            stamp.variance.array += 100
            stamp.setXY0(idx + 10, idx + 15)
            stampList.append(stamp)

        ras = np.arange(nStamps)
        decs = np.arange(nStamps) + 5
        centX = np.arange(nStamps) + 20
        centY = np.arange(nStamps) + 25
        detectorNames = ["R22_S11"] * nStamps
        camNames = ["LSSTCam"] * nStamps
        dfcTypes = [DefocalType.Extra.value] * nStamps
        halfStampIdx = int(nStamps / 2)
        dfcTypes[:halfStampIdx] = [DefocalType.Intra.value] * halfStampIdx

        metadata = PropertyList()
        metadata["RA_DEG"] = ras
        metadata["DEC_DEG"] = decs
        metadata["CENT_X"] = centX
        metadata["CENT_Y"] = centY
        metadata["DET_NAME"] = detectorNames
        metadata["CAM_NAME"] = camNames
        metadata["DFC_TYPE"] = dfcTypes

        donutStampList = [
            DonutStamp.factory(stampList[idx], metadata, idx) for idx in range(nStamps)
        ]

        return DonutStamps(donutStampList, metadata=metadata)
Exemple #5
0
 def _roundtrip(self, donutStamps):
     """Round trip a DonutStamps object to disk and check values"""
     with tempfile.NamedTemporaryFile() as f:
         donutStamps.writeFits(f.name)
         options = PropertyList()
         donutStamps2 = DonutStamps.readFitsWithOptions(f.name, options)
         self.assertEqual(len(donutStamps), len(donutStamps2))
         for stamp1, stamp2 in zip(donutStamps, donutStamps2):
             self.assertMaskedImagesAlmostEqual(stamp1.stamp_im, stamp2.stamp_im)
             self.assertAlmostEqual(
                 stamp1.sky_position.getRa().asDegrees(),
                 stamp2.sky_position.getRa().asDegrees(),
             )
             self.assertAlmostEqual(
                 stamp1.sky_position.getDec().asDegrees(),
                 stamp2.sky_position.getDec().asDegrees(),
             )
             self.assertAlmostEqual(
                 stamp1.centroid_position.getX(), stamp2.centroid_position.getX()
             )
             self.assertAlmostEqual(
                 stamp1.centroid_position.getY(), stamp2.centroid_position.getY()
             )
             self.assertEqual(stamp1.detector_name, stamp2.detector_name)
    def cutOutStamps(self, exposure, donutCatalog, defocalType, cameraName):
        """
        Cut out postage stamps for sources in catalog.

        Parameters
        ----------
        exposure: lsst.afw.image.Exposure
            Post-ISR image with defocal donuts sources.
        donutCatalog: pandas DataFrame
            Source catalog for the pointing.
        defocalType: enum 'DefocalType'
            Defocal type of the donut image.
        cameraName: str
            Name of camera for the exposure. Can accept "LSSTCam",
            "LSSTComCam", "LATISS".

        Returns
        -------
        DonutStamps
            Collection of postage stamps as
            lsst.afw.image.maskedImage.MaskedImage with additional metadata.
        """

        detectorName = exposure.getDetector().getName()
        pixelScale = exposure.getWcs().getPixelScale().asArcseconds()
        camType = getCamType(self.instName)
        template = self.getTemplate(
            detectorName,
            defocalType,
            self.donutTemplateSize,
            camType,
            self.opticalModel,
            pixelScale,
        )

        # Final list of DonutStamp objects
        finalStamps = []

        # Final locations of donut centroids in pixels
        finalXCentList = []
        finalYCentList = []

        # Final locations of BBox corners for DonutStamp images
        xCornerList = []
        yCornerList = []

        for donutRow in donutCatalog.to_records():
            # Make an initial cutout larger than the actual final stamp
            # so that we can centroid to get the stamp centered exactly
            # on the donut
            xCent = int(donutRow["centroid_x"])
            yCent = int(donutRow["centroid_y"])

            # Adjust the centroid coordinates from the catalog by convolving
            # the postage stamp with the donut template and return
            # the new centroid position as well as the corners of the
            # postage stamp to cut out of the exposure.
            finalDonutX, finalDonutY, xCorner, yCorner = self.calculateFinalCentroid(
                exposure, template, xCent, yCent)
            finalXCentList.append(finalDonutX)
            finalYCentList.append(finalDonutY)

            # Get the final cutout
            finalCorner = lsst.geom.Point2I(xCorner, yCorner)
            finalBBox = lsst.geom.Box2I(
                finalCorner, lsst.geom.Extent2I(self.donutStampSize))
            xCornerList.append(xCorner)
            yCornerList.append(yCorner)
            finalCutout = exposure[finalBBox]

            # Save MaskedImage to stamp
            finalStamp = finalCutout.getMaskedImage()
            finalStamps.append(
                DonutStamp(
                    stamp_im=finalStamp,
                    sky_position=lsst.geom.SpherePoint(
                        donutRow["coord_ra"],
                        donutRow["coord_dec"],
                        lsst.geom.radians,
                    ),
                    centroid_position=lsst.geom.Point2D(
                        finalDonutX, finalDonutY),
                    detector_name=detectorName,
                    cam_name=cameraName,
                    defocal_type=defocalType.value,
                ))

        catalogLength = len(donutCatalog)
        stampsMetadata = PropertyList()
        stampsMetadata["RA_DEG"] = np.degrees(donutCatalog["coord_ra"].values)
        stampsMetadata["DEC_DEG"] = np.degrees(
            donutCatalog["coord_dec"].values)
        stampsMetadata["DET_NAME"] = np.array([detectorName] * catalogLength,
                                              dtype=str)
        stampsMetadata["CAM_NAME"] = np.array([cameraName] * catalogLength,
                                              dtype=str)
        stampsMetadata["DFC_TYPE"] = np.array([defocalType.value] *
                                              catalogLength)
        # Save the centroid values
        stampsMetadata["CENT_X"] = np.array(finalXCentList)
        stampsMetadata["CENT_Y"] = np.array(finalYCentList)
        # Save the corner values
        stampsMetadata["X0"] = np.array(xCornerList)
        stampsMetadata["Y0"] = np.array(yCornerList)

        return DonutStamps(finalStamps, metadata=stampsMetadata)
Exemple #7
0
    def testEstimateCornerZernikes(self):
        """
        Test the rotated corner sensors (R04 and R40) and make sure no changes
        upstream in obs_lsst have created issues in Zernike estimation.
        """

        donutStampDir = os.path.join(self.testDataDir, "donutImg", "donutStamps")

        # Test R04
        donutStampsExtra = DonutStamps.readFits(
            os.path.join(donutStampDir, "R04_SW0_donutStamps.fits")
        )
        donutStampsIntra = DonutStamps.readFits(
            os.path.join(donutStampDir, "R04_SW1_donutStamps.fits")
        )
        zernCoeffAllR04 = self.task.estimateZernikes(donutStampsExtra, donutStampsIntra)
        zernCoeffAvgR04 = self.task.combineZernikes.run(
            zernCoeffAllR04
        ).combinedZernikes
        trueZernCoeffR04 = np.array(
            [
                -0.71201408,
                1.12248525,
                0.77794367,
                -0.04085477,
                -0.05272933,
                0.16054277,
                0.081405,
                -0.04382461,
                -0.04830676,
                -0.06218882,
                0.10246469,
                0.0197683,
                0.007953,
                0.00668697,
                -0.03570788,
                -0.03020376,
                0.0039522,
                0.04793133,
                -0.00804605,
            ]
        )
        # Make sure the total rms error is less than 0.35 microns off
        # from the OPD truth as a sanity check
        self.assertLess(
            np.sqrt(np.sum(np.square(zernCoeffAvgR04 - trueZernCoeffR04))), 0.35
        )

        # Test R40
        donutStampsExtra = DonutStamps.readFits(
            os.path.join(donutStampDir, "R40_SW0_donutStamps.fits")
        )
        donutStampsIntra = DonutStamps.readFits(
            os.path.join(donutStampDir, "R40_SW1_donutStamps.fits")
        )
        zernCoeffAllR40 = self.task.estimateZernikes(donutStampsExtra, donutStampsIntra)
        zernCoeffAvgR40 = self.task.combineZernikes.run(
            zernCoeffAllR40
        ).combinedZernikes
        trueZernCoeffR40 = np.array(
            [
                -0.6535694,
                1.00838499,
                0.55968811,
                -0.08899825,
                0.00173607,
                0.04133107,
                -0.10913093,
                -0.04363778,
                -0.03149601,
                -0.04941225,
                0.09980538,
                0.03704486,
                -0.00210766,
                0.01737253,
                0.01727539,
                0.01278011,
                0.01212878,
                0.03876888,
                -0.00559142,
            ]
        )
        # Make sure the total rms error is less than 0.35 microns off
        # from the OPD truth as a sanity check
        self.assertLess(
            np.sqrt(np.sum(np.square(zernCoeffAvgR40 - trueZernCoeffR40))), 0.35
        )