1 from __future__
import absolute_import, division
2 from __future__
import print_function
3 from builtins
import zip
4 from builtins
import next
5 from builtins
import input
6 from builtins
import range
7 from builtins
import object
44 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask, LoadMultiIndexes
45 from .display
import displayAstrometry
46 from .astromLib
import makeMatchStatisticsInRadians
48 from .
import sip
as astromSip
50 __all__ = [
"InitialAstrometry",
"ANetBasicAstrometryConfig",
"ANetBasicAstrometryTask"]
55 Object returned by Astrometry.determineWcs
57 getWcs(): sipWcs or tanWcs
58 getMatches(): sipMatches or tanMatches
61 solveQa (PropertyList)
63 tanMatches (MatchList)
65 sipMatches (MatchList)
66 refCat (lsst::afw::table::SimpleCatalog)
67 matchMeta (PropertyList)
81 Get "sipMatches" -- MatchList using the SIP WCS solution, if it
82 exists, or "tanMatches" -- MatchList using the TAN WCS solution
89 Returns the SIP WCS, if one was found, or a TAN WCS
93 matches = property(getMatches)
94 wcs = property(getWcs)
120 maxCpuTime = RangeField(
121 doc=
"Maximum CPU time to spend solving, in seconds",
126 matchThreshold = RangeField(
127 doc=
"Matching threshold for Astrometry.net solver (log-odds)",
129 default=math.log(1e12),
132 maxStars = RangeField(
133 doc=
"Maximum number of stars to use in Astrometry.net solving",
138 useWcsPixelScale = Field(
139 doc=
"Use the pixel scale from the input exposure\'s WCS headers?",
143 useWcsRaDecCenter = Field(
144 doc=
"Use the RA,Dec center information from the input exposure\'s WCS headers?",
148 useWcsParity = Field(
149 doc=
"Use the parity (flip / handedness) of the image from the input exposure\'s WCS headers?",
153 raDecSearchRadius = RangeField(
154 doc=
"When useWcsRaDecCenter=True, this is the radius, in degrees, around the RA,Dec center " +
155 "specified in the input exposure\'s WCS to search for a solution.",
160 pixelScaleUncertainty = RangeField(
161 doc=
"Range of pixel scales, around the value in the WCS header, to search. " +
162 "If the value of this field is X and the nominal scale is S, " +
163 "the range searched will be S/X to S*X",
168 catalogMatchDist = RangeField(
169 doc=
"Matching radius (arcsec) for matching sources to reference objects",
174 cleaningParameter = RangeField(
175 doc=
"Sigma-clipping parameter in sip/cleanBadPoints.py",
180 calculateSip = Field(
181 doc=
"Compute polynomial SIP distortion terms?",
185 sipOrder = RangeField(
186 doc=
"Polynomial order of SIP distortion terms",
191 badFlags = ListField(
192 doc=
"List of flags which cause a source to be rejected as bad",
195 "slot_Centroid_flag",
196 "base_PixelFlags_flag_edge",
197 "base_PixelFlags_flag_saturated",
198 "base_PixelFlags_flag_crCenter",
202 doc=
"Retrieve all available fluxes (and errors) from catalog?",
206 maxIter = RangeField(
207 doc=
"maximum number of iterations of match sources and fit WCS" +
208 "ignored if not fitting a WCS",
213 matchDistanceSigma = RangeField(
214 doc=
"The match and fit loop stops when maxMatchDist minimized: "
215 " maxMatchDist = meanMatchDist + matchDistanceSigma*stdDevMatchDistance " +
216 " (where the mean and std dev are computed using outlier rejection);" +
217 " ignored if not fitting a WCS",
225 """!Basic implemeentation of the astrometry.net astrometrical fitter
227 A higher-level class ANetAstrometryTask takes care of dealing with the fact
228 that the initial WCS is probably only a pure TAN SIP, yet we may have
229 significant distortion and a good estimate for that distortion.
232 About Astrometry.net index files (astrometry_net_data):
234 There are three components of an index file: a list of stars
235 (stored as a star kd-tree), a list of quadrangles of stars ("quad
236 file") and a list of the shapes ("codes") of those quadrangles,
237 stored as a code kd-tree.
239 Each index covers a region of the sky, defined by healpix nside
240 and number, and a range of angular scales. In LSST, we share the
241 list of stars in a part of the sky between multiple indexes. That
242 is, the star kd-tree is shared between multiple indices (quads and
243 code kd-trees). In the astrometry.net code, this is called a
246 It is possible to "unload" and "reload" multiindex (and index)
247 objects. When "unloaded", they consume no FILE or mmap resources.
249 The multiindex object holds the star kd-tree and gives each index
250 object it holds a pointer to it, so it is necessary to
251 multiindex_reload_starkd() before reloading the indices it holds.
252 The multiindex_unload() method, on the other hand, unloads its
253 starkd and unloads each index it holds.
255 ConfigClass = ANetBasicAstrometryConfig
256 _DefaultName =
"aNetBasicAstrometry"
262 """!Construct an ANetBasicAstrometryTask
264 @param[in] config configuration (an instance of self.ConfigClass)
265 @param[in] andConfig astrometry.net data config (an instance of AstromNetDataConfig, or None);
266 if None then use andConfig.py in the astrometry_net_data product (which must be setup)
267 @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_
269 @throw RuntimeError if andConfig is None and the configuration cannot be found,
270 either because astrometry_net_data is not setup in eups
271 or because the setup version does not include the file "andConfig.py"
273 pipeBase.Task.__init__(self, config=config, **kwargs)
282 self.refObjLoader._readIndexFiles()
286 if self.log.getLevel() > self.log.DEBUG:
288 from astrometry.util.ttime
import get_memusage
291 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
293 ss.append(key +
': ' +
' '.join(mu[key]))
295 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
296 if 'mmaps_total' in mu:
297 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
298 self.log.debug(prefix +
'Memory: ' +
', '.join(ss))
300 def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True):
301 """Get image parameters
303 @param[in] exposure exposure (an afwImage.Exposure) or None
304 @param[in] bbox bounding box (an afwGeom.Box2I) or None; if None then bbox must be specified
305 @param[in] wcs WCS (an afwImage.Wcs) or None; if None then exposure must be specified
306 @param[in] filterName filter name, a string, or None; if None exposure must be specified
307 @param[in] wcsRequired if True then either wcs must be specified or exposure must contain a wcs;
308 if False then the returned wcs may be None
310 - bbox bounding box; guaranteed to be set
311 - wcs WCS if known, else None
312 - filterName filter name if known, else None
313 @throw RuntimeError if bbox cannot be determined, or wcs cannot be determined and wcsRequired True
315 if exposure
is not None:
317 bbox = exposure.getBBox()
318 self.log.debug(
"Setting bbox = %s from exposure metadata", bbox)
320 self.log.debug(
"Setting wcs from exposure metadata")
321 wcs = exposure.getWcs()
322 if filterName
is None:
323 filterName = exposure.getFilter().getName()
324 self.log.debug(
"Setting filterName = %r from exposure metadata", filterName)
326 raise RuntimeError(
"bbox or exposure must be specified")
327 if wcs
is None and wcsRequired:
328 raise RuntimeError(
"wcs or exposure (with a WCS) must be specified")
329 return bbox, wcs, filterName
331 def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None, calculateSip=None):
332 """!Return an InitialAstrometry object, just like determineWcs,
333 but assuming the given input WCS is correct.
335 This involves searching for reference sources within the WCS
336 area, and matching them to the given 'sourceCat'. If
337 'calculateSip' is set, we will try to compute a TAN-SIP
338 distortion correction.
340 @param[in] sourceCat list of detected sources in this image.
341 @param[in] wcs your known WCS, or None to get from exposure
342 @param[in] exposure the exposure holding metadata for this image;
343 if None then you must specify wcs, filterName and bbox
344 @param[in] filterName string, filter name, eg "i", or None to get from exposure`
345 @param[in] bbox bounding box of image, or None to get from exposure
346 @param[in] calculateSip calculate SIP distortion terms for the WCS? If None
347 then use self.config.calculateSip. To disable WCS fitting set calculateSip=False
349 @note this function is also called by 'determineWcs' (via 'determineWcs2'), since the steps are all
355 if calculateSip
is None:
356 calculateSip = self.config.calculateSip
362 filterName=filterName,
365 refCat = self.refObjLoader.loadPixelBox(
368 filterName=filterName,
371 astrom.refCat = refCat
372 catids = [src.getId()
for src
in refCat]
374 self.log.debug(
'%i reference sources; %i unique IDs', len(catids), len(uids))
376 uniq = set([sm.second.getId()
for sm
in matches])
377 if len(matches) != len(uniq):
378 self.log.warn(
'The list of matched stars contains duplicate reference source IDs ' +
379 '(%i sources, %i unique ids)', len(matches), len(uniq))
380 if len(matches) == 0:
381 self.log.warn(
'No matches found between input sources and reference catalogue.')
384 self.log.debug(
'%i reference objects match input sources using input WCS', len(matches))
385 astrom.tanMatches = matches
388 srcids = [s.getId()
for s
in sourceCat]
390 assert(m.second.getId()
in srcids)
391 assert(m.second
in sourceCat)
396 self.log.debug(
'Failed to find a SIP WCS better than the initial one.')
398 self.log.debug(
'%i reference objects match input sources using SIP WCS',
400 astrom.sipWcs = sipwcs
401 astrom.sipMatches = matches
403 wcs = astrom.getWcs()
406 for src
in sourceCat:
412 """Find a WCS solution for the given 'sourceCat' in the given
413 'exposure', getting other parameters from config.
415 Valid kwargs include:
417 'radecCenter', an afw.coord.Coord giving the RA,Dec position
418 of the center of the field. This is used to limit the
419 search done by Astrometry.net (to make it faster and load
420 fewer index files, thereby using less memory). Defaults to
421 the RA,Dec center from the exposure's WCS; turn that off
422 with the boolean kwarg 'useRaDecCenter' or config option
425 'useRaDecCenter', a boolean. Don't use the RA,Dec center from
426 the exposure's initial WCS.
428 'searchRadius', in degrees, to search for a solution around
429 the given 'radecCenter'; default from config option
432 'useParity': parity is the 'flip' of the image. Knowing it
433 reduces the search space (hence time) for Astrometry.net.
434 The parity can be computed from the exposure's WCS (the
435 sign of the determinant of the CD matrix); this option
436 controls whether we do that or force Astrometry.net to
437 search both parities. Default from config.useWcsParity.
439 'pixelScale': afwGeom.Angle, estimate of the angle-per-pixel
440 (ie, arcseconds per pixel). Defaults to a value derived
441 from the exposure's WCS. If enabled, this value, plus or
442 minus config.pixelScaleUncertainty, will be used to limit
443 Astrometry.net's search.
445 'usePixelScale': boolean. Use the pixel scale to limit
446 Astrometry.net's search? Defaults to config.useWcsPixelScale.
448 'filterName', a string, the filter name of this image. Will
449 be mapped through the 'filterMap' config dictionary to a
450 column name in the astrometry_net_data index FITS files.
451 Defaults to the exposure.getFilter() value.
453 'bbox', bounding box of exposure; defaults to exposure.getBBox()
456 assert(exposure
is not None)
458 margs = kwargs.copy()
459 if 'searchRadius' not in margs:
460 margs.update(searchRadius=self.config.raDecSearchRadius * afwGeom.degrees)
461 if 'usePixelScale' not in margs:
462 margs.update(usePixelScale=self.config.useWcsPixelScale)
463 if 'useRaDecCenter' not in margs:
464 margs.update(useRaDecCenter=self.config.useWcsRaDecCenter)
465 if 'useParity' not in margs:
466 margs.update(useParity=self.config.useWcsParity)
467 margs.update(exposure=exposure)
471 """Get a blind astrometric solution for the given catalog of sources.
477 And if available, we can use:
478 -an initial Wcs estimate;
483 (all of which are metadata of Exposure).
486 imageSize: (W,H) integer tuple/iterable
487 pixelScale: afwGeom::Angle per pixel.
488 radecCenter: afwCoord::Coord
493 for key
in [
'exposure',
'bbox',
'filterName']:
495 kw[key] = kwargs[key]
496 astrom = self.
useKnownWcs(sourceCat, wcs=wcs, **kw)
513 searchRadiusScale=2.):
514 if not useRaDecCenter
and radecCenter
is not None:
515 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
516 if not usePixelScale
and pixelScale
is not None:
517 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
523 filterName=filterName,
528 xc, yc = bboxD.getCenter()
532 if pixelScale
is None:
534 pixelScale = wcs.pixelScale()
535 self.log.debug(
'Setting pixel scale estimate = %.3f from given WCS estimate',
536 pixelScale.asArcseconds())
538 if radecCenter
is None:
540 radecCenter = wcs.pixelToSky(xc, yc)
541 self.log.debug(
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS ' +
542 'estimate, using pixel center = (%.1f, %.1f)',
543 radecCenter.getLongitude().asDegrees(),
544 radecCenter.getLatitude().asDegrees(), xc, yc)
546 if searchRadius
is None:
548 assert(pixelScale
is not None)
549 pixRadius = math.hypot(*bboxD.getDimensions()) / 2
550 searchRadius = (pixelScale * pixRadius * searchRadiusScale)
551 self.log.debug(
'Using RA,Dec search radius = %.3f deg, from pixel scale, ' +
552 'image size, and searchRadiusScale = %g',
553 searchRadius, searchRadiusScale)
555 parity = wcs.isFlipped()
556 self.log.debug(
'Using parity = %s' % (parity
and 'True' or 'False'))
560 if exposure
is not None:
561 exposureBBoxD =
afwGeom.Box2D(exposure.getMaskedImage().getBBox())
563 exposureBBoxD = bboxD
565 self.log.debug(
"Trimming: kept %i of %i sources", n, len(sourceCat))
571 pixelScale=pixelScale,
572 radecCenter=radecCenter,
573 searchRadius=searchRadius,
575 filterName=filterName,
578 raise RuntimeError(
"Unable to match sources with catalog.")
579 self.log.info(
'Got astrometric solution from Astrometry.net')
581 rdc = wcs.pixelToSky(xc, yc)
582 self.log.debug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)',
583 xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees())
587 """!Get a TAN-SIP WCS, starting from an existing WCS.
589 It uses your WCS to compute a fake grid of corresponding "stars" in pixel and sky coords,
590 and feeds that to the regular SIP code.
592 @param[in] wcs initial WCS
593 @param[in] bbox bounding box of image
594 @param[in] ngrid number of grid points along x and y for fitting (fit at ngrid^2 points)
595 @param[in] linearizeAtCenter if True, get a linear approximation of the input
596 WCS at the image center and use that as the TAN initialization for
597 the TAN-SIP solution. You probably want this if your WCS has its
598 CRPIX outside the image bounding box.
601 srcSchema = afwTable.SourceTable.makeMinimalSchema()
602 key = srcSchema.addField(
"centroid", type=
"PointD")
603 srcTable = afwTable.SourceTable.make(srcSchema)
604 srcTable.defineCentroid(
"centroid")
606 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
609 (W, H) = bbox.getDimensions()
610 x0, y0 = bbox.getMin()
611 for xx
in np.linspace(0., W, ngrid):
612 for yy
in np.linspace(0, H, ngrid):
613 src = srcs.makeRecord()
614 src.set(key.getX(), x0 + xx)
615 src.set(key.getY(), y0 + yy)
618 ref = refs.makeRecord()
622 if linearizeAtCenter:
627 crval = wcs.pixelToSky(crpix)
628 crval = crval.getPosition(afwGeom.degrees)
631 aff = wcs.linearizePixelToSky(crval)
632 cd = aff.getLinear().getMatrix()
638 """Produce a SIP solution given a list of known correspondences.
640 Unlike _calculateSipTerms, this does not iterate the solution;
641 it assumes you have given it a good sets of corresponding stars.
643 NOTE that "refCat" and "sourceCat" are assumed to be the same length;
644 entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences.
646 @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS.
647 @param[in] refCat reference source catalog
648 @param[in] sourceCat source catalog
649 @param[in] bbox bounding box of image
651 sipOrder = self.config.sipOrder
653 for ci, si
in zip(refCat, sourceCat):
656 sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox)
657 return sipObject.getNewWcs()
660 """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS.
662 @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS;
663 this is used to set the baseline when determining whether a SIP
664 solution is any better; it will be returned if no better SIP solution
666 @param[in] refCat reference source catalog
667 @param[in] sourceCat sources in the image to be solved
668 @param[in] matches list of supposedly matched sources, using the "origWcs".
669 @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients.
671 sipOrder = self.config.sipOrder
674 lastMatchSize = len(matches)
676 for i
in range(self.config.maxIter):
679 sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox)
680 proposedWcs = sipObject.getNewWcs()
681 self.
plotSolution(matches, proposedWcs, bbox.getDimensions())
683 self.log.warn(
'Failed to calculate distortion terms. Error: ', str(e))
687 proposedMatchlist = self.
_getMatchList(sourceCat, refCat, proposedWcs)
688 proposedMatchSize = len(proposedMatchlist)
692 "SIP iteration %i: %i objects match, previous = %i;" %
693 (i, proposedMatchSize, lastMatchSize) +
694 " clipped mean scatter = %s arcsec, previous = %s; " %
695 (proposedMatchStats.distMean.asArcseconds(), lastMatchStats.distMean.asArcseconds()) +
696 " max match dist = %s arcsec, previous = %s" %
697 (proposedMatchStats.maxMatchDist.asArcseconds(),
698 lastMatchStats.maxMatchDist.asArcseconds())
701 if lastMatchStats.maxMatchDist <= proposedMatchStats.maxMatchDist:
703 "Fit WCS: use iter %s because max match distance no better in next iter: " % (i-1,) +
704 " %g < %g arcsec" % (lastMatchStats.maxMatchDist.asArcseconds(),
705 proposedMatchStats.maxMatchDist.asArcseconds()))
709 matches = proposedMatchlist
710 lastMatchSize = proposedMatchSize
711 lastMatchStats = proposedMatchStats
716 """Plot the solution, when debugging is turned on.
718 @param matches The list of matches
720 @param imageSize 2-tuple with the image size (W,H)
728 import matplotlib.pyplot
as plt
729 except ImportError
as e:
730 self.log.warn(
"Unable to import matplotlib: %s", e)
736 fig.canvas._tkcanvas._root().lift()
745 for i, m
in enumerate(matches):
746 x[i] = m.second.getX()
747 y[i] = m.second.getY()
748 pixel = wcs.skyToPixel(m.first.getCoord())
749 dx[i] = x[i] - pixel.getX()
750 dy[i] = y[i] - pixel.getY()
752 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
754 def plotNext(x, y, xLabel, yLabel, xMax):
756 ax.set_autoscalex_on(
False)
757 ax.set_xbound(lower=0, upper=xMax)
759 ax.set_xlabel(xLabel)
760 ax.set_ylabel(yLabel)
763 plotNext(x, dx,
"x",
"dx", imageSize[0])
764 plotNext(x, dy,
"x",
"dy", imageSize[0])
765 plotNext(y, dx,
"y",
"dx", imageSize[1])
766 plotNext(y, dy,
"y",
"dy", imageSize[1])
772 reply =
input(
"Pausing for inspection, enter to continue... [hpQ] ").strip()
776 reply = reply.split()
782 if reply
in (
"",
"h",
"p",
"Q"):
784 print(
"h[elp] p[db] Q[uit]")
794 """Compute on-sky radial distance statistics for a match list
796 @param[in] wcs WCS for match list; an lsst.afw.image.Wcs
797 @param[in] matchList list of matches between reference object and sources;
798 a list of lsst.afw.table.ReferenceMatch;
799 the source centroid and reference object coord are read
801 @return a pipe_base Struct containing these fields:
802 - distMean clipped mean of on-sky radial separation
803 - distStdDev clipped standard deviation of on-sky radial separation
804 - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev
807 afwMath.MEANCLIP | afwMath.STDEVCLIP)
808 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*afwGeom.radians
809 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*afwGeom.radians
810 return pipeBase.Struct(
812 distStdDev=distStdDev,
813 maxMatchDist=distMean + self.config.matchDistanceSigma*distStdDev,
817 dist = self.config.catalogMatchDist * afwGeom.arcseconds
818 clean = self.config.cleaningParameter
819 matcher = astromSip.MatchSrcToCatalogue(refCat, sourceCat, wcs, dist)
820 matches = matcher.getMatches()
823 X = [src.getX()
for src
in sourceCat]
824 Y = [src.getY()
for src
in sourceCat]
825 R1 = [src.getRa().asDegrees()
for src
in sourceCat]
826 D1 = [src.getDec().asDegrees()
for src
in sourceCat]
827 R2 = [src.getRa().asDegrees()
for src
in refCat]
828 D2 = [src.getDec().asDegrees()
for src
in refCat]
835 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sourceCat), len(refCat)))
838 'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
839 (min(X), max(X), min(Y), max(Y), min(R1), max(R1), min(D1), max(D1)))
841 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
842 (min(R2), max(R2), min(D2), max(D2)))
843 raise RuntimeError(
'No matches found between image and catalogue')
844 matches = astromSip.cleanBadPoints.clean(matches, wcs, nsigma=clean)
849 Returns the column name in the astrometry_net_data index file that will be used
850 for the given filter name.
852 @param filterName Name of filter used in exposure
853 @param columnMap Dict that maps filter names to column names
854 @param default Default column name
856 filterName = self.config.filterMap.get(filterName, filterName)
858 return columnMap[filterName]
860 self.log.warn(
"No column in configuration for filter '%s'; using default '%s'" %
861 (filterName, default))
864 def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter, searchRadius, parity, filterName=None):
865 solver = self.refObjLoader._getSolver()
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.getPsfFlux()) \
878 goodsources.append(s)
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)
918 multiInds = self.refObjLoader.multiInds
919 qlo, qhi = solver.getQuadSizeLow(), solver.getQuadSizeHigh()
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)
934 with LoadMultiIndexes(toload_multiInds):
935 displayAstrometry(refCat=self.refObjLoader.loadPixelBox(bbox, wcs, filterName).refCat,
938 with LoadMultiIndexes(toload_multiInds):
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()
952 self.log.debug(
'WCS: %s', wcs.getFitsMetadata().toString())
954 if x0 != 0
or y0 != 0:
955 wcs.shiftReferencePixel(x0, y0)
956 self.log.debug(
'After shifting reference pixel by x0,y0 = (%i,%i), WCS is: %s',
957 x0, y0, wcs.getFitsMetadata().toString())
960 self.log.warn(
'Did not get an astrometric solution from Astrometry.net')
966 if radecCenter
is not None:
967 self.refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName)
969 qa = solver.getSolveStats()
970 self.log.debug(
'qa: %s', qa.toString())
975 if candsource.get(k):
981 """Remove elements from catalog whose xy positions are not within the given bbox.
983 sourceCat: a Catalog of SimpleRecord or SourceRecord objects
984 bbox: an afwImage.Box2D
985 wcs: if not None, will be used to compute the xy positions on-the-fly;
986 this is required when sources actually contains SimpleRecords.
989 a list of Source objects with xAstrom, yAstrom within the bbox.
991 keep = type(sourceCat)(sourceCat.table)
993 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
994 if bbox.contains(point):
1001 Create match metadata entries required for regenerating the catalog
1003 @param bbox bounding box of image (pixels)
1004 @param filterName Name of filter, used for magnitudes
1010 cx, cy = bboxD.getCenter()
1011 radec = wcs.pixelToSky(cx, cy).toIcrs()
1012 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
1013 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
1014 pixelRadius = math.hypot(*bboxD.getDimensions())/2.0
1015 skyRadius = wcs.pixelScale() * pixelRadius
1016 meta.add(
'RADIUS', skyRadius.asDegrees(),
1017 'field radius in degrees, approximate')
1018 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
1019 if filterName
is not None:
1020 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.
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())
Compute statistics of on-sky radial separation for a match list, in radians.
A floating-point coordinate rectangle geometry.
def __init__
Construct an ANetBasicAstrometryTask.