Coverage for python/lsst/sims/maf/metrics/visitGroupsMetric.py : 9%

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
1from builtins import zip
2# Example of more complex metric
3# Takes multiple columns of data (although 'night' could be calculable from 'expmjd')
4# Returns variable length array of data
5# Uses multiple reduce functions
7import numpy as np
8from .baseMetric import BaseMetric
10__all__ = ['VisitGroupsMetric', 'PairFractionMetric']
13class PairFractionMetric(BaseMetric):
14 """What fraction of observations are part of a pair.
16 Note, an observation can be a member of more than one "pair". For example,
17 t=[0, 5, 30], all observations would be considered part of a pair because they
18 all have an observation within the given window to pair with (the observation at t=30
19 pairs twice).
21 Parameters
22 ----------
23 minGap : float, opt
24 Minimum time to consider something part of a pair (minutes). Default 15.
25 maxGap : float, opt
26 Maximum time to consider something part of a pair (minutes). Default 90.
27 """
28 def __init__(self, mjdCol='observationStartMJD', metricName='PairFraction',
29 minGap=15., maxGap=90., **kwargs):
30 self.mjdCol = mjdCol
31 self.minGap = minGap/60./24.
32 self.maxGap = maxGap/60./24.
33 units = ''
34 super(PairFractionMetric, self).__init__(col=[mjdCol], metricName=metricName, units=units, **kwargs)
36 def run(self, dataSlice, slicePoint=None):
37 nobs = np.size(dataSlice[self.mjdCol])
38 times = np.sort(dataSlice[self.mjdCol])
40 # Check which ones have a forard match
41 t_plus = times + self.maxGap
42 t_minus = times + self.minGap
43 ind1 = np.searchsorted(times, t_plus)
44 ind2 = np.searchsorted(times, t_minus)
45 # If ind1 and ind2 are the same, there is no pairable image for that exposure
46 diff1 = ind1 - ind2
48 # Check which have a back match
49 t_plus = times - self.maxGap
50 t_minus = times - self.minGap
51 ind1 = np.searchsorted(times, t_plus)
52 ind2 = np.searchsorted(times, t_minus)
54 diff2 = ind1 - ind2
56 # The exposure has a pair ahead or behind
57 is_paired = np.where((diff1 != 0) | (diff2 != 0))[0]
58 result = np.size(is_paired)/float(nobs)
59 return result
62class VisitGroupsMetric(BaseMetric):
63 """Count the number of visits per night within deltaTmin and deltaTmax."""
64 def __init__(self, timeCol='observationStartMJD', nightsCol='night', metricName='VisitGroups',
65 deltaTmin=15.0/60.0/24.0, deltaTmax=90.0/60.0/24.0, minNVisits=2, window=30, minNNights=3,
66 **kwargs):
67 """
68 Instantiate metric.
70 'timeCol' = column with the time of the visit (default expmjd),
71 'nightsCol' = column with the night of the visit (default night),
72 'deltaTmin' = minimum time of window: units are days (default 15 min),
73 'deltaTmax' = maximum time of window: units are days (default 90 min),
74 'minNVisits' = the minimum number of visits within a night (with spacing between deltaTmin/max
75 from any other visit) required,
76 'window' = the number of nights to consider within a window (for reduce methods),
77 'minNNights' = the minimum required number of nights within window to make a full 'group'.
78 """
79 self.times = timeCol
80 self.nights = nightsCol
81 eps = 1e-10
82 self.deltaTmin = float(deltaTmin) - eps
83 self.deltaTmax = float(deltaTmax)
84 self.minNVisits = int(minNVisits)
85 self.window = int(window)
86 self.minNNights = int(minNNights)
87 super(VisitGroupsMetric, self).__init__(col=[self.times, self.nights], metricName=metricName, **kwargs)
88 self.reduceOrder = {'Median':0, 'NNightsWithNVisits':1, 'NVisitsInWindow':2,
89 'NNightsInWindow':3, 'NLunations':4, 'MaxSeqLunations':5}
90 self.comment = 'Evaluation of the number of visits within a night, with separations between '
91 self.comment += 'tMin %.1f and tMax %.1f minutes.' %(self.deltaTmin*24.0*60., self.deltaTmax*24.0*60.)
92 self.comment += 'Groups of visits use a minimum number of visits per night of %d, ' %(self.minNVisits)
93 self.comment += 'and minimum number of nights of %d.' %(self.minNNights)
94 self.comment += 'Two visits within this interval would count as 2. '
95 self.comment += 'Visits closer than tMin, paired with visits that do fall within tMin/tMax, '
96 self.comment += 'count as half visits. <br>'
97 self.comment += 'VisitsGroups_Median calculates the median number of visits between tMin/tMax for '
98 self.comment += 'all nights. <br>'
99 self.comment += 'VisitGroups_NNightsWithNNights calculates the number of nights that have at '
100 self.comment += 'least %d visits. <br>' %(self.minNVisits)
101 self.comment += 'VisitGroups_NVisitsInWindow calculates the max number of visits within a window of '
102 self.comment += '%d days. <br>' %(self.window)
103 self.comment += 'VisitGroups_NNightsInWindow calculates the max number of nights that have more '
104 self.comment += 'than %d visits within %d days. <br>' %(self.minNVisits, self.window)
105 self.comment += 'VisitGroups_NLunations calculates the number of lunations (30 days) that have '
106 self.comment += 'at least one group of more than %d nights with more than %d visits, within '\
107 %(self.minNNights, self.minNVisits)
108 self.comment += '%d days. <br>' %(self.window)
109 self.comment += 'VisitGroups_MaxSeqLunations calculates the maximum sequential lunations that have '
110 self.comment += 'at least one "group". <br>'
112 def run(self, dataSlice, slicePoint=None):
113 """
114 Return a dictionary of:
115 the number of visits within a night (within delta tmin/tmax of another visit),
116 and the nights with visits > minNVisits.
117 Count two visits which are within tmin of each other, but which have another visit
118 within tmin/tmax interval, as one and a half (instead of two).
120 So for example: 4 visits, where 1, 2, 3 were all within deltaTMax of each other, and 4 was later but
121 within deltaTmax of visit 3 -- would give you 4 visits. If visit 1 and 2 were closer together
122 than deltaTmin, the two would be counted as 1.5 visits together (if only 1 and 2 existed,
123 then there would be 0 visits as none would be within the qualifying time interval).
124 """
125 uniquenights = np.unique(dataSlice[self.nights])
126 nights = []
127 visitNum = []
128 # Find the nights with visits within deltaTmin/max of one another and count the number of visits
129 for n in uniquenights:
130 condition = (dataSlice[self.nights] == n)
131 times = np.sort(dataSlice[self.times][condition])
132 nvisits = 0
133 ntooclose = 0
134 # Calculate difference between each visit and time of previous visit (tnext- tnow)
135 timediff = np.diff(times)
136 timegood = np.where((timediff <= self.deltaTmax) & (timediff >= self.deltaTmin), True, False)
137 timetooclose = np.where(timediff < self.deltaTmin, True, False)
138 if len(timegood)>1:
139 # Count visits for all but last index in timediff
140 for tg1, ttc1, tg2, ttc2 in zip(timegood[:-1], timetooclose[:-1], timegood[1:], timetooclose[1:]):
141 if tg1:
142 nvisits += 1
143 if not tg2:
144 nvisits += 1
145 if ttc1:
146 ntooclose += 1
147 if not tg2 and not ttc2:
148 ntooclose += 1
149 # Take care of last timediff
150 if timegood[-1]:
151 nvisits += 2 #close out visit sequence
152 if timetooclose[-1]:
153 ntooclose += 1
154 if not timegood[-2] and not timetooclose[-2]:
155 ntooclose += 1
156 else:
157 if timegood:
158 nvisits += 2
159 # Count up all visits for night.
160 if nvisits > 0:
161 nvisits = nvisits + ntooclose/2.0
162 visitNum.append(nvisits)
163 nights.append(n)
164 # Convert to numpy arrays.
165 visitNum = np.array(visitNum)
166 nights = np.array(nights)
167 metricval = {'visits':visitNum, 'nights':nights}
168 if len(visitNum) == 0:
169 return self.badval
170 return metricval
172 def reduceMedian(self, metricval):
173 """Reduce to median number of visits per night."""
174 return np.median(metricval['visits'])
176 def reduceNNightsWithNVisits(self, metricval):
177 """Reduce to total number of nights with more than 'minNVisits' visits."""
178 condition = (metricval['visits'] >= self.minNVisits)
179 return len(metricval['visits'][condition])
181 def _inWindow(self, visits, nights, night, window, minNVisits):
182 condition = ((nights >= night) & (nights < night+window))
183 condition2 = (visits[condition] >= minNVisits)
184 return visits[condition][condition2], nights[condition][condition2]
186 def reduceNVisitsInWindow(self, metricval):
187 """Reduce to max number of total visits on all nights with more than minNVisits,
188 within any 'window' (default=30 nights)."""
189 maxnvisits = 0
190 for n in metricval['nights']:
191 vw, nw = self._inWindow(metricval['visits'], metricval['nights'], n, self.window, self.minNVisits)
192 maxnvisits = max((vw.sum(), maxnvisits))
193 return maxnvisits
195 def reduceNNightsInWindow(self, metricval):
196 """Reduce to max number of nights with more than minNVisits, within 'window' over all windows."""
197 maxnights = 0
198 for n in metricval['nights']:
199 vw, nw = self._inWindow(metricval['visits'], metricval['nights'], n, self.window, self.minNVisits)
200 maxnights = max(len(nw), maxnights)
201 return maxnights
203 def _inLunation(self, visits, nights, lunationStart, lunationLength):
204 condition = ((nights >= lunationStart) & (nights < lunationStart+lunationLength))
205 return visits[condition], nights[condition]
207 def reduceNLunations(self, metricval):
208 """Reduce to number of lunations (unique 30 day windows) that contain at least one 'group':
209 a set of more than minNVisits per night, with more than minNNights of visits within 'window' time period.
210 """
211 lunationLength = 30
212 lunations = np.arange(metricval['nights'][0], metricval['nights'][-1]+lunationLength/2.0, lunationLength)
213 nLunations = 0
214 for l in lunations:
215 # Find visits within lunation.
216 vl, nl = self._inLunation(metricval['visits'], metricval['nights'], l, lunationLength)
217 for n in nl:
218 # Find visits which are in groups within the lunation.
219 vw, nw = self._inWindow(vl, nl, n, self.window, self.minNVisits)
220 if len(nw) >= self.minNNights:
221 nLunations += 1
222 break
223 return nLunations
225 def reduceMaxSeqLunations(self, metricval):
226 """Count the max number of sequential lunations (unique 30 day windows) that contain at least one 'group':
227 a set of more than minNVisits per night, with more than minNNights of visits within 'window' time period.
228 """
229 lunationLength = 30
230 lunations = np.arange(metricval['nights'][0], metricval['nights'][-1]+lunationLength/2.0, lunationLength)
231 maxSequence = 0
232 curSequence = 0
233 inSeq = False
234 for l in lunations:
235 # Find visits within lunation.
236 vl, nl = self._inLunation(metricval['visits'], metricval['nights'], l, lunationLength)
237 # If no visits this lunation:
238 if len(vl) == 0:
239 inSeq = False
240 maxSequence = max(maxSequence, curSequence)
241 curSequence = 0
242 # Else, look to see if groups can be made from the visits.
243 for n in nl:
244 # Find visits which are in groups within the lunation.
245 vw, nw = self._inWindow(vl, nl, n, self.window, self.minNVisits)
246 # If there was a group within this lunation:
247 if len(nw) >= self.minNNights:
248 curSequence += 1
249 inSeq = True
250 break
251 # Otherwise we're not in a sequence (anymore, or still).
252 else:
253 inSeq = False
254 maxSequence = max(maxSequence, curSequence)
255 curSequence = 0
256 # Pick up last sequence if were in a sequence at last lunation.
257 maxSequence = max(maxSequence, curSequence)
258 return maxSequence