From 7d7d458f13980d76de6deb7ada9a1e454aa48fb3 Mon Sep 17 00:00:00 2001 From: Lauren MacArthur Date: Sat, 7 Sep 2024 17:56:26 -0700 Subject: [PATCH 1/3] Make summary stats updates configurably optional Computation of these statistics can be time consuming, so this adds an option to turn any of them off for scenarios where they are not useful (which they would be for, e.g., downstream processing decision making) and/or processing time minimization is of the essence. --- .../pipe/tasks/computeExposureSummaryStats.py | 189 +++++++++++++----- 1 file changed, 135 insertions(+), 54 deletions(-) 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. From 70df5261013c65fe655ca82889a527eefc383d32 Mon Sep 17 00:00:00 2001 From: Lauren MacArthur Date: Sat, 7 Sep 2024 17:59:32 -0700 Subject: [PATCH 2/3] Add unittest for disabling summary stats updates --- tests/test_computeExposureSummaryStats.py | 46 ++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) 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 From e978344e428a9e386ed56a1948ea423761f71c51 Mon Sep 17 00:00:00 2001 From: Lauren MacArthur Date: Sun, 8 Sep 2024 12:52:45 -0700 Subject: [PATCH 3/3] Remove no longer appropriate calibreteImage overrides --- python/lsst/pipe/tasks/calibrateImage.py | 5 ----- 1 file changed, 5 deletions(-) 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()