196 def run(self, exposure, sources, background):
197 """Measure exposure statistics from the exposure, sources, and
202 exposure : `lsst.afw.image.ExposureF`
203 sources : `lsst.afw.table.SourceCatalog`
204 background : `lsst.afw.math.BackgroundList`
208 summary : `lsst.afw.image.ExposureSummary`
210 self.log.info(
"Measuring exposure statistics")
214 bbox = exposure.getBBox()
216 psf = exposure.getPsf()
217 self.
update_psf_stats(summary, psf, bbox, sources, image_mask=exposure.mask)
219 wcs = exposure.getWcs()
220 visitInfo = exposure.getInfo().getVisitInfo()
223 photoCalib = exposure.getPhotoCalib()
232 md = exposure.getMetadata()
233 if 'SFM_ASTROM_OFFSET_MEAN' in md:
234 summary.astromOffsetMean = md[
'SFM_ASTROM_OFFSET_MEAN']
235 summary.astromOffsetStd = md[
'SFM_ASTROM_OFFSET_STD']
239 def update_psf_stats(self, summary, psf, bbox, sources=None, image_mask=None, sources_is_astropy=False):
240 """Compute all summary-statistic fields that depend on the PSF model.
244 summary : `lsst.afw.image.ExposureSummaryStats`
245 Summary object to update in-place.
246 psf : `lsst.afw.detection.Psf` or `None`
247 Point spread function model. If `None`, all fields that depend on
248 the PSF will be reset (generally to NaN).
249 bbox : `lsst.geom.Box2I`
250 Bounding box of the image for which summary stats are being
252 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
253 Catalog for quantities that are computed from source table columns.
254 If `None`, these quantities will be reset (generally to NaN).
255 The type of this table must correspond to the
256 ``sources_is_astropy`` argument.
257 image_mask : `lsst.afw.image.Mask`, optional
258 Mask image that may be used to compute distance-to-nearest-star
260 sources_is_astropy : `bool`, optional
261 Whether ``sources`` is an `astropy.table.Table` instance instead
262 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
266 summary.psfSigma = nan
270 summary.psfArea = nan
272 summary.psfStarDeltaE1Median = nan
273 summary.psfStarDeltaE2Median = nan
274 summary.psfStarDeltaE1Scatter = nan
275 summary.psfStarDeltaE2Scatter = nan
276 summary.psfStarDeltaSizeMedian = nan
277 summary.psfStarDeltaSizeScatter = nan
278 summary.psfStarScaledDeltaSizeScatter = nan
279 summary.maxDistToNearestPsf = nan
280 summary.psfTraceRadiusDelta = nan
284 shape = psf.computeShape(bbox.getCenter())
285 summary.psfSigma = shape.getDeterminantRadius()
286 summary.psfIxx = shape.getIxx()
287 summary.psfIyy = shape.getIyy()
288 summary.psfIxy = shape.getIxy()
289 im = psf.computeKernelImage(bbox.getCenter())
294 summary.psfArea = float(np.sum(im.array)/np.sum(im.array**2.))
296 if image_mask
is not None:
300 sampling=self.config.psfGridSampling,
301 bad_mask_bits=self.config.psfBadMaskPlanes
303 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
312 nPsfStar = sources[self.config.starSelection].sum()
313 summary.nPsfStar = int(nPsfStar)
315 psf_mask = self.starSelector.run(sources).selected
316 nPsfStarsUsedInStats = psf_mask.sum()
318 if nPsfStarsUsedInStats == 0:
323 if sources_is_astropy:
324 psf_cat = sources[psf_mask]
326 psf_cat = sources[psf_mask].copy(deep=
True)
328 starXX = psf_cat[self.config.starShape +
'_xx']
329 starYY = psf_cat[self.config.starShape +
'_yy']
330 starXY = psf_cat[self.config.starShape +
'_xy']
331 psfXX = psf_cat[self.config.psfShape +
'_xx']
332 psfYY = psf_cat[self.config.psfShape +
'_yy']
333 psfXY = psf_cat[self.config.psfShape +
'_xy']
336 starSize = np.sqrt(starXX/2. + starYY/2.)
338 starE1 = (starXX - starYY)/(starXX + starYY)
339 starE2 = 2*starXY/(starXX + starYY)
340 starSizeMedian = np.median(starSize)
343 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
344 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
345 psfE2 = 2*psfXY/(psfXX + psfYY)
347 psfStarDeltaE1Median = np.median(starE1 - psfE1)
348 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale=
'normal')
349 psfStarDeltaE2Median = np.median(starE2 - psfE2)
350 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale=
'normal')
352 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
353 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale=
'normal')
354 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
356 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
357 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
358 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
359 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
360 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
361 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
362 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
364 if image_mask
is not None:
368 sampling=self.config.psfSampling,
369 bad_mask_bits=self.config.psfBadMaskPlanes
371 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
374 """Compute all summary-statistic fields that depend on the WCS model.
378 summary : `lsst.afw.image.ExposureSummaryStats`
379 Summary object to update in-place.
380 wcs : `lsst.afw.geom.SkyWcs` or `None`
381 Astrometric calibration model. If `None`, all fields that depend
382 on the WCS will be reset (generally to NaN).
383 bbox : `lsst.geom.Box2I`
384 Bounding box of the image for which summary stats are being
386 visitInfo : `lsst.afw.image.VisitInfo`
387 Observation information used in together with ``wcs`` to compute
391 summary.raCorners = [nan]*4
392 summary.decCorners = [nan]*4
395 summary.zenithDistance = nan
400 sph_pts = wcs.pixelToSky(
geom.Box2D(bbox).getCorners())
401 summary.raCorners = [float(sph.getRa().asDegrees())
for sph
in sph_pts]
402 summary.decCorners = [float(sph.getDec().asDegrees())
for sph
in sph_pts]
404 sph_pt = wcs.pixelToSky(bbox.getCenter())
405 summary.ra = sph_pt.getRa().asDegrees()
406 summary.dec = sph_pt.getDec().asDegrees()
408 date = visitInfo.getDate()
415 observatory = visitInfo.getObservatory()
416 loc = EarthLocation(lat=observatory.getLatitude().asDegrees()*units.deg,
417 lon=observatory.getLongitude().asDegrees()*units.deg,
418 height=observatory.getElevation()*units.m)
419 obstime = Time(visitInfo.getDate().get(system=DateTime.MJD),
420 location=loc, format=
'mjd')
422 summary.ra*units.degree,
423 summary.dec*units.degree,
427 with warnings.catch_warnings():
428 warnings.simplefilter(
'ignore')
429 altaz = coord.transform_to(AltAz)
431 summary.zenithDistance = float(90.0 - altaz.alt.degree)
477 """Compute summary-statistic fields that depend on the masked image
482 summary : `lsst.afw.image.ExposureSummaryStats`
483 Summary object to update in-place.
484 masked_image : `lsst.afw.image.MaskedImage` or `None`
485 Masked image. If `None`, all fields that depend
486 on the masked image will be reset (generally to NaN).
489 if masked_image
is None:
490 summary.skyNoise = nan
491 summary.meanVar = nan
494 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
495 statsCtrl.setNumIter(self.config.clipIter)
496 statsCtrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
497 statsCtrl.setNanSafe(
True)
500 skyNoise, _ = statObj.getResult(afwMath.STDEVCLIP)
501 summary.skyNoise = skyNoise
505 meanVar, _ = statObj.getResult(afwMath.MEANCLIP)
506 summary.meanVar = meanVar
509 """Compute effective exposure time statistics to estimate depth.
511 The effective exposure time is the equivalent shutter open
512 time that would be needed under nominal conditions to give the
513 same signal-to-noise for a point source as what is achieved by
514 the observation of interest. This metric combines measurements
515 of the point-spread function, the sky brightness, and the
518 .. _teff_definitions:
520 The effective exposure time and its subcomponents are defined in [1]_
525 .. [1] Neilsen, E.H., Bernstein, G., Gruendl, R., and Kent, S. (2016).
526 Limiting Magnitude, \tau, teff, and Image Quality in DES Year 1
527 https://www.osti.gov/biblio/1250877/
532 summary : `lsst.afw.image.ExposureSummaryStats`
533 Summary object to update in-place.
534 exposure : `lsst.afw.image.ExposureF`
535 Exposure to grab band and exposure time metadata
538 self.log.info(
"Updating effective exposure time")
541 summary.effTime = nan
542 summary.effTimePsfSigmaScale = nan
543 summary.effTimeSkyBgScale = nan
544 summary.effTimeZeroPointScale = nan
546 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
547 filterLabel = exposure.getFilter()
548 if (filterLabel
is None)
or (
not filterLabel.hasBandLabel):
551 band = filterLabel.bandLabel
554 self.log.warn(
"No band associated with exposure; effTime not calculated.")
558 if np.isnan(summary.psfSigma):
559 self.log.debug(
"PSF sigma is NaN")
561 elif band
not in self.config.fiducialPsfSigma:
562 self.log.debug(f
"Fiducial PSF value not found for {band}")
565 fiducialPsfSigma = self.config.fiducialPsfSigma[band]
566 f_eff = (summary.psfSigma / fiducialPsfSigma)**-2
569 if np.isnan(summary.zeroPoint):
570 self.log.debug(
"Zero point is NaN")
572 elif band
not in self.config.fiducialZeroPoint:
573 self.log.debug(f
"Fiducial zero point value not found for {band}")
576 fiducialZeroPoint = self.config.fiducialZeroPoint[band]
577 zeroPointDiff = fiducialZeroPoint - (summary.zeroPoint - 2.5*np.log10(exposureTime))
578 c_eff =
min(10**(-2.0*(zeroPointDiff)/2.5), self.config.maxEffectiveTransparency)
581 if np.isnan(summary.skyBg):
582 self.log.debug(
"Sky background is NaN")
584 elif band
not in self.config.fiducialSkyBackground:
585 self.log.debug(f
"Fiducial sky background value not found for {band}")
588 fiducialSkyBackground = self.config.fiducialSkyBackground[band]
589 b_eff = fiducialSkyBackground/(summary.skyBg/exposureTime)
592 t_eff = f_eff * c_eff * b_eff
595 effectiveTime = t_eff * exposureTime
598 summary.effTime = float(effectiveTime)
599 summary.effTimePsfSigmaScale = float(f_eff)
600 summary.effTimeSkyBgScale = float(b_eff)
601 summary.effTimeZeroPointScale = float(c_eff)