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.",
67 psfShape = pexConfig.Field(
68 doc=
"Base name of columns to use for the PSF shape in the PSF statistics computation.",
70 default=
"slot_PsfShape"
72 psfSampling = pexConfig.Field(
74 doc=
"Sampling rate in pixels in each dimension for the maxDistToNearestPsf metric "
75 "caclulation grid (the tradeoff is between adequate sampling versus speed).",
78 psfGridSampling = pexConfig.Field(
80 doc=
"Sampling rate in pixels in each dimension for PSF model robustness metric "
81 "caclulations grid (the tradeoff is between adequate sampling versus speed).",
84 psfBadMaskPlanes = pexConfig.ListField(
86 doc=
"Mask planes that, if set, the associated pixel should not be included in the PSF model "
87 "robutsness metric calculations (namely, maxDistToNearestPsf and psfTraceRadiusDelta).",
88 default=(
"BAD",
"CR",
"EDGE",
"INTRP",
"NO_DATA",
"SAT",
"SUSPECT"),
93 """Task to compute exposure summary statistics.
95 This task computes various quantities suitable for DPDD
and other
96 downstream processing at the detector centers, including:
114 These additional quantities are computed
from the stars
in the detector:
115 - psfStarDeltaE1Median
116 - psfStarDeltaE2Median
117 - psfStarDeltaE1Scatter
118 - psfStarDeltaE2Scatter
119 - psfStarDeltaSizeMedian
120 - psfStarDeltaSizeScatter
121 - psfStarScaledDeltaSizeScatter
123 These quantities are computed based on the PSF model
and image mask
124 to assess the robustness of the PSF model across a given detector
125 (against, e.g., extrapolation instability):
126 - maxDistToNearestPsf
127 - psfTraceRadiusDelta
129 ConfigClass = ComputeExposureSummaryStatsConfig
130 _DefaultName = "computeExposureSummaryStats"
133 def run(self, exposure, sources, background):
134 """Measure exposure statistics from the exposure, sources, and
139 exposure : `lsst.afw.image.ExposureF`
145 summary : `lsst.afw.image.ExposureSummary`
147 self.log.info("Measuring exposure statistics")
151 bbox = exposure.getBBox()
153 psf = exposure.getPsf()
154 self.
update_psf_stats(summary, psf, bbox, sources, image_mask=exposure.mask)
156 wcs = exposure.getWcs()
157 visitInfo = exposure.getInfo().getVisitInfo()
160 photoCalib = exposure.getPhotoCalib()
167 md = exposure.getMetadata()
168 if 'SFM_ASTROM_OFFSET_MEAN' in md:
169 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
170 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
174 def update_psf_stats(self, summary, psf, bbox, sources=None, image_mask=None, sources_is_astropy=False):
175 """Compute all summary-statistic fields that depend on the PSF model.
180 Summary object to update in-place.
182 Point spread function model. If `
None`, all fields that depend on
183 the PSF will be reset (generally to NaN).
185 Bounding box of the image
for which summary stats are being
188 Catalog
for quantities that are computed
from source table columns.
189 If `
None`, these quantities will be reset (generally to NaN).
190 The type of this table must correspond to the
191 ``sources_is_astropy`` argument.
193 Mask image that may be used to compute distance-to-nearest-star
195 sources_is_astropy : `bool`, optional
196 Whether ``sources``
is an `astropy.table.Table` instance instead
201 summary.psfSigma = nan
205 summary.psfArea = nan
207 summary.psfStarDeltaE1Median = nan
208 summary.psfStarDeltaE2Median = nan
209 summary.psfStarDeltaE1Scatter = nan
210 summary.psfStarDeltaE2Scatter = nan
211 summary.psfStarDeltaSizeMedian = nan
212 summary.psfStarDeltaSizeScatter = nan
213 summary.psfStarScaledDeltaSizeScatter = nan
214 summary.maxDistToNearestPsf = nan
215 summary.psfTraceRadiusDelta = nan
219 shape = psf.computeShape(bbox.getCenter())
220 summary.psfSigma = shape.getDeterminantRadius()
221 summary.psfIxx = shape.getIxx()
222 summary.psfIyy = shape.getIyy()
223 summary.psfIxy = shape.getIxy()
224 im = psf.computeKernelImage(bbox.getCenter())
229 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.))
231 if image_mask
is not None:
235 sampling=self.config.psfGridSampling,
236 bad_mask_bits=self.config.psfBadMaskPlanes
238 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
244 psf_mask = sources[self.config.starSelection] & (~sources[self.config.starShape +
'_flag'])
245 nPsfStar = psf_mask.sum()
252 if sources_is_astropy:
253 psf_cat = sources[psf_mask]
255 psf_cat = sources[psf_mask].copy(deep=
True)
257 starXX = psf_cat[self.config.starShape +
'_xx']
258 starYY = psf_cat[self.config.starShape +
'_yy']
259 starXY = psf_cat[self.config.starShape +
'_xy']
260 psfXX = psf_cat[self.config.psfShape +
'_xx']
261 psfYY = psf_cat[self.config.psfShape +
'_yy']
262 psfXY = psf_cat[self.config.psfShape +
'_xy']
264 starSize = (starXX*starYY - starXY**2.)**0.25
265 starE1 = (starXX - starYY)/(starXX + starYY)
266 starE2 = 2*starXY/(starXX + starYY)
267 starSizeMedian = np.median(starSize)
269 psfSize = (psfXX*psfYY - psfXY**2)**0.25
270 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
271 psfE2 = 2*psfXY/(psfXX + psfYY)
273 psfStarDeltaE1Median = np.median(starE1 - psfE1)
274 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
275 psfStarDeltaE2Median = np.median(starE2 - psfE2)
276 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
278 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
279 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
280 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian**2.
282 summary.nPsfStar = int(nPsfStar)
283 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
284 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
285 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
286 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
287 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
288 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
289 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
291 if image_mask
is not None:
295 sampling=self.config.psfSampling,
296 bad_mask_bits=self.config.psfBadMaskPlanes
298 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
301 """Compute all summary-statistic fields that depend on the WCS model.
306 Summary object to update in-place.
308 Astrometric calibration model. If `
None`, all fields that depend
309 on the WCS will be reset (generally to NaN).
311 Bounding box of the image
for which summary stats are being
314 Observation information used
in together
with ``wcs`` to compute
318 summary.raCorners = [nan]*4
319 summary.decCorners = [nan]*4
322 summary.zenithDistance = nan
327 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
328 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
329 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
331 sph_pt = wcs.pixelToSky(bbox.getCenter())
332 summary.ra = sph_pt.getRa().asDegrees()
333 summary.dec = sph_pt.getDec().asDegrees()
335 date = visitInfo.getDate()
342 observatory = visitInfo.getObservatory()
343 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
344 lon=observatory.getLongitude().asDegrees()*units.deg,
345 height=observatory.getElevation()*units.m)
346 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
347 location=loc, format=
'mjd')
349 summary.ra*units.degree,
350 summary.dec*units.degree,
354 with warnings.catch_warnings():
355 warnings.simplefilter(
'ignore')
356 altaz = coord.transform_to(AltAz)
358 summary.zenithDistance = float(90.0 - altaz.alt.degree)
361 """Compute all summary-statistic fields that depend on the photometric
367 Summary object to update in-place.
369 Photometric calibration model. If `
None`, all fields that depend
370 on the photometric calibration will be reset (generally to NaN).
372 if photo_calib
is not None:
373 summary.zeroPoint = float(2.5*np.log10(photo_calib.getInstFluxAtZeroMagnitude()))
375 summary.zeroPoint = float(
"nan")
378 """Compute summary-statistic fields that depend only on the
384 Summary object to update in-place.
386 Background model. If `
None`, all fields that depend on the
387 background will be reset (generally to NaN).
391 This does
not include fields that depend on the background-subtracted
392 masked image; when the background changes, it should generally be
393 applied to the image
and `update_masked_image_stats` should be called
396 if background
is not None:
397 bgStats = (bg[0].getStatsImage().getImage().array
398 for bg
in background)
399 summary.skyBg = float(sum(np.median(bg[np.isfinite(bg)])
for bg
in bgStats))
401 summary.skyBg = float(
"nan")
404 """Compute summary-statistic fields that depend on the masked image
410 Summary object to update in-place.
412 Masked image. If `
None`, all fields that depend
413 on the masked image will be reset (generally to NaN).
416 if masked_image
is None:
417 summary.skyNoise = nan
418 summary.meanVar = nan
421 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
422 statsCtrl.setNumIter(self.config.clipIter)
423 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
424 statsCtrl.setNanSafe(
True)
427 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
428 summary.skyNoise = skyNoise
432 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
433 summary.meanVar = meanVar
440 bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"],
442 """Compute the maximum distance of an unmasked pixel to its nearest PSF.
447 The mask plane associated with the exposure.
449 Catalog containing only the stars used
in the PSF modeling.
451 Sampling rate
in each dimension to create the grid of points on which
452 to evaluate the distance to the nearest PSF star. The tradeoff
is
453 between adequate sampling versus speed.
454 bad_mask_bits : `list` [`str`]
455 Mask bits required to be absent
for a pixel to be considered
460 max_dist_to_nearest_psf : `float`
461 The maximum distance (
in pixels) of an unmasked pixel to its nearest
464 mask_arr = image_mask.array[::sampling, ::sampling]
465 bitmask = image_mask.getPlaneBitMask(bad_mask_bits)
466 good = ((mask_arr & bitmask) == 0)
468 x = np.arange(good.shape[1]) * sampling
469 y = np.arange(good.shape[0]) * sampling
470 xx, yy = np.meshgrid(x, y)
472 dist_to_nearest_psf = np.full(good.shape, np.inf)
474 x_psf = psf[
"slot_Centroid_x"]
475 y_psf = psf[
"slot_Centroid_y"]
476 dist_to_nearest_psf = np.minimum(dist_to_nearest_psf, np.hypot(xx - x_psf, yy - y_psf))
477 unmasked_dists = dist_to_nearest_psf * good
478 max_dist_to_nearest_psf = np.max(unmasked_dists)
480 return max_dist_to_nearest_psf
487 bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"],
489 """Compute the delta between the maximum and minimum model PSF trace radius
490 values evaluated on a grid of points lying in the unmasked region of the
496 The mask plane associated
with the exposure.
498 The PSF model associated
with the exposure.
500 Sampling rate
in each dimension to create the grid of points at which
501 to evaluate ``image_psf``s trace radius value. The tradeoff
is between
502 adequate sampling versus speed.
503 bad_mask_bits : `list` [`str`]
504 Mask bits required to be absent
for a pixel to be considered
509 psf_trace_radius_delta : `float`
510 The delta (
in pixels) between the maximum
and minimum model PSF trace
511 radius values evaluated on the x,y-grid subsampled on the unmasked
512 detector pixels by a factor of ``sampling``. If any model PSF trace
513 radius value on the grid evaluates to NaN, then NaN
is returned
516 psf_trace_radius_list = []
517 mask_arr = image_mask.array[::sampling, ::sampling]
518 bitmask = image_mask.getPlaneBitMask(bad_mask_bits)
519 good = ((mask_arr & bitmask) == 0)
521 x = np.arange(good.shape[1]) * sampling
522 y = np.arange(good.shape[0]) * sampling
523 xx, yy = np.meshgrid(x, y)
525 for x_mesh, y_mesh, good_mesh
in zip(xx, yy, good):
526 for x_point, y_point, is_good
in zip(x_mesh, y_mesh, good_mesh):
528 psf_trace_radius = image_psf.computeShape(
geom.Point2D(x_point, y_point)).getTraceRadius()
529 if ~np.isfinite(psf_trace_radius):
531 psf_trace_radius_list.append(psf_trace_radius)
533 psf_trace_radius_delta = np.max(psf_trace_radius_list) - np.min(psf_trace_radius_list)
535 return psf_trace_radius_delta
A polymorphic base class for representing an image's Point Spread Function.
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Represent a 2-dimensional array of bitmask pixels.
A class to manipulate images, masks, and variance as a single object.
The photometric calibration of an exposure.
Information about a single exposure of an imaging camera.
Pass parameters to a Statistics object.
A floating-point coordinate rectangle geometry.
An integer coordinate rectangle.
update_masked_image_stats(self, summary, masked_image)
update_photo_calib_stats(self, summary, photo_calib)
update_wcs_stats(self, summary, wcs, bbox, visitInfo)
update_background_stats(self, summary, background)
update_psf_stats(self, summary, psf, bbox, sources=None, image_mask=None, sources_is_astropy=False)
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
psf_trace_radius_delta(image_mask, image_psf, sampling=96, bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"])
maximum_nearest_psf_distance(image_mask, psf_cat, sampling=8, bad_mask_bits=["BAD", "CR", "INTRP", "SAT", "SUSPECT", "NO_DATA", "EDGE"])