def testWarnings(self): """Test that approximateWcs raises a UserWarning when it cannot achieve desired tolerance""" radialTransform = afwGeom.makeRadialTransform([0, 2.0, 3.0]) wcs = afwGeom.makeModifiedWcs(pixelTransform=radialTransform, wcs=self.tanWcs, modifyActualPixels=False) with self.assertRaises(UserWarning): approximateWcs(wcs=wcs, bbox=self.bbox, order=2)
def doTest(self, name, transform, order=3, doPlot=False): """Add the specified distorting transform to a TAN WCS and fit it The resulting WCS pixelToSky method acts as follows: pixelToSky(transform.applyForward(pixels)) """ wcs = afwGeom.makeModifiedWcs(pixelTransform=transform, wcs=self.tanWcs, modifyActualPixels=False) fitWcs = approximateWcs( wcs=wcs, bbox=self.bbox, order=order, ) if doPlot: self.plotWcs(wcs, fitWcs, self.bbox, transform) msg = "ERROR: %s failed with order %s" % (name, order) self.assertWcsAlmostEqualOverBBox(wcs, fitWcs, self.bbox, maxDiffSky=0.001 * afwGeom.arcseconds, maxDiffPix=0.02, msg=msg)
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 doTest(self, pixelsToTanPixels): """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) print("number of stars =", len(sourceCat)) config = ANetAstrometryTask.ConfigClass() solver = ANetAstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=self.makeSourceSchema()) 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*lsst.geom.arcseconds, maxDiffPix=0.02) srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"]) refCoordKey = afwTable.CoordKey(results.refCat.schema["coord"]) maxAngSep = 0*lsst.geom.radians maxPixSep = 0 for refObj, src, d in results.matches: refCoord = refObj.get(refCoordKey) refPixPos = fitWcs.skyToPixel(refCoord) 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.005) self.assertLess(maxPixSep, 0.03) # try again, but without fitting the WCS config.forceKnownWcs = True solverNoFit = ANetAstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=self.makeSourceSchema()) self.exposure.setWcs(distortedWcs) noFitResults = solverNoFit.run( sourceCat=sourceCat, exposure=self.exposure, ) self.assertGreater(len(noFitResults.refCat), 300)
def testLargeDistortion(self): # This transform is about as extreme as I can get: # using 0.0005 in the last value appears to produce numerical issues. # It produces a maximum deviation of 459 pixels, which should be sufficient. pixelsToTanPixels = afwGeom.makeRadialTransform([0.0, 1.1, 0.0004]) self.distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels, wcs=self.wcs, modifyActualPixels=False) def applyDistortion(src): out = src.table.copyRecord(src) out.set(out.table.getCentroidKey(), pixelsToTanPixels.applyInverse(src.getCentroid())) return out self.singleTestInstance(self.filename, applyDistortion)
def rotateExposure(exp, nDegrees, kernelName='lanczos4', logger=None): """Rotate an exposure by nDegrees clockwise. Parameters ---------- exp : `lsst.afw.image.exposure.Exposure` The exposure to rotate nDegrees : `float` Number of degrees clockwise to rotate by kernelName : `str` Name of the warping kernel, used to instantiate the warper. logger : `lsst.log.Log` Logger for logging warnings Returns ------- rotatedExp : `lsst.afw.image.exposure.Exposure` A copy of the input exposure, rotated by nDegrees """ nDegrees = nDegrees % 360 if not logger: logger = lsstLog.getLogger('atmospec.utils') wcs = exp.getWcs() if not wcs: logger.warn( "Can't rotate exposure without a wcs - returning exp unrotated") return exp.clone( ) # return a clone so it's always returning a copy as this is what default does warper = afwMath.Warper(kernelName) if isinstance(exp, afwImage.ExposureU): # TODO: remove once this bug is fixed - DM-20258 logger.info('Converting ExposureU to ExposureF due to bug') logger.info('Remove this workaround after DM-20258') exp = afwImage.ExposureF(exp, deep=True) affineRotTransform = geom.AffineTransform.makeRotation(nDegrees * geom.degrees) transformP2toP2 = afwGeom.makeTransform(affineRotTransform) rotatedWcs = afwGeom.makeModifiedWcs(transformP2toP2, wcs, False) rotatedExp = warper.warpExposure(rotatedWcs, exp) # rotatedExp.setXY0(geom.Point2I(0, 0)) # TODO: check no longer required return rotatedExp
def setUp(self): # Make fake sources self.nSources = 10 self.bbox = geom.Box2I(geom.Point2I(0, 0), geom.Extent2I(1024, 1153)) self.xyLoc = 100 dataset = measTests.TestDataset(self.bbox) for srcIdx in range(self.nSources): dataset.addSource(100000.0, geom.Point2D(srcIdx*self.xyLoc, srcIdx*self.xyLoc)) schema = dataset.makeMinimalSchema() schema.addField("base_PixelFlags_flag", type="Flag") schema.addField("base_PixelFlags_flag_offimage", type="Flag") self.exposure, catalog = dataset.realize( 10.0, schema, randomSeed=1234) for src in catalog: src.setCoord(self.exposure.getWcs().pixelToSky(src.getCentroid())) # Non-invertible WCS to test robustness to distortions. # Coefficients transform (x - 1e-7*x^3 -> x, y -> y); see docs for PolyMap. pixelCoeffs = np.array([[-1.0e-7, 1, 3, 0], [1.0, 1, 1, 0], [1.0, 2, 0, 1], ]) self.exposure.setWcs(afwGeom.makeModifiedWcs( afwGeom.TransformPoint2ToPoint2(ast.PolyMap(pixelCoeffs, 2, options="IterInverse=1")), self.exposure.wcs, modifyActualPixels=False )) # Convert to task required format self.testDiaSources = catalog.asAstropy().to_pandas() self.testDiaSources.rename(columns={"coord_ra": "ra", "coord_dec": "decl"}, inplace=True,) self.testDiaSources.loc[:, "ra"] = np.rad2deg(self.testDiaSources["ra"]) self.testDiaSources.loc[:, "decl"] = np.rad2deg(self.testDiaSources["decl"]) self.testDiaSources["ssObjectId"] = 0 # Grab a subset to treat as solar system objects self.testSsObjects = self.testDiaSources[2:8].reset_index() # Assign them ids starting from 1. self.testSsObjects.loc[:, "ssObjectId"] = np.arange( 1, len(self.testSsObjects) + 1, dtype=int,) self.testSsObjects["Err(arcsec)"] = np.ones(len(self.testSsObjects))
def testMakeModifiedWcsNoActualPixels(self): """Test makeModifiedWcs on a SkyWcs that has no ACTUAL_PIXELS frame """ cdMatrix = makeCdMatrix(scale=self.scale) originalWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) originalFrameDict = originalWcs.getFrameDict() # make an arbitrary but reasonable transform to insert using makeModifiedWcs pixelTransform = makeRadialTransform([0.0, 1.0, 0.0, 0.0011]) # the result of the insertion should be as follows desiredPixelsToSky = pixelTransform.then(originalWcs.getTransform()) pixPointList = ( # arbitrary but reasonable Point2D(0.0, 0.0), Point2D(1000.0, 0.0), Point2D(0.0, 2000.0), Point2D(-1111.0, -2222.0), ) for modifyActualPixels in (False, True): modifiedWcs = makeModifiedWcs( pixelTransform=pixelTransform, wcs=originalWcs, modifyActualPixels=modifyActualPixels) modifiedFrameDict = modifiedWcs.getFrameDict() skyList = modifiedWcs.pixelToSky(pixPointList) # compare pixels to sky desiredSkyList = desiredPixelsToSky.applyForward(pixPointList) self.assertSpherePointListsAlmostEqual(skyList, desiredSkyList) # compare pixels to IWC pixelsToIwc = TransformPoint2ToPoint2( modifiedFrameDict.getMapping("PIXELS", "IWC")) desiredPixelsToIwc = TransformPoint2ToPoint2( pixelTransform.getMapping().then( originalFrameDict.getMapping("PIXELS", "IWC"))) self.assertPairListsAlmostEqual( pixelsToIwc.applyForward(pixPointList), desiredPixelsToIwc.applyForward(pixPointList)) self.checkNonFitsWcs(modifiedWcs)
def doTest(self, name, transform, order=3, doPlot=False): """Add the specified distorting transform to a TAN WCS and fit it The resulting WCS pixelToSky method acts as follows: pixelToSky(transform.applyForward(pixels)) """ wcs = afwGeom.makeModifiedWcs(pixelTransform=transform, wcs=self.tanWcs, modifyActualPixels=False) fitWcs = approximateWcs( wcs=wcs, bbox=self.bbox, order=order, ) if doPlot: self.plotWcs(wcs, fitWcs, self.bbox, transform) msg = "ERROR: %s failed with order %s" % (name, order) self.assertWcsAlmostEqualOverBBox(wcs, fitWcs, self.bbox, maxDiffSky=0.001*lsst.geom.arcseconds, maxDiffPix=0.02, msg=msg)
def testMakeModifiedWcsWithActualPixels(self): """Test makeModifiedWcs on a SkyWcs that has an ACTUAL_PIXELS frame """ cdMatrix = makeCdMatrix(scale=self.scale) baseWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) # model actual pixels to pixels as an arbitrary zoom factor; # this is not realistic, but is fine for a unit test actualPixelsToPixels = TransformPoint2ToPoint2(ast.ZoomMap(2, 0.72)) originalWcs = addActualPixelsFrame(baseWcs, actualPixelsToPixels) originalFrameDict = originalWcs.getFrameDict() # make an arbitrary but reasonable transform to insert using makeModifiedWcs pixelTransform = makeRadialTransform([0.0, 1.0, 0.0, 0.0011 ]) # arbitrary but reasonable pixPointList = ( # arbitrary but reasonable Point2D(0.0, 0.0), Point2D(1000.0, 0.0), Point2D(0.0, 2000.0), Point2D(-1111.0, -2222.0), ) for modifyActualPixels in (True, False): modifiedWcs = makeModifiedWcs( pixelTransform=pixelTransform, wcs=originalWcs, modifyActualPixels=modifyActualPixels) modifiedFrameDict = modifiedWcs.getFrameDict() self.assertEqual( modifiedFrameDict.getFrame(modifiedFrameDict.BASE).domain, "ACTUAL_PIXELS") modifiedActualPixelsToPixels = \ TransformPoint2ToPoint2(modifiedFrameDict.getMapping("ACTUAL_PIXELS", "PIXELS")) modifiedPixelsToIwc = TransformPoint2ToPoint2( modifiedFrameDict.getMapping("PIXELS", "IWC")) # compare pixels to sky skyList = modifiedWcs.pixelToSky(pixPointList) if modifyActualPixels: desiredPixelsToSky = pixelTransform.then( originalWcs.getTransform()) else: originalPixelsToSky = \ TransformPoint2ToSpherePoint(originalFrameDict.getMapping("PIXELS", "SKY")) desiredPixelsToSky = actualPixelsToPixels.then( pixelTransform).then(originalPixelsToSky) desiredSkyList = desiredPixelsToSky.applyForward(pixPointList) self.assertSpherePointListsAlmostEqual(skyList, desiredSkyList) # compare ACTUAL_PIXELS to PIXELS and PIXELS to IWC if modifyActualPixels: # check that ACTUAL_PIXELS to PIXELS has been modified as expected desiredActualPixelsToPixels = pixelTransform.then( actualPixelsToPixels) self.assertPairListsAlmostEqual( modifiedActualPixelsToPixels.applyForward(pixPointList), desiredActualPixelsToPixels.applyForward(pixPointList)) # check that PIXELS to IWC is unchanged originalPixelsToIwc = TransformPoint2ToPoint2( originalFrameDict.getMapping("PIXELS", "IWC")) self.assertPairListsAlmostEqual( modifiedPixelsToIwc.applyForward(pixPointList), originalPixelsToIwc.applyForward(pixPointList)) else: # check that ACTUAL_PIXELS to PIXELS is unchanged self.assertPairListsAlmostEqual( actualPixelsToPixels.applyForward(pixPointList), actualPixelsToPixels.applyForward(pixPointList)) # check that PIXELS to IWC has been modified as expected desiredPixelsToIwc = TransformPoint2ToPoint2( pixelTransform.getMapping().then( originalFrameDict.getMapping("PIXELS", "IWC"))) self.assertPairListsAlmostEqual( modifiedPixelsToIwc.applyForward(pixPointList), desiredPixelsToIwc.applyForward(pixPointList)) self.checkNonFitsWcs(modifiedWcs)
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*lsst.geom.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 = 0*lsst.geom.radians 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.0038) self.assertLess(maxPixSep, 0.021) # try again, invoking the reference selector config.referenceSelector.doUnresolved = True config.referenceSelector.unresolved.name = 'resolved' solverRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader) self.exposure.setWcs(distortedWcs) resultsRefSelect = solverRefSelect.run( sourceCat=sourceCat, exposure=self.exposure, ) self.assertLess(len(resultsRefSelect.matches), len(results.matches)) # try again, but without fitting the WCS, no reference selector config.referenceSelector.doUnresolved = False 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) # try once again, without fitting the WCS, with the reference selector # (this goes through a different code path) config.referenceSelector.doUnresolved = True solverNoFitRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader) resultsNoFitRefSelect = solverNoFitRefSelect.run( sourceCat=sourceCat, exposure=self.exposure, ) self.assertLess(len(resultsNoFitRefSelect.matches), len(resultsNoFit.matches))