284 ):
285 """Compute all summary-statistic fields that depend on the PSF model.
286
287 Parameters
288 ----------
289 summary : `lsst.afw.image.ExposureSummaryStats`
290 Summary object to update in-place.
291 psf : `lsst.afw.detection.Psf` or `None`
292 Point spread function model. If `None`, all fields that depend on
293 the PSF will be reset (generally to NaN).
294 bbox : `lsst.geom.Box2I`
295 Bounding box of the image for which summary stats are being
296 computed.
297 sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
298 Catalog for quantities that are computed from source table columns.
299 If `None`, these quantities will be reset (generally to NaN).
300 The type of this table must correspond to the
301 ``sources_is_astropy`` argument.
302 image_mask : `lsst.afw.image.Mask`, optional
303 Mask image that may be used to compute distance-to-nearest-star
304 metrics.
305 sources_is_astropy : `bool`, optional
306 Whether ``sources`` is an `astropy.table.Table` instance instead
307 of an `lsst.afw.table.Catalog` instance. Default is `False` (the
308 latter).
309 """
310 nan = float("nan")
311 summary.psfSigma = nan
312 summary.psfIxx = nan
313 summary.psfIyy = nan
314 summary.psfIxy = nan
315 summary.psfArea = nan
316 summary.nPsfStar = 0
317 summary.psfStarDeltaE1Median = nan
318 summary.psfStarDeltaE2Median = nan
319 summary.psfStarDeltaE1Scatter = nan
320 summary.psfStarDeltaE2Scatter = nan
321 summary.psfStarDeltaSizeMedian = nan
322 summary.psfStarDeltaSizeScatter = nan
323 summary.psfStarScaledDeltaSizeScatter = nan
324 summary.maxDistToNearestPsf = nan
325 summary.psfTraceRadiusDelta = nan
326 summary.psfApFluxDelta = nan
327 summary.psfApCorrSigmaScaledDelta = nan
328
329 if psf is None:
330 return
331 shape = psf.computeShape(bbox.getCenter())
332 summary.psfSigma = shape.getDeterminantRadius()
333 summary.psfIxx = shape.getIxx()
334 summary.psfIyy = shape.getIyy()
335 summary.psfIxy = shape.getIxy()
336 im = psf.computeKernelImage(bbox.getCenter())
337
338
339 summary.psfArea = float(np.sum(im.array)**2./np.sum(im.array**2.))
340
341 if image_mask is not None:
342 psfApRadius = max(self.config.minPsfApRadiusPix, 3.0*summary.psfSigma)
343 self.log.debug("Using radius of %.3f (pixels) for psfApFluxDelta metric", psfApRadius)
344 psfTraceRadiusDelta, psfApFluxDelta = compute_psf_image_deltas(
345 image_mask,
346 psf,
347 sampling=self.config.psfGridSampling,
348 ap_radius_pix=psfApRadius,
349 bad_mask_bits=self.config.psfBadMaskPlanes
350 )
351 summary.psfTraceRadiusDelta = float(psfTraceRadiusDelta)
352 summary.psfApFluxDelta = float(psfApFluxDelta)
353 if image_ap_corr_map is not None:
354 if self.config.psfApCorrFieldName not in image_ap_corr_map.keys():
355 self.log.warning(f"{self.config.psfApCorrFieldName} not found in "
356 "image_ap_corr_map. Setting psfApCorrSigmaScaledDelta to NaN.")
357 psfApCorrSigmaScaledDelta = nan
358 else:
359 image_ap_corr_field = image_ap_corr_map[self.config.psfApCorrFieldName]
360 psfApCorrSigmaScaledDelta = compute_ap_corr_sigma_scaled_delta(
361 image_mask,
362 image_ap_corr_field,
363 summary.psfSigma,
364 sampling=self.config.psfGridSampling,
365 bad_mask_bits=self.config.psfBadMaskPlanes,
366 )
367 summary.psfApCorrSigmaScaledDelta = float(psfApCorrSigmaScaledDelta)
368
369 if sources is None:
370
371
372
373 return
374
375
376 nPsfStar = sources[self.config.starSelection].sum()
377 summary.nPsfStar = int(nPsfStar)
378
379 psf_mask = self.starSelector.run(sources).selected
380 nPsfStarsUsedInStats = psf_mask.sum()
381
382 if nPsfStarsUsedInStats == 0:
383
384
385 return
386
387 if sources_is_astropy:
388 psf_cat = sources[psf_mask]
389 else:
390 psf_cat = sources[psf_mask].copy(deep=True)
391
392 starXX = psf_cat[self.config.starShape + '_xx']
393 starYY = psf_cat[self.config.starShape + '_yy']
394 starXY = psf_cat[self.config.starShape + '_xy']
395 psfXX = psf_cat[self.config.psfShape + '_xx']
396 psfYY = psf_cat[self.config.psfShape + '_yy']
397 psfXY = psf_cat[self.config.psfShape + '_xy']
398
399
400 starSize = np.sqrt(starXX/2. + starYY/2.)
401
402 starE1 = (starXX - starYY)/(starXX + starYY)
403 starE2 = 2*starXY/(starXX + starYY)
404 starSizeMedian = np.median(starSize)
405
406
407 psfSize = np.sqrt(psfXX/2. + psfYY/2.)
408 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
409 psfE2 = 2*psfXY/(psfXX + psfYY)
410
411 psfStarDeltaE1Median = np.median(starE1 - psfE1)
412 psfStarDeltaE1Scatter = sigmaMad(starE1 - psfE1, scale='normal')
413 psfStarDeltaE2Median = np.median(starE2 - psfE2)
414 psfStarDeltaE2Scatter = sigmaMad(starE2 - psfE2, scale='normal')
415
416 psfStarDeltaSizeMedian = np.median(starSize - psfSize)
417 psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale='normal')
418 psfStarScaledDeltaSizeScatter = psfStarDeltaSizeScatter/starSizeMedian
419
420 summary.psfStarDeltaE1Median = float(psfStarDeltaE1Median)
421 summary.psfStarDeltaE2Median = float(psfStarDeltaE2Median)
422 summary.psfStarDeltaE1Scatter = float(psfStarDeltaE1Scatter)
423 summary.psfStarDeltaE2Scatter = float(psfStarDeltaE2Scatter)
424 summary.psfStarDeltaSizeMedian = float(psfStarDeltaSizeMedian)
425 summary.psfStarDeltaSizeScatter = float(psfStarDeltaSizeScatter)
426 summary.psfStarScaledDeltaSizeScatter = float(psfStarScaledDeltaSizeScatter)
427
428 if image_mask is not None:
429 maxDistToNearestPsf = maximum_nearest_psf_distance(
430 image_mask,
431 psf_cat,
432 sampling=self.config.psfSampling,
433 bad_mask_bits=self.config.psfBadMaskPlanes
434 )
435 summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
436