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 

10import warnings 

11 

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

13 

14 

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

23 

24 extra_features = {} 

25 

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 

38 

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) 

44 

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

49 

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

54 

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 

70 

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 

77 

78 

79class Blob_survey(Greedy_survey): 

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

81 

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

127 

128 if nside is None: 

129 nside = set_default_nside() 

130 

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 

145 

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

148 

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) 

164 

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

166 

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 

176 

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

178 

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 

182 

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 

191 

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 

202 

203 def _set_block_size(self, conditions): 

204 """ 

205 Update the block size if it's getting near a break point. 

206 """ 

207 

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 

215 

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 

224 

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 

236 

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

248 

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 

252 

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

268 

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 

272 

273 # Select healpixels within some radius of the max 

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

275 

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 

282 

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 

287 

288 # Apply az cut 

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

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

291 

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 

299 

300 def simple_order_sort(self): 

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

302 """ 

303 

304 # Assuming reward has already been calcualted 

305 

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

307 

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] 

318 

319 order = np.argsort(reward_by_field) 

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

321 self.best_fields = ufields 

322 

323 def generate_observations_rough(self, conditions): 

324 """ 

325 Find a good block of observations. 

326 """ 

327 

328 self.reward = self.calc_reward_function(conditions) 

329 

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

334 

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

343 

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] 

349 

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

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

352 return [] 

353 

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) 

361 

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. 

364 

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 

376 

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 

413 

414 result = observations 

415 return result