diff options
Diffstat (limited to 'fg21sim/extragalactic/clusters/halo.py')
-rw-r--r-- | fg21sim/extragalactic/clusters/halo.py | 553 |
1 files changed, 2 insertions, 551 deletions
diff --git a/fg21sim/extragalactic/clusters/halo.py b/fg21sim/extragalactic/clusters/halo.py index c78c2c3..958dd6c 100644 --- a/fg21sim/extragalactic/clusters/halo.py +++ b/fg21sim/extragalactic/clusters/halo.py @@ -20,11 +20,7 @@ References import logging import numpy as np -import scipy.interpolate -import scipy.integrate -import scipy.optimize -from .formation import ClusterFormation from .solver import FokkerPlanckSolver from ...utils import cosmo from ...utils.units import (Units as AU, @@ -54,101 +50,10 @@ class RadioHalo: Unit: [Msun] z0 : float Redshift from where to simulate former merging history. - configs : `ConfigManager` - A `ConfigManager` instance containing default and user configurations. - For more details, see the example configuration specifications. - - Attributes - ---------- - mec : float - Unit for electron momentum (p): mec = m_e * c, p = gamma * mec, - therefore value of p is the Lorentz factor. - mtree : `~MergerTree` - Merging history of this cluster. """ - # Unit for electron momentum (p), thus its value is the Lorentz factor - mec = AU.mec # [g cm / s] - # Merger tree (i.e., merging history) of this cluster - mtree = None - - def __init__(self, M0, z0, configs): - self.M0 = M0 # [Msun] + def __init__(self, M0, z0): + self.M0 = M0 self.z0 = z0 - self.configs = configs - self._set_configs() - - def _set_configs(self): - """ - Set up the necessary class attributes according to the configs. - """ - comp = "extragalactic/halos" - self.zmax = self.configs.getn(comp+"/zmax") - self.zbinsize = self.configs.getn(comp+"/zbinsize") - # Mass threshold of the sub-cluster for a significant merger - self.merger_mass_th = self.configs.getn(comp+"/merger_mass_th") - self.radius = self.configs.getn(comp+"/radius") - self.b_mean = self.configs.getn(comp+"/b_mean") - self.b_index = self.configs.getn(comp+"/b_index") - self.eta_t = self.configs.getn(comp+"/eta_t") - self.eta_e = self.configs.getn(comp+"/eta_e") - self.pmin = self.configs.getn(comp+"/pmin") - self.pmax = self.configs.getn(comp+"/pmax") - self.pgrid_num = self.configs.getn(comp+"/pgrid_num") - self.buffer_np = self.configs.getn(comp+"/buffer_np") - self.time_step = self.configs.getn(comp+"/time_step") - self.injection_index = self.configs.getn(comp+"/injection_index") - logger.info("Loaded and set up configurations") - - @property - def zgrid(self): - """ - Redshift grid between 0 and ``self.zmax``. - """ - return np.arange(start=0.0, stop=self.zmax+self.zbinsize, - step=self.zbinsize) - - @property - def pgrid(self): - """ - Electron momentum values of the adopted logarithmic grid - used for solving the Fokker-Planck equation. - """ - grid = np.logspace(np.log10(self.pmin), np.log10(self.pmax), - num=self.pgrid_num) - return grid - - @property - def magnetic_field(self): - """ - Calculate the mean magnetic field of this cluster from the - scaling relation between magnetic field and cluster mass. - - Unit: [uG] - - Reference: Ref.[3],Eq.(1) - """ - M_mean = 1.6e15 # [Msun] - B = self.b_mean * (self.M0/M_mean) ** self.b_index - return B - - def simulate_mergertree(self): - """ - Simulate the merging history of the cluster using the extended - Press-Schechter formalism. - - Attributes - ---------- - mtree : `~MergerTree` - Generated merger tree of this cluster. - - Returns - ------- - mtree : `~MergerTree` - Generated merger tree of this cluster. - """ - self.formation = ClusterFormation(self.M0, self.z0, self.configs) - self.mtree = self.formation.simulate_mergertree() - return self.mtree def calc_electron_spectrum(self, zbegin=None, zend=None, n0_e=None): """ @@ -204,261 +109,6 @@ class RadioHalo: n_e = fpsolver.solve(u0=n0_e, tstart=tstart, tstop=tstop) return (p, n_e) - def kT_mass(self, mass): - """ - Estimate the cluster ICM temperature from its mass by assuming - an (observed) temperature-mass relation. - - TODO: upgrade this M-T relation. - - Parameters - ---------- - mass : float - Mass (unit: Msun) of the cluster - - Returns - ------- - kT : float - Temperature of the ICM (unit: keV) - - References - ---------- - [1] Nevalainen et al. 2000, ApJ, 532, 694 - Ettori et al, 2013, Space Science Review, 177, 119-154 - NOTE: H0 = 50 * h50 [km/s/Mpc] - """ - kT = 10 * (mass/1.23e15) ** (1/1.79) # [keV] - return kT - - def _radius_virial(self, mass, z=0.0): - """ - Calculate the virial radius of a cluster. - - Parameters - ---------- - mass : float - Mass (unit: Msun) of the cluster - z : float - Redshift - - Returns - ------- - Rvir : float - Virial radius (unit: kpc) of the cluster at given redshift - """ - Dc = cosmo.overdensity_virial(z) - rho = cosmo.rho_crit(z) # [g/cm^3] - R_vir = (3*mass*AUC.Msun2g / (4*np.pi * Dc * rho))**(1/3) # [cm] - R_vir *= AUC.cm2kpc # [kpc] - return R_vir - - def _radius_stripping(self, mass, M_main, z): - """ - Calculate the stripping radius of the sub-cluster at which - equipartition between static and ram pressure is established, - and the stripping is efficient outside this stripping radius. - - Note that the value of the stripping radius obtained would - give the *mean value* of the actual stripping radius during - a merger. - - Parameters - ---------- - mass : float - The mass (unit: Msun) of the sub-cluster. - M_main : float - The mass (unit: Msun) of the main cluster. - z : float - Redshift - - Returns - ------- - rs : float - The stripping radius of the sub-cluster. - Unit: kpc - - References - ---------- - [1] Cassano & Brunetti 2005, MNRAS, 357, 1313 - http://adsabs.harvard.edu/abs/2005MNRAS.357.1313C - Eq.(11) - """ - vi = self._velocity_impact(M_main, mass, z) * 1e5 # [cm/s] - kT = self.kT_mass(mass) * AUC.keV2erg # [erg] - coef = kT / (AC.mu * AC.u * vi**2) # dimensionless - rho_avg = self._density_average(M_main, z) # [g/cm^3] - - def equation(r): - return coef * self.density_profile(r, mass, z) / rho_avg - 1 - - r_vir = self._radius_virial(mass, z) # [kpc] - rs = scipy.optimize.brentq(equation, a=0, b=r_vir) # [kpc] - return rs - - def _density_average(self, mass, z=0.0): - """ - Average density of the cluster ICM. - - Returns - ------- - rho : float - Average ICM density (unit: g/cm^3) - - References - ---------- - [1] Cassano & Brunetti 2005, MNRAS, 357, 1313 - http://adsabs.harvard.edu/abs/2005MNRAS.357.1313C - Eq.(12) - """ - f_baryon = cosmo.Ob0 / cosmo.Om0 - Rv = self._radius_virial(mass, z) * AUC.kpc2cm # [cm] - V = (4*np.pi / 3) * Rv**3 # [cm^3] - rho = f_baryon * mass*AUC.Msun2g / V # [g/cm^3] - return rho - - def density_profile(self, r, mass, z): - """ - ICM (baryon) density profile, assuming the beta model. - - Parameters - ---------- - r : float - Radius (unit: kpc) where to calculate the density - mass : float - Cluster mass (unit: Msun) - z : float - Redshift - - Returns - ------- - rho_r : float - Density at the specified radius (unit: g/cm^3) - - References - ---------- - [1] Cassano & Brunetti 2005, MNRAS, 357, 1313 - http://adsabs.harvard.edu/abs/2005MNRAS.357.1313C - Eq.(13) - """ - f_baryon = cosmo.Ob0 / cosmo.Om0 - M_ICM = mass * f_baryon * AUC.Msun2g # [g] - r *= AUC.kpc2cm # [cm] - Rv = self._radius_virial(mass, z) * AUC.kpc2cm # [cm] - rc = self._beta_rc(Rv) - beta = self._beta_beta() - norm = self._beta_norm(M_ICM, beta, rc, Rv) # [g/cm^3] - rho_r = norm * (1 + (r/rc)**2) ** (-3*beta/2) # [g/cm^3] - return rho_r - - @staticmethod - def _beta_rc(r_vir): - """ - Core radius of the beta model for the ICM density profile. - - TODO: upgrade this! - """ - return 0.1*r_vir - - @staticmethod - def _beta_beta(): - """ - Beta value of the beta model for the ICM density profile. - - TODO: upgrade this! - """ - return 0.8 - - @staticmethod - def _beta_norm(mass, beta, rc, r_vir): - """ - Calculate the normalization of the beta model for the ICM - density profile. - - Parameters - ---------- - mass : float - The mass (unit: g) of ICM - beta : float - Beta value of the assumed beta profile - rc : float - Core radius (unit: cm) of the assumed beta profile - r_vir : float - The virial radius (unit: cm) of the cluster - - Returns - ------- - norm : float - Normalization of the beta model (unit: g/cm^3) - - References - ---------- - [1] Cassano & Brunetti 2005, MNRAS, 357, 1313 - http://adsabs.harvard.edu/abs/2005MNRAS.357.1313C - Eq.(14) - """ - integration = scipy.integrate.quad( - lambda r: r*r * (1+(r/rc)**2) ** (-3*beta/2), - 0, r_vir)[0] - norm = mass / (4*np.pi * integration) # [g/cm^3] - return norm - - def _velocity_impact(self, M_main, M_sub, z=0.0): - """ - Calculate the relative impact velocity between the two merging - clusters when they are at a distance of virial radius. - - Parameters - ---------- - M_main : float - Mass of the main cluster (unit: Msun) - M_sub : float - Mass of the sub cluster (unit: Msun) - z : float - Redshift - - Returns - ------- - vi : float - Relative impact velocity (unit: km/s) - - References - ---------- - [1] Cassano & Brunetti 2005, MNRAS, 357, 1313 - http://adsabs.harvard.edu/abs/2005MNRAS.357.1313C - Eq.(9) - """ - eta_v = 4 * (1 + M_main/M_sub) ** (1/3) - R_vir = self._radius_virial(M_main, z) * AUC.kpc2cm # [cm] - vi = np.sqrt(2*AC.G * (1-1/eta_v) * - (M_main+M_sub)*AUC.Msun2g / R_vir) # [cm/s] - vi /= AUC.km2cm # [km/s] - return vi - - def _time_crossing(self, M_main, M_sub, z): - """ - Calculate the crossing time of the sub-cluster during a merger. - - Parameters - ---------- - M_main : float - Mass of the main cluster (unit: Msun) - M_sub : float - Mass of the sub cluster (unit: Msun) - z : float - Redshift where the merger occurs. - - Returns - ------- - time : float - Crossing time (unit: Gyr) - """ - R_vir = self._radius_virial(M_main, z) # [kpc] - vi = self._velocity_impact(M_main, M_sub, z) # [km/s] - # Unit conversion coefficient: [s kpc/km] => [Gyr] - uconv = AUC.kpc2km * AUC.s2Gyr - time = uconv * R_vir / vi # [Gyr] - return time - def _z_end(self, z_begin, time): """ Calculate the ending redshift from ``z_begin`` after elapsing @@ -479,165 +129,6 @@ class RadioHalo: z_end = cosmo.redshift(t_end) return z_end - @property - def merger_events(self): - """ - Trace only the main cluster, and filter out the significant - merger events. - - Returns - ------- - mevents : list[dict] - List of dictionaries that records all the merger events - of the main cluster. - NOTE: - The merger events are ordered by increasing redshifts. - """ - events = [] - tree = self.mtree - while tree: - if (tree.main and tree.sub and - tree.sub.data["mass"] >= self.merger_mass_th and - tree.main.data["z"] <= self.zmax): - events.append({ - "z": tree.main.data["z"], - "age": tree.main.data["age"], - "M_main": tree.main.data["mass"], - "M_sub": tree.sub.data["mass"], - }) - tree = tree.main - return events - - def _coef_acceleration(self, z): - """ - Calculate the electron-acceleration coefficient at arbitrary - redshift. - - Parameters - ---------- - z : float - Redshift where to calculate the acceleration coefficient. - - Returns - ------- - chi : float - The calculated electron-acceleration coefficient. - (unit: Gyr^-1) - - XXX/NOTE - -------- - This coefficient may be very small and even zero, then the - diffusion coefficient of the Fokker-Planck equation is thus - very small and even zero, which cause problems for calculating - some quantities (e.g., w(x), C(x)) and wrong/invalid results. - To avoid these problems, force the minimal value of this - coefficient to be 1/(10*t0), which t0 is the present-day age - of the universe. - """ - zgrid, chigrid = self._chi_data - # XXX: force a minimal value instead of zero or too small - chi_min = 1 / (10 * cosmo.age0) - - try: - zi = np.where(z < zgrid)[0][0] - slope = (chigrid[zi]-chigrid[zi-1]) / (zgrid[zi]-zgrid[zi-1]) - chi = (z-zgrid[zi]) * slope + chigrid[zi] - except IndexError: - # Given z >= zgrid[-1] - chi = 0.0 - - if chi > chi_min: - return chi - else: - return chi_min - - @property - def _chi_data(self): - """ - Returns - ------- - zgrid : 1D `~numpy.ndarray` - Redshift positions where the chi values are calculated. - Same as the attribute ``self.zgrid`` - chigrid : 1D `~numpy.ndarray` - Values of acceleration coefficients at each redshift. - """ - if hasattr(self, "_chi_data_"): - zgrid, chigrid = self._chi_data_ - else: - # Order the merger events by decreasing redshifts - mevents = list(reversed(self.merger_events)) - chi_z = [self._chi_at_zidx(zidx, mevents) - for zidx in range(len(mevents))] - zgrid = self.zgrid - chigrid = np.zeros(zgrid.shape) - for (chi_, zbegin, zend) in chi_z: - # NOTE: zbegin > zend - mask = (zgrid <= zbegin) & (zgrid >= zend) - chigrid[mask] += chi_ - self._chi_data_ = (zgrid, chigrid) - return (zgrid, chigrid) - - def _chi_at_zidx(self, zidx, mevents): - """ - Calculate electron-acceleration coefficient at the specified - merger event which is specified with a redshift index. - - Parameters - ---------- - zidx : int - Index of the redshift where to calculate the coefficient. - mevents : list[dict] - List of dictionaries that records all the merger events - of the main cluster. - NOTE: - The merger events should be ordered by increasing time - (or decreasing redshifts). - - Returns - ------- - chi : float - The calculated electron-acceleration coefficient. - Unit: [Gyr^-1] - zbegin : float - The redshift when the merger begins - zend : float - The redshift when the merger ends - NOTE: zbegin > zend - - References: Ref.[1],Eq.(40) - """ - redshifts = np.array([ev["z"] for ev in mevents]) - zbegin = mevents[zidx]["z"] - M_main = mevents[zidx]["M_main"] - M_sub = mevents[zidx]["M_sub"] - t_crossing = self._time_crossing(M_main, M_sub, zbegin) # [Gyr] - zend = self._z_end(zbegin, t_crossing) - try: - zend_idx = np.where(redshifts < zend)[0][0] - except IndexError: - # Specified redshift already the last/smallest one - zend_idx = zidx + 1 - # - coef = 2.23e-16 * self.eta_t / (self.radius/500)**3 # [s^-1] - coef *= AUC.Gyr2s # [Gyr^-1] - chi = 0.0 - for ev in mevents[zidx:zend_idx]: - M_main = ev["M_main"] - M_sub = ev["M_sub"] - z = ev["z"] - R_vir = self._radius_virial(M_main, z) - rs = self._radius_stripping(M_sub, M_main, z) - kT = self.kT_mass(M_main) - term1 = ((M_main+M_sub)/2e15 * (2.6e3/R_vir)) ** (3/2) - term2 = (rs/500)**2 / np.sqrt(kT/7) - if rs <= self.radius: - term3 = 1.0 - else: - term3 = (self.radius/rs) ** 2 - chi += coef * term1 * term2 * term3 - return (chi, zbegin, zend) - def fp_injection(self, p, t=None): """ Electron injection term for the Fokker-Planck equation. @@ -776,47 +267,7 @@ class RadioHalo: ((self.magnetic_field/3.2)**2 + (1+z)**4)) return dpdt - @property - def e_thermal(self): - """ - Calculate the thermal energy density of the ICM. - - Returns - ------- - e_th : float - Energy density of the ICM (unit: erg/cm^3) """ - mass = self.M0 - f_baryon = cosmo.Ob0 / cosmo.Om0 - kT = self.kT_mass(mass) # [keV] - N = mass * AUC.Msun2g * f_baryon / (AC.mu * AC.u) - E_th = kT*AUC.keV2erg * N # [erg] - Rv = self._radius_virial(mass, self.z0) * AUC.kpc2cm # [cm] - V = (4*np.pi / 3) * Rv**3 # [cm^3] - e_th = E_th / V # [erg/cm^3] - return e_th - - def _n_thermal(self, mass, z=0.0): - """ - Calculate the number density of the ICM thermal plasma. - Parameters ---------- - mass : float - Mass of the cluster at the redshift - Unit: [Msun] - z : float - Redshift - - Returns - ------- - n_th : float - Number density of the ICM - Unit: [cm^-3] """ - f_baryon = cosmo.Ob0 / cosmo.Om0 - N = mass * AUC.Msun2g * f_baryon / (AC.mu * AC.u) - Rv = self._radius_virial(mass, z) * AUC.kpc2cm # [cm] - V = (4*np.pi / 3) * Rv**3 # [cm^3] - n_th = N / V # [cm^-3] - return n_th |