├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── afterglow.rst │ ├── conf.py │ ├── custom.rst │ ├── hydro.rst │ ├── index.rst │ ├── quickstart.rst │ └── weighted.rst ├── examples ├── centroid_motion.py ├── customized_radiation_model.py ├── light_curves.py ├── quick_start.py └── sky_map.py ├── jetsimpy ├── __init__.py ├── _grid.py ├── _jet_type.py ├── _jetsimpy.py ├── _shortcut.py └── src │ ├── Afterglow │ ├── afterglow.cpp │ ├── afterglow.h │ ├── blast.h │ ├── eats.cpp │ ├── eats.h │ ├── models.cpp │ └── models.h │ ├── Hydro │ ├── config.h │ ├── interpolate.cpp │ ├── interpolate.h │ ├── sim_box.cpp │ ├── sim_box.h │ ├── tools.cpp │ └── tools.h │ ├── Math │ ├── integral.h │ ├── optimize.h │ └── root.h │ ├── binding.cpp │ ├── environment.h │ ├── jetsimpy.cpp │ └── jetsimpy.h ├── pyproject.toml └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | 4 | # Distribution / packaging 5 | .Python 6 | build/ 7 | develop-eggs/ 8 | dist/ 9 | downloads/ 10 | eggs/ 11 | .eggs/ 12 | lib/ 13 | lib64/ 14 | parts/ 15 | sdist/ 16 | var/ 17 | wheels/ 18 | share/python-wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # C extensions 24 | *.so 25 | 26 | # system folders 27 | .DS* 28 | 29 | # vscode configuration 30 | .vscode/ 31 | .conda/ 32 | 33 | # test folder 34 | test/ 35 | output/ 36 | 37 | # output 38 | *.o 39 | *.mp4 40 | *.zip 41 | *.pdf 42 | 43 | 44 | # workspace 45 | jetsimpy.code-workspace -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.6" 7 | 8 | python: 9 | install: 10 | - requirements: docs/requirements.txt 11 | 12 | sphinx: 13 | configuration: docs/source/conf.py -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Hao Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include jetsimpy/data/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jetsimpy 2 | Hydrodynamic simulations of relativistic blastwave with tabulated angular energy and Lorentz factor profiles. Efficient Gamma-Ray Burst afterglow modeling. 3 | 4 | Code paper: [Wang et al. 2024](https://arxiv.org/abs/2402.19359). 5 | 6 | ## Updates 7 | Big updates: 8 | * One can now control the maximum iteration number for flux density integration and enforce the program to return a meaningful value even if the integration has not achieved the desired accuracy. This way one can significantly reduce the amount of runtime errors which frequently interrupt the program. 9 | * To define your own radiation model, you now need to define the "solid angle integrated specific intensity" instead of "emissivity" in the previous version. This way it makes more sense to introduce optical depth in the model. The relation is $I_\nu=\frac{j_\nu}{\alpha_\nu}(1-e^{-\alpha_\nu\Delta R})$. For optically thin case ($\alpha_\nu\sim 0$) this relation reduces to $I_\nu=j_\nu\Delta R$. 10 | 11 | Small updates: 12 | * For analytic jet profiles (top-hat, Gaussian, power-law), the process to generate synthetic light curves or spectrum is significantly simplified. See "examples/quick_start.py" for more details. 13 | * Some updates in the documentation. The importance of "resolution" is now highlighted. 14 | * The built-in resolution setups are renamed to increase readability. Previous names are still supported for now. 15 | 16 | ## Documentation 17 | A brief documentation is now available at: [https://jetsimpy.readthedocs.io/](https://jetsimpy.readthedocs.io/). 18 | 19 | ## Features 20 | 21 | ### These features are currently supported: 22 | * Tabulated angular energy profile 23 | * Tabulated angular Lorentz factor profile 24 | * ISM / wind / mixed external density profile: $n=n_{\rm ism}+n_{\rm wind}(r/10^{17}{\rm cm})^{-2}$ 25 | * Synthetic afterglow light curves 26 | * Apparent superluminal motion 27 | * Sky map and Gaussian equivalent image size 28 | 29 | Additionally, you can add your own radiation model by defining a lambda function in a [c++ source file](jetsimpy/src/Afterglow/models.cpp). This might be helpful if you have a more complicated model such as Synchrotron self-absorption or other cool stuffs. After adding your own model, just install the package as usual and refer to this model with its model name from Python side. 30 | 31 | ### These features are not supported yet: 32 | * Reverse shock 33 | * Energy injection 34 | 35 | ## Installation 36 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install jetsimpy. 37 | ```bash 38 | pip install . 39 | ``` 40 | 41 | Clean up the installation directory. This help avoid conflicts for future installations. 42 | ```bash 43 | python setup.py clean 44 | ``` 45 | 46 | You may install the package and clean up the directory by a single line: 47 | ```bash 48 | pip install . && python setup.py clean 49 | ``` 50 | 51 | ## Quickstart 52 | ```Python 53 | import numpy as np 54 | import jetsimpy 55 | 56 | # parameter dictionary 57 | P = dict( 58 | Eiso = 1e53, # Isotropic equivalent energy (erg) 59 | lf = 600, # initial Lorentz factor 60 | theta_c = 0.1, # half opening angle 61 | n0 = 1, # ism number density 62 | A = 0, # wind number density amplitude 63 | eps_e = 0.1, # epsilon_e 64 | eps_b = 0.01, # epsilon_b 65 | p = 2.17, # electron power index 66 | theta_v = 0.4, # viewing angle 67 | d = 474.33, # luminosity distance (Mpc) 68 | z = 0.1, # redshift 69 | ) 70 | 71 | # time and frequency 72 | tday = np.logspace(-2, 3, 100) 73 | tsecond = tday * 3600 * 24 74 | nu = 1e15 75 | 76 | # flux density 77 | fd_gaussian = jetsimpy.FluxDensity_gaussian(tsecond, nu, P) 78 | ``` 79 | 80 | More examples are available in the example folder. -------------------------------------------------------------------------------- /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 = source 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 | -------------------------------------------------------------------------------- /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=source 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 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=5.3.0 2 | sphinx-rtd-theme==1.3.0rc1 -------------------------------------------------------------------------------- /docs/source/afterglow.rst: -------------------------------------------------------------------------------- 1 | Afterglow modeling 2 | ================== 3 | The GRB afterglow observables can be calculated by calling the methods of the jet object `jetsimpy.Jet`. 4 | 5 | Light curve and spectrum 6 | ------------------------ 7 | .. py:method:: .FluxDensity(t, nu, P, model='sync', rtol=1e-3, max_iter=100, force_return=True) 8 | 9 | Calculate the flux density. 10 | 11 | :param np.array/float t: time series (s) 12 | :param np.array/float nu: frequency series (Hz) 13 | :param dict P: parameter dictionary 14 | :param str model: radiation model 15 | :param float rtol: relative tolerance 16 | :param int max_iter: adaptive integration maximum iteration number. 17 | :param int force_return: return the result without throwing an error even if the adaptive integration doesn't achieve the desired accuracy after the maximum iteration number. 18 | :return: flux density (mJy) 19 | 20 | If `t` is a 1D numpy.array and `nu` as a scalar, a light curve is generated. If `t` is a scalar and `nu` as a 1D numpy.array, a spectrum is generated. `t` and `nu` can also be 1D numpy.array of the same length, which is useful in data fitting where different data points have different frequency. 21 | 22 | The parameter dictionary `P` must be compatible with the radiation model (to be explained in the next section). For the default model `model="sync"` the required keyword parameters are `eps_e` (:math:`\epsilon_{\rm e}`), `eps_b` (:math:`\epsilon_{\rm B}`), `p`, `theta_v` (:math:`\theta_{\rm obs}`), `d` (luminosity distance), and `z` (redshift). 23 | 24 | Frequency integrated flux 25 | ------------------------- 26 | .. py:method:: .Flux(t, nu1, nu2, P, model='sync', rtol=1e-3, max_iter=100, force_return=True) 27 | 28 | Calculate the frequency integrated flux. 29 | 30 | :param np.array/float nu1: frequency integration lower limit (Hz) 31 | :param np.array/float nu2: frequency integration upper limit (Hz) 32 | :return: flux (erg/s/cm^2) 33 | 34 | Apparent superluminal motion 35 | ---------------------------- 36 | The flux centroid offset can calculated by the following method 37 | 38 | .. py:method:: .Offset(t, nu, P, model='sync', rtol=1e-3, max_iter=100, force_return=True) 39 | 40 | Calculate the flux centroid offset. 41 | 42 | :return: flux centroid offset (MAS) 43 | 44 | Image size 45 | ---------- 46 | .. py:method:: .SizeX(t, nu, P, model='sync', rtol=1e-3, max_iter=100, force_return=True) 47 | 48 | Calculate the Gaussian equivalent 1-sigma image size along the jet axis. 49 | 50 | :return: x direction image size (MAS) 51 | 52 | .. py:method:: .SizeY(t, nu, P, model='sync', rtol=1e-3, max_iter=100, force_return=True) 53 | 54 | Calculate the Gaussian equivalent 1-sigma image size perpendicular to the jet axis. 55 | 56 | :return: y direction image size (MAS) 57 | 58 | Sky map 59 | ------- 60 | .. py:method:: .IntensityOfPixel(t, nu, x_offset, y_offset, P, model="sync") 61 | 62 | The intensity of a "pixel" in the image. 63 | 64 | :param float t: time series (s) 65 | :param float nu: frequency series (Hz) 66 | :param np.array x_offset: offset of the pixel along jet axis direction (MAS). 67 | :param np.array y_offset: offset of the pixel perpendicular to the jet axis direction (MAS). 68 | :param dict P: parameter dictionary 69 | :param str model: radiation model -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # -- Project information 4 | 5 | project = 'jetsimpy' 6 | copyright = '2024, Hao Wang' 7 | author = 'Hao Wang' 8 | 9 | release = '0.5.0' 10 | version = '0.5.0' 11 | 12 | # -- General configuration 13 | 14 | extensions = [ 15 | 'sphinx.ext.duration', 16 | 'sphinx.ext.doctest', 17 | 'sphinx.ext.autodoc', 18 | 'sphinx.ext.autosummary', 19 | 'sphinx.ext.intersphinx', 20 | ] 21 | 22 | intersphinx_mapping = { 23 | 'python': ('https://docs.python.org/3/', None), 24 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 25 | } 26 | intersphinx_disabled_domains = ['std'] 27 | 28 | templates_path = ['_templates'] 29 | 30 | # -- Options for HTML output 31 | 32 | html_theme = 'sphinx_rtd_theme' 33 | 34 | # -- Options for EPUB output 35 | epub_show_urls = 'footnote' 36 | -------------------------------------------------------------------------------- /docs/source/custom.rst: -------------------------------------------------------------------------------- 1 | Customized Radiation Models 2 | =========================== 3 | It's also possible to define your own radiation model in `jetsimpy`. To do so, you need to define the solid angle integrated specific intensity (:math:`I_{\nu}`) of each fluid element in the comoving frame. You may find the defination in Rybicki & Lightman. This value can be calculated by :math:`I_\nu=j_\nu \Delta R`, where :math:`j_\nu` is the isotropic emissivity, and :math:`\Delta R` is the blast shell width in the coming frame (defined as ``blast.dR`` in the code). 4 | 5 | There are two ways to define the radiation model. The pure Python way and the C++ way. The first way is very convinient. You don't have to learn C++, and you don't need to compile the code everytime after you change your model. However the code will be 10x slower than the second way. In the second way you need to define the intensity by a C++ function, and you will have to install the code every time you make any modifications. 6 | 7 | The best practice is to do the Python way to quickly prototype and test your model. Then you can do the C++ way in actual MCMC fitting. 8 | 9 | The Python way 10 | -------------- 11 | First, you need to define a function describing the specific intensity of a fluid element with the following functional form:: 12 | 13 | def custom_intensity(nu, P, blast): 14 | ... 15 | return intensity 16 | 17 | This function must strictly take three parameters: ``nu`` the frequency, ``P`` the parameter dictionary, and ``blast`` the object to access the fluid properties. 18 | 19 | To apply this model in calculating the flux density, you just need to :: 20 | 21 | jet.FluxDensity(t, nu, P, model=custom_intensity) 22 | 23 | An example of customized radiation model can be found in the example folder of the source code. 24 | 25 | When defining your radiation model, the specific intensity may depend on the properties of the shock. For example, the cooling frequency of the spectrum depends on the evolution time scale :math:`t`, and the optical depth depends on the shell thickness :math:`\Delta R`. These values can be accessed from the function argument ``blast``. Currently, the following shock properties are supported:: 26 | 27 | # coordinate values (burster frame) 28 | blast.t # time since burst 29 | blast.theta # polar angle of the fluid element 30 | blast.phi # azimuthal angle of the fluid element 31 | blast.R # Radius of the fluid element 32 | 33 | # blast velocity (burster frame) 34 | blast.beta # (post-shock) velocity 35 | blast.gamma # (post-shock) Lorentz factor 36 | blast.beta_th # (post-shock) polar velocity component 37 | blast.beta_r # (post-shock) radial velocity component 38 | blast.beta_f # (forward shock) velocity 39 | blast.gamma_f # (forward shock) Lorentz factor 40 | blast.s # calibration coefficient 41 | blast.doppler # Doppler factor 42 | 43 | # thermodynamic values (comoving frame) 44 | blast.n_blast # (post-shock) number density 45 | blast.e_density # (post-shock) energy density (rest mass excluded) 46 | blast.pressure # (post-shock) pressure 47 | blast.dR # shell width 48 | 49 | # others (burster frame) 50 | blast.n_ambient # external number density 51 | 52 | The C++ way 53 | ----------- 54 | The C++ way is fundimentally similar to the Python way, where you need to define a "lambda function" about the specific intensity of the fluid element. You can define your model in the file "jetsimpy/src/Afterglow/models.cpp". In this file there is a function:: 55 | 56 | void Models::registerIntensity() { 57 | ... 58 | } 59 | 60 | where you can **define your radiation model as a lambda function in the function body**. All radiation models are saved in a dictionary ``radiation_models`` with a corresponding keyword. To add your own model, you must define a function with the following form:: 61 | 62 | radiation_models["model_keyword"] = [&](const double nu, const Dict& P, const Blast& blast) { 63 | ... 64 | return intensity; 65 | }; 66 | 67 | where ``"model_keyword"`` is a string specified by the user to identify the model. The arguments of this function are defined in the same way as in the Python way. 68 | 69 | After you have defined your model, you must install the code again (described in the starting page) to make it work. To apply this model in calculating the flux density, you just need to :: 70 | 71 | jet.FluxDensity(t, nu, P, model="model_keyword") 72 | 73 | In fact, all built-in models are defined in the same way, where you may find them helpful. -------------------------------------------------------------------------------- /docs/source/hydro.rst: -------------------------------------------------------------------------------- 1 | Hydrodynamic Simulation 2 | ======================= 3 | The hydrodynamic evolution of a jet with a given angular profile can be solved by creating an object `jetsimpy.Jet`. Below is 4 | 5 | simulation 6 | ---------- 7 | .. py:class:: jetsimpy.Jet(profile, nwind, nism, tmin=10, tmax=3.2e9, grid=jetsimpy.Uniform(257), tail=True, spread=True, cal_level=1, rtol=1e-6, cfl=0.9) 8 | 9 | Create an object containing the simulation results. 10 | 11 | :param tuple profile: a tuple of the tabulated data of the jet angular profile taking the form of `(theta, Eiso, lf)`. `theta` is the polar angle array starting from 0 and end with pi. `Eiso` is the isotropic equivalent energy. `lf` is the Lorentz factor. All three elements must be 1D `np.array` of the same length. 12 | :param float nwind: wind density scale. 13 | :param float nism: ism density scale. 14 | :param float tmin: start time of the simulation. 15 | :param float tmax: end time of the simulation. 16 | :param np.array grid: the polar angles of the "cell edges" of the simulation. This parameter specifies the resolution of the simulation. 17 | :param bool tail: Add an low-energy isotropic tail or not. This tail ensures that the energy is positive and the Lorentz factor is above unity. 18 | :param bool spread: turning on the spreading or not. 19 | :param int cal_level: calibration level. "`cal_level=0`": no calibration. "`cal_level=1`": calibrate with Blandford-McKee solutions throughout the simulation. "`cal_level=2`": calibrate with Blandford-McKee solution in the relativistic limit and Sedov-Taylor solution in the Newtonian limit. Setting `cal_level=2` might be aggressive since the thin shell approximation is no longer valid in the Newtonian limit. 20 | :param float rtol: relative tolerance in the primitive variable solver. **Never change this parameter unless you know what is going on.** 21 | :param float cfl: the CFL number. **Never change this parameter unless you know what is going on.** 22 | 23 | jet profile 24 | ----------- 25 | The jet profile can be arbitrary but must be physically reasonable. For general purpose, users should always provide tabulated data even if the profile is analytic. To save time, the code provides some built-in profiles. 26 | 27 | .. py:function:: jetsimpy.TopHat(theta_c, Eiso, lf0=1e100) 28 | 29 | A top-hat jet profile. 30 | 31 | :param float theta_c: half-opening angle (rad). 32 | :param float Eiso: isotropic equivalent energy. 33 | :param lf0: initial Lorentz factor. 34 | :return: a tuple (theta, Eiso, lf) 35 | 36 | .. py:function:: jetsimpy.Gaussian(theta_c, Eiso, lf0=1e100) 37 | 38 | A Gaussian jet profile defined by 39 | 40 | .. math:: 41 | E(\theta) &= E_{\rm iso} \exp\left[{-\frac{1}{2}\left(\frac{\theta}{\theta_{\rm c}}\right)^2}\right] 42 | 43 | \Gamma(\theta) &= (\Gamma_0 - 1)\exp\left[{-\frac{1}{2}\left(\frac{\theta}{\theta_{\rm c}}\right)^2}\right] + 1 44 | 45 | :param float theta_c: half-opening angle (:math:`\theta_{\rm c}`). 46 | :param float Eiso: isotropic equivalent energy (:math:`E_{\rm iso}`). 47 | :param float lf0: initial Lorentz factor (:math:`\Gamma_0`). 48 | :return: a tuple (theta, Eiso, lf) 49 | 50 | .. py:function:: jetsimpy.PowerLaw(theta_c, Eiso, lf0=1e100, s=4.0) 51 | 52 | A power-law jet profile defined by 53 | 54 | .. math:: 55 | E(\theta) &= E_{\rm iso} \left[1 + \left(\frac{\theta}{\theta_{\rm c}}\right)^2\right]^{-s/2} 56 | 57 | \Gamma(\theta) &=(\Gamma_0 - 1)\left[1 + \left(\frac{\theta}{\theta_{\rm c}}\right)^2\right]^{-s/2} + 1 58 | 59 | :param float theta_c: half-opening angle (:math:`\theta_{\rm c}`). 60 | :param float Eiso: isotropic equivalent energy (:math:`E_{\rm iso}`). 61 | :param float lf0: initial Lorentz factor (:math:`\Gamma_0`). 62 | :param float s: slope (:math:`s`). 63 | :return: a tuple (theta, Eiso, lf) 64 | 65 | coasting phase 66 | -------------- 67 | To turn off the coasting phase, just set a very large (but finite) initial Lorentz factor such as :math:`10^{100}`. 68 | 69 | external density 70 | ---------------- 71 | The external density is assumed to follow the profile below. 72 | 73 | .. math:: 74 | n = n_{\rm ism} + n_{\rm wind}(r/10^{17}{\rm cm})^{-2} 75 | 76 | start and end time 77 | ------------------ 78 | The start time `tmin` is non-zero because the initial radius must be positive. The blastwave will coast at the initial speed until `tmin`, and then the simulation starts. Physically `tmin` can be set to a value around the photospherical radius (timescale). A moderate value such as `tmin=10` is preferred. 79 | 80 | The simulation ends at `tmax`. This value should be sufficiently large to cover the time period of afterglow modeling. 81 | 82 | resolution 83 | ---------- 84 | In a finite volume method, the simulation domain is discretized into a number of cells. In this code the discretization is determined by the `grid` parameter. The `grid` parameter is a 1D numpy array of the "cell edges". Namely, the cell centers locate at the middle of adjacent elements. 85 | 86 | The resolution directly determines the speed of the code. The optimal way to choose the resolution is to set a high resolution inside the jet core and low resolution outside the core. However, to maintain numerical stability, the size of adjacent cells shouldn't change dramatically. 87 | 88 | The code provides some built-in resolution setup for jet profiles with well-defined opening angles. 89 | 90 | .. py:function:: jetsimpy.Uniform(theta_c, npoints) 91 | 92 | Uniform cell distribution. A conservative choice for all cases. 93 | 94 | :param float theta_c: half-opening angle 95 | :param int npoints: number of cell edges 96 | :return: np.array: array of cell edges 97 | 98 | .. py:function:: jetsimpy.ForwardJetRes(theta_c, npoints) 99 | 100 | Resolution optimized for forward-jet only. 101 | 102 | :param float theta_c: half-opening angle 103 | :param int npoints: number of cell edges 104 | :return: np.array: array of cell edges 105 | 106 | .. py:function:: jetsimpy.CounterJetRes(theta_c, npoints) 107 | 108 | Resolution optimized for counter-jet only. 109 | 110 | :param float theta_c: half-opening angle 111 | :param int npoints: number of cell edges 112 | :return: np.array: array of cell edges 113 | 114 | .. py:function:: jetsimpy.ForwardCounterJetRes(theta_c, npoints) 115 | 116 | Resolution optimized for forward-jet and counter-jet. 117 | 118 | :param float theta_c: half-opening angle 119 | :param int npoints: number of cell edges 120 | :return: np.array: array of cell edges 121 | 122 | simulation results 123 | ------------------ 124 | One can access the simulation data from the `jetsimpy.Jet` object. 125 | 126 | .. py:property:: .t_pde 127 | 128 | time data (1d numpy.array) 129 | 130 | .. py:property:: .y_pde 131 | 132 | primitive variable data with the shape (5, ntheta, ntime). The 5 variables are [:math:`M_{\rm sw}`, :math:`M_{\rm ej}`, :math:`\beta^2\gamma^2`, :math:`\beta_{\theta}`, :math:`R`]. 133 | 134 | .. py:property:: .theta_pde 135 | 136 | array of cell centers 137 | 138 | .. py:method:: .dE0_dOmega(t, theta) 139 | 140 | The interpolated energy profile (rest mass excluded). 141 | 142 | :param numpy.array t: time 143 | :param numpy.array theta: polar angle 144 | 145 | .. py:method:: .dMsw_dOmega(t, theta) 146 | 147 | The interpolated swept-up mass profile. 148 | 149 | :param numpy.array t: time 150 | :param numpy.array theta: polar angle 151 | 152 | .. py:method:: .dMej_dOmega(t, theta) 153 | 154 | The interpolated ejecta mass profile. 155 | 156 | :param numpy.array t: time 157 | :param numpy.array theta: polar angle 158 | 159 | .. py:method:: .beta_gamma(t, theta) 160 | 161 | The interpolated 4-velocity profile. 162 | 163 | :param numpy.array t: time 164 | :param numpy.array theta: polar angle 165 | 166 | .. py:method:: .beta_theta(t, theta) 167 | 168 | The interpolated tangent velocity profile. 169 | 170 | :param numpy.array t: time 171 | :param numpy.array theta: polar angle 172 | 173 | .. py:method:: .R(t, theta) 174 | 175 | The interpolated Radius angular profile. 176 | 177 | :param numpy.array t: time 178 | :param numpy.array theta: polar angle -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | 4 | quickstart 5 | hydro 6 | afterglow 7 | custom 8 | weighted 9 | 10 | jetsimpy 11 | ======== 12 | **jetsimpy** is a Gamma-ray burst afterglows modeling tool designed for arbitrary angular energy and Lorentz factor profile. It includes the hydrodynamic simulation of the relativistic jet, and the synchrotron radiation on top of it. 13 | 14 | Installation 15 | ============ 16 | The code is not published to PyPI yet, so please install it from source:: 17 | 18 | $ pip install . && python setup.py clean 19 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | Analytic jet profiles 5 | --------------------- 6 | The program supports an easy way to calculate the light curve or spectrum if the jet profile is Top-hat, Gaussian, or Power-law. 7 | 8 | For Top-hat jets:: 9 | 10 | # put the parameters in a dictionary 11 | P = dict( 12 | Eiso = 1e52, # Core isotropic equivalent energy 13 | lf = 300, # Core Lorentz factor 14 | theta_c = 0.1, # half opening angle 15 | n0 = 1, # ism number density 16 | A = 0, # wind number density amplitude 17 | eps_e = 0.1, # epsilon_e 18 | eps_b = 0.01, # epsilon_b 19 | p = 2.17, # electron power index 20 | theta_v = 0.0, # viewing angle (rad) 21 | d = 474.33, # distance (Mpc) 22 | z = 0.1, # redshift 23 | ) 24 | 25 | # define the observing time and frequency 26 | tday = np.logspace(-3, 3, 100) 27 | tsecond = tday * 3600 * 24 28 | nu = 1e18 29 | 30 | # flux density 31 | fd_tophat = jetsimpy.FluxDensity_tophat(tsecond, nu, P) 32 | 33 | For Gaussian jets:: 34 | 35 | # put the parameters in a dictionary 36 | P = dict( 37 | Eiso = 1e52, # Core isotropic equivalent energy 38 | lf = 300, # Core Lorentz factor 39 | theta_c = 0.1, # half opening angle 40 | n0 = 1, # ism number density 41 | A = 0, # wind number density amplitude 42 | eps_e = 0.1, # epsilon_e 43 | eps_b = 0.01, # epsilon_b 44 | p = 2.17, # electron power index 45 | theta_v = 0.0, # viewing angle (rad) 46 | d = 474.33, # distance (Mpc) 47 | z = 0.1, # redshift 48 | ) 49 | 50 | # define the observing time and frequency 51 | tday = np.logspace(-3, 3, 100) 52 | tsecond = tday * 3600 * 24 53 | nu = 1e18 54 | 55 | # flux density 56 | fd_gaussian = jetsimpy.FluxDensity_gaussian(tsecond, nu, P) 57 | 58 | For power-law jets:: 59 | 60 | # put the parameters in a dictionary 61 | P = dict( 62 | Eiso = 1e52, # Core isotropic equivalent energy 63 | lf = 300, # Core Lorentz factor 64 | theta_c = 0.1, # half opening angle 65 | n0 = 1, # ism number density 66 | A = 0, # wind number density amplitude 67 | eps_e = 0.1, # epsilon_e 68 | eps_b = 0.01, # epsilon_b 69 | p = 2.17, # electron power index 70 | theta_v = 0.0, # viewing angle (rad) 71 | d = 474.33, # distance (Mpc) 72 | z = 0.1, # redshift 73 | s = 6, # power-law jet slope 74 | ) 75 | 76 | # define the observing time and frequency 77 | tday = np.logspace(-3, 3, 100) 78 | tsecond = tday * 3600 * 24 79 | nu = 1e18 80 | 81 | # flux density 82 | fd_powerlaw = jetsimpy.FluxDensity_powerlaw(tsecond, nu, P) 83 | 84 | Tabulated jet profiles 85 | ---------------------- 86 | For tabulated jet profiles, you will need two steps to calculate the GRB observables. The first step is to simulate the hydrodynamic evolution of a jet. The second step is to calculate the afterglow observables on top of the simulation results. 87 | 88 | Below is an example:: 89 | 90 | import numpy as np 91 | import jetsimpy 92 | 93 | # generate some tabulated jet profiles 94 | theta = np.linspace(0, np.pi, 10000) # polar angles 95 | Eiso = 1e53 * np.exp(- 0.5 * (theta / 0.1) ** 2) # isotropic equivalent energy 96 | lf = (1000 - 1) * np.exp(- 0.5 * (theta / 0.1) ** 2) + 1 # initial Lorentz factor 97 | 98 | # parameter dictionary 99 | P = dict( 100 | eps_e = 0.1, # epsilon_e 101 | eps_b = 0.01, # epsilon_b 102 | p = 2.17, # electron power index 103 | theta_v = 0.5, # viewing angle (rad) 104 | d = 474.33, # luminosity distance (Mpc) 105 | z = 0.1, # redshift 106 | ) 107 | 108 | # (step 1) hydro simulation 109 | jet = jetsimpy.Jet( 110 | (theta, Eiso, lf), # specify the jet profile here 111 | 0.0, # wind number density scale 112 | 1, # ism number density scale 113 | grid=jetsimpy.ForwardJetRes(0.1, 129), # simulation resolution 114 | ) 115 | 116 | # (step 2) flux density [mJy] 117 | tday = np.logspace(-2, 3, 100) # observing time in day 118 | tsecond = tday * 3600 * 24 # observing time in second 119 | nu = 1e15 # observing frequency 120 | 121 | flux_density = jet.FluxDensity( 122 | tsecond, # [second] observing time span 123 | nu, # [Hz] observing frequency 124 | P, # parameter dictionary 125 | ) 126 | 127 | Tips on the resolution 128 | ---------------------- 129 | The resolution setup is specified by the ``grid`` keyword which takes an array ranging from 0 to :math:`\pi`. 130 | 131 | For any jet profile, the resolution is always **extremely important**. On one hand, if the specified resolution is too sparse to reveal the details of the jet profile evolution, the simulation will not converge. On the other hand, if the resolution is too dense, the simulation will be very time costly. 132 | An optimal setup is to specify high resolution around the jet core and relatively low resolution outside the core. 133 | 134 | For a jet profile where a half opening angle is well-defined, `jetsimpy` provides the following resolution setups which have been widely tested for both reliability and speed:: 135 | 136 | jetsimpy.ForwardJetRes(theta_c, n) # for forward jet 137 | jetsimpy.CounterJetRes(theta_c, n) # for counter jet 138 | jetsimpy.ForwardCounterJetRes(theta_c, n) # for both forward and counter jet 139 | 140 | where ``n`` is the number of grid points. Details of the resolution can be found in the next section. 141 | 142 | For arbitrary jet profiles, unfortunately, there is no universal solution. The default resolution is uniform, but remember this setup is in general **NOT** optimal. It is strongly recomended that users specify their own resolution. -------------------------------------------------------------------------------- /docs/source/weighted.rst: -------------------------------------------------------------------------------- 1 | Weighted Average 2 | ================ 3 | The code further provides an interface to calculate the weighted average of any customized value over the blast surface, where the weight is :math:`dL/d\Omega`. This utility is provided because most observables of GRB afterglows are fundimentally weighted average over the blast surface. 4 | 5 | Similar to the way you define your radiation model, you can define it in the Python way or in the C++ way. 6 | 7 | -------------------------------------------------------------------------------- /examples/centroid_motion.py: -------------------------------------------------------------------------------- 1 | # Apparent superluminal motion of a gaussian jet 2 | 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | import jetsimpy 6 | 7 | # put the parameters in a dictionary 8 | P = dict( 9 | Eiso = 1e52, # (Jet) Isotropic equivalent energy 10 | lf = 300, # (Jet) Lorentz factor 11 | theta_c = 0.1, # (Jet) half opening angle 12 | n0 = 1, # (ISM) constant number density 13 | A = 0, # (ISM) wind amplitude 14 | eps_e = 0.1, # (Radiation) epsilon_e 15 | eps_b = 0.01, # (Radiation) epsilon_b 16 | p = 2.17, # (Radiation) electron power index 17 | theta_v = 0.4, # (Radiation) viewing angle 18 | d = 474.33, # (radiation) distance (Mpc) 19 | z = 0.1, # (radiation) redshift 20 | ) 21 | 22 | # ---------- (step 1) hydro simulation of the jet ---------- # 23 | 24 | # jet without spreading 25 | jet1 = jetsimpy.Jet( 26 | jetsimpy.Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 27 | P["A"], # scale of wind density 28 | P["n0"], # constant number density 29 | spread=False, # w/wo spreading effect 30 | grid=jetsimpy.ForwardJetRes(P["theta_c"], 129), 31 | ) 32 | 33 | # jet with spreading 34 | jet2 = jetsimpy.Jet( 35 | jetsimpy.Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 36 | P["A"], # scale of wind density 37 | P["n0"], # constant number density 38 | spread=True, # w/wo spreading effect 39 | grid=jetsimpy.ForwardJetRes(P["theta_c"], 129), 40 | ) 41 | 42 | # ---------- (step 2) calculate centroid motion ---------- # 43 | 44 | # define the observing time and frequency 45 | tday = np.logspace(-2, 3, 100) 46 | tsecond = tday * 3600 * 24 47 | nu = 3e9 48 | 49 | # calculate the afterglow centroid motion (unit: mas) 50 | offset1 = jet1.Offset( 51 | tsecond, # [second] observing time span 52 | nu, # [Hz] observing frequency 53 | P, # parameter dictionary for radiation 54 | ) 55 | 56 | offset2 = jet2.Offset( 57 | tsecond, # [second] observing time span 58 | nu, # [Hz] observing frequency 59 | P, # parameter dictionary for radiation 60 | ) 61 | 62 | # calculate the speed of light motion 63 | MPC = 3.086e24 64 | MAS = 4.848e-9 65 | offset_c = 3e10 * tsecond / P["d"] / MPC / (1.0 + P["z"]) / (1.0 + P["z"]) / MAS 66 | 67 | # plot the light curves 68 | plt.plot(tday, offset1, label="without spreading", color="black", linestyle="--") 69 | plt.plot(tday, offset2, label="with spreading", color="black") 70 | plt.plot(tday, offset_c, label="speed of light", color="black", linestyle=":") 71 | plt.xlim(0, 1e3) 72 | plt.ylim(0, 0.3) 73 | plt.xlabel("time [day]") 74 | plt.ylabel("Centroid Offset [mas]") 75 | plt.legend() 76 | plt.show() -------------------------------------------------------------------------------- /examples/customized_radiation_model.py: -------------------------------------------------------------------------------- 1 | # define your own radiation model in a pure Python way 2 | 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | import jetsimpy 6 | 7 | # some constants 8 | CSpeed = 29979245800.0 9 | MassP = 1.672622e-24 10 | MassE = 9.109384e-28 11 | SigmaT = 6.6524587e-25 12 | ECharge = 4.803204673e-10 13 | 14 | # put the parameters in a dictionary 15 | P = dict( 16 | Eiso = 1e52, # Isotropic equivalent energy 17 | lf = 300, # Lorentz factor 18 | theta_c = 0.1, # half opening angle 19 | n0 = 1, # ism number density 20 | A = 0, # wind number density amplitude 21 | eps_e = 0.1, # epsilon_e 22 | eps_b = 0.01, # epsilon_b 23 | p = 2.17, # electron power index 24 | theta_v = 0.4, # viewing angle (rad) 25 | d = 474.33, # distance (Mpc) 26 | z = 0.1, # redshift 27 | ) 28 | 29 | # synchrotron radiation with the deep Newtonian phase described in Sironi & Giannios 2013 30 | def sync_dnp(nu, Psync, blast): 31 | eps_e = Psync["eps_e"] # epsilon_e 32 | eps_b = Psync["eps_b"] # epsilon_B 33 | p = Psync["p"] # electron power index 34 | 35 | n_blast = blast.n_blast # post-shock number density 36 | t = blast.t # time since burst 37 | gamma = blast.gamma # Lorentz factor of the blast 38 | e = blast.e_density # post-shock energy density (rest mass excluded) 39 | 40 | gamma_m = (p - 2.0) / (p - 1.0) * (eps_e * MassP / MassE * (gamma - 1.0)) 41 | f = 1 42 | if gamma_m <= 1: # if gamma_m < 1, truncate the electron population 43 | gamma_m = 1.0 44 | f = (p - 2.0) / (p - 1.0) * eps_e * MassP / MassE * (gamma - 1.0) / gamma_m 45 | 46 | B = np.sqrt(8.0 * np.pi * eps_b * e) 47 | gamma_c = 6.0 * np.pi * MassE * gamma * CSpeed / SigmaT / B / B / t 48 | nu_m = 3.0 * ECharge * B * gamma_m * gamma_m / 4.0 / np.pi / CSpeed / MassE 49 | nu_c = 3.0 * ECharge * B * gamma_c * gamma_c / 4.0 / np.pi / CSpeed / MassE 50 | e_p = f * np.sqrt(3.0) * ECharge * ECharge * ECharge * B * n_blast / MassE / CSpeed / CSpeed 51 | 52 | if nu_m < nu_c: 53 | if nu < nu_m: 54 | emissivity = e_p * np.cbrt(nu / nu_m) 55 | elif nu < nu_c: 56 | emissivity = e_p * np.power(nu / nu_m, - (p - 1) / 2.0) 57 | else: 58 | emissivity = e_p * np.power(nu_c / nu_m, - (p - 1) / 2.0) * np.power(nu / nu_c, - p / 2) 59 | else: 60 | if nu < nu_c: 61 | emissivity = e_p * np.cbrt(nu / nu_c) 62 | elif nu < nu_m: 63 | emissivity = e_p / np.sqrt(nu / nu_c) 64 | else: 65 | emissivity = e_p / np.sqrt(nu_m / nu_c) * np.power(nu / nu_m, - p / 2) 66 | 67 | isotropic_intensity = emissivity * blast.dR 68 | return isotropic_intensity 69 | 70 | # define the observing time and frequency 71 | tday = np.logspace(-2, 4, 100) 72 | tsecond = tday * 3600 * 24 73 | nu = 3e9 74 | 75 | # hydro simulation 76 | jet = jetsimpy.Jet( 77 | jetsimpy.Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 78 | 0.0, # wind number density scale 79 | P["n0"], # ism number density scale 80 | tmax=1e11, 81 | grid=jetsimpy.ForwardJetRes(P["theta_c"], 129) 82 | ) 83 | 84 | # flux density (default synchrotron model) 85 | flux_density1 = jet.FluxDensity( 86 | tsecond, # [second] observing time span 87 | nu, # [Hz] observing frequency 88 | P, # parameter dictionary 89 | rtol=1e-3, 90 | model="sync" 91 | ) 92 | 93 | # flux density (deep Newtonian phase) 94 | flux_density2 = jet.FluxDensity( 95 | tsecond, # [second] observing time span 96 | nu, # [Hz] observing frequency 97 | P, # parameter dictionary 98 | rtol=1e-3, 99 | model=sync_dnp 100 | ) 101 | 102 | # plot the light curves 103 | plt.plot(tday, flux_density1, label="default synchrotron", color="black", linestyle="--") 104 | plt.plot(tday, flux_density2, label="Deep Newtonian Phase", color="black") 105 | plt.xlim(1e-2, 1e4) 106 | plt.ylim(1e-5, 1e1) 107 | plt.xscale("log") 108 | plt.yscale("log") 109 | plt.xlabel("time [day]") 110 | plt.ylabel("flux density [mJy]") 111 | plt.legend() 112 | plt.show() -------------------------------------------------------------------------------- /examples/light_curves.py: -------------------------------------------------------------------------------- 1 | # light curves of a gaussian jet 2 | 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | import jetsimpy 6 | 7 | # put the parameters in a dictionary 8 | P = dict( 9 | Eiso = 1e52, # Isotropic equivalent energy 10 | lf = 300, # Lorentz factor 11 | theta_c = 0.1, # half opening angle 12 | n0 = 1, # ism number density 13 | A = 0, # wind number density amplitude 14 | eps_e = 0.1, # epsilon_e 15 | eps_b = 0.01, # epsilon_b 16 | p = 2.17, # electron power index 17 | theta_v = 0.4, # viewing angle (rad) 18 | d = 474.33, # distance (Mpc) 19 | z = 0.1, # redshift 20 | ) 21 | 22 | # ---------- (step 1) hydro simulation of the jet ---------- # 23 | 24 | # jet without spreading 25 | jet1 = jetsimpy.Jet( 26 | jetsimpy.Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 27 | P["A"], # wind number density scale 28 | P["n0"], # ism number density scale 29 | spread=False, # w/wo spreading effect 30 | grid=jetsimpy.ForwardJetRes(P["theta_c"], 129) # resolution 31 | ) 32 | 33 | # jet with spreading (show full argument and keyword list) 34 | theta, Eiso, lf = jetsimpy.Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]) 35 | jet2 = jetsimpy.Jet( 36 | (theta, Eiso, lf), # [tuple of tabulated data]: (polar angles, rest mass excluded energy, Lorentz factor) 37 | P["A"], # [wind density scale]: n = nwind * (r / 1e17)^-2 + nism (cm^-3) 38 | P["n0"], # [ism density scale]: n = nwind * (r / 1e17)^-2 + nism (cm^-3) 39 | tmin=10.0, # [simulation start time]: (s) 40 | tmax=3.2e9, # [simulation end time]: (s) 41 | grid=jetsimpy.ForwardJetRes(P["theta_c"], 129), # [cell edge angles]: must start with 0 and end with pi. 42 | tail=True, # [isotropic tail]: add an extremely low energy low velocity isotropic tail for safty 43 | spread=True, # w/wo spreading effect 44 | cal_level=1, # [calibration level]: 0: no calibration. 1: BM all time. 2: smoothly go from BM to ST (dangerous) 45 | rtol=1e-6, # [primitive variable solver tolerance]: Don't change it unless you know what is going on. 46 | cfl=0.9, # [cfl number]: Don't change it unless you know what is going on. 47 | ) 48 | 49 | # ---------- (step 2) calculate flux density ---------- # 50 | 51 | # define the observing time and frequency 52 | tday = np.logspace(-2, 3, 100) 53 | tsecond = tday * 3600 * 24 54 | nu = 3e9 55 | 56 | # calculate the afterglow flux density (unit: mJy) 57 | flux1 = jet1.FluxDensity( 58 | tsecond, # [second] observing time span 59 | nu, # [Hz] observing frequency 60 | P, # parameter dictionary 61 | ) 62 | 63 | # show full keyword list 64 | flux2 = jet2.FluxDensity( 65 | tsecond, # [second] observing time span 66 | nu, # [Hz] observing frequency 67 | P, # parameter dictionary 68 | model="sync", # radiation model 69 | rtol=1e-3, # integration tolerance 70 | max_iter=100, 71 | force_return=True 72 | ) 73 | 74 | # plot the light curves 75 | plt.plot(tday, flux1, label="without spreading", color="black", linestyle="--") 76 | plt.plot(tday, flux2, label="with spreading", color="black") 77 | plt.xlim(1e-2, 1e3) 78 | plt.ylim(1e-5, 1e1) 79 | plt.xscale("log") 80 | plt.yscale("log") 81 | plt.xlabel("time [day]") 82 | plt.ylabel("flux density [mJy]") 83 | plt.legend() 84 | plt.show() -------------------------------------------------------------------------------- /examples/quick_start.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import pyplot as plt 3 | import jetsimpy 4 | 5 | # put the parameters in a dictionary 6 | P = dict( 7 | Eiso = 1e52, # Core isotropic equivalent energy 8 | lf = 300, # Core Lorentz factor 9 | theta_c = 0.1, # half opening angle 10 | n0 = 1, # ism number density 11 | A = 0, # wind number density amplitude 12 | eps_e = 0.1, # epsilon_e 13 | eps_b = 0.01, # epsilon_b 14 | p = 2.17, # electron power index 15 | theta_v = 0.0, # viewing angle (rad) 16 | d = 474.33, # distance (Mpc) 17 | z = 0.1, # redshift 18 | s = 6, # power-law jet slope (required for power-law jet) 19 | ) 20 | 21 | # define the observing time and frequency 22 | tday = np.logspace(-3, 3, 100) 23 | tsecond = tday * 3600 * 24 24 | nu = 1e18 25 | 26 | # flux density 27 | fd_tophat = jetsimpy.FluxDensity_tophat(tsecond, nu, P) 28 | fd_gaussian = jetsimpy.FluxDensity_gaussian(tsecond, nu, P) 29 | fd_powerlaw = jetsimpy.FluxDensity_powerlaw(tsecond, nu, P) 30 | 31 | plt.plot(tday, fd_tophat, label="Top-Hat") 32 | plt.plot(tday, fd_gaussian, label="Gaussian") 33 | plt.plot(tday, fd_powerlaw, label="Power-law") 34 | 35 | plt.xscale("log") 36 | plt.yscale("log") 37 | plt.xlabel("t [day]") 38 | plt.ylabel(r"$F_\nu$ [mJy]") 39 | plt.legend() 40 | plt.show() -------------------------------------------------------------------------------- /examples/sky_map.py: -------------------------------------------------------------------------------- 1 | # sky map of a gaussian jet 2 | 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | import jetsimpy 6 | from matplotlib.patches import Ellipse 7 | 8 | # put the parameters in a dictionary 9 | P = dict( 10 | Eiso = 1e52, # (Jet) Isotropic equivalent energy 11 | lf = 300, # (Jet) Lorentz factor 12 | theta_c = 0.1, # (Jet) half opening angle 13 | n0 = 1, # (ISM) constant number density 14 | A = 0, # (ISM) wind amplitude 15 | eps_e = 0.1, # (Radiation) epsilon_e 16 | eps_b = 0.01, # (Radiation) epsilon_b 17 | p = 2.17, # (Radiation) electron power index 18 | theta_v = 0.4, # (Radiation) viewing angle 19 | d = 474.33, # (radiation) distance (Mpc) 20 | z = 0.1, # (radiation) redshift 21 | ) 22 | 23 | # ---------- (step 1) hydro simulation of the jet ---------- # 24 | 25 | # solve jet 26 | jet = jetsimpy.Jet( 27 | jetsimpy.Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 28 | P["A"], # scale of wind density 29 | P["n0"], # constant number density 30 | grid=jetsimpy.ForwardJetRes(P["theta_c"], 129), # resolution 31 | ) 32 | 33 | # ---------- (step 2) calculate intensity at some pixels ---------- # 34 | 35 | # time and frequency of sky map 36 | tday = 10 37 | nu = 3e9 38 | tsecond = tday * 3600 * 24 39 | 40 | # make an image with half width: (centroid + 3 * size_x) [unit: mas], 41 | # such that the whole jet is visible in the image 42 | offset = jet.Offset(tsecond, nu, P) 43 | size_x = jet.SizeX(tsecond, nu, P) 44 | size_y = jet.SizeY(tsecond, nu, P) # why? just for fun! 45 | half_width = offset + 3 * size_x 46 | 47 | # intensity map data matrix: resolution 300 x 300 48 | resolution = 300 49 | x_tilde = np.linspace(- half_width, half_width, resolution) # x pixel coordinates 50 | y_tilde = np.linspace(- half_width, half_width, resolution) # y pixel coordinates 51 | X, Y = np.meshgrid(x_tilde, y_tilde) 52 | sky_map = jet.IntensityOfPixel(tsecond, nu, X, Y, P) 53 | 54 | # configurate the plot 55 | fig, ax = plt.subplots() 56 | Inorm = sky_map / sky_map.max() # normalize to 1 57 | Inorm = np.rot90(Inorm, 2) # rotate to align the plt.imshow() extent 58 | im = ax.imshow(Inorm, interpolation='gaussian', cmap="inferno", extent=[-half_width, half_width, -half_width, half_width]) 59 | im.set_clim(vmin=0.0, vmax=1.0) 60 | 61 | # plot the origin, centroid, and [optional] size contour just for fun! (you are welcome to uncomment the line below.) 62 | ax.scatter(0, 0, marker="*", color="white", label="origin") 63 | ax.scatter(offset, 0.0, marker="+", color="white", label="centroid") 64 | #ax.add_patch(Ellipse((offset, 0), size_x * 2, size_y * 2, edgecolor="white", fill=False, linestyle="--", linewidth=1)) 65 | 66 | # show the image! 67 | ax.set_xlabel(r"$\tilde{x}$ [mas]", fontsize=12) 68 | ax.set_ylabel(r"$\tilde{y}$ [mas]", fontsize=12) 69 | ax.legend(loc="upper right", frameon=False, labelcolor="white", fontsize=12, handletextpad=0.1) 70 | fig.tight_layout() 71 | plt.show() -------------------------------------------------------------------------------- /jetsimpy/__init__.py: -------------------------------------------------------------------------------- 1 | from ._jetsimpy import Jet 2 | from ._grid import * 3 | from ._jet_type import * 4 | from ._shortcut import * -------------------------------------------------------------------------------- /jetsimpy/_grid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # equal spacing 4 | def Uniform(npoints): 5 | cells = np.linspace(0.0, np.pi, npoints) 6 | cells[0] = 0.0 7 | cells[-1] = np.pi 8 | return cells 9 | 10 | # cell edges assuming forward-jet 11 | def ForwardJetRes(theta_c, npoints): 12 | arcsinhcells = np.linspace(0, np.arcsinh(np.pi / theta_c), npoints) 13 | cells = np.sinh(arcsinhcells) * theta_c 14 | cells[0] = 0.0 15 | cells[-1] = np.pi 16 | return cells 17 | 18 | # cell edges assuming counter-jet 19 | def CounterJetRes(theta_c, npoints): 20 | cells = ForwardJetRes(theta_c, npoints) 21 | cells = np.pi - cells 22 | cells = np.flip(cells) 23 | cells[0] = 0.0 24 | cells[-1] = np.pi 25 | return cells 26 | 27 | # cell edges assuming forward-jet & counter-jet 28 | def ForwardCounterJetRes(theta_c, npoints): 29 | half_points = int(npoints / 2) + 1 30 | arcsinhcells = np.linspace(0, np.arcsinh(np.pi / theta_c / 2.0), half_points) 31 | cells_n = np.sinh(arcsinhcells) * theta_c * 2.0 32 | if npoints % 2 == 0: # even numbers 33 | cells_n = cells_n / (2.0 - (cells_n[-1] - cells_n[-2]) / np.pi) 34 | cells_s = np.flip(np.pi - cells_n) 35 | cells = np.hstack([cells_n[:-1], cells_s[1:]]) 36 | else: 37 | cells_n /= 2 38 | cells_s = np.flip(np.pi - cells_n) 39 | cells = np.hstack([cells_n, cells_s[1:]]) 40 | cells[0] = 0.0 41 | cells[-1] = np.pi 42 | return cells 43 | 44 | # ------------- same to the above but the naming is not clear enough ------------ # 45 | 46 | # cell edges assuming forward-jet 47 | def NorthPole(theta_c, npoints): 48 | arcsinhcells = np.linspace(0, np.arcsinh(np.pi / theta_c), npoints) 49 | cells = np.sinh(arcsinhcells) * theta_c 50 | cells[0] = 0.0 51 | cells[-1] = np.pi 52 | return cells 53 | 54 | # cell edges assuming counter-jet 55 | def SouthPole(theta_c, npoints): 56 | cells = NorthPole(theta_c, npoints) 57 | cells = np.pi - cells 58 | cells = np.flip(cells) 59 | cells[0] = 0.0 60 | cells[-1] = np.pi 61 | return cells 62 | 63 | # cell edges assuming forward-jet & counter-jet 64 | def BothPoles(theta_c, npoints): 65 | half_points = int(npoints / 2) + 1 66 | arcsinhcells = np.linspace(0, np.arcsinh(np.pi / theta_c / 2.0), half_points) 67 | cells_n = np.sinh(arcsinhcells) * theta_c * 2.0 68 | if npoints % 2 == 0: # even numbers 69 | cells_n = cells_n / (2.0 - (cells_n[-1] - cells_n[-2]) / np.pi) 70 | cells_s = np.flip(np.pi - cells_n) 71 | cells = np.hstack([cells_n[:-1], cells_s[1:]]) 72 | else: 73 | cells_n /= 2 74 | cells_s = np.flip(np.pi - cells_n) 75 | cells = np.hstack([cells_n, cells_s[1:]]) 76 | cells[0] = 0.0 77 | cells[-1] = np.pi 78 | return cells -------------------------------------------------------------------------------- /jetsimpy/_jet_type.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # top-hat jet 4 | def TopHat(theta_c, Eiso, lf0=1e100): 5 | theta = theta = np.linspace(0, np.pi, 10000) 6 | 7 | energy = Eiso * np.ones_like(theta) 8 | energy[theta > theta_c] = 0 9 | 10 | lf = np.ones_like(theta) 11 | lf[theta <= theta_c] = lf0 12 | 13 | return (theta, energy, lf) 14 | 15 | # Gaussian jet 16 | def Gaussian(theta_c, Eiso, lf0=1e100): 17 | theta = theta = np.linspace(0, np.pi, 10000) 18 | 19 | energy = Eiso * np.exp(- 0.5 * (theta / theta_c) ** 2) 20 | lf = (lf0 - 1) * np.exp(- 0.5 * (theta / theta_c) ** 2) + 1 21 | 22 | return (theta, energy, lf) 23 | 24 | # power-law jet 25 | def PowerLaw(theta_c, Eiso, lf0=1e100, s=4.0): 26 | theta = theta = np.linspace(0, np.pi, 10000) 27 | 28 | energy = Eiso * np.power(1 + (theta / theta_c) ** 2, - s / 2.0) 29 | lf = (lf0 - 1.0) * np.power(1 + (theta / theta_c) ** 2, - s / 2.0) + 1.0 30 | 31 | return (theta, energy, lf) -------------------------------------------------------------------------------- /jetsimpy/_jetsimpy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from . import jetsimpy_extension as _extension 3 | from ._grid import Uniform 4 | 5 | _C = 29979245800.0 6 | _Mass_P = 1.672622e-24 7 | _mJy = 1e-26 8 | _MPC = 3.09e24 9 | _MAS = 1.0 / 206264806.24709466 10 | 11 | class Jet: 12 | def __init__( # It is the user's responsibility to make sure input values are valid. 13 | self, 14 | profiles, # [tuple of tabulated data]: (angles, Eiso, Lorentz factor) 15 | nwind, # [wind density scale]: n = nwind * (r / 1e17)^-2 + nism (cm^-3) 16 | nism, # [ism density scale]: n = nwind * (r / 1e17)^-2 + nism (cm^-3) 17 | tmin=10.0, # [simulation start time]: (s) 18 | tmax=1e10, # [simulation end time]: (s) 19 | grid=Uniform(257), # [cell edge angles]: start with 0 and end with pi. 20 | tail=True, # [isotropic tail]: add an extremely low energy low velocity isotropic tail for safty 21 | spread=True, # [spreading]: spread or not 22 | cal_level=1, # [calibration level]: 0: no calibration. 1: BM all time. 2: smoothly go from BM to ST (ST is dangerous) 23 | rtol=1e-6, # [primitive variable solver tolerance]: Don't change it unless you know what is going on. 24 | cfl=0.9, # [cfl number]: Don't change it unless you know what is going on. 25 | ): 26 | # save 27 | theta, energy, lf = profiles 28 | self.theta_data = theta 29 | self.energy_data = energy 30 | self.lf_data = lf 31 | self.nwind = nwind 32 | self.nism = nism 33 | self.tmin = tmin 34 | self.tmax = tmax 35 | self.rtol = rtol 36 | self.cfl = cfl 37 | self.grid = grid 38 | self.tail = tail 39 | self.spread = spread 40 | self.cal_level = cal_level 41 | 42 | # solve jet 43 | jet_config = self._configJet() 44 | self._jet = _extension.Jet(jet_config) 45 | self._jet.solveJet() 46 | 47 | # ---------- PDE original data ---------- # 48 | @property 49 | def t_pde(self): 50 | result = np.array(self._jet.getT()) 51 | return result 52 | 53 | # original y: Msw, Mej, beta_gamma_sq, beta_th, R (shape = [5, ntheta, nt]) 54 | @property 55 | def y_pde(self): 56 | result = np.array(self._jet.getY()) 57 | return result 58 | 59 | @property 60 | def theta_pde(self): 61 | result = np.array(self._jet.getTheta()) 62 | return result 63 | 64 | # ---------- PDE data interpolation ---------- # 65 | # rest mass excluded energy (erg / sr) 66 | def dE0_dOmega(self, t, theta): 67 | return self._jet.interpolateE0(t, theta) 68 | 69 | # swetp-up mass (g / sr) 70 | def dMsw_dOmega(self, t, theta): 71 | return self._jet.interpolateMsw(t, theta) 72 | 73 | # ejecta mass (g / sr) 74 | def dMej_dOmega(self, t, theta): 75 | return self._jet.interpolateMej(t, theta) 76 | 77 | # four velocity 78 | def beta_gamma(self, t, theta): 79 | return self._jet.interpolateBetaGamma(t, theta) 80 | 81 | # polar velocity 82 | def beta_theta(self, t, theta): 83 | return self._jet.interpolateBetaTh(t, theta) 84 | 85 | # radius (cm) 86 | def R(self, t, theta): 87 | return self._jet.interpolateR(t, theta) 88 | 89 | # ---------- Radiation Related ---------- # 90 | 91 | # Simply calculate time t by equal arrival time surface. Just for fun! 92 | def EATS(self, t, theta, phi, theta_v, z): 93 | return self._jet.calculateEATS(t, theta, phi, theta_v, z) 94 | 95 | # specific intensity at jet sphreical coordinate [cgs] Could be useful for debug 96 | def Intensity(self, t, nu, theta, phi, P, model="sync"): 97 | # config parameters 98 | self._jet.configParameters(P) 99 | 100 | # config radiation model 101 | self._jet.configIntensity(model) 102 | 103 | try: 104 | I = self._jet.calculateIntensity(t, nu, theta, phi) 105 | except Exception as e: 106 | raise e 107 | 108 | return I 109 | 110 | # flux density [mJy] 111 | def FluxDensity(self, t, nu, P, model="sync", rtol=1e-3, max_iter=100, force_return=True): 112 | # config parameters 113 | self._jet.configParameters(P) 114 | 115 | # config radiation model 116 | if isinstance(model, str): 117 | self._jet.configIntensity(model) 118 | else: 119 | self._jet.configIntensityPy(model) 120 | 121 | try: 122 | L = self._jet.calculateLuminosity(t, nu, rtol, max_iter, force_return) 123 | except Exception as e: 124 | raise e 125 | 126 | return L * (1 + P["z"]) / 4 / np.pi / (P["d"] * _MPC) ** 2 / _mJy 127 | 128 | # flux [erg/s/cm^2] 129 | def Flux(self, t, nu1, nu2, P, model="sync", rtol=1e-3, max_iter=100, force_return=True): 130 | # config parameters 131 | self._jet.configParameters(P) 132 | 133 | # config radiation model 134 | if isinstance(model, str): 135 | self._jet.configIntensity(model) 136 | else: 137 | self._jet.configIntensityPy(model) 138 | 139 | try: 140 | L = self._jet.calculateFreqIntL(t, nu1, nu2, rtol, max_iter, force_return) 141 | except Exception as e: 142 | raise e 143 | 144 | return L * (1 + P["z"]) / 4 / np.pi / (P["d"] * _MPC) ** 2 145 | 146 | def WeightedAverage(self, t, nu, P, radiation_model="sync", average_model="offset", rtol=1e-3, max_iter=100, force_return=True): 147 | # config parameters 148 | self._jet.configParameters(P) 149 | 150 | # config radiation model 151 | if isinstance(radiation_model, str): 152 | self._jet.configIntensity(radiation_model) 153 | else: 154 | self._jet.configIntensityPy(radiation_model) 155 | 156 | # config average model 157 | if isinstance(average_model, str): 158 | self._jet.configAvgModel(average_model) 159 | else: 160 | self._jet.configAvgModelPy(average_model) 161 | 162 | # calculate weighted average 163 | try: 164 | weighted_average = self._jet.WeightedAverage(t, nu, rtol, max_iter, force_return) 165 | except Exception as e: 166 | raise e 167 | 168 | return weighted_average 169 | 170 | # apparent superluminal motion [mas] 171 | def Offset(self, t, nu, P, model="sync", rtol=1e-3, max_iter=100, force_return=True): 172 | offset_cgs = self.WeightedAverage(t, nu, P, radiation_model=model, average_model="offset", rtol=rtol, max_iter=max_iter, force_return=force_return) 173 | 174 | return offset_cgs / P["d"] / _MPC / (1.0 + P["z"]) / (1.0 + P["z"]) / _MAS 175 | 176 | # size along the jet axis [mas] 177 | def SizeX(self, t, nu, P, model="sync", rtol=1e-3, max_iter=100, force_return=True): 178 | # calculate xscale. The following notes are for myself in case I forget what is going on. 179 | # First, ∫x dL = xc * ∫dL based on xc defination. 180 | # Then, 181 | # sigma_x^2 = ∫(x - xc)^2 dL / ∫dL 182 | # = ∫(x^2 - 2*x*xc + xc^2)dL / ∫dL 183 | # = (∫x^2 dL - 2*xc*∫x dL + xc^2*∫dL) / ∫dL 184 | # = (∫x^2 dL - 2*xc^2*∫dL + xc^2*∫dL) / ∫dL 185 | # = (∫x^2 dL / ∫dL) - xc^2 186 | # So, I only need to calculate the weighted avergae of x^2, not the original expression. 187 | 188 | # calculate offset 189 | xc = self.WeightedAverage(t, nu, P, radiation_model=model, average_model="offset", rtol=rtol, max_iter=max_iter, force_return=force_return) 190 | 191 | # calculate x_sq 192 | x_sq = self.WeightedAverage(t, nu, P, radiation_model=model, average_model="sigma_x", rtol=rtol, max_iter=max_iter, force_return=force_return) 193 | 194 | # xscale 195 | sigma_x = np.sqrt(x_sq - xc * xc) 196 | 197 | return sigma_x / P["d"] / _MPC / (1.0 + P["z"]) / (1.0 + P["z"]) / _MAS 198 | 199 | # size perpendicular to the jet axis [mas] 200 | def SizeY(self, t, nu, P, model="sync", rtol=1e-3, max_iter=100, force_return=True): 201 | sigma_y_sq = self.WeightedAverage(t, nu, P, radiation_model=model, average_model="sigma_y", rtol=rtol, max_iter=max_iter, force_return=force_return) 202 | sigma_y = np.sqrt(sigma_y_sq) 203 | 204 | return sigma_y / P["d"] / _MPC / (1.0 + P["z"]) / (1.0 + P["z"]) / _MAS 205 | 206 | # [cgs] specific intensity at LOS frame coordinate (x_tilde, y_tilde). This method is intended for sky map. 207 | def IntensityOfPixel(self, t, nu, x_offset, y_offset, P, model="sync"): 208 | # config parameters 209 | self._jet.configParameters(P) 210 | 211 | # config Intensity model 212 | self._jet.configIntensity(model) 213 | 214 | # intensity 215 | try: 216 | intensity = self._jet.IntensityOfPixel(t, nu, x_offset, y_offset) 217 | except Exception as e: 218 | raise e 219 | 220 | return intensity 221 | 222 | def _configJet(self): 223 | # initialize parameter object 224 | jet_config = _extension.JetConfig() 225 | jet_config.nwind = self.nwind 226 | jet_config.nism = self.nism 227 | jet_config.tmin = self.tmin 228 | jet_config.tmax = self.tmax 229 | jet_config.rtol = self.rtol 230 | jet_config.cfl = self.cfl 231 | jet_config.spread = self.spread 232 | jet_config.cal_level = self.cal_level 233 | 234 | # generate grid 235 | theta_edge = self.grid 236 | theta = np.array([(theta_edge[i] + theta_edge[i + 1]) / 2 for i in range(len(theta_edge) - 1)]) 237 | jet_config.theta_edge = theta_edge 238 | 239 | # add isotropic tail 240 | if self.tail: 241 | self.energy_data[self.energy_data <= np.max(self.energy_data) * 1e-12] = np.max(self.energy_data) * 1e-12 242 | self.lf_data[self.lf_data <= 1.005] = 1.005 243 | 244 | # interpolate initial condition to grid points 245 | E0 = np.interp(theta, self.theta_data, self.energy_data / 4.0 / np.pi / _C ** 2.0) 246 | lf0 = np.interp(theta, self.theta_data, self.lf_data) 247 | 248 | # get mej and initial velocity 249 | Mej0 = E0 / (lf0 - 1) 250 | beta0 = np.sqrt(1.0 - 1.0 / lf0 ** 2) 251 | 252 | # analytically expand (coast) the blastwave from t=0 to t=tmin 253 | R0 = beta0 * _C * self.tmin 254 | Msw0 = self.nwind * _Mass_P * R0 / 1e17 * 1e51 + self.nism * _Mass_P * R0 * R0 * R0 / 3.0 255 | Eb0 = E0 + Mej0 + Msw0 256 | 257 | # config jet initial condition 258 | jet_config.Eb = Eb0 259 | jet_config.Ht = np.zeros_like(theta) 260 | jet_config.Msw = Msw0 261 | jet_config.Mej = Mej0 262 | jet_config.R = R0 263 | 264 | return jet_config -------------------------------------------------------------------------------- /jetsimpy/_shortcut.py: -------------------------------------------------------------------------------- 1 | from ._jetsimpy import Jet 2 | from ._grid import * 3 | from ._jet_type import * 4 | 5 | def FluxDensity_tophat(t, nu, P, tmin=10.0, tmax=1e10, spread=True, cal_level=1, cfl=0.9, model="sync", rtol=1e-3, max_iter=100, force_return=True): 6 | # simulation 7 | jet = Jet( 8 | TopHat(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 9 | P["A"], # wind number density scale 10 | P["n0"], # ism number density scale 11 | tmin=tmin, # [simulation start time]: (s) 12 | tmax=tmax, # [simulation end time]: (s) 13 | grid=ForwardJetRes(P["theta_c"], 129), # [cell edge angles]: must start with 0 and end with pi. 14 | tail=True, # [isotropic tail]: add an extremely low energy low velocity isotropic tail for safty 15 | spread=spread, # w/wo spreading effect 16 | cal_level=cal_level, # [calibration level]: 0: no calibration. 1: BM all time. 2: smoothly go from BM to ST (dangerous) 17 | rtol=1e-6, # [primitive variable solver tolerance]: Don't change it unless you know what is going on. 18 | cfl=cfl, # [cfl number]: Don't change it unless you know what is going on. 19 | ) 20 | 21 | # flux density 22 | flux = jet.FluxDensity( 23 | t, # [second] observing time span 24 | nu, # [Hz] observing frequency 25 | P, # parameter dictionary 26 | model=model, # emissivity model 27 | rtol=rtol, # integration tolerance 28 | max_iter=max_iter, 29 | force_return=force_return 30 | ) 31 | 32 | return flux 33 | 34 | def FluxDensity_gaussian(t, nu, P, tmin=10.0, tmax=1e10, spread=True, cal_level=1, cfl=0.9, model="sync", rtol=1e-3, max_iter=100, force_return=True): 35 | # simulation 36 | jet = Jet( 37 | Gaussian(P["theta_c"], P["Eiso"], lf0=P["lf"]), # jet profile 38 | P["A"], # wind number density scale 39 | P["n0"], # ism number density scale 40 | tmin=tmin, # [simulation start time]: (s) 41 | tmax=tmax, # [simulation end time]: (s) 42 | grid=ForwardJetRes(P["theta_c"], 129), # [cell edge angles]: must start with 0 and end with pi. 43 | tail=True, # [isotropic tail]: add an extremely low energy low velocity isotropic tail for safty 44 | spread=spread, # w/wo spreading effect 45 | cal_level=cal_level, # [calibration level]: 0: no calibration. 1: BM all time. 2: smoothly go from BM to ST (dangerous) 46 | rtol=1e-6, # [primitive variable solver tolerance]: Don't change it unless you know what is going on. 47 | cfl=cfl, # [cfl number]: Don't change it unless you know what is going on. 48 | ) 49 | 50 | # flux density 51 | flux = jet.FluxDensity( 52 | t, # [second] observing time span 53 | nu, # [Hz] observing frequency 54 | P, # parameter dictionary 55 | model=model, # emissivity model 56 | rtol=rtol, # integration tolerance 57 | max_iter=max_iter, 58 | force_return=force_return 59 | ) 60 | 61 | return flux 62 | 63 | def FluxDensity_powerlaw(t, nu, P, tmin=10.0, tmax=1e10, spread=True, cal_level=1, cfl=0.9, model="sync", rtol=1e-3, max_iter=100, force_return=True): 64 | # simulation 65 | jet = Jet( 66 | PowerLaw(P["theta_c"], P["Eiso"], lf0=P["lf"], s=P["s"]), # jet profile 67 | P["A"], # wind number density scale 68 | P["n0"], # ism number density scale 69 | tmin=tmin, # [simulation start time]: (s) 70 | tmax=tmax, # [simulation end time]: (s) 71 | grid=ForwardJetRes(P["theta_c"], 129), # [cell edge angles]: must start with 0 and end with pi. 72 | tail=True, # [isotropic tail]: add an extremely low energy low velocity isotropic tail for safty 73 | spread=spread, # w/wo spreading effect 74 | cal_level=cal_level, # [calibration level]: 0: no calibration. 1: BM all time. 2: smoothly go from BM to ST (dangerous) 75 | rtol=1e-6, # [primitive variable solver tolerance]: Don't change it unless you know what is going on. 76 | cfl=cfl, # [cfl number]: Don't change it unless you know what is going on. 77 | ) 78 | 79 | # flux density 80 | flux = jet.FluxDensity( 81 | t, # [second] observing time span 82 | nu, # [Hz] observing frequency 83 | P, # parameter dictionary 84 | model=model, # emissivity model 85 | rtol=rtol, # integration tolerance 86 | max_iter=max_iter, 87 | force_return=force_return 88 | ) 89 | 90 | return flux 91 | -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/afterglow.cpp: -------------------------------------------------------------------------------- 1 | #include "afterglow.h" 2 | 3 | void Afterglow::initialize(SimBox& sim_box, EATS& eats) { 4 | this->eats = &eats; 5 | //eats.feedData(sim_box, tool); 6 | theta_data = &(sim_box.getTheta()); 7 | models.registerIntensity(); 8 | models.registerAvgModels(); 9 | } 10 | 11 | void Afterglow::configParameters(const Dict& param) { 12 | // save important parameters and perform parameter checking 13 | try { 14 | this->theta_v = param.at("theta_v"); 15 | this->d = param.at("d"); 16 | this->z = param.at("z"); 17 | } 18 | catch (const std::exception& e) { 19 | throw std::runtime_error("The following parameters are mandatory: 'theta_v', 'd', and 'z'."); 20 | } 21 | 22 | this->param = param; 23 | } 24 | 25 | void Afterglow::configIntensity(const std::string& model_name) { 26 | try { 27 | radiation_model = models.radiation_models.at(model_name); 28 | } 29 | catch (const std::exception& e) { 30 | throw std::runtime_error("There is no such radiation model: '" + model_name + "'."); 31 | } 32 | } 33 | 34 | void Afterglow::configAvgModel(const std::string& model_name) { 35 | try { 36 | avg_model = models.avg_models[model_name]; 37 | } 38 | catch (const std::exception& e) { 39 | throw std::runtime_error("There is no such weighted average model: '" + model_name + "'."); 40 | } 41 | } 42 | 43 | void Afterglow::configIntensityPy(py::function py_f) { 44 | radiation_model = [py_f](const double nu, const Dict& P, const Blast& blast) { 45 | py::object obj = py_f(nu, P, blast); 46 | double intensity = obj.cast(); 47 | return intensity; 48 | }; 49 | } 50 | 51 | void Afterglow::configAvgModelPy(py::function py_f) { 52 | avg_model = [py_f](const double nu, const Dict& P, const Blast& blast) { 53 | py::object obj = py_f(nu, P, blast); 54 | double return_val = obj.cast(); 55 | return return_val; 56 | }; 57 | } 58 | 59 | double Afterglow::Intensity(const double Tobs, const double nu, const double theta, const double phi) { 60 | double Tobs_z = Tobs / (1 + z); 61 | double nu_z = nu * (1 + z); 62 | 63 | // solve equal-arrival-time-surface and get blast properties 64 | eats->solveBlast(Tobs_z, theta, phi, theta_v, blast); 65 | 66 | // nu in comoving frame 67 | double nu_src = nu_z / blast.doppler; 68 | 69 | // solve intensity at comoving frame 70 | double intensity = radiation_model(nu_src, param, blast); 71 | 72 | // convert to intensity at observer's frame 73 | return intensity / 4.0 / PI * blast.doppler * blast.doppler * blast.doppler; 74 | } 75 | 76 | double Afterglow::dL_dOmega(const double Tobs_z, const double nu_z, const double theta, const double phi) { 77 | // nu in comoving frame 78 | double nu_src = nu_z / blast.doppler; 79 | 80 | // solve intensity at comoving frame 81 | double intensity = radiation_model(nu_src, param, blast); 82 | 83 | return intensity * blast.R * blast.R * blast.doppler * blast.doppler * blast.doppler; 84 | } 85 | 86 | double Afterglow::Luminosity(const double Tobs, const double nu, const double rtol, const int max_iter, const bool force_return) { 87 | double Tobs_z = Tobs / (1 + z); 88 | double nu_z = nu * (1 + z); 89 | double theta_peak = findPeak(Tobs_z, nu_z); 90 | 91 | auto f = [&](double cos_theta_rot, double phi_rot) { 92 | // interruption detection 93 | if (PyErr_CheckSignals() != 0) { 94 | throw std::runtime_error("Luminosity: Keyboard interruption."); 95 | } 96 | 97 | // transform from "peak" coordinate to jet coordinate 98 | double theta_rot = std::acos(cos_theta_rot); 99 | 100 | double x = std::sin(theta_rot) * std::cos(phi_rot) * std::cos(theta_peak) + std::cos(theta_rot) * std::sin(theta_peak); 101 | double y = std::sin(theta_rot) * std::sin(phi_rot); 102 | double z = - std::sin(theta_rot) * std::cos(phi_rot) * std::sin(theta_peak) + std::cos(theta_rot) * std::cos(theta_peak); 103 | 104 | double cos_theta = std::min(1.0, std::fabs(z)) * z / std::fabs(z); 105 | double theta = std::acos(cos_theta); 106 | 107 | double phi = std::atan2(y, x); 108 | phi = (phi < 0.0) ? phi + PI * 2.0 : phi; 109 | 110 | // solve equal-arrival-time-surface and get blast properties 111 | eats->solveBlast(Tobs_z, theta, phi, theta_v, blast); 112 | 113 | return dL_dOmega(Tobs_z, nu_z, theta, phi); 114 | }; 115 | 116 | // beaming angle 117 | eats->solveBlast(Tobs_z, theta_peak, 0.0, theta_v, blast); 118 | double beaming_angle = 1.0 / blast.gamma; 119 | 120 | // initial inegral samples 121 | Array1D cos_theta_samples = {-1.0, std::cos(beaming_angle), std::cos(beaming_angle / 2.0), 1.0}; 122 | Array1D phi_samples = {0.0, PI}; 123 | 124 | // multiply by 2 because the integral domain for phi is [0, pi]. 125 | double luminosity = Adaptive_2D(f, cos_theta_samples, phi_samples, 0.0, rtol, max_iter, force_return) * 2.0; 126 | 127 | return luminosity; 128 | } 129 | 130 | double Afterglow::FreqIntL(const double Tobs, const double nu1, const double nu2, const double rtol, const int max_iter, const bool force_return) { 131 | auto f = [&](double nu) { 132 | return Luminosity(Tobs, nu, rtol, max_iter, force_return); 133 | }; 134 | 135 | double frequency_integrated_luminosity = Adaptive_1D(f, {nu1, nu2}, 0.0, rtol, max_iter, force_return); 136 | return frequency_integrated_luminosity; 137 | } 138 | 139 | double Afterglow::integrateModel(const double Tobs, const double nu, const double rtol, const int max_iter, const bool force_return) { 140 | double Tobs_z = Tobs / (1 + z); 141 | double nu_z = nu * (1 + z); 142 | double theta_peak = findPeak(Tobs_z, nu_z); 143 | double nu_src = nu_z / blast.doppler; 144 | 145 | auto f = [&](double cos_theta_rot, double phi_rot) { 146 | // interruption detection 147 | if (PyErr_CheckSignals() != 0) { 148 | throw std::runtime_error("Average Model: Keyboard interruption."); 149 | } 150 | 151 | // transform from "peak" coordinate to jet coordinate 152 | double theta_rot = std::acos(cos_theta_rot); 153 | 154 | double x = std::sin(theta_rot) * std::cos(phi_rot) * std::cos(theta_peak) + std::cos(theta_rot) * std::sin(theta_peak); 155 | double y = std::sin(theta_rot) * std::sin(phi_rot); 156 | double z = - std::sin(theta_rot) * std::cos(phi_rot) * std::sin(theta_peak) + std::cos(theta_rot) * std::cos(theta_peak); 157 | 158 | double cos_theta = std::min(1.0, std::fabs(z)) * z / std::fabs(z); 159 | double theta = std::acos(cos_theta); 160 | 161 | double phi = std::atan2(y, x); 162 | phi = (phi < 0.0) ? phi + PI * 2.0 : phi; 163 | 164 | // solve equal-arrival-time-surface and get blast properties 165 | eats->solveBlast(Tobs_z, theta, phi, theta_v, blast); 166 | 167 | return avg_model(nu_src, param, blast) * dL_dOmega(Tobs_z, nu_z, theta, phi); 168 | }; 169 | 170 | // beaming angle 171 | eats->solveBlast(Tobs_z, theta_peak, 0.0, theta_v, blast); 172 | double beaming_angle = 1.0 / blast.gamma; 173 | 174 | // initial inegral samples (phi from 0 to 2 * pi) 175 | Array1D cos_theta_samples = {-1.0, std::cos(beaming_angle), std::cos(beaming_angle / 2.0), 1.0}; 176 | Array1D phi_samples = {0.0, PI, 2.0 * PI}; 177 | 178 | double integral = Adaptive_2D(f, cos_theta_samples, phi_samples, 0.0, rtol, max_iter, force_return); 179 | 180 | return integral; 181 | } 182 | 183 | double Afterglow::findPeak(const double Tobs_z, const double nu_z) { 184 | // define function 185 | auto f = [&](const double& theta) { 186 | // solve equal-arrival-time-surface and get blast properties 187 | eats->solveBlast(Tobs_z, theta, 0.0, theta_v, blast); 188 | 189 | return - dL_dOmega(Tobs_z, nu_z, theta, 0.0); 190 | }; 191 | 192 | // solve theta_peak 193 | double theta_peak = minimization(f, *theta_data, 1e-6); 194 | return theta_peak; 195 | } 196 | 197 | double Afterglow::IntensityOfPixel(const double Tobs, const double nu, const double x_offset, const double y_offset) { 198 | // function to solve intensity from LOS spherical coordinate 199 | auto f_intensity = [&](const double theta_tilde, const double phi_tilde) { 200 | // convert to source coordinate (cartisan) 201 | double x = std::sin(theta_tilde) * std::cos(phi_tilde) * std::cos(theta_v) + std::cos(theta_tilde) * std::sin(theta_v); 202 | double y = std::sin(theta_tilde) * std::sin(phi_tilde); 203 | double z = - std::sin(theta_tilde) * std::cos(phi_tilde) * std::sin(theta_v) + std::cos(theta_tilde) * std::cos(theta_v); 204 | 205 | // convert to spherical coordinate 206 | double theta = std::acos(z); 207 | double phi = std::atan2(y, x); 208 | phi = (phi < 0.0) ? phi + PI * 2.0 : phi; 209 | 210 | double intensity = Intensity(Tobs, nu, theta, phi); 211 | return intensity; 212 | }; 213 | 214 | // projection to the axis of LOS coordinate 215 | double projection = std::sqrt(x_offset * x_offset + y_offset * y_offset) * d * MPC * (1.0 + z) * (1.0 + z) * MAS; 216 | 217 | // solve azimuthal angle 218 | double phi_tilde = std::atan2(y_offset, x_offset); 219 | phi_tilde = (phi_tilde < 0.0) ? phi_tilde + PI * 2.0 : phi_tilde; 220 | 221 | // function to solve root and optimize 222 | auto f_root = [&](const double theta_tilde) { 223 | // convert to source coordinate (cartisan) 224 | double x = std::sin(theta_tilde) * std::cos(phi_tilde) * std::cos(theta_v) + std::cos(theta_tilde) * std::sin(theta_v); 225 | double y = std::sin(theta_tilde) * std::sin(phi_tilde); 226 | double z = - std::sin(theta_tilde) * std::cos(phi_tilde) * std::sin(theta_v) + std::cos(theta_tilde) * std::cos(theta_v); 227 | 228 | // convert to spherical coordinate 229 | double theta = std::acos(z); 230 | double phi = std::atan2(y, x); 231 | phi = (phi < 0.0) ? phi + PI * 2.0 : phi; 232 | 233 | // solve EATS 234 | eats->solveBlast(Tobs / (1 + this->z), theta, phi, theta_v, blast); 235 | 236 | // why do I use arcsinh? I don't even remember! 237 | return projection - blast.R * std::sin(theta_tilde); 238 | }; 239 | 240 | // perform minimization (because there might be two intersections on two sides of theta_tilde_peak) 241 | double theta_tilde_peak = minimization(f_root, 0.0, PI, 10, 1e-4, "linear"); 242 | 243 | // initialize intensity 244 | double intensity_tot = 0.0; 245 | 246 | // root function value at three critical points 247 | double f1 = f_root(0.0); 248 | double f2 = f_root(theta_tilde_peak); 249 | double f3 = f_root(PI); 250 | 251 | // line of sight and 2D surface may have 0 or 2 intersections 252 | if (f1 * f2 <= 0.0) { 253 | double theta_tilde = brentq(f_root, 0.0, theta_tilde_peak, 1e-6, 1e-6); 254 | double intensity = f_intensity(theta_tilde, phi_tilde); 255 | intensity_tot += intensity; 256 | } 257 | if (f2 * f3 <= 0.0) { 258 | double theta_tilde = brentq(f_root, theta_tilde_peak, PI, 1e-6, 1e-6); 259 | double intensity = f_intensity(theta_tilde, phi_tilde); 260 | intensity_tot += intensity; 261 | } 262 | 263 | return intensity_tot; 264 | } 265 | -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/afterglow.h: -------------------------------------------------------------------------------- 1 | #ifndef JET_INTEGRATE 2 | #define JET_INTEGRATE 3 | 4 | #include "../environment.h" 5 | #include "../Hydro/sim_box.h" 6 | #include "../Hydro/tools.h" 7 | #include "../Math/integral.h" 8 | #include "../Math/optimize.h" 9 | #include "blast.h" 10 | #include "eats.h" 11 | #include "models.h" 12 | 13 | // This class provides a routine to calculate afterglow 14 | class Afterglow { 15 | public: 16 | Afterglow() {} 17 | void initialize(SimBox& sim_box, EATS& eats); // initialize object 18 | void configParameters(const Dict& param); // configurate parameter dictionary 19 | void configIntensity(const std::string& model_name); // configure radiation model 20 | void configAvgModel(const std::string& model_name); // configure average models 21 | 22 | void configIntensityPy(py::function py_f); // configure Intensity model from python side 23 | void configAvgModelPy(py::function py_f); // configure average models from python side 24 | 25 | // calculate the specific intensity 26 | double Intensity(const double Tobs, const double nu, const double theta, const double phi); 27 | 28 | // total luminosity 29 | double Luminosity(const double Tobs, const double nu, const double rtol, const int max_iter = 50, const bool force_return = true); 30 | 31 | // frequency integrated luminosity 32 | double FreqIntL(const double Tobs, const double nu1, const double nu2, const double rtol, const int max_iter = 50, const bool force_return = true); 33 | 34 | // integrate average model with dL_dOmega as the weight 35 | double integrateModel(const double Tobs, const double nu, const double rtol, const int max_iter = 50, const bool force_return = true); 36 | 37 | // intensity of pixel (useful for sky map) 38 | double IntensityOfPixel(const double Tobs, const double nu, const double x_tilde, const double y_tilde); 39 | 40 | private: 41 | Array1D* theta_data; 42 | Blast blast; // blast object 43 | Dict param; // parameter dictionary from python side 44 | 45 | // model function (pointer) to be called in integration 46 | std::function radiation_model; 47 | std::function avg_model; 48 | 49 | // model object to store model functions 50 | Models models; 51 | 52 | // parameters we must have 53 | double theta_v; // observing angle 54 | double z; // redshift 55 | double d; // luminosity distance 56 | 57 | // equal arrival time surface solver 58 | EATS* eats; 59 | 60 | // calculate dL/dOmega 61 | double dL_dOmega(const double Tobs_z, const double nu_z, const double theta, const double phi); 62 | 63 | // find peak 64 | double findPeak(const double Tobs_z, const double nu_z); 65 | }; 66 | 67 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/blast.h: -------------------------------------------------------------------------------- 1 | #ifndef BLAST 2 | #define BLAST 3 | 4 | struct Blast { 5 | public: 6 | // observer's parameters 7 | //double Tobs_z; // Tobs / (1 + z) 8 | 9 | // coordinate values (burster frame) 10 | double t; 11 | double theta; 12 | double phi; 13 | double R; 14 | 15 | // blast velocity (burster frame) 16 | double beta; 17 | double gamma; 18 | double beta_th; 19 | double beta_r; 20 | double beta_f; 21 | double gamma_f; 22 | double s; 23 | 24 | // angles (burster frame) 25 | double doppler; 26 | //double cos_theta_r; 27 | double cos_theta_beta; 28 | 29 | // thermodynamic values (comoving frame) 30 | double n_blast; 31 | double e_density; 32 | double pressure; 33 | double n_ambient; 34 | double dR; 35 | }; 36 | 37 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/eats.cpp: -------------------------------------------------------------------------------- 1 | #include "eats.h" 2 | 3 | void EATS::feedData(SimBox& sim_box, Tool& tool) { 4 | y_data = &(sim_box.getY()); 5 | t_data = &(sim_box.getT()); 6 | theta_data = &(sim_box.getTheta()); 7 | this->tool = &tool; 8 | ntheta = theta_data->size(); 9 | nt = t_data->size(); 10 | tmin = t_data->front(); 11 | tmax = t_data->back(); 12 | theta_min = theta_data->front(); 13 | theta_max = theta_data->back(); 14 | } 15 | 16 | void EATS::findThetaIndex(double theta, int& theta_index1, int& theta_index2) { 17 | if (theta < 0.0 || theta > PI) { 18 | // bound check error 19 | throw std::runtime_error("EATS: theta outside bounds.\n"); 20 | } 21 | else if (theta < theta_min) { 22 | // north pole 23 | theta_index1 = 0; 24 | theta_index2 = 0; 25 | } 26 | else if (theta > theta_max) { 27 | // south pole 28 | theta_index1 = ntheta - 1; 29 | theta_index2 = ntheta - 1; 30 | } 31 | else { 32 | // middle 33 | tool->findIndex(*theta_data, theta, theta_index1, theta_index2); 34 | } 35 | } 36 | 37 | void EATS::findTimeIndex(double mu, double Tobs_z, int theta_index, int& t_index1, int& t_index2) { 38 | // equal arrival time surface function at t_index 39 | auto f = [&](const int& t_index) { 40 | // r at theta_index, t_index 41 | double r = (*y_data)[4][theta_index][t_index]; 42 | 43 | // t at theta_index, r_index 44 | double t = (*t_data)[t_index]; 45 | 46 | // equal arrival time surface function 47 | return t - r * mu / CSpeed - Tobs_z; 48 | }; 49 | 50 | // find t index (binary search) 51 | if (f(0) > 0) { // smaller than tmin 52 | t_index1 = 0; 53 | t_index2 = 0; 54 | } 55 | else if (f(nt - 1) < 0) { // larger than tmax 56 | // throw error 57 | throw std::runtime_error("EATS: Observing time exceeds PDE maximum evolution time!\n"); 58 | } 59 | else { 60 | t_index1 = 0; 61 | t_index2 = nt - 1; 62 | int index_mid; 63 | while (t_index2 - t_index1 > 1) { 64 | index_mid = (t_index1 + t_index2) / 2; 65 | if (f(index_mid) > 0) { 66 | t_index2 = index_mid; 67 | } 68 | else { 69 | t_index1 = index_mid; 70 | } 71 | } 72 | } 73 | } 74 | 75 | double EATS::solveT(double mu, double Tobs_z, int theta_index, int t_index1, int t_index2) { 76 | if (t_index1 == t_index2) { // before tmin 77 | double r = (*y_data)[4][theta_index][t_index1]; 78 | double t = Tobs_z / (1 - r / (CSpeed * tmin) * mu); 79 | return t; 80 | } 81 | else { 82 | // t at two ends 83 | double t1 = (*t_data)[t_index1]; 84 | double t2 = (*t_data)[t_index2]; 85 | 86 | // r values at two ends 87 | double r1 = (*y_data)[4][theta_index][t_index1]; 88 | double r2 = (*y_data)[4][theta_index][t_index2]; 89 | 90 | // linear interpolation 91 | double slope = (r2 - r1) / (t2 - t1); 92 | double t = (Tobs_z + (r1 - slope * t1) * mu / CSpeed) / (1 - slope * mu / CSpeed); 93 | return t; 94 | } 95 | } 96 | 97 | Array1D EATS::solvePrimitive(double mu, double Tobs_z, int theta_index) { 98 | // find t_index 99 | int t_index1, t_index2; 100 | findTimeIndex(mu, Tobs_z, theta_index, t_index1, t_index2); 101 | 102 | // solve t 103 | double t = solveT(mu, Tobs_z, theta_index, t_index1, t_index2); 104 | 105 | // solve primitive 106 | Array1D val = Array(6); // [Msw, Mej, beta_gamma_sq, beta_th, R, t] 107 | for (int i = 0; i < 5; ++i) { 108 | val[i] = tool->linear( 109 | t, (*t_data)[t_index1], (*t_data)[t_index2], 110 | (*y_data)[i][theta_index][t_index1], (*y_data)[i][theta_index][t_index2] 111 | ); 112 | } 113 | val[5] = t; 114 | 115 | return val; 116 | } 117 | 118 | void EATS::deriveBlast(double theta, double phi, double theta_v, const Array1D& val, Blast& blast) { 119 | // PDE variables 120 | double Msw = val[0]; 121 | double beta_gamma = std::sqrt(val[2]); 122 | double beta_th = val[3]; 123 | double R = val[4]; 124 | 125 | // coordinate values (burster frame) 126 | blast.t = val[5]; 127 | blast.theta = theta; 128 | blast.phi = phi; 129 | blast.R = R; 130 | 131 | // blast velocity (burster frame) 132 | blast.gamma = std::sqrt(val[2] + 1.0); 133 | blast.beta = beta_gamma / blast.gamma; 134 | blast.beta_th = beta_th; 135 | blast.beta_r = (beta_th <= blast.beta) ? std::sqrt(blast.beta * blast.beta - beta_th * beta_th) : 0.0; 136 | blast.beta_f = 4.0 * blast.beta * (val[2] + 1) / (4.0 * val[2] + 3.0); 137 | blast.gamma_f = (4.0 * val[2] + 3.0) / std::sqrt(8 * val[2] + 9.0); 138 | blast.s = tool->solveS(R, val[2]); 139 | 140 | // angles 141 | double nr = blast.beta_r / blast.beta; 142 | double nth = beta_th / blast.beta; 143 | double mu_beta = (nr * sin(theta) * cos(phi) + nth * cos(theta) * cos(phi)) * sin(theta_v) 144 | + (nr * cos(theta) - nth * sin(theta)) * cos(theta_v); 145 | double mu_r = std::cos(theta) * std::cos(theta_v) + std::sin(theta) * std::cos(phi) * std::sin(theta_v); 146 | blast.doppler = (blast.gamma > 1e3) ? 147 | 1.0 / blast.gamma / (1.0 - mu_beta + 0.5 / blast.gamma / blast.gamma * mu_beta) 148 | : 1.0 / blast.gamma / (1.0 - blast.beta * mu_beta); 149 | //blast.doppler = 1.0 / blast.gamma / (1.0 - blast.beta * mu_beta); 150 | blast.cos_theta_beta = (mu_beta - blast.beta) / (1.0 - blast.beta * mu_beta); 151 | 152 | // thermaldynamic properties (comoving frame) 153 | blast.n_ambient = tool->solveDensity(R); 154 | blast.n_blast = 4.0 * blast.gamma * blast.n_ambient; 155 | blast.e_density = (blast.gamma - 1.0) * blast.n_blast * MassP * CSpeed * CSpeed * blast.s; 156 | blast.pressure = 4.0 / 3.0 * val[2] * blast.n_ambient * MassP * CSpeed * CSpeed * blast.s; 157 | blast.dR = Msw / R / R / blast.n_blast / MassP; 158 | } 159 | 160 | void EATS::solveBlast_type1(double Tobs_z, double theta, double phi, double theta_v, Blast& blast) { 161 | // find theta index 162 | int theta_index1; 163 | int theta_index2; 164 | findThetaIndex(theta, theta_index1, theta_index2); 165 | 166 | Array1D val, val_l, val_r; 167 | if (theta_index1 == theta_index2) { // near poles 168 | // solve val_l 169 | { 170 | // compute mu within the cell (east hemisphere) 171 | double mu = std::cos((*theta_data)[theta_index1]) * std::cos(theta_v) + std::sin((*theta_data)[theta_index1]) * std::cos(phi) * std::sin(theta_v); 172 | 173 | // solve primitive 174 | val_l = solvePrimitive(mu, Tobs_z, theta_index1); 175 | } 176 | 177 | // solve val_r 178 | { 179 | // compute mu (negative phi, west hemisphere) 180 | double mu = std::cos((*theta_data)[theta_index1]) * std::cos(theta_v) + std::sin((*theta_data)[theta_index1]) * std::cos(phi + PI) * std::sin(theta_v); 181 | 182 | // solve primitive 183 | val_r = solvePrimitive(mu, Tobs_z, theta_index2); 184 | } 185 | 186 | // interpolate val over theta 187 | val = Array(6); 188 | for (int i = 0; i < 6; ++i) { 189 | val[i] = tool->linear( 190 | theta, 191 | (*theta_data)[theta_index1], 192 | (theta_index1 == 0) ? - (*theta_data)[theta_index1] : 2.0 * PI - (*theta_data)[theta_index1], 193 | val_l[i], 194 | val_r[i] 195 | ); 196 | } 197 | 198 | // construct blast object 199 | deriveBlast(theta, phi, theta_v, val, blast); 200 | } 201 | else { 202 | // solve val_l 203 | { 204 | // compute mu 205 | double mu = std::cos((*theta_data)[theta_index1]) * std::cos(theta_v) + std::sin((*theta_data)[theta_index1]) * std::cos(phi) * std::sin(theta_v); 206 | 207 | // solve primitive 208 | val_l = solvePrimitive(mu, Tobs_z, theta_index1); 209 | } 210 | 211 | // solve val_r 212 | { 213 | // compute mu 214 | double mu = std::cos((*theta_data)[theta_index2]) * std::cos(theta_v) + std::sin((*theta_data)[theta_index2]) * std::cos(phi) * std::sin(theta_v); 215 | 216 | // solve primitive 217 | val_r = solvePrimitive(mu, Tobs_z, theta_index2); 218 | } 219 | 220 | // interpolate val over theta 221 | val = Array(6); 222 | for (int i = 0; i < 6; ++i) { 223 | val[i] = tool->linear( 224 | theta, 225 | (*theta_data)[theta_index1], 226 | (*theta_data)[theta_index2], 227 | val_l[i], 228 | val_r[i] 229 | ); 230 | } 231 | 232 | // construct blast object 233 | deriveBlast(theta, phi, theta_v, val, blast); 234 | } 235 | } 236 | 237 | double EATS::solveEATS(double Tobs_z, double theta, double phi, double theta_v) { 238 | // find theta 239 | int theta_index1, theta_index2; 240 | findThetaIndex(theta, theta_index1, theta_index2); 241 | 242 | // cos angle 243 | double mu = std::cos(theta) * std::cos(theta_v) + std::sin(theta) * std::cos(phi) * std::sin(theta_v); 244 | 245 | // find t 246 | double t; 247 | int t_index1, t_index2; 248 | solveInterpolatedEATS(mu, Tobs_z, theta, t, t_index1, t_index2); 249 | 250 | return t; 251 | } 252 | 253 | void EATS::solveInterpolatedEATS(double mu, double Tobs_z, double theta, double& t, int& t_index1, int& t_index2) { 254 | // find theta 255 | int theta_index1, theta_index2; 256 | findThetaIndex(theta, theta_index1, theta_index2); 257 | 258 | // equal arrival time surface function at t_index 259 | auto f = [&](const int& t_index) { 260 | // r at theta, t_index 261 | double r1 = (*y_data)[4][theta_index1][t_index]; 262 | double r2 = (*y_data)[4][theta_index2][t_index]; 263 | double r = theta_index1 == theta_index2 ? r1 : (r2 - r1) / ((*theta_data)[theta_index2] - (*theta_data)[theta_index1]) * (theta - (*theta_data)[theta_index1]) + r1; 264 | 265 | // t at t_index 266 | double t = (*t_data)[t_index]; 267 | 268 | // equal arrival time surface function 269 | return t - r * mu / CSpeed - Tobs_z; 270 | }; 271 | 272 | // find t index (binary search) 273 | //int t_index1, t_index2; 274 | if (f(0) > 0) { // smaller than tmin 275 | t_index1 = 0; 276 | t_index2 = 0; 277 | } 278 | else if (f(nt - 1) < 0) { // larger than tmax 279 | // throw error 280 | throw std::runtime_error("EATS: Observing time exceeds PDE maximum evolution time!\n"); 281 | } 282 | else { 283 | t_index1 = 0; 284 | t_index2 = nt - 1; 285 | int index_mid; 286 | while (t_index2 - t_index1 > 1) { 287 | index_mid = (t_index1 + t_index2) / 2; 288 | if (f(index_mid) > 0) { 289 | t_index2 = index_mid; 290 | } 291 | else { 292 | t_index1 = index_mid; 293 | } 294 | } 295 | } 296 | 297 | // find exact t 298 | if (t_index1 == t_index2) { 299 | t = (*t_data)[t_index1]; 300 | } 301 | else { 302 | double r1 = tool->linear(theta, (*theta_data)[theta_index1], (*theta_data)[theta_index2], (*y_data)[4][theta_index1][t_index1], (*y_data)[4][theta_index2][t_index1]); 303 | double r2 = tool->linear(theta, (*theta_data)[theta_index1], (*theta_data)[theta_index2], (*y_data)[4][theta_index1][t_index2], (*y_data)[4][theta_index2][t_index2]); 304 | double slope = (r2 - r1) / ((*t_data)[t_index2] - (*t_data)[t_index1]); 305 | t = (Tobs_z - mu / CSpeed * (slope * (*t_data)[t_index1] - r1)) / (1.0 - mu * slope / CSpeed); 306 | } 307 | } 308 | 309 | void EATS::solveBlast_type2(double Tobs_z, double theta, double phi, double theta_v, Blast& blast) { 310 | // find theta 311 | int theta_index1, theta_index2; 312 | findThetaIndex(theta, theta_index1, theta_index2); 313 | 314 | // cos angle 315 | double mu = std::cos(theta) * std::cos(theta_v) + std::sin(theta) * std::cos(phi) * std::sin(theta_v); 316 | 317 | // find t 318 | double t; 319 | int t_index1, t_index2; 320 | solveInterpolatedEATS(mu, Tobs_z, theta, t, t_index1, t_index2); 321 | 322 | // interpolate the primitive variables 323 | Array1D val = Array(6); // [Msw, Mej, beta_gamma_sq, beta_th, R, t] 324 | for (int i = 0; i < 5; ++i) { 325 | // interpolate y over theta 326 | double y1 = tool->linear( 327 | theta, (*theta_data)[theta_index1], (*theta_data)[theta_index2], 328 | (*y_data)[i][theta_index1][t_index1], (*y_data)[i][theta_index2][t_index1] 329 | ); 330 | 331 | double y2 = tool->linear( 332 | theta, (*theta_data)[theta_index1], (*theta_data)[theta_index2], 333 | (*y_data)[i][theta_index1][t_index2], (*y_data)[i][theta_index2][t_index2] 334 | ); 335 | 336 | val[i] = tool->linear( 337 | t, (*t_data)[t_index1], (*t_data)[t_index2], 338 | y1, y2 339 | ); 340 | } 341 | val[5] = t; 342 | 343 | // construct blast object 344 | deriveBlast(theta, phi, theta_v, val, blast); 345 | } 346 | 347 | void EATS::solveBlast(double Tobs_z, double theta, double phi, double theta_v, Blast& blast) { 348 | // find theta 349 | int theta_index1, theta_index2; 350 | findThetaIndex(theta, theta_index1, theta_index2); 351 | 352 | if (theta_index1 == theta_index2) { 353 | solveBlast_type2(Tobs_z, theta, phi, theta_v, blast); 354 | } 355 | else { 356 | double beta_gamma_sq1 = (*y_data)[2][theta_index1][0]; 357 | double beta_gamma_sq2 = (*y_data)[2][theta_index2][0]; 358 | if (std::min(beta_gamma_sq1, beta_gamma_sq2) * 10 < std::max(beta_gamma_sq1, beta_gamma_sq2)) { 359 | solveBlast_type1(Tobs_z, theta, phi, theta_v, blast); 360 | } 361 | else { 362 | solveBlast_type2(Tobs_z, theta, phi, theta_v, blast); 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/eats.h: -------------------------------------------------------------------------------- 1 | #ifndef EQUAL_ARRIVAL_TIME_SURFACE 2 | #define EQUAL_ARRIVAL_TIME_SURFACE 3 | 4 | #include "../environment.h" 5 | #include "../Hydro/sim_box.h" 6 | #include "../Hydro/tools.h" 7 | #include "blast.h" 8 | 9 | // equal arrival time surface solver 10 | class EATS { 11 | public: 12 | EATS() {} 13 | void feedData(SimBox& sim_box, Tool& tool); // feed PDE data 14 | 15 | // solve EATS & blast properties (by interpolate EATS) 16 | void solveBlast_type1(double Tobs_z, double theta, double phi, double theta_v, Blast& blast); 17 | 18 | // solve EATS & blast properties (by interpolate PDE) 19 | void solveBlast_type2(double Tobs_z, double theta, double phi, double theta_v, Blast& blast); 20 | 21 | // choose one of the ways 22 | void solveBlast(double Tobs_z, double theta, double phi, double theta_v, Blast& blast); 23 | 24 | // simply solve eats 25 | double solveEATS(double Tobs_z, double theta, double phi, double theta_v); 26 | 27 | private: 28 | // PDE data 29 | const Array3D* y_data; 30 | const Array1D* t_data; 31 | const Array1D* theta_data; 32 | 33 | Tool* tool; // tool 34 | int ntheta; // number of cells 35 | int nt; // number of time data 36 | double tmin; // pde start time 37 | double tmax; // pde end time 38 | double theta_min; // minimum cell center 39 | double theta_max; // maximum cell center 40 | 41 | // find index by binary search 42 | void findThetaIndex(double theta, int& theta_index1, int& theta_index2); 43 | void findTimeIndex(double mu, double Tobs_z, int theta_index, int& t_index1, int& t_index2); 44 | 45 | // find t directly on interpolated PDE data by binary search 46 | void solveInterpolatedEATS(double mu, double Tobs_z, double theta, double& t, int& t_index1, int& t_index2); 47 | 48 | // solve t for EATS at theta_index 49 | double solveT(double mu, double Tobs_z, int theta_index, int t_index1, int t_index2); 50 | 51 | // solve primitive variables and return primitive & t 52 | Array1D solvePrimitive(double mu, double Tobs_z, int theta_index); 53 | 54 | // derive blast properties given the primitive variables 55 | void deriveBlast(double theta, double phi, double theta_v, const Array1D& val, Blast& blast); 56 | }; 57 | 58 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/models.cpp: -------------------------------------------------------------------------------- 1 | #include "models.h" 2 | 3 | void Models::registerIntensity() { 4 | // The template of a radiation model: 5 | // 6 | // radiation_models["model_name"] = []( // "model_name" is the keyword used from Python side 7 | // const double nu, // frequency 8 | // const Dict& P, // parameter dictionary from Python side 9 | // const Blast& blast, // an object that contains all fluid element properties 10 | // ) { 11 | // 12 | // ... 13 | // 14 | // return isotropic_intensity; // return value must be a "double" 15 | // }; 16 | 17 | // ---------- default radiation model (Sari 1998) ---------- // 18 | radiation_models["sync"] = [&](const double nu, const Dict& P, const Blast& blast) { 19 | double eps_e, eps_b, p; 20 | try { 21 | eps_e = P.at("eps_e"); 22 | eps_b = P.at("eps_b"); 23 | p = P.at("p"); 24 | } 25 | catch (const std::exception& e) { 26 | throw std::runtime_error("The following parameters are required for the model 'sync': 'eps_e', 'eps_b', and 'p'."); 27 | } 28 | 29 | double n_blast = blast.n_blast; 30 | double t = blast.t; 31 | double gamma = blast.gamma; 32 | double e = blast.e_density; 33 | 34 | double emissivity; 35 | double gamma_m, gamma_c, B, nu_m, nu_c, e_p; 36 | 37 | gamma_m = (p - 2.0) / (p - 1.0) * (eps_e * MassP / MassE * (gamma - 1.0)); 38 | B = std::sqrt(8.0 * PI * eps_b * e); 39 | gamma_c = 6.0 * PI * MassE * gamma * CSpeed / SigmaT / B / B / t; 40 | nu_m = 3.0 * ECharge * B * gamma_m * gamma_m / 4.0 / PI / CSpeed / MassE; 41 | nu_c = 3.0 * ECharge * B * gamma_c * gamma_c / 4.0 / PI / CSpeed / MassE; 42 | e_p = std::sqrt(3.0) * ECharge * ECharge * ECharge * B * n_blast / MassE / CSpeed / CSpeed; 43 | 44 | if (nu_m < nu_c) { 45 | if (nu < nu_m) { 46 | emissivity = e_p * std::cbrt(nu / nu_m); 47 | } 48 | else if (nu < nu_c) { 49 | emissivity = e_p * std::pow(nu / nu_m, - (p - 1) / 2.0); 50 | } 51 | else { 52 | emissivity = e_p * std::pow(nu_c / nu_m, - (p - 1) / 2.0) * std::pow(nu / nu_c, - p / 2); 53 | } 54 | } 55 | else { 56 | if (nu < nu_c) { 57 | emissivity = e_p * std::cbrt(nu / nu_c); 58 | } 59 | else if (nu < nu_m) { 60 | emissivity = e_p / std::sqrt(nu / nu_c); 61 | } 62 | else { 63 | emissivity = e_p / std::sqrt(nu_m / nu_c) * std::pow(nu / nu_m, - p / 2); 64 | } 65 | } 66 | 67 | double isotropic_intensity = emissivity * blast.dR; 68 | return isotropic_intensity; 69 | }; 70 | 71 | // Bonus! deep newtonian phase correction 72 | radiation_models["sync_dnp"] = [&](const double nu, const Dict& P, const Blast& blast) { 73 | double eps_e, eps_b, p; 74 | try { 75 | eps_e = P.at("eps_e"); 76 | eps_b = P.at("eps_b"); 77 | p = P.at("p"); 78 | } 79 | catch (const std::exception& e) { 80 | throw std::runtime_error("The following parameters are required for the model 'sync_dnp': 'eps_e', 'eps_b', and 'p'."); 81 | } 82 | 83 | double n_blast = blast.n_blast; 84 | double t = blast.t; 85 | double gamma = blast.gamma; 86 | double e = blast.e_density; 87 | 88 | double emissivity; 89 | double gamma_m, gamma_c, B, nu_m, nu_c, e_p; 90 | 91 | gamma_m = (p - 2.0) / (p - 1.0) * eps_e * MassP / MassE * (gamma - 1.0); 92 | double f = 1.0; 93 | if (gamma_m <= 1) { 94 | gamma_m = 1.0; 95 | f = (p - 2.0) / (p - 1.0) * eps_e * MassP / MassE * (gamma - 1.0) / gamma_m; 96 | } 97 | B = std::sqrt(8.0 * PI * eps_b * e); 98 | gamma_c = 6.0 * PI * MassE * gamma * CSpeed / SigmaT / B / B / t; 99 | nu_m = 3.0 * ECharge * B * gamma_m * gamma_m / 4.0 / PI / CSpeed / MassE; 100 | nu_c = 3.0 * ECharge * B * gamma_c * gamma_c / 4.0 / PI / CSpeed / MassE; 101 | e_p = std::sqrt(3.0) * ECharge * ECharge * ECharge * B * f * n_blast / MassE / CSpeed / CSpeed; 102 | 103 | if (nu_m < nu_c) { 104 | if (nu < nu_m) { 105 | emissivity = e_p * std::cbrt(nu / nu_m); 106 | } 107 | else if (nu < nu_c) { 108 | emissivity = e_p * std::pow(nu / nu_m, - (p - 1) / 2.0); 109 | } 110 | else { 111 | emissivity = e_p * std::pow(nu_c / nu_m, - (p - 1) / 2.0) * std::pow(nu / nu_c, - p / 2); 112 | } 113 | } 114 | else { 115 | if (nu < nu_c) { 116 | emissivity = e_p * std::cbrt(nu / nu_c); 117 | } 118 | else if (nu < nu_m) { 119 | emissivity = e_p / std::sqrt(nu / nu_c); 120 | } 121 | else { 122 | emissivity = e_p / std::sqrt(nu_m / nu_c) * std::pow(nu / nu_m, - p / 2); 123 | } 124 | } 125 | 126 | double isotropic_intensity = emissivity * blast.dR; 127 | return isotropic_intensity; 128 | }; 129 | 130 | // ---------- define your own model below ---------- // 131 | // radiation_models["model_name"] = [](const double nu, const Dict& P, const Blast& blast) { 132 | // ... 133 | // return isotropic_intensity; 134 | // } 135 | } 136 | 137 | // weighted average models 138 | void Models::registerAvgModels() { 139 | // for offset 140 | avg_models["offset"] = [](const double nu, const Dict& P, const Blast& blast) { 141 | double theta_v = P.at("theta_v"); 142 | double x_tilde = - std::sin(blast.theta) * std::cos(blast.phi) * std::cos(theta_v) + std::cos(blast.theta) * std::sin(theta_v); 143 | return x_tilde * blast.R; 144 | }; 145 | 146 | // for sigma_x 147 | avg_models["sigma_x"] = [](const double nu, const Dict& P, const Blast& blast) { 148 | double theta_v = P.at("theta_v"); 149 | double x_tilde = - std::sin(blast.theta) * std::cos(blast.phi) * std::cos(theta_v) + std::cos(blast.theta) * std::sin(theta_v); 150 | return x_tilde * blast.R * x_tilde * blast.R; 151 | }; 152 | 153 | // for sigma_y 154 | avg_models["sigma_y"] = [](const double nu, const Dict& P, const Blast& blast) { 155 | double y = std::sin(blast.theta) * std::sin(blast.phi); 156 | return y * blast.R * y * blast.R; 157 | }; 158 | 159 | // ---------- define your own model below ---------- // 160 | // avg_models["model_name"] = [](const double nu, const Dict& P, const Blast& blast) { 161 | // ... 162 | // return xxx; 163 | // } 164 | } -------------------------------------------------------------------------------- /jetsimpy/src/Afterglow/models.h: -------------------------------------------------------------------------------- 1 | #ifndef MODELS 2 | #define MODELS 3 | 4 | #include "../environment.h" 5 | #include "blast.h" 6 | 7 | using Dict = std::map; 8 | using ModelDict = std::map>; 9 | 10 | struct Models { 11 | // constants (define your constants here) 12 | const double CSpeed = 29979245800.0; 13 | const double MassP = 1.672622e-24; 14 | const double MassE = 9.109384e-28; 15 | const double SigmaT = 6.6524587e-25; 16 | const double ECharge = 4.803204673e-10; 17 | const double MPC = 3.09e24; 18 | const double PI = 3.14159265358979323846; 19 | const double MAS = 1.0 / 206264806.24709466; 20 | 21 | // models to be registered in "models.cpp" 22 | ModelDict radiation_models; 23 | ModelDict avg_models; 24 | 25 | // register radiation models 26 | void registerIntensity(); 27 | 28 | // register average models for weighted average calculation 29 | void registerAvgModels(); 30 | }; 31 | 32 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG 2 | #define CONFIG 3 | 4 | #include "../environment.h" 5 | 6 | // simulation configuration 7 | struct JetConfig { 8 | // initial conditions 9 | Array1D theta_edge; // cell faces 10 | Array1D Eb; // Eb 11 | Array1D Ht; // Hb * beta_th 12 | Array1D Msw; // Msw 13 | Array1D Mej; // Mej 14 | Array1D R; // R 15 | double nwind; // wind density scale 16 | double nism; // interstellar medium density scale 17 | 18 | // configurations 19 | double tmin; // PDE starting time 20 | double tmax; // PDE ending time 21 | double rtol; // velocity solver tolerance 22 | double cfl; // Courant number 23 | bool spread; // Whether enabling spreading 24 | int cal_level; // calibration level: 25 | // 0: no calibration. 26 | // 1: calibrate with Blandford-McKee all time. 27 | // 2: calibrate with Blandford-McKee in ultra-relativistic phase and Sedov-Taylor in Newtonian phase. 28 | }; 29 | 30 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/interpolate.cpp: -------------------------------------------------------------------------------- 1 | #include "interpolate.h" 2 | 3 | void Interpolator::feedData(SimBox& sim_box, Tool& tool) { 4 | y_data = &(sim_box.getY()); 5 | t_data = &(sim_box.getT()); 6 | theta_data = &(sim_box.getTheta()); 7 | this->tool = &tool; 8 | ntheta = theta_data->size(); 9 | tmin = t_data->front(); 10 | tmax = t_data->back(); 11 | theta_min = theta_data->front(); 12 | theta_max = theta_data->back(); 13 | } 14 | 15 | void Interpolator::findThetaIndex(double theta, int& theta_index1, int& theta_index2) { 16 | if (theta < 0.0 || theta > PI) { 17 | // bound check error 18 | throw std::runtime_error("Interpolation: theta outside bounds.\n"); 19 | } 20 | else if (theta < theta_min) { 21 | // north pole 22 | theta_index1 = 0; 23 | theta_index2 = 0; 24 | } 25 | else if (theta > theta_max) { 26 | // south pole 27 | theta_index1 = ntheta - 1; 28 | theta_index2 = ntheta - 1; 29 | } 30 | else { 31 | // middle 32 | tool->findIndex(*theta_data, theta, theta_index1, theta_index2); 33 | } 34 | } 35 | 36 | void Interpolator::findTimeIndex(double t, int& t_index1, int& t_index2) { 37 | if (t < 0.0 || t > tmax) { 38 | // bound check error 39 | throw std::runtime_error("Interpolation: t outside bounds.\n"); 40 | } 41 | else if (t < tmin) { 42 | // before initial condition 43 | t_index1 = 0; 44 | t_index2 = 0; 45 | } 46 | else { 47 | // in the solution domain 48 | tool->findIndex(*t_data, t, t_index1, t_index2); 49 | } 50 | } 51 | 52 | // ----- interpolate over t at specific theta index ----- // 53 | 54 | double Interpolator::interpolateY(double t, double theta, int y_index) { 55 | // find index 56 | int theta_index1, theta_index2; 57 | int t_index1, t_index2; 58 | findThetaIndex(theta, theta_index1, theta_index2); 59 | findTimeIndex(t, t_index1, t_index2); 60 | 61 | // coordinate values at the corners [t, theta] 62 | double t1 = (*t_data)[t_index1]; 63 | double t2 = (*t_data)[t_index2]; 64 | double theta1 = (*theta_data)[theta_index1]; 65 | double theta2 = (*theta_data)[theta_index2]; 66 | 67 | // y values at the corners 68 | double y11 = (*y_data)[y_index][theta_index1][t_index1]; 69 | double y12 = (*y_data)[y_index][theta_index2][t_index1]; 70 | double y21 = (*y_data)[y_index][theta_index1][t_index2]; 71 | double y22 = (*y_data)[y_index][theta_index2][t_index2]; 72 | 73 | // interpolate over theta 74 | double y1 = (theta_index1 == theta_index2) ? // in the poles? 75 | y11 // constant in the pole 76 | : 77 | tool->linear(theta, theta1, theta2, y11, y12); 78 | double y2 = (theta_index1 == theta_index2) ? // in the poles? 79 | y21 // constant in the pole 80 | : 81 | tool->linear(theta, theta1, theta2, y21, y22); 82 | 83 | // interpolate over t 84 | double y = (t_index1 == t_index2) ? // before initial condition? 85 | y1 // constant extrapolation before initial condition 86 | : 87 | tool->linear(t, t1, t2, y1, y2); 88 | return y; 89 | } 90 | -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/interpolate.h: -------------------------------------------------------------------------------- 1 | #ifndef INTERPOLATE 2 | #define INTERPOLATE 3 | 4 | #include "../environment.h" 5 | #include "tools.h" 6 | #include "sim_box.h" 7 | 8 | class Interpolator { 9 | public: 10 | Interpolator() {} 11 | void feedData(SimBox& sim_box, Tool& tool); // feed PDE data 12 | 13 | // interpolate for thet y_index'th value 14 | double interpolateY(double t, double theta, int y_index); 15 | 16 | private: 17 | const Array3D* y_data; // pde data y 18 | const Array1D* t_data; // pde data t 19 | const Array1D* theta_data; // pde cell center 20 | Tool* tool; // tool 21 | int ntheta; // number of cells 22 | double tmin; // pde start time 23 | double tmax; // pde end time 24 | double theta_min; // minimum cell center 25 | double theta_max; // maximum cell center 26 | 27 | // find index by binary search 28 | void findThetaIndex(double theta, int& theta_index1, int& theta_index2); 29 | void findTimeIndex(double t, int& t_index1, int& t_index2); 30 | }; 31 | 32 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/sim_box.cpp: -------------------------------------------------------------------------------- 1 | #include "sim_box.h" 2 | 3 | // ---------- public functions ---------- // 4 | SimBox::SimBox(const JetConfig& jet_config, Tool& tool) { 5 | // member setup 6 | this->tool = &tool; 7 | cfl = jet_config.cfl; 8 | tmin = jet_config.tmin; 9 | tmax = jet_config.tmax; 10 | spread = jet_config.spread; 11 | 12 | // mesh 13 | ntheta = jet_config.Eb.size(); 14 | theta_edge = jet_config.theta_edge; 15 | theta = Array(ntheta); 16 | for (int i = 0; i < ntheta; ++i) { 17 | theta[i] = (theta_edge[i] + theta_edge[i + 1]) / 2.0; 18 | } 19 | 20 | // conserved variables 21 | Eb = jet_config.Eb; 22 | Ht = jet_config.Ht; 23 | Msw = jet_config.Msw; 24 | Mej = jet_config.Mej; 25 | R = jet_config.R; 26 | 27 | // primitive variables 28 | beta_gamma_sq = Array(ntheta); 29 | beta_th = Array(ntheta); 30 | 31 | // variables for convinience 32 | beta = Array(ntheta); 33 | gamma = Array(ntheta); 34 | Psw = Array(ntheta); 35 | Hb = Array(ntheta); 36 | s = Array(ntheta); 37 | 38 | // eigenvalues 39 | eigenvalues = Array(ntheta); 40 | alpha_R = Array(ntheta); 41 | 42 | // solpe 43 | slope = Array(5, ntheta); 44 | R_slope_l = Array(ntheta); 45 | R_slope_r = Array(ntheta); 46 | 47 | // numerical flux 48 | numerical_flux = Array(4, ntheta + 1); 49 | dR_dt = Array(ntheta); 50 | 51 | // dy/dt 52 | dy_dt = Array(5, ntheta); 53 | 54 | // solve initial condition 55 | solvePrimitive(); 56 | solveEigen(); 57 | } 58 | 59 | void SimBox::solvePDE() { 60 | if (spread) { 61 | solveSpread(); 62 | } 63 | else { 64 | solveNoSpread(); 65 | } 66 | } 67 | 68 | Array3D& SimBox::getY() { 69 | return ys; 70 | } 71 | 72 | Array1D& SimBox::getT() { 73 | return ts; 74 | } 75 | 76 | Array1D& SimBox::getTheta() { 77 | return theta; 78 | } 79 | 80 | // ---------- private functions ---------- // 81 | void SimBox::solvePrimitive() { 82 | for (int i = 0; i < ntheta; ++i) { 83 | // velocity 84 | try { 85 | beta_gamma_sq[i] = tool->solveBetaGammaSq(Msw[i] / Eb[i], Mej[i] / Eb[i], R[i]); 86 | } 87 | catch (const std::exception& e) { 88 | std::string text = "Hydro Primitive solver: "; 89 | throw std::runtime_error(text + e.what()); 90 | } 91 | 92 | // convenient variables 93 | s[i] = tool->solveS(R[i], beta_gamma_sq[i]); 94 | gamma[i] = std::sqrt(beta_gamma_sq[i] + 1.0); 95 | beta[i] = std::sqrt(beta_gamma_sq[i] / (beta_gamma_sq[i] + 1.0)); 96 | Psw[i] = s[i] * beta[i] * beta[i] * Msw[i] / 3.0; 97 | Hb[i] = Eb[i] + Psw[i]; 98 | 99 | // tangent velocity 100 | beta_th[i] = Ht[i] / Hb[i]; 101 | } 102 | } 103 | 104 | void SimBox::solveEigen() { 105 | for (int i = 0; i < ntheta; ++i) { 106 | double A = 2.0 * s[i] / 3.0 * Msw[i] * (4.0 * gamma[i] * gamma[i] * gamma[i] * gamma[i] - 1.0) + ((1.0 - s[i]) * Msw[i] + Mej[i]) * gamma[i] * gamma[i] * gamma[i]; 107 | double dPsw_dEb = 2.0 * s[i] / 3.0 * Msw[i] / A; 108 | double dPsw_dMsw = s[i] * beta[i] * beta[i] / 3.0 - 2.0 * s[i] / 3.0 * (Eb[i] - gamma[i] * Mej[i]) / A; 109 | double dPsw_dMej = - 2.0 * s[i] / 3.0 * gamma[i] * Msw[i] / A; 110 | 111 | double B = Mej[i] / Hb[i] * dPsw_dMej + Msw[i] / Hb[i] * dPsw_dMsw; 112 | double C = std::sqrt((1.0 - beta_th[i] * beta_th[i]) * (dPsw_dEb + B) + beta_th[i] * beta_th[i] / 4.0 * B * B); 113 | double alpha1 = beta_th[i]; 114 | double alpha2 = beta_th[i] * (1.0 - B / 2.0) + C; 115 | double alpha3 = beta_th[i] * (1.0 - B / 2.0) - C; 116 | 117 | alpha1 = std::abs(alpha1); 118 | alpha2 = std::abs(alpha2); 119 | alpha3 = std::abs(alpha3); 120 | 121 | eigenvalues[i] = std::max(std::max(alpha1, alpha2), alpha3) * CSpeed / R[i]; 122 | alpha_R[i] = std::abs(beta_th[i]) / R[i]; 123 | } 124 | } 125 | 126 | void SimBox::solveSlope() { 127 | // array for convinience 128 | std::vector vars_ptr = {&Msw, &Mej, &beta_gamma_sq, &beta_th, &R}; 129 | 130 | // variables to use 131 | int index1, index2; 132 | double diff1, diff2; 133 | double slope1, slope2; 134 | 135 | // msw, mej, beta_gamma_sq, beta_th 136 | for (int j = 0; j < 4; ++j) { 137 | Array1D& var = *(vars_ptr[j]); 138 | for (int i = 0; i < ntheta; ++i) { 139 | index1 = std::max(i - 1, 0); // left cell index 140 | index2 = std::min(i + 1, ntheta - 1); // right cell index 141 | diff1 = var[i] - var[index1]; // difference to left cell 142 | diff2 = var[index2] - var[i]; // difference to right cell 143 | slope1 = (i == index1) ? 0.0 : diff1 / (theta[i] - theta[index1]); // left biased slope 144 | slope2 = (i == index2) ? 0.0 : diff2 / (theta[index2] - theta[i]); // right biased slope 145 | slope[j][i] = tool->minmod(slope1, slope2); // slope limiter 146 | } 147 | } 148 | 149 | // r 150 | for (int i = 0; i < ntheta; ++i) { 151 | index1 = std::max(i - 1, 0); // left cell index (reflective boundary condition) 152 | index2 = std::min(i + 1, ntheta - 1); // right cell index (reflective boundary condition) 153 | diff1 = R[i] - R[index1]; // difference to left cell 154 | diff2 = R[index2] - R[i]; // difference to right cell 155 | R_slope_l[i] = (i == index1) ? 0.0 : diff1 / (theta[i] - theta[index1]); // left biased slope 156 | R_slope_r[i] = (i == index2) ? 0.0 : diff2 / (theta[index2] - theta[i]); // right biased slope 157 | //slope[4][i] = (R_slope_l[i] + R_slope_r[i]) / 2.0; // total slope 158 | slope[4][i] = tool->minmod(R_slope_l[i], R_slope_r[i]); 159 | } 160 | } 161 | 162 | void SimBox::solveNumericalFlux() { 163 | Array1D var_l = Array(5); // left-biased reconstructed variable 164 | Array1D var_r = Array(5); // right-biased reconstructed variable 165 | Array1D F_l = Array(5); // left-biased physical flux 166 | Array1D F_r = Array(5); // right-biased physical flux 167 | double alpha; // numerical viscocity 168 | double Eb_l, Psw_l, Ht_l; // left-biased conserved 169 | double Eb_r, Psw_r, Ht_r; // right-biased conserved 170 | double s_l; // left biased calibration coefficient 171 | double s_r; // right biased calibration coefficient 172 | 173 | // alias 174 | double& Msw_l = var_l[0]; 175 | double& Mej_l = var_l[1]; 176 | double& beta_gamma_sq_l = var_l[2]; 177 | double& beta_th_l = var_l[3]; 178 | double& R_l = var_l[4]; 179 | 180 | double& Msw_r = var_r[0]; 181 | double& Mej_r = var_r[1]; 182 | double& beta_gamma_sq_r = var_r[2]; 183 | double& beta_th_r = var_r[3]; 184 | double& R_r = var_r[4]; 185 | 186 | // array for convinience 187 | std::vector vars_ptr = {&Msw, &Mej, &beta_gamma_sq, &beta_th, &R}; 188 | 189 | // only need to loop over middle faces. Polar faces have flux=0 190 | for (int i = 1; i < ntheta; ++i) { 191 | // reconstruct primitive variables 192 | for (int j = 0; j < 5; ++j) { 193 | Array1D& var = *(vars_ptr)[j]; // variable to reconstruct 194 | 195 | // left biased reconstruction 196 | var_l[j] = var[i - 1] + slope[j][i - 1] * (theta_edge[i] - theta[i - 1]); 197 | 198 | // right biased reconstruction 199 | var_r[j] = var[i] + slope[j][i] * (theta_edge[i] - theta[i]); 200 | } 201 | 202 | // solve calibration coefficients 203 | s_l = tool->solveS(R_l, beta_gamma_sq_l); 204 | s_r = tool->solveS(R_r, beta_gamma_sq_r); 205 | 206 | // solve left-biased conserved variables 207 | Eb_l = s_l * (1.0 + beta_gamma_sq_l * beta_gamma_sq_l / (beta_gamma_sq_l + 1) / (beta_gamma_sq_l + 1) / 3.0) * (beta_gamma_sq_l + 1) * Msw_l 208 | + (1.0 - s_l) * std::sqrt(beta_gamma_sq_l + 1) * Msw_l 209 | + std::sqrt(beta_gamma_sq_l + 1) * Mej_l; 210 | Psw_l = s_l * beta_gamma_sq_l / (beta_gamma_sq_l + 1) * Msw_l / 3.0; 211 | Ht_l = (Eb_l + Psw_l) * beta_th_l; 212 | 213 | // solve right-biased conserved variables 214 | Eb_r = s_r * (1.0 + beta_gamma_sq_r * beta_gamma_sq_r / (beta_gamma_sq_r + 1) / (beta_gamma_sq_r + 1) / 3.0) * (beta_gamma_sq_r + 1) * Msw_r 215 | + (1.0 - s_r) * std::sqrt(beta_gamma_sq_r + 1) * Msw_r 216 | + std::sqrt(beta_gamma_sq_r + 1) * Mej_r; 217 | Psw_r = s_r * beta_gamma_sq_r / (beta_gamma_sq_r + 1) * Msw_r / 3.0; 218 | Ht_r = (Eb_r + Psw_r) * beta_th_r; 219 | 220 | // physical flux (left) 221 | F_l[0] = Ht_l / R_l * CSpeed; 222 | F_l[1] = (Ht_l * beta_th_l + Psw_l) / R_l * CSpeed; 223 | F_l[2] = Msw_l * beta_th_l / R_l * CSpeed; 224 | F_l[3] = Mej_l * beta_th_l / R_l * CSpeed; 225 | 226 | // physical flux (right) 227 | F_r[0] = Ht_r / R_r * CSpeed; 228 | F_r[1] = (Ht_r * beta_th_r + Psw_r) / R_r * CSpeed; 229 | F_r[2] = Msw_r * beta_th_r / R_r * CSpeed; 230 | F_r[3] = Mej_r * beta_th_r / R_r * CSpeed; 231 | 232 | // viscosity (maximum eigenvalue over 4 neighbor cells) 233 | int index_l = std::max(i - 2, 0); 234 | int index_r = std::min(i + 1, ntheta - 1); 235 | alpha = *std::max_element(eigenvalues.begin() + index_l, eigenvalues.begin() + index_r); 236 | 237 | // numerical flux 238 | numerical_flux[0][i] = 0.5 * (F_l[0] + F_r[0] - alpha * (Eb_r - Eb_l)); 239 | numerical_flux[1][i] = 0.5 * (F_l[1] + F_r[1] - alpha * (Ht_r - Ht_l)); 240 | numerical_flux[2][i] = 0.5 * (F_l[2] + F_r[2] - alpha * (Msw_r - Msw_l)); 241 | numerical_flux[3][i] = 0.5 * (F_l[3] + F_r[3] - alpha * (Mej_r - Mej_l)); 242 | for (int j = 0 ; j < 4; ++j) { 243 | numerical_flux[j][i] *= std::sin(theta_edge[i]); 244 | } 245 | } 246 | } 247 | 248 | double SimBox::solveDeltaT() { 249 | double omega, omega_all; 250 | double delta_t_min, delta_t; 251 | 252 | // find minimum value 253 | delta_t_min = std::numeric_limits::max(); 254 | for (int i = 0; i < ntheta; ++i) { 255 | // maximum possible omega 256 | omega = beta[i] * CSpeed / R[i]; 257 | 258 | // combined signal speed and maximum omega 259 | omega_all = eigenvalues[i] + 0.05 * omega; 260 | //omega_all = omega; 261 | 262 | // delta_t of a cell 263 | delta_t = cfl * (theta_edge[i + 1] - theta_edge[i]) / omega_all; 264 | 265 | // find minimum 266 | delta_t_min = std::min(delta_t_min, delta_t); 267 | } 268 | 269 | return delta_t_min; 270 | } 271 | 272 | void SimBox::solveDyDt() { 273 | for (int i = 0; i < ntheta; ++i) { 274 | // solve needed variables 275 | double beta_f = 4.0 * beta[i] * (beta_gamma_sq[i] + 1) / (4.0 * beta_gamma_sq[i] + 3.0); 276 | double beta_r = (beta_th[i] <= beta[i]) ? std::sqrt(beta[i] * beta[i] - beta_th[i] * beta_th[i]) : 0.0; 277 | double vol = std::cos(theta_edge[i]) - std::cos(theta_edge[i + 1]); 278 | 279 | // solve dR_dt 280 | int index_l = std::max(i - 1, 0); 281 | int index_r = std::min(i + 1, ntheta - 1); 282 | double alpha = std::max(std::max(alpha_R[index_l], alpha_R[i]), alpha_R[index_r]); 283 | dy_dt[4][i] = beta_f - slope[4][i] * beta_th[i] / R[i] + 0.5 * alpha * (R_slope_r[i] - R_slope_l[i]); 284 | dy_dt[4][i] *= CSpeed; 285 | 286 | // conserved variables 287 | double rho = tool->solveDensity(R[i]) * MassP; 288 | dy_dt[0][i] = (numerical_flux[0][i] - numerical_flux[0][i + 1]) / vol 289 | + dy_dt[4][i] * rho * R[i] * R[i]; 290 | dy_dt[1][i] = (numerical_flux[1][i] - numerical_flux[1][i + 1]) / vol 291 | + (std::cos(theta[i]) / std::sin(theta[i]) * Psw[i] - Ht[i] * beta_r) * CSpeed / R[i]; 292 | dy_dt[2][i] = (numerical_flux[2][i] - numerical_flux[2][i + 1]) / vol 293 | + dy_dt[4][i] * rho * R[i] * R[i]; 294 | dy_dt[3][i] = (numerical_flux[3][i] - numerical_flux[3][i + 1]) / vol 295 | + 0.0; 296 | } 297 | } 298 | 299 | void SimBox::solveDyDt_no_spread() { 300 | for (int i = 0; i < ntheta; ++i) { 301 | // solve needed variables 302 | double beta_f = 4.0 * beta[i] * (beta_gamma_sq[i] + 1) / (4.0 * beta_gamma_sq[i] + 3.0); 303 | 304 | // solve dR_dt 305 | dy_dt[4][i] = beta_f * CSpeed; 306 | 307 | // conserved variables 308 | double rho = tool->solveDensity(R[i]) * MassP; 309 | dy_dt[0][i] = dy_dt[4][i] * rho * R[i] * R[i]; 310 | dy_dt[1][i] = 0.0; 311 | dy_dt[2][i] = dy_dt[4][i] * rho * R[i] * R[i]; 312 | dy_dt[3][i] = 0.0; 313 | } 314 | } 315 | 316 | void SimBox::oneStepRK2(double dt) { 317 | // alias 318 | std::vector conserved = {&Eb, &Ht, &Msw, &Mej, &R}; 319 | 320 | // copy initial conserved variables 321 | Array2D conserved_ini = {Eb, Ht, Msw, Mej, R}; 322 | 323 | // ---------- step 1 ---------- // 324 | // solve dydy 325 | solveSlope(); 326 | solveNumericalFlux(); 327 | solveDyDt(); 328 | 329 | // update variables 330 | for (int i = 0; i < 5; ++i) { 331 | for (int j = 0; j < ntheta; ++j) { 332 | (*conserved[i])[j] += dt * dy_dt[i][j]; 333 | } 334 | } 335 | 336 | // update primitive & eigenvalues 337 | solvePrimitive(); 338 | solveEigen(); 339 | 340 | // ---------- step 2 ---------- // 341 | // solve dydy 342 | solveSlope(); 343 | solveNumericalFlux(); 344 | solveDyDt(); 345 | 346 | // update variables 347 | for (int i = 0; i < 5; ++i) { 348 | for (int j = 0; j < ntheta; ++j) { 349 | (*conserved[i])[j] = 0.5 * conserved_ini[i][j] + 0.5 * (*conserved[i])[j] + 0.5 * dt * dy_dt[i][j]; 350 | } 351 | } 352 | 353 | // update primitive & eigenvalues 354 | solvePrimitive(); 355 | solveEigen(); 356 | } 357 | 358 | void SimBox::oneStepRK45(double& dt, const double rtol, bool& succeeded) { 359 | // alias of conserved variables 360 | std::vector conserved = {&Eb, &Ht, &Msw, &Mej, &R}; 361 | 362 | // save initial conserved variables 363 | Array2D conserved_ini = {Eb, Ht, Msw, Mej, R}; 364 | 365 | // middle steps 366 | Array2D k1, k2, k3, k4, k5, k6; 367 | k1 = k2 = k3 = k4 = k5 = k6 = Array(5, ntheta); 368 | 369 | // ---------- step 1 ---------- // 370 | // solve dydt 371 | solveDyDt_no_spread(); 372 | 373 | // update variables 374 | for (int i = 0; i < 5; ++i) { 375 | for (int j = 0; j < ntheta; ++j) { 376 | k1[i][j] = dt * dy_dt[i][j]; 377 | (*conserved[i])[j] = conserved_ini[i][j] + k1[i][j] * 2.0 / 9.0; 378 | } 379 | } 380 | 381 | // update primitives 382 | solvePrimitive(); 383 | 384 | // ---------- step 2 ---------- // 385 | // solve dydt 386 | solveDyDt_no_spread(); 387 | 388 | // update variables 389 | for (int i = 0; i < 5; ++i) { 390 | for (int j = 0; j < ntheta; ++j) { 391 | k2[i][j] = dt * dy_dt[i][j]; 392 | (*conserved[i])[j] = conserved_ini[i][j] + k1[i][j] / 12.0 + k2[i][j] / 4.0; 393 | } 394 | } 395 | 396 | // update primitives 397 | solvePrimitive(); 398 | 399 | // ---------- step 3 ---------- // 400 | // solve dydt 401 | solveDyDt_no_spread(); 402 | 403 | // update variables 404 | for (int i = 0; i < 5; ++i) { 405 | for (int j = 0; j < ntheta; ++j) { 406 | k3[i][j] = dt * dy_dt[i][j]; 407 | (*conserved[i])[j] = conserved_ini[i][j] + k1[i][j] * 69.0 / 128.0 - k2[i][j] * 243.0 / 128.0 + k3[i][j] * 135.0 / 64.0; 408 | } 409 | } 410 | 411 | // update primitives 412 | solvePrimitive(); 413 | 414 | // ---------- step 4 ---------- // 415 | // solve dydt 416 | solveDyDt_no_spread(); 417 | 418 | // update variables 419 | for (int i = 0; i < 5; ++i) { 420 | for (int j = 0; j < ntheta; ++j) { 421 | k4[i][j] = dt * dy_dt[i][j]; 422 | (*conserved[i])[j] = conserved_ini[i][j] - k1[i][j] * 17.0 / 12.0 + k2[i][j] * 27.0 / 4.0 - k3[i][j] * 27.0 / 5.0 + k4[i][j] * 16.0 / 15.0; 423 | } 424 | } 425 | 426 | // update primitives 427 | solvePrimitive(); 428 | 429 | // ---------- step 5 ---------- // 430 | // solve dydt 431 | solveDyDt_no_spread(); 432 | 433 | // update variables 434 | for (int i = 0; i < 5; ++i) { 435 | for (int j = 0; j < ntheta; ++j) { 436 | k5[i][j] = dt * dy_dt[i][j]; 437 | (*conserved[i])[j] = conserved_ini[i][j] + k1[i][j] * 65.0 / 432.0 - k2[i][j] * 5.0 / 16.0 + k3[i][j] * 13.0 / 16.0 + k4[i][j] * 4.0 / 27.0 + k5[i][j] * 5.0 / 144.0; 438 | } 439 | } 440 | 441 | // update primitives 442 | solvePrimitive(); 443 | 444 | // ---------- step 6 ---------- // 445 | // solve dydt 446 | solveDyDt_no_spread(); 447 | 448 | // update variables 449 | for (int i = 0; i < 5; ++i) { 450 | for (int j = 0; j < ntheta; ++j) { 451 | k6[i][j] = dt * dy_dt[i][j]; 452 | (*conserved[i])[j] = conserved_ini[i][j] + k1[i][j] * 47.0 / 450.0 + k3[i][j] * 12.0 / 25.0 + k4[i][j] * 32.0 / 225.0 + k5[i][j] / 30.0 + k6[i][j] * 6.0 / 25.0; 453 | } 454 | } 455 | 456 | // ---------- error estimate ---------- // 457 | double error; 458 | double rerror = 0.0; 459 | for (int i = 0; i < 5; ++i) { 460 | for (int j = 0; j < ntheta; ++j) { 461 | error = std::abs(k1[i][j] / 150.0 - k3[i][j] * 3.0 / 100.0 + k4[i][j] * 16.0 / 75.0 + k5[i][j] / 20.0 - k6[i][j] * 6.0 / 25.0); 462 | rerror = std::max(rerror, error / std::abs((*conserved[i])[j])); 463 | } 464 | } 465 | 466 | if (rerror < rtol) { 467 | // good! update primitives 468 | solvePrimitive(); 469 | 470 | // mark 471 | succeeded = true; 472 | } 473 | else { 474 | // roll back to the initial conserved variables 475 | for (int i = 0; i < 5; ++i) { 476 | *conserved[i] = conserved_ini[i]; 477 | } 478 | 479 | // update primitives 480 | solvePrimitive(); 481 | 482 | // mark 483 | succeeded = false; 484 | } 485 | 486 | // update dt 487 | double boost_factor = 0.9 * std::pow(rtol / rerror, 0.2); 488 | boost_factor = std::min(1.5, boost_factor); 489 | dt *= boost_factor; 490 | } 491 | 492 | void SimBox::solveSpread() { 493 | // alias 494 | std::vector primitives = {&Msw, &Mej, &beta_gamma_sq, &beta_th, &R}; 495 | 496 | // record initial condition 497 | ts.push_back(tmin); 498 | ys = Array(5, ntheta, 1); 499 | for (int i = 0; i < 5; ++i) { 500 | for (int j = 0; j < ntheta; ++j) { 501 | ys[i][j][0] = (*primitives[i])[j]; 502 | } 503 | } 504 | 505 | // solve PDE 506 | double dt; 507 | double t = tmin; 508 | while (t < tmax) { 509 | // interruption detection 510 | if (PyErr_CheckSignals() != 0) { 511 | throw std::runtime_error("Hydro: Keyboard interruption."); 512 | } 513 | 514 | // solve one step 515 | dt = std::min(solveDeltaT(), tmax - t + 1e-6); 516 | oneStepRK2(dt); 517 | t += dt; 518 | 519 | // save variables 520 | ts.push_back(t); 521 | for (int i = 0; i < 5; ++i) { 522 | for (int j = 0; j < ntheta; ++j) { 523 | ys[i][j].push_back((*primitives[i])[j]); 524 | } 525 | } 526 | } 527 | } 528 | 529 | void SimBox::solveNoSpread() { 530 | // alias 531 | std::vector primitives = {&Msw, &Mej, &beta_gamma_sq, &beta_th, &R}; 532 | 533 | // record initial condition 534 | ts.push_back(tmin); 535 | ys = Array(5, ntheta, 1); 536 | for (int i = 0; i < 5; ++i) { 537 | for (int j = 0; j < ntheta; ++j) { 538 | ys[i][j][0] = (*primitives[i])[j]; 539 | } 540 | } 541 | 542 | // solve PDE 543 | double delta_t = 1.0; 544 | double t = tmin; 545 | while (t < tmax) { 546 | // interruption detection 547 | if (PyErr_CheckSignals() != 0) { 548 | throw std::runtime_error("Hydro: Keyboard interruption."); 549 | } 550 | 551 | // solve one step 552 | bool succeeded; 553 | double dt = delta_t; 554 | oneStepRK45(dt, 1e-6, succeeded); 555 | 556 | if (succeeded) { 557 | t += delta_t; 558 | delta_t = dt; 559 | 560 | // save variables 561 | ts.push_back(t); 562 | for (int i = 0; i < 5; ++i) { 563 | for (int j = 0; j < ntheta; ++j) { 564 | ys[i][j].push_back((*primitives[i])[j]); 565 | } 566 | } 567 | } 568 | else { 569 | delta_t = dt; 570 | } 571 | } 572 | } -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/sim_box.h: -------------------------------------------------------------------------------- 1 | #ifndef SIM_BOX 2 | #define SIM_BOX 3 | 4 | #include "../environment.h" 5 | #include "config.h" 6 | #include "tools.h" 7 | 8 | class SimBox { 9 | public: 10 | SimBox(const JetConfig& jet_config, Tool& tool); 11 | 12 | // ---------- interfaces ---------- // 13 | void solvePDE(); // solve PDE 14 | Array3D& getY(); // get PDE ys 15 | Array1D& getT(); // get PDE ts 16 | Array1D& getTheta(); // get cell centers 17 | private: 18 | // tools 19 | Tool* tool; // useful functions 20 | 21 | // configuration 22 | double tmin; // minimum pde time 23 | double tmax; // maximum pde time 24 | double cfl; // cfl number 25 | bool spread; // spread or not 26 | 27 | // mesh 28 | int ntheta; // number of cells 29 | Array1D theta; // cell center position 30 | Array1D theta_edge; // cell edge position 31 | 32 | // conserved variables 33 | Array1D Eb; // Eb 34 | Array1D Ht; // Hb * beta_th 35 | Array1D Msw; // Msw 36 | Array1D Mej; // Mej 37 | Array1D R; // R 38 | 39 | // primitive variables 40 | Array1D beta_gamma_sq; // (beta * gamma) ^ 2 41 | Array1D beta_th; // beta_theta 42 | Array1D Psw; // Psw 43 | Array1D Hb; // Hb 44 | Array1D s; // s 45 | 46 | // variables for convinience 47 | Array1D beta; // beta 48 | Array1D gamma; // gamma 49 | 50 | // eigenvalues 51 | Array1D eigenvalues; // maximum eigenvalues of the four 52 | Array1D alpha_R; // viscosity for R equation 53 | 54 | // solpe 55 | Array2D slope; // (5, ntheta) [Msw, Mej, u_sq, beta_th, R] slope 56 | Array1D R_slope_l; // left biased R slope 57 | Array1D R_slope_r; // right biased R slope 58 | 59 | // numerical flux 60 | Array2D numerical_flux; // (4, ntheta + 1) numerical flux 61 | Array1D dR_dt; // dR / dt 62 | 63 | // dy / dt 64 | Array2D dy_dt; // (5, ntheta) dy/dt 65 | 66 | // PDE solution 67 | Array3D ys; // (5, ntheta, nt) 68 | Array1D ts; // (nt) 69 | 70 | // ---------- functions ---------- // 71 | void solvePrimitive(); // solve primitive and convenient variables 72 | void solveEigen(); // solve eigenvalues 73 | void solveSlope(); // reconstruct 74 | void solveNumericalFlux(); // solve riemann problem 75 | double solveDeltaT(); // solve delta_t 76 | void solveDyDt(); // solve dy_dt (with spread) 77 | void solveDyDt_no_spread(); // solve dy_dt (without spread) 78 | void oneStepRK2(double dt); // one step forward (with spread) 79 | void oneStepRK45(double& dt, const double rtol, bool& succeeded); // one step forward (without spread) 80 | void solveSpread(); // solve PDE with spreading 81 | void solveNoSpread(); // solve PDE without spreading 82 | }; 83 | 84 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/tools.cpp: -------------------------------------------------------------------------------- 1 | #include "tools.h" 2 | 3 | Tool::Tool(const JetConfig& jet_config) { 4 | nwind = jet_config.nwind; 5 | nism = jet_config.nism; 6 | rtol = jet_config.rtol; 7 | cal_level = jet_config.cal_level; 8 | } 9 | 10 | double Tool::solveDensity(double r) { 11 | return nwind / (r / 1e17) / (r / 1e17) + nism; 12 | } 13 | 14 | double Tool::solveS(double r, double beta_gamma_sq) { 15 | // density slope 16 | double k = 2.0 * nwind / (nwind + nism * (r / 1e17) * (r / 1e17)); 17 | 18 | // calibration coefficient 19 | double s; 20 | if (cal_level == 0) { 21 | s = 1.0; 22 | } 23 | else if (cal_level == 1) { 24 | s = 0.52935729 - 0.05698377 * k - 0.00158176 * k * k - 0.00939548 * k * k * k; 25 | } 26 | else if (cal_level == 2) { 27 | double sBM = 0.52935729 - 0.05698377 * k - 0.00158176 * k * k - 0.00939548 * k * k * k; 28 | double sST = 1.635 - 0.651 * k; 29 | s = (sST + sBM * factor * beta_gamma_sq) / (1.0 + factor * beta_gamma_sq); 30 | } 31 | else { 32 | throw std::runtime_error("Hydro: invalid calibration level!"); 33 | } 34 | 35 | return s; 36 | } 37 | 38 | double Tool::solveBetaGammaSq(double msw_eb, double mej_eb, double r) { 39 | double beta_gamma_min_sq = 0.0; //(0.75 / (msw_eb + mej_eb) - 1) * 0.99; 40 | double gamma_max = 1.0 / (msw_eb + mej_eb); 41 | double beta_gamma_max_sq = (gamma_max * gamma_max - 1.0) * 1.01; 42 | 43 | // solve root 44 | auto f = [&](const double& u_sq) { 45 | double beta_sq = u_sq / (u_sq + 1.0); 46 | double gamma = std::sqrt(u_sq + 1.0); 47 | double s = solveS(r, u_sq); 48 | return s * gamma * gamma * (1.0 + beta_sq * beta_sq / 3.0) * msw_eb + gamma * ((1.0 - s) * msw_eb + mej_eb) - 1.0; 49 | }; 50 | 51 | double u_sq = brentq(f, beta_gamma_min_sq, beta_gamma_max_sq, 0.0, rtol); 52 | return u_sq; 53 | } 54 | 55 | double Tool::minmod(double x1, double x2) { 56 | if (x1 * x2 > 0) { 57 | if (std::fabs(x1) < std::fabs(x2)) { 58 | return x1; 59 | } 60 | else { 61 | return x2; 62 | } 63 | } 64 | else { 65 | return 0; 66 | } 67 | } 68 | 69 | void Tool::findIndex(const Array1D& x_array, const double x, int& index1, int& index2) { 70 | index1 = 0; 71 | index2 = x_array.size() - 1; 72 | 73 | int index_mid; 74 | while (index2 - index1 > 1) { 75 | index_mid = (index1 + index2) / 2; 76 | if (x > x_array[index_mid]) { 77 | index1 = index_mid; 78 | } 79 | else { 80 | index2 = index_mid; 81 | } 82 | } 83 | } 84 | 85 | double Tool::linear(double x, double x1, double x2, double y1, double y2) { 86 | return (x1 == x2) ? y1 : (y2 - y1) / (x2 - x1) * (x - x1) + y1; 87 | } -------------------------------------------------------------------------------- /jetsimpy/src/Hydro/tools.h: -------------------------------------------------------------------------------- 1 | #ifndef TOOLS 2 | #define TOOLS 3 | 4 | #include "../environment.h" 5 | #include "config.h" 6 | #include "../Math/root.h" 7 | 8 | // the tool to store useful functions 9 | class Tool { 10 | public: 11 | Tool(const JetConfig& jet_config); 12 | double solveDensity(double r); // solve ambient density 13 | double solveBetaGammaSq(double msw_eb, double mej_eb, double r); // solve (beta * gamma) by Msw/Eb and Mej/Eb 14 | double minmod(double x1, double x2); // Minmod function 15 | double solveS(double r, double beta_gamma_sq); // solve calibration coefficient 16 | void findIndex(const Array1D& x_array, const double x, int& index1, int& index2); // binary search for index range 17 | double linear(double x, double x1, double x2, double y1, double y2); // linear interpolation 18 | 19 | private: 20 | const double factor = 2.0; // a constant factor to interpolate BM & ST 21 | double nwind; // wind density scale 22 | double nism; // ISM density scale 23 | double rtol; // relative tolerance of solving (beta * gamma) 24 | int cal_level; // calibration level. 25 | }; 26 | 27 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Math/integral.h: -------------------------------------------------------------------------------- 1 | #ifndef INTEGRAL 2 | #define INTEGRAL 3 | 4 | #include "../environment.h" 5 | 6 | // representation of a 5-point interval for integration 7 | struct Interval { 8 | double x1; // start point x value of the interval 9 | std::array y; // array of function values 10 | double h; // interval length 11 | double lorder; // low order integral estimation 12 | double horder; // high order integral estimation 13 | 14 | // compute the low order and high order estimations 15 | void integrate() { 16 | // Simpson's rule 17 | lorder = (y[0] + 4.0 * y[2] + y[4]) * h / 6.0; 18 | 19 | // Composite Simpson's rule 20 | //horder = (y[0] + 4.0 * y[1] + 2.0 * y[2] + 4.0 * y[3] + y[4]) * h / 12.0; 21 | 22 | // Boole's rule (1 step Romberg extrapolation) 23 | horder = (7.0 * y[0] + 32.0 * y[1] + 12.0 * y[2] + 32.0 * y[3] + 7.0 * y[4]) * h / 90.0; 24 | } 25 | }; 26 | 27 | // Adaptive 1D integral with initial x samples 28 | template 29 | double Adaptive_1D(F& f, const Array1D xini, const double xtol, const double rtol, const int max_iter = 50, const bool force_return = true) { 30 | // initialize interval 31 | Interval new_itv; 32 | std::vector intervals = {}; 33 | double integral_tot = 0.0; 34 | int xsize = xini.size(); 35 | for (int i = 0; i < xsize - 1; ++i) { 36 | // assign values to a new interval 37 | new_itv.x1 = xini[i]; 38 | new_itv.h = xini[i + 1] - xini[i]; 39 | new_itv.y = {f(xini[i]), f(xini[i] + new_itv.h / 4.0), f(xini[i] + 2.0 * new_itv.h / 4.0), f(xini[i] + 3.0 * new_itv.h / 4.0), f(xini[i + 1])}; 40 | new_itv.integrate(); 41 | 42 | // push_back to interval vectors 43 | intervals.push_back(new_itv); 44 | integral_tot += new_itv.horder; 45 | } 46 | double h_tot = xini.back() - xini.front(); 47 | 48 | // control variables 49 | bool need_refine; // if a single interval needs refinement 50 | bool any_refined; // if any of the interval is refined 51 | double integral_new; // a variable to update total integral 52 | 53 | // adptive refinement 54 | for (int i = 0; i < max_iter; ++i) { 55 | // initialize some variables 56 | any_refined = false; 57 | int intervals_size = intervals.size(); 58 | integral_new = integral_tot; 59 | 60 | // loop over all intervals and make necessary refinement 61 | for (int i = 0; i < intervals_size; ++i) { 62 | // alias 63 | Interval& itv = intervals[i]; 64 | 65 | // test if refinement is needed 66 | need_refine = ( 67 | // local relative error 68 | std::abs(itv.lorder - itv.horder) > xtol * itv.h / h_tot + rtol * std::abs(itv.horder) 69 | && 70 | // globally relative local error (Gander & Gautschi 2001 ?) 71 | std::abs(itv.lorder - itv.horder) * intervals_size > xtol + rtol * std::abs(integral_tot) 72 | ); 73 | 74 | // refine 75 | if (need_refine) { 76 | // subtract the contribution from this interval 77 | integral_new -= itv.horder; 78 | 79 | // refine right half part by a new interval 80 | new_itv.x1 = itv.x1 + itv.h / 2.0; 81 | new_itv.h = itv.h / 2.0; 82 | new_itv.y = {itv.y[2], f(new_itv.x1 + new_itv.h / 4.0), itv.y[3], f(new_itv.x1 + 3.0 * new_itv.h / 4.0), itv.y[4]}; 83 | new_itv.integrate(); 84 | 85 | // refine left half part by changing original interval 86 | itv.h = itv.h / 2.0; 87 | itv.y = {itv.y[0], f(itv.x1 + itv.h / 4.0), itv.y[1], f(itv.x1 + 3.0 * itv.h / 4.0), itv.y[2]}; 88 | itv.integrate(); 89 | 90 | // update total integral 91 | integral_new += (new_itv.horder + itv.horder); 92 | 93 | // mark 94 | any_refined = true; 95 | 96 | // add new interval to the last 97 | intervals.push_back(new_itv); 98 | } 99 | else { 100 | continue; 101 | } 102 | } 103 | 104 | // update total integral 105 | integral_tot = integral_new; 106 | 107 | // if any of the intervals are refined? 108 | if (any_refined) { 109 | // do nothing, keep refinement 110 | continue; 111 | } 112 | else { 113 | // if no refinement, return the value 114 | return integral_tot; 115 | } 116 | } 117 | 118 | if (force_return) { 119 | return integral_tot; 120 | } 121 | else { 122 | // if integral does not converge in max_iter loops, throw error 123 | throw std::runtime_error("Adaptive integration: convergence NOT achieved after " + std::to_string(max_iter) + " iterations! Increase 'max_iter' or set 'force_return = True'."); 124 | } 125 | } 126 | 127 | // Adaptive 2D integral with initial x and y samples 128 | template 129 | double Adaptive_2D(F& f, const Array1D& xini, const Array1D& yini, const double xtol, const double rtol, const int max_iter = 50, const bool force_return = true) { 130 | auto g = [&](const double y) { 131 | auto h = [&](const double x) { 132 | return f(x, y); 133 | }; 134 | double result = Adaptive_1D(h, xini, xtol, rtol, max_iter, force_return); 135 | return result; 136 | }; 137 | return Adaptive_1D(g, yini, xtol, rtol, max_iter, force_return); 138 | } 139 | 140 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Math/optimize.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTIMIZATION 2 | #define OPTIMIZATION 3 | 4 | #include "../environment.h" 5 | 6 | // this algorithms is only valid to the setup of Flux calculation 7 | // this algorithm does minimization 8 | template 9 | double golden_section(F& f, const double& xx1, const double& xx2, const double& atol, const int& max_iter = 100) { 10 | double r = 0.618; 11 | 12 | double x1 = xx1; 13 | double x2 = xx2; 14 | double xm1 = (x1 + r * x2) / (1.0 + r); 15 | double f1 = f(x1); 16 | double fm1 = f(xm1); 17 | double f2 = f(x2); 18 | 19 | // braket the range 20 | int n = 0; 21 | while (n < 10) { 22 | if (fm1 > f1 && fm1 <= f2) { 23 | x2 = xm1; 24 | f2 = fm1; 25 | 26 | xm1 = (x1 + r * x2) / (1.0 + r); 27 | fm1 = f(xm1); 28 | n += 1; 29 | } 30 | else if (fm1 <= f1 && fm1 > f2) { 31 | x1 = xm1; 32 | f1 = fm1; 33 | 34 | xm1 = (x1 + r * x2) / (1.0 + r); 35 | fm1 = f(xm1); 36 | n += 1; 37 | } 38 | else if (fm1 <= f1 && fm1 <= f2) { 39 | break; 40 | } 41 | else { 42 | if (f1 < f2) { 43 | x2 = xm1; 44 | f2 = fm1; 45 | 46 | xm1 = (x1 + r * x2) / (1.0 + r); 47 | fm1 = f(xm1); 48 | n += 1; 49 | } 50 | else { 51 | x1 = xm1; 52 | f1 = fm1; 53 | 54 | xm1 = (x1 + r * x2) / (1.0 + r); 55 | fm1 = f(xm1); 56 | n += 1; 57 | } 58 | } 59 | } 60 | 61 | // if the braketing failed 62 | if (n >= 10) { 63 | if (fm1 > f1) { 64 | return x1; 65 | } 66 | else { 67 | return x2; 68 | } 69 | } 70 | 71 | // another side 72 | double xm2 = (r * x1 + x2) / (1.0 + r); 73 | double fm2 = f(xm2); 74 | 75 | // loop 76 | n = 0; 77 | while(std::fabs(xm1 - xm2) > atol && n < max_iter) { 78 | if (fm1 < fm2) { 79 | x2 = xm2; 80 | f2 = fm2; 81 | 82 | xm2 = xm1; 83 | fm2 = fm1; 84 | 85 | xm1 = (x1 + r * x2) / (1.0 + r); 86 | fm1 = f(xm1); 87 | } 88 | else { 89 | x1 = xm1; 90 | f1 = fm1; 91 | 92 | xm1 = xm2; 93 | fm1 = fm2; 94 | 95 | xm2 = (r * x1 + x2) / (1.0 + r); 96 | fm2 = f(xm2); 97 | } 98 | n += 1; 99 | } 100 | 101 | //if (n >= max_iter) { 102 | // std::string text = "Optimizer doesn't converge after " + std::to_string(max_iter) + " interations!\n"; 103 | // throw std::runtime_error(text); 104 | //} 105 | 106 | return (xm1 + xm2) / 2.0; 107 | } 108 | 109 | // minimization givn an initial sampling 110 | template 111 | double minimization(F& f, const double& x1, const double& x2, const int& n_opt, const double& atol, const std::string& type_, const int& max_iter = 100) { 112 | Array1D x_ini = Array(n_opt); 113 | Array1D f_ini = Array(n_opt); 114 | 115 | // evaluate initial sampling 116 | if (type_ == "linear") { //linear spacing 117 | for (int i = 0; i < n_opt; ++i) { 118 | x_ini[i] = x1 + i * (x2 - x1) / (n_opt - 1); 119 | f_ini[i] = f(x_ini[i]); 120 | } 121 | } 122 | else if (type_ == "log") { // spacing by arcsinh 123 | double asinh_x1 = std::asinh(x1); 124 | double asinh_x2 = std::asinh(x2); 125 | for (int i = 0; i < n_opt; ++i) { 126 | x_ini[i] = asinh_x1 + i * (asinh_x2 - asinh_x1) / (n_opt - 1); 127 | x_ini[i] = std::sinh(x_ini[i]); 128 | f_ini[i] = f(x_ini[i]); 129 | } 130 | } 131 | else { 132 | std::string text = "No such optimization method: '" + type_ + "'\n"; 133 | throw std::runtime_error(text); 134 | } 135 | 136 | // find minimum place 137 | double x_peak; 138 | int argmin = std::distance(f_ini.begin(), std::min_element(f_ini.begin(), f_ini.end())); 139 | if (argmin == 0) { 140 | x_peak = golden_section(f, x_ini[0], x_ini[1], atol, max_iter); 141 | } 142 | else if (argmin == n_opt - 1) { 143 | x_peak = golden_section(f, x_ini[n_opt - 2], x_ini[n_opt - 1], atol, max_iter); 144 | } 145 | else { 146 | x_peak = golden_section(f, x_ini[argmin - 1], x_ini[argmin + 1], atol, max_iter); 147 | } 148 | 149 | return x_peak; 150 | } 151 | 152 | template 153 | double minimization(F& f, const Array1D& x_ini, const double& atol, const int& max_iter = 100) { 154 | int n = x_ini.size(); 155 | Array1D f_ini = Array(n); 156 | 157 | // evaluate initial sampling 158 | for (int i = 0; i < n; ++i) { 159 | f_ini[i] = f(x_ini[i]); 160 | } 161 | 162 | // find minimum place 163 | double x_peak; 164 | int argmin = std::distance(f_ini.begin(), std::min_element(f_ini.begin(), f_ini.end())); 165 | if (argmin == 0) { 166 | x_peak = golden_section(f, x_ini[0], x_ini[1], atol, max_iter); 167 | } 168 | else if (argmin == n - 1) { 169 | x_peak = golden_section(f, x_ini[n - 2], x_ini[n - 1], atol, max_iter); 170 | } 171 | else { 172 | x_peak = golden_section(f, x_ini[argmin - 1], x_ini[argmin + 1], atol, max_iter); 173 | } 174 | 175 | return x_peak; 176 | } 177 | 178 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/Math/root.h: -------------------------------------------------------------------------------- 1 | #ifndef ROOTSOLVER 2 | #define ROOTSOLVER 3 | 4 | #include "../environment.h" 5 | 6 | // Brent's method. Code from Scipy. 7 | template 8 | double brentq(F& f, double xa, double xb, double xtol, double rtol = 1e-8, int iter = 100) { 9 | using namespace std; 10 | double xpre = xa, xcur = xb; 11 | double xblk = 0., fpre, fcur, fblk = 0., spre = 0., scur = 0., sbis; 12 | // the tolerance is 2*delta 13 | double delta; 14 | double stry, dpre, dblk; 15 | int i; 16 | 17 | fpre = f(xpre); 18 | fcur = f(xcur); 19 | 20 | // check if the root is at boundary 21 | if (fabs(fpre) == 0) { 22 | return xpre; 23 | } 24 | else if (fabs(fcur) == 0) { 25 | return xcur; 26 | } 27 | 28 | // check if f(a) and f(b) have different signs 29 | //try { 30 | if (fpre * fcur > 0) { 31 | string text = "f(a) and f(b) must have different signs!\n"; 32 | throw std::runtime_error(text); 33 | } 34 | 35 | // iteration 36 | for (i = 0; i < iter; i++) { 37 | 38 | if (fpre != 0 && fcur != 0 && 39 | (signbit(fpre) != signbit(fcur))) { 40 | xblk = xpre; 41 | fblk = fpre; 42 | spre = scur = xcur - xpre; 43 | } 44 | if (fabs(fblk) < fabs(fcur)) { 45 | xpre = xcur; 46 | xcur = xblk; 47 | xblk = xpre; 48 | 49 | fpre = fcur; 50 | fcur = fblk; 51 | fblk = fpre; 52 | } 53 | 54 | delta = (xtol + rtol * fabs(xcur)) / 2; 55 | sbis = (xblk - xcur) / 2; 56 | if (fcur == 0 || fabs(sbis) < delta) { 57 | return xcur; 58 | } 59 | 60 | if (fabs(spre) > delta && fabs(fcur) < fabs(fpre)) { 61 | if (xpre == xblk) { 62 | // interpolate 63 | stry = -fcur * (xcur - xpre) / (fcur - fpre); 64 | } 65 | else { 66 | // extrapolate 67 | dpre = (fpre - fcur) / (xpre - xcur); 68 | dblk = (fblk - fcur) / (xblk - xcur); 69 | stry = - fcur * (fblk * dblk - fpre * dpre) 70 | / (dblk * dpre * (fblk - fpre)); 71 | } 72 | if (2 * fabs(stry) < min(fabs(spre), 3 * fabs(sbis) - delta)) { 73 | // good short step 74 | spre = scur; 75 | scur = stry; 76 | } else { 77 | // bisect 78 | spre = sbis; 79 | scur = sbis; 80 | } 81 | } 82 | else { 83 | // bisect 84 | spre = sbis; 85 | scur = sbis; 86 | } 87 | 88 | xpre = xcur; fpre = fcur; 89 | if (fabs(scur) > delta) { 90 | xcur += scur; 91 | } 92 | else { 93 | xcur += (sbis > 0 ? delta : -delta); 94 | } 95 | 96 | fcur = f(xcur); 97 | } 98 | 99 | // the solver does not converge after xxx iterations! 100 | string text = "Solver doesn't converge after " + to_string(iter) + " iterations!\n"; 101 | throw std::runtime_error(text); 102 | 103 | return xcur; 104 | } 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /jetsimpy/src/binding.cpp: -------------------------------------------------------------------------------- 1 | #include "jetsimpy.h" 2 | 3 | PYBIND11_MODULE(jetsimpy_extension, m) { 4 | // parameter object 5 | py::class_(m, "JetConfig") 6 | .def(py::init<>()) 7 | .def_readwrite("theta_edge", &JetConfig::theta_edge) 8 | .def_readwrite("Eb", &JetConfig::Eb) 9 | .def_readwrite("Ht", &JetConfig::Ht) 10 | .def_readwrite("Msw", &JetConfig::Msw) 11 | .def_readwrite("Mej", &JetConfig::Mej) 12 | .def_readwrite("R", &JetConfig::R) 13 | .def_readwrite("nwind", &JetConfig::nwind) 14 | .def_readwrite("nism", &JetConfig::nism) 15 | .def_readwrite("tmin", &JetConfig::tmin) 16 | .def_readwrite("tmax", &JetConfig::tmax) 17 | .def_readwrite("rtol", &JetConfig::rtol) 18 | .def_readwrite("cfl", &JetConfig::cfl) 19 | .def_readwrite("spread", &JetConfig::spread) 20 | .def_readwrite("cal_level", &JetConfig::cal_level) 21 | ; 22 | 23 | // jet & afterglow calculation class 24 | py::class_(m, "Jet") 25 | .def(py::init()) 26 | // ---------- solve hydro ---------- // 27 | .def("solveJet", &Jet::solveJet) 28 | .def("getY", &Jet::getY) 29 | .def("getT", &Jet::getT) 30 | .def("getTheta", &Jet::getTheta) 31 | 32 | // ---------- hydro interpolation ---------- // 33 | .def("interpolateMsw", py::vectorize(&Jet::interpolateMsw)) 34 | .def("interpolateMej", py::vectorize(&Jet::interpolateMej)) 35 | .def("interpolateBetaGamma", py::vectorize(&Jet::interpolateBetaGamma)) 36 | .def("interpolateBetaTh", py::vectorize(&Jet::interpolateBetaTh)) 37 | .def("interpolateR", py::vectorize(&Jet::interpolateR)) 38 | .def("interpolateE0", py::vectorize(&Jet::interpolateE0)) 39 | 40 | // ---------- afterglow calculation ---------- // 41 | .def("configParameters", &Jet::configParameters) 42 | .def("configIntensity", &Jet::configIntensity) 43 | .def("configAvgModel", &Jet::configAvgModel) 44 | .def("configIntensityPy", &Jet::configIntensityPy) 45 | .def("configAvgModelPy", &Jet::configAvgModelPy) 46 | .def("calculateEATS", py::vectorize(&Jet::calculateEATS)) 47 | .def("calculateIntensity", py::vectorize(&Jet::calculateIntensity)) 48 | .def("calculateLuminosity", py::vectorize(&Jet::calculateLuminosity)) 49 | .def("calculateFreqIntL", py::vectorize(&Jet::calculateFreqIntL)) 50 | .def("WeightedAverage", py::vectorize(&Jet::WeightedAverage)) 51 | .def("IntensityOfPixel", py::vectorize(&Jet::IntensityOfPixel)) 52 | ; 53 | 54 | // bind blast object 55 | py::class_(m, "Blast") 56 | .def_readonly("t", &Blast::t) 57 | .def_readonly("theta", &Blast::theta) 58 | .def_readonly("phi", &Blast::phi) 59 | .def_readonly("R", &Blast::R) 60 | .def_readonly("beta", &Blast::beta) 61 | .def_readonly("gamma", &Blast::gamma) 62 | .def_readonly("beta_th", &Blast::beta_th) 63 | .def_readonly("beta_r", &Blast::beta_r) 64 | .def_readonly("beta_f", &Blast::beta_f) 65 | .def_readonly("gamma_f", &Blast::gamma_f) 66 | .def_readonly("s", &Blast::s) 67 | .def_readonly("doppler", &Blast::doppler) 68 | .def_readonly("n_blast", &Blast::n_blast) 69 | .def_readonly("e_density", &Blast::e_density) 70 | .def_readonly("pressure", &Blast::pressure) 71 | .def_readonly("n_ambient", &Blast::n_ambient) 72 | .def_readonly("dR", &Blast::dR) 73 | ; 74 | } 75 | -------------------------------------------------------------------------------- /jetsimpy/src/environment.h: -------------------------------------------------------------------------------- 1 | #ifndef ENVIRONMENT 2 | #define ENVIRONMENT 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | // global constants in cgs unit 22 | const double CSpeed = 29979245800.0; 23 | const double MassP = 1.672622e-24; 24 | const double MassE = 9.109384e-28; 25 | const double SigmaT = 6.6524587e-25; 26 | const double ECharge = 4.803204673e-10; 27 | const double MPC = 3.09e24; 28 | const double PI = 3.14159265358979323846; 29 | const double MAS = 1.0 / 206264806.24709466; 30 | 31 | // type alias 32 | namespace py = pybind11; 33 | using Array1D = std::vector; 34 | using Array2D = std::vector; 35 | using Array3D = std::vector; 36 | 37 | // array constructions 38 | inline Array1D Array(int n1, double fill = 0.0) { 39 | return std::vector(n1, fill); 40 | } 41 | inline Array2D Array(int n1, int n2, double fill = 0.0) { 42 | Array1D array1d = Array(n2, fill); 43 | return std::vector>(n1, array1d); 44 | } 45 | inline Array3D Array(int n1, int n2, int n3, double fill = 0.0) { 46 | Array2D array2d = Array(n2, n3, fill); 47 | return std::vector>>(n1, array2d); 48 | } 49 | 50 | // print function 51 | template 52 | void print(T var1) { 53 | std::cout << var1 << "\n"; 54 | } 55 | 56 | template 57 | void print(T var1, Types... var2) { 58 | std::cout << var1 << " "; 59 | print(var2...); 60 | } 61 | 62 | 63 | 64 | #endif -------------------------------------------------------------------------------- /jetsimpy/src/jetsimpy.cpp: -------------------------------------------------------------------------------- 1 | #include "jetsimpy.h" 2 | 3 | Jet::Jet(const JetConfig& jet_config) 4 | : jet_config (jet_config), 5 | tool (jet_config), 6 | sim_box (jet_config, tool) 7 | { 8 | 9 | } 10 | 11 | // ---------- solve hydro ---------- // 12 | 13 | void Jet::solveJet() { 14 | // solve PDE 15 | sim_box.solvePDE(); 16 | 17 | // feed data to interpolator 18 | interpolator.feedData(sim_box, tool); 19 | 20 | // feed data to eats solver 21 | eats.feedData(sim_box, tool); 22 | 23 | // initialize afterglow object 24 | afterglow.initialize(sim_box, eats); 25 | } 26 | 27 | py::array_t Jet::getY() { 28 | // get data 29 | Array3D& y_data = sim_box.getY(); 30 | 31 | // create np.ndarray object 32 | size_t nt = sim_box.getT().size(); 33 | size_t ntheta = sim_box.getTheta().size(); 34 | size_t shape[3] = {5, ntheta, nt}; 35 | size_t strides[3] = {ntheta * nt * sizeof(double), nt * sizeof(double), sizeof(double)}; 36 | auto array = py::array_t( 37 | shape, 38 | strides 39 | ); 40 | 41 | // register memory layout 42 | auto view = array.mutable_unchecked<3>(); 43 | for (size_t i = 0; i < 5; ++i) { 44 | for (size_t j = 0; j < ntheta; ++j) { 45 | for (size_t k = 0; k < nt; ++k) { 46 | view(i, j, k) = y_data[i][j][k]; 47 | } 48 | } 49 | } 50 | 51 | return array; 52 | } 53 | 54 | py::array_t Jet::getT() { 55 | // get data 56 | Array1D& t_data = sim_box.getT(); 57 | 58 | // create np.ndarray object 59 | size_t nt = sim_box.getT().size(); 60 | size_t shape[1] = {nt}; 61 | size_t strides[1] = {sizeof(double)}; 62 | auto array = py::array_t( 63 | shape, 64 | strides 65 | ); 66 | 67 | // register memory layout 68 | auto view = array.mutable_unchecked<1>(); 69 | for (size_t i = 0; i < array.shape(0); ++i) { 70 | view(i) = t_data[i]; 71 | } 72 | 73 | return array; 74 | } 75 | 76 | py::array_t Jet::getTheta() { 77 | // get data 78 | Array1D& theta_data = sim_box.getTheta(); 79 | 80 | // create np.ndarray object 81 | auto array = py::array_t( 82 | {theta_data.size()}, 83 | {sizeof(double)} 84 | ); 85 | 86 | // register memory layout 87 | auto view = array.mutable_unchecked<1>(); 88 | for (size_t i = 0; i < array.shape(0); ++i) { 89 | view(i) = theta_data[i]; 90 | } 91 | 92 | return array; 93 | } 94 | 95 | // ---------- hydro interpolation ---------- // 96 | 97 | double Jet::interpolateMsw(double t, double theta) { 98 | return interpolator.interpolateY(t, theta, 0); 99 | } 100 | 101 | double Jet::interpolateMej(double t, double theta) { 102 | return interpolator.interpolateY(t, theta, 1); 103 | } 104 | 105 | double Jet::interpolateBetaGamma(double t, double theta) { 106 | // remember what we record and interpolate is (beta * gamma)^2 107 | return std::sqrt(interpolator.interpolateY(t, theta, 2)); 108 | } 109 | 110 | double Jet::interpolateBetaTh(double t, double theta) { 111 | return interpolator.interpolateY(t, theta, 3); 112 | } 113 | 114 | double Jet::interpolateR(double t, double theta) { 115 | return interpolator.interpolateY(t, theta, 4); 116 | } 117 | 118 | double Jet::interpolateE0(double t, double theta) { 119 | double msw = interpolateMsw(t, theta); 120 | double mej = interpolateMej(t, theta); 121 | double beta_gamma_sq = std::pow(interpolateBetaGamma(t, theta), 2.0); 122 | double R = interpolateR(t, theta); 123 | double s = tool.solveS(R, beta_gamma_sq); 124 | 125 | double E0 = s * (1.0 + beta_gamma_sq * beta_gamma_sq / (beta_gamma_sq + 1.0) / (beta_gamma_sq + 1.0) / 3.0) * (beta_gamma_sq + 1.0) * msw 126 | + (1.0 - s) * std::sqrt(beta_gamma_sq + 1.0) * msw 127 | + std::sqrt(beta_gamma_sq + 1.0) * mej 128 | - msw 129 | - mej; 130 | return E0 * CSpeed * CSpeed; 131 | } 132 | 133 | // ---------- afterglow calculation ---------- // 134 | 135 | double Jet::calculateEATS(double Tobs, double theta, double phi, double theta_v, double z) { 136 | double Tobs_z = Tobs / (1.0 + z); 137 | return eats.solveEATS(Tobs_z, theta, phi, theta_v); 138 | } 139 | 140 | double Jet::calculateIntensity(double Tobs, double nu, double theta, double phi) { 141 | return afterglow.Intensity(Tobs, nu, theta, phi); 142 | } 143 | 144 | void Jet::configParameters(const Dict& param) { 145 | afterglow.configParameters(param); 146 | } 147 | 148 | void Jet::configIntensity(const std::string& model_name) { 149 | afterglow.configIntensity(model_name); 150 | } 151 | 152 | void Jet::configAvgModel(const std::string& model_name) { 153 | afterglow.configAvgModel(model_name); 154 | } 155 | 156 | void Jet::configIntensityPy(py::function py_f) { 157 | afterglow.configIntensityPy(py_f); 158 | } 159 | 160 | void Jet::configAvgModelPy(py::function py_f) { 161 | afterglow.configAvgModelPy(py_f); 162 | } 163 | 164 | double Jet::calculateLuminosity(double Tobs, double nu, double rtol, const int max_iter, const bool force_return) { 165 | return afterglow.Luminosity(Tobs, nu, rtol, max_iter, force_return); 166 | } 167 | 168 | double Jet::calculateFreqIntL(double Tobs, double nu1, double nu2, double rtol, const int max_iter, const bool force_return) { 169 | return afterglow.FreqIntL(Tobs, nu1, nu2, rtol, max_iter, force_return); 170 | } 171 | 172 | double Jet::calculateAvgModel(double Tobs, double nu, double rtol, const int max_iter, const bool force_return) { 173 | return afterglow.integrateModel(Tobs, nu, rtol, max_iter, force_return); 174 | } 175 | 176 | double Jet::WeightedAverage(double Tobs, double nu, double rtol, const int max_iter, const bool force_return) { 177 | // save time 178 | if (Tobs == 0.0) return 0.0; 179 | 180 | double luminosity = calculateLuminosity(Tobs, nu, rtol, max_iter, force_return); 181 | double integral = calculateAvgModel(Tobs, nu, rtol, max_iter, force_return); 182 | 183 | return integral / luminosity; 184 | } 185 | 186 | double Jet::IntensityOfPixel(const double Tobs, const double nu, const double x_tilde, const double y_tilde) { 187 | return afterglow.IntensityOfPixel(Tobs, nu, x_tilde, y_tilde); 188 | } 189 | -------------------------------------------------------------------------------- /jetsimpy/src/jetsimpy.h: -------------------------------------------------------------------------------- 1 | #ifndef JETSIMPY 2 | #define JETSIMPY 3 | 4 | #include "environment.h" 5 | #include "Hydro/config.h" 6 | #include "Hydro/tools.h" 7 | #include "Hydro/sim_box.h" 8 | #include "Hydro/interpolate.h" 9 | #include "Afterglow/eats.h" 10 | #include "Afterglow/afterglow.h" 11 | 12 | // The object accessable from Python 13 | class Jet { 14 | public: 15 | Jet(const JetConfig& jet_config); 16 | 17 | // ---------- solve hydro ---------- // 18 | void solveJet(); // solve hydro 19 | py::array_t getY(); // get pde ys 20 | py::array_t getT(); // get pde ts 21 | py::array_t getTheta(); // get cell centers 22 | 23 | // ---------- hydro interpolation ---------- // 24 | double interpolateMsw(double t, double theta); 25 | double interpolateMej(double t, double theta); 26 | double interpolateBetaGamma(double t, double theta); 27 | double interpolateBetaTh(double t, double theta); 28 | double interpolateR(double t, double theta); 29 | double interpolateE0(double t, double theta); 30 | 31 | // ---------- afterglow calculation ---------- // 32 | void configParameters(const Dict& param); // configurate parameter dictionary 33 | void configIntensity(const std::string& model_name); // configurate radiation model 34 | void configAvgModel(const std::string& model_name); // configure average models 35 | void configIntensityPy(py::function py_f); // configure radiation model from python side 36 | void configAvgModelPy(py::function py_f); // configure average models from python side 37 | double calculateEATS(double Tobs, double theta, double phi, double theta_v, double z); // calculate t of EATS 38 | double calculateIntensity(double Tobs, double nu, double theta, double phi); // intensity in Jet coordinate 39 | double calculateLuminosity(double Tobs, double nu, double rtol, const int max_iter = 50, const bool force_return = true); // integrate luminosity 40 | double calculateFreqIntL(double Tobs, double nu1, double nu2, double rtol, const int max_iter = 50, const bool force_return = true); // frequency integrated L 41 | double calculateAvgModel(double Tobs, double nu, double rtol, const int max_iter = 50, const bool force_return = true); // integrate average model 42 | double WeightedAverage(double Tobs, double nu, double rtol, const int max_iter = 50, const bool force_return = true); 43 | double IntensityOfPixel(const double Tobs, const double nu, const double x_tilde, const double y_tilde); 44 | 45 | private: 46 | JetConfig jet_config; // configuration data 47 | Tool tool; // the tool containing many functions 48 | SimBox sim_box; // the simulation box 49 | Interpolator interpolator; // the interpolation tool 50 | EATS eats; // equal arrival time surface solver 51 | Afterglow afterglow; // afterglow algorithms 52 | }; 53 | 54 | #endif -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "pybind11>=2.10.0" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from setuptools import setup, Command 3 | import os 4 | 5 | try: 6 | from pybind11.setup_helpers import Pybind11Extension, build_ext 7 | except ImportError: 8 | from setuptools import Extension as Pybind11Extension, build_ext 9 | 10 | __version__ = "0.5.0" 11 | name = "jetsimpy" 12 | 13 | ext_modules = [ 14 | Pybind11Extension( 15 | f"{name}.jetsimpy_extension", 16 | sorted(glob("jetsimpy/src/*.cpp") + glob("jetsimpy/src/**/*.cpp")), 17 | define_macros = [('VERSION_INFO', __version__)], 18 | extra_compile_args=["-O3", "-std=c++11"], 19 | include_dirs = ["jetsimpy/src/", "jetsimpy/src/**/"] 20 | ), 21 | ] 22 | 23 | class CleanCommand(Command): 24 | """Custom clean command to tidy up the project root.""" 25 | user_options = [] 26 | def initialize_options(self): 27 | pass 28 | def finalize_options(self): 29 | pass 30 | def run(self): 31 | os.system('rm -vrf ./build ./dist ./*.pyc ./*.egg-info') 32 | 33 | 34 | setup( 35 | name=name, 36 | version=__version__, 37 | author="Hao Wang", 38 | author_email="haowang.astro@gmail.com", 39 | description="Gamma-ray burst jet simulation & afterglow modeling", 40 | packages=[name], 41 | ext_modules=ext_modules, 42 | python_requires=">=3.6", 43 | install_requires=[ 44 | 'numpy', 45 | 'pybind11', 46 | ], 47 | include_package_data=True, 48 | cmdclass={"clean": CleanCommand}, 49 | ) 50 | 51 | --------------------------------------------------------------------------------