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
46 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask, LoadMultiIndexes
49 from .
import cleanBadPoints
54 Object returned by Astrometry.determineWcs 56 getWcs(): sipWcs or tanWcs 57 getMatches(): sipMatches or tanMatches 60 solveQa (PropertyList) 62 tanMatches (MatchList) 64 sipMatches (MatchList) 65 refCat (lsst::afw::table::SimpleCatalog) 66 matchMeta (PropertyList) 80 Get "sipMatches" -- MatchList using the SIP WCS solution, if it 81 exists, or "tanMatches" -- MatchList using the TAN WCS solution 88 Returns the SIP WCS, if one was found, or a TAN WCS 92 matches = property(getMatches)
93 wcs = property(getWcs)
120 doc=
"Maximum CPU time to spend solving, in seconds",
126 doc=
"Matching threshold for Astrometry.net solver (log-odds)",
128 default=math.log(1e12),
132 doc=
"Maximum number of stars to use in Astrometry.net solving",
138 doc=
"Use the pixel scale from the input exposure\'s WCS headers?",
143 doc=
"Use the RA,Dec center information from the input exposure\'s WCS headers?",
148 doc=
"Use the parity (flip / handedness) of the image from the input exposure\'s WCS headers?",
153 doc=
"When useWcsRaDecCenter=True, this is the radius, in degrees, around the RA,Dec center " 154 r"specified in the input exposure\'s WCS to search for a solution.",
160 doc=
"Range of pixel scales, around the value in the WCS header, to search. " 161 "If the value of this field is X and the nominal scale is S, " 162 "the range searched will be S/X to S*X",
168 doc=
"Matching radius (arcsec) for matching sources to reference objects",
174 doc=
"Sigma-clipping parameter in cleanBadPoints.py",
180 doc=
"Compute polynomial SIP distortion terms?",
185 doc=
"Polynomial order of SIP distortion terms",
191 doc=
"List of flags which cause a source to be rejected as bad",
194 "slot_Centroid_flag",
195 "base_PixelFlags_flag_edge",
196 "base_PixelFlags_flag_saturated",
197 "base_PixelFlags_flag_crCenter",
201 doc=
"Retrieve all available fluxes (and errors) from catalog?",
206 doc=
"maximum number of iterations of match sources and fit WCS" 207 "ignored if not fitting a WCS",
213 doc=
"The match and fit loop stops when maxMatchDist minimized: " 214 " maxMatchDist = meanMatchDist + matchDistanceSigma*stdDevMatchDistance " 215 " (where the mean and std dev are computed using outlier rejection);" 216 " ignored if not fitting a WCS",
224 """!Basic implemeentation of the astrometry.net astrometrical fitter 226 A higher-level class ANetAstrometryTask takes care of dealing with the fact 227 that the initial WCS is probably only a pure TAN SIP, yet we may have 228 significant distortion and a good estimate for that distortion. 231 About Astrometry.net index files (astrometry_net_data): 233 There are three components of an index file: a list of stars 234 (stored as a star kd-tree), a list of quadrangles of stars ("quad 235 file") and a list of the shapes ("codes") of those quadrangles, 236 stored as a code kd-tree. 238 Each index covers a region of the sky, defined by healpix nside 239 and number, and a range of angular scales. In LSST, we share the 240 list of stars in a part of the sky between multiple indexes. That 241 is, the star kd-tree is shared between multiple indices (quads and 242 code kd-trees). In the astrometry.net code, this is called a 245 It is possible to "unload" and "reload" multiindex (and index) 246 objects. When "unloaded", they consume no FILE or mmap resources. 248 The multiindex object holds the star kd-tree and gives each index 249 object it holds a pointer to it, so it is necessary to 250 multiindex_reload_starkd() before reloading the indices it holds. 251 The multiindex_unload() method, on the other hand, unloads its 252 starkd and unloads each index it holds. 254 ConfigClass = ANetBasicAstrometryConfig
255 _DefaultName =
"aNetBasicAstrometry" 261 r"""!Construct an ANetBasicAstrometryTask 263 @param[in] config configuration (an instance of self.ConfigClass) 264 @param[in] andConfig astrometry.net data config (an instance of AstromNetDataConfig, or None); 265 if None then use andConfig.py in the astrometry_net_data product (which must be setup) 266 @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_ 268 @throw RuntimeError if andConfig is None and the configuration cannot be found, 269 either because astrometry_net_data is not setup in eups 270 or because the setup version does not include the file "andConfig.py" 272 pipeBase.Task.__init__(self, config=config, **kwargs)
285 if self.log.
getLevel() > self.log.DEBUG:
287 from astrometry.util.ttime
import get_memusage
290 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
292 ss.append(key +
': ' +
' '.join(mu[key]))
294 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
295 if 'mmaps_total' in mu:
296 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
297 self.log.
debug(prefix +
'Memory: ' +
', '.join(ss))
299 def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True):
300 """Get image parameters 302 @param[in] exposure exposure (an afwImage.Exposure) or None 303 @param[in] bbox bounding box (an afwGeom.Box2I) or None; if None then bbox must be specified 304 @param[in] wcs WCS (an afwImage.Wcs) or None; if None then exposure must be specified 305 @param[in] filterName filter name, a string, or None; if None exposure must be specified 306 @param[in] wcsRequired if True then either wcs must be specified or exposure must contain a wcs; 307 if False then the returned wcs may be None 309 - bbox bounding box; guaranteed to be set 310 - wcs WCS if known, else None 311 - filterName filter name if known, else None 312 @throw RuntimeError if bbox cannot be determined, or wcs cannot be determined and wcsRequired True 314 if exposure
is not None:
316 bbox = exposure.getBBox()
317 self.log.
debug(
"Setting bbox = %s from exposure metadata", bbox)
319 self.log.
debug(
"Setting wcs from exposure metadata")
320 wcs = exposure.getWcs()
321 if filterName
is None:
322 filterName = exposure.getFilter().getName()
323 self.log.
debug(
"Setting filterName = %r from exposure metadata", filterName)
325 raise RuntimeError(
"bbox or exposure must be specified")
326 if wcs
is None and wcsRequired:
327 raise RuntimeError(
"wcs or exposure (with a WCS) must be specified")
328 return bbox, wcs, filterName
330 def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None, calculateSip=None):
331 """!Return an InitialAstrometry object, just like determineWcs, 332 but assuming the given input WCS is correct. 334 This involves searching for reference sources within the WCS 335 area, and matching them to the given 'sourceCat'. If 336 'calculateSip' is set, we will try to compute a TAN-SIP 337 distortion correction. 339 @param[in] sourceCat list of detected sources in this image. 340 @param[in] wcs your known WCS, or None to get from exposure 341 @param[in] exposure the exposure holding metadata for this image; 342 if None then you must specify wcs, filterName and bbox 343 @param[in] filterName string, filter name, eg "i", or None to get from exposure` 344 @param[in] bbox bounding box of image, or None to get from exposure 345 @param[in] calculateSip calculate SIP distortion terms for the WCS? If None 346 then use self.config.calculateSip. To disable WCS fitting set calculateSip=False 348 @note this function is also called by 'determineWcs' (via 'determineWcs2'), since the steps are all 354 if calculateSip
is None:
355 calculateSip = self.
config.calculateSip
361 filterName=filterName,
367 filterName=filterName,
370 astrom.refCat = refCat
371 catids = [src.getId()
for src
in refCat]
373 self.log.
debug(
'%i reference sources; %i unique IDs', len(catids), len(uids))
375 uniq =
set([sm.second.getId()
for sm
in matches])
376 if len(matches) != len(uniq):
377 self.log.
warn(
'The list of matched stars contains duplicate reference source IDs ' 378 '(%i sources, %i unique ids)', len(matches), len(uniq))
379 if len(matches) == 0:
380 self.log.
warn(
'No matches found between input sources and reference catalogue.')
383 self.log.
debug(
'%i reference objects match input sources using input WCS', len(matches))
384 astrom.tanMatches = matches
387 srcids = [s.getId()
for s
in sourceCat]
389 assert(m.second.getId()
in srcids)
390 assert(m.second
in sourceCat)
395 self.log.
debug(
'Failed to find a SIP WCS better than the initial one.')
397 self.log.
debug(
'%i reference objects match input sources using SIP WCS',
399 astrom.sipWcs = sipwcs
400 astrom.sipMatches = matches
402 wcs = astrom.getWcs()
405 for src
in sourceCat:
407 astrom.matchMeta = _createMetadata(bbox, wcs, filterName)
411 """Find a WCS solution for the given 'sourceCat' in the given 412 'exposure', getting other parameters from config. 414 Valid kwargs include: 416 'radecCenter', an afw.geom.SpherePoint giving the ICRS RA,Dec position 417 of the center of the field. This is used to limit the 418 search done by Astrometry.net (to make it faster and load 419 fewer index files, thereby using less memory). Defaults to 420 the RA,Dec center from the exposure's WCS; turn that off 421 with the boolean kwarg 'useRaDecCenter' or config option 424 'useRaDecCenter', a boolean. Don't use the RA,Dec center from 425 the exposure's initial WCS. 427 'searchRadius', in degrees, to search for a solution around 428 the given 'radecCenter'; default from config option 431 'useParity': parity is the 'flip' of the image. Knowing it 432 reduces the search space (hence time) for Astrometry.net. 433 The parity can be computed from the exposure's WCS (the 434 sign of the determinant of the CD matrix); this option 435 controls whether we do that or force Astrometry.net to 436 search both parities. Default from config.useWcsParity. 438 'pixelScale': afwGeom.Angle, estimate of the angle-per-pixel 439 (ie, arcseconds per pixel). Defaults to a value derived 440 from the exposure's WCS. If enabled, this value, plus or 441 minus config.pixelScaleUncertainty, will be used to limit 442 Astrometry.net's search. 444 'usePixelScale': boolean. Use the pixel scale to limit 445 Astrometry.net's search? Defaults to config.useWcsPixelScale. 447 'filterName', a string, the filter name of this image. Will 448 be mapped through the 'filterMap' config dictionary to a 449 column name in the astrometry_net_data index FITS files. 450 Defaults to the exposure.getFilter() value. 452 'bbox', bounding box of exposure; defaults to exposure.getBBox() 455 assert(exposure
is not None)
457 margs = kwargs.copy()
458 if 'searchRadius' not in margs:
459 margs.update(searchRadius=self.
config.raDecSearchRadius * afwGeom.degrees)
460 if 'usePixelScale' not in margs:
461 margs.update(usePixelScale=self.
config.useWcsPixelScale)
462 if 'useRaDecCenter' not in margs:
463 margs.update(useRaDecCenter=self.
config.useWcsRaDecCenter)
464 if 'useParity' not in margs:
465 margs.update(useParity=self.
config.useWcsParity)
466 margs.update(exposure=exposure)
470 """Get a blind astrometric solution for the given catalog of sources. 476 And if available, we can use: 477 -an initial Wcs estimate; 482 (all of which are metadata of Exposure). 485 imageSize: (W,H) integer tuple/iterable 486 pixelScale: afwGeom::Angle per pixel. 487 radecCenter: afwCoord::Coord 492 for key
in [
'exposure',
'bbox',
'filterName']:
494 kw[key] = kwargs[key]
495 astrom = self.
useKnownWcs(sourceCat, wcs=wcs, **kw)
512 searchRadiusScale=2.):
513 if not useRaDecCenter
and radecCenter
is not None:
514 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
515 if not usePixelScale
and pixelScale
is not None:
516 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
522 filterName=filterName,
527 xc, yc = bboxD.getCenter()
531 if pixelScale
is None:
533 pixelScale = wcs.getPixelScale()
534 self.log.
debug(
'Setting pixel scale estimate = %.3f from given WCS estimate',
535 pixelScale.asArcseconds())
537 if radecCenter
is None:
539 radecCenter = wcs.pixelToSky(xc, yc)
540 self.log.
debug(
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS ' 541 'estimate, using pixel center = (%.1f, %.1f)',
542 radecCenter.getLongitude().asDegrees(),
543 radecCenter.getLatitude().asDegrees(), xc, yc)
545 if searchRadius
is None:
547 assert(pixelScale
is not None)
548 pixRadius = math.hypot(*bboxD.getDimensions()) / 2
549 searchRadius = (pixelScale * pixRadius * searchRadiusScale)
550 self.log.
debug(
'Using RA,Dec search radius = %.3f deg, from pixel scale, ' 551 'image size, and searchRadiusScale = %g',
552 searchRadius, searchRadiusScale)
554 parity = wcs.isFlipped
555 self.log.
debug(
'Using parity = %s' % (parity
and 'True' or 'False'))
559 if exposure
is not None:
560 exposureBBoxD =
afwGeom.Box2D(exposure.getMaskedImage().getBBox())
562 exposureBBoxD = bboxD
564 self.log.
debug(
"Trimming: kept %i of %i sources", n, len(sourceCat))
570 pixelScale=pixelScale,
571 radecCenter=radecCenter,
572 searchRadius=searchRadius,
574 filterName=filterName,
577 raise RuntimeError(
"Unable to match sources with catalog.")
578 self.log.
info(
'Got astrometric solution from Astrometry.net')
580 rdc = wcs.pixelToSky(xc, yc)
581 self.log.
debug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)',
582 xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees())
586 """!Get a TAN-SIP WCS, starting from an existing WCS. 588 It uses your WCS to compute a fake grid of corresponding "stars" in pixel and sky coords, 589 and feeds that to the regular SIP code. 591 @param[in] wcs initial WCS 592 @param[in] bbox bounding box of image 593 @param[in] ngrid number of grid points along x and y for fitting (fit at ngrid^2 points) 594 @param[in] linearizeAtCenter if True, get a linear approximation of the input 595 WCS at the image center and use that as the TAN initialization for 596 the TAN-SIP solution. You probably want this if your WCS has its 597 CRPIX outside the image bounding box. 600 srcSchema = afwTable.SourceTable.makeMinimalSchema()
601 key = srcSchema.addField(
"centroid", type=
"PointD")
602 srcTable = afwTable.SourceTable.make(srcSchema)
603 srcTable.defineCentroid(
"centroid")
605 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
608 (W, H) = bbox.getDimensions()
609 x0, y0 = bbox.getMin()
610 for xx
in np.linspace(0., W, ngrid):
611 for yy
in np.linspace(0, H, ngrid):
612 src = srcs.makeRecord()
613 src.set(key.getX(), x0 + xx)
614 src.set(key.getY(), y0 + yy)
616 rd = wcs.pixelToSky(xx + x0, yy + y0)
617 ref = refs.makeRecord()
621 if linearizeAtCenter:
626 crval = wcs.pixelToSky(crpix)
627 crval = crval.getPosition(afwGeom.degrees)
630 aff = wcs.linearizePixelToSky(crval)
631 cd = aff.getLinear().getMatrix()
632 wcs = afwImage.Wcs(crval, crpix, cd)
637 """Produce a SIP solution given a list of known correspondences. 639 Unlike _calculateSipTerms, this does not iterate the solution; 640 it assumes you have given it a good sets of corresponding stars. 642 NOTE that "refCat" and "sourceCat" are assumed to be the same length; 643 entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences. 645 @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS. 646 @param[in] refCat reference source catalog 647 @param[in] sourceCat source catalog 648 @param[in] bbox bounding box of image 650 sipOrder = self.
config.sipOrder
652 for ci, si
in zip(refCat, sourceCat):
655 sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox)
656 return sipObject.getNewWcs()
658 def _calculateSipTerms(self, origWcs, refCat, sourceCat, matches, bbox):
659 """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS. 661 @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS; 662 this is used to set the baseline when determining whether a SIP 663 solution is any better; it will be returned if no better SIP solution 665 @param[in] refCat reference source catalog 666 @param[in] sourceCat sources in the image to be solved 667 @param[in] matches list of supposedly matched sources, using the "origWcs". 668 @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients. 670 sipOrder = self.
config.sipOrder
673 lastMatchSize = len(matches)
675 for i
in range(self.
config.maxIter):
678 sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox)
679 proposedWcs = sipObject.getNewWcs()
680 self.
plotSolution(matches, proposedWcs, bbox.getDimensions())
682 self.log.
warn(
'Failed to calculate distortion terms. Error: ',
str(e))
686 for source
in sourceCat:
687 skyPos = proposedWcs.pixelToSky(source.getCentroid())
688 source.setCoord(skyPos)
691 proposedMatchlist = self.
_getMatchList(sourceCat, refCat, proposedWcs)
692 proposedMatchSize = len(proposedMatchlist)
696 "SIP iteration %i: %i objects match, previous = %i;" %
697 (i, proposedMatchSize, lastMatchSize) +
698 " clipped mean scatter = %s arcsec, previous = %s; " %
699 (proposedMatchStats.distMean.asArcseconds(), lastMatchStats.distMean.asArcseconds()) +
700 " max match dist = %s arcsec, previous = %s" %
701 (proposedMatchStats.maxMatchDist.asArcseconds(),
702 lastMatchStats.maxMatchDist.asArcseconds())
705 if lastMatchStats.maxMatchDist <= proposedMatchStats.maxMatchDist:
707 "Fit WCS: use iter %s because max match distance no better in next iter: " % (i-1,) +
708 " %g < %g arcsec" % (lastMatchStats.maxMatchDist.asArcseconds(),
709 proposedMatchStats.maxMatchDist.asArcseconds()))
713 matches = proposedMatchlist
714 lastMatchSize = proposedMatchSize
715 lastMatchStats = proposedMatchStats
720 """Plot the solution, when debugging is turned on. 722 @param matches The list of matches 724 @param imageSize 2-tuple with the image size (W,H) 732 import matplotlib.pyplot
as plt
733 except ImportError
as e:
734 self.log.
warn(
"Unable to import matplotlib: %s", e)
740 fig.canvas._tkcanvas._root().lift()
749 for i, m
in enumerate(matches):
750 x[i] = m.second.getX()
751 y[i] = m.second.getY()
752 pixel = wcs.skyToPixel(m.first.getCoord())
753 dx[i] = x[i] - pixel.getX()
754 dy[i] = y[i] - pixel.getY()
756 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
758 def plotNext(x, y, xLabel, yLabel, xMax):
760 ax.set_autoscalex_on(
False)
761 ax.set_xbound(lower=0, upper=xMax)
763 ax.set_xlabel(xLabel)
764 ax.set_ylabel(yLabel)
767 plotNext(x, dx,
"x",
"dx", imageSize[0])
768 plotNext(x, dy,
"x",
"dy", imageSize[0])
769 plotNext(y, dx,
"y",
"dx", imageSize[1])
770 plotNext(y, dy,
"y",
"dy", imageSize[1])
776 reply = input(
"Pausing for inspection, enter to continue... [hpQ] ").
strip()
780 reply = reply.split()
786 if reply
in (
"",
"h",
"p",
"Q"):
788 print(
"h[elp] p[db] Q[uit]")
797 def _computeMatchStatsOnSky(self, wcs, matchList):
798 """Compute on-sky radial distance statistics for a match list 800 @param[in] wcs WCS for match list; an lsst.afw.image.Wcs 801 @param[in] matchList list of matches between reference object and sources; 802 a list of lsst.afw.table.ReferenceMatch; 803 the source centroid and reference object coord are read 805 @return a pipe_base Struct containing these fields: 806 - distMean clipped mean of on-sky radial separation 807 - distStdDev clipped standard deviation of on-sky radial separation 808 - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev 811 afwMath.MEANCLIP | afwMath.STDEVCLIP)
812 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*afwGeom.radians
813 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*afwGeom.radians
814 return pipeBase.Struct(
816 distStdDev=distStdDev,
817 maxMatchDist=distMean + self.
config.matchDistanceSigma*distStdDev,
820 def _getMatchList(self, sourceCat, refCat, wcs):
821 dist = self.
config.catalogMatchDist * afwGeom.arcseconds
822 clean = self.
config.cleaningParameter
823 matcher = astromSip.MatchSrcToCatalogue(refCat, sourceCat, wcs, dist)
824 matches = matcher.getMatches()
827 X = [src.getX()
for src
in sourceCat]
828 Y = [src.getY()
for src
in sourceCat]
829 R1 = [src.getRa().asDegrees()
for src
in sourceCat]
830 D1 = [src.getDec().asDegrees()
for src
in sourceCat]
831 R2 = [src.getRa().asDegrees()
for src
in refCat]
832 D2 = [src.getDec().asDegrees()
for src
in refCat]
839 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sourceCat), len(refCat)))
842 'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
845 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
847 raise RuntimeError(
'No matches found between image and catalogue')
848 matches = cleanBadPoints.clean(matches, wcs, nsigma=clean)
853 Returns the column name in the astrometry_net_data index file that will be used 854 for the given filter name. 856 @param filterName Name of filter used in exposure 857 @param columnMap Dict that maps filter names to column names 858 @param default Default column name 860 filterName = self.
config.filterMap.get(filterName, filterName)
862 return columnMap[filterName]
864 self.log.
warn(
"No column in configuration for filter '%s'; using default '%s'" %
865 (filterName, default))
868 def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter, searchRadius, parity, filterName=None):
870 @param[in] parity True for flipped parity, False for normal parity, None to leave parity unchanged 874 imageSize = bbox.getDimensions()
875 x0, y0 = bbox.getMin()
880 badkeys = [goodsources.getSchema().find(name).key
for name
in self.
config.badFlags]
883 if np.isfinite(s.getX())
and np.isfinite(s.getY())
and np.isfinite(s.getPsfInstFlux()) \
885 goodsources.append(s)
887 self.log.
info(
"Number of selected sources for astrometry : %d" % (len(goodsources)))
888 if len(goodsources) < len(sourceCat):
889 self.log.
debug(
'Keeping %i of %i sources with finite X,Y positions and PSF flux',
890 len(goodsources), len(sourceCat))
891 self.log.
debug(
'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' +
892 '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net',
893 xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0)
895 solver.setStars(goodsources, x0, y0)
896 solver.setMaxStars(self.
config.maxStars)
897 solver.setImageSize(*imageSize)
898 solver.setMatchThreshold(self.
config.matchThreshold)
900 if radecCenter
is not None:
901 raDecRadius = (radecCenter.getLongitude().asDegrees(), radecCenter.getLatitude().asDegrees(),
902 searchRadius.asDegrees())
903 solver.setRaDecRadius(*raDecRadius)
904 self.log.
debug(
'Searching for match around RA,Dec = (%g, %g) with radius %g deg' %
907 if pixelScale
is not None:
908 dscale = self.
config.pixelScaleUncertainty
909 scale = pixelScale.asArcseconds()
912 solver.setPixelScaleRange(lo, hi)
914 'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix',
915 scale, 100.*(dscale-1.), lo, hi)
917 if parity
is not None:
918 solver.setParity(parity)
919 self.log.
debug(
'Searching for match with parity = %s',
str(parity))
922 if radecCenter
is not None:
923 multiInds = self.
refObjLoader._getMIndexesWithinRange(radecCenter, searchRadius)
926 qlo, qhi = solver.getQuadSizeRangeArcsec()
928 toload_multiInds =
set()
931 for i
in range(len(mi)):
933 if not ind.overlapsScaleRange(qlo, qhi):
935 toload_multiInds.add(mi)
936 toload_inds.append(ind)
946 solver.addIndices(toload_inds)
947 self.
memusage(
'Index files loaded: ')
949 cpulimit = self.
config.maxCpuTime
954 self.
memusage(
'Index files unloaded: ')
956 if solver.didSolve():
957 self.log.
debug(
'Solved!')
958 wcs = solver.getWcs()
960 if x0 != 0
or y0 != 0:
964 self.log.
warn(
'Did not get an astrometric solution from Astrometry.net')
970 if radecCenter
is not None:
971 self.
refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName)
973 qa = solver.getSolveStats()
974 self.log.
debug(
'qa: %s', qa.toString())
977 def _isGoodSource(self, candsource, keys):
979 if candsource.get(k):
984 def _trimBadPoints(sourceCat, bbox, wcs=None):
985 """Remove elements from catalog whose xy positions are not within the given bbox. 987 sourceCat: a Catalog of SimpleRecord or SourceRecord objects 988 bbox: an afwImage.Box2D 989 wcs: if not None, will be used to compute the xy positions on-the-fly; 990 this is required when sources actually contains SimpleRecords. 993 a list of Source objects with xAstrom, yAstrom within the bbox. 995 keep =
type(sourceCat)(sourceCat.table)
997 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
998 if bbox.contains(point):
1003 def _createMetadata(bbox, wcs, filterName):
1005 Create match metadata entries required for regenerating the catalog 1007 @param bbox bounding box of image (pixels) 1008 @param filterName Name of filter, used for magnitudes 1014 cx, cy = bboxD.getCenter()
1015 radec = wcs.pixelToSky(cx, cy)
1016 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
1017 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
1018 pixelRadius = math.hypot(*bboxD.getDimensions())/2.0
1019 skyRadius = wcs.getPixelScale() * pixelRadius
1020 meta.add(
'RADIUS', skyRadius.asDegrees(),
1021 'field radius in degrees, approximate')
1022 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1023 if filterName
is not None:
1024 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.
A floating-point coordinate rectangle geometry.
Class for storing ordered metadata with comments.
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)
Provides consistent interface for LSST exceptions.
def _computeMatchStatsOnSky(self, wcs, matchList)
daf::base::PropertySet * set
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.
Lightweight representation of a geometric match between two records.
def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True)
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Load reference objects from astrometry.net index files.
def getColumnName(self, filterName, columnMap, default=None)
afw::math::Statistics makeMatchStatisticsInRadians(afw::geom::SkyWcs const &wcs, std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
Compute statistics of on-sky radial separation for a match list, in radians.
def _trimBadPoints(sourceCat, bbox, wcs=None)
def getMatchMetadata(self)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
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)