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', area_required=None): 

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

35 self.filtername = filtername 

36 self.block_size = block_size 

37 self.nexp = nexp 

38 self.exptime = exptime 

39 

40 def generate_observations_rough(self, conditions): 

41 """ 

42 Just point at the highest reward healpix 

43 """ 

44 self.reward = self.calc_reward_function(conditions) 

45 

46 # Check if we need to spin the tesselation 

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

48 self._spin_fields() 

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

50 

51 # Let's find the best N from the fields 

52 order = np.argsort(self.reward)[::-1] 

53 # Crop off any NaNs 

54 order = order[~np.isnan(self.reward[order])] 

55 

56 iter = 0 

57 while True: 

58 best_hp = order[iter*self.block_size:(iter+1)*self.block_size] 

59 best_fields = np.unique(self.hp2fields[best_hp]) 

60 observations = [] 

61 for field in best_fields: 

62 obs = empty_observation() 

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

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

65 obs['rotSkyPos'] = 0. 

66 obs['filter'] = self.filtername 

67 obs['nexp'] = self.nexp 

68 obs['exptime'] = self.exptime 

69 obs['field_id'] = -1 

70 obs['note'] = self.survey_name 

71 

72 observations.append(obs) 

73 break 

74 iter += 1 

75 if len(observations) > 0 or (iter+2)*self.block_size > len(order): 

76 break 

77 return observations 

78 

79 

80class Blob_survey(Greedy_survey): 

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

82 

83 Parameters 

84 ---------- 

85 filtername1 : str ('r') 

86 The filter to observe in. 

87 filtername2 : str ('g') 

88 The filter to pair with the first observation. If set to None, no pair 

89 will be observed. 

90 slew_approx : float (7.5) 

91 The approximate slewtime between neerby fields (seconds). Used to calculate 

92 how many observations can be taken in the desired time block. 

93 nexp : int (2) 

94 The number of exposures to take in a visit. 

95 exp_dict : dict (None) 

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

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

98 filter_change_approx : float (140.) 

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

100 ideal_pair_time : float (22.) 

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

102 min_pair_time : float (15.) 

103 The minimum acceptable pair time (minutes) 

104 search_radius : float (30.) 

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

106 alt_max : float (85.) 

107 The maximum altitude to include (degrees). 

108 az_range : float (90.) 

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

110 flush_time : float (30.) 

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

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

113 twilight_scale : bool (True) 

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

115 in_twilight : bool (False) 

116 Scale the block size to stay within twilight time. 

117 check_scheduled : bool (True) 

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

119 min_area : float (None) 

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

121 grow_blob : bool (True) 

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

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

124 """ 

125 def __init__(self, basis_functions, basis_weights, 

126 filtername1='r', filtername2='g', 

127 slew_approx=7.5, filter_change_approx=140., 

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

129 ideal_pair_time=22., min_pair_time=15., 

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

131 flush_time=30., 

132 smoothing_kernel=None, nside=None, 

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

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

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

136 grow_blob=True, area_required=None): 

137 

138 if nside is None: 

139 nside = set_default_nside() 

140 

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

142 basis_weights=basis_weights, 

143 filtername=None, 

144 block_size=0, smoothing_kernel=smoothing_kernel, 

145 dither=dither, seed=seed, ignore_obs=ignore_obs, 

146 nside=nside, detailers=detailers, camera=camera, 

147 area_required=area_required) 

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

149 self.nexp = nexp 

150 self.nexp_dict = nexp_dict 

151 self.exptime = exptime 

152 self.slew_approx = slew_approx 

153 self.read_approx = read_approx 

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

155 self.twilight_scale = twilight_scale 

156 self.in_twilight = in_twilight 

157 self.grow_blob = grow_blob 

158 

159 if self.twilight_scale & self.in_twilight: 

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

161 

162 self.min_area = min_area 

163 self.check_scheduled = check_scheduled 

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

165 if filtername1 == filtername2: 

166 filter_change_approx = 0 

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

168 if filtername2 is not None: 

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

170 else: 

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

172 self.filter_set = set(filtername1) 

173 if filtername2 is None: 

174 self.filter2_set = self.filter_set 

175 else: 

176 self.filter2_set = set(filtername2) 

177 

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

179 

180 self.survey_note = survey_note 

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

182 self.filtername1 = filtername1 

183 self.filtername2 = filtername2 

184 self.search_radius = np.radians(search_radius) 

185 self.az_range = np.radians(az_range) 

186 self.alt_max = np.radians(alt_max) 

187 self.min_pair_time = min_pair_time 

188 self.ideal_pair_time = ideal_pair_time 

189 

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

191 

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

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

194 self.filtername = self.filtername1 

195 

196 def _check_feasibility(self, conditions): 

197 """ 

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

199 """ 

200 for bf in self.basis_functions: 

201 result = bf.check_feasibility(conditions) 

202 if not result: 

203 return result 

204 

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

206 if self.min_area is not None: 

207 reward = 0 

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

209 basis_value = bf(conditions) 

210 reward += basis_value*weight 

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

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

213 result = False 

214 return result 

215 

216 def _set_block_size(self, conditions): 

217 """ 

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

219 """ 

220 

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

222 if self.twilight_scale: 

223 available_time = conditions.sun_n18_rising - conditions.mjd 

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

225 n_ideal_blocks = available_time / self.ideal_pair_time 

226 else: 

227 n_ideal_blocks = 4 

228 

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

230 if self.check_scheduled: 

231 if len(conditions.scheduled_observations) > 0: 

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

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

234 n_blocks = available_time / self.ideal_pair_time 

235 if n_blocks < n_ideal_blocks: 

236 n_ideal_blocks = n_blocks 

237 

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

239 if self.in_twilight: 

240 at1 = conditions.sun_n12_rising - conditions.mjd 

241 at2 = conditions.sun_n18_setting - conditions.mjd 

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

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

244 available_time = np.min(times) 

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

246 n_blocks = available_time / self.ideal_pair_time 

247 if n_blocks < n_ideal_blocks: 

248 n_ideal_blocks = n_blocks 

249 

250 if n_ideal_blocks >= 3: 

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

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

253 else: 

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

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

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

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

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

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

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

261 

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

263 if self.nvisit_block <= 0: 

264 self.nvisit_block = 1 

265 

266 def calc_reward_function(self, conditions): 

267 """ 

268 """ 

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

270 self._set_block_size(conditions) 

271 # Computing reward like usual with basis functions and weights 

272 if self._check_feasibility(conditions): 

273 self.reward = 0 

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

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

276 basis_value = bf(conditions, indx=indx) 

277 self.reward += basis_value*weight 

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

279 if self.smoothing_kernel is not None: 

280 self.smooth_reward() 

281 

282 # Apply max altitude cut 

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

284 self.reward[too_high] = np.nan 

285 

286 # Select healpixels within some radius of the max 

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

288 

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

290 if np.size(max_hp) > 0: 

291 peak_reward = np.min(max_hp) 

292 else: 

293 # Everything is masked, so get out 

294 return -np.inf 

295 

296 # Apply radius selection 

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

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

299 self.reward[out_hp] = np.nan 

300 

301 # Apply az cut 

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

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

304 

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

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

307 self.reward[az_out] = np.nan 

308 else: 

309 self.reward = -np.inf 

310 

311 if self.area_required is not None: 

312 good_area = np.where(np.abs(self.reward) >= 0)[0].size * hp.nside2pixarea(self.nside) 

313 if good_area < self.area_required: 

314 self.reward = -np.inf 

315 

316 #if ('twi' in self.survey_note) & (np.any(np.isfinite(self.reward))): 

317 # import pdb ; pdb.set_trace() 

318 

319 self.reward_checked = True 

320 return self.reward 

321 

322 def simple_order_sort(self): 

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

324 """ 

325 

326 # Assuming reward has already been calcualted 

327 

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

329 

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

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

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

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

334 self.reward[potential_hp], 

335 statistic=np.nanmax) 

336 # chop off any nans 

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

338 ufields = ufields[not_nans] 

339 reward_by_field = reward_by_field[not_nans] 

340 

341 order = np.argsort(reward_by_field) 

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

343 self.best_fields = ufields 

344 

345 def generate_observations_rough(self, conditions): 

346 """ 

347 Find a good block of observations. 

348 """ 

349 

350 self.reward = self.calc_reward_function(conditions) 

351 

352 # Check if we need to spin the tesselation 

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

354 self._spin_fields() 

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

356 

357 if self.grow_blob: 

358 # Note, returns highest first 

359 ordered_hp = hp_grow_argsort(self.reward) 

360 ordered_fields = self.hp2fields[ordered_hp] 

361 orig_order = np.arange(ordered_fields.size) 

362 # Remove duplicate field pointings 

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

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

365 best_fields = ordered_fields[u_indx[new_order]] 

366 

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

368 # Let's fall back to the simple sort 

369 self.simple_order_sort() 

370 else: 

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

372 else: 

373 self.simple_order_sort() 

374 

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

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

377 return [] 

378 

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

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

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

382 conditions.site.latitude_rad, 

383 conditions.site.longitude_rad, 

384 conditions.mjd, 

385 lmst=conditions.lmst) 

386 

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

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

389 

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

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

392 x = np.cos(pointing_az) 

393 y = np.sin(pointing_az) 

394 meanx = np.mean(x) 

395 meany = np.mean(y) 

396 angle = np.arctan2(meany, meanx) 

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

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

399 if radius < 0.1: 

400 mid_az = np.pi 

401 

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

403 # time between points rather than angular distance. 

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

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

406 scale = 1e6 

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

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

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

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

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

412 better_order = tsp_convex(towns, optimize=False) 

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

414 observations = [] 

415 counter2 = 0 

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

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

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

419 for i, indx in enumerate(better_order): 

420 field = self.best_fields[indx] 

421 obs = empty_observation() 

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

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

424 obs['rotSkyPos'] = 0. 

425 obs['filter'] = self.filtername1 

426 if self.nexp_dict is None: 

427 obs['nexp'] = self.nexp 

428 else: 

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

430 obs['exptime'] = self.exptime 

431 obs['field_id'] = -1 

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

433 obs['block_id'] = self.counter 

434 obs['flush_by_mjd'] = flush_time 

435 # Add the mjd for debugging 

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

437 # XXX temp debugging line 

438 obs['survey_id'] = i 

439 observations.append(obs) 

440 counter2 += 1 

441 

442 result = observations 

443 return result