Coverage for python/lsst/sims/featureScheduler/surveys/surveys.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
2from lsst.sims.featureScheduler.utils import (empty_observation, set_default_nside)
3import healpy as hp
4import matplotlib.pylab as plt
5from lsst.sims.featureScheduler.surveys import BaseMarkovDF_survey
6from lsst.sims.featureScheduler.utils import (int_binned_stat, int_rounded,
7 gnomonic_project_toxy, tsp_convex)
8import copy
9from lsst.sims.utils import _angularSeparation, _hpid2RaDec, _approx_RaDec2AltAz, hp_grow_argsort
10import warnings
12__all__ = ['Greedy_survey', 'Blob_survey']
15class Greedy_survey(BaseMarkovDF_survey):
16 """
17 Select pointings in a greedy way using a Markov Decision Process.
18 """
19 def __init__(self, basis_functions, basis_weights, filtername='r',
20 block_size=1, smoothing_kernel=None, nside=None,
21 dither=True, seed=42, ignore_obs=None, survey_name='',
22 nexp=2, exptime=30., detailers=None, camera='LSST'):
24 extra_features = {}
26 super(Greedy_survey, self).__init__(basis_functions=basis_functions,
27 basis_weights=basis_weights,
28 extra_features=extra_features,
29 smoothing_kernel=smoothing_kernel,
30 ignore_obs=ignore_obs,
31 nside=nside,
32 survey_name=survey_name, dither=dither,
33 detailers=detailers, camera=camera)
34 self.filtername = filtername
35 self.block_size = block_size
36 self.nexp = nexp
37 self.exptime = exptime
39 def generate_observations_rough(self, conditions):
40 """
41 Just point at the highest reward healpix
42 """
43 self.reward = self.calc_reward_function(conditions)
45 # Check if we need to spin the tesselation
46 if self.dither & (conditions.night != self.night):
47 self._spin_fields()
48 self.night = conditions.night.copy()
50 # Let's find the best N from the fields
51 order = np.argsort(self.reward)[::-1]
52 # Crop off any NaNs
53 order = order[~np.isnan(self.reward[order])]
55 iter = 0
56 while True:
57 best_hp = order[iter*self.block_size:(iter+1)*self.block_size]
58 best_fields = np.unique(self.hp2fields[best_hp])
59 observations = []
60 for field in best_fields:
61 obs = empty_observation()
62 obs['RA'] = self.fields['RA'][field]
63 obs['dec'] = self.fields['dec'][field]
64 obs['rotSkyPos'] = 0.
65 obs['filter'] = self.filtername
66 obs['nexp'] = self.nexp
67 obs['exptime'] = self.exptime
68 obs['field_id'] = -1
69 obs['note'] = self.survey_name
71 observations.append(obs)
72 break
73 iter += 1
74 if len(observations) > 0 or (iter+2)*self.block_size > len(order):
75 break
76 return observations
79class Blob_survey(Greedy_survey):
80 """Select observations in large, mostly contiguous, blobs.
82 Parameters
83 ----------
84 filtername1 : str ('r')
85 The filter to observe in.
86 filtername2 : str ('g')
87 The filter to pair with the first observation. If set to None, no pair
88 will be observed.
89 slew_approx : float (7.5)
90 The approximate slewtime between neerby fields (seconds). Used to calculate
91 how many observations can be taken in the desired time block.
92 nexp : int (2)
93 The number of exposures to take in a visit.
94 exp_dict : dict (None)
95 If set, should have keys of filtername and values of ints that are the nuber of exposures to take
96 per visit. For estimating block time, nexp is still used.
97 filter_change_approx : float (140.)
98 The approximate time it takes to change filters (seconds).
99 ideal_pair_time : float (22.)
100 The ideal time gap wanted between observations to the same pointing (minutes)
101 min_pair_time : float (15.)
102 The minimum acceptable pair time (minutes)
103 search_radius : float (30.)
104 The radius around the reward peak to look for additional potential pointings (degrees)
105 alt_max : float (85.)
106 The maximum altitude to include (degrees).
107 az_range : float (90.)
108 The range of azimuths to consider around the peak reward value (degrees).
109 flush_time : float (30.)
110 The time past the final expected exposure to flush the queue. Keeps observations
111 from lingering past when they should be executed. (minutes)
112 twilight_scale : bool (True)
113 Scale the block size to fill up to twilight. Set to False if running in twilight
114 in_twilight : bool (False)
115 Scale the block size to stay within twilight time.
116 check_scheduled : bool (True)
117 Check if there are scheduled observations and scale blob size to match
118 min_area : float (None)
119 If set, demand the reward function have an area of so many square degrees before executing
120 grow_blob : bool (True)
121 If True, try to grow the blob from the global maximum. Otherwise, just use a simple sort.
122 Simple sort will not constrain the blob to be contiguous.
123 """
124 def __init__(self, basis_functions, basis_weights,
125 filtername1='r', filtername2='g',
126 slew_approx=7.5, filter_change_approx=140.,
127 read_approx=2., exptime=30., nexp=2, nexp_dict=None,
128 ideal_pair_time=22., min_pair_time=15.,
129 search_radius=30., alt_max=85., az_range=90.,
130 flush_time=30.,
131 smoothing_kernel=None, nside=None,
132 dither=True, seed=42, ignore_obs=None,
133 survey_note='blob', detailers=None, camera='LSST',
134 twilight_scale=True, in_twilight=False, check_scheduled=True, min_area=None,
135 grow_blob=True):
137 if nside is None:
138 nside = set_default_nside()
140 super(Blob_survey, self).__init__(basis_functions=basis_functions,
141 basis_weights=basis_weights,
142 filtername=None,
143 block_size=0, smoothing_kernel=smoothing_kernel,
144 dither=dither, seed=seed, ignore_obs=ignore_obs,
145 nside=nside, detailers=detailers, camera=camera)
146 self.flush_time = flush_time/60./24. # convert to days
147 self.nexp = nexp
148 self.nexp_dict = nexp_dict
149 self.exptime = exptime
150 self.slew_approx = slew_approx
151 self.read_approx = read_approx
152 self.hpids = np.arange(hp.nside2npix(self.nside))
153 self.twilight_scale = twilight_scale
154 self.in_twilight = in_twilight
155 self.grow_blob = grow_blob
157 if self.twilight_scale & self.in_twilight:
158 warnings.warn('Both twilight_scale and in_twilight are set to True. That is probably wrong.')
160 self.min_area = min_area
161 self.check_scheduled = check_scheduled
162 # If we are taking pairs in same filter, no need to add filter change time.
163 if filtername1 == filtername2:
164 filter_change_approx = 0
165 # Compute the minimum time needed to observe a blob (or observe, then repeat.)
166 if filtername2 is not None:
167 self.time_needed = (min_pair_time*60.*2. + exptime + read_approx + filter_change_approx)/24./3600. # Days
168 else:
169 self.time_needed = (min_pair_time*60. + exptime + read_approx)/24./3600. # Days
170 self.filter_set = set(filtername1)
171 if filtername2 is None:
172 self.filter2_set = self.filter_set
173 else:
174 self.filter2_set = set(filtername2)
176 self.ra, self.dec = _hpid2RaDec(self.nside, self.hpids)
178 self.survey_note = survey_note
179 self.counter = 1 # start at 1, because 0 is default in empty observation
180 self.filtername1 = filtername1
181 self.filtername2 = filtername2
182 self.search_radius = np.radians(search_radius)
183 self.az_range = np.radians(az_range)
184 self.alt_max = np.radians(alt_max)
185 self.min_pair_time = min_pair_time
186 self.ideal_pair_time = ideal_pair_time
188 self.pixarea = hp.nside2pixarea(self.nside, degrees=True)
190 # If we are only using one filter, this could be useful
191 if (self.filtername2 is None) | (self.filtername1 == self.filtername2):
192 self.filtername = self.filtername1
194 def _check_feasibility(self, conditions):
195 """
196 Check if the survey is feasable in the current conditions.
197 """
198 for bf in self.basis_functions:
199 result = bf.check_feasibility(conditions)
200 if not result:
201 return result
203 # If we need to check that the reward function has enough area available
204 if self.min_area is not None:
205 reward = 0
206 for bf, weight in zip(self.basis_functions, self.basis_weights):
207 basis_value = bf(conditions)
208 reward += basis_value*weight
209 valid_pix = np.where(np.isnan(reward) == False)[0]
210 if np.size(valid_pix)*self.pixarea < self.min_area:
211 result = False
212 return result
214 def _set_block_size(self, conditions):
215 """
216 Update the block size if it's getting near a break point.
217 """
219 # If we are trying to get things done before twilight
220 if self.twilight_scale:
221 available_time = conditions.sun_n18_rising - conditions.mjd
222 available_time *= 24.*60. # to minutes
223 n_ideal_blocks = available_time / self.ideal_pair_time
224 else:
225 n_ideal_blocks = 4
227 # If we are trying to get things done before a scheduled simulation
228 if self.check_scheduled:
229 if len(conditions.scheduled_observations) > 0:
230 available_time = np.min(conditions.scheduled_observations) - conditions.mjd
231 available_time *= 24.*60. # to minutes
232 n_blocks = available_time / self.ideal_pair_time
233 if n_blocks < n_ideal_blocks:
234 n_ideal_blocks = n_blocks
236 # If we are trying to complete before twilight ends or the night ends
237 if self.in_twilight:
238 at1 = conditions.sun_n12_rising - conditions.mjd
239 at2 = conditions.sun_n18_setting - conditions.mjd
240 times = np.array([at1, at2])
241 times = times[np.where(times > 0)]
242 available_time = np.min(times)
243 available_time *= 24.*60. # to minutes
244 n_blocks = available_time / self.ideal_pair_time
245 if n_blocks < n_ideal_blocks:
246 n_ideal_blocks = n_blocks
248 if n_ideal_blocks >= 3:
249 self.nvisit_block = int(np.floor(self.ideal_pair_time*60. / (self.slew_approx + self.exptime +
250 self.read_approx*(self.nexp - 1))))
251 else:
252 # Now we can stretch or contract the block size to allocate the remainder time until twilight starts
253 # We can take the remaining time and try to do 1,2, or 3 blocks.
254 possible_times = available_time / np.arange(1, 4)
255 diff = np.abs(self.ideal_pair_time-possible_times)
256 best_block_time = np.max(possible_times[np.where(diff == np.min(diff))])
257 self.nvisit_block = int(np.floor(best_block_time*60. / (self.slew_approx + self.exptime +
258 self.read_approx*(self.nexp - 1))))
260 # The floor can set block to zero, make it possible to to just one
261 if self.nvisit_block <= 0:
262 self.nvisit_block = 1
264 def calc_reward_function(self, conditions):
265 """
266 """
267 # Set the number of observations we are going to try and take
268 self._set_block_size(conditions)
269 # Computing reward like usual with basis functions and weights
270 if self._check_feasibility(conditions):
271 self.reward = 0
272 indx = np.arange(hp.nside2npix(self.nside))
273 for bf, weight in zip(self.basis_functions, self.basis_weights):
274 basis_value = bf(conditions, indx=indx)
275 self.reward += basis_value*weight
276 # might be faster to pull this out into the feasabiliity check?
277 if self.smoothing_kernel is not None:
278 self.smooth_reward()
280 # Apply max altitude cut
281 too_high = np.where(int_rounded(conditions.alt) > int_rounded(self.alt_max))
282 self.reward[too_high] = np.nan
284 # Select healpixels within some radius of the max
285 # This is probably faster with a kd-tree.
287 max_hp = np.where(self.reward == np.nanmax(self.reward))[0]
288 if np.size(max_hp) > 0:
289 peak_reward = np.min(max_hp)
290 else:
291 # Everything is masked, so get out
292 return -np.inf
294 # Apply radius selection
295 dists = _angularSeparation(self.ra[peak_reward], self.dec[peak_reward], self.ra, self.dec)
296 out_hp = np.where(int_rounded(dists) > int_rounded(self.search_radius))
297 self.reward[out_hp] = np.nan
299 # Apply az cut
300 az_centered = conditions.az - conditions.az[peak_reward]
301 az_centered[np.where(az_centered < 0)] += 2.*np.pi
303 az_out = np.where((int_rounded(az_centered) > int_rounded(self.az_range/2.)) &
304 (int_rounded(az_centered) < int_rounded(2.*np.pi-self.az_range/2.)))
305 self.reward[az_out] = np.nan
306 else:
307 self.reward = -np.inf
308 self.reward_checked = True
309 return self.reward
311 def simple_order_sort(self):
312 """Fall back if we can't link contiguous blobs in the reward map
313 """
315 # Assuming reward has already been calcualted
317 potential_hp = np.where(~np.isnan(self.reward) == True)
319 # Note, using nanmax, so masked pixels might be included in the pointing.
320 # I guess I should document that it's not "NaN pixels can't be observed", but
321 # "non-NaN pixles CAN be observed", which probably is not intuitive.
322 ufields, reward_by_field = int_binned_stat(self.hp2fields[potential_hp],
323 self.reward[potential_hp],
324 statistic=np.nanmax)
325 # chop off any nans
326 not_nans = np.where(~np.isnan(reward_by_field) == True)
327 ufields = ufields[not_nans]
328 reward_by_field = reward_by_field[not_nans]
330 order = np.argsort(reward_by_field)
331 ufields = ufields[order][::-1][0:self.nvisit_block]
332 self.best_fields = ufields
334 def generate_observations_rough(self, conditions):
335 """
336 Find a good block of observations.
337 """
339 self.reward = self.calc_reward_function(conditions)
341 # Check if we need to spin the tesselation
342 if self.dither & (conditions.night != self.night):
343 self._spin_fields()
344 self.night = conditions.night.copy()
346 if self.grow_blob:
347 # Note, returns highest first
348 ordered_hp = hp_grow_argsort(self.reward)
349 ordered_fields = self.hp2fields[ordered_hp]
350 orig_order = np.arange(ordered_fields.size)
351 # Remove duplicate field pointings
352 _u_of, u_indx = np.unique(ordered_fields, return_index=True)
353 new_order = np.argsort(orig_order[u_indx])
354 best_fields = ordered_fields[u_indx[new_order]]
356 if np.size(best_fields) < self.nvisit_block:
357 # Let's fall back to the simple sort
358 self.simple_order_sort()
359 else:
360 self.best_fields = best_fields[0:self.nvisit_block]
361 else:
362 self.simple_order_sort()
364 if len(self.best_fields) == 0:
365 # everything was nans, or self.nvisit_block was zero
366 return []
368 # Let's find the alt, az coords of the points (right now, hopefully doesn't change much in time block)
369 pointing_alt, pointing_az = _approx_RaDec2AltAz(self.fields['RA'][self.best_fields],
370 self.fields['dec'][self.best_fields],
371 conditions.site.latitude_rad,
372 conditions.site.longitude_rad,
373 conditions.mjd,
374 lmst=conditions.lmst)
376 # Let's find a good spot to project the points to a plane
377 mid_alt = (np.max(pointing_alt) - np.min(pointing_alt))/2.
379 # Code snippet from MAF for computing mean of angle accounting for wrap around
380 # XXX-TODO: Maybe move this to sims_utils as a generally useful snippet.
381 x = np.cos(pointing_az)
382 y = np.sin(pointing_az)
383 meanx = np.mean(x)
384 meany = np.mean(y)
385 angle = np.arctan2(meany, meanx)
386 radius = np.sqrt(meanx**2 + meany**2)
387 mid_az = angle % (2.*np.pi)
388 if radius < 0.1:
389 mid_az = np.pi
391 # Project the alt,az coordinates to a plane. Could consider scaling things to represent
392 # time between points rather than angular distance.
393 pointing_x, pointing_y = gnomonic_project_toxy(pointing_az, pointing_alt, mid_az, mid_alt)
394 # Round off positions so that we ensure identical cross-platform performance
395 scale = 1e6
396 pointing_x = np.round(pointing_x*scale).astype(int)
397 pointing_y = np.round(pointing_y*scale).astype(int)
398 # Now I have a bunch of x,y pointings. Drop into TSP solver to get an effiencent route
399 towns = np.vstack((pointing_x, pointing_y)).T
400 # Leaving optimize=False for speed. The optimization step doesn't usually improve much.
401 better_order = tsp_convex(towns, optimize=False)
402 # XXX-TODO: Could try to roll better_order to start at the nearest/fastest slew from current position.
403 observations = []
404 counter2 = 0
405 approx_end_time = np.size(better_order)*(self.slew_approx + self.exptime +
406 self.read_approx*(self.nexp - 1))
407 flush_time = conditions.mjd + approx_end_time/3600./24. + self.flush_time
408 for i, indx in enumerate(better_order):
409 field = self.best_fields[indx]
410 obs = empty_observation()
411 obs['RA'] = self.fields['RA'][field]
412 obs['dec'] = self.fields['dec'][field]
413 obs['rotSkyPos'] = 0.
414 obs['filter'] = self.filtername1
415 if self.nexp_dict is None:
416 obs['nexp'] = self.nexp
417 else:
418 obs['nexp'] = self.nexp_dict[self.filtername1]
419 obs['exptime'] = self.exptime
420 obs['field_id'] = -1
421 obs['note'] = '%s' % (self.survey_note)
422 obs['block_id'] = self.counter
423 obs['flush_by_mjd'] = flush_time
424 # Add the mjd for debugging
425 # obs['mjd'] = conditions.mjd
426 # XXX temp debugging line
427 obs['survey_id'] = i
428 observations.append(obs)
429 counter2 += 1
431 result = observations
432 return result