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 nexp : int (2) 

93 The number of exposures to take in a visit. 

94 exp_dict : dict (None) 

95 If set, should have keys of filtername and values of ints that are the nuber of exposures to take 

96 per visit. For estimating block time, nexp is still used. 

97 filter_change_approx : float (140.) 

98 The approximate time it takes to change filters (seconds). 

99 ideal_pair_time : float (22.) 

100 The ideal time gap wanted between observations to the same pointing (minutes) 

101 min_pair_time : float (15.) 

102 The minimum acceptable pair time (minutes) 

103 search_radius : float (30.) 

104 The radius around the reward peak to look for additional potential pointings (degrees) 

105 alt_max : float (85.) 

106 The maximum altitude to include (degrees). 

107 az_range : float (90.) 

108 The range of azimuths to consider around the peak reward value (degrees). 

109 flush_time : float (30.) 

110 The time past the final expected exposure to flush the queue. Keeps observations 

111 from lingering past when they should be executed. (minutes) 

112 twilight_scale : bool (True) 

113 Scale the block size to fill up to twilight. Set to False if running in twilight 

114 in_twilight : bool (False) 

115 Scale the block size to stay within twilight time. 

116 check_scheduled : bool (True) 

117 Check if there are scheduled observations and scale blob size to match 

118 min_area : float (None) 

119 If set, demand the reward function have an area of so many square degrees before executing 

120 grow_blob : bool (True) 

121 If True, try to grow the blob from the global maximum. Otherwise, just use a simple sort. 

122 Simple sort will not constrain the blob to be contiguous. 

123 """ 

124 def __init__(self, basis_functions, basis_weights, 

125 filtername1='r', filtername2='g', 

126 slew_approx=7.5, filter_change_approx=140., 

127 read_approx=2., exptime=30., nexp=2, nexp_dict=None, 

128 ideal_pair_time=22., min_pair_time=15., 

129 search_radius=30., alt_max=85., az_range=90., 

130 flush_time=30., 

131 smoothing_kernel=None, nside=None, 

132 dither=True, seed=42, ignore_obs=None, 

133 survey_note='blob', detailers=None, camera='LSST', 

134 twilight_scale=True, in_twilight=False, check_scheduled=True, min_area=None, 

135 grow_blob=True): 

136 

137 if nside is None: 

138 nside = set_default_nside() 

139 

140 super(Blob_survey, self).__init__(basis_functions=basis_functions, 

141 basis_weights=basis_weights, 

142 filtername=None, 

143 block_size=0, smoothing_kernel=smoothing_kernel, 

144 dither=dither, seed=seed, ignore_obs=ignore_obs, 

145 nside=nside, detailers=detailers, camera=camera) 

146 self.flush_time = flush_time/60./24. # convert to days 

147 self.nexp = nexp 

148 self.nexp_dict = nexp_dict 

149 self.exptime = exptime 

150 self.slew_approx = slew_approx 

151 self.read_approx = read_approx 

152 self.hpids = np.arange(hp.nside2npix(self.nside)) 

153 self.twilight_scale = twilight_scale 

154 self.in_twilight = in_twilight 

155 self.grow_blob = grow_blob 

156 

157 if self.twilight_scale & self.in_twilight: 

158 warnings.warn('Both twilight_scale and in_twilight are set to True. That is probably wrong.') 

159 

160 self.min_area = min_area 

161 self.check_scheduled = check_scheduled 

162 # If we are taking pairs in same filter, no need to add filter change time. 

163 if filtername1 == filtername2: 

164 filter_change_approx = 0 

165 # Compute the minimum time needed to observe a blob (or observe, then repeat.) 

166 if filtername2 is not None: 

167 self.time_needed = (min_pair_time*60.*2. + exptime + read_approx + filter_change_approx)/24./3600. # Days 

168 else: 

169 self.time_needed = (min_pair_time*60. + exptime + read_approx)/24./3600. # Days 

170 self.filter_set = set(filtername1) 

171 if filtername2 is None: 

172 self.filter2_set = self.filter_set 

173 else: 

174 self.filter2_set = set(filtername2) 

175 

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

177 

178 self.survey_note = survey_note 

179 self.counter = 1 # start at 1, because 0 is default in empty observation 

180 self.filtername1 = filtername1 

181 self.filtername2 = filtername2 

182 self.search_radius = np.radians(search_radius) 

183 self.az_range = np.radians(az_range) 

184 self.alt_max = np.radians(alt_max) 

185 self.min_pair_time = min_pair_time 

186 self.ideal_pair_time = ideal_pair_time 

187 

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

189 

190 # If we are only using one filter, this could be useful 

191 if (self.filtername2 is None) | (self.filtername1 == self.filtername2): 

192 self.filtername = self.filtername1 

193 

194 def _check_feasibility(self, conditions): 

195 """ 

196 Check if the survey is feasable in the current conditions. 

197 """ 

198 for bf in self.basis_functions: 

199 result = bf.check_feasibility(conditions) 

200 if not result: 

201 return result 

202 

203 # If we need to check that the reward function has enough area available 

204 if self.min_area is not None: 

205 reward = 0 

206 for bf, weight in zip(self.basis_functions, self.basis_weights): 

207 basis_value = bf(conditions) 

208 reward += basis_value*weight 

209 valid_pix = np.where(np.isnan(reward) == False)[0] 

210 if np.size(valid_pix)*self.pixarea < self.min_area: 

211 result = False 

212 return result 

213 

214 def _set_block_size(self, conditions): 

215 """ 

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

217 """ 

218 

219 # If we are trying to get things done before twilight 

220 if self.twilight_scale: 

221 available_time = conditions.sun_n18_rising - conditions.mjd 

222 available_time *= 24.*60. # to minutes 

223 n_ideal_blocks = available_time / self.ideal_pair_time 

224 else: 

225 n_ideal_blocks = 4 

226 

227 # If we are trying to get things done before a scheduled simulation 

228 if self.check_scheduled: 

229 if len(conditions.scheduled_observations) > 0: 

230 available_time = np.min(conditions.scheduled_observations) - conditions.mjd 

231 available_time *= 24.*60. # to minutes 

232 n_blocks = available_time / self.ideal_pair_time 

233 if n_blocks < n_ideal_blocks: 

234 n_ideal_blocks = n_blocks 

235 

236 # If we are trying to complete before twilight ends or the night ends 

237 if self.in_twilight: 

238 at1 = conditions.sun_n12_rising - conditions.mjd 

239 at2 = conditions.sun_n18_setting - conditions.mjd 

240 times = np.array([at1, at2]) 

241 times = times[np.where(times > 0)] 

242 available_time = np.min(times) 

243 available_time *= 24.*60. # to minutes 

244 n_blocks = available_time / self.ideal_pair_time 

245 if n_blocks < n_ideal_blocks: 

246 n_ideal_blocks = n_blocks 

247 

248 if n_ideal_blocks >= 3: 

249 self.nvisit_block = int(np.floor(self.ideal_pair_time*60. / (self.slew_approx + self.exptime + 

250 self.read_approx*(self.nexp - 1)))) 

251 else: 

252 # Now we can stretch or contract the block size to allocate the remainder time until twilight starts 

253 # We can take the remaining time and try to do 1,2, or 3 blocks. 

254 possible_times = available_time / np.arange(1, 4) 

255 diff = np.abs(self.ideal_pair_time-possible_times) 

256 best_block_time = np.max(possible_times[np.where(diff == np.min(diff))]) 

257 self.nvisit_block = int(np.floor(best_block_time*60. / (self.slew_approx + self.exptime + 

258 self.read_approx*(self.nexp - 1)))) 

259 

260 # The floor can set block to zero, make it possible to to just one 

261 if self.nvisit_block <= 0: 

262 self.nvisit_block = 1 

263 

264 def calc_reward_function(self, conditions): 

265 """ 

266 """ 

267 # Set the number of observations we are going to try and take 

268 self._set_block_size(conditions) 

269 # Computing reward like usual with basis functions and weights 

270 if self._check_feasibility(conditions): 

271 self.reward = 0 

272 indx = np.arange(hp.nside2npix(self.nside)) 

273 for bf, weight in zip(self.basis_functions, self.basis_weights): 

274 basis_value = bf(conditions, indx=indx) 

275 self.reward += basis_value*weight 

276 # might be faster to pull this out into the feasabiliity check? 

277 if self.smoothing_kernel is not None: 

278 self.smooth_reward() 

279 

280 # Apply max altitude cut 

281 too_high = np.where(int_rounded(conditions.alt) > int_rounded(self.alt_max)) 

282 self.reward[too_high] = np.nan 

283 

284 # Select healpixels within some radius of the max 

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

286 

287 max_hp = np.where(self.reward == np.nanmax(self.reward))[0] 

288 if np.size(max_hp) > 0: 

289 peak_reward = np.min(max_hp) 

290 else: 

291 # Everything is masked, so get out 

292 return -np.inf 

293 

294 # Apply radius selection 

295 dists = _angularSeparation(self.ra[peak_reward], self.dec[peak_reward], self.ra, self.dec) 

296 out_hp = np.where(int_rounded(dists) > int_rounded(self.search_radius)) 

297 self.reward[out_hp] = np.nan 

298 

299 # Apply az cut 

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

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

302 

303 az_out = np.where((int_rounded(az_centered) > int_rounded(self.az_range/2.)) & 

304 (int_rounded(az_centered) < int_rounded(2.*np.pi-self.az_range/2.))) 

305 self.reward[az_out] = np.nan 

306 else: 

307 self.reward = -np.inf 

308 self.reward_checked = True 

309 return self.reward 

310 

311 def simple_order_sort(self): 

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

313 """ 

314 

315 # Assuming reward has already been calcualted 

316 

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

318 

319 # Note, using nanmax, so masked pixels might be included in the pointing. 

320 # I guess I should document that it's not "NaN pixels can't be observed", but 

321 # "non-NaN pixles CAN be observed", which probably is not intuitive. 

322 ufields, reward_by_field = int_binned_stat(self.hp2fields[potential_hp], 

323 self.reward[potential_hp], 

324 statistic=np.nanmax) 

325 # chop off any nans 

326 not_nans = np.where(~np.isnan(reward_by_field) == True) 

327 ufields = ufields[not_nans] 

328 reward_by_field = reward_by_field[not_nans] 

329 

330 order = np.argsort(reward_by_field) 

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

332 self.best_fields = ufields 

333 

334 def generate_observations_rough(self, conditions): 

335 """ 

336 Find a good block of observations. 

337 """ 

338 

339 self.reward = self.calc_reward_function(conditions) 

340 

341 # Check if we need to spin the tesselation 

342 if self.dither & (conditions.night != self.night): 

343 self._spin_fields() 

344 self.night = conditions.night.copy() 

345 

346 if self.grow_blob: 

347 # Note, returns highest first 

348 ordered_hp = hp_grow_argsort(self.reward) 

349 ordered_fields = self.hp2fields[ordered_hp] 

350 orig_order = np.arange(ordered_fields.size) 

351 # Remove duplicate field pointings 

352 _u_of, u_indx = np.unique(ordered_fields, return_index=True) 

353 new_order = np.argsort(orig_order[u_indx]) 

354 best_fields = ordered_fields[u_indx[new_order]] 

355 

356 if np.size(best_fields) < self.nvisit_block: 

357 # Let's fall back to the simple sort 

358 self.simple_order_sort() 

359 else: 

360 self.best_fields = best_fields[0:self.nvisit_block] 

361 else: 

362 self.simple_order_sort() 

363 

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

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

366 return [] 

367 

368 # Let's find the alt, az coords of the points (right now, hopefully doesn't change much in time block) 

369 pointing_alt, pointing_az = _approx_RaDec2AltAz(self.fields['RA'][self.best_fields], 

370 self.fields['dec'][self.best_fields], 

371 conditions.site.latitude_rad, 

372 conditions.site.longitude_rad, 

373 conditions.mjd, 

374 lmst=conditions.lmst) 

375 

376 # Let's find a good spot to project the points to a plane 

377 mid_alt = (np.max(pointing_alt) - np.min(pointing_alt))/2. 

378 

379 # Code snippet from MAF for computing mean of angle accounting for wrap around 

380 # XXX-TODO: Maybe move this to sims_utils as a generally useful snippet. 

381 x = np.cos(pointing_az) 

382 y = np.sin(pointing_az) 

383 meanx = np.mean(x) 

384 meany = np.mean(y) 

385 angle = np.arctan2(meany, meanx) 

386 radius = np.sqrt(meanx**2 + meany**2) 

387 mid_az = angle % (2.*np.pi) 

388 if radius < 0.1: 

389 mid_az = np.pi 

390 

391 # Project the alt,az coordinates to a plane. Could consider scaling things to represent 

392 # time between points rather than angular distance. 

393 pointing_x, pointing_y = gnomonic_project_toxy(pointing_az, pointing_alt, mid_az, mid_alt) 

394 # Round off positions so that we ensure identical cross-platform performance 

395 scale = 1e6 

396 pointing_x = np.round(pointing_x*scale).astype(int) 

397 pointing_y = np.round(pointing_y*scale).astype(int) 

398 # Now I have a bunch of x,y pointings. Drop into TSP solver to get an effiencent route 

399 towns = np.vstack((pointing_x, pointing_y)).T 

400 # Leaving optimize=False for speed. The optimization step doesn't usually improve much. 

401 better_order = tsp_convex(towns, optimize=False) 

402 # XXX-TODO: Could try to roll better_order to start at the nearest/fastest slew from current position. 

403 observations = [] 

404 counter2 = 0 

405 approx_end_time = np.size(better_order)*(self.slew_approx + self.exptime + 

406 self.read_approx*(self.nexp - 1)) 

407 flush_time = conditions.mjd + approx_end_time/3600./24. + self.flush_time 

408 for i, indx in enumerate(better_order): 

409 field = self.best_fields[indx] 

410 obs = empty_observation() 

411 obs['RA'] = self.fields['RA'][field] 

412 obs['dec'] = self.fields['dec'][field] 

413 obs['rotSkyPos'] = 0. 

414 obs['filter'] = self.filtername1 

415 if self.nexp_dict is None: 

416 obs['nexp'] = self.nexp 

417 else: 

418 obs['nexp'] = self.nexp_dict[self.filtername1] 

419 obs['exptime'] = self.exptime 

420 obs['field_id'] = -1 

421 obs['note'] = '%s' % (self.survey_note) 

422 obs['block_id'] = self.counter 

423 obs['flush_by_mjd'] = flush_time 

424 # Add the mjd for debugging 

425 # obs['mjd'] = conditions.mjd 

426 # XXX temp debugging line 

427 obs['survey_id'] = i 

428 observations.append(obs) 

429 counter2 += 1 

430 

431 result = observations 

432 return result