3535import lsst .afw .image as afwImage
3636import lsst .geom as geom
3737from lsst .meas .algorithms import ScienceSourceSelectorTask
38+ from lsst .meas .algorithms .computeExPsf import ComputeExPsfTask , ComputeExPsfConfig
3839from lsst .utils .timer import timeMethod
3940import lsst .ip .isr as ipIsr
4041
@@ -137,6 +138,22 @@ class ComputeExposureSummaryStatsConfig(pexConfig.Config):
137138 doc = "Signal-to-noise ratio for computing the magnitude limit depth." ,
138139 default = 5.0
139140 )
141+ psfTE1TreecorrConfig = pexConfig .ConfigField (
142+ dtype = ComputeExPsfConfig ,
143+ doc = "Treecorr config for computing scalar value of TE1." ,
144+ )
145+ psfTE2TreecorrConfig = pexConfig .ConfigField (
146+ dtype = ComputeExPsfConfig ,
147+ doc = "Treecorr config for computing scalar value of TE2." ,
148+ )
149+ psfTE3TreecorrConfig = pexConfig .ConfigField (
150+ dtype = ComputeExPsfConfig ,
151+ doc = "Treecorr config for computing scalar value of TE3." ,
152+ )
153+ psfTE4TreecorrConfig = pexConfig .ConfigField (
154+ dtype = ComputeExPsfConfig ,
155+ doc = "Treecorr config for computing scalar value of TE4." ,
156+ )
140157
141158 def setDefaults (self ):
142159 super ().setDefaults ()
@@ -159,6 +176,22 @@ def setDefaults(self):
159176 self .starSelector .signalToNoise .fluxField = "slot_PsfFlux_instFlux"
160177 self .starSelector .signalToNoise .errField = "slot_PsfFlux_instFluxErr"
161178
179+ min_theta = [1e-6 , 5.0 , 1e-6 , 5.0 ]
180+ max_theta = [1.0 , 180.0 , 5.0 , 12.0 ]
181+ TExTreecorrConfig = [
182+ self .psfTE1TreecorrConfig ,
183+ self .psfTE2TreecorrConfig ,
184+ self .psfTE3TreecorrConfig ,
185+ self .psfTE4TreecorrConfig ,
186+ ]
187+
188+ for texc , mint , maxt in zip (TExTreecorrConfig , min_theta , max_theta ):
189+ texc .treecorr .min_sep = mint / 60.0
190+ texc .treecorr .max_sep = maxt / 60.0
191+ texc .treecorr .nbins = 1
192+ texc .treecorr .bin_type = "Linear"
193+ texc .treecorr .sep_units = "degree"
194+
162195
163196class ComputeExposureSummaryStatsTask (pipeBase .Task ):
164197 """Task to compute exposure summary statistics.
@@ -200,6 +233,22 @@ class ComputeExposureSummaryStatsTask(pipeBase.Task):
200233 - psfTraceRadiusDelta
201234 - psfApFluxDelta
202235
236+ These quantities are computed as part of:
237+ https://rubinobs.atlassian.net/browse/DM-40780
238+
239+ - psfTE1e1
240+ - psfTE1e2
241+ - psfTE1ex
242+ - psfTE2e1
243+ - psfTE2e2
244+ - psfTE2ex
245+ - psfTE3e1
246+ - psfTE3e2
247+ - psfTE3ex
248+ - psfTE4e1
249+ - psfTE4e2
250+ - psfTE4ex
251+
203252 This quantity is computed based on the aperture correction map, the
204253 psfSigma, and the image mask to assess the robustness of the aperture
205254 corrections across a given detector:
@@ -219,6 +268,7 @@ def __init__(self, **kwargs):
219268 super ().__init__ (** kwargs )
220269
221270 self .makeSubtask ("starSelector" )
271+ self .isTEXComputationDone = False
222272
223273 @timeMethod
224274 def run (self , exposure , sources , background ):
@@ -408,10 +458,13 @@ def update_psf_stats(
408458 psfE1 = (psfXX - psfYY )/ (psfXX + psfYY )
409459 psfE2 = 2 * psfXY / (psfXX + psfYY )
410460
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' )
461+ e1Residuals = starE1 - psfE1
462+ e2Residuals = starE2 - psfE2
463+
464+ psfStarDeltaE1Median = np .median (e1Residuals )
465+ psfStarDeltaE1Scatter = sigmaMad (e1Residuals , scale = 'normal' )
466+ psfStarDeltaE2Median = np .median (e2Residuals )
467+ psfStarDeltaE2Scatter = sigmaMad (e2Residuals , scale = 'normal' )
415468
416469 psfStarDeltaSizeMedian = np .median (starSize - psfSize )
417470 psfStarDeltaSizeScatter = sigmaMad (starSize - psfSize , scale = 'normal' )
@@ -434,6 +487,178 @@ def update_psf_stats(
434487 )
435488 summary .maxDistToNearestPsf = float (maxDistToNearestPsf )
436489
490+ def comp_psf_TEX_visit_level (self , summary , sources , sources_is_astropy = False ):
491+ """Compute all summary-statistic fields at visit level for TEx metric of PSF.
492+
493+ Parameters
494+ ----------
495+ summary : `lsst.afw.image.ExposureSummaryStats`
496+ Summary object to update in-place.
497+ sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
498+ Catalog for quantities that are computed from source table columns.
499+ If `None`, these quantities will be reset (generally to NaN).
500+ The type of this table must correspond to the
501+ ``sources_is_astropy`` argument.
502+ sources_is_astropy : `bool`, optional
503+ Whether ``sources`` is an `astropy.table.Table` instance instead
504+ of an `lsst.afw.table.Catalog` instance. Default is `False` (the
505+ latter).
506+ """
507+
508+ if self .isTEXComputationDone :
509+
510+ summary .psfTE1e1 = self .psfTE1e1
511+ summary .psfTE1e2 = self .psfTE1e2
512+ summary .psfTE1ex = self .psfTE1ex
513+ summary .psfTE2e1 = self .psfTE2e1
514+ summary .psfTE2e2 = self .psfTE2e2
515+ summary .psfTE2ex = self .psfTE2ex
516+ summary .psfTE3e1 = self .psfTE3e1
517+ summary .psfTE3e2 = self .psfTE3e2
518+ summary .psfTE3ex = self .psfTE3ex
519+ summary .psfTE4e1 = self .psfTE4e1
520+ summary .psfTE4e2 = self .psfTE4e2
521+ summary .psfTE4ex = self .psfTE4ex
522+
523+ else :
524+
525+ self .isTEXComputationDone = True
526+
527+ nan = float ("nan" )
528+ summary .psfTE1e1 , self .psfTE1e1 = nan , nan
529+ summary .psfTE1e2 , self .psfTE1e2 = nan , nan
530+ summary .psfTE1ex , self .psfTE1ex = nan , nan
531+ summary .psfTE2e1 , self .psfTE2e1 = nan , nan
532+ summary .psfTE2e2 , self .psfTE2e2 = nan , nan
533+ summary .psfTE2ex , self .psfTE2ex = nan , nan
534+ summary .psfTE3e1 , self .psfTE3e1 = nan , nan
535+ summary .psfTE3e2 , self .psfTE3e2 = nan , nan
536+ summary .psfTE3ex , self .psfTE3ex = nan , nan
537+ summary .psfTE4e1 , self .psfTE4e1 = nan , nan
538+ summary .psfTE4e2 , self .psfTE4e2 = nan , nan
539+ summary .psfTE4ex , self .psfTE4ex = nan , nan
540+
541+ psf_mask = self .starSelector .run (sources ).selected
542+
543+ nPsfStarsUsedInStats = psf_mask .sum ()
544+
545+ if nPsfStarsUsedInStats == 0 :
546+ # No stars to measure statistics, so we must return the defaults
547+ # of 0 stars and NaN values.
548+ return
549+
550+ if sources_is_astropy :
551+ psf_cat = sources [psf_mask ]
552+ else :
553+ psf_cat = sources [psf_mask ].copy (deep = True )
554+
555+ starXX = psf_cat [self .config .starShape + '_xx' ]
556+ starYY = psf_cat [self .config .starShape + '_yy' ]
557+ starXY = psf_cat [self .config .starShape + '_xy' ]
558+ psfXX = psf_cat [self .config .psfShape + '_xx' ]
559+ psfYY = psf_cat [self .config .psfShape + '_yy' ]
560+ psfXY = psf_cat [self .config .psfShape + '_xy' ]
561+
562+ starE1 = (starXX - starYY )/ (starXX + starYY )
563+ starE2 = 2 * starXY / (starXX + starYY )
564+
565+ psfE1 = (psfXX - psfYY )/ (psfXX + psfYY )
566+ psfE2 = 2 * psfXY / (psfXX + psfYY )
567+
568+ e1Residuals = starE1 - psfE1
569+ e2Residuals = starE2 - psfE2
570+
571+ # Comp TEx
572+ ra = psf_cat ["coord_ra" ].to (units .deg )
573+ dec = psf_cat ["coord_dec" ].to (units .deg )
574+
575+ TExTreecorrConfig = {
576+ "TE1" : self .config .psfTE1TreecorrConfig ,
577+ "TE2" : self .config .psfTE2TreecorrConfig ,
578+ }
579+
580+ gatherE12Stat = {
581+ "TE1" : {"E1" : np .nan , "E2" : np .nan , "Ex" : np .nan , },
582+ "TE2" : {"E1" : np .nan , "E2" : np .nan , "Ex" : np .nan , },
583+ }
584+
585+ isNotNan = np .array ([True ] * len (ra ))
586+ isNotNan &= np .isfinite (ra )
587+ isNotNan &= np .isfinite (dec )
588+ isNotNan &= np .isfinite (e1Residuals )
589+ isNotNan &= np .isfinite (e2Residuals )
590+
591+ if np .sum (isNotNan ) >= 2 :
592+ # TE1 and TE2 computation, over visit.
593+ for TEX in ["TE1" , "TE2" ]:
594+
595+ task = ComputeExPsfTask (TExTreecorrConfig [TEX ])
596+ output = task .run (
597+ e1Residuals [isNotNan ], e2Residuals [isNotNan ],
598+ ra [isNotNan ], dec [isNotNan ],
599+ units = "degree" ,
600+ )
601+
602+ gatherE12Stat [TEX ]["E1" ] = output .metric_E1
603+ gatherE12Stat [TEX ]["E2" ] = output .metric_E2
604+ gatherE12Stat [TEX ]["Ex" ] = output .metric_Ex
605+
606+ # TE3 and TE4 loop over detector and then median on visit.
607+
608+ TExTreecorrConfig = {
609+ "TE3" : self .config .psfTE3TreecorrConfig ,
610+ "TE4" : self .config .psfTE4TreecorrConfig ,
611+ }
612+
613+ gatherE34Stat = {
614+ "TE3" : {"E1" : [], "E2" : [], "Ex" : [], },
615+ "TE4" : {"E1" : [], "E2" : [], "Ex" : [], },
616+ }
617+ # calibrateImage run at detector level,
618+ # need to wait second run of PSF to run this.
619+ if "detector" in psf_cat .colnames :
620+ detectorIds = list (set (psf_cat ["detector" ]))
621+ for TEX in ["TE3" , "TE4" ]:
622+ for ccdId in detectorIds :
623+ isccdId = (ccdId == psf_cat ["detector" ])
624+ mask = (isccdId & isNotNan )
625+ if np .sum (mask ) >= 2 :
626+ task = ComputeExPsfTask (TExTreecorrConfig [TEX ])
627+ output = task .run (
628+ e1Residuals [mask ], e2Residuals [mask ],
629+ ra [mask ], dec [mask ],
630+ units = "degree" ,
631+ )
632+ gatherE34Stat [TEX ]["E1" ].append (output .metric_E1 )
633+ gatherE34Stat [TEX ]["E2" ].append (output .metric_E2 )
634+ gatherE34Stat [TEX ]["Ex" ].append (output .metric_Ex )
635+
636+ summary .psfTE1e1 = gatherE12Stat ["TE1" ]["E1" ]
637+ summary .psfTE1e2 = gatherE12Stat ["TE1" ]["E2" ]
638+ summary .psfTE1ex = gatherE12Stat ["TE1" ]["Ex" ]
639+ summary .psfTE2e1 = gatherE12Stat ["TE2" ]["E1" ]
640+ summary .psfTE2e2 = gatherE12Stat ["TE2" ]["E2" ]
641+ summary .psfTE2ex = gatherE12Stat ["TE2" ]["Ex" ]
642+ summary .psfTE3e1 = np .nanmedian (gatherE34Stat ["TE3" ]["E1" ])
643+ summary .psfTE3e2 = np .nanmedian (gatherE34Stat ["TE3" ]["E2" ])
644+ summary .psfTE3ex = np .nanmedian (gatherE34Stat ["TE3" ]["Ex" ])
645+ summary .psfTE4e1 = np .nanmedian (gatherE34Stat ["TE4" ]["E1" ])
646+ summary .psfTE4e2 = np .nanmedian (gatherE34Stat ["TE4" ]["E2" ])
647+ summary .psfTE4ex = np .nanmedian (gatherE34Stat ["TE4" ]["Ex" ])
648+
649+ self .psfTE1e1 = summary .psfTE1e1
650+ self .psfTE1e2 = summary .psfTE1e2
651+ self .psfTE1ex = summary .psfTE1ex
652+ self .psfTE2e1 = summary .psfTE2e1
653+ self .psfTE2e2 = summary .psfTE2e2
654+ self .psfTE2ex = summary .psfTE2ex
655+ self .psfTE3e1 = summary .psfTE3e1
656+ self .psfTE3e2 = summary .psfTE3e2
657+ self .psfTE3ex = summary .psfTE3ex
658+ self .psfTE4e1 = summary .psfTE4e1
659+ self .psfTE4e2 = summary .psfTE4e2
660+ self .psfTE4ex = summary .psfTE4ex
661+
437662 def update_wcs_stats (self , summary , wcs , bbox , visitInfo ):
438663 """Compute all summary-statistic fields that depend on the WCS model.
439664
0 commit comments