15 from .config
import MeasAstromConfig, AstrometryNetDataConfig
16 import sip
as astromSip
21 getWcs(): sipWcs or tanWcs
22 getMatches(): sipMatches or tanMatches
25 solveQa (PropertyList)
27 tanMatches (MatchList)
29 sipMatches (MatchList)
30 matchMetadata (PropertyList)
42 Get "sipMatches" -- MatchList using the SIP WCS solution, if it
43 exists, or "tanMatches" -- MatchList using the TAN WCS solution
50 Returns the SIP WCS, if one was found, or a TAN WCS
54 matches = property(getMatches)
55 wcs = property(getWcs)
75 ConfigClass = MeasAstromConfig
78 About Astrometry.net index files (astrometry_net_data):
80 There are three components of an index file: a list of stars
81 (stored as a star kd-tree), a list of quadrangles of stars ("quad
82 file") and a list of the shapes ("codes") of those quadrangles,
83 stored as a code kd-tree.
85 Each index covers a region of the sky, defined by healpix nside
86 and number, and a range of angular scales. In LSST, we share the
87 list of stars in a part of the sky between multiple indexes. That
88 is, the star kd-tree is shared between multiple indices (quads and
89 code kd-trees). In the astrometry.net code, this is called a
92 It is possible to "unload" and "reload" multiindex (and index)
93 objects. When "unloaded", they consume no FILE or mmap resources.
95 The multiindex object holds the star kd-tree and gives each index
96 object it holds a pointer to it, so it is necessary to
97 multiindex_reload_starkd() before reloading the indices it holds.
98 The multiindex_unload() method, on the other hand, unloads its
99 starkd and unloads each index it holds.
119 logLevel=pexLog.Log.INFO):
121 conf: an AstromConfig object.
122 andConfig: an AstromNetDataConfig object
123 log: a pexLogging.Log
124 logLevel: if log is None, the log level to use
133 if andConfig
is None:
135 dirnm = os.environ.get(
'ASTROMETRY_NET_DATA_DIR')
137 raise RuntimeError(
"astrometry_net_data is not setup")
139 andConfig = AstrometryNetDataConfig()
140 fn = os.path.join(dirnm,
'andConfig.py')
141 if not os.path.exists(fn):
142 raise RuntimeError(
'astrometry_net_data config file \"%s\" required but not found' % fn)
149 import astrometry_net
as an
155 mifiles = ([(
True,[fn,fn])
for fn
in self.andConfig.indexFiles] +
156 [(
False,fns)
for fns
in self.andConfig.multiIndexFiles])
159 for single,fns
in mifiles:
163 self.log.log(self.log.DEBUG,
'Adding index file %s' % fns[0])
165 self.log.log(self.log.DEBUG,
'Adding multiindex files %s' % str(fns))
169 self.log.logdebug(
'Unable to find index file %s' % fn)
171 self.log.logdebug(
'Unable to find star part of multiindex file %s' % fn)
175 self.log.log(self.log.DEBUG,
'Path: %s' % fn)
177 mi = an.multiindex_new(fn)
179 raise RuntimeError(
'Failed to read stars from multiindex filename "%s"' % fn)
180 for i,fn
in enumerate(fns[1:]):
181 self.log.log(self.log.DEBUG,
'Reading index from multiindex file "%s"' % fn)
184 self.log.logdebug(
'Unable to find index part of multiindex file %s' % fn)
188 self.log.log(self.log.DEBUG,
'Path: %s' % fn)
189 if an.multiindex_add_index(mi, fn, an.INDEX_ONLY_LOAD_METADATA):
190 raise RuntimeError(
'Failed to read index from multiindex filename "%s"' % fn)
192 self.log.log(self.log.DEBUG,
' index %i, hp %i (nside %i), nstars %i, nquads %i' %
193 (ind.indexid, ind.healpix, ind.hpnside, ind.nstars, ind.nquads))
194 an.multiindex_unload_starkd(mi)
195 self.multiInds.append(mi)
198 self.log.warn(
'Unable to find any index files')
200 self.log.warn(
'Unable to find %d index files' % nMissing)
203 self.log.log(self.log.DEBUG, s)
205 self.log.log(self.log.WARN, s)
209 if self.log.getThreshold() > pexLog.Log.DEBUG:
211 from astrometry.util.ttime
import get_memusage
214 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
216 ss.append(key +
': ' +
' '.join(mu[key]))
218 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
219 if 'mmaps_total' in mu:
220 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
221 self.log.logdebug(prefix +
'Memory: ' +
', '.join(ss))
226 def _getImageParams(self, wcs, exposure, filterName=None, imageSize=None,
228 if exposure
is not None:
229 ex0,ey0 = exposure.getX0(), exposure.getY0()
234 self.
_debug(
'Got exposure x0,y0 = %i,%i' % (ex0,ey0))
235 if filterName
is None:
236 filterName = exposure.getFilter().getName()
237 self.
_debug(
'Setting filterName = "%s" from exposure metadata' % str(filterName))
238 if imageSize
is None:
239 imageSize = (exposure.getWidth(), exposure.getHeight())
240 self.
_debug(
'Setting image size = (%i, %i) from exposure metadata' % (imageSize))
245 return filterName, imageSize, x0, y0
247 def useKnownWcs(self, sources, wcs=None, exposure=None, filterName=None, imageSize=None, x0=None, y0=None):
249 Returns an InitialAstrometry object, just like determineWcs,
250 but assuming the given input WCS is correct.
252 This is enabled by the pipe_tasks AstrometryConfig
253 'forceKnownWcs' option. If you are using that option, you
254 probably also want to turn OFF 'calculateSip'.
256 This involves searching for reference sources within the WCS
257 area, and matching them to the given 'sources'. If
258 'calculateSip' is set, we will try to compute a TAN-SIP
259 distortion correction.
261 sources: list of detected sources in this image.
263 exposure: the exposure holding metadata for this image.
264 filterName: string, filter name, eg "i"
265 x0,y0: image origin / offset; these coordinates along with the
266 "imageSize" determine the bounding-box in pixel coordinates of
267 the image in question; this is used for finding reference sources
268 in the image, among other things.
270 You MUST provide a WCS, either by providing the 'wcs' kwarg
271 (an lsst.image.Wcs object), or by providing the 'exposure' on
272 which we will call 'exposure.getWcs()'.
274 You MUST provide a filter name, either by providing the
275 'filterName' kwarg (a string), or by setting the 'exposure';
276 we will call 'exposure.getFilter().getName()'.
278 You MUST provide the image size, either by providing the
279 'imageSize' kwargs, an (W,H) tuple of ints, or by providing
280 the 'exposure'; we will call 'exposure.getWidth()' and
281 'exposure.getHeight()'.
283 Note, when modifying this function, that it is also called by
284 'determineWcs' (via 'determineWcs2'), since the steps are all
292 raise RuntimeError(
'useKnownWcs: need either "wcs=" or "exposure=" kwarg.')
293 wcs = exposure.getWcs()
295 raise RuntimeError(
'useKnownWcs: wcs==None and exposure.getWcs()==None.')
297 filterName,imageSize,x0,y0 = self.
_getImageParams(exposure=exposure, wcs=wcs,
299 filterName=filterName,
304 catids = [src.getId()
for src
in cat]
306 self.log.logdebug(
'%i reference sources; %i unique IDs' % (len(catids), len(uids)))
308 uniq = set([sm.second.getId()
for sm
in matchList])
309 if len(matchList) != len(uniq):
310 self.
_warn((
'The list of matched stars contains duplicate reference source IDs ' +
311 '(%i sources, %i unique ids)') % (len(matchList), len(uniq)))
312 if len(matchList) == 0:
313 self.
_warn(
'No matches found between input sources and reference catalogue.')
316 self.
_debug(
'%i reference objects match input sources using input WCS' % (len(matchList)))
317 astrom.tanMatches = matchList
320 srcids = [s.getId()
for s
in sources]
322 assert(m.second.getId()
in srcids)
323 assert(m.second
in sources)
325 if self.config.calculateSip:
326 sipwcs,matchList = self.
_calculateSipTerms(wcs, cat, sources, matchList, imageSize, x0=x0, y0=y0)
328 self.
_debug(
'Failed to find a SIP WCS better than the initial one.')
330 self.
_debug(
'%i reference objects match input sources using SIP WCS' % (len(matchList)))
331 astrom.sipWcs = sipwcs
332 astrom.sipMatches = matchList
335 wcs = astrom.getWcs()
348 Finds a WCS solution for the given 'sources' in the given
349 'exposure', getting other parameters from config.
351 Valid kwargs include:
353 'radecCenter', an afw.coord.Coord giving the RA,Dec position
354 of the center of the field. This is used to limit the
355 search done by Astrometry.net (to make it faster and load
356 fewer index files, thereby using less memory). Defaults to
357 the RA,Dec center from the exposure's WCS; turn that off
358 with the boolean kwarg 'useRaDecCenter' or config option
361 'useRaDecCenter', a boolean. Don't use the RA,Dec center from
362 the exposure's initial WCS.
364 'searchRadius', in degrees, to search for a solution around
365 the given 'radecCenter'; default from config option
368 'useParity': parity is the 'flip' of the image. Knowing it
369 reduces the search space (hence time) for Astrometry.net.
370 The parity can be computed from the exposure's WCS (the
371 sign of the determinant of the CD matrix); this option
372 controls whether we do that or force Astrometry.net to
373 search both parities. Default from config.useWcsParity.
375 'pixelScale': afwGeom.Angle, estimate of the angle-per-pixel
376 (ie, arcseconds per pixel). Defaults to a value derived
377 from the exposure's WCS. If enabled, this value, plus or
378 minus config.pixelScaleUncertainty, will be used to limit
379 Astrometry.net's search.
381 'usePixelScale': boolean. Use the pixel scale to limit
382 Astrometry.net's search? Defaults to config.useWcsPixelScale.
384 'filterName', a string, the filter name of this image. Will
385 be mapped through the 'filterMap' config dictionary to a
386 column name in the astrometry_net_data index FITS files.
387 Defaults to the exposure.getFilter() value.
389 'imageSize', a tuple (W,H) of integers, the image size.
390 Defaults to the exposure.get{Width,Height}() values.
393 assert(exposure
is not None)
395 margs = kwargs.copy()
396 if not 'searchRadius' in margs:
397 margs.update(searchRadius = self.config.raDecSearchRadius * afwGeom.degrees)
398 if not 'usePixelScale' in margs:
399 margs.update(usePixelScale = self.config.useWcsPixelScale)
400 if not 'useRaDecCenter' in margs:
401 margs.update(useRaDecCenter = self.config.useWcsRaDecCenter)
402 if not 'useParity' in margs:
403 margs.update(useParity = self.config.useWcsParity)
404 margs.update(exposure=exposure)
409 Get a blind astrometric solution for the given list of sources.
415 And if available, we can use:
416 -an initial Wcs estimate;
421 (all of which are metadata of Exposure).
424 imageSize: (W,H) integer tuple/iterable
425 pixelScale: afwGeom::Angle per pixel.
426 radecCenter: afwCoord::Coord
431 for key
in [
'exposure',
'filterName',
'imageSize',
'x0',
'y0']:
433 kw[key] = kwargs[key]
452 searchRadiusScale=2.):
453 if not useRaDecCenter
and radecCenter
is not None:
454 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
455 if not usePixelScale
and pixelScale
is not None:
456 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
458 filterName,imageSize,x0,y0 = self.
_getImageParams(exposure=exposure, wcs=wcs,
460 filterName=filterName,
463 if exposure
is not None:
465 wcs = exposure.getWcs()
466 self.
_debug(
'Setting initial WCS estimate from exposure metadata')
468 if imageSize
is None:
470 raise RuntimeError(
'Image size must be specified by passing "exposure" or "imageSize"')
473 xc, yc = W/2. + 0.5 + x0, H/2. + 0.5 + y0
477 if pixelScale
is None:
479 pixelScale = wcs.pixelScale()
480 self.
_debug(
'Setting pixel scale estimate = %.3f from given WCS estimate' %
481 (pixelScale.asArcseconds()))
483 if radecCenter
is None:
485 radecCenter = wcs.pixelToSky(xc, yc)
486 self.
_debug((
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS '
487 +
'estimate, using pixel center = (%.1f, %.1f)') %
488 (radecCenter.getLongitude().asDegrees(),
489 radecCenter.getLatitude().asDegrees(), xc, yc))
491 if searchRadius
is None:
493 assert(pixelScale
is not None)
494 searchRadius = (pixelScale * math.hypot(W,H)/2. *
496 self.
_debug((
'Using RA,Dec search radius = %.3f deg, from pixel scale, '
497 +
'image size, and searchRadiusScale = %g') %
498 (searchRadius, searchRadiusScale))
500 parity = wcs.isFlipped()
501 self.
_debug(
'Using parity = %s' % (parity
and 'True' or 'False'))
505 if exposure
is not None:
511 self.
_debug(
"Trimming: kept %i of %i sources" % (n, len(sources)))
513 wcs,qa = self.
_solve(sources, wcs, imageSize, pixelScale, radecCenter, searchRadius, parity,
514 filterName, xy0=(x0,y0))
516 raise RuntimeError(
"Unable to match sources with catalog.")
517 self.log.info(
'Got astrometric solution from Astrometry.net')
519 rdc = wcs.pixelToSky(xc, yc)
520 self.
_debug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)' %
521 (xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees()))
525 linearizeAtCenter=
True):
527 This function allows one to get a TAN-SIP WCS, starting from
528 an existing WCS. It uses your WCS to compute a fake grid of
529 corresponding "stars" in pixel and sky coords, and feeds that
530 to the regular SIP code.
532 linearizeCenter: if True, get a linear approximation of the input
533 WCS at the image center and use that as the TAN initialization for
534 the TAN-SIP solution. You probably want this if your WCS has its
535 CRPIX outside the image bounding box.
539 srcSchema = afwTable.SourceTable.makeMinimalSchema()
540 key = srcSchema.addField(
"centroid", type=
"PointD")
541 srcTable = afwTable.SourceTable.make(srcSchema)
542 srcTable.defineCentroid(
"centroid")
544 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
548 for xx
in np.linspace(0., W, ngrid):
549 for yy
in np.linspace(0, H, ngrid):
550 src = srcs.makeRecord()
551 src.set(key.getX(), x0 + xx)
552 src.set(key.getY(), y0 + yy)
555 ref = refs.makeRecord()
559 if linearizeAtCenter:
564 crval = wcs.pixelToSky(crpix)
565 crval = crval.getPosition(afwGeom.degrees)
568 aff = wcs.linearizePixelToSky(crval)
569 cd = aff.getLinear().getMatrix()
579 Produces a SIP solution given a list of known correspondences.
580 Unlike _calculateSipTerms, this does not iterate the solution;
581 it assumes you have given it a good sets of corresponding stars.
583 NOTE that "cat" and "sources" are assumed to be the same length;
584 entries "cat[i]" and "sources[i]" are assumed to be correspondences.
586 origWcs: the WCS to linearize in order to get the TAN part of the
589 cat: reference source catalog
591 sources: image sources
593 imageSize, x0, y0: these describe the bounding-box of the image,
594 which is used when computing reverse SIP polynomials.
597 sipOrder = self.config.sipOrder
601 for ci,si
in zip(cat, sources):
604 sipObject = astromSip.makeCreateWcsWithSip(matchList, origWcs, sipOrder, bbox)
605 return sipObject.getNewWcs()
610 Iteratively calculate SIP distortions and regenerate matchList based on improved WCS.
612 origWcs: original WCS object, probably (but not necessarily) a TAN WCS;
613 this is used to set the baseline when determining whether a SIP
614 solution is any better; it will be returned if no better SIP solution
617 matchList: list of supposedly matched sources, using the "origWcs".
619 cat: reference source catalog
621 sources: sources in the image to be solved
623 imageSize, x0, y0: these determine the bounding-box of the image,
624 which is used when finding reverse SIP coefficients.
626 sipOrder = self.config.sipOrder
635 sipObject = astromSip.makeCreateWcsWithSip(matchList, wcs, sipOrder, bbox)
636 if lastScatPix
is None:
637 lastScatPix = sipObject.getLinearScatterInPixels()
638 proposedWcs = sipObject.getNewWcs()
639 scatPix = sipObject.getScatterInPixels()
642 self.
_warn(
'Failed to calculate distortion terms. Error: ' + str(e))
645 matchSize = len(matchList)
647 proposedMatchlist = self.
_getMatchList(sources, cat, proposedWcs)
649 self.
_debug(
'SIP iteration %i: %i objects match. Median scatter is %g arcsec = %g pixels (vs previous: %i matches, %g pixels)' %
650 (i, len(proposedMatchlist), sipObject.getScatterOnSky().asArcseconds(), scatPix, matchSize, lastScatPix))
653 if len(proposedMatchlist) < matchSize:
655 if len(proposedMatchlist) == matchSize
and scatPix >= lastScatPix:
659 matchList = proposedMatchlist
660 lastScatPix = scatPix
661 matchSize = len(matchList)
664 return wcs, matchList
667 """Plot the solution, when debugging is turned on.
669 @param matchList The list of matches
671 @param imageSize 2-tuple with the image size (W,H)
679 import matplotlib.pyplot
as plt
682 print >> sys.stderr,
"Unable to import matplotlib"
688 fig.canvas._tkcanvas._root().lift()
695 dx = numpy.zeros(num)
696 dy = numpy.zeros(num)
697 for i, m
in enumerate(matchList):
698 x[i] = m.second.getX()
699 y[i] = m.second.getY()
700 pixel = wcs.skyToPixel(m.first.getCoord())
701 dx[i] = x[i] - pixel.getX()
702 dy[i] = y[i] - pixel.getY()
704 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
706 def plotNext(x, y, xLabel, yLabel, xMax):
708 ax.set_autoscalex_on(
False)
709 ax.set_xbound(lower=0, upper=xMax)
711 ax.set_xlabel(xLabel)
712 ax.set_ylabel(yLabel)
715 plotNext(x, dx,
"x",
"dx", imageSize[0])
716 plotNext(x, dy,
"x",
"dy", imageSize[0])
717 plotNext(y, dx,
"y",
"dx", imageSize[1])
718 plotNext(y, dy,
"y",
"dy", imageSize[1])
724 reply = raw_input(
"Pausing for inspection, enter to continue... [hpQ] ").strip()
728 reply = reply.split()
734 if reply
in (
"",
"h",
"p",
"Q"):
736 print "h[elp] p[db] Q[uit]"
739 import pdb; pdb.set_trace()
745 dist = self.config.catalogMatchDist * afwGeom.arcseconds
746 clean = self.config.cleaningParameter
747 matcher = astromSip.MatchSrcToCatalogue(cat, sources, wcs, dist)
748 matchList = matcher.getMatches()
749 if matchList
is None:
751 X = [src.getX()
for src
in sources]
752 Y = [src.getY()
for src
in sources]
753 R1 = [src.getRa().asDegrees()
for src
in sources]
754 D1 = [src.getDec().asDegrees()
for src
in sources]
755 R2 = [src.getRa().asDegrees()
for src
in cat]
756 D2 = [src.getDec().asDegrees()
for src
in cat]
763 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sources), len(cat)))
765 self.loginfo(
'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
768 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
770 raise RuntimeError(
'No matches found between image and catalogue')
771 matchList = astromSip.cleanBadPoints.clean(matchList, wcs, nsigma=clean)
776 Returns the column name in the astrometry_net_data index file that will be used
777 for the given filter name.
779 @param filterName Name of filter used in exposure
780 @param columnMap Dict that maps filter names to column names
781 @param default Default column name
783 filterName = self.config.filterMap.get(filterName, filterName)
785 return columnMap[filterName]
787 self.log.warn(
"No column in configuration for filter '%s'; using default '%s'" %
788 (filterName, default))
792 """Deprecated method for retrieving the magnitude column name from the filter name"""
793 return self.
getColumnName(filterName, self.andConfig.magColumnMap, self.andConfig.defaultMagColumn)
798 xc, yc = W/2. + 0.5, H/2. + 0.5
799 rdc = wcs.pixelToSky(x0 + xc, y0 + yc)
800 ra,dec = rdc.getLongitude(), rdc.getLatitude()
801 self.
_debug(
'Getting reference sources using center: pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)' %
802 (xc, yc, ra.asDegrees(), dec.asDegrees()))
803 pixelScale = wcs.pixelScale()
804 rad = pixelScale * (math.hypot(W,H)/2. + pixelMargin)
805 self.
_debug(
'Getting reference sources using radius of %.3g deg' % rad.asDegrees())
811 bbox.grow(pixelMargin)
818 Searches for reference-catalog sources (in the
819 astrometry_net_data files) in the requested RA,Dec region
820 (afwGeom::Angle objects), with the requested radius (also an
821 Angle). The flux values will be set based on the requested
822 filter (None => default filter).
824 Returns: an lsst.afw.table.SimpleCatalog of reference objects
826 sgCol = self.andConfig.starGalaxyColumn
827 varCol = self.andConfig.variableColumn
828 idcolumn = self.andConfig.idColumn
830 magCol = self.
getColumnName(filterName, self.andConfig.magColumnMap, self.andConfig.defaultMagColumn)
831 magerrCol = self.
getColumnName(filterName, self.andConfig.magErrorColumnMap,
832 self.andConfig.defaultMagErrorColumn)
834 if self.config.allFluxes:
841 ecols.append(magerrCol)
843 for col,mcol
in self.andConfig.magColumnMap.items():
846 ecols.append(self.andConfig.magErrorColumnMap.get(col,
''))
847 margs = (names, mcols, ecols)
850 margs = (magCol, magerrCol)
853 Note about multiple astrometry_net index files and duplicate IDs:
855 -as of astrometry_net 0.30, we take a reference catalog and build
856 a set of astrometry_net index files from it, with each one covering a
857 region of sky and a range of angular scales. The index files covering
858 the same region of sky at different scales use exactly the same stars.
859 Therefore, if we search every index file, we will get multiple copies of
860 each reference star (one from each index file).
861 For now, we have the "unique_ids" option to solver.getCatalog().
862 -recall that the index files to be used are specified in the
863 AstrometryNetDataConfig.indexFiles flat list.
865 -as of astrometry_net 0.40, we have the capability to share
866 the reference stars between index files (called
867 "multiindex"), so we will no longer have to repeat the
868 reference stars in each index. We will, however, have to
869 change the way the index files are configured to take
870 advantage of this functionality. Once this is in place, we
871 can eliminate the horrid ID checking and deduplication (in solver.getCatalog()).
877 raDecRadius = (ra.asDegrees(), dec.asDegrees(), radius.asDegrees())
883 inds = [mi[0]
for mi
in multiInds]
885 cat = solver.getCatalog(*((inds,) + raDecRadius + (idcolumn,)
886 + margs + (sgCol, varCol)))
890 def _solve(self, sources, wcs, imageSize, pixelScale, radecCenter,
891 searchRadius, parity, filterName=
None, xy0=
None):
901 badkeys = [goodsources.getSchema().find(name).key
for name
in self.config.badFlags]
904 if np.isfinite(s.getX())
and np.isfinite(s.getY())
and np.isfinite(s.getPsfFlux())
and self.
_isGoodSource(s, badkeys) :
905 goodsources.append(s)
907 self.log.info(
"Number of selected sources for astrometry : %d" %(len(goodsources)))
908 if len(goodsources) < len(sources):
909 self.log.logdebug(
'Keeping %i of %i sources with finite X,Y positions and PSF flux' %
910 (len(goodsources), len(sources)))
911 self.
_debug((
'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' +
912 '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net') %
913 (xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0))
915 solver.setStars(goodsources, x0, y0)
916 solver.setMaxStars(self.config.maxStars)
917 solver.setImageSize(*imageSize)
918 solver.setMatchThreshold(self.config.matchThreshold)
920 if radecCenter
is not None:
921 raDecRadius = (radecCenter.getLongitude().asDegrees(),
922 radecCenter.getLatitude().asDegrees(),
923 searchRadius.asDegrees())
924 solver.setRaDecRadius(*raDecRadius)
925 self.log.logdebug(
'Searching for match around RA,Dec = (%g, %g) with radius %g deg' %
928 if pixelScale
is not None:
929 dscale = self.config.pixelScaleUncertainty
930 scale = pixelScale.asArcseconds()
933 solver.setPixelScaleRange(lo, hi)
934 self.log.logdebug(
'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix' %
935 (scale, 100.*(dscale-1.), lo, hi))
937 if parity
is not None:
938 solver.setParity(parity)
939 self.log.logdebug(
'Searching for match with parity = ' + str(parity))
942 if raDecRadius
is not None:
946 qlo,qhi = solver.getQuadSizeLow(), solver.getQuadSizeHigh()
948 toload_multiInds = set()
951 for i
in range(len(mi)):
953 if not ind.overlapsScaleRange(qlo, qhi):
955 toload_multiInds.add(mi)
956 toload_inds.append(ind)
959 solver.addIndices(toload_inds)
960 self.
memusage(
'Index files loaded: ')
962 cpulimit = self.config.maxCpuTime
967 self.
memusage(
'Index files unloaded: ')
969 if solver.didSolve():
970 self.log.logdebug(
'Solved!')
971 wcs = solver.getWcs()
972 self.log.logdebug(
'WCS: %s' % wcs.getFitsMetadata().toString())
974 if x0 != 0
or y0 != 0:
975 wcs.shiftReferencePixel(x0, y0)
976 self.log.logdebug(
'After shifting reference pixel by x0,y0 = (%i,%i), WCS is: %s' %
977 (x0, y0, wcs.getFitsMetadata().toString()))
980 self.log.warn(
'Did not get an astrometric solution from Astrometry.net')
985 if radecCenter
is not None:
986 ra = radecCenter.getLongitude()
987 dec = radecCenter.getLatitude()
989 self.log.info(
'Searching around RA,Dec = (%g,%g) with radius %g deg yields %i reference-catalog sources' %
990 (ra.asDegrees(), dec.asDegrees(), searchRadius.asDegrees(), len(refs)))
992 qa = solver.getSolveStats()
993 self.log.logdebug(
'qa: %s' % qa.toString())
998 if candsource.get(k):
1003 if os.path.isabs(fn):
1005 andir = os.getenv(
'ASTROMETRY_NET_DATA_DIR')
1006 if andir
is not None:
1007 fn2 = os.path.join(andir, fn)
1008 if os.path.exists(fn2):
1011 if os.path.exists(fn):
1012 return os.path.abspath(fn)
1018 ra,dec,radius: [deg], spatial cut based on the healpix of the index
1020 Returns list of multiindex objects within range.
1024 if mi.isWithinRange(ra, dec, radius):
1029 import astrometry_net
as an
1030 solver = an.solver_new()
1033 solver.setPixelScaleRange(lo, hi)
1038 '''Remove elements from catalog whose xy positions are not within the given bbox.
1040 sources: a Catalog of SimpleRecord or SourceRecord objects
1041 bbox: an afwImage.Box2D
1042 wcs: if not None, will be used to compute the xy positions on-the-fly;
1043 this is required when sources actually contains SimpleRecords.
1046 a list of Source objects with xAstrom,yAstrom within the bbox.
1048 keep = type(sources)(sources.table)
1050 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
1051 if bbox.contains(point):
1057 This function is required to reconstitute a ReferenceMatchVector after being
1058 unpersisted. The persisted form of a ReferenceMatchVector is the
1059 normalized Catalog of IDs produced by afw.table.packMatches(), with the result of
1060 InitialAstrometry.getMatchMetadata() in the associated tables\' metadata.
1062 The "live" form of a matchlist has links to
1063 the real record objects that are matched; it is "denormalized".
1064 This function takes a normalized match catalog, along with the catalog of
1065 sources to which the match catalog refers. It fetches the reference
1066 sources that are within range, and then denormalizes the matches
1067 -- sets the "matchList[*].first" and "matchList[*].second" entries
1068 to point to the sources in the "sources" argument, and to the
1069 reference sources fetched from the astrometry_net_data files.
1071 @param[in] packedMatches Unpersisted match list (an lsst.afw.table.BaseCatalog).
1072 packedMatches.table.getMetadata() must contain the
1073 values from InitialAstrometry.getMatchMetadata()
1074 @param[in,out] sourceCat Source catalog used for the 'second' side of the matches
1075 (an lsst.afw.table.SourceCatalog). As a side effect,
1076 the catalog will be sorted by ID.
1078 @return An lsst.afw.table.ReferenceMatchVector of denormalized matches.
1080 matchmeta = packedMatches.table.getMetadata()
1081 version = matchmeta.getInt(
'SMATCHV')
1083 raise ValueError(
'SourceMatchVector version number is %i, not 1.' % version)
1084 filterName = matchmeta.getString(
'FILTER').strip()
1086 ra = matchmeta.getDouble(
'RA') * afwGeom.degrees
1087 dec = matchmeta.getDouble(
'DEC') * afwGeom.degrees
1088 rad = matchmeta.getDouble(
'RADIUS') * afwGeom.degrees
1089 self.log.logdebug(
'Searching RA,Dec %.3f,%.3f, radius %.1f arcsec, filter "%s"' %
1090 (ra.asDegrees(), dec.asDegrees(), rad.asArcseconds(), filterName))
1092 self.log.logdebug(
'Found %i reference catalog sources in range' % len(refCat))
1100 Create match metadata entries required for regenerating the catalog
1102 @param width Width of the image (pixels)
1103 @param height Height of the image (pixels)
1104 @param x0 x offset of image origin from parent (pixels)
1105 @param y0 y offset of image origin from parent (pixels)
1106 @param filterName Name of filter, used for magnitudes
1112 cx,cy = x0 + 0.5 + width/2., y0 + 0.5 + height/2.
1113 radec = wcs.pixelToSky(cx, cy).toIcrs()
1114 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
1115 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
1116 imgSize = wcs.pixelScale() * math.hypot(width, height)/2.
1117 meta.add(
'RADIUS', imgSize.asDegrees(),
1118 'field radius in degrees, approximate')
1119 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1120 if filterName
is not None:
1121 meta.add(
'FILTER', filterName,
'LSST filter name for tagalong data')
1124 def readMatches(butler, dataId, sourcesName='icSrc', matchesName='icMatch', config=MeasAstromConfig(),
1125 sourcesFlags=afwTable.SOURCE_IO_NO_FOOTPRINTS):
1126 """Read matches, sources and catalogue; combine.
1128 @param butler Data butler
1129 @param dataId Data identifier for butler
1130 @param sourcesName Name for sources from butler
1131 @param matchesName Name for matches from butler
1132 @param sourcesFlags Flags to pass for source retrieval
1135 sources = butler.get(sourcesName, dataId, flags=sourcesFlags)
1136 packedMatches = butler.get(matchesName, dataId)
1138 return astrom.joinMatchListWithCatalog(packedMatches, sources)
def getReferenceSourcesForWcs
def _getMIndexesWithinRange
Class for storing ordered metadata with comments.
Implementation of the WCS standard for a any projection.
def getSipWcsFromCorrespondences
a place to record messages and descriptions of the state of processing.
An integer coordinate rectangle.
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.
A floating-point coordinate rectangle geometry.
def joinMatchListWithCatalog
def getSipWcs
"Not very pythonic!" complains Paul.
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...