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
4from lsst.sims.featureScheduler.surveys import BaseMarkovDF_survey
5from lsst.sims.featureScheduler.utils import (int_binned_stat, int_rounded,
6 gnomonic_project_toxy, tsp_convex)
7import copy
8from lsst.sims.utils import _angularSeparation, _hpid2RaDec, _approx_RaDec2AltAz
10__all__ = ['Greedy_survey', 'Blob_survey']
13class Greedy_survey(BaseMarkovDF_survey):
14 """
15 Select pointings in a greedy way using a Markov Decision Process.
16 """
17 def __init__(self, basis_functions, basis_weights, filtername='r',
18 block_size=1, smoothing_kernel=None, nside=None,
19 dither=True, seed=42, ignore_obs=None, survey_name='',
20 nexp=2, exptime=30., detailers=None, camera='LSST'):
22 extra_features = {}
24 super(Greedy_survey, self).__init__(basis_functions=basis_functions,
25 basis_weights=basis_weights,
26 extra_features=extra_features,
27 smoothing_kernel=smoothing_kernel,
28 ignore_obs=ignore_obs,
29 nside=nside,
30 survey_name=survey_name, dither=dither,
31 detailers=detailers, camera=camera)
32 self.filtername = filtername
33 self.block_size = block_size
34 self.nexp = nexp
35 self.exptime = exptime
37 def generate_observations_rough(self, conditions):
38 """
39 Just point at the highest reward healpix
40 """
41 self.reward = self.calc_reward_function(conditions)
43 # Check if we need to spin the tesselation
44 if self.dither & (conditions.night != self.night):
45 self._spin_fields()
46 self.night = conditions.night.copy()
48 # Let's find the best N from the fields
49 order = np.argsort(self.reward)[::-1]
50 # Crop off any NaNs
51 order = order[~np.isnan(self.reward[order])]
53 iter = 0
54 while True:
55 best_hp = order[iter*self.block_size:(iter+1)*self.block_size]
56 best_fields = np.unique(self.hp2fields[best_hp])
57 observations = []
58 for field in best_fields:
59 obs = empty_observation()
60 obs['RA'] = self.fields['RA'][field]
61 obs['dec'] = self.fields['dec'][field]
62 obs['rotSkyPos'] = 0.
63 obs['filter'] = self.filtername
64 obs['nexp'] = self.nexp
65 obs['exptime'] = self.exptime
66 obs['field_id'] = -1
67 obs['note'] = self.survey_name
69 observations.append(obs)
70 break
71 iter += 1
72 if len(observations) > 0 or (iter+2)*self.block_size > len(order):
73 break
74 return observations
77class Blob_survey(Greedy_survey):
78 """Select observations in large, mostly contiguous, blobs.
80 Parameters
81 ----------
82 filtername1 : str ('r')
83 The filter to observe in.
84 filtername2 : str ('g')
85 The filter to pair with the first observation. If set to None, no pair
86 will be observed.
87 slew_approx : float (7.5)
88 The approximate slewtime between neerby fields (seconds). Used to calculate
89 how many observations can be taken in the desired time block.
90 filter_change_approx : float (140.)
91 The approximate time it takes to change filters (seconds).
92 ideal_pair_time : float (22.)
93 The ideal time gap wanted between observations to the same pointing (minutes)
94 min_pair_time : float (15.)
95 The minimum acceptable pair time (minutes)
96 search_radius : float (30.)
97 The radius around the reward peak to look for additional potential pointings (degrees)
98 alt_max : float (85.)
99 The maximum altitude to include (degrees).
100 az_range : float (90.)
101 The range of azimuths to consider around the peak reward value (degrees).
102 flush_time : float (30.)
103 The time past the final expected exposure to flush the queue. Keeps observations
104 from lingering past when they should be executed. (minutes)
105 twilight_scale : bool (True)
106 Scale the block size to fill up to twilight. Set to False if running in twilight
107 min_area : float (None)
108 If set, demand the reward function have an area of so many square degrees before executing
109 """
110 def __init__(self, basis_functions, basis_weights,
111 filtername1='r', filtername2='g',
112 slew_approx=7.5, filter_change_approx=140.,
113 read_approx=2., exptime=30., nexp=2,
114 ideal_pair_time=22., min_pair_time=15.,
115 search_radius=30., alt_max=85., az_range=90.,
116 flush_time=30.,
117 smoothing_kernel=None, nside=None,
118 dither=True, seed=42, ignore_obs=None,
119 survey_note='blob', detailers=None, camera='LSST',
120 twilight_scale=True, min_area=None):
122 if nside is None:
123 nside = set_default_nside()
125 super(Blob_survey, self).__init__(basis_functions=basis_functions,
126 basis_weights=basis_weights,
127 filtername=None,
128 block_size=0, smoothing_kernel=smoothing_kernel,
129 dither=dither, seed=seed, ignore_obs=ignore_obs,
130 nside=nside, detailers=detailers, camera=camera)
131 self.flush_time = flush_time/60./24. # convert to days
132 self.nexp = nexp
133 self.exptime = exptime
134 self.slew_approx = slew_approx
135 self.read_approx = read_approx
136 self.hpids = np.arange(hp.nside2npix(self.nside))
137 self.twilight_scale = twilight_scale
138 self.min_area = min_area
139 # If we are taking pairs in same filter, no need to add filter change time.
140 if filtername1 == filtername2:
141 filter_change_approx = 0
142 # Compute the minimum time needed to observe a blob (or observe, then repeat.)
143 if filtername2 is not None:
144 self.time_needed = (min_pair_time*60.*2. + exptime + read_approx + filter_change_approx)/24./3600. # Days
145 else:
146 self.time_needed = (min_pair_time*60. + exptime + read_approx)/24./3600. # Days
147 self.filter_set = set(filtername1)
148 if filtername2 is None:
149 self.filter2_set = self.filter_set
150 else:
151 self.filter2_set = set(filtername2)
153 self.ra, self.dec = _hpid2RaDec(self.nside, self.hpids)
155 self.survey_note = survey_note
156 self.counter = 1 # start at 1, because 0 is default in empty observation
157 self.filtername1 = filtername1
158 self.filtername2 = filtername2
159 self.search_radius = np.radians(search_radius)
160 self.az_range = np.radians(az_range)
161 self.alt_max = np.radians(alt_max)
162 self.min_pair_time = min_pair_time
163 self.ideal_pair_time = ideal_pair_time
165 self.pixarea = hp.nside2pixarea(self.nside, degrees=True)
167 # If we are only using one filter, this could be useful
168 if (self.filtername2 is None) | (self.filtername1 == self.filtername2):
169 self.filtername = self.filtername1
171 def _check_feasibility(self, conditions):
172 """
173 Check if the survey is feasable in the current conditions.
174 """
175 for bf in self.basis_functions:
176 result = bf.check_feasibility(conditions)
177 if not result:
178 return result
180 # If we need to check that the reward function has enough area available
181 if self.min_area is not None:
182 reward = 0
183 for bf, weight in zip(self.basis_functions, self.basis_weights):
184 basis_value = bf(conditions)
185 reward += basis_value*weight
186 valid_pix = np.where(np.isnan(reward) == False)[0]
187 if np.size(valid_pix)*self.pixarea < self.min_area:
188 result = False
189 return result
191 def _set_block_size(self, conditions):
192 """
193 Update the block size if it's getting near the end of the night.
194 """
195 if self.twilight_scale:
196 available_time = conditions.sun_n18_rising - conditions.mjd
197 available_time *= 24.*60. # to minutes
198 n_ideal_blocks = available_time / self.ideal_pair_time
199 else:
200 n_ideal_blocks = 4
202 if n_ideal_blocks >= 3:
203 self.nvisit_block = int(np.floor(self.ideal_pair_time*60. / (self.slew_approx + self.exptime +
204 self.read_approx*(self.nexp - 1))))
205 else:
206 # Now we can stretch or contract the block size to allocate the remainder time until twilight starts
207 # We can take the remaining time and try to do 1,2, or 3 blocks.
208 possible_times = available_time / np.arange(1, 4)
209 diff = np.abs(self.ideal_pair_time-possible_times)
210 best_block_time = np.max(possible_times[np.where(diff == np.min(diff))])
211 self.nvisit_block = int(np.floor(best_block_time*60. / (self.slew_approx + self.exptime +
212 self.read_approx*(self.nexp - 1))))
214 # The floor can set block to zero, make it possible to to just one
215 if self.nvisit_block <= 0:
216 self.nvisit_block = 1
218 def calc_reward_function(self, conditions):
219 """
220 """
221 # Set the number of observations we are going to try and take
222 self._set_block_size(conditions)
223 # Computing reward like usual with basis functions and weights
224 if self._check_feasibility(conditions):
225 self.reward = 0
226 indx = np.arange(hp.nside2npix(self.nside))
227 for bf, weight in zip(self.basis_functions, self.basis_weights):
228 basis_value = bf(conditions, indx=indx)
229 self.reward += basis_value*weight
230 # 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 generate_observations_rough(self, conditions):
267 """
268 Find a good block of observations.
269 """
271 self.reward = self.calc_reward_function(conditions)
273 # Check if we need to spin the tesselation
274 if self.dither & (conditions.night != self.night):
275 self._spin_fields()
276 self.night = conditions.night.copy()
278 # Now that we have the reward map,
279 # Note, using nanmax, so masked pixels might be included in the pointing.
280 potential_hp = np.where(~np.isnan(self.reward) == True)
281 ufields, reward_by_field = int_binned_stat(self.hp2fields[potential_hp],
282 self.reward[potential_hp],
283 statistic=np.nanmax)
284 # chop off any nans
285 not_nans = np.where(~np.isnan(reward_by_field) == True)
286 ufields = ufields[not_nans]
287 reward_by_field = reward_by_field[not_nans]
289 order = np.argsort(reward_by_field)
290 ufields = ufields[order][::-1][0:self.nvisit_block]
291 self.best_fields = ufields
293 if len(self.best_fields) == 0:
294 # everything was nans, or self.nvisit_block was zero
295 return []
297 # Let's find the alt, az coords of the points (right now, hopefully doesn't change much in time block)
298 pointing_alt, pointing_az = _approx_RaDec2AltAz(self.fields['RA'][self.best_fields],
299 self.fields['dec'][self.best_fields],
300 conditions.site.latitude_rad,
301 conditions.site.longitude_rad,
302 conditions.mjd,
303 lmst=conditions.lmst)
305 # Let's find a good spot to project the points to a plane
306 mid_alt = (np.max(pointing_alt) - np.min(pointing_alt))/2.
308 # Code snippet from MAF for computing mean of angle accounting for wrap around
309 # XXX-TODO: Maybe move this to sims_utils as a generally useful snippet.
310 x = np.cos(pointing_az)
311 y = np.sin(pointing_az)
312 meanx = np.mean(x)
313 meany = np.mean(y)
314 angle = np.arctan2(meany, meanx)
315 radius = np.sqrt(meanx**2 + meany**2)
316 mid_az = angle % (2.*np.pi)
317 if radius < 0.1:
318 mid_az = np.pi
320 # Project the alt,az coordinates to a plane. Could consider scaling things to represent
321 # time between points rather than angular distance.
322 pointing_x, pointing_y = gnomonic_project_toxy(pointing_az, pointing_alt, mid_az, mid_alt)
323 # Round off positions so that we ensure identical cross-platform performance
324 scale = 1e6
325 pointing_x = np.round(pointing_x*scale).astype(int)
326 pointing_y = np.round(pointing_y*scale).astype(int)
327 # Now I have a bunch of x,y pointings. Drop into TSP solver to get an effiencent route
328 towns = np.vstack((pointing_x, pointing_y)).T
329 # Leaving optimize=False for speed. The optimization step doesn't usually improve much.
330 better_order = tsp_convex(towns, optimize=False)
331 # XXX-TODO: Could try to roll better_order to start at the nearest/fastest slew from current position.
332 observations = []
333 counter2 = 0
334 approx_end_time = np.size(better_order)*(self.slew_approx + self.exptime +
335 self.read_approx*(self.nexp - 1))
336 flush_time = conditions.mjd + approx_end_time/3600./24. + self.flush_time
337 for i, indx in enumerate(better_order):
338 field = self.best_fields[indx]
339 obs = empty_observation()
340 obs['RA'] = self.fields['RA'][field]
341 obs['dec'] = self.fields['dec'][field]
342 obs['rotSkyPos'] = 0.
343 obs['filter'] = self.filtername1
344 obs['nexp'] = self.nexp
345 obs['exptime'] = self.exptime
346 obs['field_id'] = -1
347 obs['note'] = '%s' % (self.survey_note)
348 obs['block_id'] = self.counter
349 obs['flush_by_mjd'] = flush_time
350 # Add the mjd for debugging
351 # obs['mjd'] = conditions.mjd
352 # XXX temp debugging line
353 obs['survey_id'] = i
354 observations.append(obs)
355 counter2 += 1
357 result = observations
358 return result