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

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

31 ignore_obs=None, survey_name='', nside=None, detailers=None): 

32 if nside is None: 

33 nside = set_default_nside() 

34 if ignore_obs is None: 

35 ignore_obs = [] 

36 

37 if isinstance(ignore_obs, str): 

38 ignore_obs = [ignore_obs] 

39 

40 self.nside = nside 

41 self.survey_name = survey_name 

42 self.ignore_obs = ignore_obs 

43 

44 self.reward = None 

45 self.survey_index = None 

46 

47 self.basis_functions = basis_functions 

48 

49 if extra_features is None: 

50 self.extra_features = {} 

51 else: 

52 self.extra_features = extra_features 

53 self.reward_checked = False 

54 

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

56 self.reward_checked = False 

57 

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

59 if detailers is None: 

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

61 else: 

62 self.detailers = detailers 

63 

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

65 # Check each posible ignore string 

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

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

68 if all(checks): 

69 for feature in self.extra_features: 

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

71 for bf in self.basis_functions: 

72 bf.add_observation(observation, **kwargs) 

73 for detailer in self.detailers: 

74 detailer.add_observation(observation, **kwargs) 

75 self.reward_checked = False 

76 

77 def _check_feasibility(self, conditions): 

78 """ 

79 Check if the survey is feasable in the current conditions 

80 """ 

81 for bf in self.basis_functions: 

82 result = bf.check_feasibility(conditions) 

83 if not result: 

84 return result 

85 return result 

86 

87 def calc_reward_function(self, conditions): 

88 """ 

89 Parameters 

90 ---------- 

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

92 

93 Returns 

94 ------- 

95 reward : float (or array) 

96 

97 """ 

98 if self._check_feasability(): 

99 self.reward = 0 

100 else: 

101 # If we don't pass feasability 

102 self.reward = -np.inf 

103 

104 self.reward_checked = True 

105 return self.reward 

106 

107 def generate_observations_rough(self, conditions): 

108 """ 

109 Returns 

110 ------- 

111 one of: 

112 1) None 

113 2) A list of observations 

114 """ 

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

116 # latest info, calculate it 

117 if not self.reward_checked: 

118 self.reward = self.calc_reward_function(conditions) 

119 obs = empty_observation() 

120 return [obs] 

121 

122 def generate_observations(self, conditions): 

123 observations = self.generate_observations_rough(conditions) 

124 for detailer in self.detailers: 

125 observations = detailer(observations, conditions) 

126 return observations 

127 

128 def viz_config(self): 

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

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

131 pass 

132 

133 

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

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

136 sin_t = np.sin(theta) 

137 cos_t = np.cos(theta) 

138 xp = x 

139 yp = y*cos_t+z*sin_t 

140 zp = -y*sin_t+z*cos_t 

141 return xp, yp, zp 

142 

143 

144class BaseMarkovDF_survey(BaseSurvey): 

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

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

147 methods for dithering and defaults to dithering nightly. 

148 

149 Parameters 

150 ---------- 

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

152 

153 basis_weights : list of float 

154 Must be same length as basis_function 

155 seed : hashable 

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

157 camera : str ('LSST') 

158 Should be 'LSST' or 'comcam' 

159 """ 

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

161 smoothing_kernel=None, 

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

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

164 

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

166 extra_features=extra_features, 

167 ignore_obs=ignore_obs, survey_name=survey_name, 

168 nside=nside, detailers=detailers) 

169 

170 self.basis_weights = basis_weights 

171 # Check that weights and basis functions are same length 

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

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

174 

175 self.camera = camera 

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

177 if self.camera == 'LSST': 

178 self.fields_init = read_fields() 

179 elif self.camera == 'comcam': 

180 self.fields_init = comcamTessellate() 

181 else: 

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

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

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

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

186 

187 if smoothing_kernel is not None: 

188 self.smoothing_kernel = np.radians(smoothing_kernel) 

189 else: 

190 self.smoothing_kernel = None 

191 

192 # Start tracking the night 

193 self.night = -1 

194 

195 # Set the seed 

196 np.random.seed(seed) 

197 self.dither = dither 

198 

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

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

201 resolution is higher than field resolution. 

202 """ 

203 if self.camera == 'LSST': 

204 pointing2hpindx = hp_in_lsst_fov(nside=self.nside) 

205 elif self.camera == 'comcam': 

206 pointing2hpindx = hp_in_comcam_fov(nside=self.nside) 

207 

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

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

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

211 self.hp2fields[hpindx] = i 

212 

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

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

215 

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

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

218 

219 Parameters 

220 ---------- 

221 lon : float (None) 

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

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

224 lat : float (None) 

225 The amount to rotate in latitude (radians). 

226 lon2 : float (None) 

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

228 """ 

229 if lon is None: 

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

231 if lat is None: 

232 # Make sure latitude points spread correctly 

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

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

235 if lon2 is None: 

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

237 # rotate longitude 

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

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

240 

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

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

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

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

245 dec = phi - np.pi/2 

246 ra = theta + np.pi 

247 

248 # One more RA rotation 

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

250 

251 self.fields['RA'] = ra 

252 self.fields['dec'] = dec 

253 # Rebuild the kdtree with the new positions 

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

255 self._hp2fieldsetup(ra, dec) 

256 

257 def smooth_reward(self): 

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

259 """ 

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

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

262 reward_temp = self.reward + 0 

263 mask = np.isnan(reward_temp) 

264 reward_temp[mask] = hp.UNSEEN 

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

266 fwhm=self.smoothing_kernel, 

267 verbose=False) 

268 self.reward_smooth[mask] = np.nan 

269 self.reward = self.reward_smooth 

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

271 # Round off to prevent strange behavior early on 

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

273 

274 def calc_reward_function(self, conditions): 

275 self.reward_checked = True 

276 if self._check_feasibility(conditions): 

277 self.reward = 0 

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

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

280 basis_value = bf(conditions, indx=indx) 

281 self.reward += basis_value*weight 

282 

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

284 self.reward = np.inf 

285 else: 

286 # If not feasable, negative infinity reward 

287 self.reward = -np.inf 

288 return self.reward 

289 if self.smoothing_kernel is not None: 

290 self.smooth_reward() 

291 

292 return self.reward 

293 

294 def generate_observations_rough(self, conditions): 

295 

296 self.reward = self.calc_reward_function(conditions) 

297 

298 # Check if we need to spin the tesselation 

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

300 self._spin_fields() 

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

302 

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

304 return None