Source code for gammapy.astro.darkmatter.utils
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Utilities to compute J-factor maps."""
import html
import numpy as np
import astropy.units as u
__all__ = ["JFactory"]
[docs]
class JFactory:
"""Compute J-Factor or D-Factor maps.
J-Factors are computed for annihilation and D-Factors for decay.
Set the argument `annihilation` to `False` to compute D-Factors.
The assumed dark matter profiles will be centered on the center of the map.
Parameters
----------
geom : `~gammapy.maps.WcsGeom`
Reference geometry.
profile : `~gammapy.astro.darkmatter.profiles.DMProfile`
Dark matter profile.
distance : `~astropy.units.Quantity`
Distance to convert angular scale of the map.
annihilation : bool, optional
Decay or annihilation. Default is True.
"""
[docs]
def __init__(self, geom, profile, distance, annihilation=True):
self.geom = geom
self.profile = profile
self.distance = distance
self.annihilation = annihilation
def _repr_html_(self):
try:
return self.to_html()
except AttributeError:
return f"<pre>{html.escape(str(self))}</pre>"
[docs]
def compute_differential_jfactor(self, ndecade=1e4):
r"""Compute differential J-Factor.
.. math::
\frac{\mathrm d J_\text{ann}}{\mathrm d \Omega} =
\int_{\mathrm{LoS}} \mathrm d l \rho(l)^2
.. math::
\frac{\mathrm d J_\text{decay}}{\mathrm d \Omega} =
\int_{\mathrm{LoS}} \mathrm d l \rho(l)
Parameters
----------
ndecade : float, optional
Number of sampling points per decade in radius used for the numerical
integration. Default is 1e4.
Returns
-------
jfactor : `~astropy.units.Quantity`
Differential j-factor.
Notes
-----
The line-of-sight (LoS) integral should include both the near and far
sides of the halo. To account for this, the integration is split into
two regions:
1. :math:`[r_{\min}, r_{\max}]` - from the observer to the source,
counted twice to include contributions from both near and far sides.
2. :math:`[r_{\max}, 4 r_{\max}]` - from the source to infinity.
The upper limit is truncated at :math:`4 r_{\max}` because
contributions beyond this are negligible.
Hence, the effective integration domain is:
.. math::
2 \times [r_{\min}, r_{\max}] \;+\; [r_{\max}, 4 r_{\max}].
The LoS integral is converted into a radial integral over the profile through:
.. math::
r^2 = l^2 + r_{\max}^2 - 2 dl \cos \theta
Rearranging for the differential gives:
.. math::
\mathrm dl = \frac{2 r}{\sqrt{r^2 - r_{\min}^2}} \, \mathrm dr.
This substitution allows the integral to be evaluated directly as
radial integrals using ``profile.integral``, giving
.. math::
\int_0^{l_\mathrm{max}} \rho^2(r(l, \theta)) \, \mathrm dl
= 2 \int_{r_{\min}}^{r_{\max}} \frac{r \, \rho^2(r)}{\sqrt{r^2 - r_{\min}^2}} \, \mathrm dr
+ \int_{r_{\max}}^{4 r_{\max}} \frac{r \, \rho^2(r)}{\sqrt{r^2 - r_{\min}^2}} \, \mathrm dr.
"""
separation = self.geom.separation(self.geom.center_skydir).rad
rmin = u.Quantity(
value=np.tan(separation) * self.distance, unit=self.distance.unit
)
rmax = self.distance
val = [
(
2
* self.profile.integral(
_.value * u.kpc,
rmax,
np.arctan(_.value / self.distance.value),
ndecade,
self.annihilation,
)
+ self.profile.integral(
self.distance,
4 * rmax,
np.arctan(_.value / self.distance.value),
ndecade,
self.annihilation,
)
)
for _ in rmin.ravel()
]
integral_unit = u.Unit("GeV2 cm-5") if self.annihilation else u.Unit("GeV cm-2")
jfact = u.Quantity(val).to(integral_unit).reshape(rmin.shape)
return jfact / u.steradian
[docs]
def compute_jfactor(self, ndecade=1e4):
r"""Compute astrophysical J-Factor.
.. math::
J(\Delta\Omega) =
\int_{\Delta\Omega} \mathrm d \Omega^{\prime}
\frac{\mathrm d J}{\mathrm d \Omega^{\prime}}
Parameters
----------
ndecade : float, optional
Number of sampling points per decade in radius used for the numerical
integration. Default is 1e4.
Returns
-------
jfactor : `~astropy.units.Quantity`
The j-factor.
"""
diff_jfact = self.compute_differential_jfactor(ndecade)
return diff_jfact * self.geom.to_image().solid_angle()