├── agnpy
├── tests
│ ├── __init__.py
│ ├── crosscheck_figures
│ │ ├── ec_blr_comparison_figure_10_finke_2016.png
│ │ ├── ec_disk_comparison_figure_8_finke_2016.png
│ │ ├── ec_dt_comparison_figure_11_finke_2016.png
│ │ ├── ssc_comparison_figure_7_4_dermer_menon_2009.png
│ │ └── synch_comparison_figure_7_4_dermer_menon_2009.png
│ ├── sampled_seds
│ │ ├── synch_figure_7_4_dermer_menon_2009.txt
│ │ ├── ec_blr_figure_10_finke_2016.txt
│ │ ├── ssc_figure_7_4_dermer_menon_2009.txt
│ │ ├── ec_disk_figure_8_finke_2016.txt
│ │ ├── ec_dt_figure_11_finke_2016.txt
│ │ └── ssa_sed_agnpy_v0_0_6.txt
│ ├── test_synchrotron.py
│ ├── test_spectra_emission_regions.py
│ ├── test_compton.py
│ └── test_targets.py
├── __init__.py
├── absorption.py
├── synchrotron.py
├── spectra.py
├── emission_regions.py
└── targets.py
├── docs
├── requirements.txt
├── _static
│ ├── logo.pdf
│ ├── logo.png
│ ├── ssc.png
│ ├── tau.png
│ ├── synch.png
│ ├── ec_disk.png
│ ├── n_e_gamma.png
│ ├── u_ph_blr.png
│ ├── compton_disk.pdf
│ ├── compton_reprocessed.pdf
│ └── disk_torus_black_bodies.png
├── modules.rst
├── tutorials
│ └── figures
│ │ ├── figure_8_finke_2016.png
│ │ ├── figure_10_finke_2016.png
│ │ ├── figure_11_finke_2016.png
│ │ └── figure_7_4_dermer_2009.png
├── spectra.rst
├── Makefile
├── make.bat
├── agnpy.rst
├── bibliography.rst
├── index.rst
├── emission_regions.rst
├── conf.py
├── synchrotron.rst
├── absorption.rst
├── compton.rst
└── targets.rst
├── experiments
├── crosschecks
│ ├── README.md
│ └── figures
│ │ ├── b0218_pars.png
│ │ ├── b0218_sed.png
│ │ ├── pks1510_pars.png
│ │ └── pks1510_sed.png
├── basic
│ ├── targets_radiation.py
│ ├── spectra.py
│ ├── delta_approx_synch.py
│ ├── beaming.py
│ ├── ec_point_like_comparison.py
│ ├── gmax.py
│ └── ec_gmax.py
├── profiling
│ ├── profile_synchrotron.py
│ ├── profile_tau.py
│ └── profile_external_compton.py
└── fit
│ └── data
│ └── sed_mrk421.ecsv
├── .gitignore
├── readthedocs.yaml
├── setup.py
├── .github
└── workflows
│ └── test.yml
└── README.md
/agnpy/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | sphinx-astropy
3 | nbsphinx
4 | ipython
5 |
--------------------------------------------------------------------------------
/docs/_static/logo.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/logo.pdf
--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/logo.png
--------------------------------------------------------------------------------
/docs/_static/ssc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/ssc.png
--------------------------------------------------------------------------------
/docs/_static/tau.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/tau.png
--------------------------------------------------------------------------------
/docs/_static/synch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/synch.png
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | agnpy
2 | =====
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | agnpy
8 |
--------------------------------------------------------------------------------
/docs/_static/ec_disk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/ec_disk.png
--------------------------------------------------------------------------------
/docs/_static/n_e_gamma.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/n_e_gamma.png
--------------------------------------------------------------------------------
/docs/_static/u_ph_blr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/u_ph_blr.png
--------------------------------------------------------------------------------
/docs/_static/compton_disk.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/compton_disk.pdf
--------------------------------------------------------------------------------
/docs/_static/compton_reprocessed.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/compton_reprocessed.pdf
--------------------------------------------------------------------------------
/experiments/crosschecks/README.md:
--------------------------------------------------------------------------------
1 | In this directory we use notebooks to reproduce some old results from the literature
2 |
--------------------------------------------------------------------------------
/docs/_static/disk_torus_black_bodies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/_static/disk_torus_black_bodies.png
--------------------------------------------------------------------------------
/docs/tutorials/figures/figure_8_finke_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/tutorials/figures/figure_8_finke_2016.png
--------------------------------------------------------------------------------
/experiments/crosschecks/figures/b0218_pars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/experiments/crosschecks/figures/b0218_pars.png
--------------------------------------------------------------------------------
/experiments/crosschecks/figures/b0218_sed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/experiments/crosschecks/figures/b0218_sed.png
--------------------------------------------------------------------------------
/docs/tutorials/figures/figure_10_finke_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/tutorials/figures/figure_10_finke_2016.png
--------------------------------------------------------------------------------
/docs/tutorials/figures/figure_11_finke_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/tutorials/figures/figure_11_finke_2016.png
--------------------------------------------------------------------------------
/experiments/crosschecks/figures/pks1510_pars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/experiments/crosschecks/figures/pks1510_pars.png
--------------------------------------------------------------------------------
/experiments/crosschecks/figures/pks1510_sed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/experiments/crosschecks/figures/pks1510_sed.png
--------------------------------------------------------------------------------
/docs/tutorials/figures/figure_7_4_dermer_2009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/docs/tutorials/figures/figure_7_4_dermer_2009.png
--------------------------------------------------------------------------------
/agnpy/__init__.py:
--------------------------------------------------------------------------------
1 | from .spectra import *
2 | from .emission_regions import *
3 | from .synchrotron import *
4 | from .compton import *
5 | from .targets import *
6 | from .absorption import *
7 |
--------------------------------------------------------------------------------
/agnpy/tests/crosscheck_figures/ec_blr_comparison_figure_10_finke_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/agnpy/tests/crosscheck_figures/ec_blr_comparison_figure_10_finke_2016.png
--------------------------------------------------------------------------------
/agnpy/tests/crosscheck_figures/ec_disk_comparison_figure_8_finke_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/agnpy/tests/crosscheck_figures/ec_disk_comparison_figure_8_finke_2016.png
--------------------------------------------------------------------------------
/agnpy/tests/crosscheck_figures/ec_dt_comparison_figure_11_finke_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/agnpy/tests/crosscheck_figures/ec_dt_comparison_figure_11_finke_2016.png
--------------------------------------------------------------------------------
/agnpy/tests/crosscheck_figures/ssc_comparison_figure_7_4_dermer_menon_2009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/agnpy/tests/crosscheck_figures/ssc_comparison_figure_7_4_dermer_menon_2009.png
--------------------------------------------------------------------------------
/agnpy/tests/crosscheck_figures/synch_comparison_figure_7_4_dermer_menon_2009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwcraig/agnpy/master/agnpy/tests/crosscheck_figures/synch_comparison_figure_7_4_dermer_menon_2009.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | build
3 | dist
4 | _build
5 | *.py[cod]
6 | *$py.class
7 | *.eggs*
8 | *.egg*
9 |
10 | # vim
11 | *.swp*
12 |
13 | # C extensions
14 | *.so
15 |
16 | # generated by cython
17 | *.o
18 | *.c
19 |
20 | # Jupyter Notebook
21 | .ipynb_checkpoints
22 |
23 | # pyenv
24 | .python-version
25 |
26 | # mypy
27 | .mypy_cache/
28 | .idea
29 | .code
30 |
31 | # submission
32 | *.err
33 | *.log
34 | *.out
35 |
36 | # generated from opening from remote
37 | .DS_Store
38 | ._pdf
39 |
40 |
--------------------------------------------------------------------------------
/docs/spectra.rst:
--------------------------------------------------------------------------------
1 | .. _spectra:
2 |
3 |
4 | Non-thermal Electrons Spectra
5 | =============================
6 |
7 | The electrons distributions can be described with simple or broken power laws, dependent on the electrons Lorentz factor.
8 | The user is not supposed to interact directly with this classes, the electrons spectra will be defined from the :ref:`emission_regions`.
9 |
10 | API
11 | ---
12 |
13 | .. automodule:: agnpy.spectra
14 | :noindex:
15 | :members: PowerLaw, BrokenPowerLaw, BrokenPowerLaw2
--------------------------------------------------------------------------------
/experiments/basic/targets_radiation.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import numpy as np
3 | import astropy.units as u
4 | import matplotlib.pyplot as plt
5 |
6 | sys.path.append("../../")
7 | from agnpy.targets import SphericalShellBLR, RingDustTorus
8 |
9 | blr = SphericalShellBLR(1e46 * u.Unit("erg s-1"), 0.1, "Lyalpha", 1e17 * u.cm)
10 | dt = RingDustTorus(1e46 * u.Unit("erg s-1"), 0.6, 1000 * u.K)
11 | print(blr)
12 | print(dt)
13 |
14 | r = np.logspace(14, 21, 200) * u.cm
15 |
16 | plt.loglog(r, blr.u(r), label="BLR")
17 | plt.loglog(r, dt.u(r), label="Torus")
18 | plt.xlabel(r"$r\,/\,{\rm cm}$")
19 | plt.ylabel(r"$u\,/\,({\rm erg}\,{\rm cm}^{-3})$")
20 | plt.legend()
21 | plt.show()
22 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Build documentation in the docs/ directory with Sphinx
9 | sphinx:
10 | configuration: docs/conf.py
11 |
12 | # Build documentation with MkDocs
13 | #mkdocs:
14 | # configuration: mkdocs.yml
15 |
16 | # Optionally build your docs in additional formats such as PDF and ePub
17 | #formats: all
18 |
19 | # Optionally set the version of Python and requirements required to build your docs
20 | python:
21 | version: 3.7
22 | install:
23 | - requirements: docs/requirements.txt
24 | - method: pip
25 | path: .
26 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="agnpy",
8 | version="0.0.6",
9 | author="Cosimo Nigro",
10 | author_email="cosimonigro2@gmail.com.com",
11 | description="Modelling jetted Active Galactic Nuclei radiative processes with python",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/cosimoNigro/agnpy",
15 | packages=setuptools.find_packages(),
16 | classifiers=[
17 | "Programming Language :: Python :: 3",
18 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
19 | "Operating System :: OS Independent",
20 | ],
21 | install_requires=["astropy", "numpy", "scipy", "matplotlib"],
22 | python_requires=">=3.6",
23 | )
24 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/agnpy/tests/sampled_seds/synch_figure_7_4_dermer_menon_2009.txt:
--------------------------------------------------------------------------------
1 | # the following points are taken with webplotdigitizer from
2 | # the synchrotron SED in Figure 7.4 of Dermer and Menon 2009
3 | # nu / Hz | nuFnu / erg cm-2 s-1
4 | 19281002208.06707, 1.0517919063817168e-12
5 | 31081114937.37773, 1.8893646675076006e-12
6 | 102544896679.54123, 7.768745191781145e-12
7 | 300254668040.4201, 2.1763026869027324e-11
8 | 1051545583501.7872, 4.368686466212362e-11
9 | 3078958391175.496, 5.2927844856926507e-11
10 | 10783063321401.867, 6.035340185482218e-11
11 | 29743837423956.23, 6.609603971655472e-11
12 | 104168241858427.1, 7.613405252175284e-11
13 | 305007873549266.3, 8.422453006484642e-11
14 | 1006302410404936.6, 9.507584094653489e-11
15 | 2946484963836226.5, 1.0517919063817168e-10
16 | 10319117670334754, 1.1753722651306415e-10
17 | 30214699618441012, 1.2614707735098417e-10
18 | 105817285532851420, 1.187302558090956e-10
19 | 309836325057640770, 7.927254606285227e-11
20 | 1085102261187576200, 1.1402958502838823e-11
--------------------------------------------------------------------------------
/agnpy/tests/sampled_seds/ec_blr_figure_10_finke_2016.txt:
--------------------------------------------------------------------------------
1 | # the following points are taken with webplotdigitizer from
2 | # the SED for EC on BLR in Figure 10 of Finke 2016, r = 10^(18) cm
3 | # nu / Hz | nuFnu / erg cm-2 s-1
4 | 1081456789522189000, 1.3111339374215845e-22
5 | 3063428719358744000, 9.378561332911641e-22
6 | 9962016606138997000, 4.565020419171616e-21
7 | 30189288866580580000, 7.960288219017259e-21
8 | 108791015476632500000, 1.5120514307731143e-20
9 | 307609813376138940000, 2.526252638912735e-20
10 | 1.0707861367317256e+21, 4.7986000155581883e-20
11 | 3.027679466385275e+21, 8.017237843683877e-20
12 | 1.0538751668568505e+22, 1.4591054758125214e-19
13 | 3.085014422610478e+22, 2.544325997017482e-19
14 | 1.0372868692990064e+23, 4.630572357816586e-19
15 | 3.0362926618641315e+23, 7.736506444512481e-19
16 | 1.0938742208520713e+24, 1.1865974359833954e-18
17 | 2.984978607611047e+24, 9.581293602915839e-19
18 | 1.002307440729752e+25, 5.985353196598315e-19
19 | 3.0330395245754055e+25, 3.288727302518049e-19
20 | 1.0538751668568591e+26, 1.4591054758125214e-19
21 | 3.0792324042422173e+26, 5.69400647477118e-20
22 | 9.976978712957852e+26, 1.5120514307731143e-20
23 | 2.718594130223553e+27, 3.8471517641434835e-21
--------------------------------------------------------------------------------
/docs/agnpy.rst:
--------------------------------------------------------------------------------
1 | agnpy package
2 | =============
3 |
4 | Submodules
5 | ----------
6 |
7 | agnpy.compton module
8 | --------------------
9 |
10 | .. automodule:: agnpy.compton
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | agnpy.emission\_regions module
16 | ------------------------------
17 |
18 | .. automodule:: agnpy.emission_regions
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | agnpy.spectra module
24 | --------------------
25 |
26 | .. automodule:: agnpy.spectra
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | agnpy.synchrotron module
32 | ------------------------
33 |
34 | .. automodule:: agnpy.synchrotron
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | agnpy.targets module
40 | --------------------
41 |
42 | .. automodule:: agnpy.targets
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | agnpy.absorption module
48 | -----------------------
49 |
50 | .. automodule:: agnpy.absorption
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 |
56 | Module contents
57 | ---------------
58 |
59 | .. automodule:: agnpy
60 | :members:
61 | :undoc-members:
62 | :show-inheritance:
63 |
--------------------------------------------------------------------------------
/agnpy/tests/sampled_seds/ssc_figure_7_4_dermer_menon_2009.txt:
--------------------------------------------------------------------------------
1 | # the following points are taken with webplotdigitizer from
2 | # the SSC SED in Figure 7.4 of Dermer and Menon 2009
3 | # nu / Hz | nuFnu / erg cm-2 s-1
4 | 2614944543238824, 1.0517919063817168e-12
5 | 10319117670334754, 3.605932093335428e-12
6 | 30214699618441012, 7.927254606285227e-12
7 | 105817285532851420, 1.5129499502366456e-11
8 | 309836325057640770, 2.266016969803532e-11
9 | 1085102261187576200, 3.3260580486968143e-11
10 | 2993129529441971700, 4.368686466212362e-11
11 | 10482475286977960000, 5.974695679970744e-11
12 | 30693015834511335000, 7.613405252175284e-11
13 | 101264454118678560000, 9.701571868867811e-11
14 | 296505492133032170000, 1.1753722651306415e-10
15 | 1.0384152984910122e+21, 1.482697760456359e-10
16 | 3.040512505569831e+21, 1.7604108438655598e-10
17 | 1.0648418949421582e+22, 1.90854214400669e-10
18 | 2.9372436441613516e+22, 1.9474830399087612e-10
19 | 1.091941020968905e+23, 1.889364667507593e-10
20 | 3.0119934601316744e+23, 1.7604108438655598e-10
21 | 9.937370612056698e+23, 1.5438193898779022e-10
22 | 3.0886455816865645e+24, 1.1635618505359118e-10
23 | 1.0190266426796636e+25, 7.311981563944311e-11
24 | 2.9837419143177227e+25, 2.8013567611988813e-11
25 | 5.1056273492242735e+25, 1.0841458689358372e-11
--------------------------------------------------------------------------------
/agnpy/tests/sampled_seds/ec_disk_figure_8_finke_2016.txt:
--------------------------------------------------------------------------------
1 | # the following points are taken with webplotdigitizer from
2 | # the SED for EC on SS Disk in Figure 8 of Finke 2016, r = 10^(17) cm
3 | # nu / Hz | nuFnu / erg cm-2 s-1
4 | 1112415642769511200, 2.4628305335157195e-18
5 | 3060584795427569700, 1.423658914014292e-17
6 | 10146333855503233000, 6.845442832835306e-17
7 | 30237563461308654000, 1.3082675210964568e-16
8 | 100242415865672140000, 2.3880587480091747e-16
9 | 306800413855403540000, 4.358431900581429e-16
10 | 1.0727393623511784e+21, 7.956477467394626e-16
11 | 3.0310864112531795e+21, 1.3239795384038383e-15
12 | 1.0319765321261307e+22, 2.5309385429697406e-15
13 | 2.994612920181297e+22, 4.2117549854675844e-15
14 | 1.019558612699237e+23, 7.688340486408773e-15
15 | 3.0384342456162894e+23, 1.3398802537289314e-14
16 | 1.0344782069700262e+24, 2.1298207141550297e-14
17 | 3.0018723370152843e+24, 1.7744439919575108e-14
18 | 1.0220301845362466e+25, 1.070760165701928e-14
19 | 3.0457998922956793e+25, 6.168591134922943e-15
20 | 1.0937206560666405e+26, 2.4579931696008673e-15
21 | 3.009149351827457e+26, 9.7895950862789e-16
22 | 1.0521605614326959e+27, 2.348547613991268e-16
23 | 3.0531833944712536e+27, 5.13599906743024e-17
24 | 1.039499764729201e+28, 6.4596216389320304e-18
25 | 3.097861804238422e+28, 8.122391350533852e-19
--------------------------------------------------------------------------------
/agnpy/tests/sampled_seds/ec_dt_figure_11_finke_2016.txt:
--------------------------------------------------------------------------------
1 | # the following points are taken with webplotdigitizer from
2 | # the SED for EC on DT in Figure 11 of Finke 2016, r = 10^(20) cm
3 | # nu / Hz | nuFnu / erg cm-2 s-1
4 | 104779796012291650, 4.741145205620621e-25
5 | 307149293742396540, 3.585349441435531e-24
6 | 1052053544070012400, 1.1317837154918573e-23
7 | 2979076290764362000, 1.9651457989240414e-23
8 | 10192142922110018000, 3.572690471530978e-23
9 | 30737978819376120000, 6.2033563258452e-23
10 | 101900467539281130000, 1.1277876709704326e-22
11 | 307316570896211900000, 1.9582073618391975e-22
12 | 1.0187951016854783e+21, 3.56007619727511e-22
13 | 2.977238592619107e+21, 6.1814538300582155e-22
14 | 1.0511883282068531e+22, 1.1238057354802552e-21
15 | 3.0719017530321392e+22, 1.9512934226359744e-21
16 | 1.0177848219524333e+23, 2.692201282980408e-21
17 | 3.0641435529080844e+23, 2.0431105138359064e-21
18 | 1.0139377121513272e+24, 1.5505157798326222e-21
19 | 3.0522659880823847e+24, 1.1238057354802552e-21
20 | 1.0099096196157332e+25, 8.145285249755026e-22
21 | 3.0398459325306247e+25, 5.638350552739192e-22
22 | 1.0709066477058109e+26, 3.56007619727511e-22
23 | 3.0260115695368094e+26, 2.247845786077903e-22
24 | 1.0993028166984927e+27, 9.824704762060703e-23
25 | 3.007869859653206e+27, 4.4961602512456314e-23
26 | 1.024989482210628e+28, 1.2408006060435296e-23
27 | 3.0783757152870423e+28, 2.9830077238879184e-24
28 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI test
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version: [3.6, 3.7, 3.8]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | pip install flake8 pytest pytest-cov
27 | python setup.py install
28 | - name: Lint with flake8
29 | run: |
30 | # stop the build if there are Python syntax errors or undefined names
31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
34 | - name: Test with pytest
35 | run: |
36 | pytest -v --cov=./ --cov-report=xml
37 | - name: Upload coverage to Codecov
38 | uses: codecov/codecov-action@v1
39 | with:
40 | file: ./coverage.xml
41 | flags: unittests
42 | name: codecov-umbrella
43 | fail_ci_if_error: true
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # agnpy
2 |
3 |
4 |
5 |
6 | Modelling Active Galactic Nuclei radiative processes with python.
7 |
8 | ## descritpion
9 | `agnpy` focuses on the numerical computation of the photon spectra produced by leptonic radiative processes in jetted Active Galactic Nuclei (AGN).
10 |
11 | ## documentation and quickstart
12 | You are invited to check the documentation at https://agnpy.readthedocs.io/en/latest/.
13 | To get familiar with the code you can run the notebooks in the `tutorials` section
14 | of the documentation.
15 |
16 | ## dependencies
17 | The only dependencies are:
18 |
19 | * [numpy](https://numpy.org) managing the numerical computation;
20 |
21 | * [astropy](https://www.astropy.org) managing physical units and astronomical distances.
22 |
23 | * [matplotlib](https://matplotlib.org) for visualisation and reproduction of the tutorials.
24 |
25 | ## installation
26 | The code is available in the [python package index](https://pypi.org/project/agnpy/) and can be installed via `pip`
27 |
28 | ```bash
29 | pip install agnpy
30 | ```
31 |
32 | ## tests
33 | A test suite is available in [`agnpy/tests`](https://github.com/cosimoNigro/agnpy/tree/master/agnpy/tests), to run it just type
34 | `pytest` in the main directory.
35 |
36 | ## shields
37 | 
38 |
39 | 
40 |
41 | 
42 |
--------------------------------------------------------------------------------
/experiments/basic/spectra.py:
--------------------------------------------------------------------------------
1 | # test different normalisations for the spectra
2 | import astropy.units as u
3 | from astropy.coordinates import Distance
4 | from agnpy.emission_regions import Blob
5 | import matplotlib.pyplot as plt
6 |
7 | spectrum_norm = 1e-13 * u.Unit("cm-3")
8 |
9 | spectrum_dict_pwl = {
10 | "type": "PowerLaw",
11 | "parameters": {"p": 2.8, "gamma_min": 1, "gamma_max": 1e7},
12 | }
13 |
14 | spectrum_dict_bpl = {
15 | "type": "BrokenPowerLaw",
16 | "parameters": {
17 | "p1": 2.0,
18 | "p2": 3.0,
19 | "gamma_b": 1e2,
20 | "gamma_min": 1,
21 | "gamma_max": 1e7,
22 | },
23 | }
24 |
25 | spectrum_dict_bpl_2 = {
26 | "type": "BrokenPowerLaw2",
27 | "parameters": {
28 | "p1": 2.0,
29 | "p2": 3.0,
30 | "gamma_b": 1e2,
31 | "gamma_min": 1,
32 | "gamma_max": 1e7,
33 | },
34 | }
35 |
36 | R_b = 1e16 * u.cm
37 | B = 1 * u.G
38 | z = Distance(1e27, unit=u.cm).z
39 | delta_D = 10
40 | Gamma = 10
41 |
42 | for spectrum_dict in (spectrum_dict_pwl, spectrum_dict_bpl, spectrum_dict_bpl_2):
43 | for norm_type in ("integral", "differential", "gamma=1"):
44 | blob = Blob(
45 | R_b,
46 | z,
47 | delta_D,
48 | Gamma,
49 | B,
50 | spectrum_norm,
51 | spectrum_dict,
52 | spectrum_norm_type=norm_type,
53 | )
54 | blob.plot_n_e()
55 |
56 | # let us trigger the error
57 | blob = Blob(
58 | R_b,
59 | z,
60 | delta_D,
61 | Gamma,
62 | B,
63 | 1e48 * u.Unit("erg"),
64 | spectrum_dict_bpl_2,
65 | spectrum_norm_type="gamma=1",
66 | )
67 |
--------------------------------------------------------------------------------
/experiments/basic/delta_approx_synch.py:
--------------------------------------------------------------------------------
1 | # test delta approximation for the synchrotron SED
2 | import sys
3 |
4 | sys.path.append("../../")
5 | import astropy.units as u
6 | import numpy as np
7 | from astropy.coordinates import Distance
8 | from agnpy.emission_regions import Blob
9 | from agnpy.synchrotron import Synchrotron, nu_synch_peak, synch_sed_param_bpl
10 | import matplotlib.pyplot as plt
11 |
12 | # set the spectrum normalisation (total energy in electrons in this case)
13 | spectrum_norm = 1e48 * u.Unit("erg")
14 | # define the spectral function through a dictionary
15 | spectrum_dict = {
16 | "type": "BrokenPowerLaw",
17 | "parameters": {
18 | "p1": 2.5,
19 | "p2": 3.5,
20 | "gamma_b": 1e4,
21 | "gamma_min": 1e2,
22 | "gamma_max": 1e7,
23 | },
24 | }
25 | R_b = 1e16 * u.cm
26 | B = 1 * u.G
27 | z = Distance(1e27, unit=u.cm).z
28 | delta_D = 10
29 | Gamma = 10
30 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
31 |
32 | nu = np.logspace(8, 23) * u.Hz
33 |
34 | synch = Synchrotron(blob)
35 |
36 | sed = synch.sed_flux(nu)
37 | sed_delta_approx = synch.sed_flux_delta_approx(nu)
38 | # check that the synchrotron parameterisation work
39 | y = blob.B.value * blob.delta_D
40 | k_eq = (blob.u_e / blob.U_B).to_value("")
41 | sed_param = synch_sed_param_bpl(
42 | nu.value,
43 | y,
44 | k_eq,
45 | blob.n_e.p1,
46 | blob.n_e.p2,
47 | blob.n_e.gamma_b,
48 | blob.n_e.gamma_min,
49 | blob.n_e.gamma_max,
50 | blob.d_L.cgs.value,
51 | blob.R_b.cgs.value,
52 | blob.z,
53 | )
54 |
55 | plt.loglog(nu, sed, ls="-", label="numerical integration")
56 | plt.loglog(nu, sed_delta_approx, ls="--", label=r"$\delta$" + "-function approx.")
57 | plt.loglog(nu, sed_param, ls=":", label="parametrisation in " + r"$(y,\,k_{\rm eq})$")
58 |
59 | plt.xlabel(r"$\nu\,/\,{\rm Hz}$")
60 | plt.ylabel(r"$\nu F_{\nu}\,/\,({\rm erg}\,{\rm cm}^{-2}\,{\rm s}^{-1})$")
61 | plt.legend()
62 | plt.show()
63 |
--------------------------------------------------------------------------------
/docs/bibliography.rst:
--------------------------------------------------------------------------------
1 | .. _bibliography:
2 |
3 | Bibliography
4 | ============
5 |
6 | .. [DermerMenon2009] `Dermer and Menon (2009) `_,
7 | "High Energy Radiation from Black Holes: Gamma Rays, Cosmic Rays, and Neutrinos";
8 |
9 | .. [Finke2008] `Finke et al. (2008) `_,
10 | "Synchrotron Self-Compton Analysis of TeV X-Ray-Selected BL Lacertae Objects";
11 |
12 | .. [Aharonian2010] `Aharonian et al. (2010) `_,
13 | "Angular, spectral, and time distributions of highest energy protons and associated secondary gamma rays and neutrinos propagating through extragalactic magnetic and radiation fields";
14 |
15 | .. [Dermer2009] `Dermer et al. (2009) `_,
16 | "Gamma-Ray Studies of Blazars: Synchro-Compton Analysis of Flat Spectrum Radio Quasars";
17 |
18 | .. [Finke2016] `Finke (2016) `_,
19 | "External Compton Scattering in Blazar Jets and the Location of the Gamma-Ray Emitting Region";
20 |
21 | .. [Tavecchio1998] `Tavecchio et al. (1998) `_,
22 | "Constraints on the Physical Parameters of TeV Blazars";
23 |
24 | .. [Shakura1973] `Shakuraa and Sunyaev (1973) `_,
25 | "Black holes in binary systems. Observational appearance.";
26 |
27 | .. [Dermer2002] `Dermer and Schlickeiser (2002) `_,
28 | "Transformation Properties of External Radiation Fields, Energy-Loss Rates and Scattered Spectra, and a Model for Blazar Variability";
29 |
30 | .. [Dermer1994] `Dermer and Schlickeiser (1994) `_,
31 | "On the Location of the Acceleration and Emission Sites in Gamma-Ray Blazars";
32 |
33 |
--------------------------------------------------------------------------------
/experiments/basic/beaming.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import math
3 | import astropy.units as u
4 | import astropy.constants as const
5 | from astropy.coordinates import Distance
6 | import matplotlib.pyplot as plt
7 | from scipy.interpolate import interp1d
8 | import sys
9 |
10 | sys.path.append("/home/jsitarek/zdalne/agnpy/agnpy/")
11 | from agnpy.emission_regions import Blob
12 | from agnpy.synchrotron import Synchrotron
13 |
14 | nu = np.logspace(8, 27, 100) * u.Hz # for SED calculations
15 |
16 | spectrum_norm = 1.0 * u.Unit("erg cm-3")
17 | parameters = {
18 | "p1": 1.5,
19 | "p2": 2.5,
20 | "gamma_b": 1.0e3,
21 | "gamma_min": 1,
22 | "gamma_max": 1.0e6,
23 | }
24 | spectrum_dict = {"type": "BrokenPowerLaw", "parameters": parameters}
25 | delta_D = 1.01
26 | Gamma = 1.01
27 | B = 1.0 * u.G
28 | r_b = 1.0e15 * u.cm
29 | # no beaming
30 | blob0 = Blob(r_b, 0.01, delta_D, Gamma, B, spectrum_norm, spectrum_dict, xi=0.01)
31 |
32 | synch0 = Synchrotron(blob0, ssa=True)
33 | synch0_sed = synch0.sed_flux(nu)
34 |
35 | # beaming
36 | delta_D = 20
37 | Gamma = 15
38 | blob1 = Blob(r_b, 0.01, delta_D, Gamma, B, spectrum_norm, spectrum_dict, xi=0.01)
39 |
40 | synch1 = Synchrotron(blob1, ssa=True)
41 | synch1_sed = synch1.sed_flux(nu)
42 |
43 | # doing beaming by hand: dN/dOmega dt depsilon scales like D^2, and E^2 in SED scales with another D^2
44 | synch0_sed_scale = synch0_sed * delta_D ** 4
45 | nu_scale = nu * delta_D
46 | plt.rc("figure", figsize=(7.5, 5.5))
47 | plt.rc("font", size=12)
48 | plt.rc("axes", grid=True)
49 | plt.rc("grid", ls=":")
50 | sed_x_label = r"$\nu\,/\,Hz$"
51 | sed_y_label = r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$"
52 |
53 | plt.loglog(nu, synch0_sed, color="k", ls=":", lw=1, label="No beaming") #
54 | plt.loglog(nu, synch1_sed, color="r", ls=":", lw=1, label="Beaming") #
55 | plt.loglog(
56 | nu_scale, synch0_sed_scale * 1.1, color="b", ls=":", lw=1, label="scaled"
57 | ) # 1.1 so both curves show up
58 |
59 | plt.ylim(1e-15, 1e-7)
60 | plt.xlim(1e8, 1e27)
61 | plt.xscale("log")
62 | plt.yscale("log")
63 | plt.xlabel(sed_x_label)
64 | plt.ylabel(sed_y_label)
65 | plt.legend()
66 | plt.show()
67 |
--------------------------------------------------------------------------------
/agnpy/tests/sampled_seds/ssa_sed_agnpy_v0_0_6.txt:
--------------------------------------------------------------------------------
1 | # the following points are produced by agnpy version 0.0.6
2 | # nu / Hz | nuFnu / erg cm-2 s-1
3 | 100000000.0, 2.049370330910917e-22
4 | 193069772.88832536, 1.4751291638308774e-21
5 | 372759372.0314938, 1.0618878950529181e-20
6 | 719685673.0011529, 7.645211898182171e-20
7 | 1389495494.373136, 5.505538959748514e-19
8 | 2682695795.2797275, 3.966152233934647e-18
9 | 5179474679.231202, 2.8588716007348915e-17
10 | 10000000000.0, 2.0626889026188195e-16
11 | 19306977288.832455, 1.4905727435227422e-15
12 | 37275937203.14938, 1.0799905566111886e-14
13 | 71968567300.11528, 7.86038750801606e-14
14 | 138949549437.3136, 5.756231330689738e-13
15 | 268269579527.97272, 4.111713352140478e-12
16 | 517947467923.1202, 1.945435338199389e-11
17 | 1000000000000.0, 3.9405967422349685e-11
18 | 1930697728883.2456, 4.9235154618603477e-11
19 | 3727593720314.938, 5.352606270476045e-11
20 | 7196856730011.528, 5.723942385437154e-11
21 | 13894954943731.36, 6.113158524506098e-11
22 | 26826957952797.164, 6.528821116999929e-11
23 | 51794746792312.02, 6.972700097016005e-11
24 | 100000000000000.0, 7.446645763710228e-11
25 | 193069772888324.56, 7.952538606441291e-11
26 | 372759372031493.8, 8.492158333215765e-11
27 | 719685673001152.9, 9.066860709184194e-11
28 | 1389495494373136.0, 9.676801532028709e-11
29 | 2682695795279716.5, 1.0319108627693772e-10
30 | 5179474679231202.0, 1.0983664309941879e-10
31 | 1e+16, 1.1643660589747942e-10
32 | 1.9306977288832456e+16, 1.2235471063828613e-10
33 | 3.7275937203149224e+16, 1.2619534943889563e-10
34 | 7.1968567300114696e+16, 1.251833493990155e-10
35 | 1.389495494373136e+17, 1.1468502764178843e-10
36 | 2.6826957952797382e+17, 8.963671685967324e-11
37 | 5.179474679231223e+17, 5.132862152753252e-11
38 | 1e+18, 1.6114919835125643e-11
39 | 1.9306977288832456e+18, 1.5977050743998757e-12
40 | 3.7275937203149225e+18, 1.746693927711423e-14
41 | 7.19685673001147e+18, 2.8261237409980647e-18
42 | 1.389495494373136e+19, 1.4331439630936873e-25
43 | 2.6826957952797164e+19, 1.1819474794991626e-39
44 | 5.179474679231223e+19, 5.3863264469691696e-67
45 | 1e+20, 4.8785747579257544e-120
46 | 1.9306977288832457e+20, 1.0264441392960389e-222
47 | 3.727593720314923e+20, 0.0
48 | 7.196856730011469e+20, 0.0
49 | 1.3894954943731361e+21, 0.0
50 | 2.682695795279716e+21, 0.0
51 | 5.179474679231223e+21, 0.0
52 | 1e+22, 0.0
53 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. image:: _static/logo.pdf
2 | :width: 400px
3 | :align: center
4 |
5 | agnpy docs
6 | =================================
7 | `agnpy` focuses on the numerical computation of the photon spectra produced by leptonic radiative processes in jetted Active Galactic Nuclei (AGN).
8 |
9 | Description
10 | -----------
11 | References
12 | ..........
13 | Notation and basic formulas are borrowed from [DermerMenon2009]_ which constitutes the fundamental reference for this code. The implementation of synchrotron and synchrotron self-Compton radiative processes relies on [DermerMenon2009]_ and [Finke2008]_; [Dermer2009]_ and [Finke2016]_ are instead the main references for the external Compton radiative processes.
14 |
15 | Implementation
16 | ..............
17 | The numerical operations are delegated to `numpy arrays `_, all the physical quantities computed are casted as `astropy Quantities `_.
18 |
19 | License
20 | -------
21 | The code is licensed under `GNU General Public License v3.0 `_ (see `LICENSE.md` in the main directory).
22 |
23 | Installation
24 | ------------
25 | The code is available in the `python package index `_ and can be installed via `pip
26 |
27 | .. code-block:: bash
28 |
29 | pip install agnpy
30 |
31 | Dependencies
32 | ------------
33 | The only dependencies are:
34 |
35 | * `numpy `_ managing the numerical computation;
36 |
37 | * `astropy `_ managing physical units and astronomical distances.
38 |
39 | * `matplotlib `_ for visualisation and reproduction of the tutorials.
40 |
41 |
42 | Overview
43 | --------
44 |
45 | .. toctree::
46 | :maxdepth: 2
47 |
48 | spectra
49 | emission_regions
50 | synchrotron
51 | targets
52 | tutorials/energy_densities.ipynb
53 | compton
54 | tutorials/synchrotron_self_compton.ipynb
55 | tutorials/external_compton.ipynb
56 | absorption
57 | calc_u_sed
58 | bibliography
59 | modules
60 |
61 |
62 | After checking the documentation, to get more familiar with the package it is suggested to run the notebooks in `agnpy/tutorials`.
63 |
64 | Indices and tables
65 | ------------------
66 | * :ref:`genindex`
67 | * :ref:`modindex`
68 | * :ref:`search`
69 |
--------------------------------------------------------------------------------
/experiments/basic/ec_point_like_comparison.py:
--------------------------------------------------------------------------------
1 | # compare if in the limit of large distances the SED for EC on the BLR and on
2 | # the dust torus tend to the one generated by a point like source behind the jet
3 |
4 | # import numpy, astropy and matplotlib for basic functionalities
5 | import numpy as np
6 | import astropy.units as u
7 | import astropy.constants as const
8 | from astropy.coordinates import Distance
9 | import matplotlib.pyplot as plt
10 |
11 | # import agnpy classes
12 | from agnpy.emission_regions import Blob
13 | from agnpy.compton import ExternalCompton
14 | from agnpy.targets import PointSourceBehindJet, SphericalShellBLR, RingDustTorus
15 |
16 | spectrum_norm = 6e42 * u.erg
17 | parameters = {
18 | "p1": 2.0,
19 | "p2": 3.5,
20 | "gamma_b": 1e4,
21 | "gamma_min": 20,
22 | "gamma_max": 5e7,
23 | }
24 | spectrum_dict = {"type": "BrokenPowerLaw", "parameters": parameters}
25 | R_b = 1e16 * u.cm
26 | B = 0.56 * u.G
27 | z = 1
28 | delta_D = 40
29 | Gamma = 40
30 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
31 | blob.set_gamma_size(500)
32 |
33 |
34 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
35 | # dust torus
36 | T_dt = 1e3 * u.K
37 | csi_dt = 0.1
38 | dt = RingDustTorus(L_disk, csi_dt, T_dt)
39 | # blr
40 | xi_line = 0.024
41 | R_line = 1e17 * u.cm
42 | blr = SphericalShellBLR(L_disk, xi_line, "Lyalpha", R_line)
43 |
44 | # point source behind the jet approximating the DT
45 | ps_dt = PointSourceBehindJet(dt.xi_dt * L_disk, dt.epsilon_dt)
46 | # point source behind the jet approximating the BLR
47 | ps_blr = PointSourceBehindJet(blr.xi_line * L_disk, blr.epsilon_line)
48 |
49 | ec_dt = ExternalCompton(blob, dt, r=1e22 * u.cm)
50 | ec_blr = ExternalCompton(blob, blr, r=1e22 * u.cm)
51 | ec_ps_dt = ExternalCompton(blob, ps_dt, r=1e22 * u.cm)
52 | ec_ps_blr = ExternalCompton(blob, ps_blr, r=1e22 * u.cm)
53 |
54 | # seds
55 | nu = np.logspace(15, 30) * u.Hz
56 |
57 | sed_blr = ec_blr.sed_flux(nu)
58 | sed_ps_blr = ec_ps_blr.sed_flux(nu)
59 | sed_dt = ec_dt.sed_flux(nu)
60 | sed_ps_dt = ec_ps_dt.sed_flux(nu)
61 |
62 | plt.loglog(nu, sed_blr, ls="-", lw=2, color="k", label="EC on BLR")
63 | plt.loglog(nu, sed_dt, ls="-", lw=2, color="dimgray", label="EC on DT")
64 | plt.loglog(
65 | nu,
66 | sed_ps_blr,
67 | ls=":",
68 | lw=2,
69 | color="crimson",
70 | label="EC on point source approx. BLR",
71 | )
72 | plt.loglog(
73 | nu,
74 | sed_ps_dt,
75 | ls=":",
76 | lw=2,
77 | color="darkorange",
78 | label="EC on point source approx. DT",
79 | )
80 | plt.legend()
81 | plt.xlabel(r"$\nu\,/\,{\rm Hz}$")
82 | plt.ylabel(r"$\nu F_{\nu}\,/\,({\rm erg}\,{\rm cm}^{-2}\,{\rm s}^{-1})$")
83 | plt.show()
84 |
--------------------------------------------------------------------------------
/experiments/profiling/profile_synchrotron.py:
--------------------------------------------------------------------------------
1 | """profile and test synchrotron and synchrotron self Comton radiation"""
2 | import sys
3 |
4 | sys.path.append("../")
5 | import numpy as np
6 | import astropy.units as u
7 | import astropy.constants as const
8 | from astropy.coordinates import Distance
9 | import matplotlib.pyplot as plt
10 | from agnpy.emission_regions import Blob
11 | from agnpy.synchrotron import Synchrotron
12 | from agnpy.compton import SynchrotronSelfCompton
13 |
14 | # to profile
15 | import cProfile, pstats
16 | import timeit
17 |
18 | # functions to profile and time
19 | def profile(command, label):
20 | """function to profile a given command"""
21 | print(f"->{command} profiling section...")
22 | cProfile.run(command, f"Profile_{label}.prof")
23 | prof = pstats.Stats(f"Profile_{label}.prof")
24 | prof.strip_dirs().sort_stats("time").print_stats(10)
25 |
26 |
27 | def timing(command, number):
28 | """function to time a given command, returns time in seconds"""
29 | return timeit.timeit(command, globals=globals(), number=number)
30 |
31 |
32 | # define the blob
33 | spectrum_norm = 1e48 * u.Unit("erg")
34 | spectrum_dict = {
35 | "type": "PowerLaw",
36 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e7},
37 | }
38 | R_b = 1e16 * u.cm
39 | B = 1 * u.G
40 | z = Distance(1e27, unit=u.cm).z
41 | delta_D = 10
42 | Gamma = 10
43 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
44 | print("blob definition:")
45 | print(blob)
46 | synch = Synchrotron(blob)
47 | synch_ssa = Synchrotron(blob, ssa=True)
48 | ssc = SynchrotronSelfCompton(blob, synch)
49 | ssc_ssa = SynchrotronSelfCompton(blob, synch_ssa)
50 | nu_syn = np.logspace(8, 23) * u.Hz
51 | nu_ssc = np.logspace(15, 30) * u.Hz
52 |
53 | # commands to profile
54 | syn_sed_command = "synch.sed_flux(nu_syn)"
55 | syn_sed_ssa_command = "synch_ssa.sed_flux(nu_syn)"
56 | ssc_sed_command = "ssc.sed_flux(nu_ssc)"
57 | ssc_ssa_sed_command = "ssc_ssa.sed_flux(nu_ssc)"
58 |
59 | n = 100
60 | print("\nprofiling synchrotron sed computation:")
61 | profile(syn_sed_command, "syn_sed")
62 | time_syn = timing(syn_sed_command, n)
63 | time_syn /= n
64 | print(f"time: {time_syn:.2e} s")
65 |
66 | print("\nprofiling synchrotron w/ SSA sed computation:")
67 | profile(syn_sed_ssa_command, "syn_sed_ssa")
68 | time_syn_ssa = timing(syn_sed_ssa_command, n)
69 | time_syn_ssa /= n
70 | print(f"time: {time_syn_ssa:.2e} s")
71 |
72 | print("\nprofiling SSC sed computation:")
73 | profile(ssc_sed_command, "ssc_sed")
74 | time_ssc = timing(ssc_sed_command, n)
75 | time_ssc /= n
76 | print(f"time: {time_ssc:.2e} s")
77 |
78 | print("\nprofiling SSC w/ SSA sed computation:")
79 | profile(ssc_ssa_sed_command, "ssc_ssa_sed")
80 | time_ssc_ssa = timing(ssc_ssa_sed_command, n)
81 | time_ssc_ssa /= n
82 | print(f"time: {time_ssc_ssa:.2e} s")
83 |
--------------------------------------------------------------------------------
/docs/emission_regions.rst:
--------------------------------------------------------------------------------
1 | .. _emission_regions:
2 |
3 |
4 | Emission Regions
5 | ================
6 |
7 | At the moment the only emission region available in the code is a simple spherical plasmoid, commonly referred to as **blob** in the literature.
8 | For more details on the electrons spectra you can read the :ref:`spectra` API.
9 |
10 | Follows an example of how to initialise a `Blob` using `astropy` quantities:
11 |
12 | .. code-block:: python
13 |
14 | import astropy.units as u
15 | from astropy.coordinates import Distance
16 | from agnpy.emission_regions import Blob
17 |
18 | # set the spectrum normalisation (total energy in electrons in this case)
19 | spectrum_norm = 1e48 * u.Unit("erg")
20 | # define the spectral function through a dictionary
21 | spectrum_dict = {
22 | "type": "PowerLaw",
23 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e7}
24 | }
25 | R_b = 1e16 * u.cm
26 | B = 1 * u.G
27 | z = Distance(1e27, unit=u.cm).z
28 | delta_D = 10
29 | Gamma = 10
30 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
31 |
32 | The :class:`~agnpy.emission_regions.Blob` can be printed at any moment
33 |
34 | .. code-block:: python
35 |
36 | print(blob)
37 |
38 | returning a summary of its properties
39 |
40 | .. code-block:: text
41 |
42 | * spherical emission region
43 | - R_b (radius of the blob): 1.00e+16 cm
44 | - V_b (volume of the blob): 4.19e+48 cm3
45 | - z (source redshift): 0.07
46 | - d_L (source luminosity distance):1.00e+27 cm
47 | - delta_D (blob Doppler factor): 1.00e+01
48 | - Gamma (blob Lorentz factor): 1.00e+01
49 | - Beta (blob relativistic velocity): 9.95e-01
50 | - mu_s (cosine of the jet viewing angle): 9.95e-01
51 | - B (magnetic field tangled to the jet): 1.00e+00 G
52 | * electron spectrum
53 | - power law
54 | - k_e: 9.29e+06 1 / cm3
55 | - p: 2.80
56 | - gamma_min: 1.00e+02
57 | - gamma_max: 1.00e+07
58 |
59 | the electron distribution accelerated in the blob :math:`n_e` can be also visualised
60 | (multiplying it by an arbitrary power of gamma):
61 |
62 | .. code-block:: python
63 |
64 | import matplotlib.pyplot as plt
65 | blob.plot_n_e(gamma_power=2)
66 | plt.show()
67 |
68 | .. image:: _static/n_e_gamma.png
69 | :width: 500px
70 | :align: center
71 |
72 |
73 | Normalisation modes
74 | -------------------
75 |
76 | By chosing a normalisation in units of :math:`{\rm cm}^{-3}` one can select -
77 | specifying the parameter `spectrum_norm_type` - three different modes of normalising
78 | the electron distribution.
79 |
80 | * `integral`: (default) the spectrum is set such that :math:`n_{e,\,tot}` equals the value provided by `spectrum_norm`;
81 |
82 | * `differential`: the spectrum is set such that :math:`k_e` equals the value provided by `spectrum_norm`;
83 |
84 | * `gamma=1`: the spectrum is set such that :math:`n_e(\gamma=1)` equals the value provided by `spectrum_norm`.
85 |
86 |
87 | API
88 | ---
89 |
90 | .. automodule:: agnpy.emission_regions
91 | :noindex:
92 | :members: Blob
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 |
16 | sys.path.insert(0, os.path.abspath("../"))
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = "agnpy"
21 | copyright = "2019, Cosimo Nigro"
22 | author = "Cosimo Nigro"
23 |
24 | # The full version, including alpha/beta/rc tags
25 | release = "0.0.6"
26 |
27 |
28 | # -- General configuration ---------------------------------------------------
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = [
34 | "sphinx.ext.autodoc",
35 | "sphinx.ext.autosummary",
36 | "sphinx.ext.napoleon",
37 | "sphinx.ext.intersphinx",
38 | "sphinx.ext.extlinks",
39 | "sphinx.ext.mathjax",
40 | "nbsphinx",
41 | ]
42 | # Add any paths that contain templates here, relative to this directory.
43 | templates_path = ["_templates"]
44 |
45 | # List of patterns, relative to source directory, that match files and
46 | # directories to ignore when looking for source files.
47 | # This pattern also affects html_static_path and html_extra_path.
48 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
49 |
50 | numfig = True
51 |
52 | # -- Options for HTML output -------------------------------------------------
53 |
54 | # The theme to use for HTML and HTML Help pages. See the documentation for
55 | # a list of builtin themes.
56 | #
57 | html_theme = "sphinx_rtd_theme"
58 |
59 | # Add any paths that contain custom static files (such as style sheets) here,
60 | # relative to this directory. They are copied after the builtin static files,
61 | # so a file named "default.css" will overwrite the builtin "default.css".
62 | html_static_path = ["_static"]
63 |
64 | # set main node of the documentation to index.rst (contents.rst is the default)
65 | master_doc = "index"
66 |
67 | # dictionary with external packages references
68 | intersphinx_mapping = {
69 | "numpy": ("https://numpy.org/doc/stable/", None),
70 | "astropy": ("http://docs.astropy.org/en/latest/", None),
71 | "matplotlib": ("https://matplotlib.org", None),
72 | }
73 |
74 | # latex support
75 | mathjax_config = {
76 | "TeX": {
77 | "Macros": {
78 | "Beta": r"{\mathcal{B}}",
79 | "uunits": r"{{\rm erg}\,{\rm cm}^{-3}}",
80 | "diff": r"{\mathrm{d}}",
81 | "utransform": r"{\Gamma^3 (1 + \Beta \mu')^3}",
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/experiments/profiling/profile_tau.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | sys.path.append("../../")
4 | import numpy as np
5 | import astropy.units as u
6 | import astropy.constants as const
7 | from agnpy.emission_regions import Blob
8 | from agnpy.targets import SSDisk, SphericalShellBLR, RingDustTorus
9 | from agnpy.absorption import Absorption
10 | import matplotlib.pyplot as plt
11 |
12 | MEC2 = const.m_e * const.c * const.c
13 |
14 | # define the blob
15 | spectrum_norm = 1e47 * u.erg
16 | parameters = {"p": 2.8, "gamma_min": 10, "gamma_max": 1e6}
17 | spectrum_dict = {"type": "PowerLaw", "parameters": parameters}
18 | R_b = 1e16 * u.cm
19 | B = 0.56 * u.G
20 | z = 0
21 | delta_D = 40
22 | Gamma = 40
23 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
24 | print("blob definition:")
25 | print(blob)
26 |
27 | # disk parameters
28 | M_sun = const.M_sun.cgs
29 | M_BH = 1.2 * 1e9 * M_sun
30 | R_g = ((const.G * M_BH) / (const.c * const.c)).cgs
31 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
32 | eta = 1 / 12
33 | R_in = 6 * R_g
34 | R_out = 200 * R_g
35 | disk = SSDisk(M_BH, L_disk, eta, R_in, R_out)
36 | print("disk definition:")
37 | print(disk)
38 |
39 | # blr definition
40 | epsilon_line = 2e-5
41 | csi_line = 0.024
42 | R_line = 1e17 * u.cm
43 | blr = SphericalShellBLR(disk, csi_line, epsilon_line, R_line)
44 | print("blr definition:")
45 | print(blr)
46 |
47 | # dust torus definition
48 | T_dt = 1e3 * u.K
49 | epsilon_dt = 2.7 * ((const.k_B * T_dt) / (const.m_e * const.c * const.c)).decompose()
50 | csi_dt = 0.1
51 | dt = RingDustTorus(disk, csi_dt, epsilon_dt)
52 | print("torus definition:")
53 | print(dt)
54 |
55 | # let us make a 2D plot of where s will be bigger than 1
56 | r = 1.1e16 * u.cm
57 |
58 | absorption_disk = Absorption(blob, disk, r=r)
59 | absorption_blr = Absorption(blob, blr, r=r)
60 | absorption_dt = Absorption(blob, dt, r=r)
61 |
62 | # a check on the values for which s > 1 in the case of the disk
63 | E = np.logspace(0, 5) * u.GeV
64 | epsilon_1 = (E / MEC2).decompose().value
65 | epsilon_disk = disk._epsilon_mu(absorption_disk.mu, r.value)
66 | E_disk = (epsilon_disk * MEC2).to("eV")
67 |
68 |
69 | def where_s_1(mu, r):
70 | s = epsilon_1 * disk._epsilon_mu(mu, r) * (1 - mu) / 2
71 | return E[s > 1][0]
72 |
73 |
74 | for _r in [1e15, 1e16, 1e17]:
75 | E_thr = [where_s_1(mu, _r).value for mu in absorption_disk.mu]
76 | plt.semilogy(absorption_disk.mu, E_thr, label=f"r = {_r:.0e}")
77 |
78 | plt.xlabel(r"$\mu$")
79 | plt.ylabel("E (s>1) / GeV")
80 | plt.legend()
81 | plt.show()
82 |
83 |
84 | # let's plot the opacities
85 |
86 | nu = E.to("Hz", equivalencies=u.spectral())
87 |
88 | tau_disk = absorption_disk.tau(nu)
89 | tau_blr = absorption_blr.tau(nu)
90 | tau_dt = absorption_dt.tau(nu)
91 |
92 | fig, ax = plt.subplots()
93 | ax.loglog(E, tau_disk, lw=2, ls="-", label="SS disk")
94 | ax.loglog(E, tau_blr, lw=2, ls="--", label="spherical shell BLR")
95 | ax.loglog(E, tau_dt, lw=2, ls="-.", label="ring dust torus")
96 | ax.legend()
97 | ax.set_xlabel("E / GeV")
98 | ax.set_ylabel(r"$\tau_{\gamma \gamma}$")
99 | ax.set_xlim([1, 1e5])
100 | ax.set_ylim([1e-3, 1e5])
101 | plt.show()
102 |
--------------------------------------------------------------------------------
/experiments/profiling/profile_external_compton.py:
--------------------------------------------------------------------------------
1 | """profile and test external Comton radiation"""
2 | import sys
3 |
4 | sys.path.append("../../")
5 | import numpy as np
6 | import astropy.units as u
7 | import astropy.constants as const
8 | from astropy.coordinates import Distance
9 | from agnpy.emission_regions import Blob
10 | from agnpy.targets import SSDisk, SphericalShellBLR, RingDustTorus
11 | from agnpy.compton import ExternalCompton
12 | import matplotlib.pyplot as plt
13 |
14 | # to profile
15 | import cProfile, pstats
16 | import timeit
17 |
18 | # functions to profile and time
19 | def profile(command, label):
20 | """function to profile a given command"""
21 | print(f"->{command} profiling section...")
22 | cProfile.run(command, f"Profile_{label}.prof")
23 | prof = pstats.Stats(f"Profile_{label}.prof")
24 | prof.strip_dirs().sort_stats("time").print_stats(10)
25 |
26 |
27 | def timing(command, number):
28 | """function to time a given command, returns time in seconds"""
29 | return timeit.timeit(command, globals=globals(), number=number)
30 |
31 |
32 | # define the blob
33 | spectrum_norm = 6e42 * u.erg
34 | parameters = {"p1": 2, "p2": 3.5, "gamma_b": 1e4, "gamma_min": 20, "gamma_max": 5e7}
35 | spectrum_dict = {"type": "BrokenPowerLaw", "parameters": parameters}
36 | R_b = 1e16 * u.cm
37 | B = 0.56 * u.G
38 | z = 1
39 | delta_D = 40
40 | Gamma = 40
41 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
42 | print("\nblob definition:")
43 | print(blob)
44 |
45 | # disk parameters
46 | M_sun = const.M_sun.cgs
47 | M_BH = 1.2 * 1e9 * M_sun
48 | R_g = ((const.G * M_BH) / (const.c * const.c)).cgs
49 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
50 | eta = 1 / 12
51 | R_in = 6 * R_g
52 | R_out = 200 * R_g
53 | disk = SSDisk(M_BH, L_disk, eta, R_in, R_out)
54 | print("\ndisk definition:")
55 | print(disk)
56 |
57 | # blr definition
58 | epsilon_line = 2e-5
59 | csi_line = 0.024
60 | R_line = 1e17 * u.cm
61 | blr = SphericalShellBLR(disk, csi_line, epsilon_line, R_line)
62 | print("\nblr definition:")
63 | print(blr)
64 |
65 | # dust torus definition
66 | T_dt = 1e3 * u.K
67 | epsilon_dt = 2.7 * ((const.k_B * T_dt) / (const.m_e * const.c * const.c)).decompose()
68 | csi_dt = 0.1
69 | dt = RingDustTorus(disk, csi_dt, epsilon_dt)
70 | print("\ntorus definition:")
71 | print(dt)
72 |
73 | # define the External Compton
74 | ec_disk = ExternalCompton(blob, disk, r=1e17 * u.cm)
75 | ec_blr = ExternalCompton(blob, blr, r=1e17 * u.cm)
76 | ec_dt = ExternalCompton(blob, dt, r=1e17 * u.cm)
77 | nu = np.logspace(15, 30) * u.Hz
78 |
79 | # commands to profile
80 | ec_disk_sed_command = "ec_disk.sed_flux(nu)"
81 | ec_blr_sed_command = "ec_blr.sed_flux(nu)"
82 | ec_dt_sed_command = "ec_dt.sed_flux(nu)"
83 |
84 | n = 100
85 |
86 | print("\nprofiling sed computation external compton on disk:")
87 | profile(ec_disk_sed_command, "ec_disk_sed")
88 | time_ec_disk = timing(ec_disk_sed_command, n)
89 | time_ec_disk /= n
90 | print(f"time: {time_ec_disk:.2e} s")
91 |
92 | print("\nprofiling sed computation external compton on BLR:")
93 | profile(ec_blr_sed_command, "ec_blr_sed")
94 | time_ec_blr = timing(ec_blr_sed_command, n)
95 | time_ec_blr /= n
96 | print(f"time: {time_ec_blr:.2e} s")
97 |
98 | print("\nprofiling sed computation external compton on dust torus:")
99 | profile(ec_dt_sed_command, "ec_dt_sed")
100 | time_ec_dt = timing(ec_dt_sed_command, n)
101 | time_ec_dt /= n
102 | print(f"time: {time_ec_dt:.2e} s")
103 |
--------------------------------------------------------------------------------
/docs/synchrotron.rst:
--------------------------------------------------------------------------------
1 | .. _synchrotron:
2 |
3 |
4 | Synchrotron Radiation
5 | =====================
6 |
7 | The synchrotron radiation is computed following the approach of [DermerMenon2009]_ and [Finke2008]_.
8 |
9 | Expanding the example in :ref:`emission_regions`, it is here illustrated how to produce a synchrotron spectral energy distribution (SED) staring from a :class:`~agnpy.emission_regions.Blob`. The Synchrotron Self Absorption (SSA) mechanism can be considered.
10 |
11 | .. code-block:: python
12 |
13 | import numpy as np
14 | import astropy.units as u
15 | from astropy.coordinates import Distance
16 | from agnpy.emission_regions import Blob
17 | from agnpy.synchrotron import Synchrotron
18 | import matplotlib.pyplot as plt
19 |
20 | # set the spectrum normalisation (total energy in electrons in this case)
21 | spectrum_norm = 1e48 * u.Unit("erg")
22 | # define the spectral function through a dictionary
23 | spectrum_dict = {
24 | "type": "PowerLaw",
25 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e7}
26 | }
27 | R_b = 1e16 * u.cm
28 | B = 1 * u.G
29 | z = Distance(1e27, unit=u.cm).z
30 | delta_D = 10
31 | Gamma = 10
32 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
33 |
34 | to initialise the synchrotron radiation the :class:`~agnpy.emission_regions.Blob` instance has to be passed to the :class:`~agnpy.synchrotron.Synchrotron` class initialiser
35 |
36 | .. code-block:: python
37 |
38 | synch = Synchrotron(blob)
39 |
40 | the optional argument `ssa` specifies if self absorption has to be taken into account (by default it is not)
41 |
42 | .. code-block:: python
43 |
44 | synch_ssa = Synchrotron(blob, ssa=True)
45 |
46 | to compute the spectral energy distribution (SED), an array of frequencies (astropy units) has to be passed to the :func:`~agnpy.synchrotron.Synchrotron.sed_flux` function
47 |
48 | .. code-block:: python
49 |
50 | nu = np.logspace(8, 23) * u.Hz
51 | synch_sed = synch.sed_flux(nu)
52 | print(synch_sed)
53 |
54 | this produces an array of :class:`~astropy.units.Quantity`
55 |
56 | .. code-block:: text
57 |
58 | [9.07847669e-16 2.32031314e-15 5.92493269e-15 1.51066953e-14
59 | 3.84226036e-14 9.73303142e-14 2.44919574e-13 6.09602590e-13
60 | 1.49002575e-12 3.53274373e-12 7.95174501e-12 1.63760127e-11
61 | 2.91395265e-11 4.20897124e-11 4.96023285e-11 5.36547089e-11
62 | 5.75763018e-11 6.17811534e-11 6.62930892e-11 7.11345358e-11
63 | 7.63295578e-11 8.19039772e-11 8.78855014e-11 9.43038616e-11
64 | 1.01190960e-10 1.08581027e-10 1.16510791e-10 1.25019658e-10
65 | 1.34149896e-10 1.43946820e-10 1.54458961e-10 1.65738143e-10
66 | 1.77839337e-10 1.90819907e-10 2.04737266e-10 2.19642496e-10
67 | 2.35563787e-10 2.52464517e-10 2.70139515e-10 2.87965730e-10
68 | 3.04330969e-10 3.15437517e-10 3.13250591e-10 2.83748181e-10
69 | 2.12000058e-10 1.06769402e-10 2.42794829e-11 1.12784249e-12
70 | 2.22960744e-15 8.03667875e-21] erg / (cm2 s)
71 |
72 | Let us examine the different SEDs produced by the normal and self-absorbed synchrotron processes
73 |
74 | .. code-block:: python
75 |
76 | synch_sed_ssa = synch_ssa.sed_flux(nu)
77 | plt.loglog(nu, synch_sed, color="k", lw=2, label="synchr.")
78 | plt.loglog(nu, synch_sed_ssa, lw=2, ls="--", color="gray", label="self absorbed synchr.")
79 | plt.xlabel(r"$\nu\,/\,\mathrm{Hz}$")
80 | plt.ylabel(r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$")
81 | plt.legend()
82 | plt.show()
83 |
84 | .. image:: _static/synch.png
85 | :width: 500px
86 | :align: center
87 |
88 | For more examples of Synchrotron radiation and cross-checks of literature results, check the
89 | check the `tutorial notebook on synchrotron and sycnrotron self Compton `_.
90 |
91 |
92 | API
93 | ---
94 |
95 | .. automodule:: agnpy.synchrotron
96 | :noindex:
97 | :members: Synchrotron
--------------------------------------------------------------------------------
/docs/absorption.rst:
--------------------------------------------------------------------------------
1 | .. _absorption:
2 |
3 |
4 | :math:`\gamma`-:math:`\gamma` absorption
5 | ========================================
6 |
7 | The photon fields that represent the target for the Compton scattering might re-absorb the scattered photons via :math:`\gamma`-:math:`\gamma` pair production. `agnpy` computes the optical depth (or opacity) :math:`\tau_{\gamma \gamma}` as a function of the frequency :math:`\nu` of the photon hitting the target.
8 |
9 | .. math::
10 | \tau_{\gamma \gamma}(\nu) = \int_{r}^{\infty} {\rm d}l \; \int_{0}^{2\pi} {\rm d}\phi \;
11 | \int_{-1}^{1} {\rm d}\mu \; (1 - \cos\psi) \int_{0}^{\infty} {\rm d}\epsilon \;
12 | \frac{u(\epsilon, \mu, \phi; l)}{\epsilon m_e c^2} \, \sigma_{\gamma \gamma}(s),
13 |
14 | where:
15 | - :math:`\cos\psi = \mu\mu_s + \sqrt{1 - \mu^2}\sqrt{1 - \mu_s^2} \cos\phi` is the cosine of the angle between the hitting and the absorbing photon;
16 | - :math:`u(\epsilon, \mu, \phi; l)` is the energy density of the target photon field;
17 | - :math:`\sigma_{\gamma \gamma}(s)` is the pair-production cross section, with :math:`s = \epsilon_1 \epsilon \, (1 - \cos\psi)\,/\,2` and :math:`\epsilon_1 = h \nu\,/\,(m_e c^2)` the dimensionless hitting photon energy.
18 |
19 | Photoabsorption results in an attenuation of the photon flux by a factor :math:`\exp(-\tau_{\gamma \gamma})`.
20 |
21 | Basic formulas are borrowed from [Finke2016]_. The approach presented therein (and in [Dermer2009]_) simplifies the integration by assuming that the hitting photons travel in the direction parallel to the jet axis (:math:`\mu_s \rightarrow 1`), decoupling the cross section and the :math:`(1 - \cos\psi)` term from the integral on :math:`\phi`. The optical depths thus calculated are therefore valid only for blazars.
22 |
23 | `agnpy` carries on the full integration, such that the optical depths are valid for any jetted AGN.
24 |
25 | Absorption on target photon fields
26 | ----------------------------------
27 |
28 | In the following example we compute the optical depths produced by the disk, the broad line region and the dust torus photon fileds
29 |
30 | .. code-block:: python
31 |
32 | import numpy as np
33 | import astropy.units as u
34 | import astropy.constants as const
35 | from agnpy.emission_regions import Blob
36 | from agnpy.targets import SSDisk, SphericalShellBLR, RingDustTorus
37 | from agnpy.absorption import Absorption
38 | import matplotlib.pyplot as plt
39 |
40 | # define the blob
41 | spectrum_norm = 1e47 * u.erg
42 | parameters = {"p": 2.8, "gamma_min": 10, "gamma_max": 1e6}
43 | spectrum_dict = {"type": "PowerLaw", "parameters": parameters}
44 | R_b = 1e16 * u.cm
45 | B = 0.56 * u.G
46 | z = 0
47 | delta_D = 40
48 | Gamma = 40
49 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
50 |
51 | # disk parameters
52 | M_BH = 1.2 * 1e9 * const.M_sun.cgs
53 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
54 | eta = 1 / 12
55 | R_in = 6
56 | R_out = 200
57 | disk = SSDisk(M_BH, L_disk, eta, R_in, R_out, R_g_units=True)
58 |
59 | # blr definition
60 | csi_line = 0.024
61 | R_line = 1e17 * u.cm
62 | blr = SphericalShellBLR(L_disk, csi_line, "Lyalpha", R_line)
63 |
64 | # dust torus definition
65 | T_dt = 1e3 * u.K
66 | csi_dt = 0.1
67 | dt = RingDustTorus(L_disk, csi_dt, T_dt)
68 |
69 | as for the :class:`~agnpy.compton.ExternalCompton` radiation, the absortpion can
70 | be computed passing to the :class:`~agnpy.absorption.Absorption` class the
71 | :class:`~agnpy.emission_regions.Blob` and :class:`~agnpy.targets.SSDisk`
72 | (or any other target) instances.
73 | Remember also to set the distance between the blob and the target photon field (:math:`r`)
74 |
75 | .. code-block:: python
76 |
77 | # consider a fixed distance of the blob from the target fields
78 | r = 1.1e16 * u.cm
79 |
80 | absorption_disk = Absorption(blob, disk, r=r)
81 | absorption_blr = Absorption(blob, blr, r=r)
82 | absorption_dt = Absorption(blob, dt, r=r)
83 |
84 | E = np.logspace(0, 5) * u.GeV
85 | nu = E.to("Hz", equivalencies=u.spectral())
86 |
87 | tau_disk = absorption_disk.tau(nu)
88 | tau_blr = absorption_blr.tau(nu)
89 | tau_dt = absorption_dt.tau(nu)
90 |
91 | fig, ax = plt.subplots()
92 | ax.loglog(E, tau_disk, lw=2, ls="-", label = "SS disk")
93 | ax.loglog(E, tau_blr, lw=2, ls="--", label = "spherical shell BLR")
94 | ax.loglog(E, tau_dt, lw=2, ls="-.", label = "ring dust torus")
95 | ax.legend()
96 | ax.set_xlabel("E / GeV")
97 | ax.set_ylabel(r"$\tau_{\gamma \gamma}$")
98 | ax.set_xlim([1, 1e5])
99 | ax.set_ylim([1e-3, 1e5])
100 | plt.show()
101 |
102 | .. image:: _static/tau.png
103 | :width: 500px
104 | :align: center
105 |
106 |
107 | API
108 | ---
109 |
110 | .. automodule:: agnpy.absorption
111 | :noindex:
112 | :members: Absorption
--------------------------------------------------------------------------------
/docs/compton.rst:
--------------------------------------------------------------------------------
1 | .. _compton:
2 |
3 |
4 | Inverse Compton
5 | ===============
6 |
7 | Two different Compton processes are considered:
8 |
9 | * Synchrotron Self Compton (SSC), implemented in the class :class:`~agnpy.compton.SynchrotronSelfCompton`, foreseeing as target for the inverse Compton the synchrotron photons produced in the blob by the accelerated electrons (based on [DermerMenon2009]_ and [Finke2008]_);
10 |
11 | * External Compton (EC), implemented in the class :class:`~agnpy.compton.ExternalCompton`, foreseeing as a target for the inverse Compton the (direct and reprocessed) photon fields generated by the accretion phenomena (based on [Dermer2009]_ and [Finke2016]_).
12 |
13 | Synchrotron Self-Compton
14 | ------------------------
15 | Let us keep expanding the example in :ref:`synchrotron` to compute the SSC radiation.
16 | Again let us define the blob
17 |
18 | .. code-block:: python
19 |
20 | import numpy as np
21 | import astropy.units as u
22 | from astropy.coordinates import Distance
23 | from agnpy.emission_regions import Blob
24 | from agnpy.synchrotron import Synchrotron
25 | from agnpy.compton import SynchrotronSelfCompton
26 | import matplotlib.pyplot as plt
27 |
28 | # set the spectrum normalisation (total energy in electrons in this case)
29 | spectrum_norm = 1e48 * u.Unit("erg")
30 | # define the spectral function through a dictionary
31 | spectrum_dict = {
32 | "type": "PowerLaw",
33 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e7}
34 | }
35 | R_b = 1e16 * u.cm
36 | B = 1 * u.G
37 | z = Distance(1e27, unit=u.cm).z
38 | delta_D = 10
39 | Gamma = 10
40 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
41 |
42 | and the synchrotron radiation produced by it
43 |
44 | .. code-block:: python
45 |
46 | synch = Synchrotron(blob)
47 |
48 | the SSC radiation can be computed passing to the :class:`~agnpy.compton.SynchrotronSelfCompton` class the :class:`~agnpy.emission_regions.Blob` and :class:`~agnpy.synchrotron.Synchrotron` instances
49 |
50 | .. code-block:: python
51 |
52 | ssc = SynchrotronSelfCompton(blob, synch)
53 |
54 | we can now plot the complete SED
55 |
56 | .. code-block:: python
57 |
58 | nu = np.logspace(8, 30) * u.Hz
59 | # let us compute the SED values at these frequencies
60 | synch_sed = synch.sed_flux(nu)
61 | ssc_sed = ssc.sed_flux(nu)
62 | plt.loglog(nu, synch_sed, lw=2, label="Synchrotron")
63 | plt.loglog(nu, ssc_sed, lw=2, label="SSC")
64 | plt.xlabel(r"$\nu\,/\,\mathrm{Hz}$")
65 | plt.ylabel(r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$")
66 | plt.ylim([1e-12, 1e-8])
67 | plt.legend()
68 | plt.show()
69 |
70 | .. image:: _static/ssc.png
71 | :width: 500px
72 | :align: center
73 |
74 |
75 | External Compton
76 | ----------------
77 |
78 | As an example of this process let us consider Compton scattering of the photon field produced by the disk by a broken power-law electron distribution.
79 |
80 | .. code-block:: python
81 |
82 | import numpy as np
83 | import astropy.units as u
84 | import astropy.constants as const
85 | from astropy.coordinates import Distance
86 | from agnpy.emission_regions import Blob
87 | from agnpy.targets import SSDisk
88 | from agnpy.compton import ExternalCompton
89 | import matplotlib.pyplot as plt
90 |
91 | spectrum_norm = 6e42 * u.erg
92 | parameters = {
93 | "p1": 2.1,
94 | "p2": 3.5,
95 | "gamma_b": 1e4,
96 | "gamma_min": 20,
97 | "gamma_max": 5e7,
98 | }
99 | spectrum_dict = {"type": "BrokenPowerLaw", "parameters": parameters}
100 | R_b = 1e16 * u.cm
101 | B = 0.56 * u.G
102 | z = 1
103 | delta_D = 40
104 | Gamma = 40
105 | blob = Blob(R_b, z, delta_D, Gamma, B, spectrum_norm, spectrum_dict)
106 |
107 | let us define the target disk
108 |
109 | .. code-block:: python
110 |
111 | # disk parameters
112 | M_BH = 1.2 * 1e9 * const.M_sun
113 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
114 | eta = 1 / 12
115 | R_in = 6
116 | R_out = 200
117 | disk = SSDisk(M_BH, L_disk, eta, R_in, R_out, R_g_units=True)
118 |
119 | the EC radiation can be computed passing to the :class:`~agnpy.compton.ExternalCompton` class the :class:`~agnpy.emission_regions.Blob` and :class:`~agnpy.targets.SSDisk` instances. Remember also to set the distance between the blob and the target photon field (`r`)
120 |
121 | .. code-block:: python
122 |
123 | ec = ExternalCompton(blob, disk, r=1e17 * u.cm)
124 |
125 | let us plot the resulting SED:
126 |
127 | .. code-block:: python
128 |
129 | nu = np.logspace(15, 30) * u.Hz
130 | ec_disk_sed = ec.sed_flux(nu)
131 | plt.loglog(nu, ec_disk_sed, lw=2, label="EC on Disk")
132 | plt.xlabel(r"$\nu\,/\,\mathrm{Hz}$")
133 | plt.ylabel(r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$")
134 | plt.ylim([1e-20, 1e-12])
135 | plt.legend()
136 | plt.show()
137 |
138 | .. image:: _static/ec_disk.png
139 | :width: 500px
140 | :align: center
141 |
142 | You can use any object in the :py:mod:`~agnpy.targets` module as target for external Compton.
143 | For more examples of Inverse Compton radiation and reproduction of literature results,
144 | check the `tutorial notebook on external Compton `_.
145 |
146 |
147 | API
148 | ---
149 |
150 | .. automodule:: agnpy.compton
151 | :noindex:
152 | :members: SynchrotronSelfCompton, ExternalCompton
--------------------------------------------------------------------------------
/experiments/basic/gmax.py:
--------------------------------------------------------------------------------
1 | # macro for testing various limits on the gamma factors of electrons
2 |
3 | import numpy as np
4 | import math
5 | import astropy.units as u
6 | import astropy.constants as const
7 | from astropy.coordinates import Distance
8 | import matplotlib.pyplot as plt
9 |
10 | # plt.ion()
11 | import sys
12 |
13 | sys.path.append("../../")
14 | from agnpy.emission_regions import Blob
15 | from agnpy.synchrotron import Synchrotron
16 | from agnpy.compton import ExternalCompton, SynchrotronSelfCompton
17 | from agnpy.targets import SSDisk, SphericalShellBLR, RingDustTorus
18 |
19 | # parameters of the blob
20 | B0 = 0.1 * u.G
21 | gmin0 = 10.0
22 | gmax0 = 3000.0
23 | gbreak = 300.0
24 | z = 0.94
25 | delta_D = 20
26 | Gamma = 17
27 | # r0=1.e16 * u.cm
28 | r0 = 1.0e14 * u.m
29 | dist = 3.0e16 * u.cm
30 | xi = 1.0e-4
31 | nu = np.logspace(8, 26, 200) * u.Hz
32 | norm = 15000.0 * u.Unit("cm-3")
33 |
34 | parameters = {
35 | "p1": 2.0,
36 | "p2": 3.0,
37 | "gamma_b": gbreak,
38 | "gamma_min": gmin0,
39 | "gamma_max": gmax0,
40 | }
41 | spectrum_dict = {"type": "BrokenPowerLaw", "parameters": parameters}
42 |
43 | blob = Blob(r0, z, delta_D, Gamma, B0, norm, spectrum_dict, xi=xi)
44 |
45 | # plt.loglog(blob.gamma, blob.n_e (blob.gamma))
46 |
47 | #############################################
48 | # limits from confinement of particles inside the blob:
49 | gmaxconf = blob.gamma_max_larmor
50 | # computing larmor radius of this electron, should be of the size of the blob
51 | # R_L = 33.36 km * (p/(GeV/c)) * (G/B) * Z^-1
52 | # https://w3.iihe.ac.be/~aguilar/PHYS-467/PA3.pdf
53 | rlarmor = (33.36 * u.km * gmaxconf * 511.0e3 / 1.0e9 / (blob.B / u.G)).to("cm")
54 |
55 | # both values are similar
56 | print("R_L (gmaxconf)=", rlarmor, "R_b=", blob.R_b)
57 |
58 | #############################################
59 | # now maximum from balistic time
60 | gmaxbal = blob.gamma_max_ballistic
61 |
62 | # compute acceleration time for those electrons
63 | # eq 2 from https://arxiv.org/abs/1208.6200a, note that this is rough scaling accurate to ~10%
64 | tau_acc = 1.0 * gmaxbal * 511.0e3 / 1.0e9 / (blob.xi / 1.0e-4 * blob.B / u.G) * u.s
65 | # during this time side of R_b of the jet should pass through the blob (in the blob frame!)
66 | dist_cross = (tau_acc * const.c).to("cm")
67 |
68 | # again both values are similar
69 | print(f"dist_cross (tau_acc(gmaxbal))={dist_cross:.2e}, R_b={blob.R_b:.2e}")
70 |
71 |
72 | #############################################
73 | # now maximum from synchrotron losses
74 | gmaxsyn = blob.gamma_max_synch
75 |
76 | # calculate t_acc
77 | tau_acc = 1.0 * gmaxsyn * 511.0e3 / 1.0e9 / (blob.xi / 1.0e-4 * blob.B / u.G) * u.s
78 | # calculate synchrotron energy loss from the well known formula:
79 | # dE/dt = 4/3 * gamma^2 *U_b * c * sigma_T
80 | Ub = (blob.B / u.G) ** 2 / (8 * np.pi) * u.Unit("erg cm-3")
81 | dEdt = 4.0 / 3.0 * (gmaxsyn) ** 2 * Ub * (const.c * const.sigma_T).to("cm3 s-1")
82 | Elost = (dEdt * tau_acc).to("GeV")
83 | Emax = (gmaxsyn * const.m_e * (const.c) ** 2).to("GeV")
84 |
85 | # both values are similar
86 | print(f"E(gmaxsyn) = {Emax:.2e}, Elost = {Elost:.2e}")
87 |
88 | # print(gmaxconf, gmaxbal, gmaxsyn)
89 |
90 | #############################################
91 | # check of synchrotron cooling break
92 |
93 | # eq F.1 from https://ui.adsabs.harvard.edu/abs/2020arXiv200107729M/abstract
94 | # gammab = 3pi me c^2 / sigma_T B^2 R
95 | # here we use 6 instead of 3 because we only have synchrotron losses and compare then
96 | # with dynamical time scale of crossing R
97 |
98 | # now compare the value from the class with the formula below
99 | gamma_b = blob.gamma_break_synch
100 | gamma_break_check = (
101 | 6
102 | * np.pi
103 | * 511.0e3
104 | * u.eV.to("erg")
105 | / (0.665e-24 * (blob.B / u.G) ** 2 * (blob.R_b / u.cm))
106 | )
107 |
108 | print(f"gamma_break = {gamma_b:.5e}, gamma_break_check = {gamma_break_check:.5e}")
109 |
110 | #############################################
111 | # limits for SSC
112 | # print(blob.u_e)
113 | # print(blob.u_dens_synchr)
114 |
115 | # redo blob without beaming
116 | Gamma = 1.01
117 | delta_D = 1.02
118 | z = 0.01
119 | blob1 = Blob(r0, z, delta_D, Gamma, B0 * 10.0, norm, spectrum_dict, xi=xi)
120 |
121 | u_ph_synch = blob1.u_ph_synch # energy density of synchr photons
122 | # u_dens * V_b is the total energy in the blob,
123 | # photons spend an average time of 0.75 * R_b/c in the blob
124 | # so the total energy flux is:
125 | # total energy in blob / (average time * 4 pi dist^2)
126 | energy_flux_predicted = (
127 | blob.u_ph_synch
128 | * blob1.V_b
129 | / (0.75 * blob1.R_b / const.c.cgs)
130 | * np.power(blob1.d_L, -2)
131 | / (4 * np.pi)
132 | ).to("erg cm-2 s-1")
133 |
134 | synch1 = Synchrotron(blob1, ssa=False)
135 | synch1_sed = synch1.sed_flux(nu)
136 |
137 | energy_flux_sim = np.trapz(synch1_sed / (nu * const.h.cgs), nu * const.h.cgs)
138 | print(
139 | f"predicted energy flux: {energy_flux_predicted:.5e}, simulated energy flux: {energy_flux_sim:.5e}"
140 | )
141 | # nice agreement
142 |
143 | ssc1 = SynchrotronSelfCompton(blob1, synch1)
144 | ssc1_sed = ssc1.sed_flux(nu)
145 |
146 | print("UB/Usynch = ", blob1.U_B / u_ph_synch)
147 | print(
148 | "SED_synch/SED_SSC=",
149 | energy_flux_sim / np.trapz(ssc1_sed / (nu * const.h.cgs), nu * const.h.cgs),
150 | )
151 | # same energy densities mean in Thomson regime the same energy losses ==> the same energy flux
152 | print("break_synchr/break_SSC = ", blob1.gamma_break_synch / blob1.gamma_break_SSC)
153 |
154 | print("gmax_synchr/gmax_SSC = ", blob1.gamma_max_synch / blob1.gamma_max_SSC)
155 |
156 | # SSC is at the same level as Synchr. so the cooling breaks and maximum energies are also same
157 |
158 |
159 | plt.rc("figure", figsize=(7.5, 5.5))
160 | plt.rc("font", size=12)
161 | plt.rc("axes", grid=True)
162 | plt.rc("grid", ls=":")
163 | sed_x_label = r"$\nu\,/\,Hz$"
164 | sed_y_label = r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$"
165 |
166 | plt.loglog(nu, synch1_sed, color="k", ls="-", lw=1, label="Synchr.") #
167 | plt.loglog(nu, ssc1_sed, color="r", ls="-", lw=1, label="SSC") #
168 | plt.ylim(1e-15, 1e-10)
169 | plt.xlim(1e8, 1e27)
170 | plt.xscale("log")
171 | plt.yscale("log")
172 | plt.xlabel(sed_x_label)
173 | plt.ylabel(sed_y_label)
174 | plt.legend()
175 | plt.show()
176 |
--------------------------------------------------------------------------------
/agnpy/tests/test_synchrotron.py:
--------------------------------------------------------------------------------
1 | # test on synchrotron module
2 | import numpy as np
3 | import astropy.units as u
4 | from astropy.constants import m_e, c, h
5 | from astropy.coordinates import Distance
6 | from agnpy.emission_regions import Blob
7 | from agnpy.synchrotron import Synchrotron, nu_synch_peak, epsilon_B, synch_sed_param_bpl
8 | import matplotlib.pyplot as plt
9 | from pathlib import Path
10 | import pytest
11 |
12 |
13 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
14 | epsilon_equivalency = [
15 | (u.Hz, u.Unit(""), lambda x: h.cgs * x / mec2, lambda x: x * mec2 / h.cgs)
16 | ]
17 | tests_dir = Path(__file__).parent
18 |
19 |
20 | def make_sed_comparison_plot(nu, reference_sed, agnpy_sed, fig_title, fig_name):
21 | """make a SED comparison plot for visual inspection"""
22 | fig, ax = plt.subplots(
23 | 2, sharex=True, gridspec_kw={"height_ratios": [2, 1]}, figsize=(8, 6)
24 | )
25 | # plot the SEDs in the upper panel
26 | ax[0].loglog(nu, reference_sed, marker=".", ls="-", lw=1.5, label="reference")
27 | ax[0].loglog(nu, agnpy_sed, marker=".", ls="--", lw=1.5, label="agnpy")
28 | ax[0].legend()
29 | ax[0].set_xlabel(r"$\nu\,/\,{\rm Hz}$")
30 | ax[0].set_ylabel(r"$\nu F_{\nu}\,/\,({\rm erg}\,{\rm cm}^{-2}\,{\rm s}^{-1})$")
31 | ax[0].set_title(fig_title)
32 | # plot the deviation in the bottom panel
33 | deviation = 1 - agnpy_sed / reference_sed
34 | ax[1].semilogx(
35 | nu,
36 | deviation,
37 | lw=1.5,
38 | label=r"$1 - \nu F_{\nu, \rm agnpy} \, / \,\nu F_{\nu, \rm reference}$",
39 | )
40 | ax[1].legend(loc=2)
41 | ax[1].axhline(0, ls="-", lw=1.5, color="dimgray")
42 | ax[1].axhline(0.2, ls="--", lw=1.5, color="dimgray")
43 | ax[1].axhline(-0.2, ls="--", lw=1.5, color="dimgray")
44 | ax[1].axhline(0.3, ls=":", lw=1.5, color="dimgray")
45 | ax[1].axhline(-0.3, ls=":", lw=1.5, color="dimgray")
46 | ax[1].set_ylim([-0.5, 0.5])
47 | ax[1].set_xlabel(r"$\nu / Hz$")
48 | fig.savefig(f"{tests_dir}/crosscheck_figures/{fig_name}.png")
49 |
50 |
51 | # global PWL blob, same parameters of Figure 7.4 in Dermer Menon 2009
52 | SPECTRUM_NORM = 1e48 * u.Unit("erg")
53 | PWL_DICT = {
54 | "type": "PowerLaw",
55 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e5},
56 | }
57 | R_B = 1e16 * u.cm
58 | B = 1 * u.G
59 | Z = Distance(1e27, unit=u.cm).z
60 | DELTA_D = 10
61 | GAMMA = 10
62 | PWL_BLOB = Blob(R_B, Z, DELTA_D, GAMMA, B, SPECTRUM_NORM, PWL_DICT)
63 |
64 | # global blob with BPL law of electrons, to test the parametrisation of the
65 | # delta function approximation
66 | BPL_DICT = {
67 | "type": "BrokenPowerLaw",
68 | "parameters": {
69 | "p1": 2.5,
70 | "p2": 3.5,
71 | "gamma_b": 1e4,
72 | "gamma_min": 1e2,
73 | "gamma_max": 1e7,
74 | },
75 | }
76 | BPL_BLOB = Blob(R_B, Z, DELTA_D, GAMMA, B, SPECTRUM_NORM, BPL_DICT)
77 |
78 |
79 | class TestSynchrotron:
80 | """class grouping all tests related to the Synchrotron class"""
81 |
82 | def test_synch_reference_sed(self):
83 | """test agnpy synchrotron SED against the one sampled from Figure
84 | 7.4 of Dermer Menon 2009"""
85 | sampled_synch_sed_table = np.loadtxt(
86 | f"{tests_dir}/sampled_seds/synch_figure_7_4_dermer_menon_2009.txt",
87 | delimiter=",",
88 | comments="#",
89 | )
90 | sampled_synch_nu = sampled_synch_sed_table[:, 0] * u.Hz
91 | sampled_synch_sed = sampled_synch_sed_table[:, 1] * u.Unit("erg cm-2 s-1")
92 | synch = Synchrotron(PWL_BLOB)
93 | # recompute the SED at the same ordinates where the figure was sampled
94 | agnpy_synch_sed = synch.sed_flux(sampled_synch_nu)
95 | # sed comparison plot
96 | make_sed_comparison_plot(
97 | sampled_synch_nu,
98 | sampled_synch_sed,
99 | agnpy_synch_sed,
100 | "Synchrotron",
101 | "synch_comparison_figure_7_4_dermer_menon_2009",
102 | )
103 | # requires that the SED points deviate less than 15% from the figure
104 | assert u.allclose(
105 | agnpy_synch_sed,
106 | sampled_synch_sed,
107 | atol=0 * u.Unit("erg cm-2 s-1"),
108 | rtol=0.15,
109 | )
110 |
111 | def test_ssa_sed(self):
112 | """test this version SSA SED against the one generated with version 0.0.6"""
113 | sampled_ssa_sed_table = np.loadtxt(
114 | f"{tests_dir}/sampled_seds/ssa_sed_agnpy_v0_0_6.txt",
115 | delimiter=",",
116 | comments="#",
117 | )
118 | sampled_ssa_nu = sampled_ssa_sed_table[:, 0] * u.Hz
119 | sampled_ssa_sed = sampled_ssa_sed_table[:, 1] * u.Unit("erg cm-2 s-1")
120 | ssa = Synchrotron(PWL_BLOB, ssa=True)
121 | agnpy_ssa_sed = ssa.sed_flux(sampled_ssa_nu)
122 | assert u.allclose(
123 | agnpy_ssa_sed, sampled_ssa_sed, atol=0 * u.Unit("erg cm-2 s-1"),
124 | )
125 |
126 | def test_nu_synch_peak(self):
127 | gamma = 100
128 | nu_synch = nu_synch_peak(B, gamma).to_value("Hz")
129 | assert np.isclose(nu_synch, 27992489872.33304, atol=0)
130 |
131 | def test_epsilon_B(self):
132 | assert np.isclose(epsilon_B(B), 2.2655188038060715e-14, atol=0)
133 |
134 | def test_synch_sed_param_bpl(self):
135 | """check that the parametrised delta-function approximation for the
136 | synchrotron radiation gives exactly the same result of the full formula
137 | from which it was derived"""
138 | nu = np.logspace(8, 23) * u.Hz
139 | synch = Synchrotron(BPL_BLOB)
140 | sed_delta_approx = synch.sed_flux_delta_approx(nu)
141 | # check that the synchrotron parameterisation work
142 | y = BPL_BLOB.B.value * BPL_BLOB.delta_D
143 | k_eq = (BPL_BLOB.u_e / BPL_BLOB.U_B).to_value("")
144 | sed_param = synch_sed_param_bpl(
145 | nu.value,
146 | y,
147 | k_eq,
148 | BPL_BLOB.n_e.p1,
149 | BPL_BLOB.n_e.p2,
150 | BPL_BLOB.n_e.gamma_b,
151 | BPL_BLOB.n_e.gamma_min,
152 | BPL_BLOB.n_e.gamma_max,
153 | BPL_BLOB.d_L.cgs.value,
154 | BPL_BLOB.R_b.cgs.value,
155 | BPL_BLOB.z,
156 | )
157 | assert np.allclose(sed_param, sed_delta_approx.value, atol=0, rtol=1e-2)
158 |
--------------------------------------------------------------------------------
/experiments/fit/data/sed_mrk421.ecsv:
--------------------------------------------------------------------------------
1 | # %ECSV 0.9
2 | # ---
3 | # datatype:
4 | # - {name: x, unit: Hz, datatype: float64}
5 | # - {name: y, unit: erg/(cm2 s) , datatype: float64}
6 | # - {name: dy, unit: erg/(cm2 s) ,datatype: float64}
7 | # - {name: data_set, datatype: string}
8 | # meta: !!omap
9 | # - {z: 0.0308 }
10 | # - {restframe: obs}
11 | # - {data_scale: lin-lin}
12 | # - {obj_name: 'J1104+3812,Mrk421'}
13 | # schema: astropy-2.0
14 | x y dy data_set
15 | 2.299540e+09 1.340900e-14 3.910000e-16 campaing-2009
16 | 2.639697e+09 1.793088e-14 3.231099e-26 campaing-2009
17 | 4.799040e+09 2.313600e-14 2.400000e-16 campaing-2009
18 | 4.805039e+09 1.773414e-14 1.773414e-15 campaing-2009
19 | 4.843552e+09 2.776140e-14 2.615339e-26 campaing-2009
20 | 7.698460e+09 3.696000e-14 4.620000e-16 campaing-2009
21 | 8.267346e+09 2.836267e-14 2.836267e-15 campaing-2009
22 | 8.331867e+09 3.989630e-14 3.627671e-26 campaing-2009
23 | 8.388659e+09 3.163450e-14 1.931495e-15 campaing-2009
24 | 8.399994e+09 4.000500e-14 5.041094e-15 campaing-2009
25 | 1.044892e+10 4.626737e-14 3.297726e-26 campaing-2009
26 | 1.109778e+10 4.617600e-14 6.660000e-16 campaing-2009
27 | 1.456571e+10 5.628417e-14 4.453463e-26 campaing-2009
28 | 1.492481e+10 6.368235e-14 1.621386e-16 campaing-2009
29 | 1.499967e+10 3.855000e-14 2.709359e-15 campaing-2009
30 | 1.499967e+10 4.837500e-14 3.395769e-15 campaing-2009
31 | 1.536243e+10 4.978422e-14 4.978422e-15 campaing-2009
32 | 2.199977e+10 1.122000e-13 4.943885e-14 campaing-2009
33 | 2.219556e+10 6.793200e-14 6.216000e-15 campaing-2009
34 | 2.301043e+10 8.090550e-14 2.300000e-25 campaing-2009
35 | 2.379974e+10 7.117545e-14 7.117546e-15 campaing-2009
36 | 3.198437e+10 1.097600e-13 1.100000e-25 campaing-2009
37 | 3.700000e+10 1.462923e-13 8.140199e-15 campaing-2009
38 | 4.320886e+10 1.153907e-13 1.153907e-14 campaing-2009
39 | 4.321195e+10 7.199119e-14 7.216404e-15 campaing-2009
40 | 4.321195e+10 9.096126e-14 9.117732e-15 campaing-2009
41 | 2.300000e+11 5.681000e-13 4.753333e-14 campaing-2009
42 | 1.404800e+14 6.062274e-11 5.669552e-13 campaing-2009
43 | 1.419646e+14 4.288929e-11 4.948616e-13 campaing-2009
44 | 1.843360e+14 6.168793e-11 7.277505e-13 campaing-2009
45 | 2.403267e+14 9.562824e-11 5.808794e-13 campaing-2009
46 | 2.410828e+14 6.163756e-11 7.916657e-13 campaing-2009
47 | 3.312401e+14 6.089521e-11 1.334518e-13 campaing-2009
48 | 3.738181e+14 7.568818e-11 3.985502e-13 campaing-2009
49 | 4.339585e+14 6.649275e-11 1.165315e-13 campaing-2009
50 | 4.339585e+14 7.199342e-11 2.468092e-13 campaing-2009
51 | 4.591579e+14 9.270129e-11 8.574353e-13 campaing-2009
52 | 4.713597e+14 7.753473e-11 1.600079e-13 campaing-2009
53 | 4.713597e+14 9.794059e-11 2.883171e-13 campaing-2009
54 | 5.203998e+14 7.893424e-11 1.446908e-13 campaing-2009
55 | 5.462307e+14 9.097549e-11 1.656096e-13 campaing-2009
56 | 5.462307e+14 1.095292e-10 3.327595e-13 campaing-2009
57 | 6.280127e+14 9.460537e-11 6.613772e-13 campaing-2009
58 | 6.644170e+14 8.150921e-11 2.025489e-13 campaing-2009
59 | 6.858017e+14 8.796137e-11 2.840166e-13 campaing-2009
60 | 1.200838e+15 1.199390e-10 6.507510e-13 campaing-2009
61 | 1.383232e+15 1.375669e-10 5.290993e-13 campaing-2009
62 | 1.630285e+15 1.429960e-10 6.075196e-13 campaing-2009
63 | 8.351535e+16 2.975200e-10 1.061128e-12 campaing-2009
64 | 1.004073e+17 3.038032e-10 9.435480e-13 campaing-2009
65 | 1.207159e+17 3.016012e-10 9.226051e-13 campaing-2009
66 | 1.451320e+17 3.083210e-10 9.560387e-13 campaing-2009
67 | 1.744867e+17 3.106028e-10 8.797329e-13 campaing-2009
68 | 2.097787e+17 3.017214e-10 7.934078e-13 campaing-2009
69 | 2.522089e+17 2.911740e-10 7.512085e-13 campaing-2009
70 | 3.032211e+17 2.773099e-10 7.288943e-13 campaing-2009
71 | 6.119268e+17 2.189431e-10 1.096225e-12 campaing-2009
72 | 7.312803e+17 2.026132e-10 1.122510e-12 campaing-2009
73 | 8.358087e+17 2.275909e-10 5.963279e-13 campaing-2009
74 | 8.739133e+17 1.815045e-10 1.134076e-12 campaing-2009
75 | 9.330559e+17 2.137195e-10 5.745879e-13 campaing-2009
76 | 1.030417e+18 2.034302e-10 5.785770e-13 campaing-2009
77 | 1.044366e+18 1.599814e-10 1.161699e-12 campaing-2009
78 | 1.127900e+18 1.927660e-10 5.965604e-13 campaing-2009
79 | 1.225506e+18 1.831281e-10 6.300546e-13 campaing-2009
80 | 1.248065e+18 1.371831e-10 1.268533e-12 campaing-2009
81 | 1.323238e+18 1.714314e-10 6.651987e-13 campaing-2009
82 | 1.421099e+18 1.650449e-10 6.975880e-13 campaing-2009
83 | 1.491494e+18 1.182152e-10 1.516473e-12 campaing-2009
84 | 1.519089e+18 1.584728e-10 7.223261e-13 campaing-2009
85 | 1.617209e+18 1.506239e-10 7.457634e-13 campaing-2009
86 | 1.715457e+18 1.455495e-10 7.819029e-13 campaing-2009
87 | 1.782404e+18 9.858767e-11 2.038083e-12 campaing-2009
88 | 1.813836e+18 1.374508e-10 8.202885e-13 campaing-2009
89 | 1.912344e+18 1.319425e-10 8.654677e-13 campaing-2009
90 | 2.010980e+18 1.293873e-10 9.133146e-13 campaing-2009
91 | 2.109744e+18 1.236334e-10 9.587381e-13 campaing-2009
92 | 2.130053e+18 8.460679e-11 3.450220e-12 campaing-2009
93 | 2.208635e+18 1.184789e-10 1.011287e-12 campaing-2009
94 | 2.307654e+18 1.150234e-10 1.081205e-12 campaing-2009
95 | 2.502254e+18 1.076853e-10 7.177670e-13 campaing-2009
96 | 2.801190e+18 9.785241e-11 9.049351e-13 campaing-2009
97 | 3.195310e+18 8.746557e-11 9.949986e-13 campaing-2009
98 | 3.618915e+18 5.219438e-11 7.539957e-12 campaing-2009
99 | 3.698774e+18 7.450991e-11 1.552041e-12 campaing-2009
100 | 4.103464e+18 5.256700e-11 7.685355e-12 campaing-2009
101 | 4.204720e+18 6.377387e-11 2.580904e-12 campaing-2009
102 | 4.811738e+18 4.491810e-11 5.877010e-12 campaing-2009
103 | 4.995334e+18 5.159649e-11 3.157928e-12 campaing-2009
104 | 6.001292e+18 3.521743e-11 5.723799e-12 campaing-2009
105 | 6.539646e+18 4.170940e-11 8.500545e-12 campaing-2009
106 | 8.544779e+18 1.785744e-11 4.783685e-12 campaing-2009
107 | #1.402192e+19 2.008310e-11 6.316832e-12 campaing-2009
108 | #2.931204e+19 1.749813e-11 1.261572e-11 campaing-2009
109 | 3.830816e+22 2.169504e-11 2.958277e-12 campaing-2009
110 | 9.622899e+22 2.778789e-11 2.101036e-12 campaing-2009
111 | 2.418153e+23 2.825620e-11 2.332301e-12 campaing-2009
112 | 6.073865e+23 4.085394e-11 3.976984e-12 campaing-2009
113 | 1.525634e+24 4.949722e-11 6.866338e-12 campaing-2009
114 | 3.832265e+24 6.512987e-11 1.158324e-11 campaing-2009
115 | 9.626236e+24 5.386962e-11 1.636900e-11 campaing-2009
116 | 2.412270e+25 7.491067e-11 1.122301e-11 campaing-2009
117 | 2.417992e+25 9.754259e-11 3.560456e-11 campaing-2009
118 | 3.823193e+25 8.199207e-11 7.050657e-12 campaing-2009
119 | 6.059363e+25 5.614334e-11 5.793969e-12 campaing-2009
120 | 6.073707e+25 1.147050e-10 6.573696e-11 campaing-2009
121 | 9.603433e+25 4.662219e-11 5.097912e-12 campaing-2009
122 | 1.522041e+26 5.221583e-11 4.890630e-12 campaing-2009
123 | 2.412270e+26 3.668340e-11 4.682033e-12 campaing-2009
124 | 3.823193e+26 2.247871e-11 4.343216e-12 campaing-2009
125 | 6.059363e+26 1.972081e-11 4.407365e-12 campaing-2009
126 | 9.603433e+26 7.994215e-12 3.469109e-12 campaing-2009
--------------------------------------------------------------------------------
/experiments/basic/ec_gmax.py:
--------------------------------------------------------------------------------
1 | # macro for testing various limits on the gamma factors of electrons
2 |
3 | import numpy as np
4 | import math
5 | import astropy.units as u
6 | import astropy.constants as const
7 | from astropy.coordinates import Distance
8 | import matplotlib.pyplot as plt
9 |
10 | plt.ion()
11 | import sys
12 |
13 | sys.path.append("../../")
14 | from agnpy.emission_regions import Blob
15 | from agnpy.synchrotron import Synchrotron
16 | from agnpy.compton import ExternalCompton, SynchrotronSelfCompton
17 | from agnpy.targets import SSDisk, SphericalShellBLR, RingDustTorus
18 |
19 | # parameters of the blob with a narrow EED
20 | B0 = 10.1 * u.G
21 | gmin0 = 500.0 * 2
22 | gmax0 = 800.0 * 2
23 | gbreak = gmin0
24 | z = 0.01
25 | Gamma = 17
26 | delta_D = 1.99 * Gamma
27 | r0 = 1.0e15 * u.cm
28 | xi = 1.0e-4
29 | nu = np.logspace(8, 26, 200) * u.Hz
30 | norm = 0.1 * u.Unit("erg cm-3")
31 |
32 | parameters = {
33 | "p1": 2.0,
34 | "p2": 3.0,
35 | "gamma_b": gbreak,
36 | "gamma_min": gmin0,
37 | "gamma_max": gmax0,
38 | }
39 | spectrum_dict = {"type": "BrokenPowerLaw", "parameters": parameters}
40 |
41 |
42 | #####################
43 | # test one with emission region in the center of the DT
44 | L_disk = 0.91e45 * u.Unit("erg s-1")
45 | xi_dt = 0.6
46 | T_dt = 100 * u.K
47 | R_dt = 1.0e18 * u.cm
48 | h = 0.01 * R_dt
49 |
50 | ## test with lower numbers, gives virtually the same
51 | # B0/=10
52 | # T_dt/=10
53 | # L_disk/=100
54 |
55 | blob1 = Blob(r0, z, delta_D, Gamma, B0, norm, spectrum_dict, xi=xi)
56 | dt1 = RingDustTorus(L_disk, xi_dt, T_dt, R_dt=R_dt)
57 |
58 | # energy density of DT radiation field in the blob
59 | u_dt1 = dt1.u_ph(h, blob1)
60 | u_synch1 = blob1.u_ph_synch
61 | print(
62 | "energy density in the blob, DT radiation: ",
63 | u_dt1,
64 | "synchrotron photons: ",
65 | u_synch1,
66 | )
67 | dt1_sed = dt1.sed_flux(nu, z)
68 | # energy density was set to be the same
69 |
70 | synch1 = Synchrotron(blob1, ssa=False)
71 | synch1_sed = synch1.sed_flux(nu)
72 |
73 | ssc1 = SynchrotronSelfCompton(blob1, synch1)
74 | ssc1_sed = ssc1.sed_flux(nu)
75 |
76 | ec_dt1 = ExternalCompton(blob1, dt1, h)
77 | ec_dt1_sed = ec_dt1.sed_flux(nu)
78 |
79 | ssc1_total = (np.trapz(ssc1_sed / nu, nu)).to("erg cm-2 s-1")
80 | ec_dt1_total = (np.trapz(ec_dt1_sed / nu, nu)).to("erg cm-2 s-1")
81 | print("SSC total=", ssc1_total, ", EC DT total=", ec_dt1_total)
82 | # similar density of radiation but there is a factor of ~1.4 difference in the integrated flux
83 | # this probably comes from different angular distribution of the radiation
84 |
85 | gbreakssc = blob1.gamma_break_SSC
86 | gbreakdt = blob1.gamma_break_EC_DT(dt1, h)
87 | print(
88 | "break SSC=", gbreakssc, ", break EC=", gbreakdt, ", ratio: ", gbreakssc / gbreakdt
89 | )
90 | # values of the break scale with energy density so they are the same
91 |
92 | gmaxssc = blob1.gamma_max_SSC
93 | gmaxdt = blob1.gamma_max_EC_DT(dt1, h)
94 | print("max SSC=", gmaxssc, ", max EC=", gmaxdt, ", ratio: ", gmaxssc / gmaxdt)
95 | # the same with values of the maximum gamma factor
96 |
97 | plt.rc("figure", figsize=(7.5, 5.5))
98 | plt.rc("font", size=12)
99 | plt.rc("axes", grid=True)
100 | plt.rc("grid", ls=":")
101 | sed_x_label = r"$\nu\,/\,Hz$"
102 | sed_y_label = r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$"
103 |
104 | plt.loglog(
105 | nu / Gamma, synch1_sed, color="k", ls="-", lw=1, label="Synchr. (shifted)"
106 | ) # /np.power(delta_D,4)*np.power(R_dt/r0,2)
107 | plt.loglog(nu, ssc1_sed, color="r", ls="-", lw=1, label="SSC") #
108 | plt.loglog(nu, ec_dt1_sed, color="b", ls="-", lw=1, label="EC DT") #
109 | plt.loglog(
110 | nu * Gamma, dt1_sed, color="g", ls="-", lw=1, label="DT1 (shifted)"
111 | ) # *np.power(Gamma,2)
112 | plt.ylim(1e-19, 1e-6)
113 | plt.xlim(1e8, 1e27)
114 | plt.xscale("log")
115 | plt.yscale("log")
116 | plt.xlabel(sed_x_label)
117 | plt.ylabel(sed_y_label)
118 |
119 | ######################
120 | # test with emission region further along the jet
121 |
122 | h = 0.9 * R_dt
123 | dist = np.sqrt(h * h + R_dt * R_dt)
124 | factor = blob1.Gamma * (1 - blob1.Beta * h / dist)
125 |
126 | # before we had boosting by Gamma, now by factor
127 | T_dt2 = T_dt / (factor / blob1.Gamma) # shift the energies
128 | L_disk2 = (
129 | L_disk * np.power(factor / blob1.Gamma, -2) * np.power(dist / R_dt, 2)
130 | ) # shift the luminosity for the beaming and larger distance
131 | dt2 = RingDustTorus(L_disk2, xi_dt, T_dt2, R_dt=R_dt)
132 | ec_dt2 = ExternalCompton(blob1, dt2, h)
133 | ec_dt2_sed = ec_dt2.sed_flux(nu)
134 | dt2_sed = dt2.sed_flux(nu, z)
135 |
136 | u_dt2 = dt2.u_ph(h, blob1)
137 | print("DT energy density (test 1)=", u_dt1, ", (test2)=", u_dt2)
138 | # same energy densities
139 |
140 | ec_dt2_total = (np.trapz(ec_dt2_sed / nu, nu)).to("erg cm-2 s-1")
141 | print(
142 | "SSC total=",
143 | ssc1_total,
144 | ", EC DT total (test1)=",
145 | ec_dt1_total,
146 | ", (test2)=",
147 | ec_dt2_total,
148 | )
149 | # the obtained spectrum from EC in test 1 and 2 is the same, but even while the radiation density is the same, it has different angular distribution. Still, both test1 and test2 are nearly head-on in blob's frame, so the resulting EC is very similar
150 |
151 | angle2 = np.arccos((h / dist - blob1.Beta) / (1 - blob1.Beta * h / dist)).to("deg")
152 | print("test2, photons in blob at angle", angle2)
153 |
154 | plt.loglog(nu * factor, dt2_sed, color="g", ls=":", lw=1, label="DT2 (shifted)")
155 | plt.loglog(nu, ec_dt2_sed, color="b", ls=":", lw=1, label="EC DT2 (over EC DT1)")
156 |
157 |
158 | ######################
159 | # test with emission region far enough along the jet that beaming disappears
160 |
161 | h = 17.0 * R_dt
162 | dist = np.sqrt(h * h + R_dt * R_dt)
163 | factor = blob1.Gamma * (1 - blob1.Beta * h / dist)
164 |
165 | print(factor)
166 | # before we had boosting by Gamma, now by factor
167 | T_dt3 = T_dt / (factor / blob1.Gamma) # shift the energies
168 | L_disk3 = (
169 | L_disk * np.power(factor / blob1.Gamma, -2) * np.power(dist / R_dt, 2)
170 | ) # shift the luminosity for the beaming and larger distance
171 | dt3 = RingDustTorus(L_disk3, xi_dt, T_dt3, R_dt=R_dt)
172 | ec_dt3 = ExternalCompton(blob1, dt3, h)
173 | ec_dt3_sed = ec_dt3.sed_flux(nu)
174 | dt3_sed = dt3.sed_flux(nu, z)
175 |
176 | u_dt3 = dt3.u_ph(h, blob1)
177 | print("DT energy density (test 1)=", u_dt1, ", (test3)=", u_dt3)
178 | # same energy densities
179 |
180 | ec_dt3_total = (np.trapz(ec_dt3_sed / nu, nu)).to("erg cm-2 s-1")
181 | print(
182 | "SSC total=",
183 | ssc1_total,
184 | ", EC DT total (test1)=",
185 | ec_dt1_total,
186 | ", (test3)=",
187 | ec_dt3_total,
188 | )
189 | # now with the same energy density, but with photons perpendicular to the jet direction in the frame of the blob we get a factor of ~4 lower flux.
190 | # the factor of 2 should come from the cross section,
191 | # and another factor of 2 from transformation of energy of the photon to the electron's frame
192 | # seems consistent
193 | print(ec_dt1_total / ec_dt3_total)
194 | angle3 = np.arccos((h / dist - blob1.Beta) / (1 - blob1.Beta * h / dist)).to("deg")
195 | print("test3, photons in blob at angle", angle3)
196 |
197 | plt.loglog(nu * factor, dt3_sed, color="g", ls="-.", lw=1, label="DT3 (shifted)")
198 | plt.loglog(nu, ec_dt3_sed, color="b", ls="-.", lw=1, label="EC DT3")
199 |
200 | plt.legend()
201 | plt.show()
202 |
203 | # sys.exit()
204 |
--------------------------------------------------------------------------------
/docs/targets.rst:
--------------------------------------------------------------------------------
1 | .. _targets:
2 |
3 |
4 | Photon Targets for External Compton
5 | ===================================
6 | The classes here described will provide the targets for the external Compton scattering.
7 | They also allow, in the case of the accretion disk and dust torus, to compute their own black-body radiative emission.
8 |
9 | The following objects are implemented:
10 |
11 | * :class:`~agnpy.targets.CMB`, representing the Cosmic Microwave Background;
12 |
13 | * :class:`~agnpy.targets.PointSourceBehindJet`, representing a monochromatic point source behind the jet.
14 | This is mostly used to crosscheck that the energy densities and External Compton SEDs of the other targets reduce to
15 | this simplified case for large enough distances;
16 |
17 | * :class:`~agnpy.targets.SSDisk`, representing a [Shakura1973]_ (i.e. a geometrically thin, optically thick) accretion disk;
18 |
19 | * :class:`~agnpy.targets.SphericalShellBLR`, representing the Broad Line Region as an infinitesimally thin spherical shell, on the lines of [Finke2016]_;
20 |
21 | * :class:`~agnpy.targets.RingDustTorus`, representing the Dust Torus as an infintesimally thin ring, see treatment of [Finke2016]_.
22 |
23 | Shakura Sunyaev disk
24 | --------------------
25 | The accretion disk can be intialised specifying the mass of the central Black Hole, :math:`M_{\mathrm{BH}}`, the disk
26 | luminosity, :math:`L_{\mathrm{disk}}`, the efficiency to transform accreted matter to escaping radiant energy, :math:`\eta`,
27 | the inner and outer disk radii, :math:`R_{\mathrm{in}}` and :math:`R_{\mathrm{out}}`.
28 |
29 | .. code-block:: python
30 |
31 | import numpy as np
32 | import astropy.units as u
33 | import astropy.constants as const
34 | from agnpy.targets import SSDisk
35 |
36 | # quantities defining the disk
37 | M_BH = 1.2 * 1e9 * const.M_sun
38 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
39 | eta = 1 / 12
40 | R_g = 1.77 * 1e14 * u.cm
41 | R_in = 6 * R_g
42 | R_out = 200 * R_g
43 |
44 | disk = SSDisk(M_BH, L_disk, eta, R_in, R_out)
45 |
46 | Alternatively the disk can be initialised specifying R_in and R_out in dimensionless units of gravitational radius, setting the
47 | `R_g_units` argument to `True` (`False` by default).
48 |
49 | .. code-block:: python
50 |
51 | disk = SSDisk(M_BH, L_disk, eta, 6, 200, R_g_units=True)
52 |
53 | as for other `agnpy` objects, also the disk can be printed to display a summary of its values
54 |
55 | .. code-block:: python
56 |
57 | print(disk)
58 |
59 | .. code-block:: text
60 |
61 | * Shakura Sunyaev accretion disk:
62 | - M_BH (central black hole mass): 2.39e+42 g
63 | - L_disk (disk luminosity): 2.00e+46 erg / s
64 | - eta (accretion efficiency): 8.33e-02
65 | - dot(m) (mass accretion rate): 2.67e+26 g / s
66 | - R_in (disk inner radius): 1.06e+15 cm
67 | - R_out (disk inner radius): 3.54e+16 cm
68 |
69 | Broad Line Region (BLR)
70 | -----------------------
71 | The BLR can be initialised specifying the luminosity of the disk whose radiation is being reprocessed, :math:`L_{\mathrm{disk}}`,
72 | the fraction of radiation reprocessed, :math:`\xi_{\mathrm{line}}`, the type of line emitted (for a complete list see the
73 | :func:`~agnpy.targets.print_lines_list`), the radius at which the line is emitted, :math:`R_{\mathrm{line}}`.
74 | Let us continue from the previous snippet considering a BLR reprocessing the previous disk luminosity and re-emitting
75 | the :math:`\mathrm{Ly}\alpha` line:
76 |
77 | .. code-block:: python
78 |
79 | from agnpy.targets import SphericalShellBLR
80 |
81 | # quantities defining the BLR
82 | xi_line = 0.024
83 | R_line = 1e17 * u.cm
84 |
85 | blr = SphericalShellBLR(L_disk, xi_line, "Lyalpha", R_line)
86 |
87 | we can print a summary of the BLR properties via
88 |
89 | .. code-block:: python
90 |
91 | print(blr)
92 |
93 | .. code-block:: text
94 |
95 | * Spherical Shell Broad Line Region:
96 | - L_disk (accretion disk luminosity): 2.00e+46 erg / s
97 | - xi_line (fraction of the disk radiation reprocessed by the BLR): 2.40e-02
98 | - line (type of emitted line): Lyalpha, lambda = 0.00 cm
99 | - R_line (radius of the BLR shell): 1.00e+17 cm
100 |
101 | Dust Torus (DT)
102 | ---------------
103 | The DT can be initialised specifying the luminosity of the disk whose radiation is being reprocessed, :math:`L_{\mathrm{disk}}`,
104 | the fraction of radiation reprocessed, :math:`\xi_{\mathrm{dt}}`, the temperature where the black-body radiation peaks,
105 | :math:`T_{\mathrm{dt}}`, the radius of the ring representing the torus, :math:`R_{\mathrm{dt}}`. The latter is optional
106 | and if not specified will be automatically set at the sublimation radius (Eq. 96 in [Finke2016]_).
107 | Let us continue from the previous snippet considering a DT reprocessing the disk luminosity in the infrared
108 | (:math:`T_{\mathrm{dt}} = 1000 \, \mathrm{K}`):
109 |
110 | .. code-block:: python
111 |
112 | from agnpy.targets import RingDustTorus
113 |
114 | # quantities defining the DT
115 | T_dt = 1e3 * u.K
116 | xi_dt = 0.1
117 |
118 | dt = RingDustTorus(L_disk, xi_dt, T_dt)
119 |
120 | .. code-block:: python
121 |
122 | print(dt)
123 |
124 | .. code-block:: text
125 |
126 | * Ring Dust Torus:
127 | - L_disk (accretion disk luminosity): 2.00e+46 erg / s
128 | - xi_dt (fraction of the disk radiation reprocessed by the torus): 1.00e-01
129 | - T_dt (temperature of the dust torus): 1.00e+03 K
130 | - R_dt (radius of the torus): 1.57e+19 cm
131 |
132 |
133 | Black-Body SEDs
134 | ---------------
135 | The SEDs due to the black-body emission by the disk and the DT can be computed via the
136 | `sed_flux` members of the two classes. An array of frequencies over which to compute the SEDs
137 | and the redshift of the galaxy have to be specified.
138 |
139 | .. code-block:: python
140 |
141 | import matplotlib.pyplot as plt
142 | # redshift of the host galaxy
143 | z = 0.1
144 | # array of frequencies to compute the SEDs
145 | nu = np.logspace(12, 18) * u.Hz
146 | # compute the SEDs
147 | disk_bb_sed = disk.sed_flux(nu, z)
148 | dt_bb_sed = dt.sed_flux(nu, z)
149 | # plot them
150 | plt.loglog(nu, disk_bb_sed, lw=2, label="Accretion Disk")
151 | plt.loglog(nu, dt_bb_sed, lw=2, label="Dust Torus")
152 | plt.xlabel(r"$\nu\,/\,\mathrm{Hz}$")
153 | plt.ylabel(r"$\nu F_{\nu}\,/\,(\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1})$")
154 | plt.ylim([1e-12, 1e-8])
155 | plt.legend()
156 | plt.show()
157 |
158 | .. image:: _static/disk_torus_black_bodies.png
159 | :width: 500px
160 | :align: center
161 |
162 |
163 | Energy densities
164 | ----------------
165 | `agnpy` allows also to compute the energy densities produced by the photon targets,
166 | check the `tutorial notebook on energy densities `_.
167 |
168 | API
169 | ---
170 |
171 | .. automodule:: agnpy.targets
172 | :noindex:
173 | :members: CMB, PointSourceBehindJet, SSDisk, SphericalShellBLR, RingDustTorus
174 |
--------------------------------------------------------------------------------
/agnpy/tests/test_spectra_emission_regions.py:
--------------------------------------------------------------------------------
1 | # tests on spectra and emission region modules
2 | import numpy as np
3 | import astropy.units as u
4 | from astropy.constants import e, c, m_e
5 | from agnpy.emission_regions import Blob
6 | import pytest
7 |
8 |
9 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
10 | # a global test blob with spectral index 2
11 | SPECTRUM_NORM = 1e-13 * u.Unit("cm-3")
12 | GAMMA_MIN = 1
13 | GAMMA_B = 100
14 | GAMMA_MAX = 1e6
15 | PWL_IDX_2_DICT = {
16 | "type": "PowerLaw",
17 | "parameters": {"p": 2.0, "gamma_min": GAMMA_MIN, "gamma_max": GAMMA_MAX},
18 | }
19 | BPL_DICT = {
20 | "type": "BrokenPowerLaw",
21 | "parameters": {
22 | "p1": 2.4,
23 | "p2": 3.4,
24 | "gamma_b": GAMMA_B,
25 | "gamma_min": GAMMA_MIN,
26 | "gamma_max": GAMMA_MAX,
27 | },
28 | }
29 | # blob parameters
30 | R_B = 1e16 * u.cm
31 | Z = 0.1
32 | DELTA_D = 10
33 | GAMMA = 10
34 | B = 0.1 * u.G
35 | PWL_BLOB = Blob(R_B, Z, DELTA_D, GAMMA, B, SPECTRUM_NORM, PWL_IDX_2_DICT)
36 | BPL_BLOB = Blob(R_B, Z, DELTA_D, GAMMA, B, SPECTRUM_NORM, BPL_DICT)
37 | # useful for checks
38 | BETA = 1 - 1 / np.power(GAMMA, 2)
39 | V_B = 4 / 3 * np.pi * np.power(R_B, 3)
40 |
41 |
42 | class TestBlob:
43 | """class grouping all tests related to the Blob emission region"""
44 |
45 | def test_default_norm_type(self):
46 | """the default norm type should be 'integral'"""
47 | assert PWL_BLOB.spectrum_norm_type == "integral"
48 |
49 | # tests for the power law normalisation
50 | # - tests for normalisations in cm3
51 | def test_pwl_integral_norm_cm3(self):
52 | """test if the integral norm in cm-3 is correctly set"""
53 | PWL_BLOB.set_n_e(SPECTRUM_NORM, PWL_IDX_2_DICT, "integral")
54 | assert u.allclose(
55 | PWL_BLOB.n_e_tot, SPECTRUM_NORM, atol=0 * u.Unit("cm-3"), rtol=1e-2
56 | )
57 |
58 | def test_pwl_differential_norm_cm3(self):
59 | """test if the differential norm in cm-3 is correctly set"""
60 | PWL_BLOB.set_n_e(SPECTRUM_NORM, PWL_IDX_2_DICT, "differential")
61 | assert u.allclose(
62 | PWL_BLOB.n_e.k_e, SPECTRUM_NORM, atol=0 * u.Unit("cm-3"), rtol=1e-2
63 | )
64 |
65 | def test_pwl_gamma_1_norm_cm3(self):
66 | """test if the norm at gamma = 1 in cm-3 is correctly set"""
67 | PWL_BLOB.set_n_e(SPECTRUM_NORM, PWL_IDX_2_DICT, "gamma=1")
68 | n_e_gamma_1 = PWL_BLOB.n_e(np.asarray([1]))
69 | assert u.allclose(
70 | n_e_gamma_1, SPECTRUM_NORM, atol=0 * u.Unit("cm-3"), rtol=1e-2
71 | )
72 |
73 | # - tests for integral normalisations in erg cm-3 and erg
74 | def test_pwl_integral_norm_erg_cm3(self):
75 | """test if the integral norm in erg cm-3 is correctly set"""
76 | PWL_BLOB.set_n_e(3e-4 * u.Unit("erg cm-3"), PWL_IDX_2_DICT, "integral")
77 | assert u.allclose(
78 | PWL_BLOB.u_e,
79 | 3e-4 * u.Unit("erg cm-3"),
80 | atol=0 * u.Unit("erg cm-3"),
81 | rtol=1e-2,
82 | )
83 |
84 | def test_pwl_integral_norm_erg(self):
85 | """test if the integral norm in erg is correctly set"""
86 | PWL_BLOB.set_n_e(1e48 * u.erg, PWL_IDX_2_DICT, "integral")
87 | assert u.allclose(PWL_BLOB.W_e, 1e48 * u.erg, atol=0 * u.erg, rtol=1e-2)
88 |
89 | # tests for the broken power law normalisation
90 | # - tests for normalisations in cm3
91 | def test_bpl_integral_norm_cm3(self):
92 | """test if the integral norm in cm-3 is correctly set"""
93 | BPL_BLOB.set_n_e(SPECTRUM_NORM, BPL_DICT, "integral")
94 | assert u.allclose(
95 | BPL_BLOB.n_e_tot, SPECTRUM_NORM, atol=0 * u.Unit("cm-3"), rtol=1e-2
96 | )
97 |
98 | def test_bpl_differential_norm_cm3(self):
99 | """test if the differential norm in cm-3 is correctly set"""
100 | BPL_BLOB.set_n_e(SPECTRUM_NORM, BPL_DICT, "differential")
101 | assert u.allclose(
102 | BPL_BLOB.n_e.k_e, SPECTRUM_NORM, atol=0 * u.Unit("cm-3"), rtol=1e-2
103 | )
104 |
105 | def test_bpl_gamma_1_norm_cm3(self):
106 | """test if the norm at gamma = 1 in cm-3 is correctly set"""
107 | BPL_BLOB.set_n_e(SPECTRUM_NORM, BPL_DICT, "gamma=1")
108 | n_e_gamma_1 = BPL_BLOB.n_e(np.asarray([1]))
109 | assert u.allclose(
110 | n_e_gamma_1, SPECTRUM_NORM, atol=0 * u.Unit("cm-3"), rtol=1e-2
111 | )
112 |
113 | # - tests for integral normalisations in erg cm-3 and erg
114 | def test_bpl_integral_norm_erg_cm3(self):
115 | """test if the integral norm in erg cm-3 is correctly set"""
116 | BPL_BLOB.set_n_e(3e-4 * u.Unit("erg cm-3"), BPL_DICT, "integral")
117 | assert u.allclose(
118 | BPL_BLOB.u_e,
119 | 3e-4 * u.Unit("erg cm-3"),
120 | atol=0 * u.Unit("erg cm-3"),
121 | rtol=1e-2,
122 | )
123 |
124 | def test_bpl_integral_norm_erg(self):
125 | """test if the integral norm in erg is correctly set"""
126 | BPL_BLOB.set_n_e(1e48 * u.erg, BPL_DICT, "integral")
127 | assert u.allclose(BPL_BLOB.W_e, 1e48 * u.erg, atol=0 * u.erg, rtol=1e-2)
128 |
129 | # test if mismatching unit and normalisation type raises an NameError
130 | @pytest.mark.parametrize(
131 | "spectrum_norm, spectrum_norm_type",
132 | [
133 | (1e48 * u.erg, "differential"),
134 | (1e48 * u.erg, "gamma=1"),
135 | (1e2 * u.Unit("erg cm-3"), "differential"),
136 | (1e2 * u.Unit("erg cm-3"), "gamma=1"),
137 | ],
138 | )
139 | def test_non_available_norm_type(self, spectrum_norm, spectrum_norm_type):
140 | """check that the spectrum_norm_type 'differential' and 'gamma=1'
141 | raise a NameError for a spectrum_norm in erg or erg cm-3"""
142 | with pytest.raises(NameError):
143 | PWL_BLOB.set_n_e(spectrum_norm, PWL_IDX_2_DICT, spectrum_norm_type)
144 |
145 | # test on blob properties
146 | def test_set_delta_D(self):
147 | PWL_BLOB.set_delta_D(Gamma=10, theta_s=20 * u.deg)
148 | assert np.allclose(PWL_BLOB.delta_D, 1.53804, atol=0)
149 |
150 | def test_set_gamma_size(self):
151 | PWL_BLOB.set_gamma_size(1000)
152 | assert len(PWL_BLOB.gamma) == 1000
153 |
154 | def test_N_e(self):
155 | """check that N_e is n_e * V_b i.e. test their ratio to be V_b"""
156 | PWL_BLOB.set_n_e(SPECTRUM_NORM, PWL_IDX_2_DICT, "differential")
157 | n_e = PWL_BLOB.n_e(PWL_BLOB.gamma)
158 | N_e = PWL_BLOB.N_e(PWL_BLOB.gamma)
159 | assert u.allclose(N_e / n_e, V_B, atol=0 * u.Unit("cm3"), rtol=1e-3)
160 |
161 | def test_pwl_n_e_tot_analytical(self):
162 | n_e_expected = SPECTRUM_NORM * (1 / GAMMA_MIN - 1 / GAMMA_MAX)
163 | assert u.allclose(
164 | PWL_BLOB.n_e_tot, n_e_expected, atol=0 * u.Unit("cm-3"), rtol=1e-3
165 | )
166 |
167 | def test_pwl_N_e_tot_analytical(self):
168 | N_e_expected = V_B * SPECTRUM_NORM * (1 / GAMMA_MIN - 1 / GAMMA_MAX)
169 | assert np.allclose(PWL_BLOB.N_e_tot, N_e_expected.to_value(""), rtol=1e-3)
170 |
171 | def test_pwl_u_e_analyitcal(self):
172 | u_e_expected = mec2 * SPECTRUM_NORM * np.log(GAMMA_MAX / GAMMA_MIN)
173 | assert u.allclose(
174 | PWL_BLOB.u_e, u_e_expected, atol=0 * u.Unit("erg cm-3"), rtol=1e-4
175 | )
176 |
177 | def test_pwl_W_e_analytical(self):
178 | W_e_expected = mec2 * V_B * SPECTRUM_NORM * np.log(GAMMA_MAX / GAMMA_MIN)
179 | assert u.allclose(PWL_BLOB.W_e, W_e_expected, atol=0 * u.erg, rtol=1e-4)
180 |
181 | def test_U_B(self):
182 | # strip the units for convenience on this one
183 | U_B_expected = np.power(B.value, 2) / (8 * np.pi) * u.Unit("erg cm-3")
184 | assert np.allclose(PWL_BLOB.U_B, U_B_expected, atol=0 * u.Unit("erg cm-3"))
185 |
186 | def test_P_jet_e(self):
187 | u_e_expected = mec2 * SPECTRUM_NORM * np.log(GAMMA_MAX / GAMMA_MIN)
188 | P_jet_e_expected = (
189 | 2 * np.pi * np.power(R_B, 2) * BETA * np.power(GAMMA, 2) * c * u_e_expected
190 | )
191 | assert u.allclose(
192 | PWL_BLOB.P_jet_e, P_jet_e_expected, atol=0 * u.Unit("erg s-1"), rtol=1e-2
193 | )
194 |
195 | def test_P_jet_B(self):
196 | U_B_expected = np.power(B.value, 2) / (8 * np.pi) * u.Unit("erg cm-3")
197 | P_jet_B_expected = (
198 | 2 * np.pi * np.power(R_B, 2) * BETA * np.power(GAMMA, 2) * c * U_B_expected
199 | )
200 | assert u.allclose(
201 | PWL_BLOB.P_jet_B, P_jet_B_expected, atol=0 * u.Unit("erg s-1"), rtol=1e-2
202 | )
203 |
--------------------------------------------------------------------------------
/agnpy/absorption.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from astropy.constants import h, c, m_e, sigma_T
3 | import astropy.units as u
4 | from .compton import cos_psi, x_re_shell, x_re_ring, mu_star
5 |
6 |
7 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
8 | # equivalency to transform frequencies to energies in electron rest mass units
9 | epsilon_equivalency = [
10 | (u.Hz, u.Unit(""), lambda x: h.cgs * x / mec2, lambda x: x * mec2 / h.cgs)
11 | ]
12 |
13 |
14 | __all__ = ["sigma", "Absorption"]
15 |
16 |
17 | def sigma(s):
18 | """photon-photon pair production cross section, Eq. 17 of [Dermer2009]"""
19 | beta_cm = np.sqrt(1 - np.power(s, -1))
20 | _prefactor = 3 / 16 * sigma_T * (1 - np.power(beta_cm, 2))
21 | _term1 = (3 - np.power(beta_cm, 4)) * np.log((1 + beta_cm) / (1 - beta_cm))
22 | _term2 = -2 * beta_cm * (2 - np.power(beta_cm, 2))
23 | values = _prefactor * (_term1 + _term2)
24 | values[s < 1] = 0
25 | return values
26 |
27 |
28 | class Absorption:
29 | """class to compute the absorption due to gamma-gamma pair production
30 |
31 | Parameters
32 | ----------
33 | blob : :class:`~agnpy.emission_regions.Blob`
34 | emission region and electron distribution hitting the photon target
35 | target : :class:`~agnpy.targets`
36 | class describing the target photon field
37 | r : :class:`~astropy.units.Quantity`
38 | distance of the blob from the Black Hole (i.e. from the target photons)
39 | """
40 |
41 | def __init__(self, blob, target, r):
42 | self.blob = blob
43 | self.target = target
44 | self.r = r
45 | self.set_mu()
46 | self.set_phi()
47 | self.set_l()
48 |
49 | def set_mu(self, mu_size=100):
50 | self.mu_size = mu_size
51 | if self.target.type == "SSDisk":
52 | # in case of hte disk the mu interval does not go from -1 to 1
53 | r_tilde = (self.r / self.target.R_g).to_value("")
54 | self.mu = self.target.mu_from_r_tilde(r_tilde)
55 | else:
56 | self.mu = np.linspace(-1, 1, self.mu_size)
57 |
58 | def set_phi(self, phi_size=50):
59 | self.phi_size = phi_size
60 | self.phi = np.linspace(0, 2 * np.pi, self.phi_size)
61 |
62 | def set_l(self, l_size=50):
63 | """set the range of integration for the distance
64 | """
65 | # integrate up 3000 pc
66 | self.l_size = l_size
67 | l_max = 3000 * u.pc
68 | self.l = (
69 | np.logspace(
70 | np.log10(self.r.to_value("cm")),
71 | np.log10(l_max.to_value("cm")),
72 | self.l_size,
73 | )
74 | * u.cm
75 | )
76 |
77 | def _opacity_disk(self, nu):
78 | """opacity generated by a Shakura Sunyaev disk
79 |
80 | Parameters
81 | ----------
82 | nu : `~astropy.units.Quantity`
83 | array of frequencies, in Hz, to compute the sed, **note** these are
84 | observed frequencies (observer frame).
85 | """
86 | # define the dimensionless energy
87 | epsilon_1 = nu.to("", equivalencies=epsilon_equivalency)
88 | # transform to BH frame
89 | epsilon_1 *= 1 + self.blob.z
90 | # for multidimensional integration
91 | # axis 0: mu
92 | # axis 1: phi
93 | # axis 2: l
94 | # axis 3: epsilon_1
95 | # arrays starting with _ are multidimensional and used for integration
96 | l_tilde = (self.r / self.target.R_g).to_value("")
97 | _mu = np.reshape(self.mu, (self.mu.size, 1, 1, 1))
98 | _phi = np.reshape(self.phi, (1, self.phi.size, 1, 1))
99 | _l = np.reshape(self.l, (1, 1, self.l.size, 1))
100 | _l_tilde = np.reshape(l_tilde, (1, 1, l_tilde.size, 1))
101 | _epsilon_1 = np.reshape(epsilon_1, (1, 1, 1, epsilon_1.size))
102 | # epsilon and phi of the disk have the same dimensions of mu
103 | # this time though they do not depend on a fixed distance r, but
104 | # on the variable distance l
105 | _epsilon = self.target.epsilon_mu(_mu, _l_tilde)
106 | _phi_disk_mu = self.target.phi_disk_mu(_mu, _l_tilde)
107 | _cos_psi = cos_psi(self.blob.mu_s, _mu, _phi)
108 | _s = _epsilon_1 * _epsilon * (1 - _cos_psi) / 2
109 | _integrand_mu = _phi_disk_mu / (
110 | _epsilon * np.power(_l, 3) * np.power(np.power(_mu, -2) - 1, 3 / 2)
111 | )
112 | _integrand = (1 - _cos_psi) * _integrand_mu * sigma(_s)
113 |
114 | prefactor_num = 3 * self.target.L_disk * self.target.R_g
115 | prefactor_denum = 16 * np.pi * self.target.eta * m_e * np.power(c, 3)
116 |
117 | integral_mu = np.trapz(_integrand, self.mu, axis=0)
118 | integral_phi = np.trapz(integral_mu, self.phi, axis=0)
119 | integral = np.trapz(integral_phi, self.l, axis=0)
120 |
121 | tau = prefactor_num / prefactor_denum * integral
122 | return tau.to_value("")
123 |
124 | def _opacity_shell_blr(self, nu):
125 | """opacity generated by a spherical shell Broad Line Region
126 |
127 | Parameters
128 | ----------
129 | nu : `~astropy.units.Quantity`
130 | array of frequencies, in Hz, to compute the sed, **note** these are
131 | observed frequencies (observer frame).
132 | """
133 | # define the dimensionless energy
134 | epsilon_1 = nu.to("", equivalencies=epsilon_equivalency)
135 | # transform to BH frame
136 | epsilon_1 *= 1 + self.blob.z
137 | # for multidimensional integration
138 | # axis 0: mu_re
139 | # axis 1: phi
140 | # axis 2: l
141 | # axis 3: epsilon_1
142 | # arrays starting with _ are multidimensional and used for integration
143 | _mu = np.reshape(self.mu, (self.mu.size, 1, 1, 1))
144 | _phi = np.reshape(self.phi, (1, self.phi.size, 1, 1))
145 | _l = np.reshape(self.l, (1, 1, self.l.size, 1))
146 | _epsilon_1 = np.reshape(epsilon_1, (1, 1, 1, epsilon_1.size))
147 | # define integrating function
148 | _x = x_re_shell(_mu, self.target.R_line, _l)
149 | _mu_star = mu_star(_mu, self.target.R_line, _l)
150 |
151 | _cos_psi = cos_psi(self.blob.mu_s, _mu_star, _phi)
152 | _s = _epsilon_1 * self.target.epsilon_line * (1 - _cos_psi) / 2
153 | _integrand = (1 - _cos_psi) * np.power(_x, -2) * sigma(_s)
154 |
155 | prefactor_num = self.target.xi_line * self.target.L_disk
156 | prefactor_denum = (
157 | np.power(4 * np.pi, 2) * self.target.epsilon_line * m_e * np.power(c, 3)
158 | )
159 |
160 | integral_mu = np.trapz(_integrand, self.mu, axis=0)
161 | integral_phi = np.trapz(integral_mu, self.phi, axis=0)
162 | integral = np.trapz(integral_phi, self.l, axis=0)
163 |
164 | tau = prefactor_num / prefactor_denum * integral
165 | return tau.to_value("")
166 |
167 | def _opacity_ring_torus(self, nu):
168 | """opacity generated by a ring Dust Torus
169 |
170 | Parameters
171 | ----------
172 | nu : `~astropy.units.Quantity`
173 | array of frequencies, in Hz, to compute the sed, **note** these are
174 | observed frequencies (observer frame).
175 | """
176 | # define the dimensionless energy
177 | epsilon_1 = nu.to("", equivalencies=epsilon_equivalency)
178 | # transform to BH frame
179 | epsilon_1 *= 1 + self.blob.z
180 | # for multidimensional integration
181 | # axis 0: phi
182 | # axis 1: l
183 | # axis 2: epsilon_1
184 | # arrays starting with _ are multidimensional and used for integration
185 | _phi = np.reshape(self.phi, (self.phi.size, 1, 1))
186 | _l = np.reshape(self.l, (1, self.l.size, 1))
187 | _epsilon_1 = np.reshape(epsilon_1, (1, 1, epsilon_1.size))
188 | _x = x_re_ring(self.target.R_dt, _l)
189 | _mu = _l / _x
190 |
191 | _cos_psi = cos_psi(self.blob.mu_s, _mu, _phi)
192 | _s = _epsilon_1 * self.target.epsilon_dt * (1 - _cos_psi) / 2
193 | _integrand = (1 - _cos_psi) * np.power(_x, -2) * sigma(_s)
194 |
195 | prefactor_num = self.target.xi_dt * self.target.L_disk
196 | prefactor_denum = (
197 | np.power(4 * np.pi, 2) * self.target.epsilon_dt * m_e * np.power(c, 3)
198 | )
199 |
200 | integral_phi = np.trapz(_integrand, self.phi, axis=0)
201 | integral = np.trapz(integral_phi, self.l, axis=0)
202 |
203 | tau = prefactor_num / prefactor_denum * integral
204 | return tau.to_value("")
205 |
206 | def tau(self, nu):
207 | """optical depth
208 |
209 | .. math::
210 | \\tau_{\\gamma \\gamma}(\\nu)
211 |
212 | Parameters
213 | ----------
214 | nu : `~astropy.units.Quantity`
215 | array of frequencies, in Hz, to compute the opacity, **note** these are
216 | observed frequencies (observer frame).
217 | """
218 | if self.target.type == "SSDisk":
219 | return self._opacity_disk(nu)
220 | if self.target.type == "SphericalShellBLR":
221 | return self._opacity_shell_blr(nu)
222 | if self.target.type == "RingDustTorus":
223 | return self._opacity_ring_torus(nu)
224 |
--------------------------------------------------------------------------------
/agnpy/synchrotron.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import astropy.units as u
3 | from astropy.constants import h, e, c, m_e, sigma_T
4 | from .spectra import _broken_power_law, _broken_power_law_times_gamma_integral
5 |
6 | e = e.gauss
7 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
8 | B_cr = 4.414e13 * u.G # critical magnetic field
9 | lambda_c = (h / (m_e * c)).to("cm") # Compton wavelength
10 | # equivalency for decomposing Gauss in Gaussian-cgs units (not available in astropy)
11 | Gauss_cgs_unit = "cm(-1/2) g(1/2) s-1"
12 | Gauss_cgs_equivalency = [(u.G, u.Unit(Gauss_cgs_unit), lambda x: x, lambda x: x)]
13 | # equivalency to transform frequencies to energies in electron rest mass units
14 | epsilon_equivalency = [
15 | (u.Hz, u.Unit(""), lambda x: h.cgs * x / mec2, lambda x: x * mec2 / h.cgs)
16 | ]
17 |
18 |
19 | __all__ = [
20 | "R",
21 | "nu_synch_peak",
22 | "Synchrotron",
23 | "synch_sed_param_bpl",
24 | ]
25 |
26 |
27 | def R(x):
28 | """Eq. 7.45 in [Dermer2009]_, angle-averaged integrand of the radiated power, the
29 | approximation of this function, given in Eq. D7 of [Aharonian2010]_, is used.
30 | """
31 | term_1_num = 1.808 * np.power(x, 1 / 3)
32 | term_1_denom = np.sqrt(1 + 3.4 * np.power(x, 2 / 3))
33 | term_2_num = 1 + 2.21 * np.power(x, 2 / 3) + 0.347 * np.power(x, 4 / 3)
34 | term_2_denom = 1 + 1.353 * np.power(x, 2 / 3) + 0.217 * np.power(x, 4 / 3)
35 | return term_1_num / term_1_denom * term_2_num / term_2_denom * np.exp(-x)
36 |
37 |
38 | def nu_synch_peak(B, gamma):
39 | """observed peak frequency for monoenergetic electrons
40 | Eq. 7.19 in [DermerMenon2009]_"""
41 | B = B.to(Gauss_cgs_unit, equivalencies=Gauss_cgs_equivalency)
42 | nu_peak = (e * B / (2 * np.pi * m_e * c)) * np.power(gamma, 2)
43 | return nu_peak.to("Hz")
44 |
45 |
46 | def epsilon_B(B):
47 | r""":math:`\epsilon_B`, Eq. 7.21 [DermerMenon2009]_"""
48 | return (B / B_cr).to_value("")
49 |
50 |
51 | def synch_sed_param_bpl(
52 | nu, y, k_eq, p1, p2, gamma_b, gamma_min, gamma_max, d_L, R_b, z
53 | ):
54 | """fast function (no units) providing a parametric fit based on the delta
55 | function approximation parameterisation:
56 | y = gamma * delta_D
57 | k_eq = u'_e / U_B
58 | z and d_L are repeated to avoid to invoke astropy's distance
59 | """
60 | epsilon = h.cgs.value * nu / mec2.value
61 | gamma_s = np.sqrt(epsilon * (1 + z) * B_cr.value / y)
62 | N_e = _broken_power_law(gamma_s, p1, p2, gamma_b, gamma_min, gamma_max)
63 | bpl_integral = _broken_power_law_times_gamma_integral(
64 | p1, p2, gamma_b, gamma_min, gamma_max
65 | )
66 | prefactor_num = np.power(y, 4) * sigma_T.cgs.value * np.power(R_b, 3) * k_eq
67 | prefactor_denum = (
68 | np.power(3, 2)
69 | * np.power(2, 5)
70 | * np.power(np.pi, 2)
71 | * np.power(d_L, 2)
72 | * m_e.cgs.value
73 | * c.cgs.value
74 | * bpl_integral
75 | )
76 | return prefactor_num / prefactor_denum * np.power(gamma_s, 3) * N_e
77 |
78 |
79 | class Synchrotron:
80 | """Class for synchrotron radiation computation
81 |
82 | Parameters
83 | ----------
84 | blob : :class:`~agnpy.emission_region.Blob`
85 | emitting region and electron distribution
86 |
87 | ssa : bool
88 | whether or not to consider synchrotron self absorption (SSA).
89 | The absorption factor will be taken into account in
90 | :func:`~agnpy.synchrotron.Synchrotron.com_sed_emissivity`, in order to be
91 | propagated to :func:`~agnpy.synchrotron.Synchrotron.sed_luminosity` and
92 | :func:`~agnpy.synchrotron.Synchrotron.sed_flux`.
93 | """
94 |
95 | def __init__(self, blob, ssa=False):
96 | self.blob = blob
97 | self.epsilon_B = (self.blob.B / B_cr).to_value("")
98 | self.ssa = ssa
99 |
100 | def k_epsilon(self, epsilon):
101 | r"""SSA absorption factor Eq. 7.142 in [DermerMenon2009]_.
102 | The part of the integrand that is dependent on :math:`\gamma` is
103 | computed analytically in each of the :class:`~agnpy.spectra` classes."""
104 | gamma = self.blob.gamma
105 | SSA_integrand = self.blob.n_e.SSA_integrand(gamma)
106 | # for multidimensional integration
107 | # axis 0: electrons gamma
108 | # axis 1: photons epsilon
109 | # arrays starting with _ are multidimensional and used for integration
110 | _gamma = np.reshape(gamma, (gamma.size, 1))
111 | _SSA_integrand = np.reshape(SSA_integrand, (SSA_integrand.size, 1))
112 | _epsilon = np.reshape(epsilon, (1, epsilon.size))
113 | prefactor_P_syn = np.sqrt(3) * np.power(e, 3) * self.blob.B_cgs / h
114 | prefactor_k_epsilon = (
115 | -1 / (8 * np.pi * m_e * np.power(epsilon, 2)) * np.power(lambda_c / c, 3)
116 | )
117 | x_num = 4 * np.pi * _epsilon * np.power(m_e, 2) * np.power(c, 3)
118 | x_denom = 3 * e * self.blob.B_cgs * h * np.power(_gamma, 2)
119 | x = (x_num / x_denom).to_value("")
120 | integrand = R(x) * _SSA_integrand
121 | integral = np.trapz(integrand, gamma, axis=0)
122 | return (prefactor_P_syn * prefactor_k_epsilon * integral).to("cm-1")
123 |
124 | def tau_ssa(self, epsilon):
125 | """SSA opacity, Eq. before 7.122 in [DermerMenon2009]_
126 | since we will have formulas dividing by tau, avoid 0 or very small
127 | float values, replacing them with 1e-99"""
128 | tau = (2 * self.k_epsilon(epsilon) * self.blob.R_b).to_value("")
129 | tau[tau < 1e-99] = 1e-99
130 | return tau
131 |
132 | def attenuation_ssa(self, epsilon):
133 | """SSA attenuation, Eq. 7.122 in [DermerMenon2009]_"""
134 | tau = self.tau_ssa(epsilon)
135 | u = 1 / 2 + np.exp(-tau) / tau - (1 - np.exp(-tau)) / np.power(tau, 2)
136 | attenuation = 3 * u / tau
137 | condition = tau < 1e-3
138 | attenuation[condition] = 1
139 | return attenuation
140 |
141 | def com_sed_emissivity(self, epsilon):
142 | r"""Synchrotron emissivity:
143 |
144 | .. math::
145 | \epsilon'\,J'_{\mathrm{syn}}(\epsilon')\,[\mathrm{erg}\,\mathrm{s}^{-1}]
146 |
147 | Eq. 7.116 in [DermerMenon2009]_ or Eq. 18 in [Finke2008]_.
148 |
149 | The **SSA** is taken into account by this function and propagated
150 | to the other ones computing SEDs by invoking this one.
151 |
152 | **Note:** This emissivity is computed in the co-moving frame of the blob.
153 | When calling this function from another, these energies
154 | have to be transformed in the co-moving frame of the plasmoid.
155 |
156 | Parameters
157 | ----------
158 | epsilon : :class:`~numpy.ndarray`
159 | array of dimensionless energies (in electron rest mass units)
160 | to compute the sed, :math:`\epsilon = h \nu / (m_e c^2)`
161 |
162 | Returns
163 | -------
164 | :class:`~astropy.units.Quantity`
165 | array of the emissivity corresponding to each dimensionless energy
166 | """
167 | gamma = self.blob.gamma
168 | N_e = self.blob.N_e(gamma)
169 | prefactor = np.sqrt(3) * epsilon * np.power(e, 3) * self.blob.B_cgs / h
170 | # for multidimensional integration
171 | # axis 0: electrons gamma
172 | # axis 1: photons epsilon
173 | # arrays starting with _ are multidimensional and used for integration
174 | _gamma = np.reshape(gamma, (gamma.size, 1))
175 | _N_e = np.reshape(N_e, (N_e.size, 1))
176 | _epsilon = np.reshape(epsilon, (1, epsilon.size))
177 | x_num = 4 * np.pi * _epsilon * np.power(m_e, 2) * np.power(c, 3)
178 | x_denom = 3 * e * self.blob.B_cgs * h * np.power(_gamma, 2)
179 | x = (x_num / x_denom).to_value("")
180 | integrand = _N_e * R(x)
181 | integral = np.trapz(integrand, gamma, axis=0)
182 | emissivity = (prefactor * integral).to("erg s-1")
183 | if self.ssa:
184 | emissivity *= self.attenuation_ssa(epsilon)
185 | return emissivity.to("erg s-1")
186 |
187 | def sed_luminosity(self, nu):
188 | r"""Synchrotron luminosity SED:
189 |
190 | .. math::
191 | \nu L_{\nu} \, [\mathrm{erg}\,\mathrm{s}^{-1}]
192 |
193 | Parameters
194 | ----------
195 | nu : :class:`~astropy.units.Quantity`
196 | array of frequencies, in Hz, to compute the sed, **note** these are
197 | observed frequencies (observer frame).
198 |
199 | Returns
200 | -------
201 | :class:`~astropy.units.Quantity`
202 | array of the SED values corresponding to each frequency
203 | """
204 | epsilon = nu.to("", equivalencies=epsilon_equivalency)
205 | # correct epsilon to the jet comoving frame
206 | epsilon_prime = (1 + self.blob.z) * epsilon / self.blob.delta_D
207 | prefactor = np.power(self.blob.delta_D, 4)
208 | return prefactor * self.com_sed_emissivity(epsilon_prime)
209 |
210 | def sed_flux(self, nu):
211 | r"""Synchrotron flux SED:
212 |
213 | .. math::
214 | \nu F_{\nu} \, [\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1}]
215 |
216 | Eq. 21 in [Finke2008]_.
217 |
218 | Parameters
219 | ----------
220 | nu : :class:`~astropy.units.Quantity`
221 | array of frequencies, in Hz, to compute the sed, **note** these are
222 | observed frequencies (observer frame).
223 |
224 | Returns
225 | -------
226 | :class:`~astropy.units.Quantity`
227 | array of the SED values corresponding to each frequency
228 | """
229 | epsilon = nu.to("", equivalencies=epsilon_equivalency)
230 | # correct epsilon to the jet comoving frame
231 | epsilon_prime = (1 + self.blob.z) * epsilon / self.blob.delta_D
232 | prefactor = np.power(self.blob.delta_D, 4) / (
233 | 4 * np.pi * np.power(self.blob.d_L, 2)
234 | )
235 | sed = prefactor * self.com_sed_emissivity(epsilon_prime)
236 | return sed.to("erg cm-2 s-1")
237 |
238 | def sed_flux_delta_approx(self, nu):
239 | """synchrotron flux SED using the delta approximation for the synchrotron
240 | radiation Eq. 7.70 [DermerMenon2009]_"""
241 | epsilon = nu.to("", equivalencies=epsilon_equivalency)
242 | # correct epsilon to the jet comoving frame
243 | epsilon_prime = (1 + self.blob.z) * epsilon / self.blob.delta_D
244 | gamma_s = np.sqrt(epsilon_prime / epsilon_B(self.blob.B))
245 | prefactor = (
246 | np.power(self.blob.delta_D, 4)
247 | / (6 * np.pi * np.power(self.blob.d_L, 2))
248 | * c
249 | * sigma_T
250 | * self.blob.U_B
251 | )
252 | value = prefactor * np.power(gamma_s, 3) * self.blob.N_e(gamma_s)
253 | return value.to("erg cm-2 s-1")
254 |
255 | def sed_peak_flux(self, nu):
256 | """provided a grid of frequencies nu, returns the peak flux of the SED
257 | """
258 | return self.sed_flux(nu).max()
259 |
260 | def sed_peak_nu(self, nu):
261 | """provided a grid of frequencies nu, returns the frequency at which the SED peaks
262 | """
263 | idx_max = self.sed_flux(nu).argmax()
264 | return nu[idx_max]
265 |
--------------------------------------------------------------------------------
/agnpy/tests/test_compton.py:
--------------------------------------------------------------------------------
1 | # test on compton module
2 | import numpy as np
3 | import astropy.units as u
4 | from astropy.constants import m_e, c, M_sun
5 | from astropy.coordinates import Distance
6 | from agnpy.emission_regions import Blob
7 | from agnpy.synchrotron import Synchrotron
8 | from agnpy.targets import PointSourceBehindJet, SSDisk, SphericalShellBLR, RingDustTorus
9 | from agnpy.compton import SynchrotronSelfCompton, ExternalCompton
10 | import matplotlib.pyplot as plt
11 | from pathlib import Path
12 | import pytest
13 |
14 |
15 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
16 | tests_dir = Path(__file__).parent
17 |
18 |
19 | def make_sed_comparison_plot(nu, reference_sed, agnpy_sed, fig_title, fig_name):
20 | """make a SED comparison plot for visual inspection"""
21 | fig, ax = plt.subplots(
22 | 2, sharex=True, gridspec_kw={"height_ratios": [2, 1]}, figsize=(8, 6)
23 | )
24 | # plot the SEDs in the upper panel
25 | ax[0].loglog(nu, reference_sed, marker=".", ls="-", lw=1.5, label="reference")
26 | ax[0].loglog(nu, agnpy_sed, marker=".", ls="--", lw=1.5, label="agnpy")
27 | ax[0].legend()
28 | ax[0].set_xlabel(r"$\nu\,/\,{\rm Hz}$")
29 | ax[0].set_ylabel(r"$\nu F_{\nu}\,/\,({\rm erg}\,{\rm cm}^{-2}\,{\rm s}^{-1})$")
30 | ax[0].set_title(fig_title)
31 | # plot the deviation in the bottom panel
32 | deviation = 1 - agnpy_sed / reference_sed
33 | ax[1].semilogx(
34 | nu,
35 | deviation,
36 | lw=1.5,
37 | label=r"$|1 - \nu F_{\nu, \rm agnpy} \, / \,\nu F_{\nu, \rm reference}|$",
38 | )
39 | ax[1].legend(loc=2)
40 | ax[1].axhline(0, ls="-", lw=1.5, color="dimgray")
41 | ax[1].axhline(0.2, ls="--", lw=1.5, color="dimgray")
42 | ax[1].axhline(-0.2, ls="--", lw=1.5, color="dimgray")
43 | ax[1].axhline(0.3, ls=":", lw=1.5, color="dimgray")
44 | ax[1].axhline(-0.3, ls=":", lw=1.5, color="dimgray")
45 | ax[1].set_ylim([-0.5, 0.5])
46 | ax[1].set_xlabel(r"$\nu / Hz$")
47 | fig.savefig(f"{tests_dir}/crosscheck_figures/{fig_name}.png")
48 |
49 |
50 | # global PWL blob, same parameters of Figure 7.4 in Dermer Menon 2009
51 | PWL_SPECTRUM_NORM = 1e48 * u.Unit("erg")
52 | PWL_DICT = {
53 | "type": "PowerLaw",
54 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e5},
55 | }
56 | R_B_PWL = 1e16 * u.cm
57 | B_PWL = 1 * u.G
58 | Z_PWL = Distance(1e27, unit=u.cm).z
59 | DELTA_D_PWL = 10
60 | GAMMA_PWL = 10
61 | PWL_BLOB = Blob(
62 | R_B_PWL, Z_PWL, DELTA_D_PWL, GAMMA_PWL, B_PWL, PWL_SPECTRUM_NORM, PWL_DICT
63 | )
64 |
65 | # global BPL blob, same parameters of the examples in Finke 2016
66 | BPL_SPECTRUM_NORM = 6e42 * u.Unit("erg")
67 | BPL_DICT = {
68 | "type": "BrokenPowerLaw",
69 | "parameters": {
70 | "p1": 2.0,
71 | "p2": 3.5,
72 | "gamma_b": 1e4,
73 | "gamma_min": 20,
74 | "gamma_max": 5e7,
75 | },
76 | }
77 | R_B_BPL = 1e16 * u.cm
78 | B_BPL = 0.56 * u.G
79 | Z_BPL = 1
80 | DELTA_D_BPL = 40
81 | GAMMA_BPL = 40
82 | BPL_BLOB = Blob(
83 | R_B_BPL, Z_BPL, DELTA_D_BPL, GAMMA_BPL, B_BPL, BPL_SPECTRUM_NORM, BPL_DICT
84 | )
85 | BPL_BLOB.set_gamma_size(500)
86 |
87 |
88 | class TestSynchrotronSelfCompton:
89 | """class grouping all tests related to the Synchrotron Slef Compton class"""
90 |
91 | def test_ssc_reference_sed(self):
92 | """test agnpy SSC SED against the one in Figure 7.4 of Dermer Menon"""
93 | sampled_ssc_table = np.loadtxt(
94 | f"{tests_dir}/sampled_seds/ssc_figure_7_4_dermer_menon_2009.txt",
95 | delimiter=",",
96 | comments="#",
97 | )
98 | sampled_ssc_nu = sampled_ssc_table[:, 0] * u.Hz
99 | sampled_ssc_sed = sampled_ssc_table[:, 1] * u.Unit("erg cm-2 s-1")
100 | # agnpy
101 | synch = Synchrotron(PWL_BLOB)
102 | ssc = SynchrotronSelfCompton(PWL_BLOB, synch)
103 | # recompute the SED at the same ordinates where the figure was sampled
104 | agnpy_ssc_sed = ssc.sed_flux(sampled_ssc_nu)
105 | # sed comparison plot
106 | make_sed_comparison_plot(
107 | sampled_ssc_nu,
108 | sampled_ssc_sed,
109 | agnpy_ssc_sed,
110 | "Synchrotron Self Compton",
111 | "ssc_comparison_figure_7_4_dermer_menon_2009",
112 | )
113 | # requires that the SED points deviate less than 15% from the figure
114 | assert u.allclose(
115 | agnpy_ssc_sed, sampled_ssc_sed, atol=0 * u.Unit("erg cm-2 s-1"), rtol=0.15
116 | )
117 |
118 |
119 | class TestExternalCompton:
120 | """class grouping all tests related to the Synchrotron Slef Compton class"""
121 |
122 | def test_ec_disk_reference_sed(self):
123 | """test agnpy SED for EC on Disk against the one in Figure 8 of Finke 2016"""
124 | # reference SED
125 | sampled_ec_disk_table = np.loadtxt(
126 | f"{tests_dir}/sampled_seds/ec_disk_figure_8_finke_2016.txt",
127 | delimiter=",",
128 | comments="#",
129 | )
130 | sampled_ec_disk_nu = sampled_ec_disk_table[:, 0] * u.Hz
131 | sampled_ec_disk_sed = sampled_ec_disk_table[:, 1] * u.Unit("erg cm-2 s-1")
132 | # agnpy SED
133 | M_BH = 1.2 * 1e9 * M_sun.cgs
134 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
135 | eta = 1 / 12
136 | R_in = 6
137 | R_out = 200
138 | disk = SSDisk(M_BH, L_disk, eta, R_in, R_out, R_g_units=True)
139 | # recompute the SED at the same ordinates where the figure was sampled
140 | ec_disk = ExternalCompton(BPL_BLOB, disk, r=1e17 * u.cm)
141 | agnpy_ec_disk_sed = ec_disk.sed_flux(sampled_ec_disk_nu)
142 | # sed comparison plot
143 | make_sed_comparison_plot(
144 | sampled_ec_disk_nu,
145 | sampled_ec_disk_sed,
146 | agnpy_ec_disk_sed,
147 | "External Compton on Shakura Sunyaev Disk",
148 | "ec_disk_comparison_figure_8_finke_2016",
149 | )
150 | # requires that the SED points deviate less than 40% from the figure
151 | assert u.allclose(
152 | agnpy_ec_disk_sed,
153 | sampled_ec_disk_sed,
154 | atol=0 * u.Unit("erg cm-2 s-1"),
155 | rtol=0.4,
156 | )
157 |
158 | def test_ec_blr_reference_sed(self):
159 | """test agnpy SED for EC on BLR against the one in Figure 10 of Finke 2016"""
160 | # reference SED
161 | sampled_ec_blr_table = np.loadtxt(
162 | f"{tests_dir}/sampled_seds/ec_blr_figure_10_finke_2016.txt",
163 | delimiter=",",
164 | comments="#",
165 | )
166 | sampled_ec_blr_nu = sampled_ec_blr_table[:, 0] * u.Hz
167 | sampled_ec_blr_sed = sampled_ec_blr_table[:, 1] * u.Unit("erg cm-2 s-1")
168 | # agnpy SED
169 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
170 | xi_line = 0.024
171 | R_line = 1e17 * u.cm
172 | blr = SphericalShellBLR(L_disk, xi_line, "Lyalpha", R_line)
173 | # recompute the SED at the same ordinates where the figure was sampled
174 | ec_blr = ExternalCompton(BPL_BLOB, blr, r=1e18 * u.cm)
175 | agnpy_ec_blr_sed = ec_blr.sed_flux(sampled_ec_blr_nu)
176 | # sed comparison plot
177 | make_sed_comparison_plot(
178 | sampled_ec_blr_nu,
179 | sampled_ec_blr_sed,
180 | agnpy_ec_blr_sed,
181 | "External Compton on Spherical Shell Broad Line Region",
182 | "ec_blr_comparison_figure_10_finke_2016",
183 | )
184 | # requires that the SED points deviate less than 30% from the figure
185 | assert u.allclose(
186 | agnpy_ec_blr_sed,
187 | sampled_ec_blr_sed,
188 | atol=0 * u.Unit("erg cm-2 s-1"),
189 | rtol=0.3,
190 | )
191 |
192 | def test_ec_dt_reference_sed(self):
193 | """test agnpy SED for EC on DT against the one in Figure 11 of Finke 2016"""
194 | # reference SED
195 | sampled_ec_dt_table = np.loadtxt(
196 | f"{tests_dir}/sampled_seds/ec_dt_figure_11_finke_2016.txt",
197 | delimiter=",",
198 | comments="#",
199 | )
200 | sampled_ec_dt_nu = sampled_ec_dt_table[:, 0] * u.Hz
201 | # multiply the reference SED for 2 as this is the missing factor
202 | # in the emissivity expression in Eq. 90 of Finke 2016
203 | sampled_ec_dt_sed = 2 * sampled_ec_dt_table[:, 1] * u.Unit("erg cm-2 s-1")
204 | # agnpy SED
205 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
206 | T_dt = 1e3 * u.K
207 | csi_dt = 0.1
208 | dt = RingDustTorus(L_disk, csi_dt, T_dt)
209 | # recompute the SED at the same ordinates where the figure was sampled
210 | ec_dt = ExternalCompton(BPL_BLOB, dt, r=1e20 * u.cm)
211 | agnpy_ec_dt_sed = ec_dt.sed_flux(sampled_ec_dt_nu)
212 | # sed comparison plot
213 | make_sed_comparison_plot(
214 | sampled_ec_dt_nu,
215 | sampled_ec_dt_sed,
216 | agnpy_ec_dt_sed,
217 | "External Compton on Ring Dust Torus",
218 | "ec_dt_comparison_figure_11_finke_2016",
219 | )
220 | # requires that the SED points deviate less than 30% from the figure
221 | assert u.allclose(
222 | agnpy_ec_dt_sed,
223 | sampled_ec_dt_sed,
224 | atol=0 * u.Unit("erg cm-2 s-1"),
225 | rtol=0.3,
226 | )
227 |
228 | def test_ec_blr_vs_point_source(self):
229 | """check if in the limit of large distances the EC on the BLR tends to
230 | the one of a point-like source approximating it"""
231 | # broad line region
232 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
233 | xi_line = 0.024
234 | R_line = 1e17 * u.cm
235 | blr = SphericalShellBLR(L_disk, xi_line, "Lyalpha", R_line)
236 | # point like source approximating the blr
237 | ps_blr = PointSourceBehindJet(blr.xi_line * L_disk, blr.epsilon_line)
238 | # external Compton
239 | ec_blr = ExternalCompton(BPL_BLOB, blr, r=1e22 * u.cm)
240 | ec_ps_blr = ExternalCompton(BPL_BLOB, ps_blr, r=1e22 * u.cm)
241 | # seds
242 | nu = np.logspace(15, 28) * u.Hz
243 | ec_blr_sed = ec_blr.sed_flux(nu)
244 | ec_ps_blr_sed = ec_ps_blr.sed_flux(nu)
245 | # requires a 20% deviation from the two SED points
246 | assert u.allclose(
247 | ec_blr_sed, ec_ps_blr_sed, atol=0 * u.Unit("erg cm-2 s-1"), rtol=0.2
248 | )
249 |
250 | def test_ec_dt_vs_point_source(self):
251 | """check if in the limit of large distances the EC on the DT tends to
252 | the one of a point-like source approximating it"""
253 | # dust torus
254 | L_disk = 2 * 1e46 * u.Unit("erg s-1")
255 | T_dt = 1e3 * u.K
256 | csi_dt = 0.1
257 | dt = RingDustTorus(L_disk, csi_dt, T_dt)
258 | # point like source approximating the dt
259 | ps_dt = PointSourceBehindJet(dt.xi_dt * L_disk, dt.epsilon_dt)
260 | # external Compton
261 | ec_dt = ExternalCompton(BPL_BLOB, dt, r=1e22 * u.cm)
262 | ec_ps_dt = ExternalCompton(BPL_BLOB, ps_dt, r=1e22 * u.cm)
263 | # seds
264 | nu = np.logspace(15, 28) * u.Hz
265 | ec_dt_sed = ec_dt.sed_flux(nu)
266 | ec_ps_dt_sed = ec_ps_dt.sed_flux(nu)
267 | # requires a 20% deviation from the two SED points
268 | assert u.allclose(
269 | ec_dt_sed, ec_ps_dt_sed, atol=0 * u.Unit("erg cm-2 s-1"), rtol=0.2
270 | )
271 |
--------------------------------------------------------------------------------
/agnpy/tests/test_targets.py:
--------------------------------------------------------------------------------
1 | # tests on targets module
2 | import numpy as np
3 | import astropy.units as u
4 | from astropy.coordinates import Distance
5 | from astropy.constants import e, c, m_e, M_sun, G, sigma_sb
6 | from agnpy.emission_regions import Blob
7 | from agnpy.targets import (
8 | CMB,
9 | PointSourceBehindJet,
10 | SSDisk,
11 | SphericalShellBLR,
12 | RingDustTorus,
13 | )
14 | import pytest
15 |
16 | # global PWL blob, used to compute energy densities of targets
17 | # in the reference frame comoving with it
18 | SPECTRUM_NORM = 1e48 * u.Unit("erg")
19 | PWL_DICT = {
20 | "type": "PowerLaw",
21 | "parameters": {"p": 2.8, "gamma_min": 1e2, "gamma_max": 1e5},
22 | }
23 | R_B = 1e16 * u.cm
24 | B = 1 * u.G
25 | Z = Distance(1e27, unit=u.cm).z
26 | DELTA_D = 10
27 | GAMMA = 10
28 | PWL_BLOB = Blob(R_B, Z, DELTA_D, GAMMA, B, SPECTRUM_NORM, PWL_DICT)
29 |
30 | # global CMB at z = 1
31 | CMB_Z_1 = CMB(z=1)
32 |
33 | # global PointSourceBehindJet
34 | PS = PointSourceBehindJet(1e46 * u.Unit("erg s-1"), 1e-5)
35 |
36 | # global disk
37 | M_BH = 1.2 * 1e9 * M_sun
38 | L_DISK = 1.512 * 1e46 * u.Unit("erg s-1")
39 | ETA = 1 / 12
40 | R_G = 1.77 * 1e14 * u.cm
41 | R_IN_G_UNITS = 6
42 | R_OUT_G_UNITS = 200
43 | R_IN = R_IN_G_UNITS * R_G
44 | R_OUT = R_OUT_G_UNITS * R_G
45 | DISK = SSDisk(M_BH, L_DISK, ETA, R_IN, R_OUT)
46 | # useful for checks
47 | L_EDD = 15.12 * 1e46 * u.Unit("erg s-1")
48 | M_DOT = 2.019 * 1e26 * u.Unit("g s-1")
49 |
50 | # global SphericalShellBLR
51 | BLR = SphericalShellBLR(L_DISK, 0.1, "Lyalpha", 1e17 * u.cm)
52 |
53 | # dust torus definition
54 | DT = RingDustTorus(L_DISK, 0.1, 1000 * u.K)
55 |
56 |
57 | class TestCMB:
58 | """class grouping all the tests related to the CMB"""
59 |
60 | def test_u(self):
61 | """test u in the stationary reference frame"""
62 | assert u.isclose(
63 | 6.67945605e-12 * u.Unit("erg / cm3"),
64 | CMB_Z_1.u(),
65 | atol=0 * u.Unit("erg / cm3"),
66 | rtol=1e-3,
67 | )
68 |
69 | def test_u_comoving(self):
70 | """test u in the reference frame comoving with the blob"""
71 | assert u.isclose(
72 | 8.88373221e-10 * u.Unit("erg / cm3"),
73 | CMB_Z_1.u(PWL_BLOB),
74 | atol=0 * u.Unit("erg / cm3"),
75 | rtol=1e-3,
76 | )
77 |
78 |
79 | class TestPointSourceBehindJet:
80 | """class grouping all the tests related to the PointSourceBehindJet"""
81 |
82 | def test_u(self):
83 | """test u in the stationary reference frame"""
84 | r = np.asarray([1e18, 1e19, 1e20]) * u.cm
85 | assert u.allclose(
86 | [2.65441873e-02, 2.65441873e-04, 2.65441873e-06] * u.Unit("erg / cm3"),
87 | PS.u(r),
88 | atol=0 * u.Unit("erg / cm3"),
89 | rtol=1e-3,
90 | )
91 |
92 | def test_u_comoving(self):
93 | """test u in the reference frame comoving with the blob"""
94 | r = np.asarray([1e18, 1e19, 1e20]) * u.cm
95 | assert u.allclose(
96 | [6.6693519e-05, 6.6693519e-07, 6.6693519e-09] * u.Unit("erg / cm3"),
97 | PS.u(r, PWL_BLOB),
98 | atol=0 * u.Unit("erg / cm3"),
99 | rtol=1e-3,
100 | )
101 |
102 |
103 | class TestSSDisk:
104 | """class grouping all the tests related to the SSDisk target"""
105 |
106 | # global quantities defining the test disk
107 | def test_L_Edd(self):
108 | assert u.isclose(DISK.L_Edd, L_EDD, atol=0 * u.Unit("erg s-1"), rtol=1e-3)
109 |
110 | def test_l_Edd(self):
111 | assert u.isclose(DISK.l_Edd, 0.1, atol=0, rtol=1e-3)
112 |
113 | def test_m_dot(self):
114 | assert u.isclose(DISK.m_dot, M_DOT, atol=0 * u.Unit("g s-1"), rtol=1e-3)
115 |
116 | @pytest.mark.parametrize(
117 | "R_in, R_out, R_g_units",
118 | [(R_IN_G_UNITS, R_OUT_G_UNITS, False), (R_IN, R_OUT, True),],
119 | )
120 | def test_R_in_R_out_units(self, R_in, R_out, R_g_units):
121 | """check if a TypeError is raised when passing R_in and R_out with
122 | (without) units but specifiying R_g_units True (False)"""
123 | with pytest.raises(TypeError):
124 | disk = SSDisk(M_BH, L_DISK, ETA, R_in, R_out, R_g_units)
125 |
126 | def test_R_g(self):
127 | assert u.isclose(DISK.R_g, R_G, atol=0 * u.cm, rtol=1e-2)
128 |
129 | def test_mu_from_r_tilde(self):
130 | mu = DISK.mu_from_r_tilde(10)
131 | mu_min_expected = 0.050
132 | mu_max_expected = 0.858
133 | assert np.isclose(mu[0], mu_min_expected, atol=0, rtol=1e-3)
134 | assert np.isclose(mu[-1], mu_max_expected, atol=0, rtol=1e-3)
135 |
136 | def test_phi_disk(self):
137 | R_tilde = 10
138 | phi_expected = 0.225
139 | assert np.isclose(DISK.phi_disk(R_tilde), phi_expected, atol=0, rtol=1e-2)
140 |
141 | def test_phi_disk_mu(self):
142 | r_tilde = 10
143 | # assume R_tilde = 10 as before
144 | mu = 1 / np.sqrt(2)
145 | phi_expected = 0.225
146 | assert np.allclose(
147 | DISK.phi_disk_mu(mu, r_tilde), phi_expected, atol=0, rtol=1e-2
148 | )
149 |
150 | def test_epsilon(self):
151 | R_tilde = 10
152 | epsilon_expected = 2.7e-5
153 | assert np.allclose(DISK.epsilon(R_tilde), epsilon_expected, atol=0, rtol=1e-2)
154 |
155 | def test_epsilon_mu(self):
156 | r_tilde = 10
157 | # assume R_tilde = 10 as before
158 | mu = 1 / np.sqrt(2)
159 | epsilon_expected = 2.7e-5
160 | assert np.allclose(
161 | DISK.epsilon_mu(mu, r_tilde), epsilon_expected, atol=0, rtol=1e-2
162 | )
163 |
164 | def test_T(self):
165 | R_tilde = 10
166 | R = 10 * R_G
167 | phi_expected = 0.225
168 | # Eq. 64 [Dermer2009]
169 | T_expected = np.power(
170 | 3 * G * M_BH * M_DOT / (8 * np.pi * np.power(R, 3) * sigma_sb), 1 / 4
171 | ).to("K")
172 | assert u.isclose(DISK.T(R_tilde), T_expected, atol=0 * u.K, rtol=1e-2)
173 |
174 | def test_Theta(R_tilde):
175 | R_tilde = 10
176 | epsilon = DISK.epsilon(R_tilde)
177 | assert np.isclose(epsilon, 2.7 * DISK.Theta(R_tilde), atol=0, rtol=1e-2)
178 |
179 |
180 | class TestSphericalShellBLR:
181 | """class grouping all the tests related to the SphericalShellBLR target"""
182 |
183 | @pytest.mark.parametrize(
184 | "line, lambda_line",
185 | [
186 | ("Lyalpha", 1215.67 * u.Angstrom),
187 | ("Lybeta", 1025.72 * u.Angstrom),
188 | ("Halpha", 6564.61 * u.Angstrom),
189 | ("Hbeta", 4862.68 * u.Angstrom),
190 | ],
191 | )
192 | def test_line_dict(self, line, lambda_line):
193 | """test correct loading of some of the emission line"""
194 | blr = SphericalShellBLR(1e46 * u.Unit("erg s-1"), 0.1, line, 1e17)
195 | assert u.isclose(blr.lambda_line, lambda_line, atol=0 * u.Angstrom)
196 |
197 | def test_u(self):
198 | """test u in the stationary reference frame"""
199 | r = np.logspace(16, 20, 10) * u.cm
200 | assert u.allclose(
201 | [
202 | 4.02698710e-01,
203 | 4.12267268e-01,
204 | 5.49297935e-01,
205 | 9.36951182e-02,
206 | 1.12734943e-02,
207 | 1.44410780e-03,
208 | 1.86318218e-04,
209 | 2.40606696e-05,
210 | 3.10750072e-06,
211 | 4.01348246e-07,
212 | ]
213 | * u.Unit("erg / cm3"),
214 | BLR.u(r),
215 | atol=0 * u.Unit("erg / cm3"),
216 | )
217 |
218 | def test_u_blr_vs_point_source(self):
219 | """test that for large enough distances the energy density of the
220 | BLR tends to the one of a point like source approximating it"""
221 | # point source with the same luminosity as the BLR
222 | ps_blr = PointSourceBehindJet(BLR.xi_line * BLR.L_disk, BLR.epsilon_line)
223 | # r >> R_line
224 | r = np.logspace(19, 23, 10) * u.cm
225 | assert u.allclose(BLR.u(r), ps_blr.u(r), atol=0 * u.Unit("erg cm-3"), rtol=1e-2)
226 |
227 | def test_u_comoving(self):
228 | """test u in the reference frame comoving with the blob"""
229 | r = np.logspace(16, 20, 10) * u.cm
230 | assert u.allclose(
231 | [
232 | 1.35145224e01,
233 | 1.39362806e01,
234 | 1.98574615e01,
235 | 7.22733332e-02,
236 | 2.50276530e-04,
237 | 5.60160671e-06,
238 | 4.97415343e-07,
239 | 6.09348663e-08,
240 | 7.81583912e-09,
241 | 1.00855244e-09,
242 | ]
243 | * u.Unit("erg / cm3"),
244 | BLR.u(r, PWL_BLOB),
245 | atol=0 * u.Unit("erg / cm3"),
246 | )
247 |
248 | def test_u_blr_vs_point_source_comoving(self):
249 | """test that for large enough distances the energy density of the
250 | BLR tends to the one of a point like source approximating it"""
251 | # point source with the same luminosity as the BLR
252 | ps_blr = PointSourceBehindJet(BLR.xi_line * BLR.L_disk, BLR.epsilon_line)
253 | # r >> R_line
254 | r = np.logspace(19, 23, 10) * u.cm
255 | assert u.allclose(
256 | BLR.u(r, PWL_BLOB),
257 | ps_blr.u(r, PWL_BLOB),
258 | atol=0 * u.Unit("erg cm-3"),
259 | rtol=1e-1,
260 | )
261 |
262 |
263 | class TestRingDustTorus:
264 | """class grouping all the tests related to the RingDustTorus target"""
265 |
266 | def test_sublimation_radius(self):
267 | assert u.allclose(DT.R_dt, 1.361 * 1e19 * u.cm, atol=0 * u.cm, rtol=1e-3)
268 |
269 | def test_setting_radius(self):
270 | """check that, when passed manually, the radius is correctly set"""
271 | dt = RingDustTorus(L_DISK, 0.1, 1e3 * u.K, 1e19 * u.cm)
272 | assert u.allclose(dt.R_dt, 1e19 * u.cm, atol=0 * u.cm)
273 |
274 | def test_u(self):
275 | """test u in the stationary reference frame"""
276 | r = np.logspace(17, 23, 10) * u.cm
277 | assert u.allclose(
278 | [
279 | 2.16675545e-05,
280 | 2.16435491e-05,
281 | 2.11389842e-05,
282 | 1.40715277e-05,
283 | 1.71541601e-06,
284 | 8.61241559e-08,
285 | 4.01273788e-09,
286 | 1.86287690e-10,
287 | 8.64677950e-12,
288 | 4.01348104e-13,
289 | ]
290 | * u.Unit("erg / cm3"),
291 | DT.u(r),
292 | atol=0 * u.Unit("erg / cm3"),
293 | )
294 |
295 | def test_u_dt_vs_point_source(self):
296 | """test that in the stationary reference frame, for large enough
297 | distances, the energy density of the DT tends to the one of a point like
298 | source approximating it"""
299 | # point source with the same luminosity as the DT
300 | ps_dt = PointSourceBehindJet(DT.xi_dt * DT.L_disk, DT.epsilon_dt)
301 | # r >> R_dt
302 | r = np.logspace(21, 24, 10) * u.cm
303 | assert u.allclose(DT.u(r), ps_dt.u(r), atol=0 * u.Unit("erg cm-3"), rtol=1e-2)
304 |
305 | def test_u_comoving(self):
306 | """test u in the reference frame comoving with the blob"""
307 | r = np.logspace(17, 23, 10) * u.cm
308 | assert u.allclose(
309 | [
310 | 2.13519004e-03,
311 | 2.02003750e-03,
312 | 1.50733234e-03,
313 | 2.37521472e-04,
314 | 3.50603715e-07,
315 | 4.21027697e-10,
316 | 1.04563603e-11,
317 | 4.68861573e-13,
318 | 2.17274347e-14,
319 | 1.00842240e-15,
320 | ]
321 | * u.Unit("erg / cm3"),
322 | DT.u(r, PWL_BLOB),
323 | atol=0 * u.Unit("erg / cm3"),
324 | )
325 |
326 | def test_u_dt_vs_point_source_comoving(self):
327 | """test that in the reference frame comoving with the Blob, for large
328 | enough distances, the energy density of the DT tends to the one of
329 | a point like source approximating it"""
330 | # point source with the same luminosity as the DT
331 | ps_dt = PointSourceBehindJet(DT.xi_dt * DT.L_disk, DT.epsilon_dt)
332 | # r >> R_line
333 | r = np.logspace(21, 24, 10) * u.cm
334 | assert u.allclose(
335 | DT.u(r, PWL_BLOB),
336 | ps_dt.u(r, PWL_BLOB),
337 | atol=0 * u.Unit("erg cm-3"),
338 | rtol=1e-1,
339 | )
340 |
--------------------------------------------------------------------------------
/agnpy/spectra.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import astropy.units as u
3 | from astropy.constants import m_e
4 |
5 |
6 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
7 |
8 |
9 | __all__ = ["PowerLaw", "BrokenPowerLaw", "BrokenPowerLaw2"]
10 |
11 |
12 | # the following functions describe the dependency on the Lorentz factor
13 | # of the electron distributions, they do not depend on units
14 | def _power_law(gamma, p, gamma_min, gamma_max):
15 | """simple power law"""
16 | pwl = np.power(gamma, -p)
17 | null_condition = (gamma_min <= gamma) * (gamma <= gamma_max)
18 | pwl[~null_condition] = 0
19 | return pwl
20 |
21 |
22 | def _power_law_integral(p, gamma_min, gamma_max):
23 | """analytical integral of the simple power law"""
24 | if np.isclose(p, 1.0):
25 | return np.log(gamma_max / gamma_min)
26 | else:
27 | return (np.power(gamma_max, 1 - p) - np.power(gamma_min, 1 - p)) / (1 - p)
28 |
29 |
30 | def _power_law_times_gamma_integral(p, gamma_min, gamma_max):
31 | """analytical integral of the simple power law multiplied by gamma"""
32 | if np.isclose(p, 2.0):
33 | return np.log(gamma_max / gamma_min)
34 | else:
35 | return (np.power(gamma_max, 2 - p) - np.power(gamma_min, 2 - p)) / (2 - p)
36 |
37 |
38 | def _power_law_ssa_integrand(gamma, p, gamma_min, gamma_max):
39 | """analytical form of the SSA integrand"""
40 | pwl = np.power(gamma, -p - 1)
41 | null_condition = (gamma_min <= gamma) * (gamma <= gamma_max)
42 | pwl[~null_condition] = 0
43 | return (-p - 2) * pwl
44 |
45 |
46 | def _broken_power_law(gamma, p1, p2, gamma_b, gamma_min, gamma_max):
47 | """power law with two spectral indexes"""
48 | pwl = np.power(gamma / gamma_b, -p1)
49 | # compute power law with the second spectral index
50 | p2_condition = gamma > gamma_b
51 | pwl[p2_condition] = np.power(gamma[p2_condition] / gamma_b, -p2)
52 | # return zero outside minimum and maximum Lorentz factor values
53 | null_condition = (gamma_min <= gamma) * (gamma <= gamma_max)
54 | pwl[~null_condition] = 0
55 | return pwl
56 |
57 |
58 | def _broken_power_law_integral(p1, p2, gamma_b, gamma_min, gamma_max):
59 | """analytical integral of the power law with two spectral indexes"""
60 | if np.allclose(p1, 1.0):
61 | term_1 = gamma_b * np.log(gamma_b / gamma_min)
62 | else:
63 | term_1 = gamma_b * (1 - np.power(gamma_min / gamma_b, 1 - p1)) / (1 - p1)
64 | if np.allclose(p2, 1.0):
65 | term_2 = gamma_b * np.log(gamma_max / gamma_b)
66 | else:
67 | term_2 = gamma_b * (np.power(gamma_max / gamma_b, 1 - p2) - 1) / (1 - p2)
68 | return term_1 + term_2
69 |
70 |
71 | def _broken_power_law_times_gamma_integral(p1, p2, gamma_b, gamma_min, gamma_max):
72 | """analytical integral of the power law with two spectral indexes multiplied
73 | by gamma"""
74 | if np.allclose(p1, 2.0):
75 | term_1 = np.power(gamma_b, 2) * np.log(gamma_b / gamma_min)
76 | else:
77 | term_1 = (
78 | np.power(gamma_b, 2)
79 | * (1 - np.power(gamma_min / gamma_b, 2 - p1))
80 | / (2 - p1)
81 | )
82 | if np.allclose(p2, 2.0):
83 | term_2 = np.power(gamma_b, 2) * np.log(gamma_max / gamma_b)
84 | else:
85 | term_2 = (
86 | np.power(gamma_b, 2)
87 | * (np.power(gamma_max / gamma_b, 2 - p2) - 1)
88 | / (2 - p2)
89 | )
90 | return term_1 + term_2
91 |
92 |
93 | def _broken_power_law_ssa_integrand(gamma, p1, p2, gamma_b, gamma_min, gamma_max):
94 | """analytical form of the SSA integrand"""
95 | pwl = np.power(gamma / gamma_b, -p1 - 1)
96 | pwl_prefactor = (-p1 - 2) / gamma
97 | # compute power law with the second spectral index
98 | p2_condition = gamma > gamma_b
99 | pwl[p2_condition] = np.power(gamma[p2_condition] / gamma_b, -p2 - 1)
100 | pwl_prefactor[p2_condition] = (-p2 - 2) / gamma[p2_condition]
101 | # return zero outside minimum and maximum Lorentz factor values
102 | null_condition = (gamma_min <= gamma) * (gamma <= gamma_max)
103 | pwl[~null_condition] = 0
104 | return pwl_prefactor * pwl
105 |
106 |
107 | def _broken_power_law_2(gamma, p1, p2, gamma_b, gamma_min, gamma_max):
108 | """Tavecchio's Broken Power Law
109 | https://ui.adsabs.harvard.edu/abs/1998ApJ...509..608T/abstract"""
110 | pwl = np.power(gamma, -p1)
111 | p2_condition = gamma > gamma_b
112 | pwl[p2_condition] = np.power(gamma_b, p2 - p1) * np.power(gamma[p2_condition], -p2)
113 | # return zero outside minimum and maximum Lorentz factor values
114 | null_condition = (gamma_min <= gamma) * (gamma <= gamma_max)
115 | pwl[~null_condition] = 0
116 | return pwl
117 |
118 |
119 | def _broken_power_law_2_integral(p1, p2, gamma_b, gamma_min, gamma_max):
120 | """analytical integral of Tavecchio's broken power law"""
121 | term_1 = _power_law_integral(p1, gamma_min, gamma_b)
122 | term_2 = np.power(gamma_b, p2 - p1) * _power_law_integral(p2, gamma_b, gamma_max)
123 | return term_1 + term_2
124 |
125 |
126 | def _broken_power_law_2_times_gamma_integral(p1, p2, gamma_b, gamma_min, gamma_max):
127 | """analytical integral of Tavecchio's broken power law multiplied by gamma"""
128 | term_1 = _power_law_times_gamma_integral(p1, gamma_min, gamma_b)
129 | term_2 = np.power(gamma_b, p2 - p1) * _power_law_times_gamma_integral(
130 | p2, gamma_b, gamma_max
131 | )
132 | return term_1 + term_2
133 |
134 |
135 | def _broken_power_law_2_ssa_integrand(gamma, p1, p2, gamma_b, gamma_min, gamma_max):
136 | """analytical form of the SSA integrand"""
137 | pwl = (-p1 - 2) * np.power(gamma, -p1 - 1)
138 | # compute power law with the second spectral index
139 | p2_condition = gamma > gamma_b
140 | pwl[p2_condition] = (
141 | (-p2 - 2) * np.power(gamma_b, p2 - p1) * np.power(gamma[p2_condition], -p2 - 1)
142 | )
143 | # return zero outside minimum and maximum Lorentz factor values
144 | null_condition = (gamma_min <= gamma) * (gamma <= gamma_max)
145 | pwl[~null_condition] = 0
146 | return pwl
147 |
148 |
149 | class PowerLaw:
150 | r"""Class for power-law particle spectrum.
151 | When called, the particle density :math:`n_e(\gamma)` in :math:`\mathrm{cm}^{-3}` is returned.
152 |
153 | .. math::
154 | n_e(\gamma') = k_e \, \gamma'^{-p} \, H(\gamma'; \gamma'_{\rm min}, \gamma'_{\rm max})
155 |
156 | Parameters
157 | ----------
158 | k_e : :class:`~astropy.units.Quantity`
159 | spectral normalisation
160 | p : float
161 | spectral index, note it is positive by definition, will change sign in the function
162 | gamma_min : float
163 | minimum Lorentz factor of the electron distribution
164 | gamma_max : float
165 | maximum Lorentz factor of the electron distribution
166 | """
167 |
168 | def __init__(self, k_e=1e-13 * u.Unit("cm-3"), p=2.0, gamma_min=10, gamma_max=1e5):
169 | self.k_e = k_e
170 | self.p = p
171 | self.gamma_min = gamma_min
172 | self.gamma_max = gamma_max
173 |
174 | def __call__(self, gamma):
175 | return self.k_e * _power_law(gamma, self.p, self.gamma_min, self.gamma_max)
176 |
177 | def __str__(self):
178 | return (
179 | f"* electron spectrum\n"
180 | + f" - power law\n"
181 | + f" - k_e: {self.k_e:.2e}\n"
182 | + f" - p: {self.p:.2f}\n"
183 | + f" - gamma_min: {self.gamma_min:.2e}\n"
184 | + f" - gamma_max: {self.gamma_max:.2e}\n"
185 | )
186 |
187 | @classmethod
188 | def from_normalised_density(cls, n_e_tot, p, gamma_min, gamma_max):
189 | r"""sets the normalisation :math:`k_e` from the total particle density
190 | :math:`n_{e,\,tot}`"""
191 | k_e = n_e_tot / _power_law_integral(p, gamma_min, gamma_max)
192 | return cls(k_e.to("cm-3"), p, gamma_min, gamma_max)
193 |
194 | @classmethod
195 | def from_normalised_u_e(cls, u_e, p, gamma_min, gamma_max):
196 | r"""sets the normalisation :math:`k_e` from the total energy density
197 | :math:`u_e`, Eq. 6.64 in [DermerMenon2009]_"""
198 | k_e = u_e / (mec2 * _power_law_times_gamma_integral(p, gamma_min, gamma_max))
199 | return cls(k_e.to("cm-3"), p, gamma_min, gamma_max)
200 |
201 | @classmethod
202 | def from_norm_at_gamma_1(cls, norm, p, gamma_min, gamma_max):
203 | r"""sets :math:`k_e` such that `norm` = :math:`n_e(\gamma=1)`."""
204 | return cls(norm.to("cm-3"), p, gamma_min, gamma_max)
205 |
206 | def SSA_integrand(self, gamma):
207 | r"""(analytical) integrand for the synchrotron self-absorption:
208 | :math:`\gamma'^2 \frac{d}{d \gamma'} \left(\frac{n_e(\gamma)}{\gamma'^2}\right)`"""
209 | return self.k_e * _power_law_ssa_integrand(
210 | gamma, self.p, self.gamma_min, self.gamma_max
211 | )
212 |
213 |
214 | class BrokenPowerLaw:
215 | r"""Class for broken power-law particle spectrum.
216 | When called, the particle density :math:`n_e(\gamma)` in :math:`\mathrm{cm}^{-3}` is returned.
217 |
218 | .. math::
219 | n_e(\gamma') = k_e \left[
220 | \left(\frac{\gamma'}{\gamma'_b}\right)^{-p_1} \, H(\gamma'; \gamma'_{\rm min}, \gamma'_b) +
221 | \left(\frac{\gamma'}{\gamma'_b}\right)^{-p_2} \, H(\gamma'; \gamma'_{b}, \gamma'_{\rm max})
222 | \right]
223 |
224 | Parameters
225 | ----------
226 | k_e : :class:`~astropy.units.Quantity`
227 | spectral normalisation
228 | p1 : float
229 | spectral index before the break (positive by definition)
230 | p2 : float
231 | spectral index after the break (positive by definition)
232 | gamma_b : float
233 | Lorentz factor at which the change in spectral index is occurring
234 | gamma_min : float
235 | minimum Lorentz factor of the electron distribution
236 | gamma_max : float
237 | maximum Lorentz factor of the electron distribution
238 | """
239 |
240 | def __init__(
241 | self,
242 | k_e=1e-13 * u.Unit("cm-3"),
243 | p1=2.0,
244 | p2=3.0,
245 | gamma_b=1e3,
246 | gamma_min=10,
247 | gamma_max=1e5,
248 | ):
249 | self.k_e = k_e
250 | self.p1 = p1
251 | self.p2 = p2
252 | self.gamma_b = gamma_b
253 | self.gamma_min = gamma_min
254 | self.gamma_max = gamma_max
255 |
256 | def __call__(self, gamma):
257 | return self.k_e * _broken_power_law(
258 | gamma, self.p1, self.p2, self.gamma_b, self.gamma_min, self.gamma_max,
259 | )
260 |
261 | def __str__(self):
262 | return (
263 | f"* electron spectrum\n"
264 | + f" - broken power law\n"
265 | + f" - k_e: {self.k_e:.2e}\n"
266 | + f" - p1: {self.p1:.2f}\n"
267 | + f" - p2: {self.p2:.2f}\n"
268 | + f" - gamma_b: {self.gamma_b:.2e}\n"
269 | + f" - gamma_min: {self.gamma_min:.2e}\n"
270 | + f" - gamma_max: {self.gamma_max:.2e}\n"
271 | )
272 |
273 | @classmethod
274 | def from_normalised_density(cls, n_e_tot, p1, p2, gamma_b, gamma_min, gamma_max):
275 | r"""sets the normalisation :math:`k_e` from the total particle density
276 | :math:`n_{e,\,tot}`"""
277 | k_e = n_e_tot / _broken_power_law_integral(
278 | p1, p2, gamma_b, gamma_min, gamma_max
279 | )
280 | return cls(k_e.to("cm-3"), p1, p2, gamma_b, gamma_min, gamma_max)
281 |
282 | @classmethod
283 | def from_normalised_u_e(cls, u_e, p1, p2, gamma_b, gamma_min, gamma_max):
284 | r"""sets the normalisation :math:`k_e` from the total energy density
285 | :math:`u_e`, Eq. 6.64 in [DermerMenon2009]_"""
286 | k_e = u_e / (
287 | mec2
288 | * _broken_power_law_times_gamma_integral(
289 | p1, p2, gamma_b, gamma_min, gamma_max
290 | )
291 | )
292 | return cls(k_e.to("cm-3"), p1, p2, gamma_b, gamma_min, gamma_max)
293 |
294 | @classmethod
295 | def from_norm_at_gamma_1(cls, norm, p1, p2, gamma_b, gamma_min, gamma_max):
296 | r"""sets :math:`k_e` such that `norm` = :math:`n_e(\gamma=1)`."""
297 | k_e = norm.to("cm-3") * np.power(gamma_b, -p1)
298 | print(
299 | f"normalising broken power-law to value {norm:.2e} at gamma = 1, and {k_e: .2e} at gamma_b = {gamma_b:.2e}"
300 | )
301 | return cls(k_e, p1, p2, gamma_b, gamma_min, gamma_max)
302 |
303 | def SSA_integrand(self, gamma):
304 | r"""(analytical) integrand for the synchrotron self-absorption:
305 | :math:`\gamma'^2 \frac{d}{d \gamma'} \left(\frac{n_e(\gamma)}{\gamma'^2}\right)`"""
306 | return self.k_e * _broken_power_law_ssa_integrand(
307 | gamma, self.p1, self.p2, self.gamma_b, self.gamma_min, self.gamma_max,
308 | )
309 |
310 |
311 | class BrokenPowerLaw2:
312 | r"""Broken power law as in Eq. 1 of [Tavecchio1998]_.
313 | When called, the particle density :math:`n_e(\gamma)` in :math:`\mathrm{cm}^{-3}` is returned.
314 |
315 | .. math::
316 | n_e(\gamma') = k_e \left[
317 | \gamma'^{-p_1} \, H(\gamma'; \gamma'_{\rm min}, \gamma'_b) +
318 | \gamma'^{(p_2 - p_1)}_b \, \gamma'^{-p_2} \, H(\gamma'; \gamma'_{b}, \gamma'_{\rm max})
319 | \right]
320 |
321 | Parameters
322 | ----------
323 | k_e : :class:`~astropy.units.Quantity`
324 | spectral normalisation
325 | p1 : float
326 | spectral index before the break (positive by definition)
327 | p2 : float
328 | spectral index after the break (positive by definition)
329 | gamma_b : float
330 | Lorentz factor at which the change in spectral index is occurring
331 | gamma_min : float
332 | minimum Lorentz factor of the electron distribution
333 | gamma_max : float
334 | maximum Lorentz factor of the electron distribution
335 | """
336 |
337 | def __init__(
338 | self,
339 | k_e=1e-13 * u.Unit("cm-3"),
340 | p1=2.0,
341 | p2=3.0,
342 | gamma_b=1e3,
343 | gamma_min=10,
344 | gamma_max=1e5,
345 | ):
346 | self.k_e = k_e
347 | self.p1 = p1
348 | self.p2 = p2
349 | self.gamma_b = gamma_b
350 | self.gamma_min = gamma_min
351 | self.gamma_max = gamma_max
352 |
353 | def __call__(self, gamma):
354 | return self.k_e * _broken_power_law_2(
355 | gamma, self.p1, self.p2, self.gamma_b, self.gamma_min, self.gamma_max,
356 | )
357 |
358 | def __str__(self):
359 | return (
360 | f"* electron spectrum\n"
361 | + f" - broken power law 2\n"
362 | + f" - k_e: {self.k_e:.2e}\n"
363 | + f" - p1: {self.p1:.2f}\n"
364 | + f" - p2: {self.p2:.2f}\n"
365 | + f" - gamma_b: {self.gamma_b:.2e}\n"
366 | + f" - gamma_min: {self.gamma_min:.2e}\n"
367 | + f" - gamma_max: {self.gamma_max:.2e}\n"
368 | )
369 |
370 | @classmethod
371 | def from_normalised_density(cls, n_e_tot, p1, p2, gamma_b, gamma_min, gamma_max):
372 | r"""sets the normalisation :math:`k_e` from the total particle density
373 | :math:`n_{e,\,tot}`"""
374 | k_e = n_e_tot / _broken_power_law_2_integral(
375 | p1, p2, gamma_b, gamma_min, gamma_max
376 | )
377 | return cls(k_e.to("cm-3"), p1, p2, gamma_b, gamma_min, gamma_max)
378 |
379 | @classmethod
380 | def from_normalised_u_e(cls, u_e, p1, p2, gamma_b, gamma_min, gamma_max):
381 | r"""sets the normalisation :math:`k_e` from the total energy density
382 | :math:`u_e`, Eq. 6.64 in [DermerMenon2009]_"""
383 | k_e = u_e / (
384 | mec2
385 | * _broken_power_law_2_times_gamma_integral(
386 | p1, p2, gamma_b, gamma_min, gamma_max
387 | )
388 | )
389 | return cls(k_e.to("cm-3"), p1, p2, gamma_b, gamma_min, gamma_max)
390 |
391 | @classmethod
392 | def from_norm_at_gamma_1(cls, norm, p1, p2, gamma_b, gamma_min, gamma_max):
393 | r"""sets :math:`k_e` such that `norm` = :math:`n_e(\gamma=1)`."""
394 | return cls(norm.to("cm-3"), p1, p2, gamma_b, gamma_min, gamma_max)
395 |
396 | def SSA_integrand(self, gamma):
397 | r"""(analytical) integrand for the synchrotron self-absorption:
398 | :math:`\gamma'^2 \frac{d}{d \gamma'} \left(\frac{n_e(\gamma)}{\gamma'^2}\right)`"""
399 | return self.k_e * _broken_power_law_2_ssa_integrand(
400 | gamma, self.p1, self.p2, self.gamma_b, self.gamma_min, self.gamma_max,
401 | )
402 |
--------------------------------------------------------------------------------
/agnpy/emission_regions.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import astropy.units as u
3 | from astropy.constants import e, c, m_e, sigma_T
4 | from astropy.coordinates import Distance
5 | import matplotlib.pyplot as plt
6 | from . import spectra
7 |
8 |
9 | e = e.gauss
10 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
11 | # equivalency for decomposing Gauss in Gaussian-cgs units (not available in astropy)
12 | Gauss_cgs_unit = "cm(-1/2) g(1/2) s-1"
13 | Gauss_cgs_equivalency = [(u.G, u.Unit(Gauss_cgs_unit), lambda x: x, lambda x: x)]
14 |
15 |
16 | __all__ = ["Blob"]
17 |
18 |
19 | class Blob:
20 | r"""Simple spherical emission region.
21 |
22 | **Note:** all these quantities are defined in the comoving frame so they are actually
23 | primed quantities, when referring the notation in [DermerMenon2009]_.
24 |
25 | Parameters
26 | ----------
27 | R_b : :class:`~astropy.units.Quantity`
28 | radius of the blob
29 | z : float
30 | redshift of the source
31 | delta_D : float
32 | Doppler factor of the relativistic outflow
33 | Gamma : float
34 | Lorentz factor of the relativistic outflow
35 | B : :class:`~astropy.units.Quantity`
36 | magnetic field in the blob (Gauss)
37 | xi : float
38 | acceleration coefficient :math:`\xi` for first-order Fermi acceleration
39 | :math:`(\mathrm{d}E/\mathrm{d}t \propto v \approx c)`
40 | used to compute limits on the maximum Lorentz factor via
41 | :math:`(\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} = \xi c E / R_L`
42 |
43 | spectrum_norm : :class:`~astropy.units.Quantity`
44 | normalisation of the electron spectra, by default can be, following
45 | the notation in [DermerMenon2009]_:
46 |
47 | - :math:`n_{e,\,tot}`: total electrons density, in :math:`\mathrm{cm}^{-3}`
48 | - :math:`u_e` : total electrons energy density, in :math:`\mathrm{erg}\,\mathrm{cm}^{-3}`
49 | - :math:`W_e` : total energy in electrons, in :math:`\mathrm{erg}`
50 |
51 | see `spectrum_norm_type` for more details on the normalisation
52 |
53 | spectrum_dict : dictionary
54 | dictionary containing type and spectral shape information, e.g.:
55 |
56 | .. code-block:: python
57 |
58 | spectrum_dict = {
59 | "type": "PowerLaw",
60 | "parameters": {
61 | "p": 2.8,
62 | "gamma_min": 1e2,
63 | "gamma_max": 1e7
64 | }
65 | }
66 |
67 | spectrum_norm_type : ["integral", "differential", "gamma=1"]
68 | only with a normalisation in "cm-3" one can select among three types:
69 |
70 | * `integral`: (default) the spectrum is set such that :math:`n_{e,\,tot}` equals the value provided by `spectrum_norm`;
71 |
72 | * `differential`: the spectrum is set such that :math:`k_e` equals the value provided by `spectrum_norm`;
73 |
74 | * `gamma=1`: the spectrum is set such that :math:`n_e(\gamma=1)` equals the value provided by `spectrum_norm`.
75 |
76 | gamma_size : int
77 | size of the array of electrons Lorentz factors
78 | """
79 |
80 | def __init__(
81 | self,
82 | R_b,
83 | z,
84 | delta_D,
85 | Gamma,
86 | B,
87 | spectrum_norm,
88 | spectrum_dict,
89 | spectrum_norm_type="integral",
90 | xi=1.0,
91 | gamma_size=200,
92 | ):
93 | self.R_b = R_b.to("cm")
94 | self.z = z
95 | self.d_L = Distance(z=self.z).cgs
96 | self.V_b = 4 / 3 * np.pi * np.power(self.R_b, 3)
97 | self.delta_D = delta_D
98 | self.Gamma = Gamma
99 | self.Beta = np.sqrt(1 - 1 / np.power(self.Gamma, 2))
100 | # viewing angle
101 | self.mu_s = (1 - 1 / (self.Gamma * self.delta_D)) / self.Beta
102 | self.theta_s = (np.arccos(self.mu_s) * u.rad).to("deg")
103 | self.B = B
104 | # B decomposed in Gaussian-cgs units
105 | self.B_cgs = B.to(Gauss_cgs_unit, equivalencies=Gauss_cgs_equivalency)
106 | self.spectrum_norm = spectrum_norm
107 | self.spectrum_norm_type = spectrum_norm_type
108 | self.spectrum_dict = spectrum_dict
109 | self.xi = xi
110 | # size of the electron Lorentz factor grid
111 | self.gamma_size = gamma_size
112 | self.gamma_min = self.spectrum_dict["parameters"]["gamma_min"]
113 | self.gamma_max = self.spectrum_dict["parameters"]["gamma_max"]
114 | # grid of Lorentz factor for the integration in the blob comoving frame
115 | self.gamma = np.logspace(
116 | np.log10(self.gamma_min), np.log10(self.gamma_max), self.gamma_size
117 | )
118 | # grid of Lorentz factors for integration in the external frame
119 | self.gamma_to_integrate = np.logspace(1, 9, self.gamma_size)
120 | # model for the electron density
121 | self.set_n_e(self.spectrum_norm, self.spectrum_dict, self.spectrum_norm_type)
122 |
123 | def set_n_e(self, spectrum_norm, spectrum_dict, spectrum_norm_type):
124 | r"""set the spectrum :math:`n_e` for the blob"""
125 | model_dict = {
126 | "PowerLaw": spectra.PowerLaw,
127 | "BrokenPowerLaw": spectra.BrokenPowerLaw,
128 | "BrokenPowerLaw2": spectra.BrokenPowerLaw2,
129 | }
130 | spectrum_type = spectrum_dict["type"]
131 |
132 | if spectrum_norm_type != "integral" and spectrum_norm.unit in (
133 | u.Unit("erg"),
134 | u.Unit("erg cm-3"),
135 | ):
136 | raise NameError(
137 | "Normalisations different than 'integral' available only for 'spectrum_norm' in cm-3"
138 | )
139 |
140 | # check the units of the normalisation
141 | # cm-3 is the only one allowing more than one normalisation type
142 | if spectrum_norm.unit == u.Unit("cm-3"):
143 |
144 | if spectrum_norm_type == "integral":
145 | self.n_e = model_dict[spectrum_type].from_normalised_density(
146 | spectrum_norm, **spectrum_dict["parameters"]
147 | )
148 | elif spectrum_norm_type == "differential":
149 | self.n_e = model_dict[spectrum_type](
150 | spectrum_norm, **spectrum_dict["parameters"]
151 | )
152 | elif spectrum_norm_type == "gamma=1":
153 | self.n_e = model_dict[spectrum_type].from_norm_at_gamma_1(
154 | spectrum_norm, **spectrum_dict["parameters"]
155 | )
156 |
157 | elif spectrum_norm.unit == u.Unit("erg cm-3"):
158 | self.n_e = model_dict[spectrum_type].from_normalised_u_e(
159 | spectrum_norm, **spectrum_dict["parameters"]
160 | )
161 |
162 | elif spectrum_norm.unit == u.Unit("erg"):
163 | u_e = (spectrum_norm / self.V_b).to("erg cm-3")
164 | self.n_e = model_dict[spectrum_type].from_normalised_u_e(
165 | u_e, **spectrum_dict["parameters"]
166 | )
167 |
168 | def __str__(self):
169 | """printable summary of the blob"""
170 | return (
171 | "* spherical emission region\n"
172 | + f" - R_b (radius of the blob): {self.R_b.cgs:.2e}\n"
173 | + f" - V_b (volume of the blob): {self.V_b.cgs:.2e}\n"
174 | + f" - z (source redshift): {self.z:.2f}\n"
175 | + f" - d_L (source luminosity distance):{self.d_L.cgs:.2e}\n"
176 | + f" - delta_D (blob Doppler factor): {self.delta_D:.2e}\n"
177 | + f" - Gamma (blob Lorentz factor): {self.Gamma:.2e}\n"
178 | + f" - Beta (blob relativistic velocity): {self.Beta:.2e}\n"
179 | + f" - theta_s (jet viewing angle): {self.theta_s:.2e}\n"
180 | + f" - B (magnetic field tangled to the jet): {self.B:.2e}\n"
181 | + str(self.n_e)
182 | )
183 |
184 | def set_delta_D(self, Gamma, theta_s):
185 | """set the viewing angle and the Lorentz factor of the outflow to
186 | obtain a specific Doppler factor
187 |
188 | Parameters
189 | ----------
190 | Gamma : float
191 | Lorentz factor of the relativistic outflow
192 | theta_s : :class:`~astropy.units.Quantity`
193 | viewing angle of the jet
194 | """
195 | mu_s = np.cos(theta_s.to("rad").value)
196 | Beta = np.sqrt(1 - 1 / np.power(Gamma, 2))
197 | delta_D = 1 / (Gamma * (1 - Beta * mu_s))
198 |
199 | self.theta_s = theta_s
200 | self.mu_s = mu_s
201 | self.Gamma = Gamma
202 | self.Beta = Beta
203 | self.delta_D = delta_D
204 |
205 | def set_gamma_size(self, gamma_size):
206 | """change size of the array of electrons Lorentz factors"""
207 | self.gamma_size = gamma_size
208 | self.gamma = np.logspace(
209 | np.log10(self.gamma_min), np.log10(self.gamma_max), self.gamma_size
210 | )
211 | self.gamma_to_integrate = np.logspace(1, 9, self.gamma_size)
212 |
213 | def N_e(self, gamma):
214 | r"""number of electrons as a function of the Lorentz factor,
215 | :math:`N_e(\gamma') = V_b\,n_e(\gamma')`"""
216 | return self.V_b * self.n_e(gamma)
217 |
218 | @property
219 | def n_e_tot(self):
220 | r"""total electrons density
221 |
222 | .. math::
223 | n_{e,\,tot} = \int^{\gamma'_{\rm max}}_{\gamma'_{\rm min}} {\rm d}\gamma' n_e(\gamma')
224 | """
225 | return np.trapz(self.n_e(self.gamma), self.gamma)
226 |
227 | @property
228 | def N_e_tot(self):
229 | r"""total number of electrons
230 |
231 | .. math::
232 | N_{e,\,tot} = \int^{\gamma'_{\rm max}}_{\gamma'_{\rm min}} {\rm d}\gamma' N_e(\gamma')
233 | """
234 | return np.trapz(self.N_e(self.gamma), self.gamma)
235 |
236 | @property
237 | def u_e(self):
238 | r"""total energy density in non-thermal electrons
239 |
240 | .. math::
241 | u_{e} = m_e c^2\,\int^{\gamma'_{\rm max}}_{\gamma'_{\rm min}} {\rm d}\gamma' \gamma' n_e(\gamma')
242 | """
243 | return mec2 * np.trapz(self.gamma * self.n_e(self.gamma), self.gamma)
244 |
245 | @property
246 | def W_e(self):
247 | r"""total energy in non-thermal electrons
248 |
249 | .. math::
250 | W_{e} = m_e c^2\,\int^{\gamma'_{\rm max}}_{\gamma'_{\rm min}} {\rm}\gamma' \gamma' N_e(\gamma')
251 | """
252 | return mec2 * np.trapz(self.gamma * self.N_e(self.gamma), self.gamma)
253 |
254 | @property
255 | def U_B(self):
256 | r"""energy density of magnetic field
257 |
258 | .. math::
259 | U_B = B^2 / (8 \pi)
260 | """
261 | U_B = np.power(self.B_cgs, 2) / (8 * np.pi)
262 | return U_B.to("erg cm-3")
263 |
264 | @property
265 | def k_eq(self):
266 | """equipartition parameter: ratio between totoal electron energy density
267 | magnetic field energy density, Eq. 7.75 of [DermerMenon2009]_"""
268 | return (self.u_e / self.U_B).to_value("")
269 |
270 | @property
271 | def P_jet_e(self):
272 | r"""jet power in electrons
273 |
274 | .. math::
275 | P_{jet,\,e} = 2 \pi R_b^2 \beta \Gamma^2 c u_e
276 | """
277 | prefactor = (
278 | 2 * np.pi * np.power(self.R_b, 2) * self.Beta * np.power(self.Gamma, 2) * c
279 | )
280 | return (prefactor * self.u_e).to("erg s-1")
281 |
282 | @property
283 | def P_jet_B(self):
284 | r"""jet power in magnetic field
285 |
286 | .. math::
287 | P_{jet,\,B} = 2 \pi R_b^2 beta \Gamma^2 c \frac{B^2}{8\pi}
288 | """
289 | prefactor = (
290 | 2 * np.pi * np.power(self.R_b, 2) * self.Beta * np.power(self.Gamma, 2) * c
291 | )
292 | return (prefactor * self.U_B).to("erg s-1")
293 |
294 | @property
295 | def gamma_max_larmor(self):
296 | r"""maximum Lorentz factor of electrons that have their Larmour radius
297 | smaller than the blob radius: :math:`R_L < R_b`.
298 | The Larmor frequency and radius in Gaussian units read
299 |
300 | .. math::
301 |
302 | \omega_L &= \frac{eB}{\gamma m_e c} \\
303 | R_L &= \frac{v}{\omega_L} = \frac{\gamma m_e v c}{e B} \approx \frac{\gamma m_e c^2}{e B}
304 |
305 | therefore
306 |
307 | .. math::
308 |
309 | R_L < R_b \Rightarrow \gamma_{\mathrm{max}} < \frac{R_b e B}{m_e c^2}
310 | """
311 | return (self.R_b * e * self.B_cgs / mec2).to_value("")
312 |
313 | @property
314 | def gamma_max_ballistic(self):
315 | r"""Naive estimation of maximum Lorentz factor of electrons comparing
316 | acceleration time scale with ballistic time scale.
317 | For the latter we assume that the particles crosses the blob radius.
318 |
319 | .. math::
320 |
321 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} &= \xi c E / R_L \\
322 | T_{\mathrm{acc}} &= E \,/\,(\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} = R_L / (\xi c) \\
323 | T_{\mathrm{bal}} &= R_b / c \\
324 | T_{\mathrm{acc}} &< T_{\mathrm{bal}}
325 | \Rightarrow \gamma_{\mathrm{max}} < \frac{\xi R_b e B}{m_e c^2}
326 | """
327 | return self.xi * self.gamma_max_larmor
328 |
329 | @property
330 | def gamma_max_synch(self):
331 | r"""Simple estimation of maximum Lorentz factor of electrons
332 | comparing the acceleration time scale with the synchrotron energy loss
333 |
334 | .. math::
335 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} &= \xi c E / R_L \\
336 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{synch}} &= 4 / 3 \sigma_T c U_B \gamma^2 \\
337 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} &= (\mathrm{d}E/\mathrm{d}t)_{\mathrm{synch}}
338 | \Rightarrow \gamma_{\mathrm{max}} < \sqrt{\frac{6 \pi \xi e}{\sigma_T B}}
339 | """
340 | return np.sqrt(6 * np.pi * self.xi * e / (sigma_T * self.B_cgs)).to_value("")
341 |
342 | @property
343 | def gamma_max_SSC(self):
344 | r"""Simple estimation of maximum Lorentz factor of electrons
345 | comparing the acceleration time scale with the SSC energy loss (in Thomson range)
346 | WARNING: the highest energy electrons will most often scatter in Klein-Nishina range instead
347 |
348 | .. math::
349 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} &= \xi c E / R_L \\
350 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{SSC}} &= 4 / 3 \sigma_T c U_{\mathrm{synch}} \gamma^2 \\
351 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{acc}} &= (\mathrm{d}E/\mathrm{d}t)_{\mathrm{SSC}}
352 | \Rightarrow \gamma_{\mathrm{max}} < \sqrt{\frac{3 \xi e B }{\sigma_T U_SSC}}
353 | """
354 | return np.sqrt(
355 | 3 * self.xi * e * self.B_cgs / (4 * sigma_T * self.u_ph_synch)
356 | ).to_value("")
357 |
358 | def gamma_max_EC_DT(self, dt, r=0 * u.cm):
359 | r"""Simple estimation of maximum Lorentz factor of electrons comparing the acceleration time scale
360 | with the EC energy loss (in Thomson range, see B&G 1970), like in gamma_max_SSC
361 | WARNING: assumes Thomson regime
362 |
363 | .. math::
364 | \gamma_{\mathrm{max}} = \sqrt{\frac{3 \xi e B }{ \sigma_T U'_\mathrm{ext}}}
365 | """
366 | return np.sqrt(
367 | 3 * self.xi * e * self.B_cgs / (4 * sigma_T * dt.u_ph(r, self))
368 | ).to_value("")
369 |
370 | @property
371 | def gamma_break_synch(self):
372 | r"""Simple estimation of the cooling break of electrons comparing
373 | synchrotron cooling time scale with the ballistic time scale:
374 |
375 | .. math::
376 |
377 | T_{\mathrm{synch}} &= E\,/\,(\mathrm{d}E/\mathrm{d}t)_{\mathrm{synch}}
378 | = 3 m_e c^2 / (4 \sigma_T U_B \gamma) \\
379 | T_{\mathrm{bal}} &= R_b / c \\
380 | T_{\mathrm{synch}} &= T_{\mathrm{bal}} \Rightarrow \gamma_b = 6 \pi m_e c^2 / \sigma_T B^2 R_b
381 | """
382 | gamma_max = (
383 | (6 * np.pi * mec2 / (sigma_T * np.power(self.B_cgs, 2) * self.R_b))
384 | .to("")
385 | .value
386 | )
387 | return gamma_max
388 |
389 | @property
390 | def gamma_break_SSC(self):
391 | r"""Simple estimation of the cooling break of electrons comparing
392 | SSC time scale (see B&G 1970) with the ballistic time scale:
393 | WARNING: only applicable in Thomson regime
394 |
395 | .. math::
396 | T_{\mathrm{SSC}} &= E\,/\,(\mathrm{d}E/\mathrm{d}t)_{\mathrm{SSC}}
397 | = 3 m_e c^2 / (4 \sigma_T U_{\mathrm{SSC}} \gamma) \\
398 | T_{\mathrm{bal}} &= R_b / c \\
399 | T_{\mathrm{SSC}} &= T_{\mathrm{bal}} \Rightarrow \gamma_b = 3 m_e c^2 / 4 \sigma_T U_{\mathrm{SSC}} R_b
400 | """
401 | return (3 * mec2 / (4 * sigma_T * self.u_ph_synch * self.R_b)).to("").value
402 |
403 | def gamma_break_EC_DT(self, dt, r=0 * u.cm):
404 | r"""Simple estimation of the cooling break of electrons comparing
405 | EC time scale (see B&G 1970) with the ballistic time scale, like in gamma_break_SSC
406 | WARNING: assumes Thomson regime
407 |
408 | .. math::
409 | \gamma_b = 3 m_e c^2 / 4 \sigma_T U'_{\mathrm{ext}} R_b
410 | """
411 | # u_ext=np.power(self.Gamma,2) * np.power(1-mu*self.Beta,2) * dt.xi_dt*dt.L_disk/(4*np.pi*np.power(d,2) * c)
412 | return (3 * mec2 / (4 * sigma_T * dt.u_ph(r, self) * self.R_b)).to("").value
413 |
414 | @property
415 | def u_ph_synch(self):
416 | r"""energy density of the synchrotron photons energy losses are:
417 |
418 | .. math::
419 | (\mathrm{d}E/\mathrm{d}t)_{\mathrm{synch}} = 4 / 3 \sigma_T c U_B \gamma^2
420 |
421 | the radiation stays an average time of :math:`(3/4) (R_b/c)` (the factor of 3/4 cames from averaging over a sphere),
422 | so an e- with Lorentz factor :math:`\gamma` produces:
423 |
424 | .. math::
425 | 0.75\,(\mathrm{d}E/\mathrm{d}t)_{\mathrm{synch}}\,(R_b/c)\,/\,V_b
426 |
427 | of radiation. We need to integrate over the electron spectrum (and multiply back by V_b)
428 |
429 | .. math::
430 | 0.75\,\int n_e(\gamma) (\mathrm{d}E/\mathrm{d}t)_{\mathrm{synch}} R_b \mathrm{d}\gamma
431 |
432 | so
433 |
434 | .. math::
435 | u_{\mathrm{synch}} = \sigma_T U_B R_b \int n_e(\gamma) \, \gamma^2 \mathrm{d}\gamma
436 |
437 | WARNING: this does not take into account SSA!
438 | """
439 | u_ph = (
440 | sigma_T.cgs
441 | * self.U_B
442 | * self.R_b
443 | * np.trapz(np.power(self.gamma, 2) * self.n_e(self.gamma), self.gamma)
444 | )
445 | return u_ph.to("erg cm-3")
446 |
447 | def plot_n_e(self, ax=None, gamma_power=0):
448 | """plot the electron distribution
449 |
450 | Parameters
451 | ----------
452 | ax : :class:`~matplotlib.axes.Axes`, optional
453 | Axis
454 | gamma_power : float
455 | power of gamma to raise the electron distribution
456 | """
457 | ax = plt.gca() if ax is None else ax
458 |
459 | ax.loglog(self.gamma, np.power(self.gamma, gamma_power) * self.n_e(self.gamma))
460 | ax.set_xlabel(r"$\gamma$")
461 | if gamma_power == 0:
462 | ax.set_ylabel(r"$n_e(\gamma)\,/\,{\rm cm}^{-3}$")
463 | else:
464 | ax.set_ylabel(
465 | r"$\gamma^{"
466 | + str(gamma_power)
467 | + r"}$"
468 | + r"$\,n_e(\gamma)\,/\,{\rm cm}^{-3}$"
469 | )
470 | return ax
471 |
--------------------------------------------------------------------------------
/agnpy/targets.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from astropy.constants import m_e, h, c, k_B, M_sun, G
3 | import astropy.units as u
4 | from astropy.coordinates import Distance
5 |
6 |
7 | mec2 = m_e.to("erg", equivalencies=u.mass_energy())
8 | lambda_c = (h / (m_e * c)).to("cm") # Compton wavelength
9 | # equivalency to transform frequencies to energies in electron rest mass units
10 | epsilon_equivalency = [
11 | (u.Hz, u.Unit(""), lambda x: h.cgs * x / mec2, lambda x: x * mec2 / h.cgs)
12 | ]
13 |
14 |
15 | # dictionary with all the available spectral lines
16 | lines_dictionary = {
17 | "Lyepsilon": {"lambda": 937.80 * u.Angstrom, "R_Hbeta_ratio": 2.7},
18 | "Lydelta": {"lambda": 949.74 * u.Angstrom, "R_Hbeta_ratio": 2.8},
19 | "CIII": {"lambda": 977.02 * u.Angstrom, "R_Hbeta_ratio": 0.83},
20 | "NIII": {"lambda": 990.69 * u.Angstrom, "R_Hbeta_ratio": 0.85},
21 | "Lybeta": {"lambda": 1025.72 * u.Angstrom, "R_Hbeta_ratio": 1.2},
22 | "OVI": {"lambda": 1033.83 * u.Angstrom, "R_Hbeta_ratio": 1.2},
23 | "ArI": {"lambda": 1066.66 * u.Angstrom, "R_Hbeta_ratio": 4.5},
24 | "Lyalpha": {"lambda": 1215.67 * u.Angstrom, "R_Hbeta_ratio": 0.27},
25 | "OI": {"lambda": 1304.35 * u.Angstrom, "R_Hbeta_ratio": 4.0},
26 | "SiII": {"lambda": 1306.82 * u.Angstrom, "R_Hbeta_ratio": 4.0},
27 | "SiIV": {"lambda": 1396.76 * u.Angstrom, "R_Hbeta_ratio": 0.83},
28 | "OIV]": {"lambda": 1402.06 * u.Angstrom, "R_Hbeta_ratio": 0.83},
29 | "CIV": {"lambda": 1549.06 * u.Angstrom, "R_Hbeta_ratio": 0.83},
30 | "NIV": {"lambda": 1718.55 * u.Angstrom, "R_Hbeta_ratio": 3.8},
31 | "AlII": {"lambda": 1721.89 * u.Angstrom, "R_Hbeta_ratio": 3.8},
32 | "CIII]": {"lambda": 1908.73 * u.Angstrom, "R_Hbeta_ratio": 0.46},
33 | "[NeIV]": {"lambda": 2423.83 * u.Angstrom, "R_Hbeta_ratio": 5.8},
34 | "MgII": {"lambda": 2798.75 * u.Angstrom, "R_Hbeta_ratio": 0.45},
35 | "HeI": {"lambda": 3188.67 * u.Angstrom, "R_Hbeta_ratio": 4.3},
36 | "Hdelta": {"lambda": 4102.89 * u.Angstrom, "R_Hbeta_ratio": 3.4},
37 | "Hgamma": {"lambda": 4341.68 * u.Angstrom, "R_Hbeta_ratio": 3.2},
38 | "HeII": {"lambda": 4687.02 * u.Angstrom, "R_Hbeta_ratio": 0.63},
39 | "Hbeta": {"lambda": 4862.68 * u.Angstrom, "R_Hbeta_ratio": 1.0},
40 | "[ClIII]": {"lambda": 5539.43 * u.Angstrom, "R_Hbeta_ratio": 4.8},
41 | "HeI": {"lambda": 5877.29 * u.Angstrom, "R_Hbeta_ratio": 0.39},
42 | "Halpha": {"lambda": 6564.61 * u.Angstrom, "R_Hbeta_ratio": 1.3},
43 | }
44 |
45 |
46 | __all__ = [
47 | "CMB",
48 | "SSDisk",
49 | "PointSourceBehindJet",
50 | "SphericalShellBLR",
51 | "RingDustTorus",
52 | "print_lines_list",
53 | ]
54 |
55 |
56 | def print_lines_list():
57 | r"""Print the list of the available spectral lines.
58 | The dictionary with the possible emission lines is taken from Table 5 in
59 | [Finke2016]_ and contains the value of the line wavelength and the ratio of
60 | its radius to the radius of the :math:`H_{\beta}` shell, not used at the moment.
61 | """
62 | for line in lines_dictionary.keys():
63 | print(f"{line}: {lines_dictionary[line]}")
64 |
65 |
66 | def I_epsilon_bb(epsilon, Theta):
67 | r"""Black-Body intensity :math:`I_{\nu}^{bb}`, Eq. 5.15 of [DermerMenon2009]_.
68 |
69 | Parameters
70 | ----------
71 | epsilon : :class:`~numpy.ndarray`
72 | array of dimensionless energies (in electron rest mass units)
73 | Theta : float
74 | dimensionless temperature of the Black Body
75 | """
76 | num = 2 * m_e * np.power(c, 3) * np.power(epsilon, 3)
77 | denum = np.power(lambda_c, 3) * (np.exp(epsilon / Theta) - 1)
78 | I = num / denum
79 | return I.to("erg cm-2 s-1")
80 |
81 |
82 | class CMB:
83 | """Cosmic Microwave Background radiation, approximated as an isotropic
84 | monochromatic target.
85 |
86 | Parameters
87 | ----------
88 | z : float
89 | redshift at which the CMB is considered
90 | """
91 |
92 | def __init__(self, z):
93 | self.name = "Cosmic Microwave Background Radiation"
94 | a = 7.5657 * 1e-15 * u.Unit("erg cm-3 K-4") # radiation constant
95 | T = 2.72548 * u.K
96 | self.u_0 = (a * np.power(T, 4)).to("erg cm-3") * np.power(1 + z, 4)
97 | self.epsilon_0 = (2.7 * k_B * T / mec2).to_value("") * (1 + z)
98 |
99 | def u(self, blob=None):
100 | """integral energy density of the CMB
101 |
102 | Parameters
103 | ----------
104 | blob : :class:`~agnpy.emission_regions.Blob`
105 | if provided, the energy density is computed in a reference frame
106 | comvoing with the blob
107 | """
108 | if blob:
109 | return self.u_0 * np.power(blob.Gamma, 2) * (1 + np.power(blob.Beta, 2) / 3)
110 | else:
111 | return self.u_0
112 |
113 |
114 | class PointSourceBehindJet:
115 | """Monochromatic point source behind the jet.
116 |
117 | Parameters
118 | ----------
119 | L_0 : :class:`~astropy.units.Quantity`
120 | luminosity of the source
121 | epsilon_0 : float
122 | dimensionless monochromatic energy of the source
123 | """
124 |
125 | def __init__(self, L_0, epsilon_0):
126 | self.name = "Monochromatic Point Source Behind the Jet"
127 | self.L_0 = L_0
128 | self.epsilon_0 = epsilon_0
129 |
130 | def u(self, r, blob=None):
131 | """integral energy density of the point source at distance r along the
132 | jet axis
133 |
134 | Parameters
135 | ----------
136 | r : :class:`~astropy.units.Quantity`
137 | array of distances along the jet axis
138 | blob : :class:`~agnpy.emission_regions.Blob`
139 | if provided, the energy density is computed in a reference frame
140 | comvoing with the blob
141 | """
142 | u_0 = (self.L_0 / (4 * np.pi * c * np.power(r, 2))).to("erg cm-3")
143 | if blob:
144 | return u_0 / (np.power(blob.Gamma, 2) * np.power(1 + blob.Beta, 2))
145 | else:
146 | return u_0
147 |
148 |
149 | class SSDisk:
150 | """[Shakura1973]_ accretion disk.
151 |
152 | Parameters
153 | ----------
154 | M_BH : :class:`~astropy.units.Quantity`
155 | Black Hole mass
156 | L_disk : :class:`~astropy.units.Quantity`
157 | luminosity of the disk
158 | eta : float
159 | accretion efficiency
160 | R_in : :class:`~astropy.units.Quantity` / float
161 | inner disk radius
162 | R_out : :class:`~astropy.units.Quantity` / float
163 | outer disk radius
164 | R_g_units : bool
165 | whether or not input radiuses are specified in units of the gravitational radius
166 | """
167 |
168 | def __init__(self, M_BH, L_disk, eta, R_in, R_out, R_g_units=False):
169 | self.name = "Shakura Sunyaev Accretion Disk"
170 | # masses and luminosities
171 | self.M_BH = M_BH
172 | self.M_8 = (M_BH / (1e8 * M_sun)).to_value("")
173 | self.L_Edd = 1.26 * 1e46 * self.M_8 * u.Unit("erg s-1")
174 | self.L_disk = L_disk
175 | # fraction of the Eddington luminosity at which the disk is accreting
176 | self.l_Edd = (self.L_disk / self.L_Edd).to_value("")
177 | self.eta = eta
178 | self.m_dot = (self.L_disk / (self.eta * np.power(c, 2))).to("g s-1")
179 | # gravitational radius
180 | self.R_g = (G * self.M_BH / np.power(c, 2)).to("cm")
181 | if R_g_units:
182 | # check that numbers have been passed
183 | R_in_unit_check = isinstance(R_in, int) or isinstance(R_in, float)
184 | R_out_unit_check = isinstance(R_out, int) or isinstance(R_out, float)
185 | if R_in_unit_check and R_out_unit_check:
186 | self.R_in = R_in * self.R_g
187 | self.R_out = R_out * self.R_g
188 | self.R_in_tilde = R_in
189 | self.R_out_tilde = R_out
190 | else:
191 | raise TypeError("R_in / R_out passed with units, int / float expected")
192 | else:
193 | # check that quantities have been passed
194 | R_in_unit_check = isinstance(R_in, u.Quantity)
195 | R_out_unit_check = isinstance(R_out, u.Quantity)
196 | if R_in_unit_check and R_out_unit_check:
197 | self.R_in = R_in
198 | self.R_out = R_out
199 | self.R_in_tilde = (self.R_in / self.R_g).to_value("")
200 | self.R_out_tilde = (self.R_out / self.R_g).to_value("")
201 | else:
202 | raise TypeError("R_in / R_out passed without units")
203 | # array of R_tile values
204 | self.R_tilde = np.linspace(self.R_in_tilde, self.R_out_tilde)
205 |
206 | def __str__(self):
207 | return (
208 | f"* Shakura Sunyaev accretion disk:\n"
209 | + f" - M_BH (central black hole mass): {self.M_BH.cgs:.2e}\n"
210 | + f" - L_disk (disk luminosity): {self.L_disk.cgs:.2e}\n"
211 | + f" - eta (accretion efficiency): {self.eta:.2e}\n"
212 | + f" - dot(m) (mass accretion rate): {self.m_dot.cgs:.2e}\n"
213 | + f" - R_in (disk inner radius): {self.R_in.cgs:.2e}\n"
214 | + f" - R_out (disk inner radius): {self.R_out.cgs:.2e}"
215 | )
216 |
217 | def mu_from_r_tilde(self, r_tilde, size=100):
218 | r"""array of cosine angles, spanning from :math:`R_{\mathrm{in}}` to
219 | :math:`R_{\mathrm{out}}`, viewed from a given distance :math:`\tilde{r}`
220 | along the jet axis, Eq. 72 and 73 in [Finke2016]_."""
221 | mu_min = 1 / np.sqrt(1 + np.power((self.R_out_tilde / r_tilde), 2))
222 | mu_max = 1 / np.sqrt(1 + np.power((self.R_in_tilde / r_tilde), 2))
223 | return np.linspace(mu_min, mu_max, size)
224 |
225 | def phi_disk(self, R_tilde):
226 | """Radial dependency of disk temperature, Eq. 63 in [Dermer2009]_.
227 |
228 | Parameters
229 | ----------
230 | R_tilde : :class:`~nump.ndarray`
231 | radial coordinate along the disk normalised to R_g
232 | """
233 | return 1 - np.sqrt(self.R_in_tilde / R_tilde)
234 |
235 | def phi_disk_mu(self, mu, r_tilde):
236 | r"""same as phi_disk but computed with cosine of zenith mu and normalised
237 | distance from the black hole :math:`\tilde{r}` Eq. 67 in [Dermer2009]_."""
238 | R_tilde = r_tilde * np.sqrt(np.power(mu, -2) - 1)
239 | return self.phi_disk(R_tilde)
240 |
241 | def epsilon(self, R_tilde):
242 | r"""monochromatic approximation for the mean photon energy at radius
243 | :math:`\tilde{R}` of the accretion disk. Eq. 65 in [Dermer2009]_."""
244 | xi = np.power(self.l_Edd / (self.M_8 * self.eta), 1 / 4)
245 | return 2.7 * 1e-4 * xi * np.power(R_tilde, -3 / 4)
246 |
247 | def epsilon_mu(self, mu, r_tilde):
248 | r"""same as epsilon but computed with cosine of zenith mu and distance
249 | from the black hole :math:`\tilde{r}`. Eq. 67 in [Dermer2009]_."""
250 | R_tilde = r_tilde * np.sqrt(np.power(mu, -2) - 1)
251 | return self.epsilon(R_tilde)
252 |
253 | def T(self, R_tilde):
254 | r"""Temperature of the disk at distance :math:`\tilde{R}`.
255 | Eq. 64 in [Dermer2009]_."""
256 | value = mec2 / (2.7 * k_B) * self.epsilon(R_tilde)
257 | return value.to("K")
258 |
259 | def Theta(self, R_tilde):
260 | r"""Dimensionless temperature of the black body at distance :math:`\tilde{R}`"""
261 | theta = k_B * self.T(R_tilde) / mec2
262 | return theta.to_value("")
263 |
264 | def u(self, r, blob=None):
265 | """integral energy density of radiation produced by the Disk at the distance
266 | r along the jet axis. Integral over the solid angle of Eq. 69 in [Dermer2009]_.
267 |
268 | Parameters
269 | ----------
270 | r : :class:`~astropy.units.Quantity`
271 | array of distances along the jet axis
272 | blob : :class:`~agnpy.emission_regions.Blob`
273 | if provided, the energy density is computed in a reference frame
274 | comvoing with the blob
275 | """
276 | r_tilde = (r / self.R_g).to_value("")
277 | mu = self.mu_from_r_tilde(r_tilde)
278 | prefactor = (
279 | 3
280 | * self.l_Edd
281 | * self.L_Edd
282 | * self.R_g
283 | / (8 * np.pi * c * self.eta * np.power(r, 3))
284 | )
285 | integrand = (
286 | 1
287 | / mu
288 | * np.power(np.power(mu, -2) - 1, -3 / 2)
289 | * self.phi_disk_mu(mu, r_tilde)
290 | )
291 | if blob:
292 | mu_prime = (mu - blob.Beta) / (1 - blob.Beta * mu)
293 | integrand_prefactor = 1 / (
294 | np.power(blob.Gamma, 6)
295 | * np.power(1 - blob.Beta * mu, 2)
296 | * np.power(1 + blob.Beta * mu_prime, 4)
297 | )
298 | integrand *= integrand_prefactor
299 |
300 | integral = np.trapz(integrand, mu, axis=0)
301 | return (prefactor * integral).to("erg cm-3")
302 |
303 | def sed_flux(self, nu, z):
304 | r"""Black Body SED generated by the SS Disk, considered as a
305 | multi-dimensional black body. I obtain the formula following
306 | Chapter 5 of [DermerMenon2009]_
307 |
308 | .. math::
309 | f_{\epsilon} (= \nu F_{\nu}) &=
310 | \epsilon \, \int_{\Omega_s} \mu I_{\epsilon} \mathrm{d}\Omega \\\\
311 | &= \epsilon \, 2 \pi \int_{\mu_{\mathrm{min}}}^{\mu_{\mathrm{max}}}
312 | \mu I_{\epsilon} \mathrm{d}\mu
313 |
314 | where the cosine of the angle under which an observer at :math:`d_L`
315 | sees the disk is :math:`\mu = 1 / \sqrt{1 + (R / d_L)^2}`, integrating
316 | over :math:`R` rather than :math:`\mu`
317 |
318 | .. math::
319 | f_{\epsilon} &= \epsilon \, 2 \pi \int_{R_{\mathrm{in}}}^{R_{\mathrm{out}}}
320 | (1 + R^2 / d_L^2)^{-3/2} \frac{R}{d_L^2} \, I_{\epsilon}(R) \, \mathrm{d}R \\\\
321 | &= \epsilon \, 2 \pi \frac{R_g^2}{d_L^2}
322 | \int_{\\tilde{R}_{\mathrm{in}}}^{\tilde{R}_{\mathrm{out}}}
323 | \left(1 + \\tilde{R}^2 / \tilde{d_L}^2 \right)^{-3/2} \,
324 | \tilde{R} \, I_{\epsilon}(\tilde{R}) \, \mathrm{d}\tilde{R}
325 |
326 | where in the last integral distances with :math:`\tilde{}` have been
327 | scaled to the gravitational radius :math:`R_g`.
328 |
329 | Parameters
330 | ----------
331 | nu : :class:`~astropy.units.Quantity`
332 | array of frequencies, in Hz, to compute the sed, **note** these are
333 | observed frequencies (observer frame).
334 | z : float
335 | redshift of the galaxy, to correct the observed frequencies and to
336 | compute the flux once the distance is obtained
337 | """
338 | nu *= 1 + z
339 | epsilon = nu.to("", equivalencies=epsilon_equivalency)
340 | d_L = Distance(z=z).to("cm")
341 | d_L_tilde = (d_L / self.R_g).to_value("")
342 | Theta = self.Theta(self.R_tilde)
343 | # for multidimensional integration
344 | # axis 0: radiuses (and temperatures)
345 | # axis 1: photons epsilon
346 | _R_tilde = np.reshape(self.R_tilde, (self.R_tilde.size, 1))
347 | _Theta = np.reshape(Theta, (Theta.size, 1))
348 | _epsilon = np.reshape(epsilon, (1, epsilon.size))
349 | _integrand = (
350 | np.power(1 + np.power(_R_tilde / d_L_tilde, 2), -3 / 2)
351 | * _R_tilde
352 | * I_epsilon_bb(_epsilon, _Theta)
353 | )
354 | prefactor = 2 * np.pi * np.power(self.R_g, 2) / np.power(d_L, 2)
355 | sed = epsilon * prefactor * np.trapz(_integrand, self.R_tilde, axis=0)
356 | return sed.to("erg cm-2 s-1")
357 |
358 |
359 | class SphericalShellBLR:
360 | """Spherical Shell Broad Line Region, from [Finke2016]_.
361 | Each line is emitted from an infinitesimally thin spherical shell.
362 |
363 | Parameters
364 | ----------
365 | L_disk : :class:`~astropy.units.Quantity`
366 | Luminosity of the disk whose radiation is being reprocessed by the BLR
367 | xi_line : float
368 | fraction of the disk radiation reprocessed by the BLR
369 | line : string
370 | type of line emitted
371 | R_line : :class:`~astropy.units.Quantity`
372 | radius of the BLR spherical shell
373 | """
374 |
375 | def __init__(self, L_disk, xi_line, line, R_line):
376 | self.name = "SphericalShellBLR"
377 | self.L_disk = L_disk
378 | self.xi_line = xi_line
379 | if line in lines_dictionary:
380 | self.line = line
381 | self.lambda_line = lines_dictionary[line]["lambda"]
382 | else:
383 | raise NameError(f"{line} not available in the line dictionary")
384 | self.epsilon_line = (
385 | self.lambda_line.to("erg", equivalencies=u.spectral()) / mec2
386 | ).to_value("")
387 | self.R_line = R_line
388 |
389 | def __str__(self):
390 | return (
391 | f"* Spherical Shell Broad Line Region:\n"
392 | + f" - L_disk (accretion disk luminosity): {self.L_disk.cgs:.2e}\n"
393 | + f" - xi_line (fraction of the disk radiation reprocessed by the BLR): {self.xi_line:.2e}\n"
394 | + f" - line (type of emitted line): {self.line}, lambda = {self.lambda_line.cgs:.2f}\n"
395 | + f" - R_line (radius of the BLR shell): {self.R_line.cgs:.2e}\n"
396 | )
397 |
398 | def u(self, r, blob=None):
399 | """Density of radiation produced by the BLR at the distance r along the
400 | jet axis. Integral over the solid angle of Eq. 80 in [Finke2016]_.
401 |
402 | Parameters
403 | ----------
404 | r : :class:`~astropy.units.Quantity`
405 | array of distances along the jet axis
406 | blob : :class:`~agnpy.emission_regions.Blob`
407 | if provided, the energy density is computed in a reference frame
408 | comvoing with the blob
409 | """
410 | mu = np.linspace(-1, 1)
411 | _mu = mu.reshape(mu.size, 1)
412 | _r = r.reshape(1, r.size)
413 | _x2 = np.power(_r, 2) + np.power(self.R_line, 2) - 2 * _r * self.R_line * _mu
414 | prefactor = self.xi_line * self.L_disk / (8 * np.pi * c)
415 | integrand = 1 / _x2
416 | if blob:
417 | _mu_star = np.sqrt(
418 | 1 - np.power(self.R_line, 2) / _x2 * (1 - np.power(_mu, 2))
419 | )
420 | integrand_prefactor = np.power(blob.Gamma, 2) * np.power(
421 | 1 - blob.Beta * _mu_star, 2
422 | )
423 | integrand *= integrand_prefactor
424 | integral = np.trapz(integrand, mu, axis=0)
425 | return (prefactor * integral).to("erg cm-3")
426 |
427 |
428 | class RingDustTorus:
429 | """Dust Torus as infinitesimally thin annulus, from [Finke2016]_.
430 | For the Compton scattering monochromatic emission at the peak energy of the
431 | Black Body spectrum is considered.
432 |
433 | Parameters
434 | ----------
435 | L_disk : :class:`~astropy.units.Quantity`
436 | Luminosity of the disk whose radiation is being reprocessed by the Torus
437 | xi_dt : float
438 | fraction of the disk radiation reprocessed
439 | T_dt : :class:`~astropy.units.Quantity`
440 | peak temperature of the black body emission of the Torus
441 | R_dt : :class:`~astropy.units.Quantity`
442 | radius of the Torus, if not specified the saturation radius of Eq. 96 in
443 | [Finke2016]_ will be used
444 | """
445 |
446 | def __init__(self, L_disk, xi_dt, T_dt, R_dt=None):
447 | self.name = "RingDustTorus"
448 | self.L_disk = L_disk
449 | self.xi_dt = xi_dt
450 | self.T_dt = T_dt
451 | # dimensionless temperature of the torus
452 | self.Theta = (k_B * self.T_dt / mec2).to_value("")
453 | self.epsilon_dt = 2.7 * self.Theta
454 |
455 | # if the radius is not specified use saturation radius Eq. 96 of [Finke2016]_
456 | if R_dt is None:
457 | self.R_dt = (
458 | 3.5
459 | * 1e18
460 | * np.sqrt((self.L_disk / (1e45 * u.Unit("erg s-1"))).to_value(""))
461 | * np.power((self.T_dt / (1e3 * u.K)).to_value(""), -2.6)
462 | ) * u.cm
463 | else:
464 | self.R_dt = R_dt
465 |
466 | def __str__(self):
467 | return (
468 | f"* Ring Dust Torus:\n"
469 | + f" - L_disk (accretion disk luminosity): {self.L_disk.cgs:.2e}\n"
470 | + f" - xi_dt (fraction of the disk radiation reprocessed by the torus): {self.xi_dt:.2e}\n"
471 | + f" - T_dt (temperature of the dust torus): {self.T_dt:.2e}\n"
472 | + f" - R_dt (radius of the torus): {self.R_dt.cgs:.2e}\n"
473 | )
474 |
475 | def u(self, r, blob=None):
476 | r"""Density of radiation produced by the Torus at the distance r along the
477 | jet axis. Integral over the solid angle of Eq. 85 in [Finke2016]_
478 |
479 | Parameters
480 | ----------
481 | r : :class:`~astropy.units.Quantity`
482 | array of distances along the jet axis
483 | blob : :class:`~agnpy.emission_regions.Blob`
484 | if provided, the energy density is computed in a reference frame
485 | comvoing with the blob
486 | """
487 | x2 = np.power(self.R_dt, 2) + np.power(r, 2)
488 | x = np.sqrt(x2)
489 | integral = self.xi_dt * self.L_disk / (4 * np.pi * c * x2)
490 | if blob:
491 | mu = (r / x).to_value("")
492 | integral *= np.power(blob.Gamma * (1 - blob.Beta * mu), 2)
493 | return integral.to("erg cm-3")
494 |
495 | def sed_flux(self, nu, z):
496 | r"""Black Body SED generated by the Dust Torus:
497 |
498 | .. math::
499 | \nu F_{\nu} \, [\mathrm{erg}\,\mathrm{cm}^{-2}\,\mathrm{s}^{-1}]
500 |
501 | ----------
502 | nu : :class:`~astropy.units.Quantity`
503 | array of frequencies, in Hz, to compute the sed, **note** these are
504 | observed frequencies (observer frame).
505 | z : float
506 | redshift of the galaxy, to correct the observed frequencies and to
507 | compute the flux once the distance is obtained
508 | """
509 | nu *= 1 + z
510 | epsilon = nu.to("", equivalencies=epsilon_equivalency)
511 | d_L = Distance(z=z).to("cm")
512 | prefactor = np.pi * np.power((self.R_dt / d_L).to_value(""), 2)
513 | sed = prefactor * epsilon * I_epsilon_bb(epsilon, self.Theta)
514 | return sed * u.Unit("erg cm-2 s-1")
515 |
--------------------------------------------------------------------------------