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=2.5):
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 (2.5)
97 Most of the models are only accurate to airmass 2.5. If set higher, airmass values
98 higher than 2.5 are set to 2.5.
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 = 17001
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 setComponents(self, twilight=True, zodiacal=True, moon=True,
163 airglow=True, lowerAtm=False, upperAtm=False, scatteredStar=False,
164 mergedSpec=True):
165 """
166 Convience function for turning on/off different sky components.
167 """
168 self.moon = moon
169 self.lowerAtm = lowerAtm
170 self.twilight = twilight
171 self.zodiacal = zodiacal
172 self.upperAtm = upperAtm
173 self.airglow = airglow
174 self.scatteredStar = scatteredStar
175 self.mergedSpec = mergedSpec
177 def _initPoints(self):
178 """
179 Set up an array for all the interpolation points
180 """
182 names = ['airmass', 'nightTimes', 'alt', 'az', 'azRelMoon', 'moonSunSep', 'moonAltitude',
183 'altEclip', 'azEclipRelSun', 'sunAlt', 'azRelSun', 'solarFlux']
184 types = [float]*len(names)
185 self.points = np.zeros(self.npts, list(zip(names, types)))
187 def setRaDecMjd(self, lon, lat, mjd, degrees=False, azAlt=False, solarFlux=130.,
188 filterNames=['u', 'g', 'r', 'i', 'z', 'y']):
189 """
190 Set the sky parameters by computing the sky conditions on a given MJD and sky location.
194 lon: Longitude-like (RA or Azimuth). Can be single number, list, or numpy array
195 lat: Latitude-like (Dec or Altitude)
196 mjd: Modified Julian Date for the calculation. Must be single number.
197 degrees: (False) Assumes lon and lat are radians unless degrees=True
198 azAlt: (False) Assume lon, lat are RA, Dec unless azAlt=True
199 solarFlux: solar flux in SFU Between 50 and 310. Default=130. 1 SFU=10^4 Jy.
200 filterNames: list of fitlers to return magnitudes for (if initialized with mags=True).
201 """
202 self.filterNames = filterNames
203 if self.mags:
204 self.npix = len(self.filterNames)
205 # Wrap in array just in case single points were passed
206 if np.size(lon) == 1:
207 lon = np.array([lon]).ravel()
208 lat = np.array([lat]).ravel()
209 else:
210 lon = np.array(lon)
211 lat = np.array(lat)
212 if degrees:
213 self.ra = np.radians(lon)
214 self.dec = np.radians(lat)
215 else:
216 self.ra = lon
217 self.dec = lat
218 if np.size(mjd) > 1:
219 raise ValueError('mjd must be single value.')
220 self.mjd = mjd
221 if azAlt:
222 self.azs = self.ra.copy()
223 self.alts = self.dec.copy()
224 if self.preciseAltAz:
225 self.ra, self.dec = _raDecFromAltAz(self.alts, self.azs,
226 ObservationMetaData(mjd=self.mjd, site=self.telescope))
227 else:
228 self.ra, self.dec = _approx_altAz2RaDec(self.alts, self.azs,
229 self.telescope.latitude_rad,
230 self.telescope.longitude_rad, mjd)
231 else:
232 if self.preciseAltAz:
233 self.alts, self.azs, pa = _altAzPaFromRaDec(self.ra, self.dec,
234 ObservationMetaData(mjd=self.mjd,
235 site=self.telescope))
236 else:
237 self.alts, self.azs = _approx_RaDec2AltAz(self.ra, self.dec,
238 self.telescope.latitude_rad,
239 self.telescope.longitude_rad, mjd)
241 self.npts = self.ra.size
242 self._initPoints()
244 self.solarFlux = solarFlux
245 self.points['solarFlux'] = self.solarFlux
247 self._setupPointGrid()
249 self.paramsSet = True
251 # Assume large airmasses are the same as airmass=2.5
252 to_fudge = np.where((self.points['airmass'] > 2.5) & (self.points['airmass'] < self.airmassLimit))
253 self.points['airmass'][to_fudge] = 2.499
254 self.points['alt'][to_fudge] = np.pi/2-np.arccos(1./self.airmassLimit)
256 # Interpolate the templates to the set paramters
257 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
258 if self.goodPix.size > 0:
259 self._interpSky()
260 else:
261 warnings.warn('No valid points to interpolate')
263 def setRaDecAltAzMjd(self, ra, dec, alt, az, mjd, degrees=False, solarFlux=130.,
264 filterNames=['u', 'g', 'r', 'i', 'z', 'y']):
265 """
266 Set the sky parameters by computing the sky conditions on a given MJD and sky location.
268 Use if you already have alt az coordinates so you can skip the coordinate conversion.
269 """
270 self.filterNames = filterNames
271 if self.mags:
272 self.npix = len(self.filterNames)
273 # Wrap in array just in case single points were passed
274 if not type(ra).__module__ == np.__name__:
275 if np.size(ra) == 1:
276 ra = np.array([ra]).ravel()
277 dec = np.array([dec]).ravel()
278 alt = np.array(alt).ravel()
279 az = np.array(az).ravel()
280 else:
281 ra = np.array(ra)
282 dec = np.array(dec)
283 alt = np.array(alt)
284 az = np.array(az)
285 if degrees:
286 self.ra = np.radians(ra)
287 self.dec = np.radians(dec)
288 self.alts = np.radians(alt)
289 self.azs = np.radians(az)
290 else:
291 self.ra = ra
292 self.dec = dec
293 self.azs = az
294 self.alts = alt
295 if np.size(mjd) > 1:
296 raise ValueError('mjd must be single value.')
297 self.mjd = mjd
299 self.npts = self.ra.size
300 self._initPoints()
302 self.solarFlux = solarFlux
303 self.points['solarFlux'] = self.solarFlux
305 self._setupPointGrid()
307 self.paramsSet = True
309 # Assume large airmasses are the same as airmass=2.5
310 to_fudge = np.where((self.points['airmass'] > 2.5) & (self.points['airmass'] < self.airmassLimit))
311 self.points['airmass'][to_fudge] = 2.5
312 self.points['alt'][to_fudge] = np.pi/2-np.arccos(1./self.airmassLimit)
314 # Interpolate the templates to the set paramters
315 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
316 if self.goodPix.size > 0:
317 self._interpSky()
318 else:
319 warnings.warn('No valid points to interpolate')
321 def getComputedVals(self):
322 """
323 Return the intermediate values that are caluculated by setRaDecMjd and used for interpolation.
324 All of these values are also accesible as class atributes, this is a convience method to grab them
325 all at once and document the formats.
327 Returns
328 -------
329 out : dict
330 Dictionary of all the intermediate calculated values that may be of use outside
331 (the key:values in the output dict)
332 ra : numpy.array
333 RA of the interpolation points (radians)
334 dec : np.array
335 Dec of the interpolation points (radians)
336 alts : np.array
337 Altitude (radians)
338 azs : np.array
339 Azimuth of interpolation points (radians)
340 airmass : np.array
341 Airmass values for each point, computed via 1./np.cos(np.pi/2.-self.alts).
342 solarFlux : float
343 The solar flux used (SFU).
344 sunAz : float
345 Azimuth of the sun (radians)
346 sunAlt : float
347 Altitude of the sun (radians)
348 sunRA : float
349 RA of the sun (radians)
350 sunDec : float
351 Dec of the sun (radians)
352 azRelSun : np.array
353 Azimuth of each point relative to the sun (0=same direction as sun) (radians)
354 moonAz : float
355 Azimuth of the moon (radians)
356 moonAlt : float
357 Altitude of the moon (radians)
358 moonRA : float
359 RA of the moon (radians)
360 moonDec : float
361 Dec of the moon (radians). Note, if you want distances
362 moonPhase : float
363 Phase of the moon (0-100)
364 moonSunSep : float
365 Seperation of moon and sun (degrees)
366 azRelMoon : np.array
367 Azimuth of each point relative to teh moon
368 eclipLon : np.array
369 Ecliptic longitude (radians) of each point
370 eclipLat : np.array
371 Ecliptic latitude (radians) of each point
372 sunEclipLon: np.array
373 Ecliptic longitude (radians) of each point with the sun at longitude zero
375 Note that since the alt and az can be calculated using the fast approximation, if one wants
376 to compute the distance between the the points and the sun or moon, it is probably better to
377 use the ra,dec positions rather than the alt,az positions.
378 """
380 result = {}
381 attributes = ['ra', 'dec', 'alts', 'azs', 'airmass', 'solarFlux', 'moonPhase',
382 'moonAz', 'moonAlt', 'sunAlt', 'sunAz', 'azRelSun', 'moonSunSep',
383 'azRelMoon', 'eclipLon', 'eclipLat', 'moonRA', 'moonDec', 'sunRA',
384 'sunDec', 'sunEclipLon']
386 for attribute in attributes:
387 if hasattr(self, attribute):
388 result[attribute] = getattr(self, attribute)
389 else:
390 result[attribute] = None
392 return result
394 def _setupPointGrid(self):
395 """
396 Setup the points for the interpolation functions.
397 """
398 # Switch to Dublin Julian Date for pyephem
399 self.Observatory.date = mjd2djd(self.mjd)
401 sun = ephem.Sun()
402 sun.compute(self.Observatory)
403 self.sunAlt = sun.alt
404 self.sunAz = sun.az
405 self.sunRA = sun.ra
406 self.sunDec = sun.dec
408 # Compute airmass the same way as ESO model
409 self.airmass = 1./np.cos(np.pi/2.-self.alts)
411 self.points['airmass'] = self.airmass
412 self.points['nightTimes'] = 2
413 self.points['alt'] = self.alts
414 self.points['az'] = self.azs
416 if self.twilight:
417 self.points['sunAlt'] = self.sunAlt
418 self.azRelSun = wrapRA(self.azs - self.sunAz)
419 self.points['azRelSun'] = self.azRelSun
421 if self.moon:
422 moon = ephem.Moon()
423 moon.compute(self.Observatory)
424 self.moonPhase = moon.phase
425 self.moonAlt = moon.alt
426 self.moonAz = moon.az
427 self.moonRA = moon.ra
428 self.moonDec = moon.dec
429 # Calc azimuth relative to moon
430 self.azRelMoon = calcAzRelMoon(self.azs, self.moonAz)
431 self.moonTargSep = haversine(self.azs, self.alts, self.moonAz, self.moonAlt)
432 self.points['moonAltitude'] += np.degrees(self.moonAlt)
433 self.points['azRelMoon'] += self.azRelMoon
434 self.moonSunSep = self.moonPhase/100.*180.
435 self.points['moonSunSep'] += self.moonSunSep
437 if self.zodiacal:
438 self.eclipLon = np.zeros(self.npts)
439 self.eclipLat = np.zeros(self.npts)
441 for i, temp in enumerate(self.ra):
442 eclip = ephem.Ecliptic(ephem.Equatorial(self.ra[i], self.dec[i], epoch='2000'))
443 self.eclipLon[i] += eclip.lon
444 self.eclipLat[i] += eclip.lat
445 # Subtract off the sun ecliptic longitude
446 sunEclip = ephem.Ecliptic(sun)
447 self.sunEclipLon = sunEclip.lon
448 self.points['altEclip'] += self.eclipLat
449 self.points['azEclipRelSun'] += wrapRA(self.eclipLon - self.sunEclipLon)
451 self.mask = np.where((self.airmass > self.airmassLimit) | (self.airmass < 1.))[0]
452 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
454 def setParams(self, airmass=1., azs=90., alts=None, moonPhase=31.67, moonAlt=45.,
455 moonAz=0., sunAlt=-12., sunAz=0., sunEclipLon=0.,
456 eclipLon=135., eclipLat=90., degrees=True, solarFlux=130.,
457 filterNames=['u', 'g', 'r', 'i', 'z', 'y']):
458 """
459 Set parameters manually.
460 Note, you can put in unphysical combinations of paramters if you want to
461 (e.g., put a full moon at zenith at sunset).
462 if the alts kwarg is set it will override the airmass kwarg.
463 MoonPhase is percent of moon illuminated (0-100)
464 """
466 # Convert all values to radians for internal use.
467 self.filterNames = filterNames
468 if self.mags:
469 self.npix = len(self.filterNames)
470 if degrees:
471 convertFunc = np.radians
472 else:
473 convertFunc = justReturn
475 self.solarFlux = solarFlux
476 self.sunAlt = convertFunc(sunAlt)
477 self.moonPhase = moonPhase
478 self.moonAlt = convertFunc(moonAlt)
479 self.moonAz = convertFunc(moonAz)
480 self.eclipLon = convertFunc(eclipLon)
481 self.eclipLat = convertFunc(eclipLat)
482 self.sunEclipLon = convertFunc(sunEclipLon)
483 self.azs = convertFunc(azs)
484 if alts is not None:
485 self.airmass = 1./np.cos(np.pi/2.-convertFunc(alts))
486 self.alts = convertFunc(alts)
487 else:
488 self.airmass = airmass
489 self.alts = np.pi/2.-np.arccos(1./airmass)
490 self.moonTargSep = haversine(self.azs, self.alts, moonAz, self.moonAlt)
491 self.npts = np.size(self.airmass)
492 self._initPoints()
494 self.points['airmass'] = self.airmass
495 self.points['nightTimes'] = 2
496 self.points['alt'] = self.alts
497 self.points['az'] = self.azs
498 self.azRelMoon = calcAzRelMoon(self.azs, self.moonAz)
499 self.points['moonAltitude'] += np.degrees(self.moonAlt)
500 self.points['azRelMoon'] = self.azRelMoon
501 self.points['moonSunSep'] += self.moonPhase/100.*180.
503 self.eclipLon = convertFunc(eclipLon)
504 self.eclipLat = convertFunc(eclipLat)
506 self.sunEclipLon = convertFunc(sunEclipLon)
507 self.points['altEclip'] += self.eclipLat
508 self.points['azEclipRelSun'] += wrapRA(self.eclipLon - self.sunEclipLon)
510 self.sunAz = convertFunc(sunAz)
511 self.points['sunAlt'] = self.sunAlt
512 self.points['azRelSun'] = wrapRA(self.azs - self.sunAz)
513 self.points['solarFlux'] = solarFlux
515 self.paramsSet = True
517 self.mask = np.where((self.airmass > self.airmassLimit) | (self.airmass < 1.))[0]
518 self.goodPix = np.where((self.airmass <= self.airmassLimit) & (self.airmass >= 1.))[0]
519 # Interpolate the templates to the set paramters
520 if self.goodPix.size > 0:
521 self._interpSky()
522 else:
523 warnings.warn('No points in interpolation range')
525 def _interpSky(self):
526 """
527 Interpolate the template spectra to the set RA, Dec and MJD.
529 the results are stored as attributes of the class:
530 .wave = the wavelength in nm
531 .spec = array of spectra with units of ergs/s/cm^2/nm
532 """
534 if not self.paramsSet:
535 raise ValueError(
536 'No parameters have been set. Must run setRaDecMjd or setParams before running interpSky.')
538 # set up array to hold the resulting spectra for each ra, dec point.
539 self.spec = np.zeros((self.npts, self.npix), dtype=float)
541 # Rebuild the components dict so things can be turned on/off
542 self.components = {'moon': self.moon, 'lowerAtm': self.lowerAtm, 'twilight': self.twilight,
543 'upperAtm': self.upperAtm, 'airglow': self.airglow, 'zodiacal': self.zodiacal,
544 'scatteredStar': self.scatteredStar, 'mergedSpec': self.mergedSpec}
546 # Loop over each component and add it to the result.
547 mask = np.ones(self.npts)
548 for key in self.components:
549 if self.components[key]:
550 result = self.interpObjs[key](self.points[self.goodPix], filterNames=self.filterNames)
551 # Make sure the component has something
552 if np.size(result['spec']) == 0:
553 self.spec[self.mask, :] = np.nan
554 return
555 if np.max(result['spec']) > 0:
556 mask[np.where(np.sum(result['spec'], axis=1) == 0)] = 0
557 self.spec[self.goodPix] += result['spec']
558 if not hasattr(self, 'wave'):
559 self.wave = result['wave']
560 else:
561 if not np.allclose(result['wave'], self.wave, rtol=1e-5, atol=1e-5):
562 warnings.warn('Wavelength arrays of components do not match.')
563 if self.airmassLimit <= 2.5:
564 self.spec[np.where(mask == 0), :] = 0
565 self.spec[self.mask, :] = np.nan
567 def returnWaveSpec(self):
568 """
569 Return the wavelength and spectra.
570 Wavelenth in nm
571 spectra is flambda in ergs/cm^2/s/nm
572 """
573 if self.azs is None:
574 raise ValueError('No coordinates set. Use setRaDecMjd, setRaDecAltAzMjd, or setParams methods before calling returnWaveSpec.')
575 if self.mags:
576 raise ValueError('SkyModel set to interpolate magnitudes. Initialize object with mags=False')
577 # Mask out high airmass points
578 # self.spec[self.mask] *= 0
579 return self.wave, self.spec
581 def returnMags(self, bandpasses=None):
582 """
583 Convert the computed spectra to a magnitude using the supplied bandpass,
584 or, if self.mags=True, return the mags in the LSST filters
586 If mags=True when initialized, return mags returns an structured array with
587 dtype names u,g,r,i,z,y.
589 bandpasses: optional dictionary with bandpass name keys and bandpass object values.
591 """
592 if self.azs is None:
593 raise ValueError('No coordinates set. Use setRaDecMjd, setRaDecAltAzMjd, or setParams methods before calling returnMags.')
595 if self.mags:
596 if bandpasses:
597 warnings.warn('Ignoring set bandpasses and returning LSST ugrizy.')
598 mags = -2.5*np.log10(self.spec)+np.log10(3631.)
599 # Mask out high airmass
600 mags[self.mask] *= np.nan
601 mags = mags.swapaxes(0, 1)
602 magsBack = {}
603 for i, f in enumerate(self.filterNames):
604 magsBack[f] = mags[i]
605 else:
606 magsBack = {}
607 for key in bandpasses:
608 mags = np.zeros(self.npts, dtype=float)-666
609 tempSed = Sed()
610 isThrough = np.where(bandpasses[key].sb > 0)
611 minWave = bandpasses[key].wavelen[isThrough].min()
612 maxWave = bandpasses[key].wavelen[isThrough].max()
613 inBand = np.where((self.wave >= minWave) & (self.wave <= maxWave))
614 for i, ra in enumerate(self.ra):
615 # Check that there is flux in the band, otherwise calcMag fails
616 if np.max(self.spec[i, inBand]) > 0:
617 tempSed.setSED(self.wave, flambda=self.spec[i, :])
618 mags[i] = tempSed.calcMag(bandpasses[key])
619 # Mask out high airmass
620 mags[self.mask] *= np.nan
621 magsBack[key] = mags
622 return magsBack