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

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 ChoiceField, 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", "ModelPhotRepTask")
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)
50 def run(self, matchedCatalog, metricName):
51 """Calculate the photometric repeatability.
53 Parameters
54 ----------
55 matchedCatalog : `lsst.afw.table.base.Catalog`
56 `~lsst.afw.table.base.Catalog` object as created by
57 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
58 metricName : `str`
59 The name of the metric.
61 Returns
62 -------
63 measurement : `lsst.verify.Measurement`
64 Measurement of the repeatability and its associated metadata.
65 """
66 self.log.info("Measuring %s", metricName)
68 pa1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.config.nMinPhotRepeat,
69 snrMax=self.config.brightSnrMax, snrMin=self.config.brightSnrMin)
71 if 'magMean' in pa1.keys():
72 if self.config.writeExtras:
73 extras = {
74 'rms': Datum(pa1['rms'], label='RMS',
75 description='Photometric repeatability rms for each star.'),
76 'count': Datum(pa1['count']*u.count, label='count',
77 description='Number of detections used to calculate repeatability.'),
78 'mean_mag': Datum(pa1['magMean'], label='mean_mag',
79 description='Mean magnitude of each star.'),
80 }
81 return Struct(measurement=Measurement("PA1", pa1['repeatability'], extras=extras))
82 else:
83 return Struct(measurement=Measurement("PA1", pa1['repeatability']))
84 else:
85 return Struct(measurement=Measurement("PA1", np.nan*u.mmag))
88class PF1TaskConfig(Config):
89 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.",
90 dtype=float, default=200)
91 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.",
92 dtype=float, default=np.Inf)
93 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.",
94 dtype=int, default=50)
95 # The defaults for threshPA2 correspond to the SRD "design" thresholds.
96 threshPA2 = Field(doc="Threshold in mmag for PF1 calculation.", dtype=float, default=15.0)
99class PF1Task(Task):
100 """A Task that computes PF1, the percentage of photometric repeatability measurements
101 that deviate by more than PA2 mmag from the mean.
103 Notes
104 -----
105 The intended usage is to retarget the run method of
106 `lsst.faro.measurement.TractMatchedMeasurementTask` to PF1Task. This Task uses the
107 same set of photometric residuals that are calculated for the PA1 metric.
108 This metric is calculated on a set of matched visits, and aggregated at the tract level.
109 """
110 ConfigClass = PF1TaskConfig
111 _DefaultName = "PF1Task"
113 def __init__(self, config: PF1TaskConfig, *args, **kwargs):
114 super().__init__(*args, config=config, **kwargs)
116 def run(self, matchedCatalog, metricName):
117 """Calculate the percentage of outliers in the photometric repeatability values.
119 Parameters
120 ----------
121 matchedCatalog : `lsst.afw.table.base.Catalog`
122 `~lsst.afw.table.base.Catalog` object as created by
123 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
124 metricName : `str`
125 The name of the metric.
127 Returns
128 -------
129 measurement : `lsst.verify.Measurement`
130 Measurement of the percentage of repeatability outliers, and associated metadata.
131 """
132 self.log.info("Measuring %s", metricName)
133 pa2_thresh = self.config.threshPA2 * u.mmag
135 pf1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.config.nMinPhotRepeat,
136 snrMax=self.config.brightSnrMax, snrMin=self.config.brightSnrMin)
138 if 'magResid' in pf1.keys():
139 # Previously, validate_drp used the first random sample from PA1 measurement
140 # Now, use all of them.
141 # Keep only stars with > 2 observations:
142 okrms = (pf1['count'] > 2)
143 magResid0 = pf1['magResid']
144 magResid = np.concatenate(magResid0[okrms])
146 percentileAtPA2 = 100 * np.mean(np.abs(magResid.value) > pa2_thresh.value) * u.percent
148 return Struct(measurement=Measurement("PF1", percentileAtPA2))
149 else:
150 return Struct(measurement=Measurement("PF1", np.nan*u.percent))
153def isSorted(a):
154 return all(a[i] <= a[i+1] for i in range(len(a)-1))
157def bins(window, n):
158 delta = window/n
159 return [i*delta for i in range(n+1)]
162class AMxTaskConfig(Config):
163 annulus_r = Field(doc="Radial distance of the annulus in arcmin (5, 20, or 200 for AM1, AM2, AM3)",
164 dtype=float, default=5.)
165 width = Field(doc="Width of annulus in arcmin",
166 dtype=float, default=2.)
167 bright_mag_cut = Field(doc="Bright limit of catalog entries to include",
168 dtype=float, default=17.0)
169 faint_mag_cut = Field(doc="Faint limit of catalog entries to include",
170 dtype=float, default=21.5)
171 # The defaults for threshADx and threshAFx correspond to the SRD "design" thresholds.
172 threshAD = Field(doc="Threshold in mas for AFx calculation.", dtype=float, default=20.0)
173 threshAF = Field(doc="Percentile of differences that can vary by more than threshAD.",
174 dtype=float, default=10.0)
175 bins = ListField(doc="Bins for histogram.",
176 dtype=float, minLength=2, maxLength=1500,
177 listCheck=isSorted, default=bins(30, 200))
180class AMxTask(Task):
181 ConfigClass = AMxTaskConfig
182 _DefaultName = "AMxTask"
184 def run(self, matchedCatalog, metric_name):
185 self.log.info("Measuring %s", metric_name)
187 filteredCat = filterMatches(matchedCatalog)
189 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag
190 D = self.config.annulus_r * u.arcmin
191 width = self.config.width * u.arcmin
192 annulus = D + (width/2)*np.array([-1, +1])
194 rmsDistances = calcRmsDistances(
195 filteredCat,
196 annulus,
197 magRange=magRange)
199 values, bins = np.histogram(rmsDistances.to(u.marcsec), bins=self.config.bins*u.marcsec)
200 extras = {'bins': Datum(bins, label='binvalues', description='bins'),
201 'values': Datum(values*u.count, label='counts', description='icounts in bins')}
203 if len(rmsDistances) == 0:
204 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec, extras=extras))
206 return Struct(measurement=Measurement(metric_name, np.median(rmsDistances.to(u.marcsec)),
207 extras=extras))
210class ADxTask(Task):
211 ConfigClass = AMxTaskConfig
212 _DefaultName = "ADxTask"
214 def run(self, matchedCatalog, metric_name):
215 self.log.info("Measuring %s", metric_name)
217 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut,
218 self.config.faint_mag_cut, self.config.annulus_r,
219 self.config.width)
221 afThresh = self.config.threshAF * u.percent
222 afPercentile = 100.0*u.percent - afThresh
224 if len(sepDistances) <= 1:
225 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
226 else:
227 # absolute value of the difference between each astrometric rms
228 # and the median astrometric RMS
229 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec)
230 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec)
231 return Struct(measurement=Measurement(metric_name, np.percentile(absDiffsMarcsec.value,
232 afPercentile.value)*u.marcsec))
235class AFxTask(Task):
236 ConfigClass = AMxTaskConfig
237 _DefaultName = "AFxTask"
239 def run(self, matchedCatalog, metric_name):
240 self.log.info("Measuring %s", metric_name)
242 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut,
243 self.config.faint_mag_cut, self.config.annulus_r,
244 self.config.width)
246 adxThresh = self.config.threshAD * u.marcsec
248 if len(sepDistances) <= 1:
249 return Struct(measurement=Measurement(metric_name, np.nan*u.percent))
250 else:
251 # absolute value of the difference between each astrometric rms
252 # and the median astrometric RMS
253 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec)
254 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec)
255 percentileAtADx = 100 * np.mean(np.abs(absDiffsMarcsec.value) > adxThresh.value) * u.percent
256 return Struct(measurement=Measurement(metric_name, percentileAtADx))
259class AB1TaskConfig(Config):
260 bright_mag_cut = Field(doc="Bright limit of catalog entries to include",
261 dtype=float, default=17.0)
262 faint_mag_cut = Field(doc="Faint limit of catalog entries to include",
263 dtype=float, default=21.5)
264 ref_filter = Field(doc="String representing the filter to use as reference",
265 dtype=str, default="r")
268class AB1Task(Task):
269 ConfigClass = AB1TaskConfig
270 _DefaultName = "AB1Task"
272 def run(self, matchedCatalogMulti, metric_name, in_id, out_id):
273 self.log.info("Measuring %s", metric_name)
275 if self.config.ref_filter not in filter_dict:
276 raise Exception('Reference filter supplied for AB1 not in dictionary.')
278 filteredCat = filterMatches(matchedCatalogMulti)
279 rmsDistancesAll = []
281 if len(filteredCat) > 0:
283 filtnum = filter_dict[self.config.ref_filter]
285 refVisits = set()
286 for id in filteredCat.ids:
287 grptmp = filteredCat[id]
288 filtmch = (grptmp['filt'] == filtnum)
289 if len(filtmch) > 0:
290 refVisits.update(set(grptmp[filtmch]['visit']))
292 refVisits = list(refVisits)
294 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag
295 for rv in refVisits:
296 rmsDistances = calcRmsDistancesVsRef(
297 filteredCat,
298 rv,
299 magRange=magRange,
300 band=filter_dict[out_id['band']])
301 finiteEntries = np.where(np.isfinite(rmsDistances))[0]
302 if len(finiteEntries) > 0:
303 rmsDistancesAll.append(rmsDistances[finiteEntries])
305 if len(rmsDistancesAll) == 0:
306 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
307 else:
308 rmsDistancesAll = np.concatenate(rmsDistancesAll)
309 return Struct(measurement=Measurement(metric_name, np.mean(rmsDistancesAll)))
311 else:
312 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))
315class ModelPhotRepTaskConfig(Config):
316 """Config fields for the *ModelPhotRep photometric repeatability metrics.
317 """
318 index = ChoiceField(doc="Index of the metric definition", dtype=int,
319 allowed={x: f"Nth (N={x}) lowest S/N bin" for x in range(1, 5)},
320 optional=False)
321 magName = Field(doc="Name of the magnitude column", dtype=str, default="slot_ModelFlux_mag")
322 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.",
323 dtype=int, default=50)
324 prefix = Field(doc="Prefix of the metric name", dtype=str, default="model")
325 selectExtended = Field(doc="Whether to select extended sources", dtype=bool)
326 selectSnrMin = Field(doc="Minimum median SNR for a source to be selected.",
327 dtype=float)
328 selectSnrMax = Field(doc="Maximum median SNR for a source to be selected.",
329 dtype=float)
330 writeExtras = Field(doc="Write out the magnitude residuals and rms values for debugging.",
331 dtype=bool, default=False)
334class ModelPhotRepTask(Task):
335 """A Task that computes a *ModelPhotRep photometric repeatability metric
336 from an input set of multiple visits of the same field.
338 Notes
339 -----
340 The intended usage is to retarget the run method of
341 `lsst.faro.measurement.TractMatchedMeasurementTask` to ModelPhotRepTask.
342 This metric is calculated on a set of matched visits, and aggregated at the tract level.
343 """
345 ConfigClass = ModelPhotRepTaskConfig
346 _DefaultName = "ModelPhotRepTask"
348 def __init__(self, config: ModelPhotRepTaskConfig, *args, **kwargs):
349 super().__init__(*args, config=config, **kwargs)
351 def run(self, matchedCatalog, metricName):
352 """Calculate the photometric repeatability.
354 Parameters
355 ----------
356 matchedCatalog : `lsst.afw.table.base.Catalog`
357 `~lsst.afw.table.base.Catalog` object as created by
358 `~lsst.afw.table.multiMatch` matching of sources from multiple visits.
359 metricName : `str`
360 The name of the metric.
362 Returns
363 -------
364 measurement : `lsst.verify.Measurement`
365 Measurement of the repeatability and its associated metadata.
366 """
367 self.log.info("Measuring %s", metricName)
369 meas = photRepeat(matchedCatalog, nMinPhotRepeat=self.config.nMinPhotRepeat,
370 snrMax=self.config.selectSnrMax, snrMin=self.config.selectSnrMin,
371 magName=self.config.magName, extended=self.config.selectExtended)
373 name_type = "Gal" if self.config.selectExtended else "Star"
374 name_meas = f'{self.config.prefix}PhotRep{name_type}{self.config.index}'
376 if 'magMean' in meas.keys():
377 if self.config.writeExtras:
378 extras = {
379 'rms': Datum(meas['rms'], label='RMS',
380 description='Photometric repeatability rms for each star.'),
381 'count': Datum(meas['count']*u.count, label='count',
382 description='Number of detections used to calculate repeatability.'),
383 'mean_mag': Datum(meas['magMean'], label='mean_mag',
384 description='Mean magnitude of each star.'),
385 }
386 return Struct(measurement=Measurement(name_meas, meas['repeatability'], extras=extras))
387 else:
388 return Struct(measurement=Measurement(name_meas, meas['repeatability']))
389 else:
390 return Struct(measurement=Measurement(name_meas, np.nan*u.mmag))