Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Week 4 Solutions

Reading questions

  1. What is the difference between collisionless and collisional systems? Strong gravitational interactions between individual stars dominate the evolution of a collisional system, whereas they are are negligible in a collisionless system.

  2. (a) What are the relaxation time and dynamical time of a galaxy? (b) How can one argue that galaxies are collisionless, by comparing their two-body relaxation time with the age of the Universe? (a) The relaxation time of a galaxy is the time required to change one of its star’s velocity by 100%. The dynamical time of a galaxy is the crossing time for individual stars. The crossing time is tcross=2πR/vt_\mathrm{cross} = 2\pi R/v where RR is the galaxy size and vv is the velocity of the star. We also have that the relaxation time is trelax=nrelaxtcrosst_\mathrm{relax} = n_\mathrm{relax}t_\mathrm{cross}, where nrelax=N/8lnNn_\mathrm{relax} = N/8 \ln N, where NN is the number of stars in the galaxy. (b) For a typical galaxy, we have N1011N\approx 10^{11} and tcross100 Myrt_\mathrm{cross}\approx 100\ \mathrm{Myr}. This yields trelax5×1010 yrt_\mathrm{relax}\approx 5\times 10^{10}\ \mathrm{yr}, or \approx 5 million times the age of our Universe. This means that two-body relaxation cannot be the mechanism through which galaxies reach dynamical equilibrium; otherwise, they would be far from equilibrium today.

  3. (a) What’s the assumption required for dynamical equilibrium, based on the virial theorem? (b) Under this assumption, what is the relation between kinetic and potential energy? (a) The fundamental assumption of the virial theorem is that the system is in equilibrium, e.g. that dG/dt=0dG/dt = 0, where GG is the virial quantity and is defined as G=i=1NwixiviG=\sum^{N}_{i=1} w_i\vec{x}_i\cdot\vec{v}_i, where wiw_i are arbitrary weights and xi\vec{x}_i and vi\vec{v}_i are the position and velocity of the ii-th particle, respectively. (b) With this assumption, the result of the virial theorem is that 2K+W=02K+ W = 0, where KK is the kinetic energy of the system and WW is its gravitational potential energy.

  4. What is the collisionless Boltzmann equation? The collisionless Boltzmann equation is:

    ft+xxf+vvf=0{\partial f\over \partial t} + \vec{x}\cdot\vec{\nabla}_x f + \vec{v}\cdot\vec{\nabla}_v f = 0

    where f=f(x,v,t)f= f(x,v,t) is the distribution function of the system. The collisionless Boltzmann equation describes the behavior of ff under the influence of gravity.

  5. What are the Jeans equations, and what applicability do they have for studying astronomical systems of many objects? Jeans equations are the collisionless Boltzmann equation multiplied by powers of xx or vv and integrated over a part of phase-space. They are moment equations of the collisionless Boltzmann equation. They are useful in astronomy because astronomical observations are usually performed at a fixed location in the sky. Thus, multiplying the collisionless Boltzmann equation by v and integrating over all components of the velocity connects to observables.

Numerical Exercise

In this exercise, we will use galpy\texttt{galpy} to investigate the gravitational potential of dark matter profiles in dwarf galaxies using the Jeans equations formulation for collisionless systems. We will use mock data based on Walker et al. (2009) https://iopscience.iop.org/article/10.1088/0004-637X/704/2/1274#apj320201s3. Dwarf galaxies are >90%>90\% dark matter, so we can assume the gravitational potential is only given by dark matter density profile.

import matplotlib.pyplot as plt
import time
import numpy as np
import galpy
from galpy.df import jeans

Select one of the dwarf galaxies data for line-of-sight (LOS) velocity dispersion profiles. The columns are (1) radial distance in the plane of the sky in pc, (2) LOS velocity dispersion measured at the given radial distance in km/s, (3) error in the velocity dispersion measured in km/s

These values might be useful since galpy standard units are given in velocity normalized by the circular velocity of the Milky Way v0=220 km/sv_{0}=220\ \mathrm{km/s}, and the approximate distance of the sun to the Milky Way’s center r0=8 kpcr_{0}=8\ \mathrm{kpc}.

v0=220 #km/s
r0=8e3 #pc
G_c=4.30091e-3 #pc (km/s)^2 Msun^-1

In Walker et al. (2009) two of the dark matter profiles that are studied are the Navarro–Frenk–White (NFW) profile https://docs.galpy.org/en/latest/reference/potentialnfw.html, and a core-shaped profile, which for practical use we will assume as the Burkert profile https://docs.galpy.org/en/latest/reference/potentialburkert.html. In order to have less free parameters in our problem we can assume the values of the scale radius to be aNFW=795 pca_{\mathrm{NFW}}=795\ \mathrm{pc} and aBurkert=150 pca_{\mathrm{Burkert}}=150\ \mathrm{pc} (remember to normalize for by r0r_{0})

We can define a potential we are working with in galpy as follows (NFW as example): NFW=galpy.potential.NFWPotential(amp=my_amp, a=my_a,vo=v0, ro=r0/1e3)\texttt{NFW=galpy.potential.NFWPotential(amp=my\_amp, a=my\_a,vo=v0, ro=r0/1e3)} (giving ro and vo will produce outputs in M pc3\mathrm{M_{\odot}\ pc^{-3}} for densities and km/s\mathrm{km/s} for velocity dispersions, inputs must be on galpy units though).

To explore the dark matter profile of dwarf galaxies, we will use the density profiles that define the potential with a characteristic density ρ0\rho_{0}. Such a density will be the input amp\texttt{amp} in our galpy\texttt{galpy} potentials (for the NFW profile as defined in galpy\texttt{galpy} you might want to write amp=ρ04πa3\mathrm{amp}=\rho_{0}4\pi a^{3}). Density inputs have units v02Gr02\frac{v_{0}^{2}}{G r_{0}^{2}}

Exercise:\textbf{Exercise}:

(1) After selecting one of the galaxies, define an NFW and Burkert potential in galpy\texttt{galpy} and plot the corresponding density profiles in a range of 10 to 2000 pc. Taking the previous example, you can use NFW.dens(r,0)\texttt{NFW.dens(r,0)} to evaluate the density of the given potential at different distances rr. Use at least 3 values of scale density in the range ρ0=102\rho_{0}=10^{-2}-10 M pc310\ \mathrm{M_{\odot}\ pc^{-3}} for each profile. What are the main differences between the two density distributions (you might use a log scale in your plots).

rho0=np.array([0.01,0.05,5])/((v0**2)/(G_c*(r0**2)))
nfwr0=795/r0 # Normalized by 8kpc
core0=150/r0#Normalized by 8kpc

rho02=np.array([0.02,1,10])/((v0**2)/(G_c*(r0**2)))
NFW1=galpy.potential.NFWPotential(amp=rho0[0]*4*np.pi*(nfwr0**3), a=nfwr0,vo=v0, ro=r0/1e3)
NFW2=galpy.potential.NFWPotential(amp=rho0[1]*4*np.pi*(nfwr0**3), a=nfwr0,vo=v0, ro=r0/1e3)
NFW3=galpy.potential.NFWPotential(amp=rho0[2]*4*np.pi*(nfwr0**3), a=nfwr0,vo=v0, ro=r0/1e3)
core1=galpy.potential.BurkertPotential(amp=rho02[0], a=core0,vo=v0, ro=r0/1e3)
core2=galpy.potential.BurkertPotential(amp=rho02[1], a=core0,vo=v0, ro=r0/1e3)
core3=galpy.potential.BurkertPotential(amp=rho02[2], a=core0,vo=v0, ro=r0/1e3)
rr=np.geomspace(20,2000,100)/r0
import matplotlib
matplotlib.use('TkAgg')
%matplotlib inline
plt.loglog(rr*r0,NFW1.dens(rr,0),label=r'NFW, $\rho_{0}=0.01 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.loglog(rr*r0,NFW2.dens(rr,0),label=r'NFW, $\rho_{0}=0.05 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.loglog(rr*r0,NFW3.dens(rr,0),label=r'NFW, $\rho_{0}=5 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.loglog(rr*r0,core1.dens(rr,0),label=r'core, $\rho_{0}=0.02 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.loglog(rr*r0,core2.dens(rr,0),label=r'core, $\rho_{0}=1 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.loglog(rr*r0,core3.dens(rr,0),label=r'core, $\rho_{0}=10 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.xlabel('r (pc)')
plt.ylabel(r'$\rho \ (\mathrm{M_{\odot} pc^{-3}}$)')
plt.legend()
plt.show()
<Figure size 640x480 with 1 Axes>

(2) Since we only have access to LOS velocities, we need the anisotropy β\beta to compute the velocity dispersions from the Jeans equations. Use at least 5 values of the anisotropy in the range β=10\beta=-10-1 and compute the radial velocity dispersions with galpy\texttt{galpy}. Use jeans.sigmar(NFW,r,beta=beta)\texttt{jeans.sigmar(NFW,r,beta=beta)} to compute σr\sigma_{r} at a given radial distance rr. Plot σr\sigma_{r} vs rr (do not compare with the data yet).

beta=np.array([-5,-0.5,0,0.5,1])
sigs1=[]
sigs2=[]
sigs3=[]

sigs1_=[]
sigs2_=[]
sigs3_=[]

for i in beta:
    sig1=np.array([jeans.sigmar(NFW1,r,beta=i) for r in rr])/v0
    sigs1.append(sig1)

    sig2=np.array([jeans.sigmar(NFW2,r,beta=i) for r in rr])/v0
    sigs2.append(sig2)

    sig3=np.array([jeans.sigmar(NFW3,r,beta=i) for r in rr])/v0
    sigs3.append(sig3)


    sig1_= np.array([jeans.sigmar(core1,r,beta=i) for r in rr])/v0
    sigs1_.append(sig1_)

    sig2_= np.array([jeans.sigmar(core2,r,beta=i) for r in rr])/v0
    sigs2_.append(sig2_)

    sig3_= np.array([jeans.sigmar(core3,r,beta=i) for r in rr])/v0
    sigs3_.append(sig3_)
    
f,ax=plt.subplots(3,1,figsize=(5,10))
for i in range(5):
    ax[0].semilogy(rr*r0,sigs1[i],label=r'$\beta=$'+str(beta[i]))
    ax[1].semilogy(rr*r0,sigs2[i],label=r'$\beta=$'+str(beta[i]))
    ax[2].semilogy(rr*r0,sigs3[i],label=r'$\beta=$'+str(beta[i]))

for i in range(3):
    ax[i].set_xlabel('r (pc)')
    ax[i].set_ylabel(r'$\sigma_{r}\ (\mathrm{km/s}$)')
    ax[i].legend()
<Figure size 500x1000 with 3 Axes>
f,ax=plt.subplots(3,1,figsize=(5,10))
for i in range(5):
    ax[0].semilogy(rr*r0,sigs1_[i],label=r'$\beta=$'+str(beta[i]))
    ax[1].semilogy(rr*r0,sigs2_[i],label=r'$\beta=$'+str(beta[i]))
    ax[2].semilogy(rr*r0,sigs3_[i],label=r'$\beta=$'+str(beta[i]))
for i in range(3):
    ax[i].set_xlabel('r (pc)')
    ax[i].set_ylabel(r'$\sigma_{r}\ (\mathrm{km/s}$)')
    ax[i].legend()
<Figure size 500x1000 with 3 Axes>

(3) σr\sigma_{r} is defined in an arbitrary radial position of the sky, β\beta contains information about the inclination of our system, and then it can be used to obtain projected σLOS\sigma_{\mathrm{LOS}} that we can compare with our data. Use jeans.sigmalos(NFW,r,dens=nu,surfdens=I,beta=beta,sigma_r=sig_r\texttt{jeans.sigmalos(NFW,r,dens=nu,surfdens=I,beta=beta,sigma\_r=sig\_r} to compute σLOS\sigma_{\mathrm{LOS}} at given distances. The densities of stars ν\nu (dens) and II (surfdens) should be functions of rr (use def function (r):...\texttt{def\ function (r):...}), you can use the analytical formulas given in section 3.1 of Walker et al. (2009), note that LL can be ignored and Table 1 lists the rhalfr_{\mathrm{half}} for each galaxy. Plot the σLOS\sigma_{\mathrm{LOS}} obtained with the different values of ρ0\rho_{0} and β\beta for the two dark matter profiles and compare with your data.

a=221/r0 #half radius (pc) /r0
def nu(r):
    """3D Plummer tracer density"""
    return 3*((4*np.pi*(a**3))**-1)*((1 + r**2 / a**2)**(-5/2))

def I(r):
    """Projected Plummer surface density"""
    return ((np.pi*(a**2))**(-1))*((1 + r**2 / a**2)**(-2))
sigs_losp1=[]
sigs_losp2=[]
sigs_losp3=[]

sigs_losp1_=[]
sigs_losp2_=[]
sigs_losp3_=[]

for i in range(len(beta)):
    #for j in range(len(5)):
        #sig1=numpy.array([jeans.sigmar(NFW1,r,beta=i) for r in rr])/v0
        sig_losp1=np.array([jeans.sigmalos(NFW1,rr[j],dens=nu,surfdens=I,beta=beta[i],sigma_r=sigs1[i][j]) for j in range(len(sigs1[i]))])
        sigs_losp1.append(sig_losp1)

        sig_losp2=np.array([jeans.sigmalos(NFW2,rr[j],dens=nu,surfdens=I,beta=beta[i],sigma_r=sigs2[i][j]) for j in range(len(sigs2[i]))])
        sigs_losp2.append(sig_losp2)

        sig_losp3=np.array([jeans.sigmalos(NFW3,rr[j],dens=nu,surfdens=I,beta=beta[i],sigma_r=sigs3[i][j]) for j in range(len(sigs3[i]))])
        sigs_losp3.append(sig_losp3)

        sig_losp1_=np.array([jeans.sigmalos(core1,rr[j],dens=nu,surfdens=I,beta=beta[i],sigma_r=sigs1_[i][j]) for j in range(len(sigs1[i]))])
        sigs_losp1_.append(sig_losp1_)

        sig_losp2_=np.array([jeans.sigmalos(core2,rr[j],dens=nu,surfdens=I,beta=beta[i],sigma_r=sigs2_[i][j]) for j in range(len(sigs2[i]))])
        sigs_losp2_.append(sig_losp2_)

        sig_losp3_=np.array([jeans.sigmalos(core3,rr[j],dens=nu,surfdens=I,beta=beta[i],sigma_r=sigs3_[i][j]) for j in range(len(sigs3[i]))])
        sigs_losp3_.append(sig_losp3_)
dr_data=np.loadtxt('draco_walker2009.csv',skiprows=1,delimiter=',', unpack=True)
f,ax=plt.subplots(3,1,figsize=(5,10))
for i in range(5):
    ax[0].semilogy(rr*r0,sigs_losp1[i],label=r'$\beta=$'+str(beta[i]))
    ax[1].semilogy(rr*r0,sigs_losp2[i],label=r'$\beta=$'+str(beta[i]))
    ax[2].semilogy(rr*r0,sigs_losp3[i],label=r'$\beta=$'+str(beta[i]))

for i in range(3):
    ax[i].errorbar(dr_data[0],dr_data[1],yerr=dr_data[2],fmt='o')
    ax[i].set_xlabel('r (pc)')
    ax[i].set_ylabel(r'$\sigma_{\mathrm{LOS}}\ (\mathrm{km/s}$)')
    ax[i].legend()
<Figure size 500x1000 with 3 Axes>
f,ax=plt.subplots(3,1,figsize=(5,10))
for i in range(5):
    ax[0].semilogy(rr*r0,sigs_losp1_[i],label=r'$\beta=$'+str(beta[i]))
    ax[1].semilogy(rr*r0,sigs_losp2_[i],label=r'$\beta=$'+str(beta[i]))
    ax[2].semilogy(rr*r0,sigs_losp3_[i],label=r'$\beta=$'+str(beta[i]))

for i in range(3):
    ax[i].errorbar(dr_data[0],dr_data[1],yerr=dr_data[2],fmt='o')
    ax[i].set_xlabel('r (pc)')
    ax[i].set_ylabel(r'$\sigma_{\mathrm{LOS}}\ (\mathrm{km/s}$)')
    ax[i].legend()
<Figure size 500x1000 with 3 Axes>

(4) Select your best model to compute the total mass of dark matter contained in a radius of 300 pc and rhalfr_{\mathrm{half}} of your selected galaxy and compare with Table 2 of Walker et al. (2009).

plt.plot(rr*r0,sigs_losp2[0],label=r'NFW, $\beta=-5,\ \rho_{0}=0.05 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.plot(rr*r0,sigs_losp2_[1],label=r'core, $\beta=-1,\ \rho_{0}=1 \ \mathrm{M_{\odot}\ pc^{-3}}$')
plt.errorbar(dr_data[0],dr_data[1],yerr=dr_data[2],fmt='o')
plt.xlabel('r (pc)')
plt.ylabel(r'$\sigma_{\mathrm{LOS}}\ (\mathrm{km/s}$)')
plt.legend()
<Figure size 640x480 with 1 Axes>
import scipy.integrate as si
def mass_NFW(r):
    return 4*np.pi*NFW2.dens(r/r0,0)*(r**2)
def mass_core(r):
    return 4*np.pi*core2.dens(r/r0,0)*(r**2)  
si.quad(mass_NFW,0,300,points=300)[0]/1e7,si.quad(mass_NFW,0,a*r0,points=300)[0]/1e7 #in 10^7 Msun
(1.4583937189357992, 0.8766107602579909)
si.quad(mass_core,0,300,points=300)[0]/1e7,si.quad(mass_core,0,a*r0,points=300)[0]/1e7 #in 10^7 Msun
(1.688361941247572, 1.077373655671226)