Coverage for python/lsst/sims/photUtils/Bandpass.py : 9%

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#
2# LSST Data Management System
3# Copyright 2008, 2009, 2010, 2011, 2012 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
23"""
24bandpass -
26Class data:
27 wavelen (nm)
28 sb (Transmission, 0-1)
29 phi (Normalized system response)
30 wavelen/sb are guaranteed gridded.
31 phi will be None until specifically needed;
32 any updates to wavelen/sb within class will reset phi to None.
33 the name of the bandpass file
35Note that Bandpass objects are required to maintain a uniform grid in wavelength, rather than
36being allowed to have variable wavelength bins. This is because of the method used in 'Sed' to
37calculate magnitudes, but is simpler to enforce here.
39Methods:
40 __init__ : pass wavelen/sb arrays and set values (on grid) OR set data to None's
41 setWavelenLimits / getWavelenLimits: set or get the wavelength limits of bandpass
42 setBandpass: set bandpass using wavelen/sb input values
43 getBandpass: return copies of wavelen/sb values
44 imsimBandpass : set up a bandpass which is 0 everywhere but one wavelength
45 (this can be useful for imsim magnitudes)
46 readThroughput : set up a bandpass by reading data from a single file
47 readThroughtputList : set up a bandpass by reading data from many files and multiplying
48 the individual throughputs
49 resampleBandpass : use linear interpolation to resample wavelen/sb arrays onto a regular grid
50 (grid is specified by min/max/step size)
51 sbTophi : calculate phi from sb - needed for calculating magnitudes
52 multiplyThroughputs : multiply self.wavelen/sb by given wavelen/sb and return
53 new wavelen/sb arrays (gridded like self)
54 calcZP_t : calculate instrumental zeropoint for this bandpass
55 calcEffWavelen: calculate the effective wavelength (using both Sb and Phi) for this bandpass
56 writeThroughput : utility to write bandpass information to file
58"""
59from __future__ import print_function
60from builtins import range
61from builtins import object
62import os
63import warnings
64import numpy
65import scipy.interpolate as interpolate
66import gzip
67from .PhysicalParameters import PhysicalParameters
68from .Sed import Sed # For ZP_t and M5 calculations. And for 'fast mags' calculation.
70__all__ = ["Bandpass"]
73class Bandpass(object):
74 """
75 Class for holding and utilizing telescope bandpasses.
76 """
77 def __init__(self, wavelen=None, sb=None,
78 wavelen_min=None, wavelen_max=None, wavelen_step=None):
79 """
80 Initialize bandpass object, with option to pass wavelen/sb arrays in directly.
82 Also can specify wavelength grid min/max/step or use default - sb and wavelen will
83 be resampled to this grid. If wavelen/sb are given, these will be set, but phi
84 will be set to None.
85 Otherwise all set to None and user should call readThroughput, readThroughputList,
86 or imsimBandpass to populate bandpass data.
87 """
89 self._physParams = PhysicalParameters()
91 if wavelen_min is None:
92 if wavelen is None:
93 wavelen_min = self._physParams.minwavelen
94 else:
95 wavelen_min = wavelen.min()
96 if wavelen_max is None:
97 if wavelen is None:
98 wavelen_max = self._physParams.maxwavelen
99 else:
100 wavelen_max = wavelen.max()
101 if wavelen_step is None:
102 if wavelen is None:
103 wavelen_step = self._physParams.wavelenstep
104 else:
105 wavelen_step = numpy.diff(wavelen).min()
106 self.setWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
107 self.wavelen=None
108 self.sb=None
109 self.phi=None
110 self.bandpassname = None
111 if (wavelen is not None) and (sb is not None):
112 self.setBandpass(wavelen, sb, wavelen_min, wavelen_max, wavelen_step)
114 return
116 ## getters and setters
117 def setWavelenLimits(self, wavelen_min, wavelen_max, wavelen_step):
118 """
119 Set internal records of wavelen limits, _min, _max, _step.
120 """
121 # If we've been given values for wavelen_min, _max, _step, set them here.
122 if wavelen_min is not None:
123 self.wavelen_min = wavelen_min
124 if wavelen_max is not None:
125 self.wavelen_max = wavelen_max
126 if wavelen_step is not None:
127 self.wavelen_step = wavelen_step
128 return
130 def getWavelenLimits(self, wavelen_min, wavelen_max, wavelen_step):
131 """
132 Return appropriate wavelen limits (_min, _max, _step) if passed None values.
133 """
134 if wavelen_min is None:
135 wavelen_min = self.wavelen_min
136 if wavelen_max is None:
137 wavelen_max = self.wavelen_max
138 if wavelen_step is None:
139 wavelen_step = self.wavelen_step
140 return wavelen_min, wavelen_max, wavelen_step
142 def setBandpass(self, wavelen, sb,
143 wavelen_min=None, wavelen_max=None, wavelen_step=None):
144 """
145 Populate bandpass data with wavelen/sb arrays.
147 Sets self.wavelen/sb on a grid of wavelen_min/max/step. Phi set to None.
148 """
149 self.setWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
150 # Check data type.
151 if (isinstance(wavelen, numpy.ndarray)==False) or (isinstance(sb, numpy.ndarray)==False):
152 raise ValueError("Wavelen and sb arrays must be numpy arrays.")
153 # Check data matches in length.
154 if (len(wavelen)!=len(sb)):
155 raise ValueError("Wavelen and sb arrays must have the same length.")
156 # Data seems ok then, make a new copy of this data for self.
157 self.wavelen = numpy.copy(wavelen)
158 self.phi = None
159 self.sb = numpy.copy(sb)
160 # Resample wavelen/sb onto grid.
161 self.resampleBandpass(wavelen_min=wavelen_min, wavelen_max=wavelen_max, wavelen_step=wavelen_step)
162 self.bandpassname = 'FromArrays'
163 return
165 def imsimBandpass(self, imsimwavelen=500.0,
166 wavelen_min=None, wavelen_max=None, wavelen_step=None):
167 """
168 Populate bandpass data with sb=0 everywhere except sb=1 at imsimwavelen.
170 Sets wavelen/sb, with grid min/max/step as optional parameters. Does NOT set phi.
171 """
172 self.setWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
173 # Set up arrays.
174 self.wavelen = numpy.arange(self.wavelen_min, self.wavelen_max+self.wavelen_step,
175 self.wavelen_step, dtype='float')
176 self.phi = None
177 # Set sb.
178 self.sb = numpy.zeros(len(self.wavelen), dtype='float')
179 self.sb[abs(self.wavelen-imsimwavelen)<self.wavelen_step/2.0] = 1.0
180 self.bandpassname = 'IMSIM'
181 return
183 def readThroughput(self, filename, wavelen_min=None, wavelen_max=None, wavelen_step=None):
184 """
185 Populate bandpass data with data (wavelen/sb) read from file, resample onto grid.
187 Sets wavelen/sb, with grid min/max/step as optional parameters. Does NOT set phi.
188 """
189 self.setWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
190 # Set self values to None in case of file read error.
191 self.wavelen = None
192 self.phi = None
193 self.sb = None
194 # Check for filename error.
195 # If given list of filenames, pass to (and return from) readThroughputList.
196 if isinstance(filename, list):
197 warnings.warn("Was given list of files, instead of a single file. Using readThroughputList instead")
198 self.readThroughputList(componentList=filename,
199 wavelen_min=self.wavelen_min, wavelen_max=self.wavelen_max,
200 wavelen_step=self.wavelen_step)
201 # Filename is single file, now try to open file and read data.
202 try:
203 if filename.endswith('.gz'):
204 f = gzip.open(filename, 'rt')
205 else:
206 f = open(filename, 'r')
207 except IOError:
208 try:
209 if filename.endswith('.gz'):
210 f = open(filename[:-3], 'r')
211 else:
212 f = gzip.open(filename+'.gz', 'rt')
213 except IOError:
214 raise IOError('The throughput file %s does not exist' %(filename))
215 # The throughput file should have wavelength(A), throughput(Sb) as first two columns.
216 wavelen = []
217 sb = []
218 for line in f:
219 if line.startswith("#") or line.startswith('$') or line.startswith('!'):
220 continue
221 values = line.split()
222 if len(values)<2:
223 continue
224 if (values[0] == '$') or (values[0] =='#') or (values[0] =='!'):
225 continue
226 wavelen.append(float(values[0]))
227 sb.append(float(values[1]))
228 f.close()
229 self.bandpassname = filename
230 # Set up wavelen/sb.
231 self.wavelen = numpy.array(wavelen, dtype='float')
232 self.sb = numpy.array(sb, dtype='float')
233 # Check that wavelength is monotonic increasing and non-repeating in wavelength. (Sort on wavelength).
234 if len(self.wavelen) != len(numpy.unique(self.wavelen)):
235 raise ValueError('The wavelength values in file %s are non-unique.' %(filename))
236 # Sort values.
237 p = self.wavelen.argsort()
238 self.wavelen = self.wavelen[p]
239 self.sb = self.sb[p]
240 # Resample throughput onto grid.
241 if self.needResample():
242 self.resampleBandpass()
243 if self.sb.sum() < 1e-300:
244 raise Exception("Bandpass data from %s has no throughput in "
245 "desired grid range %f, %f" %(filename, wavelen_min, wavelen_max))
246 return
248 def readThroughputList(self, componentList=['detector.dat', 'lens1.dat',
249 'lens2.dat', 'lens3.dat',
250 'm1.dat', 'm2.dat', 'm3.dat',
251 'atmos_std.dat'],
252 rootDir = '.',
253 wavelen_min=None, wavelen_max=None, wavelen_step=None):
254 """
255 Populate bandpass data by reading from a series of files with wavelen/Sb data.
257 Multiplies throughputs (sb) from each file to give a final bandpass throughput.
258 Sets wavelen/sb, with grid min/max/step as optional parameters. Does NOT set phi.
259 """
260 # ComponentList = names of files in that directory.
261 # A typical component list of all files to build final component list, including filter, might be:
262 # componentList=['detector.dat', 'lens1.dat', 'lens2.dat', 'lens3.dat',
263 # 'm1.dat', 'm2.dat', 'm3.dat', 'atmos_std.dat', 'ideal_g.dat']
264 #
265 # Set wavelen limits for this object, if any updates have been given.
266 self.setWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
267 # Set up wavelen/sb on grid.
268 self.wavelen = numpy.arange(self.wavelen_min, self.wavelen_max+self.wavelen_step/2., self.wavelen_step,
269 dtype='float')
270 self.phi = None
271 self.sb = numpy.ones(len(self.wavelen), dtype='float')
272 # Set up a temporary bandpass object to hold data from each file.
273 tempbandpass = Bandpass(wavelen_min=self.wavelen_min, wavelen_max=self.wavelen_max,
274 wavelen_step=self.wavelen_step)
275 for component in componentList:
276 # Read data from file.
277 tempbandpass.readThroughput(os.path.join(rootDir, component))
278 # Multiply self by new sb values.
279 self.sb = self.sb * tempbandpass.sb
280 self.bandpassname = ''.join(componentList)
281 return
283 def getBandpass(self):
284 wavelen = numpy.copy(self.wavelen)
285 sb = numpy.copy(self.sb)
286 return wavelen, sb
288 ## utilities
290 def checkUseSelf(self, wavelen, sb):
291 """
292 Simple utility to check if should be using self.wavelen/sb or passed arrays.
294 Useful for other methods in this class.
295 Also does data integrity check on wavelen/sb if not self.
296 """
297 update_self = False
298 if (wavelen is None) or (sb is None):
299 # Then one of the arrays was not passed - check if true for both.
300 if (wavelen is not None) or (sb is not None):
301 # Then only one of the arrays was passed - raise exception.
302 raise ValueError("Must either pass *both* wavelen/sb pair, or use self defaults")
303 # Okay, neither wavelen or sb was passed in - using self only.
304 update_self = True
305 else:
306 # Both of the arrays were passed in - check their validity.
307 if (isinstance(wavelen, numpy.ndarray)==False) or (isinstance(sb, numpy.ndarray)==False):
308 raise ValueError("Must pass wavelen/sb as numpy arrays")
309 if len(wavelen)!=len(sb):
310 raise ValueError("Must pass equal length wavelen/sb arrays")
311 return update_self
313 def needResample(self, wavelen=None,
314 wavelen_min=None, wavelen_max=None, wavelen_step=None):
315 """
316 Return true/false of whether wavelen need to be resampled onto a grid.
318 Given wavelen OR defaults to self.wavelen/sb - return True/False check on whether
319 the arrays need to be resampled to match wavelen_min/max/step grid.
320 """
321 # Thought about adding wavelen_match option here (to give this an array to match to, rather than
322 # the grid parameters .. but then thought bandpass always needs to be on a regular grid (because
323 # of magnitude calculations). So, this will stay match to the grid parameters only.
324 # Check wavelength limits.
325 wavelen_min, wavelen_max, wavelen_step = self.getWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
326 # Check if method acting on self or other data (here, using data type checks primarily).
327 update_self = self.checkUseSelf(wavelen, wavelen)
328 if update_self:
329 wavelen = self.wavelen
330 wavelen_max_in = wavelen[len(wavelen)-1]
331 wavelen_min_in = wavelen[0]
332 wavelen_step_in = wavelen[1]-wavelen[0]
333 # Start check if data is already gridded.
334 need_regrid=True
335 # First check minimum/maximum and first step in array.
336 if ((wavelen_min_in == wavelen_min) and (wavelen_max_in == wavelen_max)):
337 # Then check on step size.
338 stepsize = numpy.unique(numpy.diff(wavelen))
339 if (len(stepsize) == 1) and (stepsize[0] == wavelen_step):
340 need_regrid = False
341 # At this point, need_grid=True unless it's proven to be False, so return value.
342 return need_regrid
344 def resampleBandpass(self, wavelen=None, sb=None,
345 wavelen_min=None, wavelen_max=None, wavelen_step=None):
346 """
347 Resamples wavelen/sb (or self.wavelen/sb) onto grid defined by min/max/step.
349 Either returns wavelen/sb (if given those arrays) or updates wavelen / Sb in self.
350 If updating self, resets phi to None.
351 """
352 # Check wavelength limits.
353 wavelen_min, wavelen_max, wavelen_step = self.getWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
354 # Is method acting on self.wavelen/sb or passed in wavelen/sb? Sort it out.
355 update_self = self.checkUseSelf(wavelen, sb)
356 if update_self:
357 wavelen = self.wavelen
358 sb = self.sb
359 # Now, on with the resampling.
360 if (wavelen.min() > wavelen_max) or (wavelen.max() < wavelen_min):
361 raise Exception("No overlap between known wavelength range and desired wavelength range.")
362 # Set up gridded wavelength.
363 wavelen_grid = numpy.arange(wavelen_min, wavelen_max+wavelen_step/2.0, wavelen_step, dtype='float')
364 # Do the interpolation of wavelen/sb onto the grid. (note wavelen/sb type failures will die here).
365 f = interpolate.interp1d(wavelen, sb, fill_value=0, bounds_error=False)
366 sb_grid = f(wavelen_grid)
367 # Update self values if necessary.
368 if update_self:
369 self.phi = None
370 self.wavelen = wavelen_grid
371 self.sb = sb_grid
372 self.setWavelenLimits(wavelen_min, wavelen_max, wavelen_step)
373 return
374 return wavelen_grid, sb_grid
376 ## more complicated bandpass functions
378 def sbTophi(self):
379 """
380 Calculate and set phi - the normalized system response.
382 This function only pdates self.phi.
383 """
384 # The definition of phi = (Sb/wavelength)/\int(Sb/wavelength)dlambda.
385 # Due to definition of class, self.sb and self.wavelen are guaranteed equal-gridded.
386 dlambda = self.wavelen[1]-self.wavelen[0]
387 self.phi = self.sb/self.wavelen
388 # Normalize phi so that the integral of phi is 1.
389 phisum = self.phi.sum()
390 if phisum < 1e-300:
391 raise Exception("Phi is poorly defined (nearly 0) over bandpass range.")
392 norm = phisum * dlambda
393 self.phi = self.phi / norm
394 return
396 def multiplyThroughputs(self, wavelen_other, sb_other):
397 """
398 Multiply self.sb by another wavelen/sb pair, return wavelen/sb arrays.
400 The returned arrays will be gridded like this bandpass.
401 This method does not affect self.
402 """
403 # Resample wavelen_other/sb_other to match this bandpass.
404 if self.needResample(wavelen=wavelen_other):
405 wavelen_other, sb_other = self.resampleBandpass(wavelen=wavelen_other, sb=sb_other)
406 # Make new memory copy of wavelen.
407 wavelen_new = numpy.copy(self.wavelen)
408 # Calculate new transmission - this is also new memory.
409 sb_new = self.sb * sb_other
410 return wavelen_new, sb_new
412 def calcZP_t(self, photometricParameters):
413 """
414 Calculate the instrumental zeropoint for a bandpass.
416 @param [in] photometricParameters is an instantiation of the
417 PhotometricParameters class that carries details about the
418 photometric response of the telescope. Defaults to LSST values.
419 """
420 # ZP_t is the magnitude of a (F_nu flat) source which produced 1 count per second.
421 # This is often also known as the 'instrumental zeropoint'.
422 # Set gain to 1 if want to explore photo-electrons rather than adu.
423 # The typical LSST exposure time is 15s and this is default here, but typical zp_t definition is for 1s.
424 # SED class uses flambda in ergs/cm^2/s/nm, so need effarea in cm^2.
425 #
426 # Check dlambda value for integral.
427 dlambda = self.wavelen[1] - self.wavelen[0]
428 # Set up flat source of arbitrary brightness,
429 # but where the units of fnu are Jansky (for AB mag zeropoint = -8.9).
430 flatsource = Sed()
431 flatsource.setFlatSED(wavelen_min=self.wavelen_min, wavelen_max=self.wavelen_max,
432 wavelen_step=self.wavelen_step)
433 adu = flatsource.calcADU(self, photParams=photometricParameters)
434 # Scale fnu so that adu is 1 count/expTime.
435 flatsource.fnu = flatsource.fnu * (1/adu)
436 # Now need to calculate AB magnitude of the source with this fnu.
437 if self.phi is None:
438 self.sbTophi()
439 zp_t = flatsource.calcMag(self)
440 return zp_t
443 def calcEffWavelen(self):
444 """
445 Calculate effective wavelengths for filters.
446 """
447 # This is useful for summary numbers for filters.
448 # Calculate effective wavelength of filters.
449 if self.phi is None:
450 self.sbTophi()
451 effwavelenphi = (self.wavelen*self.phi).sum()/self.phi.sum()
452 effwavelensb = (self.wavelen*self.sb).sum()/self.sb.sum()
453 return effwavelenphi, effwavelensb
456 def writeThroughput(self, filename, print_header=None, write_phi=False):
457 """
458 Write throughput to a file.
459 """
460 # Useful if you build a throughput up from components and need to record the combined value.
461 f = open(filename, 'w')
462 # Print header.
463 if print_header is not None:
464 if not print_header.startswith('#'):
465 print_header = '#' + print_header
466 f.write(print_header)
467 if write_phi:
468 if self.phi is None:
469 self.sbTophi()
470 print("# Wavelength(nm) Throughput(0-1) Phi", file=f)
471 else:
472 print("# Wavelength(nm) Throughput(0-1)", file=f)
473 # Loop through data, printing out to file.
474 for i in range(0, len(self.wavelen), 1):
475 if write_phi:
476 print(self.wavelen[i], self.sb[i], self.phi[i], file=f)
477 else:
478 print(self.wavelen[i], self.sb[i], file=f)
479 f.close()
480 return