217 def run(self, exposure, sources, background):
218 """Measure exposure statistics from the exposure, sources, and
223 exposure : `lsst.afw.image.ExposureF`
224 sources : `lsst.afw.table.SourceCatalog`
225 background : `lsst.afw.math.BackgroundList`
229 summary : `lsst.afw.image.ExposureSummary`
231 self.log.info(
"Measuring exposure statistics")
236 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
237 summary.expTime = exposureTime
239 bbox = exposure.getBBox()
241 psf = exposure.getPsf()
243 summary, psf, bbox, sources, image_mask=exposure.mask, image_ap_corr_map=exposure.apCorrMap
246 wcs = exposure.getWcs()
247 visitInfo = exposure.getInfo().getVisitInfo()
250 photoCalib = exposure.getPhotoCalib()
259 md = exposure.getMetadata()
260 if 'SFM_ASTROM_OFFSET_MEAN' in md:
261 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
262 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
273 image_ap_corr_map=None,
274 sources_is_astropy=False,
276 """Compute all summary-statistic fields that depend on the PSF model.
280 summary : `lsst.afw.image.ExposureSummaryStats`
281 Summary object to update in-place.
282 psf : `lsst.afw.detection.Psf` or `None`
283 Point spread function model. If `None`, all fields that depend on
284 the PSF will be reset (generally to NaN).
285 bbox : `lsst.geom.Box2I`
286 Bounding box of the image for which summary stats are being
288 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
289 Catalog for quantities that are computed from source table columns.
290 If `None`, these quantities will be reset (generally to NaN).
291 The type of this table must correspond to the
292 ``sources_is_astropy`` argument.
293 image_mask : `lsst.afw.image.Mask`, optional
294 Mask image that may be used to compute distance-to-nearest-star
296 sources_is_astropy : `bool`, optional
297 Whether ``sources`` is an `astropy.table.Table` instance instead
298 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
302 summary.psfSigma = nan
306 summary.psfArea = nan
308 summary.psfStarDeltaE1Median = nan
309 summary.psfStarDeltaE2Median = nan
310 summary.psfStarDeltaE1Scatter = nan
311 summary.psfStarDeltaE2Scatter = nan
312 summary.psfStarDeltaSizeMedian = nan
313 summary.psfStarDeltaSizeScatter = nan
314 summary.psfStarScaledDeltaSizeScatter = nan
315 summary.maxDistToNearestPsf = nan
316 summary.psfTraceRadiusDelta = nan
317 summary.psfApFluxDelta = nan
318 summary.psfApCorrSigmaScaledDelta = nan
322 shape = psf.computeShape(bbox.getCenter())
323 summary.psfSigma = shape.getDeterminantRadius()
324 summary.psfIxx = shape.getIxx()
325 summary.psfIyy = shape.getIyy()
326 summary.psfIxy = shape.getIxy()
327 im = psf.computeKernelImage(bbox.getCenter())
332 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.))
334 if image_mask
is not None:
335 psfApRadius =
max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
336 self.log.debug(
"Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
340 sampling=self.config.psfGridSampling,
341 ap_radius_pix=psfApRadius,
342 bad_mask_bits=self.config.psfBadMaskPlanes
344 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
345 summary.psfApFluxDelta = float(psfApFluxDelta)
346 if image_ap_corr_map
is not None:
347 if self.config.psfApCorrFieldName
not in image_ap_corr_map.keys():
348 self.log.warn(f
"{self.config.psfApCorrFieldName} not found in "
349 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
350 psfApCorrSigmaScaledDelta = nan
352 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
357 sampling=self.config.psfGridSampling,
358 bad_mask_bits=self.config.psfBadMaskPlanes,
360 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
369 nPsfStar = sources[self.config.starSelection].sum()
370 summary.nPsfStar = int(nPsfStar)
372 psf_mask = self.starSelector.run(sources).selected
373 nPsfStarsUsedInStats = psf_mask.sum()
375 if nPsfStarsUsedInStats == 0:
380 if sources_is_astropy:
381 psf_cat = sources[psf_mask]
383 psf_cat = sources[psf_mask].copy(deep=
True)
385 starXX = psf_cat[self.config.starShape +
'_xx']
386 starYY = psf_cat[self.config.starShape +
'_yy']
387 starXY = psf_cat[self.config.starShape +
'_xy']
388 psfXX = psf_cat[self.config.psfShape +
'_xx']
389 psfYY = psf_cat[self.config.psfShape +
'_yy']
390 psfXY = psf_cat[self.config.psfShape +
'_xy']
393 starSize = np.sqrt(starXX/2. + starYY/2.)
395 starE1 = (starXX - starYY)/(starXX + starYY)
396 starE2 = 2*starXY/(starXX + starYY)
397 starSizeMedian = np.median(starSize)
400 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
401 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
402 psfE2 = 2*psfXY/(psfXX + psfYY)
404 psfStarDeltaE1Median = np.median(starE1 - psfE1)
405 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
406 psfStarDeltaE2Median = np.median(starE2 - psfE2)
407 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
409 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
410 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
411 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
413 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
414 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
415 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
416 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
417 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
418 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
419 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
421 if image_mask
is not None:
425 sampling=self.config.psfSampling,
426 bad_mask_bits=self.config.psfBadMaskPlanes
428 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
431 """Compute all summary-statistic fields that depend on the WCS model.
435 summary : `lsst.afw.image.ExposureSummaryStats`
436 Summary object to update in-place.
437 wcs : `lsst.afw.geom.SkyWcs` or `None`
438 Astrometric calibration model. If `None`, all fields that depend
439 on the WCS will be reset (generally to NaN).
440 bbox : `lsst.geom.Box2I`
441 Bounding box of the image for which summary stats are being
443 visitInfo : `lsst.afw.image.VisitInfo`
444 Observation information used in together with ``wcs`` to compute
448 summary.raCorners = [nan]*4
449 summary.decCorners = [nan]*4
452 summary.pixelScale = nan
453 summary.zenithDistance = nan
458 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
459 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
460 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
462 sph_pt = wcs.pixelToSky(bbox.getCenter())
463 summary.ra = sph_pt.getRa().asDegrees()
464 summary.dec = sph_pt.getDec().asDegrees()
465 summary.pixelScale = wcs.getPixelScale().asArcseconds()
467 date = visitInfo.getDate()
474 observatory = visitInfo.getObservatory()
475 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
476 lon=observatory.getLongitude().asDegrees()*units.deg,
477 height=observatory.getElevation()*units.m)
478 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
479 location=loc, format=
'mjd')
481 summary.ra*units.degree,
482 summary.dec*units.degree,
486 with warnings.catch_warnings():
487 warnings.simplefilter(
'ignore')
488 altaz = coord.transform_to(AltAz)
490 summary.zenithDistance = float(90.0 - altaz.alt.degree)
536 """Compute summary-statistic fields that depend on the masked image
541 summary : `lsst.afw.image.ExposureSummaryStats`
542 Summary object to update in-place.
543 masked_image : `lsst.afw.image.MaskedImage` or `None`
544 Masked image. If `None`, all fields that depend
545 on the masked image will be reset (generally to NaN).
548 if masked_image
is None:
549 summary.skyNoise = nan
550 summary.meanVar = nan
553 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
554 statsCtrl.setNumIter(self.config.clipIter)
555 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
556 statsCtrl.setNanSafe(
True)
559 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
560 summary.skyNoise = skyNoise
564 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
565 summary.meanVar = meanVar
568 """Compute effective exposure time statistics to estimate depth.
570 The effective exposure time is the equivalent shutter open
571 time that would be needed under nominal conditions to give the
572 same signal-to-noise for a point source as what is achieved by
573 the observation of interest. This metric combines measurements
574 of the point-spread function, the sky brightness, and the
577 .. _teff_definitions:
579 The effective exposure time and its subcomponents are defined in [1]_
584 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
585 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
586 https://www.osti.gov/biblio/1250877/
591 summary : `lsst.afw.image.ExposureSummaryStats`
592 Summary object to update in-place.
593 exposure : `lsst.afw.image.ExposureF`
594 Exposure to grab band and exposure time metadata
597 self.log.info(
"Updating effective exposure time")
600 summary.effTime = nan
601 summary.effTimePsfSigmaScale = nan
602 summary.effTimeSkyBgScale = nan
603 summary.effTimeZeroPointScale = nan
605 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
606 filterLabel = exposure.getFilter()
607 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
610 band = filterLabel.bandLabel
613 self.log.warn(
"No band associated with exposure; effTime not calculated.")
617 if np.isnan(summary.psfSigma):
618 self.log.debug(
"PSF sigma is NaN")
620 elif band
not in self.config.fiducialPsfSigma:
621 self.log.debug(f
"Fiducial PSF value not found for {band}")
624 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
625 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
628 if np.isnan(summary.zeroPoint):
629 self.log.debug(
"Zero point is NaN")
631 elif band
not in self.config.fiducialZeroPoint:
632 self.log.debug(f
"Fiducial zero point value not found for {band}")
635 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
636 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
637 c_eff =
min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
640 if np.isnan(summary.skyBg):
641 self.log.debug(
"Sky background is NaN")
643 elif band
not in self.config.fiducialSkyBackground:
644 self.log.debug(f
"Fiducial sky background value not found for {band}")
647 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
648 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
651 t_eff = f_eff * c_eff * b_eff
654 effectiveTime = t_eff * exposureTime
657 summary.effTime = float(effectiveTime)
658 summary.effTimePsfSigmaScale = float(f_eff)
659 summary.effTimeSkyBgScale = float(b_eff)
660 summary.effTimeZeroPointScale = float(c_eff)