Hide keyboard shortcuts

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 

10 

11__all__ = ['Greedy_survey', 'Blob_survey'] 

12 

13 

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'): 

22 

23 extra_features = {} 

24 

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 

37 

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) 

43 

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() 

48 

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])] 

53 

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 

69 

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 

76 

77 

78class Blob_survey(Greedy_survey): 

79 """Select observations in large, mostly contiguous, blobs. 

80 

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): 

122 

123 if nside is None: 

124 nside = set_default_nside() 

125 

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) 

153 

154 self.ra, self.dec = _hpid2RaDec(self.nside, self.hpids) 

155 

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 

165 

166 self.pixarea = hp.nside2pixarea(self.nside, degrees=True) 

167 

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 

171 

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 

180 

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 

191 

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 

202 

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)))) 

214 

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 

218 

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() 

234 

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 

238 

239 # Select healpixels within some radius of the max 

240 # This is probably faster with a kd-tree. 

241 

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 

248 

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 

253 

254 # Apply az cut 

255 az_centered = conditions.az - conditions.az[peak_reward] 

256 az_centered[np.where(az_centered < 0)] += 2.*np.pi 

257 

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 

265 

266 def simple_order_sort(self): 

267 """Fall back if we can't link contiguous blobs in the reward map 

268 """ 

269 

270 # Assuming reward has already been calcualted 

271 

272 potential_hp = np.where(~np.isnan(self.reward) == True) 

273 

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] 

284 

285 order = np.argsort(reward_by_field) 

286 ufields = ufields[order][::-1][0:self.nvisit_block] 

287 self.best_fields = ufields 

288 

289 def generate_observations_rough(self, conditions): 

290 """ 

291 Find a good block of observations. 

292 """ 

293 

294 self.reward = self.calc_reward_function(conditions) 

295 

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() 

300 

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]] 

309 

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] 

315 

316 if len(self.best_fields) == 0: 

317 # everything was nans, or self.nvisit_block was zero 

318 return [] 

319 

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) 

327 

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. 

330 

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 

342 

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 

379 

380 result = observations 

381 return result