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, 

3 hp_in_lsst_fov, read_fields, hp_in_comcam_fov, 

4 comcamTessellate) 

5import healpy as hp 

6from lsst.sims.featureScheduler.thomson import xyz2thetaphi, thetaphi2xyz 

7from lsst.sims.featureScheduler.detailers import Zero_rot_detailer 

8 

9__all__ = ['BaseSurvey', 'BaseMarkovDF_survey'] 

10 

11 

12class BaseSurvey(object): 

13 """A baseclass for survey objects.  

14 

15 Parameters 

16 ---------- 

17 basis_functions : list 

18 List of basis_function objects 

19 extra_features : list XXX--should this be a dict for clarity? 

20 List of any additional features the survey may want to use 

21 e.g., for computing final dither positions. 

22 extra_basis_functions : dict of lsst.sims.featureScheduler.basis_function objects 

23 Extra basis function objects. Typically not psased in, but et in the __init__. 

24 ignore_obs : list of str (None) 

25 If an incoming observation has this string in the note, ignore it. Handy if 

26 one wants to ignore DD fields or observations requested by self. Take note, 

27 if a survey is called 'mysurvey23', setting ignore_obs to 'mysurvey2' will 

28 ignore it because 'mysurvey2' is a substring of 'mysurvey23'. 

29 detailers : list of lsst.sims.featureScheduler.detailers objects 

30 The detailers to apply to the list of observations. 

31 scheduled_obs : np.array 

32 An array of MJD values for when observations should execute. 

33 """ 

34 def __init__(self, basis_functions, extra_features=None, extra_basis_functions=None, 

35 ignore_obs=None, survey_name='', nside=None, detailers=None, 

36 scheduled_obs=None): 

37 if nside is None: 

38 nside = set_default_nside() 

39 if ignore_obs is None: 

40 ignore_obs = [] 

41 

42 if isinstance(ignore_obs, str): 

43 ignore_obs = [ignore_obs] 

44 

45 self.nside = nside 

46 self.survey_name = survey_name 

47 self.ignore_obs = ignore_obs 

48 

49 self.reward = None 

50 self.survey_index = None 

51 

52 self.basis_functions = basis_functions 

53 

54 if extra_features is None: 

55 self.extra_features = {} 

56 else: 

57 self.extra_features = extra_features 

58 if extra_basis_functions is None: 

59 self.extra_basis_functions = {} 

60 else: 

61 self.extra_basis_functions = extra_basis_functions 

62 

63 self.reward_checked = False 

64 

65 # Attribute to track if the reward function is up-to-date. 

66 self.reward_checked = False 

67 

68 # If there's no detailers, add one to set rotation to near zero 

69 if detailers is None: 

70 self.detailers = [Zero_rot_detailer(nside=nside)] 

71 else: 

72 self.detailers = detailers 

73 

74 # Scheduled observations 

75 self.scheduled_obs = scheduled_obs 

76 

77 def get_scheduled_obs(self): 

78 return self.scheduled_obs 

79 

80 def add_observation(self, observation, **kwargs): 

81 # Check each posible ignore string 

82 checks = [io not in str(observation['note']) for io in self.ignore_obs] 

83 # ugh, I think here I have to assume observation is an array and not a dict. 

84 if all(checks): 

85 for feature in self.extra_features: 

86 self.extra_features[feature].add_observation(observation, **kwargs) 

87 for bf in self.extra_basis_functions: 

88 self.extra_basis_functions[bf].add_observation(observation, **kwargs) 

89 for bf in self.basis_functions: 

90 bf.add_observation(observation, **kwargs) 

91 for detailer in self.detailers: 

92 detailer.add_observation(observation, **kwargs) 

93 self.reward_checked = False 

94 

95 def _check_feasibility(self, conditions): 

96 """ 

97 Check if the survey is feasable in the current conditions 

98 """ 

99 for bf in self.basis_functions: 

100 result = bf.check_feasibility(conditions) 

101 if not result: 

102 return result 

103 return result 

104 

105 def calc_reward_function(self, conditions): 

106 """ 

107 Parameters 

108 ---------- 

109 conditions : lsst.sims.featureScheduler.features.Conditions object 

110 

111 Returns 

112 ------- 

113 reward : float (or array) 

114 

115 """ 

116 if self._check_feasability(): 

117 self.reward = 0 

118 else: 

119 # If we don't pass feasability 

120 self.reward = -np.inf 

121 

122 self.reward_checked = True 

123 return self.reward 

124 

125 def generate_observations_rough(self, conditions): 

126 """ 

127 Returns 

128 ------- 

129 one of: 

130 1) None 

131 2) A list of observations 

132 """ 

133 # If the reward function hasn't been updated with the 

134 # latest info, calculate it 

135 if not self.reward_checked: 

136 self.reward = self.calc_reward_function(conditions) 

137 obs = empty_observation() 

138 return [obs] 

139 

140 def generate_observations(self, conditions): 

141 observations = self.generate_observations_rough(conditions) 

142 for detailer in self.detailers: 

143 observations = detailer(observations, conditions) 

144 return observations 

145 

146 def viz_config(self): 

147 # XXX--zomg, we should have a method that goes through all the objects and 

148 # makes plots/prints info so there can be a little notebook showing the config! 

149 pass 

150 

151 

152def rotx(theta, x, y, z): 

153 """rotate the x,y,z points theta radians about x axis""" 

154 sin_t = np.sin(theta) 

155 cos_t = np.cos(theta) 

156 xp = x 

157 yp = y*cos_t+z*sin_t 

158 zp = -y*sin_t+z*cos_t 

159 return xp, yp, zp 

160 

161 

162class BaseMarkovDF_survey(BaseSurvey): 

163 """ A Markov Decision Function survey object. Uses Basis functions to compute a 

164 final reward function and decide what to observe based on the reward. Includes 

165 methods for dithering and defaults to dithering nightly. 

166 

167 Parameters 

168 ---------- 

169 basis_function : list of lsst.sims.featureSchuler.basis_function objects 

170 

171 basis_weights : list of float 

172 Must be same length as basis_function 

173 seed : hashable 

174 Random number seed, used for randomly orienting sky tessellation. 

175 camera : str ('LSST') 

176 Should be 'LSST' or 'comcam' 

177 area_required : float (None) 

178 The valid area that should be present in the reward function (square degrees). 

179 """ 

180 def __init__(self, basis_functions, basis_weights, extra_features=None, 

181 smoothing_kernel=None, 

182 ignore_obs=None, survey_name='', nside=None, seed=42, 

183 dither=True, detailers=None, camera='LSST', area_required=None): 

184 

185 super(BaseMarkovDF_survey, self).__init__(basis_functions=basis_functions, 

186 extra_features=extra_features, 

187 ignore_obs=ignore_obs, survey_name=survey_name, 

188 nside=nside, detailers=detailers) 

189 

190 self.basis_weights = basis_weights 

191 # Check that weights and basis functions are same length 

192 if len(basis_functions) != np.size(basis_weights): 

193 raise ValueError('basis_functions and basis_weights must be same length.') 

194 

195 self.camera = camera 

196 # Load the OpSim field tesselation and map healpix to fields 

197 if self.camera == 'LSST': 

198 self.fields_init = read_fields() 

199 elif self.camera == 'comcam': 

200 self.fields_init = comcamTessellate() 

201 else: 

202 ValueError('camera %s unknown, should be "LSST" or "comcam"' % camera) 

203 self.fields = self.fields_init.copy() 

204 self.hp2fields = np.array([]) 

205 self._hp2fieldsetup(self.fields['RA'], self.fields['dec']) 

206 

207 if smoothing_kernel is not None: 

208 self.smoothing_kernel = np.radians(smoothing_kernel) 

209 else: 

210 self.smoothing_kernel = None 

211 

212 if area_required is None: 

213 self.area_required = area_required 

214 else: 

215 self.area_required = area_required * (np.pi/180.)**2 # To steradians 

216 

217 # Start tracking the night 

218 self.night = -1 

219 

220 # Set the seed 

221 np.random.seed(seed) 

222 self.dither = dither 

223 

224 def _check_feasibility(self, conditions): 

225 """ 

226 Check if the survey is feasable in the current conditions 

227 """ 

228 for bf in self.basis_functions: 

229 result = bf.check_feasibility(conditions) 

230 if not result: 

231 return result 

232 if self.area_required is not None: 

233 reward = self.calc_reward_function(conditions) 

234 good_pix = np.where(np.isfinite(reward) == True)[0] 

235 area = hp.nside2pixarea(self.nside) * np.size(good_pix) 

236 if area < self.area_required: 

237 return False 

238 return result 

239 

240 def _hp2fieldsetup(self, ra, dec, leafsize=100): 

241 """Map each healpixel to nearest field. This will only work if healpix 

242 resolution is higher than field resolution. 

243 """ 

244 if self.camera == 'LSST': 

245 pointing2hpindx = hp_in_lsst_fov(nside=self.nside) 

246 elif self.camera == 'comcam': 

247 pointing2hpindx = hp_in_comcam_fov(nside=self.nside) 

248 

249 self.hp2fields = np.zeros(hp.nside2npix(self.nside), dtype=np.int) 

250 for i in range(len(ra)): 

251 hpindx = pointing2hpindx(ra[i], dec[i], rotSkyPos=0.) 

252 self.hp2fields[hpindx] = i 

253 

254 def _spin_fields(self, lon=None, lat=None, lon2=None): 

255 """Spin the field tessellation to generate a random orientation 

256 

257 The default field tesselation is rotated randomly in longitude, and then the 

258 pole is rotated to a random point on the sphere. 

259 

260 Parameters 

261 ---------- 

262 lon : float (None) 

263 The amount to initially rotate in longitude (radians). Will use a random value 

264 between 0 and 2 pi if None (default). 

265 lat : float (None) 

266 The amount to rotate in latitude (radians). 

267 lon2 : float (None) 

268 The amount to rotate the pole in longitude (radians). 

269 """ 

270 if lon is None: 

271 lon = np.random.rand()*np.pi*2 

272 if lat is None: 

273 # Make sure latitude points spread correctly 

274 # http://mathworld.wolfram.com/SpherePointPicking.html 

275 lat = np.arccos(2.*np.random.rand() - 1.) 

276 if lon2 is None: 

277 lon2 = np.random.rand()*np.pi*2 

278 # rotate longitude 

279 ra = (self.fields_init['RA'] + lon) % (2.*np.pi) 

280 dec = self.fields_init['dec'] + 0 

281 

282 # Now to rotate ra and dec about the x-axis 

283 x, y, z = thetaphi2xyz(ra, dec+np.pi/2.) 

284 xp, yp, zp = rotx(lat, x, y, z) 

285 theta, phi = xyz2thetaphi(xp, yp, zp) 

286 dec = phi - np.pi/2 

287 ra = theta + np.pi 

288 

289 # One more RA rotation 

290 ra = (ra + lon2) % (2.*np.pi) 

291 

292 self.fields['RA'] = ra 

293 self.fields['dec'] = dec 

294 # Rebuild the kdtree with the new positions 

295 # XXX-may be doing some ra,dec to conversions xyz more than needed. 

296 self._hp2fieldsetup(ra, dec) 

297 

298 def smooth_reward(self): 

299 """If we want to smooth the reward function. 

300 """ 

301 if hp.isnpixok(self.reward.size): 

302 # Need to swap NaNs to hp.UNSEEN so smoothing doesn't spread mask 

303 reward_temp = self.reward + 0 

304 mask = np.isnan(reward_temp) 

305 reward_temp[mask] = hp.UNSEEN 

306 self.reward_smooth = hp.sphtfunc.smoothing(reward_temp, 

307 fwhm=self.smoothing_kernel, 

308 verbose=False) 

309 self.reward_smooth[mask] = np.nan 

310 self.reward = self.reward_smooth 

311 #good = ~np.isnan(self.reward_smooth) 

312 # Round off to prevent strange behavior early on 

313 #self.reward_smooth[good] = np.round(self.reward_smooth[good], decimals=4) 

314 

315 def calc_reward_function(self, conditions): 

316 self.reward_checked = True 

317 if self._check_feasibility(conditions): 

318 self.reward = 0 

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

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

321 basis_value = bf(conditions, indx=indx) 

322 self.reward += basis_value*weight 

323 

324 if np.any(np.isinf(self.reward)): 

325 self.reward = np.inf 

326 else: 

327 # If not feasable, negative infinity reward 

328 self.reward = -np.inf 

329 return self.reward 

330 if self.smoothing_kernel is not None: 

331 self.smooth_reward() 

332 

333 if self.area_required is not None: 

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

335 if good_area < self.area_required: 

336 self.reward = -np.inf 

337 

338 return self.reward 

339 

340 def generate_observations_rough(self, conditions): 

341 

342 self.reward = self.calc_reward_function(conditions) 

343 

344 # Check if we need to spin the tesselation 

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

346 self._spin_fields() 

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

348 

349 # XXX Use self.reward to decide what to observe. 

350 return None