Coverage for python/lsst/sims/skybrightness_pre/SkyModelPre.py : 5%

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
10__all__ = ['SkyModelPre', 'interp_angle']
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
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.
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 """
39 # Where are the interpolation points
40 x = np.atleast_1d(x_out)
41 left = np.searchsorted(xp, x)-1
42 right = left+1
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]
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
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 """
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 """
81 self.info = None
82 self.sb = None
83 self.header = None
84 self.filter_names = None
86 self.opsimFields = opsimFields
87 self.verbose = verbose
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')
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]))
117 self.mjd_left = np.array(mjd_left)
118 self.mjd_right = np.array(mjd_right)
120 # Set that nothing is loaded at this point
121 self.loaded_range = np.array([-1])
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'))
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.
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
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)
159 filename = self.files[file_indx]
161 self.loaded_range = np.array([self.mjd_left[file_indx], self.mjd_right[file_indx]])
162 else:
163 self.loaded_range = None
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)
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)
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
199 if self.verbose:
200 print('%s loaded' % os.path.split(filename)[1])
202 if not self.opsimFields:
203 self.nside = hp.npix2nside(self.sb[self.filter_names[0]][0, :].size)
205 if self.loaded_range is None:
206 self.loaded_range = np.array([self.info['mjds'].min(), self.info['mjds'].max()])
208 def returnSunMoon(self, mjd):
209 """
210 Parameters
211 ----------
212 mjd : float
213 Modified Julian Date(s) to interpolate to
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 """
223 #warnings.warn('Method returnSunMoon to be depreciated. Interpolating angles is bad!')
225 keys = ['sunAlts', 'moonAlts', 'moonRAs', 'moonDecs', 'sunRAs',
226 'sunDecs', 'moonSunSep']
228 degrees = [False, False, False, False, False, False, True]
230 if (mjd < self.loaded_range.min() or (mjd > self.loaded_range.max())):
231 self._load_data(mjd)
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
248 def returnAirmass(self, mjd, maxAM=10., indx=None, badval=hp.UNSEEN):
249 """
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
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)
269 left = np.searchsorted(self.info['mjds'], mjd)-1
270 right = left+1
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]
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()
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
308 return airmass
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
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.
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)
348 mask_rules = {'airmass': airmass_mask, 'planet': planet_mask,
349 'moon': moon_mask, 'zenith': zenith_mask}
351 left = np.searchsorted(self.info['mjds'], mjd)-1
352 right = left+1
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
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]
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
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])
429 return sbs