Coverage for python/lsst/validate/drp/calcsrd/amx.py: 8%
78 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 19:03 -0800
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 19:03 -0800
1# LSST Data Management System
2# Copyright 2016 AURA/LSST.
3#
4# This product includes software developed by the
5# LSST Project (http://www.lsst.org/).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the LSST License Statement and
18# the GNU General Public License along with this program. If not,
19# see <https://www.lsstcorp.org/LegalNotices/>.
21import numpy as np
22import astropy.units as u
24from lsst.verify import Measurement, Datum
26from ..util import (averageRaFromCat, averageDecFromCat,
27 sphDist)
30def measureAMx(metric, matchedDataset, D, width=2., magRange=None, verbose=False):
31 r"""Measurement of AMx (x=1,2,3): The maximum rms of the astrometric
32 distance distribution for stellar pairs with separations of D arcmin
33 (repeatability).
35 Parameters
36 ----------
37 metric : `lsst.verify.Metric`
38 An AM1, AM2 or AM3 `~lsst.verify.Metric` instance.
39 matchedDataset : lsst.verify.Blob
40 Contains the spacially matched dataset for the measurement
41 D : `astropy.units.Quantity`
42 Radial distance of the annulus in arcmin
43 width : `float` or `astropy.units.Quantity`, optional
44 Width around fiducial distance to include. [arcmin]
45 magRange : 2-element `list`, `tuple`, or `numpy.ndarray`, optional
46 brighter, fainter limits of the magnitude range to include.
47 Default: ``[17.5, 21.5]`` mag.
48 verbose : `bool`, optional
49 Output additional information on the analysis steps.
51 Returns
52 -------
53 measurement : `lsst.verify.Measurement`
54 Measurement of AMx (x=1,2,3) and associated metadata.
56 Notes
57 -----
58 This table below is provided ``validate_drp``\ 's :file:`metrics.yaml`.
60 LPM-17 dated 2011-07-06
62 Specification:
63 The rms of the astrometric distance distribution for
64 stellar pairs with separation of D arcmin (repeatability)
65 will not exceed AMx milliarcsec (median distribution for a large number
66 of sources). No more than AFx % of the sample will deviate by more than
67 ADx milliarcsec from the median. AMx, AFx, and ADx are specified for
68 D=5, 20 and 200 arcmin for x= 1, 2, and 3, in the same order (Table 18).
70 The three selected characteristic distances reflect the size of an
71 individual sensor, a raft, and the camera. The required median astrometric
72 precision is driven by the desire to achieve a proper motion accuracy of
73 0.2 mas/yr and parallax accuracy of 1.0 mas over the course of the survey.
74 These two requirements correspond to relative astrometric precision for a
75 single image of 10 mas (per coordinate).
77 ========================= ====== ======= =======
78 Astrometric Repeatability Specification
79 ------------------------- ----------------------
80 Metric Design Minimum Stretch
81 ========================= ====== ======= =======
82 AM1 (milliarcsec) 10 20 5
83 AF1 (%) 10 20 5
84 AD1 (milliarcsec) 20 40 10
85 AM2 (milliarcsec) 10 20 5
86 AF2 (%) 10 20 5
87 AD2 (milliarcsec) 20 40 10
88 AM3 (milliarcsec) 15 30 10
89 AF3 (%) 10 20 5
90 AD3 (milliarcsec) 30 50 20
91 ========================= ====== ======= =======
93 Table 18: The specifications for astrometric precision.
94 The three blocks of values correspond to D=5, 20 and 200 arcmin,
95 and to astrometric measurements performed in the r and i bands.
96 """
98 matches = matchedDataset.matchesBright
100 datums = {}
102 # Measurement Parameters
103 datums['D'] = Datum(quantity=D, label="Distance", description="Radial distance of annulus (arcmin)")
105 if not isinstance(width, u.Quantity):
106 width = width * u.arcmin
107 datums['Width'] = Datum(quantity=width, label='Width', description='Width of annulus')
108 if magRange is None:
109 magRange = np.array([17.0, 21.5]) * u.mag
110 else:
111 assert len(magRange) == 2
112 if not isinstance(magRange, u.Quantity):
113 magRange = np.array(magRange) * u.mag
114 datums['magRange'] = Datum(quantity=magRange, description='Stellar magnitude selection range.')
116 annulus = D + (width/2)*np.array([-1, +1])
117 datums['annulus'] = Datum(quantity=annulus, label='annulus radii',
118 description='Inner and outer radii of selection annulus.')
120 # Register measurement extras
121 rmsDistances = calcRmsDistances(
122 matches,
123 annulus,
124 magRange=magRange,
125 verbose=verbose)
127 if len(rmsDistances) == 0:
128 # Should be a proper log message
129 print('No stars found that are {0:.1f}--{1:.1f} apart.'.format(
130 annulus[0], annulus[1]))
131 datums['rmsDistMas'] = Datum(quantity=None, label='RMS')
132 quantity = np.nan * u.marcsec
133 else:
134 datums['rmsDistMas'] = Datum(quantity=rmsDistances.to(u.marcsec), label='RMS')
135 quantity = np.median(rmsDistances.to(u.marcsec))
137 return Measurement(metric.name, quantity, extras=datums)
140def calcRmsDistances(groupView, annulus, magRange, verbose=False):
141 """Calculate the RMS distance of a set of matched objects over visits.
143 Parameters
144 ----------
145 groupView : lsst.afw.table.GroupView
146 GroupView object of matched observations from MultiMatch.
147 annulus : length-2 `astropy.units.Quantity`
148 Distance range (i.e., arcmin) in which to compare objects.
149 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all
150 objects separated from each other between 19 and 21 arcminutes.
151 magRange : length-2 `astropy.units.Quantity`
152 Magnitude range from which to select objects.
153 verbose : bool, optional
154 Output additional information on the analysis steps.
156 Returns
157 -------
158 rmsDistances : `astropy.units.Quantity`
159 RMS angular separations of a set of matched objects over visits.
160 """
162 # First we make a list of the keys that we want the fields for
163 importantKeys = [groupView.schema.find(name).key for
164 name in ['id', 'coord_ra', 'coord_dec',
165 'object', 'visit', 'base_PsfFlux_mag']]
167 minMag, maxMag = magRange.to(u.mag).value
169 def magInRange(cat):
170 mag = cat.get('base_PsfFlux_mag')
171 w, = np.where(np.isfinite(mag))
172 medianMag = np.median(mag[w])
173 return minMag <= medianMag and medianMag < maxMag
175 groupViewInMagRange = groupView.where(magInRange)
177 # List of lists of id, importantValue
178 matchKeyOutput = [obj.get(key)
179 for key in importantKeys
180 for obj in groupViewInMagRange.groups]
182 jump = len(groupViewInMagRange)
184 ra = matchKeyOutput[1*jump:2*jump]
185 dec = matchKeyOutput[2*jump:3*jump]
186 visit = matchKeyOutput[4*jump:5*jump]
188 # Calculate the mean position of each object from its constituent visits
189 # `aggregate` calulates a quantity for each object in the groupView.
190 meanRa = groupViewInMagRange.aggregate(averageRaFromCat)
191 meanDec = groupViewInMagRange.aggregate(averageDecFromCat)
193 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value)
195 rmsDistances = list()
196 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)):
197 dist = sphDist(ra1, dec1, meanRa[obj1+1:], meanDec[obj1+1:])
198 objectsInAnnulus, = np.where((annulusRadians[0] <= dist)
199 & (dist < annulusRadians[1]))
200 for obj2 in objectsInAnnulus:
201 distances = matchVisitComputeDistance(
202 visit[obj1], ra[obj1], dec[obj1],
203 visit[obj2], ra[obj2], dec[obj2])
204 if not distances:
205 if verbose:
206 print("No matching visits found for objs: %d and %d" %
207 (obj1, obj2))
208 continue
210 finiteEntries, = np.where(np.isfinite(distances))
211 # Need at least 2 distances to get a finite sample stdev
212 if len(finiteEntries) > 1:
213 # ddof=1 to get sample standard deviation (e.g., 1/(n-1))
214 rmsDist = np.std(np.array(distances)[finiteEntries], ddof=1)
215 rmsDistances.append(rmsDist)
217 # return quantity
218 rmsDistances = np.array(rmsDistances) * u.radian
219 return rmsDistances
222def matchVisitComputeDistance(visit_obj1, ra_obj1, dec_obj1,
223 visit_obj2, ra_obj2, dec_obj2):
224 """Calculate obj1-obj2 distance for each visit in which both objects are seen.
226 For each visit shared between visit_obj1 and visit_obj2,
227 calculate the spherical distance between the obj1 and obj2.
229 visit_obj1 and visit_obj2 are assumed to be unsorted.
231 Parameters
232 ----------
233 visit_obj1 : scalar, list, or numpy.array of int or str
234 List of visits for object 1.
235 ra_obj1 : scalar, list, or numpy.array of float
236 List of RA in each visit for object 1. [radians]
237 dec_obj1 : scalar, list or numpy.array of float
238 List of Dec in each visit for object 1. [radians]
239 visit_obj2 : list or numpy.array of int or str
240 List of visits for object 2.
241 ra_obj2 : list or numpy.array of float
242 List of RA in each visit for object 2. [radians]
243 dec_obj2 : list or numpy.array of float
244 List of Dec in each visit for object 2. [radians]
246 Results
247 -------
248 list of float
249 spherical distances (in radians) for matching visits.
250 """
251 distances = []
252 visit_obj1_idx = np.argsort(visit_obj1)
253 visit_obj2_idx = np.argsort(visit_obj2)
254 j_raw = 0
255 j = visit_obj2_idx[j_raw]
256 for i in visit_obj1_idx:
257 while (visit_obj2[j] < visit_obj1[i]) and (j_raw < len(visit_obj2_idx)-1):
258 j_raw += 1
259 j = visit_obj2_idx[j_raw]
260 if visit_obj2[j] == visit_obj1[i]:
261 if np.isfinite([ra_obj1[i], dec_obj1[i],
262 ra_obj2[j], dec_obj2[j]]).all():
263 distances.append(sphDist(ra_obj1[i], dec_obj1[i],
264 ra_obj2[j], dec_obj2[j]))
265 return distances
268def radiansToMilliarcsec(rad):
269 return np.rad2deg(rad)*3600*1000
272def arcminToRadians(arcmin):
273 return np.deg2rad(arcmin/60)