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

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

179 smoothing_kernel=None, 

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

181 dither=True, detailers=None, camera='LSST'): 

182 

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

184 extra_features=extra_features, 

185 ignore_obs=ignore_obs, survey_name=survey_name, 

186 nside=nside, detailers=detailers) 

187 

188 self.basis_weights = basis_weights 

189 # Check that weights and basis functions are same length 

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

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

192 

193 self.camera = camera 

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

195 if self.camera == 'LSST': 

196 self.fields_init = read_fields() 

197 elif self.camera == 'comcam': 

198 self.fields_init = comcamTessellate() 

199 else: 

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

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

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

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

204 

205 if smoothing_kernel is not None: 

206 self.smoothing_kernel = np.radians(smoothing_kernel) 

207 else: 

208 self.smoothing_kernel = None 

209 

210 # Start tracking the night 

211 self.night = -1 

212 

213 # Set the seed 

214 np.random.seed(seed) 

215 self.dither = dither 

216 

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

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

219 resolution is higher than field resolution. 

220 """ 

221 if self.camera == 'LSST': 

222 pointing2hpindx = hp_in_lsst_fov(nside=self.nside) 

223 elif self.camera == 'comcam': 

224 pointing2hpindx = hp_in_comcam_fov(nside=self.nside) 

225 

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

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

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

229 self.hp2fields[hpindx] = i 

230 

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

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

233 

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

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

236 

237 Parameters 

238 ---------- 

239 lon : float (None) 

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

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

242 lat : float (None) 

243 The amount to rotate in latitude (radians). 

244 lon2 : float (None) 

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

246 """ 

247 if lon is None: 

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

249 if lat is None: 

250 # Make sure latitude points spread correctly 

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

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

253 if lon2 is None: 

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

255 # rotate longitude 

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

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

258 

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

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

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

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

263 dec = phi - np.pi/2 

264 ra = theta + np.pi 

265 

266 # One more RA rotation 

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

268 

269 self.fields['RA'] = ra 

270 self.fields['dec'] = dec 

271 # Rebuild the kdtree with the new positions 

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

273 self._hp2fieldsetup(ra, dec) 

274 

275 def smooth_reward(self): 

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

277 """ 

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

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

280 reward_temp = self.reward + 0 

281 mask = np.isnan(reward_temp) 

282 reward_temp[mask] = hp.UNSEEN 

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

284 fwhm=self.smoothing_kernel, 

285 verbose=False) 

286 self.reward_smooth[mask] = np.nan 

287 self.reward = self.reward_smooth 

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

289 # Round off to prevent strange behavior early on 

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

291 

292 def calc_reward_function(self, conditions): 

293 self.reward_checked = True 

294 if self._check_feasibility(conditions): 

295 self.reward = 0 

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

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

298 basis_value = bf(conditions, indx=indx) 

299 self.reward += basis_value*weight 

300 

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

302 self.reward = np.inf 

303 else: 

304 # If not feasable, negative infinity reward 

305 self.reward = -np.inf 

306 return self.reward 

307 if self.smoothing_kernel is not None: 

308 self.smooth_reward() 

309 

310 return self.reward 

311 

312 def generate_observations_rough(self, conditions): 

313 

314 self.reward = self.calc_reward_function(conditions) 

315 

316 # Check if we need to spin the tesselation 

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

318 self._spin_fields() 

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

320 

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

322 return None