Coverage for python/lsst/sims/featureScheduler/surveys/surveys.py : 8%

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