1 """Galaxy luminosity functions (Schechter functions).
2
3 The `LFHistory` class implements a luminosity function history,
4 encapsulating the changes in the galaxy luminosity distribution
5 parameters as a function of redshift.
6
7 """
8
9 import os
10 import optparse
11
12 import numpy
13 import scipy.special
14
15 import cosmolopy.reionization as cr
16 import cosmolopy.distance as cd
17 import cosmolopy.parameters as cp
18 import cosmolopy.constants as cc
19 import cosmolopy.utils as utils
20 from cosmolopy.saveable import Saveable
21 import cosmolopy.magnitudes as magnitudes
22
24 """Use Labbe et al. (2009) relation between stellar mass and SFR.
25
26 See arXiv:0911.1356v4
27
28 Parameters
29 ----------
30
31 sfr:
32 the star formation rate in M_Sun / year.
33
34 Returns
35 -------
36
37 mass:
38 stellar mass in units of M_Sun
39 """
40 return 10**(8.7 + 1.06 * numpy.log10(sfr))
41
43 """Use Labbe et al. (2009) relation between stellar mass and SFR.
44
45 See arXiv:0911.1356v4
46
47 Parameters
48 ----------
49
50 mass:
51 stellar mass in units of M_Sun
52
53 Returns
54 -------
55
56 sfr:
57 the star formation rate in M_Sun / year.
58
59 """
60 return 10**((numpy.log10(mass) - 8.7)/1.06)
61
63 """Use Kennicutt (1998) conversion from UV luminosity to star formation rate.
64
65 Parameters
66 ----------
67
68 luminosity:
69 the luminosity in units of ergs s^-1 Hz^-1 anywhere between
70 1500-2800 Angstroms.
71
72 Returns
73 -------
74
75 The SFR in Msun/year.
76
77 Notes
78 -----
79
80 Kennicutt (1998ARA&A..36..189K) says:
81
82 SFR/(MSun/year) = 1.4 * 10^-28 (L_nu/ergs s^-1 Hz^-1)
83
84 where L_nu is the UV luminosity anywhere between 1500-2800 Angstroms.
85 """
86 return 1.4e-28 * luminosity
87
89 """Use Kennicutt (1998) conversion from UV luminosity to star formation rate.
90
91 Parameters
92 ----------
93
94 sfr:
95 The SFR in Msun/year.
96
97 Returns
98 -------
99
100 luminosity:
101 the luminosity in units of ergs s^-1 Hz^-1 anywhere between
102 1500-2800 Angstroms.
103
104 Notes
105 -----
106
107 Kennicutt (1998ARA&A..36..189K) says:
108
109 SFR/(MSun/year) = 1.4 * 10^-28 (L_nu/ergs s^-1 Hz^-1)
110
111 where L_nu is the UV luminosity anywhere between 1500-2800 Angstroms.
112 """
113 return sfr/(1.4e-28)
114
116 """Use Kennicutt (1998) conversion from UV luminosity to AB magnitude.
117
118 Convenience function: uses L_nu_from_sfr and
119 magnitudes.magnitude_AB_from_L_nu.
120 """
121 lnu = L_nu_from_sfr(sfr)
122 return magnitudes.magnitude_AB_from_L_nu(lnu)
123
124 -def schechterL(luminosity, phiStar, alpha, LStar):
125 """Schechter luminosity function."""
126 LOverLStar = (luminosity/LStar)
127 return (phiStar/LStar) * LOverLStar**alpha * numpy.exp(- LOverLStar)
128
130 """Schechter luminosity function by magnitudes."""
131 MStarMinM = 0.4 * (MStar - magnitude)
132 return (0.4 * numpy.log(10) * phiStar *
133 10.0**(MStarMinM * (alpha + 1.)) * numpy.exp(-10.**MStarMinM))
134
136 """Integrate luminosity in galaxies above luminosity=L.
137
138 Uses an analytical formula.
139 """
140
141
142
143
144 return (schechterTotLL(phiStar, alpha, LStar) *
145 (1. - scipy.special.gammainc(alpha+2., luminosity/LStar)))
146
155
157 """Integrate total luminosity in galaxies.
158
159 Uses an analytical formula.
160 """
161 return phiStar * LStar * scipy.special.gamma(alpha + 2.)
162
170
171 -def iPhotonRateDensity(schechterParams,
172 maglim=None,
173 sedParams={},
174 wavelength=1500.):
175 """Ionizing photon rate density from a luminosity function.
176
177 in units of photons s^-1.
178
179 Given schecterParams, the parameters of a Schechter luminosity
180 function (in terms of AB Magnitudes), sedParams, the parameters of
181 the galactic Spectral Energy Distribution, and the wavelength of
182 the AB Magnitudes, calculate the emission rate density of ionizing
183 photons.
184
185 See Also
186 --------
187
188 BrokenPowerlawSED
189
190 schechterTotLM
191
192 """
193 if maglim is None:
194 lum = schechterTotLM(**schechterParams)
195 else:
196 lum = schechterCumuLM(maglim, **schechterParams)
197 rQL = BrokenPowerlawSED(**sedParams).iPhotonRateRatio(wavelength)
198 return lum * rQL
199
200
201 B2007_z4 = {'MStar': -20.98,
202 'phiStar': 1.3e-3,
203 'alpha': -1.73,
204 'z':3.8}
205
206 B2007_z5 = {'MStar': -20.64,
207 'phiStar': 1.0e-3,
208 'alpha': -1.66,
209 'z':5.0}
210
211 B2007_z6 = {'MStar': -20.24,
212 'phiStar': 1.4e-3,
213 'alpha': -1.74,
214 'z':5.9}
215
216 B2007_z7 = {'MStar': -19.3,
217 'phiStar': 1.4e-3,
218 'alpha': -1.74,
219 'z':7.4
220 }
221
222
223 O2010_z7 = {'MStar': -19.9,
224 'phiStar': 1.4e-3,
225 'alpha': -1.77,
226 'z':6.8
227 }
228
229
230 B2008 = {'z' : [3.8, 5.0, 5.9, 7.3, 9.0],
231 'MStar': [-20.98, -20.64, -20.24, -19.8, -19.6],
232 'phiStar': [1.3e-3, 1.0e-3, 1.4e-3, 1.1e-3, 1.1e-3],
233 'alpha': [-1.73, -1.66, -1.74, -1.74, -1.74]}
234
235
236 B2008_fixed = {'z' : [3.8, 5.0, 5.9, 7.3, 9.0],
237 'MStar': [-20.98, -20.64, -20.24, -19.8, -19.6],
238 'phiStar': [1.1e-3, 1.1e-3, 1.1e-3, 1.1e-3, 1.1e-3],
239 'alpha': [-1.74, -1.74, -1.74, -1.74, -1.74]}
240
241
242 zlinear = numpy.array([3.8, 5.0, 5.9, 7.3, 9.0])
243 B2008_linear_z5 = {'z' : zlinear,
244 'MStar': 0.36 * (zlinear - 5.0) - 20.64,
245 'phiStar': [1.1e-3, 1.1e-3, 1.1e-3, 1.1e-3, 1.1e-3],
246 'alpha': [-1.74, -1.74, -1.74, -1.74, -1.74]}
247
248
249 T2010ICLF = {'z' : [4., 5., 7., 8., 9.],
250 'MStar' : [-20.90, -20.55, -20.00, -19.70, -19.55],
251 'phiStar' : [1.3e-3, 1.5e-3, 1.0e-3, 0.60e-3, 0.22e-3],
252 'alpha' : [-1.57, -1.63, -1.84, -1.90, -1.999]}
253
254 -class LFHistory(Saveable):
255 """Interpolate / extrapolate the Schechter parameters.
256
257 By default, above the observed redshift range:
258
259 MStar is linearly extrapolated as a function of time (not z) to high z.
260
261 phiStar and alpha are constant at high z.
262
263 """
264
265 - def __init__(self, params=B2008,
266 MStar_bounds = ['extrapolate', float('NaN')],
267 phiStar_bounds = ['constant', float('NaN')],
268 alpha_bounds = ['constant', float('NaN')],
269 extrap_args = {},
270 extrap_var = 'z',
271 sedParams = {},
272 wavelength = 1500.,
273 **cosmo):
274
275 for (k, v) in params.iteritems():
276 params[k] = numpy.asarray(v)
277
278 self.params = params
279 self.MStar_bounds = MStar_bounds
280 self.phiStar_bounds = phiStar_bounds
281 self.alpha_bounds = alpha_bounds
282 self.extrap_args = extrap_args
283 self.extrap_var = extrap_var
284 self.sedParams = sedParams
285 self.wavelength = wavelength
286 self.cosmo = cosmo
287
288 self.zobs = params['z']
289 self.tobs = cd.age(self.zobs, **cosmo)[0]
290 self.MStar = params['MStar']
291 self.phiStar = params['phiStar']
292 self.alpha = params['alpha']
293
294 if extrap_var == 't':
295 self.xobs = self.tobs
296 self._iPhotFunc = \
297 lambda t1, mag: (self.iPhotonRateDensity_t(t1,
298 maglim=mag))
299
300 elif extrap_var == 'z':
301 self.xobs = self.zobs
302 MStar_bounds = MStar_bounds[::-1]
303 phiStar_bounds = phiStar_bounds[::-1]
304 alpha_bounds = alpha_bounds[::-1]
305 self._iPhotFunc = \
306 lambda z1, mag: (self.iPhotonRateDensity_z(z1,
307 maglim=mag))
308
309 self._MStarfunc = utils.Extrapolate1d(self.xobs, self.MStar,
310 bounds_behavior=MStar_bounds,
311 **extrap_args
312 )
313 print "M*:",
314 print self._MStarfunc.extrap_string()
315
316 self._phiStarfunc = utils.Extrapolate1d(self.xobs, self.phiStar,
317 bounds_behavior=phiStar_bounds,
318 **extrap_args
319 )
320 print "phi*:",
321 print self._phiStarfunc.extrap_string()
322 self._alphafunc = utils.Extrapolate1d(self.xobs, self.alpha,
323 bounds_behavior=alpha_bounds,
324 **extrap_args
325 )
326 print "alpha:",
327 print self._alphafunc.extrap_string()
328
329 self._SED = BrokenPowerlawSED(**sedParams)
330 self._rQL = self._SED.iPhotonRateRatio(wavelength)
331
332 for name, func in globals().items():
333 if not name.startswith('schechter'):
334 continue
335 def newfunc(z, _name=name, _func=func, **kwargs):
336 params = self.params_z(z)
337 M = params['MStar']
338 phi = params['phiStar']
339 alpha = params['alpha']
340 return _func(MStar=M, phiStar=phi, alpha=alpha, **kwargs)
341 newfunc.__name__ = func.__name__
342 newfunc.__doc__ = func.__doc__
343 self.__dict__[name] = newfunc
344
345 - def iPhotonRateDensity_z(self, z, maglim=None):
346 """Ionizing photon rate density from a luminosity function.
347
348 See the iPhotonRateRatio function.
349 """
350 params = self.params_z(z)
351 if maglim is None:
352 lum = schechterTotLM(**params)
353 else:
354 lum = schechterCumuLM(maglim, **params)
355 return lum * self._rQL
356
357 - def iPhotonRateDensity_t(self, t, maglim=None):
358 """Ionizing photon rate density from a luminosity function.
359
360 See the iPhotonRateRatio function.
361 """
362 params = self.params_t(t)
363 if maglim is None:
364 lum = schechterTotLM(**params)
365 else:
366 lum = schechterCumuLM(maglim, **params)
367 return lum * self._rQL
368
369 - def ionization(self, z, maglim=None):
370 xH = cr.ionization_from_luminosity(z,
371 self._iPhotFunc,
372 rate_is_tfunc =
373 self.extrap_var == 't',
374 ratedensityfunc_args=[maglim],
375 **self.cosmo)
376 return xH
377
378 - def params_t(self, t):
379 """Return interp/extrapolated Schechter function parameters."""
380 if self.extrap_var == 't':
381 return {'MStar':self._MStarfunc(t),
382 'phiStar':self._phiStarfunc(t),
383 'alpha':self._alphafunc(t)}
384 elif self.extrap_var == 'z':
385
386 raise NotImplementedError, "params_t not implemented for z interps!"
387
388 - def params_z(self, z):
389 """Return interp/extrapolated Schechter function parameters."""
390 if self.extrap_var == 'z':
391 return {'MStar':self._MStarfunc(z),
392 'phiStar':self._phiStarfunc(z),
393 'alpha':self._alphafunc(z)}
394 elif self.extrap_var == 't':
395 z = numpy.atleast_1d(z)
396 t = cd.age(z, **self.cosmo)[0]
397 return self.params_t(t)
398
399 -def plotLFevo(hist=None,
400 params=B2008,
401 extrap_var='t',
402
403 maglim = -21. - 2.5 * numpy.log10(0.2),
404 z_max=20.0,
405 skipIon=True
406 ):
407
408 """Plot evolution of luminosity function params and total luminsity.
409
410 Schechter function at each redshift is integrated up to maglim to
411 find total luminsity.
412 """
413
414 for (k, v) in params.iteritems():
415 params[k] = numpy.asarray(v)
416
417 if hist is None:
418 cosmo = cp.WMAP7_BAO_H0_mean(flat=True)
419 hist = LFHistory(params, extrap_var=extrap_var, **cosmo)
420 else:
421 params = hist.params
422 extrap_var = hist.extrap_var
423 cosmo = hist.cosmo
424
425 z = params['z']
426 t = cd.age(z, **cosmo)[0] / cc.yr_s
427 MStar = params['MStar']
428 phiStar = params['phiStar']
429 alpha = params['alpha']
430
431 if maglim is None:
432 ltot = schechterTotLM(MStar=MStar, phiStar=phiStar, alpha=alpha)
433 else:
434 ltot = schechterCumuLM(magnitudeAB=maglim,
435 MStar=MStar, phiStar=phiStar, alpha=alpha)
436
437 print hist._MStarfunc.extrap_string()
438
439 zPlot = numpy.arange(z.min()-0.1, z_max, 0.1)
440 tPlot = cd.age(zPlot, **cosmo)[0] / cc.yr_s
441 newparams = hist.params_z(zPlot)
442 MPlot = newparams['MStar']
443 phiPlot = newparams['phiStar']
444 alphaPlot = newparams['alpha']
445
446 if maglim is None:
447 ltotPlot = hist.schechterTotLM(zPlot)
448 else:
449 ltotPlot = hist.schechterCumuLM(zPlot, magnitudeAB=maglim)
450
451 lB2008 =10.** numpy.array([26.18, 25.85, 25.72, 25.32, 25.14])
452
453 iPhot = hist.iPhotonRateDensity_z(zPlot, maglim=maglim)
454
455
456
457 if not skipIon:
458 xH = hist.ionization(zPlot, maglim)
459 import pylab
460 pylab.figure(1)
461 pylab.gcf().set_label('LFion_vs_z')
462 if skipIon:
463 pylab.subplot(111)
464 else:
465 pylab.subplot(211)
466 pylab.plot(zPlot, iPhot)
467 if not skipIon:
468 pylab.subplot(212)
469 pylab.plot(zPlot, xH)
470 pylab.ylim(0.0, 1.5)
471
472 pylab.figure(2)
473 pylab.gcf().set_label('LFparams_vs_z')
474 pylab.subplot(311)
475 pylab.plot(z, MStar, '-')
476 pylab.plot(z, MStar, 'o')
477 pylab.plot(zPlot, MPlot, ':')
478
479 pylab.subplot(312)
480 pylab.plot(z, phiStar, '-')
481 pylab.plot(z, phiStar, 'o')
482 pylab.plot(zPlot, phiPlot, ':')
483
484 pylab.subplot(313)
485 pylab.plot(z, alpha, '-')
486 pylab.plot(z, alpha, 'o')
487 pylab.plot(zPlot, alphaPlot, ':')
488
489 pylab.figure(3)
490 pylab.gcf().set_label('LFparams_vs_t')
491 pylab.subplot(311)
492 pylab.plot(t, MStar, '-')
493 pylab.plot(t, MStar, 'o')
494 pylab.plot(tPlot, MPlot, ':')
495
496 pylab.subplot(312)
497 pylab.plot(t, phiStar, '-')
498 pylab.plot(t, phiStar, '.')
499 pylab.plot(tPlot, phiPlot, ':')
500
501 pylab.subplot(313)
502 pylab.plot(t, alpha, '-')
503 pylab.plot(t, alpha, 'o')
504 pylab.plot(tPlot, alphaPlot, ':')
505
506 pylab.figure(4)
507 pylab.gcf().set_label('LFlum_vs_z')
508 pylab.subplot(121)
509 pylab.plot(z, ltot, 'o')
510 pylab.plot(z, lB2008, 'x')
511 pylab.plot(zPlot, ltotPlot)
512
513 pylab.subplot(122)
514 pylab.plot(t, ltot, 'o')
515 pylab.plot(t, lB2008, 'x')
516 pylab.plot(tPlot, ltotPlot)
517
519
520 phiStar = 1.8e-3
521 MStar = -20.04
522 alpha = -1.71
523
524 LStar = magnitudes.L_nu_from_magAB(MStar)
525
526 mags = numpy.arange(-22, -11.0, 0.5)
527 lums = magnitudes.L_nu_from_magAB(mags)
528
529 phi_L = schechterL(lums, phiStar, alpha, LStar)
530 phi_M = schechterM(mags, phiStar, alpha, MStar)
531
532 L_L = schechterCumuLL(lums, phiStar, alpha, LStar)
533 L_M = schechterCumuLM(mags, phiStar, alpha, MStar)
534
535 phi_L_func = lambda l: l * schechterL(l, phiStar, alpha, LStar)
536 L_L_num = utils.logquad(phi_L_func, lums, 1e35)[0]
537 L_L_num2 = utils.vecquad(phi_L_func, lums, 1e29)[0]
538
539 phi_M_func = lambda m: (magnitudes.L_nu_from_magAB(m) *
540 schechterM(m, phiStar, alpha, MStar))
541 L_M_num2 = utils.vecquad(phi_M_func, -25, mags)[0]
542
543 Ltot_L = schechterTotLL(phiStar, alpha, LStar)
544 Ltot_M = schechterTotLM(phiStar, alpha, MStar)
545
546 pylab.figure()
547 pylab.subplot(221)
548 pylab.plot(lums, lums * lums * phi_L)
549 pylab.xscale('log')
550 pylab.yscale('log')
551 pylab.ylabel(r'$ L^2 \Phi_L$')
552
553 pylab.subplot(222)
554 pylab.plot(mags, -mags * lums * phi_M)
555 pylab.yscale('log')
556 pylab.ylabel(r'$ -M L \Phi_M$')
557
558 pylab.subplot(223)
559 pylab.plot(lums, Ltot_L - L_L)
560 pylab.plot(lums, L_M)
561 pylab.plot(lums, L_L_num, '--')
562 pylab.plot(lums, L_L_num2, ':')
563 pylab.plot(lums, L_M_num2, 'x')
564 pylab.axhline(y=Ltot_L)
565 pylab.axhline(y=Ltot_M)
566 pylab.xscale('log')
567 pylab.yscale('log')
568
569 pylab.subplot(224)
570 pylab.plot(mags, Ltot_M - L_M)
571 pylab.plot(mags, L_L)
572 pylab.plot(mags, L_L_num, '--')
573 pylab.plot(mags, L_L_num2, ':')
574 pylab.plot(mags, L_M_num2, 'x')
575 pylab.axhline(y=Ltot_L)
576 pylab.axhline(y=Ltot_M)
577 pylab.yscale('log')
578
579
580
582 """Define an SED with a break at 912 Angstroms and different
583 slopes above and below the break.
584 """
585
586
587 planck_erg_s = 6.6260755e-27
588
590 """Convert between wavelength (Ang) and frequency (Hz) or vice-versa.
591 """
592 if numpy.all(wavelength == 0):
593 wavelength = numpy.array(wavelength)
594 return cc.c_light_cm_s / (wavelength * cc.angstrom_cm)
595
596
598 """Normalized luminosity in the defined band.
599
600 wavelength0 and wavelength1 in Angstroms.
601
602 This is the fraction of total luminosity in the band.
603 Multiply the result by the total luminosity (energy per
604 second) to get physical units.
605 """
606 return self.sed.integrate(self.lambdanu(wavelength0),
607 self.lambdanu(wavelength1))
608
610 """Normalized photon emission rate in the band between
611 wavelength0 and wavelength1.
612
613 Units are erg^-1 (which could also be expressed as s^-1 per
614 (erg/s)).
615
616 Multiply the result by the total luminosity (in ergs per unit
617 time), to get the actual photon emission rate.
618
619 Example
620 -------
621
622 To get the ionizing photon emission rate (per unit luminosity):
623 >>> BrokenPowerlawSED().photonRate_wavelength(0., 912.)
624 3272819078.0292048
625 """
626 return ((1./self.planck_erg_s) *
627 self.sed.integrate(self.lambdanu(wavelength0),
628 self.lambdanu(wavelength1),
629 weight_power=-1.))
630
631 - def __init__(self, s_ion=-3.0, s_red=0.0, break_factor=6.0):
632 """Return a model SED for a galaxy.
633
634 Parameters
635 ----------
636
637 s_ion:
638 spectral index (f_nu ~ nu^s_ion) at lambda < 912 Ang
639
640 s_red:
641 spectral index (f_nu ~ nu^s_red) at lambda > 912 Ang
642
643 Notes
644 -----
645
646 Bolton and Haehnelt (2007MNRAS.382..325B) use an SED with
647
648 eps_nu ~ v^0 for 912 < lambda < 3000 Ang.
649 ~ v^-3 for labmda < 912 Ang.
650
651 'with an additional break in the spectrum at the Lyman limit'
652
653 eps_L = eps(1500)/6
654
655 """
656
657 limits_Ang=numpy.array((3000., 912., 0.))
658 limits_nu = self.lambdanu(limits_Ang)
659
660 nu_912 = self.lambdanu(912.)
661
662
663 ratio_1500_912 = (912./1500.)**s_red
664
665
666 coeff_ratio = ratio_1500_912 * nu_912**(s_red - s_ion) / break_factor
667
668 coefficients = numpy.array((1., coeff_ratio))
669
670
671 powers = numpy.array((s_red, s_ion))
672 sed = utils.PiecewisePowerlaw(limits_nu, powers, coefficients)
673 self.sed = sed
674 self.break_factor = break_factor
675 self.s_ion = s_ion
676 self.s_red = s_red
677
679 """The spectrum at the given frequency/frequencies.
680
681 Multiply by the total luminosity to get the luminosity per
682 unit frequency.
683
684 Units are (fraction of total luminosity) per Hz.
685 """
686 return self.sed(nu)
687
689 """The ratio of ionizing photon emission rate to luminosity.
690
691 Paramters
692 ---------
693
694 lambda:
695 wavelength in Angstroms
696
697 Returns
698 -------
699
700 The ratio of ionizing photon emission rate to luminosity at
701 the given wavelength Q/L_nu(lambda) in units of photons s^-1
702 (erg s^-1 Hz^-1)^-1.
703
704 Notes
705 -----
706
707 While this function takes an argument in angstroms, the ratio
708 is calculated using the luminosity per unit frequence, so as
709 to be easily comensurate with luminosities inferred from AB
710 magnitudes.
711
712 """
713
714 return (self.photonRate_wavelength(0.,912.) /
715 self(self.lambdanu(wavelength)))
716
717 if __name__ == '__main__':
718
719 usage = """Run with a filename argument to produce image files, e.g.:
720 python luminosityfunction.py lum.png
721 """
722
723
724 parser = optparse.OptionParser(usage)
725 parser.add_option("-f", "--file", action='store_true', dest="filename",
726 default=None)
727 (options, args) = parser.parse_args()
728 if (len(args) > 0) and (options.filename is None):
729 options.filename = args[0]
730
731 if options.filename is None:
732 print "No filename given."
733 print usage
734 else:
735 prefix, extension = os.path.splitext(options.filename)
736
737 import pylab
738
739 plotLFevo()
740 plotLFevo(extrap_var='z')
741 plotLFevo(params=B2008_fixed)
742 plotLFevo(params=B2008_fixed, extrap_var='z')
743
744 test_plot_schechter()
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760 if options.filename is None:
761 pylab.show()
762 else:
763 from matplotlib import _pylab_helpers
764 for manager in _pylab_helpers.Gcf.get_all_fig_managers():
765 fig = manager.canvas.figure
766 if len(fig.get_label()) > 0:
767 label = fig.get_label()
768 else:
769 label = '_Fig' + str(manager.num)
770 newfilename = prefix + '_' + label + extension
771 fig.savefig(newfilename, dpi=75)
772