def setUp(self): # Load sample input from disk testDir = os.path.dirname(__file__) self.srcSet = SourceCatalog.readFits(os.path.join(testDir, "v695833-e0-c000.xy.fits")) self.bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(2048, 4612)) # approximate # create an exposure with the right metadata; the closest thing we have is # apparently v695833-e0-c000-a00.sci.fits, which is much too small smallExposure = ExposureF(os.path.join(testDir, "v695833-e0-c000-a00.sci.fits")) self.exposure = ExposureF(self.bbox) self.exposure.setWcs(smallExposure.getWcs()) self.exposure.setFilter(smallExposure.getFilter()) # copy the pixels we can, in case the user wants a debug display mi = self.exposure.getMaskedImage() mi.assign(smallExposure.getMaskedImage(), smallExposure.getBBox()) logLevel = Log.INFO refCatDir = os.path.join(testDir, "data", "sdssrefcat") butler = Butler(refCatDir) refObjLoader = LoadIndexedReferenceObjectsTask(butler=butler) astrometryConfig = AstrometryTask.ConfigClass() self.astrom = AstrometryTask(config=astrometryConfig, refObjLoader=refObjLoader) self.astrom.log.setLevel(logLevel) # Since our sourceSelector is a registry object we have to wait for it to be created # before setting default values. self.astrom.matcher.sourceSelector.config.minSnr = 0
def testUsedFlag(self): """Test that the solver will record number of sources used to table if it is passed a schema on initialization. """ self.exposure.setWcs(self.tanWcs) config = AstrometryTask.ConfigClass() config.wcsFitter.order = 2 config.wcsFitter.numRejIter = 0 sourceSchema = afwTable.SourceTable.makeMinimalSchema() measBase.SingleFrameMeasurementTask( schema=sourceSchema) # expand the schema # schema must be passed to the solver task constructor solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema) sourceCat = self.makeSourceCat(self.tanWcs, sourceSchema=sourceSchema) results = solver.run( sourceCat=sourceCat, exposure=self.exposure, ) # check that the used flag is set the right number of times count = 0 for source in sourceCat: if source.get('calib_astrometry_used'): count += 1 self.assertEqual(count, len(results.matches))
def doTest(self, pixelsToTanPixels, order=3): """Test using pixelsToTanPixels to distort the source positions """ distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels, wcs=self.tanWcs, modifyActualPixels=False) self.exposure.setWcs(distortedWcs) sourceCat = self.makeSourceCat(distortedWcs) config = AstrometryTask.ConfigClass() config.wcsFitter.order = order config.wcsFitter.numRejIter = 0 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader) results = solver.run( sourceCat=sourceCat, exposure=self.exposure, ) fitWcs = self.exposure.getWcs() self.assertRaises(Exception, self.assertWcsAlmostEqualOverBBox, fitWcs, distortedWcs) self.assertWcsAlmostEqualOverBBox(distortedWcs, fitWcs, self.bbox, maxDiffSky=0.01*afwGeom.arcseconds, maxDiffPix=0.02) srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"]) refCoordKey = afwTable.CoordKey(results.refCat.schema["coord"]) refCentroidKey = afwTable.Point2DKey(results.refCat.schema["centroid"]) maxAngSep = afwGeom.Angle(0) maxPixSep = 0 for refObj, src, d in results.matches: refCoord = refObj.get(refCoordKey) refPixPos = refObj.get(refCentroidKey) srcCoord = src.get(srcCoordKey) srcPixPos = src.getCentroid() angSep = refCoord.separation(srcCoord) maxAngSep = max(maxAngSep, angSep) pixSep = math.hypot(*(srcPixPos-refPixPos)) maxPixSep = max(maxPixSep, pixSep) print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),)) print("max pixel separation = %0.3f" % (maxPixSep,)) self.assertLess(maxAngSep.asArcseconds(), 0.0026) self.assertLess(maxPixSep, 0.015) # try again, but without fitting the WCS config.forceKnownWcs = True solverNoFit = AstrometryTask(config=config, refObjLoader=self.refObjLoader) self.exposure.setWcs(distortedWcs) resultsNoFit = solverNoFit.run( sourceCat=sourceCat, exposure=self.exposure, ) self.assertIsNone(resultsNoFit.scatterOnSky) # fitting should result in matches that are at least as good # (strictly speaking fitting might result in a larger match list with # some outliers, but in practice this test passes) meanFitDist = np.mean([match.distance for match in results.matches]) meanNoFitDist = np.mean([match.distance for match in resultsNoFit.matches]) self.assertLessEqual(meanFitDist, meanNoFitDist)
def testMagnitudeOutlierRejection(self): """Test rejection of magnitude outliers. This test only tests the outlier rejection, and not any other part of the matching or astrometry fitter. """ config = AstrometryTask.ConfigClass() config.doMagnitudeOutlierRejection = True config.magnitudeOutlierRejectionNSigma = 4.0 solver = AstrometryTask(config=config, refObjLoader=None) nTest = 100 refSchema = lsst.afw.table.SimpleTable.makeMinimalSchema() refSchema.addField('refFlux', 'F') refCat = lsst.afw.table.SimpleCatalog(refSchema) refCat.resize(nTest) srcSchema = lsst.afw.table.SourceTable.makeMinimalSchema() srcSchema.addField('srcFlux', 'F') srcCat = lsst.afw.table.SourceCatalog(srcSchema) srcCat.resize(nTest) np.random.seed(12345) refMag = np.full(nTest, 20.0) srcMag = np.random.normal(size=nTest, loc=0.0, scale=1.0) # Determine the sigma of the random sample zp = np.median(refMag[:-4] - srcMag[:-4]) sigma = scipy.stats.median_abs_deviation(srcMag[:-4], scale='normal') # Deliberately alter some magnitudes to be outliers. srcMag[-3] = (config.magnitudeOutlierRejectionNSigma + 0.1) * sigma + ( 20.0 - zp) srcMag[-4] = -(config.magnitudeOutlierRejectionNSigma + 0.1) * sigma + (20.0 - zp) refCat['refFlux'] = (refMag * units.ABmag).to_value(units.nJy) srcCat['srcFlux'] = 10.0**(srcMag / (-2.5)) # Deliberately poison some reference fluxes. refCat['refFlux'][-1] = np.inf refCat['refFlux'][-2] = np.nan matchesIn = [] for ref, src in zip(refCat, srcCat): matchesIn.append( lsst.afw.table.ReferenceMatch(first=ref, second=src, distance=0.0)) matchesOut = solver._removeMagnitudeOutliers('srcFlux', 'refFlux', matchesIn) # We should lose the 4 outliers we created. self.assertEqual(len(matchesOut), len(matchesIn) - 4)
def run(): exposure, srcCat = loadData() schema = srcCat.getSchema() # # Create the astrometry task # config = AstrometryTask.ConfigClass() config.refObjLoader.filterMap = {"_unknown_": "r"} config.matcher.sourceFluxType = "Psf" # sample catalog does not contain aperture flux aTask = AstrometryTask(config=config) # # And the photometry Task # config = PhotoCalTask.ConfigClass() config.applyColorTerms = False # we don't have any available, so this suppresses a warning pTask = PhotoCalTask(config=config, schema=schema) # # The tasks may have added extra elements to the schema (e.g. AstrometryTask's centroidKey to # handle distortion; photometryTask's config.outputField). If this is so, we need to add # these columns to the Source table. # # We wouldn't need to do this if we created the schema prior to measuring the exposure, # but in this case we read the sources from disk # if schema != srcCat.getSchema(): # the tasks added fields print("Adding columns to the source catalogue") cat = afwTable.SourceCatalog(schema) cat.table.defineCentroid(srcCat.table.getCentroidDefinition()) cat.table.definePsfFlux(srcCat.table.getPsfFluxDefinition()) scm = afwTable.SchemaMapper(srcCat.getSchema(), schema) for schEl in srcCat.getSchema(): scm.addMapping(schEl.getKey(), True) cat.extend(srcCat, True, scm) # copy srcCat to cat, adding new columns srcCat = cat del cat # # Process the data # matches = aTask.run(exposure, srcCat).matches result = pTask.run(exposure, matches) calib = result.calib fm0, fm0Err = calib.getFluxMag0() print("Used %d calibration sources out of %d matches" % (len(result.matches), len(matches))) delta = result.arrays.refMag - result.arrays.srcMag q25, q75 = np.percentile(delta, [25, 75]) print("RMS error is %.3fmmsg (robust %.3f, Calib says %.3f)" % (np.std(delta), 0.741 * (q75 - q25), 2.5 / np.log(10) * fm0Err / fm0))
def testMaxMeanDistance(self): """If the astrometric fit does not satisfy the maxMeanDistanceArcsec threshold, ensure task raises an lsst.pipe.base.TaskError. """ self.exposure.setWcs(self.tanWcs) config = AstrometryTask.ConfigClass() config.maxMeanDistanceArcsec = 0.0 # To ensure a "deemed" WCS failure solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader) sourceCat = self.makeSourceCat(self.tanWcs, doScatterCentroids=True) with self.assertRaisesRegex(pipeBase.TaskError, "Fatal astrometry failure detected"): solver.run(sourceCat=sourceCat, exposure=self.exposure)
def testUsedFlag(self): """Test that the solver will record number of sources used to table if it is passed a schema on initialization. """ distortedWcs = afwImage.DistortedTanWcs(self.tanWcs, afwGeom.IdentityXYTransform()) self.exposure.setWcs(distortedWcs) loadRes = self.refObjLoader.loadPixelBox(bbox=self.bbox, wcs=distortedWcs, filterName="r") refCat = loadRes.refCat refCentroidKey = afwTable.Point2DKey(refCat.schema["centroid"]) refFluxRKey = refCat.schema["r_flux"].asKey() sourceSchema = afwTable.SourceTable.makeMinimalSchema() measBase.SingleFrameMeasurementTask( schema=sourceSchema) # expand the schema config = AstrometryTask.ConfigClass() config.wcsFitter.order = 2 config.wcsFitter.numRejIter = 0 # schema must be passed to the solver task constructor solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema) sourceCat = afwTable.SourceCatalog(sourceSchema) sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) sourceFluxKey = sourceSchema["slot_ApFlux_flux"].asKey() sourceFluxSigmaKey = sourceSchema["slot_ApFlux_fluxSigma"].asKey() for refObj in refCat: src = sourceCat.addNew() src.set(sourceCentroidKey, refObj.get(refCentroidKey)) src.set(sourceFluxKey, refObj.get(refFluxRKey)) src.set(sourceFluxSigmaKey, refObj.get(refFluxRKey) / 100) results = solver.run( sourceCat=sourceCat, exposure=self.exposure, ) # check that the used flag is set the right number of times count = 0 for source in sourceCat: if source.get('calib_astrometryUsed'): count += 1 self.assertEqual(count, len(results.matches))
def runAstrometry(self, butler, exp, icSrc): refObjLoaderConfig = LoadIndexedReferenceObjectsTask.ConfigClass() refObjLoaderConfig.ref_dataset_name = 'gaia_dr2_20191105' refObjLoaderConfig.pixelMargin = 1000 refObjLoader = LoadIndexedReferenceObjectsTask( butler=butler, config=refObjLoaderConfig) astromConfig = AstrometryTask.ConfigClass() astromConfig.wcsFitter.retarget(FitAffineWcsTask) astromConfig.referenceSelector.doMagLimit = True magLimit = MagnitudeLimit() magLimit.minimum = 1 magLimit.maximum = 15 astromConfig.referenceSelector.magLimit = magLimit astromConfig.referenceSelector.magLimit.fluxField = "phot_g_mean_flux" astromConfig.matcher.maxRotationDeg = 5.99 astromConfig.matcher.maxOffsetPix = 3000 astromConfig.sourceSelector['matcher'].minSnr = 10 solver = AstrometryTask(config=astromConfig, refObjLoader=refObjLoader) # TODO: Change this to doing this the proper way referenceFilterName = self.config.referenceFilterOverride referenceFilterLabel = afwImage.FilterLabel( physical=referenceFilterName, band=referenceFilterName) originalFilterLabel = exp.getFilterLabel( ) # there's a better way of doing this with the task I think exp.setFilterLabel(referenceFilterLabel) try: astromResult = solver.run(sourceCat=icSrc, exposure=exp) exp.setFilterLabel(originalFilterLabel) except (RuntimeError, TaskError): self.log.warn("Solver failed to run completely") exp.setFilterLabel(originalFilterLabel) return None scatter = astromResult.scatterOnSky.asArcseconds() if scatter < 1: return astromResult else: self.log.warn("Failed to find an acceptable match") return None
def run(): exposure, srcCat = loadData() schema = srcCat.getSchema() # # Create the reference catalog loader # refCatDir = os.path.join(lsst.utils.getPackageDir('meas_astrom'), 'tests', 'data', 'sdssrefcat') butler = Butler(refCatDir) refObjLoader = LoadIndexedReferenceObjectsTask(butler=butler) # # Create the astrometry task # config = AstrometryTask.ConfigClass() config.matcher.sourceFluxType = "Psf" # sample catalog does not contain aperture flux config.matcher.minSnr = 0 # disable S/N test because sample catalog does not contain flux sigma aTask = AstrometryTask(config=config, refObjLoader=refObjLoader) # # And the photometry Task # config = PhotoCalTask.ConfigClass() config.applyColorTerms = False # we don't have any available, so this suppresses a warning # The associated data has been prepared on the basis that we use PsfFlux to perform photometry. config.fluxField = "base_PsfFlux_flux" pTask = PhotoCalTask(config=config, schema=schema) # # The tasks may have added extra elements to the schema (e.g. AstrometryTask's centroidKey to # handle distortion; photometryTask's config.outputField). If this is so, we need to add # these columns to the Source table. # # We wouldn't need to do this if we created the schema prior to measuring the exposure, # but in this case we read the sources from disk # if schema != srcCat.getSchema(): # the tasks added fields print("Adding columns to the source catalogue") cat = afwTable.SourceCatalog(schema) cat.table.defineCentroid(srcCat.table.getCentroidDefinition()) cat.table.definePsfFlux(srcCat.table.getPsfFluxDefinition()) scm = afwTable.SchemaMapper(srcCat.getSchema(), schema) for schEl in srcCat.getSchema(): scm.addMapping(schEl.getKey(), True) cat.extend(srcCat, True, scm) # copy srcCat to cat, adding new columns srcCat = cat del cat # # Process the data # matches = aTask.run(exposure, srcCat).matches result = pTask.run(exposure, matches) calib = result.calib fm0, fm0Err = calib.getFluxMag0() print("Used %d calibration sources out of %d matches" % (len(result.matches), len(matches))) delta = result.arrays.refMag - result.arrays.srcMag q25, q75 = np.percentile(delta, [25, 75]) print("RMS error is %.3fmmsg (robust %.3f, Calib says %.3f)" % (np.std(delta), 0.741 * (q75 - q25), 2.5 / np.log(10) * fm0Err / fm0))