diff --git a/python/lsst/pipe/tasks/calibrateImage.py b/python/lsst/pipe/tasks/calibrateImage.py index 1fad9a131..e4047aa5d 100644 --- a/python/lsst/pipe/tasks/calibrateImage.py +++ b/python/lsst/pipe/tasks/calibrateImage.py @@ -408,11 +408,6 @@ def setDefaults(self): self.photometry.match.sourceSelection.doRequirePrimary = False self.photometry.match.sourceSelection.doUnresolved = False - # All sources should be good for PSF summary statistics. - # TODO: These should both be changed to calib_psf_used with DM-41640. - self.compute_summary_stats.starSelection = "calib_photometry_used" - self.compute_summary_stats.starSelector.flags.good = ["calib_photometry_used"] - def validate(self): super().validate() diff --git a/python/lsst/pipe/tasks/computeExposureSummaryStats.py b/python/lsst/pipe/tasks/computeExposureSummaryStats.py index ed9351434..90c423362 100644 --- a/python/lsst/pipe/tasks/computeExposureSummaryStats.py +++ b/python/lsst/pipe/tasks/computeExposureSummaryStats.py @@ -41,6 +41,55 @@ class ComputeExposureSummaryStatsConfig(pexConfig.Config): """Config for ComputeExposureSummaryTask""" + doUpdatePsfModelStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the grid-based PSF model maximum - minimum range metrics (psfTraceRadiusDelta & " + "psfApFluxDelta)? Set to False if speed is of the essence.", + ) + doUpdateApCorrModelStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the grid-based apCorr model maximum - minimum range metric (psfApCorrSigmaScaledDelta)? " + "Set to False if speed is of the essence.", + ) + doUpdateMaxDistToNearestPsfStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the grid-based maximum distance to the nearest PSF star PSF model metric " + "(maxDistToNearestPsf)? Set to False if speed is of the essence.", + ) + doUpdateWcsStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the wcs statistics? Set to False if speed is of the essence.", + ) + doUpdatePhotoCalibStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the photoCalib statistics? Set to False if speed is of the essence.", + ) + doUpdateBackgroundStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the background statistics? Set to False if speed is of the essence.", + ) + doUpdateMaskedImageStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the masked image (i.e. skyNoise & meanVar) statistics? Set to False " + "if speed is of the essence.", + ) + doUpdateMagnitudeLimitStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the magnitude limit depth statistics? Set to False if speed is of the essence.", + ) + doUpdateEffectiveTimeStats = pexConfig.Field( + dtype=bool, + default=True, + doc="Update the effective time statistics? Set to False if speed is of the essence.", + ) sigmaClip = pexConfig.Field( dtype=float, doc="Sigma for outlier rejection for sky noise.", @@ -164,13 +213,26 @@ class ComputeExposureSummaryStatsTask(pipeBase.Task): """Task to compute exposure summary statistics. This task computes various quantities suitable for DPDD and other - downstream processing at the detector centers, including: + downstream processing at the detector centers. The non-optionally + computed quantities are: - expTime - psfSigma - psfArea - psfIxx - psfIyy - psfIxy + + And these quantities which are computed from the stars in the detector: + - psfStarDeltaE1Median + - psfStarDeltaE2Median + - psfStarDeltaE1Scatter + - psfStarDeltaE2Scatter + - psfStarDeltaSizeMedian + - psfStarDeltaSizeScatter + - psfStarScaledDeltaSizeScatter + + The subsequently listed quatities are optionally computed via the + "doUpdateX" config parameters (which all default to True): - ra - dec - pixelScale (arcsec/pixel) @@ -184,15 +246,6 @@ class ComputeExposureSummaryStatsTask(pipeBase.Task): - astromOffsetMean - astromOffsetStd - These additional quantities are computed from the stars in the detector: - - psfStarDeltaE1Median - - psfStarDeltaE2Median - - psfStarDeltaE1Scatter - - psfStarDeltaE2Scatter - - psfStarDeltaSizeMedian - - psfStarDeltaSizeScatter - - psfStarScaledDeltaSizeScatter - These quantities are computed based on the PSF model and image mask to assess the robustness of the PSF model across a given detector (against, e.g., extrapolation instability): @@ -250,20 +303,26 @@ def run(self, exposure, sources, background): summary, psf, bbox, sources, image_mask=exposure.mask, image_ap_corr_map=exposure.apCorrMap ) - wcs = exposure.getWcs() - visitInfo = exposure.getInfo().getVisitInfo() - self.update_wcs_stats(summary, wcs, bbox, visitInfo) + if self.config.doUpdateWcsStats: + wcs = exposure.getWcs() + visitInfo = exposure.getInfo().getVisitInfo() + self.update_wcs_stats(summary, wcs, bbox, visitInfo) - photoCalib = exposure.getPhotoCalib() - self.update_photo_calib_stats(summary, photoCalib) + if self.config.doUpdatePhotoCalibStats: + photoCalib = exposure.getPhotoCalib() + self.update_photo_calib_stats(summary, photoCalib) - self.update_background_stats(summary, background) + if self.config.doUpdateBackgroundStats: + self.update_background_stats(summary, background) - self.update_masked_image_stats(summary, exposure.getMaskedImage()) + if self.config.doUpdateMaskedImageStats: + self.update_masked_image_stats(summary, exposure.getMaskedImage()) - self.update_magnitude_limit_stats(summary, exposure) + if self.config.doUpdateMagnitudeLimitStats: + self.update_magnitude_limit_stats(summary, exposure) - self.update_effective_time_stats(summary, exposure) + if self.config.doUpdateEffectiveTimeStats: + self.update_effective_time_stats(summary, exposure) md = exposure.getMetadata() if 'SFM_ASTROM_OFFSET_MEAN' in md: @@ -340,33 +399,49 @@ def update_psf_stats( # 750bffe6620e565bda731add1509507f5c40c8bb/src/PsfFlux.cc#L112 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.)) - if image_mask is not None: - psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma) - self.log.debug("Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius) - psfTraceRadiusDelta, psfApFluxDelta = compute_psf_image_deltas( - image_mask, - psf, - sampling=self.config.psfGridSampling, - ap_radius_pix=psfApRadius, - bad_mask_bits=self.config.psfBadMaskPlanes - ) - summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta) - summary.psfApFluxDelta = float(psfApFluxDelta) - if image_ap_corr_map is not None: - if self.config.psfApCorrFieldName not in image_ap_corr_map.keys(): - self.log.warn(f"{self.config.psfApCorrFieldName} not found in " - "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.") - psfApCorrSigmaScaledDelta = nan - else: - image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName] - psfApCorrSigmaScaledDelta = compute_ap_corr_sigma_scaled_delta( - image_mask, - image_ap_corr_field, - summary.psfSigma, - sampling=self.config.psfGridSampling, - bad_mask_bits=self.config.psfBadMaskPlanes, - ) - summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta) + if not self.config.doUpdatePsfModelStats: + self.log.info("Note: not computing grid-based PSF model maximum - minimum range metrics " + "psfTraceRadiusDelta & psfApFluxDelta.") + else: + if image_mask is None: + self.log.info("Note: computation of grid-based PSF model maximum - minimum range metrics " + "was requested, but required image_mask parameter was not provided.") + else: + psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma) + self.log.debug("Using radius of %.3f (pixels) for psfApFluxDelta metric.", psfApRadius) + + psfTraceRadiusDelta, psfApFluxDelta = compute_psf_image_deltas( + image_mask, + psf, + sampling=self.config.psfGridSampling, + ap_radius_pix=psfApRadius, + bad_mask_bits=self.config.psfBadMaskPlanes + ) + summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta) + summary.psfApFluxDelta = float(psfApFluxDelta) + if not self.config.doUpdateApCorrModelStats: + self.log.info("Note: not computing grid-based apCorr model maximum - minimum range metric " + "psfApCorrSigmaScaledDelta.") + else: + if image_mask is None: + self.log.info("Note: computation of grid-based apCorr model maximum - minimum metric " + "was requested, but required image_mask parameter was not provided.") + else: + if image_ap_corr_map is not None: + if self.config.psfApCorrFieldName not in image_ap_corr_map.keys(): + self.log.warn(f"{self.config.psfApCorrFieldName} not found in " + "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.") + psfApCorrSigmaScaledDelta = nan + else: + image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName] + psfApCorrSigmaScaledDelta = compute_ap_corr_sigma_scaled_delta( + image_mask, + image_ap_corr_field, + summary.psfSigma, + sampling=self.config.psfGridSampling, + bad_mask_bits=self.config.psfBadMaskPlanes, + ) + summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta) if sources is None: # No sources are available (as in some tests and rare cases where @@ -427,14 +502,20 @@ def update_psf_stats( summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter) summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter) - if image_mask is not None: - maxDistToNearestPsf = maximum_nearest_psf_distance( - image_mask, - psf_cat, - sampling=self.config.psfSampling, - bad_mask_bits=self.config.psfBadMaskPlanes - ) - summary.maxDistToNearestPsf = float(maxDistToNearestPsf) + if not self.config.doUpdateMaxDistToNearestPsfStats: + self.log.info("Note: not computing grid-based maxDistToNearestPsf PSF model metric.") + else: + if image_mask is None: + self.log.info("Note: computation of maxDistToNearestPsf PSF model metric was " + "requested, but required image_mask parameter was not provided.") + else: + maxDistToNearestPsf = maximum_nearest_psf_distance( + image_mask, + psf_cat, + sampling=self.config.psfSampling, + bad_mask_bits=self.config.psfBadMaskPlanes + ) + summary.maxDistToNearestPsf = float(maxDistToNearestPsf) def update_wcs_stats(self, summary, wcs, bbox, visitInfo): """Compute all summary-statistic fields that depend on the WCS model. diff --git a/tests/test_computeExposureSummaryStats.py b/tests/test_computeExposureSummaryStats.py index b2debe300..bb2253795 100644 --- a/tests/test_computeExposureSummaryStats.py +++ b/tests/test_computeExposureSummaryStats.py @@ -114,12 +114,56 @@ def testComputeExposureSummary(self): background.append(backobj) # Configure and run the task + expSummaryTaskNoUpdates = ComputeExposureSummaryStatsTask() expSummaryTask = ComputeExposureSummaryStatsTask() # Configure nominal values for effective time calculation (normalized to 1s exposure) expSummaryTask.config.fiducialZeroPoint = {band: float(zp - 2.5*np.log10(expTime))} expSummaryTask.config.fiducialPsfSigma = {band: float(psfSize)} expSummaryTask.config.fiducialSkyBackground = {band: float(skyMean/expTime)} - # Run the task + # Run the task with optianal updates turned off + expSummaryTaskNoUpdates.config.doUpdatePsfModelStats = False + expSummaryTaskNoUpdates.config.doUpdateApCorrModelStats = False + expSummaryTaskNoUpdates.config.doUpdateMaxDistToNearestPsfStats = False + expSummaryTaskNoUpdates.config.doUpdateWcsStats = False + expSummaryTaskNoUpdates.config.doUpdatePhotoCalibStats = False + expSummaryTaskNoUpdates.config.doUpdateBackgroundStats = False + expSummaryTaskNoUpdates.config.doUpdateMaskedImageStats = False + expSummaryTaskNoUpdates.config.doUpdateMagnitudeLimitStats = False + expSummaryTaskNoUpdates.config.doUpdateEffectiveTimeStats = False + + summary = expSummaryTaskNoUpdates.run(exposure, None, background) + # Test the outputs + self.assertTrue(np.isnan(summary.ra)) + self.assertTrue(np.isnan(summary.dec)) + + # The following PSF metrics are always updated + self.assertFloatsAlmostEqual(summary.expTime, expTime) + self.assertFloatsAlmostEqual(summary.psfSigma, psfSize) + self.assertFloatsAlmostEqual(summary.psfIxx, psfSize**2.) + self.assertFloatsAlmostEqual(summary.psfIyy, psfSize**2.) + self.assertFloatsAlmostEqual(summary.psfIxy, 0.0) + self.assertFloatsAlmostEqual(summary.psfArea, 23.088975164455444) + + # The following should not have been updated (i.e. set to nan) + self.assertTrue(np.isnan(summary.psfTraceRadiusDelta)) + self.assertTrue(np.isnan(summary.psfApFluxDelta)) + self.assertTrue(np.isnan(summary.psfApCorrSigmaScaledDelta)) + self.assertTrue(np.isnan(summary.maxDistToNearestPsf)) + self.assertTrue(np.isnan(summary.pixelScale)) + + self.assertTrue(np.isnan(summary.zenithDistance)) + self.assertTrue(np.isnan(summary.skyBg)) + self.assertTrue(np.isnan(summary.skyNoise)) + self.assertTrue(np.isnan(summary.meanVar)) + self.assertTrue(np.isnan(summary.zeroPoint)) + + self.assertTrue(np.isnan(summary.effTime)) + self.assertTrue(np.isnan(summary.effTimePsfSigmaScale)) + self.assertTrue(np.isnan(summary.effTimeSkyBgScale)) + self.assertTrue(np.isnan(summary.effTimeZeroPointScale)) + self.assertTrue(np.isnan(summary.magLim)) + + # Run the task with updates summary = expSummaryTask.run(exposure, None, background) # Test the outputs