Coverage for python/lsst/sims/catUtils/mixins/PhotometryMixin.py : 12%

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
1"""
2photUtils -
5ljones@astro.washington.edu (and ajc@astro.washington.edu)
7and now (2014 March 28): scott.f.daniel@gmail.com
9Collection of utilities to aid usage of Sed and Bandpass with dictionaries.
11"""
13from builtins import zip
14from builtins import range
15from builtins import object
16import os
17import numpy as np
18from collections import OrderedDict
19from lsst.utils import getPackageDir
20from lsst.sims.photUtils import Sed, Bandpass, LSSTdefaults, calcGamma, \
21 calcMagError_m5, calcSNR_m5, PhotometricParameters, magErrorFromSNR, \
22 BandpassDict
23from lsst.sims.utils import defaultSpecMap
24from lsst.sims.catalogs.decorators import compound
25from lsst.sims.photUtils import SedList
26from lsst.sims.utils import defaultSpecMap
27from lsst.utils import getPackageDir
29__all__ = ["PhotometryBase", "PhotometryGalaxies", "PhotometryStars", "PhotometrySSM"]
32class PhotometryBase(object):
33 """
34 This class provides an InstanceCatalog with a member variable photParams,
35 which is an instance of the class PhotometricParameters.
37 It also provides the method calculateMagnitudeUncertainty, which takes magnitudes,
38 a BandpassDict, and an ObservationMetaData as inputs and returns the uncertainties
39 on those magnitudes.
40 """
42 #an object carrying around photometric parameters like readnoise, effective area, plate scale, etc.
43 #defaults to LSST values
44 photParams = PhotometricParameters()
47 def _cacheGamma(self, m5_names, bandpassDict):
48 """
49 Generate or populate the cache of gamma values used by this InstanceCatalog
50 to calculate photometric uncertainties (gamma is defined in equation 5 of
51 the LSST overview paper arXiv:0805.2366)
53 @param [in] m5_names is a list of the names of keys by which m5 values are
54 referred to in the dict self.obs_metadata.m5
56 @param [in] bandpassDict is the bandpassDict containing the bandpasses
57 corresponding to those m5 values.
58 """
60 if not hasattr(self, '_gamma_cache'):
61 self._gamma_cache = {}
63 for mm, bp in zip(m5_names, bandpassDict.values()):
64 if mm not in self._gamma_cache and mm in self.obs_metadata.m5:
65 self._gamma_cache[mm] = calcGamma(bp, self.obs_metadata.m5[mm], photParams=self.photParams)
68 def _magnitudeUncertaintyGetter(self, column_name_list, m5_name_list, bandpassDict_name):
69 """
70 Generic getter for magnitude uncertainty columns.
72 Columns must be named 'sigma_xx' where 'xx' is the column name of
73 the associated magnitude
75 @param [in] column_name_list is the list of magnitude column names
76 associated with the uncertainties calculated by this getter
77 (the 'xx' in the 'sigma_xx' above)
79 @param [in] m5_name_list are the keys to the self.obs_metadata.m5 dict
80 corresponding to the bandpasses in column_names. For example: in
81 the case of galaxies, the magnitude columns
83 column_names = ['uBulge', 'gBulge', 'rBulge', 'iBulge', 'zBulge', 'yBulge']
85 may correspond to m5 values keyed to
87 m5_name_list = ['u', 'g', 'r', 'i', 'z', 'y']
89 @param [in] bandpassDict_name is a string indicating the name of
90 the InstanceCatalog member variable containing the BandpassDict
91 to be used when calculating these magnitude uncertainties.
92 The BandpassDict itself will be accessed using
93 getattr(self, bandpassDict_name)
95 @param [out] returns a 2-D numpy array in which the first index
96 is the uncertainty column and the second index is the catalog row
97 (i.e. output suitable for a getter in an InstanceCatalog)
98 """
100 # make sure that the magnitudes associated with any requested
101 # uncertainties actually are calculated
102 num_elements = None
103 mag_dict = {}
104 for name in column_name_list:
105 if 'sigma_%s' % name in self._actually_calculated_columns:
106 mag_dict[name] = self.column_by_name(name)
107 if num_elements is None:
108 num_elements = len(mag_dict[name])
110 # These lines must come after the preceding lines;
111 # the bandpassDict will not be loaded until the magnitude
112 # getters are called
113 bandpassDict = getattr(self, bandpassDict_name)
114 self._cacheGamma(m5_name_list, bandpassDict)
117 output = []
119 for name, m5_name, bp in zip(column_name_list, m5_name_list, bandpassDict.values()):
120 if 'sigma_%s' % name not in self._actually_calculated_columns:
121 output.append(np.ones(num_elements)*np.NaN)
122 else:
123 try:
124 m5 = self.obs_metadata.m5[m5_name]
125 gamma = self._gamma_cache[m5_name]
127 sigma_list, gamma = calcMagError_m5(mag_dict[name], bp, m5, self.photParams, gamma=gamma)
129 output.append(sigma_list)
131 except KeyError as kk:
132 msg = 'You got the KeyError: %s' % kk.args[0]
133 raise KeyError('%s \n' % msg \
134 + 'Is it possible your ObservationMetaData does not have the proper\n'
135 'm5 values defined?')
138 return np.array(output)
141 @compound('sigma_lsst_u','sigma_lsst_g','sigma_lsst_r','sigma_lsst_i',
142 'sigma_lsst_z','sigma_lsst_y')
143 def get_lsst_photometric_uncertainties(self):
144 """
145 Getter for photometric uncertainties associated with lsst bandpasses
146 """
148 return self._magnitudeUncertaintyGetter(['lsst_u', 'lsst_g', 'lsst_r',
149 'lsst_i', 'lsst_z', 'lsst_y'],
150 ['u', 'g', 'r', 'i', 'z', 'y'],
151 'lsstBandpassDict')
154 def calculateVisibility(self, magFilter, sigma=0.1, randomSeed=None, pre_generate_randoms=False):
155 """
156 Determine (probabilistically) whether a source was detected or not.
158 The 'completeness' of source detection at any magnitude is calculated by
159 completeness = (1 + e^^(magFilter-m5)/sigma)^(-1)
160 For each source with a magnitude magFilter, if a uniform random number [0-1)
161 is less than or equal to 'completeness', then it is counted as "detected".
162 See equation 24, figure 8 and table 5 from SDSS completeness analysis in
163 http://iopscience.iop.org/0004-637X/794/2/120/pdf/apj_794_2_120.pdf
164 "THE SLOAN DIGITAL SKY SURVEY COADD: 275 deg2 OF DEEP SLOAN DIGITAL SKY SURVEY IMAGING ON STRIPE 82"
166 @ param [in] magFilter is the magnitude of the object in the observed filter.
168 @ param [in] sigma is the FWHM of the distribution (default = 0.1)
170 @ param [in] randomSeed is an option to set a random seed (default None)
171 @ param [in] pre_generate_randoms is an option (default False) to pre-generate a series of 12,000,000 random numbers
172 for use throughout the visibility calculation [the random numbers used are randoms[objId]].
174 @ param [out] visibility (None/1).
175 """
176 if len(magFilter) == 0:
177 return np.array([])
178 # Calculate the completeness at the magnitude of each object.
179 completeness = 1.0 / (1 + np.exp((magFilter - self.obs_metadata.m5[self.obs_metadata.bandpass])/sigma))
180 # Seed numpy if desired and not previously done.
181 if (randomSeed is not None) and (not hasattr(self, 'ssm_random_seeded')):
182 np.random.seed(randomSeed)
183 self.ssm_random_seeded = True
184 # Pre-generate random numbers, if desired and not previously done.
185 if pre_generate_randoms and not hasattr(self, 'ssm_randoms'):
186 self.ssm_randoms = np.random.rand(14000000)
187 # Calculate probability values to compare to completeness.
188 if hasattr(self, 'ssm_randoms'):
189 # Grab the random numbers from self.randoms.
190 probability = self.ssm_randoms[self.column_by_name('objId')]
191 else:
192 probability = np.random.random_sample(len(magFilter))
193 # Compare the random number to the completeness.
194 visibility = np.where(probability <= completeness, 1, None)
195 return visibility
197 def _variabilityGetter(self, columnNames):
198 """
199 Find columns named 'delta_*' and return them to be added
200 to '*' magnitude columns (i.e. look for delta_lsst_u so that
201 it can be added to lsst_u)
203 Parameters
204 ----------
205 columnNames is a list of the quiescent columns (lsst_u in the
206 example above) whose deltas we are looking for
208 Returns
209 -------
210 A numpy array in which each row is a delta magnitude and each
211 column is an astrophysical object/database row
212 """
214 num_obj = len(self.column_by_name(self.db_obj.idColKey))
215 delta = []
217 # figure out which of these columns we are actually calculating
218 indices = [ii for ii, name in enumerate(columnNames)
219 if name in self._actually_calculated_columns]
221 for ix, columnName in enumerate(columnNames):
222 if indices is None or ix in indices:
223 delta_name = 'delta_' + columnName
224 if delta_name in self._all_available_columns:
225 delta.append(self.column_by_name(delta_name))
226 else:
227 delta.append(np.zeros(num_obj))
228 else:
229 delta.append(np.zeros(num_obj))
231 return np.array(delta)
233class PhotometryGalaxies(PhotometryBase):
234 """
235 This mixin provides the code necessary for calculating the component magnitudes associated with
236 galaxies. It assumes that we want LSST filters.
237 """
239 def _hasCosmoDistMod(self):
240 """
241 Determine whether or not this InstanceCatalog has a column
242 specifically devoted to the cosmological distance modulus.
243 """
244 if 'cosmologicalDistanceModulus' in self._all_available_columns:
245 return True
246 return False
249 def _loadBulgeSedList(self, wavelen_match):
250 """
251 Load a SedList of galaxy bulge Seds.
252 The list will be stored in the variable self._bulgeSedList.
254 @param [in] wavelen_match is the wavelength grid (in nm)
255 on which the Seds are to be sampled.
256 """
258 sedNameList = self.column_by_name('sedFilenameBulge')
259 magNormList = self.column_by_name('magNormBulge')
260 redshiftList = self.column_by_name('redshift')
261 internalAvList = self.column_by_name('internalAvBulge')
262 cosmologicalDimming = not self._hasCosmoDistMod()
264 if len(sedNameList)==0:
265 return np.ones((0))
267 if not hasattr(self, '_bulgeSedList'):
268 self._bulgeSedList = SedList(sedNameList, magNormList,
269 internalAvList=internalAvList,
270 redshiftList=redshiftList,
271 cosmologicalDimming=cosmologicalDimming,
272 wavelenMatch=wavelen_match,
273 fileDir=getPackageDir('sims_sed_library'),
274 specMap=defaultSpecMap)
275 else:
276 self._bulgeSedList.flush()
277 self._bulgeSedList.loadSedsFromList(sedNameList, magNormList,
278 internalAvList=internalAvList,
279 redshiftList=redshiftList)
282 def _loadDiskSedList(self, wavelen_match):
283 """
284 Load a SedList of galaxy disk Seds.
285 The list will be stored in the variable self._bulgeSedList.
287 @param [in] wavelen_match is the wavelength grid (in nm)
288 on which the Seds are to be sampled.
289 """
291 sedNameList = self.column_by_name('sedFilenameDisk')
292 magNormList = self.column_by_name('magNormDisk')
293 redshiftList = self.column_by_name('redshift')
294 internalAvList = self.column_by_name('internalAvDisk')
295 cosmologicalDimming = not self._hasCosmoDistMod()
297 if len(sedNameList)==0:
298 return np.ones((0))
300 if not hasattr(self, '_diskSedList'):
301 self._diskSedList = SedList(sedNameList, magNormList,
302 internalAvList=internalAvList,
303 redshiftList=redshiftList,
304 cosmologicalDimming=cosmologicalDimming,
305 wavelenMatch=wavelen_match,
306 fileDir=getPackageDir('sims_sed_library'),
307 specMap=defaultSpecMap)
308 else:
309 self._diskSedList.flush()
310 self._diskSedList.loadSedsFromList(sedNameList, magNormList,
311 internalAvList=internalAvList,
312 redshiftList=redshiftList)
315 def _loadAgnSedList(self, wavelen_match):
316 """
317 Load a SedList of galaxy AGN Seds.
318 The list will be stored in the variable self._bulgeSedList.
320 @param [in] wavelen_match is the wavelength grid (in nm)
321 on which the Seds are to be sampled.
322 """
324 sedNameList = self.column_by_name('sedFilenameAgn')
325 magNormList = self.column_by_name('magNormAgn')
326 redshiftList = self.column_by_name('redshift')
327 cosmologicalDimming = not self._hasCosmoDistMod()
329 if len(sedNameList)==0:
330 return np.ones((0))
332 if not hasattr(self, '_agnSedList'):
333 self._agnSedList = SedList(sedNameList, magNormList,
334 redshiftList=redshiftList,
335 cosmologicalDimming=cosmologicalDimming,
336 wavelenMatch=wavelen_match,
337 fileDir=getPackageDir('sims_sed_library'),
338 specMap=defaultSpecMap)
339 else:
340 self._agnSedList.flush()
341 self._agnSedList.loadSedsFromList(sedNameList, magNormList,
342 redshiftList=redshiftList)
345 def sum_magnitudes(self, disk = None, bulge = None, agn = None):
346 """
347 Sum the component magnitudes of a galaxy and return the answer
349 @param [in] disk is the disk magnitude must be a numpy array or a float
351 @param [in] bulge is the bulge magnitude must be a numpy array or a float
353 @param [in] agn is the agn magnitude must be a numpy array or a float
355 @param [out] outMag is the total magnitude of the galaxy
356 """
357 with np.errstate(divide='ignore', invalid='ignore'):
358 baselineType = type(None)
359 if not isinstance(disk, type(None)):
360 baselineType = type(disk)
361 if baselineType == np.ndarray:
362 elements=len(disk)
364 if not isinstance(bulge, type(None)):
365 if baselineType == type(None):
366 baselineType = type(bulge)
367 if baselineType == np.ndarray:
368 elements = len(bulge)
369 elif not isinstance(bulge, baselineType):
370 raise RuntimeError("All non-None arguments of sum_magnitudes need to be " +
371 "of the same type (float or numpy array)")
373 elif not isinstance(agn, type(None)):
374 if baseLineType == type(None):
375 baselineType = type(agn)
376 if baselineType == np.ndarray:
377 elements = len(agn)
378 elif not isinstance(agn, baselineType):
379 raise RuntimeError("All non-None arguments of sum_magnitudes need to be " +
380 "of the same type (float or numpy array)")
382 if baselineType is not float and \
383 baselineType is not np.ndarray and \
384 baselineType is not np.float and \
385 baselineType is not np.float64:
387 raise RuntimeError("Arguments of sum_magnitudes need to be " +
388 "either floats or numpy arrays; you appear to have passed %s " % baselineType)
390 mm_0 = 22.
391 tol = 1.0e-30
393 if baselineType == np.ndarray:
394 nn = np.zeros(elements)
395 else:
396 nn = 0.0
398 if disk is not None:
399 nn += np.where(np.isnan(disk), 0.0, np.power(10, -0.4*(disk - mm_0)))
401 if bulge is not None:
402 nn += np.where(np.isnan(bulge), 0.0, np.power(10, -0.4*(bulge - mm_0)))
404 if agn is not None:
405 nn += np.where(np.isnan(agn), 0.0, np.power(10, -0.4*(agn - mm_0)))
407 if baselineType == np.ndarray:
408 # according to this link
409 # http://stackoverflow.com/questions/25087769/runtimewarning-divide-by-zero-error-how-to-avoid-python-numpy
410 # we will still get a divide by zero error from log10, but np.where will be
411 # circumventing the offending value, so it is probably okay
412 return np.where(nn>tol, -2.5*np.log10(nn) + mm_0, np.NaN)
413 else:
414 if nn>tol:
415 return -2.5*np.log10(nn) + mm_0
416 else:
417 return np.NaN
420 def _quiescentMagnitudeGetter(self, componentName, bandpassDict, columnNameList):
421 """
422 A generic getter for quiescent magnitudes of galaxy components.
424 @param [in] componentName is either 'bulge', 'disk', or 'agn'
426 @param [in] bandpassDict is a BandpassDict of the bandpasses
427 in which to calculate the magnitudes
429 @param [in] columnNameList is a list of the columns corresponding to
430 these magnitudes (for purposes of applying variability).
432 @param [out] magnitudes is a 2-D numpy array of magnitudes in which
433 rows correspond to bandpasses and columns correspond to astronomical
434 objects.
435 """
437 # figure out which of these columns we are actually calculating
438 indices = [ii for ii, name in enumerate(columnNameList)
439 if name in self._actually_calculated_columns]
441 if len(indices) == len(columnNameList):
442 indices = None
444 if componentName == 'bulge':
445 self._loadBulgeSedList(bandpassDict.wavelenMatch)
446 if not hasattr(self, '_bulgeSedList'):
447 sedList = None
448 else:
449 sedList = self._bulgeSedList
450 elif componentName == 'disk':
451 self._loadDiskSedList(bandpassDict.wavelenMatch)
452 if not hasattr(self, '_diskSedList'):
453 sedList = None
454 else:
455 sedList = self._diskSedList
456 elif componentName == 'agn':
457 self._loadAgnSedList(bandpassDict.wavelenMatch)
458 if not hasattr(self, '_agnSedList'):
459 sedList = None
460 else:
461 sedList = self._agnSedList
462 else:
463 raise RuntimeError('_quiescentMagnitudeGetter does not understand component %s ' \
464 % componentName)
466 if sedList is None:
467 magnitudes = np.ones((len(columnNameList), 0))
468 else:
469 magnitudes = bandpassDict.magListForSedList(sedList, indices=indices).transpose()
471 if self._hasCosmoDistMod():
472 cosmoDistMod = self.column_by_name('cosmologicalDistanceModulus')
473 if len(cosmoDistMod)>0:
474 for ix in range(magnitudes.shape[0]):
475 magnitudes[ix] += cosmoDistMod
477 return magnitudes
480 @compound('sigma_uBulge', 'sigma_gBulge', 'sigma_rBulge',
481 'sigma_iBulge', 'sigma_zBulge', 'sigma_yBulge')
482 def get_photometric_uncertainties_bulge(self):
483 """
484 Getter for photometric uncertainties associated with galaxy bulges
485 """
487 return self._magnitudeUncertaintyGetter(['uBulge', 'gBulge', 'rBulge',
488 'iBulge', 'zBulge', 'yBulge'],
489 ['u', 'g', 'r', 'i', 'z', 'y'],
490 'lsstBandpassDict')
493 @compound('sigma_uDisk', 'sigma_gDisk', 'sigma_rDisk',
494 'sigma_iDisk', 'sigma_zDisk', 'sigma_yDisk')
495 def get_photometric_uncertainties_disk(self):
496 """
497 Getter for photometeric uncertainties associated with galaxy disks
498 """
500 return self._magnitudeUncertaintyGetter(['uDisk', 'gDisk', 'rDisk',
501 'iDisk', 'zDisk', 'yDisk'],
502 ['u', 'g', 'r', 'i', 'z', 'y'],
503 'lsstBandpassDict')
506 @compound('sigma_uAgn', 'sigma_gAgn', 'sigma_rAgn',
507 'sigma_iAgn', 'sigma_zAgn', 'sigma_yAgn')
508 def get_photometric_uncertainties_agn(self):
509 """
510 Getter for photometric uncertainties associated with Agn
511 """
513 return self._magnitudeUncertaintyGetter(['uAgn', 'gAgn', 'rAgn',
514 'iAgn', 'zAgn', 'yAgn'],
515 ['u', 'g', 'r', 'i', 'z', 'y'],
516 'lsstBandpassDict')
519 @compound('uBulge', 'gBulge', 'rBulge', 'iBulge', 'zBulge', 'yBulge')
520 def get_lsst_bulge_mags(self):
521 """
522 Getter for bulge magnitudes in LSST bandpasses
523 """
525 # load a BandpassDict of LSST bandpasses, if not done already
526 if not hasattr(self, 'lsstBandpassDict'):
527 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles()
529 # actually calculate the magnitudes
530 mag = self._quiescentMagnitudeGetter('bulge', self.lsstBandpassDict,
531 self.get_lsst_bulge_mags._colnames)
533 mag += self._variabilityGetter(self.get_lsst_bulge_mags._colnames)
534 return mag
537 @compound('uDisk', 'gDisk', 'rDisk', 'iDisk', 'zDisk', 'yDisk')
538 def get_lsst_disk_mags(self):
539 """
540 Getter for galaxy disk magnitudes in the LSST bandpasses
541 """
543 # load a BandpassDict of LSST bandpasses, if not done already
544 if not hasattr(self, 'lsstBandpassDict'):
545 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles()
547 # actually calculate the magnitudes
548 mag = self._quiescentMagnitudeGetter('disk', self.lsstBandpassDict,
549 self.get_lsst_disk_mags._colnames)
551 mag += self._variabilityGetter(self.get_lsst_disk_mags._colnames)
552 return mag
555 @compound('uAgn', 'gAgn', 'rAgn', 'iAgn', 'zAgn', 'yAgn')
556 def get_lsst_agn_mags(self):
557 """
558 Getter for AGN magnitudes in the LSST bandpasses
559 """
561 # load a BandpassDict of LSST bandpasses, if not done already
562 if not hasattr(self, 'lsstBandpassDict'):
563 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles()
565 # actually calculate the magnitudes
566 mag = self._quiescentMagnitudeGetter('agn', self.lsstBandpassDict,
567 self.get_lsst_agn_mags._colnames)
569 mag += self._variabilityGetter(self.get_lsst_agn_mags._colnames)
570 return mag
572 @compound('lsst_u', 'lsst_g', 'lsst_r', 'lsst_i', 'lsst_z', 'lsst_y')
573 def get_lsst_total_mags(self):
574 """
575 Getter for total galaxy magnitudes in the LSST bandpasses
576 """
578 idList = self.column_by_name('uniqueId')
579 numObj = len(idList)
580 output = []
582 # Loop over the columns calculated by this getter. For each
583 # column, calculate the bluge, disk, and agn magnitude in the
584 # corresponding bandpass, then sum them using the
585 # sum_magnitudes method.
586 for columnName in self.get_lsst_total_mags._colnames:
587 if columnName not in self._actually_calculated_columns:
588 sub_list = [np.NaN]*numObj
589 else:
590 bandpass = columnName[-1]
591 bulge = self.column_by_name('%sBulge' % bandpass)
592 disk = self.column_by_name('%sDisk' % bandpass)
593 agn = self.column_by_name('%sAgn' % bandpass)
594 sub_list = self.sum_magnitudes(bulge=bulge, disk=disk, agn=agn)
596 output.append(sub_list)
597 return np.array(output)
602class PhotometryStars(PhotometryBase):
603 """
604 This mixin provides the infrastructure for doing photometry on stars
606 It assumes that we want LSST filters.
607 """
609 def _loadSedList(self, wavelen_match):
610 """
611 Method to load the member variable self._sedList, which is a SedList.
612 If self._sedList does not already exist, this method sets it up.
613 If it does already exist, this method flushes its contents and loads a new
614 chunk of Seds.
615 """
617 sedNameList = self.column_by_name('sedFilename')
618 magNormList = self.column_by_name('magNorm')
619 galacticAvList = self.column_by_name('galacticAv')
621 if len(sedNameList)==0:
622 return np.ones((0))
624 if not hasattr(self, '_sedList'):
625 self._sedList = SedList(sedNameList, magNormList,
626 galacticAvList=galacticAvList,
627 wavelenMatch=wavelen_match,
628 fileDir=getPackageDir('sims_sed_library'),
629 specMap=defaultSpecMap)
630 else:
631 self._sedList.flush()
632 self._sedList.loadSedsFromList(sedNameList, magNormList,
633 galacticAvList=galacticAvList)
636 def _quiescentMagnitudeGetter(self, bandpassDict, columnNameList):
637 """
638 This method gets the magnitudes for an InstanceCatalog, returning them
639 in a 2-D numpy array in which rows correspond to bandpasses and columns
640 correspond to astronomical objects.
642 @param [in] bandpassDict is a BandpassDict containing the bandpasses
643 whose magnitudes are to be calculated
645 @param [in] columnNameList is a list of the names of the magnitude columns
646 being calculated
648 @param [out] magnitudes is a 2-D numpy array of magnitudes in which
649 rows correspond to bandpasses in bandpassDict and columns correspond
650 to astronomical objects.
651 """
653 # figure out which of these columns we are actually calculating
654 indices = [ii for ii, name in enumerate(columnNameList)
655 if name in self._actually_calculated_columns]
657 if len(indices) == len(columnNameList):
658 indices = None
660 self._loadSedList(bandpassDict.wavelenMatch)
662 if not hasattr(self, '_sedList'):
663 magnitudes = np.ones((len(columnNameList),0))
664 else:
665 magnitudes = bandpassDict.magListForSedList(self._sedList, indices=indices).transpose()
667 return magnitudes
669 @compound('quiescent_lsst_u', 'quiescent_lsst_g', 'quiescent_lsst_r',
670 'quiescent_lsst_i', 'quiescent_lsst_z', 'quiescent_lsst_y')
671 def get_quiescent_lsst_magnitudes(self):
673 if not hasattr(self, 'lsstBandpassDict'):
674 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles()
676 return self._quiescentMagnitudeGetter(self.lsstBandpassDict,
677 self.get_quiescent_lsst_magnitudes._colnames)
679 @compound('lsst_u','lsst_g','lsst_r','lsst_i','lsst_z','lsst_y')
680 def get_lsst_magnitudes(self):
681 """
682 getter for LSST stellar magnitudes
683 """
685 magnitudes = np.array([self.column_by_name('quiescent_lsst_u'),
686 self.column_by_name('quiescent_lsst_g'),
687 self.column_by_name('quiescent_lsst_r'),
688 self.column_by_name('quiescent_lsst_i'),
689 self.column_by_name('quiescent_lsst_z'),
690 self.column_by_name('quiescent_lsst_y')])
692 delta = self._variabilityGetter(self.get_lsst_magnitudes._colnames)
693 magnitudes += delta
695 return magnitudes
697class PhotometrySSM(PhotometryBase):
698 """
699 A mixin to calculate photometry for solar system objects.
700 """
701 # Because solar system objects will not have dust extinctions, we should be able to read in every
702 # SED exactly once, calculate the colors and magnitudes, and then get actual magnitudes by adding
703 # an offset based on magNorm.
705 def _quiescentMagnitudeGetter(self, bandpassDict, columnNameList, bandpassTag='lsst'):
706 """
707 Method that actually does the work calculating magnitudes for solar system objects.
709 Because solar system objects have no dust extinction, this method works by loading
710 each unique Sed once, normalizing it, calculating its magnitudes in the desired
711 bandpasses, and then storing the normalizing magnitudes and the bandpass magnitudes
712 in a dict. Magnitudes for subsequent objects with identical Seds will be calculated
713 by adding an offset to the magnitudes. The offset is determined by comparing normalizing
714 magnitues.
716 @param [in] bandpassDict is an instantiation of BandpassDict representing the bandpasses
717 to be integrated over
719 @param [in] columnNameList is a list of the names of the columns being calculated
720 by this getter
722 @param [in] bandpassTag (optional) is a string indicating the name of the bandpass system
723 (i.e. 'lsst', 'sdss', etc.). This is in case the user wants to calculate the magnitudes
724 in multiple systems simultaneously. In that case, the dict will store magnitudes for each
725 Sed in each magnitude system separately.
727 @param [out] a numpy array of magnitudes corresponding to bandpassDict.
728 """
730 # figure out which of these columns we are actually calculating
731 indices = [ii for ii, name in enumerate(columnNameList)
732 if name in self._actually_calculated_columns]
734 if len(indices) == len(columnNameList):
735 indices = None
737 if not hasattr(self, '_ssmMagDict'):
738 self._ssmMagDict = {}
739 self._ssmMagNormDict = {}
740 self._file_dir = getPackageDir('sims_sed_library')
741 self._spec_map = defaultSpecMap
742 self._normalizing_bandpass = Bandpass()
743 self._normalizing_bandpass.imsimBandpass()
745 sedNameList = self.column_by_name('sedFilename')
746 magNormList = self.column_by_name('magNorm')
748 if len(sedNameList)==0:
749 # need to return something when InstanceCatalog goes through
750 # it's "dry run" to determine what columns are required from
751 # the database
752 return np.zeros((len(bandpassDict.keys()),0))
754 magListOut = []
756 for sedName, magNorm in zip(sedNameList, magNormList):
757 magTag = bandpassTag+'_'+sedName
758 if sedName not in self._ssmMagNormDict or magTag not in self._ssmMagDict:
759 dummySed = Sed()
760 dummySed.readSED_flambda(os.path.join(self._file_dir, self._spec_map[sedName]))
761 fnorm = dummySed.calcFluxNorm(magNorm, self._normalizing_bandpass)
762 dummySed.multiplyFluxNorm(fnorm)
763 magList = bandpassDict.magListForSed(dummySed, indices=indices)
764 self._ssmMagDict[magTag] = magList
765 self._ssmMagNormDict[sedName] = magNorm
766 else:
767 dmag = magNorm - self._ssmMagNormDict[sedName]
768 magList = self._ssmMagDict[magTag] + dmag
769 magListOut.append(magList)
771 return np.array(magListOut).transpose()
774 @compound('lsst_u','lsst_g','lsst_r','lsst_i','lsst_z','lsst_y')
775 def get_lsst_magnitudes(self):
776 """
777 getter for LSST magnitudes of solar system objects
778 """
779 if not hasattr(self, 'lsstBandpassDict'):
780 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles()
782 return self._quiescentMagnitudeGetter(self.lsstBandpassDict, self.get_lsst_magnitudes._colnames)
785 def get_magFilter(self):
786 """
787 Generate the magnitude in the filter of the observation.
788 """
789 magFilter = 'lsst_' + self.obs_metadata.bandpass
790 return self.column_by_name(magFilter)
792 def get_magSNR(self):
793 """
794 Calculate the SNR for the observation, given m5 from obs_metadata and the trailing losses.
795 """
796 magFilter = self.column_by_name('magFilter')
797 bandpass = self.lsstBandpassDict[self.obs_metadata.bandpass]
798 # Get m5 for the visit
799 m5 = self.obs_metadata.m5[self.obs_metadata.bandpass]
800 # Adjust the magnitude of the source for the trailing losses.
801 dmagSNR = self.column_by_name('dmagTrailing')
802 magObj = magFilter - dmagSNR
803 if len(magObj) == 0:
804 snr = []
805 else:
806 snr, gamma = calcSNR_m5(magObj, bandpass, m5, self.photParams)
807 return snr
809 def get_visibility(self):
810 """
811 Generate a None/1 flag indicating whether the object was detected or not.
813 Sets the random seed for 'calculateVisibility' using the obs_metadata.obsHistId
814 """
815 magFilter = self.column_by_name('magFilter')
816 dmagDetect = self.column_by_name('dmagDetection')
817 magObj = magFilter - dmagDetect
818 # Adjusted m5 value, accounting for the fact these are moving objects.
819 mjdSeed = np.int(self.obs_metadata.mjd.TAI * 1000000) % 4294967295
820 visibility = self.calculateVisibility(magObj, randomSeed=mjdSeed, pre_generate_randoms=True)
821 return visibility
824 @compound('dmagTrailing', 'dmagDetection')
825 def get_ssm_dmag(self):
826 """
827 This getter will calculate:
829 dmagTrailing: the offset in m5 used to represent the loss in signal to noise
830 resulting from the fact that the object's motion smears out its PSF
832 dmagDetection: the offset in m5 used to represent the shift in detection
833 threshold resulting from the fact that the object's motion smears out
834 its PSF
835 """
837 if self.obs_metadata.seeing is None:
838 raise RuntimeError("Cannot calculate dmagTraling/dmagDetection. "
839 "Your catalog's ObservationMetaData does not "
840 "specify seeing.")
842 if len(self.obs_metadata.seeing)>1:
843 valueList = list(self.obs_metadata.seeing.values())
844 for ix in range(1, len(valueList)):
845 if np.abs(valueList[ix]-valueList[0])>0.0001:
847 raise RuntimeError("dmagTrailing/dmagDetection calculation is confused. "
848 "Your catalog's ObservationMetaData contains multiple "
849 "seeing values. Re-create your catalog with only one seeing value.")
851 if not hasattr(self, 'photParams') or self.photParams is None:
852 raise RuntimeError("You cannot calculate dmagTrailing/dmagDetection. "
853 "Your catalog does not have an associated PhotometricParameters "
854 "member variable. It is impossible to know what the exposure time is.")
856 dradt = self.column_by_name('velRa') # in radians per day (actual sky velocity;
857 # i.e., no need to divide by cos(dec))
859 ddecdt = self.column_by_name('velDec') # in radians per day
861 if len(dradt)==0:
862 return np.zeros((2,0))
864 a_trail = 0.76
865 b_trail = 1.16
866 a_det = 0.42
867 b_det = 0.00
868 seeing = self.obs_metadata.seeing[self.obs_metadata.bandpass] # this will be in arcsec
869 texp = self.photParams.nexp*self.photParams.exptime # in seconds
870 velocity = np.sqrt(np.power(np.degrees(dradt),2) + np.power(np.degrees(ddecdt),2)) # in degrees/day
871 x = velocity*texp/(24.0*seeing)
872 xsq = np.power(x,2)
873 dmagTrail = 1.25*np.log10(1.0 + a_trail * xsq/(1.0+b_trail*x))
874 dmagDetect = 1.25*np.log10(1.0 + a_det * xsq/(1.0 + b_det*x))
876 return np.array([dmagTrail, dmagDetect])