Coverage for python/lsst/sims/skybrightness/skyModel.py : 6%

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 zip
2from builtins import object
3import numpy as np
4import ephem
5from lsst.sims.utils import (haversine, _raDecFromAltAz, _altAzPaFromRaDec, Site,
6 ObservationMetaData, _approx_altAz2RaDec, _approx_RaDec2AltAz)
7import warnings
8from lsst.sims.skybrightness.utils import wrapRA, mjd2djd
9from .interpComponents import (ScatteredStar, Airglow, LowerAtm, UpperAtm, MergedSpec, TwilightInterp,
10 MoonInterp, ZodiacalInterp)
11from lsst.sims.photUtils import Sed
14__all__ = ['justReturn', 'SkyModel']
17def justReturn(inval):
18 """
19 Really, just return the input.
21 Parameters
22 ----------
23 input : anything
25 Returns
26 -------
27 input : anything
28 Just return whatever you sent in.
29 """
30 return inval
33def inrange(inval, minimum=-1., maximum=1.):
34 """
35 Make sure values are within min/max
36 """
37 inval = np.array(inval)
38 below = np.where(inval < minimum)
39 inval[below] = minimum
40 above = np.where(inval > maximum)
41 inval[above] = maximum
42 return inval
45def calcAzRelMoon(azs, moonAz):
46 azRelMoon = wrapRA(azs - moonAz)
47 if isinstance(azs, np.ndarray):
48 over = np.where(azRelMoon > np.pi)
49 azRelMoon[over] = 2. * np.pi - azRelMoon[over]
50 else:
51 if azRelMoon > np.pi:
52 azRelMoon = 2.0 * np.pi - azRelMoon
53 return azRelMoon
56class SkyModel(object):
58 def __init__(self, observatory='LSST',
59 twilight=True, zodiacal=True, moon=True,
60 airglow=True, lowerAtm=False, upperAtm=False, scatteredStar=False,
61 mergedSpec=True, mags=False, preciseAltAz=False, airmass_limit=3.0):
62 """
63 Instatiate the SkyModel. This loads all the required template spectra/magnitudes
64 that will be used for interpolation.
66 Parameters
67 ----------
68 Observatory : Site object
69 object with attributes lat, lon, elev. But default loads LSST.
71 twilight : bool (True)
72 Include twilight component (True)
73 zodiacal : bool (True)
74 Include zodiacal light component (True)
75 moon : bool (True)
76 Include scattered moonlight component (True)
77 airglow : bool (True)
78 Include airglow component
79 lowerAtm : bool (False)
80 Include lower atmosphere component. This component is part of `mergedSpec`.
81 upperAtm : bool (False)
82 Include upper atmosphere component. This component is part of `mergedSpec`.
83 scatteredStar : bool (False)
84 Include scattered starlight component. This component is part of `mergedSpec`.
85 mergedSpec : bool (True)
86 Compute the lowerAtm, upperAtm, and scatteredStar simultaneously since they are all
87 functions of only airmass.
88 mags : bool (False)
89 By default, the sky model computes a 17,001 element spectrum. If `mags` is True,
90 the model will return the LSST ugrizy magnitudes (in that order).
91 preciseAltAz : bool (False)
92 If False, use the fast alt, az to ra, dec coordinate
93 transformations that do not take abberation, diffraction, etc
94 into account. Results in errors up to ~1.5 degrees,
95 but an order of magnitude faster than coordinate transforms in sims_utils.
96 airmass_limit : float (3.0)
97 Most of the models are only accurate to airmass 3.0. If set higher, airmass values
98 higher than 3.0 are set to 3.0.
99 """
101 self.moon = moon
102 self.lowerAtm = lowerAtm
103 self.twilight = twilight
104 self.zodiacal = zodiacal
105 self.upperAtm = upperAtm
106 self.airglow = airglow
107 self.scatteredStar = scatteredStar
108 self.mergedSpec = mergedSpec
109 self.mags = mags
110 self.preciseAltAz = preciseAltAz
112 # set this as a way to track if coords have been set
113 self.azs = None
115 # Airmass limit.
116 self.airmassLimit = airmass_limit
118 if self.mags:
119 self.npix = 6
120 else:
121 self.npix = 11001
123 self.components = {'moon': self.moon, 'lowerAtm': self.lowerAtm, 'twilight': self.twilight,
124 'upperAtm': self.upperAtm, 'airglow': self.airglow, 'zodiacal': self.zodiacal,
125 'scatteredStar': self.scatteredStar, 'mergedSpec': self.mergedSpec}
127 # Check that the merged component isn't being run with other components
128 mergedComps = [self.lowerAtm, self.upperAtm, self.scatteredStar]
129 for comp in mergedComps:
130 if comp & self.mergedSpec:
131 warnings.warn("Adding component multiple times to the final output spectra.")
133 interpolators = {'scatteredStar': ScatteredStar, 'airglow': Airglow, 'lowerAtm': LowerAtm,
134 'upperAtm': UpperAtm, 'mergedSpec': MergedSpec, 'moon': MoonInterp,
135 'zodiacal': ZodiacalInterp, 'twilight': TwilightInterp}
137 # Load up the interpolation objects for each component
138 self.interpObjs = {}
139 for key in self.components:
140 if self.components[key]:
141 self.interpObjs[key] = interpolators[key](mags=self.mags)
143 # Set up a pyephem observatory object
144 if hasattr(observatory, 'latitude_rad') & hasattr(observatory, 'longitude_rad') & hasattr(observatory, 'height'):
145 self.telescope = observatory
146 self.Observatory = ephem.Observer()
147 self.Observatory.lat = self.telescope.latitude_rad
148 self.Observatory.lon = self.telescope.longitude_rad
149 self.Observatory.elevation = self.telescope.height
150 elif observatory == 'LSST':
151 self.telescope = Site('LSST')
152 self.Observatory = ephem.Observer()
153 self.Observatory.lat = self.telescope.latitude_rad
154 self.Observatory.lon = self.telescope.longitude_rad
155 self.Observatory.elevation = self.telescope.height
156 else:
157 self.Observatory = observatory
159 # Note that observing conditions have not been set
160 self.paramsSet = False
162 def _initPoints(self):
163 """
164 Set up an array for all the interpolation points
165 """
167 names = ['airmass', 'nightTimes', 'alt', 'az', 'azRelMoon', 'moonSunSep', 'moonAltitude',
168 'altEclip', 'azEclipRelSun', 'sunAlt', 'azRelSun', 'solarFlux']
169 types = [float]*len(names)
170 self.points = np.zeros(self.npts, list(zip(names, types)))
172 def setRaDecMjd(self, lon, lat, mjd, degrees=False, azAlt=False, solarFlux=130.,
173 filterNames=['u', 'g', 'r', 'i', 'z', 'y']):
174 """
175 Set the sky parameters by computing the sky conditions on a given MJD and sky location.
179 lon: Longitude-like (RA or Azimuth). Can be single number, list, or numpy array
180 lat: Latitude-like (Dec or Altitude)
181 mjd: Modified Julian Date for the calculation. Must be single number.
182 degrees: (False) Assumes lon and lat are radians unless degrees=True
183 azAlt: (False) Assume lon, lat are RA, Dec unless azAlt=True
184 solarFlux: solar flux in SFU Between 50 and 310. Default=130. 1 SFU=10^4 Jy.
185 filterNames: list of fitlers to return magnitudes for (if initialized with mags=True).
186 """
187 self.filterNames = filterNames
188 if self.mags:
189 self.npix = len(self.filterNames)
190 # Wrap in array just in case single points were passed
191 if np.size(lon) == 1:
192 lon = np.array([lon]).ravel()
193 lat = np.array([lat]).ravel()
194 else:
195 lon = np.array(lon)
196 lat = np.array(lat)
197 if degrees:
198 self.ra = np.radians(lon)
199 self.dec = np.radians(lat)
200 else:
201 self.ra = lon
202 self.dec = lat
203 if np.size(mjd) > 1:
204 raise ValueError('mjd must be single value.')
205 self.mjd = mjd
206 if azAlt:
207 self.azs = self.ra.copy()
208 self.alts = self.dec.copy()
209 if self.preciseAltAz:
210 self.ra, self.dec = _raDecFromAltAz(self.alts, self.azs,
211 ObservationMetaData(mjd=self.mjd, site=self.telescope))
212 else:
213 self.ra, self.dec = _approx_altAz2RaDec(self.alts, self.azs,
214 self.telescope.latitude_rad,
215 self.telescope.longitude_rad, mjd)
216 else:
217 if self.preciseAltAz:
218 self.alts, self.azs, pa = _altAzPaFromRaDec(self.ra, self.dec,
219 ObservationMetaData(mjd=self.mjd,
220 site=self.telescope))
221 else:
222 self.alts, self.azs = _approx_RaDec2AltAz(self.ra, self.dec,
223 self.telescope.latitude_rad,
224 self.telescope.longitude_rad, mjd)
226 self.npts = self.ra.size
227 self._initPoints()
229 self.solarFlux = solarFlux
230 self.points['solarFlux'] = self.solarFlux
232 self._setupPointGrid()
234 self.paramsSet = True
236 # Interpolate the templates to the set paramters
237 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
238 if self.goodPix.size > 0:
239 self._interpSky()
240 else:
241 warnings.warn('No valid points to interpolate')
243 def setRaDecAltAzMjd(self, ra, dec, alt, az, mjd, degrees=False, solarFlux=130.,
244 filterNames=['u', 'g', 'r', 'i', 'z', 'y']):
245 """
246 Set the sky parameters by computing the sky conditions on a given MJD and sky location.
248 Use if you already have alt az coordinates so you can skip the coordinate conversion.
249 """
250 self.filterNames = filterNames
251 if self.mags:
252 self.npix = len(self.filterNames)
253 # Wrap in array just in case single points were passed
254 if not type(ra).__module__ == np.__name__:
255 if np.size(ra) == 1:
256 ra = np.array([ra]).ravel()
257 dec = np.array([dec]).ravel()
258 alt = np.array(alt).ravel()
259 az = np.array(az).ravel()
260 else:
261 ra = np.array(ra)
262 dec = np.array(dec)
263 alt = np.array(alt)
264 az = np.array(az)
265 if degrees:
266 self.ra = np.radians(ra)
267 self.dec = np.radians(dec)
268 self.alts = np.radians(alt)
269 self.azs = np.radians(az)
270 else:
271 self.ra = ra
272 self.dec = dec
273 self.azs = az
274 self.alts = alt
275 if np.size(mjd) > 1:
276 raise ValueError('mjd must be single value.')
277 self.mjd = mjd
279 self.npts = self.ra.size
280 self._initPoints()
282 self.solarFlux = solarFlux
283 self.points['solarFlux'] = self.solarFlux
285 self._setupPointGrid()
287 self.paramsSet = True
289 # Interpolate the templates to the set paramters
290 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
291 if self.goodPix.size > 0:
292 self._interpSky()
293 else:
294 warnings.warn('No valid points to interpolate')
296 def getComputedVals(self):
297 """
298 Return the intermediate values that are caluculated by setRaDecMjd and used for interpolation.
299 All of these values are also accesible as class atributes, this is a convience method to grab them
300 all at once and document the formats.
302 Returns
303 -------
304 out : dict
305 Dictionary of all the intermediate calculated values that may be of use outside
306 (the key:values in the output dict)
307 ra : numpy.array
308 RA of the interpolation points (radians)
309 dec : np.array
310 Dec of the interpolation points (radians)
311 alts : np.array
312 Altitude (radians)
313 azs : np.array
314 Azimuth of interpolation points (radians)
315 airmass : np.array
316 Airmass values for each point, computed via 1./np.cos(np.pi/2.-self.alts).
317 solarFlux : float
318 The solar flux used (SFU).
319 sunAz : float
320 Azimuth of the sun (radians)
321 sunAlt : float
322 Altitude of the sun (radians)
323 sunRA : float
324 RA of the sun (radians)
325 sunDec : float
326 Dec of the sun (radians)
327 azRelSun : np.array
328 Azimuth of each point relative to the sun (0=same direction as sun) (radians)
329 moonAz : float
330 Azimuth of the moon (radians)
331 moonAlt : float
332 Altitude of the moon (radians)
333 moonRA : float
334 RA of the moon (radians)
335 moonDec : float
336 Dec of the moon (radians). Note, if you want distances
337 moonPhase : float
338 Phase of the moon (0-100)
339 moonSunSep : float
340 Seperation of moon and sun (degrees)
341 azRelMoon : np.array
342 Azimuth of each point relative to teh moon
343 eclipLon : np.array
344 Ecliptic longitude (radians) of each point
345 eclipLat : np.array
346 Ecliptic latitude (radians) of each point
347 sunEclipLon: np.array
348 Ecliptic longitude (radians) of each point with the sun at longitude zero
350 Note that since the alt and az can be calculated using the fast approximation, if one wants
351 to compute the distance between the the points and the sun or moon, it is probably better to
352 use the ra,dec positions rather than the alt,az positions.
353 """
355 result = {}
356 attributes = ['ra', 'dec', 'alts', 'azs', 'airmass', 'solarFlux', 'moonPhase',
357 'moonAz', 'moonAlt', 'sunAlt', 'sunAz', 'azRelSun', 'moonSunSep',
358 'azRelMoon', 'eclipLon', 'eclipLat', 'moonRA', 'moonDec', 'sunRA',
359 'sunDec', 'sunEclipLon']
361 for attribute in attributes:
362 if hasattr(self, attribute):
363 result[attribute] = getattr(self, attribute)
364 else:
365 result[attribute] = None
367 return result
369 def _setupPointGrid(self):
370 """
371 Setup the points for the interpolation functions.
372 """
373 # Switch to Dublin Julian Date for pyephem
374 self.Observatory.date = mjd2djd(self.mjd)
376 sun = ephem.Sun()
377 sun.compute(self.Observatory)
378 self.sunAlt = sun.alt
379 self.sunAz = sun.az
380 self.sunRA = sun.ra
381 self.sunDec = sun.dec
383 # Compute airmass the same way as ESO model
384 self.airmass = 1./np.cos(np.pi/2.-self.alts)
386 self.points['airmass'] = self.airmass
387 self.points['nightTimes'] = 0
388 self.points['alt'] = self.alts
389 self.points['az'] = self.azs
391 if self.twilight:
392 self.points['sunAlt'] = self.sunAlt
393 self.azRelSun = wrapRA(self.azs - self.sunAz)
394 self.points['azRelSun'] = self.azRelSun
396 if self.moon:
397 moon = ephem.Moon()
398 moon.compute(self.Observatory)
399 self.moonPhase = moon.phase
400 self.moonAlt = moon.alt
401 self.moonAz = moon.az
402 self.moonRA = moon.ra
403 self.moonDec = moon.dec
404 # Calc azimuth relative to moon
405 self.azRelMoon = calcAzRelMoon(self.azs, self.moonAz)
406 self.moonTargSep = haversine(self.azs, self.alts, self.moonAz, self.moonAlt)
407 self.points['moonAltitude'] += np.degrees(self.moonAlt)
408 self.points['azRelMoon'] += self.azRelMoon
409 self.moonSunSep = self.moonPhase/100.*180.
410 self.points['moonSunSep'] += self.moonSunSep
412 if self.zodiacal:
413 self.eclipLon = np.zeros(self.npts)
414 self.eclipLat = np.zeros(self.npts)
416 for i, temp in enumerate(self.ra):
417 eclip = ephem.Ecliptic(ephem.Equatorial(self.ra[i], self.dec[i], epoch='2000'))
418 self.eclipLon[i] += eclip.lon
419 self.eclipLat[i] += eclip.lat
420 # Subtract off the sun ecliptic longitude
421 sunEclip = ephem.Ecliptic(sun)
422 self.sunEclipLon = sunEclip.lon
423 self.points['altEclip'] += self.eclipLat
424 self.points['azEclipRelSun'] += wrapRA(self.eclipLon - self.sunEclipLon)
426 self.mask = np.where((self.airmass > self.airmassLimit) | (self.airmass < 1.))[0]
427 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
429 def setParams(self, airmass=1., azs=90., alts=None, moonPhase=31.67, moonAlt=45.,
430 moonAz=0., sunAlt=-12., sunAz=0., sunEclipLon=0.,
431 eclipLon=135., eclipLat=90., degrees=True, solarFlux=130.,
432 filterNames=['u', 'g', 'r', 'i', 'z', 'y']):
433 """
434 Set parameters manually.
435 Note, you can put in unphysical combinations of paramters if you want to
436 (e.g., put a full moon at zenith at sunset).
437 if the alts kwarg is set it will override the airmass kwarg.
438 MoonPhase is percent of moon illuminated (0-100)
439 """
441 # Convert all values to radians for internal use.
442 self.filterNames = filterNames
443 if self.mags:
444 self.npix = len(self.filterNames)
445 if degrees:
446 convertFunc = np.radians
447 else:
448 convertFunc = justReturn
450 self.solarFlux = solarFlux
451 self.sunAlt = convertFunc(sunAlt)
452 self.moonPhase = moonPhase
453 self.moonAlt = convertFunc(moonAlt)
454 self.moonAz = convertFunc(moonAz)
455 self.eclipLon = convertFunc(eclipLon)
456 self.eclipLat = convertFunc(eclipLat)
457 self.sunEclipLon = convertFunc(sunEclipLon)
458 self.azs = convertFunc(azs)
459 if alts is not None:
460 self.airmass = 1./np.cos(np.pi/2.-convertFunc(alts))
461 self.alts = convertFunc(alts)
462 else:
463 self.airmass = airmass
464 self.alts = np.pi/2.-np.arccos(1./airmass)
465 self.moonTargSep = haversine(self.azs, self.alts, moonAz, self.moonAlt)
466 self.npts = np.size(self.airmass)
467 self._initPoints()
469 self.points['airmass'] = self.airmass
470 self.points['nightTimes'] = 0
471 self.points['alt'] = self.alts
472 self.points['az'] = self.azs
473 self.azRelMoon = calcAzRelMoon(self.azs, self.moonAz)
474 self.points['moonAltitude'] += np.degrees(self.moonAlt)
475 self.points['azRelMoon'] = self.azRelMoon
476 self.points['moonSunSep'] += self.moonPhase/100.*180.
478 self.eclipLon = convertFunc(eclipLon)
479 self.eclipLat = convertFunc(eclipLat)
481 self.sunEclipLon = convertFunc(sunEclipLon)
482 self.points['altEclip'] += self.eclipLat
483 self.points['azEclipRelSun'] += wrapRA(self.eclipLon - self.sunEclipLon)
485 self.sunAz = convertFunc(sunAz)
486 self.points['sunAlt'] = self.sunAlt
487 self.points['azRelSun'] = wrapRA(self.azs - self.sunAz)
488 self.points['solarFlux'] = solarFlux
490 self.paramsSet = True
492 self.mask = np.where((self.airmass > self.airmassLimit) | (self.airmass < 1.))[0]
493 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
494 # Interpolate the templates to the set paramters
495 if self.goodPix.size > 0:
496 self._interpSky()
497 else:
498 warnings.warn('No points in interpolation range')
500 def _interpSky(self):
501 """
502 Interpolate the template spectra to the set RA, Dec and MJD.
504 the results are stored as attributes of the class:
505 .wave = the wavelength in nm
506 .spec = array of spectra with units of ergs/s/cm^2/nm
507 """
509 if not self.paramsSet:
510 raise ValueError(
511 'No parameters have been set. Must run setRaDecMjd or setParams before running interpSky.')
513 # set up array to hold the resulting spectra for each ra, dec point.
514 self.spec = np.zeros((self.npts, self.npix), dtype=float)
516 # Rebuild the components dict so things can be turned on/off
517 self.components = {'moon': self.moon, 'lowerAtm': self.lowerAtm, 'twilight': self.twilight,
518 'upperAtm': self.upperAtm, 'airglow': self.airglow, 'zodiacal': self.zodiacal,
519 'scatteredStar': self.scatteredStar, 'mergedSpec': self.mergedSpec}
521 # Loop over each component and add it to the result.
522 mask = np.ones(self.npts)
523 for key in self.components:
524 if self.components[key]:
525 result = self.interpObjs[key](self.points[self.goodPix], filterNames=self.filterNames)
526 # Make sure the component has something
527 if np.size(result['spec']) == 0:
528 self.spec[self.mask, :] = np.nan
529 return
530 if np.max(result['spec']) > 0:
531 mask[np.where(np.sum(result['spec'], axis=1) == 0)] = 0
532 self.spec[self.goodPix] += result['spec']
533 if not hasattr(self, 'wave'):
534 self.wave = result['wave']
535 else:
536 if not np.allclose(result['wave'], self.wave, rtol=1e-5, atol=1e-5):
537 warnings.warn('Wavelength arrays of components do not match.')
538 if self.airmassLimit <= 2.5:
539 self.spec[np.where(mask == 0), :] = 0
540 self.spec[self.mask, :] = np.nan
542 def returnWaveSpec(self):
543 """
544 Return the wavelength and spectra.
545 Wavelenth in nm
546 spectra is flambda in ergs/cm^2/s/nm
547 """
548 if self.azs is None:
549 raise ValueError('No coordinates set. Use setRaDecMjd, setRaDecAltAzMjd, or setParams methods before calling returnWaveSpec.')
550 if self.mags:
551 raise ValueError('SkyModel set to interpolate magnitudes. Initialize object with mags=False')
552 # Mask out high airmass points
553 # self.spec[self.mask] *= 0
554 return self.wave, self.spec
556 def returnMags(self, bandpasses=None):
557 """
558 Convert the computed spectra to a magnitude using the supplied bandpass,
559 or, if self.mags=True, return the mags in the LSST filters
561 If mags=True when initialized, return mags returns an structured array with
562 dtype names u,g,r,i,z,y.
564 bandpasses: optional dictionary with bandpass name keys and bandpass object values.
566 """
567 if self.azs is None:
568 raise ValueError('No coordinates set. Use setRaDecMjd, setRaDecAltAzMjd, or setParams methods before calling returnMags.')
570 if self.mags:
571 if bandpasses:
572 warnings.warn('Ignoring set bandpasses and returning LSST ugrizy.')
573 mags = -2.5*np.log10(self.spec)+np.log10(3631.)
574 # Mask out high airmass
575 mags[self.mask] *= np.nan
576 mags = mags.swapaxes(0, 1)
577 magsBack = {}
578 for i, f in enumerate(self.filterNames):
579 magsBack[f] = mags[i]
580 else:
581 magsBack = {}
582 for key in bandpasses:
583 mags = np.zeros(self.npts, dtype=float)-666
584 tempSed = Sed()
585 isThrough = np.where(bandpasses[key].sb > 0)
586 minWave = bandpasses[key].wavelen[isThrough].min()
587 maxWave = bandpasses[key].wavelen[isThrough].max()
588 inBand = np.where((self.wave >= minWave) & (self.wave <= maxWave))
589 for i, ra in enumerate(self.ra):
590 # Check that there is flux in the band, otherwise calcMag fails
591 if np.max(self.spec[i, inBand]) > 0:
592 tempSed.setSED(self.wave, flambda=self.spec[i, :])
593 mags[i] = tempSed.calcMag(bandpasses[key])
594 # Mask out high airmass
595 mags[self.mask] *= np.nan
596 magsBack[key] = mags
597 return magsBack