23 __all__ = [
"InitialAstrometry",
"ANetBasicAstrometryConfig",
"ANetBasicAstrometryTask"]
31 from lsst.pex.config
import Field, RangeField, ListField
39 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask, LoadMultiIndexes
42 from .
import cleanBadPoints
47 Object returned by Astrometry.determineWcs 49 getWcs(): sipWcs or tanWcs 50 getMatches(): sipMatches or tanMatches 53 solveQa (PropertyList) 55 tanMatches (MatchList) 57 sipMatches (MatchList) 58 refCat (lsst::afw::table::SimpleCatalog) 59 matchMeta (PropertyList) 73 Get "sipMatches" -- MatchList using the SIP WCS solution, if it 74 exists, or "tanMatches" -- MatchList using the TAN WCS solution 81 Returns the SIP WCS, if one was found, or a TAN WCS 85 matches = property(getMatches)
86 wcs = property(getWcs)
112 maxCpuTime = RangeField(
113 doc=
"Maximum CPU time to spend solving, in seconds",
118 matchThreshold = RangeField(
119 doc=
"Matching threshold for Astrometry.net solver (log-odds)",
121 default=math.log(1e12),
124 maxStars = RangeField(
125 doc=
"Maximum number of stars to use in Astrometry.net solving",
130 useWcsPixelScale = Field(
131 doc=
"Use the pixel scale from the input exposure\'s WCS headers?",
135 useWcsRaDecCenter = Field(
136 doc=
"Use the RA,Dec center information from the input exposure\'s WCS headers?",
140 useWcsParity = Field(
141 doc=
"Use the parity (flip / handedness) of the image from the input exposure\'s WCS headers?",
145 raDecSearchRadius = RangeField(
146 doc=
"When useWcsRaDecCenter=True, this is the radius, in degrees, around the RA,Dec center " 147 r"specified in the input exposure\'s WCS to search for a solution.",
152 pixelScaleUncertainty = RangeField(
153 doc=
"Range of pixel scales, around the value in the WCS header, to search. " 154 "If the value of this field is X and the nominal scale is S, " 155 "the range searched will be S/X to S*X",
160 catalogMatchDist = RangeField(
161 doc=
"Matching radius (arcsec) for matching sources to reference objects",
166 cleaningParameter = RangeField(
167 doc=
"Sigma-clipping parameter in cleanBadPoints.py",
172 calculateSip = Field(
173 doc=
"Compute polynomial SIP distortion terms?",
177 sipOrder = RangeField(
178 doc=
"Polynomial order of SIP distortion terms",
183 badFlags = ListField(
184 doc=
"List of flags which cause a source to be rejected as bad",
187 "slot_Centroid_flag",
188 "base_PixelFlags_flag_edge",
189 "base_PixelFlags_flag_saturated",
190 "base_PixelFlags_flag_crCenter",
194 doc=
"Retrieve all available fluxes (and errors) from catalog?",
198 maxIter = RangeField(
199 doc=
"maximum number of iterations of match sources and fit WCS" 200 "ignored if not fitting a WCS",
205 matchDistanceSigma = RangeField(
206 doc=
"The match and fit loop stops when maxMatchDist minimized: " 207 " maxMatchDist = meanMatchDist + matchDistanceSigma*stdDevMatchDistance " 208 " (where the mean and std dev are computed using outlier rejection);" 209 " ignored if not fitting a WCS",
217 """!Basic implemeentation of the astrometry.net astrometrical fitter 219 A higher-level class ANetAstrometryTask takes care of dealing with the fact 220 that the initial WCS is probably only a pure TAN SIP, yet we may have 221 significant distortion and a good estimate for that distortion. 224 About Astrometry.net index files (astrometry_net_data): 226 There are three components of an index file: a list of stars 227 (stored as a star kd-tree), a list of quadrangles of stars ("quad 228 file") and a list of the shapes ("codes") of those quadrangles, 229 stored as a code kd-tree. 231 Each index covers a region of the sky, defined by healpix nside 232 and number, and a range of angular scales. In LSST, we share the 233 list of stars in a part of the sky between multiple indexes. That 234 is, the star kd-tree is shared between multiple indices (quads and 235 code kd-trees). In the astrometry.net code, this is called a 238 It is possible to "unload" and "reload" multiindex (and index) 239 objects. When "unloaded", they consume no FILE or mmap resources. 241 The multiindex object holds the star kd-tree and gives each index 242 object it holds a pointer to it, so it is necessary to 243 multiindex_reload_starkd() before reloading the indices it holds. 244 The multiindex_unload() method, on the other hand, unloads its 245 starkd and unloads each index it holds. 247 ConfigClass = ANetBasicAstrometryConfig
248 _DefaultName =
"aNetBasicAstrometry" 254 r"""!Construct an ANetBasicAstrometryTask 256 @param[in] config configuration (an instance of self.ConfigClass) 257 @param[in] andConfig astrometry.net data config (an instance of AstromNetDataConfig, or None); 258 if None then use andConfig.py in the astrometry_net_data product (which must be setup) 259 @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_ 261 @throw RuntimeError if andConfig is None and the configuration cannot be found, 262 either because astrometry_net_data is not setup in eups 263 or because the setup version does not include the file "andConfig.py" 265 pipeBase.Task.__init__(self, config=config, **kwargs)
278 if self.log.
getLevel() > self.log.DEBUG:
280 from astrometry.util.ttime
import get_memusage
283 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
285 ss.append(key +
': ' +
' '.join(mu[key]))
287 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
288 if 'mmaps_total' in mu:
289 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
290 self.log.
debug(prefix +
'Memory: ' +
', '.join(ss))
292 def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True):
293 """Get image parameters 295 @param[in] exposure exposure (an afwImage.Exposure) or None 296 @param[in] bbox bounding box (an geom.Box2I) or None; if None then bbox must be specified 297 @param[in] wcs WCS (an afwImage.Wcs) or None; if None then exposure must be specified 298 @param[in] filterName filter name, a string, or None; if None exposure must be specified 299 @param[in] wcsRequired if True then either wcs must be specified or exposure must contain a wcs; 300 if False then the returned wcs may be None 302 - bbox bounding box; guaranteed to be set 303 - wcs WCS if known, else None 304 - filterName filter name if known, else None 305 @throw RuntimeError if bbox cannot be determined, or wcs cannot be determined and wcsRequired True 307 if exposure
is not None:
309 bbox = exposure.getBBox()
310 self.log.
debug(
"Setting bbox = %s from exposure metadata", bbox)
312 self.log.
debug(
"Setting wcs from exposure metadata")
313 wcs = exposure.getWcs()
314 if filterName
is None:
315 filterName = exposure.getFilter().getName()
316 self.log.
debug(
"Setting filterName = %r from exposure metadata", filterName)
318 raise RuntimeError(
"bbox or exposure must be specified")
319 if wcs
is None and wcsRequired:
320 raise RuntimeError(
"wcs or exposure (with a WCS) must be specified")
321 return bbox, wcs, filterName
323 def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None, calculateSip=None):
324 """!Return an InitialAstrometry object, just like determineWcs, 325 but assuming the given input WCS is correct. 327 This involves searching for reference sources within the WCS 328 area, and matching them to the given 'sourceCat'. If 329 'calculateSip' is set, we will try to compute a TAN-SIP 330 distortion correction. 332 @param[in] sourceCat list of detected sources in this image. 333 @param[in] wcs your known WCS, or None to get from exposure 334 @param[in] exposure the exposure holding metadata for this image; 335 if None then you must specify wcs, filterName and bbox 336 @param[in] filterName string, filter name, eg "i", or None to get from exposure` 337 @param[in] bbox bounding box of image, or None to get from exposure 338 @param[in] calculateSip calculate SIP distortion terms for the WCS? If None 339 then use self.config.calculateSip. To disable WCS fitting set calculateSip=False 341 @note this function is also called by 'determineWcs' (via 'determineWcs2'), since the steps are all 347 if calculateSip
is None:
348 calculateSip = self.
config.calculateSip
354 filterName=filterName,
360 filterName=filterName,
363 astrom.refCat = refCat
364 catids = [src.getId()
for src
in refCat]
366 self.log.
debug(
'%i reference sources; %i unique IDs', len(catids), len(uids))
368 uniq =
set([sm.second.getId()
for sm
in matches])
369 if len(matches) != len(uniq):
370 self.log.
warn(
'The list of matched stars contains duplicate reference source IDs ' 371 '(%i sources, %i unique ids)', len(matches), len(uniq))
372 if len(matches) == 0:
373 self.log.
warn(
'No matches found between input sources and reference catalogue.')
376 self.log.
debug(
'%i reference objects match input sources using input WCS', len(matches))
377 astrom.tanMatches = matches
380 srcids = [s.getId()
for s
in sourceCat]
382 assert(m.second.getId()
in srcids)
383 assert(m.second
in sourceCat)
388 self.log.
debug(
'Failed to find a SIP WCS better than the initial one.')
390 self.log.
debug(
'%i reference objects match input sources using SIP WCS',
392 astrom.sipWcs = sipwcs
393 astrom.sipMatches = matches
395 wcs = astrom.getWcs()
398 for src
in sourceCat:
400 astrom.matchMeta = _createMetadata(bbox, wcs, filterName)
404 """Find a WCS solution for the given 'sourceCat' in the given 405 'exposure', getting other parameters from config. 407 Valid kwargs include: 409 'radecCenter', an afw.geom.SpherePoint giving the ICRS RA,Dec position 410 of the center of the field. This is used to limit the 411 search done by Astrometry.net (to make it faster and load 412 fewer index files, thereby using less memory). Defaults to 413 the RA,Dec center from the exposure's WCS; turn that off 414 with the boolean kwarg 'useRaDecCenter' or config option 417 'useRaDecCenter', a boolean. Don't use the RA,Dec center from 418 the exposure's initial WCS. 420 'searchRadius', in degrees, to search for a solution around 421 the given 'radecCenter'; default from config option 424 'useParity': parity is the 'flip' of the image. Knowing it 425 reduces the search space (hence time) for Astrometry.net. 426 The parity can be computed from the exposure's WCS (the 427 sign of the determinant of the CD matrix); this option 428 controls whether we do that or force Astrometry.net to 429 search both parities. Default from config.useWcsParity. 431 'pixelScale': geom.Angle, estimate of the angle-per-pixel 432 (ie, arcseconds per pixel). Defaults to a value derived 433 from the exposure's WCS. If enabled, this value, plus or 434 minus config.pixelScaleUncertainty, will be used to limit 435 Astrometry.net's search. 437 'usePixelScale': boolean. Use the pixel scale to limit 438 Astrometry.net's search? Defaults to config.useWcsPixelScale. 440 'filterName', a string, the filter name of this image. Will 441 be mapped through the 'filterMap' config dictionary to a 442 column name in the astrometry_net_data index FITS files. 443 Defaults to the exposure.getFilter() value. 445 'bbox', bounding box of exposure; defaults to exposure.getBBox() 448 assert(exposure
is not None)
450 margs = kwargs.copy()
451 if 'searchRadius' not in margs:
452 margs.update(searchRadius=self.
config.raDecSearchRadius * geom.degrees)
453 if 'usePixelScale' not in margs:
454 margs.update(usePixelScale=self.
config.useWcsPixelScale)
455 if 'useRaDecCenter' not in margs:
456 margs.update(useRaDecCenter=self.
config.useWcsRaDecCenter)
457 if 'useParity' not in margs:
458 margs.update(useParity=self.
config.useWcsParity)
459 margs.update(exposure=exposure)
463 """Get a blind astrometric solution for the given catalog of sources. 469 And if available, we can use: 470 -an initial Wcs estimate; 475 (all of which are metadata of Exposure). 478 imageSize: (W,H) integer tuple/iterable 479 pixelScale: lsst.geom.Angle per pixel. 480 radecCenter: lsst.afw.coord.Coord 485 for key
in [
'exposure',
'bbox',
'filterName']:
487 kw[key] = kwargs[key]
488 astrom = self.
useKnownWcs(sourceCat, wcs=wcs, **kw)
505 searchRadiusScale=2.):
506 if not useRaDecCenter
and radecCenter
is not None:
507 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
508 if not usePixelScale
and pixelScale
is not None:
509 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
515 filterName=filterName,
520 xc, yc = bboxD.getCenter()
524 if pixelScale
is None:
526 pixelScale = wcs.getPixelScale()
527 self.log.
debug(
'Setting pixel scale estimate = %.3f from given WCS estimate',
528 pixelScale.asArcseconds())
530 if radecCenter
is None:
532 radecCenter = wcs.pixelToSky(xc, yc)
533 self.log.
debug(
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS ' 534 'estimate, using pixel center = (%.1f, %.1f)',
535 radecCenter.getLongitude().asDegrees(),
536 radecCenter.getLatitude().asDegrees(), xc, yc)
538 if searchRadius
is None:
540 assert(pixelScale
is not None)
541 pixRadius = math.hypot(*bboxD.getDimensions()) / 2
542 searchRadius = (pixelScale * pixRadius * searchRadiusScale)
543 self.log.
debug(
'Using RA,Dec search radius = %.3f deg, from pixel scale, ' 544 'image size, and searchRadiusScale = %g',
545 searchRadius, searchRadiusScale)
547 parity = wcs.isFlipped
548 self.log.
debug(
'Using parity = %s' % (parity
and 'True' or 'False'))
552 if exposure
is not None:
553 exposureBBoxD =
geom.Box2D(exposure.getMaskedImage().getBBox())
555 exposureBBoxD = bboxD
557 self.log.
debug(
"Trimming: kept %i of %i sources", n, len(sourceCat))
563 pixelScale=pixelScale,
564 radecCenter=radecCenter,
565 searchRadius=searchRadius,
567 filterName=filterName,
570 raise RuntimeError(
"Unable to match sources with catalog.")
571 self.log.
info(
'Got astrometric solution from Astrometry.net')
573 rdc = wcs.pixelToSky(xc, yc)
574 self.log.
debug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)',
575 xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees())
579 """!Get a TAN-SIP WCS, starting from an existing WCS. 581 It uses your WCS to compute a fake grid of corresponding "stars" in pixel and sky coords, 582 and feeds that to the regular SIP code. 584 @param[in] wcs initial WCS 585 @param[in] bbox bounding box of image 586 @param[in] ngrid number of grid points along x and y for fitting (fit at ngrid^2 points) 587 @param[in] linearizeAtCenter if True, get a linear approximation of the input 588 WCS at the image center and use that as the TAN initialization for 589 the TAN-SIP solution. You probably want this if your WCS has its 590 CRPIX outside the image bounding box. 593 srcSchema = afwTable.SourceTable.makeMinimalSchema()
594 key = srcSchema.addField(
"centroid", type=
"PointD")
595 srcTable = afwTable.SourceTable.make(srcSchema)
596 srcTable.defineCentroid(
"centroid")
598 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
601 (W, H) = bbox.getDimensions()
602 x0, y0 = bbox.getMin()
603 for xx
in np.linspace(0., W, ngrid):
604 for yy
in np.linspace(0, H, ngrid):
605 src = srcs.makeRecord()
606 src.set(key.getX(), x0 + xx)
607 src.set(key.getY(), y0 + yy)
609 rd = wcs.pixelToSky(xx + x0, yy + y0)
610 ref = refs.makeRecord()
614 if linearizeAtCenter:
619 crval = wcs.pixelToSky(crpix)
620 crval = crval.getPosition(geom.degrees)
623 aff = wcs.linearizePixelToSky(crval)
624 cd = aff.getLinear().getMatrix()
625 wcs = afwImage.Wcs(crval, crpix, cd)
630 """Produce a SIP solution given a list of known correspondences. 632 Unlike _calculateSipTerms, this does not iterate the solution; 633 it assumes you have given it a good sets of corresponding stars. 635 NOTE that "refCat" and "sourceCat" are assumed to be the same length; 636 entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences. 638 @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS. 639 @param[in] refCat reference source catalog 640 @param[in] sourceCat source catalog 641 @param[in] bbox bounding box of image 643 sipOrder = self.
config.sipOrder
645 for ci, si
in zip(refCat, sourceCat):
648 sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox)
649 return sipObject.getNewWcs()
651 def _calculateSipTerms(self, origWcs, refCat, sourceCat, matches, bbox):
652 """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS. 654 @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS; 655 this is used to set the baseline when determining whether a SIP 656 solution is any better; it will be returned if no better SIP solution 658 @param[in] refCat reference source catalog 659 @param[in] sourceCat sources in the image to be solved 660 @param[in] matches list of supposedly matched sources, using the "origWcs". 661 @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients. 663 sipOrder = self.
config.sipOrder
666 lastMatchSize = len(matches)
668 for i
in range(self.
config.maxIter):
671 sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox)
672 proposedWcs = sipObject.getNewWcs()
673 self.
plotSolution(matches, proposedWcs, bbox.getDimensions())
675 self.log.
warn(
'Failed to calculate distortion terms. Error: ', str(e))
679 for source
in sourceCat:
680 skyPos = proposedWcs.pixelToSky(source.getCentroid())
681 source.setCoord(skyPos)
684 proposedMatchlist = self.
_getMatchList(sourceCat, refCat, proposedWcs)
685 proposedMatchSize = len(proposedMatchlist)
689 "SIP iteration %i: %i objects match, previous = %i;" %
690 (i, proposedMatchSize, lastMatchSize) +
691 " clipped mean scatter = %s arcsec, previous = %s; " %
692 (proposedMatchStats.distMean.asArcseconds(), lastMatchStats.distMean.asArcseconds()) +
693 " max match dist = %s arcsec, previous = %s" %
694 (proposedMatchStats.maxMatchDist.asArcseconds(),
695 lastMatchStats.maxMatchDist.asArcseconds())
698 if lastMatchStats.maxMatchDist <= proposedMatchStats.maxMatchDist:
700 "Fit WCS: use iter %s because max match distance no better in next iter: " % (i-1,) +
701 " %g < %g arcsec" % (lastMatchStats.maxMatchDist.asArcseconds(),
702 proposedMatchStats.maxMatchDist.asArcseconds()))
706 matches = proposedMatchlist
707 lastMatchSize = proposedMatchSize
708 lastMatchStats = proposedMatchStats
713 """Plot the solution, when debugging is turned on. 715 @param matches The list of matches 717 @param imageSize 2-tuple with the image size (W,H) 725 import matplotlib.pyplot
as plt
726 except ImportError
as e:
727 self.log.
warn(
"Unable to import matplotlib: %s", e)
733 fig.canvas._tkcanvas._root().lift()
742 for i, m
in enumerate(matches):
743 x[i] = m.second.getX()
744 y[i] = m.second.getY()
745 pixel = wcs.skyToPixel(m.first.getCoord())
746 dx[i] = x[i] - pixel.getX()
747 dy[i] = y[i] - pixel.getY()
749 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
751 def plotNext(x, y, xLabel, yLabel, xMax):
753 ax.set_autoscalex_on(
False)
754 ax.set_xbound(lower=0, upper=xMax)
756 ax.set_xlabel(xLabel)
757 ax.set_ylabel(yLabel)
760 plotNext(x, dx,
"x",
"dx", imageSize[0])
761 plotNext(x, dy,
"x",
"dy", imageSize[0])
762 plotNext(y, dx,
"y",
"dx", imageSize[1])
763 plotNext(y, dy,
"y",
"dy", imageSize[1])
769 reply = input(
"Pausing for inspection, enter to continue... [hpQ] ").
strip()
773 reply = reply.split()
779 if reply
in (
"",
"h",
"p",
"Q"):
781 print(
"h[elp] p[db] Q[uit]")
790 def _computeMatchStatsOnSky(self, wcs, matchList):
791 """Compute on-sky radial distance statistics for a match list 793 @param[in] wcs WCS for match list; an lsst.afw.image.Wcs 794 @param[in] matchList list of matches between reference object and sources; 795 a list of lsst.afw.table.ReferenceMatch; 796 the source centroid and reference object coord are read 798 @return a pipe_base Struct containing these fields: 799 - distMean clipped mean of on-sky radial separation 800 - distStdDev clipped standard deviation of on-sky radial separation 801 - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev 804 afwMath.MEANCLIP | afwMath.STDEVCLIP)
805 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*geom.radians
806 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*geom.radians
807 return pipeBase.Struct(
809 distStdDev=distStdDev,
810 maxMatchDist=distMean + self.
config.matchDistanceSigma*distStdDev,
813 def _getMatchList(self, sourceCat, refCat, wcs):
814 dist = self.
config.catalogMatchDist * geom.arcseconds
815 clean = self.
config.cleaningParameter
816 matcher = astromSip.MatchSrcToCatalogue(refCat, sourceCat, wcs, dist)
817 matches = matcher.getMatches()
820 X = [src.getX()
for src
in sourceCat]
821 Y = [src.getY()
for src
in sourceCat]
822 R1 = [src.getRa().asDegrees()
for src
in sourceCat]
823 D1 = [src.getDec().asDegrees()
for src
in sourceCat]
824 R2 = [src.getRa().asDegrees()
for src
in refCat]
825 D2 = [src.getDec().asDegrees()
for src
in refCat]
832 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sourceCat), len(refCat)))
835 'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
838 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
840 raise RuntimeError(
'No matches found between image and catalogue')
841 matches = cleanBadPoints.clean(matches, wcs, nsigma=clean)
846 Returns the column name in the astrometry_net_data index file that will be used 847 for the given filter name. 849 @param filterName Name of filter used in exposure 850 @param columnMap Dict that maps filter names to column names 851 @param default Default column name 853 filterName = self.
config.filterMap.get(filterName, filterName)
855 return columnMap[filterName]
857 self.log.
warn(
"No column in configuration for filter '%s'; using default '%s'" %
858 (filterName, default))
861 def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter, searchRadius, parity, filterName=None):
863 @param[in] parity True for flipped parity, False for normal parity, None to leave parity unchanged 867 imageSize = bbox.getDimensions()
868 x0, y0 = bbox.getMin()
873 badkeys = [goodsources.getSchema().find(name).key
for name
in self.
config.badFlags]
876 if np.isfinite(s.getX())
and np.isfinite(s.getY())
and np.isfinite(s.getPsfInstFlux()) \
878 goodsources.append(s)
879 xybb.include(
geom.Point2D(s.getX() - x0, s.getY() - y0))
880 self.log.
info(
"Number of selected sources for astrometry : %d" % (len(goodsources)))
881 if len(goodsources) < len(sourceCat):
882 self.log.
debug(
'Keeping %i of %i sources with finite X,Y positions and PSF flux',
883 len(goodsources), len(sourceCat))
884 self.log.
debug(
'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' +
885 '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net',
886 xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0)
888 solver.setStars(goodsources, x0, y0)
889 solver.setMaxStars(self.
config.maxStars)
890 solver.setImageSize(*imageSize)
891 solver.setMatchThreshold(self.
config.matchThreshold)
893 if radecCenter
is not None:
894 raDecRadius = (radecCenter.getLongitude().asDegrees(), radecCenter.getLatitude().asDegrees(),
895 searchRadius.asDegrees())
896 solver.setRaDecRadius(*raDecRadius)
897 self.log.
debug(
'Searching for match around RA,Dec = (%g, %g) with radius %g deg' %
900 if pixelScale
is not None:
901 dscale = self.
config.pixelScaleUncertainty
902 scale = pixelScale.asArcseconds()
905 solver.setPixelScaleRange(lo, hi)
907 'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix',
908 scale, 100.*(dscale-1.), lo, hi)
910 if parity
is not None:
911 solver.setParity(parity)
912 self.log.
debug(
'Searching for match with parity = %s', str(parity))
915 if radecCenter
is not None:
916 multiInds = self.
refObjLoader._getMIndexesWithinRange(radecCenter, searchRadius)
919 qlo, qhi = solver.getQuadSizeRangeArcsec()
921 toload_multiInds =
set()
924 for i
in range(len(mi)):
926 if not ind.overlapsScaleRange(qlo, qhi):
928 toload_multiInds.add(mi)
929 toload_inds.append(ind)
939 solver.addIndices(toload_inds)
940 self.
memusage(
'Index files loaded: ')
942 cpulimit = self.
config.maxCpuTime
947 self.
memusage(
'Index files unloaded: ')
949 if solver.didSolve():
950 self.log.
debug(
'Solved!')
951 wcs = solver.getWcs()
953 if x0 != 0
or y0 != 0:
957 self.log.
warn(
'Did not get an astrometric solution from Astrometry.net')
963 if radecCenter
is not None:
964 self.
refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName)
966 qa = solver.getSolveStats()
967 self.log.
debug(
'qa: %s', qa.toString())
970 def _isGoodSource(self, candsource, keys):
972 if candsource.get(k):
977 def _trimBadPoints(sourceCat, bbox, wcs=None):
978 """Remove elements from catalog whose xy positions are not within the given bbox. 980 sourceCat: a Catalog of SimpleRecord or SourceRecord objects 981 bbox: an afwImage.Box2D 982 wcs: if not None, will be used to compute the xy positions on-the-fly; 983 this is required when sources actually contains SimpleRecords. 986 a list of Source objects with xAstrom, yAstrom within the bbox. 988 keep =
type(sourceCat)(sourceCat.table)
990 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
991 if bbox.contains(point):
996 def _createMetadata(bbox, wcs, filterName):
998 Create match metadata entries required for regenerating the catalog 1000 @param bbox bounding box of image (pixels) 1001 @param filterName Name of filter, used for magnitudes 1007 cx, cy = bboxD.getCenter()
1008 radec = wcs.pixelToSky(cx, cy)
1009 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
1010 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
1011 pixelRadius = math.hypot(*bboxD.getDimensions())/2.0
1012 skyRadius = wcs.getPixelScale() * pixelRadius
1013 meta.add(
'RADIUS', skyRadius.asDegrees(),
1014 'field radius in degrees, approximate')
1015 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1016 if filterName
is not None:
1017 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)