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 filter_change_approx : float (140.)
93 The approximate time it takes to change filters (seconds).
94 ideal_pair_time : float (22.)
95 The ideal time gap wanted between observations to the same pointing (minutes)
96 min_pair_time : float (15.)
97 The minimum acceptable pair time (minutes)
98 search_radius : float (30.)
99 The radius around the reward peak to look for additional potential pointings (degrees)
100 alt_max : float (85.)
101 The maximum altitude to include (degrees).
102 az_range : float (90.)
103 The range of azimuths to consider around the peak reward value (degrees).
104 flush_time : float (30.)
105 The time past the final expected exposure to flush the queue. Keeps observations
106 from lingering past when they should be executed. (minutes)
107 twilight_scale : bool (True)
108 Scale the block size to fill up to twilight. Set to False if running in twilight
109 in_twilight : bool (False)
110 Scale the block size to stay within twilight time.
111 check_scheduled : bool (True)
112 Check if there are scheduled observations and scale blob size to match
113 min_area : float (None)
114 If set, demand the reward function have an area of so many square degrees before executing
115 """
116 def __init__(self, basis_functions, basis_weights,
117 filtername1='r', filtername2='g',
118 slew_approx=7.5, filter_change_approx=140.,
119 read_approx=2., exptime=30., nexp=2,
120 ideal_pair_time=22., min_pair_time=15.,
121 search_radius=30., alt_max=85., az_range=90.,
122 flush_time=30.,
123 smoothing_kernel=None, nside=None,
124 dither=True, seed=42, ignore_obs=None,
125 survey_note='blob', detailers=None, camera='LSST',
126 twilight_scale=True, in_twilight=False, check_scheduled=True, min_area=None):
128 if nside is None:
129 nside = set_default_nside()
131 super(Blob_survey, self).__init__(basis_functions=basis_functions,
132 basis_weights=basis_weights,
133 filtername=None,
134 block_size=0, smoothing_kernel=smoothing_kernel,
135 dither=dither, seed=seed, ignore_obs=ignore_obs,
136 nside=nside, detailers=detailers, camera=camera)
137 self.flush_time = flush_time/60./24. # convert to days
138 self.nexp = nexp
139 self.exptime = exptime
140 self.slew_approx = slew_approx
141 self.read_approx = read_approx
142 self.hpids = np.arange(hp.nside2npix(self.nside))
143 self.twilight_scale = twilight_scale
144 self.in_twilight = in_twilight
146 if self.twilight_scale & self.in_twilight:
147 warnings.warn('Both twilight_scale and in_twilight are set to True. That is probably wrong.')
149 self.min_area = min_area
150 self.check_scheduled = check_scheduled
151 # If we are taking pairs in same filter, no need to add filter change time.
152 if filtername1 == filtername2:
153 filter_change_approx = 0
154 # Compute the minimum time needed to observe a blob (or observe, then repeat.)
155 if filtername2 is not None:
156 self.time_needed = (min_pair_time*60.*2. + exptime + read_approx + filter_change_approx)/24./3600. # Days
157 else:
158 self.time_needed = (min_pair_time*60. + exptime + read_approx)/24./3600. # Days
159 self.filter_set = set(filtername1)
160 if filtername2 is None:
161 self.filter2_set = self.filter_set
162 else:
163 self.filter2_set = set(filtername2)
165 self.ra, self.dec = _hpid2RaDec(self.nside, self.hpids)
167 self.survey_note = survey_note
168 self.counter = 1 # start at 1, because 0 is default in empty observation
169 self.filtername1 = filtername1
170 self.filtername2 = filtername2
171 self.search_radius = np.radians(search_radius)
172 self.az_range = np.radians(az_range)
173 self.alt_max = np.radians(alt_max)
174 self.min_pair_time = min_pair_time
175 self.ideal_pair_time = ideal_pair_time
177 self.pixarea = hp.nside2pixarea(self.nside, degrees=True)
179 # If we are only using one filter, this could be useful
180 if (self.filtername2 is None) | (self.filtername1 == self.filtername2):
181 self.filtername = self.filtername1
183 def _check_feasibility(self, conditions):
184 """
185 Check if the survey is feasable in the current conditions.
186 """
187 for bf in self.basis_functions:
188 result = bf.check_feasibility(conditions)
189 if not result:
190 return result
192 # If we need to check that the reward function has enough area available
193 if self.min_area is not None:
194 reward = 0
195 for bf, weight in zip(self.basis_functions, self.basis_weights):
196 basis_value = bf(conditions)
197 reward += basis_value*weight
198 valid_pix = np.where(np.isnan(reward) == False)[0]
199 if np.size(valid_pix)*self.pixarea < self.min_area:
200 result = False
201 return result
203 def _set_block_size(self, conditions):
204 """
205 Update the block size if it's getting near a break point.
206 """
208 # If we are trying to get things done before twilight
209 if self.twilight_scale:
210 available_time = conditions.sun_n18_rising - conditions.mjd
211 available_time *= 24.*60. # to minutes
212 n_ideal_blocks = available_time / self.ideal_pair_time
213 else:
214 n_ideal_blocks = 4
216 # If we are trying to get things done before a scheduled simulation
217 if self.check_scheduled:
218 if len(conditions.scheduled_observations) > 0:
219 available_time = np.min(conditions.scheduled_observations) - conditions.mjd
220 available_time *= 24.*60. # to minutes
221 n_blocks = available_time / self.ideal_pair_time
222 if n_blocks < n_ideal_blocks:
223 n_ideal_blocks = n_blocks
225 # If we are trying to complete before twilight ends or the night ends
226 if self.in_twilight:
227 at1 = conditions.sun_n12_rising - conditions.mjd
228 at2 = conditions.sun_n18_setting - conditions.mjd
229 times = np.array([at1, at2])
230 times = times[np.where(times > 0)]
231 available_time = np.min(times)
232 available_time *= 24.*60. # to minutes
233 n_blocks = available_time / self.ideal_pair_time
234 if n_blocks < n_ideal_blocks:
235 n_ideal_blocks = n_blocks
237 if n_ideal_blocks >= 3:
238 self.nvisit_block = int(np.floor(self.ideal_pair_time*60. / (self.slew_approx + self.exptime +
239 self.read_approx*(self.nexp - 1))))
240 else:
241 # Now we can stretch or contract the block size to allocate the remainder time until twilight starts
242 # We can take the remaining time and try to do 1,2, or 3 blocks.
243 possible_times = available_time / np.arange(1, 4)
244 diff = np.abs(self.ideal_pair_time-possible_times)
245 best_block_time = np.max(possible_times[np.where(diff == np.min(diff))])
246 self.nvisit_block = int(np.floor(best_block_time*60. / (self.slew_approx + self.exptime +
247 self.read_approx*(self.nexp - 1))))
249 # The floor can set block to zero, make it possible to to just one
250 if self.nvisit_block <= 0:
251 self.nvisit_block = 1
253 def calc_reward_function(self, conditions):
254 """
255 """
256 # Set the number of observations we are going to try and take
257 self._set_block_size(conditions)
258 # Computing reward like usual with basis functions and weights
259 if self._check_feasibility(conditions):
260 self.reward = 0
261 indx = np.arange(hp.nside2npix(self.nside))
262 for bf, weight in zip(self.basis_functions, self.basis_weights):
263 basis_value = bf(conditions, indx=indx)
264 self.reward += basis_value*weight
265 # might be faster to pull this out into the feasabiliity check?
266 if self.smoothing_kernel is not None:
267 self.smooth_reward()
269 # Apply max altitude cut
270 too_high = np.where(int_rounded(conditions.alt) > int_rounded(self.alt_max))
271 self.reward[too_high] = np.nan
273 # Select healpixels within some radius of the max
274 # This is probably faster with a kd-tree.
276 max_hp = np.where(self.reward == np.nanmax(self.reward))[0]
277 if np.size(max_hp) > 0:
278 peak_reward = np.min(max_hp)
279 else:
280 # Everything is masked, so get out
281 return -np.inf
283 # Apply radius selection
284 dists = _angularSeparation(self.ra[peak_reward], self.dec[peak_reward], self.ra, self.dec)
285 out_hp = np.where(int_rounded(dists) > int_rounded(self.search_radius))
286 self.reward[out_hp] = np.nan
288 # Apply az cut
289 az_centered = conditions.az - conditions.az[peak_reward]
290 az_centered[np.where(az_centered < 0)] += 2.*np.pi
292 az_out = np.where((int_rounded(az_centered) > int_rounded(self.az_range/2.)) &
293 (int_rounded(az_centered) < int_rounded(2.*np.pi-self.az_range/2.)))
294 self.reward[az_out] = np.nan
295 else:
296 self.reward = -np.inf
297 self.reward_checked = True
298 return self.reward
300 def simple_order_sort(self):
301 """Fall back if we can't link contiguous blobs in the reward map
302 """
304 # Assuming reward has already been calcualted
306 potential_hp = np.where(~np.isnan(self.reward) == True)
308 # Note, using nanmax, so masked pixels might be included in the pointing.
309 # I guess I should document that it's not "NaN pixels can't be observed", but
310 # "non-NaN pixles CAN be observed", which probably is not intuitive.
311 ufields, reward_by_field = int_binned_stat(self.hp2fields[potential_hp],
312 self.reward[potential_hp],
313 statistic=np.nanmax)
314 # chop off any nans
315 not_nans = np.where(~np.isnan(reward_by_field) == True)
316 ufields = ufields[not_nans]
317 reward_by_field = reward_by_field[not_nans]
319 order = np.argsort(reward_by_field)
320 ufields = ufields[order][::-1][0:self.nvisit_block]
321 self.best_fields = ufields
323 def generate_observations_rough(self, conditions):
324 """
325 Find a good block of observations.
326 """
328 self.reward = self.calc_reward_function(conditions)
330 # Check if we need to spin the tesselation
331 if self.dither & (conditions.night != self.night):
332 self._spin_fields()
333 self.night = conditions.night.copy()
335 # Note, returns highest first
336 ordered_hp = hp_grow_argsort(self.reward)
337 ordered_fields = self.hp2fields[ordered_hp]
338 orig_order = np.arange(ordered_fields.size)
339 # Remove duplicate field pointings
340 _u_of, u_indx = np.unique(ordered_fields, return_index=True)
341 new_order = np.argsort(orig_order[u_indx])
342 best_fields = ordered_fields[u_indx[new_order]]
344 if np.size(best_fields) < self.nvisit_block:
345 # Let's fall back to the simple sort
346 self.simple_order_sort()
347 else:
348 self.best_fields = best_fields[0:self.nvisit_block]
350 if len(self.best_fields) == 0:
351 # everything was nans, or self.nvisit_block was zero
352 return []
354 # Let's find the alt, az coords of the points (right now, hopefully doesn't change much in time block)
355 pointing_alt, pointing_az = _approx_RaDec2AltAz(self.fields['RA'][self.best_fields],
356 self.fields['dec'][self.best_fields],
357 conditions.site.latitude_rad,
358 conditions.site.longitude_rad,
359 conditions.mjd,
360 lmst=conditions.lmst)
362 # Let's find a good spot to project the points to a plane
363 mid_alt = (np.max(pointing_alt) - np.min(pointing_alt))/2.
365 # Code snippet from MAF for computing mean of angle accounting for wrap around
366 # XXX-TODO: Maybe move this to sims_utils as a generally useful snippet.
367 x = np.cos(pointing_az)
368 y = np.sin(pointing_az)
369 meanx = np.mean(x)
370 meany = np.mean(y)
371 angle = np.arctan2(meany, meanx)
372 radius = np.sqrt(meanx**2 + meany**2)
373 mid_az = angle % (2.*np.pi)
374 if radius < 0.1:
375 mid_az = np.pi
377 # Project the alt,az coordinates to a plane. Could consider scaling things to represent
378 # time between points rather than angular distance.
379 pointing_x, pointing_y = gnomonic_project_toxy(pointing_az, pointing_alt, mid_az, mid_alt)
380 # Round off positions so that we ensure identical cross-platform performance
381 scale = 1e6
382 pointing_x = np.round(pointing_x*scale).astype(int)
383 pointing_y = np.round(pointing_y*scale).astype(int)
384 # Now I have a bunch of x,y pointings. Drop into TSP solver to get an effiencent route
385 towns = np.vstack((pointing_x, pointing_y)).T
386 # Leaving optimize=False for speed. The optimization step doesn't usually improve much.
387 better_order = tsp_convex(towns, optimize=False)
388 # XXX-TODO: Could try to roll better_order to start at the nearest/fastest slew from current position.
389 observations = []
390 counter2 = 0
391 approx_end_time = np.size(better_order)*(self.slew_approx + self.exptime +
392 self.read_approx*(self.nexp - 1))
393 flush_time = conditions.mjd + approx_end_time/3600./24. + self.flush_time
394 for i, indx in enumerate(better_order):
395 field = self.best_fields[indx]
396 obs = empty_observation()
397 obs['RA'] = self.fields['RA'][field]
398 obs['dec'] = self.fields['dec'][field]
399 obs['rotSkyPos'] = 0.
400 obs['filter'] = self.filtername1
401 obs['nexp'] = self.nexp
402 obs['exptime'] = self.exptime
403 obs['field_id'] = -1
404 obs['note'] = '%s' % (self.survey_note)
405 obs['block_id'] = self.counter
406 obs['flush_by_mjd'] = flush_time
407 # Add the mjd for debugging
408 # obs['mjd'] = conditions.mjd
409 # XXX temp debugging line
410 obs['survey_id'] = i
411 observations.append(obs)
412 counter2 += 1
414 result = observations
415 return result