Defining the MilkyWayPotential model#

Introduction#

gala provides simplified mass models for the Milky Way to use in orbit integration or dynamical calculations. Some of these mass models come from other publications or packages (e.g., the Law and Majewski 2010 model LM10Potential). Some of the potential models are defined and provided by Gala. This document describes how we determined the parameters of the Gala Milky Way models.

We determine parameters of the Gala Milky Way models using compilations of enclosed mass measurements of the Milky Way and measurements of the mass structure of the Galactic disk. We then fit for the parameters of a multi-component model (e.g., disk, bulge, halo, etc.) using these measurements.

[3]:
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
from astropy.constants import G
from astropy.io import ascii
from scipy.optimize import leastsq

import gala.potential as gp
from gala.units import galactic

MilkyWayPotential version 1 (circa 2017)#

This model was previously just known as MilkyWayPotential in Gala, now known as “version 1,” and represents an older model based on measurements that are now out of date. We still describe the process of fitting for this model, for completeness.

The source data for this model was compiled from published values and is included with Gala:

[4]:
mwdata1 = ascii.read("data/MW_mass_enclosed.csv")
mwdata1
[4]:
Table length=16
rMencMenc_err_negMenc_err_posref
float64float64float64float64str27
0.0130000000.010000000.010000000.0Feldmeier et al. (2014)
0.12800000000.0200000000.0200000000.0Launhardt et al. (2002)
8.189502860861.524294994562473.7977144858963492.608627Bovy et al. (2012)
8.3110417867208.190554475949382.6968844387023236.020782McMillan (2011)
8.4102421035406.9035616733918715.62994415468328224.531876Koposov et al. (2010)
19.0208023299175.3043844317988008.3810134833267089.920685Kuepper et al. (2015)
50.0539884832748.4897519995734543.31433268490735257.4718Wilkinson & Evans (1999)
50.0529886965173.187269997867269.65930238536752776.21277Sakamoto et al. (2003)
50.0399914690706.92847109976539940.5371172696676468.2511Smith et al. (2007)
50.0419910425325.726839991469076.96850638172735113.64386Deason et al. (2012)
60.0399914690957.518869985070910.6335464344945146.92987Xue et al. (2008)
80.0689852841359.0248299936018002.3314110361048549.1029Gnedin et al. (2010)
100.01399701417307.7747899808054059.3811831336271726.1903Watkins et al. (2010)
120.0539884832260.29584199957345314.72906123854764645.32489Battaglia et al. (2005)
150.0750000000000.0250000000000.0250000000000.0Deason et al. (2012)
200.0679854974257.2006409912558030.05396313652012195.06256Bhattacherjee et al. (2014)

We can now plot the above data and uncertainties:

[5]:
fig, ax = plt.subplots(1, 1, figsize=(6, 4), layout="tight")

ax.errorbar(
    mwdata1["r"],
    mwdata1["Menc"],
    yerr=(mwdata1["Menc_err_neg"], mwdata1["Menc_err_pos"]),
    marker="o",
    markersize=2,
    color="k",
    alpha=1.0,
    ecolor="#aaaaaa",
    capthick=0,
    linestyle="none",
    elinewidth=1.0,
)

ax.set_xlim(1e-3, 10**2.6)
ax.set_ylim(7e6, 10**12.25)

ax.set_xlabel("$r$ [kpc]")
ax.set_ylabel(r"$M(<r)$ [M$_\odot$]")

ax.set_xscale("log")
ax.set_yscale("log")
../_images/supporting_define-milky-way-model_7_0.png

We now need to assume some form for the potential. We use a four component potential model consisting of a Hernquist (1990) bulge and nucleus, a Miyamoto-Nagai (1975) disk, and an NFW (1997) halo. We fix the parameters of the disk and bulge to be consistent with previous work (Bovy 2015 - please cite that paper if you use this potential model) and vary the scale mass and scale radius of the nucleus and halo, respectively. We’ll fit for these parameters in log-space, so we’ll first define a function that returns a gala.potential.CCompositePotential object given these four parameters:

[6]:
def get_potential(log_M_h, log_r_s, log_M_n, log_a):
    mw_potential = gp.CCompositePotential()
    mw_potential["bulge"] = gp.HernquistPotential(m=5e9, c=1.0, units=galactic)
    mw_potential["disk"] = gp.MiyamotoNagaiPotential(
        m=6.8e10 * u.Msun, a=3 * u.kpc, b=280 * u.pc, units=galactic
    )
    mw_potential["nucl"] = gp.HernquistPotential(
        m=np.exp(log_M_n), c=np.exp(log_a) * u.pc, units=galactic
    )
    mw_potential["halo"] = gp.NFWPotential(
        m=np.exp(log_M_h), r_s=np.exp(log_r_s), units=galactic
    )

    return mw_potential

We now need to specify an initial guess for the parameters - let’s do that (by making them up), and then plot the initial guess potential over the data:

[7]:
# Initial guess for the parameters- units are:
#     [Msun, kpc, Msun, pc]
x0 = [np.log(6e11), np.log(20.0), np.log(2e9), np.log(100.0)]
init_potential = get_potential(*x0)
[8]:
r = np.logspace(-3, 3, 256) * u.kpc

fig, ax = plt.subplots(1, 1, figsize=(6, 4), layout="tight")

ax.errorbar(
    mwdata1["r"],
    mwdata1["Menc"],
    yerr=(mwdata1["Menc_err_neg"], mwdata1["Menc_err_pos"]),
    marker="o",
    markersize=2,
    color="k",
    alpha=1.0,
    ecolor="#aaaaaa",
    capthick=0,
    linestyle="none",
    elinewidth=1.0,
)

# Use symmetry coordinates for spherical mass_enclosed calculation
fit_menc = init_potential.mass_enclosed(R=r)
ax.loglog(r.value, fit_menc.value, marker="", color="#3182bd", linewidth=2, alpha=0.7)

ax.set_xlim(1e-3, 10**2.6)
ax.set_ylim(7e6, 10**12.25)

ax.set_xlabel("$r$ [kpc]")
ax.set_ylabel(r"$M(<r)$ [M$_\odot$]")

ax.set_xscale("log")
ax.set_yscale("log")
../_images/supporting_define-milky-way-model_12_0.png

It looks pretty good already! But let’s now use least-squares fitting to optimize our nucleus and halo parameters. We first need to define an error function:

[9]:
def err_func(p, r, Menc, Menc_err):
    pot = get_potential(*p)
    model_menc = pot.mass_enclosed(R=r * u.kpc).to(u.Msun).value
    return (model_menc - Menc) / Menc_err

Because the uncertainties are all approximately but not exactly symmetric, we’ll take the maximum of the upper and lower uncertainty values and assume that the uncertainties in the mass measurements are Gaussian (a bad but simple assumption):

[10]:
err = np.max([mwdata1["Menc_err_pos"], mwdata1["Menc_err_neg"]], axis=0)
p_opt, ier = leastsq(err_func, x0=x0, args=(mwdata1["r"], mwdata1["Menc"], err))
assert ier in range(1, 4 + 1), "least-squares fit failed!"
fit_potential = get_potential(*p_opt)

Now we have a best-fit potential! Let’s plot the enclosed mass of the fit potential over the data:

[11]:
r = np.logspace(-3, 3, 256) * u.kpc

fig, ax = plt.subplots(1, 1, figsize=(6, 4), layout="tight")

ax.errorbar(
    mwdata1["r"],
    mwdata1["Menc"],
    yerr=(mwdata1["Menc_err_neg"], mwdata1["Menc_err_pos"]),
    marker="o",
    markersize=2,
    color="k",
    alpha=1.0,
    ecolor="#aaaaaa",
    capthick=0,
    linestyle="none",
    elinewidth=1.0,
)

fit_menc = fit_potential.mass_enclosed(R=r)
ax.loglog(r.value, fit_menc.value, marker="", color="#3182bd", linewidth=2, alpha=0.7)

ax.set_xlim(1e-3, 10**2.6)
ax.set_ylim(7e6, 10**12.25)

ax.set_xlabel("$r$ [kpc]")
ax.set_ylabel(r"$M(<r)$ [M$_\odot$]")

ax.set_xscale("log")
ax.set_yscale("log")
../_images/supporting_define-milky-way-model_18_0.png

This potential model can be loaded and used in Gala with:

import gala.potential as gp
potential = gp.MilkyWayPotential(version="v1")

MilkyWayPotential version 2 (circa 2022)#

This model was previously known as MilkyWayPotential2022 in Gala, now known as MilkyWayPotential “version 2,” and represents an updated model of the Milky Way based on more recent measurements of the halo and disk structure.

The source data for this model was compiled from published values and is included with Gala — here we use the Eilers et al. (2019) circular velocity measurements from APOGEE for mass enclosed measurements within the disk region:

[12]:
eilers = ascii.read("data/Eilers2019-circ-velocity.txt")

mwdata2 = mwdata1.copy()

# remove old measurements within disk region:
mwdata2.remove_rows([2, 3, 4])

Here we convert the circular velocity measurements to enclosed mass measurements, to be consistent with the other input data. We also adopt a 5% uncertainty on the enclosed mass measurements:

[13]:
eilers_r = eilers["R"][eilers["R"] < 15] * u.kpc
eilers_v = eilers["v_c"][eilers["R"] < 15] * u.km / u.s
eilers_M = (eilers_v**2 * eilers_r / G).to(u.Msun)
for r, M in zip(eilers_r, eilers_M):
    mwdata2.add_row(
        [r.value, M.value, 0.05 * M.value, 0.05 * M.value, "Eilers et al. (2019)"]
    )
[14]:
r = np.logspace(-3, 3, 256) * u.kpc

fig, ax = plt.subplots(1, 1, figsize=(6, 4), layout="tight")

ax.errorbar(
    mwdata2["r"],
    mwdata2["Menc"],
    yerr=(mwdata2["Menc_err_neg"], mwdata2["Menc_err_pos"]),
    marker="o",
    markersize=2,
    color="k",
    alpha=1.0,
    ecolor="#aaaaaa",
    capthick=0,
    linestyle="none",
    elinewidth=1.0,
)

ax.set_xlim(1e-3, 10**2.6)
ax.set_ylim(7e6, 10**12.25)

ax.set_xlabel("$r$ [kpc]")
ax.set_ylabel(r"$M(<r)$ [M$_\odot$]")

ax.set_xscale("log")
ax.set_yscale("log")
../_images/supporting_define-milky-way-model_24_0.png

For hte new model, we use the same bulge component as with v1, but we now use a mixture of Miyamoto-Nagai disk models to represent an approximately radially exponential and vertically sech^2 disk density profile:

[15]:
def get_potential_v2(log_M_h, log_r_s, log_M_n, log_a, log_M_d):
    mw_potential = gp.CCompositePotential()

    # Fixed bulge component
    mw_potential["bulge"] = gp.HernquistPotential(m=5e9, c=1.0, units=galactic)

    # Scale radius and height are fixed to Bland-Hawthorn & Gerhard (2016) values
    mw_potential["disk"] = gp.MN3ExponentialDiskPotential(
        m=np.exp(log_M_d) * u.Msun,
        h_R=2.6 * u.kpc,
        h_z=300 * u.pc,
        units=galactic,
        sech2_z=False,
    )

    mw_potential["nucl"] = gp.HernquistPotential(
        m=np.exp(log_M_n), c=np.exp(log_a) * u.pc, units=galactic
    )
    mw_potential["halo"] = gp.NFWPotential(
        m=np.exp(log_M_h), r_s=np.exp(log_r_s), units=galactic
    )

    return mw_potential
[16]:
x0 = [np.log(6e11), np.log(20.0), np.log(2e9), np.log(100.0), np.log(4e10)]
init_potential_v2 = get_potential_v2(*x0)
[17]:
def err_func_v2(p, r, Menc, Menc_err):
    pot = get_potential_v2(*p)
    xyz = np.zeros((3, len(r)))
    xyz[0] = r
    model_menc = pot.mass_enclosed(xyz).to(u.Msun).value
    return (model_menc - Menc) / Menc_err
[18]:
err = np.max([mwdata2["Menc_err_pos"], mwdata2["Menc_err_neg"]], axis=0)
p_opt, ier = leastsq(err_func_v2, x0=x0, args=(mwdata2["r"], mwdata2["Menc"], err))
assert ier in range(1, 4 + 1), "least-squares fit failed!"

fit_potential_v2 = get_potential_v2(*p_opt)

Here is the fitted potential compared to the data:

[19]:
r = np.logspace(-3, 3, 256) * u.kpc

fig, ax = plt.subplots(1, 1, figsize=(6, 4), layout="tight")

ax.errorbar(
    mwdata2["r"],
    mwdata2["Menc"],
    yerr=(mwdata2["Menc_err_neg"], mwdata2["Menc_err_pos"]),
    marker="o",
    markersize=2,
    color="k",
    alpha=1.0,
    ecolor="#aaaaaa",
    capthick=0,
    linestyle="none",
    elinewidth=1.0,
)

fit_menc = fit_potential_v2.mass_enclosed(R=r)
ax.loglog(r.value, fit_menc.value, marker="", color="#3182bd", linewidth=2, alpha=0.7)

ax.set_xlim(1e-3, 10**2.6)
ax.set_ylim(7e6, 10**12.25)

ax.set_xlabel("$r$ [kpc]")
ax.set_ylabel(r"$M(<r)$ [M$_\odot$]")

ax.set_xscale("log")
ax.set_yscale("log")
../_images/supporting_define-milky-way-model_31_0.png

And a comparison of the circular velocity curve between v1 and v2, compared to the Eilers et al. (2019) data:

[20]:
mwpot_v1 = gp.MilkyWayPotential(version="v1")

r_grid = np.linspace(0.5, 30, 128)

fig, ax = plt.subplots(1, 1, figsize=(6, 4), layout="tight")
ax.plot(
    r_grid,
    fit_potential_v2.circular_velocity(R=r_grid),
    marker="",
    lw=2,
    label="v2",
    color="tab:blue",
)
ax.plot(
    r_grid,
    mwpot_v1.circular_velocity(R=r_grid),
    marker="",
    label="v1",
    linestyle="--",
    color="tab:orange",
)
ax.scatter(
    eilers["R"][eilers["R"] < 15],
    eilers["v_c"][eilers["R"] < 15],
    marker="o",
    s=8,
    color="k",
)
ax.legend(loc="best", fontsize=16)
ax.set_xlabel("$R$ [kpc]")
ax.set_ylabel("$v_c$")
[20]:
Text(0, 0.5, '$v_c$')
../_images/supporting_define-milky-way-model_33_1.png

Note that the parameters of this fit were further tweaked (as described in Hunt et al. 2022) to provide a better match to the vertical phase spiral morphology.