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 

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 

9 

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

11 

12 

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

21 

22 extra_features = {} 

23 

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 

36 

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) 

42 

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

47 

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

52 

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 

68 

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 

75 

76 

77class Blob_survey(Greedy_survey): 

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

79 

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

121 

122 if nside is None: 

123 nside = set_default_nside() 

124 

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) 

152 

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

154 

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 

164 

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

166 

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 

170 

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 

179 

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 

190 

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 

201 

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

213 

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 

217 

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? 

231 

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 generate_observations_rough(self, conditions): 

267 """ 

268 Find a good block of observations. 

269 """ 

270 

271 self.reward = self.calc_reward_function(conditions) 

272 

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

277 

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] 

288 

289 order = np.argsort(reward_by_field) 

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

291 self.best_fields = ufields 

292 

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

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

295 return [] 

296 

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) 

304 

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. 

307 

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 

319 

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 

356 

357 result = observations 

358 return result