summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xcalc_mass_potential.py329
1 files changed, 329 insertions, 0 deletions
diff --git a/calc_mass_potential.py b/calc_mass_potential.py
new file mode 100755
index 0000000..22c3ccd
--- /dev/null
+++ b/calc_mass_potential.py
@@ -0,0 +1,329 @@
+#!/usr/bin/env python3
+#
+# Weitian LI
+# Created: 2016-06-24
+# Updated: 2016-06-24
+#
+
+"""
+Calculate the (gas and gravitational) mass profile and gravitational
+potential profile from the electron number density profile.
+The temperature profile is required.
+
+References:
+[1] Ettori et al, 2013, Space Science Review, 177, 119-154
+
+
+Sample configuration file:
+------------------------------------------------------------
+## Configuration for `calc_mass_potential.py`
+## Date: 2016-06-24
+
+# redshift used for pixel to distance conversion
+redshift = <REDSHIFT>
+
+# electron density profile
+ne_profile = ne_profile.txt
+
+# cooling function profile
+cf_profile = coolfunc_profile.txt
+# unit of the CF profile radius (default: pixel)
+cf_unit = "pixel"
+
+# temperature profile
+t_profile = t_profile.txt
+# unit of the T profile radius (default: pixel)
+t_unit = "pixel"
+
+# number of data points for the output profile calculation
+num_dp = 100
+
+# output gas mass profile
+m_gas_profile = mass_gas_profile.txt
+
+------------------------------------------------------------
+"""
+
+import argparse
+
+import numpy as np
+import astropy.units as au
+import scipy.interpolate as interpolate
+import scipy.integrate as integrate
+from configobj import ConfigObj
+
+from astro_params import AstroParams, ChandraPixel
+from projection import Projection
+
+
+class DensityProfile:
+ """
+ Utilize the 3D (electron number or gas mass) density profile to
+ calculate the following quantities:
+ * 2D projected surface brightness (requires cooling function profile)
+ * gas mass profile (integrated, M_gas(<r))
+ * gravitational mass profile (M(<r); requires temperature profile)
+ * gravitational potential profile (cut at the largest available radius)
+
+ NOTE:
+ * The radii (of density profile and cooling function profile)
+ should have unit [ cm ]
+ * The density should have unit [ cm^-3 ] or [ g cm^-3 ]
+ """
+ # allowed density profile types
+ DENSITY_TYPES = ["electron", "gas"]
+ # input density data: [r, r_err, d]
+ r = None
+ r_err = None
+ d = None
+ # electron number density
+ ne = None
+ # gas mass density
+ rho_gas = None
+ # cooling function profile
+ cf_radius = None
+ cf_value = None
+ # temperature profile
+ t_radius = None
+ t_value = None
+ # interpolated profiles
+ d_interp = None
+ cf_interp = None
+ t_interp = None
+ # generated radial data points for profile calculation
+ radius = None
+ radius_err = None
+ # gas mass profile: M_gas(<r); same length as the above 'radius'
+ m_gas = None
+ # total (gravitational) mass profile: M_total(<r)
+ m_total = None
+ # potential profile (cut at the largest available radius)
+ potential = None
+
+ def __init__(self, density, density_type="electron"):
+ self.load_data(data=density, density_type=density_type)
+
+ def load_data(self, data, density_type="electron"):
+ if density_type not in self.DENSITY_TYPES:
+ raise ValueError("invalid density_types: %s" % density_type)
+ # 3-column density profile: [r, r_err, density]
+ self.r = data[:, 0].copy()
+ self.r_err = data[:, 1].copy()
+ self.d = data[:, 2].copy()
+ self.density_type = density_type
+
+ def load_cf_profile(self, data):
+ # 2-column cooling function profile: r[cm], cf[flux/EM]
+ self.cf_radius = data[:, 0].copy()
+ self.cf_value = data[:, 1].copy()
+
+ def load_t_profile(self, data):
+ # 2-column temperature profile: r[cm], T[keV]
+ self.t_radius = data[:, 0].copy()
+ self.t_value = data[:, 1].copy()
+
+ def calc_brightness(self):
+ """
+ Project the electron number density or gas mass density profile
+ to calculate the 2D surface brightness profile.
+ """
+ if self.cf_radius is None or self.cf_value is None:
+ raise ValueError("cooling function profile missing")
+ ne = self.calc_electron_density()
+ # flux per unit volume
+ flux = self.cf_interp(self.r) * ne ** 2 / AstroParams.ratio_ne_np
+ # project the 3D flux
+ projector = Projection(rout=self.r+self.r_err)
+ brightness = projector.project(flux)
+ return brightness
+
+ def calc_electron_density(self):
+ """
+ Calculate the electron number density from the gas mass density
+ if necessary.
+ """
+ if self.density_type == "electron":
+ self.ne = self.d.copy()
+ elif self.density_type == "gas":
+ self.ne = self.d / AstroParams.m_atom / AstroParams.mu_e
+ return self.ne
+
+ def calc_gas_density(self):
+ """
+ Calculate the gas mass density from the electron number density
+ if necessary.
+ """
+ if self.density_type == "electron":
+ self.rho_gas = self.d * AstroParams.mu_e * AstroParams.m_atom
+ elif self.density_type == "gas":
+ self.rho_gas = self.d.copy()
+ return self.rho_gas
+
+ def interpolate(self):
+ """
+ Interpolate the density profile, cooling function profile,
+ and temperature profile.
+
+ NOTE:
+ * Linear interpolation may be bad because the total mass calculation
+ needs to take the derivative of electron density profile and
+ temperature profile. Therefore, smooth spline is a better choice.
+ * Allow cooling function profile and temperature profile to be
+ extrapolated by filling with the last value if necessary.
+
+ XXX:
+ * How to extrapolate the smooth spline if necessary ??
+ """
+ # density profile
+ # insert a data point at radius of zero
+ self.d_interp = interpolate.interp1d(
+ x=np.concatenate([[0.0], self.r]),
+ y=np.concatenate([[self.d[0]], self.d]),
+ kind="linear",
+ bounds_error=False, fill_value=self.d[-1],
+ assume_sorted=True)
+ if self.ne is not None:
+ self.ne_interp = interpolate.interp1d(
+ x=np.concatenate([[0.0], self.r]),
+ y=np.concatenate([[self.ne[0]], self.ne]),
+ kind="linear",
+ bounds_error=False, fill_value=self.ne[-1],
+ assume_sorted=True)
+ if self.rho_gas is not None:
+ self.rho_gas_interp = interpolate.interp1d(
+ x=np.concatenate([[0.0], self.r]),
+ y=np.concatenate([[self.rho_gas[0]], self.rho_gas]),
+ kind="linear",
+ bounds_error=False, fill_value=self.rho_gas[-1],
+ assume_sorted=True)
+ # cooling function profile
+ self.cf_interp = interpolate.interp1d(
+ x=self.cf_radius, y=self.cf_value, kind="linear",
+ bounds_error=False, fill_value=self.cf_value[-1],
+ assume_sorted=True)
+ # temperature profile
+ self.t_interp = interpolate.interp1d(
+ x=self.t_radius, y=self.t_value, kind="linear",
+ bounds_error=False, fill_value=self.t_value[-1],
+ assume_sorted=True)
+
+ def gen_radius(self, num=100):
+ """
+ Generate radial points for following mass and potential calculation.
+
+ The generated radial points are logarithmic-evenly distributed.
+ """
+ rout = self.r + self.r_err
+ rout_new = np.logspace(np.log10(rout[0]), np.log10(rout[-1]),
+ num=num, base=10.0)
+ rin_new = np.concatenate([[0.0], rout_new[:-1]])
+ self.radius = (rout_new + rin_new) / 2.0
+ self.radius_err = (rout_new - rin_new) / 2.0
+
+ def calc_mass_gas(self, verbose=False):
+ """
+ Calculate the gas mass profile, i.e., the mass of the gas within
+ each radius.
+
+ Reference: ref.[1], eq.(9)
+ """
+ def _f_rho_gas(r):
+ return self.rho_gas_interp(r) * 4*np.pi * r**2
+ #
+ m_gas = np.zeros(self.radius.shape)
+ if verbose:
+ print("Calculating the gas mass profile (#%d) ... " % len(m_gas))
+ for i, r in enumerate(self.radius):
+ if verbose and (i+1) % 10 == 0:
+ print("%d..." % (i+1), end="", flush=True)
+ # integrated gas mass [ g ]
+ m_gas[i] = integrate.quad(_f_rho_gas, a=0.0, b=r,
+ epsabs=1.0e5, epsrel=1.e-2)[0]
+ if verbose:
+ print("DONE!", flush=True)
+ self.m_gas = m_gas
+ return m_gas
+
+ def calc_mass_total(self, verbose=True):
+ """
+ Calculate the total mass (i.e., gravitational mass) profile,
+ under the assumption of hydrostatic equilibrium (HE).
+
+ References: ref.[1], eq.(5,6,7)
+ """
+ if self.cf_radius is None or self.cf_value is None:
+ raise ValueError("cooling function profile required")
+ if self.t_radius is None or self.t_value is None:
+ raise ValueError("temperature profile required")
+ #
+ m_total = np.zeros(self.radius.shape)
+ if verbose:
+ print("Calculating the total mass profile (#%d) ... " %
+ len(m_total))
+ # TODO:
+ self.m_total = m_total
+ return m_total
+
+ def save(self, profile, outfile):
+ if profile == "mass_gas":
+ data = np.column_stack([self.radius,
+ self.radius_err,
+ self.m_gas])
+ header = "radius[cm] radius_err[cm] mass_gas(<r)[g]"
+ else:
+ raise ValueError("unknown profile name: %s" % profile)
+ np.savetxt(outfile, data, header=header)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Calculate the mass and potential profiles")
+ parser.add_argument("config", nargs="?", default="mass_potential.conf",
+ help="config for mass and potential profiles " +
+ "calculation (default: mass_potential.conf)")
+ args = parser.parse_args()
+
+ config = ConfigObj(args.config)
+
+ redshift = config.as_float("redshift")
+ pixel = ChandraPixel(z=redshift)
+
+ ne_profile = np.loadtxt(config["ne_profile"])
+
+ cf_profile = np.loadtxt(config["cf_profile"])
+ cf_unit = "pixel"
+ try:
+ cf_unit = config["cf_unit"]
+ except KeyError:
+ pass
+ if cf_unit == "pixel":
+ conv_factor = pixel.get_length().to(au.cm).value
+ else:
+ conv_factor = au.Unit(cf_unit).to(au.cm)
+ cf_profile[:, 0] *= conv_factor
+
+ t_profile = np.loadtxt(config["t_profile"])
+ t_unit = "pixel"
+ try:
+ t_unit = config["t_unit"]
+ except KeyError:
+ pass
+ if t_unit == "pixel":
+ conv_factor = pixel.get_length().to(au.cm).value
+ else:
+ conv_factor = au.Unit(t_unit).to(au.cm)
+ t_profile[:, 0] *= conv_factor
+
+ density_profile = DensityProfile(density=ne_profile,
+ density_type="electron")
+ density_profile.load_cf_profile(cf_profile)
+ density_profile.load_t_profile(t_profile)
+ density_profile.calc_gas_density()
+ density_profile.interpolate()
+ density_profile.gen_radius(num=config.as_int("num_dp"))
+ density_profile.calc_mass_gas(verbose=True)
+ density_profile.save(profile="mass_gas", outfile=config["m_gas_profile"])
+
+
+if __name__ == "__main__":
+ main()