lsst.skymap g93973518b8+2ee706bf2d
Loading...
Searching...
No Matches
showVisitSkyMap.py
Go to the documentation of this file.
1# This file is part of skymap.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import argparse
23import matplotlib
24import matplotlib.patches as mpatches
25import matplotlib.pyplot as plt
26import numpy as np
27from astropy import units
28from astropy.coordinates import SkyCoord
29from matplotlib.legend import Legend
30
31import lsst.afw.cameraGeom as cameraGeom
32import lsst.daf.butler as dafButler
33import lsst.geom as geom
34import lsst.log as lsstLog
35import lsst.sphgeom as sphgeom
36
37logger = lsstLog.Log.getLogger("showVisitSkyMap.py")
38
39
40def bboxToRaDec(bbox, wcs):
41 """Get the corners of a BBox and convert them to lists of RA and Dec."""
42 sphPoints = wcs.pixelToSky(geom.Box2D(bbox).getCorners())
43 ra = [float(sph.getRa().asDegrees()) for sph in sphPoints]
44 dec = [float(sph.getDec().asDegrees()) for sph in sphPoints]
45 return ra, dec
46
47
48def getValueAtPercentile(values, percentile=0.5):
49 """Return a value a fraction of the way between the min and max values in a
50 list.
51
52 Parameters
53 ----------
54 values : `list` [`float`]
55 The list of values under consideration.
56 percentile : `float`, optional
57 The percentile (expressed as a number between 0.0 and 1.0) at which
58 to determine the value in `values`.
59
60 Returns
61 -------
62 result : `float`
63 The value at the given percentile of ``values``.
64 """
65 m = min(values)
66 interval = max(values) - m
67 return m + percentile*interval
68
69
70def get_cmap(n, name="hsv"):
71 """Returns a function that maps each index in 0, 1, ..., n-1 to a distinct
72 RGB color; the keyword argument name must be a standard mpl colormap name.
73 """
74 return matplotlib.colormaps[name].resampled(n)
75
76
77def main(repo, collections, skymapName=None, tracts=None, visits=None, physicalFilters=None, bands=None,
78 ccds=None, ccdKey="detector", showPatch=False, saveFile=None, showCcds=False, visitVetoFile=None,
79 minOverlapFraction=None, trimToTracts=False):
80 if minOverlapFraction is not None and tracts is None:
81 raise RuntimeError("Must specify --tracts if --minOverlapFraction is set")
82 logger.info("Making butler for collections = {} in repo {}".format(collections, repo))
83 butler = dafButler.Butler(repo, collections=collections)
84 instrument = butler.registry.findDataset("camera").dataId["instrument"]
85 detectorSkipList = []
86 # Make a guess at the skymapName if not provided
87 if skymapName is None:
88 if instrument == "HSC":
89 skymapName = "hsc_rings_v1"
90 detectorSkipList = [9] # detector 9 has long been dead for HSC
91 elif instrument == "LSSTCam-imSim":
92 skymapName = "DC2"
93 elif instrument == "LATISS":
94 skymapName = "latiss_v1"
95 elif instrument == "DECam":
96 skymapName = "decam_rings_v1"
97 else:
98 logger.error("Unknown skymapName for instrument: %s. Must specify --skymapName on command line.",
99 instrument)
100 logger.info("instrument = {} skymapName = {}".format(instrument, skymapName))
101 camera = butler.get("camera", instrument=instrument)
102 skymap = butler.get("skyMap", instrument=instrument, skymap=skymapName)
103
104 whereStr = ""
105 if tracts is not None:
106 tractStr = makeWhereInStr("tract", tracts, int)
107 whereStr += tractStr
108
109 if physicalFilters is not None:
110 physicalFilterStr = makeWhereInStr("physical_filter", physicalFilters, str)
111 whereStr += " AND " + physicalFilterStr if len(whereStr) else " " + physicalFilterStr
112
113 if bands is not None:
114 bandStr = makeWhereInStr("band", bands, str)
115 whereStr += " AND " + bandStr if len(whereStr) else " " + bandStr
116
117 if len(whereStr) > 1:
118 whereStr = "instrument=\'" + instrument + "\' AND skymap=\'" + skymapName + "\' AND " + whereStr
119 else:
120 whereStr = "instrument=\'" + instrument + "\' AND skymap=\'" + skymapName + "\'"
121 logger.info("Applying the following where clause in dataId search: {} ".format(whereStr))
122
123 visitVetoList = []
124 if visitVetoFile is not None:
125 with open(visitVetoFile) as f:
126 content = f.readlines()
127 visitVetoList = [int(visit.strip()) for visit in content]
128
129 if visits is None:
130 dataRefs = list(butler.registry.queryDatasets("calexp", where=whereStr).expanded())
131 visits = []
132 for dataRef in dataRefs:
133 visit = dataRef.dataId.visit.id
134 if visit not in visits and visit not in visitVetoList:
135 visits.append(visit)
136 visits.sort()
137 logger.info("List of visits (N={}) satisfying where and veto clauses: {}".format(len(visits),
138 visits))
139 else:
140 if len(visitVetoList) > 1:
141 visitListTemp = visits.copy()
142 for visit in visitListTemp:
143 if visit in visitVetoList:
144 visits.remove(visit)
145 logger.info("List of visits (N={}) excluding veto list: {}".format(len(visits), visits))
146
147 ccdIdList = []
148 for ccd in camera:
149 ccdId = ccd.getId()
150 if ((ccds is None or ccdId in ccds) and ccd.getType() == cameraGeom.DetectorType.SCIENCE
151 and ccdId not in detectorSkipList):
152 ccdIdList.append(ccdId)
153 ccdIdList.sort()
154 nDetTot = len(ccdIdList)
155
156 visitIncludeList = []
157 # Determine the fraction of detectors that overlap any tract under
158 # consideration. If this fraction does not exceed minOverlapFraction,
159 # skip this visit.
160 if minOverlapFraction is not None:
161 for i_v, visit in enumerate(visits):
162 ccdOverlapList = []
163 try:
164 visitSummary = butler.get("visitSummary", visit=visit)
165 except LookupError as e:
166 logger.warn("%s Will try to get wcs from calexp.", e)
167 visitSummary = None
168 if tracts is not None:
169 for tract in tracts:
170 tractInfo = skymap[tract]
171 sphCorners = tractInfo.wcs.pixelToSky(geom.Box2D(tractInfo.bbox).getCorners())
172 tractConvexHull = sphgeom.ConvexPolygon.convexHull(
173 [coord.getVector() for coord in sphCorners])
174 for ccdId in ccdIdList:
175 if ccdId not in ccdOverlapList:
176 raCorners, decCorners = getDetRaDecCorners(
177 ccdKey, ccdId, visit, visitSummary=visitSummary, butler=butler,
178 doLogWarn=False)
179 if raCorners is not None and decCorners is not None:
180 detSphCorners = []
181 for ra, dec in zip(raCorners, decCorners):
182 pt = geom.SpherePoint(geom.Angle(ra, geom.degrees),
183 geom.Angle(dec, geom.degrees))
184 detSphCorners.append(pt)
185 detConvexHull = sphgeom.ConvexPolygon.convexHull(
186 [coord.getVector() for coord in detSphCorners])
187 if tractConvexHull.contains(detConvexHull):
188 ccdOverlapList.append(ccdId)
189
190 if len(ccdOverlapList)/nDetTot >= minOverlapFraction:
191 break
192 if len(ccdOverlapList)/nDetTot < minOverlapFraction:
193 logger.info("Fraction of detectors overlaping any tract for visit %d (%.2f) < "
194 "minimum required (%.2f). Skipping visit...",
195 visit, len(ccdOverlapList)/nDetTot, minOverlapFraction)
196 else:
197 if visit not in visitIncludeList:
198 visitIncludeList.append(visit)
199 else:
200 visitIncludeList = visits
201
202 # draw the CCDs
203 ras, decs = [], []
204 bboxesPlotted = []
205 cmap = get_cmap(len(visitIncludeList))
206 alphaEdge = 0.7
207 maxVisitForLegend = 20
208 finalVisitList = []
209 includedBands = []
210 includedPhysicalFilters = []
211 for i_v, visit in enumerate(visitIncludeList):
212 print("Working on visit %d [%d of %d]" % (visit, i_v + 1, len(visitIncludeList)), end="\r")
213 inLegend = False
214 color = cmap(i_v)
215 fillKwargs = {"fill": False, "alpha": alphaEdge, "facecolor": None, "edgecolor": color, "lw": 0.6}
216 try:
217 visitSummary = butler.get("visitSummary", visit=visit)
218 except LookupError as e:
219 logger.warn("%s Will try to get wcs from calexp.", e)
220 visitSummary = None
221
222 band, physicalFilter = getBand(visitSummary=visitSummary, butler=butler, visit=visit)
223 if band not in includedBands:
224 includedBands.append(band)
225 if physicalFilter not in includedPhysicalFilters:
226 includedPhysicalFilters.append(physicalFilter)
227
228 for ccdId in ccdIdList:
229 raCorners, decCorners = getDetRaDecCorners(
230 ccdKey, ccdId, visit, visitSummary=visitSummary, butler=butler)
231 if raCorners is not None and decCorners is not None:
232 ras += raCorners
233 decs += decCorners
234 if not inLegend and len(visitIncludeList) <= maxVisitForLegend:
235 plt.fill(raCorners, decCorners, label=str(visit), **fillKwargs)
236 inLegend = True
237 else:
238 plt.fill(raCorners, decCorners, **fillKwargs)
239 plt.fill(raCorners, decCorners, fill=True, alpha=alphaEdge/4, color=color,
240 edgecolor=color)
241 if visit not in finalVisitList:
242 finalVisitList.append(visit)
243 # add CCD serial numbers
244 if showCcds:
245 overlapFrac = 0.2
246 deltaRa = max(raCorners) - min(raCorners)
247 deltaDec = max(decCorners) - min(decCorners)
248 minPoint = geom.Point2D(min(raCorners) + overlapFrac*deltaRa,
249 min(decCorners) + overlapFrac*deltaDec)
250 maxPoint = geom.Point2D(max(raCorners) - overlapFrac*deltaRa,
251 max(decCorners) - overlapFrac*deltaDec)
252 # Use doubles in Box2D to check overlap
253 bboxDouble = geom.Box2D(minPoint, maxPoint)
254 overlaps = [not bboxDouble.overlaps(otherBbox) for otherBbox in bboxesPlotted]
255 if all(overlaps):
256 plt.text(getValueAtPercentile(raCorners), getValueAtPercentile(decCorners),
257 str(ccdId), fontsize=6, ha="center", va="center", color="darkblue")
258 bboxesPlotted.append(bboxDouble)
259
260 logger.info("Final list of visits (N={}) satisfying where and minOverlapFraction clauses: {}"
261 .format(len(finalVisitList), finalVisitList))
262
263 raToDecLimitRatio = None
264 if len(ras) > 0:
265 tractList = list(set(skymap.findTractIdArray(ras, decs, degrees=True)))
266 minVisitRa, maxVisitRa = min(ras), max(ras)
267 minVisitDec, maxVisitDec = min(decs), max(decs)
268 raVisitDiff = maxVisitRa - minVisitRa
269 decVisitDiff = maxVisitDec - minVisitDec
270 midVisitRa = minVisitRa + 0.5*raVisitDiff
271 midVisitDec = minVisitDec + 0.5*decVisitDiff
272 midRa = np.atleast_1d((midVisitRa*units.deg).to(units.radian).value).astype(np.float64)
273 midDec = np.atleast_1d((midVisitDec*units.deg).to(units.radian).value).astype(np.float64)
274 midSkyCoord = SkyCoord(midVisitRa*units.deg, midVisitDec*units.deg)
275 else:
276 if tracts is not None:
277 logger.info("No calexps were found, but --tracts list was provided, so will go ahead and "
278 "plot the empty tracts.")
279 tractList = tracts
280 trimToTracts = True
281 raToDecLimitRatio = 1.0
282 else:
283 raise RuntimeError("No data to plot (if you want to plot empty tracts, include them as "
284 "a blank-space separated list to the --tracts option.")
285 tractList.sort()
286 logger.info("List of tracts overlapping data: {}".format(tractList))
287 tractLimitsDict = getTractLimitsDict(skymap, tractList)
288
289 # Find a detector that contains the mid point in RA/Dec (or the closest
290 # one) to set the plot aspect ratio.
291 minDistToMidCood = 1e12
292 minSepVisit = None
293 minSepCcdId = None
294 for i_v, visit in enumerate(visits):
295 try:
296 visitSummary = butler.get("visitSummary", visit=visit)
297 except LookupError as e:
298 logger.warn("%s Will try to get wcs from calexp.", e)
299 visitSummary = None
300 for ccdId in ccdIdList:
301 raCorners, decCorners = getDetRaDecCorners(
302 ccdKey, ccdId, visit, visitSummary=visitSummary, butler=butler, doLogWarn=False)
303 if raCorners is not None and decCorners is not None:
304 detSphCorners = []
305 for ra, dec in zip(raCorners, decCorners):
306 pt = geom.SpherePoint(geom.Angle(ra, geom.degrees),
307 geom.Angle(dec, geom.degrees))
308 detSphCorners.append(pt)
309 ptSkyCoord = SkyCoord(ra*units.deg, dec*units.deg)
310 separation = (midSkyCoord.separation(ptSkyCoord)).degree
311 if separation < minDistToMidCood:
312 minSepVisit = visit
313 minSepCcdId = ccdId
314 minDistToMidCood = separation
315 detConvexHull = sphgeom.ConvexPolygon(
316 [coord.getVector() for coord in detSphCorners])
317 if detConvexHull.contains(midRa, midDec):
318 logger.info("visit/det overlapping plot coord mid point in RA/Dec: %d %d", visit, ccdId)
319 raToDecLimitRatio = (max(raCorners) - min(raCorners))/(max(decCorners) - min(decCorners))
320 det = camera[ccdId]
321 width = det.getBBox().getWidth()
322 height = det.getBBox().getHeight()
323 if raToDecLimitRatio > 1.0:
324 raToDecLimitRatio /= max(height/width, width/height)
325 else:
326 if raToDecLimitRatio < 1.0:
327 raToDecLimitRatio *= max(height/width, width/height)
328 break
329 if raToDecLimitRatio is not None:
330 break
331
332 if raToDecLimitRatio is None:
333 try:
334 visitSummary = butler.get("visitSummary", visit=minSepVisit)
335 except LookupError as e:
336 logger.warn("%s Will try to get wcs from calexp.", e)
337 visitSummary = None
338 raCorners, decCorners = getDetRaDecCorners(
339 ccdKey, minSepCcdId, minSepVisit, visitSummary=visitSummary, butler=butler, doLogWarn=False)
340 for ra, dec in zip(raCorners, decCorners):
341 pt = geom.SpherePoint(geom.Angle(ra, geom.degrees),
342 geom.Angle(dec, geom.degrees))
343 detSphCorners.append(pt)
344 detConvexHull = sphgeom.ConvexPolygon([coord.getVector() for coord in detSphCorners])
345 logger.info("visit/det closest to plot coord mid point in RA/Dec (none actually overlat it): %d %d",
346 minSepVisit, minSepCcdId)
347 raToDecLimitRatio = (max(raCorners) - min(raCorners))/(max(decCorners) - min(decCorners))
348 det = camera[minSepCcdId]
349 width = det.getBBox().getWidth()
350 height = det.getBBox().getHeight()
351 if raToDecLimitRatio > 1.0:
352 raToDecLimitRatio /= max(height/width, width/height)
353 else:
354 if raToDecLimitRatio < 1.0:
355 raToDecLimitRatio *= max(height/width, width/height)
356
357 if trimToTracts is True:
358 xlim, ylim = derivePlotLimits(tractLimitsDict, raToDecLimitRatio=raToDecLimitRatio, buffFrac=0.04)
359 else:
360 visitLimitsDict = {"allVisits": {"ras": [minVisitRa, maxVisitRa], "decs": [minVisitDec, maxVisitDec]}}
361 xlim, ylim = derivePlotLimits(visitLimitsDict, raToDecLimitRatio=raToDecLimitRatio, buffFrac=0.04)
362
363 # draw the skymap
364 alpha0 = 1.0
365 tractHandleList = []
366 tractStrList = []
367 if tracts is not None:
368 tractOutlineList = list(set(tracts + tractList))
369 else:
370 tractOutlineList = tractList
371 tractOutlineList.sort()
372 logger.info("List of tract outlines being plotted: {}".format(tractOutlineList))
373 for i_t, tract in enumerate(tractOutlineList):
374 alpha = max(0.1, alpha0 - i_t*1.0/len(tractOutlineList))
375 tractInfo = skymap[tract]
376 tCenter = tractInfo.ctr_coord
377 tCenterRa = tCenter.getRa().asDegrees()
378 tCenterDec = tCenter.getDec().asDegrees()
379 fracDeltaX = 0.02*abs((xlim[1] - xlim[0]))
380 fracDeltaY = 0.02*abs((ylim[1] - ylim[0]))
381 if (xlim[1] + fracDeltaX < tCenterRa < xlim[0] - fracDeltaX
382 and ylim[0] + fracDeltaY < tCenterDec < ylim[1] - fracDeltaY):
383 if len(tractOutlineList) > 1 or not showPatch:
384 plt.text(tCenterRa, tCenterDec, tract, fontsize=7, alpha=alpha, ha="center", va="center")
385 ra, dec = bboxToRaDec(tractInfo.bbox, tractInfo.getWcs())
386 plt.fill(ra, dec, fill=False, edgecolor="k", lw=1, linestyle="dashed", alpha=alpha)
387 tractArtist = mpatches.Patch(fill=False, edgecolor="k", linestyle="dashed", alpha=alpha)
388 tractHandleList.append(tractArtist)
389 tractStrList.append(str(tract))
390 if showPatch:
391 patchColor = "k"
392 for patch in tractInfo:
393 ra, dec = bboxToRaDec(patch.getInnerBBox(), tractInfo.getWcs())
394 plt.fill(ra, dec, fill=False, edgecolor=patchColor, lw=0.5, linestyle=(0, (5, 6)),
395 alpha=alpha)
396 if (xlim[1] + fracDeltaX < getValueAtPercentile(ra) < xlim[0] - fracDeltaX
397 and ylim[0] + fracDeltaY < getValueAtPercentile(dec) < ylim[1] - fracDeltaY):
399 str(patch.sequential_index), fontsize=5, color=patchColor,
400 ha="center", va="center", alpha=alpha)
401
402 # add labels and save
403 ax = plt.gca()
404 ax.set_xlim(xlim)
405 ax.set_ylim(ylim)
406 ax.set_box_aspect(1)
407 if abs(xlim[1] > 99.99):
408 ax.tick_params("x", labelrotation=45, pad=0, labelsize=8)
409 else:
410 ax.tick_params("x", labelrotation=0, pad=0, labelsize=8)
411 ax.tick_params("y", labelsize=8)
412 ax.set_xlabel("RA (deg)", fontsize=9)
413 ax.set_ylabel("Dec (deg)", fontsize=9)
414
415 visitScaleOffset = None
416 if len(visitIncludeList) > maxVisitForLegend:
417 nz = matplotlib.colors.Normalize()
418 colorBarScale = finalVisitList
419 if max(finalVisitList) > 9999999:
420 visitScaleOffset = min(finalVisitList)
421 colorBarScale = [visit - visitScaleOffset for visit in finalVisitList]
422 nz.autoscale(colorBarScale)
423 cax, _ = matplotlib.colorbar.make_axes(plt.gca(), pad=0.03)
424 cax.tick_params(labelsize=7)
425 cb = matplotlib.colorbar.ColorbarBase(cax, cmap=cmap, norm=nz, alpha=alphaEdge,
426 format=lambda x, _: f"{x:.0f}")
427 cb.ax.yaxis.get_offset_text().set_fontsize(7)
428 colorBarLabel = "visit number"
429 if visitScaleOffset is not None:
430 colorBarLabel += " - {:d}".format(visitScaleOffset)
431 cb.set_label(colorBarLabel, rotation=-90, labelpad=13, fontsize=9)
432 tractLegend = Legend(ax, tractHandleList, tractStrList, loc="upper right",
433 fancybox=True, shadow=True, fontsize=5, title_fontsize=6, title="tracts")
434 ax.add_artist(tractLegend)
435 else:
436 if len(visitIncludeList) > 0:
437 ax.legend(loc="center left", bbox_to_anchor=(1.15, 0.5), fancybox=True, shadow=True,
438 fontsize=6, title_fontsize=6, title="visits")
439 # Create the second legend and add the artist manually.
440 tractLegend = Legend(ax, tractHandleList, tractStrList, loc="center left", bbox_to_anchor=(1.0, 0.5),
441 fancybox=True, shadow=True, fontsize=6, title_fontsize=6, title="tracts")
442 ax.add_artist(tractLegend)
443
444 titleStr = repo + "\n" + collections[0]
445 if len(collections) > 1:
446 for collection in collections[1:]:
447 titleStr += "\n" + collection
448 titleStr += "\nnVisit: {}".format(str(len(finalVisitList)))
449 if minOverlapFraction is not None:
450 titleStr += " (minOvlpFrac = {:.2f})".format(minOverlapFraction)
451 if len(includedBands) > 0:
452 titleStr += " bands: {}".format(str(includedBands).translate({ord(i): None for i in "[]\'"}))
453 if len(includedPhysicalFilters) > 0:
454 if len(includedPhysicalFilters[0]) > 9:
455 titleStr += "\n"
456 titleStr += " physical filters: {}".format(str(includedPhysicalFilters).translate(
457 {ord(i): None for i in "[]\'"}))
458 ax.set_title("{}".format(titleStr), fontsize=8)
459
460 fig = plt.gcf()
461 if saveFile is not None:
462 logger.info("Saving file in: %s", saveFile)
463 fig.savefig(saveFile, bbox_inches="tight", dpi=150)
464 else:
465 fig.show()
466
467
468def makeWhereInStr(parameterName, parameterList, parameterType):
469 """Create the string to be used in the where clause for registry lookup.
470 """
471 typeStr = "\'" if parameterType is str else ""
472 whereInStr = parameterName + " IN (" + typeStr + str(parameterList[0]) + typeStr
473 if len(parameterList) > 1:
474 for param in parameterList[1:]:
475 whereInStr += ", " + typeStr + str(param) + typeStr
476 whereInStr += ")"
477
478 return whereInStr
479
480
481def getTractLimitsDict(skymap, tractList):
482 """Return a dict containing tract limits needed for outline plotting.
483
484 Parameters
485 ----------
486 skymap : `lsst.skymap.BaseSkyMap`
487 The sky map used for this dataset. Used to obtain tract
488 parameters.
489 tractList : `list` [`int`]
490 The list of tract ids (as integers) for which to determine the
491 limits.
492
493 Returns
494 -------
495 tractLimitsDict : `dict` [`dict`]
496 A dictionary keyed on tract id. Each entry includes a `dict`
497 including the tract RA corners, Dec corners, and the tract center,
498 all in units of degrees. These are used for plotting the tract
499 outlines.
500 """
501 tractLimitsDict = {}
502 for tract in tractList:
503 tractInfo = skymap[tract]
504 tractBbox = tractInfo.outer_sky_polygon.getBoundingBox()
505 tractCenter = tractBbox.getCenter()
506 tractRa0 = (tractCenter[0] - tractBbox.getWidth() / 2).asDegrees()
507 tractRa1 = (tractCenter[0] + tractBbox.getWidth() / 2).asDegrees()
508 tractDec0 = (tractCenter[1] - tractBbox.getHeight() / 2).asDegrees()
509 tractDec1 = (tractCenter[1] + tractBbox.getHeight() / 2).asDegrees()
510 tractLimitsDict[tract] = {
511 "ras": [tractRa0, tractRa1, tractRa1, tractRa0, tractRa0],
512 "decs": [tractDec0, tractDec0, tractDec1, tractDec1, tractDec0],
513 "center": [tractCenter[0].asDegrees(), tractCenter[1].asDegrees()],
514 }
515
516 return tractLimitsDict
517
518
519def derivePlotLimits(limitsDict, raToDecLimitRatio=1.0, buffFrac=0.0):
520 """Derive the axis limits to encompass all points in limitsDict.
521
522 Parameters
523 ----------
524 limitsDict : `dict` [`dict`]
525 A dictionary keyed on any id. Each entry includes a `dict`
526 keyed on "ras" and "decs" including (at least the minimum
527 and maximum) RA and Dec values in units of degrees.
528 raToDecLimitRatio : `float`, optional
529 The aspect ratio between RA and Dec to set the plot limits to. This
530 is to namely to set this ratio to that of the focal plane (i.e. such
531 that a square detector appears as a square), but any aspect ratio can,
532 in principle, be requested.
533
534 Returns
535 -------
536 xlim, ylim : `tuple` [`float`]
537 Two tuples containing the derived min and max values for the x and
538 y-axis limits (in degrees), respectively.
539 """
540 xLimMin, yLimMin = 1e12, 1e12
541 xLimMax, yLimMax = -1e12, -1e12
542 for limitId, limits in limitsDict.items():
543 xLimMin = min(xLimMin, min(limits["ras"]))
544 xLimMax = max(xLimMax, max(limits["ras"]))
545 yLimMin = min(yLimMin, min(limits["decs"]))
546 yLimMax = max(yLimMax, max(limits["decs"]))
547 xDelta0 = xLimMax - xLimMin
548 yDelta0 = yLimMax - yLimMin
549
550 if raToDecLimitRatio == 1.0:
551 if xDelta0 > yDelta0:
552 xLimMin -= buffFrac*yDelta0
553 xLimMax += buffFrac*yDelta0
554 else:
555 yLimMin -= buffFrac*yDelta0
556 yLimMax += buffFrac*yDelta0
557 xLimMin, xLimMax, yLimMin, yLimMax = setLimitsToEqualRatio(xLimMin, xLimMax, yLimMin, yLimMax)
558 else:
559 xLimMin -= buffFrac*xDelta0
560 xLimMax += buffFrac*xDelta0
561 yLimMin -= buffFrac*yDelta0
562 yLimMax += buffFrac*yDelta0
563 xLimMin, xLimMax, yLimMin, yLimMax = setLimitsToEqualRatio(xLimMin, xLimMax, yLimMin, yLimMax)
564 xDelta = xLimMax - xLimMin
565 yDelta = yLimMax - yLimMin
566 if raToDecLimitRatio > 1.0:
567 if yDelta0 > xDelta:
568 xMid = xLimMin + 0.5*(xDelta)
569 xLimMin = xMid - 0.5*yDelta*raToDecLimitRatio
570 xLimMax = xMid + 0.5*yDelta*raToDecLimitRatio
571 else:
572 yMid = yLimMin + 0.5*(yDelta)
573 yLimMin = yMid - 0.5*xDelta/raToDecLimitRatio
574 yLimMax = yMid + 0.5*xDelta/raToDecLimitRatio
575 else:
576 if xDelta0 > yDelta0:
577 yMid = yLimMin + 0.5*(yDelta)
578 yLimMin = yMid - 0.5*xDelta/raToDecLimitRatio
579 yLimMax = yMid + 0.5*xDelta/raToDecLimitRatio
580 else:
581 xMid = xLimMin + 0.5*(xDelta)
582 xLimMin = xMid - 0.5*yDelta*raToDecLimitRatio
583 xLimMax = xMid + 0.5*yDelta*raToDecLimitRatio
584 xlim = xLimMax, xLimMin
585 ylim = yLimMin, yLimMax
586 return xlim, ylim
587
588
589def setLimitsToEqualRatio(xMin, xMax, yMin, yMax):
590 """For a given set of x/y min/max, redefine to have equal aspect ratio.
591
592 The limits are extended on both ends such that the central value is
593 preserved.
594
595 Parameters
596 ----------
597 xMin, xMax, yMin, yMax : `float`
598 The min/max values of the x/y ranges for which to match in dynamic
599 range while perserving the central values.
600
601 Returns
602 -------
603 xMin, xMax, yMin, yMax : `float`
604 The adjusted min/max values of the x/y ranges with equal aspect ratios.
605 """
606 xDelta = xMax - xMin
607 yDelta = yMax - yMin
608 deltaDiff = yDelta - xDelta
609 if deltaDiff > 0:
610 xMin -= 0.5 * deltaDiff
611 xMax += 0.5 * deltaDiff
612 elif deltaDiff < 0:
613 yMin -= 0.5 * abs(deltaDiff)
614 yMax += 0.5 * abs(deltaDiff)
615 return xMin, xMax, yMin, yMax
616
617
618def getDetRaDecCorners(ccdKey, ccdId, visit, visitSummary=None, butler=None, doLogWarn=True):
619 """Compute the RA/Dec corners lists for a given detector in a visit.
620 """
621 raCorners, decCorners = None, None
622 if visitSummary is not None:
623 row = visitSummary.find(ccdId)
624 if row is None:
625 if doLogWarn:
626 logger.warn("No row found for %d in visitSummary of visit %d. "
627 "Skipping and continuing...", ccdId, visit)
628 else:
629 raCorners = list(row["raCorners"])
630 decCorners = list(row["decCorners"])
631 else:
632 try:
633 dataId = {"visit": visit, ccdKey: ccdId}
634 wcs = butler.get("calexp.wcs", dataId)
635 bbox = butler.get("calexp.bbox", dataId)
636 raCorners, decCorners = bboxToRaDec(bbox, wcs)
637 except LookupError as e:
638 logger.warn("%s Skipping and continuing...", e)
639
640 return raCorners, decCorners
641
642
643def getBand(visitSummary=None, butler=None, visit=None):
644 """Determine band and physical filter for given visit.
645
646 Parameters
647 ----------
648 visitSummary : `lsst.afw.table.ExposureCatalog` or `None`, optional
649 The visitSummary table for the visit for which to determine the band.
650 butler : `lsst.daf.butler.Butler` or `None`, optional
651 The butler from which to look up the Dimension Records. Only needed
652 if ``visitSummary`` is `None`.
653 visit : `int` or `None, optional
654 The visit number for which to determine the band. Only needed
655 if ``visitSummary`` is `None`.
656
657 Returns
658 -------
659 band, physicalFilter : `str`
660 The band and physical filter for the given visit.
661 """
662 if visitSummary is not None:
663 band = visitSummary[0]["band"]
664 physicalFilter = visitSummary[0]["physical_filter"]
665 else:
666 record = list(butler.registry.queryDimensionRecords("band", visit=visit))[0]
667 band = record.name
668 record = list(butler.registry.queryDimensionRecords("physical_filter", visit=visit))[0]
669 physicalFilter = record.name
670 return band, physicalFilter
671
672
673if __name__ == "__main__":
674 parser = argparse.ArgumentParser()
675 parser.add_argument("repo", type=str,
676 help="URI or path to an existing data repository root or configuration file")
677 parser.add_argument("--collections", type=str, nargs="+",
678 help="Blank-space separated list of collection names for butler instantiation",
679 metavar=("COLLECTION1", "COLLECTION2"), required=True)
680 parser.add_argument("--skymapName", default=None, help="Name of the skymap for the collection")
681 parser.add_argument("--tracts", type=int, nargs="+", default=None,
682 help=("Blank-space separated list of tract outlines to constrain search for "
683 "visit overlap"), metavar=("TRACT1", "TRACT2"))
684 parser.add_argument("--visits", type=int, nargs="+", default=None,
685 help="Blank-space separated list of visits to include",
686 metavar=("VISIT1", "VISIT2"))
687 parser.add_argument("--physicalFilters", type=str, nargs="+", default=None,
688 help=("Blank-space separated list of physical filter names to constrain search for "
689 "visits"), metavar=("PHYSICAL_FILTER1", "PHYSICAL_FILTER2"))
690 parser.add_argument("--bands", type=str, nargs="+", default=None,
691 help=("Blank-space separated list of canonical band names to constrin search for "
692 "visits"), metavar=("BAND1", "BAND2"))
693 parser.add_argument("-c", "--ccds", nargs="+", type=int, default=None,
694 help="Blank-space separated list of CCDs to show", metavar=("CCD1", "CCD2"))
695 parser.add_argument("-p", "--showPatch", action="store_true", default=False,
696 help="Show the patch boundaries")
697 parser.add_argument("--saveFile", type=str, default="showVisitSkyMap.png",
698 help="Filename to write the plot to")
699 parser.add_argument("--ccdKey", default="detector", help="Data ID name of the CCD key")
700 parser.add_argument("--showCcds", action="store_true", default=False,
701 help="Show ccd ID numbers on output image")
702 parser.add_argument("--visitVetoFile", type=str, default=None,
703 help="Full path to single-column file containing a list of visits to veto")
704 parser.add_argument("--minOverlapFraction", type=float, default=None,
705 help="Minimum fraction of detectors that overlap any tract for visit to be included")
706 parser.add_argument("--trimToTracts", action="store_true", default=False,
707 help="Set plot limits based on extent of visits (as opposed to tracts) plotted?")
708 args = parser.parse_args()
709 main(args.repo, args.collections, skymapName=args.skymapName, tracts=args.tracts, visits=args.visits,
710 physicalFilters=args.physicalFilters, bands=args.bands, ccds=args.ccds, ccdKey=args.ccdKey,
711 showPatch=args.showPatch, saveFile=args.saveFile, showCcds=args.showCcds,
712 visitVetoFile=args.visitVetoFile, minOverlapFraction=args.minOverlapFraction,
713 trimToTracts=args.trimToTracts)
getBand(visitSummary=None, butler=None, visit=None)
derivePlotLimits(limitsDict, raToDecLimitRatio=1.0, buffFrac=0.0)
bboxToRaDec(bbox, wcs)
main(repo, collections, skymapName=None, tracts=None, visits=None, physicalFilters=None, bands=None, ccds=None, ccdKey="detector", showPatch=False, saveFile=None, showCcds=False, visitVetoFile=None, minOverlapFraction=None, trimToTracts=False)
getDetRaDecCorners(ccdKey, ccdId, visit, visitSummary=None, butler=None, doLogWarn=True)
getTractLimitsDict(skymap, tractList)
get_cmap(n, name="hsv")
makeWhereInStr(parameterName, parameterList, parameterType)
getValueAtPercentile(values, percentile=0.5)
setLimitsToEqualRatio(xMin, xMax, yMin, yMax)