Coverage for python/lsst/faro/utils/separations.py : 7%

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 numpy as np
2import astropy.units as u
3import logging
4import lsst.geom as geom
5from lsst.faro.utils.filtermatches import filterMatches
6from lsst.faro.utils.coord_util import averageRaFromCat, averageDecFromCat, sphDist
8__all__ = (
9 "astromRms",
10 "astromResiduals",
11 "calcRmsDistances",
12 "calcSepOutliers",
13 "matchVisitComputeDistance",
14 "calcRmsDistancesVsRef",
15)
18def astromRms(
19 matchedCatalog, mag_bright_cut, mag_faint_cut, annulus_r, width, **filterargs
20):
21 filteredCat = filterMatches(matchedCatalog, **filterargs)
23 magRange = np.array([mag_bright_cut, mag_faint_cut]) * u.mag
24 D = annulus_r * u.arcmin
25 width = width * u.arcmin
26 annulus = D + (width / 2) * np.array([-1, +1])
28 # Require at least 2 measurements to calculate the repeatability:
29 nMinMeas = 2
30 if filteredCat.count > nMinMeas:
31 astrom_resid_rms_meas = calcRmsDistances(
32 filteredCat, annulus, magRange=magRange
33 )
34 return astrom_resid_rms_meas
35 else:
36 return {"nomeas": np.nan * u.marcsec}
39def astromResiduals(
40 matchedCatalog, mag_bright_cut, mag_faint_cut, annulus_r, width, **filterargs
41):
42 filteredCat = filterMatches(matchedCatalog, **filterargs)
44 magRange = np.array([mag_bright_cut, mag_faint_cut]) * u.mag
45 D = annulus_r * u.arcmin
46 width = width * u.arcmin
47 annulus = D + (width / 2) * np.array([-1, +1])
49 # Require at least 2 measurements to calculate the repeatability:
50 nMinMeas = 2
51 if filteredCat.count > nMinMeas:
52 astrom_resid_meas = calcSepOutliers(filteredCat, annulus, magRange=magRange)
53 return astrom_resid_meas
54 else:
55 return {"nomeas": np.nan * u.marcsec}
58def calcRmsDistances(groupView, annulus, magRange, verbose=False):
59 """Calculate the RMS distance of a set of matched objects over visits.
60 Parameters
61 ----------
62 groupView : lsst.afw.table.GroupView
63 GroupView object of matched observations from MultiMatch.
64 annulus : length-2 `astropy.units.Quantity`
65 Distance range (i.e., arcmin) in which to compare objects.
66 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all
67 objects separated from each other between 19 and 21 arcminutes.
68 magRange : length-2 `astropy.units.Quantity`
69 Magnitude range from which to select objects.
70 verbose : bool, optional
71 Output additional information on the analysis steps.
72 Returns
73 -------
74 rmsDistances : `astropy.units.Quantity`
75 RMS angular separations of a set of matched objects over visits.
76 """
77 log = logging.getLogger(__name__)
79 # First we make a list of the keys that we want the fields for
80 importantKeys = [
81 groupView.schema.find(name).key
82 for name in [
83 "id",
84 "coord_ra",
85 "coord_dec",
86 "object",
87 "visit",
88 "base_PsfFlux_mag",
89 ]
90 ]
92 minMag, maxMag = magRange.to(u.mag).value
94 def magInRange(cat):
95 mag = cat.get("base_PsfFlux_mag")
96 (w,) = np.where(np.isfinite(mag))
97 medianMag = np.median(mag[w])
98 return minMag <= medianMag and medianMag < maxMag
100 groupViewInMagRange = groupView.where(magInRange)
102 # List of lists of id, importantValue
103 matchKeyOutput = [
104 obj.get(key) for key in importantKeys for obj in groupViewInMagRange.groups
105 ]
107 jump = len(groupViewInMagRange)
109 ra = matchKeyOutput[1 * jump: 2 * jump]
110 dec = matchKeyOutput[2 * jump: 3 * jump]
111 visit = matchKeyOutput[4 * jump: 5 * jump]
113 # Calculate the mean position of each object from its constituent visits
114 # `aggregate` calculates a quantity for each object in the groupView.
115 meanRa = groupViewInMagRange.aggregate(averageRaFromCat)
116 meanDec = groupViewInMagRange.aggregate(averageDecFromCat)
118 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value)
120 rmsDistances = list()
121 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)):
122 dist = sphDist(ra1, dec1, meanRa[obj1 + 1:], meanDec[obj1 + 1:])
123 (objectsInAnnulus,) = np.where(
124 (annulusRadians[0] <= dist) & (dist < annulusRadians[1])
125 )
126 objectsInAnnulus += obj1 + 1
127 for obj2 in objectsInAnnulus:
128 distances = matchVisitComputeDistance(
129 visit[obj1], ra[obj1], dec[obj1], visit[obj2], ra[obj2], dec[obj2]
130 )
131 if not distances:
132 if verbose:
133 log.debug(
134 "No matching visits " "found for objs: %d and %d" % obj1, obj2
135 )
136 continue
138 (finiteEntries,) = np.where(np.isfinite(distances))
139 # Need at least 2 distances to get a finite sample stdev
140 if len(finiteEntries) > 1:
141 # ddof=1 to get sample standard deviation (e.g., 1/(n-1))
142 rmsDist = np.std(np.array(distances)[finiteEntries], ddof=1)
143 rmsDistances.append(rmsDist)
145 # return quantity
146 rmsDistances = np.array(rmsDistances) * u.radian
147 return rmsDistances
150def calcSepOutliers(groupView, annulus, magRange, verbose=False):
151 """Calculate the RMS distance of a set of matched objects over visits.
152 Parameters
153 ----------
154 groupView : lsst.afw.table.GroupView
155 GroupView object of matched observations from MultiMatch.
156 annulus : length-2 `astropy.units.Quantity`
157 Distance range (i.e., arcmin) in which to compare objects.
158 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all
159 objects separated from each other between 19 and 21 arcminutes.
160 magRange : length-2 `astropy.units.Quantity`
161 Magnitude range from which to select objects.
162 verbose : bool, optional
163 Output additional information on the analysis steps.
164 Returns
165 -------
166 rmsDistances : `astropy.units.Quantity`
167 RMS angular separations of a set of matched objects over visits.
168 """
170 log = logging.getLogger(__name__)
172 # First we make a list of the keys that we want the fields for
173 importantKeys = [
174 groupView.schema.find(name).key
175 for name in [
176 "id",
177 "coord_ra",
178 "coord_dec",
179 "object",
180 "visit",
181 "base_PsfFlux_mag",
182 ]
183 ]
185 minMag, maxMag = magRange.to(u.mag).value
187 def magInRange(cat):
188 mag = cat.get("base_PsfFlux_mag")
189 (w,) = np.where(np.isfinite(mag))
190 medianMag = np.median(mag[w])
191 return minMag <= medianMag and medianMag < maxMag
193 groupViewInMagRange = groupView.where(magInRange)
195 # List of lists of id, importantValue
196 matchKeyOutput = [
197 obj.get(key) for key in importantKeys for obj in groupViewInMagRange.groups
198 ]
200 jump = len(groupViewInMagRange)
202 ra = matchKeyOutput[1 * jump: 2 * jump]
203 dec = matchKeyOutput[2 * jump: 3 * jump]
204 visit = matchKeyOutput[4 * jump: 5 * jump]
206 # Calculate the mean position of each object from its constituent visits
207 # `aggregate` calulates a quantity for each object in the groupView.
208 meanRa = groupViewInMagRange.aggregate(averageRaFromCat)
209 meanDec = groupViewInMagRange.aggregate(averageDecFromCat)
211 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value)
213 sepResiduals = list()
214 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)):
215 dist = sphDist(ra1, dec1, meanRa[obj1 + 1:], meanDec[obj1 + 1:])
216 (objectsInAnnulus,) = np.where(
217 (annulusRadians[0] <= dist) & (dist < annulusRadians[1])
218 )
219 objectsInAnnulus += obj1 + 1
220 for obj2 in objectsInAnnulus:
221 distances = matchVisitComputeDistance(
222 visit[obj1], ra[obj1], dec[obj1], visit[obj2], ra[obj2], dec[obj2]
223 )
224 if not distances:
225 if verbose:
226 log.debug(
227 "No matching visits found " "for objs: %d and %d" % obj1, obj2
228 )
229 continue
231 (finiteEntries,) = np.where(np.isfinite(distances))
232 # Need at least 3 matched pairs so that the median position makes sense
233 if len(finiteEntries) >= 3:
234 okdist = np.array(distances)[finiteEntries]
235 # Get rid of zeros from stars measured against themselves:
236 (realdist,) = np.where(okdist > 0.0)
237 if np.size(realdist) > 0:
238 sepResiduals.append(
239 np.abs(okdist[realdist] - np.median(okdist[realdist]))
240 )
242 # return quantity
243 # import pdb; pdb.set_trace()
244 if len(sepResiduals) > 0:
245 sepResiduals = np.concatenate(np.array(sepResiduals)) * u.radian
246 return sepResiduals
249def matchVisitComputeDistance(
250 visit_obj1, ra_obj1, dec_obj1, visit_obj2, ra_obj2, dec_obj2
251):
252 """Calculate obj1-obj2 distance for each visit in which both objects are seen.
253 For each visit shared between visit_obj1 and visit_obj2,
254 calculate the spherical distance between the obj1 and obj2.
255 visit_obj1 and visit_obj2 are assumed to be unsorted.
256 Parameters
257 ----------
258 visit_obj1 : scalar, list, or numpy.array of int or str
259 List of visits for object 1.
260 ra_obj1 : scalar, list, or numpy.array of float
261 List of RA in each visit for object 1. [radians]
262 dec_obj1 : scalar, list or numpy.array of float
263 List of Dec in each visit for object 1. [radians]
264 visit_obj2 : list or numpy.array of int or str
265 List of visits for object 2.
266 ra_obj2 : list or numpy.array of float
267 List of RA in each visit for object 2. [radians]
268 dec_obj2 : list or numpy.array of float
269 List of Dec in each visit for object 2. [radians]
270 Results
271 -------
272 list of float
273 spherical distances (in radians) for matching visits.
274 """
275 distances = []
276 visit_obj1_idx = np.argsort(visit_obj1)
277 visit_obj2_idx = np.argsort(visit_obj2)
278 j_raw = 0
279 j = visit_obj2_idx[j_raw]
280 for i in visit_obj1_idx:
281 while (visit_obj2[j] < visit_obj1[i]) and (j_raw < len(visit_obj2_idx) - 1):
282 j_raw += 1
283 j = visit_obj2_idx[j_raw]
284 if visit_obj2[j] == visit_obj1[i]:
285 if np.isfinite([ra_obj1[i], dec_obj1[i], ra_obj2[j], dec_obj2[j]]).all():
286 distances.append(
287 sphDist(ra_obj1[i], dec_obj1[i], ra_obj2[j], dec_obj2[j])
288 )
289 return distances
292def calcRmsDistancesVsRef(groupView, refVisit, magRange, band, verbose=False):
293 """Calculate the RMS distance of a set of matched objects over visits.
294 Parameters
295 ----------
296 groupView : lsst.afw.table.GroupView
297 GroupView object of matched observations from MultiMatch.
298 magRange : length-2 `astropy.units.Quantity`
299 Magnitude range from which to select objects.
300 verbose : bool, optional
301 Output additional information on the analysis steps.
302 Returns
303 -------
304 rmsDistances : `astropy.units.Quantity`
305 RMS angular separations of a set of matched objects over visits.
306 separations : `astropy.units.Quantity`
307 Angular separations of the set a matched objects.
308 """
310 minMag, maxMag = magRange.to(u.mag).value
312 def magInRange(cat):
313 mag = cat.get("base_PsfFlux_mag")
314 (w,) = np.where(np.isfinite(mag))
315 medianMag = np.median(mag[w])
316 return minMag <= medianMag and medianMag < maxMag
318 groupViewInMagRange = groupView.where(magInRange)
320 # Get lists of the unique objects and visits:
321 uniqObj = groupViewInMagRange.ids
322 uniqVisits = set()
323 for id in uniqObj:
324 for v, f in zip(
325 groupViewInMagRange[id].get("visit"), groupViewInMagRange[id].get("filt")
326 ):
327 if f == band:
328 uniqVisits.add(v)
330 uniqVisits = list(uniqVisits)
332 if not isinstance(refVisit, int):
333 refVisit = int(refVisit)
335 if refVisit in uniqVisits:
336 # Remove the reference visit from the set of visits:
337 uniqVisits.remove(refVisit)
339 rmsDistances = list()
341 # Loop over visits, calculating the RMS for each:
342 for vis in uniqVisits:
344 distancesVisit = list()
346 for obj in uniqObj:
347 visMatch = np.where(groupViewInMagRange[obj].get("visit") == vis)
348 refMatch = np.where(groupViewInMagRange[obj].get("visit") == refVisit)
350 raObj = groupViewInMagRange[obj].get("coord_ra")
351 decObj = groupViewInMagRange[obj].get("coord_dec")
353 # Require it to have a match in both the reference and visit image:
354 if np.size(visMatch[0]) > 0 and np.size(refMatch[0]) > 0:
355 distances = sphDist(
356 raObj[refMatch], decObj[refMatch], raObj[visMatch], decObj[visMatch]
357 )
359 distancesVisit.append(distances)
361 finiteEntries = np.where(np.isfinite(distancesVisit))[0]
362 # Need at least 2 distances to get a finite sample stdev
363 if len(finiteEntries) > 1:
364 # Calculate the RMS of these offsets:
365 # ddof=1 to get sample standard deviation (e.g., 1/(n-1))
366 pos_rms_rad = np.std(np.array(distancesVisit)[finiteEntries], ddof=1)
367 pos_rms_mas = geom.radToMas(pos_rms_rad) # milliarcsec
368 rmsDistances.append(pos_rms_mas)
370 else:
371 rmsDistances.append(np.nan)
373 rmsDistances = np.array(rmsDistances) * u.marcsec
374 return rmsDistances
377def radiansToMilliarcsec(rad):
378 return np.rad2deg(rad) * 3600 * 1000
381def arcminToRadians(arcmin):
382 return np.deg2rad(arcmin / 60)