21from __future__
import annotations
24 "PeekExposureTaskConfig",
32import numpy.typing
as npt
47from lsst.geom import Box2I, Extent2I, LinearTransform, Point2D, Point2I, SpherePoint, arcseconds, degrees
62 """Check if an exposure is dispersed.
64 Note this is copied from `atmospec.utils.isDispersedExp` to avoid a
69 exp : `lsst.afw.image.Exposure`
75 Whether it is a dispersed image or not.
77 filterFullName = exp.filter.physicalLabel
78 if FILTER_DELIMITER
not in filterFullName:
79 raise RuntimeError(f
"Error parsing filter name {filterFullName}")
80 filt, grating = filterFullName.split(FILTER_DELIMITER)
81 if grating.upper().startswith(
'EMPTY'):
86def _estimateMode(data: npt.NDArray[np.float64], frac: float = 0.5) -> float:
87 """Estimate the mode of a 1d distribution.
89 Finds the smallest interval containing the fraction ``frac`` of the data,
90 then takes the median of the values in that interval.
95 1d array of data values
96 frac : float, optional
97 Fraction of data to include in the mode interval. Default is 0.5.
102 Estimated mode of the data.
105 data = data[np.isfinite(data)]
112 interval = int(np.ceil(frac * len(data)))
113 spans = data[interval:] - data[:-interval]
114 start = np.argmin(spans)
115 return np.median(data[start: start + interval])
124 """Compute unit vector along given bearing at given point in the sky.
128 wcs : `lsst.afw.geom.SkyWcs`
129 World Coordinate System of image.
130 bearing : `lsst.geom.Angle`
131 Bearing (angle North of East) at which to compute unit vector.
132 imagePoint : `lsst.geom.Point2D`
134 skyPoint : `lsst.geom.SpherePoint`, optional
139 unitVector : `lsst.geom.Extent2D`
140 Unit vector in the direction of bearing.
143 skyPoint = wcs.pixelToSky(imagePoint)
144 dpt = wcs.skyToPixel(skyPoint.offset(bearing, 1e-4 * degrees)) - imagePoint
145 return dpt / dpt.computeNorm()
149 """Compute unit vectors in the N/W and optionally alt/az directions.
153 wcs : `lsst.afw.geom.SkyWcs`
154 World Coordinate System of image.
155 imagePoint : `lsst.geom.Point2D`
157 parAng : `lsst.geom.Angle`, optional
158 Parallactic angle (position angle of zenith measured East from North)
163 unitVectors : `dict` of `lsst.geom.Extent2D`
164 Unit vectors in the N, W, alt, and az directions.
167 skyPoint = wcs.pixelToSky(imagePoint)
168 bearing = skyPoint.bearingTo(ncp)
174 if parAng
is not None:
176 out[
"az"] =
_bearingToUnitVector(wcs, bearing - parAng + 90 * degrees, imagePoint, skyPoint=skyPoint)
182 display: afwDisplay.Display,
188 """Display unit vectors along N/W and optionally alt/az directions.
192 display : `lsst.afw.display.Display`
193 Display on which to render rose.
194 wcs : `lsst.afw.geom.SkyWcs`
195 World Coordinate System of image.
196 imagePoint : `lsst.geom.Point2D`
197 Point in the image at which to render rose.
198 parAng : `lsst.geom.Angle`, optional
199 Parallactic angle (position angle of zenith measured East from North)
201 len : `float`, optional
202 Length of the rose vectors (default: 50)
204 unitVectors =
roseVectors(wcs, imagePoint, parAng=parAng)
205 colors = dict(N=
"r", W=
"r", alt=
"g", az=
"g")
206 for name, unitVector
in unitVectors.items():
207 display.line([imagePoint, imagePoint + len * unitVector], ctype=colors[name])
208 display.dot(name, *(imagePoint + 1.6 * len * unitVector), ctype=colors[name])
212 def __init__(self, size: float, outerRad: float, innerRad: float):
213 Psf.__init__(self, isFixed=
True)
222 def resized(self, width: float, height: float) -> DonutPsf:
223 assert width == height
227 self, position: Point2D |
None =
None, color:
afwImage.Color |
None =
None
230 img = ImageD(bbox, 0.0)
231 x, y = np.ogrid[bbox.minY: bbox.maxY + 1, bbox.minX: bbox.maxX + 1]
235 img.array /= np.sum(img.array)
242 self, position: Point2D |
None =
None, color:
afwImage.Color |
None =
None
249 self, radius: float, position: Point2D |
None =
None, color:
afwImage.Color |
None =
None
251 return 1 - np.exp(-0.5 * (radius / self.
sigma) ** 2)
254 if isinstance(rhs, DonutPsf):
255 return self.
size == rhs.size
and self.
outerRad == rhs.outerRad
and self.
innerRad == rhs.innerRad
260 """Config class for the PeekTask."""
262 installPsf = pexConfig.ConfigurableField(
263 target=InstallGaussianPsfTask,
264 doc=
"Install a PSF model",
266 doInstallPsf: pexConfig.Field[bool] = pexConfig.Field(
269 doc=
"Install a PSF model?",
271 background = pexConfig.ConfigurableField(
272 target=SubtractBackgroundTask,
273 doc=
"Estimate background",
275 detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc=
"Detect sources")
276 measurement = pexConfig.ConfigurableField(
277 target=SingleFrameMeasurementTask, doc=
"Measure sources"
279 defaultBinSize: pexConfig.Field[int] = pexConfig.Field(
282 doc=
"Default binning factor for exposure (often overridden).",
289 self.
detection.includeThresholdMultiplier = 10.0
290 self.
detection.reEstimateBackground =
False
291 self.
detection.doTempLocalBackground =
False
300 "ext_shapeHSM_HsmSourceMoments",
303 "base_CircularApertureFlux",
305 self.
measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
309 """Peek at exposure to quickly detect and measure both the brightest source
310 in the image, and a set of sources representative of the exposure's overall
313 Optionally bins image and then:
314 - installs a simple PSF model
315 - measures and subtracts the background
319 Designed to be quick at the expense of primarily completeness, but also to
320 a lesser extent accuracy.
323 ConfigClass = PeekTaskConfig
324 config: PeekTaskConfig
325 installPsf: InstallGaussianPsfTask
326 background: SubtractBackgroundTask
327 detection: SourceDetectionTask
328 measurement: SingleFrameMeasurementTask
329 _DefaultName =
"peek"
331 def __init__(self, schema: Any |
None =
None, **kwargs: Any):
335 schema = SourceTable.makeMinimalSchema()
338 self.makeSubtask(
"installPsf")
339 self.makeSubtask(
"background")
340 self.makeSubtask(
"detection", schema=self.
schema)
342 self.makeSubtask(
"measurement", schema=self.
schema, algMetadata=self.
algMetadata)
344 def run(self, exposure: afwImage.Exposure, binSize: int |
None =
None) -> pipeBase.Struct:
349 exposure : `lsst.afw.image.Exposure`
350 Exposure at which to peek.
351 binSize : `int`, optional
352 Binning factor for exposure. Default is None, which will use the
353 default binning factor from the config.
357 result : `pipeBase.Struct`
360 - sourceCat : `lsst.afw.table.SourceCatalog`
361 Source catalog from the binned exposure.
364 binSize = self.
config.defaultBinSize
367 mi = exposure.getMaskedImage()
369 exposure.setMaskedImage(binned)
371 if self.
config.doInstallPsf:
377 sourceIdFactory = idGenerator.make_table_id_factory()
378 table = SourceTable.make(self.
schema, sourceIdFactory)
380 sourceCat = self.
detection.run(table=table, exposure=exposure, doSmooth=
True).sources
382 self.
measurement.run(measCat=sourceCat, exposure=exposure, exposureId=idGenerator.catalog_id)
384 return pipeBase.Struct(
390 """Config class for the PeekDonutTask."""
392 peek = pexConfig.ConfigurableField(
394 doc=
"Peek configuration",
396 resolution = pexConfig.Field(
399 doc=
"Target number of pixels for a binned donut",
401 binSizeMax = pexConfig.Field(
404 doc=
"Maximum binning factor for donut mode",
410 self.
peek.installPsf.fwhm = 10.0
411 self.
peek.installPsf.width = 31
413 self.
peek.doInstallPsf =
False
417 """Peek at a donut exposure.
419 The main modification for donuts is to aggressively bin the image to reduce
420 the size of sources (donuts) from ~100 pixels or more to ~10 pixels. This
421 greatly increases the speed and detection capabilities of PeekTask with
422 little loss of accuracy for centroids.
425 ConfigClass = PeekDonutTaskConfig
426 config: PeekDonutTaskConfig
428 _DefaultName =
"peekDonut"
431 super().
__init__(config=config, **kwargs)
432 self.makeSubtask(
"peek")
435 self, exposure: afwImage.Exposure, donutDiameter: float, binSize: int |
None =
None
436 ) -> pipeBase.Struct:
437 """Peek at donut exposure.
441 exposure : `lsst.afw.image.Exposure`
442 Exposure at which to peek.
443 donutDiameter : `float`
444 Donut diameter in pixels.
445 binSize : `int`, optional
446 Binning factor for exposure. Default is None, which will use the
447 resolution config value to determine the binSize.
451 result : `pipeBase.Struct`
452 Result of donut peeking.
455 Peek mode that was run.
458 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
459 Source catalog from the binned exposure.
465 donutDiameter / self.
config.resolution,
471 binnedDonutDiameter = donutDiameter / binSize
473 binnedDonutDiameter * 1.5, binnedDonutDiameter * 0.5, binnedDonutDiameter * 0.5 * 0.3525
481 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
482 factor = 8 * sigma / (
min(exposure.getDimensions()) / binSize)
486 binnedDonutDiameter * 1.5 / factor,
487 binnedDonutDiameter * 0.5 / factor,
488 binnedDonutDiameter * 0.5 * 0.3525 / factor,
492 peekResult = self.
peek.run(exposure, binSize)
494 return pipeBase.Struct(
497 binnedSourceCat=peekResult.sourceCat,
501 """Perform any filtering on the source catalog.
505 binnedSourceCat : `lsst.afw.table.SourceCatalog`
506 Source catalog from the binned exposure.
510 goodSourceMask : `numpy.ndarray`
511 Boolean array indicating which sources are good.
514 goodSourceMask = np.ones(len(binnedSourceCat), dtype=bool)
515 return goodSourceMask
519 """Config class for the PeekPhotoTask."""
521 peek = pexConfig.ConfigurableField(
523 doc=
"Peek configuration",
525 binSize: pexConfig.Field[int] = pexConfig.Field(
528 doc=
"Binning factor for exposure",
534 self.
peek.detection.includeThresholdMultiplier = 1.0
535 self.
peek.detection.thresholdValue = 10.0
536 self.
peek.detection.minPixels = 10
540 """Peek at a photo (imaging) exposure.
542 For photo mode, we keep a relatively small detection threshold value, so we
543 can detect faint sources to use for image quality assessment.
546 ConfigClass = PeekPhotoTaskConfig
547 config: PeekPhotoTaskConfig
549 _DefaultName =
"peekPhoto"
552 super().
__init__(config=config, **kwargs)
553 self.makeSubtask(
"peek")
555 def run(self, exposure: afwImage.Exposure, binSize: int |
None =
None) -> pipeBase.Struct:
556 """Peek at donut exposure.
560 exposure : `lsst.afw.image.Exposure`
561 Exposure at which to peek.
562 binSize : `int`, optional
563 Binning factor for exposure. Default is None, which will use the
564 binning factor from the config.
568 result : `pipeBase.Struct`
569 Result of photo peeking.
572 Peek mode that was run.
575 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
576 Source catalog from the binned exposure.
579 binSize = self.
config.binSize
581 peekResult = self.
peek.run(exposure, binSize)
583 return pipeBase.Struct(
586 binnedSourceCat=peekResult.sourceCat,
590 """Perform any filtering on the source catalog.
594 binnedSourceCat : `lsst.afw.table.SourceCatalog`
595 Source catalog from the binned exposure.
599 goodSourceMask : `numpy.ndarray`
600 Boolean array indicating which sources are good.
603 goodSourceMask = np.ones(len(binnedSourceCat), dtype=bool)
604 return goodSourceMask
608 """Config class for the PeekSpecTask."""
610 peek = pexConfig.ConfigurableField(
612 doc=
"Peek configuration",
614 binSize: pexConfig.Field[int] = pexConfig.Field(
617 doc=
"binning factor for exposure",
619 maxFootprintAspectRatio: pexConfig.Field[int] = pexConfig.Field(
622 doc=
"Maximum detection footprint aspect ratio to consider as 0th order" " (non-dispersed) light.",
628 self.
peek.detection.includeThresholdMultiplier = 1.0
629 self.
peek.detection.thresholdValue = 500.0
632 self.
peek.measurement.slots.apFlux =
"base_CircularApertureFlux_70_0"
635 self.
peek.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 15.0
639 """Peek at a spectroscopic exposure.
641 For spec mode, we dramatically increase the detection threshold to avoid
642 creating blends with the long spectra objects that appear in these images.
643 We also change the default aperture flux slot to a larger aperture, which
644 helps overcome challenges with lost flux in the interpolated cores of
648 ConfigClass = PeekSpecTaskConfig
649 config: PeekSpecTaskConfig
651 _DefaultName =
"peekSpec"
654 super().
__init__(config=config, **kwargs)
655 self.makeSubtask(
"peek")
657 def run(self, exposure: afwImage.Exposure, binSize: int |
None =
None) -> pipeBase.Struct:
658 """Peek at spectroscopic exposure.
662 exposure : `lsst.afw.image.Exposure`
663 Exposure at which to peek.
664 binSize : `int`, optional
665 Binning factor for exposure. Default is None, which will use the
666 binning factor from the config.
670 result : `pipeBase.Struct`
671 Result of spec peeking.
674 Peek mode that was run.
677 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
678 Source catalog from the binned exposure.
681 binSize = self.
config.binSize
683 peekResult = self.
peek.run(exposure, binSize)
685 return pipeBase.Struct(
688 binnedSourceCat=peekResult.sourceCat,
692 """Perform any filtering on the source catalog.
696 binnedSourceCat : `lsst.afw.table.SourceCatalog`
697 Source catalog from the binned exposure.
701 goodSourceMask : `numpy.ndarray`
702 Boolean array indicating which sources are good.
705 goodSourceMask = np.ones(len(binnedSourceCat), dtype=bool)
706 fpShapes = [src.getFootprint().getShape()
for src
in binnedSourceCat]
708 goodSourceMask &= np.array(
709 [sh.getIyy() < self.
config.maxFootprintAspectRatio * sh.getIxx()
for sh
in fpShapes],
712 return goodSourceMask
716 """Config class for the PeekExposureTask."""
718 donutThreshold: pexConfig.Field[float] = pexConfig.Field(
721 doc=
"Size threshold in pixels for when to switch to donut mode.",
723 doPhotoFallback: pexConfig.Field[bool] = pexConfig.Field(
726 doc=
"If True, fall back to photo mode if spec mode fails.",
728 donut = pexConfig.ConfigurableField(
729 target=PeekDonutTask,
730 doc=
"PeekDonut task",
732 photo = pexConfig.ConfigurableField(
733 target=PeekPhotoTask,
734 doc=
"PeekPhoto task",
736 spec = pexConfig.ConfigurableField(
743 """Peek at exposure to quickly detect and measure both the brightest
744 source in the image, and a set of sources representative of the
745 exposure's overall image quality.
749 config : `lsst.summit.utils.peekExposure.PeekExposureTaskConfig`
750 Configuration for the task.
751 display : `lsst.afw.display.Display`, optional
752 For displaying the exposure and sources.
756 The basic philosophy of PeekExposureTask is to:
757 1) Classify exposures based on metadata into 'donut', 'spec', or 'photo'.
758 2) Run PeekTask on the exposure through a wrapper with class specific
760 3) Try only to branch in the code based on the metadata, and not on the
761 data itself. This avoids problematic discontinuities in measurements.
763 The main knobs we fiddle with based on the classification are:
764 - Detection threshold
765 - Minimum number of pixels for a detection
766 - Binning of the image
770 ConfigClass = PeekExposureTaskConfig
771 config: PeekExposureTaskConfig
775 _DefaultName =
"peekExposureTask"
777 def __init__(self, config: Any, *, display: Any =
None, **kwargs: Any):
778 super().
__init__(config=config, **kwargs)
780 self.makeSubtask(
"donut")
781 self.makeSubtask(
"photo")
782 self.makeSubtask(
"spec")
787 """Estimate donut diameter from exposure metadata.
791 exposure : `lsst.afw.image.Exposure`
792 Exposure to estimate donut diameter for.
796 donutDiameter : `float`
797 Estimated donut diameter in pixels.
799 visitInfo = exposure.getInfo().getVisitInfo()
800 focusZ = visitInfo.focusZ
801 instrumentLabel = visitInfo.instrumentLabel
803 match instrumentLabel:
807 case
"LSSTCam" |
"LSSTComCam" |
"LSSTComCamSim":
810 raise ValueError(f
"Unknown instrument label: {instrumentLabel}")
812 donutDiameter = abs(focusZ) / fratio / 10e-3
813 self.log.info(f
"{focusZ=} mm")
814 self.log.info(f
"donutDiameter = {donutDiameter} pixels")
819 exposure: afwImage.Exposure,
821 doDisplay: bool =
False,
822 doDisplayIndices: bool =
False,
824 binSize: int |
None =
None,
825 donutDiameter: float |
None =
None,
826 ) -> pipeBase.Struct:
830 exposure : `lsst.afw.image.Exposure`
831 Exposure at which to peek.
832 doDisplay : `bool`, optional
833 Display the exposure and sources? Default False. (Requires
834 display to have been passed to task constructor)
835 doDisplayIndices : `bool`, optional
836 Display the source indices? Default False. (Requires display to
837 have been passed to task constructor)
838 mode : {'auto', 'donut', 'spec', 'photo'}, optional
839 Mode to run in. Default 'auto'.
840 binSize : `int`, optional
841 Binning factor for exposure. Default is None, which let's subtasks
842 control rebinning directly.
843 donutDiameter : `float`, optional
844 Donut diameter in pixels. Default is None, which will estimate the
845 donut diameter from the exposure metadata.
849 result : `pipeBase.Struct`
853 Peek mode that was run.
856 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
857 Source catalog from the binned exposure.
858 - table : `astropy.table.Table`
859 Curated source table in unbinned coordinates.
860 - brightestIdx : `int`
861 Index of brightest source in source catalog.
862 - brightestCentroid : `lsst.geom.Point2D`
863 Brightest source centroid in unbinned pixel coords.
864 - brightestPixelShape : `lsst.afw.geom.Quadrupole`
865 Brightest source shape in unbinned pixel coords.
866 - brightestEquatorialShape : `lsst.afw.geom.Quadrupole`
867 Brightest source shape in equitorial coordinates (arcsec).
868 - brightestAltAzShape : `lsst.afw.geom.Quadrupole`
869 Brightest source shape in alt/az coordinates (arcsec).
870 - psfPixelShape : `lsst.afw.geom.Quadrupole`
871 Estimated PSF shape in unbinned pixel coords.
872 - psfEquatorialShape : `lsst.afw.geom.Quadrupole`
873 Estimated PSF shape in equitorial coordinates (arcsec).
874 - psfAltAzShape : `lsst.afw.geom.Quadrupole`
875 Estimated PSF shape in alt/az coordinates (arcsec).
876 - pixelMedian : `float`
877 Median estimate of entire image.
878 - pixelMode : `float`
879 Mode estimate of entire image.
882 exposure = exposure.clone()
884 result = self.
_run(exposure, doDisplay, doDisplayIndices, mode, binSize, donutDiameter)
885 except Exception
as e:
886 self.log.warning(f
"Peek failed: {e}")
887 result = pipeBase.Struct(
890 binnedSourceCat=
None,
893 brightestCentroid=Point2D(np.nan, np.nan),
894 brightestPixelShape=
Quadrupole(np.nan, np.nan, np.nan),
895 brightestEquatorialShape=
Quadrupole(np.nan, np.nan, np.nan),
896 brightestAltAzShape=
Quadrupole(np.nan, np.nan, np.nan),
897 psfPixelShape=
Quadrupole(np.nan, np.nan, np.nan),
898 psfEquatorialShape=
Quadrupole(np.nan, np.nan, np.nan),
899 psfAltAzShape=
Quadrupole(np.nan, np.nan, np.nan),
907 exposure: afwImage.Exposure,
909 doDisplayIndices: bool,
912 donutDiameter: float |
None,
913 ) -> pipeBase.Struct:
914 """The actual run method, called by run()."""
917 arr = exposure.getMaskedImage().getImage().array
919 if arr.size > 250_000:
920 sampling = int(np.floor(np.sqrt(arr.size / 250_000)))
921 pixelMedian = np.nanmedian(arr[::sampling, ::sampling])
924 if donutDiameter
is None:
927 mode, binSize, binnedSourceCat = self.
runPeek(exposure, mode, donutDiameter, binSize)
933 goodSourceMask = self.
donut.getGoodSources(binnedSourceCat)
935 goodSourceMask = self.
spec.getGoodSources(binnedSourceCat)
937 goodSourceMask = self.
photo.getGoodSources(binnedSourceCat)
940 maxFluxIdx, brightCentroid, brightShape = self.
getBrightest(binnedSourceCat, binSize, goodSourceMask)
941 psfShape = self.
getPsfShape(binnedSourceCat, binSize, goodSourceMask)
943 equatorialShapes, altAzShapes = self.
transformShapes([brightShape, psfShape], exposure, binSize)
946 self.
updateDisplay(exposure, binSize, binnedSourceCat, maxFluxIdx, doDisplayIndices)
948 return pipeBase.Struct(
951 binnedSourceCat=binnedSourceCat,
953 brightestIdx=maxFluxIdx,
954 brightestCentroid=brightCentroid,
955 brightestPixelShape=brightShape,
956 brightestEquatorialShape=equatorialShapes[0],
957 brightestAltAzShape=altAzShapes[0],
958 psfPixelShape=psfShape,
959 psfEquatorialShape=equatorialShapes[1],
960 psfAltAzShape=altAzShapes[1],
961 pixelMedian=pixelMedian,
967 exposure: afwImage.Exposure,
969 donutDiameter: float,
970 binSize: int |
None =
None,
972 """Classify exposure and run appropriate PeekTask wrapper.
976 exposure : `lsst.afw.image.Exposure`
978 mode : {'auto', 'donut', 'spec', 'photo'}
980 donutDiameter : `float`
981 Donut diameter in pixels.
982 binSize : `int`, optional
983 Binning factor for exposure. Default is None, which let's subtasks
984 control rebinning directly.
988 result : `pipeBase.Struct`
992 Peek mode that was run.
995 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
996 Source catalog from the binned exposure.
1001 if donutDiameter > self.
config.donutThreshold:
1004 if exposure.getInfo().getVisitInfo().instrumentLabel ==
"LATISS":
1013 result = self.
donut.run(exposure, donutDiameter, binSize=binSize)
1014 binSizeOut = result.binSize
1016 result = self.
spec.run(exposure, binSize=binSize)
1017 binSizeOut = result.binSize
1018 if len(result.binnedSourceCat) == 0:
1019 self.log.warn(
"No sources found in spec mode.")
1020 if self.
config.doPhotoFallback:
1021 self.log.warn(
"Falling back to photo mode.")
1024 result = self.
photo.run(exposure, binSize=1)
1026 result = self.
photo.run(exposure, binSize=binSize)
1027 binSizeOut = result.binSize
1029 raise ValueError(f
"Unknown mode {mode}")
1030 return result.mode, binSizeOut, result.binnedSourceCat
1033 """Make an astropy table from the source catalog but with
1034 transformations back to the original unbinned coordinates.
1036 Since there's some ambiguity in the apFlux apertures when binning,
1037 we'll only populate the table with the slots columns (slot_apFlux
1038 doesn't indicate an aperture radius). For simplicity, do the same for
1039 centroids and shapes too.
1041 And since we're only copying over the slots_* columns, we remove the
1042 "slots_" part of the column names and lowercase the first remaining
1048 Binning factor used.
1049 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1050 Source catalog from the binned exposure.
1054 table : `astropy.table.Table`
1055 Curated source table in unbinned coordinates.
1057 table = binnedSourceCat.asAstropy()
1058 cols = [n
for n
in table.colnames
if n.startswith(
"slot")]
1060 if "slot_Centroid_x" in cols:
1061 table[
"slot_Centroid_x"] = binSize * table[
"slot_Centroid_x"] + (binSize - 1) / 2
1062 table[
"slot_Centroid_y"] = binSize * table[
"slot_Centroid_y"] + (binSize - 1) / 2
1063 if "slot_Shape_x" in cols:
1064 table[
"slot_Shape_x"] = binSize * table[
"slot_Shape_x"] + (binSize - 1) / 2
1065 table[
"slot_Shape_y"] = binSize * table[
"slot_Shape_y"] + (binSize - 1) / 2
1066 table[
"slot_Shape_xx"] *= binSize**2
1067 table[
"slot_Shape_xy"] *= binSize**2
1068 table[
"slot_Shape_yy"] *= binSize**2
1070 if "slot_PsfFlux_area" in cols:
1071 del table[
"slot_PsfFlux_area"]
1072 if "slot_PsfFlux_npixels" in cols:
1073 del table[
"slot_PsfFlux_npixels"]
1076 [n
for n
in table.colnames
if n.startswith(
"slot_")],
1077 [n[5:6].lower() + n[6:]
for n
in table.colnames
if n.startswith(
"slot_")],
1083 self, binnedSourceCat: afwTable.SourceCatalog, binSize: int, goodSourceMask: npt.NDArray[np.bool_]
1085 """Find the brightest source in the catalog.
1089 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1090 Source catalog from the binned exposure.
1092 Binning factor used.
1093 goodSourceMask : `numpy.ndarray`
1094 Boolean array indicating which sources are good.
1099 Index of the brightest source in the catalog.
1100 brightCentroid : `lsst.geom.Point2D`
1101 Centroid of the brightest source (unbinned coords).
1102 brightShape : `lsst.afw.geom.Quadrupole`
1103 Shape of the brightest source (unbinned coords).
1105 fluxes = np.array([source.getApInstFlux()
for source
in binnedSourceCat])
1106 idxs = np.arange(len(binnedSourceCat))
1108 good = goodSourceMask & np.isfinite(fluxes)
1110 if np.sum(good) == 0:
1111 maxFluxIdx = IDX_SENTINEL
1112 brightCentroid = Point2D(np.nan, np.nan)
1113 brightShape =
Quadrupole(np.nan, np.nan, np.nan)
1114 return maxFluxIdx, brightCentroid, brightShape
1116 fluxes = fluxes[good]
1118 maxFluxIdx = idxs[np.nanargmax(fluxes)]
1119 brightest = binnedSourceCat[maxFluxIdx]
1123 brightX, brightY = brightest.getCentroid()
1124 brightX = binSize * brightX + (binSize - 1) / 2
1125 brightY = binSize * brightY + (binSize - 1) / 2
1126 brightCentroid = Point2D(brightX, brightY)
1127 brightIXX = brightest.getIxx() * binSize**2
1128 brightIXY = brightest.getIxy() * binSize**2
1129 brightIYY = brightest.getIyy() * binSize**2
1130 brightShape =
Quadrupole(brightIXX, brightIYY, brightIXY)
1132 return maxFluxIdx, brightCentroid, brightShape
1135 self, binnedSourceCat: afwTable.SourceCatalog, binSize: int, goodSourceMask: npt.NDArray[np.bool_]
1137 """Estimate the modal PSF shape from the sources.
1141 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1142 Source catalog from the binned exposure.
1144 Binning factor used.
1145 goodSourceMask : `numpy.ndarray`
1146 Boolean array indicating which sources are good.
1150 psfShape : `lsst.afw.geom.Quadrupole`
1151 Estimated PSF shape (unbinned coords).
1153 fluxes = np.array([source.getApInstFlux()
for source
in binnedSourceCat])
1154 idxs = np.arange(len(binnedSourceCat))
1156 good = goodSourceMask & np.isfinite(fluxes)
1158 if np.sum(good) == 0:
1161 fluxes = fluxes[good]
1164 psfIXX =
_estimateMode(np.array([source.getIxx()
for source
in binnedSourceCat])[goodSourceMask])
1165 psfIYY =
_estimateMode(np.array([source.getIyy()
for source
in binnedSourceCat])[goodSourceMask])
1166 psfIXY =
_estimateMode(np.array([source.getIxy()
for source
in binnedSourceCat])[goodSourceMask])
1169 psfIXX * binSize**2,
1170 psfIYY * binSize**2,
1171 psfIXY * binSize**2,
1177 """Transform shapes from x/y pixel coordinates to equitorial and
1178 horizon coordinates.
1182 shapes : `list` of `lsst.afw.geom.Quadrupole`
1183 List of shapes (in pixel coordinates) to transform.
1184 exposure : `lsst.afw.image.Exposure`
1185 Exposure containing WCS and VisitInfo for transformation.
1187 Binning factor used.
1191 equatorialShapes : `list` of `lsst.afw.geom.Quadrupole`
1192 List of shapes transformed to equitorial (North and West)
1193 coordinates. Units are arcseconds.
1194 altAzShapes : `list` of `lsst.afw.geom.Quadrupole`
1195 List of shapes transformed to alt/az coordinates. Units are
1198 pt = Point2D(np.array([*exposure.getBBox().getCenter()]) / binSize)
1200 visitInfo = exposure.info.getVisitInfo()
1201 parAngle = visitInfo.boresightParAngle
1203 equatorialShapes = []
1205 for shape
in shapes:
1207 equatorialShapes.append(
Quadrupole(np.nan, np.nan, np.nan))
1208 altAzShapes.append(
Quadrupole(np.nan, np.nan, np.nan))
1213 neTransform = wcs.linearizePixelToSky(pt, arcseconds).getLinear()
1214 nwTransform =
LinearTransform(np.array([[-1, 0], [0, 1]]) @ neTransform.getMatrix())
1215 equatorialShapes.append(shape.transform(nwTransform))
1219 rot = LinearTransform.makeRotation(parAngle).getMatrix()
1221 altAzShapes.append(shape.transform(aaTransform))
1223 return equatorialShapes, altAzShapes
1227 exposure: afwImage.Exposure,
1231 doDisplayIndices: bool,
1233 """Update the afwDisplay with the exposure and sources.
1237 exposure : `lsst.afw.image.Exposure`
1240 Binning factor used.
1241 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1242 Source catalog from the binned exposure.
1244 Index of the brightest source in the catalog.
1245 doDisplayIndices : `bool`
1246 Display the source indices?
1249 raise RuntimeError(
"Display failed as no display provided during init()")
1251 visitInfo = exposure.info.getVisitInfo()
1258 Point2D(200 / binSize, 200 / binSize),
1259 parAng=visitInfo.boresightParAngle,
1263 for idx, source
in enumerate(binnedSourceCat):
1264 x, y = source.getCentroid()
1265 sh = source.getShape()
1267 if doDisplayIndices:
1270 if maxFluxIdx != IDX_SENTINEL:
1273 *binnedSourceCat[maxFluxIdx].getCentroid(),
1274 ctype=afwDisplay.RED,
A polymorphic base class for representing an image's Point Spread Function.
lsst::geom::Box2I computeBBox(lsst::geom::Point2D position, image::Color color=image::Color()) const
Return the bounding box of the image returned by computeKernelImage()
virtual lsst::geom::Point2D getAveragePosition() const
Return the average position of the stars used to construct the Psf.
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
An ellipse core with quadrupole moments as parameters.
Describe the colour of a source.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Tag types used to declare specialized field types.
Class for storing ordered metadata with comments.
A class representing an angle.
An integer coordinate rectangle.
Point in an unspecified spherical coordinate system.
bool __eq__(self, object rhs)
float _doComputeApertureFlux(self, float radius, Point2D|None position=None, afwImage.Color|None color=None)
ImageD _doComputeKernelImage(self, Point2D|None position=None, afwImage.Color|None color=None)
DonutPsf __deepcopy__(self, Any|None memo=None)
__init__(self, float size, float outerRad, float innerRad)
Box2I _doComputeBBox(self, Point2D|None position=None, afwImage.Color|None color=None)
Quadrupole _doComputeShape(self, Point2D|None position=None, afwImage.Color|None color=None)
DonutPsf resized(self, float width, float height)
pipeBase.Struct run(self, afwImage.Exposure exposure, float donutDiameter, int|None binSize=None)
PeekDonutTaskConfig config
__init__(self, Any config, **Any kwargs)
np.ndarray getGoodSources(self, afwTable.SourceCatalog binnedSourceCat)
pipeBase.Struct _run(self, afwImage.Exposure exposure, bool doDisplay, bool doDisplayIndices, str mode, int|None binSize, float|None donutDiameter)
float getDonutDiameter(self, afwImage.Exposure exposure)
tuple[int, geom.Point2D, afwGeom.Quadrupole] getBrightest(self, afwTable.SourceCatalog binnedSourceCat, int binSize, npt.NDArray[np.bool_] goodSourceMask)
pipeBase.Struct run(self, afwImage.Exposure exposure, *bool doDisplay=False, bool doDisplayIndices=False, str mode="auto", int|None binSize=None, float|None donutDiameter=None)
__init__(self, Any config, *Any display=None, **Any kwargs)
PeekExposureTaskConfig config
afwGeom.Quadrupole getPsfShape(self, afwTable.SourceCatalog binnedSourceCat, int binSize, npt.NDArray[np.bool_] goodSourceMask)
tuple[str, int, afwTable.SourceCatalog] runPeek(self, afwImage.Exposure exposure, str mode, float donutDiameter, int|None binSize=None)
astropy.table.Table transformTable(self, int binSize, afwTable.SourceCatalog binnedSourceCat)
None updateDisplay(self, afwImage.Exposure exposure, int binSize, afwTable.SourceCatalog binnedSourceCat, int maxFluxIdx, bool doDisplayIndices)
tuple[list[afwGeom.Quadrupole], list[afwGeom.Quadrupole]] transformShapes(self, afwGeom.Quadrupole shapes, afwImage.Exposure exposure, int binSize)
PeekPhotoTaskConfig config
np.ndarray getGoodSources(self, afwTable.SourceCatalog binnedSourceCat)
__init__(self, Any config, **Any kwargs)
pipeBase.Struct run(self, afwImage.Exposure exposure, int|None binSize=None)
np.ndarray getGoodSources(self, afwTable.SourceCatalog binnedSourceCat)
PeekSpecTaskConfig config
pipeBase.Struct run(self, afwImage.Exposure exposure, int|None binSize=None)
__init__(self, Any config, **Any kwargs)
SubtractBackgroundTask background
__init__(self, Any|None schema=None, **Any kwargs)
SourceDetectionTask detection
InstallGaussianPsfTask installPsf
pipeBase.Struct run(self, afwImage.Exposure exposure, int|None binSize=None)
SingleFrameMeasurementTask measurement
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
float _estimateMode(npt.NDArray[np.float64] data, float frac=0.5)
dict roseVectors(afwGeom.SkyWcs wcs, geom.Point2D imagePoint, geom.Angle|None parAng=None)
geom.Extent2D _bearingToUnitVector(afwGeom.SkyWcs wcs, geom.Angle bearing, geom.Point2D imagePoint, geom.SpherePoint|None skyPoint=None)
None plotRose(afwDisplay.Display display, afwGeom.SkyWcs wcs, geom.Point2D imagePoint, geom.Angle|None parAng=None, float len=50)