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

1from builtins import object 

2import numpy as np 

3import glob 

4import os 

5import healpy as hp 

6from lsst.utils import getPackageDir 

7import warnings 

8from lsst.sims.utils import _angularSeparation 

9 

10__all__ = ['SkyModelPre', 'interp_angle'] 

11 

12 

13def shortAngleDist(a0, a1): 

14 """ 

15 from https://gist.github.com/shaunlebron/8832585 

16 """ 

17 max_angle = 2.*np.pi 

18 da = (a1 - a0) % max_angle 

19 return 2.*da % max_angle - da 

20 

21 

22def interp_angle(x_out, xp, anglep, degrees=False): 

23 """ 

24 Interpolate angle values (handle wrap around properly). Does nearest neighbor 

25 interpolation if values out of range. 

26 

27 Parameters 

28 ---------- 

29 x_out : float (or array) 

30 The points to interpolate to. 

31 xp : array 

32 Points to interpolate between (must be sorted) 

33 anglep : array 

34 The angles ascociated with xp 

35 degrees : bool (False) 

36 Set if anglep is degrees (True) or radidian (False) 

37 """ 

38 

39 # Where are the interpolation points 

40 x = np.atleast_1d(x_out) 

41 left = np.searchsorted(xp, x)-1 

42 right = left+1 

43 

44 # If we are out of bounds, just use the edges 

45 right[np.where(right >= xp.size)] -= 1 

46 left[np.where(left < 0)] += 1 

47 baseline = xp[right] - xp[left] 

48 

49 wterm = (x - xp[left])/baseline 

50 wterm[np.where(baseline == 0)] = 0 

51 if degrees: 

52 result = np.radians(anglep[left]) + shortAngleDist(np.radians(anglep[left]), np.radians(anglep[right]))*wterm 

53 result = result % (2.*np.pi) 

54 result = np.degrees(result) 

55 else: 

56 result = anglep[left] + shortAngleDist(anglep[left], anglep[right])*wterm 

57 result = result % (2.*np.pi) 

58 return result 

59 

60 

61class SkyModelPre(object): 

62 """ 

63 Load pre-computed sky brighntess maps for the LSST site and use them to interpolate to 

64 arbitrary dates. 

65 """ 

66 

67 def __init__(self, data_path=None, opsimFields=False, 

68 speedLoad=True, verbose=False): 

69 """ 

70 Parameters 

71 ---------- 

72 data_path : str (None) 

73 path to the numpy save files. Looks in standard plances if set to None. 

74 opsimFields : bool (False) 

75 Mostly depreciated, if True, loads sky brightnesses computed at field centers. 

76 Otherwise uses healpixels. 

77 speedLoad : bool (True) 

78 If True, use the small 3-day file to load found in the usual spot. 

79 """ 

80 

81 self.info = None 

82 self.sb = None 

83 self.header = None 

84 self.filter_names = None 

85 

86 self.opsimFields = opsimFields 

87 self.verbose = verbose 

88 

89 # Look in default location for .npz files to load 

90 if 'SIMS_SKYBRIGHTNESS_DATA' in os.environ: 

91 data_dir = os.environ['SIMS_SKYBRIGHTNESS_DATA'] 

92 else: 

93 data_dir = os.path.join(getPackageDir('sims_skybrightness_pre'), 'data') 

94 

95 if data_path is None: 

96 if opsimFields: 

97 data_path = os.path.join(data_dir, 'opsimFields') 

98 else: 

99 data_path = os.path.join(data_dir, 'healpix') 

100 # Expect filenames of the form mjd1_mjd2.npz, e.g., 59632.155_59633.2.npz 

101 self.files = glob.glob(os.path.join(data_path, '*.npz')) 

102 if len(self.files) == 0: 

103 errmssg = 'Failed to find pre-computed .npz files. ' 

104 errmssg += 'Copy data from NCSA with sims_skybrightness_pre/data/data_down.sh \n' 

105 errmssg += 'or build by running sims_skybrightness_pre/data/generate_sky.py' 

106 warnings.warn(errmssg) 

107 self.filesizes = np.array([os.path.getsize(filename) for filename in self.files]) 

108 mjd_left = [] 

109 mjd_right = [] 

110 # glob does not always order things I guess? 

111 self.files.sort() 

112 for filename in self.files: 

113 temp = os.path.split(filename)[-1].replace('.npz', '').split('_') 

114 mjd_left.append(float(temp[0])) 

115 mjd_right.append(float(temp[1])) 

116 

117 self.mjd_left = np.array(mjd_left) 

118 self.mjd_right = np.array(mjd_right) 

119 

120 # Set that nothing is loaded at this point 

121 self.loaded_range = np.array([-1]) 

122 

123 # Go ahead and load the small one in the repo by default 

124 if speedLoad: 

125 self._load_data(59853., 

126 filename=os.path.join(data_dir, 'healpix/59853_59856.npz'), 

127 npyfile=os.path.join(data_dir, 'healpix/59853_59856.npy')) 

128 

129 def _load_data(self, mjd, filename=None, npyfile=None): 

130 """ 

131 Load up the .npz file to interpolate things. After python 3 upgrade, numpy.savez refused 

132 to write large .npz files, so data is split between .npz and .npy files. 

133 

134 Parameters 

135 ---------- 

136 mjd : float 

137 The Modified Julian Date that we want to load 

138 filename : str (None) 

139 The filename to restore. If None, it checks the filenames on disk to find one that 

140 should have the requested MJD 

141 npyfile : str (None) 

142 If sky brightness data not in npz file, checks the .npy file with same root name. 

143 """ 

144 del self.info 

145 del self.sb 

146 del self.header 

147 del self.filter_names 

148 

149 if filename is None: 

150 # Figure out which file to load. 

151 file_indx = np.where((mjd >= self.mjd_left) & (mjd <= self.mjd_right))[0] 

152 if np.size(file_indx) == 0: 

153 raise ValueError('MJD = %f is out of range for the files found (%f-%f)' % (mjd, 

154 self.mjd_left.min(), 

155 self.mjd_right.max())) 

156 # Just take the later one, assuming we're probably simulating forward in time 

157 file_indx = np.max(file_indx) 

158 

159 filename = self.files[file_indx] 

160 

161 self.loaded_range = np.array([self.mjd_left[file_indx], self.mjd_right[file_indx]]) 

162 else: 

163 self.loaded_range = None 

164 

165 if self.verbose: 

166 print('Loading file %s' % filename) 

167 # Add encoding kwarg to restore Python 2.7 generated files 

168 data = np.load(filename, encoding='bytes', allow_pickle=True) 

169 self.info = data['dict_of_lists'][()] 

170 self.header = data['header'][()] 

171 if 'sky_brightness' in data.keys(): 

172 self.sb = data['sky_brightness'][()] 

173 data.close() 

174 else: 

175 # the sky brightness had to go in it's own npy file 

176 data.close() 

177 if npyfile is None: 

178 npyfile = filename[:-3]+'npy' 

179 self.sb = np.load(npyfile) 

180 if self.verbose: 

181 print('also loading %s' % npyfile) 

182 

183 # Step to make sure keys are strings not bytes 

184 all_dicts = [self.info, self.sb, self.header] 

185 all_dicts = [single_dict for single_dict in all_dicts if hasattr(single_dict, 'keys')] 

186 for selfDict in all_dicts: 

187 for key in list(selfDict.keys()): 

188 if type(key) != str: 

189 selfDict[key.decode("utf-8")] = selfDict.pop(key) 

190 

191 # Ugh, different versions of the save files could have dicts or np.array. 

192 # Let's hope someone fits some Fourier components to the sky brightnesses and gets rid 

193 # of the giant save files for good. 

194 if hasattr(self.sb, 'keys'): 

195 self.filter_names = list(self.sb.keys()) 

196 else: 

197 self.filter_names = self.sb.dtype.names 

198 

199 if self.verbose: 

200 print('%s loaded' % os.path.split(filename)[1]) 

201 

202 if not self.opsimFields: 

203 self.nside = hp.npix2nside(self.sb[self.filter_names[0]][0, :].size) 

204 

205 if self.loaded_range is None: 

206 self.loaded_range = np.array([self.info['mjds'].min(), self.info['mjds'].max()]) 

207 

208 def returnSunMoon(self, mjd): 

209 """ 

210 Parameters 

211 ---------- 

212 mjd : float 

213 Modified Julian Date(s) to interpolate to 

214 

215 Returns 

216 ------- 

217 sunMoon : dict 

218 Dict with keys for the sun and moon RA and Dec and the 

219 mooon-sun separation. All values in radians, except for moonSunSep 

220 that is in degrees for some reason (that reason is probably because I'm sloppy). 

221 """ 

222 

223 #warnings.warn('Method returnSunMoon to be depreciated. Interpolating angles is bad!') 

224 

225 keys = ['sunAlts', 'moonAlts', 'moonRAs', 'moonDecs', 'sunRAs', 

226 'sunDecs', 'moonSunSep'] 

227 

228 degrees = [False, False, False, False, False, False, True] 

229 

230 if (mjd < self.loaded_range.min() or (mjd > self.loaded_range.max())): 

231 self._load_data(mjd) 

232 

233 result = {} 

234 for key, degree in zip(keys, degrees): 

235 if key[-1] == 's': 

236 newkey = key[:-1] 

237 else: 

238 newkey = key 

239 if 'RA' in key: 

240 result[newkey] = interp_angle(mjd, self.info['mjds'], self.info[key], degrees=degree) 

241 # Return a scalar if only doing 1 date. 

242 if np.size(result[newkey]) == 1: 

243 result[newkey] = np.max(result[newkey]) 

244 else: 

245 result[newkey] = np.interp(mjd, self.info['mjds'], self.info[key]) 

246 return result 

247 

248 def returnAirmass(self, mjd, maxAM=10., indx=None, badval=hp.UNSEEN): 

249 """ 

250 

251 Parameters 

252 ---------- 

253 mjd : float 

254 Modified Julian Date to interpolate to 

255 indx : List of int(s) (None) 

256 indices to interpolate the sky values at. Returns full sky if None. If the class was 

257 instatiated with opsimFields, indx is the field ID, otherwise it is the healpix ID. 

258 maxAM : float (10) 

259 The maximum airmass to return, everything above this airmass will be set to badval 

260 

261 Returns 

262 ------- 

263 airmass : np.array 

264 Array of airmass values. If the MJD is between sunrise and sunset, all values are masked. 

265 """ 

266 if (mjd < self.loaded_range.min() or (mjd > self.loaded_range.max())): 

267 self._load_data(mjd) 

268 

269 left = np.searchsorted(self.info['mjds'], mjd)-1 

270 right = left+1 

271 

272 # If we are out of bounds 

273 if right >= self.info['mjds'].size: 

274 right -= 1 

275 baseline = 1. 

276 elif left < 0: 

277 left += 1 

278 baseline = 1. 

279 else: 

280 baseline = self.info['mjds'][right] - self.info['mjds'][left] 

281 

282 if indx is None: 

283 result_size = self.sb[self.filter_names[0]][left, :].size 

284 indx = np.arange(result_size) 

285 else: 

286 result_size = len(indx) 

287 # Check if we are between sunrise/set 

288 if baseline > self.header['timestep_max']: 

289 warnings.warn('Requested MJD between sunrise and sunset, returning closest maps') 

290 diff = np.abs(self.info['mjds'][left.max():right.max()+1]-mjd) 

291 closest_indx = np.array([left, right])[np.where(diff == np.min(diff))] 

292 airmass = self.info['airmass'][closest_indx, indx] 

293 mask = np.where((self.info['airmass'][closest_indx, indx].ravel() < 1.) | 

294 (self.info['airmass'][closest_indx, indx].ravel() > maxAM)) 

295 airmass = airmass.ravel() 

296 

297 else: 

298 wterm = (mjd - self.info['mjds'][left])/baseline 

299 w1 = (1. - wterm) 

300 w2 = wterm 

301 airmass = self.info['airmass'][left, indx] * w1 + self.info['airmass'][right, indx] * w2 

302 mask = np.where((self.info['airmass'][left, indx] < 1.) | 

303 (self.info['airmass'][left, indx] > maxAM) | 

304 (self.info['airmass'][right, indx] < 1.) | 

305 (self.info['airmass'][right, indx] > maxAM)) 

306 airmass[mask] = badval 

307 

308 return airmass 

309 

310 def returnMags(self, mjd, indx=None, airmass_mask=True, planet_mask=True, 

311 moon_mask=True, zenith_mask=True, badval=hp.UNSEEN, 

312 filters=['u', 'g', 'r', 'i', 'z', 'y'], extrapolate=False): 

313 """ 

314 Return a full sky map or individual pixels for the input mjd 

315 

316 Parameters 

317 ---------- 

318 mjd : float 

319 Modified Julian Date to interpolate to 

320 indx : List of int(s) (None) 

321 indices to interpolate the sky values at. Returns full sky if None. If the class was 

322 instatiated with opsimFields, indx is the field ID, otherwise it is the healpix ID. 

323 airmass_mask : bool (True) 

324 Set high (>2.5) airmass pixels to badval. 

325 planet_mask : bool (True) 

326 Set sky maps to badval near (2 degrees) bright planets. 

327 moon_mask : bool (True) 

328 Set sky maps near (10 degrees) the moon to badval. 

329 zenith_mask : bool (True) 

330 Set sky maps at high altitude (>86.5) to badval. 

331 badval : float (-1.6375e30) 

332 Mask value. Defaults to the healpy mask value. 

333 filters : list 

334 List of strings for the filters that should be returned. 

335 extrapolate : bool (False) 

336 In indx is set, extrapolate any masked pixels to be the same as the nearest non-masked 

337 value from the full sky map. 

338 

339 Returns 

340 ------- 

341 sbs : dict 

342 A dictionary with filter names as keys and np.arrays as values which 

343 hold the sky brightness maps in mag/sq arcsec. 

344 """ 

345 if (mjd < self.loaded_range.min() or (mjd > self.loaded_range.max())): 

346 self._load_data(mjd) 

347 

348 mask_rules = {'airmass': airmass_mask, 'planet': planet_mask, 

349 'moon': moon_mask, 'zenith': zenith_mask} 

350 

351 left = np.searchsorted(self.info['mjds'], mjd)-1 

352 right = left+1 

353 

354 # Do full sky by default 

355 if indx is None: 

356 indx = np.arange(self.sb['r'].shape[1]) 

357 full_sky = True 

358 else: 

359 full_sky = False 

360 

361 # If we are out of bounds 

362 if right >= self.info['mjds'].size: 

363 right -= 1 

364 baseline = 1. 

365 elif left < 0: 

366 left += 1 

367 baseline = 1. 

368 else: 

369 baseline = self.info['mjds'][right] - self.info['mjds'][left] 

370 

371 # Check if we are between sunrise/set 

372 if baseline > self.header['timestep_max']: 

373 warnings.warn('Requested MJD between sunrise and sunset, returning closest maps') 

374 diff = np.abs(self.info['mjds'][left.max():right.max()+1]-mjd) 

375 closest_indx = np.array([left, right])[np.where(diff == np.min(diff))].min() 

376 sbs = {} 

377 for filter_name in filters: 

378 sbs[filter_name] = self.sb[filter_name][closest_indx, indx] 

379 for mask_name in mask_rules: 

380 if mask_rules[mask_name]: 

381 toMask = np.where(self.info[mask_name+'_masks'][closest_indx, indx]) 

382 sbs[filter_name][toMask] = badval 

383 sbs[filter_name][np.isinf(sbs[filter_name])] = badval 

384 sbs[filter_name][np.where(sbs[filter_name] == hp.UNSEEN)] = badval 

385 else: 

386 wterm = (mjd - self.info['mjds'][left])/baseline 

387 w1 = (1. - wterm) 

388 w2 = wterm 

389 sbs = {} 

390 for filter_name in filters: 

391 sbs[filter_name] = self.sb[filter_name][left, indx] * w1 + \ 

392 self.sb[filter_name][right, indx] * w2 

393 for mask_name in mask_rules: 

394 if mask_rules[mask_name]: 

395 toMask = np.where(self.info[mask_name+'_masks'][left, indx] | 

396 self.info[mask_name+'_masks'][right, indx] | 

397 np.isinf(sbs[filter_name])) 

398 sbs[filter_name][toMask] = badval 

399 sbs[filter_name][np.where(sbs[filter_name] == hp.UNSEEN)] = badval 

400 sbs[filter_name][np.where(sbs[filter_name] == hp.UNSEEN)] = badval 

401 

402 # If requested a certain pixel(s), and want to extrapolate. 

403 if (not full_sky) & extrapolate: 

404 masked_pix = False 

405 for filter_name in filters: 

406 if (badval in sbs[filter_name]) | (True in np.isnan(sbs[filter_name])): 

407 masked_pix = True 

408 if masked_pix: 

409 # We have pixels that are masked that we want reasonable values for 

410 full_sky_sb = self.returnMags(mjd, airmass_mask=False, planet_mask=False, moon_mask=False, 

411 zenith_mask=False, filters=filters) 

412 good = np.where((full_sky_sb[filters[0]] != badval) & ~np.isnan(full_sky_sb[filters[0]]))[0] 

413 ra_full = np.radians(self.header['ra'][good]) 

414 dec_full = np.radians(self.header['dec'][good]) 

415 for filtername in filters: 

416 full_sky_sb[filtername] = full_sky_sb[filtername][good] 

417 # Going to assume the masked pixels are the same in all filters 

418 masked_indx = np.where((sbs[filters[0]].ravel() == badval) | 

419 np.isnan(sbs[filters[0]].ravel()))[0] 

420 for i, mi in enumerate(masked_indx): 

421 # Note, this is going to be really slow for many pixels, should use a kdtree 

422 dist = _angularSeparation(np.radians(self.header['ra'][indx[i]]), 

423 np.radians(self.header['dec'][indx[i]]), 

424 ra_full, dec_full) 

425 closest = np.where(dist == dist.min())[0] 

426 for filtername in filters: 

427 sbs[filtername].ravel()[mi] = np.min(full_sky_sb[filtername][closest]) 

428 

429 return sbs