23 from __future__
import absolute_import, division, print_function
25 __all__ = [
"InitialAstrometry",
"ANetBasicAstrometryConfig",
"ANetBasicAstrometryTask"]
27 from builtins
import zip
28 from builtins
import next
29 from builtins
import input
30 from builtins
import range
31 from builtins
import object
40 import lsst.pipe.base
as pipeBase
45 import lsst.meas.algorithms.utils
as maUtils
46 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask, LoadMultiIndexes
47 from lsst.meas.astrom
import displayAstrometry, makeMatchStatisticsInRadians
48 import lsst.meas.astrom.sip
as astromSip
53 Object returned by Astrometry.determineWcs 55 getWcs(): sipWcs or tanWcs 56 getMatches(): sipMatches or tanMatches 59 solveQa (PropertyList) 61 tanMatches (MatchList) 63 sipMatches (MatchList) 64 refCat (lsst::afw::table::SimpleCatalog) 65 matchMeta (PropertyList) 79 Get "sipMatches" -- MatchList using the SIP WCS solution, if it 80 exists, or "tanMatches" -- MatchList using the TAN WCS solution 87 Returns the SIP WCS, if one was found, or a TAN WCS 91 matches = property(getMatches)
92 wcs = property(getWcs)
118 maxCpuTime = RangeField(
119 doc=
"Maximum CPU time to spend solving, in seconds",
124 matchThreshold = RangeField(
125 doc=
"Matching threshold for Astrometry.net solver (log-odds)",
127 default=math.log(1e12),
130 maxStars = RangeField(
131 doc=
"Maximum number of stars to use in Astrometry.net solving",
136 useWcsPixelScale = Field(
137 doc=
"Use the pixel scale from the input exposure\'s WCS headers?",
141 useWcsRaDecCenter = Field(
142 doc=
"Use the RA,Dec center information from the input exposure\'s WCS headers?",
146 useWcsParity = Field(
147 doc=
"Use the parity (flip / handedness) of the image from the input exposure\'s WCS headers?",
151 raDecSearchRadius = RangeField(
152 doc=
"When useWcsRaDecCenter=True, this is the radius, in degrees, around the RA,Dec center " +
153 "specified in the input exposure\'s WCS to search for a solution.",
158 pixelScaleUncertainty = RangeField(
159 doc=
"Range of pixel scales, around the value in the WCS header, to search. " +
160 "If the value of this field is X and the nominal scale is S, " +
161 "the range searched will be S/X to S*X",
166 catalogMatchDist = RangeField(
167 doc=
"Matching radius (arcsec) for matching sources to reference objects",
172 cleaningParameter = RangeField(
173 doc=
"Sigma-clipping parameter in sip/cleanBadPoints.py",
178 calculateSip = Field(
179 doc=
"Compute polynomial SIP distortion terms?",
183 sipOrder = RangeField(
184 doc=
"Polynomial order of SIP distortion terms",
189 badFlags = ListField(
190 doc=
"List of flags which cause a source to be rejected as bad",
193 "slot_Centroid_flag",
194 "base_PixelFlags_flag_edge",
195 "base_PixelFlags_flag_saturated",
196 "base_PixelFlags_flag_crCenter",
200 doc=
"Retrieve all available fluxes (and errors) from catalog?",
204 maxIter = RangeField(
205 doc=
"maximum number of iterations of match sources and fit WCS" +
206 "ignored if not fitting a WCS",
211 matchDistanceSigma = RangeField(
212 doc=
"The match and fit loop stops when maxMatchDist minimized: " 213 " maxMatchDist = meanMatchDist + matchDistanceSigma*stdDevMatchDistance " +
214 " (where the mean and std dev are computed using outlier rejection);" +
215 " ignored if not fitting a WCS",
223 """!Basic implemeentation of the astrometry.net astrometrical fitter 225 A higher-level class ANetAstrometryTask takes care of dealing with the fact 226 that the initial WCS is probably only a pure TAN SIP, yet we may have 227 significant distortion and a good estimate for that distortion. 230 About Astrometry.net index files (astrometry_net_data): 232 There are three components of an index file: a list of stars 233 (stored as a star kd-tree), a list of quadrangles of stars ("quad 234 file") and a list of the shapes ("codes") of those quadrangles, 235 stored as a code kd-tree. 237 Each index covers a region of the sky, defined by healpix nside 238 and number, and a range of angular scales. In LSST, we share the 239 list of stars in a part of the sky between multiple indexes. That 240 is, the star kd-tree is shared between multiple indices (quads and 241 code kd-trees). In the astrometry.net code, this is called a 244 It is possible to "unload" and "reload" multiindex (and index) 245 objects. When "unloaded", they consume no FILE or mmap resources. 247 The multiindex object holds the star kd-tree and gives each index 248 object it holds a pointer to it, so it is necessary to 249 multiindex_reload_starkd() before reloading the indices it holds. 250 The multiindex_unload() method, on the other hand, unloads its 251 starkd and unloads each index it holds. 253 ConfigClass = ANetBasicAstrometryConfig
254 _DefaultName =
"aNetBasicAstrometry" 260 """!Construct an ANetBasicAstrometryTask 262 @param[in] config configuration (an instance of self.ConfigClass) 263 @param[in] andConfig astrometry.net data config (an instance of AstromNetDataConfig, or None); 264 if None then use andConfig.py in the astrometry_net_data product (which must be setup) 265 @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_ 267 @throw RuntimeError if andConfig is None and the configuration cannot be found, 268 either because astrometry_net_data is not setup in eups 269 or because the setup version does not include the file "andConfig.py" 271 pipeBase.Task.__init__(self, config=config, **kwargs)
284 if self.log.getLevel() > self.log.DEBUG:
286 from astrometry.util.ttime
import get_memusage
289 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
291 ss.append(key +
': ' +
' '.join(mu[key]))
293 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
294 if 'mmaps_total' in mu:
295 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
296 self.log.debug(prefix +
'Memory: ' +
', '.join(ss))
298 def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True):
299 """Get image parameters 301 @param[in] exposure exposure (an afwImage.Exposure) or None 302 @param[in] bbox bounding box (an afwGeom.Box2I) or None; if None then bbox must be specified 303 @param[in] wcs WCS (an afwImage.Wcs) or None; if None then exposure must be specified 304 @param[in] filterName filter name, a string, or None; if None exposure must be specified 305 @param[in] wcsRequired if True then either wcs must be specified or exposure must contain a wcs; 306 if False then the returned wcs may be None 308 - bbox bounding box; guaranteed to be set 309 - wcs WCS if known, else None 310 - filterName filter name if known, else None 311 @throw RuntimeError if bbox cannot be determined, or wcs cannot be determined and wcsRequired True 313 if exposure
is not None:
315 bbox = exposure.getBBox()
316 self.log.debug(
"Setting bbox = %s from exposure metadata", bbox)
318 self.log.debug(
"Setting wcs from exposure metadata")
319 wcs = exposure.getWcs()
320 if filterName
is None:
321 filterName = exposure.getFilter().getName()
322 self.log.debug(
"Setting filterName = %r from exposure metadata", filterName)
324 raise RuntimeError(
"bbox or exposure must be specified")
325 if wcs
is None and wcsRequired:
326 raise RuntimeError(
"wcs or exposure (with a WCS) must be specified")
327 return bbox, wcs, filterName
329 def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None, calculateSip=None):
330 """!Return an InitialAstrometry object, just like determineWcs, 331 but assuming the given input WCS is correct. 333 This involves searching for reference sources within the WCS 334 area, and matching them to the given 'sourceCat'. If 335 'calculateSip' is set, we will try to compute a TAN-SIP 336 distortion correction. 338 @param[in] sourceCat list of detected sources in this image. 339 @param[in] wcs your known WCS, or None to get from exposure 340 @param[in] exposure the exposure holding metadata for this image; 341 if None then you must specify wcs, filterName and bbox 342 @param[in] filterName string, filter name, eg "i", or None to get from exposure` 343 @param[in] bbox bounding box of image, or None to get from exposure 344 @param[in] calculateSip calculate SIP distortion terms for the WCS? If None 345 then use self.config.calculateSip. To disable WCS fitting set calculateSip=False 347 @note this function is also called by 'determineWcs' (via 'determineWcs2'), since the steps are all 353 if calculateSip
is None:
354 calculateSip = self.
config.calculateSip
360 filterName=filterName,
366 filterName=filterName,
369 astrom.refCat = refCat
370 catids = [src.getId()
for src
in refCat]
372 self.log.debug(
'%i reference sources; %i unique IDs', len(catids), len(uids))
374 uniq = set([sm.second.getId()
for sm
in matches])
375 if len(matches) != len(uniq):
376 self.log.warn(
'The list of matched stars contains duplicate reference source IDs ' +
377 '(%i sources, %i unique ids)', len(matches), len(uniq))
378 if len(matches) == 0:
379 self.log.warn(
'No matches found between input sources and reference catalogue.')
382 self.log.debug(
'%i reference objects match input sources using input WCS', len(matches))
383 astrom.tanMatches = matches
386 srcids = [s.getId()
for s
in sourceCat]
388 assert(m.second.getId()
in srcids)
389 assert(m.second
in sourceCat)
394 self.log.debug(
'Failed to find a SIP WCS better than the initial one.')
396 self.log.debug(
'%i reference objects match input sources using SIP WCS',
398 astrom.sipWcs = sipwcs
399 astrom.sipMatches = matches
401 wcs = astrom.getWcs()
404 for src
in sourceCat:
406 astrom.matchMeta = _createMetadata(bbox, wcs, filterName)
410 """Find a WCS solution for the given 'sourceCat' in the given 411 'exposure', getting other parameters from config. 413 Valid kwargs include: 415 'radecCenter', an afw.geom.SpherePoint giving the ICRS RA,Dec position 416 of the center of the field. This is used to limit the 417 search done by Astrometry.net (to make it faster and load 418 fewer index files, thereby using less memory). Defaults to 419 the RA,Dec center from the exposure's WCS; turn that off 420 with the boolean kwarg 'useRaDecCenter' or config option 423 'useRaDecCenter', a boolean. Don't use the RA,Dec center from 424 the exposure's initial WCS. 426 'searchRadius', in degrees, to search for a solution around 427 the given 'radecCenter'; default from config option 430 'useParity': parity is the 'flip' of the image. Knowing it 431 reduces the search space (hence time) for Astrometry.net. 432 The parity can be computed from the exposure's WCS (the 433 sign of the determinant of the CD matrix); this option 434 controls whether we do that or force Astrometry.net to 435 search both parities. Default from config.useWcsParity. 437 'pixelScale': afwGeom.Angle, estimate of the angle-per-pixel 438 (ie, arcseconds per pixel). Defaults to a value derived 439 from the exposure's WCS. If enabled, this value, plus or 440 minus config.pixelScaleUncertainty, will be used to limit 441 Astrometry.net's search. 443 'usePixelScale': boolean. Use the pixel scale to limit 444 Astrometry.net's search? Defaults to config.useWcsPixelScale. 446 'filterName', a string, the filter name of this image. Will 447 be mapped through the 'filterMap' config dictionary to a 448 column name in the astrometry_net_data index FITS files. 449 Defaults to the exposure.getFilter() value. 451 'bbox', bounding box of exposure; defaults to exposure.getBBox() 454 assert(exposure
is not None)
456 margs = kwargs.copy()
457 if 'searchRadius' not in margs:
458 margs.update(searchRadius=self.
config.raDecSearchRadius * afwGeom.degrees)
459 if 'usePixelScale' not in margs:
460 margs.update(usePixelScale=self.
config.useWcsPixelScale)
461 if 'useRaDecCenter' not in margs:
462 margs.update(useRaDecCenter=self.
config.useWcsRaDecCenter)
463 if 'useParity' not in margs:
464 margs.update(useParity=self.
config.useWcsParity)
465 margs.update(exposure=exposure)
469 """Get a blind astrometric solution for the given catalog of sources. 475 And if available, we can use: 476 -an initial Wcs estimate; 481 (all of which are metadata of Exposure). 484 imageSize: (W,H) integer tuple/iterable 485 pixelScale: afwGeom::Angle per pixel. 486 radecCenter: afwCoord::Coord 491 for key
in [
'exposure',
'bbox',
'filterName']:
493 kw[key] = kwargs[key]
494 astrom = self.
useKnownWcs(sourceCat, wcs=wcs, **kw)
511 searchRadiusScale=2.):
512 if not useRaDecCenter
and radecCenter
is not None:
513 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
514 if not usePixelScale
and pixelScale
is not None:
515 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
521 filterName=filterName,
526 xc, yc = bboxD.getCenter()
530 if pixelScale
is None:
532 pixelScale = wcs.getPixelScale()
533 self.log.debug(
'Setting pixel scale estimate = %.3f from given WCS estimate',
534 pixelScale.asArcseconds())
536 if radecCenter
is None:
538 radecCenter = wcs.pixelToSky(xc, yc)
539 self.log.debug(
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS ' +
540 'estimate, using pixel center = (%.1f, %.1f)',
541 radecCenter.getLongitude().asDegrees(),
542 radecCenter.getLatitude().asDegrees(), xc, yc)
544 if searchRadius
is None:
546 assert(pixelScale
is not None)
547 pixRadius = math.hypot(*bboxD.getDimensions()) / 2
548 searchRadius = (pixelScale * pixRadius * searchRadiusScale)
549 self.log.debug(
'Using RA,Dec search radius = %.3f deg, from pixel scale, ' +
550 'image size, and searchRadiusScale = %g',
551 searchRadius, searchRadiusScale)
553 parity = wcs.isFlipped
554 self.log.debug(
'Using parity = %s' % (parity
and 'True' or 'False'))
558 if exposure
is not None:
559 exposureBBoxD =
afwGeom.Box2D(exposure.getMaskedImage().getBBox())
561 exposureBBoxD = bboxD
563 self.log.debug(
"Trimming: kept %i of %i sources", n, len(sourceCat))
569 pixelScale=pixelScale,
570 radecCenter=radecCenter,
571 searchRadius=searchRadius,
573 filterName=filterName,
576 raise RuntimeError(
"Unable to match sources with catalog.")
577 self.log.info(
'Got astrometric solution from Astrometry.net')
579 rdc = wcs.pixelToSky(xc, yc)
580 self.log.debug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)',
581 xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees())
585 """!Get a TAN-SIP WCS, starting from an existing WCS. 587 It uses your WCS to compute a fake grid of corresponding "stars" in pixel and sky coords, 588 and feeds that to the regular SIP code. 590 @param[in] wcs initial WCS 591 @param[in] bbox bounding box of image 592 @param[in] ngrid number of grid points along x and y for fitting (fit at ngrid^2 points) 593 @param[in] linearizeAtCenter if True, get a linear approximation of the input 594 WCS at the image center and use that as the TAN initialization for 595 the TAN-SIP solution. You probably want this if your WCS has its 596 CRPIX outside the image bounding box. 599 srcSchema = afwTable.SourceTable.makeMinimalSchema()
600 key = srcSchema.addField(
"centroid", type=
"PointD")
601 srcTable = afwTable.SourceTable.make(srcSchema)
602 srcTable.defineCentroid(
"centroid")
604 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
607 (W, H) = bbox.getDimensions()
608 x0, y0 = bbox.getMin()
609 for xx
in np.linspace(0., W, ngrid):
610 for yy
in np.linspace(0, H, ngrid):
611 src = srcs.makeRecord()
612 src.set(key.getX(), x0 + xx)
613 src.set(key.getY(), y0 + yy)
615 rd = wcs.pixelToSky(xx + x0, yy + y0)
616 ref = refs.makeRecord()
620 if linearizeAtCenter:
625 crval = wcs.pixelToSky(crpix)
626 crval = crval.getPosition(afwGeom.degrees)
629 aff = wcs.linearizePixelToSky(crval)
630 cd = aff.getLinear().getMatrix()
631 wcs = afwImage.Wcs(crval, crpix, cd)
636 """Produce a SIP solution given a list of known correspondences. 638 Unlike _calculateSipTerms, this does not iterate the solution; 639 it assumes you have given it a good sets of corresponding stars. 641 NOTE that "refCat" and "sourceCat" are assumed to be the same length; 642 entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences. 644 @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS. 645 @param[in] refCat reference source catalog 646 @param[in] sourceCat source catalog 647 @param[in] bbox bounding box of image 649 sipOrder = self.
config.sipOrder
651 for ci, si
in zip(refCat, sourceCat):
654 sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox)
655 return sipObject.getNewWcs()
657 def _calculateSipTerms(self, origWcs, refCat, sourceCat, matches, bbox):
658 """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS. 660 @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS; 661 this is used to set the baseline when determining whether a SIP 662 solution is any better; it will be returned if no better SIP solution 664 @param[in] refCat reference source catalog 665 @param[in] sourceCat sources in the image to be solved 666 @param[in] matches list of supposedly matched sources, using the "origWcs". 667 @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients. 669 sipOrder = self.
config.sipOrder
672 lastMatchSize = len(matches)
674 for i
in range(self.
config.maxIter):
677 sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox)
678 proposedWcs = sipObject.getNewWcs()
679 self.
plotSolution(matches, proposedWcs, bbox.getDimensions())
680 except pexExceptions.Exception
as e:
681 self.log.warn(
'Failed to calculate distortion terms. Error: ', str(e))
685 for source
in sourceCat:
686 skyPos = proposedWcs.pixelToSky(source.getCentroid())
687 source.setCoord(skyPos)
690 proposedMatchlist = self.
_getMatchList(sourceCat, refCat, proposedWcs)
691 proposedMatchSize = len(proposedMatchlist)
695 "SIP iteration %i: %i objects match, previous = %i;" %
696 (i, proposedMatchSize, lastMatchSize) +
697 " clipped mean scatter = %s arcsec, previous = %s; " %
698 (proposedMatchStats.distMean.asArcseconds(), lastMatchStats.distMean.asArcseconds()) +
699 " max match dist = %s arcsec, previous = %s" %
700 (proposedMatchStats.maxMatchDist.asArcseconds(),
701 lastMatchStats.maxMatchDist.asArcseconds())
704 if lastMatchStats.maxMatchDist <= proposedMatchStats.maxMatchDist:
706 "Fit WCS: use iter %s because max match distance no better in next iter: " % (i-1,) +
707 " %g < %g arcsec" % (lastMatchStats.maxMatchDist.asArcseconds(),
708 proposedMatchStats.maxMatchDist.asArcseconds()))
712 matches = proposedMatchlist
713 lastMatchSize = proposedMatchSize
714 lastMatchStats = proposedMatchStats
719 """Plot the solution, when debugging is turned on. 721 @param matches The list of matches 723 @param imageSize 2-tuple with the image size (W,H) 731 import matplotlib.pyplot
as plt
732 except ImportError
as e:
733 self.log.warn(
"Unable to import matplotlib: %s", e)
739 fig.canvas._tkcanvas._root().lift()
748 for i, m
in enumerate(matches):
749 x[i] = m.second.getX()
750 y[i] = m.second.getY()
751 pixel = wcs.skyToPixel(m.first.getCoord())
752 dx[i] = x[i] - pixel.getX()
753 dy[i] = y[i] - pixel.getY()
755 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
757 def plotNext(x, y, xLabel, yLabel, xMax):
759 ax.set_autoscalex_on(
False)
760 ax.set_xbound(lower=0, upper=xMax)
762 ax.set_xlabel(xLabel)
763 ax.set_ylabel(yLabel)
766 plotNext(x, dx,
"x",
"dx", imageSize[0])
767 plotNext(x, dy,
"x",
"dy", imageSize[0])
768 plotNext(y, dx,
"y",
"dx", imageSize[1])
769 plotNext(y, dy,
"y",
"dy", imageSize[1])
775 reply = input(
"Pausing for inspection, enter to continue... [hpQ] ").strip()
779 reply = reply.split()
785 if reply
in (
"",
"h",
"p",
"Q"):
787 print(
"h[elp] p[db] Q[uit]")
796 def _computeMatchStatsOnSky(self, wcs, matchList):
797 """Compute on-sky radial distance statistics for a match list 799 @param[in] wcs WCS for match list; an lsst.afw.image.Wcs 800 @param[in] matchList list of matches between reference object and sources; 801 a list of lsst.afw.table.ReferenceMatch; 802 the source centroid and reference object coord are read 804 @return a pipe_base Struct containing these fields: 805 - distMean clipped mean of on-sky radial separation 806 - distStdDev clipped standard deviation of on-sky radial separation 807 - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev 809 distStatsInRadians = makeMatchStatisticsInRadians(wcs, matchList,
810 afwMath.MEANCLIP | afwMath.STDEVCLIP)
811 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*afwGeom.radians
812 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*afwGeom.radians
813 return pipeBase.Struct(
815 distStdDev=distStdDev,
816 maxMatchDist=distMean + self.
config.matchDistanceSigma*distStdDev,
819 def _getMatchList(self, sourceCat, refCat, wcs):
820 dist = self.
config.catalogMatchDist * afwGeom.arcseconds
821 clean = self.
config.cleaningParameter
822 matcher = astromSip.MatchSrcToCatalogue(refCat, sourceCat, wcs, dist)
823 matches = matcher.getMatches()
826 X = [src.getX()
for src
in sourceCat]
827 Y = [src.getY()
for src
in sourceCat]
828 R1 = [src.getRa().asDegrees()
for src
in sourceCat]
829 D1 = [src.getDec().asDegrees()
for src
in sourceCat]
830 R2 = [src.getRa().asDegrees()
for src
in refCat]
831 D2 = [src.getDec().asDegrees()
for src
in refCat]
838 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sourceCat), len(refCat)))
841 'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
842 (min(X), max(X), min(Y), max(Y), min(R1), max(R1), min(D1), max(D1)))
844 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
845 (min(R2), max(R2), min(D2), max(D2)))
846 raise RuntimeError(
'No matches found between image and catalogue')
847 matches = astromSip.cleanBadPoints.clean(matches, wcs, nsigma=clean)
852 Returns the column name in the astrometry_net_data index file that will be used 853 for the given filter name. 855 @param filterName Name of filter used in exposure 856 @param columnMap Dict that maps filter names to column names 857 @param default Default column name 859 filterName = self.
config.filterMap.get(filterName, filterName)
861 return columnMap[filterName]
863 self.log.warn(
"No column in configuration for filter '%s'; using default '%s'" %
864 (filterName, default))
867 def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter, searchRadius, parity, filterName=None):
869 @param[in] parity True for flipped parity, False for normal parity, None to leave parity unchanged 873 imageSize = bbox.getDimensions()
874 x0, y0 = bbox.getMin()
879 badkeys = [goodsources.getSchema().find(name).key
for name
in self.
config.badFlags]
882 if np.isfinite(s.getX())
and np.isfinite(s.getY())
and np.isfinite(s.getPsfFlux()) \
884 goodsources.append(s)
886 self.log.info(
"Number of selected sources for astrometry : %d" % (len(goodsources)))
887 if len(goodsources) < len(sourceCat):
888 self.log.debug(
'Keeping %i of %i sources with finite X,Y positions and PSF flux',
889 len(goodsources), len(sourceCat))
890 self.log.debug(
'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' +
891 '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net',
892 xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0)
894 solver.setStars(goodsources, x0, y0)
895 solver.setMaxStars(self.
config.maxStars)
896 solver.setImageSize(*imageSize)
897 solver.setMatchThreshold(self.
config.matchThreshold)
899 if radecCenter
is not None:
900 raDecRadius = (radecCenter.getLongitude().asDegrees(), radecCenter.getLatitude().asDegrees(),
901 searchRadius.asDegrees())
902 solver.setRaDecRadius(*raDecRadius)
903 self.log.debug(
'Searching for match around RA,Dec = (%g, %g) with radius %g deg' %
906 if pixelScale
is not None:
907 dscale = self.
config.pixelScaleUncertainty
908 scale = pixelScale.asArcseconds()
911 solver.setPixelScaleRange(lo, hi)
913 'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix',
914 scale, 100.*(dscale-1.), lo, hi)
916 if parity
is not None:
917 solver.setParity(parity)
918 self.log.debug(
'Searching for match with parity = %s', str(parity))
921 if radecCenter
is not None:
922 multiInds = self.
refObjLoader._getMIndexesWithinRange(radecCenter, searchRadius)
925 qlo, qhi = solver.getQuadSizeRangeArcsec()
927 toload_multiInds = set()
930 for i
in range(len(mi)):
932 if not ind.overlapsScaleRange(qlo, qhi):
934 toload_multiInds.add(mi)
935 toload_inds.append(ind)
941 displayAstrometry(refCat=self.
refObjLoader.loadPixelBox(bbox, wcs, filterName).refCat,
945 solver.addIndices(toload_inds)
946 self.
memusage(
'Index files loaded: ')
948 cpulimit = self.
config.maxCpuTime
953 self.
memusage(
'Index files unloaded: ')
955 if solver.didSolve():
956 self.log.debug(
'Solved!')
957 wcs = solver.getWcs()
959 if x0 != 0
or y0 != 0:
963 self.log.warn(
'Did not get an astrometric solution from Astrometry.net')
969 if radecCenter
is not None:
970 self.
refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName)
972 qa = solver.getSolveStats()
973 self.log.debug(
'qa: %s', qa.toString())
976 def _isGoodSource(self, candsource, keys):
978 if candsource.get(k):
983 def _trimBadPoints(sourceCat, bbox, wcs=None):
984 """Remove elements from catalog whose xy positions are not within the given bbox. 986 sourceCat: a Catalog of SimpleRecord or SourceRecord objects 987 bbox: an afwImage.Box2D 988 wcs: if not None, will be used to compute the xy positions on-the-fly; 989 this is required when sources actually contains SimpleRecords. 992 a list of Source objects with xAstrom, yAstrom within the bbox. 994 keep = type(sourceCat)(sourceCat.table)
996 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
997 if bbox.contains(point):
1002 def _createMetadata(bbox, wcs, filterName):
1004 Create match metadata entries required for regenerating the catalog 1006 @param bbox bounding box of image (pixels) 1007 @param filterName Name of filter, used for magnitudes 1010 meta = dafBase.PropertyList()
1013 cx, cy = bboxD.getCenter()
1014 radec = wcs.pixelToSky(cx, cy)
1015 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
1016 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
1017 pixelRadius = math.hypot(*bboxD.getDimensions())/2.0
1018 skyRadius = wcs.getPixelScale() * pixelRadius
1019 meta.add(
'RADIUS', skyRadius.asDegrees(),
1020 'field radius in degrees, approximate')
1021 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1022 if filterName
is not None:
1023 meta.add(
'FILTER', str(filterName),
'LSST filter name for tagalong data')
def plotSolution(self, matches, wcs, imageSize)
def getSipWcsFromWcs(self, wcs, bbox, ngrid=20, linearizeAtCenter=True)
Get a TAN-SIP WCS, starting from an existing WCS.
def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None, calculateSip=None)
Return an InitialAstrometry object, just like determineWcs, but assuming the given input WCS is corre...
def _getMatchList(self, sourceCat, refCat, wcs)
def _computeMatchStatsOnSky(self, wcs, matchList)
def getBlindWcsSolution(self, sourceCat, exposure=None, wcs=None, bbox=None, radecCenter=None, searchRadius=None, pixelScale=None, filterName=None, doTrim=False, usePixelScale=True, useRaDecCenter=True, useParity=True, searchRadiusScale=2.)
def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter, searchRadius, parity, filterName=None)
def getSipWcsFromCorrespondences(self, origWcs, refCat, sourceCat, bbox)
def memusage(self, prefix='')
Basic implemeentation of the astrometry.net astrometrical fitter.
def __init__(self, config, andConfig=None, kwargs)
Construct an ANetBasicAstrometryTask.
def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True)
Load reference objects from astrometry.net index files.
def getColumnName(self, filterName, columnMap, default=None)
def _trimBadPoints(sourceCat, bbox, wcs=None)
def getMatchMetadata(self)
def getSolveQaMetadata(self)
def determineWcs2(self, sourceCat, kwargs)
def _isGoodSource(self, candsource, keys)
def _calculateSipTerms(self, origWcs, refCat, sourceCat, matches, bbox)
Iteratively calculate SIP distortions and regenerate matches based on improved WCS.
def determineWcs(self, sourceCat, exposure, kwargs)