1 from __future__
import absolute_import, division
19 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask, LoadMultiIndexes
20 from .display
import displayAstrometry
21 from .astromLib
import makeMatchStatisticsInRadians
23 from .
import sip
as astromSip
25 __all__ = [
"InitialAstrometry",
"ANetBasicAstrometryConfig",
"ANetBasicAstrometryTask"]
29 Object returned by Astrometry.determineWcs
31 getWcs(): sipWcs or tanWcs
32 getMatches(): sipMatches or tanMatches
35 solveQa (PropertyList)
37 tanMatches (MatchList)
39 sipMatches (MatchList)
40 refCat (lsst::afw::table::SimpleCatalog)
41 matchMeta (PropertyList)
54 Get "sipMatches" -- MatchList using the SIP WCS solution, if it
55 exists, or "tanMatches" -- MatchList using the TAN WCS solution
62 Returns the SIP WCS, if one was found, or a TAN WCS
66 matches = property(getMatches)
67 wcs = property(getWcs)
88 maxCpuTime = RangeField(
89 doc =
"Maximum CPU time to spend solving, in seconds",
94 matchThreshold = RangeField(
95 doc =
"Matching threshold for Astrometry.net solver (log-odds)",
97 default=math.log(1e12),
100 maxStars = RangeField(
101 doc =
"Maximum number of stars to use in Astrometry.net solving",
106 useWcsPixelScale = Field(
107 doc =
"Use the pixel scale from the input exposure\'s WCS headers?",
111 useWcsRaDecCenter = Field(
112 doc=
"Use the RA,Dec center information from the input exposure\'s WCS headers?",
116 useWcsParity = Field(
117 doc=
"Use the parity (flip / handedness) of the image from the input exposure\'s WCS headers?",
121 raDecSearchRadius = RangeField(
122 doc =
"When useWcsRaDecCenter=True, this is the radius, in degrees, around the RA,Dec center " +
123 "specified in the input exposure\'s WCS to search for a solution.",
128 pixelScaleUncertainty = RangeField(
129 doc =
"Range of pixel scales, around the value in the WCS header, to search. If the value of this field " +
130 "is X and the nominal scale is S, the range searched will be S/X to S*X",
135 catalogMatchDist = RangeField(
136 doc =
"Matching radius (arcsec) for matching sources to reference objects",
141 cleaningParameter = RangeField(
142 doc =
"Sigma-clipping parameter in sip/cleanBadPoints.py",
147 calculateSip = Field(
148 doc =
"Compute polynomial SIP distortion terms?",
152 sipOrder = RangeField(
153 doc =
"Polynomial order of SIP distortion terms",
158 badFlags = ListField(
159 doc =
"List of flags which cause a source to be rejected as bad",
162 "slot_Centroid_flag",
163 "base_PixelFlags_flag_edge",
164 "base_PixelFlags_flag_saturated",
165 "base_PixelFlags_flag_crCenter",
169 doc =
"Retrieve all available fluxes (and errors) from catalog?",
173 maxIter = RangeField(
174 doc =
"maximum number of iterations of match sources and fit WCS" +
175 "ignored if not fitting a WCS",
180 matchDistanceSigma = RangeField(
181 doc =
"The match and fit loop stops when maxMatchDist minimized: "
182 " maxMatchDist = meanMatchDist + matchDistanceSigma*stdDevMatchDistance " +
183 " (where the mean and std dev are computed using outlier rejection);" +
184 " ignored if not fitting a WCS",
192 """!Basic implemeentation of the astrometry.net astrometrical fitter
194 A higher-level class ANetAstrometryTask takes care of dealing with the fact
195 that the initial WCS is probably only a pure TAN SIP, yet we may have
196 significant distortion and a good estimate for that distortion.
199 About Astrometry.net index files (astrometry_net_data):
201 There are three components of an index file: a list of stars
202 (stored as a star kd-tree), a list of quadrangles of stars ("quad
203 file") and a list of the shapes ("codes") of those quadrangles,
204 stored as a code kd-tree.
206 Each index covers a region of the sky, defined by healpix nside
207 and number, and a range of angular scales. In LSST, we share the
208 list of stars in a part of the sky between multiple indexes. That
209 is, the star kd-tree is shared between multiple indices (quads and
210 code kd-trees). In the astrometry.net code, this is called a
213 It is possible to "unload" and "reload" multiindex (and index)
214 objects. When "unloaded", they consume no FILE or mmap resources.
216 The multiindex object holds the star kd-tree and gives each index
217 object it holds a pointer to it, so it is necessary to
218 multiindex_reload_starkd() before reloading the indices it holds.
219 The multiindex_unload() method, on the other hand, unloads its
220 starkd and unloads each index it holds.
222 ConfigClass = ANetBasicAstrometryConfig
223 _DefaultName =
"aNetBasicAstrometry"
228 """!Construct an ANetBasicAstrometryTask
230 @param[in] config configuration (an instance of self.ConfigClass)
231 @param[in] andConfig astrometry.net data config (an instance of AstromNetDataConfig, or None);
232 if None then use andConfig.py in the astrometry_net_data product (which must be setup)
233 @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_
235 @throw RuntimeError if andConfig is None and the configuration cannot be found,
236 either because astrometry_net_data is not setup in eups
237 or because the setup version does not include the file "andConfig.py"
239 pipeBase.Task.__init__(self, config=config, **kwargs)
242 self.
refObjLoader = LoadAstrometryNetObjectsTask(config=self.
config, andConfig=andConfig, log=self.log,
244 self.refObjLoader._readIndexFiles()
248 if self.log.getThreshold() > pexLog.Log.DEBUG:
250 from astrometry.util.ttime
import get_memusage
253 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
255 ss.append(key +
': ' +
' '.join(mu[key]))
257 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
258 if 'mmaps_total' in mu:
259 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
260 self.log.logdebug(prefix +
'Memory: ' +
', '.join(ss))
262 def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True):
263 """Get image parameters
265 @param[in] exposure exposure (an afwImage.Exposure) or None
266 @param[in] bbox bounding box (an afwGeom.Box2I) or None; if None then bbox must be specified
267 @param[in] wcs WCS (an afwImage.Wcs) or None; if None then exposure must be specified
268 @param[in] filterName filter name, a string, or None; if None exposure must be specified
269 @param[in] wcsRequired if True then either wcs must be specified or exposure must contain a wcs;
270 if False then the returned wcs may be None
272 - bbox bounding box; guaranteed to be set
273 - wcs WCS if known, else None
274 - filterName filter name if known, else None
275 @throw RuntimeError if bbox cannot be determined, or wcs cannot be determined and wcsRequired True
277 if exposure
is not None:
279 bbox = exposure.getBBox()
280 self.log.logdebug(
"Setting bbox = %s from exposure metadata" % (bbox,))
282 self.log.logdebug(
"Setting wcs from exposure metadata")
283 wcs = exposure.getWcs()
284 if filterName
is None:
285 filterName = exposure.getFilter().getName()
286 self.log.logdebug(
"Setting filterName = %r from exposure metadata" % (filterName,))
288 raise RuntimeError(
"bbox or exposure must be specified")
289 if wcs
is None and wcsRequired:
290 raise RuntimeError(
"wcs or exposure (with a WCS) must be specified")
291 return bbox, wcs, filterName
293 def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None, calculateSip=None):
294 """!Return an InitialAstrometry object, just like determineWcs,
295 but assuming the given input WCS is correct.
297 This involves searching for reference sources within the WCS
298 area, and matching them to the given 'sourceCat'. If
299 'calculateSip' is set, we will try to compute a TAN-SIP
300 distortion correction.
302 @param[in] sourceCat list of detected sources in this image.
303 @param[in] wcs your known WCS, or None to get from exposure
304 @param[in] exposure the exposure holding metadata for this image;
305 if None then you must specify wcs, filterName and bbox
306 @param[in] filterName string, filter name, eg "i", or None to get from exposure`
307 @param[in] bbox bounding box of image, or None to get from exposure
308 @param[in] calculateSip calculate SIP distortion terms for the WCS? If None
309 then use self.config.calculateSip. To disable WCS fitting set calculateSip=False
311 @note this function is also called by 'determineWcs' (via 'determineWcs2'), since the steps are all
317 if calculateSip
is None:
318 calculateSip = self.config.calculateSip
324 filterName = filterName,
327 refCat = self.refObjLoader.loadPixelBox(
330 filterName = filterName,
333 astrom.refCat = refCat
334 catids = [src.getId()
for src
in refCat]
336 self.log.logdebug(
'%i reference sources; %i unique IDs' % (len(catids), len(uids)))
338 uniq = set([sm.second.getId()
for sm
in matches])
339 if len(matches) != len(uniq):
340 self.log.warn((
'The list of matched stars contains duplicate reference source IDs ' +
341 '(%i sources, %i unique ids)') % (len(matches), len(uniq)))
342 if len(matches) == 0:
343 self.log.warn(
'No matches found between input sources and reference catalogue.')
346 self.log.logdebug(
'%i reference objects match input sources using input WCS' % (len(matches)))
347 astrom.tanMatches = matches
350 srcids = [s.getId()
for s
in sourceCat]
352 assert(m.second.getId()
in srcids)
353 assert(m.second
in sourceCat)
358 self.log.logdebug(
'Failed to find a SIP WCS better than the initial one.')
360 self.log.logdebug(
'%i reference objects match input sources using SIP WCS' %
362 astrom.sipWcs = sipwcs
363 astrom.sipMatches = matches
365 wcs = astrom.getWcs()
368 for src
in sourceCat:
377 """Find a WCS solution for the given 'sourceCat' in the given
378 'exposure', getting other parameters from config.
380 Valid kwargs include:
382 'radecCenter', an afw.coord.Coord giving the RA,Dec position
383 of the center of the field. This is used to limit the
384 search done by Astrometry.net (to make it faster and load
385 fewer index files, thereby using less memory). Defaults to
386 the RA,Dec center from the exposure's WCS; turn that off
387 with the boolean kwarg 'useRaDecCenter' or config option
390 'useRaDecCenter', a boolean. Don't use the RA,Dec center from
391 the exposure's initial WCS.
393 'searchRadius', in degrees, to search for a solution around
394 the given 'radecCenter'; default from config option
397 'useParity': parity is the 'flip' of the image. Knowing it
398 reduces the search space (hence time) for Astrometry.net.
399 The parity can be computed from the exposure's WCS (the
400 sign of the determinant of the CD matrix); this option
401 controls whether we do that or force Astrometry.net to
402 search both parities. Default from config.useWcsParity.
404 'pixelScale': afwGeom.Angle, estimate of the angle-per-pixel
405 (ie, arcseconds per pixel). Defaults to a value derived
406 from the exposure's WCS. If enabled, this value, plus or
407 minus config.pixelScaleUncertainty, will be used to limit
408 Astrometry.net's search.
410 'usePixelScale': boolean. Use the pixel scale to limit
411 Astrometry.net's search? Defaults to config.useWcsPixelScale.
413 'filterName', a string, the filter name of this image. Will
414 be mapped through the 'filterMap' config dictionary to a
415 column name in the astrometry_net_data index FITS files.
416 Defaults to the exposure.getFilter() value.
418 'bbox', bounding box of exposure; defaults to exposure.getBBox()
421 assert(exposure
is not None)
423 margs = kwargs.copy()
424 if not 'searchRadius' in margs:
425 margs.update(searchRadius = self.config.raDecSearchRadius * afwGeom.degrees)
426 if not 'usePixelScale' in margs:
427 margs.update(usePixelScale = self.config.useWcsPixelScale)
428 if not 'useRaDecCenter' in margs:
429 margs.update(useRaDecCenter = self.config.useWcsRaDecCenter)
430 if not 'useParity' in margs:
431 margs.update(useParity = self.config.useWcsParity)
432 margs.update(exposure=exposure)
436 """Get a blind astrometric solution for the given catalog of sources.
442 And if available, we can use:
443 -an initial Wcs estimate;
448 (all of which are metadata of Exposure).
451 imageSize: (W,H) integer tuple/iterable
452 pixelScale: afwGeom::Angle per pixel.
453 radecCenter: afwCoord::Coord
458 for key
in [
'exposure',
'bbox',
'filterName']:
460 kw[key] = kwargs[key]
461 astrom = self.
useKnownWcs(sourceCat, wcs=wcs, **kw)
478 searchRadiusScale=2.):
479 if not useRaDecCenter
and radecCenter
is not None:
480 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
481 if not usePixelScale
and pixelScale
is not None:
482 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
488 filterName = filterName,
493 xc, yc = bboxD.getCenter()
497 if pixelScale
is None:
499 pixelScale = wcs.pixelScale()
500 self.log.logdebug(
'Setting pixel scale estimate = %.3f from given WCS estimate' %
501 (pixelScale.asArcseconds()))
503 if radecCenter
is None:
505 radecCenter = wcs.pixelToSky(xc, yc)
506 self.log.logdebug((
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS '
507 +
'estimate, using pixel center = (%.1f, %.1f)') %
508 (radecCenter.getLongitude().asDegrees(),
509 radecCenter.getLatitude().asDegrees(), xc, yc))
511 if searchRadius
is None:
513 assert(pixelScale
is not None)
514 pixRadius = math.hypot(*bboxD.getDimensions()) / 2
515 searchRadius = (pixelScale * pixRadius * searchRadiusScale)
516 self.log.logdebug((
'Using RA,Dec search radius = %.3f deg, from pixel scale, '
517 +
'image size, and searchRadiusScale = %g') %
518 (searchRadius, searchRadiusScale))
520 parity = wcs.isFlipped()
521 self.log.logdebug(
'Using parity = %s' % (parity
and 'True' or 'False'))
525 if exposure
is not None:
526 exposureBBoxD =
afwGeom.Box2D(exposure.getMaskedImage().getBBox())
528 exposureBBoxD = bboxD
530 self.log.logdebug(
"Trimming: kept %i of %i sources" % (n, len(sourceCat)))
533 sourceCat = sourceCat,
536 pixelScale = pixelScale,
537 radecCenter = radecCenter,
538 searchRadius = searchRadius,
540 filterName = filterName,
543 raise RuntimeError(
"Unable to match sources with catalog.")
544 self.log.info(
'Got astrometric solution from Astrometry.net')
546 rdc = wcs.pixelToSky(xc, yc)
547 self.log.logdebug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)' %
548 (xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees()))
552 """!Get a TAN-SIP WCS, starting from an existing WCS.
554 It uses your WCS to compute a fake grid of corresponding "stars" in pixel and sky coords,
555 and feeds that to the regular SIP code.
557 @param[in] wcs initial WCS
558 @param[in] bbox bounding box of image
559 @param[in] ngrid number of grid points along x and y for fitting (fit at ngrid^2 points)
560 @param[in] linearizeAtCenter if True, get a linear approximation of the input
561 WCS at the image center and use that as the TAN initialization for
562 the TAN-SIP solution. You probably want this if your WCS has its
563 CRPIX outside the image bounding box.
566 srcSchema = afwTable.SourceTable.makeMinimalSchema()
567 key = srcSchema.addField(
"centroid", type=
"PointD")
568 srcTable = afwTable.SourceTable.make(srcSchema)
569 srcTable.defineCentroid(
"centroid")
571 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
574 (W,H) = bbox.getDimensions()
575 x0, y0 = bbox.getMin()
576 for xx
in np.linspace(0., W, ngrid):
577 for yy
in np.linspace(0, H, ngrid):
578 src = srcs.makeRecord()
579 src.set(key.getX(), x0 + xx)
580 src.set(key.getY(), y0 + yy)
583 ref = refs.makeRecord()
587 if linearizeAtCenter:
592 crval = wcs.pixelToSky(crpix)
593 crval = crval.getPosition(afwGeom.degrees)
596 aff = wcs.linearizePixelToSky(crval)
597 cd = aff.getLinear().getMatrix()
605 """Produce a SIP solution given a list of known correspondences.
607 Unlike _calculateSipTerms, this does not iterate the solution;
608 it assumes you have given it a good sets of corresponding stars.
610 NOTE that "refCat" and "sourceCat" are assumed to be the same length;
611 entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences.
613 @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS.
614 @param[in] refCat reference source catalog
615 @param[in] sourceCat source catalog
616 @param[in] bbox bounding box of image
618 sipOrder = self.config.sipOrder
620 for ci,si
in zip(refCat, sourceCat):
623 sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox)
624 return sipObject.getNewWcs()
627 """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS.
629 @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS;
630 this is used to set the baseline when determining whether a SIP
631 solution is any better; it will be returned if no better SIP solution
633 @param[in] refCat reference source catalog
634 @param[in] sourceCat sources in the image to be solved
635 @param[in] matches list of supposedly matched sources, using the "origWcs".
636 @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients.
638 sipOrder = self.config.sipOrder
641 lastMatchSize = len(matches)
643 for i
in range(self.config.maxIter):
646 sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox)
647 proposedWcs = sipObject.getNewWcs()
648 self.
plotSolution(matches, proposedWcs, bbox.getDimensions())
650 self.log.warn(
'Failed to calculate distortion terms. Error: ' + str(e))
654 proposedMatchlist = self.
_getMatchList(sourceCat, refCat, proposedWcs)
655 proposedMatchSize = len(proposedMatchlist)
659 "SIP iteration %i: %i objects match, previous = %i;" %
660 (i, proposedMatchSize, lastMatchSize) +
661 " clipped mean scatter = %s arcsec, previous = %s; " %
662 (proposedMatchStats.distMean.asArcseconds(), lastMatchStats.distMean.asArcseconds()) +
663 " max match dist = %s arcsec, previous = %s" %
664 (proposedMatchStats.maxMatchDist.asArcseconds(),
665 lastMatchStats.maxMatchDist.asArcseconds())
668 if lastMatchStats.maxMatchDist <= proposedMatchStats.maxMatchDist:
670 "Fit WCS: use iter %s because max match distance no better in next iter: " % (i-1,) +
671 " %g < %g arcsec" % (lastMatchStats.maxMatchDist.asArcseconds(),
672 proposedMatchStats.maxMatchDist.asArcseconds()))
677 matches = proposedMatchlist
678 lastMatchSize = proposedMatchSize
679 lastMatchStats = proposedMatchStats
684 """Plot the solution, when debugging is turned on.
686 @param matches The list of matches
688 @param imageSize 2-tuple with the image size (W,H)
696 import matplotlib.pyplot
as plt
699 print >> sys.stderr,
"Unable to import matplotlib"
705 fig.canvas._tkcanvas._root().lift()
712 dx = numpy.zeros(num)
713 dy = numpy.zeros(num)
714 for i, m
in enumerate(matches):
715 x[i] = m.second.getX()
716 y[i] = m.second.getY()
717 pixel = wcs.skyToPixel(m.first.getCoord())
718 dx[i] = x[i] - pixel.getX()
719 dy[i] = y[i] - pixel.getY()
721 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
723 def plotNext(x, y, xLabel, yLabel, xMax):
725 ax.set_autoscalex_on(
False)
726 ax.set_xbound(lower=0, upper=xMax)
728 ax.set_xlabel(xLabel)
729 ax.set_ylabel(yLabel)
732 plotNext(x, dx,
"x",
"dx", imageSize[0])
733 plotNext(x, dy,
"x",
"dy", imageSize[0])
734 plotNext(y, dx,
"y",
"dx", imageSize[1])
735 plotNext(y, dy,
"y",
"dy", imageSize[1])
741 reply = raw_input(
"Pausing for inspection, enter to continue... [hpQ] ").strip()
745 reply = reply.split()
751 if reply
in (
"",
"h",
"p",
"Q"):
753 print "h[elp] p[db] Q[uit]"
756 import pdb; pdb.set_trace()
762 """Compute on-sky radial distance statistics for a match list
764 @param[in] wcs WCS for match list; an lsst.afw.image.Wcs
765 @param[in] matchList list of matches between reference object and sources;
766 a list of lsst.afw.table.ReferenceMatch;
767 the source centroid and reference object coord are read
769 @return a pipe_base Struct containing these fields:
770 - distMean clipped mean of on-sky radial separation
771 - distStdDev clipped standard deviation of on-sky radial separation
772 - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev
775 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*afwGeom.radians
776 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*afwGeom.radians
777 return pipeBase.Struct(
779 distStdDev = distStdDev,
780 maxMatchDist = distMean + self.config.matchDistanceSigma*distStdDev,
784 dist = self.config.catalogMatchDist * afwGeom.arcseconds
785 clean = self.config.cleaningParameter
786 matcher = astromSip.MatchSrcToCatalogue(refCat, sourceCat, wcs, dist)
787 matches = matcher.getMatches()
790 X = [src.getX()
for src
in sourceCat]
791 Y = [src.getY()
for src
in sourceCat]
792 R1 = [src.getRa().asDegrees()
for src
in sourceCat]
793 D1 = [src.getDec().asDegrees()
for src
in sourceCat]
794 R2 = [src.getRa().asDegrees()
for src
in refCat]
795 D2 = [src.getDec().asDegrees()
for src
in refCat]
802 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sourceCat), len(refCat)))
805 'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
806 (min(X), max(X), min(Y), max(Y), min(R1), max(R1), min(D1), max(D1)))
808 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
809 (min(R2), max(R2), min(D2), max(D2)))
810 raise RuntimeError(
'No matches found between image and catalogue')
811 matches = astromSip.cleanBadPoints.clean(matches, wcs, nsigma=clean)
816 Returns the column name in the astrometry_net_data index file that will be used
817 for the given filter name.
819 @param filterName Name of filter used in exposure
820 @param columnMap Dict that maps filter names to column names
821 @param default Default column name
823 filterName = self.config.filterMap.get(filterName, filterName)
825 return columnMap[filterName]
827 self.log.warn(
"No column in configuration for filter '%s'; using default '%s'" %
828 (filterName, default))
831 def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter,
832 searchRadius, parity, filterName=
None):
833 solver = self.refObjLoader._getSolver()
835 imageSize = bbox.getDimensions()
836 x0, y0 = bbox.getMin()
841 badkeys = [goodsources.getSchema().find(name).key
for name
in self.config.badFlags]
844 if np.isfinite(s.getX())
and np.isfinite(s.getY())
and np.isfinite(s.getPsfFlux()) \
846 goodsources.append(s)
848 self.log.info(
"Number of selected sources for astrometry : %d" %(len(goodsources)))
849 if len(goodsources) < len(sourceCat):
850 self.log.logdebug(
'Keeping %i of %i sources with finite X,Y positions and PSF flux' %
851 (len(goodsources), len(sourceCat)))
852 self.log.logdebug((
'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' +
853 '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net') %
854 (xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0))
856 solver.setStars(goodsources, x0, y0)
857 solver.setMaxStars(self.config.maxStars)
858 solver.setImageSize(*imageSize)
859 solver.setMatchThreshold(self.config.matchThreshold)
861 if radecCenter
is not None:
862 raDecRadius = (radecCenter.getLongitude().asDegrees(),
863 radecCenter.getLatitude().asDegrees(),
864 searchRadius.asDegrees())
865 solver.setRaDecRadius(*raDecRadius)
866 self.log.logdebug(
'Searching for match around RA,Dec = (%g, %g) with radius %g deg' %
869 if pixelScale
is not None:
870 dscale = self.config.pixelScaleUncertainty
871 scale = pixelScale.asArcseconds()
874 solver.setPixelScaleRange(lo, hi)
876 'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix' %
877 (scale, 100.*(dscale-1.), lo, hi))
879 if parity
is not None:
880 solver.setParity(parity)
881 self.log.logdebug(
'Searching for match with parity = ' + str(parity))
884 if radecCenter
is not None:
885 multiInds = self.refObjLoader._getMIndexesWithinRange(radecCenter, searchRadius)
887 multiInds = self.refObjLoader.multiInds
888 qlo,qhi = solver.getQuadSizeLow(), solver.getQuadSizeHigh()
890 toload_multiInds = set()
893 for i
in range(len(mi)):
895 if not ind.overlapsScaleRange(qlo, qhi):
897 toload_multiInds.add(mi)
898 toload_inds.append(ind)
903 with LoadMultiIndexes(toload_multiInds):
904 displayAstrometry(refCat=self.refObjLoader.loadPixelBox(bbox, wcs, filterName).refCat,
907 with LoadMultiIndexes(toload_multiInds):
908 solver.addIndices(toload_inds)
909 self.
memusage(
'Index files loaded: ')
911 cpulimit = self.config.maxCpuTime
916 self.
memusage(
'Index files unloaded: ')
918 if solver.didSolve():
919 self.log.logdebug(
'Solved!')
920 wcs = solver.getWcs()
921 self.log.logdebug(
'WCS: %s' % wcs.getFitsMetadata().toString())
923 if x0 != 0
or y0 != 0:
924 wcs.shiftReferencePixel(x0, y0)
925 self.log.logdebug(
'After shifting reference pixel by x0,y0 = (%i,%i), WCS is: %s' %
926 (x0, y0, wcs.getFitsMetadata().toString()))
929 self.log.warn(
'Did not get an astrometric solution from Astrometry.net')
935 if radecCenter
is not None:
936 self.refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName)
938 qa = solver.getSolveStats()
939 self.log.logdebug(
'qa: %s' % qa.toString())
944 if candsource.get(k):
950 """Remove elements from catalog whose xy positions are not within the given bbox.
952 sourceCat: a Catalog of SimpleRecord or SourceRecord objects
953 bbox: an afwImage.Box2D
954 wcs: if not None, will be used to compute the xy positions on-the-fly;
955 this is required when sources actually contains SimpleRecords.
958 a list of Source objects with xAstrom,yAstrom within the bbox.
960 keep = type(sourceCat)(sourceCat.table)
962 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
963 if bbox.contains(point):
969 This function is required to reconstitute a ReferenceMatchVector after being
970 unpersisted. The persisted form of a ReferenceMatchVector is the
971 normalized Catalog of IDs produced by afw.table.packMatches(), with the result of
972 InitialAstrometry.getMatchMetadata() in the associated tables\' metadata.
974 The "live" form of a matchlist has links to
975 the real record objects that are matched; it is "denormalized".
976 This function takes a normalized match catalog, along with the catalog of
977 sources to which the match catalog refers. It fetches the reference
978 sources that are within range, and then denormalizes the matches
979 -- sets the "matches[*].first" and "matches[*].second" entries
980 to point to the sources in the "sourceCat" argument, and to the
981 reference sources fetched from the astrometry_net_data files.
983 @param[in] packedMatches Unpersisted match list (an lsst.afw.table.BaseCatalog).
984 packedMatches.table.getMetadata() must contain the
985 values from InitialAstrometry.getMatchMetadata()
986 @param[in,out] sourceCat Source catalog used for the 'second' side of the matches
987 (an lsst.afw.table.SourceCatalog). As a side effect,
988 the catalog will be sorted by ID.
990 @return An lsst.afw.table.ReferenceMatchVector of denormalized matches.
992 matchmeta = packedMatches.table.getMetadata()
993 version = matchmeta.getInt(
'SMATCHV')
995 raise ValueError(
'SourceMatchVector version number is %i, not 1.' % version)
996 filterName = matchmeta.getString(
'FILTER').strip()
998 matchmeta.getDouble(
'RA') * afwGeom.degrees,
999 matchmeta.getDouble(
'DEC') * afwGeom.degrees,
1001 rad = matchmeta.getDouble(
'RADIUS') * afwGeom.degrees
1002 refCat = self.refObjLoader.loadSkyCircle(ctrCoord, rad, filterName).refCat
1010 Create match metadata entries required for regenerating the catalog
1012 @param bbox bounding box of image (pixels)
1013 @param filterName Name of filter, used for magnitudes
1019 cx, cy = bboxD.getCenter()
1020 radec = wcs.pixelToSky(cx, cy).toIcrs()
1021 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
1022 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
1023 pixelRadius = math.hypot(*bboxD.getDimensions())/2.0
1024 skyRadius = wcs.pixelScale() * pixelRadius
1025 meta.add(
'RADIUS', skyRadius.asDegrees(),
1026 'field radius in degrees, approximate')
1027 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1028 if filterName
is not None:
1029 meta.add(
'FILTER', filterName,
'LSST filter name for tagalong data')
Basic implemeentation of the astrometry.net astrometrical fitter.
def _computeMatchStatsOnSky
Class for storing ordered metadata with comments.
def getSipWcs
"Not very pythonic!" complains Paul.
Implementation of the WCS standard for a any projection.
def useKnownWcs
Return an InitialAstrometry object, just like determineWcs, but assuming the given input WCS is corre...
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Lightweight representation of a geometric match between two records.
def _calculateSipTerms
Iteratively calculate SIP distortions and regenerate matches based on improved WCS.
def getSipWcsFromCorrespondences
def getSipWcsFromWcs
Get a TAN-SIP WCS, starting from an existing WCS.
afw::math::Statistics makeMatchStatisticsInRadians(afw::image::Wcs const &wcs, std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
def joinMatchListWithCatalog
A floating-point coordinate rectangle geometry.
def __init__
Construct an ANetBasicAstrometryTask.
A class to handle Icrs coordinates (inherits from Coord)
std::vector< Match< typename Cat1::Record, typename Cat2::Record > > unpackMatches(BaseCatalog const &matches, Cat1 const &cat1, Cat2 const &cat2)
Reconstruct a MatchVector from a BaseCatalog representation of the matches and a pair of catalogs...