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, )
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, )
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 )
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)
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)
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 )