def delta_sigma(self, r, r_off=None): """Difference between the mean Sigma within a disk and its boundary. Parameters ---------- r : float or array_like Projected radial distance from the halo center. r_off : float, optional Projected radial offset of the halo center. If provided, the computed Delta Sigma is the mean over all azimuthal angles, i.e., averaged over the circle of radius r_off centered on the halo. Notes ----- The calculation is much slower when r_off is given due to the numerical integration necessary. Value(s) are returned in units of solar mass per square parsec. """ prefactor = conv(2 * self.rs * self.rho0, "solMass / pc2") r = np.atleast_1d(r) # Basic centered calculation (excluding the constant prefactor) def _delta_sigma(r): # Recast radius in units of rs x = (conv(r, self.rs.unit.to_string()) / self.rs).value return self._func2(x) # Off-centered Delta Sigma as a function of angle and offset radius def _delta_sigma_of_theta(theta, r, r_off): term1 = np.power(r, 2) + np.power(r_off, 2) term2 = 2 * r * r_off * np.cos(theta) return _delta_sigma(np.sqrt(term1 - term2)) if r_off is None or r_off == 0: # Simple centered Delta Sigma result = _delta_sigma(r) else: # Average over all azimuthal angles r_off = conv(r_off, self.rs.unit.to_string()) a, b = (0, 2 * np.pi) # Integration limits integrals = [ quad(_delta_sigma_of_theta, a, b / 2, args=(rr, r_off))[0] for rr in r ] result = 2 * np.array(integrals) / (b - a) # Clean up as necessary if len(result) == 1: result = result[0] return prefactor * result
def lsq_fit(theta, gamma_t, err, z_halo, z_src, cosmology, model='nfw'): """Fit a halo profile to measured tangential shear data by least squares. Parameters ---------- theta : array_like Angular distances (e.g. bin centers) from the halo/cluster center. Units of arcmin are assumed if not given as an astropy quantity. gamma_t : array_like Mean measured tangential shear at distances `theta`. err : array_like Error on `gamma_t`. This is typically the standard error of the mean in each `theta` bin. z_halo : float Redshift of the halo/cluster. z_src : float or array_like Effective redshift of source galaxies per `theta` bin. If only a single float is given, this value is used for all `theta` bins. cosmology : astropy.cosmology.core.Cosmology Assumed cosmological model in which the halo/cluster lives. model : {'nfw', 'bmo', 'einasto', 'sis'}, optional Halo model type. Currently only 'nfw' is supported, so it is default. See lenspack.halos.profiles for available options. Returns ------- tuple of numpy arrays Best-fit parameters as ((c200, m200), cov), where cov is the 2x2 covariance matrix. """ # Check inputs assert len(theta) == len(gamma_t) == len(err), "Input lengths not equal." assert model in ('nfw', 'bmo', 'einasto', 'sis'), "Invalid model." # Convert angular theta to proper distance [Mpc] arcmin2mpc = conv(cosmology.kpc_proper_per_arcmin(z_halo), "Mpc / arcmin") r = conv(theta, 'arcmin') * arcmin2mpc def nfw_gamma_t(r, c200, m200): """Predicted shear profile of an NFW model.""" halo = nfw_profile(z_halo, c200, m200=m200, cosmology=cosmology) # g_t = halo.gamma_t(r, z_src) / (1 - halo.kappa(r, z_src)) return halo.gamma_t(r, z_src) def bmo_gamma_t(r, c200, m200): """Predicted shear profile of a BMO model.""" # halo = bmo_profile(z_halo, c200, m200=m200, cosmology=cosmology) # return halo.gamma_t(r, z_src) pass # Add options here once the profiles are defined in lenspack.halo.profiles if model == 'nfw': model_gamma_t = nfw_gamma_t else: raise ValueError("Only the NFW model is currently supported.") # Fit the model p0 = (4, 5e14) # Initial (c200, m200) values fit = curve_fit(model_gamma_t, xdata=r, ydata=gamma_t, sigma=err, p0=p0) return fit
def mass_enclosed(self, r): """Mass (3D) inside a sphere of a given radius.""" r = conv(r, "Mpc") x = r / self.rs prefactor = 4 * np.pi * np.power(self.rs, 3) * self.rho0 return conv(prefactor * (np.log(1 + x) - x / (1 + x)), "g")
def rho(self, r): """Density at a given distance from the halo center.""" r = conv(r, "Mpc") return self.rho0 / (r / self.rs) / np.power(1 + r / self.rs, 2)
def _delta_sigma(r): # Recast radius in units of rs x = (conv(r, self.rs.unit.to_string()) / self.rs).value return self._func2(x)
def __init__(self, z, c200, m200=None, r200=None, cosmology='default'): """Navarro, Frenk, & White (1997) radial halo density profile. rho(r; rho0, rs) = rho0 / [(r / rs) * (1 + r / rs)^2], where rho0 can be written as the product delta_c * rho_crit. The more common (and useful) parameterization is (c200, m200) instead of (rho0, rs). Parameters ---------- z : float Halo redshift. [dimensionless] c200 : float Halo concentration. [dimensionless] m200 : float, optional Spherical mass contained within a radius `r200`. [solar mass] r200 : float, optional Characteristic halo radius. [Mpc] cosmology : {'default', instance of `astropy.cosmology`} Cosmological model in which to calculate distances. The default is a `FlatLambdaCDM` object with parameters H0=70, Om0=0.3, Ob0=0.044, and Tcmb0=2.725. Alternatively, a custom cosmology can be provided as an instance of `astropy.cosmology`. Notes ----- (1) Either `m200` or `r200` must be given to fully define the profile, and `m200` takes precedence over `r200` if both are given. (2) The reference background density implicitly used in the definition of `c200`, `m200`, and `r200` is the critical density at the redshift of the halo. """ self.z = z self.c200 = c200 # Background cosmological model if cosmology == 'default': self.cosmo = astropy.cosmology.FlatLambdaCDM(H0=70, Om0=0.3, Ob0=0.044, Tcmb0=2.725) else: if not isinstance(cosmology, astropy.cosmology.core.Cosmology): raise TypeError("Invalid cosmology.") self.cosmo = cosmology # Reference background density rho_crit = self.cosmo.critical_density(z) # Either m200 or r200 must be provided assert (m200 is not None) or (r200 is not None) if m200 is not None: # Compute r200 m200 = conv(m200, "solMass") r200 = np.power(3. * m200 / (800 * np.pi * rho_crit), 1. / 3) r200 = conv(r200, "Mpc") else: # Compute m200 r200 = conv(r200, "Mpc") m200 = 800. * np.pi * rho_crit * np.power(r200, 3) / 3 m200 = conv(m200, "solMass") self.m200 = m200 self.r200 = r200 # Standard NFW parameters fc = np.log(1. + self.c200) - self.c200 / (1. + self.c200) self.rs = self.r200 / self.c200 self.delta_c = (200. / 3) * np.power(self.c200, 3) / fc self.rho0 = self.delta_c * rho_crit