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