###############################################################################
# AdiabaticContractionWrapperPotential.py: Wrapper to adiabatically
# contract a DM halo in response
# to the growth of a baryonic
# component
###############################################################################
import numpy
from scipy import integrate
from scipy.interpolate import interp1d
from scipy.optimize import fixed_point
from .Force import Force
from .interpSphericalPotential import interpSphericalPotential
from ..util import conversion
# Note: not actually implemented as a WrapperPotential!
[docs]class AdiabaticContractionWrapperPotential(interpSphericalPotential):
"""AdiabaticContractionWrapperPotential: Wrapper to adiabatically contract a DM halo in response to the growth of a baryonic component. Use for example as::
dm= AdiabaticContractionWrapperPotential(pot=MWPotential2014[2],baryonpot=MWPotential2014[:2])
to contract the dark-matter halo in MWPotential2014 according to the baryon distribution within it. The basic physics of the adiabatic contraction is that a fraction `f_bar` of the mass in the original potential `pot` cools adiabatically to form a baryonic component `baryonpot`; this wrapper computes the resulting dark-matter potential using different approximations in the literature.
"""
[docs] def __init__(self,amp=1.,pot=None,baryonpot=None,
method='cautun',f_bar=0.157,rmin=None,rmax=50.,
ro=None,vo=None):
"""
NAME:
__init__
PURPOSE:
initialize a AdiabaticContractionWrapper Potential
INPUT:
amp - amplitude to be applied to the potential (default: 1.)
pot - Potential instance or list thereof representing the density that is adiabatically contracted
baryonpot - Potential instance or list thereof representing the density of baryons whose growth causes the contraction
method= ('cautun') Type of adiabatic-contraction formula:
* 'cautun' for that from Cautun et al. 2020 (`2020MNRAS.494.4291C <https://ui.adsabs.harvard.edu/abs/2020MNRAS.494.4291C>`__),
* 'blumenthal' for that from Blumenthal et al. 1986 (`1986ApJ...301...27B 1986ApJ...301...27B <https://ui.adsabs.harvard.edu/abs/1986ApJ...301...27B>`__)
* 'gnedin' for that from Gnedin et al. 2004 (`2004ApJ...616...16G <https://ui.adsabs.harvard.edu/abs/2004ApJ...616...16G>`__)
f_bar= (0.157) universal baryon fraction; if None, calculated from pot and baryonpot assuming that at rmax the halo contains the universal baryon fraction; leave this at the default value unless you know what you are doing
rmin= (None) minimum radius to consider (default: rmax/2500; don't set this to zero)
rmax= (50.) maximum radius to consider (can be Quantity)
ro, vo= standard unit-conversion parameters
OUTPUT:
(none)
HISTORY:
2021-03-21 - Started based on Marius Cautun's code - Bovy (UofT)
"""
# Initialize with Force just to parse (ro,vo)
Force.__init__(self,ro=ro,vo=vo)
rmax= conversion.parse_length(rmax,ro=self._ro)
rmin= conversion.parse_length(rmin,ro=self._ro) if not rmin is None \
else rmax/2500.
# Compute baryon and DM enclosed masses on radial grid
from ..potential import mass
rgrid= numpy.geomspace(rmin,rmax,301)
baryon_mass= numpy.array([mass(baryonpot,r,use_physical=False)
for r in rgrid])
dm_mass= numpy.array([mass(pot,r,use_physical=False)
for r in rgrid])
# Adiabatic contraction
if f_bar is None:
f_bar= baryon_mass[-1]/(baryon_mass[-1]+dm_mass[-1])
if method.lower() == 'cautun':
new_rforce= _contraction_Cautun2020(rgrid,dm_mass,baryon_mass,
f_bar)
elif method.lower() == 'gnedin':
new_rforce= \
_contraction_Gnedin2004(rgrid,dm_mass,baryon_mass,
pot.rvir(overdens=180.,
wrtcrit=False),
f_bar)
elif method.lower() == 'blumenthal':
new_rforce= _contraction_Blumenthal1986(rgrid,dm_mass,
baryon_mass,f_bar)
else: # pragma: no cover
raise ValueError("Adiabatic contraction method '{}' not recognized".format(method))
# Add central point
rgrid= numpy.concatenate(([0.],rgrid))
new_rforce= numpy.concatenate(([0.],new_rforce))
new_rforce_func= lambda r: -numpy.interp(r,rgrid,new_rforce)
# Potential at zero = int_0^inf dr rforce, and enc. mass constant
# outside of last rgrid point
Phi0= integrate.quad(new_rforce_func,rgrid[0],rgrid[-1])[0]\
-new_rforce[-1]*rgrid[-1]
interpSphericalPotential.__init__(self,
rforce=new_rforce_func,
rgrid=rgrid,
Phi0=Phi0,
ro=ro,vo=vo)
def _contraction_Cautun2020(r,M_DMO,Mbar,fbar):
# solve for the contracted enclosed DM mass
func_M_DM_contract= lambda M: M_DMO*1.023*(M_DMO/(1.-fbar)/(M+Mbar))**-0.54
M_DM= fixed_point(func_M_DM_contract,M_DMO)
return M_DM/M_DMO*M_DMO/r**2.
def _contraction_Blumenthal1986(r,M_DMO,Mbar,fbar):
# solve for the contracted radius 'rf' containing the same DM mass
# as enclosed for r
func_M_bar= interp1d(r,Mbar,bounds_error=False,
fill_value=(Mbar[0],Mbar[-1]) )
func_r_contract= lambda rf: r*(M_DMO/(1.-fbar))/(M_DMO+func_M_bar(rf))
rf= fixed_point(func_r_contract,r)
# now find how much the enclosed mass increased at r
func_M_DM= interp1d(rf,M_DMO,bounds_error=False,
fill_value=(M_DMO[0],M_DMO[-1]))
return func_M_DM(r)/r**2.
def _contraction_Gnedin2004(r,M_DMO,M_bar,Rvir,fbar):
# solve for the contracted radius 'rf' containing the same DM mass
# as enclosed for r
func_M_bar= interp1d(r,M_bar,bounds_error=False,
fill_value=(M_bar[0],M_bar[-1]))
func_M_DMO= interp1d(r,M_DMO,bounds_error=False,
fill_value=(M_DMO[0],M_DMO[-1]))
A, w= 0.85, 0.8
func_r_mean= lambda ri: A*Rvir*(ri/Rvir)**w
M_DMO_rmean= func_M_DMO(func_r_mean(r))
func_r_contract= lambda rf: r*(M_DMO_rmean/(1.-fbar))\
/(M_DMO_rmean+func_M_bar(func_r_mean(rf)))
rf= fixed_point(func_r_contract,r)
# now find how much the enclosed mass increased at r
func_M_DM = interp1d(rf,M_DMO,bounds_error=False,
fill_value=(M_DMO[0],M_DMO[-1]))
return func_M_DM(r)/r**2.