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

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, ThresholdSpecification, 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
10from lsst.faro.utils.tex import (correlation_function_ellipticity_from_matches,
11 select_bin_from_corr)
13__all__ = ("PA1TaskConfig", "PA1Task", "PF1TaskConfig", "PF1Task", "TExTaskConfig",
14 "TExTask", "AMxTaskConfig", "AMxTask", "ADxTask", "AFxTask", "AB1TaskConfig", "AB1Task")
17filter_dict = {'u': 1, 'g': 2, 'r': 3, 'i': 4, 'z': 5, 'y': 6,
18 'HSC-U': 1, 'HSC-G': 2, 'HSC-R': 3, 'HSC-I': 4, 'HSC-Z': 5, 'HSC-Y': 6}
21class PA1TaskConfig(Config):
22 """Config fields for the PA1 photometric repeatability metric.
23 """
24 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.",
25 dtype=float, default=200)
26 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.",
27 dtype=float, default=np.Inf)
28 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.",
29 dtype=int, default=50)
30 writeExtras = Field(doc="Write out the magnitude residuals and rms values for debugging.",
31 dtype=bool, default=False)
34class PA1Task(Task):
35 """A Task that computes the PA1 photometric repeatability metric from an
36 input set of multiple visits of the same field.
38 Notes
39 -----
40 The intended usage is to retarget the run method of
41 `lsst.faro.measurement.TractMatchedMeasurementTask` to PA1Task.
42 This metric is calculated on a set of matched visits, and aggregated at the tract level.
43 """
45 ConfigClass = PA1TaskConfig
46 _DefaultName = "PA1Task"
48 def __init__(self, config: PA1TaskConfig, *args, **kwargs):
49 super().__init__(*args, config=config, **kwargs)
50 self.brightSnrMin = self.config.brightSnrMin
51 self.brightSnrMax = self.config.brightSnrMax
52 self.nMinPhotRepeat = self.config.nMinPhotRepeat
53 self.writeExtras = self.config.writeExtras
55 def run(self, matchedCatalog, metricName):
56 """Calculate the photometric repeatability.
58 Parameters
59 ----------
60 matchedCatalog : `lsst.afw.table.base.Catalog`
61 `~lsst.afw.table.base.Catalog` object as created by
62 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
63 metricName : `str`
64 The name of the metric.
66 Returns
67 -------
68 measurement : `lsst.verify.Measurement`
69 Measurement of the repeatability and its associated metadata.
70 """
71 self.log.info(f"Measuring {metricName}")
73 pa1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.nMinPhotRepeat,
74 snrMax=self.brightSnrMax, snrMin=self.brightSnrMin)
76 if 'magMean' in pa1.keys():
77 if self.writeExtras:
78 extras = {}
79 extras['rms'] = Datum(pa1['rms'], label='RMS',
80 description='Photometric repeatability rms for each star.')
81 extras['count'] = Datum(pa1['count']*u.count, label='count',
82 description='Number of detections used to calculate '
83 'repeatability.')
84 extras['mean_mag'] = Datum(pa1['magMean'], label='mean_mag',
85 description='Mean magnitude of each star.')
86 return Struct(measurement=Measurement("PA1", pa1['repeatability'], extras=extras))
87 else:
88 return Struct(measurement=Measurement("PA1", pa1['repeatability']))
89 else:
90 return Struct(measurement=Measurement("PA1", np.nan*u.mmag))
93class PF1TaskConfig(Config):
94 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.",
95 dtype=float, default=200)
96 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.",
97 dtype=float, default=np.Inf)
98 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.",
99 dtype=int, default=50)
100 # The defaults for threshPA2 and threshPF1 correspond to the SRD "design" thresholds.
101 threshPA2 = Field(doc="Threshold in mmag for PF1 calculation.", dtype=float, default=15.0)
104class PF1Task(Task):
105 """A Task that computes PF1, the percentage of photometric repeatability measurements
106 that deviate by more than PA2 mmag from the mean.
108 Notes
109 -----
110 The intended usage is to retarget the run method of
111 `lsst.faro.measurement.TractMatchedMeasurementTask` to PF1Task. This Task uses the
112 same set of photometric residuals that are calculated for the PA1 metric.
113 This metric is calculated on a set of matched visits, and aggregated at the tract level.
114 """
115 ConfigClass = PF1TaskConfig
116 _DefaultName = "PF1Task"
118 def __init__(self, config: PF1TaskConfig, *args, **kwargs):
119 super().__init__(*args, config=config, **kwargs)
120 self.brightSnrMin = self.config.brightSnrMin
121 self.brightSnrMax = self.config.brightSnrMax
122 self.nMinPhotRepeat = self.config.nMinPhotRepeat
123 self.threshPA2 = self.config.threshPA2
125 def run(self, matchedCatalog, metricName):
126 """Calculate the percentage of outliers in the photometric repeatability values.
128 Parameters
129 ----------
130 matchedCatalog : `lsst.afw.table.base.Catalog`
131 `~lsst.afw.table.base.Catalog` object as created by
132 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
133 metricName : `str`
134 The name of the metric.
136 Returns
137 -------
138 measurement : `lsst.verify.Measurement`
139 Measurement of the percentage of repeatability outliers, and associated metadata.
140 """
141 self.log.info(f"Measuring {metricName}")
142 pa2_thresh = self.threshPA2 * u.mmag
144 pf1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.nMinPhotRepeat,
145 snrMax=self.brightSnrMax, snrMin=self.brightSnrMin)
147 if 'magResid' in pf1.keys():
148 # Previously, validate_drp used the first random sample from PA1 measurement
149 # Now, use all of them.
150 # Keep only stars with > 2 observations:
151 okrms = (pf1['count'] > 2)
152 magResid0 = pf1['magResid']
153 magResid = np.concatenate(magResid0[okrms])
155 percentileAtPA2 = 100 * np.mean(np.abs(magResid.value) > pa2_thresh.value) * u.percent
157 return Struct(measurement=Measurement("PF1", percentileAtPA2))
158 else:
159 return Struct(measurement=Measurement("PF1", np.nan*u.percent))
162class TExTaskConfig(Config):
163 annulus_r = Field(doc="Radial size of the annulus in arcmin",
164 dtype=float, default=1.)
165 comparison_operator = Field(doc="String representation of the operator to use in comparisons",
166 dtype=str, default="<=")
169class TExTask(Task):
170 ConfigClass = TExTaskConfig
171 _DefaultName = "TExTask"
173 def run(self, matchedCatalog, metric_name):
174 self.log.info(f"Measuring {metric_name}")
176 D = self.config.annulus_r * u.arcmin
177 filteredCat = filterMatches(matchedCatalog)
178 nMinTEx = 50
179 if filteredCat.count <= nMinTEx:
180 return Struct(measurement=Measurement(metric_name, np.nan*u.Unit('')))
182 radius, xip, xip_err = correlation_function_ellipticity_from_matches(filteredCat)
183 operator = ThresholdSpecification.convert_operator_str(self.config.comparison_operator)
184 corr, corr_err = select_bin_from_corr(radius, xip, xip_err, radius=D, operator=operator)
185 return Struct(measurement=Measurement(metric_name, np.abs(corr)*u.Unit('')))
188def isSorted(a):
189 return all(a[i] <= a[i+1] for i in range(len(a)-1))
192def bins(window, n):
193 delta = window/n
194 return [i*delta for i in range(n+1)]
197class AMxTaskConfig(Config):
198 annulus_r = Field(doc="Radial distance of the annulus in arcmin (5, 20, or 200 for AM1, AM2, AM3)",
199 dtype=float, default=5.)
200 width = Field(doc="Width of annulus in arcmin",
201 dtype=float, default=2.)
202 bright_mag_cut = Field(doc="Bright limit of catalog entries to include",
203 dtype=float, default=17.0)
204 faint_mag_cut = Field(doc="Faint limit of catalog entries to include",
205 dtype=float, default=21.5)
206 # The defaults for threshADx and threshAFx correspond to the SRD "design" thresholds.
207 threshAD = Field(doc="Threshold in mas for AFx calculation.", dtype=float, default=20.0)
208 threshAF = Field(doc="Percentile of differences that can vary by more than threshAD.",
209 dtype=float, default=10.0)
210 bins = ListField(doc="Bins for histogram.",
211 dtype=float, minLength=2, maxLength=1500,
212 listCheck=isSorted, default=bins(30, 200))
215class AMxTask(Task):
216 ConfigClass = AMxTaskConfig
217 _DefaultName = "AMxTask"
219 def run(self, matchedCatalog, metric_name):
220 self.log.info(f"Measuring {metric_name}")
222 filteredCat = filterMatches(matchedCatalog)
224 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag
225 D = self.config.annulus_r * u.arcmin
226 width = self.config.width * u.arcmin
227 annulus = D + (width/2)*np.array([-1, +1])
229 rmsDistances = calcRmsDistances(
230 filteredCat,
231 annulus,
232 magRange=magRange)
234 values, bins = np.histogram(rmsDistances.to(u.marcsec), bins=self.config.bins*u.marcsec)
235 extras = {'bins': Datum(bins, label='binvalues', description='bins'),
236 'values': Datum(values*u.count, label='counts', description='icounts in bins')}
238 if len(rmsDistances) == 0:
239 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec, extras=extras))
241 return Struct(measurement=Measurement(metric_name, np.median(rmsDistances.to(u.marcsec)),
242 extras=extras))
245class ADxTask(Task):
246 ConfigClass = AMxTaskConfig
247 _DefaultName = "ADxTask"
249 def run(self, matchedCatalog, metric_name):
250 self.log.info(f"Measuring {metric_name}")
252 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut,
253 self.config.faint_mag_cut, self.config.annulus_r,
254 self.config.width)
256 afThresh = self.config.threshAF * u.percent
257 afPercentile = 100.0*u.percent - afThresh
259 if len(sepDistances) <= 1:
260 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
261 else:
262 # absolute value of the difference between each astrometric rms
263 # and the median astrometric RMS
264 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec)
265 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec)
266 return Struct(measurement=Measurement(metric_name, np.percentile(absDiffsMarcsec.value,
267 afPercentile.value)*u.marcsec))
270class AFxTask(Task):
271 ConfigClass = AMxTaskConfig
272 _DefaultName = "AFxTask"
274 def run(self, matchedCatalog, metric_name):
275 self.log.info(f"Measuring {metric_name}")
277 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut,
278 self.config.faint_mag_cut, self.config.annulus_r,
279 self.config.width)
281 adxThresh = self.config.threshAD * u.marcsec
283 if len(sepDistances) <= 1:
284 return Struct(measurement=Measurement(metric_name, np.nan*u.percent))
285 else:
286 # absolute value of the difference between each astrometric rms
287 # and the median astrometric RMS
288 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec)
289 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec)
290 percentileAtADx = 100 * np.mean(np.abs(absDiffsMarcsec.value) > adxThresh.value) * u.percent
291 return Struct(measurement=Measurement(metric_name, percentileAtADx))
294class AB1TaskConfig(Config):
295 bright_mag_cut = Field(doc="Bright limit of catalog entries to include",
296 dtype=float, default=17.0)
297 faint_mag_cut = Field(doc="Faint limit of catalog entries to include",
298 dtype=float, default=21.5)
299 ref_filter = Field(doc="String representing the filter to use as reference",
300 dtype=str, default="r")
303class AB1Task(Task):
304 ConfigClass = AB1TaskConfig
305 _DefaultName = "AB1Task"
307 def run(self, matchedCatalogMulti, metric_name, in_id, out_id):
308 self.log.info(f"Measuring {metric_name}")
310 if self.config.ref_filter not in filter_dict:
311 raise Exception('Reference filter supplied for AB1 not in dictionary.')
313 filteredCat = filterMatches(matchedCatalogMulti)
314 rmsDistancesAll = []
316 if len(filteredCat) > 0:
318 filtnum = filter_dict[self.config.ref_filter]
320 refVisits = set()
321 for id in filteredCat.ids:
322 grptmp = filteredCat[id]
323 filtmch = (grptmp['filt'] == filtnum)
324 if len(filtmch) > 0:
325 refVisits.update(set(grptmp[filtmch]['visit']))
327 refVisits = list(refVisits)
329 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag
330 for rv in refVisits:
331 rmsDistances = calcRmsDistancesVsRef(
332 filteredCat,
333 rv,
334 magRange=magRange,
335 band=filter_dict[out_id['band']])
336 finiteEntries = np.where(np.isfinite(rmsDistances))[0]
337 if len(finiteEntries) > 0:
338 rmsDistancesAll.append(rmsDistances[finiteEntries])
340 if len(rmsDistancesAll) == 0:
341 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
342 else:
343 rmsDistancesAll = np.concatenate(rmsDistancesAll)
344 return Struct(measurement=Measurement(metric_name, np.mean(rmsDistancesAll)))
346 else:
347 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))