Coverage for python/lsst/sims/maf/metrics/seasonMetrics.py : 15%

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
1"""A group of metrics that work together to evaluate season characteristics (length, number, etc).
2In addition, these supports the time delay metric calculation for strong lensing.
3"""
5import numpy as np
6from .baseMetric import BaseMetric
7from lsst.sims.photUtils import Dust_values
9__all__ = ['calcSeason', 'findSeasonEdges',
10 'SeasonLengthMetric', 'CampaignLengthMetric',
11 'MeanCampaignFrequencyMetric', 'TdcMetric']
14def calcSeason(ra, time):
15 """Calculate the 'season' in the survey for a series of ra/dec/time values of an observation.
16 Based only on the RA of the point on the sky, it calculates the 'season' based on when this
17 point would be overhead .. the season is considered +/- 0.5 years around this time.
19 Parameters
20 ----------
21 ra : float
22 The RA (in degrees) of the point on the sky
23 time : np.ndarray
24 The times of the observations, in MJD
26 Returns
27 -------
28 np.ndarray
29 The season values
30 """
31 # Reference RA and equinox to anchor ra/season reference - RA = 0 is overhead at autumnal equinox
32 # autumn equinox 2014 happened on september 23 --> equinox MJD
33 Equinox = 2456923.5 - 2400000.5
34 # convert ra into 'days'
35 dayRA = ra / 360 * 365.25
36 firstSeasonBegan = Equinox + dayRA - 0.5 * 365.25
37 seasons = (time - firstSeasonBegan) / 365.25
38 # Set first season to 0
39 seasons = seasons - np.floor(np.min(seasons))
40 return seasons
42def findSeasonEdges(seasons):
43 """Given the seasons, return the indexes of each start/end of the season.
45 Parameters
46 ----------
47 seasons: np.ndarray
48 Seasons, such as calculated by calcSeason.
49 Note that seasons should be sorted!!
51 Returns
52 -------
53 np.ndarray, np.ndarray
54 The indexes of the first and last date in the season.
55 """
56 intSeasons = np.floor(seasons)
57 # Get the unique seasons, so that we can separate each one
58 season_list = np.unique(intSeasons)
59 # Find the first and last observation of each season.
60 firstOfSeason = np.searchsorted(intSeasons, season_list)
61 lastOfSeason = np.searchsorted(intSeasons, season_list, side='right') - 1
62 return firstOfSeason, lastOfSeason
65class SeasonLengthMetric(BaseMetric):
66 """
67 Calculate the length of LSST seasons, in days.
69 Parameters
70 ----------
71 minExpTime: float, opt
72 Minimum visit exposure time to count for a 'visit', in seconds. Default 20.
73 reduceFunc : function, optional
74 Function that can operate on array-like structures. Typically numpy function.
75 This reduces the season length in each season from 10 separate values to a single value.
76 Default np.median.
77 """
78 def __init__(self, mjdCol='observationStartMJD', expTimeCol='visitExposureTime', minExpTime=20,
79 reduceFunc=np.median, metricName='SeasonLength', **kwargs):
80 units = 'days'
81 self.mjdCol = mjdCol
82 self.expTimeCol = expTimeCol
83 self.minExpTime = minExpTime
84 self.reduceFunc = reduceFunc
85 super().__init__(col=[self.mjdCol, self.expTimeCol],
86 units=units, metricName=metricName, **kwargs)
88 def run(self, dataSlice, slicePoint):
89 """Calculate the (reduceFunc) of the length of each season.
90 Uses the slicePoint RA/Dec to calculate the position in question, then uses the times of the visits
91 to assign them into seasons (based on where the sun is relative to the slicePoint RA).
93 Parameters
94 ----------
95 dataSlice : numpy.array
96 Numpy structured array containing the data related to the visits provided by the slicer.
97 slicePoint : dict
98 Dictionary containing information about the slicepoint currently active in the slicer.
100 Returns
101 -------
102 float
103 The (reduceFunc) of the length of each season, in days.
104 """
105 # Order data Slice/times and exclude visits which are too short.
106 long = np.where(dataSlice[self.expTimeCol] > self.minExpTime)
107 if len(long[0]) == 0:
108 return self.badval
109 data = np.sort(dataSlice[long], order=self.mjdCol)
110 # SlicePoints ra/dec are always in radians - convert to degrees to calculate season
111 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol])
112 firstOfSeason, lastOfSeason = findSeasonEdges(seasons)
113 seasonlengths = data[self.mjdCol][lastOfSeason] - data[self.mjdCol][firstOfSeason]
114 result = self.reduceFunc(seasonlengths)
115 return result
118class CampaignLengthMetric(BaseMetric):
119 """Calculate the number of seasons (roughly, years) a pointing is observed for.
120 This corresponds to the 'campaign length' for lensed quasar time delays.
121 """
122 def __init__(self, mjdCol='observationStartMJD', expTimeCol='visitExposureTime', minExpTime=20, **kwargs):
123 units = ''
124 self.expTimeCol = expTimeCol
125 self.minExpTime = minExpTime
126 self.mjdCol = mjdCol
127 super().__init__(col=[self.mjdCol, self.expTimeCol], units=units, **kwargs)
129 def run(self, dataSlice, slicePoint):
130 # Order data Slice/times and exclude visits which are too short.
131 long = np.where(dataSlice[self.expTimeCol] > self.minExpTime)
132 if len(long[0]) == 0:
133 return self.badval
134 data = np.sort(dataSlice[long], order=self.mjdCol)
135 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol])
136 intSeasons = np.floor(seasons)
137 count = len(np.unique(intSeasons))
138 return count
141class MeanCampaignFrequencyMetric(BaseMetric):
142 """Calculate the mean separation between nights, within a season - then the mean over the campaign.
143 Calculate per season, to avoid any influence from season gaps.
144 """
145 def __init__(self, mjdCol='observationStartMJD', expTimeCol='visitExposureTime', minExpTime=20,
146 nightCol='night', **kwargs):
147 self.mjdCol = mjdCol
148 self.expTimeCol = expTimeCol
149 self.minExpTime = minExpTime
150 self.nightCol = nightCol
151 units = 'nights'
152 super().__init__(col=[self.mjdCol, self.expTimeCol, self.nightCol], units=units, **kwargs)
154 def run(self, dataSlice, slicePoint):
155 # Order data Slice/times and exclude visits which are too short.
156 long = np.where(dataSlice[self.expTimeCol] > self.minExpTime)
157 if len(long[0]) == 0:
158 return self.badval
159 data = np.sort(dataSlice[long], order=self.mjdCol)
160 # SlicePoints ra/dec are always in radians - convert to degrees to calculate season
161 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol])
162 firstOfSeason, lastOfSeason = findSeasonEdges(seasons)
163 seasonMeans = np.zeros(len(firstOfSeason), float)
164 for i, (first, last) in enumerate(zip(firstOfSeason, lastOfSeason)):
165 if first < last:
166 n = data[self.nightCol][first:last+1]
167 deltaNights = np.diff(np.unique(n))
168 if len(deltaNights) > 0:
169 seasonMeans[i] = np.mean(deltaNights)
170 return np.mean(seasonMeans)
173class TdcMetric(BaseMetric):
174 """Calculate the Time Delay Challenge metric, as described in Liao et al 2015
175 (https://arxiv.org/pdf/1409.1254.pdf).
177 This combines the MeanCampaignFrequency/MeanNightSeparation, the SeasonLength, and the CampaignLength
178 metrics above, but rewritten to calculate season information only once.
180 cadNorm = in units of days
181 seaNorm = in units of months
182 campNorm = in units of years
184 This metric also adds a requirement to achieve limiting magnitudes after galactic dust extinction,
185 in various bandpasses, in order to exclude visits which are not useful for detecting quasars
186 (due to being short or having high sky brightness, etc.) and to reject regions with
187 high galactic dust extinction.
189 Parameters
190 ----------
191 mjdCol: str, opt
192 Column name for mjd. Default observationStartMJD.
193 nightCol: str, opt
194 Column name for night. Default night.
195 filterCol: str, opt
196 Column name for filter. Default filter.
197 m5Col: str, opt
198 Column name for five-sigma depth. Default fiveSigmaDepth.
199 magCuts: dict, opt
200 Dictionary with filtername:mag limit (after dust extinction). Default None in kwarg.
201 Defaults set within metric: {'u': 22.7, 'g': 24.1, 'r': 23.7, 'i': 23.1, 'z': 22.2, 'y': 21.4}
202 metricName: str, opt
203 Metric Name. Default TDC.
204 cadNorm: float, opt
205 Cadence normalization constant, in units of days. Default 3.
206 seaNorm: float, opt
207 Season normalization constant, in units of months. Default 4.
208 campNorm: float, opt
209 Campaign length normalization constant, in units of years. Default 5.
210 badval: float, opt
211 Return this value instead of the dictionary for bad points.
213 Returns
214 -------
215 dictionary
216 Dictionary of values for {'rate', 'precision', 'accuracy'} at this point in the sky.
217 """
218 def __init__(self, mjdCol='observationStartMJD', nightCol='night', filterCol='filter',
219 m5Col='fiveSigmaDepth', magCuts=None,
220 metricName = 'TDC', cadNorm=3., seaNorm=4., campNorm=5., badval=-999, **kwargs):
221 # Save the normalization values.
222 self.cadNorm = cadNorm
223 self.seaNorm = seaNorm
224 self.campNorm = campNorm
225 self.mjdCol = mjdCol
226 self.m5Col = m5Col
227 self.nightCol = nightCol
228 self.filterCol = filterCol
229 if magCuts is None:
230 self.magCuts = {'u': 22.7, 'g': 24.1, 'r': 23.7, 'i': 23.1, 'z': 22.2, 'y': 21.4}
231 else:
232 self.magCuts = magCuts
233 if not isinstance(self.magCuts, dict):
234 raise Exception('magCuts should be a dictionary')
235 # Set up dust map requirement
236 maps = ['DustMap']
237 # Set the default wavelength limits for the lsst filters. These are approximately correct.
238 dust_properties = Dust_values()
239 self.Ax1 = dust_properties.Ax1
240 super().__init__(col=[self.mjdCol, self.m5Col, self.nightCol, self.filterCol],
241 badval=badval, maps=maps,
242 metricName = metricName, units = '%s' %('%'), **kwargs)
244 def run(self, dataSlice, slicePoint):
245 # Calculate dust-extinction limiting magnitudes for each visit.
246 filterlist = np.unique(dataSlice[self.filterCol])
247 m5Dust = np.zeros(len(dataSlice), float)
248 for f in filterlist:
249 match = np.where(dataSlice[self.filterCol] == f)
250 A_x = self.Ax1[f] * slicePoint['ebv']
251 m5Dust[match] = dataSlice[self.m5Col][match] - A_x
252 m5Dust[match] = np.where(m5Dust[match] > self.magCuts[f], m5Dust[match], -999)
253 idxs = np.where(m5Dust > -998)
254 if len(idxs[0]) == 0:
255 return self.badval
256 data = np.sort(dataSlice[idxs], order=self.mjdCol)
257 # SlicePoints ra/dec are always in radians - convert to degrees to calculate season
258 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol])
259 intSeasons = np.floor(seasons)
260 firstOfSeason, lastOfSeason = findSeasonEdges(seasons)
261 # Campaign length
262 camp = len(np.unique(intSeasons))
263 # Season length
264 seasonlengths = data[self.mjdCol][lastOfSeason] - data[self.mjdCol][firstOfSeason]
265 sea = np.median(seasonlengths)
266 # Convert to months
267 sea = sea / 30.0
268 # Campaign frequency / mean night separation
269 seasonMeans = np.zeros(len(firstOfSeason), float)
270 for i, (first, last) in enumerate(zip(firstOfSeason, lastOfSeason)):
271 n = data[self.nightCol][first:last+1]
272 deltaNights = np.diff(np.unique(n))
273 if len(deltaNights) > 0:
274 seasonMeans[i] = np.mean(deltaNights)
275 cad = np.mean(seasonMeans)
276 # Evaluate precision and accuracy for TDC
277 if sea == 0 or cad == 0 or camp == 0:
278 return self.badval
279 else:
280 accuracy = 0.06 * (self.seaNorm / sea) * \
281 (self.campNorm / camp)**(1.1)
282 precision = 4.0 * (cad / self.cadNorm)**(0.7) * \
283 (self.seaNorm/sea)**(0.3) * \
284 (self.campNorm / camp)**(0.6)
285 rate = 30. * (self.cadNorm / cad)**(0.4) * \
286 (sea / self.seaNorm)**(0.8) * \
287 (self.campNorm / camp)**(0.2)
288 return {'accuracy':accuracy, 'precision':precision, 'rate':rate,
289 'cadence':cad, 'season':sea, 'campaign':camp}
291 def reduceAccuracy(self, metricValue):
292 return metricValue['accuracy']
294 def reducePrecision(self, metricValue):
295 return metricValue['precision']
297 def reduceRate(self, metricValue):
298 return metricValue['rate']
300 def reduceCadence(self, metricValue):
301 return metricValue['cadence']
303 def reduceSeason(self, metricValue):
304 return metricValue['season']
306 def reduceCampaign(self, metricValue):
307 return metricValue['campaign']