Coverage for python/lsst/faro/measurement/MatchedCatalogMeasurementTasks.py : 36%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import astropy.units as u
2import numpy as np
3from lsst.pipe.base import Struct, Task
4from lsst.pex.config import Config, Field, ListField
5from lsst.verify import Measurement, Datum
6from lsst.faro.utils.filtermatches import filterMatches
7from lsst.faro.utils.separations import (calcRmsDistances, calcRmsDistancesVsRef,
8 astromResiduals)
9from lsst.faro.utils.phot_repeat import photRepeat
12__all__ = ("PA1TaskConfig", "PA1Task", "PF1TaskConfig", "PF1Task",
13 "AMxTaskConfig", "AMxTask", "ADxTask", "AFxTask", "AB1TaskConfig", "AB1Task")
16filter_dict = {'u': 1, 'g': 2, 'r': 3, 'i': 4, 'z': 5, 'y': 6,
17 'HSC-U': 1, 'HSC-G': 2, 'HSC-R': 3, 'HSC-I': 4, 'HSC-Z': 5, 'HSC-Y': 6}
20class PA1TaskConfig(Config):
21 """Config fields for the PA1 photometric repeatability metric.
22 """
23 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.",
24 dtype=float, default=200)
25 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.",
26 dtype=float, default=np.Inf)
27 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.",
28 dtype=int, default=50)
29 writeExtras = Field(doc="Write out the magnitude residuals and rms values for debugging.",
30 dtype=bool, default=False)
33class PA1Task(Task):
34 """A Task that computes the PA1 photometric repeatability metric from an
35 input set of multiple visits of the same field.
37 Notes
38 -----
39 The intended usage is to retarget the run method of
40 `lsst.faro.measurement.TractMatchedMeasurementTask` to PA1Task.
41 This metric is calculated on a set of matched visits, and aggregated at the tract level.
42 """
44 ConfigClass = PA1TaskConfig
45 _DefaultName = "PA1Task"
47 def __init__(self, config: PA1TaskConfig, *args, **kwargs):
48 super().__init__(*args, config=config, **kwargs)
49 self.brightSnrMin = self.config.brightSnrMin
50 self.brightSnrMax = self.config.brightSnrMax
51 self.nMinPhotRepeat = self.config.nMinPhotRepeat
52 self.writeExtras = self.config.writeExtras
54 def run(self, matchedCatalog, metricName):
55 """Calculate the photometric repeatability.
57 Parameters
58 ----------
59 matchedCatalog : `lsst.afw.table.base.Catalog`
60 `~lsst.afw.table.base.Catalog` object as created by
61 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
62 metricName : `str`
63 The name of the metric.
65 Returns
66 -------
67 measurement : `lsst.verify.Measurement`
68 Measurement of the repeatability and its associated metadata.
69 """
70 self.log.info(f"Measuring {metricName}")
72 pa1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.nMinPhotRepeat,
73 snrMax=self.brightSnrMax, snrMin=self.brightSnrMin)
75 if 'magMean' in pa1.keys():
76 if self.writeExtras:
77 extras = {}
78 extras['rms'] = Datum(pa1['rms'], label='RMS',
79 description='Photometric repeatability rms for each star.')
80 extras['count'] = Datum(pa1['count']*u.count, label='count',
81 description='Number of detections used to calculate '
82 'repeatability.')
83 extras['mean_mag'] = Datum(pa1['magMean'], label='mean_mag',
84 description='Mean magnitude of each star.')
85 return Struct(measurement=Measurement("PA1", pa1['repeatability'], extras=extras))
86 else:
87 return Struct(measurement=Measurement("PA1", pa1['repeatability']))
88 else:
89 return Struct(measurement=Measurement("PA1", np.nan*u.mmag))
92class PF1TaskConfig(Config):
93 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.",
94 dtype=float, default=200)
95 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.",
96 dtype=float, default=np.Inf)
97 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.",
98 dtype=int, default=50)
99 # The defaults for threshPA2 and threshPF1 correspond to the SRD "design" thresholds.
100 threshPA2 = Field(doc="Threshold in mmag for PF1 calculation.", dtype=float, default=15.0)
103class PF1Task(Task):
104 """A Task that computes PF1, the percentage of photometric repeatability measurements
105 that deviate by more than PA2 mmag from the mean.
107 Notes
108 -----
109 The intended usage is to retarget the run method of
110 `lsst.faro.measurement.TractMatchedMeasurementTask` to PF1Task. This Task uses the
111 same set of photometric residuals that are calculated for the PA1 metric.
112 This metric is calculated on a set of matched visits, and aggregated at the tract level.
113 """
114 ConfigClass = PF1TaskConfig
115 _DefaultName = "PF1Task"
117 def __init__(self, config: PF1TaskConfig, *args, **kwargs):
118 super().__init__(*args, config=config, **kwargs)
119 self.brightSnrMin = self.config.brightSnrMin
120 self.brightSnrMax = self.config.brightSnrMax
121 self.nMinPhotRepeat = self.config.nMinPhotRepeat
122 self.threshPA2 = self.config.threshPA2
124 def run(self, matchedCatalog, metricName):
125 """Calculate the percentage of outliers in the photometric repeatability values.
127 Parameters
128 ----------
129 matchedCatalog : `lsst.afw.table.base.Catalog`
130 `~lsst.afw.table.base.Catalog` object as created by
131 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
132 metricName : `str`
133 The name of the metric.
135 Returns
136 -------
137 measurement : `lsst.verify.Measurement`
138 Measurement of the percentage of repeatability outliers, and associated metadata.
139 """
140 self.log.info(f"Measuring {metricName}")
141 pa2_thresh = self.threshPA2 * u.mmag
143 pf1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.nMinPhotRepeat,
144 snrMax=self.brightSnrMax, snrMin=self.brightSnrMin)
146 if 'magResid' in pf1.keys():
147 # Previously, validate_drp used the first random sample from PA1 measurement
148 # Now, use all of them.
149 # Keep only stars with > 2 observations:
150 okrms = (pf1['count'] > 2)
151 magResid0 = pf1['magResid']
152 magResid = np.concatenate(magResid0[okrms])
154 percentileAtPA2 = 100 * np.mean(np.abs(magResid.value) > pa2_thresh.value) * u.percent
156 return Struct(measurement=Measurement("PF1", percentileAtPA2))
157 else:
158 return Struct(measurement=Measurement("PF1", np.nan*u.percent))
161def isSorted(a):
162 return all(a[i] <= a[i+1] for i in range(len(a)-1))
165def bins(window, n):
166 delta = window/n
167 return [i*delta for i in range(n+1)]
170class AMxTaskConfig(Config):
171 annulus_r = Field(doc="Radial distance of the annulus in arcmin (5, 20, or 200 for AM1, AM2, AM3)",
172 dtype=float, default=5.)
173 width = Field(doc="Width of annulus in arcmin",
174 dtype=float, default=2.)
175 bright_mag_cut = Field(doc="Bright limit of catalog entries to include",
176 dtype=float, default=17.0)
177 faint_mag_cut = Field(doc="Faint limit of catalog entries to include",
178 dtype=float, default=21.5)
179 # The defaults for threshADx and threshAFx correspond to the SRD "design" thresholds.
180 threshAD = Field(doc="Threshold in mas for AFx calculation.", dtype=float, default=20.0)
181 threshAF = Field(doc="Percentile of differences that can vary by more than threshAD.",
182 dtype=float, default=10.0)
183 bins = ListField(doc="Bins for histogram.",
184 dtype=float, minLength=2, maxLength=1500,
185 listCheck=isSorted, default=bins(30, 200))
188class AMxTask(Task):
189 ConfigClass = AMxTaskConfig
190 _DefaultName = "AMxTask"
192 def run(self, matchedCatalog, metric_name):
193 self.log.info(f"Measuring {metric_name}")
195 filteredCat = filterMatches(matchedCatalog)
197 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag
198 D = self.config.annulus_r * u.arcmin
199 width = self.config.width * u.arcmin
200 annulus = D + (width/2)*np.array([-1, +1])
202 rmsDistances = calcRmsDistances(
203 filteredCat,
204 annulus,
205 magRange=magRange)
207 values, bins = np.histogram(rmsDistances.to(u.marcsec), bins=self.config.bins*u.marcsec)
208 extras = {'bins': Datum(bins, label='binvalues', description='bins'),
209 'values': Datum(values*u.count, label='counts', description='icounts in bins')}
211 if len(rmsDistances) == 0:
212 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec, extras=extras))
214 return Struct(measurement=Measurement(metric_name, np.median(rmsDistances.to(u.marcsec)),
215 extras=extras))
218class ADxTask(Task):
219 ConfigClass = AMxTaskConfig
220 _DefaultName = "ADxTask"
222 def run(self, matchedCatalog, metric_name):
223 self.log.info(f"Measuring {metric_name}")
225 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut,
226 self.config.faint_mag_cut, self.config.annulus_r,
227 self.config.width)
229 afThresh = self.config.threshAF * u.percent
230 afPercentile = 100.0*u.percent - afThresh
232 if len(sepDistances) <= 1:
233 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
234 else:
235 # absolute value of the difference between each astrometric rms
236 # and the median astrometric RMS
237 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec)
238 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec)
239 return Struct(measurement=Measurement(metric_name, np.percentile(absDiffsMarcsec.value,
240 afPercentile.value)*u.marcsec))
243class AFxTask(Task):
244 ConfigClass = AMxTaskConfig
245 _DefaultName = "AFxTask"
247 def run(self, matchedCatalog, metric_name):
248 self.log.info(f"Measuring {metric_name}")
250 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut,
251 self.config.faint_mag_cut, self.config.annulus_r,
252 self.config.width)
254 adxThresh = self.config.threshAD * u.marcsec
256 if len(sepDistances) <= 1:
257 return Struct(measurement=Measurement(metric_name, np.nan*u.percent))
258 else:
259 # absolute value of the difference between each astrometric rms
260 # and the median astrometric RMS
261 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec)
262 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec)
263 percentileAtADx = 100 * np.mean(np.abs(absDiffsMarcsec.value) > adxThresh.value) * u.percent
264 return Struct(measurement=Measurement(metric_name, percentileAtADx))
267class AB1TaskConfig(Config):
268 bright_mag_cut = Field(doc="Bright limit of catalog entries to include",
269 dtype=float, default=17.0)
270 faint_mag_cut = Field(doc="Faint limit of catalog entries to include",
271 dtype=float, default=21.5)
272 ref_filter = Field(doc="String representing the filter to use as reference",
273 dtype=str, default="r")
276class AB1Task(Task):
277 ConfigClass = AB1TaskConfig
278 _DefaultName = "AB1Task"
280 def run(self, matchedCatalogMulti, metric_name, in_id, out_id):
281 self.log.info(f"Measuring {metric_name}")
283 if self.config.ref_filter not in filter_dict:
284 raise Exception('Reference filter supplied for AB1 not in dictionary.')
286 filteredCat = filterMatches(matchedCatalogMulti)
287 rmsDistancesAll = []
289 if len(filteredCat) > 0:
291 filtnum = filter_dict[self.config.ref_filter]
293 refVisits = set()
294 for id in filteredCat.ids:
295 grptmp = filteredCat[id]
296 filtmch = (grptmp['filt'] == filtnum)
297 if len(filtmch) > 0:
298 refVisits.update(set(grptmp[filtmch]['visit']))
300 refVisits = list(refVisits)
302 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag
303 for rv in refVisits:
304 rmsDistances = calcRmsDistancesVsRef(
305 filteredCat,
306 rv,
307 magRange=magRange,
308 band=filter_dict[out_id['band']])
309 finiteEntries = np.where(np.isfinite(rmsDistances))[0]
310 if len(finiteEntries) > 0:
311 rmsDistancesAll.append(rmsDistances[finiteEntries])
313 if len(rmsDistancesAll) == 0:
314 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
315 else:
316 rmsDistancesAll = np.concatenate(rmsDistancesAll)
317 return Struct(measurement=Measurement(metric_name, np.mean(rmsDistancesAll)))
319 else:
320 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))