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 ignore_obs : list of str (None) 

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

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

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

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

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

28 The detailers to apply to the list of observations. 

29 scheduled_obs : np.array 

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

31 """ 

32 def __init__(self, basis_functions, extra_features=None, 

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

34 scheduled_obs=None): 

35 if nside is None: 

36 nside = set_default_nside() 

37 if ignore_obs is None: 

38 ignore_obs = [] 

39 

40 if isinstance(ignore_obs, str): 

41 ignore_obs = [ignore_obs] 

42 

43 self.nside = nside 

44 self.survey_name = survey_name 

45 self.ignore_obs = ignore_obs 

46 

47 self.reward = None 

48 self.survey_index = None 

49 

50 self.basis_functions = basis_functions 

51 

52 if extra_features is None: 

53 self.extra_features = {} 

54 else: 

55 self.extra_features = extra_features 

56 self.reward_checked = False 

57 

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

59 self.reward_checked = False 

60 

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

62 if detailers is None: 

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

64 else: 

65 self.detailers = detailers 

66 

67 # Scheduled observations 

68 self.scheduled_obs = scheduled_obs 

69 

70 def get_scheduled_obs(self): 

71 return self.scheduled_obs 

72 

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

74 # Check each posible ignore string 

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

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

77 if all(checks): 

78 for feature in self.extra_features: 

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

80 for bf in self.basis_functions: 

81 bf.add_observation(observation, **kwargs) 

82 for detailer in self.detailers: 

83 detailer.add_observation(observation, **kwargs) 

84 self.reward_checked = False 

85 

86 def _check_feasibility(self, conditions): 

87 """ 

88 Check if the survey is feasable in the current conditions 

89 """ 

90 for bf in self.basis_functions: 

91 result = bf.check_feasibility(conditions) 

92 if not result: 

93 return result 

94 return result 

95 

96 def calc_reward_function(self, conditions): 

97 """ 

98 Parameters 

99 ---------- 

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

101 

102 Returns 

103 ------- 

104 reward : float (or array) 

105 

106 """ 

107 if self._check_feasability(): 

108 self.reward = 0 

109 else: 

110 # If we don't pass feasability 

111 self.reward = -np.inf 

112 

113 self.reward_checked = True 

114 return self.reward 

115 

116 def generate_observations_rough(self, conditions): 

117 """ 

118 Returns 

119 ------- 

120 one of: 

121 1) None 

122 2) A list of observations 

123 """ 

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

125 # latest info, calculate it 

126 if not self.reward_checked: 

127 self.reward = self.calc_reward_function(conditions) 

128 obs = empty_observation() 

129 return [obs] 

130 

131 def generate_observations(self, conditions): 

132 observations = self.generate_observations_rough(conditions) 

133 for detailer in self.detailers: 

134 observations = detailer(observations, conditions) 

135 return observations 

136 

137 def viz_config(self): 

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

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

140 pass 

141 

142 

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

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

145 sin_t = np.sin(theta) 

146 cos_t = np.cos(theta) 

147 xp = x 

148 yp = y*cos_t+z*sin_t 

149 zp = -y*sin_t+z*cos_t 

150 return xp, yp, zp 

151 

152 

153class BaseMarkovDF_survey(BaseSurvey): 

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

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

156 methods for dithering and defaults to dithering nightly. 

157 

158 Parameters 

159 ---------- 

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

161 

162 basis_weights : list of float 

163 Must be same length as basis_function 

164 seed : hashable 

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

166 camera : str ('LSST') 

167 Should be 'LSST' or 'comcam' 

168 """ 

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

170 smoothing_kernel=None, 

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

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

173 

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

175 extra_features=extra_features, 

176 ignore_obs=ignore_obs, survey_name=survey_name, 

177 nside=nside, detailers=detailers) 

178 

179 self.basis_weights = basis_weights 

180 # Check that weights and basis functions are same length 

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

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

183 

184 self.camera = camera 

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

186 if self.camera == 'LSST': 

187 self.fields_init = read_fields() 

188 elif self.camera == 'comcam': 

189 self.fields_init = comcamTessellate() 

190 else: 

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

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

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

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

195 

196 if smoothing_kernel is not None: 

197 self.smoothing_kernel = np.radians(smoothing_kernel) 

198 else: 

199 self.smoothing_kernel = None 

200 

201 # Start tracking the night 

202 self.night = -1 

203 

204 # Set the seed 

205 np.random.seed(seed) 

206 self.dither = dither 

207 

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

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

210 resolution is higher than field resolution. 

211 """ 

212 if self.camera == 'LSST': 

213 pointing2hpindx = hp_in_lsst_fov(nside=self.nside) 

214 elif self.camera == 'comcam': 

215 pointing2hpindx = hp_in_comcam_fov(nside=self.nside) 

216 

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

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

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

220 self.hp2fields[hpindx] = i 

221 

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

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

224 

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

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

227 

228 Parameters 

229 ---------- 

230 lon : float (None) 

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

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

233 lat : float (None) 

234 The amount to rotate in latitude (radians). 

235 lon2 : float (None) 

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

237 """ 

238 if lon is None: 

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

240 if lat is None: 

241 # Make sure latitude points spread correctly 

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

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

244 if lon2 is None: 

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

246 # rotate longitude 

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

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

249 

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

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

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

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

254 dec = phi - np.pi/2 

255 ra = theta + np.pi 

256 

257 # One more RA rotation 

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

259 

260 self.fields['RA'] = ra 

261 self.fields['dec'] = dec 

262 # Rebuild the kdtree with the new positions 

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

264 self._hp2fieldsetup(ra, dec) 

265 

266 def smooth_reward(self): 

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

268 """ 

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

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

271 reward_temp = self.reward + 0 

272 mask = np.isnan(reward_temp) 

273 reward_temp[mask] = hp.UNSEEN 

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

275 fwhm=self.smoothing_kernel, 

276 verbose=False) 

277 self.reward_smooth[mask] = np.nan 

278 self.reward = self.reward_smooth 

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

280 # Round off to prevent strange behavior early on 

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

282 

283 def calc_reward_function(self, conditions): 

284 self.reward_checked = True 

285 if self._check_feasibility(conditions): 

286 self.reward = 0 

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

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

289 basis_value = bf(conditions, indx=indx) 

290 self.reward += basis_value*weight 

291 

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

293 self.reward = np.inf 

294 else: 

295 # If not feasable, negative infinity reward 

296 self.reward = -np.inf 

297 return self.reward 

298 if self.smoothing_kernel is not None: 

299 self.smooth_reward() 

300 

301 return self.reward 

302 

303 def generate_observations_rough(self, conditions): 

304 

305 self.reward = self.calc_reward_function(conditions) 

306 

307 # Check if we need to spin the tesselation 

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

309 self._spin_fields() 

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

311 

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

313 return None