127 physicalFilters=None,
132 showPatchSelectedTractsOnly=False,
136 minOverlapFraction=None,
138 doUnscaledLimitRatio=False,
139 forceScaledLimitRatio=False,
140 imageDatasetType=None,
141 visitSummaryDatasetType=None,
144 if minOverlapFraction
is not None and tracts
is None:
145 raise RuntimeError(
"Must specify --tracts if --minOverlapFraction is set")
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 = []
156 if skymapName
is None:
157 if instrument ==
"HSC":
158 skymapName =
"hsc_rings_v1"
159 detectorSkipList = [9]
160 elif instrument ==
"LSSTCam-imSim":
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"
170 f
"Unknown skymapName for instrument: {instrument}. Must specify --skymapName on command line."
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)
178 if tracts
is not None:
182 if physicalFilters
is not None:
183 physicalFilterStr =
makeWhereInStr(
"physical_filter", physicalFilters, str)
184 whereStr +=
" AND " + physicalFilterStr
if len(whereStr)
else " " + physicalFilterStr
186 if bands
is not None:
188 whereStr +=
" AND " + bandStr
if len(whereStr)
else " " + bandStr
190 if len(whereStr) > 1:
191 whereStr = f
"instrument='{instrument}' AND skymap='{skymapName}' AND {whereStr}"
193 whereStr = f
"instrument='{instrument}' AND skymap='{skymapName}'"
194 logger.info(
"Querying the butler with the following dataId where clause: %s", whereStr)
197 butler, whereStr, imageDatasetType=imageDatasetType
199 logger.info(
"Using image dataset type: %s", imageDatasetTypeUsed)
201 visitSummaryDatasetTypeUsed = visitSummaryDatasetType
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]
211 for dataRef
in imageDataRefs:
212 visit = dataRef.dataId.visit.id
213 if visit
not in visits
and visit
not in visitVetoList:
216 logger.info(
"List of visits (N=%d) satisfying where and veto clauses: %s", len(visits), visits)
218 if len(visitVetoList) > 1:
219 visitListTemp = visits.copy()
220 for visit
in visitListTemp:
221 if visit
in visitVetoList:
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)
229 butler, visits[0], visitSummaryDatasetType=visitSummaryDatasetType
231 logger.info(
"Using visit summary dataset type: %s", visitSummaryDatasetTypeUsed)
233 if visitSummaryDatasetType
is None:
235 "No visit summary dataset type found for auto-detection; "
236 "will fall back to detector-level WCS/bbox lookups."
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,
249 (ccds
is None or ccdId
in ccds)
250 and ccd.getType() == DetectorType.SCIENCE
251 and ccdId
not in detectorSkipList
253 ccdIdList.append(ccdId)
255 nDetTot = len(ccdIdList)
256 missingVisitSummaryRows = {}
257 nonFiniteDetectorCorners = {}
259 visitIncludeList = []
263 if minOverlapFraction
is not None:
264 for i_v, visit
in enumerate(visits):
268 butler, visit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
270 except LookupError
as e:
271 logger.warning(
"%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
273 if tracts
is not None:
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:
284 visitSummary=visitSummary,
286 imageDatasetType=imageDatasetTypeUsed,
289 if raCorners
is not None and decCorners
is not None:
291 for ra, dec
in zip(raCorners, decCorners):
293 detSphCorners.append(pt)
294 detConvexHull = ConvexPolygon.convexHull(
295 [coord.getVector()
for coord
in detSphCorners]
297 if tractConvexHull.contains(detConvexHull):
298 ccdOverlapList.append(ccdId)
300 if len(ccdOverlapList) / nDetTot >= minOverlapFraction:
302 if len(ccdOverlapList) / nDetTot < minOverlapFraction:
304 "Fraction of detectors overlapping any tract for visit %d (%.2f) < "
305 "minimum required (%.2f). Skipping visit...",
307 len(ccdOverlapList) / nDetTot,
311 if visit
not in visitIncludeList:
312 visitIncludeList.append(visit)
314 visitIncludeList = visits
319 cmap =
get_cmap(len(visitIncludeList))
321 maxVisitForLegend = 20
323 finalVisitColorIndices = []
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")
330 fillKwargs = {
"fill":
False,
"alpha": alphaEdge,
"facecolor":
None,
"edgecolor": color,
"lw": 0.6}
333 butler, visit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
335 except Exception
as e:
336 logger.warning(
"%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
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)
345 for ccdId
in ccdIdList:
350 visitSummary=visitSummary,
352 imageDatasetType=imageDatasetTypeUsed,
353 missingVisitSummaryRows=missingVisitSummaryRows,
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)
360 if len(finiteCornerPairs) < len(cornerPairs):
361 nonFiniteDetectorCorners.setdefault(visit, set()).add(ccdId)
367 if not inLegend
and len(visitIncludeList) <= maxVisitForLegend:
368 plt.fill(raCorners, decCorners, label=
str(visit), **fillKwargs)
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)
379 deltaRa = max(raCorners) - min(raCorners)
380 deltaDec = max(decCorners) - min(decCorners)
382 min(raCorners) + overlapFrac * deltaRa, min(decCorners) + overlapFrac * deltaDec
385 max(raCorners) - overlapFrac * deltaRa, max(decCorners) - overlapFrac * deltaDec
388 bboxDouble =
Box2D(minPoint, maxPoint)
389 overlaps = [
not bboxDouble.overlaps(otherBbox)
for otherBbox
in bboxesPlotted]
400 bboxesPlotted.append(bboxDouble)
402 if visit
in missingVisitSummaryRows:
403 missingDetectors = sorted(missingVisitSummaryRows[visit])
405 "visit summary table for visit %d missing detector rows: %s",
410 if visit
in nonFiniteDetectorCorners:
411 badDetectors = sorted(nonFiniteDetectorCorners[visit])
413 "Non-finite detector corners for visit %d (N=%d): %s",
420 "Final list of visits (N=%d) satisfying where and minOverlapFraction clauses: %s",
425 raToDecLimitRatio =
None
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:
431 "Dropping %d non-finite detector corner coordinates before tract lookup",
434 if len(finiteCoordPairs) == 0:
435 if tracts
is not None:
437 "No finite detector corners found, but --tracts list was provided, so will go ahead and "
438 "plot the empty tracts."
443 raise RuntimeError(
"No finite detector corners available for tract lookup")
445 ras, decs = zip(*finiteCoordPairs)
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)
459 if tracts
is not None:
461 "No detectors were found, but --tracts list was provided, so will go ahead and "
462 "plot the empty tracts."
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)."
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)
479 if forceScaledLimitRatio:
480 doUnscaledLimitRatio =
False
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))
494 xDelta0 = xLimMax - xLimMin
495 yDelta0 = yLimMax - yLimMin
497 xDelta0 = raVisitDiff
498 yDelta0 = decVisitDiff
499 yLimMin = minVisitDec
500 yLimMax = maxVisitDec
501 raDecScaleThresh = 1.5
503 (xDelta0 / yDelta0 > raDecScaleThresh
or yDelta0 / xDelta0 > raDecScaleThresh)
504 and max(xDelta0, yDelta0) > 70 * detectorRadiusDeg
509 "Sky coverage is large (and not too close to a pole), so not scaling to detector coords."
511 doUnscaledLimitRatio =
True
513 if not doUnscaledLimitRatio:
516 minDistToMidCoord = 1e12
519 for i_v, visit
in enumerate(visits):
522 butler, visit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
524 except Exception
as e:
525 logger.warning(
"%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
527 for ccdId
in ccdIdList:
532 visitSummary=visitSummary,
534 imageDatasetType=imageDatasetTypeUsed,
537 if raCorners
is not None and decCorners
is not None:
539 for ra, dec
in zip(raCorners, decCorners):
541 detSphCorners.append(pt)
542 ptSkyCoord = SkyCoord(ra * units.deg, dec * units.deg)
543 separation = (midSkyCoord.separation(ptSkyCoord)).degree
544 if separation < minDistToMidCoord:
547 minDistToMidCoord = separation
548 detConvexHull =
ConvexPolygon([coord.getVector()
for coord
in detSphCorners])
549 if detConvexHull.contains(midRa, midDec)
and raToDecLimitRatio
is None:
551 "visit/det overlapping plot coord mid point in RA/Dec: %d %d", visit, ccdId
553 raToDecLimitRatio = (max(raCorners) - min(raCorners)) / (
554 max(decCorners) - min(decCorners)
557 width = det.getBBox().getWidth()
558 height = det.getBBox().getHeight()
559 if raToDecLimitRatio > 1.0:
560 raToDecLimitRatio /= max(height / width, width / height)
562 if raToDecLimitRatio < 1.0:
563 raToDecLimitRatio *= max(height / width, width / height)
565 if raToDecLimitRatio
is not None:
568 if raToDecLimitRatio
is None and minSepVisit
is not None:
571 butler, minSepVisit, visitSummaryDatasetType=visitSummaryDatasetTypeUsed
573 except Exception
as e:
574 logger.warning(
"%s Will try to get wcs from %s.", e, imageDatasetTypeUsed)
580 visitSummary=visitSummary,
582 imageDatasetType=imageDatasetTypeUsed,
585 for ra, dec
in zip(raCorners, decCorners):
587 detSphCorners.append(pt)
588 detConvexHull =
ConvexPolygon([coord.getVector()
for coord
in detSphCorners])
590 "visit/det closest to plot coord mid point in RA/Dec (none actually overlap it): %d %d",
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)
601 if raToDecLimitRatio < 1.0:
602 raToDecLimitRatio *= max(height / width, width / height)
605 xlim, ylim =
derivePlotLimits(tractLimitsDict, raToDecLimitRatio=raToDecLimitRatio, buffFrac=0.04)
607 visitLimitsDict = {
"allVisits": {
"ras": [minVisitRa, maxVisitRa],
"decs": [minVisitDec, maxVisitDec]}}
608 xlim, ylim =
derivePlotLimits(visitLimitsDict, raToDecLimitRatio=raToDecLimitRatio, buffFrac=0.04)
610 if doUnscaledLimitRatio:
611 boxAspectRatio = abs((ylim[1] - ylim[0]) / (xlim[1] - xlim[0]))
619 if tracts
is not None:
620 tractOutlineList = list(set(tracts + tractList))
622 tractOutlineList = tractList
623 tractOutlineList.sort()
624 selectedPatchTracts = set(tracts)
if tracts
is not None else None
625 if showPatchSelectedTractsOnly
and selectedPatchTracts
is None:
627 "--showPatchSelectedTractsOnly was set without --tracts; showing patches for all plotted tracts."
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]))
639 xlim[1] + fracDeltaX < tCenterRa < xlim[0] - fracDeltaX
640 and ylim[0] + fracDeltaY < tCenterDec < ylim[1] - fracDeltaY
642 if len(tractOutlineList) > 1
or not showPatch:
644 plt.text(tCenterRa, tCenterDec, tract, fontsize=7, alpha=alpha, ha=
"center", va=
"center")
653 path_effects=[pathEffects.withStroke(linewidth=3, foreground=
"black")],
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))
665 not showPatchSelectedTractsOnly
or selectedPatchTracts
is None or tract
in selectedPatchTracts
668 for patch
in tractInfo:
669 ra, dec =
bboxToRaDec(patch.getInnerBBox(), tractInfo.getWcs())
671 ra, dec, fill=
False, edgecolor=patchColor, lw=0.5, linestyle=(0, (5, 6)), alpha=alpha
680 str(patch.sequential_index),
692 ax.set_box_aspect(boxAspectRatio)
693 if abs(xlim[1] > 99.99):
694 ax.tick_params(
"x", labelrotation=45, pad=0, labelsize=8)
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)
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(
724 ax.add_artist(tractLegend)
726 if len(visitIncludeList) > 0:
727 xBboxAnchor = min(1.25, max(1.03, boxAspectRatio * 1.15))
730 bbox_to_anchor=(xBboxAnchor, 0.5),
738 tractLegend = Legend(
743 bbox_to_anchor=(1.0, 0.5),
750 ax.add_artist(tractLegend)
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:
766 titleStr +=
" physical filters: {}".
format(
767 str(includedPhysicalFilters).translate({ord(i):
None for i
in "[]'"})
769 ax.set_title(
"{}".
format(titleStr), fontsize=8)
772 if boxAspectRatio > 1.0:
773 minInches = max(4.0, 0.3 * abs(xlim[1] - xlim[0]))
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)
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)