Skip to content

Commit fb31b2e

Browse files
committed
Compute TEX metric.
1 parent 511e8cf commit fb31b2e

File tree

2 files changed

+230
-4
lines changed

2 files changed

+230
-4
lines changed

python/lsst/pipe/tasks/computeExposureSummaryStats.py

Lines changed: 218 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import lsst.afw.image as afwImage
3636
import lsst.geom as geom
3737
from lsst.meas.algorithms import ScienceSourceSelectorTask
38+
from lsst.meas.algorithms.computeExPsf import ComputeExPsfTask
3839
from lsst.utils.timer import timeMethod
3940
import 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+
psfTE1 = pexConfig.ConfigurableField(
142+
target=ComputeExPsfTask,
143+
doc="Use treecorr for computing scalar value of TE1.",
144+
)
145+
psfTE2 = pexConfig.ConfigurableField(
146+
target=ComputeExPsfTask,
147+
doc="Use treecorr for computing scalar value of TE2.",
148+
)
149+
psfTE3 = pexConfig.ConfigurableField(
150+
target=ComputeExPsfTask,
151+
doc="Use treecorr for computing scalar value of TE3.",
152+
)
153+
psfTE4 = pexConfig.ConfigurableField(
154+
target=ComputeExPsfTask,
155+
doc="Use treecorr for computing scalar value of TE4.",
156+
)
140157

141158
def setDefaults(self):
142159
super().setDefaults()
@@ -159,6 +176,23 @@ 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, 100.0, 5.0, 20.0]
181+
psfTEx = [
182+
self.psfTE1,
183+
self.psfTE2,
184+
self.psfTE3,
185+
self.psfTE4,
186+
]
187+
188+
for tex, mint, maxt in zip(psfTEx, min_theta, max_theta):
189+
tex.setDefaults()
190+
tex.treecorr.min_sep = mint / 60.0
191+
tex.treecorr.max_sep = maxt / 60.0
192+
tex.treecorr.nbins = 1
193+
tex.treecorr.bin_type = "Linear"
194+
tex.treecorr.sep_units = "degree"
195+
162196

163197
class ComputeExposureSummaryStatsTask(pipeBase.Task):
164198
"""Task to compute exposure summary statistics.
@@ -200,6 +234,22 @@ class ComputeExposureSummaryStatsTask(pipeBase.Task):
200234
- psfTraceRadiusDelta
201235
- psfApFluxDelta
202236
237+
These quantities are computed as part of:
238+
https://rubinobs.atlassian.net/browse/DM-40780
239+
240+
- psfTE1e1
241+
- psfTE1e2
242+
- psfTE1ex
243+
- psfTE2e1
244+
- psfTE2e2
245+
- psfTE2ex
246+
- psfTE3e1
247+
- psfTE3e2
248+
- psfTE3ex
249+
- psfTE4e1
250+
- psfTE4e2
251+
- psfTE4ex
252+
203253
This quantity is computed based on the aperture correction map, the
204254
psfSigma, and the image mask to assess the robustness of the aperture
205255
corrections across a given detector:
@@ -219,6 +269,11 @@ def __init__(self, **kwargs):
219269
super().__init__(**kwargs)
220270

221271
self.makeSubtask("starSelector")
272+
self.makeSubtask("psfTE1")
273+
self.makeSubtask("psfTE2")
274+
self.makeSubtask("psfTE3")
275+
self.makeSubtask("psfTE4")
276+
self._isTEXComputationDone = False
222277

223278
@timeMethod
224279
def run(self, exposure, sources, background):
@@ -408,10 +463,13 @@ def update_psf_stats(
408463
psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
409464
psfE2 = 2*psfXY/(psfXX + psfYY)
410465

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')
466+
e1Residuals = starE1 - psfE1
467+
e2Residuals = starE2 - psfE2
468+
469+
psfStarDeltaE1Median = np.median(e1Residuals)
470+
psfStarDeltaE1Scatter = sigmaMad(e1Residuals, scale='normal')
471+
psfStarDeltaE2Median = np.median(e2Residuals)
472+
psfStarDeltaE2Scatter = sigmaMad(e2Residuals, scale='normal')
415473

416474
psfStarDeltaSizeMedian = np.median(starSize - psfSize)
417475
psfStarDeltaSizeScatter = sigmaMad(starSize - psfSize, scale='normal')
@@ -434,6 +492,162 @@ def update_psf_stats(
434492
)
435493
summary.maxDistToNearestPsf = float(maxDistToNearestPsf)
436494

495+
def comp_psf_TEX_visit_level(self, summary, sources, sources_is_astropy=False):
496+
"""Compute all summary-statistic fields at visit level for TEx metric of PSF.
497+
498+
Parameters
499+
----------
500+
summary : `lsst.afw.image.ExposureSummaryStats`
501+
Summary object to update in-place.
502+
sources : `lsst.afw.table.SourceCatalog` or `astropy.table.Table`
503+
Catalog for quantities that are computed from source table columns.
504+
If `None`, these quantities will be reset (generally to NaN).
505+
The type of this table must correspond to the
506+
``sources_is_astropy`` argument.
507+
sources_is_astropy : `bool`, optional
508+
Whether ``sources`` is an `astropy.table.Table` instance instead
509+
of an `lsst.afw.table.Catalog` instance.
510+
"""
511+
512+
if self._isTEXComputationDone:
513+
514+
summary.psfTE1e1 = self.psfTE1e1
515+
summary.psfTE1e2 = self.psfTE1e2
516+
summary.psfTE1ex = self.psfTE1ex
517+
summary.psfTE2e1 = self.psfTE2e1
518+
summary.psfTE2e2 = self.psfTE2e2
519+
summary.psfTE2ex = self.psfTE2ex
520+
summary.psfTE3e1 = self.psfTE3e1
521+
summary.psfTE3e2 = self.psfTE3e2
522+
summary.psfTE3ex = self.psfTE3ex
523+
summary.psfTE4e1 = self.psfTE4e1
524+
summary.psfTE4e2 = self.psfTE4e2
525+
summary.psfTE4ex = self.psfTE4ex
526+
527+
else:
528+
529+
self._isTEXComputationDone = True
530+
531+
nan = float("nan")
532+
summary.psfTE1e1, self.psfTE1e1 = nan, nan
533+
summary.psfTE1e2, self.psfTE1e2 = nan, nan
534+
summary.psfTE1ex, self.psfTE1ex = nan, nan
535+
summary.psfTE2e1, self.psfTE2e1 = nan, nan
536+
summary.psfTE2e2, self.psfTE2e2 = nan, nan
537+
summary.psfTE2ex, self.psfTE2ex = nan, nan
538+
summary.psfTE3e1, self.psfTE3e1 = nan, nan
539+
summary.psfTE3e2, self.psfTE3e2 = nan, nan
540+
summary.psfTE3ex, self.psfTE3ex = nan, nan
541+
summary.psfTE4e1, self.psfTE4e1 = nan, nan
542+
summary.psfTE4e2, self.psfTE4e2 = nan, nan
543+
summary.psfTE4ex, self.psfTE4ex = nan, nan
544+
545+
psf_mask = self.starSelector.run(sources).selected
546+
547+
nPsfStarsUsedInStats = psf_mask.sum()
548+
549+
if nPsfStarsUsedInStats == 0:
550+
# No stars to measure statistics, so we must return the defaults
551+
# of 0 stars and NaN values.
552+
return
553+
554+
if sources_is_astropy:
555+
psf_cat = sources[psf_mask]
556+
else:
557+
psf_cat = sources[psf_mask].copy(deep=True)
558+
559+
starXX = psf_cat[self.config.starShape + '_xx']
560+
starYY = psf_cat[self.config.starShape + '_yy']
561+
starXY = psf_cat[self.config.starShape + '_xy']
562+
psfXX = psf_cat[self.config.psfShape + '_xx']
563+
psfYY = psf_cat[self.config.psfShape + '_yy']
564+
psfXY = psf_cat[self.config.psfShape + '_xy']
565+
566+
starE1 = (starXX - starYY)/(starXX + starYY)
567+
starE2 = 2*starXY/(starXX + starYY)
568+
569+
psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
570+
psfE2 = 2*psfXY/(psfXX + psfYY)
571+
572+
e1Residuals = starE1 - psfE1
573+
e2Residuals = starE2 - psfE2
574+
575+
# Comp TEx
576+
ra = psf_cat["coord_ra"].to(units.deg)
577+
dec = psf_cat["coord_dec"].to(units.deg)
578+
579+
psfTEx = {
580+
"TE1": self.psfTE1,
581+
"TE2": self.psfTE2,
582+
}
583+
584+
gatherE12Stat = {
585+
"TE1": {"E1": np.nan, "E2": np.nan, "Ex": np.nan, },
586+
"TE2": {"E1": np.nan, "E2": np.nan, "Ex": np.nan, },
587+
}
588+
589+
isNotNan = np.array([True] * len(ra))
590+
isNotNan &= np.isfinite(ra)
591+
isNotNan &= np.isfinite(dec)
592+
isNotNan &= np.isfinite(e1Residuals)
593+
isNotNan &= np.isfinite(e2Residuals)
594+
595+
if np.sum(isNotNan) >= 2:
596+
# TE1 and TE2 computation, over visit.
597+
for TEX in ["TE1", "TE2"]:
598+
599+
output = psfTEx[TEX].run(
600+
e1Residuals[isNotNan], e2Residuals[isNotNan],
601+
ra[isNotNan], dec[isNotNan],
602+
units="degree",
603+
)
604+
605+
gatherE12Stat[TEX]["E1"] = output.metric_E1
606+
gatherE12Stat[TEX]["E2"] = output.metric_E2
607+
gatherE12Stat[TEX]["Ex"] = output.metric_Ex
608+
609+
# TE3 and TE4 loop over detector and then median on visit.
610+
611+
psfTEx = {
612+
"TE3": self.psfTE3,
613+
"TE4": self.psfTE4,
614+
}
615+
616+
gatherE34Stat = {
617+
"TE3": {"E1": [], "E2": [], "Ex": [], },
618+
"TE4": {"E1": [], "E2": [], "Ex": [], },
619+
}
620+
# calibrateImage run at detector level,
621+
# need to wait second run of PSF to run this.
622+
if "detector" in psf_cat.colnames:
623+
detectorIds = list(set(psf_cat["detector"]))
624+
for TEX in ["TE3", "TE4"]:
625+
for ccdId in detectorIds:
626+
isccdId = (ccdId == psf_cat["detector"])
627+
mask = (isccdId & isNotNan)
628+
if np.sum(mask) >= 2:
629+
output = psfTEx[TEX].run(
630+
e1Residuals[mask], e2Residuals[mask],
631+
ra[mask], dec[mask],
632+
units="degree",
633+
)
634+
gatherE34Stat[TEX]["E1"].append(output.metric_E1)
635+
gatherE34Stat[TEX]["E2"].append(output.metric_E2)
636+
gatherE34Stat[TEX]["Ex"].append(output.metric_Ex)
637+
638+
self.psfTE1e1 = summary.psfTE1e1 = gatherE12Stat["TE1"]["E1"]
639+
self.psfTE1e2 = summary.psfTE1e2 = gatherE12Stat["TE1"]["E2"]
640+
self.psfTE1ex = summary.psfTE1ex = gatherE12Stat["TE1"]["Ex"]
641+
self.psfTE2e1 = summary.psfTE2e1 = gatherE12Stat["TE2"]["E1"]
642+
self.psfTE2e2 = summary.psfTE2e2 = gatherE12Stat["TE2"]["E2"]
643+
self.psfTE2ex = summary.psfTE2ex = gatherE12Stat["TE2"]["Ex"]
644+
self.psfTE3e1 = summary.psfTE3e1 = np.nanmedian(gatherE34Stat["TE3"]["E1"])
645+
self.psfTE3e2 = summary.psfTE3e2 = np.nanmedian(gatherE34Stat["TE3"]["E2"])
646+
self.psfTE3ex = summary.psfTE3ex = np.nanmedian(gatherE34Stat["TE3"]["Ex"])
647+
self.psfTE4e1 = summary.psfTE4e1 = np.nanmedian(gatherE34Stat["TE4"]["E1"])
648+
self.psfTE4e2 = summary.psfTE4e2 = np.nanmedian(gatherE34Stat["TE4"]["E2"])
649+
self.psfTE4ex = summary.psfTE4ex = np.nanmedian(gatherE34Stat["TE4"]["Ex"])
650+
437651
def update_wcs_stats(self, summary, wcs, bbox, visitInfo):
438652
"""Compute all summary-statistic fields that depend on the WCS model.
439653

python/lsst/pipe/tasks/postprocess.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,18 @@ def run(self, visitSummaries):
15681568
visitEntry["obsStart"] = visitEntry["expMidpt"] - 0.5 * np.timedelta64(int(expTime * 1E9), "ns")
15691569
expTime_days = expTime / (60*60*24)
15701570
visitEntry["obsStartMJD"] = visitEntry["expMidptMJD"] - 0.5 * expTime_days
1571+
visitEntry["psfTE1e1"] = visitRow["psfTE1e1"]
1572+
visitEntry["psfTE1e2"] = visitRow["psfTE1e2"]
1573+
visitEntry["psfTE1ex"] = visitRow["psfTE1ex"]
1574+
visitEntry["psfTE2e1"] = visitRow["psfTE2e1"]
1575+
visitEntry["psfTE2e2"] = visitRow["psfTE2e2"]
1576+
visitEntry["psfTE2ex"] = visitRow["psfTE2ex"]
1577+
visitEntry["psfTE3e1"] = visitRow["psfTE3e1"]
1578+
visitEntry["psfTE3e2"] = visitRow["psfTE3e2"]
1579+
visitEntry["psfTE3ex"] = visitRow["psfTE3ex"]
1580+
visitEntry["psfTE4e1"] = visitRow["psfTE4e1"]
1581+
visitEntry["psfTE4e2"] = visitRow["psfTE4e2"]
1582+
visitEntry["psfTE4ex"] = visitRow["psfTE4ex"]
15711583
visitEntries.append(visitEntry)
15721584

15731585
# TODO: DM-30623, Add programId, exposureType, cameraTemp,

0 commit comments

Comments
 (0)