lsst.skymap g91c88c04ca+f675d1467b
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
25import logging
26
27import matplotlib
28import matplotlib.patheffects as pathEffects
29import matplotlib.pyplot as plt
30import numpy as np
31from astropy import units
32from astropy.coordinates import SkyCoord
33from lsst.afw.cameraGeom import FIELD_ANGLE, FOCAL_PLANE, DetectorType
34from lsst.daf.butler import Butler
35from lsst.geom import Angle, Box2D, Point2D, SpherePoint, degrees
36from lsst.sphgeom import ConvexPolygon
37from matplotlib.legend import Legend
38
39logger = logging.getLogger("lsst.skymap.bin.showVisitSkyMap")
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(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="gist_rainbow"):
73 """Returns a function that maps each index in 0, 1, ..., n-1 to a distinct
74 RGB color.
75
76 Uses ``gist_rainbow`` by default: vivid, non-cyclic, so first and last
77 colors are visually distinct.
78 """
79 return matplotlib.colormaps[name].resampled(n)
80
81
82def queryImageDatasets(butler, whereStr, imageDatasetType=None):
83 """Query image datasets with support for current and legacy names."""
84 datasetTypes = (
85 [imageDatasetType]
86 if imageDatasetType is not None
87 else [
88 "visit_image",
89 "preliminary_visit_image",
90 "calexp",
91 ]
92 )
93 for datasetType in datasetTypes:
94 logger.info("Querying image dataset type: %s", datasetType)
95 dataRefs = list(butler.registry.queryDatasets(datasetType, where=whereStr).expanded())
96 if len(dataRefs) > 0:
97 return datasetType, dataRefs
98 logger.warning("No visit-level data found for any of the tested dataset types; unable to plot visits.")
99 return None, []
100
101
102def getVisitSummaryForVisit(butler, visit, visitSummaryDatasetType=None):
103 """Fetch visit summary for a visit, supporting legacy and newer names."""
104 datasetTypes = (
105 [visitSummaryDatasetType]
106 if visitSummaryDatasetType is not None
107 else [
108 "visit_summary",
109 "preliminary_visit_summary",
110 "visitSummary",
111 ]
112 )
113 for datasetType in datasetTypes:
114 try:
115 return butler.get(datasetType, visit=visit), datasetType
116 except LookupError:
117 pass
118 raise LookupError(f"Visit summary for visit {visit!r} not found in any of {datasetTypes!r}.")
119
120
122 repo,
123 collections,
124 skymapName=None,
125 tracts=None,
126 visits=None,
127 physicalFilters=None,
128 bands=None,
129 ccds=None,
130 ccdKey="detector",
131 showPatch=False,
132 showPatchSelectedTractsOnly=False,
133 saveFile=None,
134 showCcds=False,
135 visitVetoFile=None,
136 minOverlapFraction=None,
137 trimToTracts=False,
138 doUnscaledLimitRatio=False,
139 forceScaledLimitRatio=False,
140 imageDatasetType=None,
141 visitSummaryDatasetType=None,
142 dpi=150,
143):
144 if minOverlapFraction is not None and tracts is None:
145 raise RuntimeError("Must specify --tracts if --minOverlapFraction is set")
146 if dpi <= 0:
147 raise RuntimeError("--dpi must be > 0")
148 logger.info("Instantiating butler for repo '%s' with collections = %s", repo, collections)
149 butler = Butler.from_config(repo, collections=collections)
150 cameraDataset = butler.find_dataset("camera")
151 if cameraDataset is None:
152 raise RuntimeError("Could not find required dataset type: camera")
153 instrument = str(cameraDataset.dataId["instrument"])
154 detectorSkipList = []
155 # Make a guess at the skymapName if not provided
156 if skymapName is None:
157 if instrument == "HSC":
158 skymapName = "hsc_rings_v1"
159 detectorSkipList = [9] # detector 9 has long been dead for HSC
160 elif instrument == "LSSTCam-imSim":
161 skymapName = "DC2"
162 elif instrument == "LSSTComCamSim":
163 skymapName = "ops_rehersal_prep_2k_v1"
164 elif instrument == "LATISS":
165 skymapName = "latiss_v1"
166 elif instrument == "DECam":
167 skymapName = "decam_rings_v1"
168 else:
169 raise RuntimeError(
170 f"Unknown skymapName for instrument: {instrument}. Must specify --skymapName on command line."
171 )
172
173 logger.info("Using instrument = '%s' and skymapName = '%s'", instrument, skymapName)
174 camera = butler.get("camera", instrument=instrument)
175 skymap = butler.get("skyMap", instrument=instrument, skymap=skymapName)
176
177 whereStr = ""
178 if tracts is not None:
179 tractStr = makeWhereInStr("tract", tracts, int)
180 whereStr += tractStr
181
182 if physicalFilters is not None:
183 physicalFilterStr = makeWhereInStr("physical_filter", physicalFilters, str)
184 whereStr += " AND " + physicalFilterStr if len(whereStr) else " " + physicalFilterStr
185
186 if bands is not None:
187 bandStr = makeWhereInStr("band", bands, str)
188 whereStr += " AND " + bandStr if len(whereStr) else " " + bandStr
189
190 if len(whereStr) > 1:
191 whereStr = f"instrument='{instrument}' AND skymap='{skymapName}' AND {whereStr}"
192 else:
193 whereStr = f"instrument='{instrument}' AND skymap='{skymapName}'"
194 logger.info("Querying the butler with the following dataId where clause: %s", whereStr)
195
196 imageDatasetTypeUsed, imageDataRefs = queryImageDatasets(
197 butler, whereStr, imageDatasetType=imageDatasetType
198 )
199 logger.info("Using image dataset type: %s", imageDatasetTypeUsed)
200
201 visitSummaryDatasetTypeUsed = visitSummaryDatasetType
202
203 visitVetoList = []
204 if visitVetoFile is not None:
205 with open(visitVetoFile) as f:
206 content = f.readlines()
207 visitVetoList = [int(visit.strip()) for visit in content]
208
209 if visits is None:
210 visits = []
211 for dataRef in imageDataRefs:
212 visit = dataRef.dataId.visit.id
213 if visit not in visits and visit not in visitVetoList:
214 visits.append(visit)
215 visits.sort()
216 logger.info("List of visits (N=%d) satisfying where and veto clauses: %s", len(visits), visits)
217 else:
218 if len(visitVetoList) > 1:
219 visitListTemp = visits.copy()
220 for visit in visitListTemp:
221 if visit in visitVetoList:
222 visits.remove(visit)
223 logger.info("List of visits (N=%d) excluding veto list: %s}", len(visits), visits)
224 logger.info("List of visits (N=%d): %s", len(visits), visits)
225
226 if len(visits) > 0:
227 try:
228 _, visitSummaryDatasetTypeUsed = getVisitSummaryForVisit(
229 butler, visits[0], visitSummaryDatasetType=visitSummaryDatasetType
230 )
231 logger.info("Using visit summary dataset type: %s", visitSummaryDatasetTypeUsed)
232 except LookupError:
233 if visitSummaryDatasetType is None:
234 logger.info(
235 "No visit summary dataset type found for auto-detection; "
236 "will fall back to detector-level WCS/bbox lookups."
237 )
238 else:
239 logger.info(
240 "Configured visit summary dataset type '%s' was not found for sampled visit; "
241 "will fall back to detector-level WCS/bbox lookups.",
242 visitSummaryDatasetType,
243 )
244
245 ccdIdList = []
246 for ccd in camera:
247 ccdId = ccd.getId()
248 if (
249 (ccds is None or ccdId in ccds)
250 and ccd.getType() == DetectorType.SCIENCE
251 and ccdId not in detectorSkipList
252 ):
253 ccdIdList.append(ccdId)
254 ccdIdList.sort()
255 nDetTot = len(ccdIdList)
256 missingVisitSummaryRows = {}
257 nonFiniteDetectorCorners = {}
258
259 visitIncludeList = []
260 # Determine the fraction of detectors that overlap any tract under
261 # consideration. If this fraction does not exceed minOverlapFraction,
262 # skip this visit.
263 if minOverlapFraction is not None:
264 for i_v, visit in enumerate(visits):
265 ccdOverlapList = []
266 try:
267 visitSummary, _ = getVisitSummaryForVisit(
268 butler, visit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
269 )
270 except LookupError as e:
271 logger.warning("%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
272 visitSummary = None
273 if tracts is not None:
274 for tract in tracts:
275 tractInfo = skymap[tract]
276 sphCorners = tractInfo.wcs.pixelToSky(Box2D(tractInfo.bbox).getCorners())
277 tractConvexHull = ConvexPolygon.convexHull([coord.getVector() for coord in sphCorners])
278 for ccdId in ccdIdList:
279 if ccdId not in ccdOverlapList:
280 raCorners, decCorners = getDetRaDecCorners(
281 ccdKey,
282 ccdId,
283 visit,
284 visitSummary=visitSummary,
285 butler=butler,
286 imageDatasetType=imageDatasetTypeUsed,
287 doLogWarn=False,
288 )
289 if raCorners is not None and decCorners is not None:
290 detSphCorners = []
291 for ra, dec in zip(raCorners, decCorners):
292 pt = SpherePoint(Angle(ra, degrees), Angle(dec, degrees))
293 detSphCorners.append(pt)
294 detConvexHull = ConvexPolygon.convexHull(
295 [coord.getVector() for coord in detSphCorners]
296 )
297 if tractConvexHull.contains(detConvexHull):
298 ccdOverlapList.append(ccdId)
299
300 if len(ccdOverlapList) / nDetTot >= minOverlapFraction:
301 break
302 if len(ccdOverlapList) / nDetTot < minOverlapFraction:
303 logger.info(
304 "Fraction of detectors overlapping any tract for visit %d (%.2f) < "
305 "minimum required (%.2f). Skipping visit...",
306 visit,
307 len(ccdOverlapList) / nDetTot,
308 minOverlapFraction,
309 )
310 else:
311 if visit not in visitIncludeList:
312 visitIncludeList.append(visit)
313 else:
314 visitIncludeList = visits
315
316 # Draw the CCDs.
317 ras, decs = [], []
318 bboxesPlotted = []
319 cmap = get_cmap(len(visitIncludeList))
320 alphaEdge = 0.7
321 maxVisitForLegend = 20
322 finalVisitList = []
323 finalVisitColorIndices = []
324 includedBands = []
325 includedPhysicalFilters = []
326 for i_v, visit in enumerate(visitIncludeList):
327 print("Working on visit %d [%d of %d]" % (visit, i_v + 1, len(visitIncludeList)), end="\r")
328 inLegend = False
329 color = cmap(i_v)
330 fillKwargs = {"fill": False, "alpha": alphaEdge, "facecolor": None, "edgecolor": color, "lw": 0.6}
331 try:
332 visitSummary, _ = getVisitSummaryForVisit(
333 butler, visit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
334 )
335 except Exception as e:
336 logger.warning("%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
337 visitSummary = None
338
339 band, physicalFilter = getBand(visitSummary=visitSummary, butler=butler, visit=visit)
340 if band not in includedBands:
341 includedBands.append(band)
342 if physicalFilter not in includedPhysicalFilters:
343 includedPhysicalFilters.append(physicalFilter)
344
345 for ccdId in ccdIdList:
346 raCorners, decCorners = getDetRaDecCorners(
347 ccdKey,
348 ccdId,
349 visit,
350 visitSummary=visitSummary,
351 butler=butler,
352 imageDatasetType=imageDatasetTypeUsed,
353 missingVisitSummaryRows=missingVisitSummaryRows,
354 )
355 if raCorners is not None and decCorners is not None:
356 cornerPairs = list(zip(raCorners, decCorners))
357 finiteCornerPairs = [
358 (ra, dec) for ra, dec in cornerPairs if np.isfinite(ra) and np.isfinite(dec)
359 ]
360 if len(finiteCornerPairs) < len(cornerPairs):
361 nonFiniteDetectorCorners.setdefault(visit, set()).add(ccdId)
362 # Skip plotting malformed polygons for this detector.
363 continue
364
365 ras += raCorners
366 decs += decCorners
367 if not inLegend and len(visitIncludeList) <= maxVisitForLegend:
368 plt.fill(raCorners, decCorners, label=str(visit), **fillKwargs)
369 inLegend = True
370 else:
371 plt.fill(raCorners, decCorners, **fillKwargs)
372 plt.fill(raCorners, decCorners, fill=True, alpha=alphaEdge / 4, color=color, edgecolor=color)
373 if visit not in finalVisitList:
374 finalVisitList.append(visit)
375 finalVisitColorIndices.append(i_v)
376 # add CCD serial numbers
377 if showCcds:
378 overlapFrac = 0.2
379 deltaRa = max(raCorners) - min(raCorners)
380 deltaDec = max(decCorners) - min(decCorners)
381 minPoint = Point2D(
382 min(raCorners) + overlapFrac * deltaRa, min(decCorners) + overlapFrac * deltaDec
383 )
384 maxPoint = Point2D(
385 max(raCorners) - overlapFrac * deltaRa, max(decCorners) - overlapFrac * deltaDec
386 )
387 # Use doubles in Box2D to check overlap
388 bboxDouble = Box2D(minPoint, maxPoint)
389 overlaps = [not bboxDouble.overlaps(otherBbox) for otherBbox in bboxesPlotted]
390 if all(overlaps):
391 plt.text(
392 getValueAtPercentile(raCorners),
393 getValueAtPercentile(decCorners),
394 str(ccdId),
395 fontsize=6,
396 ha="center",
397 va="center",
398 color="darkblue",
399 )
400 bboxesPlotted.append(bboxDouble)
401
402 if visit in missingVisitSummaryRows:
403 missingDetectors = sorted(missingVisitSummaryRows[visit])
404 logger.warning(
405 "visit summary table for visit %d missing detector rows: %s",
406 visit,
407 missingDetectors,
408 )
409
410 if visit in nonFiniteDetectorCorners:
411 badDetectors = sorted(nonFiniteDetectorCorners[visit])
412 logger.warning(
413 "Non-finite detector corners for visit %d (N=%d): %s",
414 visit,
415 len(badDetectors),
416 badDetectors,
417 )
418
419 logger.info(
420 "Final list of visits (N=%d) satisfying where and minOverlapFraction clauses: %s",
421 len(finalVisitList),
422 finalVisitList,
423 )
424
425 raToDecLimitRatio = None
426 if len(ras) > 0:
427 finiteCoordPairs = [(ra, dec) for ra, dec in zip(ras, decs) if np.isfinite(ra) and np.isfinite(dec)]
428 droppedCoordCount = len(ras) - len(finiteCoordPairs)
429 if droppedCoordCount > 0:
430 logger.warning(
431 "Dropping %d non-finite detector corner coordinates before tract lookup",
432 droppedCoordCount,
433 )
434 if len(finiteCoordPairs) == 0:
435 if tracts is not None:
436 logger.info(
437 "No finite detector corners found, but --tracts list was provided, so will go ahead and "
438 "plot the empty tracts."
439 )
440 tractList = tracts
441 trimToTracts = True
442 else:
443 raise RuntimeError("No finite detector corners available for tract lookup")
444 else:
445 ras, decs = zip(*finiteCoordPairs)
446 ras = list(ras)
447 decs = list(decs)
448 tractList = list(set(skymap.findTractIdArray(ras, decs, degrees=True)))
449 minVisitRa, maxVisitRa = min(ras), max(ras)
450 minVisitDec, maxVisitDec = min(decs), max(decs)
451 raVisitDiff = maxVisitRa - minVisitRa
452 decVisitDiff = maxVisitDec - minVisitDec
453 midVisitRa = minVisitRa + 0.5 * raVisitDiff
454 midVisitDec = minVisitDec + 0.5 * decVisitDiff
455 midRa = np.atleast_1d((midVisitRa * units.deg).to(units.radian).value).astype(np.float64)
456 midDec = np.atleast_1d((midVisitDec * units.deg).to(units.radian).value).astype(np.float64)
457 midSkyCoord = SkyCoord(midVisitRa * units.deg, midVisitDec * units.deg)
458 else:
459 if tracts is not None:
460 logger.info(
461 "No detectors were found, but --tracts list was provided, so will go ahead and "
462 "plot the empty tracts."
463 )
464 tractList = tracts
465 trimToTracts = True
466 else:
467 raise RuntimeError(
468 "No data to plot (if you want to plot empty tracts, include them as "
469 "a blank-space separated list to the --tracts option)."
470 )
471 tractList, invalidTracts = sanitizeTractList(skymap, tractList)
472 if len(invalidTracts) > 0:
473 logger.warning("Ignoring invalid tract ids: %s", invalidTracts)
474 if len(tractList) == 0:
475 raise RuntimeError("No valid tract ids found for plotting")
476 logger.info("List of tracts overlapping data: %s", tractList)
477 tractLimitsDict = getTractLimitsDict(skymap, tractList)
478
479 if forceScaledLimitRatio:
480 doUnscaledLimitRatio = False
481 else:
482 # Roughly compute radius in degrees of a single detector. If RA/Dec
483 # coverage is more than 30 times the detector radius, and the RA/Dec
484 # limit ratio is greater than raDecScaleThresh, don't try to scale to
485 # detector coords.
486 radiusMm = camera.computeMaxFocalPlaneRadius()
487 fpRadiusPt = Point2D(radiusMm, radiusMm)
488 focalPlaneToFieldAngle = camera.getTransformMap().getTransform(FOCAL_PLANE, FIELD_ANGLE)
489 fpRadiusDeg = np.rad2deg(focalPlaneToFieldAngle.applyForward(fpRadiusPt))[0]
490 detectorRadiusDeg = fpRadiusDeg / np.sqrt(len(camera))
491
492 if trimToTracts:
493 xLimMin, xLimMax, yLimMin, yLimMax = getMinMaxLimits(tractLimitsDict)
494 xDelta0 = xLimMax - xLimMin
495 yDelta0 = yLimMax - yLimMin
496 else:
497 xDelta0 = raVisitDiff
498 yDelta0 = decVisitDiff
499 yLimMin = minVisitDec
500 yLimMax = maxVisitDec
501 raDecScaleThresh = 1.5 # This is a best guess with current testing.
502 if (
503 (xDelta0 / yDelta0 > raDecScaleThresh or yDelta0 / xDelta0 > raDecScaleThresh)
504 and max(xDelta0, yDelta0) > 70 * detectorRadiusDeg
505 and yLimMin < 75.0
506 and yLimMax > -75.0
507 ):
508 logger.info(
509 "Sky coverage is large (and not too close to a pole), so not scaling to detector coords."
510 )
511 doUnscaledLimitRatio = True
512
513 if not doUnscaledLimitRatio:
514 # Find a detector that contains the mid point in RA/Dec (or the closest
515 # one) to set the plot aspect ratio.
516 minDistToMidCoord = 1e12
517 minSepVisit = None
518 minSepCcdId = None
519 for i_v, visit in enumerate(visits):
520 try:
521 visitSummary, _ = getVisitSummaryForVisit(
522 butler, visit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
523 )
524 except Exception as e:
525 logger.warning("%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
526 visitSummary = None
527 for ccdId in ccdIdList:
528 raCorners, decCorners = getDetRaDecCorners(
529 ccdKey,
530 ccdId,
531 visit,
532 visitSummary=visitSummary,
533 butler=butler,
534 imageDatasetType=imageDatasetTypeUsed,
535 doLogWarn=False,
536 )
537 if raCorners is not None and decCorners is not None:
538 detSphCorners = []
539 for ra, dec in zip(raCorners, decCorners):
540 pt = SpherePoint(Angle(ra, degrees), Angle(dec, degrees))
541 detSphCorners.append(pt)
542 ptSkyCoord = SkyCoord(ra * units.deg, dec * units.deg)
543 separation = (midSkyCoord.separation(ptSkyCoord)).degree
544 if separation < minDistToMidCoord:
545 minSepVisit = visit
546 minSepCcdId = ccdId
547 minDistToMidCoord = separation
548 detConvexHull = ConvexPolygon([coord.getVector() for coord in detSphCorners])
549 if detConvexHull.contains(midRa, midDec) and raToDecLimitRatio is None:
550 logger.info(
551 "visit/det overlapping plot coord mid point in RA/Dec: %d %d", visit, ccdId
552 )
553 raToDecLimitRatio = (max(raCorners) - min(raCorners)) / (
554 max(decCorners) - min(decCorners)
555 )
556 det = camera[ccdId]
557 width = det.getBBox().getWidth()
558 height = det.getBBox().getHeight()
559 if raToDecLimitRatio > 1.0:
560 raToDecLimitRatio /= max(height / width, width / height)
561 else:
562 if raToDecLimitRatio < 1.0:
563 raToDecLimitRatio *= max(height / width, width / height)
564 break
565 if raToDecLimitRatio is not None:
566 break
567
568 if raToDecLimitRatio is None and minSepVisit is not None:
569 try:
570 visitSummary, _ = getVisitSummaryForVisit(
571 butler, minSepVisit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
572 )
573 except Exception as e:
574 logger.warning("%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
575 visitSummary = None
576 raCorners, decCorners = getDetRaDecCorners(
577 ccdKey,
578 minSepCcdId,
579 minSepVisit,
580 visitSummary=visitSummary,
581 butler=butler,
582 imageDatasetType=imageDatasetTypeUsed,
583 doLogWarn=False,
584 )
585 for ra, dec in zip(raCorners, decCorners):
586 pt = SpherePoint(Angle(ra, degrees), Angle(dec, degrees))
587 detSphCorners.append(pt)
588 detConvexHull = ConvexPolygon([coord.getVector() for coord in detSphCorners])
589 logger.info(
590 "visit/det closest to plot coord mid point in RA/Dec (none actually overlap it): %d %d",
591 minSepVisit,
592 minSepCcdId,
593 )
594 raToDecLimitRatio = (max(raCorners) - min(raCorners)) / (max(decCorners) - min(decCorners))
595 det = camera[minSepCcdId]
596 width = det.getBBox().getWidth()
597 height = det.getBBox().getHeight()
598 if raToDecLimitRatio > 1.0:
599 raToDecLimitRatio /= max(height / width, width / height)
600 else:
601 if raToDecLimitRatio < 1.0:
602 raToDecLimitRatio *= max(height / width, width / height)
603
604 if trimToTracts:
605 xlim, ylim = derivePlotLimits(tractLimitsDict, raToDecLimitRatio=raToDecLimitRatio, buffFrac=0.04)
606 else:
607 visitLimitsDict = {"allVisits": {"ras": [minVisitRa, maxVisitRa], "decs": [minVisitDec, maxVisitDec]}}
608 xlim, ylim = derivePlotLimits(visitLimitsDict, raToDecLimitRatio=raToDecLimitRatio, buffFrac=0.04)
609
610 if doUnscaledLimitRatio:
611 boxAspectRatio = abs((ylim[1] - ylim[0]) / (xlim[1] - xlim[0]))
612 else:
613 boxAspectRatio = 1.0
614
615 # Draw the skymap.
616 alpha0 = 1.0
617 tractHandleList = []
618 tractStrList = []
619 if tracts is not None:
620 tractOutlineList = list(set(tracts + tractList))
621 else:
622 tractOutlineList = tractList
623 tractOutlineList.sort()
624 selectedPatchTracts = set(tracts) if tracts is not None else None
625 if showPatchSelectedTractsOnly and selectedPatchTracts is None:
626 logger.info(
627 "--showPatchSelectedTractsOnly was set without --tracts; showing patches for all plotted tracts."
628 )
629 logger.info("List of tract outlines being plotted: %s", tractOutlineList)
630 for i_t, tract in enumerate(tractOutlineList):
631 alpha = max(0.1, alpha0 - i_t * 1.0 / len(tractOutlineList))
632 tractInfo = skymap[tract]
633 tCenter = tractInfo.ctr_coord
634 tCenterRa = tCenter.getRa().asDegrees()
635 tCenterDec = tCenter.getDec().asDegrees()
636 fracDeltaX = 0.02 * abs((xlim[1] - xlim[0]))
637 fracDeltaY = 0.02 * abs((ylim[1] - ylim[0]))
638 if (
639 xlim[1] + fracDeltaX < tCenterRa < xlim[0] - fracDeltaX
640 and ylim[0] + fracDeltaY < tCenterDec < ylim[1] - fracDeltaY
641 ):
642 if len(tractOutlineList) > 1 or not showPatch:
643 if not showPatch:
644 plt.text(tCenterRa, tCenterDec, tract, fontsize=7, alpha=alpha, ha="center", va="center")
645 else:
646 plt.text(
647 tCenterRa,
648 tCenterDec,
649 tract,
650 fontsize=7,
651 alpha=1,
652 color="white",
653 path_effects=[pathEffects.withStroke(linewidth=3, foreground="black")],
654 fontweight=500,
655 ha="center",
656 va="center",
657 zorder=5,
658 )
659 ra, dec = bboxToRaDec(tractInfo.bbox, tractInfo.getWcs())
660 plt.fill(ra, dec, fill=False, edgecolor="k", lw=1, linestyle="dashed", alpha=alpha)
661 tractArtist = matplotlib.patches.Patch(fill=False, edgecolor="k", linestyle="dashed", alpha=alpha)
662 tractHandleList.append(tractArtist)
663 tractStrList.append(str(tract))
664 if showPatch and (
665 not showPatchSelectedTractsOnly or selectedPatchTracts is None or tract in selectedPatchTracts
666 ):
667 patchColor = "k"
668 for patch in tractInfo:
669 ra, dec = bboxToRaDec(patch.getInnerBBox(), tractInfo.getWcs())
670 plt.fill(
671 ra, dec, fill=False, edgecolor=patchColor, lw=0.5, linestyle=(0, (5, 6)), alpha=alpha
672 )
673 if (
674 xlim[1] + fracDeltaX < getValueAtPercentile(ra) < xlim[0] - fracDeltaX
675 and ylim[0] + fracDeltaY < getValueAtPercentile(dec) < ylim[1] - fracDeltaY
676 ):
677 plt.text(
680 str(patch.sequential_index),
681 fontsize=5,
682 color=patchColor,
683 ha="center",
684 va="center",
685 alpha=alpha,
686 )
687
688 # Add labels and save.
689 ax = plt.gca()
690 ax.set_xlim(xlim)
691 ax.set_ylim(ylim)
692 ax.set_box_aspect(boxAspectRatio)
693 if abs(xlim[1] > 99.99):
694 ax.tick_params("x", labelrotation=45, pad=0, labelsize=8)
695 else:
696 ax.tick_params("x", labelrotation=0, pad=0, labelsize=8)
697 ax.tick_params("y", labelsize=8)
698 ax.set_xlabel("RA (deg)", fontsize=9)
699 ax.set_ylabel("Dec (deg)", fontsize=9)
700
701 if len(visitIncludeList) > maxVisitForLegend:
702 nVisits = len(finalVisitList)
703 nz = matplotlib.colors.Normalize(vmin=0, vmax=len(visitIncludeList) - 1)
704 cax, _ = matplotlib.colorbar.make_axes(plt.gca(), pad=0.03)
705 cax.tick_params(labelsize=7)
706 cb = matplotlib.colorbar.ColorbarBase(cax, cmap=cmap, norm=nz, alpha=alphaEdge)
707 nTicks = min(8, nVisits)
708 tickPositions = [finalVisitColorIndices[int(round(x))] for x in np.linspace(0, nVisits - 1, nTicks)]
709 cb.set_ticks(tickPositions)
710 cb.set_ticklabels([str(finalVisitList[int(round(x))]) for x in np.linspace(0, nVisits - 1, nTicks)])
711 cb.ax.yaxis.get_offset_text().set_fontsize(7)
712 cb.set_label("visit", rotation=-90, labelpad=13, fontsize=9)
713 tractLegend = Legend(
714 ax,
715 tractHandleList,
716 tractStrList,
717 loc="upper right",
718 fancybox=True,
719 shadow=True,
720 fontsize=5,
721 title_fontsize=6,
722 title="tracts",
723 )
724 ax.add_artist(tractLegend)
725 else:
726 if len(visitIncludeList) > 0:
727 xBboxAnchor = min(1.25, max(1.03, boxAspectRatio * 1.15))
728 ax.legend(
729 loc="center left",
730 bbox_to_anchor=(xBboxAnchor, 0.5),
731 fancybox=True,
732 shadow=True,
733 fontsize=6,
734 title_fontsize=6,
735 title="visits",
736 )
737 # Create the second legend and add the artist manually.
738 tractLegend = Legend(
739 ax,
740 tractHandleList,
741 tractStrList,
742 loc="center left",
743 bbox_to_anchor=(1.0, 0.5),
744 fancybox=True,
745 shadow=True,
746 fontsize=6,
747 title_fontsize=6,
748 title="tracts",
749 )
750 ax.add_artist(tractLegend)
751
752 titleStr = repo + "\n" + collections[0]
753 if len(collections) > 1:
754 for collection in collections[1:]:
755 titleStr += "\n" + collection
756 titleStr += "\nnVisit: {}".format(str(len(finalVisitList)))
757 if minOverlapFraction is not None:
758 titleStr += " (minOverlapFraction = {:.2f})".format(minOverlapFraction)
759 if len(includedBands) > 0:
760 titleStr += ", bands: {}".format(str(includedBands).translate({ord(i): None for i in "[]'"}))
761 if len(includedPhysicalFilters) > 0:
762 if len(includedPhysicalFilters[0]) > 9:
763 titleStr += "\n"
764 else:
765 titleStr += ","
766 titleStr += " physical filters: {}".format(
767 str(includedPhysicalFilters).translate({ord(i): None for i in "[]'"})
768 )
769 ax.set_title("{}".format(titleStr), fontsize=8)
770
771 fig = plt.gcf()
772 if boxAspectRatio > 1.0:
773 minInches = max(4.0, 0.3 * abs(xlim[1] - xlim[0]))
774 xInches = minInches
775 yInches = min(120.0, boxAspectRatio * minInches)
776 fig.set_size_inches(xInches, yInches)
777 if boxAspectRatio < 1.0:
778 minInches = max(4.0, 0.3 * abs(ylim[1] - ylim[0]))
779 xInches = min(120.0, minInches / boxAspectRatio)
780 yInches = minInches
781 fig.set_size_inches(xInches, yInches)
782 if saveFile is not None:
783 logger.info("Saving file in: %s", saveFile)
784 fig.savefig(saveFile, bbox_inches="tight", dpi=dpi)
785 else:
786 fig.show()
787
788
789def makeWhereInStr(parameterName, parameterList, parameterType):
790 """Create the string to be used in the where clause for registry lookup."""
791 typeStr = "'" if parameterType is str else ""
792 whereInStr = parameterName + " IN (" + typeStr + str(parameterList[0]) + typeStr
793 if len(parameterList) > 1:
794 for param in parameterList[1:]:
795 whereInStr += ", " + typeStr + str(param) + typeStr
796 whereInStr += ")"
797
798 return whereInStr
799
800
801def sanitizeTractList(skymap, tractList):
802 """Split tract ids into valid and invalid entries for the given skymap."""
803 validTracts = []
804 invalidTracts = []
805 for tract in tractList:
806 tractInt = int(tract)
807 try:
808 _ = skymap[tractInt]
809 except IndexError:
810 invalidTracts.append(tractInt)
811 continue
812 if tractInt not in validTracts:
813 validTracts.append(tractInt)
814 validTracts.sort()
815 invalidTracts.sort()
816 return validTracts, invalidTracts
817
818
819def getTractLimitsDict(skymap, tractList):
820 """Return a dict containing tract limits needed for outline plotting.
821
822 Parameters
823 ----------
824 skymap : `lsst.skymap.BaseSkyMap`
825 The sky map used for this dataset. Used to obtain tract
826 parameters.
827 tractList : `list` [`int`]
828 The list of tract ids (as integers) for which to determine the
829 limits.
830
831 Returns
832 -------
833 tractLimitsDict : `dict` [`dict`]
834 A dictionary keyed on tract id. Each entry includes a `dict`
835 including the tract RA corners, Dec corners, and the tract center,
836 all in units of degrees. These are used for plotting the tract
837 outlines.
838 """
839 tractLimitsDict = {}
840 for tract in tractList:
841 tractInfo = skymap[tract]
842 tractBbox = tractInfo.outer_sky_polygon.getBoundingBox()
843 tractCenter = tractBbox.getCenter()
844 tractRa0 = (tractCenter[0] - tractBbox.getWidth() / 2).asDegrees()
845 tractRa1 = (tractCenter[0] + tractBbox.getWidth() / 2).asDegrees()
846 tractDec0 = (tractCenter[1] - tractBbox.getHeight() / 2).asDegrees()
847 tractDec1 = (tractCenter[1] + tractBbox.getHeight() / 2).asDegrees()
848 tractLimitsDict[tract] = {
849 "ras": [tractRa0, tractRa1, tractRa1, tractRa0, tractRa0],
850 "decs": [tractDec0, tractDec0, tractDec1, tractDec1, tractDec0],
851 "center": [tractCenter[0].asDegrees(), tractCenter[1].asDegrees()],
852 }
853
854 return tractLimitsDict
855
856
857def getMinMaxLimits(limitsDict):
858 """Derive the min and max axis limits of points in limitsDict.
859
860 Parameters
861 ----------
862 limitsDict : `dict` [`dict`]
863 A dictionary keyed on any id. Each entry includes a `dict`
864 keyed on "ras" and "decs" including (at least the minimum
865 and maximum) RA and Dec values in units of degrees.
866
867 Returns
868 -------
869 xLimMin, xLimMax, yLimMin, yLimMax : `tuple` [`float`]
870 The min and max values for the x and y-axis limits, respectively.
871 """
872 xLimMin, yLimMin = 1e12, 1e12
873 xLimMax, yLimMax = -1e12, -1e12
874 for limitId, limits in limitsDict.items():
875 xLimMin = min(xLimMin, min(limits["ras"]))
876 xLimMax = max(xLimMax, max(limits["ras"]))
877 yLimMin = min(yLimMin, min(limits["decs"]))
878 yLimMax = max(yLimMax, max(limits["decs"]))
879
880 return xLimMin, xLimMax, yLimMin, yLimMax
881
882
883def derivePlotLimits(limitsDict, raToDecLimitRatio=1.0, buffFrac=0.0):
884 """Derive the axis limits to encompass all points in limitsDict.
885
886 Parameters
887 ----------
888 limitsDict : `dict` [`dict`]
889 A dictionary keyed on any id. Each entry includes a `dict`
890 keyed on "ras" and "decs" including (at least the minimum
891 and maximum) RA and Dec values in units of degrees.
892 raToDecLimitRatio : `float`, optional
893 The aspect ratio between RA and Dec to set the plot limits to. This
894 is to namely to set this ratio to that of the focal plane (i.e. such
895 that a square detector appears as a square), but any aspect ratio can,
896 in principle, be requested.
897
898 Returns
899 -------
900 xlim, ylim : `tuple` [`float`]
901 Two tuples containing the derived min and max values for the x and
902 y-axis limits (in degrees), respectively.
903 """
904 xLimMin, xLimMax, yLimMin, yLimMax = getMinMaxLimits(limitsDict)
905
906 xDelta0 = xLimMax - xLimMin
907 yDelta0 = yLimMax - yLimMin
908 if raToDecLimitRatio is None:
909 padFrac = 0.05
910 xlim = xLimMax + padFrac * xDelta0, xLimMin - padFrac * xDelta0
911 ylim = yLimMin - padFrac * yDelta0, yLimMax + padFrac * yDelta0
912 return xlim, ylim
913
914 if raToDecLimitRatio == 1.0:
915 if xDelta0 > yDelta0:
916 xLimMin -= buffFrac * yDelta0
917 xLimMax += buffFrac * yDelta0
918 else:
919 yLimMin -= buffFrac * yDelta0
920 yLimMax += buffFrac * yDelta0
921 xLimMin, xLimMax, yLimMin, yLimMax = setLimitsToEqualRatio(xLimMin, xLimMax, yLimMin, yLimMax)
922 else:
923 xLimMin -= buffFrac * xDelta0
924 xLimMax += buffFrac * xDelta0
925 yLimMin -= buffFrac * yDelta0
926 yLimMax += buffFrac * yDelta0
927 xLimMin, xLimMax, yLimMin, yLimMax = setLimitsToEqualRatio(xLimMin, xLimMax, yLimMin, yLimMax)
928 xDelta = xLimMax - xLimMin
929 yDelta = yLimMax - yLimMin
930 if raToDecLimitRatio > 1.0:
931 if yDelta0 > xDelta:
932 xMid = xLimMin + 0.5 * (xDelta)
933 xLimMin = xMid - 0.5 * yDelta * raToDecLimitRatio
934 xLimMax = xMid + 0.5 * yDelta * raToDecLimitRatio
935 else:
936 yMid = yLimMin + 0.5 * (yDelta)
937 yLimMin = yMid - 0.5 * xDelta / raToDecLimitRatio
938 yLimMax = yMid + 0.5 * xDelta / raToDecLimitRatio
939 else:
940 if xDelta0 > yDelta0:
941 yMid = yLimMin + 0.5 * (yDelta)
942 yLimMin = yMid - 0.5 * xDelta / raToDecLimitRatio
943 yLimMax = yMid + 0.5 * xDelta / raToDecLimitRatio
944 else:
945 xMid = xLimMin + 0.5 * (xDelta)
946 xLimMin = xMid - 0.5 * yDelta * raToDecLimitRatio
947 xLimMax = xMid + 0.5 * yDelta * raToDecLimitRatio
948 xlim = xLimMax, xLimMin
949 ylim = yLimMin, yLimMax
950 return xlim, ylim
951
952
953def setLimitsToEqualRatio(xMin, xMax, yMin, yMax):
954 """For a given set of x/y min/max, redefine to have equal aspect ratio.
955
956 The limits are extended on both ends such that the central value is
957 preserved.
958
959 Parameters
960 ----------
961 xMin, xMax, yMin, yMax : `float`
962 The min/max values of the x/y ranges for which to match in dynamic
963 range while preserving the central values.
964
965 Returns
966 -------
967 xMin, xMax, yMin, yMax : `float`
968 The adjusted min/max values of the x/y ranges with equal aspect ratios.
969 """
970 xDelta = xMax - xMin
971 yDelta = yMax - yMin
972 deltaDiff = yDelta - xDelta
973 if deltaDiff > 0:
974 xMin -= 0.5 * deltaDiff
975 xMax += 0.5 * deltaDiff
976 elif deltaDiff < 0:
977 yMin -= 0.5 * abs(deltaDiff)
978 yMax += 0.5 * abs(deltaDiff)
979 return xMin, xMax, yMin, yMax
980
981
983 ccdKey,
984 ccdId,
985 visit,
986 visitSummary=None,
987 butler=None,
988 imageDatasetType="calexp",
989 doLogWarn=True,
990 missingVisitSummaryRows=None,
991):
992 """Compute the RA/Dec corners lists for a given detector in a visit."""
993 raCorners, decCorners = None, None
994 if visitSummary is not None:
995 row = visitSummary.find(ccdId)
996 if row is None:
997 if doLogWarn:
998 if missingVisitSummaryRows is not None:
999 missingVisitSummaryRows.setdefault(visit, set()).add(ccdId)
1000 else:
1001 logger.warning(
1002 "No row found for %d in visit summary table for visit %d. Skipping and continuing...",
1003 ccdId,
1004 visit,
1005 )
1006 else:
1007 raCorners = list(row["raCorners"])
1008 decCorners = list(row["decCorners"])
1009 else:
1010 if butler is None:
1011 raise RuntimeError("A butler instance is required when visitSummary is not provided")
1012 try:
1013 dataId = {"visit": visit, ccdKey: ccdId}
1014 wcs = butler.get(f"{imageDatasetType}.wcs", dataId)
1015 bbox = butler.get(f"{imageDatasetType}.bbox", dataId)
1016 raCorners, decCorners = bboxToRaDec(bbox, wcs)
1017 except LookupError as e:
1018 logger.warning("%s Skipping and continuing...", e)
1019
1020 return raCorners, decCorners
1021
1022
1023def getBand(visitSummary=None, butler=None, visit=None):
1024 """Determine band and physical filter for given visit.
1025
1026 Parameters
1027 ----------
1028 visitSummary : `lsst.afw.table.ExposureCatalog` or `None`, optional
1029 The visitSummary table for the visit for which to determine the band.
1030 butler : `lsst.daf.butler.Butler` or `None`, optional
1031 The butler from which to look up the Dimension Records. Only needed
1032 if ``visitSummary`` is `None`.
1033 visit : `int` or `None, optional
1034 The visit number for which to determine the band. Only needed
1035 if ``visitSummary`` is `None`.
1036
1037 Returns
1038 -------
1039 band, physicalFilter : `str`
1040 The band and physical filter for the given visit.
1041 """
1042 if visitSummary is not None:
1043 band = visitSummary[0]["band"]
1044 physicalFilter = visitSummary[0]["physical_filter"]
1045 else:
1046 if butler is None:
1047 raise RuntimeError("A butler instance is required when visitSummary is not provided")
1048 record = list(butler.registry.queryDimensionRecords("band", visit=visit))[0]
1049 band = record.name
1050 record = list(butler.registry.queryDimensionRecords("physical_filter", visit=visit))[0]
1051 physicalFilter = record.name
1052 return band, physicalFilter
1053
1054
1055if __name__ == "__main__":
1056 parser = argparse.ArgumentParser()
1057 parser.add_argument(
1058 "repo", type=str, help="URI or path to an existing data repository root or configuration file"
1059 )
1060 parser.add_argument(
1061 "--collections",
1062 type=str,
1063 nargs="+",
1064 help="Blank-space separated list of collection names for butler instantiation",
1065 metavar=("COLLECTION1", "COLLECTION2"),
1066 required=True,
1067 )
1068 parser.add_argument("--skymapName", default=None, help="Name of the skymap for the collection")
1069 parser.add_argument(
1070 "--tracts",
1071 type=int,
1072 nargs="+",
1073 default=None,
1074 help=("Blank-space separated list of tract outlines to constrain search for visit overlap"),
1075 metavar=("TRACT1", "TRACT2"),
1076 )
1077 parser.add_argument(
1078 "--visits",
1079 type=int,
1080 nargs="+",
1081 default=None,
1082 help="Blank-space separated list of visits to include",
1083 metavar=("VISIT1", "VISIT2"),
1084 )
1085 parser.add_argument(
1086 "--physicalFilters",
1087 type=str,
1088 nargs="+",
1089 default=None,
1090 help=("Blank-space separated list of physical filter names to constrain search for visits"),
1091 metavar=("PHYSICAL_FILTER1", "PHYSICAL_FILTER2"),
1092 )
1093 parser.add_argument(
1094 "--bands",
1095 type=str,
1096 nargs="+",
1097 default=None,
1098 help=("Blank-space separated list of canonical band names to constrain search for visits"),
1099 metavar=("BAND1", "BAND2"),
1100 )
1101 parser.add_argument(
1102 "-c",
1103 "--ccds",
1104 nargs="+",
1105 type=int,
1106 default=None,
1107 help="Blank-space separated list of CCDs to show",
1108 metavar=("CCD1", "CCD2"),
1109 )
1110 parser.add_argument(
1111 "-p", "--showPatch", action="store_true", default=False, help="Show the patch boundaries"
1112 )
1113 parser.add_argument(
1114 "--showPatchSelectedTractsOnly",
1115 action="store_true",
1116 default=False,
1117 help="When showing patches, only draw patch boundaries for the tracts passed to --tracts.",
1118 )
1119 parser.add_argument(
1120 "--saveFile", type=str, default="showVisitSkyMap.png", help="Filename to write the plot to"
1121 )
1122 parser.add_argument("--ccdKey", default="detector", help="Data ID name of the CCD key")
1123 parser.add_argument(
1124 "--showCcds", action="store_true", default=False, help="Show ccd ID numbers on output image"
1125 )
1126 parser.add_argument(
1127 "--visitVetoFile",
1128 type=str,
1129 default=None,
1130 help="Full path to single-column file containing a list of visits to veto",
1131 )
1132 parser.add_argument(
1133 "--minOverlapFraction",
1134 type=float,
1135 default=None,
1136 help="Minimum fraction of detectors that overlap any tract for visit to be included",
1137 )
1138 parser.add_argument(
1139 "--trimToTracts",
1140 action="store_true",
1141 default=False,
1142 help="Set plot limits based on extent of visits (as opposed to tracts) plotted?",
1143 )
1144 parser.add_argument(
1145 "--doUnscaledLimitRatio",
1146 action="store_true",
1147 default=False,
1148 help="Let axis limits get set by sky coordinate range without scaling to focal "
1149 "plane based projection (ignored if --forceScaledLimitRatio is passed).",
1150 )
1151 parser.add_argument(
1152 "--forceScaledLimitRatio",
1153 action="store_true",
1154 default=False,
1155 help="Force the axis limit scaling to focal plane based projection (takes "
1156 "precedence over --doUnscaledLimitRatio.",
1157 )
1158 parser.add_argument(
1159 "--imageDatasetType",
1160 type=str,
1161 default=None,
1162 help=(
1163 "Image dataset type used for visit discovery and WCS/bbox fallback; "
1164 "defaults to commonly used detector storage types."
1165 ),
1166 )
1167 parser.add_argument(
1168 "--visitSummaryDatasetType",
1169 type=str,
1170 default=None,
1171 help=("Visit summary dataset type to use; defaults to commonly used visit summary types."),
1172 )
1173 parser.add_argument(
1174 "--dpi",
1175 type=int,
1176 default=150,
1177 help="DPI used when writing output via --saveFile.",
1178 )
1179 args = parser.parse_args()
1180 logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
1181 main(
1182 args.repo,
1183 args.collections,
1184 skymapName=args.skymapName,
1185 tracts=args.tracts,
1186 visits=args.visits,
1187 physicalFilters=args.physicalFilters,
1188 bands=args.bands,
1189 ccds=args.ccds,
1190 ccdKey=args.ccdKey,
1191 showPatch=args.showPatch,
1192 showPatchSelectedTractsOnly=args.showPatchSelectedTractsOnly,
1193 saveFile=args.saveFile,
1194 showCcds=args.showCcds,
1195 visitVetoFile=args.visitVetoFile,
1196 minOverlapFraction=args.minOverlapFraction,
1197 trimToTracts=args.trimToTracts,
1198 doUnscaledLimitRatio=args.doUnscaledLimitRatio,
1199 forceScaledLimitRatio=args.forceScaledLimitRatio,
1200 imageDatasetType=args.imageDatasetType,
1201 visitSummaryDatasetType=args.visitSummaryDatasetType,
1202 dpi=args.dpi,
1203 )
getBand(visitSummary=None, butler=None, visit=None)
derivePlotLimits(limitsDict, raToDecLimitRatio=1.0, buffFrac=0.0)
sanitizeTractList(skymap, tractList)
bboxToRaDec(bbox, wcs)
getTractLimitsDict(skymap, tractList)
getVisitSummaryForVisit(butler, visit, visitSummaryDatasetType=None)
queryImageDatasets(butler, whereStr, imageDatasetType=None)
makeWhereInStr(parameterName, parameterList, parameterType)
getDetRaDecCorners(ccdKey, ccdId, visit, visitSummary=None, butler=None, imageDatasetType="calexp", doLogWarn=True, missingVisitSummaryRows=None)
getValueAtPercentile(values, percentile=0.5)
setLimitsToEqualRatio(xMin, xMax, yMin, yMax)
main(repo, collections, skymapName=None, tracts=None, visits=None, physicalFilters=None, bands=None, ccds=None, ccdKey="detector", showPatch=False, showPatchSelectedTractsOnly=False, saveFile=None, showCcds=False, visitVetoFile=None, minOverlapFraction=None, trimToTracts=False, doUnscaledLimitRatio=False, forceScaledLimitRatio=False, imageDatasetType=None, visitSummaryDatasetType=None, dpi=150)
getMinMaxLimits(limitsDict)
get_cmap(n, name="gist_rainbow")