Coverage for python/lsst/faro/utils/separations.py: 7%
158 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-05 11:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-05 11:14 +0000
1# This file is part of faro.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import numpy as np
23import astropy.units as u
24import logging
25import lsst.geom as geom
26from lsst.faro.utils.filtermatches import filterMatches
27from lsst.faro.utils.coord_util import averageRaFromCat, averageDecFromCat, sphDist
29__all__ = (
30 "astromRms",
31 "astromResiduals",
32 "calcRmsDistances",
33 "calcSepOutliers",
34 "matchVisitComputeDistance",
35 "calcRmsDistancesVsRef",
36)
39def astromRms(
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_rms_meas = calcRmsDistances(
53 filteredCat, annulus, magRange=magRange
54 )
55 return astrom_resid_rms_meas
56 else:
57 return {"nomeas": np.nan * u.marcsec}
60def astromResiduals(
61 matchedCatalog, mag_bright_cut, mag_faint_cut, annulus_r, width, **filterargs
62):
63 filteredCat = filterMatches(matchedCatalog, **filterargs)
65 magRange = np.array([mag_bright_cut, mag_faint_cut]) * u.mag
66 D = annulus_r * u.arcmin
67 width = width * u.arcmin
68 annulus = D + (width / 2) * np.array([-1, +1])
70 # Require at least 2 measurements to calculate the repeatability:
71 nMinMeas = 2
72 if filteredCat.count > nMinMeas:
73 astrom_resid_meas = calcSepOutliers(filteredCat, annulus, magRange=magRange)
74 return astrom_resid_meas
75 else:
76 return {"nomeas": np.nan * u.marcsec}
79def calcRmsDistances(groupView, annulus, magRange, verbose=False):
80 """Calculate the RMS distance of a set of matched objects over visits.
81 Parameters
82 ----------
83 groupView : lsst.afw.table.GroupView
84 GroupView object of matched observations from MultiMatch.
85 annulus : length-2 `astropy.units.Quantity`
86 Distance range (i.e., arcmin) in which to compare objects.
87 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all
88 objects separated from each other between 19 and 21 arcminutes.
89 magRange : length-2 `astropy.units.Quantity`
90 Magnitude range from which to select objects.
91 verbose : bool, optional
92 Output additional information on the analysis steps.
93 Returns
94 -------
95 rmsDistances : `astropy.units.Quantity`
96 RMS angular separations of a set of matched objects over visits.
97 """
98 log = logging.getLogger(__name__)
100 # First we make a list of the keys that we want the fields for
101 importantKeys = [
102 groupView.schema.find(name).key
103 for name in [
104 "id",
105 "coord_ra",
106 "coord_dec",
107 "object",
108 "visit",
109 "base_PsfFlux_mag",
110 ]
111 ]
113 minMag, maxMag = magRange.to(u.mag).value
115 def magInRange(cat):
116 mag = cat.get("base_PsfFlux_mag")
117 (w,) = np.where(np.isfinite(mag))
118 medianMag = np.median(mag[w])
119 return minMag <= medianMag and medianMag < maxMag
121 groupViewInMagRange = groupView.where(magInRange)
123 # List of lists of id, importantValue
124 matchKeyOutput = [
125 obj.get(key) for key in importantKeys for obj in groupViewInMagRange.groups
126 ]
128 jump = len(groupViewInMagRange)
130 ra = matchKeyOutput[1 * jump: 2 * jump]
131 dec = matchKeyOutput[2 * jump: 3 * jump]
132 visit = matchKeyOutput[4 * jump: 5 * jump]
134 # Calculate the mean position of each object from its constituent visits
135 # `aggregate` calculates a quantity for each object in the groupView.
136 meanRa = groupViewInMagRange.aggregate(averageRaFromCat)
137 meanDec = groupViewInMagRange.aggregate(averageDecFromCat)
139 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value)
141 rmsDistances = list()
142 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)):
143 dist = sphDist(ra1, dec1, meanRa[obj1 + 1:], meanDec[obj1 + 1:])
144 (objectsInAnnulus,) = np.where(
145 (annulusRadians[0] <= dist) & (dist < annulusRadians[1])
146 )
147 objectsInAnnulus += obj1 + 1
148 for obj2 in objectsInAnnulus:
149 distances = matchVisitComputeDistance(
150 visit[obj1], ra[obj1], dec[obj1], visit[obj2], ra[obj2], dec[obj2]
151 )
152 if not distances:
153 if verbose:
154 log.debug(
155 "No matching visits " "found for objs: %d and %d" % obj1, obj2
156 )
157 continue
159 (finiteEntries,) = np.where(np.isfinite(distances))
160 # Need at least 2 distances to get a finite sample stdev
161 if len(finiteEntries) > 1:
162 # ddof=1 to get sample standard deviation (e.g., 1/(n-1))
163 rmsDist = np.std(np.array(distances)[finiteEntries], ddof=1)
164 rmsDistances.append(rmsDist)
166 # return quantity
167 rmsDistances = np.array(rmsDistances) * u.radian
168 return rmsDistances
171def calcSepOutliers(groupView, annulus, magRange, verbose=False):
172 """Calculate the RMS distance of a set of matched objects over visits.
173 Parameters
174 ----------
175 groupView : lsst.afw.table.GroupView
176 GroupView object of matched observations from MultiMatch.
177 annulus : length-2 `astropy.units.Quantity`
178 Distance range (i.e., arcmin) in which to compare objects.
179 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all
180 objects separated from each other between 19 and 21 arcminutes.
181 magRange : length-2 `astropy.units.Quantity`
182 Magnitude range from which to select objects.
183 verbose : bool, optional
184 Output additional information on the analysis steps.
185 Returns
186 -------
187 rmsDistances : `astropy.units.Quantity`
188 RMS angular separations of a set of matched objects over visits.
189 """
191 log = logging.getLogger(__name__)
193 # First we make a list of the keys that we want the fields for
194 importantKeys = [
195 groupView.schema.find(name).key
196 for name in [
197 "id",
198 "coord_ra",
199 "coord_dec",
200 "object",
201 "visit",
202 "base_PsfFlux_mag",
203 ]
204 ]
206 minMag, maxMag = magRange.to(u.mag).value
208 def magInRange(cat):
209 mag = cat.get("base_PsfFlux_mag")
210 (w,) = np.where(np.isfinite(mag))
211 medianMag = np.median(mag[w])
212 return minMag <= medianMag and medianMag < maxMag
214 groupViewInMagRange = groupView.where(magInRange)
216 # List of lists of id, importantValue
217 matchKeyOutput = [
218 obj.get(key) for key in importantKeys for obj in groupViewInMagRange.groups
219 ]
221 jump = len(groupViewInMagRange)
223 ra = matchKeyOutput[1 * jump: 2 * jump]
224 dec = matchKeyOutput[2 * jump: 3 * jump]
225 visit = matchKeyOutput[4 * jump: 5 * jump]
227 # Calculate the mean position of each object from its constituent visits
228 # `aggregate` calulates a quantity for each object in the groupView.
229 meanRa = groupViewInMagRange.aggregate(averageRaFromCat)
230 meanDec = groupViewInMagRange.aggregate(averageDecFromCat)
232 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value)
234 sepResiduals = list()
235 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)):
236 dist = sphDist(ra1, dec1, meanRa[obj1 + 1:], meanDec[obj1 + 1:])
237 (objectsInAnnulus,) = np.where(
238 (annulusRadians[0] <= dist) & (dist < annulusRadians[1])
239 )
240 objectsInAnnulus += obj1 + 1
241 for obj2 in objectsInAnnulus:
242 distances = matchVisitComputeDistance(
243 visit[obj1], ra[obj1], dec[obj1], visit[obj2], ra[obj2], dec[obj2]
244 )
245 if not distances:
246 if verbose:
247 log.debug(
248 "No matching visits found " "for objs: %d and %d" % obj1, obj2
249 )
250 continue
252 (finiteEntries,) = np.where(np.isfinite(distances))
253 # Need at least 3 matched pairs so that the median position makes sense
254 if len(finiteEntries) >= 3:
255 okdist = np.array(distances)[finiteEntries]
256 # Get rid of zeros from stars measured against themselves:
257 (realdist,) = np.where(okdist > 0.0)
258 if np.size(realdist) > 0:
259 sepResiduals.append(
260 np.abs(okdist[realdist] - np.median(okdist[realdist]))
261 )
263 # return quantity
264 # import pdb; pdb.set_trace()
265 if len(sepResiduals) > 0:
266 sepResiduals = np.concatenate(np.array(sepResiduals)) * u.radian
267 return sepResiduals
270def matchVisitComputeDistance(
271 visit_obj1, ra_obj1, dec_obj1, visit_obj2, ra_obj2, dec_obj2
272):
273 """Calculate obj1-obj2 distance for each visit in which both objects are seen.
274 For each visit shared between visit_obj1 and visit_obj2,
275 calculate the spherical distance between the obj1 and obj2.
276 visit_obj1 and visit_obj2 are assumed to be unsorted.
277 Parameters
278 ----------
279 visit_obj1 : scalar, list, or numpy.array of int or str
280 List of visits for object 1.
281 ra_obj1 : scalar, list, or numpy.array of float
282 List of RA in each visit for object 1. [radians]
283 dec_obj1 : scalar, list or numpy.array of float
284 List of Dec in each visit for object 1. [radians]
285 visit_obj2 : list or numpy.array of int or str
286 List of visits for object 2.
287 ra_obj2 : list or numpy.array of float
288 List of RA in each visit for object 2. [radians]
289 dec_obj2 : list or numpy.array of float
290 List of Dec in each visit for object 2. [radians]
291 Results
292 -------
293 list of float
294 spherical distances (in radians) for matching visits.
295 """
296 distances = []
297 visit_obj1_idx = np.argsort(visit_obj1)
298 visit_obj2_idx = np.argsort(visit_obj2)
299 j_raw = 0
300 j = visit_obj2_idx[j_raw]
301 for i in visit_obj1_idx:
302 while (visit_obj2[j] < visit_obj1[i]) and (j_raw < len(visit_obj2_idx) - 1):
303 j_raw += 1
304 j = visit_obj2_idx[j_raw]
305 if visit_obj2[j] == visit_obj1[i]:
306 if np.isfinite([ra_obj1[i], dec_obj1[i], ra_obj2[j], dec_obj2[j]]).all():
307 distances.append(
308 sphDist(ra_obj1[i], dec_obj1[i], ra_obj2[j], dec_obj2[j])
309 )
310 return distances
313def calcRmsDistancesVsRef(groupView, refVisit, magRange, band, verbose=False):
314 """Calculate the RMS distance of a set of matched objects over visits.
315 Parameters
316 ----------
317 groupView : lsst.afw.table.GroupView
318 GroupView object of matched observations from MultiMatch.
319 magRange : length-2 `astropy.units.Quantity`
320 Magnitude range from which to select objects.
321 verbose : bool, optional
322 Output additional information on the analysis steps.
323 Returns
324 -------
325 rmsDistances : `astropy.units.Quantity`
326 RMS angular separations of a set of matched objects over visits.
327 separations : `astropy.units.Quantity`
328 Angular separations of the set a matched objects.
329 """
331 minMag, maxMag = magRange.to(u.mag).value
333 def magInRange(cat):
334 mag = cat.get("base_PsfFlux_mag")
335 (w,) = np.where(np.isfinite(mag))
336 medianMag = np.median(mag[w])
337 return minMag <= medianMag and medianMag < maxMag
339 groupViewInMagRange = groupView.where(magInRange)
341 # Get lists of the unique objects and visits:
342 uniqObj = groupViewInMagRange.ids
343 uniqVisits = set()
344 for id in uniqObj:
345 for v, f in zip(
346 groupViewInMagRange[id].get("visit"), groupViewInMagRange[id].get("filt")
347 ):
348 if f == band:
349 uniqVisits.add(v)
351 uniqVisits = list(uniqVisits)
353 if not isinstance(refVisit, int):
354 refVisit = int(refVisit)
356 if refVisit in uniqVisits:
357 # Remove the reference visit from the set of visits:
358 uniqVisits.remove(refVisit)
360 rmsDistances = list()
362 # Loop over visits, calculating the RMS for each:
363 for vis in uniqVisits:
365 distancesVisit = list()
367 for obj in uniqObj:
368 visMatch = np.where(groupViewInMagRange[obj].get("visit") == vis)
369 refMatch = np.where(groupViewInMagRange[obj].get("visit") == refVisit)
371 raObj = groupViewInMagRange[obj].get("coord_ra")
372 decObj = groupViewInMagRange[obj].get("coord_dec")
374 # Require it to have a match in both the reference and visit image:
375 if np.size(visMatch[0]) > 0 and np.size(refMatch[0]) > 0:
376 distances = sphDist(
377 raObj[refMatch], decObj[refMatch], raObj[visMatch], decObj[visMatch]
378 )
380 distancesVisit.append(distances)
382 finiteEntries = np.where(np.isfinite(distancesVisit))[0]
383 # Need at least 2 distances to get a finite sample stdev
384 if len(finiteEntries) > 1:
385 # Calculate the RMS of these offsets:
386 # ddof=1 to get sample standard deviation (e.g., 1/(n-1))
387 pos_rms_rad = np.std(np.array(distancesVisit)[finiteEntries], ddof=1)
388 pos_rms_mas = geom.radToMas(pos_rms_rad) # milliarcsec
389 rmsDistances.append(pos_rms_mas)
391 else:
392 rmsDistances.append(np.nan)
394 rmsDistances = np.array(rmsDistances) * u.marcsec
395 return rmsDistances
398def radiansToMilliarcsec(rad):
399 return np.rad2deg(rad) * 3600 * 1000
402def arcminToRadians(arcmin):
403 return np.deg2rad(arcmin / 60)