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