22__all__ = [
"ComputeExposureSummaryStatsTask",
"ComputeExposureSummaryStatsConfig"]
26from scipy.stats
import median_abs_deviation
as sigmaMad
27import astropy.units
as units
28from astropy.time
import Time
29from astropy.coordinates
import AltAz, SkyCoord, EarthLocation
37from lsst.utils.timer
import timeMethod
41 """Config for ComputeExposureSummaryTask"""
42 sigmaClip = pexConfig.Field(
44 doc=
"Sigma for outlier rejection for sky noise.",
47 clipIter = pexConfig.Field(
49 doc=
"Number of iterations of outlier rejection for sky noise.",
52 badMaskPlanes = pexConfig.ListField(
54 doc=
"Mask planes that, if set, the associated pixel should not be included sky noise calculation.",
55 default=(
"NO_DATA",
"SUSPECT"),
57 starSelection = pexConfig.Field(
58 doc=
"Field to select sources to be used in the PSF statistics computation.",
60 default=
"calib_psf_used"
62 starShape = pexConfig.Field(
63 doc=
"Base name of columns to use for the source shape in the PSF statistics computation.",
65 default=
"base_SdssShape"
67 psfShape = pexConfig.Field(
68 doc=
"Base name of columns to use for the PSF shape in the PSF statistics computation.",
70 default=
"base_SdssShape_psf"
75 """Task to compute exposure summary statistics.
77 This task computes various quantities suitable for DPDD
and other
78 downstream processing at the detector centers, including:
96 These additional quantities are computed
from the stars
in the detector:
97 - psfStarDeltaE1Median
98 - psfStarDeltaE2Median
99 - psfStarDeltaE1Scatter
100 - psfStarDeltaE2Scatter
101 - psfStarDeltaSizeMedian
102 - psfStarDeltaSizeScatter
103 - psfStarScaledDeltaSizeScatter
105 ConfigClass = ComputeExposureSummaryStatsConfig
106 _DefaultName = "computeExposureSummaryStats"
109 def run(self, exposure, sources, background):
110 """Measure exposure statistics from the exposure, sources, and background.
114 exposure : `lsst.afw.image.ExposureF`
116 background : `lsst.afw.math.BackgroundList`
120 summary : `lsst.afw.image.ExposureSummary`
122 self.log.info("Measuring exposure statistics")
124 summary = afwImage.ExposureSummaryStats()
126 bbox = exposure.getBBox()
128 psf = exposure.getPsf()
131 wcs = exposure.getWcs()
132 visitInfo = exposure.getInfo().getVisitInfo()
135 photoCalib = exposure.getPhotoCalib()
142 md = exposure.getMetadata()
143 if 'SFM_ASTROM_OFFSET_MEAN' in md:
144 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
145 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
149 def update_psf_stats(self, summary, psf, bbox, sources=None, mask=None, sources_columns=None):
150 """Compute all summary-statistic fields that depend on the PSF model.
154 summary : `lsst.afw.image.ExposureSummaryStats`
155 Summary object to update in-place.
157 Point spread function model. If `
None`, all fields that depend on
158 the PSF will be reset (generally to NaN).
160 Bounding box of the image
for which summary stats are being
163 Catalog
for quantities that are computed
from source table columns.
164 If `
None`, these quantities will be reset (generally to NaN).
166 Mask image that may be used to compute distance-to-nearest-star
168 sources_columns : `collections.abc.Set` [ `str` ], optional
169 Set of all column names
in ``sources``. If provided, ``sources``
170 may be any table type
for which string indexes
yield column arrays.
171 If
not provided, ``sources``
is assumed to be an
175 summary.psfSigma = nan
179 summary.psfArea = nan
181 summary.psfStarDeltaE1Median = nan
182 summary.psfStarDeltaE2Median = nan
183 summary.psfStarDeltaE1Scatter = nan
184 summary.psfStarDeltaE2Scatter = nan
185 summary.psfStarDeltaSizeMedian = nan
186 summary.psfStarDeltaSizeScatter = nan
187 summary.psfStarScaledDeltaSizeScatter = nan
191 shape = psf.computeShape(bbox.getCenter())
192 summary.psfSigma = shape.getDeterminantRadius()
193 summary.psfIxx = shape.getIxx()
194 summary.psfIyy = shape.getIyy()
195 summary.psfIxy = shape.getIxy()
196 im = psf.computeKernelImage(bbox.getCenter())
201 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.))
207 if sources_columns
is None:
208 sources_columns = sources.schema.getNames()
210 self.config.starSelection
not in sources_columns
211 or self.config.starShape +
'_flag' not in sources_columns
216 mask = sources[self.config.starSelection] & (~sources[self.config.starShape +
'_flag'])
217 nPsfStar = mask.sum()
224 starXX = sources[self.config.starShape +
'_xx'][mask]
225 starYY = sources[self.config.starShape +
'_yy'][mask]
226 starXY = sources[self.config.starShape +
'_xy'][mask]
227 psfXX = sources[self.config.psfShape +
'_xx'][mask]
228 psfYY = sources[self.config.psfShape +
'_yy'][mask]
229 psfXY = sources[self.config.psfShape +
'_xy'][mask]
231 starSize = (starXX*starYY - starXY**2.)**0.25
232 starE1 = (starXX - starYY)/(starXX + starYY)
233 starE2 = 2*starXY/(starXX + starYY)
234 starSizeMedian = np.median(starSize)
236 psfSize = (psfXX*psfYY - psfXY**2)**0.25
237 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
238 psfE2 = 2*psfXY/(psfXX + psfYY)
240 psfStarDeltaE1Median = np.median(starE1 - psfE1)
241 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
242 psfStarDeltaE2Median = np.median(starE2 - psfE2)
243 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
245 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
246 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
247 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian**2.
249 summary.nPsfStar = int(nPsfStar)
250 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
251 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
252 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
253 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
254 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
255 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
256 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
259 """Compute all summary-statistic fields that depend on the WCS model.
263 summary : `lsst.afw.image.ExposureSummaryStats`
264 Summary object to update in-place.
266 Astrometric calibration model. If `
None`, all fields that depend
267 on the WCS will be reset (generally to NaN).
269 Bounding box of the image
for which summary stats are being
272 Observation information used
in together
with ``wcs`` to compute
276 summary.raCorners = [nan]*4
277 summary.decCorners = [nan]*4
280 summary.zenithDistance = nan
286 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
287 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
289 sph_pt = wcs.pixelToSky(bbox.getCenter())
290 summary.ra = sph_pt.getRa().asDegrees()
291 summary.decl = sph_pt.getDec().asDegrees()
293 date = visitInfo.getDate()
299 observatory = visitInfo.getObservatory()
300 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
301 lon=observatory.getLongitude().asDegrees()*units.deg,
302 height=observatory.getElevation()*units.m)
303 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
304 location=loc, format=
'mjd')
306 summary.ra*units.degree,
307 summary.decl*units.degree,
311 with warnings.catch_warnings():
312 warnings.simplefilter(
'ignore')
313 altaz = coord.transform_to(AltAz)
315 summary.zenithDistance = float(90.0 - altaz.alt.degree)
318 """Compute all summary-statistic fields that depend on the photometric
323 summary : `lsst.afw.image.ExposureSummaryStats`
324 Summary object to update in-place.
326 Photometric calibration model. If `
None`, all fields that depend
327 on the photometric calibration will be reset (generally to NaN).
329 if photo_calib
is not None:
330 summary.zeroPoint = float(2.5*np.log10(photo_calib.getInstFluxAtZeroMagnitude()))
332 summary.zeroPoint = float(
"nan")
335 """Compute summary-statistic fields that depend only on the
340 summary : `lsst.afw.image.ExposureSummaryStats`
341 Summary object to update in-place.
342 background : `lsst.afw.math.BackgroundList`
or `
None`
343 Background model. If `
None`, all fields that depend on the
344 background will be reset (generally to NaN).
348 This does
not include fields that depend on the background-subtracted
349 masked image; when the background changes, it should generally be
350 applied to the image
and `update_masked_image_stats` should be called
353 if background
is not None:
354 bgStats = (bg[0].getStatsImage().getImage().array
355 for bg
in background)
356 summary.skyBg = float(sum(np.median(bg[np.isfinite(bg)])
for bg
in bgStats))
358 summary.skyBg = float(
"nan")
361 """Compute summary-statistic fields that depend on the masked image
366 summary : `lsst.afw.image.ExposureSummaryStats`
367 Summary object to update in-place.
369 Masked image. If `
None`, all fields that depend
370 on the masked image will be reset (generally to NaN).
373 if masked_image
is None:
374 summary.skyNoise = nan
375 summary.meanVar = nan
377 statsCtrl = afwMath.StatisticsControl()
378 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
379 statsCtrl.setNumIter(self.config.clipIter)
380 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
381 statsCtrl.setNanSafe(
True)
383 statObj = afwMath.makeStatistics(masked_image, afwMath.STDEVCLIP, statsCtrl)
384 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
385 summary.skyNoise = skyNoise
387 statObj = afwMath.makeStatistics(masked_image.variance, masked_image.mask, afwMath.MEANCLIP,
389 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
390 summary.meanVar = meanVar
def update_masked_image_stats(self, summary, masked_image)
def update_wcs_stats(self, summary, wcs, bbox, visitInfo)
def update_background_stats(self, summary, background)
def update_photo_calib_stats(self, summary, photo_calib)
def update_psf_stats(self, summary, psf, bbox, sources=None, mask=None, sources_columns=None)