├── 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 | ![](https://github.com/cosimoNigro/agnpy/workflows/CI%20test/badge.svg) 38 | 39 | ![](https://codecov.io/gh/cosimoNigro/agnpy/branch/master/graph/badge.svg) 40 | 41 | ![](http://img.shields.io/pypi/v/agnpy.svg?text=version) 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 | --------------------------------------------------------------------------------