Hide keyboard shortcuts

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# 

22 

23""" 

24bandpass - 

25 

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 

34 

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. 

38 

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 

57 

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. 

69 

70__all__ = ["Bandpass"] 

71 

72 

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. 

81 

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 """ 

88 

89 self._physParams = PhysicalParameters() 

90 

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) 

113 

114 return 

115 

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 

129 

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 

141 

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. 

146 

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 

164 

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. 

169 

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 

182 

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. 

186 

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 

247 

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. 

256 

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 

282 

283 def getBandpass(self): 

284 wavelen = numpy.copy(self.wavelen) 

285 sb = numpy.copy(self.sb) 

286 return wavelen, sb 

287 

288 ## utilities 

289 

290 def checkUseSelf(self, wavelen, sb): 

291 """ 

292 Simple utility to check if should be using self.wavelen/sb or passed arrays. 

293 

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 

312 

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. 

317 

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 

343 

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. 

348 

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 

375 

376 ## more complicated bandpass functions 

377 

378 def sbTophi(self): 

379 """ 

380 Calculate and set phi - the normalized system response. 

381 

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 

395 

396 def multiplyThroughputs(self, wavelen_other, sb_other): 

397 """ 

398 Multiply self.sb by another wavelen/sb pair, return wavelen/sb arrays. 

399 

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 

411 

412 def calcZP_t(self, photometricParameters): 

413 """ 

414 Calculate the instrumental zeropoint for a bandpass. 

415 

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 

441 

442 

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 

454 

455 

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