def testLists(self): """Check updating lists of reference objects and sources""" # arbitrary but reasonable values that are intentionally different than testCatalogs maxPix = 1000 numPoints = 10 self.setCatalogs(maxPix=maxPix, numPoints=numPoints) # update the catalogs as lists afwTable.updateSourceCoords(self.wcs, [s for s in self.sourceCat]) afwTable.updateRefCentroids(self.wcs, [r for r in self.refCat]) self.checkCatalogs()
def testLists(self): """Check updating lists of reference objects and sources""" # arbitrary but reasonable values that are intentionally different than # testCatalogs maxPix = 1000 numPoints = 10 self.setCatalogs(maxPix=maxPix, numPoints=numPoints) # update the catalogs as lists afwTable.updateSourceCoords(self.wcs, [s for s in self.sourceCat]) afwTable.updateRefCentroids(self.wcs, [r for r in self.refCat]) self.checkCatalogs()
def testCatalogs(self): """Check updating catalogs of reference objects and sources""" # arbitrary but reasonable values that are intentionally different than testLists maxPix = 2000 numPoints = 9 self.setCatalogs(maxPix=maxPix, numPoints=numPoints) # update the catalogs afwTable.updateSourceCoords(self.wcs, self.sourceCat) afwTable.updateRefCentroids(self.wcs, self.refCat) # check that centroids and coords match self.checkCatalogs()
def testCatalogs(self): """Check updating catalogs of reference objects and sources""" # arbitrary but reasonable values that are intentionally different than # testLists maxPix = 2000 numPoints = 9 self.setCatalogs(maxPix=maxPix, numPoints=numPoints) # update the catalogs afwTable.updateSourceCoords(self.wcs, self.sourceCat) afwTable.updateRefCentroids(self.wcs, self.refCat) # check that centroids and coords match self.checkCatalogs()
def testSourceCenter(self): """Check that a source at the center is handled as expected""" src = self.sourceCat.addNew() src.set(self.srcCentroidKey, self.crpix) # initial coord should be nan; as a sanity-check nanSourceCoord = self.sourceCat[0].get(self.srcCoordKey) for val in nanSourceCoord: self.assertTrue(math.isnan(val)) # compute coord should be crval afwTable.updateSourceCoords(self.wcs, self.sourceCat) srcCoord = self.sourceCat[0].get(self.srcCoordKey) self.assertPairsAlmostEqual(srcCoord, self.crval) # centroid should not be changed; also make sure that getCentroid words self.assertEqual(self.sourceCat[0].getCentroid(), self.crpix)
def testSourceCenter(self): """Check that a source at the center is handled as expected""" src = self.sourceCat.addNew() src.set(self.srcCentroidKey, self.crpix) # initial coord should be nan; as a sanity-check nanSourceCoord = self.sourceCat[0].get(self.srcCoordKey) for val in nanSourceCoord: self.assertTrue(math.isnan(val)) # compute coord should be crval afwTable.updateSourceCoords(self.wcs, self.sourceCat) srcCoord = self.sourceCat[0].get(self.srcCoordKey) self.assertPairsAlmostEqual(srcCoord, self.crval) # centroid should not be changed; also make sure that getCentroid words self.assertEqual(self.sourceCat[0].getCentroid(), self.crpix)
def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None): """!Fit a TAN-SIP WCS from a list of reference object/source matches @param[in,out] matches a list of lsst::afw::table::ReferenceMatch The following fields are read: - match.first (reference object) coord - match.second (source) centroid The following fields are written: - match.first (reference object) centroid, - match.second (source) centroid - match.distance (on sky separation, in radians) @param[in] initWcs initial WCS @param[in] bbox the region over which the WCS will be valid (an lsst:afw::geom::Box2I); if None or an empty box then computed from matches @param[in,out] refCat reference object catalog, or None. If provided then all centroids are updated with the new WCS, otherwise only the centroids for ref objects in matches are updated. Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". @param[in,out] sourceCat source catalog, or None. If provided then coords are updated with the new WCS; otherwise only the coords for sources in matches are updated. Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec". @param[in] exposure Ignored; present for consistency with FitSipDistortionTask. @return an lsst.pipe.base.Struct with the following fields: - wcs the fit WCS as an lsst.afw.geom.Wcs - scatterOnSky median on-sky separation between reference objects and sources in "matches", as an lsst.afw.geom.Angle """ if bbox is None: bbox = afwGeom.Box2I() import lsstDebug debug = lsstDebug.Info(__name__) wcs = self.initialWcs(matches, initWcs) rejected = np.zeros(len(matches), dtype=bool) for rej in range(self.config.numRejIter): sipObject = self._fitWcs( [mm for i, mm in enumerate(matches) if not rejected[i]], wcs) wcs = sipObject.getNewWcs() rejected = self.rejectMatches(matches, wcs, rejected) if rejected.sum() == len(rejected): raise RuntimeError("All matches rejected in iteration %d" % (rej + 1, )) self.log.debug( "Iteration {0} of astrometry fitting: rejected {1} outliers, " "out of {2} total matches.".format(rej, rejected.sum(), len(rejected))) if debug.plot: print("Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter)) self.plotFit(matches, wcs, rejected) # Final fit after rejection sipObject = self._fitWcs( [mm for i, mm in enumerate(matches) if not rejected[i]], wcs) wcs = sipObject.getNewWcs() if debug.plot: print("Plotting final fit") self.plotFit(matches, wcs, rejected) if refCat is not None: self.log.debug("Updating centroids in refCat") afwTable.updateRefCentroids(wcs, refList=refCat) else: self.log.warn( "Updating reference object centroids in match list; refCat is None" ) afwTable.updateRefCentroids( wcs, refList=[match.first for match in matches]) if sourceCat is not None: self.log.debug("Updating coords in sourceCat") afwTable.updateSourceCoords(wcs, sourceList=sourceCat) else: self.log.warn( "Updating source coords in match list; sourceCat is None") afwTable.updateSourceCoords( wcs, sourceList=[match.second for match in matches]) self.log.debug("Updating distance in match list") setMatchDistance(matches) scatterOnSky = sipObject.getScatterOnSky() if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec: raise pipeBase.TaskError( "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" % (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec)) return pipeBase.Struct( wcs=wcs, scatterOnSky=scatterOnSky, )
def refitWcs(self, sourceCat, exposure, matches): """!A final Wcs solution after matching and removing distortion Specifically, fitting the non-linear part, since the linear part has been provided by the matching engine. @param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog @param exposure Exposure of interest, an lsst.afw.image.ExposureF or D @param matches Astrometric matches, as a list of lsst.afw.table.ReferenceMatch @return the resolved-Wcs object, or None if config.solver.calculateSip is False. """ sip = None if self.config.solver.calculateSip: self.log.info("Refitting WCS") origMatches = matches wcs = exposure.getWcs() import lsstDebug display = lsstDebug.Info(__name__).display frame = lsstDebug.Info(__name__).frame pause = lsstDebug.Info(__name__).pause def fitWcs(initialWcs, title=None): """!Do the WCS fitting and display of the results""" sip = makeCreateWcsWithSip(matches, initialWcs, self.config.solver.sipOrder) resultWcs = sip.getNewWcs() if display: showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame, title=title, pause=pause) return resultWcs, sip.getScatterOnSky() numRejected = 0 try: for i in range(self.config.rejectIter): wcs, scatter = fitWcs(wcs, title="Iteration %d" % i) ref = np.array([wcs.skyToPixel(m.first.getCoord()) for m in matches]) src = np.array([m.second.getCentroid() for m in matches]) diff = ref - src rms = diff.std() trimmed = [] for d, m in zip(diff, matches): if np.all(np.abs(d) < self.config.rejectThresh*rms): trimmed.append(m) else: numRejected += 1 if len(matches) == len(trimmed): break matches = trimmed # Final fit after rejection iterations wcs, scatter = fitWcs(wcs, title="Final astrometry") except lsst.pex.exceptions.LengthError as e: self.log.warn("Unable to fit SIP: %s", e) self.log.info("Astrometric scatter: %f arcsec (%d matches, %d rejected)", scatter.asArcseconds(), len(matches), numRejected) exposure.setWcs(wcs) # Apply WCS to sources updateSourceCoords(wcs, sourceCat) else: self.log.warn("Not calculating a SIP solution; matches may be suspect") if self._display: frame = lsstDebug.Info(__name__).frame displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches, frame=frame, pause=False) return sip
def doTest(self, name, func, order=3, numIter=4, specifyBBox=False, doPlot=False, doPrint=False): """Apply func(x, y) to each source in self.sourceCat, then fit and check the resulting WCS """ bbox = lsst.geom.Box2I() for refObj, src, d in self.matches: origPos = src.get(self.srcCentroidKey) x, y = func(*origPos) distortedPos = lsst.geom.Point2D(*func(*origPos)) src.set(self.srcCentroidKey, distortedPos) bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(distortedPos))) tanSipWcs = self.tanWcs for i in range(numIter): if specifyBBox: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order, bbox) else: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order) tanSipWcs = sipObject.getNewWcs() setMatchDistance(self.matches) fitRes = lsst.pipe.base.Struct( wcs=tanSipWcs, scatterOnSky=sipObject.getScatterOnSky(), ) if doPrint: print("TAN-SIP metadata fit over bbox=", bbox) metadata = makeTanSipMetadata( crpix=tanSipWcs.getPixelOrigin(), crval=tanSipWcs.getSkyOrigin(), cdMatrix=tanSipWcs.getCdMatrix(), sipA=sipObject.getSipA(), sipB=sipObject.getSipB(), sipAp=sipObject.getSipAp(), sipBp=sipObject.getSipBp(), ) print(metadata.toString()) if doPlot: self.plotWcs(tanSipWcs, name=name) self.checkResults(fitRes, catsUpdated=False) if self.MatchClass == afwTable.ReferenceMatch: # reset source coord and reference centroid based on initial WCS afwTable.updateRefCentroids(wcs=self.tanWcs, refList=self.refCat) afwTable.updateSourceCoords(wcs=self.tanWcs, sourceList=self.sourceCat) fitterConfig = FitTanSipWcsTask.ConfigClass() fitterConfig.order = order fitterConfig.numIter = numIter fitter = FitTanSipWcsTask(config=fitterConfig) self.loadData() if specifyBBox: fitRes = fitter.fitWcs( matches=self.matches, initWcs=self.tanWcs, bbox=bbox, refCat=self.refCat, sourceCat=self.sourceCat, ) else: fitRes = fitter.fitWcs( matches=self.matches, initWcs=self.tanWcs, bbox=bbox, refCat=self.refCat, sourceCat=self.sourceCat, ) self.checkResults(fitRes, catsUpdated=True)
def testNull(self): """Check that an empty list causes no problems for either function""" afwTable.updateRefCentroids(self.wcs, []) afwTable.updateSourceCoords(self.wcs, [])
def _loadAndMatchCatalogs(repo, dataIds, matchRadius, doApplyExternalPhotoCalib=False, externalPhotoCalibName=None, doApplyExternalSkyWcs=False, externalSkyWcsName=None, skipTEx=False, skipNonSrd=False): """Load data from specific visits and returned a calibrated catalog matched with a reference. Parameters ---------- repo : `str` or `lsst.daf.persistence.Butler` A Butler or a repository URL that can be used to construct one. dataIds : list of dict List of butler data IDs of Image catalogs to compare to reference. The calexp cpixel image is needed for the photometric calibration. matchRadius : `lsst.geom.Angle`, optional Radius for matching. Default is 1 arcsecond. doApplyExternalPhotoCalib : bool, optional Apply external photoCalib to calibrate fluxes. externalPhotoCalibName : str, optional Type of external `PhotoCalib` to apply. Currently supported are jointcal, fgcm, and fgcm_tract. Must be set if doApplyExternalPhotoCalib is True. doApplyExternalSkyWcs : bool, optional Apply external wcs to calibrate positions. externalSkyWcsName : str, optional Type of external `wcs` to apply. Currently supported is jointcal. Must be set if "doApplyExternalWcs" is True. skipTEx : `bool`, optional Skip TEx calculations (useful for older catalogs that don't have PsfShape measurements). skipNonSrd : `bool`, optional Skip any metrics not defined in the LSST SRD; default False. Returns ------- catalog : `lsst.afw.table.SourceCatalog` A new calibrated SourceCatalog. matches : `lsst.afw.table.GroupView` A GroupView of the matched sources. Raises ------ RuntimeError: Raised if "doApplyExternalPhotoCalib" is True and "externalPhotoCalibName" is None, or if "doApplyExternalSkyWcs" is True and "externalSkyWcsName" is None. """ if doApplyExternalPhotoCalib and externalPhotoCalibName is None: raise RuntimeError( "Must set externalPhotoCalibName if doApplyExternalPhotoCalib is True." ) if doApplyExternalSkyWcs and externalSkyWcsName is None: raise RuntimeError( "Must set externalSkyWcsName if doApplyExternalSkyWcs is True.") # Following # https://github.com/lsst/afw/blob/tickets/DM-3896/examples/repeatability.ipynb if isinstance(repo, dafPersist.Butler): butler = repo else: butler = dafPersist.Butler(repo) dataset = 'src' # 2016-02-08 MWV: # I feel like I could be doing something more efficient with # something along the lines of the following: # dataRefs = [dafPersist.ButlerDataRef(butler, vId) for vId in dataIds] ccdKeyName = getCcdKeyName(dataIds[0]) # Hack to support raft and sensor 0,1 IDs as ints for multimatch if ccdKeyName == 'sensor': ccdKeyName = 'raft_sensor_int' for vId in dataIds: vId[ccdKeyName] = raftSensorToInt(vId) schema = butler.get(dataset + "_schema").schema mapper = SchemaMapper(schema) mapper.addMinimalSchema(schema) mapper.addOutputField(Field[float]('base_PsfFlux_snr', 'PSF flux SNR')) mapper.addOutputField(Field[float]('base_PsfFlux_mag', 'PSF magnitude')) mapper.addOutputField(Field[float]('base_PsfFlux_magErr', 'PSF magnitude uncertainty')) if not skipNonSrd: # Needed because addOutputField(... 'slot_ModelFlux_mag') will add a field with that literal name aliasMap = schema.getAliasMap() # Possibly not needed since base_GaussianFlux is the default, but this ought to be safe modelName = aliasMap[ 'slot_ModelFlux'] if 'slot_ModelFlux' in aliasMap.keys( ) else 'base_GaussianFlux' mapper.addOutputField(Field[float](f'{modelName}_mag', 'Model magnitude')) mapper.addOutputField(Field[float](f'{modelName}_magErr', 'Model magnitude uncertainty')) mapper.addOutputField(Field[float](f'{modelName}_snr', 'Model flux snr')) mapper.addOutputField(Field[float]('e1', 'Source Ellipticity 1')) mapper.addOutputField(Field[float]('e2', 'Source Ellipticity 1')) mapper.addOutputField(Field[float]('psf_e1', 'PSF Ellipticity 1')) mapper.addOutputField(Field[float]('psf_e2', 'PSF Ellipticity 1')) newSchema = mapper.getOutputSchema() newSchema.setAliasMap(schema.getAliasMap()) # Create an object that matches multiple catalogs with same schema mmatch = MultiMatch(newSchema, dataIdFormat={ 'visit': np.int32, ccdKeyName: np.int32 }, radius=matchRadius, RecordClass=SimpleRecord) # create the new extented source catalog srcVis = SourceCatalog(newSchema) for vId in dataIds: if not butler.datasetExists('src', vId): print(f'Could not find source catalog for {vId}; skipping.') continue photoCalib = _loadPhotoCalib(butler, vId, doApplyExternalPhotoCalib, externalPhotoCalibName) if photoCalib is None: continue if doApplyExternalSkyWcs: wcs = _loadExternalSkyWcs(butler, vId, externalSkyWcsName) if wcs is None: continue # We don't want to put this above the first _loadPhotoCalib call # because we need to use the first `butler.get` in there to quickly # catch dataIDs with no usable outputs. try: # HSC supports these flags, which dramatically improve I/O # performance; support for other cameras is DM-6927. oldSrc = butler.get('src', vId, flags=SOURCE_IO_NO_FOOTPRINTS) except (OperationalError, sqlite3.OperationalError): oldSrc = butler.get('src', vId) print(len(oldSrc), "sources in ccd %s visit %s" % (vId[ccdKeyName], vId["visit"])) # create temporary catalog tmpCat = SourceCatalog(SourceCatalog(newSchema).table) tmpCat.extend(oldSrc, mapper=mapper) tmpCat['base_PsfFlux_snr'][:] = tmpCat['base_PsfFlux_instFlux'] \ / tmpCat['base_PsfFlux_instFluxErr'] if doApplyExternalSkyWcs: afwTable.updateSourceCoords(wcs, tmpCat) photoCalib.instFluxToMagnitude(tmpCat, "base_PsfFlux", "base_PsfFlux") if not skipNonSrd: tmpCat['slot_ModelFlux_snr'][:] = ( tmpCat['slot_ModelFlux_instFlux'] / tmpCat['slot_ModelFlux_instFluxErr']) photoCalib.instFluxToMagnitude(tmpCat, "slot_ModelFlux", "slot_ModelFlux") if not skipTEx: _, psf_e1, psf_e2 = ellipticity_from_cat( oldSrc, slot_shape='slot_PsfShape') _, star_e1, star_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_Shape') tmpCat['e1'][:] = star_e1 tmpCat['e2'][:] = star_e2 tmpCat['psf_e1'][:] = psf_e1 tmpCat['psf_e2'][:] = psf_e2 srcVis.extend(tmpCat, False) mmatch.add(catalog=tmpCat, dataId=vId) # Complete the match, returning a catalog that includes # all matched sources with object IDs that can be used to group them. matchCat = mmatch.finish() # Create a mapping object that allows the matches to be manipulated # as a mapping of object ID to catalog of sources. allMatches = GroupView.build(matchCat) return srcVis, allMatches
def testNull(self): """Check that an empty list causes no problems for either function""" afwTable.updateRefCentroids(self.wcs, []) afwTable.updateSourceCoords(self.wcs, [])
def match_catalogs(inputs, photoCalibs, astromCalibs, vIds, matchRadius, apply_external_wcs=False, logger=None): schema = inputs[0].schema mapper = SchemaMapper(schema) mapper.addMinimalSchema(schema) mapper.addOutputField(Field[float]('base_PsfFlux_snr', 'PSF flux SNR')) mapper.addOutputField(Field[float]('base_PsfFlux_mag', 'PSF magnitude')) mapper.addOutputField(Field[float]('base_PsfFlux_magErr', 'PSF magnitude uncertainty')) # Needed because addOutputField(... 'slot_ModelFlux_mag') will add a field with that literal name aliasMap = schema.getAliasMap() # Possibly not needed since base_GaussianFlux is the default, but this ought to be safe modelName = aliasMap['slot_ModelFlux'] if 'slot_ModelFlux' in aliasMap.keys( ) else 'base_GaussianFlux' mapper.addOutputField(Field[float](f'{modelName}_mag', 'Model magnitude')) mapper.addOutputField(Field[float](f'{modelName}_magErr', 'Model magnitude uncertainty')) mapper.addOutputField(Field[float](f'{modelName}_snr', 'Model flux snr')) mapper.addOutputField(Field[float]('e1', 'Source Ellipticity 1')) mapper.addOutputField(Field[float]('e2', 'Source Ellipticity 1')) mapper.addOutputField(Field[float]('psf_e1', 'PSF Ellipticity 1')) mapper.addOutputField(Field[float]('psf_e2', 'PSF Ellipticity 1')) mapper.addOutputField(Field[np.int32]('filt', 'filter code')) newSchema = mapper.getOutputSchema() newSchema.setAliasMap(schema.getAliasMap()) # Create an object that matches multiple catalogs with same schema mmatch = MultiMatch(newSchema, dataIdFormat={ 'visit': np.int32, 'detector': np.int32 }, radius=matchRadius, RecordClass=SimpleRecord) # create the new extended source catalog srcVis = SourceCatalog(newSchema) filter_dict = { 'u': 1, 'g': 2, 'r': 3, 'i': 4, 'z': 5, 'y': 6, 'HSC-U': 1, 'HSC-G': 2, 'HSC-R': 3, 'HSC-I': 4, 'HSC-Z': 5, 'HSC-Y': 6 } # Sort by visit, detector, then filter vislist = [v['visit'] for v in vIds] ccdlist = [v['detector'] for v in vIds] filtlist = [v['band'] for v in vIds] tab_vids = Table([vislist, ccdlist, filtlist], names=['vis', 'ccd', 'filt']) sortinds = np.argsort(tab_vids, order=('vis', 'ccd', 'filt')) for ind in sortinds: oldSrc = inputs[ind] photoCalib = photoCalibs[ind] wcs = astromCalibs[ind] vId = vIds[ind] if logger: logger.debug( f"{len(oldSrc)} sources in ccd {vId['detector']} visit {vId['visit']}" ) # create temporary catalog tmpCat = SourceCatalog(SourceCatalog(newSchema).table) tmpCat.extend(oldSrc, mapper=mapper) filtnum = filter_dict[vId['band']] tmpCat['filt'] = np.repeat(filtnum, len(oldSrc)) tmpCat['base_PsfFlux_snr'][:] = tmpCat['base_PsfFlux_instFlux'] \ / tmpCat['base_PsfFlux_instFluxErr'] if apply_external_wcs and wcs is not None: updateSourceCoords(wcs, tmpCat) photoCalib.instFluxToMagnitude(tmpCat, "base_PsfFlux", "base_PsfFlux") tmpCat['slot_ModelFlux_snr'][:] = ( tmpCat['slot_ModelFlux_instFlux'] / tmpCat['slot_ModelFlux_instFluxErr']) photoCalib.instFluxToMagnitude(tmpCat, "slot_ModelFlux", "slot_ModelFlux") _, psf_e1, psf_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_PsfShape') _, star_e1, star_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_Shape') tmpCat['e1'][:] = star_e1 tmpCat['e2'][:] = star_e2 tmpCat['psf_e1'][:] = psf_e1 tmpCat['psf_e2'][:] = psf_e2 srcVis.extend(tmpCat, False) mmatch.add(catalog=tmpCat, dataId=vId) # Complete the match, returning a catalog that includes # all matched sources with object IDs that can be used to group them. matchCat = mmatch.finish() # Create a mapping object that allows the matches to be manipulated # as a mapping of object ID to catalog of sources. # I don't think I can persist a group view, so this may need to be called in a subsequent task # allMatches = GroupView.build(matchCat) return srcVis, matchCat
def doTest(self, name, func, order=3, numIter=4, specifyBBox=False, doPlot=False, doPrint=False): """Apply func(x, y) to each source in self.sourceCat, then fit and check the resulting WCS """ bbox = lsst.geom.Box2I() for refObj, src, d in self.matches: origPos = src.get(self.srcCentroidKey) x, y = func(*origPos) distortedPos = lsst.geom.Point2D(*func(*origPos)) src.set(self.srcCentroidKey, distortedPos) bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(distortedPos))) tanSipWcs = self.tanWcs for i in range(numIter): if specifyBBox: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order, bbox) else: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order) tanSipWcs = sipObject.getNewWcs() setMatchDistance(self.matches) fitRes = lsst.pipe.base.Struct( wcs=tanSipWcs, scatterOnSky=sipObject.getScatterOnSky(), ) if doPrint: print("TAN-SIP metadata fit over bbox=", bbox) metadata = makeTanSipMetadata( crpix=tanSipWcs.getPixelOrigin(), crval=tanSipWcs.getSkyOrigin(), cdMatrix=tanSipWcs.getCdMatrix(), sipA=sipObject.getSipA(), sipB=sipObject.getSipB(), sipAp=sipObject.getSipAp(), sipBp=sipObject.getSipBp(), ) print(metadata.toString()) if doPlot: self.plotWcs(tanSipWcs, name=name) self.checkResults(fitRes, catsUpdated=False) if self.MatchClass == afwTable.ReferenceMatch: # reset source coord and reference centroid based on initial WCS afwTable.updateRefCentroids(wcs=self.tanWcs, refList=self.refCat) afwTable.updateSourceCoords(wcs=self.tanWcs, sourceList=self.sourceCat) fitterConfig = FitTanSipWcsTask.ConfigClass() fitterConfig.order = order fitterConfig.numIter = numIter fitter = FitTanSipWcsTask(config=fitterConfig) self.loadData() if specifyBBox: fitRes = fitter.fitWcs( matches=self.matches, initWcs=self.tanWcs, bbox=bbox, refCat=self.refCat, sourceCat=self.sourceCat, ) else: fitRes = fitter.fitWcs( matches=self.matches, initWcs=self.tanWcs, bbox=bbox, refCat=self.refCat, sourceCat=self.sourceCat, ) self.checkResults(fitRes, catsUpdated=True)
def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None): """Fit a TAN-SIP WCS from a list of reference object/source matches Parameters ---------- matches : `list` of `lsst.afw.table.ReferenceMatch` The following fields are read: - match.first (reference object) coord - match.second (source) centroid The following fields are written: - match.first (reference object) centroid, - match.second (source) centroid - match.distance (on sky separation, in radians) initWcs : `lsst.afw.geom.SkyWcs` initial WCS bbox : `lsst.geom.Box2I` the region over which the WCS will be valid (an lsst:afw::geom::Box2I); if None or an empty box then computed from matches refCat : `lsst.afw.table.SimpleCatalog` reference object catalog, or None. If provided then all centroids are updated with the new WCS, otherwise only the centroids for ref objects in matches are updated. Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". sourceCat : `lsst.afw.table.SourceCatalog` source catalog, or None. If provided then coords are updated with the new WCS; otherwise only the coords for sources in matches are updated. Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec". exposure : `lsst.afw.image.Exposure` Ignored; present for consistency with FitSipDistortionTask. Returns ------- result : `lsst.pipe.base.Struct` with the following fields: - ``wcs`` : the fit WCS (`lsst.afw.geom.SkyWcs`) - ``scatterOnSky`` : median on-sky separation between reference objects and sources in "matches" (`lsst.afw.geom.Angle`) """ if bbox is None: bbox = lsst.geom.Box2I() import lsstDebug debug = lsstDebug.Info(__name__) wcs = self.initialWcs(matches, initWcs) rejected = np.zeros(len(matches), dtype=bool) for rej in range(self.config.numRejIter): sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs) wcs = sipObject.getNewWcs() rejected = self.rejectMatches(matches, wcs, rejected) if rejected.sum() == len(rejected): raise RuntimeError("All matches rejected in iteration %d" % (rej + 1,)) self.log.debug( "Iteration {0} of astrometry fitting: rejected {1} outliers, " "out of {2} total matches.".format( rej, rejected.sum(), len(rejected) ) ) if debug.plot: print("Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter)) self.plotFit(matches, wcs, rejected) # Final fit after rejection sipObject = self._fitWcs([mm for i, mm in enumerate(matches) if not rejected[i]], wcs) wcs = sipObject.getNewWcs() if debug.plot: print("Plotting final fit") self.plotFit(matches, wcs, rejected) if refCat is not None: self.log.debug("Updating centroids in refCat") afwTable.updateRefCentroids(wcs, refList=refCat) else: self.log.warn("Updating reference object centroids in match list; refCat is None") afwTable.updateRefCentroids(wcs, refList=[match.first for match in matches]) if sourceCat is not None: self.log.debug("Updating coords in sourceCat") afwTable.updateSourceCoords(wcs, sourceList=sourceCat) else: self.log.warn("Updating source coords in match list; sourceCat is None") afwTable.updateSourceCoords(wcs, sourceList=[match.second for match in matches]) self.log.debug("Updating distance in match list") setMatchDistance(matches) scatterOnSky = sipObject.getScatterOnSky() if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec: raise pipeBase.TaskError( "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" % (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec)) return pipeBase.Struct( wcs=wcs, scatterOnSky=scatterOnSky, )